synapse-orch-ai 1.6.0 → 1.6.2
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/cache/response_cache.py +18 -0
- package/backend/core/mcp_client.py +7 -0
- package/backend/core/models_orchestration.py +3 -1
- package/backend/core/native_builder/__init__.py +1 -1
- package/backend/core/orchestration/context.py +191 -54
- package/backend/core/orchestration/steps.py +21 -7
- 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 +1 -1
- package/frontend-build/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/frontend-build/.next/server/app/index.segments/_full.segment.rsc +1 -1
- 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/settings/[tab]/page_client-reference-manifest.js +1 -1
- package/frontend-build/.next/server/chunks/ssr/_0b~n.nn._.js +3 -3
- package/frontend-build/.next/server/chunks/ssr/_0rwng5.._.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/{139v3bz1_4_sh.js → 0xjv-b-~qzwt1.js} +22 -22
- package/package.json +1 -1
- /package/frontend-build/.next/static/{cs3gFMScfwOahyeFSLcqL → 65DqAqVD6tjvCkcwNTV2U}/_buildManifest.js +0 -0
- /package/frontend-build/.next/static/{cs3gFMScfwOahyeFSLcqL → 65DqAqVD6tjvCkcwNTV2U}/_clientMiddlewareManifest.js +0 -0
- /package/frontend-build/.next/static/{cs3gFMScfwOahyeFSLcqL → 65DqAqVD6tjvCkcwNTV2U}/_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
|
},
|
|
@@ -93,6 +93,9 @@ def set_exact(
|
|
|
93
93
|
# semantic hits limited to nearly-identical prompts.
|
|
94
94
|
|
|
95
95
|
_semantic_collections: dict[str, Any] = {}
|
|
96
|
+
# Track step IDs we've already warned about so the "embedding unavailable" log
|
|
97
|
+
# fires at most once per step per process — avoids spamming during a long run.
|
|
98
|
+
_warned_no_embed_steps: set[str] = set()
|
|
96
99
|
|
|
97
100
|
|
|
98
101
|
def _get_memory_store():
|
|
@@ -132,6 +135,19 @@ def _embed(text: str) -> Optional[list[float]]:
|
|
|
132
135
|
return None
|
|
133
136
|
|
|
134
137
|
|
|
138
|
+
def _warn_no_embedding(step_id: str) -> None:
|
|
139
|
+
"""One-shot log when the embedding provider is unreachable for this step."""
|
|
140
|
+
if step_id in _warned_no_embed_steps:
|
|
141
|
+
return
|
|
142
|
+
_warned_no_embed_steps.add(step_id)
|
|
143
|
+
print(
|
|
144
|
+
f"DEBUG cache: ⚠️ semantic cache disabled for step '{step_id}' — "
|
|
145
|
+
f"embedding provider unavailable. Start Ollama with `nomic-embed-text` "
|
|
146
|
+
f"or set `embedding_model` in Settings. Exact-match cache still works.",
|
|
147
|
+
flush=True,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
|
|
135
151
|
def get_semantic(
|
|
136
152
|
step_id: str,
|
|
137
153
|
model: str,
|
|
@@ -145,6 +161,7 @@ def get_semantic(
|
|
|
145
161
|
return None
|
|
146
162
|
emb = _embed((system or "") + "\n\n" + user_message)
|
|
147
163
|
if emb is None:
|
|
164
|
+
_warn_no_embedding(step_id)
|
|
148
165
|
return None
|
|
149
166
|
try:
|
|
150
167
|
res = coll.query(query_embeddings=[emb], n_results=1)
|
|
@@ -183,6 +200,7 @@ def set_semantic(
|
|
|
183
200
|
return
|
|
184
201
|
emb = _embed((system or "") + "\n\n" + user_message)
|
|
185
202
|
if emb is None:
|
|
203
|
+
_warn_no_embedding(step_id)
|
|
186
204
|
return
|
|
187
205
|
# Reuse the exact-cache key as the Chroma document ID so storage stays unified.
|
|
188
206
|
key = store.make_key("resp_semantic", model, step_id, user_message)
|
|
@@ -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
|
|
@@ -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
|
|
@@ -52,13 +52,15 @@ class AgentStepExecutor:
|
|
|
52
52
|
if transition is None:
|
|
53
53
|
from .context import TransitionContext
|
|
54
54
|
transition = TransitionContext(origin_type="entry", execution_number=1)
|
|
55
|
-
prompt, system_prompt_extra = build_origin_aware_context(
|
|
55
|
+
prompt, system_prompt_extra, system_prompt_prefix = build_origin_aware_context(
|
|
56
56
|
step, run, engine, transition
|
|
57
57
|
)
|
|
58
58
|
inputs_snapshot = snapshot_inputs(step, run, engine)
|
|
59
59
|
|
|
60
60
|
# Emit prompt for the orchestration logger (filtered out before SSE)
|
|
61
|
-
yield {"type": "_log_prompt", "orch_step_id": step.id, "prompt": prompt,
|
|
61
|
+
yield {"type": "_log_prompt", "orch_step_id": step.id, "prompt": prompt,
|
|
62
|
+
"system_prompt_extra": system_prompt_extra,
|
|
63
|
+
"system_prompt_prefix": system_prompt_prefix}
|
|
62
64
|
|
|
63
65
|
# ── Orchestrator-as-agent: delegate to a nested OrchestrationEngine ──
|
|
64
66
|
target_agent = next(
|
|
@@ -99,6 +101,7 @@ class AgentStepExecutor:
|
|
|
99
101
|
source="orchestration",
|
|
100
102
|
run_id=run.run_id,
|
|
101
103
|
system_prompt_extra=system_prompt_extra,
|
|
104
|
+
system_prompt_prefix=system_prompt_prefix,
|
|
102
105
|
model_override=step.model,
|
|
103
106
|
):
|
|
104
107
|
execution_events.append(event)
|
|
@@ -271,10 +274,12 @@ class ToolStepExecutor:
|
|
|
271
274
|
transition = getattr(engine, "current_transition", None)
|
|
272
275
|
if transition is None:
|
|
273
276
|
transition = TransitionContext(origin_type="entry", execution_number=1)
|
|
274
|
-
prompt, system_prompt_extra = build_origin_aware_context(step, run, engine, transition)
|
|
277
|
+
prompt, system_prompt_extra, system_prompt_prefix = build_origin_aware_context(step, run, engine, transition)
|
|
275
278
|
inputs_snapshot = snapshot_inputs(step, run, engine)
|
|
276
279
|
|
|
277
|
-
yield {"type": "_log_prompt", "orch_step_id": step.id, "prompt": prompt,
|
|
280
|
+
yield {"type": "_log_prompt", "orch_step_id": step.id, "prompt": prompt,
|
|
281
|
+
"system_prompt_extra": system_prompt_extra,
|
|
282
|
+
"system_prompt_prefix": system_prompt_prefix}
|
|
278
283
|
|
|
279
284
|
# Model resolution — same pattern as EvaluatorStepExecutor and LLMStepExecutor
|
|
280
285
|
settings = load_settings()
|
|
@@ -325,9 +330,12 @@ class ToolStepExecutor:
|
|
|
325
330
|
|
|
326
331
|
print(f"DEBUG TOOL STEP: turn {turn + 1}/{max_turns} model={model} tool={tool_name}", flush=True)
|
|
327
332
|
try:
|
|
333
|
+
tool_sys_prompt = "You are a tool-calling assistant. Output ONLY valid JSON."
|
|
334
|
+
if system_prompt_prefix:
|
|
335
|
+
tool_sys_prompt = system_prompt_prefix + "\n\n" + tool_sys_prompt
|
|
328
336
|
response = await llm_generate(
|
|
329
337
|
prompt_msg=turn_prompt,
|
|
330
|
-
sys_prompt=
|
|
338
|
+
sys_prompt=tool_sys_prompt,
|
|
331
339
|
mode=mode,
|
|
332
340
|
current_model=model,
|
|
333
341
|
current_settings=settings,
|
|
@@ -1178,10 +1186,12 @@ class LLMStepExecutor:
|
|
|
1178
1186
|
if transition is None:
|
|
1179
1187
|
from .context import TransitionContext
|
|
1180
1188
|
transition = TransitionContext(origin_type="entry", execution_number=1)
|
|
1181
|
-
prompt, system_prompt_extra = build_origin_aware_context(step, run, engine, transition)
|
|
1189
|
+
prompt, system_prompt_extra, system_prompt_prefix = build_origin_aware_context(step, run, engine, transition)
|
|
1182
1190
|
inputs_snapshot = snapshot_inputs(step, run, engine)
|
|
1183
1191
|
|
|
1184
|
-
yield {"type": "_log_prompt", "orch_step_id": step.id, "prompt": prompt,
|
|
1192
|
+
yield {"type": "_log_prompt", "orch_step_id": step.id, "prompt": prompt,
|
|
1193
|
+
"system_prompt_extra": system_prompt_extra,
|
|
1194
|
+
"system_prompt_prefix": system_prompt_prefix}
|
|
1185
1195
|
yield {
|
|
1186
1196
|
"type": "thinking",
|
|
1187
1197
|
"orch_step_id": step.id,
|
|
@@ -1196,7 +1206,11 @@ class LLMStepExecutor:
|
|
|
1196
1206
|
mode = detect_mode_from_model(model)
|
|
1197
1207
|
|
|
1198
1208
|
# system_prompt_extra already contains datetime + workflow graph + position.
|
|
1209
|
+
# system_prompt_prefix is the iteration banner (re-runs only); prepend it
|
|
1210
|
+
# so it appears at the top of the system prompt.
|
|
1199
1211
|
sys_prompt = f"You are a helpful assistant. Be concise and accurate.\n\n{system_prompt_extra}"
|
|
1212
|
+
if system_prompt_prefix:
|
|
1213
|
+
sys_prompt = system_prompt_prefix + "\n\n" + sys_prompt
|
|
1200
1214
|
|
|
1201
1215
|
try:
|
|
1202
1216
|
response = await llm_generate(
|
|
@@ -512,6 +512,7 @@ async def run_agent_step(
|
|
|
512
512
|
run_id: str | None = None,
|
|
513
513
|
images: list[str] | None = None,
|
|
514
514
|
system_prompt_extra: str | None = None,
|
|
515
|
+
system_prompt_prefix: str | None = None,
|
|
515
516
|
# ── optional extension params (used by builder wrapper) ───────────────────
|
|
516
517
|
agent_override: dict | None = None, # skip _resolve_agent_by_id
|
|
517
518
|
tools_override: list | None = None, # skip aggregate_all_tools; list of OpenAI-format tool dicts
|
|
@@ -637,6 +638,10 @@ async def run_agent_step(
|
|
|
637
638
|
# Inject orchestration-awareness block when called from an orchestration step
|
|
638
639
|
if system_prompt_extra:
|
|
639
640
|
system_prompt_text = system_prompt_text + "\n\n" + system_prompt_extra
|
|
641
|
+
# Iteration banner: prepended so it can't be drowned out by a long agent
|
|
642
|
+
# system prompt. Only set on re-runs (execution_number > 1).
|
|
643
|
+
if system_prompt_prefix:
|
|
644
|
+
system_prompt_text = system_prompt_prefix + "\n\n" + system_prompt_text
|
|
640
645
|
|
|
641
646
|
async def generate_response(prompt_msg, sys_prompt, tools=None, history_messages=None, memory_context_text="", images_for_turn=None, tool_name_for_log=None):
|
|
642
647
|
return await llm_generate_response(
|
|
@@ -719,6 +724,8 @@ async def run_agent_step(
|
|
|
719
724
|
# step context, shared state, or turn-budget instructions.
|
|
720
725
|
if system_prompt_extra:
|
|
721
726
|
active_sys_prompt = active_sys_prompt + "\n\n" + system_prompt_extra
|
|
727
|
+
if system_prompt_prefix:
|
|
728
|
+
active_sys_prompt = system_prompt_prefix + "\n\n" + active_sys_prompt
|
|
722
729
|
|
|
723
730
|
# Determine prompt
|
|
724
731
|
if turn == 0:
|
|
@@ -307,10 +307,6 @@ async def get_config():
|
|
|
307
307
|
# Mask: show only last 4 chars, e.g. ****h453
|
|
308
308
|
masked_client_id = ("****" + client_id_full[-8:]) if len(client_id_full) > 8 else "****"
|
|
309
309
|
|
|
310
|
-
# Connected only if both the main token and the workspace-mcp token exist
|
|
311
|
-
mcp_token_file = os.path.join(DATA_DIR, "google-credentials", "token.json")
|
|
312
|
-
is_connected = os.path.exists(TOKEN_FILE) and os.path.exists(mcp_token_file)
|
|
313
|
-
|
|
314
310
|
# Read user email from token.json if available
|
|
315
311
|
user_email = None
|
|
316
312
|
if has_token:
|
|
@@ -330,6 +326,20 @@ async def get_config():
|
|
|
330
326
|
except Exception:
|
|
331
327
|
pass
|
|
332
328
|
|
|
329
|
+
# Real connectivity check: token must be valid (or refreshable) AND
|
|
330
|
+
# workspace-mcp must have a token file it can read. get_google_credentials()
|
|
331
|
+
# validates, auto-refreshes, and syncs to the MCP dir on success.
|
|
332
|
+
is_connected = False
|
|
333
|
+
try:
|
|
334
|
+
from services.google import get_google_credentials
|
|
335
|
+
if get_google_credentials() is not None:
|
|
336
|
+
mcp_dir = os.path.join(DATA_DIR, "google-credentials")
|
|
337
|
+
per_user = user_email and os.path.exists(os.path.join(mcp_dir, f"{user_email}.json"))
|
|
338
|
+
generic = os.path.exists(os.path.join(mcp_dir, "token.json"))
|
|
339
|
+
is_connected = bool(per_user or generic)
|
|
340
|
+
except Exception as e:
|
|
341
|
+
print(f"Warning: get_google_credentials() check failed: {e}")
|
|
342
|
+
|
|
333
343
|
return {
|
|
334
344
|
"has_credentials": True,
|
|
335
345
|
"client_id": masked_client_id,
|
|
@@ -338,9 +348,7 @@ async def get_config():
|
|
|
338
348
|
"user_email": user_email,
|
|
339
349
|
}
|
|
340
350
|
except Exception as e:
|
|
341
|
-
|
|
342
|
-
is_connected = os.path.exists(TOKEN_FILE) and os.path.exists(mcp_token_file)
|
|
343
|
-
return {"has_credentials": True, "error": str(e), "is_connected": is_connected}
|
|
351
|
+
return {"has_credentials": True, "error": str(e), "is_connected": False}
|
|
344
352
|
|
|
345
353
|
|
|
346
354
|
@router.get("/api/file")
|
package/backend/core/server.py
CHANGED
|
@@ -118,6 +118,15 @@ def _get_google_oauth_env() -> dict[str, str]:
|
|
|
118
118
|
token_file = DATA_DIR / "token.json"
|
|
119
119
|
if not creds_file.exists():
|
|
120
120
|
return {}
|
|
121
|
+
|
|
122
|
+
# Refresh the Google token (if expired-but-refreshable) before launching
|
|
123
|
+
# workspace-mcp so the subprocess inherits valid creds and doesn't emit
|
|
124
|
+
# "ACTION REQUIRED: Google Authentication Needed" on the first tool call.
|
|
125
|
+
try:
|
|
126
|
+
from services.google import get_google_credentials
|
|
127
|
+
get_google_credentials()
|
|
128
|
+
except Exception as e:
|
|
129
|
+
print(f"Warning: Token refresh at startup failed: {e}")
|
|
121
130
|
try:
|
|
122
131
|
creds = json.loads(creds_file.read_text())
|
|
123
132
|
installed = creds.get("installed", creds.get("web", {}))
|