synapse-orch-ai 1.6.1 → 1.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/backend/core/builder_tools.py +5 -4
- package/backend/core/mcp_client.py +7 -0
- package/backend/core/models_orchestration.py +6 -1
- package/backend/core/native_builder/__init__.py +1 -1
- package/backend/core/orchestration/context.py +191 -54
- package/backend/core/orchestration/engine.py +130 -0
- package/backend/core/orchestration/steps.py +35 -8
- package/backend/core/react_engine.py +7 -0
- package/backend/core/routes/settings.py +15 -7
- package/backend/core/server.py +9 -0
- package/backend/services/google.py +48 -7
- package/frontend-build/.next/BUILD_ID +1 -1
- package/frontend-build/.next/build-manifest.json +3 -3
- package/frontend-build/.next/prerender-manifest.json +3 -3
- package/frontend-build/.next/server/app/_global-error.html +1 -1
- package/frontend-build/.next/server/app/_global-error.rsc +1 -1
- package/frontend-build/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/frontend-build/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/frontend-build/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/frontend-build/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/frontend-build/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/frontend-build/.next/server/app/_not-found.html +1 -1
- package/frontend-build/.next/server/app/_not-found.rsc +1 -1
- package/frontend-build/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/frontend-build/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/frontend-build/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/frontend-build/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/frontend-build/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/frontend-build/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/frontend-build/.next/server/app/index.html +1 -1
- package/frontend-build/.next/server/app/index.rsc +2 -2
- package/frontend-build/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/frontend-build/.next/server/app/index.segments/_full.segment.rsc +2 -2
- package/frontend-build/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/frontend-build/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/frontend-build/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/frontend-build/.next/server/app/login.html +1 -1
- package/frontend-build/.next/server/app/login.rsc +1 -1
- package/frontend-build/.next/server/app/login.segments/_full.segment.rsc +1 -1
- package/frontend-build/.next/server/app/login.segments/_head.segment.rsc +1 -1
- package/frontend-build/.next/server/app/login.segments/_index.segment.rsc +1 -1
- package/frontend-build/.next/server/app/login.segments/_tree.segment.rsc +1 -1
- package/frontend-build/.next/server/app/login.segments/login/__PAGE__.segment.rsc +1 -1
- package/frontend-build/.next/server/app/login.segments/login.segment.rsc +1 -1
- package/frontend-build/.next/server/app/page_client-reference-manifest.js +1 -1
- package/frontend-build/.next/server/app/settings/[tab]/page_client-reference-manifest.js +1 -1
- package/frontend-build/.next/server/chunks/ssr/_0b~n.nn._.js +2 -2
- package/frontend-build/.next/server/chunks/ssr/_0rwng5.._.js +1 -1
- package/frontend-build/.next/server/chunks/ssr/src_app_page_tsx_0ss2.w7._.js +1 -1
- package/frontend-build/.next/server/middleware-build-manifest.js +3 -3
- package/frontend-build/.next/server/middleware-manifest.json +5 -5
- package/frontend-build/.next/server/pages/404.html +1 -1
- package/frontend-build/.next/server/pages/500.html +1 -1
- package/frontend-build/.next/server/server-reference-manifest.js +1 -1
- package/frontend-build/.next/server/server-reference-manifest.json +1 -1
- package/frontend-build/.next/static/chunks/{0w5f~4w31j03v.js → 02k3e9vyxajlw.js} +1 -1
- package/frontend-build/.next/static/chunks/{0htooj7jtj_tj.js → 0v0eoz45t7rie.js} +1 -1
- package/frontend-build/.next/static/chunks/{0-h50ksae_qf6.js → 0xjv-b-~qzwt1.js} +22 -22
- package/package.json +1 -1
- /package/frontend-build/.next/static/{NvFb2r3QeKnSOYYi1xaC7 → cGnS1OeVwebS_oHgbU140}/_buildManifest.js +0 -0
- /package/frontend-build/.next/static/{NvFb2r3QeKnSOYYi1xaC7 → cGnS1OeVwebS_oHgbU140}/_clientMiddlewareManifest.js +0 -0
- /package/frontend-build/.next/static/{NvFb2r3QeKnSOYYi1xaC7 → cGnS1OeVwebS_oHgbU140}/_ssgManifest.js +0 -0
|
@@ -637,9 +637,10 @@ BUILDER_TOOL_SCHEMAS = [
|
|
|
637
637
|
"include_full_history": {
|
|
638
638
|
"type": "boolean",
|
|
639
639
|
"description": (
|
|
640
|
-
"For agent/llm/tool steps:
|
|
641
|
-
"
|
|
642
|
-
"
|
|
640
|
+
"For agent/llm/tool steps: controls revision-history rendering on re-runs. "
|
|
641
|
+
"Default (unset) auto-enables full history whenever this step runs more than once. "
|
|
642
|
+
"Set false to force last-attempt only (smaller prompt); set true is equivalent to default. "
|
|
643
|
+
"Only relevant if you want to explicitly opt OUT of full history."
|
|
643
644
|
),
|
|
644
645
|
},
|
|
645
646
|
},
|
|
@@ -698,7 +699,7 @@ BUILDER_TOOL_SCHEMAS = [
|
|
|
698
699
|
"max_turns": {"type": "integer"},
|
|
699
700
|
"timeout_seconds": {"type": "integer"},
|
|
700
701
|
"model": {"type": "string"},
|
|
701
|
-
"include_full_history": {"type": "boolean", "description": "
|
|
702
|
+
"include_full_history": {"type": "boolean", "description": "Opt OUT of full revision history on re-runs (default: auto-on for any re-run). Set false to keep prompts small."},
|
|
702
703
|
},
|
|
703
704
|
"required": ["step_id", "name", "type"],
|
|
704
705
|
},
|
|
@@ -441,6 +441,13 @@ class MCPClientManager:
|
|
|
441
441
|
if session:
|
|
442
442
|
self._set_status(name, "connected")
|
|
443
443
|
await self._auto_register(name) # ← register on startup
|
|
444
|
+
else:
|
|
445
|
+
# Remote servers without a bearer token use OAuth — failure likely means re-auth needed.
|
|
446
|
+
# Stdio and bearer-token servers just go disconnected.
|
|
447
|
+
if server_type == "remote" and not config.get("token"):
|
|
448
|
+
self._set_status(name, "reauth_needed")
|
|
449
|
+
else:
|
|
450
|
+
self._set_status(name, "disconnected")
|
|
444
451
|
return self.sessions
|
|
445
452
|
|
|
446
453
|
# ── add_server ─────────────────────────────────────────────────────────────
|
|
@@ -100,7 +100,9 @@ class StepConfig(BaseModel):
|
|
|
100
100
|
|
|
101
101
|
# On re-invocation (evaluator feedback or loop), include every prior turn's
|
|
102
102
|
# inputs/tools/output in the prompt instead of only the last attempt.
|
|
103
|
-
|
|
103
|
+
# Tri-state: True = always include, False = always last-attempt only,
|
|
104
|
+
# None (default) = auto (full history on any re-run).
|
|
105
|
+
include_full_history: bool | None = None
|
|
104
106
|
|
|
105
107
|
# Graph routing
|
|
106
108
|
next_step_id: str | None = None # Linear next step / loop "done" path
|
|
@@ -145,6 +147,9 @@ class OrchestrationRun(BaseModel):
|
|
|
145
147
|
waiting_for_human: bool = False
|
|
146
148
|
human_prompt: str | None = None
|
|
147
149
|
human_fields: list[dict[str, str]] = []
|
|
150
|
+
# Nested-orchestration human-in-the-loop tracking
|
|
151
|
+
nested_run_id: str | None = None # sub-run paused waiting for human input
|
|
152
|
+
nested_orch_id: str | None = None # sub-orchestration definition ID
|
|
148
153
|
|
|
149
154
|
# Cost tracking
|
|
150
155
|
total_tokens_used: int = 0
|
|
@@ -24,7 +24,7 @@ __all__ = [
|
|
|
24
24
|
# their zero-defaults (`route_map: {}`, `parallel_branches: []`, etc.) so the
|
|
25
25
|
# engine deserialises cleanly.
|
|
26
26
|
STEP_TYPE_CHEATSHEET = """\
|
|
27
|
-
- **agent**: runs a configured sub-agent with a prompt + its tool set. Required: `agent_id`, `prompt_template`. Optional: `include_full_history` (bool) —
|
|
27
|
+
- **agent**: runs a configured sub-agent with a prompt + its tool set. Required: `agent_id`, `prompt_template`. Optional: `include_full_history` (bool) — default behaviour auto-shows full revision history on any re-run; set this to `false` only if you need to keep the prompt small. Use for any step that needs multi-turn reasoning or tool use.
|
|
28
28
|
- **llm**: single one-shot LLM call, no tools. Required: `prompt_template` (optional `model`). Use for lightweight summarisation, rewriting, or deterministic prose generation.
|
|
29
29
|
- **tool**: forces a single tool call with no LLM reasoning. Required: `forced_tool` (+ `agent_id` for tool-resolution). Use when the arguments are already in state and just need forwarding.
|
|
30
30
|
- **evaluator**: pure routing node. Required: `route_map_json` (JSON-encoded `{label: target_step_id}`), `route_descriptions_json` (JSON), `evaluator_prompt`. Output_key stores the bare route label. Use to fork on a classifier decision.
|
|
@@ -472,14 +472,22 @@ def _origin_label(origin: dict | None, execution: int) -> str:
|
|
|
472
472
|
return otype or "re-invocation"
|
|
473
473
|
|
|
474
474
|
|
|
475
|
-
def _render_full_history(memory: list[dict]) -> str:
|
|
476
|
-
"""Render every recorded turn as inputs
|
|
475
|
+
def _render_full_history(memory: list[dict], skip_last_output: bool = False) -> str:
|
|
476
|
+
"""Render every recorded turn as inputs -> tools -> output.
|
|
477
|
+
|
|
478
|
+
`skip_last_output`: when the caller is also rendering the most recent
|
|
479
|
+
turn's output as a standalone YOUR PREVIOUS OUTPUT section, set this to
|
|
480
|
+
True so the history doesn't duplicate that body. The last turn still
|
|
481
|
+
shows its tools/inputs/label; only the Output: block is replaced with
|
|
482
|
+
a pointer back to the standalone section.
|
|
483
|
+
"""
|
|
477
484
|
lines: list[str] = ["## REVISION HISTORY (all prior turns)"]
|
|
478
|
-
|
|
485
|
+
last_idx = len(memory) - 1
|
|
486
|
+
for idx, entry in enumerate(memory):
|
|
479
487
|
execution = entry.get("execution", 0)
|
|
480
488
|
origin = entry.get("origin") or {}
|
|
481
489
|
label = _origin_label(origin, execution)
|
|
482
|
-
lines.append(f"\n### Turn {execution}
|
|
490
|
+
lines.append(f"\n### Turn {execution} - {label}")
|
|
483
491
|
|
|
484
492
|
# Inputs at the time of this turn
|
|
485
493
|
inputs = entry.get("inputs") or {}
|
|
@@ -509,10 +517,13 @@ def _render_full_history(memory: list[dict]) -> str:
|
|
|
509
517
|
# Final output produced on that turn
|
|
510
518
|
final_out = str(trace.get("final_output") or "")
|
|
511
519
|
if final_out:
|
|
512
|
-
if
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
520
|
+
if skip_last_output and idx == last_idx:
|
|
521
|
+
lines.append("Output: (see YOUR PREVIOUS OUTPUT section above)")
|
|
522
|
+
else:
|
|
523
|
+
if len(final_out) > 800:
|
|
524
|
+
from .summarizer import smart_truncate
|
|
525
|
+
final_out = smart_truncate(final_out, 800)
|
|
526
|
+
lines.append(f"Output:\n{final_out}")
|
|
516
527
|
|
|
517
528
|
return "\n".join(lines)
|
|
518
529
|
|
|
@@ -603,18 +614,21 @@ def build_origin_aware_context(
|
|
|
603
614
|
run: "OrchestrationRun",
|
|
604
615
|
engine: "OrchestrationEngine",
|
|
605
616
|
transition: TransitionContext,
|
|
606
|
-
) -> tuple[str, str]:
|
|
617
|
+
) -> tuple[str, str, str]:
|
|
607
618
|
"""
|
|
608
|
-
Build (prompt, system_prompt_extra) for an
|
|
609
|
-
the structured, origin-aware format.
|
|
610
|
-
|
|
611
|
-
Returns
|
|
612
|
-
prompt
|
|
613
|
-
system_prompt_extra
|
|
614
|
-
|
|
619
|
+
Build (prompt, system_prompt_extra, system_prompt_prefix) for an
|
|
620
|
+
agent/tool/llm step using the structured, origin-aware format.
|
|
621
|
+
|
|
622
|
+
Returns three strings:
|
|
623
|
+
prompt full user-turn message to send to the LLM
|
|
624
|
+
system_prompt_extra short orchestration-awareness block APPENDED to
|
|
625
|
+
the agent's system prompt (can be empty)
|
|
626
|
+
system_prompt_prefix iteration banner PREPENDED to the system prompt
|
|
627
|
+
on re-runs (execution_number > 1); empty otherwise
|
|
615
628
|
"""
|
|
616
629
|
import re
|
|
617
630
|
sections: list[str] = []
|
|
631
|
+
is_rerun = transition.execution_number > 1
|
|
618
632
|
|
|
619
633
|
# ------------------------------------------------------------------
|
|
620
634
|
# Section: ROLE
|
|
@@ -659,36 +673,99 @@ def build_origin_aware_context(
|
|
|
659
673
|
sections.append("\n".join(role_lines))
|
|
660
674
|
|
|
661
675
|
# ------------------------------------------------------------------
|
|
662
|
-
# Section:
|
|
676
|
+
# Section: MAIN GOAL (always present)
|
|
677
|
+
# The user's original request is the root of every step in this
|
|
678
|
+
# workflow. Keep it at the top of every prompt so re-runs, branching,
|
|
679
|
+
# and deep step chains do not lose sight of what we are ultimately
|
|
680
|
+
# solving for. Sourced from run.shared_state["user_input"] which
|
|
681
|
+
# the engine seeds on the first step.
|
|
663
682
|
# ------------------------------------------------------------------
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
if len(prev_out) > 2000:
|
|
677
|
-
from .summarizer import smart_truncate
|
|
678
|
-
prev_out = smart_truncate(prev_out, 2000)
|
|
679
|
-
feedback_lines.append(f"\n### Your previous output\n{prev_out}")
|
|
683
|
+
main_goal = run.shared_state.get("user_input")
|
|
684
|
+
if main_goal:
|
|
685
|
+
goal_text = str(main_goal).strip()
|
|
686
|
+
if len(goal_text) > 2000:
|
|
687
|
+
from .summarizer import smart_truncate
|
|
688
|
+
goal_text = smart_truncate(goal_text, 2000)
|
|
689
|
+
sections.append(
|
|
690
|
+
"## MAIN GOAL (original user request)\n"
|
|
691
|
+
"Everything this workflow does exists to solve this. "
|
|
692
|
+
"Keep it in mind when deciding what to produce:\n\n"
|
|
693
|
+
f"{goal_text}"
|
|
694
|
+
)
|
|
680
695
|
|
|
681
|
-
|
|
696
|
+
# ------------------------------------------------------------------
|
|
697
|
+
# Section: WHY YOU ARE RUNNING AGAIN (any re-run, any origin)
|
|
698
|
+
# ------------------------------------------------------------------
|
|
699
|
+
if is_rerun:
|
|
700
|
+
why_lines = [
|
|
701
|
+
"## WHY YOU ARE RUNNING AGAIN",
|
|
702
|
+
f"This is execution #{transition.execution_number} of this step.",
|
|
703
|
+
]
|
|
704
|
+
if transition.origin_type == "evaluator":
|
|
705
|
+
if transition.routing_decision:
|
|
706
|
+
why_lines.append(f"Evaluator decision: \"{transition.routing_decision}\"")
|
|
707
|
+
if transition.routing_reasoning:
|
|
708
|
+
why_lines.append(f"Evaluator reasoning: {transition.routing_reasoning}")
|
|
709
|
+
why_lines.append(
|
|
710
|
+
"An evaluator reviewed your previous output and routed you back. "
|
|
711
|
+
"See HOW TO PROCEED below."
|
|
712
|
+
)
|
|
713
|
+
elif transition.origin_type == "loop":
|
|
714
|
+
iter_str = (
|
|
715
|
+
f"iteration {transition.loop_iteration} of {transition.loop_total}"
|
|
716
|
+
if transition.loop_iteration and transition.loop_total
|
|
717
|
+
else f"iteration #{transition.execution_number}"
|
|
718
|
+
)
|
|
719
|
+
why_lines.append(
|
|
720
|
+
f"You are inside a loop ({iter_str}). See HOW TO PROCEED below."
|
|
721
|
+
)
|
|
722
|
+
else:
|
|
723
|
+
why_lines.append(
|
|
724
|
+
"The workflow has routed control back to this step. See HOW TO PROCEED below."
|
|
725
|
+
)
|
|
726
|
+
sections.append("\n".join(why_lines))
|
|
727
|
+
|
|
728
|
+
# ------------------------------------------------------------------
|
|
729
|
+
# Section: YOUR PREVIOUS OUTPUT (any re-run)
|
|
730
|
+
# ------------------------------------------------------------------
|
|
731
|
+
if is_rerun and step.output_key and step.output_key in run.shared_state:
|
|
732
|
+
prev_out = str(run.shared_state[step.output_key])
|
|
733
|
+
if len(prev_out) > 2000:
|
|
734
|
+
from .summarizer import smart_truncate
|
|
735
|
+
prev_out = smart_truncate(prev_out, 2000)
|
|
736
|
+
sections.append(
|
|
737
|
+
"## YOUR PREVIOUS OUTPUT\n"
|
|
738
|
+
"This is what you produced last turn. Read it before responding:\n\n"
|
|
739
|
+
f"{prev_out}"
|
|
740
|
+
)
|
|
682
741
|
|
|
683
742
|
# ------------------------------------------------------------------
|
|
684
743
|
# Section: YOUR PREVIOUS WORK / REVISION HISTORY (on any re-invocation)
|
|
685
744
|
# ------------------------------------------------------------------
|
|
686
|
-
if
|
|
745
|
+
if is_rerun:
|
|
687
746
|
memory = get_execution_memory(run, step.id)
|
|
688
747
|
if memory:
|
|
689
|
-
|
|
690
|
-
|
|
748
|
+
use_full = (
|
|
749
|
+
step.include_full_history
|
|
750
|
+
if step.include_full_history is not None
|
|
751
|
+
else True
|
|
752
|
+
)
|
|
753
|
+
# Detect whether the standalone YOUR PREVIOUS OUTPUT section was
|
|
754
|
+
# rendered above (same condition used there). If so, avoid
|
|
755
|
+
# duplicating the most recent turn's body in the history.
|
|
756
|
+
previous_output_rendered = bool(
|
|
757
|
+
step.output_key and step.output_key in run.shared_state
|
|
758
|
+
)
|
|
759
|
+
if use_full:
|
|
760
|
+
# With only one prior turn, the history would just restate
|
|
761
|
+
# YOUR PREVIOUS OUTPUT under a turn label. Skip it then.
|
|
762
|
+
if len(memory) > 1:
|
|
763
|
+
sections.append(_render_full_history(
|
|
764
|
+
memory, skip_last_output=previous_output_rendered
|
|
765
|
+
))
|
|
691
766
|
else:
|
|
767
|
+
# Last-attempt mode shows only tools/inputs (no output body),
|
|
768
|
+
# so it never duplicates YOUR PREVIOUS OUTPUT.
|
|
692
769
|
sections.append(_render_last_attempt(memory[-1]))
|
|
693
770
|
|
|
694
771
|
# ------------------------------------------------------------------
|
|
@@ -722,11 +799,10 @@ def build_origin_aware_context(
|
|
|
722
799
|
# ------------------------------------------------------------------
|
|
723
800
|
context_parts = []
|
|
724
801
|
|
|
725
|
-
#
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
)
|
|
802
|
+
# NOTE: user_input is rendered at the top under "## MAIN GOAL" so it is
|
|
803
|
+
# not duplicated here. If a step explicitly lists user_input in its
|
|
804
|
+
# input_keys, the explicit-input loop below will skip it for the same
|
|
805
|
+
# reason.
|
|
730
806
|
|
|
731
807
|
# Human response keys (always inject unless already listed)
|
|
732
808
|
human_keys = {"human_response"}
|
|
@@ -745,9 +821,9 @@ def build_origin_aware_context(
|
|
|
745
821
|
val = smart_truncate(val, 3000)
|
|
746
822
|
context_parts.append(f"### {hkey}\nSource: human response\n{val}")
|
|
747
823
|
|
|
748
|
-
# Explicitly declared input_keys
|
|
824
|
+
# Explicitly declared input_keys (skip user_input — shown under MAIN GOAL)
|
|
749
825
|
for key in (step.input_keys or []):
|
|
750
|
-
if key not in run.shared_state:
|
|
826
|
+
if key not in run.shared_state or key == "user_input":
|
|
751
827
|
continue
|
|
752
828
|
val = run.shared_state[key]
|
|
753
829
|
label = key
|
|
@@ -755,14 +831,48 @@ def build_origin_aware_context(
|
|
|
755
831
|
(s for s in engine.step_map.values() if s.output_key == key), None
|
|
756
832
|
)
|
|
757
833
|
if producer and producer.agent_id and producer.agent_id in engine.agent_names:
|
|
758
|
-
label = f"{engine.agent_names[producer.agent_id]}
|
|
834
|
+
label = f"{engine.agent_names[producer.agent_id]} → {key}"
|
|
759
835
|
context_parts.append(_format_context_value(key, val, label))
|
|
760
836
|
|
|
761
837
|
if context_parts:
|
|
762
838
|
sections.append("## CONTEXT FROM PREVIOUS STEPS\n" + "\n\n".join(context_parts))
|
|
763
839
|
|
|
764
840
|
# ------------------------------------------------------------------
|
|
765
|
-
# Section:
|
|
841
|
+
# Section: HOW TO PROCEED (any re-run)
|
|
842
|
+
# Loose framing: agent decides between refine / redo / push back. The
|
|
843
|
+
# only firm requirement is that the new output explain the change (or
|
|
844
|
+
# the deliberate non-change) relative to the previous attempt.
|
|
845
|
+
# ------------------------------------------------------------------
|
|
846
|
+
if is_rerun:
|
|
847
|
+
if transition.origin_type == "loop":
|
|
848
|
+
proceed_block = (
|
|
849
|
+
"## HOW TO PROCEED\n"
|
|
850
|
+
"This is another iteration of a loop. Your previous iteration's output and "
|
|
851
|
+
"any sibling iterations are shown above.\n\n"
|
|
852
|
+
"Use your judgement: produce the next item, refine, or take a different angle. "
|
|
853
|
+
"Just don't silently re-emit what you produced before.\n\n"
|
|
854
|
+
"At the top of your output, include a brief note (1-3 lines) explaining what "
|
|
855
|
+
"this iteration adds or changes relative to the previous one."
|
|
856
|
+
)
|
|
857
|
+
else:
|
|
858
|
+
proceed_block = (
|
|
859
|
+
"## HOW TO PROCEED\n"
|
|
860
|
+
"You are running this step again. Your previous output and the reason for re-running "
|
|
861
|
+
"are shown above. Read them before responding.\n\n"
|
|
862
|
+
"You decide how to respond — any of these are valid:\n"
|
|
863
|
+
" - Refine the previous output (small targeted edits).\n"
|
|
864
|
+
" - Redo it from scratch if it was fundamentally off.\n"
|
|
865
|
+
" - Push back if you believe the feedback is mistaken: explain why and either "
|
|
866
|
+
"defend the previous output or propose a different correction.\n\n"
|
|
867
|
+
"Whichever path you take, include a brief reasoning note at the top of your output "
|
|
868
|
+
"(1-3 lines) that explains:\n"
|
|
869
|
+
" - What changed from your previous output (or why you kept it), and\n"
|
|
870
|
+
" - Why you believe this is the right response to the feedback."
|
|
871
|
+
)
|
|
872
|
+
sections.append(proceed_block)
|
|
873
|
+
|
|
874
|
+
# ------------------------------------------------------------------
|
|
875
|
+
# Section: TASK (first run) / ORIGINAL TASK reference (re-run)
|
|
766
876
|
# ------------------------------------------------------------------
|
|
767
877
|
prompt_template = step.prompt_template or run.shared_state.get("user_input", "")
|
|
768
878
|
|
|
@@ -773,12 +883,17 @@ def build_origin_aware_context(
|
|
|
773
883
|
|
|
774
884
|
task_text = re.sub(r"\{state\.(\w+)\}", replace_ref, prompt_template)
|
|
775
885
|
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
886
|
+
if is_rerun:
|
|
887
|
+
task_header = "## ORIGINAL TASK (reference only)"
|
|
888
|
+
task_suffix = (
|
|
889
|
+
"\n\nThis is the original task statement, included for reference. "
|
|
890
|
+
"Use it together with YOUR PREVIOUS OUTPUT and HOW TO PROCEED above to decide your response."
|
|
891
|
+
)
|
|
892
|
+
else:
|
|
893
|
+
task_header = "## TASK"
|
|
894
|
+
task_suffix = ""
|
|
895
|
+
if transition.origin_type == "human_response":
|
|
896
|
+
task_suffix = "\n\nIncorporate the human's input above."
|
|
782
897
|
|
|
783
898
|
sections.append(f"{task_header}\n{task_text}{task_suffix}")
|
|
784
899
|
|
|
@@ -799,7 +914,7 @@ def build_origin_aware_context(
|
|
|
799
914
|
if sid:
|
|
800
915
|
exec_counts[sid] = exec_counts.get(sid, 0) + 1
|
|
801
916
|
# Use execution_number for the active step (counts this in-flight run).
|
|
802
|
-
if
|
|
917
|
+
if is_rerun:
|
|
803
918
|
exec_counts[step.id] = transition.execution_number
|
|
804
919
|
|
|
805
920
|
graph_md = build_workflow_graph_markdown(engine.orch, step.id, exec_counts)
|
|
@@ -816,4 +931,26 @@ def build_origin_aware_context(
|
|
|
816
931
|
)
|
|
817
932
|
system_prompt_extra = "\n".join(sys_lines)
|
|
818
933
|
|
|
819
|
-
|
|
934
|
+
# ------------------------------------------------------------------
|
|
935
|
+
# System prompt PREFIX: iteration banner (re-runs only).
|
|
936
|
+
# Prepended to the system prompt so a long agent role description
|
|
937
|
+
# cannot drown out the iteration signal.
|
|
938
|
+
# ------------------------------------------------------------------
|
|
939
|
+
system_prompt_prefix = ""
|
|
940
|
+
if is_rerun:
|
|
941
|
+
why_short = ""
|
|
942
|
+
if transition.origin_type == "evaluator" and transition.routing_decision:
|
|
943
|
+
why_short = f" (evaluator routed back: \"{transition.routing_decision}\")"
|
|
944
|
+
elif transition.origin_type == "loop" and transition.loop_iteration:
|
|
945
|
+
why_short = f" (loop iteration {transition.loop_iteration})"
|
|
946
|
+
system_prompt_prefix = (
|
|
947
|
+
"ITERATION CONTEXT - READ BEFORE RESPONDING\n"
|
|
948
|
+
f"You are on execution #{transition.execution_number} of step "
|
|
949
|
+
f"\"{step_name}\"{why_short}. This is NOT your first attempt.\n"
|
|
950
|
+
"In the user message below, read YOUR PREVIOUS OUTPUT, WHY YOU ARE RUNNING AGAIN, "
|
|
951
|
+
"and HOW TO PROCEED. Then decide how to respond — refine, redo, or push back on the "
|
|
952
|
+
"feedback if you think it is mistaken. Whichever you choose, start your output with a "
|
|
953
|
+
"brief reasoning note explaining what changed (or why you kept the previous output)."
|
|
954
|
+
)
|
|
955
|
+
|
|
956
|
+
return prompt, system_prompt_extra, system_prompt_prefix
|
|
@@ -175,6 +175,14 @@ class OrchestrationEngine:
|
|
|
175
175
|
run.waiting_for_human = True
|
|
176
176
|
run.status = "paused"
|
|
177
177
|
run.current_step_id = step.id
|
|
178
|
+
# Track the sub-run that needs human input when the
|
|
179
|
+
# request originated inside a nested orchestration.
|
|
180
|
+
if event.get("nested_run_id"):
|
|
181
|
+
run.nested_run_id = event["nested_run_id"]
|
|
182
|
+
run.nested_orch_id = event.get("nested_orch_id")
|
|
183
|
+
else:
|
|
184
|
+
run.nested_run_id = None
|
|
185
|
+
run.nested_orch_id = None
|
|
178
186
|
state.checkpoint()
|
|
179
187
|
if logger:
|
|
180
188
|
logger.step_end(step.id, "paused")
|
|
@@ -370,6 +378,14 @@ class OrchestrationEngine:
|
|
|
370
378
|
session_id=run.session_id,
|
|
371
379
|
)
|
|
372
380
|
|
|
381
|
+
# If the parent was paused because a NESTED orchestration hit a human step,
|
|
382
|
+
# resume the sub-run and let it complete before continuing the parent.
|
|
383
|
+
if run.nested_run_id:
|
|
384
|
+
async for event in cls._resume_nested_orch(run, engine, human_response, server_module):
|
|
385
|
+
yield event
|
|
386
|
+
return
|
|
387
|
+
|
|
388
|
+
# Normal path: human step is directly in this orchestration.
|
|
373
389
|
# Move to next step after the HUMAN step
|
|
374
390
|
current_step = engine.step_map.get(run.current_step_id)
|
|
375
391
|
|
|
@@ -392,6 +408,120 @@ class OrchestrationEngine:
|
|
|
392
408
|
async for event in engine._execute_loop(run, state):
|
|
393
409
|
yield event
|
|
394
410
|
|
|
411
|
+
@classmethod
|
|
412
|
+
async def _resume_nested_orch(
|
|
413
|
+
cls,
|
|
414
|
+
run: OrchestrationRun,
|
|
415
|
+
engine: "OrchestrationEngine",
|
|
416
|
+
human_response: dict,
|
|
417
|
+
server_module,
|
|
418
|
+
) -> AsyncGenerator[dict, None]:
|
|
419
|
+
"""Resume a parent run whose nested sub-orchestration was paused at a human step.
|
|
420
|
+
|
|
421
|
+
Resumes the sub-run, forwards its events tagged with the parent step's
|
|
422
|
+
context, then writes the sub-orch result to parent shared_state and
|
|
423
|
+
continues the parent execution loop.
|
|
424
|
+
"""
|
|
425
|
+
parent_step = engine.step_map.get(run.current_step_id)
|
|
426
|
+
nested_run_id = run.nested_run_id
|
|
427
|
+
nested_orch_id = run.nested_orch_id
|
|
428
|
+
_NESTED_FILTER_TYPES = {"orchestration_start", "orchestration_complete", "orchestration_end"}
|
|
429
|
+
|
|
430
|
+
final_response: str | None = None
|
|
431
|
+
sub_events: list[dict] = []
|
|
432
|
+
|
|
433
|
+
async for sub_event in cls.resume(nested_run_id, human_response, server_module):
|
|
434
|
+
sub_events.append(sub_event)
|
|
435
|
+
|
|
436
|
+
# Sub-orch hit another human step (multi-turn human interaction inside nested orch)
|
|
437
|
+
if sub_event.get("type") == "human_input_required":
|
|
438
|
+
run.nested_run_id = sub_event.get("nested_run_id") or nested_run_id
|
|
439
|
+
run.nested_orch_id = sub_event.get("nested_orch_id") or nested_orch_id
|
|
440
|
+
state = SharedState(run)
|
|
441
|
+
state.checkpoint()
|
|
442
|
+
yield {
|
|
443
|
+
**sub_event,
|
|
444
|
+
"run_id": run.run_id,
|
|
445
|
+
"orch_step_id": run.current_step_id,
|
|
446
|
+
"step_name": parent_step.name if parent_step else "",
|
|
447
|
+
"nested_run_id": nested_run_id,
|
|
448
|
+
"nested_orch_id": nested_orch_id,
|
|
449
|
+
}
|
|
450
|
+
return
|
|
451
|
+
|
|
452
|
+
if sub_event.get("type") == "final" and sub_event.get("intent") == "orchestration":
|
|
453
|
+
final_response = sub_event.get("response", "")
|
|
454
|
+
|
|
455
|
+
# Skip sub-orch lifecycle meta-events (same filter as initial execution)
|
|
456
|
+
if sub_event.get("type") in _NESTED_FILTER_TYPES:
|
|
457
|
+
continue
|
|
458
|
+
|
|
459
|
+
yield {
|
|
460
|
+
**sub_event,
|
|
461
|
+
"run_id": run.run_id,
|
|
462
|
+
"orch_step_id": run.current_step_id,
|
|
463
|
+
"step_name": parent_step.name if parent_step else "",
|
|
464
|
+
"nested_run_id": nested_run_id,
|
|
465
|
+
"nested_orch_id": nested_orch_id,
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
# Fallback: extract result from orchestration_complete if no "final" event was emitted
|
|
469
|
+
if final_response is None:
|
|
470
|
+
from core.routes.orchestrations import load_orchestrations
|
|
471
|
+
from core.models_orchestration import Orchestration
|
|
472
|
+
orchs = load_orchestrations()
|
|
473
|
+
sub_orch_data = next((o for o in orchs if o["id"] == nested_orch_id), None)
|
|
474
|
+
if sub_orch_data:
|
|
475
|
+
sub_orch = Orchestration.model_validate(sub_orch_data)
|
|
476
|
+
for ev in reversed(sub_events):
|
|
477
|
+
if ev.get("type") == "orchestration_complete":
|
|
478
|
+
state_data = ev.get("final_state") or {}
|
|
479
|
+
for sub_step in reversed(sub_orch.steps):
|
|
480
|
+
if sub_step.output_key and sub_step.output_key in state_data:
|
|
481
|
+
final_response = str(state_data[sub_step.output_key])
|
|
482
|
+
break
|
|
483
|
+
break
|
|
484
|
+
|
|
485
|
+
if final_response is None:
|
|
486
|
+
run.status = "failed"
|
|
487
|
+
state = SharedState(run)
|
|
488
|
+
state.checkpoint()
|
|
489
|
+
yield {
|
|
490
|
+
"type": "orchestration_error",
|
|
491
|
+
"error": "Nested orchestration failed to produce a result after human input",
|
|
492
|
+
}
|
|
493
|
+
return
|
|
494
|
+
|
|
495
|
+
# Write sub-orch result into parent shared_state
|
|
496
|
+
if parent_step and parent_step.output_key:
|
|
497
|
+
run.shared_state[parent_step.output_key] = final_response
|
|
498
|
+
|
|
499
|
+
# Record agent step completion in history
|
|
500
|
+
import time as _time
|
|
501
|
+
run.step_history.append({
|
|
502
|
+
"step_id": run.current_step_id,
|
|
503
|
+
"step_name": parent_step.name if parent_step else "",
|
|
504
|
+
"step_type": "agent",
|
|
505
|
+
"status": "completed",
|
|
506
|
+
"ended_at": _time.strftime("%Y-%m-%dT%H:%M:%SZ", _time.gmtime()),
|
|
507
|
+
})
|
|
508
|
+
|
|
509
|
+
# Clear nested context and resume parent from next step after agent step
|
|
510
|
+
run.nested_run_id = None
|
|
511
|
+
run.nested_orch_id = None
|
|
512
|
+
run.waiting_for_human = False
|
|
513
|
+
run.status = "running"
|
|
514
|
+
|
|
515
|
+
if parent_step:
|
|
516
|
+
next_id, extra_event = engine._resolve_next(parent_step, run)
|
|
517
|
+
if extra_event:
|
|
518
|
+
yield extra_event
|
|
519
|
+
run.current_step_id = next_id
|
|
520
|
+
|
|
521
|
+
state = SharedState(run)
|
|
522
|
+
async for event in engine._execute_loop(run, state):
|
|
523
|
+
yield event
|
|
524
|
+
|
|
395
525
|
def _init_state(self, user_input: str) -> dict:
|
|
396
526
|
"""Initialize shared state from schema defaults + user input."""
|
|
397
527
|
state = {}
|