synapse-orch-ai 1.5.5 → 1.5.6
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/llm_providers.py +78 -41
- package/backend/core/orchestration/context.py +23 -10
- package/backend/core/orchestration/logger.py +9 -1
- package/backend/core/react_engine.py +184 -109
- package/backend/core/routes/chat.py +9 -1
- package/backend/core/tools.py +109 -53
- 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/page_client-reference-manifest.js +1 -1
- package/frontend-build/.next/server/app/_not-found.html +1 -1
- package/frontend-build/.next/server/app/_not-found.rsc +2 -2
- package/frontend-build/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
- 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 +2 -2
- 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 +2 -2
- package/frontend-build/.next/server/app/index.html +1 -1
- package/frontend-build/.next/server/app/index.rsc +3 -3
- package/frontend-build/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/frontend-build/.next/server/app/index.segments/_full.segment.rsc +3 -3
- package/frontend-build/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/frontend-build/.next/server/app/index.segments/_index.segment.rsc +2 -2
- package/frontend-build/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/frontend-build/.next/server/app/login/page_client-reference-manifest.js +1 -1
- package/frontend-build/.next/server/app/login.html +1 -1
- package/frontend-build/.next/server/app/login.rsc +2 -2
- package/frontend-build/.next/server/app/login.segments/_full.segment.rsc +2 -2
- package/frontend-build/.next/server/app/login.segments/_head.segment.rsc +1 -1
- package/frontend-build/.next/server/app/login.segments/_index.segment.rsc +2 -2
- package/frontend-build/.next/server/app/login.segments/_tree.segment.rsc +2 -2
- 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/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/0htooj7jtj_tj.js +1 -0
- package/frontend-build/.next/static/chunks/0oxr2cvb1sal0.css +1 -0
- package/package.json +1 -1
- package/frontend-build/.next/static/chunks/0m6b8x86zmjhr.js +0 -1
- package/frontend-build/.next/static/chunks/0sifx3jp~fn_h.css +0 -1
- /package/frontend-build/.next/static/{wMcHp1vhEa3V0OBQS57Hg → BSH_GDVYozCn9fUMJ5J-6}/_buildManifest.js +0 -0
- /package/frontend-build/.next/static/{wMcHp1vhEa3V0OBQS57Hg → BSH_GDVYozCn9fUMJ5J-6}/_clientMiddlewareManifest.js +0 -0
- /package/frontend-build/.next/static/{wMcHp1vhEa3V0OBQS57Hg → BSH_GDVYozCn9fUMJ5J-6}/_ssgManifest.js +0 -0
|
@@ -663,15 +663,11 @@ async def call_openai(model, messages, api_key, tools=None, images=None):
|
|
|
663
663
|
# Handle tool_calls response
|
|
664
664
|
choice = data["choices"][0]
|
|
665
665
|
msg = choice.get("message", {})
|
|
666
|
+
text = _openai_compat_extract(msg)
|
|
666
667
|
if msg.get("tool_calls"):
|
|
667
|
-
tc = msg["tool_calls"][0]
|
|
668
|
-
text = json.dumps({
|
|
669
|
-
"tool": tc["function"]["name"],
|
|
670
|
-
"arguments": json.loads(tc["function"].get("arguments", "{}"))
|
|
671
|
-
})
|
|
672
668
|
return text, input_tokens, output_tokens
|
|
673
669
|
print(f"DEBUG: ✅ OpenAI call complete (attempt {attempt})", flush=True)
|
|
674
|
-
return
|
|
670
|
+
return text, input_tokens, output_tokens
|
|
675
671
|
except httpx.TimeoutException:
|
|
676
672
|
last_error = f"Request timed out ({OPENAI_TIMEOUT}s)"
|
|
677
673
|
print(f"DEBUG: ⏱️ OpenAI timeout on attempt {attempt}/{MAX_RETRIES}. Retrying in {backoff}s...", flush=True)
|
|
@@ -723,22 +719,56 @@ def _convert_tools_for_anthropic(ollama_tools: list[dict] | None) -> list[dict]
|
|
|
723
719
|
return tools if tools else None
|
|
724
720
|
|
|
725
721
|
|
|
722
|
+
def _openai_compat_extract(msg: dict) -> str:
|
|
723
|
+
"""Normalize an OpenAI-compatible chat-completion message into the
|
|
724
|
+
`{"tool": ..., "arguments": ...}` shape (or plain text).
|
|
725
|
+
|
|
726
|
+
When the model emits BOTH content text and tool_calls in the same response
|
|
727
|
+
(newer reasoning models, some Claude-compat APIs), the text is preserved as
|
|
728
|
+
a `[REASONING]...[/REASONING]` preamble so the ReAct loop can project it
|
|
729
|
+
into the llm_reasoning event for the UI and orchestration log.
|
|
730
|
+
"""
|
|
731
|
+
tool_calls = msg.get("tool_calls") or []
|
|
732
|
+
if tool_calls:
|
|
733
|
+
tc = tool_calls[0]
|
|
734
|
+
call_json = json.dumps({
|
|
735
|
+
"tool": tc["function"]["name"],
|
|
736
|
+
"arguments": json.loads(tc["function"].get("arguments") or "{}"),
|
|
737
|
+
})
|
|
738
|
+
reasoning = (msg.get("content") or "").strip()
|
|
739
|
+
if reasoning:
|
|
740
|
+
return f"[REASONING]\n{reasoning}\n[/REASONING]\n{call_json}"
|
|
741
|
+
return call_json
|
|
742
|
+
return msg.get("content", "") or ""
|
|
743
|
+
|
|
744
|
+
|
|
726
745
|
def _extract_anthropic_response(response) -> str:
|
|
727
|
-
"""Extract text or tool call from an Anthropic SDK response.
|
|
746
|
+
"""Extract text and/or tool call from an Anthropic SDK response.
|
|
728
747
|
|
|
729
|
-
|
|
730
|
-
|
|
748
|
+
When the model emits a text block before a tool_use block (Claude does this
|
|
749
|
+
routinely), preserve the text as a [REASONING]...[/REASONING] preamble so
|
|
750
|
+
the downstream ReAct loop can project it into the llm_reasoning event.
|
|
751
|
+
Without this, native tool calling silently drops the model's pre-call
|
|
752
|
+
reasoning.
|
|
731
753
|
"""
|
|
732
754
|
if not response.content:
|
|
733
755
|
return "Error: Empty Anthropic response."
|
|
734
756
|
|
|
735
|
-
|
|
757
|
+
text_parts: list[str] = []
|
|
758
|
+
tool_use_call: dict | None = None
|
|
736
759
|
for block in response.content:
|
|
737
|
-
if block.type == "tool_use":
|
|
738
|
-
|
|
760
|
+
if block.type == "tool_use" and tool_use_call is None:
|
|
761
|
+
tool_use_call = {"tool": block.name, "arguments": block.input or {}}
|
|
762
|
+
elif block.type == "text" and block.text:
|
|
763
|
+
text_parts.append(block.text)
|
|
764
|
+
|
|
765
|
+
if tool_use_call is not None:
|
|
766
|
+
call_json = json.dumps(tool_use_call)
|
|
767
|
+
reasoning = "\n".join(t for t in text_parts if t and t.strip()).strip()
|
|
768
|
+
if reasoning:
|
|
769
|
+
return f"[REASONING]\n{reasoning}\n[/REASONING]\n{call_json}"
|
|
770
|
+
return call_json
|
|
739
771
|
|
|
740
|
-
# Collect text blocks
|
|
741
|
-
text_parts = [block.text for block in response.content if block.type == "text" and block.text]
|
|
742
772
|
if text_parts:
|
|
743
773
|
return "\n".join(text_parts)
|
|
744
774
|
|
|
@@ -943,7 +973,14 @@ def _convert_messages_for_gemini(messages: list[dict], images: list[str] | None
|
|
|
943
973
|
|
|
944
974
|
|
|
945
975
|
def _extract_gemini_response(response) -> str:
|
|
946
|
-
"""Extract text or function call from a Gemini response.
|
|
976
|
+
"""Extract text and/or function call from a Gemini response.
|
|
977
|
+
|
|
978
|
+
When the model emits BOTH text and a function_call in the same response,
|
|
979
|
+
the text is preserved as a [REASONING]...[/REASONING] preamble so the
|
|
980
|
+
downstream ReAct loop can project it into the llm_reasoning event for the
|
|
981
|
+
chat UI and orchestration log. Without this, native function calling
|
|
982
|
+
silently drops the model's pre-call reasoning.
|
|
983
|
+
"""
|
|
947
984
|
if not response.candidates:
|
|
948
985
|
return "Error: No response candidates from Gemini."
|
|
949
986
|
|
|
@@ -956,23 +993,27 @@ def _extract_gemini_response(response) -> str:
|
|
|
956
993
|
reason = candidate.finish_reason.name if candidate.finish_reason else "UNKNOWN"
|
|
957
994
|
return f"Error: Empty Gemini response. Finish Reason: {reason}"
|
|
958
995
|
|
|
959
|
-
#
|
|
960
|
-
|
|
996
|
+
# Walk parts in order, collecting text (reasoning) and function calls.
|
|
997
|
+
text_parts: list[str] = []
|
|
998
|
+
function_calls: list[dict] = []
|
|
961
999
|
for p in candidate.content.parts:
|
|
962
1000
|
if p.function_call:
|
|
963
1001
|
fc = p.function_call
|
|
964
1002
|
args = dict(fc.args) if fc.args else {}
|
|
965
1003
|
function_calls.append({"tool": fc.name, "arguments": args})
|
|
1004
|
+
elif p.text:
|
|
1005
|
+
text_parts.append(p.text)
|
|
966
1006
|
|
|
967
1007
|
if function_calls:
|
|
968
|
-
# Return the first function call (ReAct loop processes one at a time)
|
|
969
1008
|
if len(function_calls) > 1:
|
|
970
1009
|
names = [fc["tool"] for fc in function_calls]
|
|
971
1010
|
print(f"DEBUG: ⚠️ Gemini returned {len(function_calls)} function calls: {names}. Using first: {names[0]}")
|
|
972
|
-
|
|
1011
|
+
call_json = json.dumps(function_calls[0])
|
|
1012
|
+
reasoning = "\n".join(t for t in text_parts if t and t.strip()).strip()
|
|
1013
|
+
if reasoning:
|
|
1014
|
+
return f"[REASONING]\n{reasoning}\n[/REASONING]\n{call_json}"
|
|
1015
|
+
return call_json
|
|
973
1016
|
|
|
974
|
-
# Collect text parts
|
|
975
|
-
text_parts = [p.text for p in candidate.content.parts if p.text]
|
|
976
1017
|
if text_parts:
|
|
977
1018
|
return "\n".join(text_parts)
|
|
978
1019
|
|
|
@@ -1141,15 +1182,11 @@ async def call_grok(model, messages, system, api_key, tools=None, images=None):
|
|
|
1141
1182
|
output_tokens = usage.get("completion_tokens", 0)
|
|
1142
1183
|
choice = data["choices"][0]
|
|
1143
1184
|
msg = choice.get("message", {})
|
|
1185
|
+
text = _openai_compat_extract(msg)
|
|
1144
1186
|
if msg.get("tool_calls"):
|
|
1145
|
-
tc = msg["tool_calls"][0]
|
|
1146
|
-
text = json.dumps({
|
|
1147
|
-
"tool": tc["function"]["name"],
|
|
1148
|
-
"arguments": json.loads(tc["function"].get("arguments", "{}"))
|
|
1149
|
-
})
|
|
1150
1187
|
return text, input_tokens, output_tokens
|
|
1151
1188
|
print(f"DEBUG: ✅ Grok call complete (attempt {attempt})", flush=True)
|
|
1152
|
-
return
|
|
1189
|
+
return text, input_tokens, output_tokens
|
|
1153
1190
|
except httpx.TimeoutException:
|
|
1154
1191
|
last_error = f"Request timed out ({GROK_TIMEOUT}s)"
|
|
1155
1192
|
print(f"DEBUG: ⏱️ Grok timeout on attempt {attempt}/{MAX_RETRIES}. Retrying in {backoff}s...", flush=True)
|
|
@@ -1233,15 +1270,11 @@ async def call_deepseek(model, messages, system, api_key, tools=None, images=Non
|
|
|
1233
1270
|
output_tokens = usage.get("completion_tokens", 0)
|
|
1234
1271
|
choice = data["choices"][0]
|
|
1235
1272
|
msg = choice.get("message", {})
|
|
1273
|
+
text = _openai_compat_extract(msg)
|
|
1236
1274
|
if msg.get("tool_calls"):
|
|
1237
|
-
tc = msg["tool_calls"][0]
|
|
1238
|
-
text = json.dumps({
|
|
1239
|
-
"tool": tc["function"]["name"],
|
|
1240
|
-
"arguments": json.loads(tc["function"].get("arguments", "{}"))
|
|
1241
|
-
})
|
|
1242
1275
|
return text, input_tokens, output_tokens
|
|
1243
1276
|
print(f"DEBUG: ✅ DeepSeek call complete (attempt {attempt})", flush=True)
|
|
1244
|
-
return
|
|
1277
|
+
return text, input_tokens, output_tokens
|
|
1245
1278
|
except httpx.TimeoutException:
|
|
1246
1279
|
last_error = f"Request timed out ({DEEPSEEK_TIMEOUT}s)"
|
|
1247
1280
|
print(f"DEBUG: ⏱️ DeepSeek timeout on attempt {attempt}/{MAX_RETRIES}. Retrying in {backoff}s...", flush=True)
|
|
@@ -1346,15 +1379,11 @@ async def call_v1_compatible(model, messages, system, base_url, api_key, tools=N
|
|
|
1346
1379
|
output_tokens = usage.get("completion_tokens", 0)
|
|
1347
1380
|
choice = data["choices"][0]
|
|
1348
1381
|
msg = choice.get("message", {})
|
|
1382
|
+
text = _openai_compat_extract(msg)
|
|
1349
1383
|
if msg.get("tool_calls"):
|
|
1350
|
-
tc = msg["tool_calls"][0]
|
|
1351
|
-
text = json.dumps({
|
|
1352
|
-
"tool": tc["function"]["name"],
|
|
1353
|
-
"arguments": json.loads(tc["function"].get("arguments", "{}"))
|
|
1354
|
-
})
|
|
1355
1384
|
return text, input_tokens, output_tokens
|
|
1356
1385
|
print(f"DEBUG: ✅ V1-compatible call complete (attempt {attempt})", flush=True)
|
|
1357
|
-
return
|
|
1386
|
+
return text, input_tokens, output_tokens
|
|
1358
1387
|
except httpx.TimeoutException:
|
|
1359
1388
|
last_error = f"Request timed out ({V1_TIMEOUT}s)"
|
|
1360
1389
|
print(f"DEBUG: ⏱️ V1-compatible timeout on attempt {attempt}/{MAX_RETRIES}. Retrying in {backoff}s...", flush=True)
|
|
@@ -1848,13 +1877,21 @@ async def generate_response(
|
|
|
1848
1877
|
|
|
1849
1878
|
# Check for native tool calls
|
|
1850
1879
|
if "tool_calls" in msg and msg["tool_calls"]:
|
|
1851
|
-
# Convert Ollama native tool call to our internal JSON format
|
|
1880
|
+
# Convert Ollama native tool call to our internal JSON format.
|
|
1881
|
+
# Preserve any text content the model emitted alongside the
|
|
1882
|
+
# call as a [REASONING] preamble (some Ollama models emit
|
|
1883
|
+
# both — qwen3, deepseek-r1 routinely do).
|
|
1852
1884
|
tc = msg["tool_calls"][0]
|
|
1853
1885
|
print(f"DEBUG: Native Tool Call received: {tc['function']['name']}", flush=True)
|
|
1854
|
-
|
|
1886
|
+
call_json = json.dumps({
|
|
1855
1887
|
"tool": tc["function"]["name"],
|
|
1856
1888
|
"arguments": tc["function"]["arguments"]
|
|
1857
1889
|
})
|
|
1890
|
+
reasoning = (msg.get("content") or "").strip()
|
|
1891
|
+
if reasoning:
|
|
1892
|
+
result_text = f"[REASONING]\n{reasoning}\n[/REASONING]\n{call_json}"
|
|
1893
|
+
else:
|
|
1894
|
+
result_text = call_json
|
|
1858
1895
|
else:
|
|
1859
1896
|
result_text = msg.get("content", "")
|
|
1860
1897
|
|
|
@@ -564,7 +564,17 @@ def _format_context_value(
|
|
|
564
564
|
label: str,
|
|
565
565
|
max_chars: int = 5000,
|
|
566
566
|
) -> str:
|
|
567
|
-
"""Format a single shared_state value for inclusion in the prompt.
|
|
567
|
+
"""Format a single shared_state value for inclusion in the prompt.
|
|
568
|
+
|
|
569
|
+
Format:
|
|
570
|
+
### <key>
|
|
571
|
+
Source: <producer agent or upstream description>
|
|
572
|
+
<value>
|
|
573
|
+
|
|
574
|
+
The bare key (no brackets) is used as the header so the heading cannot be
|
|
575
|
+
visually confused with `{state.<key>}` template placeholders. The Source
|
|
576
|
+
line carries provenance (which agent / step produced this value).
|
|
577
|
+
"""
|
|
568
578
|
# List values from loop/parallel accumulation
|
|
569
579
|
if isinstance(val, list) and val and isinstance(val[0], dict) and "result" in val[0]:
|
|
570
580
|
parts = []
|
|
@@ -575,16 +585,17 @@ def _format_context_value(
|
|
|
575
585
|
if len(result_str) > max_chars:
|
|
576
586
|
from .summarizer import smart_truncate
|
|
577
587
|
result_str = smart_truncate(result_str, max_chars)
|
|
578
|
-
|
|
579
|
-
source = f"{agent} → {key}
|
|
580
|
-
parts.append(f"###
|
|
588
|
+
iter_suffix = f" (iteration {iteration})" if iteration else ""
|
|
589
|
+
source = f"{agent} → {key}" if agent else f"loop accumulator → {key}"
|
|
590
|
+
parts.append(f"### {key}{iter_suffix}\nSource: {source}\n{result_str}")
|
|
581
591
|
return "\n\n".join(parts)
|
|
582
592
|
|
|
583
593
|
val_str = str(val)
|
|
584
594
|
if len(val_str) > max_chars:
|
|
585
595
|
from .summarizer import smart_truncate
|
|
586
596
|
val_str = smart_truncate(val_str, max_chars)
|
|
587
|
-
|
|
597
|
+
source = label if label and label != key else "shared state"
|
|
598
|
+
return f"### {key}\nSource: {source}\n{val_str}"
|
|
588
599
|
|
|
589
600
|
|
|
590
601
|
def build_origin_aware_context(
|
|
@@ -713,7 +724,9 @@ def build_origin_aware_context(
|
|
|
713
724
|
|
|
714
725
|
# Always include user_input unless explicitly in input_keys
|
|
715
726
|
if "user_input" in run.shared_state and "user_input" not in (step.input_keys or []):
|
|
716
|
-
context_parts.append(
|
|
727
|
+
context_parts.append(
|
|
728
|
+
f"### user_input\nSource: initial input\n{run.shared_state['user_input']}"
|
|
729
|
+
)
|
|
717
730
|
|
|
718
731
|
# Human response keys (always inject unless already listed)
|
|
719
732
|
human_keys = {"human_response"}
|
|
@@ -730,7 +743,7 @@ def build_origin_aware_context(
|
|
|
730
743
|
if len(val) > 3000:
|
|
731
744
|
from .summarizer import smart_truncate
|
|
732
745
|
val = smart_truncate(val, 3000)
|
|
733
|
-
context_parts.append(f"###
|
|
746
|
+
context_parts.append(f"### {hkey}\nSource: human response\n{val}")
|
|
734
747
|
|
|
735
748
|
# Explicitly declared input_keys
|
|
736
749
|
for key in (step.input_keys or []):
|
|
@@ -775,7 +788,9 @@ def build_origin_aware_context(
|
|
|
775
788
|
prompt = "\n\n---\n\n".join(sections)
|
|
776
789
|
|
|
777
790
|
# ------------------------------------------------------------------
|
|
778
|
-
# System prompt addition —
|
|
791
|
+
# System prompt addition — workflow graph + step position
|
|
792
|
+
# (Date/time is injected separately by build_system_prompt for every
|
|
793
|
+
# agent including orchestration steps — do not duplicate it here.)
|
|
779
794
|
# ------------------------------------------------------------------
|
|
780
795
|
# Count completed executions per step for the graph (×N badges).
|
|
781
796
|
exec_counts: dict[str, int] = {}
|
|
@@ -789,8 +804,6 @@ def build_origin_aware_context(
|
|
|
789
804
|
|
|
790
805
|
graph_md = build_workflow_graph_markdown(engine.orch, step.id, exec_counts)
|
|
791
806
|
sys_lines = [
|
|
792
|
-
datetime_context(),
|
|
793
|
-
"",
|
|
794
807
|
graph_md,
|
|
795
808
|
"",
|
|
796
809
|
f"You are currently executing step **\"{step_name}\"** (execution #{transition.execution_number}).",
|
|
@@ -181,11 +181,19 @@ class OrchestrationLogger:
|
|
|
181
181
|
Preview: {preview}
|
|
182
182
|
""")
|
|
183
183
|
|
|
184
|
+
elif etype == "llm_reasoning":
|
|
185
|
+
reasoning = event.get("reasoning", "")
|
|
186
|
+
turn = event.get("turn", "")
|
|
187
|
+
self._write(f"""
|
|
188
|
+
💭 REASONING (turn {turn}):
|
|
189
|
+
{self._indent(reasoning)}
|
|
190
|
+
""")
|
|
191
|
+
|
|
184
192
|
elif etype == "llm_thought":
|
|
185
193
|
thought = event.get("thought", "")
|
|
186
194
|
turn = event.get("turn", "")
|
|
187
195
|
self._write(f"""
|
|
188
|
-
|
|
196
|
+
🛠️ ACTION (turn {turn}):
|
|
189
197
|
{self._indent(thought)}
|
|
190
198
|
""")
|
|
191
199
|
|