synapse-orch-ai 1.2.0 → 1.3.0

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.
Files changed (86) hide show
  1. package/README.md +52 -0
  2. package/backend/core/builder_tools.py +87 -4
  3. package/backend/core/models_orchestration.py +17 -0
  4. package/backend/core/native_builder/agents/saver_create.json +1 -1
  5. package/backend/core/native_builder/agents/saver_update.json +2 -2
  6. package/backend/core/orchestration/engine.py +40 -0
  7. package/backend/core/orchestration/steps.py +321 -0
  8. package/bin/synapse.js +26 -0
  9. package/frontend-build/.next/BUILD_ID +1 -1
  10. package/frontend-build/.next/app-path-routes-manifest.json +1 -1
  11. package/frontend-build/.next/build-manifest.json +3 -3
  12. package/frontend-build/.next/prerender-manifest.json +7 -7
  13. package/frontend-build/.next/routes-manifest.json +3 -3
  14. package/frontend-build/.next/server/app/_global-error.html +1 -1
  15. package/frontend-build/.next/server/app/_global-error.rsc +1 -1
  16. package/frontend-build/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  17. package/frontend-build/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  18. package/frontend-build/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  19. package/frontend-build/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  20. package/frontend-build/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  21. package/frontend-build/.next/server/app/_not-found/page.js +4 -4
  22. package/frontend-build/.next/server/app/_not-found/page.js.nft.json +1 -1
  23. package/frontend-build/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  24. package/frontend-build/.next/server/app/_not-found.html +1 -1
  25. package/frontend-build/.next/server/app/_not-found.rsc +3 -3
  26. package/frontend-build/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
  27. package/frontend-build/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  28. package/frontend-build/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  29. package/frontend-build/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  30. package/frontend-build/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  31. package/frontend-build/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  32. package/frontend-build/.next/server/app/icon.png/route/app-paths-manifest.json +3 -0
  33. package/frontend-build/.next/server/app/icon.png/route.js +7 -0
  34. package/frontend-build/.next/server/app/{icon.svg → icon.png}/route.js.nft.json +1 -1
  35. package/frontend-build/.next/server/app/icon.png.body +0 -0
  36. package/frontend-build/.next/server/app/icon.png.meta +1 -0
  37. package/frontend-build/.next/server/app/index.html +1 -1
  38. package/frontend-build/.next/server/app/index.rsc +3 -3
  39. package/frontend-build/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  40. package/frontend-build/.next/server/app/index.segments/_full.segment.rsc +3 -3
  41. package/frontend-build/.next/server/app/index.segments/_head.segment.rsc +1 -1
  42. package/frontend-build/.next/server/app/index.segments/_index.segment.rsc +2 -2
  43. package/frontend-build/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  44. package/frontend-build/.next/server/app/page.js +4 -4
  45. package/frontend-build/.next/server/app/page.js.nft.json +1 -1
  46. package/frontend-build/.next/server/app/page_client-reference-manifest.js +1 -1
  47. package/frontend-build/.next/server/app/settings/[tab]/page.js +4 -4
  48. package/frontend-build/.next/server/app/settings/[tab]/page.js.nft.json +1 -1
  49. package/frontend-build/.next/server/app/settings/[tab]/page_client-reference-manifest.js +1 -1
  50. package/frontend-build/.next/server/app-paths-manifest.json +1 -1
  51. package/frontend-build/.next/server/chunks/[externals]_next_dist_0arv.vj._.js +3 -0
  52. package/frontend-build/.next/server/chunks/_next-internal_server_app_icon_png_route_actions_12.gv.r.js +3 -0
  53. package/frontend-build/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_0dh.2jf.js +3 -0
  54. package/frontend-build/.next/server/chunks/ssr/{[root-of-the-server]__08gf6t7._.js → [root-of-the-server]__0ffv7p5._.js} +2 -2
  55. package/frontend-build/.next/server/chunks/ssr/[root-of-the-server]__0m2-0f1._.js +3 -0
  56. package/frontend-build/.next/server/chunks/ssr/[root-of-the-server]__120kr3r._.js +3 -0
  57. package/frontend-build/.next/server/chunks/ssr/_0b~n.nn._.js +15 -9
  58. package/frontend-build/.next/server/chunks/ssr/{node_modules_next_dist_esm_build_templates_app-page_0k.0n04.js → node_modules_next_dist_esm_build_templates_app-page_00t_ate.js} +3 -3
  59. package/frontend-build/.next/server/chunks/ssr/{node_modules_next_dist_esm_build_templates_app-page_08ib7il.js → node_modules_next_dist_esm_build_templates_app-page_031v14j.js} +3 -3
  60. package/frontend-build/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0p6f2go.js +4 -0
  61. package/frontend-build/.next/server/middleware-build-manifest.js +3 -3
  62. package/frontend-build/.next/server/pages/404.html +1 -1
  63. package/frontend-build/.next/server/pages/500.html +1 -1
  64. package/frontend-build/.next/server/server-reference-manifest.js +1 -1
  65. package/frontend-build/.next/server/server-reference-manifest.json +1 -1
  66. package/frontend-build/.next/static/chunks/0n_aip.6onjkv.js +52 -0
  67. package/frontend-build/.next/static/chunks/0or4d2ll~v9-c.css +1 -0
  68. package/frontend-build/.next/static/media/icon.17u0z6r5r.04f.png +0 -0
  69. package/package.json +1 -1
  70. package/frontend-build/.next/server/app/icon.svg/route/app-paths-manifest.json +0 -3
  71. package/frontend-build/.next/server/app/icon.svg/route.js +0 -6
  72. package/frontend-build/.next/server/app/icon.svg.body +0 -4
  73. package/frontend-build/.next/server/app/icon.svg.meta +0 -1
  74. package/frontend-build/.next/server/chunks/[root-of-the-server]__02je2si._.js +0 -3
  75. package/frontend-build/.next/server/chunks/_next-internal_server_app_icon_svg_route_actions_0-0ehc~.js +0 -3
  76. package/frontend-build/.next/server/chunks/ssr/[root-of-the-server]__0hfoz0-._.js +0 -3
  77. package/frontend-build/.next/server/chunks/ssr/[root-of-the-server]__0uo38ip._.js +0 -3
  78. package/frontend-build/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_13pf1l2.js +0 -4
  79. package/frontend-build/.next/static/chunks/14j.u32zioxms.js +0 -46
  80. package/frontend-build/.next/static/chunks/17yu_4zcwjlqz.css +0 -1
  81. package/frontend-build/.next/static/media/icon.08txep8y1sjlr.svg +0 -4
  82. /package/frontend-build/.next/server/app/{icon.svg → icon.png}/route/build-manifest.json +0 -0
  83. /package/frontend-build/.next/server/app/{icon.svg → icon.png}/route.js.map +0 -0
  84. /package/frontend-build/.next/static/{HD0MBamLacNNZ7iSAQZxb → cNaZfKm9YP2TSkTyHzETq}/_buildManifest.js +0 -0
  85. /package/frontend-build/.next/static/{HD0MBamLacNNZ7iSAQZxb → cNaZfKm9YP2TSkTyHzETq}/_clientMiddlewareManifest.js +0 -0
  86. /package/frontend-build/.next/static/{HD0MBamLacNNZ7iSAQZxb → cNaZfKm9YP2TSkTyHzETq}/_ssgManifest.js +0 -0
package/README.md CHANGED
@@ -294,8 +294,60 @@ An orchestration is a directed graph (DAG) of steps — you wire agents together
294
294
  | **Loop** | Repeat a set of steps N times. Use with transforms to iterate over lists or refine outputs. |
295
295
  | **Transform** | Execute arbitrary Python against the shared state dict. Reshape data, compute values, filter lists. |
296
296
  | **Human** | Pause and ask a human for input via a generated form. Execution resumes when the user responds. Fully resumable. |
297
+ | **Extract JSON** | Parse JSON out of any text — handles raw JSON, markdown code fences, and multiple objects (stored as an array). No LLM call. Perfect for pulling structured data out of an agent's raw output. |
298
+ | **Print** | Render a text or Markdown template with `{state.key}` interpolation and store it in the shared state. Use for building formatted summaries, reports, or notification bodies without an LLM call. |
299
+ | **IF / Else** | Evaluate a Python expression against the shared state and branch to one of two steps — true path or false path. Supports dot-notation (`state.result.flag`). Missing keys evaluate to `None`. No LLM call. |
300
+ | **Switch** | Match a Python expression's string result against a set of named cases. Each case routes to a different step; unmatched values fall through to the default route. No LLM call. |
297
301
  | **End** | Finalize the workflow. |
298
302
 
303
+ ### Deterministic Control-Flow Steps
304
+
305
+ Four step types execute **without any LLM call** — they are fast, free, and completely predictable. Use them to add control flow and data handling between your agent steps.
306
+
307
+ #### Extract JSON
308
+ Finds and parses JSON from raw text. Works with:
309
+ - Plain JSON objects / arrays
310
+ - Markdown code fences (` ```json ... ``` `)
311
+ - Multiple JSON blocks in a single string (stored as an array)
312
+
313
+ ```
314
+ Input key: llm_raw_output (e.g. "The answer is: ```json\n{\"score\": 8}\n```")
315
+ Output key: parsed (→ { "score": 8 })
316
+ ```
317
+
318
+ #### Print
319
+ Renders a Markdown or plain-text template with `{state.key}` and `{state.key.nested}` placeholders resolved from the shared state, then stores the result.
320
+
321
+ ```
322
+ print_content: "# Report\n\nScore: {state.score}\nCategory: {state.category}"
323
+ output_key: report_text
324
+ ```
325
+
326
+ #### IF / Else
327
+ Evaluates a Python expression against the shared state and branches to one of two steps. Dot-notation is supported — missing keys are `None`.
328
+
329
+ ```
330
+ if_condition: state.score > 7
331
+ if_true_step_id: step_approve
332
+ if_false_step_id: step_reject
333
+ ```
334
+
335
+ Safe built-ins only (`len`, `str`, `int`, `float`, `bool`, `list`, `dict`, `max`, `min`, `abs`, `round`, `any`, `all`). No imports.
336
+
337
+ #### Switch
338
+ Converts a Python expression to a string and matches it against named cases. Unmatched values fall through to `switch_default_step_id`.
339
+
340
+ ```
341
+ switch_expression: state.category
342
+ switch_cases:
343
+ "sports" → step_sports_handler
344
+ "politics" → step_politics_handler
345
+ "science" → step_science_handler
346
+ switch_default_step_id: step_general_handler
347
+ ```
348
+
349
+ > **Tip:** Chain these steps to build lightweight classification pipelines — use an LLM step to classify, an **Extract JSON** step to parse its output, and a **Switch** step to route — all without extra LLM calls.
350
+
299
351
  ### Shared State
300
352
 
301
353
  Every step reads from and writes to a shared state dictionary. Define the schema upfront:
@@ -40,7 +40,8 @@ STEPS_JSON_DESCRIPTION = (
40
40
  "in your system prompt for the full field list per type. Keep each step "
41
41
  "lean — include only fields that differ from zero-defaults.\n"
42
42
  "Sub-structures MUST be JSON strings inside each step: route_map_json, "
43
- "route_descriptions_json, parallel_branches_json, human_fields_json.\n"
43
+ "route_descriptions_json, parallel_branches_json, human_fields_json, "
44
+ "switch_cases_json.\n"
44
45
  "Minimal example (2 agent steps):\n"
45
46
  "'[{\"id\":\"step_abc1234\",\"name\":\"Analyse\",\"type\":\"agent\","
46
47
  "\"agent_id\":\"agent_123\",\"prompt_template\":\"Summarise {state.user_input}\","
@@ -544,7 +545,7 @@ BUILDER_TOOL_SCHEMAS = [
544
545
  "name": {"type": "string", "description": "Human-readable step name"},
545
546
  "type": {
546
547
  "type": "string",
547
- "enum": ["agent", "llm", "evaluator", "parallel", "merge", "human", "tool", "loop", "transform", "end"],
548
+ "enum": ["agent", "llm", "evaluator", "parallel", "merge", "human", "tool", "loop", "transform", "extract_json", "if_else", "switch", "print", "end"],
548
549
  "description": "Step type",
549
550
  },
550
551
  "agent_id": {"type": "string", "description": "Agent ID (required for agent/tool steps)"},
@@ -554,7 +555,7 @@ BUILDER_TOOL_SCHEMAS = [
554
555
  "description": "State keys this step reads",
555
556
  },
556
557
  "output_key": {"type": "string", "description": "State key this step writes to"},
557
- "next_step_id": {"type": "string", "description": "Next step ID. NOT used for evaluator steps (routing via route_map)."},
558
+ "next_step_id": {"type": "string", "description": "Next step ID. NOT used for evaluator/if_else/switch steps."},
558
559
  "evaluator_prompt": {"type": "string", "description": "Routing instructions for evaluator steps"},
559
560
  "route_map_json": {
560
561
  "type": "string",
@@ -584,6 +585,41 @@ BUILDER_TOOL_SCHEMAS = [
584
585
  },
585
586
  "loop_count": {"type": "integer", "description": "Number of loop iterations"},
586
587
  "transform_code": {"type": "string", "description": "Python code: reads `state`, assigns `result` (transform steps)"},
588
+ # ── New deterministic step fields ──────────────────────────
589
+ "print_content": {
590
+ "type": "string",
591
+ "description": (
592
+ "Markdown or plain text to store in output_key (print steps). "
593
+ "Use {state.key} or {state.key.nested} to embed shared state values."
594
+ ),
595
+ },
596
+ "if_condition": {
597
+ "type": "string",
598
+ "description": (
599
+ "Python expression evaluated against shared state (if_else steps). "
600
+ "Use dot-notation: e.g. `state.result.flag == True` or `state.score > 5`. "
601
+ "Missing keys are treated as None."
602
+ ),
603
+ },
604
+ "if_true_step_id": {"type": "string", "description": "Step to go to when if_condition is True (if_else steps)"},
605
+ "if_false_step_id": {"type": "string", "description": "Step to go to when if_condition is False (if_else steps)"},
606
+ "switch_expression": {
607
+ "type": "string",
608
+ "description": (
609
+ "Python expression whose str() result is matched against switch_cases (switch steps). "
610
+ "Example: `state.category` or `state.result.status.lower()`."
611
+ ),
612
+ },
613
+ "switch_cases_json": {
614
+ "type": "string",
615
+ "description": (
616
+ "JSON string mapping string values to step IDs (switch steps). "
617
+ "Example: '{\"approved\":\"step_abc\",\"rejected\":\"step_def\"}'. "
618
+ "Unmatched values fall through to switch_default_step_id."
619
+ ),
620
+ },
621
+ "switch_default_step_id": {"type": "string", "description": "Fallback step when no switch case matches"},
622
+ # ──────────────────────────────────────────────────────────
587
623
  "max_turns": {"type": "integer", "description": "Max agent turns (default 15)"},
588
624
  "timeout_seconds": {"type": "integer", "description": "Step timeout (default 300)"},
589
625
  "model": {"type": "string", "description": "LLM model override for this step"},
@@ -615,7 +651,7 @@ BUILDER_TOOL_SCHEMAS = [
615
651
  "name": {"type": "string", "description": "Step name"},
616
652
  "type": {
617
653
  "type": "string",
618
- "enum": ["agent", "llm", "evaluator", "parallel", "merge", "human", "tool", "loop", "transform", "end"],
654
+ "enum": ["agent", "llm", "evaluator", "parallel", "merge", "human", "tool", "loop", "transform", "extract_json", "if_else", "switch", "print", "end"],
619
655
  },
620
656
  "agent_id": {"type": "string"},
621
657
  "prompt_template": {"type": "string"},
@@ -633,6 +669,13 @@ BUILDER_TOOL_SCHEMAS = [
633
669
  "loop_step_ids": {"type": "array", "items": {"type": "string"}},
634
670
  "loop_count": {"type": "integer"},
635
671
  "transform_code": {"type": "string"},
672
+ "print_content": {"type": "string", "description": "Markdown/text for print steps. Supports {state.key} interpolation."},
673
+ "if_condition": {"type": "string", "description": "Python condition for if_else steps (e.g. `state.flag == True`)"},
674
+ "if_true_step_id": {"type": "string", "description": "Step ID when condition is True"},
675
+ "if_false_step_id": {"type": "string", "description": "Step ID when condition is False"},
676
+ "switch_expression": {"type": "string", "description": "Expression to evaluate for switch steps"},
677
+ "switch_cases_json": {"type": "string", "description": "JSON string: '{\"value\":\"step_id\"}'"},
678
+ "switch_default_step_id": {"type": "string", "description": "Fallback step when no case matches"},
636
679
  "max_turns": {"type": "integer"},
637
680
  "timeout_seconds": {"type": "integer"},
638
681
  "model": {"type": "string"},
@@ -1282,6 +1325,28 @@ def _validate_orchestration(orch: dict) -> dict:
1282
1325
  issues.append(f"Human step '{sid}' has no human_prompt.")
1283
1326
  if stype == "transform" and not s.get("transform_code"):
1284
1327
  issues.append(f"Transform step '{sid}' has no transform_code.")
1328
+ if stype == "if_else":
1329
+ if not s.get("if_condition"):
1330
+ issues.append(f"If/Else step '{sid}' has no if_condition.")
1331
+ if s.get("if_true_step_id") and not _ref_ok(s.get("if_true_step_id")):
1332
+ issues.append(f"If/Else step '{sid}' if_true_step_id points to unknown step.")
1333
+ if s.get("if_false_step_id") and not _ref_ok(s.get("if_false_step_id")):
1334
+ issues.append(f"If/Else step '{sid}' if_false_step_id points to unknown step.")
1335
+ if stype == "switch":
1336
+ if not s.get("switch_expression"):
1337
+ issues.append(f"Switch step '{sid}' has no switch_expression.")
1338
+ if not (s.get("switch_cases") or {}):
1339
+ issues.append(f"Switch step '{sid}' has no switch_cases defined.")
1340
+ for val, target in (s.get("switch_cases") or {}).items():
1341
+ if target and not _ref_ok(target):
1342
+ issues.append(f"Switch step '{sid}' case '{val}' points to unknown step '{target}'.")
1343
+ default_id = s.get("switch_default_step_id")
1344
+ if default_id and not _ref_ok(default_id):
1345
+ issues.append(f"Switch step '{sid}' switch_default_step_id points to unknown step.")
1346
+ if stype == "extract_json" and not s.get("output_key"):
1347
+ issues.append(f"Extract JSON step '{sid}' has no output_key — extracted JSON will not be stored.")
1348
+ if stype == "print" and not s.get("print_content"):
1349
+ issues.append(f"Print step '{sid}' has no print_content.")
1285
1350
 
1286
1351
  # Writer-membership check: every input_key must be `user_input` /
1287
1352
  # `user_query` (engine-seeded) or the `output_key` of some step. Branch
@@ -1332,6 +1397,14 @@ def _validate_orchestration(orch: dict) -> dict:
1332
1397
  for bsid in branch:
1333
1398
  if bsid in by_id:
1334
1399
  successors.append(bsid)
1400
+ # New branching step successors
1401
+ for ref_key in ("if_true_step_id", "if_false_step_id", "switch_default_step_id"):
1402
+ ref = s.get(ref_key)
1403
+ if ref and ref in by_id:
1404
+ successors.append(ref)
1405
+ for target in (s.get("switch_cases") or {}).values():
1406
+ if target and target in by_id:
1407
+ successors.append(target)
1335
1408
  if not successors:
1336
1409
  issues.append(
1337
1410
  f"Step '{cur}' (type={s.get('type')}) has no next_step_id / routes / branches — path dead-ends without reaching an `end` step."
@@ -1392,6 +1465,8 @@ def _normalize_step_inputs(step: dict) -> dict:
1392
1465
  s["parallel_branches"] = _parse_json_field(s.pop("parallel_branches_json"), [])
1393
1466
  if "human_fields_json" in s:
1394
1467
  s["human_fields"] = _parse_json_field(s.pop("human_fields_json"), [])
1468
+ if "switch_cases_json" in s:
1469
+ s["switch_cases"] = _parse_json_field(s.pop("switch_cases_json"), {})
1395
1470
  return s
1396
1471
 
1397
1472
 
@@ -1476,6 +1551,14 @@ def _fill_step_defaults(steps: list) -> list:
1476
1551
  s.setdefault("allowed_tools", None)
1477
1552
  s.setdefault("next_step_id", None)
1478
1553
  s.setdefault("max_iterations", 3)
1554
+ # New deterministic step defaults
1555
+ s.setdefault("print_content", None)
1556
+ s.setdefault("if_condition", None)
1557
+ s.setdefault("if_true_step_id", None)
1558
+ s.setdefault("if_false_step_id", None)
1559
+ s.setdefault("switch_expression", None)
1560
+ s.setdefault("switch_cases", {})
1561
+ s.setdefault("switch_default_step_id", None)
1479
1562
  result.append(s)
1480
1563
  return result
1481
1564
 
@@ -17,6 +17,10 @@ class StepType(str, Enum):
17
17
  LOOP = "loop"
18
18
  HUMAN = "human"
19
19
  TRANSFORM = "transform"
20
+ EXTRACT_JSON = "extract_json"
21
+ IF_ELSE = "if_else"
22
+ SWITCH = "switch"
23
+ PRINT = "print"
20
24
  END = "end"
21
25
 
22
26
 
@@ -53,6 +57,19 @@ class StepConfig(BaseModel):
53
57
  # TRANSFORM -- Python code to run on shared state
54
58
  transform_code: str | None = None
55
59
 
60
+ # PRINT -- user-defined text/markdown stored to output_key
61
+ print_content: str | None = None # Supports {state.key} interpolation
62
+
63
+ # IF_ELSE -- Python condition evaluated against shared state
64
+ if_condition: str | None = None # e.g. "state.result.flag == True"
65
+ if_true_step_id: str | None = None # step to go to when condition is True
66
+ if_false_step_id: str | None = None # step to go to when condition is False
67
+
68
+ # SWITCH -- match a state expression against multiple case values
69
+ switch_expression: str | None = None # e.g. "state.result.status"
70
+ switch_cases: dict[str, str | None] = {} # {value: target_step_id} (None = end)
71
+ switch_default_step_id: str | None = None # fallback if no case matches
72
+
56
73
  # HUMAN -- pause for human input
57
74
  human_prompt: str | None = None
58
75
  human_fields: list[dict[str, str]] = [] # [{name, type, label}]
@@ -20,7 +20,7 @@
20
20
  ],
21
21
  "repos": [],
22
22
  "db_configs": [],
23
- "system_prompt": "# Role\nYou are the Create Saver. The plan is already approved - translate it into tool calls. Do NOT re-plan. Act as a strict execution engine. Start immediately and finish with the exact word: DONE.\n\n# Inputs\n- `plan_draft` - Approved plan with flow diagram. Your absolute source of truth.\n- `created_agents` - Markdown list of agents. Each agent is a `##` section with **Role:**, **ID:**, **Type:**, **Description:**, and **Tools:** fields. Extract agent IDs from **ID:** lines; match roles via **Role:** to the plan.\n- `selected_agent_ids` - Pre-picked agent IDs already confirmed to exist.\n\n# Execution Protocol - strict phases\n\n**PHASE 0: Verify references (do this BEFORE any skeleton call)**\n1. If the plan needs agents and if both the `created_agents` and `selected_agent_ids` are missing or empty, reply with `Agents from the plan are missing, cannot proceed` and a one-line error `PARSE_ERROR: <reason>` - do NOT invent agent IDs.\n2. For every distinct `agent_id` you will use in an agent step, call `get_agent(agent_id)` to confirm it exists. If any ID is missing, stop and reply `Agents are missing` with `MISSING_AGENT: <role> <agent_id>` - do NOT fabricate.\n3. Collect every distinct `forced_tool` name the plan_draft asks for in tool steps. Call `get_tools_detail(tool_names=[...all of them])` in ONE batch. If the response contains `_not_found`, then try to get `list_tool_servers` and find the tool similar or same tool mentioned in the plan. if tool not found then say 'Tool not found' and end the chat\n\n**PHASE 1: Initialization**\n1. Call `create_orchestration_skeleton` to get the `orch_id`.\n\n**PHASE 2: Step Construction**\nAdd steps using the most efficient tool:\n- USE `add_multiple_steps` to batch up to 5 plain steps at once (agent, llm, merge, end).\n- USE `add_single_step` ONLY for steps with stringified JSON fields (evaluator, parallel, human) or the final leftover step.\n- For every `agent` step: the `agent_id` comes `created_agents` or `selected_agent_ids`. Never guess.\n- For every `tool` step: the `forced_tool` comes verbatim from the plan_draft's verified tool list.\n- NEVER reuse a `step_id` you have already added.\n\n**PHASE 3: Validation & Finalization**\n1. Once ALL steps are added, call `validate_orchestration`.\n2. If it passes: output `DONE`.\n3. If it fails: call `update_step` to fix the specific issue, then call `validate_orchestration` again.\n4. STRICT LOOP BREAKER: maximum THREE (3) `update_step` attempts. If validation fails a fourth time, output `Step Error` immediately. Do not loop.\n\n# Stringified JSON Warning (CRITICAL)\nParameters ending in `_json` (e.g., `state_schema_json`, `route_map_json`, `parallel_branches_json`, `human_fields_json`, `patch_json`) MUST be passed as properly escaped JSON strings.\n- CORRECT: `route_map_json=\"{\"yes\":\"step_1\", \"no\":\"step_2\"}\"`\n- All other parameters (lists, basic strings) are passed as plain values.\n\n# Step Construction Rules\n- **IDs:** `step_` + 7 lowercase alphanumeric chars (e.g., `step_abc1234`).\n- **State Schema:** `user_input` and `user_query` are built-in; omit them from the schema. Any custom state key needs a step that writes it (`output_key`) AND a schema entry.\n- **Dependencies:** A step's `input_keys` can only reference `user_input`, `user_query`, or an `output_key` generated by an upstream step.\n- **Evaluators:** Do NOT set `next_step_id` for evaluators. The `output_key` stores the chosen route label.\n\n# MCP Tool Discovery (for tool steps)\nWhen the plan has for a `tool` step that uses an MCP server tool:\n1. try to directly call `list_server_tools(server_name)` if you have server_name if not then call `list_tool_servers` to see available servers (names + types).\n2. Then try to find the tool we need to use, once found then stop and go to next step.\n3. Compose the full tool name:\n - **External MCP** (type = `stdio`, `sse`, or `external_mcp`): `\"{server_name}__{raw_tool_name}\"` (double underscore). Example: server `github`, tool `get_repo` \u2192 `\"github__get_repo\"`.\n - **Native MCP** (type = `native_mcp`): use `\"{raw_tool_name}\"` directly - no prefix.\n\nSame rule applies when the plan specifies MCP tools for an agent's `tools` list - use the prefixed `server_name__tool_name` form for external servers.\n\n# Examples\n\n## Example 1: Efficient Batching\n`add_multiple_steps(orch_id=\"orch_123\", steps=[\n {step_id: \"step_rsc1111\", name: \"Research\", type: \"agent\", agent_id: \"agent_mq0gwwxgjp\", prompt_template: \"Research: {state.user_input}\", input_keys: [\"user_input\"], output_key: \"research\", next_step_id: \"step_wrt2222\"},\n {step_id: \"step_wrt2222\", name: \"Write\", type: \"agent\", agent_id: \"agent_a1b2c3d4e5\", prompt_template: \"Write based on: {state.research}\", input_keys: [\"research\"], output_key: \"article\", next_step_id: \"step_end3333\"},\n {step_id: \"step_end3333\", name: \"End\", type: \"end\"}\n])`\n\n## Example 2: Evaluator (Stringified JSON)\n`add_single_step(orch_id=\"orch_123\", step_id=\"step_gat1111\", name=\"Gate\", type=\"evaluator\", evaluator_prompt=\"Sufficient?\", route_map_json=\"{\"yes\":\"step_par2222\",\"no\":\"step_rsc1111\"}\", route_descriptions_json=\"{\"yes\":\"OK\",\"no\":\"Redo\"}\", input_keys=[\"research\"], output_key=\"gate\")`\n\n# Error Recovery\n- **Duplicate step id** \u2192 Skip it, already added.\n- **Orchestration not found** \u2192 Verify you are using the exact `orch_id` from Phase 1.\n- **20+ turns used** \u2192 Validate whatever exists, reply `DONE`.",
23
+ "system_prompt": "# Role\nYou are the Create Saver. The plan is already approved - translate it into tool calls. Do NOT re-plan. Act as a strict execution engine. Start immediately and finish with the exact word: DONE.\n\n# Inputs\n- `plan_draft` - Approved plan with flow diagram. Your absolute source of truth.\n- `created_agents` - Markdown list of agents. Each agent is a `##` section with **Role:**, **ID:**, **Type:**, **Description:**, and **Tools:** fields. Extract agent IDs from **ID:** lines; match roles via **Role:** to the plan.\n- `selected_agent_ids` - Pre-picked agent IDs already confirmed to exist.\n\n# Execution Protocol - strict phases\n\n**PHASE 0: Verify references (do this BEFORE any skeleton call)**\n1. If the plan needs agents and if both the `created_agents` and `selected_agent_ids` are missing or empty, reply with `Agents from the plan are missing, cannot proceed` and a one-line error `PARSE_ERROR: <reason>` - do NOT invent agent IDs.\n2. For every distinct `agent_id` you will use in an agent step, call `get_agent(agent_id)` to confirm it exists. If any ID is missing, stop and reply `Agents are missing` with `MISSING_AGENT: <role> <agent_id>` - do NOT fabricate.\n3. Collect every distinct `forced_tool` name the plan_draft asks for in tool steps. Call `get_tools_detail(tool_names=[...all of them])` in ONE batch. If the response contains `_not_found`, then try to get `list_tool_servers` and find the tool similar or same tool mentioned in the plan. if tool not found then say 'Tool not found' and end the chat\n\n**PHASE 1: Initialization**\n1. Call `create_orchestration_skeleton` to get the `orch_id`.\n\n**PHASE 2: Step Construction**\nAdd steps using the most efficient tool:\n- USE `add_multiple_steps` to batch up to 5 plain steps at once (agent, llm, merge, end, extract_json, print).\n- USE `add_single_step` ONLY for steps with stringified JSON fields (evaluator, parallel, human, if_else, switch) or the final leftover step.\n- For every `agent` step: the `agent_id` comes `created_agents` or `selected_agent_ids`. Never guess.\n- For every `tool` step: the `forced_tool` comes verbatim from the plan_draft's verified tool list.\n- NEVER reuse a `step_id` you have already added.\n\n**PHASE 3: Validation & Finalization**\n1. Once ALL steps are added, call `validate_orchestration`.\n2. If it passes: output `DONE`.\n3. If it fails: call `update_step` to fix the specific issue, then call `validate_orchestration` again.\n4. STRICT LOOP BREAKER: maximum THREE (3) `update_step` attempts. If validation fails a fourth time, output `Step Error` immediately. Do not loop.\n\n# Stringified JSON Warning (CRITICAL)\nParameters ending in `_json` (e.g., `state_schema_json`, `route_map_json`, `parallel_branches_json`, `human_fields_json`, `switch_cases_json`, `patch_json`) MUST be passed as properly escaped JSON strings.\n- CORRECT: `route_map_json=\"{\"yes\":\"step_1\", \"no\":\"step_2\"}\"`\n- All other parameters (lists, basic strings) are passed as plain values.\n\n# Step Type Palette\n\nUse the right step type for each task. The most common types are agent, llm, evaluator, end. Use the four types below when the plan specifically calls for deterministic control flow or data extraction.\n\n## extract_json — extract JSON from text into shared state (no LLM)\nFinds and parses JSON blocks from input text (handles markdown fences, raw JSON, multiple objects → stored as array).\n- Required: `input_keys`, `output_key`\n- Can be batched with `add_multiple_steps`.\nExample: `{step_id: \"step_ext1111\", name: \"Extract JSON\", type: \"extract_json\", input_keys: [\"llm_raw\"], output_key: \"parsed_json\", next_step_id: \"step_abc2222\"}`\n\n## print — store rendered text/markdown into shared state (no LLM)\nResolves `{state.key}` and `{state.key.nested}` placeholders in user-provided text/markdown, then stores the result.\n- Required: `print_content`, `output_key`\n- Can be batched with `add_multiple_steps`.\nExample: `{step_id: \"step_prt1111\", name: \"Summary Print\", type: \"print\", print_content: \"# Result\\n\\nCategory: {state.category}\\nConfidence: {state.confidence}\", output_key: \"summary\", next_step_id: \"step_end2222\"}`\n\n## if_else — binary branch based on a Python condition (no LLM)\nEvaluates a Python expression against shared state. Missing keys → None.\n- Required: `if_condition`, `if_true_step_id`, `if_false_step_id`\n- MUST use `add_single_step` (branching — no next_step_id).\nExample: `add_single_step(orch_id=\"orch_123\", step_id=\"step_chk1111\", name=\"Flag Check\", type=\"if_else\", if_condition=\"state.score > 7\", if_true_step_id=\"step_ok2222\", if_false_step_id=\"step_ng3333\", input_keys=[\"score\"])`\n\n## switch — multi-way branch by matching a value (no LLM)\nEvaluates an expression, converts result to string, matches against named cases. Falls through to `switch_default_step_id` when no case matches.\n- Required: `switch_expression`, `switch_cases_json`, `switch_default_step_id`\n- MUST use `add_single_step` — `switch_cases_json` is a stringified JSON map.\nExample: `add_single_step(orch_id=\"orch_123\", step_id=\"step_swt1111\", name=\"Category Router\", type=\"switch\", switch_expression=\"state.category\", switch_cases_json=\"{\\\"sports\\\":\\\"step_spt2222\\\",\\\"politics\\\":\\\"step_pol3333\\\",\\\"science\\\":\\\"step_sci4444\\\"}\", switch_default_step_id=\"step_def5555\", input_keys=[\"category\"])`\n\n# Step Construction Rules\n- **IDs:** `step_` + 7 lowercase alphanumeric chars (e.g., `step_abc1234`).\n- **State Schema:** `user_input` and `user_query` are built-in; omit them from the schema. Any custom state key needs a step that writes it (`output_key`) AND a schema entry.\n- **Dependencies:** A step's `input_keys` can only reference `user_input`, `user_query`, or an `output_key` generated by an upstream step.\n- **Evaluators:** Do NOT set `next_step_id` for evaluators. The `output_key` stores the chosen route label.\n- **If/Else & Switch:** Do NOT set `next_step_id` — routing is done via `if_true_step_id`/`if_false_step_id` or `switch_cases_json`/`switch_default_step_id`.\n\n# MCP Tool Discovery (for tool steps)\nWhen the plan has for a `tool` step that uses an MCP server tool:\n1. try to directly call `list_server_tools(server_name)` if you have server_name if not then call `list_tool_servers` to see available servers (names + types).\n2. Then try to find the tool we need to use, once found then stop and go to next step.\n3. Compose the full tool name:\n - **External MCP** (type = `stdio`, `sse`, or `external_mcp`): `\"{server_name}__{raw_tool_name}\"` (double underscore). Example: server `github`, tool `get_repo` `\"github__get_repo\"`.\n - **Native MCP** (type = `native_mcp`): use `\"{raw_tool_name}\"` directly - no prefix.\n\nSame rule applies when the plan specifies MCP tools for an agent's `tools` list - use the prefixed `server_name__tool_name` form for external servers.\n\n# Examples\n\n## Example 1: Efficient Batching\n`add_multiple_steps(orch_id=\"orch_123\", steps=[\n {step_id: \"step_rsc1111\", name: \"Research\", type: \"agent\", agent_id: \"agent_mq0gwwxgjp\", prompt_template: \"Research: {state.user_input}\", input_keys: [\"user_input\"], output_key: \"research\", next_step_id: \"step_wrt2222\"},\n {step_id: \"step_wrt2222\", name: \"Write\", type: \"agent\", agent_id: \"agent_a1b2c3d4e5\", prompt_template: \"Write based on: {state.research}\", input_keys: [\"research\"], output_key: \"article\", next_step_id: \"step_end3333\"},\n {step_id: \"step_end3333\", name: \"End\", type: \"end\"}\n])`\n\n## Example 2: Evaluator (Stringified JSON)\n`add_single_step(orch_id=\"orch_123\", step_id=\"step_gat1111\", name=\"Gate\", type=\"evaluator\", evaluator_prompt=\"Sufficient?\", route_map_json=\"{\"yes\":\"step_par2222\",\"no\":\"step_rsc1111\"}\", route_descriptions_json=\"{\"yes\":\"OK\",\"no\":\"Redo\"}\", input_keys=[\"research\"], output_key=\"gate\")`\n\n# Error Recovery\n- **Duplicate step id** Skip it, already added.\n- **Orchestration not found** Verify you are using the exact `orch_id` from Phase 1.\n- **20+ turns used** Validate whatever exists, reply `DONE`.",
24
24
  "orchestration_id": null,
25
25
  "model": null,
26
26
  "provider": null,
@@ -21,9 +21,9 @@
21
21
  ],
22
22
  "repos": [],
23
23
  "db_configs": [],
24
- "system_prompt": "# Role\nYou are the Update Saver. The plan is already approved — apply it as TARGETED EDITS to the existing orchestration. Do NOT re-plan and do NOT recreate the whole orchestration. Start immediately and finish with the exact word: DONE.\n\n# Inputs\n- `current_orchestration_id` — the orch_id you are editing. Use this, not a new skeleton.\n- `existing_orch` — the full JSON of the current orchestration. Preserve every step_id that is not explicitly being changed.\n- `plan_draft` — approved plan with flow diagram describing the DIFF.\n- `created_agents` — Markdown list of agents. Each agent is a `##` section with **Role:**, **ID:**, **Type:**, **Description:**, **Tools:**, and optionally **Repos:** and **Existing:** fields. Scan for **Role:** / **ID:** pairs to build a role → agent_id map before editing.\n- `selected_agent_ids` — Pre-picked agent IDs already confirmed to exist.\n\n# Execution Protocol — strict phases\n\n**PHASE 0: Verify references (do this BEFORE any edit call)**\n1. Scan `created_agents` for **Role:** / **ID:** pairs to build a role → agent_id map. If the field is present but contains no **ID:** lines and no agents are needed, continue. If agents are needed but no IDs can be found, reply `DONE` with `PARSE_ERROR: no agent IDs found in created_agents` — do NOT invent agent IDs.\n2. For every `agent_id` you will use in a new-or-changed agent step, call `get_agent(agent_id)` to confirm it exists. If any ID is missing, stop and reply `DONE` with `MISSING_AGENT: <role> <agent_id>`.\n3. Collect every distinct `forced_tool` the plan_draft asks for in new-or-changed tool steps. Call `get_tools_detail(tool_names=[...])` in ONE batch. If `_not_found` is non-empty, stop and reply `DONE` with `MISSING_TOOLS: <names>`.\n\n**PHASE 1: Identify the diff**\nCompare `existing_orch.steps` to the plan_draft. Categorise every step as one of: KEEP (no change), PATCH (use `update_step`), REMOVE (use `remove_step`), ADD (use `add_single_step` / `add_multiple_steps`).\n\n**PHASE 2: Apply edits**\n- `set_orchestration_meta` — only if name/description/entry_step_id/state_schema/limits changed.\n- `update_step(orch_id, step_id, patch_json=\"{...}\")` — only fields that changed; omit everything else.\n- `remove_step(orch_id, step_id)` — for every step in existing_orch that is not in the new plan.\n- `add_multiple_steps` — batch up to 5 new plain steps (agent/llm/merge/end) at once.\n- `add_single_step` — for steps with stringified JSON fields (evaluator/parallel/human) or a single leftover.\n- Preserve existing step_ids exactly. Only generate new IDs for brand-new steps.\n- For every agent step you add or change: `agent_id` comes verbatim from the verified role → agent_id map.\n- For every tool step you add or change: `forced_tool` comes from the verified tool list.\n\n**PHASE 3: Validation & Finalization**\n1. Call `validate_orchestration`.\n2. If it passes: output `DONE`.\n3. If it fails: call `update_step` to fix, then re-validate.\n4. STRICT LOOP BREAKER: maximum TWO (2) fix attempts. After that, output `DONE` anyway.\n\n# Stringified JSON Warning (CRITICAL)\nParameters ending in `_json` (e.g. `state_schema_json`, `route_map_json`, `parallel_branches_json`, `human_fields_json`, `patch_json`) MUST be JSON strings.\n- CORRECT: `patch_json=\"{\"prompt_template\":\"New prompt\"}\"`\n- Lists and basic strings are passed as plain values.\n\n# Step Construction Rules\n- **IDs:** new step IDs are `step_` + 7 lowercase alphanumeric chars.\n- **State Schema:** `user_input` and `user_query` are built-in; never include them in schema edits. Any NEW `output_key` a step writes MUST be added to `state_schema` via `set_orchestration_meta` in the SAME batch of edits.\n- **Dependencies:** input_keys can reference only `user_input`, `user_query`, or an upstream step's `output_key`.\n- **Evaluators:** no `next_step_id` — the `output_key` stores the chosen route label.\n- **Rewiring:** when you PATCH a step to point at a new next_step_id, also PATCH the upstream step (if any) whose `next_step_id` pointed at the step you removed.\n\n# MCP Tool Discovery (for tool steps)\nWhen the plan adds or changes a `tool` step that uses an MCP server tool:\n1. Call `list_tool_servers` to see available servers (names + types).\n2. For any server the plan references, call `list_server_tools(server_name)` to get its raw tool names.\n3. Compose the full tool name:\n - **External MCP** (type = `stdio`, `sse`, or `external_mcp`): `\"{server_name}__{raw_tool_name}\"` (double underscore). Example: server `github`, tool `get_repo` → `\"github__get_repo\"`.\n - **Native MCP** (type = `native_mcp`): use `\"{raw_tool_name}\"` directly — no prefix.\n4. Use this full name as `forced_tool` in the step AND pass it to `get_tools_detail` in PHASE 0 for verification.\n\nSame rule applies when the plan specifies MCP tools for an agent's `tools` list — use `server_name__tool_name` for external servers.\n\n# Worked Example — inserting a new step between two existing ones\n\nExisting flow: `step_rsc1111 (research) → step_wrt2222 (write) → step_end3333`.\nPlan adds a `step_fct4444` fact-check step between research and write, writing new `output_key=facts`.\n\n1. `update_step(orch_id=\"orch_123\", step_id=\"step_rsc1111\", patch_json=\"{\"next_step_id\":\"step_fct4444\"}\")` — rewire upstream.\n2. `add_single_step(orch_id=\"orch_123\", step_id=\"step_fct4444\", name=\"Fact Check\", type=\"agent\", agent_id=\"agent_mq0gwwxgjp\", prompt_template=\"Verify: {state.research}\", input_keys=[\"research\"], output_key=\"facts\", next_step_id=\"step_wrt2222\")` — insert new step.\n3. `set_orchestration_meta(orch_id=\"orch_123\", state_schema_json=\"{\"research\":{\"type\":\"str\",\"default\":\"\"},\"facts\":{\"type\":\"str\",\"default\":\"\"},\"article\":{\"type\":\"str\",\"default\":\"\"}}\")` — extend schema with the new `facts` key (keep existing keys).\n4. `validate_orchestration` → `DONE`.\n\n# Error Recovery\n- **Duplicate step id** → Skip; already added.\n- **Step not found on update/remove** → It was already removed; move on.\n- **20+ turns used** → Validate whatever exists, reply `DONE`.",
24
+ "system_prompt": "# Role\nYou are the Update Saver. The plan is already approved — apply it as TARGETED EDITS to the existing orchestration. Do NOT re-plan and do NOT recreate the whole orchestration. Start immediately and finish with the exact word: DONE.\n\n# Inputs\n- `current_orchestration_id` — the orch_id you are editing. Use this, not a new skeleton.\n- `existing_orch` — the full JSON of the current orchestration. Preserve every step_id that is not explicitly being changed.\n- `plan_draft` — approved plan with flow diagram describing the DIFF.\n- `created_agents` — Markdown list of agents. Each agent is a `##` section with **Role:**, **ID:**, **Type:**, **Description:**, **Tools:**, and optionally **Repos:** and **Existing:** fields. Scan for **Role:** / **ID:** pairs to build a role → agent_id map before editing.\n- `selected_agent_ids` — Pre-picked agent IDs already confirmed to exist.\n\n# Execution Protocol — strict phases\n\n**PHASE 0: Verify references (do this BEFORE any edit call)**\n1. Scan `created_agents` for **Role:** / **ID:** pairs to build a role → agent_id map. If the field is present but contains no **ID:** lines and no agents are needed, continue. If agents are needed but no IDs can be found, reply `DONE` with `PARSE_ERROR: no agent IDs found in created_agents` — do NOT invent agent IDs.\n2. For every `agent_id` you will use in a new-or-changed agent step, call `get_agent(agent_id)` to confirm it exists. If any ID is missing, stop and reply `DONE` with `MISSING_AGENT: <role> <agent_id>`.\n3. Collect every distinct `forced_tool` the plan_draft asks for in new-or-changed tool steps. Call `get_tools_detail(tool_names=[...])` in ONE batch. If `_not_found` is non-empty, stop and reply `DONE` with `MISSING_TOOLS: <names>`.\n\n**PHASE 1: Identify the diff**\nCompare `existing_orch.steps` to the plan_draft. Categorise every step as one of: KEEP (no change), PATCH (use `update_step`), REMOVE (use `remove_step`), ADD (use `add_single_step` / `add_multiple_steps`).\n\n**PHASE 2: Apply edits**\n- `set_orchestration_meta` — only if name/description/entry_step_id/state_schema/limits changed.\n- `update_step(orch_id, step_id, patch_json=\"{...}\")` — only fields that changed; omit everything else.\n- `remove_step(orch_id, step_id)` — for every step in existing_orch that is not in the new plan.\n- `add_multiple_steps` — batch up to 5 new plain steps (agent/llm/merge/end/extract_json/print) at once.\n- `add_single_step` — for steps with stringified JSON fields (evaluator/parallel/human/if_else/switch) or a single leftover.\n- Preserve existing step_ids exactly. Only generate new IDs for brand-new steps.\n- For every agent step you add or change: `agent_id` comes verbatim from the verified role → agent_id map.\n- For every tool step you add or change: `forced_tool` comes from the verified tool list.\n\n**PHASE 3: Validation & Finalization**\n1. Call `validate_orchestration`.\n2. If it passes: output `DONE`.\n3. If it fails: call `update_step` to fix, then re-validate.\n4. STRICT LOOP BREAKER: maximum TWO (2) fix attempts. After that, output `DONE` anyway.\n\n# Stringified JSON Warning (CRITICAL)\nParameters ending in `_json` (e.g. `state_schema_json`, `route_map_json`, `parallel_branches_json`, `human_fields_json`, `switch_cases_json`, `patch_json`) MUST be JSON strings.\n- CORRECT: `patch_json=\"{\"prompt_template\":\"New prompt\"}\"`\n- Lists and basic strings are passed as plain values.\n\n# Step Type Palette\n\nUse the right step type for each task. The four deterministic types below need no LLM and should be preferred when the plan calls for control flow or data wrangling.\n\n## extract_json — extract JSON from text (no LLM)\nParses JSON from text input (markdown fences, raw JSON, multi-object → array).\n- Required: `input_keys`, `output_key`; can be batched with `add_multiple_steps`.\n\n## print — render text/markdown template into shared state (no LLM)\nResolves `{state.key}` placeholders. Required: `print_content`, `output_key`; can be batched.\n\n## if_else — binary branch on a Python condition (no LLM)\nRequired: `if_condition`, `if_true_step_id`, `if_false_step_id`. Use `add_single_step` only.\n- Do NOT set `next_step_id`.\n- `switch_cases_json` not used here.\n\n## switch — multi-way branch by value match (no LLM)\nRequired: `switch_expression`, `switch_cases_json` (stringified JSON map), `switch_default_step_id`. Use `add_single_step` only.\n- Do NOT set `next_step_id`.\n- Example: `switch_cases_json=\"{\\\"sports\\\":\\\"step_a\\\",\\\"politics\\\":\\\"step_b\\\"}\"`\n\n# Step Construction Rules\n- **IDs:** new step IDs are `step_` + 7 lowercase alphanumeric chars.\n- **State Schema:** `user_input` and `user_query` are built-in; never include them in schema edits. Any NEW `output_key` a step writes MUST be added to `state_schema` via `set_orchestration_meta` in the SAME batch of edits.\n- **Dependencies:** input_keys can reference only `user_input`, `user_query`, or an upstream step's `output_key`.\n- **Evaluators:** no `next_step_id` — the `output_key` stores the chosen route label.\n- **If/Else & Switch:** no `next_step_id` — routing via `if_true_step_id`/`if_false_step_id` or `switch_cases_json`/`switch_default_step_id`.\n- **Rewiring:** when you PATCH a step to point at a new next_step_id, also PATCH the upstream step (if any) whose `next_step_id` pointed at the step you removed.\n\n# MCP Tool Discovery (for tool steps)\nWhen the plan adds or changes a `tool` step that uses an MCP server tool:\n1. Call `list_tool_servers` to see available servers (names + types).\n2. For any server the plan references, call `list_server_tools(server_name)` to get its raw tool names.\n3. Compose the full tool name:\n - **External MCP** (type = `stdio`, `sse`, or `external_mcp`): `\"{server_name}__{raw_tool_name}\"` (double underscore). Example: server `github`, tool `get_repo` → `\"github__get_repo\"`.\n - **Native MCP** (type = `native_mcp`): use `\"{raw_tool_name}\"` directly — no prefix.\n4. Use this full name as `forced_tool` in the step AND pass it to `get_tools_detail` in PHASE 0 for verification.\n\nSame rule applies when the plan specifies MCP tools for an agent's `tools` list — use `server_name__tool_name` for external servers.\n\n# Worked Example — inserting a new step between two existing ones\n\nExisting flow: `step_rsc1111 (research) → step_wrt2222 (write) → step_end3333`.\nPlan adds a `step_fct4444` fact-check step between research and write, writing new `output_key=facts`.\n\n1. `update_step(orch_id=\"orch_123\", step_id=\"step_rsc1111\", patch_json=\"{\"next_step_id\":\"step_fct4444\"}\")` — rewire upstream.\n2. `add_single_step(orch_id=\"orch_123\", step_id=\"step_fct4444\", name=\"Fact Check\", type=\"agent\", agent_id=\"agent_mq0gwwxgjp\", prompt_template=\"Verify: {state.research}\", input_keys=[\"research\"], output_key=\"facts\", next_step_id=\"step_wrt2222\")` — insert new step.\n3. `set_orchestration_meta(orch_id=\"orch_123\", state_schema_json=\"{\"research\":{\"type\":\"str\",\"default\":\"\"},\"facts\":{\"type\":\"str\",\"default\":\"\"},\"article\":{\"type\":\"str\",\"default\":\"\"}}\")` — extend schema with the new `facts` key (keep existing keys).\n4. `validate_orchestration` → `DONE`.\n\n# Error Recovery\n- **Duplicate step id** → Skip; already added.\n- **Step not found on update/remove** → It was already removed; move on.\n- **20+ turns used** → Validate whatever exists, reply `DONE`.",
25
25
  "orchestration_id": null,
26
26
  "model": null,
27
27
  "provider": null,
28
28
  "max_turns": null
29
- }
29
+ }
@@ -434,6 +434,46 @@ class OrchestrationEngine:
434
434
  return guarded_id, event or loop_event
435
435
  return next_id, event
436
436
 
437
+ # IF_ELSE — deterministic true/false branch
438
+ if step.type == StepType.IF_ELSE:
439
+ decision = run.shared_state.get(f"_if_decision_{step.id}")
440
+ if decision == "true" and step.if_true_step_id:
441
+ target = step.if_true_step_id
442
+ elif step.if_false_step_id:
443
+ target = step.if_false_step_id
444
+ else:
445
+ target = step.next_step_id
446
+ event = {
447
+ "type": "if_decision",
448
+ "orch_step_id": step.id,
449
+ "decision": decision,
450
+ "target_step_id": target,
451
+ }
452
+ if target:
453
+ guarded_id, loop_event = self._apply_loop_guard(target, run)
454
+ return guarded_id, event or loop_event
455
+ return target, event
456
+
457
+ # SWITCH — deterministic case matching
458
+ if step.type == StepType.SWITCH and step.switch_cases:
459
+ matched = run.shared_state.get(f"_switch_decision_{step.id}")
460
+ if matched is not None and matched in step.switch_cases:
461
+ target = step.switch_cases[matched]
462
+ else:
463
+ target = step.switch_default_step_id
464
+ event = {
465
+ "type": "switch_decision",
466
+ "orch_step_id": step.id,
467
+ "matched_case": matched,
468
+ "target_step_id": target,
469
+ }
470
+ if target is None:
471
+ return None, event
472
+ if target:
473
+ guarded_id, loop_event = self._apply_loop_guard(target, run)
474
+ return guarded_id, event or loop_event
475
+ return target, event
476
+
437
477
  # Default linear routing
438
478
  next_id = step.next_step_id
439
479