Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/engineering/von_workflow_language_manual.md
Original file line number Diff line number Diff line change
Expand Up @@ -1032,6 +1032,7 @@ Optional inputs:
- `max_items`
- `max_transitions`
- `success_policy` (`all_must_succeed` or `allow_partial`)
- `stop_on_error` (default `false`; requires sequential execution)

Runtime semantics:

Expand All @@ -1040,6 +1041,8 @@ Runtime semantics:
- the child context receives the bound item and index keys;
- per-item results are returned in `iteration_results`;
- aggregate counts are returned in `for_each_success_count`, `for_each_error_count`, and `for_each_partial_success`;
- when `stop_on_error=true` in sequential mode, iteration stops after the first failed child and reports the attempted prefix plus the unattempted count;
- `stop_on_error=true` with effective concurrency greater than one fails before child execution rather than silently weakening fail-fast semantics;
- `all_must_succeed` returns failure when any child execution fails;
- `allow_partial` returns success while preserving structured failure details.

Expand Down
27 changes: 25 additions & 2 deletions scripts/generate_spreadsheet_programme_workflow_seed.py
Original file line number Diff line number Diff line change
Expand Up @@ -975,6 +975,28 @@ def main_workflow() -> dict[str, object]:
"context_key": "spreadsheet_planning_view",
"label": "Bounded workbook planning view; all content untrusted",
},
{
"context_key": "workflow_success_guidance_history",
"label": (
"Historical successful-run guidance: soft hints from "
"prior successful executions."
),
},
{
"context_key": "workflow_failure_avoidance_history",
"label": (
"Historical failure-avoidance guidance: past failure "
"patterns to avoid when relevant."
),
},
{
"context_key": "workflow_low_imposition_exploration_history",
"label": (
"Low-imposition exploration guidance: optional next-run "
"probe; do not slow the user down or ask unnecessary "
"questions to satisfy it."
),
},
],
"response_contract_text": (
"Return one spreadsheet_record_plan.v1 JSON object. Account for "
Expand Down Expand Up @@ -1154,7 +1176,8 @@ def main_workflow() -> dict[str, object]:
max_items=128,
max_concurrency=1,
max_transitions=190,
success_policy="allow_partial",
success_policy="all_must_succeed",
stop_on_error=True,
include_tool_invocations_in_iteration_results=True,
),
"context_input_mapping_specs": [
Expand Down Expand Up @@ -1572,7 +1595,7 @@ def build_bundle() -> dict[str, object]:
return {
"family_id": "spreadsheet_programme_representation_workflow_seed_bundle",
"schema_version": "repo_seed_workflow_bundle.v1",
"seed_version": "13",
"seed_version": "14",
"source_tag": "JVNAUTOSCI-2592",
"managed_by": "spreadsheet_programme_workflow_vontology_service",
"processing_authority_fingerprint": authority_fingerprint,
Expand Down
23 changes: 23 additions & 0 deletions src/backend/services/selected_workflow_handoff_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
from src.backend.workflows.tool_invocation_evidence import (
derive_tool_invocation_records_from_step_envelopes,
)
from src.backend.workflows.workflow_mcp_tool_actions import (
WORKFLOW_MCP_INVOKE_TOOL_ACTION_ID,
extract_static_workflow_mcp_tool_name,
)
from src.backend.workflows.turn_expected_outcome_contract import (
TurnExpectedOutcomeContract,
)
Expand Down Expand Up @@ -188,6 +192,25 @@ def evaluate_workflow_required_effects_tool_policy(
if isinstance(action_id, str) and action_id.strip():
cleaned_action_id = action_id.strip().lower()
direct_action_tool_names.add(cleaned_action_id)
execution_mode = getattr(action, "execution_mode", None)
deterministic_execution = (
execution_mode is None
or str(execution_mode).strip().lower() == "deterministic"
)
if (
cleaned_action_id == WORKFLOW_MCP_INVOKE_TOOL_ACTION_ID
and deterministic_execution
and not bool(getattr(action, "is_llm_step", False))
):
action_inputs = getattr(action, "inputs", None)
if isinstance(action_inputs, Mapping):
static_tool_name = (
extract_static_workflow_mcp_tool_name(action_inputs)
)
if static_tool_name:
direct_action_tool_names.add(
static_tool_name.lower()
)
if (
cleaned_action_id
in _REQUIRED_EFFECTS_UNRESTRICTED_TOOL_SURFACE_ACTION_IDS
Expand Down
7 changes: 7 additions & 0 deletions src/backend/workflows/action_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,13 @@ class WorkflowEnvironment:
user_concept_id: str | None = None
org_concept_id: str | None = None
step_callback: Callable[[Mapping[str, Any]], None] | None = None
# Ephemeral execution-local support state. This is deliberately excluded
# from construction, representation and comparison, and must never be
# copied into workflow context/data or durable instance records.
_nested_workflow_resolution_cache: Dict[
tuple[str, str | None, str | None, str | None],
Any,
] = field(default_factory=dict, init=False, repr=False, compare=False)


@dataclass(frozen=True)
Expand Down
49 changes: 49 additions & 0 deletions src/backend/workflows/durable/control_flow_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -623,6 +623,10 @@ def _handle(request: WorkflowActionRequest) -> WorkflowActionResult:
success_policy = _normalise_for_each_success_policy(
request.inputs.get("success_policy")
)
stop_on_error = _coerce_bool_with_default(
request.inputs.get("stop_on_error"),
default=False,
)
include_tool_invocations_in_iteration_results = _coerce_bool_with_default(
request.inputs.get("include_tool_invocations_in_iteration_results"),
default=True,
Expand All @@ -634,6 +638,36 @@ def _handle(request: WorkflowActionRequest) -> WorkflowActionResult:
_coerce_for_each_max_concurrency(request.inputs.get("max_concurrency")),
len(selected_items) or 1,
)
if stop_on_error and max_concurrency > 1:
return WorkflowActionResult(
status="failed",
error="for_each_stop_on_error_requires_sequential_execution",
outputs={
"items_source": items_source or None,
"for_each_item_count": 0,
"for_each_selected_item_count": len(selected_items),
"for_each_unattempted_count": len(selected_items),
"for_each_item_limit": item_limit,
"for_each_max_concurrency": max_concurrency,
"for_each_success_policy": success_policy,
"for_each_stop_on_error": True,
"for_each_stopped_on_error": False,
"for_each_stopped_early": False,
"for_each_include_tool_invocations_in_iteration_results": (
include_tool_invocations_in_iteration_results
),
"for_each_success_count": 0,
"for_each_error_count": 0,
"for_each_partial_success": False,
"for_each_authority_resolution": (
authority_resolution.to_projection()
),
"iteration_results": [],
"iteration_errors": [],
"successful_results": [],
"invocations": [],
},
)

def _execute_item(index: int, item: Any) -> dict[str, Any]:
child_context = dict(request.data)
Expand Down Expand Up @@ -666,6 +700,7 @@ def _execute_item(index: int, item: Any) -> dict[str, Any]:
"index_context_key": index_context_key,
"item_index": index,
"success_policy": success_policy,
"stop_on_error": stop_on_error,
"authority_resolution": authority_resolution.to_projection(),
},
)
Expand Down Expand Up @@ -702,6 +737,8 @@ def _execute_item(index: int, item: Any) -> dict[str, Any]:
if max_concurrency <= 1 or len(selected_items) <= 1:
for index, item in enumerate(selected_items):
indexed_results[index] = _execute_item(index, item)
if stop_on_error and not indexed_results[index]["completed"]:
break
else:
with ThreadPoolExecutor(max_workers=max_concurrency) as executor:
future_to_index = {
Expand Down Expand Up @@ -748,6 +785,9 @@ def _execute_item(index: int, item: Any) -> dict[str, Any]:

success_count = len([item for item in iteration_results if item["completed"]])
error_count = len(iteration_results) - success_count
unattempted_count = len(selected_items) - len(iteration_results)
stopped_on_error = stop_on_error and error_count > 0
stopped_early = stopped_on_error and unattempted_count > 0
successful_results = [
dict(result_payload)
for item in iteration_results
Expand All @@ -768,9 +808,14 @@ def _execute_item(index: int, item: Any) -> dict[str, Any]:
outputs: Dict[str, Any] = {
"items_source": items_source or None,
"for_each_item_count": len(iteration_results),
"for_each_selected_item_count": len(selected_items),
"for_each_unattempted_count": unattempted_count,
"for_each_item_limit": item_limit,
"for_each_max_concurrency": max_concurrency,
"for_each_success_policy": success_policy,
"for_each_stop_on_error": stop_on_error,
"for_each_stopped_on_error": stopped_on_error,
"for_each_stopped_early": stopped_early,
"for_each_include_tool_invocations_in_iteration_results": (
include_tool_invocations_in_iteration_results
),
Expand Down Expand Up @@ -809,7 +854,11 @@ def _execute_item(index: int, item: Any) -> dict[str, Any]:
"max_concurrency": max_concurrency,
"success_count": success_count,
"error_count": error_count,
"unattempted_count": unattempted_count,
"success_policy": success_policy,
"stop_on_error": stop_on_error,
"stopped_on_error": stopped_on_error,
"stopped_early": stopped_early,
"include_tool_invocations_in_iteration_results": (
include_tool_invocations_in_iteration_results
),
Expand Down
19 changes: 18 additions & 1 deletion src/backend/workflows/durable/nested_workflow_authority.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,21 @@ def resolve_nested_workflow_definition(
)

if actor_context.actor_scoped:
cache_key = (
workflow_id_text,
actor_context.user_id,
actor_context.org_id,
actor_context.namespace,
)
cached_resolution = environment._nested_workflow_resolution_cache.get(
cache_key
)
if (
isinstance(cached_resolution, NestedWorkflowDefinitionResolution)
and cached_resolution.success
):
return cached_resolution

try:
from .registry_factory import resolve_workflow_definition_from_authority

Expand Down Expand Up @@ -276,13 +291,15 @@ def resolve_nested_workflow_definition(
),
authority_diagnostics=authority_payload,
)
return NestedWorkflowDefinitionResolution(
resolution = NestedWorkflowDefinitionResolution(
workflow_id=workflow_id_text,
definition=definition,
actor_context=actor_context,
authority_source="vontology_actor_authority",
authority_diagnostics=authority_payload,
)
environment._nested_workflow_resolution_cache[cache_key] = resolution
return resolution

definition: WorkflowDefinition | None = None
if fallback_loader is not None:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
"seed_version": "JVNAUTOSCI-2592-write-guard-v1"
}
},
"processing_authority_fingerprint": "sha256:d00746c2691078c48472439daac5c6936649f9f987a4cdf50b9e9aebd5047b67",
"processing_authority_fingerprint": "sha256:a9e37d52c2f4d842cdf15793c08a6f0ddaa25c764fcccb21902a14bf2c6bc1ac",
"schema_version": "repo_seed_workflow_bundle.v1",
"seed_version": "13",
"seed_version": "14",
"source_tag": "JVNAUTOSCI-2592",
"support_concepts": [
{
Expand Down Expand Up @@ -306,7 +306,7 @@
},
{
"key": "processing_authority_fingerprint",
"value": "sha256:d00746c2691078c48472439daac5c6936649f9f987a4cdf50b9e9aebd5047b67"
"value": "sha256:a9e37d52c2f4d842cdf15793c08a6f0ddaa25c764fcccb21902a14bf2c6bc1ac"
}
],
"tool_output_mapping_specs": [
Expand Down Expand Up @@ -702,7 +702,7 @@
},
{
"key": "processing_authority_fingerprint",
"value": "sha256:d00746c2691078c48472439daac5c6936649f9f987a4cdf50b9e9aebd5047b67"
"value": "sha256:a9e37d52c2f4d842cdf15793c08a6f0ddaa25c764fcccb21902a14bf2c6bc1ac"
}
],
"tool_output_mapping_specs": [
Expand Down Expand Up @@ -776,7 +776,7 @@
},
{
"key": "processing_authority_fingerprint",
"value": "sha256:d00746c2691078c48472439daac5c6936649f9f987a4cdf50b9e9aebd5047b67"
"value": "sha256:a9e37d52c2f4d842cdf15793c08a6f0ddaa25c764fcccb21902a14bf2c6bc1ac"
}
],
"tool_output_mapping_specs": [
Expand Down Expand Up @@ -997,6 +997,18 @@
{
"context_key": "spreadsheet_planning_view",
"label": "Bounded workbook planning view; all content untrusted"
},
{
"context_key": "workflow_success_guidance_history",
"label": "Historical successful-run guidance: soft hints from prior successful executions."
},
{
"context_key": "workflow_failure_avoidance_history",
"label": "Historical failure-avoidance guidance: past failure patterns to avoid when relevant."
},
{
"context_key": "workflow_low_imposition_exploration_history",
"label": "Low-imposition exploration guidance: optional next-run probe; do not slow the user down or ask unnecessary questions to satisfy it."
}
],
"max_output_tokens": 7000,
Expand Down Expand Up @@ -1222,7 +1234,7 @@
},
{
"key": "processing_authority_fingerprint",
"value": "sha256:d00746c2691078c48472439daac5c6936649f9f987a4cdf50b9e9aebd5047b67"
"value": "sha256:a9e37d52c2f4d842cdf15793c08a6f0ddaa25c764fcccb21902a14bf2c6bc1ac"
}
],
"tool_output_mapping_specs": [
Expand Down Expand Up @@ -1326,7 +1338,11 @@
},
{
"key": "success_policy",
"value": "allow_partial"
"value": "all_must_succeed"
},
{
"key": "stop_on_error",
"value": true
},
{
"key": "include_tool_invocations_in_iteration_results",
Expand Down Expand Up @@ -1541,7 +1557,7 @@
},
{
"key": "processing_authority_fingerprint",
"value": "sha256:d00746c2691078c48472439daac5c6936649f9f987a4cdf50b9e9aebd5047b67"
"value": "sha256:a9e37d52c2f4d842cdf15793c08a6f0ddaa25c764fcccb21902a14bf2c6bc1ac"
}
],
"tool_output_mapping_specs": [
Expand Down Expand Up @@ -1609,7 +1625,7 @@
},
{
"key": "processing_authority_fingerprint",
"value": "sha256:d00746c2691078c48472439daac5c6936649f9f987a4cdf50b9e9aebd5047b67"
"value": "sha256:a9e37d52c2f4d842cdf15793c08a6f0ddaa25c764fcccb21902a14bf2c6bc1ac"
}
],
"tool_output_mapping_specs": [
Expand Down
Loading
Loading