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.
- algo_cli/__init__.py +3 -0
- algo_cli/__main__.py +7 -0
- algo_cli/_internal/__init__.py +12 -0
- algo_cli/_internal/policy_chain.py +259 -0
- algo_cli/action_registry.py +1047 -0
- algo_cli/agent_blocks.py +550 -0
- algo_cli/agent_pipeline.py +1457 -0
- algo_cli/agent_threads.py +308 -0
- algo_cli/animations.py +316 -0
- algo_cli/cache_admission.py +209 -0
- algo_cli/capability_mask.py +66 -0
- algo_cli/chat_protocol.py +116 -0
- algo_cli/chatgpt_auth.py +510 -0
- algo_cli/chatgpt_client.py +657 -0
- algo_cli/code_rag.py +479 -0
- algo_cli/config.py +651 -0
- algo_cli/context_budget.py +679 -0
- algo_cli/credential_helpers.py +315 -0
- algo_cli/deliberation.py +29 -0
- algo_cli/display.py +1470 -0
- algo_cli/evals/__init__.py +21 -0
- algo_cli/evals/algorithm_effectiveness.py +560 -0
- algo_cli/evals/competitive_harness_rating.py +702 -0
- algo_cli/evals/cot_quality.py +220 -0
- algo_cli/evals/harness_retrieval_benchmark.py +401 -0
- algo_cli/evals/performance_regression.py +136 -0
- algo_cli/evals/scorecard_grading.py +308 -0
- algo_cli/evals/session_distribution.py +84 -0
- algo_cli/execution_guardrails.py +806 -0
- algo_cli/extensions_manifest.py +84 -0
- algo_cli/git_evidence.py +227 -0
- algo_cli/google_workspace.py +407 -0
- algo_cli/google_workspace_auth.py +523 -0
- algo_cli/harness.py +2587 -0
- algo_cli/identity.py +557 -0
- algo_cli/index_compute_lab.py +228 -0
- algo_cli/inference_harness.py +70 -0
- algo_cli/intelligence/__init__.py +1103 -0
- algo_cli/intelligence/acrobat_config.py +307 -0
- algo_cli/intelligence/acrobat_manifests.py +338 -0
- algo_cli/intelligence/acrobat_models.py +195 -0
- algo_cli/intelligence/acrobat_pipeline.py +295 -0
- algo_cli/intelligence/acrobat_runtime.py +302 -0
- algo_cli/intelligence/acrobat_security.py +261 -0
- algo_cli/intelligence/acrobat_workflows.py +226 -0
- algo_cli/intelligence/actionability.py +165 -0
- algo_cli/intelligence/adversarial_audit.py +136 -0
- algo_cli/intelligence/agent_arena.py +92 -0
- algo_cli/intelligence/agent_benchmark.py +236 -0
- algo_cli/intelligence/agent_runtime.py +171 -0
- algo_cli/intelligence/agents_as_tools.py +70 -0
- algo_cli/intelligence/artifact_binding.py +80 -0
- algo_cli/intelligence/autonomous_engineer.py +1976 -0
- algo_cli/intelligence/backpressure.py +99 -0
- algo_cli/intelligence/bloom_filter.py +186 -0
- algo_cli/intelligence/bonferroni.py +66 -0
- algo_cli/intelligence/boundary_compaction.py +98 -0
- algo_cli/intelligence/catalog_verifier.py +172 -0
- algo_cli/intelligence/cavecrew.py +118 -0
- algo_cli/intelligence/changelog.py +176 -0
- algo_cli/intelligence/checkpoint_resume.py +92 -0
- algo_cli/intelligence/circuit_breaker.py +88 -0
- algo_cli/intelligence/clarification_gate.py +101 -0
- algo_cli/intelligence/code_graph.py +180 -0
- algo_cli/intelligence/coderank.py +97 -0
- algo_cli/intelligence/consistent_hash.py +150 -0
- algo_cli/intelligence/consortium_synthesis.py +139 -0
- algo_cli/intelligence/construction/__init__.py +241 -0
- algo_cli/intelligence/construction/common.py +273 -0
- algo_cli/intelligence/construction/documents.py +496 -0
- algo_cli/intelligence/construction/labor_units.py +1395 -0
- algo_cli/intelligence/construction/payments.py +470 -0
- algo_cli/intelligence/construction/risk.py +784 -0
- algo_cli/intelligence/content_extractor.py +132 -0
- algo_cli/intelligence/context_adaptive.py +102 -0
- algo_cli/intelligence/context_ops.py +95 -0
- algo_cli/intelligence/count_min.py +145 -0
- algo_cli/intelligence/cow_state.py +103 -0
- algo_cli/intelligence/critic_loop.py +119 -0
- algo_cli/intelligence/cross_source.py +113 -0
- algo_cli/intelligence/daemon_mode.py +99 -0
- algo_cli/intelligence/dag_orchestration.py +151 -0
- algo_cli/intelligence/deep_research.py +155 -0
- algo_cli/intelligence/degenerate_detector.py +78 -0
- algo_cli/intelligence/delta_report.py +92 -0
- algo_cli/intelligence/discovery_event_log.py +92 -0
- algo_cli/intelligence/document_ingest.py +298 -0
- algo_cli/intelligence/dual_layer_validate.py +151 -0
- algo_cli/intelligence/echo_fidelity.py +73 -0
- algo_cli/intelligence/ema_tuning.py +104 -0
- algo_cli/intelligence/event_log.py +92 -0
- algo_cli/intelligence/evidence_graph.py +114 -0
- algo_cli/intelligence/extension_host.py +162 -0
- algo_cli/intelligence/extension_manifest.py +115 -0
- algo_cli/intelligence/falsification_suite.py +178 -0
- algo_cli/intelligence/finance/__init__.py +169 -0
- algo_cli/intelligence/finance/anomalies.py +135 -0
- algo_cli/intelligence/finance/ap_ar.py +351 -0
- algo_cli/intelligence/finance/cash.py +162 -0
- algo_cli/intelligence/finance/close.py +332 -0
- algo_cli/intelligence/finance/common.py +244 -0
- algo_cli/intelligence/finance/construction.py +135 -0
- algo_cli/intelligence/finance/controls.py +172 -0
- algo_cli/intelligence/finance/evidence.py +119 -0
- algo_cli/intelligence/finance/exceptions.py +157 -0
- algo_cli/intelligence/finance/reconciliations.py +254 -0
- algo_cli/intelligence/finance/revenue.py +109 -0
- algo_cli/intelligence/finance/tax.py +74 -0
- algo_cli/intelligence/finance/workpapers.py +111 -0
- algo_cli/intelligence/finding_record.py +120 -0
- algo_cli/intelligence/flow_dag.py +267 -0
- algo_cli/intelligence/gatherer.py +223 -0
- algo_cli/intelligence/golden_master.py +98 -0
- algo_cli/intelligence/graph_rag.py +195 -0
- algo_cli/intelligence/group_chat.py +143 -0
- algo_cli/intelligence/hash_dedup.py +145 -0
- algo_cli/intelligence/hyperloglog.py +128 -0
- algo_cli/intelligence/incremental_index.py +316 -0
- algo_cli/intelligence/index_store.py +16 -0
- algo_cli/intelligence/iteration_plan.py +133 -0
- algo_cli/intelligence/kernel_plugins.py +167 -0
- algo_cli/intelligence/lesson_catalog.py +135 -0
- algo_cli/intelligence/llm_fallback.py +169 -0
- algo_cli/intelligence/log2_histogram.py +267 -0
- algo_cli/intelligence/lsp_integration.py +147 -0
- algo_cli/intelligence/memory_evolution.py +117 -0
- algo_cli/intelligence/minhash_lsh.py +182 -0
- algo_cli/intelligence/multi_model_score.py +174 -0
- algo_cli/intelligence/multi_tier_grade.py +211 -0
- algo_cli/intelligence/negative_controls.py +113 -0
- algo_cli/intelligence/numeric_clamp.py +63 -0
- algo_cli/intelligence/occ_editor.py +66 -0
- algo_cli/intelligence/output_normalize.py +112 -0
- algo_cli/intelligence/parallel_delegation.py +98 -0
- algo_cli/intelligence/parallel_fanout.py +104 -0
- algo_cli/intelligence/permission_modes.py +105 -0
- algo_cli/intelligence/pre_push_gate.py +68 -0
- algo_cli/intelligence/prefetch.py +171 -0
- algo_cli/intelligence/process_framework.py +217 -0
- algo_cli/intelligence/project_graph.py +387 -0
- algo_cli/intelligence/query_expansion.py +146 -0
- algo_cli/intelligence/ralph_loop.py +117 -0
- algo_cli/intelligence/rate_limiter.py +153 -0
- algo_cli/intelligence/refactor_transaction.py +94 -0
- algo_cli/intelligence/research_workspace.py +108 -0
- algo_cli/intelligence/retraction_ledger.py +72 -0
- algo_cli/intelligence/saga_pattern.py +88 -0
- algo_cli/intelligence/session_fork.py +100 -0
- algo_cli/intelligence/shadow_editor.py +67 -0
- algo_cli/intelligence/shell_session.py +213 -0
- algo_cli/intelligence/source_registry.py +143 -0
- algo_cli/intelligence/spawn_scales.py +99 -0
- algo_cli/intelligence/stat_stability.py +104 -0
- algo_cli/intelligence/structural_validator.py +148 -0
- algo_cli/intelligence/subagent_spawner.py +111 -0
- algo_cli/intelligence/symmetric_verify.py +70 -0
- algo_cli/intelligence/task_classifier.py +129 -0
- algo_cli/intelligence/team_execution.py +122 -0
- algo_cli/intelligence/tiered_access.py +121 -0
- algo_cli/intelligence/utility_registry.py +159 -0
- algo_cli/intuition_engine.py +560 -0
- algo_cli/intuition_injector.py +82 -0
- algo_cli/kernels/__init__.py +5 -0
- algo_cli/kernels/manifest.py +763 -0
- algo_cli/main.py +3903 -0
- algo_cli/memory_candidates.py +541 -0
- algo_cli/memory_echo_veil.py +394 -0
- algo_cli/memory_runtime.py +112 -0
- algo_cli/model_info.py +548 -0
- algo_cli/model_profile.py +160 -0
- algo_cli/model_routing.py +74 -0
- algo_cli/oneshot.py +331 -0
- algo_cli/perf_telemetry.py +389 -0
- algo_cli/plugins.py +245 -0
- algo_cli/private_event_store.py +654 -0
- algo_cli/quantization/__init__.py +24 -0
- algo_cli/quantization/lloyd_max.py +98 -0
- algo_cli/quantization/turbo_quant.py +308 -0
- algo_cli/reasoning/__init__.py +46 -0
- algo_cli/reasoning/combinatorial.py +356 -0
- algo_cli/reasoning/graph_of_thought.py +297 -0
- algo_cli/reasoning/mcts.py +220 -0
- algo_cli/reasoning/neuro_symbolic.py +250 -0
- algo_cli/reasoning/react.py +246 -0
- algo_cli/reasoning/reflexion.py +225 -0
- algo_cli/reasoning/tree_of_thought.py +241 -0
- algo_cli/reasoning_bridge.py +150 -0
- algo_cli/reconciliation.py +284 -0
- algo_cli/reflex.py +385 -0
- algo_cli/resources/docs/ALGO.md +13958 -0
- algo_cli/resources/docs/algo-cli-algorithm-evidence-contract.md +60 -0
- algo_cli/resources/docs/algo-cli-execution-verification-contract.md +59 -0
- algo_cli/resources/docs/algo-cli-memory-lifecycle-contract.md +72 -0
- algo_cli/resources/docs/harness-extension-cleanup-recommendation.md +41 -0
- algo_cli/resources/docs/index-compute-lab-integration.md +32 -0
- algo_cli/resources/docs/inference-harness-loop-blueprint-2026-06.md +55 -0
- algo_cli/resources/docs/main-split-map.md +35 -0
- algo_cli/resources/docs/privacy-and-context.md +48 -0
- algo_cli/resources/docs/reflex-loop-v0.2.md +354 -0
- algo_cli/resources/skills/README.md +26 -0
- algo_cli/resources/skills/algo-cli.md +59 -0
- algo_cli/resources/skills/edit-file-precision.md +49 -0
- algo_cli/resources/skills/harness-search-first.md +47 -0
- algo_cli/resources/skills/memory-recall-ritual.md +51 -0
- algo_cli/resources/skills/qol-algorithms.md +224 -0
- algo_cli/resources/skills/smart-error-recovery.md +56 -0
- algo_cli/resources/skills/tool-selection-cheatsheet.md +65 -0
- algo_cli/retrieval_algorithms.py +127 -0
- algo_cli/runtime_qos.py +236 -0
- algo_cli/runtime_services.py +320 -0
- algo_cli/session_commands.py +95 -0
- algo_cli/session_mode.py +113 -0
- algo_cli/skills.py +430 -0
- algo_cli/slash_dispatch.py +1265 -0
- algo_cli/small_context.py +206 -0
- algo_cli/spawn_budget.py +89 -0
- algo_cli/task_ledger.py +84 -0
- algo_cli/task_router.py +197 -0
- algo_cli/tool_context.py +94 -0
- algo_cli/tool_contract.py +99 -0
- algo_cli/tool_policy.py +357 -0
- algo_cli/tool_runtime.py +647 -0
- algo_cli/tools.py +3056 -0
- algo_cli/url_scheme.py +174 -0
- algo_cli/verify.py +154 -0
- algo_cli/version_manifest.py +178 -0
- algo_cli/vision_screenshot_verify.py +76 -0
- algo_cli/workspace_resolver.py +68 -0
- algo_cli/x_account.py +209 -0
- algo_cli/xai_auth.py +374 -0
- algo_cli/xai_client.py +600 -0
- algo_cli_runtime-0.14.0.dist-info/METADATA +369 -0
- algo_cli_runtime-0.14.0.dist-info/RECORD +237 -0
- algo_cli_runtime-0.14.0.dist-info/WHEEL +4 -0
- algo_cli_runtime-0.14.0.dist-info/entry_points.txt +3 -0
- algo_cli_runtime-0.14.0.dist-info/licenses/LICENSE +21 -0
- 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
|
+
)
|