algo-cli-runtime 0.14.0__py3-none-any.whl

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 (237) hide show
  1. algo_cli/__init__.py +3 -0
  2. algo_cli/__main__.py +7 -0
  3. algo_cli/_internal/__init__.py +12 -0
  4. algo_cli/_internal/policy_chain.py +259 -0
  5. algo_cli/action_registry.py +1047 -0
  6. algo_cli/agent_blocks.py +550 -0
  7. algo_cli/agent_pipeline.py +1457 -0
  8. algo_cli/agent_threads.py +308 -0
  9. algo_cli/animations.py +316 -0
  10. algo_cli/cache_admission.py +209 -0
  11. algo_cli/capability_mask.py +66 -0
  12. algo_cli/chat_protocol.py +116 -0
  13. algo_cli/chatgpt_auth.py +510 -0
  14. algo_cli/chatgpt_client.py +657 -0
  15. algo_cli/code_rag.py +479 -0
  16. algo_cli/config.py +651 -0
  17. algo_cli/context_budget.py +679 -0
  18. algo_cli/credential_helpers.py +315 -0
  19. algo_cli/deliberation.py +29 -0
  20. algo_cli/display.py +1470 -0
  21. algo_cli/evals/__init__.py +21 -0
  22. algo_cli/evals/algorithm_effectiveness.py +560 -0
  23. algo_cli/evals/competitive_harness_rating.py +702 -0
  24. algo_cli/evals/cot_quality.py +220 -0
  25. algo_cli/evals/harness_retrieval_benchmark.py +401 -0
  26. algo_cli/evals/performance_regression.py +136 -0
  27. algo_cli/evals/scorecard_grading.py +308 -0
  28. algo_cli/evals/session_distribution.py +84 -0
  29. algo_cli/execution_guardrails.py +806 -0
  30. algo_cli/extensions_manifest.py +84 -0
  31. algo_cli/git_evidence.py +227 -0
  32. algo_cli/google_workspace.py +407 -0
  33. algo_cli/google_workspace_auth.py +523 -0
  34. algo_cli/harness.py +2587 -0
  35. algo_cli/identity.py +557 -0
  36. algo_cli/index_compute_lab.py +228 -0
  37. algo_cli/inference_harness.py +70 -0
  38. algo_cli/intelligence/__init__.py +1103 -0
  39. algo_cli/intelligence/acrobat_config.py +307 -0
  40. algo_cli/intelligence/acrobat_manifests.py +338 -0
  41. algo_cli/intelligence/acrobat_models.py +195 -0
  42. algo_cli/intelligence/acrobat_pipeline.py +295 -0
  43. algo_cli/intelligence/acrobat_runtime.py +302 -0
  44. algo_cli/intelligence/acrobat_security.py +261 -0
  45. algo_cli/intelligence/acrobat_workflows.py +226 -0
  46. algo_cli/intelligence/actionability.py +165 -0
  47. algo_cli/intelligence/adversarial_audit.py +136 -0
  48. algo_cli/intelligence/agent_arena.py +92 -0
  49. algo_cli/intelligence/agent_benchmark.py +236 -0
  50. algo_cli/intelligence/agent_runtime.py +171 -0
  51. algo_cli/intelligence/agents_as_tools.py +70 -0
  52. algo_cli/intelligence/artifact_binding.py +80 -0
  53. algo_cli/intelligence/autonomous_engineer.py +1976 -0
  54. algo_cli/intelligence/backpressure.py +99 -0
  55. algo_cli/intelligence/bloom_filter.py +186 -0
  56. algo_cli/intelligence/bonferroni.py +66 -0
  57. algo_cli/intelligence/boundary_compaction.py +98 -0
  58. algo_cli/intelligence/catalog_verifier.py +172 -0
  59. algo_cli/intelligence/cavecrew.py +118 -0
  60. algo_cli/intelligence/changelog.py +176 -0
  61. algo_cli/intelligence/checkpoint_resume.py +92 -0
  62. algo_cli/intelligence/circuit_breaker.py +88 -0
  63. algo_cli/intelligence/clarification_gate.py +101 -0
  64. algo_cli/intelligence/code_graph.py +180 -0
  65. algo_cli/intelligence/coderank.py +97 -0
  66. algo_cli/intelligence/consistent_hash.py +150 -0
  67. algo_cli/intelligence/consortium_synthesis.py +139 -0
  68. algo_cli/intelligence/construction/__init__.py +241 -0
  69. algo_cli/intelligence/construction/common.py +273 -0
  70. algo_cli/intelligence/construction/documents.py +496 -0
  71. algo_cli/intelligence/construction/labor_units.py +1395 -0
  72. algo_cli/intelligence/construction/payments.py +470 -0
  73. algo_cli/intelligence/construction/risk.py +784 -0
  74. algo_cli/intelligence/content_extractor.py +132 -0
  75. algo_cli/intelligence/context_adaptive.py +102 -0
  76. algo_cli/intelligence/context_ops.py +95 -0
  77. algo_cli/intelligence/count_min.py +145 -0
  78. algo_cli/intelligence/cow_state.py +103 -0
  79. algo_cli/intelligence/critic_loop.py +119 -0
  80. algo_cli/intelligence/cross_source.py +113 -0
  81. algo_cli/intelligence/daemon_mode.py +99 -0
  82. algo_cli/intelligence/dag_orchestration.py +151 -0
  83. algo_cli/intelligence/deep_research.py +155 -0
  84. algo_cli/intelligence/degenerate_detector.py +78 -0
  85. algo_cli/intelligence/delta_report.py +92 -0
  86. algo_cli/intelligence/discovery_event_log.py +92 -0
  87. algo_cli/intelligence/document_ingest.py +298 -0
  88. algo_cli/intelligence/dual_layer_validate.py +151 -0
  89. algo_cli/intelligence/echo_fidelity.py +73 -0
  90. algo_cli/intelligence/ema_tuning.py +104 -0
  91. algo_cli/intelligence/event_log.py +92 -0
  92. algo_cli/intelligence/evidence_graph.py +114 -0
  93. algo_cli/intelligence/extension_host.py +162 -0
  94. algo_cli/intelligence/extension_manifest.py +115 -0
  95. algo_cli/intelligence/falsification_suite.py +178 -0
  96. algo_cli/intelligence/finance/__init__.py +169 -0
  97. algo_cli/intelligence/finance/anomalies.py +135 -0
  98. algo_cli/intelligence/finance/ap_ar.py +351 -0
  99. algo_cli/intelligence/finance/cash.py +162 -0
  100. algo_cli/intelligence/finance/close.py +332 -0
  101. algo_cli/intelligence/finance/common.py +244 -0
  102. algo_cli/intelligence/finance/construction.py +135 -0
  103. algo_cli/intelligence/finance/controls.py +172 -0
  104. algo_cli/intelligence/finance/evidence.py +119 -0
  105. algo_cli/intelligence/finance/exceptions.py +157 -0
  106. algo_cli/intelligence/finance/reconciliations.py +254 -0
  107. algo_cli/intelligence/finance/revenue.py +109 -0
  108. algo_cli/intelligence/finance/tax.py +74 -0
  109. algo_cli/intelligence/finance/workpapers.py +111 -0
  110. algo_cli/intelligence/finding_record.py +120 -0
  111. algo_cli/intelligence/flow_dag.py +267 -0
  112. algo_cli/intelligence/gatherer.py +223 -0
  113. algo_cli/intelligence/golden_master.py +98 -0
  114. algo_cli/intelligence/graph_rag.py +195 -0
  115. algo_cli/intelligence/group_chat.py +143 -0
  116. algo_cli/intelligence/hash_dedup.py +145 -0
  117. algo_cli/intelligence/hyperloglog.py +128 -0
  118. algo_cli/intelligence/incremental_index.py +316 -0
  119. algo_cli/intelligence/index_store.py +16 -0
  120. algo_cli/intelligence/iteration_plan.py +133 -0
  121. algo_cli/intelligence/kernel_plugins.py +167 -0
  122. algo_cli/intelligence/lesson_catalog.py +135 -0
  123. algo_cli/intelligence/llm_fallback.py +169 -0
  124. algo_cli/intelligence/log2_histogram.py +267 -0
  125. algo_cli/intelligence/lsp_integration.py +147 -0
  126. algo_cli/intelligence/memory_evolution.py +117 -0
  127. algo_cli/intelligence/minhash_lsh.py +182 -0
  128. algo_cli/intelligence/multi_model_score.py +174 -0
  129. algo_cli/intelligence/multi_tier_grade.py +211 -0
  130. algo_cli/intelligence/negative_controls.py +113 -0
  131. algo_cli/intelligence/numeric_clamp.py +63 -0
  132. algo_cli/intelligence/occ_editor.py +66 -0
  133. algo_cli/intelligence/output_normalize.py +112 -0
  134. algo_cli/intelligence/parallel_delegation.py +98 -0
  135. algo_cli/intelligence/parallel_fanout.py +104 -0
  136. algo_cli/intelligence/permission_modes.py +105 -0
  137. algo_cli/intelligence/pre_push_gate.py +68 -0
  138. algo_cli/intelligence/prefetch.py +171 -0
  139. algo_cli/intelligence/process_framework.py +217 -0
  140. algo_cli/intelligence/project_graph.py +387 -0
  141. algo_cli/intelligence/query_expansion.py +146 -0
  142. algo_cli/intelligence/ralph_loop.py +117 -0
  143. algo_cli/intelligence/rate_limiter.py +153 -0
  144. algo_cli/intelligence/refactor_transaction.py +94 -0
  145. algo_cli/intelligence/research_workspace.py +108 -0
  146. algo_cli/intelligence/retraction_ledger.py +72 -0
  147. algo_cli/intelligence/saga_pattern.py +88 -0
  148. algo_cli/intelligence/session_fork.py +100 -0
  149. algo_cli/intelligence/shadow_editor.py +67 -0
  150. algo_cli/intelligence/shell_session.py +213 -0
  151. algo_cli/intelligence/source_registry.py +143 -0
  152. algo_cli/intelligence/spawn_scales.py +99 -0
  153. algo_cli/intelligence/stat_stability.py +104 -0
  154. algo_cli/intelligence/structural_validator.py +148 -0
  155. algo_cli/intelligence/subagent_spawner.py +111 -0
  156. algo_cli/intelligence/symmetric_verify.py +70 -0
  157. algo_cli/intelligence/task_classifier.py +129 -0
  158. algo_cli/intelligence/team_execution.py +122 -0
  159. algo_cli/intelligence/tiered_access.py +121 -0
  160. algo_cli/intelligence/utility_registry.py +159 -0
  161. algo_cli/intuition_engine.py +560 -0
  162. algo_cli/intuition_injector.py +82 -0
  163. algo_cli/kernels/__init__.py +5 -0
  164. algo_cli/kernels/manifest.py +763 -0
  165. algo_cli/main.py +3903 -0
  166. algo_cli/memory_candidates.py +541 -0
  167. algo_cli/memory_echo_veil.py +394 -0
  168. algo_cli/memory_runtime.py +112 -0
  169. algo_cli/model_info.py +548 -0
  170. algo_cli/model_profile.py +160 -0
  171. algo_cli/model_routing.py +74 -0
  172. algo_cli/oneshot.py +331 -0
  173. algo_cli/perf_telemetry.py +389 -0
  174. algo_cli/plugins.py +245 -0
  175. algo_cli/private_event_store.py +654 -0
  176. algo_cli/quantization/__init__.py +24 -0
  177. algo_cli/quantization/lloyd_max.py +98 -0
  178. algo_cli/quantization/turbo_quant.py +308 -0
  179. algo_cli/reasoning/__init__.py +46 -0
  180. algo_cli/reasoning/combinatorial.py +356 -0
  181. algo_cli/reasoning/graph_of_thought.py +297 -0
  182. algo_cli/reasoning/mcts.py +220 -0
  183. algo_cli/reasoning/neuro_symbolic.py +250 -0
  184. algo_cli/reasoning/react.py +246 -0
  185. algo_cli/reasoning/reflexion.py +225 -0
  186. algo_cli/reasoning/tree_of_thought.py +241 -0
  187. algo_cli/reasoning_bridge.py +150 -0
  188. algo_cli/reconciliation.py +284 -0
  189. algo_cli/reflex.py +385 -0
  190. algo_cli/resources/docs/ALGO.md +13958 -0
  191. algo_cli/resources/docs/algo-cli-algorithm-evidence-contract.md +60 -0
  192. algo_cli/resources/docs/algo-cli-execution-verification-contract.md +59 -0
  193. algo_cli/resources/docs/algo-cli-memory-lifecycle-contract.md +72 -0
  194. algo_cli/resources/docs/harness-extension-cleanup-recommendation.md +41 -0
  195. algo_cli/resources/docs/index-compute-lab-integration.md +32 -0
  196. algo_cli/resources/docs/inference-harness-loop-blueprint-2026-06.md +55 -0
  197. algo_cli/resources/docs/main-split-map.md +35 -0
  198. algo_cli/resources/docs/privacy-and-context.md +48 -0
  199. algo_cli/resources/docs/reflex-loop-v0.2.md +354 -0
  200. algo_cli/resources/skills/README.md +26 -0
  201. algo_cli/resources/skills/algo-cli.md +59 -0
  202. algo_cli/resources/skills/edit-file-precision.md +49 -0
  203. algo_cli/resources/skills/harness-search-first.md +47 -0
  204. algo_cli/resources/skills/memory-recall-ritual.md +51 -0
  205. algo_cli/resources/skills/qol-algorithms.md +224 -0
  206. algo_cli/resources/skills/smart-error-recovery.md +56 -0
  207. algo_cli/resources/skills/tool-selection-cheatsheet.md +65 -0
  208. algo_cli/retrieval_algorithms.py +127 -0
  209. algo_cli/runtime_qos.py +236 -0
  210. algo_cli/runtime_services.py +320 -0
  211. algo_cli/session_commands.py +95 -0
  212. algo_cli/session_mode.py +113 -0
  213. algo_cli/skills.py +430 -0
  214. algo_cli/slash_dispatch.py +1265 -0
  215. algo_cli/small_context.py +206 -0
  216. algo_cli/spawn_budget.py +89 -0
  217. algo_cli/task_ledger.py +84 -0
  218. algo_cli/task_router.py +197 -0
  219. algo_cli/tool_context.py +94 -0
  220. algo_cli/tool_contract.py +99 -0
  221. algo_cli/tool_policy.py +357 -0
  222. algo_cli/tool_runtime.py +647 -0
  223. algo_cli/tools.py +3056 -0
  224. algo_cli/url_scheme.py +174 -0
  225. algo_cli/verify.py +154 -0
  226. algo_cli/version_manifest.py +178 -0
  227. algo_cli/vision_screenshot_verify.py +76 -0
  228. algo_cli/workspace_resolver.py +68 -0
  229. algo_cli/x_account.py +209 -0
  230. algo_cli/xai_auth.py +374 -0
  231. algo_cli/xai_client.py +600 -0
  232. algo_cli_runtime-0.14.0.dist-info/METADATA +369 -0
  233. algo_cli_runtime-0.14.0.dist-info/RECORD +237 -0
  234. algo_cli_runtime-0.14.0.dist-info/WHEEL +4 -0
  235. algo_cli_runtime-0.14.0.dist-info/entry_points.txt +3 -0
  236. algo_cli_runtime-0.14.0.dist-info/licenses/LICENSE +21 -0
  237. ollama_cli/__init__.py +67 -0
@@ -0,0 +1,1457 @@
1
+ """Agent block execution, required-change contracts, recovery, and pipelines."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import copy
6
+ import logging
7
+ import shlex
8
+ import threading
9
+ import time
10
+ from concurrent.futures import ThreadPoolExecutor, as_completed
11
+ from contextlib import contextmanager
12
+ from dataclasses import dataclass, field
13
+ from typing import Any, Callable
14
+
15
+ from rich import box
16
+ from rich.table import Table
17
+ from rich.text import Text
18
+
19
+ from . import agent_blocks
20
+ from . import agent_threads
21
+ from . import spawn_budget
22
+ from . import git_evidence
23
+ from . import execution_guardrails
24
+ from . import harness
25
+ from . import inference_harness
26
+ from . import memory_runtime
27
+ from . import reflex
28
+ from . import task_router
29
+ from . import tool_policy
30
+ from . import model_info as _model_info_module
31
+ from . import tools as tools_module
32
+ from .chat_protocol import (
33
+ collapse_tool_history_for_gemini,
34
+ get_attr,
35
+ normalize_tool_call,
36
+ serialize_tool_call,
37
+ )
38
+ from .config import Config
39
+ from .display import (
40
+ console,
41
+ finish_thinking_block,
42
+ show_agent_block_complete,
43
+ show_agent_block_start,
44
+ show_agent_pipeline_complete,
45
+ show_agent_recovery_start,
46
+ show_error,
47
+ show_info,
48
+ show_recalled_context,
49
+ show_thinking_text,
50
+ show_tool_result,
51
+ )
52
+ from .perf_telemetry import flush_perf_records, record_chat_metrics, record_perf_event
53
+ from .runtime_services import client_for_model, create_client
54
+ from .tool_runtime import (
55
+ execute_tool_call_for_pipeline,
56
+ record_tool_attempt,
57
+ summarize_tool_result,
58
+ tool_result_message,
59
+ )
60
+
61
+ TOOL_MAP = tools_module.TOOL_MAP
62
+ logger = logging.getLogger(__name__)
63
+
64
+
65
+ @dataclass
66
+ class AgentRunResult:
67
+ """Bounded result returned to the CLI or the parent runtime agent."""
68
+
69
+ thread_id: str = ""
70
+ status: str = "failed"
71
+ pipeline: str = "default"
72
+ output: str = ""
73
+ error: str = ""
74
+ children: list[str] = field(default_factory=list)
75
+ blocks: list[dict[str, Any]] = field(default_factory=list)
76
+
77
+ def for_tool(self) -> str:
78
+ lines = [
79
+ f"Agent thread {self.thread_id or '-'}: {self.status}",
80
+ f"Pipeline: {self.pipeline}",
81
+ ]
82
+ if self.children:
83
+ lines.append(f"Child threads: {', '.join(self.children)}")
84
+ if self.error:
85
+ lines.append(f"Error: {self.error}")
86
+ if self.blocks:
87
+ block_text = ", ".join(
88
+ f"{item.get('role', '?')}={item.get('status', '?')}" for item in self.blocks
89
+ )
90
+ lines.append(f"Blocks: {block_text}")
91
+ if self.output:
92
+ lines.append(f"Output:\n{self.output[:12_000]}")
93
+ return "\n".join(lines)
94
+
95
+
96
+ _execution_state = threading.local()
97
+
98
+
99
+ def agent_execution_active() -> bool:
100
+ """Prevent recursive /agent calls while an Agent Blocks run is active."""
101
+
102
+ return bool(getattr(_execution_state, "depth", 0))
103
+
104
+
105
+ @contextmanager
106
+ def _agent_execution_scope():
107
+ depth = int(getattr(_execution_state, "depth", 0))
108
+ _execution_state.depth = depth + 1
109
+ try:
110
+ yield
111
+ finally:
112
+ _execution_state.depth = depth
113
+
114
+
115
+ def run_agent_block(
116
+ block: agent_blocks.AgentBlock,
117
+ *,
118
+ task: str,
119
+ completed: list[agent_blocks.AgentBlock],
120
+ cfg: Config,
121
+ client: Any,
122
+ route: task_router.TaskRoute | None = None,
123
+ completion_check: Callable[[agent_blocks.AgentBlock], None] | None = None,
124
+ ) -> None:
125
+ block.status = "running"
126
+ block.status_code = ""
127
+ block.status_reason = ""
128
+ policy = tool_policy.compute_policy(
129
+ route or task_router.route_task(task),
130
+ block.role,
131
+ block.allowed_tools,
132
+ cfg.safe_mode,
133
+ cfg.auto_approve_active,
134
+ )
135
+ runtime_tool_names = policy.allowed_tools if cfg.algorithmic_tool_policy_enabled else block.allowed_tools
136
+ allowed_tools = [
137
+ TOOL_MAP[name]
138
+ for name in sorted(runtime_tool_names)
139
+ if name in TOOL_MAP
140
+ ]
141
+ block_model = block.model or cfg.model
142
+ block_client = client_for_model(block_model, cfg, client)
143
+ if block_client is client and block_model != cfg.model:
144
+ block_model = cfg.model
145
+ policy_summary = tool_policy.format_policy_summary(policy)
146
+ if block.requires_change:
147
+ policy_summary += "; file edits: write_file only"
148
+ show_agent_block_start(
149
+ block.role,
150
+ block_model,
151
+ len(allowed_tools),
152
+ policy_summary=policy_summary,
153
+ policy_enforced=cfg.algorithmic_tool_policy_enabled,
154
+ cwd=cfg.cwd,
155
+ )
156
+ started = time.perf_counter()
157
+ from . import session_commands
158
+
159
+ mercury = harness.resolve_mercury_stop_conditions(
160
+ user_message=task,
161
+ session_mode=cfg.session_mode,
162
+ include_external=cfg.external_harness_sources_enabled,
163
+ )
164
+ system_parts = [block.prompt]
165
+ if inference_harness.should_inject(task):
166
+ system_parts.append(f"\n\n{inference_harness.context_block()}")
167
+ if block.requires_change:
168
+ system_parts.append(agent_blocks.REQUIRED_CHANGE_PROMPT)
169
+ system_parts.append(
170
+ "\n\n## Session Workspace\n"
171
+ "Relative tool paths resolve from the active session workspace. Use path '.' for its root; "
172
+ "do not guess or disclose the absolute workspace path.\n"
173
+ f"{session_commands.catalog_for_prompt()}"
174
+ )
175
+ if mercury:
176
+ system_parts.append(f"\n\n## Mercury gates\n{mercury}")
177
+ messages: list[dict[str, Any]] = [
178
+ {
179
+ "role": "system",
180
+ "content": "".join(system_parts),
181
+ },
182
+ {"role": "user", "content": agent_blocks.pipeline_context(task, completed)},
183
+ ]
184
+ block.messages = messages
185
+ completion_nudged = False
186
+
187
+ def finish_with_partial_output() -> None:
188
+ messages.append(
189
+ {
190
+ "role": "user",
191
+ "content": (
192
+ "The block has reached its tool-iteration budget. Do not call any more tools. "
193
+ "Produce a partial but useful ## Block Output summary from evidence gathered so far. "
194
+ "State any incomplete checks explicitly."
195
+ ),
196
+ }
197
+ )
198
+ partial_text = ""
199
+ try:
200
+ stream = block_client.chat(
201
+ model=block_model,
202
+ messages=messages,
203
+ tools=[],
204
+ stream=True,
205
+ think=cfg.show_thinking,
206
+ keep_alive=cfg.keep_alive,
207
+ options={"temperature": cfg.temperature, "num_ctx": cfg.num_ctx},
208
+ )
209
+ for chunk in stream:
210
+ record_chat_metrics(cfg, chunk)
211
+ message = get_attr(chunk, "message", {})
212
+ thinking = get_attr(message, "thinking", "")
213
+ content = get_attr(message, "content", "")
214
+ if thinking and cfg.show_thinking:
215
+ show_thinking_text(thinking)
216
+ if content:
217
+ finish_thinking_block()
218
+ partial_text += content
219
+ except Exception as exc:
220
+ logger.debug("Agent block partial wrap-up failed for %s: %s", block.role, exc)
221
+ finally:
222
+ finish_thinking_block()
223
+ if partial_text.strip():
224
+ block.output = partial_text.strip()
225
+ elif not block.output.strip():
226
+ block.output = "## Block Output\n\nPartial review: tool budget reached before a written summary was produced."
227
+ block.status = "partial"
228
+ block.status_code = "max_iterations"
229
+ block.status_reason = (
230
+ f"Iteration budget exhausted after {max(1, int(block.max_iterations))} cycles; "
231
+ "showing a tool-free partial summary."
232
+ )
233
+
234
+ try:
235
+ execution_scope = execution_guardrails.begin_execution_scope(cfg.cwd)
236
+ except execution_guardrails.ExecutionGuardrailError as exc:
237
+ block.status = "failed"
238
+ block.status_code = "unsafe_workspace"
239
+ block.status_reason = f"Cannot start a safe execution scope: {exc}"
240
+ block.output = block.status_reason
241
+ block.duration_ms = round((time.perf_counter() - started) * 1000, 2)
242
+ show_agent_block_complete(
243
+ block.role,
244
+ block.output,
245
+ duration_ms=block.duration_ms,
246
+ tool_calls=block.tool_calls,
247
+ status=block.status,
248
+ status_reason=block.status_reason,
249
+ status_code=block.status_code,
250
+ model=block_model if block.model else "",
251
+ policy_summary=policy_summary,
252
+ )
253
+ return
254
+
255
+ try:
256
+ for _ in range(max(1, int(block.max_iterations))):
257
+ request_messages = messages
258
+ if _model_info_module.is_gemini_model(block_model):
259
+ request_messages = collapse_tool_history_for_gemini(request_messages)
260
+ stream = block_client.chat(
261
+ model=block_model,
262
+ messages=request_messages,
263
+ tools=allowed_tools,
264
+ stream=True,
265
+ think=cfg.show_thinking,
266
+ keep_alive=cfg.keep_alive,
267
+ options={"temperature": cfg.temperature, "num_ctx": cfg.num_ctx},
268
+ )
269
+ thinking_text = ""
270
+ content_text = ""
271
+ tool_calls: list[Any] = []
272
+ try:
273
+ for chunk in stream:
274
+ record_chat_metrics(cfg, chunk)
275
+ message = get_attr(chunk, "message", {})
276
+ thinking = get_attr(message, "thinking", "")
277
+ content = get_attr(message, "content", "")
278
+ calls = get_attr(message, "tool_calls", None)
279
+ if thinking and cfg.show_thinking:
280
+ show_thinking_text(thinking)
281
+ thinking_text += thinking
282
+ if content:
283
+ finish_thinking_block()
284
+ content_text += content
285
+ if calls:
286
+ tool_calls.extend(calls)
287
+ finally:
288
+ finish_thinking_block()
289
+
290
+ assistant: dict[str, Any] = {"role": "assistant"}
291
+ if content_text:
292
+ assistant["content"] = content_text
293
+ block.output = content_text
294
+ if thinking_text:
295
+ assistant["thinking"] = thinking_text
296
+ serialized_calls = [serialize_tool_call(call) for call in tool_calls]
297
+ if tool_calls:
298
+ assistant["tool_calls"] = serialized_calls
299
+ messages.append(assistant)
300
+
301
+ if not tool_calls:
302
+ completion = execution_guardrails.completion_decision()
303
+ if not completion.allowed:
304
+ if not completion_nudged and _ + 1 < max(1, int(block.max_iterations)):
305
+ completion_nudged = True
306
+ block.output = ""
307
+ show_info(
308
+ f"{block.role} completion deferred until a post-mutation verifier passes."
309
+ )
310
+ messages.append(
311
+ {
312
+ "role": "user",
313
+ "content": (
314
+ "[Internal completion gate] The last workspace mutation is not "
315
+ "verified. Run one appropriate non-mutating test, lint/type check, "
316
+ "or git_diff tool now. Then provide a final ## Block Output grounded "
317
+ "in that verifier."
318
+ ),
319
+ }
320
+ )
321
+ continue
322
+ block.status = "partial"
323
+ block.status_code = "verification_missing"
324
+ block.status_reason = (
325
+ "The block stopped without a successful test, lint/type check, or git diff "
326
+ "after its last workspace mutation."
327
+ )
328
+ block.verification_warning = block.status_reason
329
+ block.output = f"## Block Output\n\nUNVERIFIED: {block.status_reason}"
330
+ elif block.output.strip():
331
+ block.status = "complete"
332
+ else:
333
+ block.status = "failed"
334
+ block.status_code = "model_error"
335
+ block.status_reason = "Block returned neither tool calls nor a usable output."
336
+ block.output = "(no output produced)"
337
+ break
338
+
339
+ policy_denied_batch = False
340
+ for call, serialized_call in zip(tool_calls, serialized_calls):
341
+ name, args = normalize_tool_call(call)
342
+ if policy_denied_batch or name not in runtime_tool_names:
343
+ if name not in runtime_tool_names:
344
+ result = f"Tool not allowed in {block.role} block: {name}"
345
+ block.status_reason = f"Tool policy violation: {name} is not allowed in the {block.role} block."
346
+ else:
347
+ result = "Skipped because another tool call in this assistant message violated block policy."
348
+ show_tool_result(name, result, approved=False)
349
+ messages.append(tool_result_message(name, result, str(serialized_call.get("id") or "") or None))
350
+ block.status = "failed"
351
+ block.status_code = "policy_denied"
352
+ if not block.output:
353
+ block.output = result
354
+ policy_denied_batch = True
355
+ continue
356
+ block.tool_calls += 1
357
+ shell_decision = tool_policy.evaluate_shell_command(
358
+ str(args.get("command", "")),
359
+ requires_change=(block.requires_change and name == "run_shell"),
360
+ safe_mode=cfg.safe_mode,
361
+ )
362
+ if shell_decision.blocked:
363
+ result = (
364
+ "Blocked by required-change policy in safe mode: "
365
+ f"{shell_decision.reason}."
366
+ )
367
+ show_tool_result(name, result, approved=False)
368
+ record_tool_attempt(cfg, name=name, args=args, result=result, status="denied")
369
+ record_perf_event("tool", tool=name, status="denied", duration_ms=0.0)
370
+ messages.append(tool_result_message(name, result, str(serialized_call.get("id") or "") or None))
371
+ block.mutation_denied = True
372
+ continue
373
+ tool_message, _result = execute_tool_call_for_pipeline(
374
+ name,
375
+ args,
376
+ cfg,
377
+ tool_call_id=str(serialized_call.get("id") or "") or None,
378
+ force_approval=tool_policy.requires_explicit_approval(
379
+ name,
380
+ block_policy=policy,
381
+ shell_decision=shell_decision,
382
+ policy_enforced=cfg.algorithmic_tool_policy_enabled,
383
+ ),
384
+ )
385
+ mutation_action = tool_policy.describes_mutation_action(name, args)
386
+ mutation_succeeded = (
387
+ (name == "write_file" and str(_result).lstrip().startswith("Wrote "))
388
+ or (name == "edit_file" and str(_result).lstrip().startswith("Edited "))
389
+ or (name == "batch_edit" and str(_result).lstrip().startswith("Batch-edited "))
390
+ )
391
+ if mutation_succeeded:
392
+ written_path = str(args.get("path", "")).strip()
393
+ if written_path and written_path not in block.successful_writes:
394
+ block.successful_writes.append(written_path)
395
+ if mutation_action and mutation_action not in block.mutation_actions:
396
+ block.mutation_actions.append(mutation_action)
397
+ elif name in {"write_file", "edit_file", "batch_edit"}:
398
+ lowered_result = str(_result).strip().lower()
399
+ if lowered_result.startswith("user denied"):
400
+ block.mutation_denied = True
401
+ elif lowered_result.startswith(("error", "tool error", "tool argument error")):
402
+ block.failed_writes.append(summarize_tool_result(str(_result)))
403
+ elif (
404
+ name == "run_shell"
405
+ and mutation_action
406
+ and not str(_result).startswith(("User denied", "Skipped repeated", "Blocked"))
407
+ and mutation_action not in block.mutation_actions
408
+ ):
409
+ block.mutation_actions.append(mutation_action)
410
+ messages.append(tool_message)
411
+ if block.status == "failed":
412
+ break
413
+ else:
414
+ finish_with_partial_output()
415
+ finally:
416
+ completion_error = ""
417
+ try:
418
+ if completion_check is not None:
419
+ completion_check(block)
420
+ except Exception as exc:
421
+ completion_error = f"Completion check failed: {type(exc).__name__}: {exc}"
422
+ block.status = "failed"
423
+ block.status_code = "completion_check_error"
424
+ block.status_reason = completion_error
425
+ try:
426
+ execution_guardrails.end_execution_scope(execution_scope)
427
+ except execution_guardrails.ExecutionGuardrailError as exc:
428
+ block.status = "failed"
429
+ block.status_code = "guardrail_scope_error"
430
+ block.status_reason = f"Execution evidence scope failed to close: {exc}"
431
+ block.duration_ms = round((time.perf_counter() - started) * 1000, 2)
432
+ show_agent_block_complete(
433
+ block.role,
434
+ block.output,
435
+ duration_ms=block.duration_ms,
436
+ tool_calls=block.tool_calls,
437
+ status=block.status,
438
+ status_reason=block.status_reason,
439
+ verification_warning=block.verification_warning,
440
+ status_code=block.status_code,
441
+ model=block_model if block.model else "",
442
+ policy_summary=policy_summary,
443
+ successful_writes=list(block.successful_writes),
444
+ )
445
+
446
+
447
+ AGENT_USAGE = "Usage: /agent [--pipeline NAME] <task>"
448
+ AGENT_TEAM_USAGE = "Usage: /agent team [--roles ROLE,ROLE[,ROLE,ROLE]] <task>"
449
+ AGENT_THREAD_USAGE = "Usage: /agent show THREAD | resume THREAD [task] | fork THREAD <task>"
450
+ MIN_TEAM_ROLES = 2
451
+ MAX_TEAM_ROLES = 4
452
+
453
+
454
+ def agent_usage_text() -> str:
455
+ return (
456
+ f"{AGENT_USAGE}\n"
457
+ f"{AGENT_TEAM_USAGE}\n"
458
+ f"{AGENT_THREAD_USAGE}\n"
459
+ "Thread commands: /agent threads | show THREAD | resume THREAD [task] | fork THREAD <task>\n"
460
+ f"Available pipelines: {', '.join(agent_blocks.pipeline_names())}\n"
461
+ "Examples:\n"
462
+ " /agent --pipeline code-change Fix the failing tests\n"
463
+ " /agent team --roles scout,critic,verifier Review the current worktree\n"
464
+ " /agent resume 7d12a9 Finish the remaining verification"
465
+ )
466
+
467
+
468
+ def _normalize_team_role(role: str) -> str:
469
+ cleaned = "-".join(role.strip().lower().split())
470
+ if not cleaned or len(cleaned) > 32:
471
+ return ""
472
+ if not all(char.isalnum() or char in {"-", "_"} for char in cleaned):
473
+ return ""
474
+ return cleaned
475
+
476
+
477
+ def default_team_roles(route: task_router.TaskRoute) -> list[str]:
478
+ if route.task_type == "coding":
479
+ return ["code-scout", "solution-designer", "risk-verifier"]
480
+ if route.task_type == "review":
481
+ return ["correctness-reviewer", "security-reviewer", "test-reviewer"]
482
+ if route.task_type == "research":
483
+ return ["source-scout", "counterpoint", "fact-checker"]
484
+ return ["planner", "analyst", "critic"]
485
+
486
+
487
+ def parse_agent_team_invocation(arg: str) -> tuple[list[str], str, str]:
488
+ """Parse the portion after `/agent team`."""
489
+
490
+ try:
491
+ parts = shlex.split((arg or "").strip())
492
+ except ValueError as exc:
493
+ return [], "", f"{AGENT_TEAM_USAGE} ({exc})"
494
+ roles: list[str] = []
495
+ task_parts: list[str] = []
496
+ index = 0
497
+ while index < len(parts):
498
+ part = parts[index]
499
+ if part == "--roles":
500
+ if index + 1 >= len(parts):
501
+ return [], "", AGENT_TEAM_USAGE
502
+ raw_roles = parts[index + 1]
503
+ index += 2
504
+ elif part.startswith("--roles="):
505
+ raw_roles = part.split("=", 1)[1]
506
+ index += 1
507
+ elif part.startswith("--"):
508
+ return [], "", f"Unknown team option '{part}'. {AGENT_TEAM_USAGE}"
509
+ else:
510
+ task_parts.append(part)
511
+ index += 1
512
+ continue
513
+ roles = [_normalize_team_role(item) for item in raw_roles.split(",")]
514
+ if not all(roles):
515
+ return [], "", "Team roles must be short names using letters, numbers, '-' or '_'."
516
+ task = " ".join(task_parts).strip()
517
+ if not task:
518
+ return [], "", AGENT_TEAM_USAGE
519
+ if roles:
520
+ if not MIN_TEAM_ROLES <= len(roles) <= MAX_TEAM_ROLES:
521
+ return [], "", f"Team runs require {MIN_TEAM_ROLES}-{MAX_TEAM_ROLES} roles."
522
+ if len(set(roles)) != len(roles):
523
+ return [], "", "Team roles must be unique."
524
+ return roles, task, ""
525
+
526
+
527
+ def parse_agent_invocation_checked(arg: str) -> tuple[str, str, str]:
528
+ """Return (pipeline_name, task, error) from /agent arguments."""
529
+ text = (arg or "").strip()
530
+ if not text:
531
+ return "default", "", ""
532
+ try:
533
+ parts = shlex.split(text)
534
+ except ValueError:
535
+ return "default", text, ""
536
+ if len(parts) >= 3 and parts[0] == "--pipeline":
537
+ pipeline = parts[1]
538
+ task = " ".join(parts[2:]).strip()
539
+ return pipeline, task, ""
540
+ if len(parts) >= 2 and parts[0].startswith("--pipeline="):
541
+ pipeline = parts[0].split("=", 1)[1]
542
+ task = " ".join(parts[1:]).strip()
543
+ if not pipeline.strip() or not task:
544
+ return "default", "", AGENT_USAGE
545
+ return pipeline, task, ""
546
+ if parts and parts[0].startswith("--pipeline"):
547
+ return "default", "", AGENT_USAGE
548
+ return "default", text, ""
549
+
550
+
551
+ def parse_agent_invocation(arg: str) -> tuple[str, str]:
552
+ """Return (pipeline_name, task) from /agent arguments."""
553
+ pipeline, task, _error = parse_agent_invocation_checked(arg)
554
+ return pipeline, task
555
+
556
+
557
+ def resolve_pipeline_for_cli(name: str) -> tuple[list[agent_blocks.AgentBlock], str] | None:
558
+ try:
559
+ return agent_blocks.resolve_pipeline(name)
560
+ except agent_blocks.BlocksConfigError as exc:
561
+ show_error(str(exc))
562
+ try:
563
+ pipeline = agent_blocks.builtin_pipeline_by_name(name)
564
+ except ValueError as builtin_exc:
565
+ show_error(str(builtin_exc))
566
+ return None
567
+ show_info(f"Using built-in '{name}' pipeline instead.")
568
+ return pipeline, "built-in fallback"
569
+ except ValueError as exc:
570
+ show_error(str(exc))
571
+ return None
572
+
573
+
574
+ def show_task_route(route: task_router.TaskRoute, cfg: Config, prompt: str = "") -> None:
575
+ budget = spawn_budget.compute_budget(route, prompt)
576
+ table = Table(title="Task Route", box=box.SIMPLE, show_header=False, padding=(0, 1))
577
+ table.add_column("Field", style="muted")
578
+ table.add_column("Value", style="text")
579
+ table.add_row("type", route.task_type)
580
+ table.add_row("complexity", route.complexity)
581
+ table.add_row("mode", route.recommended_mode)
582
+ table.add_row("pipeline", route.suggested_pipeline)
583
+ table.add_row("tool groups", ", ".join(route.allowed_tool_groups) or "-")
584
+ table.add_row("risk", route.risk)
585
+ table.add_row("reason", route.reason)
586
+ table.add_row(
587
+ "budget",
588
+ (
589
+ f"{budget.max_blocks} blocks, {budget.max_iterations_per_block} iterations/block, "
590
+ f"parallelism: {spawn_budget.parallelism_label(budget.parallelism)}"
591
+ if budget.max_blocks
592
+ else "chat/user-directed only"
593
+ ),
594
+ )
595
+ resolved = resolve_pipeline_for_cli(route.suggested_pipeline)
596
+ if resolved is None:
597
+ console.print(table)
598
+ return
599
+ pipeline, pipeline_source = resolved
600
+ table.add_row("pipeline source", pipeline_source)
601
+ console.print(table)
602
+ policy_table = Table(
603
+ title=f"Advisory Tool Policy - {route.suggested_pipeline}",
604
+ box=box.SIMPLE,
605
+ padding=(0, 1),
606
+ )
607
+ policy_table.add_column("Block", style="primary")
608
+ policy_table.add_column("Effective tools", style="text", overflow="fold")
609
+ policy_table.add_column("Approval", style="warning", overflow="fold")
610
+ policy_table.add_column("Denied", style="muted", overflow="fold")
611
+ for block in pipeline:
612
+ decision = tool_policy.compute_policy(
613
+ route,
614
+ block.role,
615
+ block.allowed_tools,
616
+ cfg.safe_mode,
617
+ cfg.auto_approve_active,
618
+ )
619
+ policy_table.add_row(
620
+ block.role,
621
+ ", ".join(sorted(decision.allowed_tools)) or "-",
622
+ ", ".join(sorted(decision.approval_required)) or "-",
623
+ ", ".join(sorted(decision.denied_tools)) or "-",
624
+ )
625
+ console.print(policy_table)
626
+ pipeline_blocks = len(pipeline)
627
+ if budget.max_blocks and pipeline_blocks > budget.max_blocks:
628
+ show_info(
629
+ f"Budget recommends at most {budget.max_blocks} blocks; "
630
+ f"pipeline {route.suggested_pipeline} defines {pipeline_blocks}. Execution remains unchanged."
631
+ )
632
+ over_iteration_roles = [
633
+ block.role
634
+ for block in pipeline
635
+ if budget.max_iterations_per_block and block.max_iterations > budget.max_iterations_per_block
636
+ ]
637
+ if over_iteration_roles:
638
+ show_info(
639
+ f"Budget recommends at most {budget.max_iterations_per_block} iterations/block; "
640
+ f"blocks exceeding it: {', '.join(over_iteration_roles)}. Execution remains unchanged."
641
+ )
642
+ for reason in budget.reasons:
643
+ show_info(f"Budget: {reason}")
644
+ if cfg.algorithmic_tool_policy_enabled:
645
+ show_info("Policy enforcement is ON; Agent Block tool sets use this policy.")
646
+ else:
647
+ show_info("Policy preview is advisory; Agent Block execution tools are unchanged.")
648
+ if route.recommended_mode == "agent":
649
+ suffix = f" {prompt.strip()}" if prompt.strip() else " <task>"
650
+ show_info(f"Suggested command: /agent --pipeline {route.suggested_pipeline}{suffix}")
651
+ elif route.risk == "high":
652
+ show_info("High-risk task: review the action before running tools or Agent Blocks.")
653
+
654
+
655
+ def maybe_show_route_suggestion(user_message: str) -> None:
656
+ route = task_router.route_task(user_message)
657
+ if not task_router.should_suggest(route):
658
+ return
659
+ if route.recommended_mode == "agent":
660
+ show_info(
661
+ "Route suggestion: "
662
+ f"{route.task_type} task -> /agent --pipeline {route.suggested_pipeline} "
663
+ f"(reason: {route.reason})"
664
+ )
665
+ elif route.risk == "high":
666
+ show_info(f"Route warning: high-risk task detected ({route.reason})")
667
+
668
+
669
+ def enforce_required_change_contract(
670
+ block: agent_blocks.AgentBlock,
671
+ before: git_evidence.GitSnapshot,
672
+ after: git_evidence.GitSnapshot,
673
+ ) -> None:
674
+ """Prevent change-producing blocks from completing without final evidence."""
675
+
676
+ block.git_evidence = git_evidence.format_git_evidence(before, after)
677
+ if not block.requires_change or block.status != "complete":
678
+ return
679
+
680
+ # Git evidence is the strict path; when it is unavailable but recorded
681
+ # write_file evidence exists, the contract is satisfied with a manual-
682
+ # verification notice rather than a partial downgrade. Returning here
683
+ # preserves block.status == "complete" and skips the produced_change gate.
684
+ if (not before.available or not after.available) and block.successful_writes:
685
+ block.verification_warning = (
686
+ "Git verification was unavailable. Successful write_file operations were recorded, "
687
+ "but review must manually confirm the written files."
688
+ )
689
+ return
690
+
691
+ produced_change = git_evidence.has_verified_delta(before, after) or (
692
+ bool(block.successful_writes) and git_evidence.has_observed_delta(before, after)
693
+ )
694
+ if produced_change:
695
+ return
696
+
697
+ reported_output = block.output.strip()
698
+ block.status = "partial"
699
+ if block.mutation_denied:
700
+ block.status_code = "policy_denied"
701
+ block.status_reason = "Required change not verified: a requested mutation was denied or blocked by policy."
702
+ elif block.failed_writes:
703
+ block.status_code = "write_blocked"
704
+ block.status_reason = "Required change not verified: write_file was attempted but failed before producing a verified change."
705
+ elif not before.available or not after.available:
706
+ block.status_code = "no_write_evidence"
707
+ block.status_reason = "Required change not verified: Git evidence is unavailable and no successful write_file action was recorded."
708
+ elif before.head != after.head:
709
+ block.status_code = "attribution_unsafe"
710
+ block.status_reason = "Required change not verified: repository HEAD changed during execution, so attribution is unsafe."
711
+ elif block.successful_writes:
712
+ block.status_code = "no_verified_delta"
713
+ block.status_reason = "Required change not verified: recorded writes left no attributable final-state Git delta."
714
+ else:
715
+ block.status_code = "no_write_evidence"
716
+ block.status_reason = "Required change not verified: no successful write_file action or attributable Git delta was detected."
717
+ block.output = (
718
+ "## Block Output\n\n"
719
+ "No verified code change was produced. "
720
+ "No successful write_file operation with a remaining final-state delta or attributable Git delta was detected."
721
+ )
722
+ if reported_output:
723
+ block.output += f"\n\nUnverified reported output:\n{reported_output}"
724
+
725
+
726
+ def capture_optional_mutation_audit(
727
+ block: agent_blocks.AgentBlock,
728
+ before: git_evidence.GitSnapshot,
729
+ after: git_evidence.GitSnapshot,
730
+ ) -> None:
731
+ """Report unexpected mutation actions by blocks without a change contract."""
732
+
733
+ if block.requires_change or not block.mutation_actions:
734
+ return
735
+ actions = "\n".join(f"- {action}" for action in block.mutation_actions)
736
+ block.audit_evidence = (
737
+ "Audit notice: a block without requires_change executed mutation-capable actions.\n"
738
+ f"Actions:\n{actions}\n\n"
739
+ f"{git_evidence.format_git_evidence(before, after)}"
740
+ )
741
+ show_info(f"Mutation audit: {block.role} executed mutation-capable actions; evidence was captured for review.")
742
+
743
+
744
+ RECOVERABLE_IMPLEMENT_CODES = frozenset({"max_iterations", "no_write_evidence", "write_blocked", "no_verified_delta"})
745
+ MAX_RECOVERY_IMPLEMENT_ITERATIONS = 8
746
+
747
+
748
+ def should_recover_implementation(block: agent_blocks.AgentBlock) -> bool:
749
+ """Return whether a partial implementation should receive one focused retry."""
750
+
751
+ return (
752
+ block.requires_change
753
+ and block.status == "partial"
754
+ and block.status_code in RECOVERABLE_IMPLEMENT_CODES
755
+ and not block.mutation_denied
756
+ and not (block.status_code == "max_iterations" and block.successful_writes)
757
+ )
758
+
759
+
760
+ def recovery_plan_block(failed_block: agent_blocks.AgentBlock, cfg: Config) -> agent_blocks.AgentBlock:
761
+ recent_attempts = cfg.attempt_ledger[-6:]
762
+ attempt_lines = [
763
+ f"- {item.get('status', '?').upper()} {item.get('tool', '?')}: {item.get('summary', '')}"
764
+ for item in recent_attempts
765
+ ]
766
+ attempt_context = "\n".join(attempt_lines) if attempt_lines else "- (no recent tool attempts recorded)"
767
+ return agent_blocks.AgentBlock(
768
+ role="recovery-plan",
769
+ model=failed_block.model,
770
+ max_iterations=1,
771
+ prompt=(
772
+ "You are a focused recovery planner. The previous required-change implementation did not complete. "
773
+ "Use only the provided failure output, status, write evidence, Git evidence, and attempt ledger context. "
774
+ "Do not call tools. Produce a concise corrected execution plan for a single retry, prioritizing the "
775
+ "specific write_file call(s) and minimal verification needed. Return Markdown beginning with "
776
+ "## Block Output.\n\nRecent tool-attempt summary:\n"
777
+ f"{attempt_context}"
778
+ ),
779
+ )
780
+
781
+
782
+ def retry_implementation_block(failed_block: agent_blocks.AgentBlock) -> agent_blocks.AgentBlock:
783
+ return agent_blocks.AgentBlock(
784
+ role="implement-retry",
785
+ prompt=(
786
+ f"{failed_block.prompt}\n\n"
787
+ "This is the only recovery retry. Follow the recovery-plan output already in context. "
788
+ "Avoid repeating broad exploration; execute the targeted write_file edits and focused verification."
789
+ ),
790
+ allowed_tools=failed_block.allowed_tools,
791
+ model=failed_block.model,
792
+ max_iterations=min(MAX_RECOVERY_IMPLEMENT_ITERATIONS, max(1, failed_block.max_iterations)),
793
+ requires_change=True,
794
+ )
795
+
796
+
797
+ # Session-scoped buffer holding the most recent pipeline's completed blocks.
798
+ # Surfaced by `/diff` and `/changes`. Cleared on `/clear`, overwritten on each
799
+ # new pipeline run. Not persisted to disk — purely in-process state.
800
+ _session_pipeline_blocks: list[agent_blocks.AgentBlock] = []
801
+
802
+
803
+ def session_pipeline_blocks() -> list[agent_blocks.AgentBlock]:
804
+ return _session_pipeline_blocks
805
+
806
+
807
+ def clear_session_pipeline_blocks() -> None:
808
+ _session_pipeline_blocks.clear()
809
+
810
+
811
+ def resolve_agent_workspace(task: str, cfg: Config) -> bool:
812
+ """Point cfg.cwd at a recognized project root inferred from the task."""
813
+ from . import workspace_resolver
814
+
815
+ if workspace_resolver.resolve_agent_workspace(task, cfg):
816
+ show_info(f"Agent workspace set to {cfg.cwd}")
817
+ return True
818
+ return False
819
+
820
+
821
+ def _block_record(block: agent_blocks.AgentBlock) -> dict[str, Any]:
822
+ return {
823
+ "role": block.role,
824
+ "status": block.status,
825
+ "status_code": block.status_code,
826
+ "status_reason": block.status_reason,
827
+ "tool_calls": block.tool_calls,
828
+ "duration_ms": block.duration_ms,
829
+ "successful_writes": list(block.successful_writes),
830
+ "verification_warning": block.verification_warning,
831
+ }
832
+
833
+
834
+ def _start_thread_record(
835
+ task: str,
836
+ cfg: Config,
837
+ pipeline_name: str,
838
+ *,
839
+ thread_id: str | None,
840
+ parent_id: str,
841
+ ) -> str:
842
+ try:
843
+ if thread_id:
844
+ agent_threads.begin_turn(thread_id, task, pipeline=pipeline_name, model=cfg.model)
845
+ return thread_id
846
+ record = agent_threads.create_thread(
847
+ task,
848
+ pipeline=pipeline_name,
849
+ model=cfg.model,
850
+ parent_id=parent_id,
851
+ status="queued",
852
+ start_turn=True,
853
+ )
854
+ return str(record["id"])
855
+ except (OSError, ValueError, KeyError) as exc:
856
+ logger.debug("Agent thread persistence unavailable: %s", exc)
857
+ show_info(f"Agent thread history unavailable for this run: {exc}")
858
+ return ""
859
+
860
+
861
+ def _finish_thread_record(
862
+ thread_id: str,
863
+ *,
864
+ status: str,
865
+ output: str,
866
+ error: str,
867
+ blocks: list[dict[str, Any]],
868
+ pipeline: str,
869
+ ) -> None:
870
+ if not thread_id:
871
+ return
872
+ try:
873
+ agent_threads.finish_turn(
874
+ thread_id,
875
+ status=status,
876
+ output=output,
877
+ error=error,
878
+ blocks=blocks,
879
+ pipeline=pipeline,
880
+ )
881
+ except (OSError, ValueError, KeyError) as exc:
882
+ logger.debug("Could not finish agent thread record %s: %s", thread_id, exc)
883
+
884
+
885
+ def run_agent_pipeline(
886
+ task: str,
887
+ cfg: Config,
888
+ client: Any,
889
+ pipeline_name: str = "default",
890
+ *,
891
+ thread_id: str | None = None,
892
+ parent_id: str = "",
893
+ prior_context: str = "",
894
+ thread_pipeline_label: str | None = None,
895
+ ) -> AgentRunResult:
896
+ if not task.strip():
897
+ show_error(AGENT_USAGE)
898
+ return AgentRunResult(status="failed", pipeline=pipeline_name, error=AGENT_USAGE)
899
+ reflex.begin_agent_pipeline(cfg)
900
+ resolve_agent_workspace(task, cfg)
901
+ started = time.perf_counter()
902
+ completed: list[agent_blocks.AgentBlock] = []
903
+ resolved = resolve_pipeline_for_cli(pipeline_name)
904
+ if resolved is None:
905
+ return AgentRunResult(status="failed", pipeline=pipeline_name, error=f"Pipeline '{pipeline_name}' is unavailable.")
906
+ pipeline, _pipeline_source = resolved
907
+ record_pipeline = thread_pipeline_label or pipeline_name
908
+ active_thread_id = _start_thread_record(
909
+ task,
910
+ cfg,
911
+ record_pipeline,
912
+ thread_id=thread_id,
913
+ parent_id=parent_id,
914
+ )
915
+ if active_thread_id:
916
+ show_info(f"Agent thread {active_thread_id} · {record_pipeline}")
917
+ route = task_router.route_task(task)
918
+ pipeline_task = task
919
+ if prior_context.strip():
920
+ pipeline_task = (
921
+ f"{task}\n\n## Parent Thread Handoff\n"
922
+ "Treat this as bounded evidence from independent specialist threads. "
923
+ "Verify consequential claims before acting.\n\n"
924
+ f"{prior_context.strip()[:24_000]}"
925
+ )
926
+ from . import main as _main
927
+
928
+ engine = _main._intuition_engine
929
+ if engine is not None and cfg.intuition_recall_enabled:
930
+ try:
931
+ recalled_blocks = engine.recall(
932
+ task,
933
+ enabled=True,
934
+ embed_fn=_main.intuition_embed_fn(cfg),
935
+ )
936
+ if recalled_blocks:
937
+ show_recalled_context(recalled_blocks)
938
+ injection = engine.format_for_injection(recalled_blocks)
939
+ if injection:
940
+ pipeline_task = f"{pipeline_task}\n\n{injection}"
941
+ except Exception as exc:
942
+ logger.debug("Agent pipeline intuition recall failed: %s", exc)
943
+
944
+ def run_pipeline_block(block: agent_blocks.AgentBlock) -> None:
945
+ before_git = (
946
+ git_evidence.capture_git_snapshot(cfg.cwd)
947
+ if block.requires_change or tool_policy.supports_mutation_audit(block.allowed_tools)
948
+ else None
949
+ )
950
+ completion_check = None
951
+ if before_git is not None:
952
+ def completion_check(completed_block: agent_blocks.AgentBlock, baseline=before_git) -> None:
953
+ after_git = git_evidence.capture_git_snapshot(cfg.cwd)
954
+ if completed_block.requires_change:
955
+ enforce_required_change_contract(completed_block, baseline, after_git)
956
+ else:
957
+ capture_optional_mutation_audit(completed_block, baseline, after_git)
958
+ run_agent_block(
959
+ block,
960
+ task=pipeline_task,
961
+ completed=completed,
962
+ cfg=cfg,
963
+ client=client,
964
+ route=route,
965
+ completion_check=completion_check,
966
+ )
967
+
968
+ def append_pipeline_block(block: agent_blocks.AgentBlock) -> None:
969
+ block.context_output = agent_blocks.compact_block_output(block.output)
970
+ completed.append(block)
971
+
972
+ terminal_block: agent_blocks.AgentBlock | None = None
973
+ cancelled = False
974
+ run_error = ""
975
+ try:
976
+ with _agent_execution_scope():
977
+ for block in pipeline:
978
+ terminal_block = block
979
+ run_pipeline_block(block)
980
+ if block.status not in {"complete", "partial"}:
981
+ detail = f" ({block.status_reason})" if block.status_reason else ""
982
+ show_error(f"Agent pipeline stopped at {block.role}: {block.status}{detail}")
983
+ break
984
+ append_pipeline_block(block)
985
+ if block.status_code == "verification_missing":
986
+ show_error(
987
+ f"Agent pipeline stopped at {block.role}: post-mutation verification is missing."
988
+ )
989
+ break
990
+ if should_recover_implementation(block):
991
+ retry_iterations = min(MAX_RECOVERY_IMPLEMENT_ITERATIONS, max(1, block.max_iterations))
992
+ show_agent_recovery_start(block.role, block.status_reason, retry_iterations)
993
+ replan = recovery_plan_block(block, cfg)
994
+ terminal_block = replan
995
+ run_pipeline_block(replan)
996
+ if replan.status in {"complete", "partial"}:
997
+ append_pipeline_block(replan)
998
+ if replan.status == "complete":
999
+ retry = retry_implementation_block(block)
1000
+ terminal_block = retry
1001
+ run_pipeline_block(retry)
1002
+ append_pipeline_block(retry)
1003
+ else:
1004
+ show_info("Recovery replan did not complete; continuing with the original partial evidence.")
1005
+ if (
1006
+ completed
1007
+ and completed[-1].role == "final"
1008
+ and all(block.status == "complete" for block in completed)
1009
+ ):
1010
+ show_agent_pipeline_complete(
1011
+ completed[-1].output,
1012
+ block_count=len(completed),
1013
+ duration_ms=round((time.perf_counter() - started) * 1000, 2),
1014
+ )
1015
+ except KeyboardInterrupt:
1016
+ cancelled = True
1017
+ finish_thinking_block()
1018
+ show_error("Agent pipeline cancelled.")
1019
+ except Exception as exc:
1020
+ run_error = str(exc)
1021
+ raise
1022
+ finally:
1023
+ # Overwrite the session buffer with whichever blocks made it into
1024
+ # `completed`. Partial / stopped runs still expose what they did.
1025
+ _session_pipeline_blocks[:] = completed
1026
+ persisted_blocks = list(completed)
1027
+ if terminal_block is not None and terminal_block not in persisted_blocks:
1028
+ persisted_blocks.append(terminal_block)
1029
+ output = (
1030
+ completed[-1].output
1031
+ if completed
1032
+ else terminal_block.output if terminal_block is not None else ""
1033
+ )
1034
+ if cancelled:
1035
+ status = "cancelled"
1036
+ error = "Agent pipeline cancelled."
1037
+ elif run_error:
1038
+ status = "failed"
1039
+ error = run_error
1040
+ elif terminal_block is not None and terminal_block.status == "failed":
1041
+ status = "failed"
1042
+ error = terminal_block.status_reason or terminal_block.output
1043
+ elif any(block.status == "partial" for block in persisted_blocks):
1044
+ status = "partial"
1045
+ error = next(
1046
+ (block.status_reason for block in persisted_blocks if block.status == "partial" and block.status_reason),
1047
+ "",
1048
+ )
1049
+ elif completed and completed[-1].role == "final":
1050
+ status = "complete"
1051
+ error = ""
1052
+ else:
1053
+ status = "partial"
1054
+ error = "Pipeline ended before the final block."
1055
+ block_records = [_block_record(block) for block in persisted_blocks]
1056
+ _finish_thread_record(
1057
+ active_thread_id,
1058
+ status=status,
1059
+ output=output,
1060
+ error=error,
1061
+ blocks=block_records,
1062
+ pipeline=record_pipeline,
1063
+ )
1064
+ cfg.save()
1065
+ flush_perf_records()
1066
+ return AgentRunResult(
1067
+ thread_id=active_thread_id,
1068
+ status=status,
1069
+ pipeline=record_pipeline,
1070
+ output=output,
1071
+ error=error,
1072
+ blocks=block_records,
1073
+ )
1074
+
1075
+
1076
+ def _specialist_prompt(role: str) -> str:
1077
+ return (
1078
+ f"You are the {role} specialist in an Algo CLI multi-agent team. You have a fresh, "
1079
+ "isolated context and a read-only tool set. Work independently; do not assume another "
1080
+ "specialist will cover your angle.\n\n"
1081
+ "Use the Algo loop:\n"
1082
+ "1. Define the question, invariant, or failure mode assigned to your role.\n"
1083
+ "2. Gather the smallest useful set of direct evidence.\n"
1084
+ "3. Compare alternatives or challenge the leading assumption.\n"
1085
+ "4. Separate verified facts from hypotheses and unresolved risks.\n"
1086
+ "5. Produce a concise handoff that an integration agent can verify and act on.\n\n"
1087
+ "Do not modify files, memory, configuration, or external systems. Cite concrete paths, "
1088
+ "commands, or sources when available. Return Markdown starting with exactly:\n"
1089
+ "## Block Output"
1090
+ )
1091
+
1092
+
1093
+ def _finish_specialist_thread(
1094
+ thread_id: str,
1095
+ block: agent_blocks.AgentBlock,
1096
+ *,
1097
+ error: str = "",
1098
+ ) -> None:
1099
+ if not thread_id:
1100
+ return
1101
+ status = block.status if block.status in {"complete", "partial", "failed", "cancelled"} else "failed"
1102
+ try:
1103
+ agent_threads.finish_turn(
1104
+ thread_id,
1105
+ status=status,
1106
+ output=block.output,
1107
+ error=error or block.status_reason,
1108
+ blocks=[_block_record(block)],
1109
+ )
1110
+ except (OSError, ValueError, KeyError) as exc:
1111
+ logger.debug("Could not finish specialist thread %s: %s", thread_id, exc)
1112
+
1113
+
1114
+ def run_agent_team(
1115
+ task: str,
1116
+ cfg: Config,
1117
+ client: Any,
1118
+ *,
1119
+ roles: list[str] | None = None,
1120
+ ) -> AgentRunResult:
1121
+ """Fan out independent read-only specialists, then integrate in one pipeline."""
1122
+
1123
+ if not task.strip():
1124
+ show_error(AGENT_TEAM_USAGE)
1125
+ return AgentRunResult(status="failed", pipeline="team", error=AGENT_TEAM_USAGE)
1126
+ route = task_router.route_task(task)
1127
+ requested_roles = roles or default_team_roles(route)
1128
+ selected_roles = [_normalize_team_role(role) for role in requested_roles]
1129
+ if not all(selected_roles):
1130
+ error = "Team roles must be short names using letters, numbers, '-' or '_'."
1131
+ show_error(error)
1132
+ return AgentRunResult(status="failed", pipeline="team", error=error)
1133
+ if not MIN_TEAM_ROLES <= len(selected_roles) <= MAX_TEAM_ROLES:
1134
+ error = f"Team runs require {MIN_TEAM_ROLES}-{MAX_TEAM_ROLES} roles."
1135
+ show_error(error)
1136
+ return AgentRunResult(status="failed", pipeline="team", error=error)
1137
+ if len(set(selected_roles)) != len(selected_roles):
1138
+ error = "Team roles must be unique."
1139
+ show_error(error)
1140
+ return AgentRunResult(status="failed", pipeline="team", error=error)
1141
+
1142
+ parent_id = ""
1143
+ child_ids: list[str] = []
1144
+ child_by_role: dict[str, str] = {}
1145
+ try:
1146
+ parent = agent_threads.create_thread(
1147
+ task,
1148
+ role="orchestrator",
1149
+ pipeline="team",
1150
+ model=cfg.model,
1151
+ status="running",
1152
+ title=f"Team: {' '.join(task.split())[:72]}",
1153
+ )
1154
+ parent_id = str(parent["id"])
1155
+ for role in selected_roles:
1156
+ child = agent_threads.create_thread(
1157
+ task,
1158
+ role=role,
1159
+ pipeline="specialist",
1160
+ model=cfg.model,
1161
+ parent_id=parent_id,
1162
+ status="queued",
1163
+ start_turn=True,
1164
+ title=f"{role}: {' '.join(task.split())[:64]}",
1165
+ )
1166
+ child_id = str(child["id"])
1167
+ child_ids.append(child_id)
1168
+ child_by_role[role] = child_id
1169
+ except (OSError, ValueError, KeyError) as exc:
1170
+ logger.debug("Could not initialize complete team thread tree: %s", exc)
1171
+ show_info(f"Some team thread history may be unavailable: {exc}")
1172
+
1173
+ show_info(
1174
+ f"Agent team {parent_id or '(unrecorded)'}: launching {len(selected_roles)} read-only specialists "
1175
+ f"({', '.join(selected_roles)})."
1176
+ )
1177
+
1178
+ def run_specialist(role: str) -> agent_blocks.AgentBlock:
1179
+ member_cfg = copy.deepcopy(cfg)
1180
+ member_cfg.messages = []
1181
+ member_cfg.session_summary = ""
1182
+ member_cfg.attempt_ledger = []
1183
+ block = agent_blocks.AgentBlock(
1184
+ role=role,
1185
+ prompt=_specialist_prompt(role),
1186
+ allowed_tools=agent_blocks.READ_TOOLS,
1187
+ max_iterations=min(8, max(2, int(cfg.max_tool_iterations))),
1188
+ )
1189
+ try:
1190
+ member_client = create_client(member_cfg)
1191
+ with _agent_execution_scope():
1192
+ run_agent_block(
1193
+ block,
1194
+ task=task,
1195
+ completed=[],
1196
+ cfg=member_cfg,
1197
+ client=member_client,
1198
+ route=route,
1199
+ )
1200
+ except Exception as exc:
1201
+ block.status = "failed"
1202
+ block.status_code = "specialist_error"
1203
+ block.status_reason = str(exc)
1204
+ block.output = block.output or f"## Block Output\n\nSpecialist failed: {exc}"
1205
+ block.context_output = agent_blocks.compact_block_output(block.output)
1206
+ return block
1207
+
1208
+ specialists: dict[str, agent_blocks.AgentBlock] = {}
1209
+ with ThreadPoolExecutor(max_workers=len(selected_roles), thread_name_prefix="algo-agent") as pool:
1210
+ futures = {pool.submit(run_specialist, role): role for role in selected_roles}
1211
+ try:
1212
+ for future in as_completed(futures):
1213
+ role = futures[future]
1214
+ block = future.result()
1215
+ specialists[role] = block
1216
+ _finish_specialist_thread(child_by_role.get(role, ""), block)
1217
+ except KeyboardInterrupt:
1218
+ for future in futures:
1219
+ future.cancel()
1220
+ for role in selected_roles:
1221
+ if role in specialists:
1222
+ continue
1223
+ block = agent_blocks.AgentBlock(
1224
+ role=role,
1225
+ prompt=_specialist_prompt(role),
1226
+ status="cancelled",
1227
+ status_reason="Team run cancelled.",
1228
+ )
1229
+ _finish_specialist_thread(child_by_role.get(role, ""), block)
1230
+ if parent_id:
1231
+ try:
1232
+ agent_threads.update_thread(parent_id, status="cancelled", error="Team run cancelled.")
1233
+ except (OSError, ValueError, KeyError):
1234
+ pass
1235
+ show_error("Agent team cancelled.")
1236
+ return AgentRunResult(
1237
+ thread_id=parent_id,
1238
+ status="cancelled",
1239
+ pipeline="team",
1240
+ error="Team run cancelled.",
1241
+ children=child_ids,
1242
+ )
1243
+
1244
+ ordered = [specialists[role] for role in selected_roles if role in specialists]
1245
+ useful = [block for block in ordered if block.status in {"complete", "partial"} and block.output.strip()]
1246
+ if not useful:
1247
+ error = "All specialist threads failed; integration was not started."
1248
+ if parent_id:
1249
+ try:
1250
+ agent_threads.update_thread(parent_id, status="failed", error=error)
1251
+ except (OSError, ValueError, KeyError):
1252
+ pass
1253
+ show_error(error)
1254
+ return AgentRunResult(
1255
+ thread_id=parent_id,
1256
+ status="failed",
1257
+ pipeline="team",
1258
+ error=error,
1259
+ children=child_ids,
1260
+ blocks=[_block_record(block) for block in ordered],
1261
+ )
1262
+
1263
+ handoff_parts = []
1264
+ for block in ordered:
1265
+ handoff_parts.append(
1266
+ f"### Thread {child_by_role.get(block.role, '-')} · {block.role} · {block.status}\n"
1267
+ f"{block.context_output or block.output or '(no output)'}"
1268
+ )
1269
+ handoff = "\n\n".join(handoff_parts)
1270
+ integration_pipeline = (
1271
+ route.suggested_pipeline
1272
+ if route.task_type in {"coding", "research", "review"}
1273
+ else "research"
1274
+ )
1275
+ show_info(
1276
+ f"Agent team {parent_id or '(unrecorded)'}: specialists joined; "
1277
+ f"integrating through '{integration_pipeline}' with verification gates."
1278
+ )
1279
+ result = run_agent_pipeline(
1280
+ task,
1281
+ cfg,
1282
+ client,
1283
+ pipeline_name=integration_pipeline,
1284
+ thread_id=parent_id or None,
1285
+ prior_context=handoff,
1286
+ thread_pipeline_label=f"team:{integration_pipeline}",
1287
+ )
1288
+ result.children = child_ids
1289
+ return result
1290
+
1291
+
1292
+ def _thread_list_text(records: list[dict[str, Any]]) -> str:
1293
+ if not records:
1294
+ return "No agent threads recorded."
1295
+ lines = ["Agent threads:"]
1296
+ for record in records:
1297
+ parent = f" <- {record['parent_id']}" if record.get("parent_id") else ""
1298
+ lines.append(
1299
+ f"- {record['id']}{parent} [{record['status']}] {record['role']} · "
1300
+ f"{record['pipeline']} · {record['title']}"
1301
+ )
1302
+ return "\n".join(lines)
1303
+
1304
+
1305
+ def show_agent_threads() -> str:
1306
+ records = agent_threads.list_threads(limit=20)
1307
+ if not records:
1308
+ message = "No agent threads recorded. Run /agent TASK or /agent team TASK."
1309
+ show_info(message)
1310
+ return message
1311
+ table = Table(title="Agent Threads", box=box.SIMPLE, padding=(0, 1))
1312
+ table.add_column("ID", style="primary", no_wrap=True)
1313
+ table.add_column("Status", style="text")
1314
+ table.add_column("Role", style="muted")
1315
+ table.add_column("Pipeline", style="text")
1316
+ table.add_column("Task", style="text", overflow="fold")
1317
+ for record in records:
1318
+ table.add_row(
1319
+ record["id"],
1320
+ record["status"],
1321
+ record["role"],
1322
+ record["pipeline"],
1323
+ record["title"],
1324
+ )
1325
+ console.print(table)
1326
+ return _thread_list_text(records)
1327
+
1328
+
1329
+ def show_agent_thread(thread_ref: str) -> str:
1330
+ record = agent_threads.resolve_thread(thread_ref)
1331
+ table = Table(title=f"Agent Thread {record['id']}", box=box.SIMPLE, show_header=False, padding=(0, 1))
1332
+ table.add_column("Field", style="muted")
1333
+ table.add_column("Value", style="text", overflow="fold")
1334
+ for label, value in (
1335
+ ("status", record["status"]),
1336
+ ("role", record["role"]),
1337
+ ("pipeline", record["pipeline"]),
1338
+ ("model", record["model"] or "-"),
1339
+ ("parent", record["parent_id"] or "-"),
1340
+ ("children", ", ".join(record["children"]) or "-"),
1341
+ ("updated", record["updated_at"]),
1342
+ ("task", record["task"]),
1343
+ ("error", record["error"] or "-"),
1344
+ ):
1345
+ table.add_row(label, str(value))
1346
+ console.print(table)
1347
+ if record["output"]:
1348
+ console.print(Text(record["output"]))
1349
+ return agent_threads.context_handoff(record)
1350
+
1351
+
1352
+ def _pipeline_for_thread(record: dict[str, Any]) -> str:
1353
+ pipeline = str(record.get("pipeline") or "default")
1354
+ if pipeline.startswith("team:"):
1355
+ pipeline = pipeline.split(":", 1)[1]
1356
+ if pipeline in agent_blocks.pipeline_names():
1357
+ return pipeline
1358
+ route = task_router.route_task(str(record.get("task") or ""))
1359
+ return route.suggested_pipeline if route.recommended_mode == "agent" else "research"
1360
+
1361
+
1362
+ def _completed_agent_result_for_tool(
1363
+ result: AgentRunResult,
1364
+ *,
1365
+ task: str,
1366
+ cfg: Config,
1367
+ ) -> str:
1368
+ memory_result = memory_runtime.capture_completed_user_turn(
1369
+ cfg,
1370
+ task,
1371
+ completed=result.status == "complete",
1372
+ source="agent",
1373
+ )
1374
+ flush_perf_records()
1375
+ if memory_result.get("status") == "stored":
1376
+ show_info("Saved 1 durable memory automatically; review it with /memories.")
1377
+ return result.for_tool()
1378
+
1379
+
1380
+ def execute_agent_command(arg: str, cfg: Config, client: Any) -> str:
1381
+ """Execute `/agent` for either the TUI or a parent runtime model."""
1382
+
1383
+ text = (arg or "").strip()
1384
+ lowered = text.lower()
1385
+ if lowered in {"help", "--help", "-h", "?"}:
1386
+ message = agent_usage_text()
1387
+ show_info(message)
1388
+ return message
1389
+ if lowered == "init":
1390
+ try:
1391
+ path = agent_blocks.write_starter_config()
1392
+ except FileExistsError as exc:
1393
+ show_error(str(exc))
1394
+ return f"Error: {exc}"
1395
+ message = f"Wrote Agent Blocks starter config: {path}"
1396
+ show_info(message)
1397
+ return message
1398
+ if lowered in {"threads", "list", "status"}:
1399
+ return show_agent_threads()
1400
+ if lowered.startswith("show "):
1401
+ try:
1402
+ return show_agent_thread(text.split(maxsplit=1)[1])
1403
+ except KeyError as exc:
1404
+ message = str(exc).strip("'")
1405
+ show_error(message)
1406
+ return f"Error: {message}"
1407
+ if lowered == "show":
1408
+ show_error(AGENT_THREAD_USAGE)
1409
+ return f"Error: {AGENT_THREAD_USAGE}"
1410
+ if lowered.startswith("team") and (len(text) == 4 or text[4].isspace()):
1411
+ roles, task, error = parse_agent_team_invocation(text[4:].strip())
1412
+ if error:
1413
+ show_error(error)
1414
+ return f"Error: {error}"
1415
+ return _completed_agent_result_for_tool(
1416
+ run_agent_team(task, cfg, client, roles=roles or None),
1417
+ task=task,
1418
+ cfg=cfg,
1419
+ )
1420
+ for action in ("resume", "fork"):
1421
+ if lowered == action or lowered.startswith(f"{action} "):
1422
+ try:
1423
+ parts = shlex.split(text)
1424
+ except ValueError as exc:
1425
+ message = f"{AGENT_THREAD_USAGE} ({exc})"
1426
+ show_error(message)
1427
+ return f"Error: {message}"
1428
+ if len(parts) < 2 or (action == "fork" and len(parts) < 3):
1429
+ show_error(AGENT_THREAD_USAGE)
1430
+ return f"Error: {AGENT_THREAD_USAGE}"
1431
+ try:
1432
+ record = agent_threads.resolve_thread(parts[1])
1433
+ except KeyError as exc:
1434
+ message = str(exc).strip("'")
1435
+ show_error(message)
1436
+ return f"Error: {message}"
1437
+ task = " ".join(parts[2:]).strip() or "Continue from the latest verified state and finish remaining work."
1438
+ handoff = agent_threads.context_handoff(record)
1439
+ result = run_agent_pipeline(
1440
+ task,
1441
+ cfg,
1442
+ client,
1443
+ pipeline_name=_pipeline_for_thread(record),
1444
+ thread_id=record["id"] if action == "resume" else None,
1445
+ parent_id=record["id"] if action == "fork" else "",
1446
+ prior_context=handoff,
1447
+ )
1448
+ return _completed_agent_result_for_tool(result, task=task, cfg=cfg)
1449
+ pipeline_name, task, error = parse_agent_invocation_checked(text)
1450
+ if error:
1451
+ show_error(error)
1452
+ return f"Error: {error}"
1453
+ return _completed_agent_result_for_tool(
1454
+ run_agent_pipeline(task, cfg, client, pipeline_name=pipeline_name),
1455
+ task=task,
1456
+ cfg=cfg,
1457
+ )