Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
d041ed0
Add the workflow hand-off setting-off golden, captured on fd842e199
paullizer Oct 3, 2026
b741e46
Add workflow hand-off settings, one-time origin marker and capability…
paullizer Oct 3, 2026
5255406
Add the v3 For each hand-off builder and draft service
paullizer Oct 3, 2026
ddc6c8b
Merge V2 ca95275ab (#1630) into Phase 7a
paullizer Oct 3, 2026
cfc8569
Re-capture the workflow hand-off off-golden on V2 ca95275ab
paullizer Oct 3, 2026
a9a77cb
Add workflow hand-off planning context, adapter and plan rules
paullizer Oct 3, 2026
af97472
Merge remote-tracking branch 'origin/paullizer-react-v2-ui' into paul…
paullizer Oct 3, 2026
761ef23
Re-capture the workflow hand-off off-golden on f7e268c06
paullizer Oct 3, 2026
54fee07
Wire workflow hand-off into the planner and deliverables guidance
paullizer Oct 3, 2026
6bb1f8a
Phase 7a: hand-off planner tests and singular plan-limit wording
paullizer Oct 3, 2026
9b53076
Phase 7a: keep the hand-off sidecar on the server step record
paullizer Oct 4, 2026
8b60147
Phase 7a: read a one-time hand-off's report in the result reader
paullizer Oct 4, 2026
bfed474
Add the workflow hand-off accept-and-run routes
paullizer Oct 4, 2026
dbb5485
Test the hand-off origin, the one-time run refusal, and create races
paullizer Oct 4, 2026
3f58dfd
Phase 7a: record how a one-time hand-off ended
paullizer Oct 4, 2026
31b2012
Add the 200-item hand-off end-to-end test
paullizer Oct 5, 2026
ce16933
Clarify the end-to-end test's contract bounds
paullizer Oct 5, 2026
08d28e2
Re-capture the hand-off off-golden on V2 f1aeef13d
paullizer Oct 5, 2026
15f1e50
Restore LF line endings in three files
paullizer Oct 5, 2026
bacbf0a
Restore modules the draft save-parity harness replaces
paullizer Oct 5, 2026
26f68fa
docs: document one-time workflow hand-off from chat (Phase 7a)
paullizer Oct 5, 2026
f31d45f
test: kill the surviving hand-off mutants 3b, 57b, 49b and 55
paullizer Oct 5, 2026
d2af803
Renumber workflow hand-off to 0.261.238
paullizer Oct 5, 2026
ab87e79
Merge V2 efee70775 (#1635) into Phase 7a
paullizer Oct 5, 2026
0323873
Update the save parity harness comment after #1635
paullizer Oct 5, 2026
45253ed
Merge V2 5be1d84bd (#1636) into Phase 7a
paullizer Oct 5, 2026
f368d31
Add a __main__ runner to the hand-off adapter test
paullizer Oct 5, 2026
ac9e524
Gate the hand-off daily limit on personal workflows too
paullizer Oct 5, 2026
6fdef66
Merge V2 25470e7c5 (#1643) into Phase 7a
paullizer Oct 5, 2026
0572bf2
Merge V2 4f3eacbc4 (#1644) into Phase 7a
paullizer Oct 5, 2026
7ccf17e
Renumber workflow hand-off to 0.261.239
paullizer Oct 5, 2026
b8e4730
Merge V2 a5a5b1c5 (#1641) into Phase 7a
paullizer Oct 6, 2026
319b268
Renumber workflow hand-off to 0.261.250
paullizer Oct 6, 2026
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
60 changes: 59 additions & 1 deletion application/single_app/admin_settings_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,9 @@
normalize_terms_of_use_text,
)
from functions_workflow_limits import (
CHAT_ORCHESTRATION_MAX_HANDOFFS_PER_DAY_DEFAULT,
CHAT_ORCHESTRATION_MAX_HANDOFFS_PER_DAY_MAX,
CHAT_ORCHESTRATION_MAX_HANDOFFS_PER_DAY_MIN,
CHAT_ORCHESTRATION_MAX_WORKFLOWS_DEFAULT,
CHAT_ORCHESTRATION_MAX_WORKFLOWS_MAX,
CHAT_ORCHESTRATION_MAX_WORKFLOWS_MIN,
Expand All @@ -150,6 +153,7 @@
WORKFLOW_REPEAT_ITERATIONS_MAX,
WORKFLOW_REPEAT_ITERATIONS_MIN,
WorkflowLoopLimitError,
validate_chat_orchestration_max_workflow_handoffs_per_day,

Check warning on line 156 in application/single_app/admin_settings_fields.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
validate_chat_orchestration_max_workflows_per_user,
validate_chat_orchestration_min_workflow_interval_seconds,
validate_workflow_max_loop_items,
Expand Down Expand Up @@ -3806,6 +3810,29 @@
{"key": "allow_user_workflows", "equals": True},
],
},
{
"key": "enable_chat_orchestration_workflow_handoff",
"type": "switch",
"label": "Hand Off Large Work From Chat",
"help": (
"When a request is too large for a chat plan, such as reviewing hundreds of "
"documents, a plan can hand the work to a one-time durable workflow instead. The "
"user approves the plan, then approves a hand-off card that names the documents, "
"or says how many matching documents at most, the workflow will review. Approving "
"the card creates the workflow turned off, so it never runs on a schedule, and "
"starts it once; when the run finishes, its result is posted back into the "
"conversation. The daily limit under Limits applies. Off by default, because a "

Check warning on line 3824 in application/single_app/admin_settings_fields.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
"hand-off starts background work that keeps running with the user's access after "
"the chat turn ends. Requires Chat Orchestration, Enable Personal Workflows, "
"Propose Workflows From Chat, Run Workflows From Chat and Use Workflow Results "
"In Chat."
),
"default": False,
"depends_on": [
{"key": "enable_chat_orchestration", "equals": True},
{"key": "allow_user_workflows", "equals": True},
],
},
{
"key": "chat_orchestration_enabled_capabilities",
"type": "checkbox_set",
Expand All @@ -3819,7 +3846,9 @@
"requires Enable Action Access. Propose workflows also requires Propose "
"Workflows From Chat and personal workflows. Run workflows also requires Run "
"Workflows From Chat and personal workflows. Read workflow results also "
"requires Use Workflow Results In Chat and personal workflows."
"requires Use Workflow Results In Chat and personal workflows. Hand off large "
"work also requires Hand Off Large Work From Chat, Propose Workflows From Chat, "
"Run Workflows From Chat, Use Workflow Results In Chat and personal workflows."
),
"default": [],
"options": [
Expand All @@ -3841,6 +3870,7 @@
{"value": "workflow_propose", "label": "Propose workflows"},
{"value": "workflow_run", "label": "Run workflows"},
{"value": "workflow_results", "label": "Read workflow results"},
{"value": "workflow_handoff", "label": "Hand off large work"},
],
"depends_on": {"key": "enable_chat_orchestration", "equals": True},
},
Expand Down Expand Up @@ -3961,6 +3991,28 @@
"depends_on": {"key": "enable_chat_orchestration", "equals": True},
"group": {"id": "limits", "label": "Limits", "variant": "limits"},
},
{
"key": "chat_orchestration_max_workflow_handoffs_per_day",
"type": "number",
"label": "Hand-Offs From Chat Per User Per Day",
"help": (
"How many hand-offs one user may start from chat in any rolling 24 hours. Each "
"hand-off creates a one-time workflow and starts a durable run, so this bounds "
"how much background work one user can start from chat. Hand-off workflows do "
"not count toward Workflows Created From Chat Per User. Default is 5; "
"supported range is 1 to 100."
),
"default": CHAT_ORCHESTRATION_MAX_HANDOFFS_PER_DAY_DEFAULT,
"min": CHAT_ORCHESTRATION_MAX_HANDOFFS_PER_DAY_MIN,
"max": CHAT_ORCHESTRATION_MAX_HANDOFFS_PER_DAY_MAX,
"step": 1,
"depends_on": [
{"key": "enable_chat_orchestration", "equals": True},
{"key": "allow_user_workflows", "equals": True},
{"key": "enable_chat_orchestration_workflow_handoff", "equals": True},
],
"group": {"id": "limits", "label": "Limits", "variant": "limits"},
},
],
"chat-orchestration-planner-model-section": [
# One dropdown writes the four planner keys below as a set. They stay declared, as
Expand Down Expand Up @@ -6982,6 +7034,12 @@
except WorkflowLoopLimitError as error:
return None, error.public_message, None

if key == "chat_orchestration_max_workflow_handoffs_per_day":
try:

Check warning on line 7038 in application/single_app/admin_settings_fields.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
return validate_chat_orchestration_max_workflow_handoffs_per_day(value), None, None

Check warning on line 7039 in application/single_app/admin_settings_fields.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
except WorkflowLoopLimitError as error:
return None, error.public_message, None

if key == "chat_orchestration_min_workflow_interval_seconds":
try:
return validate_chat_orchestration_min_workflow_interval_seconds(value), None, None
Expand Down
2 changes: 1 addition & 1 deletion application/single_app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@
EXECUTOR_TYPE = 'thread'
EXECUTOR_MAX_WORKERS = 30
SESSION_TYPE = 'filesystem'
VERSION = "0.261.248"
VERSION = "0.261.250"
IS_DEVELOPMENT = is_development_env_enabled()

# Opt-out for deployments where App Service Easy Auth is active but the platform
Expand Down
15 changes: 15 additions & 0 deletions application/single_app/functions_orchestration_adapters.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@
CAPABILITY_TABULAR_MERGE,
CAPABILITY_URL_FETCH,
CAPABILITY_WEB_SEARCH,
CAPABILITY_WORKFLOW_HANDOFF,
CAPABILITY_WORKFLOW_PROPOSE,
CAPABILITY_WORKFLOW_RESULTS,
CAPABILITY_WORKFLOW_RUN,
Expand Down Expand Up @@ -2139,6 +2140,19 @@ def run_workflow_results(step, context, *, settings, user_id, emit=None, cancel_
)


def run_workflow_handoff(step, context, *, settings, user_id, emit=None, cancel_requested=None):
"""Prepare a one-time workflow hand-off for the user's approval; nothing is created or queued.

The workflow draft service loads only when a plan hands work off, so resolving this adapter
imports nothing beyond this module.
"""
from functions_orchestration_workflow_handoffs import adapter_workflow_handoff

return adapter_workflow_handoff(
step, context, settings=settings, user_id=user_id, emit=emit, cancel_requested=cancel_requested,
)


# Keyed by capability id, which is also each descriptor's declared ``adapter`` name, so the
# executor can look an adapter up straight from the step's capability without a second map.
ADAPTER_REGISTRY = {
Expand All @@ -2157,6 +2171,7 @@ def run_workflow_results(step, context, *, settings, user_id, emit=None, cancel_
CAPABILITY_WORKFLOW_PROPOSE: run_workflow_propose,
CAPABILITY_WORKFLOW_RUN: run_workflow_run,
CAPABILITY_WORKFLOW_RESULTS: run_workflow_results,
CAPABILITY_WORKFLOW_HANDOFF: run_workflow_handoff,
}


Expand Down
16 changes: 16 additions & 0 deletions application/single_app/functions_orchestration_deliverables.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
CAPABILITY_RENDER_FILE,
CAPABILITY_TABULAR_INSPECT,
CAPABILITY_TABULAR_MERGE,
CAPABILITY_WORKFLOW_HANDOFF,
CAPABILITY_WORKFLOW_PROPOSE,
CAPABILITY_WORKFLOW_RESULTS,
CAPABILITY_WORKFLOW_RUN,
Expand Down Expand Up @@ -477,6 +478,21 @@
f'{WORKFLOW_RESULTS_UNAVAILABLE_REASONS[_workflow_results_reason(unavailable)]} '
'When the user asks what a saved workflow found, say why in the answer.'
)
# Handing work off is described only when this request offers it; otherwise the planner is told
# exactly what it was before.
if CAPABILITY_WORKFLOW_HANDOFF in available:
facts.append(
'workflow_handoff hands work that is too big for one chat plan to a one-time workflow. The '
'user reviews its hand-off card after approving the plan, and nothing is created or run until '
'the user approves that card; the run\'s summary is posted back into this conversation when it '

Check warning on line 487 in application/single_app/functions_orchestration_deliverables.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
'finishes. The server adds a note about the hand-off to the reply, so never say that the work '
'was done, started or scheduled.'
)
recipes.append({'for': 'Work too big for one chat plan', 'steps': (
'one workflow_handoff step with no depends_on, no inputs and no deliverable. When the request '
'is only the hand-off, declare no deliverables and no final_response; otherwise the compose '
'final_response answers only what the other steps can answer now.'
)})
truth.update({'unavailable_reasons': unavailable_reasons, 'facts': facts, 'recipes': recipes})
return truth

Expand Down
19 changes: 17 additions & 2 deletions application/single_app/functions_orchestration_execution.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,8 +153,8 @@
resolve_orchestration_model,
)
from functions_orchestration_registry import (
CAPABILITY_WORKFLOW_RESULTS, CapabilityResolutionError, resolve_admitted_export_catalog,
resolve_available_capability_ids,
CAPABILITY_WORKFLOW_HANDOFF, CAPABILITY_WORKFLOW_RESULTS, CapabilityResolutionError,
resolve_admitted_export_catalog, resolve_available_capability_ids,
)
from functions_orchestration_result_contracts import InputBinding, ResultContractError, ResultRef, TaskResult
from functions_orchestration_result_runtime import read_complete_input, read_result_document_citations
Expand Down Expand Up @@ -1210,6 +1210,12 @@ def _reads_workflow_results(self):
for step in self.record["plan"].get("steps") or []
)

def _mentions_workflow_handoff(self):
plan = self.record["plan"]
return bool(plan.get("workflow_handoff_notes")) or any(
step.get("capability_id") == CAPABILITY_WORKFLOW_HANDOFF for step in plan.get("steps") or []
)

def _workflow_result_lineage(self, record_state):
"""The workflow result contexts the answer read, re-authorized now; [] when it read none."""
if not self._reads_workflow_results():
Expand Down Expand Up @@ -1339,6 +1345,15 @@ def _finalize(self, result, error, *, refreshes=0):
)
if workflow_runs:
content.append(workflow_runs)
if self._mentions_workflow_handoff():
# Imported here, like the run step's module. Deterministic, model-free: the hand-off the
# plan prepared or left out, at every status, as its card says. Its note also keeps a
# hand-off-only plan from the "content is prepared" fallback.
from functions_orchestration_workflow_handoffs import workflow_handoff_note

workflow_handoff = workflow_handoff_note(self.record["plan"], current.get("execution_steps") or [])
if workflow_handoff:
content.append(workflow_handoff)
# Only a reply that carries the composed answer names the results it read; any other reply
# keeps just the fixed lines about results that were not read.
workflow_result_reads = error is None and bool(prepared)
Expand Down
38 changes: 36 additions & 2 deletions application/single_app/functions_orchestration_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,9 @@
from functions_orchestration_deliverables import explicit_image_shortfalls
from functions_orchestration_registry import (
CAPABILITY_DOCUMENT_MERGE, CAPABILITY_TABULAR_ANALYZE, CAPABILITY_TABULAR_INSPECT, CAPABILITY_TABULAR_MERGE,
CAPABILITY_WORKFLOW_PROPOSE, CAPABILITY_WORKFLOW_RESULTS, CAPABILITY_WORKFLOW_RUN, DEPENDENCY_PLAN_CONTRACT_VERSION,
admitted_export_pairs, external_effect_capability_ids, get_capability, resolve_available_capability_ids,
CAPABILITY_WORKFLOW_HANDOFF, CAPABILITY_WORKFLOW_PROPOSE, CAPABILITY_WORKFLOW_RESULTS, CAPABILITY_WORKFLOW_RUN,
DEPENDENCY_PLAN_CONTRACT_VERSION, admitted_export_pairs, external_effect_capability_ids, get_capability,
resolve_available_capability_ids,
)
from functions_orchestration_result_contracts import (
InputBinding, PartialInputNotAcceptedError, ProducerIdentity, ResultContractError, ResultRef, TaskResult, digest,
Expand Down Expand Up @@ -612,6 +613,9 @@
if step.get('capability_id') == CAPABILITY_WORKFLOW_RESULTS and isinstance(result.get('workflow_results'), dict):
# Server-only: the result read's lineage and reply descriptor; public_step_record drops it.
record['workflow_results'] = deepcopy(result['workflow_results'])
if step.get('capability_id') == CAPABILITY_WORKFLOW_HANDOFF and isinstance(result.get('workflow_handoff'), dict):
# Server-only: the blueprint, handles and selections the hand-off routes read; public_step_record drops it.
record['workflow_handoff'] = deepcopy(result['workflow_handoff'])
return record


Expand Down Expand Up @@ -697,6 +701,35 @@
return {**result, 'workflow_results': sidecar} if isinstance(sidecar, dict) else result


def _restore_workflow_handoff(step, context, status, result, *, settings, user_id):
"""Describe again a completed hand-off whose step result came back without its sidecar.

A retained, recovered or reused result is rebuilt from the task result alone, which carries
the card but never the blueprint's handles or the captured selections. The rebuild dry-runs
the blueprint again and writes nothing.
"""
if (
step.get('capability_id') != CAPABILITY_WORKFLOW_HANDOFF or status != STEP_STATUS_COMPLETED
or not isinstance(result, dict) or result.get('task_result') is None
or isinstance(result.get('workflow_handoff'), dict)
):
return result
try:

Check warning on line 717 in application/single_app/functions_orchestration_executor.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
from functions_orchestration_workflow_handoffs import rebuild_workflow_handoff

sidecar = rebuild_workflow_handoff(
step, context, settings=settings, user_id=user_id, task=result['task_result'],
)
except Exception as exc:

Check warning on line 723 in application/single_app/functions_orchestration_executor.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
log_event(

Check warning on line 724 in application/single_app/functions_orchestration_executor.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
f'{_LOG_PREFIX} A recovered workflow hand-off could not be described.',
level=logging.WARNING,
extra={**_step_log_context(context, step), 'error_type': type(exc).__name__},
)
return result
return {**result, 'workflow_handoff': sidecar} if isinstance(sidecar, dict) else result


def _persist(persist, record_type, record):
if not callable(persist):
return
Expand Down Expand Up @@ -1554,6 +1587,7 @@
result = _restore_workflow_proposal(step, context, status, result, settings=settings, user_id=user_id)
result = _restore_workflow_run(step, context, status, result, user_id=user_id)
result = _restore_workflow_results(step, context, status, result, user_id=user_id)
result = _restore_workflow_handoff(step, context, status, result, settings=settings, user_id=user_id)
record = _step_record(context, step, index, status, result, started_at, completed_at, elapsed)
record.update({
'checkpoint_available': checkpoints is not None
Expand Down
Loading
Loading