mycode-coding-agent 0.1.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.
- mycode/__init__.py +0 -0
- mycode/adapters/__init__.py +21 -0
- mycode/adapters/jsonl.py +692 -0
- mycode/agent/__init__.py +25 -0
- mycode/agent/events.py +111 -0
- mycode/agent/outcome.py +103 -0
- mycode/agent/progress.py +373 -0
- mycode/agent/runner.py +1481 -0
- mycode/application/__init__.py +38 -0
- mycode/application/agent_session.py +367 -0
- mycode/application/events.py +59 -0
- mycode/application/runtime.py +211 -0
- mycode/application/sessions.py +180 -0
- mycode/cli.py +840 -0
- mycode/config.py +355 -0
- mycode/context/__init__.py +1 -0
- mycode/context/artifacts.py +672 -0
- mycode/context/budget.py +752 -0
- mycode/context/builder.py +112 -0
- mycode/context/compact.py +795 -0
- mycode/context/tool_result_format.py +199 -0
- mycode/context/tool_result_retention.py +261 -0
- mycode/conversation.py +78 -0
- mycode/error_handling.py +481 -0
- mycode/event_format.py +147 -0
- mycode/instructions.py +285 -0
- mycode/llm.py +771 -0
- mycode/mcp/__init__.py +41 -0
- mycode/mcp/client.py +44 -0
- mycode/mcp/config.py +207 -0
- mycode/mcp/errors.py +302 -0
- mycode/mcp/manager.py +339 -0
- mycode/mcp/models.py +20 -0
- mycode/mcp/result_adapter.py +58 -0
- mycode/mcp/tool_adapter.py +145 -0
- mycode/mcp/trust.py +313 -0
- mycode/memory.py +570 -0
- mycode/memory_context.py +245 -0
- mycode/messages.py +63 -0
- mycode/observability.py +28 -0
- mycode/permissions.py +262 -0
- mycode/persistence/__init__.py +1 -0
- mycode/persistence/filesystem.py +291 -0
- mycode/persistence/project_storage.py +208 -0
- mycode/persistence/session_lock.py +138 -0
- mycode/persistence/session_store.py +503 -0
- mycode/presentation/__init__.py +1 -0
- mycode/presentation/cli/__init__.py +14 -0
- mycode/presentation/cli/confirmer.py +116 -0
- mycode/presentation/cli/mcp_trust.py +61 -0
- mycode/presentation/cli/presenter.py +320 -0
- mycode/presentation/cli/session_menu.py +146 -0
- mycode/presentation/cli/subagent_observer.py +124 -0
- mycode/presentation/command_format.py +90 -0
- mycode/presentation/commands.py +95 -0
- mycode/presentation/tui/__init__.py +6 -0
- mycode/presentation/tui/app.py +1351 -0
- mycode/presentation/tui/interactions.py +253 -0
- mycode/presentation/tui/presenter.py +266 -0
- mycode/presentation/tui/screens.py +305 -0
- mycode/presentation/tui/widgets.py +214 -0
- mycode/project.py +22 -0
- mycode/prompts.py +181 -0
- mycode/reasoning.py +40 -0
- mycode/session.py +86 -0
- mycode/skills/__init__.py +27 -0
- mycode/skills/builtin/database-recovery/SKILL.md +138 -0
- mycode/skills/builtin/database-recovery/references/sqlite.md +235 -0
- mycode/skills/registry.py +295 -0
- mycode/skills/state.py +68 -0
- mycode/subagents/__init__.py +1 -0
- mycode/subagents/audit.py +212 -0
- mycode/subagents/concurrency.py +124 -0
- mycode/subagents/contracts.py +421 -0
- mycode/subagents/delegate.py +80 -0
- mycode/subagents/delegation.py +128 -0
- mycode/subagents/lifecycle.py +86 -0
- mycode/subagents/limits.py +7 -0
- mycode/subagents/observability.py +150 -0
- mycode/subagents/persistence.py +152 -0
- mycode/subagents/profiles.py +184 -0
- mycode/subagents/prompts.py +67 -0
- mycode/subagents/results.py +178 -0
- mycode/subagents/runtime.py +528 -0
- mycode/subagents/snapshots.py +211 -0
- mycode/subagents/tool_batch.py +260 -0
- mycode/tools/__init__.py +81 -0
- mycode/tools/base.py +222 -0
- mycode/tools/bounds.py +14 -0
- mycode/tools/command_executor.py +167 -0
- mycode/tools/command_output.py +166 -0
- mycode/tools/command_risk.py +596 -0
- mycode/tools/defaults.py +59 -0
- mycode/tools/edit_file.py +524 -0
- mycode/tools/file_mutation.py +30 -0
- mycode/tools/glob.py +247 -0
- mycode/tools/grep.py +324 -0
- mycode/tools/ignore.py +122 -0
- mycode/tools/inspect_changes.py +269 -0
- mycode/tools/load_skill.py +92 -0
- mycode/tools/memory.py +264 -0
- mycode/tools/path_permissions.py +78 -0
- mycode/tools/patterns.py +48 -0
- mycode/tools/permission_metadata.py +27 -0
- mycode/tools/process_tree.py +166 -0
- mycode/tools/read_file.py +242 -0
- mycode/tools/read_skill_resource.py +93 -0
- mycode/tools/registry.py +279 -0
- mycode/tools/run_command.py +237 -0
- mycode/tools/run_skill_script.py +206 -0
- mycode/tools/run_validation.py +107 -0
- mycode/tools/submit_result.py +93 -0
- mycode/tools/text.py +15 -0
- mycode/tools/validation_command.py +377 -0
- mycode/tools/workspace.py +33 -0
- mycode/tools/write_file.py +169 -0
- mycode_coding_agent-0.1.0.dist-info/METADATA +244 -0
- mycode_coding_agent-0.1.0.dist-info/RECORD +121 -0
- mycode_coding_agent-0.1.0.dist-info/WHEEL +4 -0
- mycode_coding_agent-0.1.0.dist-info/entry_points.txt +2 -0
- mycode_coding_agent-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
from collections.abc import Sequence
|
|
2
|
+
|
|
3
|
+
from mycode.subagents.contracts import (
|
|
4
|
+
BoundedResultArgs,
|
|
5
|
+
ExplorerResult,
|
|
6
|
+
ReviewerResult,
|
|
7
|
+
SubAgentPayload,
|
|
8
|
+
SubAgentRole,
|
|
9
|
+
TesterReport,
|
|
10
|
+
TesterResult,
|
|
11
|
+
ValidationExecution,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
DEFAULT_MAX_FINAL_PAYLOAD_CHARS = 12000
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ResultCompressionError(ValueError):
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def finalize_submitted_payload(
|
|
23
|
+
role: SubAgentRole,
|
|
24
|
+
submitted: BoundedResultArgs,
|
|
25
|
+
*,
|
|
26
|
+
validation_executions: Sequence[ValidationExecution] = (),
|
|
27
|
+
max_chars: int = DEFAULT_MAX_FINAL_PAYLOAD_CHARS,
|
|
28
|
+
) -> SubAgentPayload:
|
|
29
|
+
if max_chars < 1:
|
|
30
|
+
raise ValueError("max_chars must be at least 1.")
|
|
31
|
+
|
|
32
|
+
if role == "explorer" and isinstance(submitted, ExplorerResult):
|
|
33
|
+
payload: SubAgentPayload = submitted
|
|
34
|
+
elif role == "tester" and isinstance(submitted, TesterReport):
|
|
35
|
+
payload = TesterResult(
|
|
36
|
+
status=submitted.status,
|
|
37
|
+
summary=submitted.summary,
|
|
38
|
+
executions=list(validation_executions),
|
|
39
|
+
failure_summary=submitted.failure_summary,
|
|
40
|
+
blocked_reason=submitted.blocked_reason,
|
|
41
|
+
uncertainties=submitted.uncertainties,
|
|
42
|
+
truncated=submitted.truncated,
|
|
43
|
+
omitted_count=submitted.omitted_count,
|
|
44
|
+
)
|
|
45
|
+
elif role == "reviewer" and isinstance(submitted, ReviewerResult):
|
|
46
|
+
payload = submitted
|
|
47
|
+
else:
|
|
48
|
+
raise ValueError(f"Submitted result does not match SubAgent role: {role}")
|
|
49
|
+
|
|
50
|
+
return compress_subagent_payload(payload, max_chars=max_chars)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def compress_subagent_payload(
|
|
54
|
+
payload: SubAgentPayload,
|
|
55
|
+
*,
|
|
56
|
+
max_chars: int = DEFAULT_MAX_FINAL_PAYLOAD_CHARS,
|
|
57
|
+
) -> SubAgentPayload:
|
|
58
|
+
if max_chars < 1:
|
|
59
|
+
raise ValueError("max_chars must be at least 1.")
|
|
60
|
+
if _serialized_chars(payload) <= max_chars:
|
|
61
|
+
return payload
|
|
62
|
+
|
|
63
|
+
candidate = _drop_uncertainties(payload, max_chars=max_chars)
|
|
64
|
+
if _serialized_chars(candidate) <= max_chars:
|
|
65
|
+
return candidate
|
|
66
|
+
|
|
67
|
+
if isinstance(candidate, ExplorerResult):
|
|
68
|
+
candidate = _compress_explorer(candidate, max_chars=max_chars)
|
|
69
|
+
elif isinstance(candidate, ReviewerResult):
|
|
70
|
+
candidate = _compress_reviewer(candidate, max_chars=max_chars)
|
|
71
|
+
else:
|
|
72
|
+
candidate = _compress_tester(candidate, max_chars=max_chars)
|
|
73
|
+
|
|
74
|
+
if _serialized_chars(candidate) > max_chars:
|
|
75
|
+
raise ResultCompressionError(
|
|
76
|
+
"Structured result cannot fit the final payload budget without "
|
|
77
|
+
"removing protected summary, location, command, exit-status, or "
|
|
78
|
+
"error fields."
|
|
79
|
+
)
|
|
80
|
+
return candidate
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _drop_uncertainties(
|
|
84
|
+
payload: SubAgentPayload,
|
|
85
|
+
*,
|
|
86
|
+
max_chars: int,
|
|
87
|
+
) -> SubAgentPayload:
|
|
88
|
+
candidate = payload
|
|
89
|
+
while candidate.uncertainties and _serialized_chars(candidate) > max_chars:
|
|
90
|
+
candidate = _copy_with_omissions(
|
|
91
|
+
candidate,
|
|
92
|
+
uncertainties=candidate.uncertainties[:-1],
|
|
93
|
+
)
|
|
94
|
+
return candidate
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _compress_explorer(
|
|
98
|
+
payload: ExplorerResult,
|
|
99
|
+
*,
|
|
100
|
+
max_chars: int,
|
|
101
|
+
) -> ExplorerResult:
|
|
102
|
+
candidate = payload
|
|
103
|
+
minimum_findings = 1 if candidate.findings else 0
|
|
104
|
+
while (
|
|
105
|
+
len(candidate.findings) > minimum_findings
|
|
106
|
+
and _serialized_chars(candidate) > max_chars
|
|
107
|
+
):
|
|
108
|
+
candidate = _copy_with_omissions(
|
|
109
|
+
candidate,
|
|
110
|
+
findings=candidate.findings[:-1],
|
|
111
|
+
)
|
|
112
|
+
return candidate
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _compress_reviewer(
|
|
116
|
+
payload: ReviewerResult,
|
|
117
|
+
*,
|
|
118
|
+
max_chars: int,
|
|
119
|
+
) -> ReviewerResult:
|
|
120
|
+
candidate = payload
|
|
121
|
+
minimum_findings = 1 if candidate.findings else 0
|
|
122
|
+
severity_priority = {"critical": 0, "high": 1, "medium": 2, "low": 3}
|
|
123
|
+
while (
|
|
124
|
+
len(candidate.findings) > minimum_findings
|
|
125
|
+
and _serialized_chars(candidate) > max_chars
|
|
126
|
+
):
|
|
127
|
+
removable_index = max(
|
|
128
|
+
range(len(candidate.findings)),
|
|
129
|
+
key=lambda index: (severity_priority[candidate.findings[index].severity], index),
|
|
130
|
+
)
|
|
131
|
+
findings = list(candidate.findings)
|
|
132
|
+
del findings[removable_index]
|
|
133
|
+
candidate = _copy_with_omissions(candidate, findings=findings)
|
|
134
|
+
return candidate
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _compress_tester(
|
|
138
|
+
payload: TesterResult,
|
|
139
|
+
*,
|
|
140
|
+
max_chars: int,
|
|
141
|
+
) -> TesterResult:
|
|
142
|
+
candidate = payload
|
|
143
|
+
while len(candidate.executions) > 1 and _serialized_chars(candidate) > max_chars:
|
|
144
|
+
removable_index = _tester_execution_to_remove(candidate)
|
|
145
|
+
executions = list(candidate.executions)
|
|
146
|
+
del executions[removable_index]
|
|
147
|
+
candidate = _copy_with_omissions(candidate, executions=executions)
|
|
148
|
+
return candidate
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _tester_execution_to_remove(payload: TesterResult) -> int:
|
|
152
|
+
failed_indexes = [
|
|
153
|
+
index
|
|
154
|
+
for index, execution in enumerate(payload.executions)
|
|
155
|
+
if execution.timed_out
|
|
156
|
+
or execution.exit_code is None
|
|
157
|
+
or execution.exit_code != 0
|
|
158
|
+
]
|
|
159
|
+
successful_indexes = [
|
|
160
|
+
index for index in range(len(payload.executions)) if index not in failed_indexes
|
|
161
|
+
]
|
|
162
|
+
if payload.status in {"failed", "blocked"} and successful_indexes:
|
|
163
|
+
return successful_indexes[-1]
|
|
164
|
+
if payload.status == "passed":
|
|
165
|
+
return successful_indexes[-1]
|
|
166
|
+
return len(payload.executions) - 1
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _copy_with_omissions(payload, **updates):
|
|
170
|
+
data = payload.model_dump()
|
|
171
|
+
data.update(updates)
|
|
172
|
+
data["truncated"] = True
|
|
173
|
+
data["omitted_count"] = payload.omitted_count + 1
|
|
174
|
+
return type(payload).model_validate(data)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _serialized_chars(payload: SubAgentPayload) -> int:
|
|
178
|
+
return len(payload.model_dump_json())
|
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
from collections.abc import Callable, Iterator
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
from datetime import UTC, datetime
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from tempfile import TemporaryDirectory
|
|
8
|
+
|
|
9
|
+
from mycode.context.artifacts import ToolResultArtifactStore
|
|
10
|
+
from uuid import uuid4
|
|
11
|
+
|
|
12
|
+
from mycode.agent.events import AgentEvent, AgentModelResponse
|
|
13
|
+
from mycode.context.budget import ContextBudget, MemoryContextStats, TokenEstimator, TokenUsage
|
|
14
|
+
from mycode.conversation import Conversation
|
|
15
|
+
from mycode.instructions import InstructionBundle, load_instruction_bundle
|
|
16
|
+
from mycode.llm import LLMClient
|
|
17
|
+
from mycode.memory import MemoryStore
|
|
18
|
+
from mycode.memory_context import MemoryContextSelector, MemoryRecallPolicy
|
|
19
|
+
from mycode.messages import Message
|
|
20
|
+
from mycode.observability import ObservationSink
|
|
21
|
+
from mycode.permissions import Confirmer, RejectingConfirmer
|
|
22
|
+
from mycode.agent.runner import (
|
|
23
|
+
DEFAULT_REPEATED_TOOL_CALL_LIMIT,
|
|
24
|
+
AgentRunner,
|
|
25
|
+
)
|
|
26
|
+
from mycode.subagents.contracts import (
|
|
27
|
+
SubAgentEnvelopeStatus,
|
|
28
|
+
SubAgentResult,
|
|
29
|
+
SubAgentStopReason,
|
|
30
|
+
SubAgentTask,
|
|
31
|
+
)
|
|
32
|
+
from mycode.subagents.concurrency import SubAgentInteractionGate
|
|
33
|
+
from mycode.subagents.lifecycle import (
|
|
34
|
+
RunTracker,
|
|
35
|
+
StateTransitionHandler,
|
|
36
|
+
SubAgentStateTransition,
|
|
37
|
+
TrackingConfirmer,
|
|
38
|
+
)
|
|
39
|
+
from mycode.subagents.limits import MAX_VALIDATION_EXECUTIONS
|
|
40
|
+
from mycode.subagents.observability import (
|
|
41
|
+
SubAgentObserver,
|
|
42
|
+
SynchronizedSubAgentObserver,
|
|
43
|
+
)
|
|
44
|
+
from mycode.subagents.profiles import create_subagent_tool_registry, get_agent_profile
|
|
45
|
+
from mycode.subagents.prompts import build_subagent_system_prompt
|
|
46
|
+
from mycode.subagents.results import (
|
|
47
|
+
DEFAULT_MAX_FINAL_PAYLOAD_CHARS,
|
|
48
|
+
)
|
|
49
|
+
from mycode.subagents.snapshots import (
|
|
50
|
+
FrozenMemoryRecallProvider,
|
|
51
|
+
SubAgentSnapshotMetadata,
|
|
52
|
+
create_runtime_context_snapshot,
|
|
53
|
+
)
|
|
54
|
+
from mycode.subagents.tool_batch import SubAgentToolBatchHandler
|
|
55
|
+
from mycode.tools.workspace import Workspace
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
DEFAULT_MAX_VALIDATION_CALLS = 20
|
|
59
|
+
DEFAULT_SUBAGENT_MAX_TURNS = 20
|
|
60
|
+
DEFAULT_SUBAGENT_NEAR_LIMIT_REMAINING_TURNS = 3
|
|
61
|
+
MAX_RUNTIME_ERROR_CHARS = 2000
|
|
62
|
+
|
|
63
|
+
InstructionLoader = Callable[[Path, Path], InstructionBundle]
|
|
64
|
+
LLMClientFactory = Callable[[], LLMClient]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass(frozen=True)
|
|
68
|
+
class SubAgentModelContextStats:
|
|
69
|
+
estimated_input_tokens: int
|
|
70
|
+
max_input_tokens: int
|
|
71
|
+
selected_message_count: int
|
|
72
|
+
original_message_count: int
|
|
73
|
+
compressed_tool_result_count: int
|
|
74
|
+
memory_stats: MemoryContextStats | None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass(frozen=True)
|
|
78
|
+
class SubAgentExecution:
|
|
79
|
+
result: SubAgentResult
|
|
80
|
+
transitions: tuple[SubAgentStateTransition, ...]
|
|
81
|
+
snapshot: SubAgentSnapshotMetadata | None
|
|
82
|
+
context: SubAgentModelContextStats | None
|
|
83
|
+
token_usage: TokenUsage | None
|
|
84
|
+
conversation_message_count: int
|
|
85
|
+
tool_call_count: int
|
|
86
|
+
validation_execution_count: int
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass(frozen=True)
|
|
90
|
+
class SubAgentRuntime:
|
|
91
|
+
workspace: Workspace
|
|
92
|
+
llm_client_factory: LLMClientFactory
|
|
93
|
+
confirmer: Confirmer | None = None
|
|
94
|
+
memory_store: MemoryStore | None = None
|
|
95
|
+
memory_recall_policy: MemoryRecallPolicy = field(default_factory=MemoryRecallPolicy)
|
|
96
|
+
context_budget: ContextBudget = field(default_factory=ContextBudget)
|
|
97
|
+
observability_sink: ObservationSink | None = field(default=None, repr=False)
|
|
98
|
+
max_turns: int = DEFAULT_SUBAGENT_MAX_TURNS
|
|
99
|
+
near_limit_remaining_turns: int | None = (
|
|
100
|
+
DEFAULT_SUBAGENT_NEAR_LIMIT_REMAINING_TURNS
|
|
101
|
+
)
|
|
102
|
+
repeated_tool_call_limit: int = DEFAULT_REPEATED_TOOL_CALL_LIMIT
|
|
103
|
+
max_final_payload_chars: int = DEFAULT_MAX_FINAL_PAYLOAD_CHARS
|
|
104
|
+
max_validation_calls: int = DEFAULT_MAX_VALIDATION_CALLS
|
|
105
|
+
working_directory: Path | None = None
|
|
106
|
+
instruction_loader: InstructionLoader = field(
|
|
107
|
+
default=lambda root, working: load_instruction_bundle(
|
|
108
|
+
root,
|
|
109
|
+
working_directory=working,
|
|
110
|
+
)
|
|
111
|
+
)
|
|
112
|
+
clock: Callable[[], datetime] = field(default=lambda: datetime.now(UTC))
|
|
113
|
+
run_id_factory: Callable[[], str] = field(default=lambda: uuid4().hex)
|
|
114
|
+
interaction_gate: SubAgentInteractionGate = field(
|
|
115
|
+
default_factory=SubAgentInteractionGate,
|
|
116
|
+
repr=False,
|
|
117
|
+
compare=False,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
def __post_init__(self) -> None:
|
|
121
|
+
if self.max_turns < 1:
|
|
122
|
+
raise ValueError("max_turns must be at least 1.")
|
|
123
|
+
if self.near_limit_remaining_turns is not None and not (
|
|
124
|
+
0 < self.near_limit_remaining_turns < self.max_turns
|
|
125
|
+
):
|
|
126
|
+
raise ValueError(
|
|
127
|
+
"near_limit_remaining_turns must be greater than 0 and less than "
|
|
128
|
+
"max_turns."
|
|
129
|
+
)
|
|
130
|
+
if self.repeated_tool_call_limit < 1:
|
|
131
|
+
raise ValueError("repeated_tool_call_limit must be at least 1.")
|
|
132
|
+
if self.max_final_payload_chars < 1:
|
|
133
|
+
raise ValueError("max_final_payload_chars must be at least 1.")
|
|
134
|
+
if self.max_validation_calls < 1:
|
|
135
|
+
raise ValueError("max_validation_calls must be at least 1.")
|
|
136
|
+
if self.max_validation_calls > MAX_VALIDATION_EXECUTIONS:
|
|
137
|
+
raise ValueError(
|
|
138
|
+
"max_validation_calls must not exceed "
|
|
139
|
+
f"{MAX_VALIDATION_EXECUTIONS}."
|
|
140
|
+
)
|
|
141
|
+
if (
|
|
142
|
+
self.memory_store is not None
|
|
143
|
+
and self.memory_store.project.workspace_root != self.workspace.root
|
|
144
|
+
):
|
|
145
|
+
raise ValueError(
|
|
146
|
+
"memory_store project must match the SubAgent workspace."
|
|
147
|
+
)
|
|
148
|
+
working_directory = self._resolved_working_directory()
|
|
149
|
+
if not working_directory.is_relative_to(self.workspace.root):
|
|
150
|
+
raise ValueError("working_directory must be inside the workspace.")
|
|
151
|
+
|
|
152
|
+
def execute(
|
|
153
|
+
self,
|
|
154
|
+
task: SubAgentTask,
|
|
155
|
+
*,
|
|
156
|
+
on_state_transition: StateTransitionHandler | None = None,
|
|
157
|
+
observer: SubAgentObserver | None = None,
|
|
158
|
+
) -> SubAgentExecution:
|
|
159
|
+
run_observer = (
|
|
160
|
+
None
|
|
161
|
+
if observer is None
|
|
162
|
+
else SynchronizedSubAgentObserver(
|
|
163
|
+
observer,
|
|
164
|
+
self.interaction_gate,
|
|
165
|
+
)
|
|
166
|
+
)
|
|
167
|
+
run_state_transition = (
|
|
168
|
+
None
|
|
169
|
+
if on_state_transition is None
|
|
170
|
+
else lambda transition: self.interaction_gate.run(
|
|
171
|
+
lambda: on_state_transition(transition)
|
|
172
|
+
)
|
|
173
|
+
)
|
|
174
|
+
run_id = self.interaction_gate.run(self.run_id_factory)
|
|
175
|
+
tracker = RunTracker(
|
|
176
|
+
run_id=run_id,
|
|
177
|
+
role=task.role,
|
|
178
|
+
clock=self._now,
|
|
179
|
+
handler=_state_transition_handler(
|
|
180
|
+
task,
|
|
181
|
+
on_state_transition=run_state_transition,
|
|
182
|
+
observer=run_observer,
|
|
183
|
+
),
|
|
184
|
+
)
|
|
185
|
+
snapshot_metadata: SubAgentSnapshotMetadata | None = None
|
|
186
|
+
runner: AgentRunner | None = None
|
|
187
|
+
batch_handler: SubAgentToolBatchHandler | None = None
|
|
188
|
+
artifact_directory: TemporaryDirectory | None = None
|
|
189
|
+
|
|
190
|
+
try:
|
|
191
|
+
tracker.transition("running", "run_started")
|
|
192
|
+
instruction_bundle = self.instruction_loader(
|
|
193
|
+
self.workspace.root,
|
|
194
|
+
self._resolved_working_directory(),
|
|
195
|
+
)
|
|
196
|
+
memory_recall = None
|
|
197
|
+
if self.memory_store is not None:
|
|
198
|
+
memory_recall = MemoryContextSelector(
|
|
199
|
+
self.memory_store,
|
|
200
|
+
policy=self.memory_recall_policy,
|
|
201
|
+
token_estimator=TokenEstimator(),
|
|
202
|
+
).recall(_memory_query(task))
|
|
203
|
+
runtime_snapshot = create_runtime_context_snapshot(
|
|
204
|
+
instruction_bundle,
|
|
205
|
+
memory_recall=memory_recall,
|
|
206
|
+
loaded_at=self._now(),
|
|
207
|
+
)
|
|
208
|
+
snapshot_metadata = runtime_snapshot.metadata
|
|
209
|
+
if run_observer is not None:
|
|
210
|
+
run_observer.on_snapshot(
|
|
211
|
+
task,
|
|
212
|
+
run_id,
|
|
213
|
+
snapshot_metadata,
|
|
214
|
+
snapshot_metadata.loaded_at,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
profile = get_agent_profile(task.role)
|
|
218
|
+
batch_handler = SubAgentToolBatchHandler(
|
|
219
|
+
role=task.role,
|
|
220
|
+
max_final_payload_chars=self.max_final_payload_chars,
|
|
221
|
+
max_validation_calls=self.max_validation_calls,
|
|
222
|
+
audit_handler=(
|
|
223
|
+
None
|
|
224
|
+
if run_observer is None
|
|
225
|
+
else lambda audit: run_observer.on_tool_audit(
|
|
226
|
+
task,
|
|
227
|
+
run_id,
|
|
228
|
+
audit,
|
|
229
|
+
self._now(),
|
|
230
|
+
)
|
|
231
|
+
),
|
|
232
|
+
)
|
|
233
|
+
tracking_confirmer = TrackingConfirmer(
|
|
234
|
+
delegate=(
|
|
235
|
+
RejectingConfirmer() if self.confirmer is None else self.confirmer
|
|
236
|
+
),
|
|
237
|
+
tracker=tracker,
|
|
238
|
+
interaction_gate=self.interaction_gate,
|
|
239
|
+
)
|
|
240
|
+
registry = create_subagent_tool_registry(
|
|
241
|
+
profile,
|
|
242
|
+
self.workspace,
|
|
243
|
+
confirmer=tracking_confirmer,
|
|
244
|
+
result_validator=batch_handler.validate_submission,
|
|
245
|
+
)
|
|
246
|
+
conversation = Conversation.from_messages(
|
|
247
|
+
[
|
|
248
|
+
Message(
|
|
249
|
+
role="system",
|
|
250
|
+
content=build_subagent_system_prompt(
|
|
251
|
+
profile,
|
|
252
|
+
registry,
|
|
253
|
+
project_instructions=runtime_snapshot.project_instructions,
|
|
254
|
+
),
|
|
255
|
+
)
|
|
256
|
+
]
|
|
257
|
+
)
|
|
258
|
+
artifact_directory = TemporaryDirectory(prefix="mycode-subagent-")
|
|
259
|
+
runner = AgentRunner(
|
|
260
|
+
llm_client=self.interaction_gate.run(self.llm_client_factory),
|
|
261
|
+
tool_registry=registry,
|
|
262
|
+
conversation=conversation,
|
|
263
|
+
max_turns=self.max_turns,
|
|
264
|
+
near_limit_remaining_turns=self.near_limit_remaining_turns,
|
|
265
|
+
near_limit_prompt=(
|
|
266
|
+
None
|
|
267
|
+
if self.near_limit_remaining_turns is None
|
|
268
|
+
else profile.near_limit_prompt
|
|
269
|
+
),
|
|
270
|
+
repeated_tool_call_limit=self.repeated_tool_call_limit,
|
|
271
|
+
finalize_on_max_turns=False,
|
|
272
|
+
context_budget=self.context_budget,
|
|
273
|
+
tool_result_artifact_store=ToolResultArtifactStore(
|
|
274
|
+
root=Path(artifact_directory.name),
|
|
275
|
+
threshold_chars=self.context_budget.tool_result_compression_threshold_chars,
|
|
276
|
+
),
|
|
277
|
+
token_estimator=TokenEstimator(),
|
|
278
|
+
memory_context_selector=(
|
|
279
|
+
None
|
|
280
|
+
if runtime_snapshot.memory_recall is None
|
|
281
|
+
else FrozenMemoryRecallProvider(runtime_snapshot.memory_recall)
|
|
282
|
+
),
|
|
283
|
+
tool_batch_handler=batch_handler,
|
|
284
|
+
observability_sink=self.observability_sink,
|
|
285
|
+
observability_scope="subagent",
|
|
286
|
+
observability_run_id=run_id,
|
|
287
|
+
)
|
|
288
|
+
response = _collect_agent_events(runner.run(_task_message(task)))
|
|
289
|
+
result = _result_from_response(
|
|
290
|
+
run_id=run_id,
|
|
291
|
+
task=task,
|
|
292
|
+
response=response,
|
|
293
|
+
batch_handler=batch_handler,
|
|
294
|
+
)
|
|
295
|
+
tracker.transition(result.status, result.stop_reason)
|
|
296
|
+
except (KeyboardInterrupt, SystemExit):
|
|
297
|
+
tracker.transition_once("interrupted", "interrupted")
|
|
298
|
+
raise
|
|
299
|
+
except Exception as error:
|
|
300
|
+
result = _failed_result(
|
|
301
|
+
run_id=run_id,
|
|
302
|
+
task=task,
|
|
303
|
+
status="failed",
|
|
304
|
+
stop_reason="runtime_error",
|
|
305
|
+
summary="SubAgent runtime failed before producing a valid result.",
|
|
306
|
+
error=_safe_error(error),
|
|
307
|
+
)
|
|
308
|
+
tracker.transition_once("failed", "runtime_error")
|
|
309
|
+
|
|
310
|
+
finally:
|
|
311
|
+
if artifact_directory is not None:
|
|
312
|
+
artifact_directory.cleanup()
|
|
313
|
+
|
|
314
|
+
execution = SubAgentExecution(
|
|
315
|
+
result=result,
|
|
316
|
+
transitions=tuple(tracker.transitions),
|
|
317
|
+
snapshot=snapshot_metadata,
|
|
318
|
+
context=_context_stats(runner),
|
|
319
|
+
token_usage=None if runner is None else runner.run_token_usage,
|
|
320
|
+
conversation_message_count=(
|
|
321
|
+
0 if runner is None else len(runner.conversation.get_messages())
|
|
322
|
+
),
|
|
323
|
+
tool_call_count=0 if batch_handler is None else batch_handler.tool_call_count,
|
|
324
|
+
validation_execution_count=(
|
|
325
|
+
0
|
|
326
|
+
if batch_handler is None
|
|
327
|
+
else len(batch_handler.validation_executions)
|
|
328
|
+
),
|
|
329
|
+
)
|
|
330
|
+
if run_observer is not None:
|
|
331
|
+
run_observer.on_result(task, execution, self._now())
|
|
332
|
+
return execution
|
|
333
|
+
|
|
334
|
+
def _resolved_working_directory(self) -> Path:
|
|
335
|
+
if self.working_directory is None:
|
|
336
|
+
return self.workspace.root
|
|
337
|
+
path = self.working_directory
|
|
338
|
+
if not path.is_absolute():
|
|
339
|
+
path = self.workspace.root / path
|
|
340
|
+
return path.resolve(strict=False)
|
|
341
|
+
|
|
342
|
+
def _now(self) -> datetime:
|
|
343
|
+
return self.interaction_gate.run(self.clock)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _state_transition_handler(
|
|
347
|
+
task: SubAgentTask,
|
|
348
|
+
*,
|
|
349
|
+
on_state_transition: StateTransitionHandler | None,
|
|
350
|
+
observer: SubAgentObserver | None,
|
|
351
|
+
) -> StateTransitionHandler | None:
|
|
352
|
+
if on_state_transition is None and observer is None:
|
|
353
|
+
return None
|
|
354
|
+
|
|
355
|
+
def handle(transition: SubAgentStateTransition) -> None:
|
|
356
|
+
if on_state_transition is not None:
|
|
357
|
+
on_state_transition(transition)
|
|
358
|
+
if observer is not None:
|
|
359
|
+
observer.on_state(task, transition)
|
|
360
|
+
|
|
361
|
+
return handle
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _collect_agent_events(events: Iterator[AgentEvent]) -> AgentModelResponse:
|
|
365
|
+
current_turn_content: list[str] = []
|
|
366
|
+
current_error: str | None = None
|
|
367
|
+
final_content = ""
|
|
368
|
+
stop_reason = None
|
|
369
|
+
|
|
370
|
+
for event in events:
|
|
371
|
+
if event.type in {"turn", "model_start"}:
|
|
372
|
+
current_turn_content = []
|
|
373
|
+
current_error = None
|
|
374
|
+
elif event.type == "text_delta":
|
|
375
|
+
current_turn_content.append(event.content)
|
|
376
|
+
elif event.type == "error":
|
|
377
|
+
if event.error is not None:
|
|
378
|
+
current_error = event.error
|
|
379
|
+
elif event.type == "stop":
|
|
380
|
+
stop_reason = event.stop_reason or "model_error"
|
|
381
|
+
final_content = "".join(current_turn_content)
|
|
382
|
+
if final_content:
|
|
383
|
+
continue
|
|
384
|
+
if stop_reason in {"model_error", "context_overflow"} and current_error:
|
|
385
|
+
final_content = current_error
|
|
386
|
+
elif event.content:
|
|
387
|
+
final_content = event.content
|
|
388
|
+
|
|
389
|
+
return AgentModelResponse(
|
|
390
|
+
content=final_content,
|
|
391
|
+
stop_reason=stop_reason or "model_error",
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def _result_from_response(
|
|
396
|
+
*,
|
|
397
|
+
run_id: str,
|
|
398
|
+
task: SubAgentTask,
|
|
399
|
+
response: AgentModelResponse,
|
|
400
|
+
batch_handler: SubAgentToolBatchHandler,
|
|
401
|
+
) -> SubAgentResult:
|
|
402
|
+
if response.stop_reason == "control_tool" and batch_handler.submitted_payload is not None:
|
|
403
|
+
return SubAgentResult(
|
|
404
|
+
run_id=run_id,
|
|
405
|
+
role=task.role,
|
|
406
|
+
status="completed",
|
|
407
|
+
stop_reason="submitted",
|
|
408
|
+
summary=batch_handler.submitted_payload.summary,
|
|
409
|
+
payload=batch_handler.submitted_payload,
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
if response.stop_reason == "max_turns":
|
|
413
|
+
stop_reason: SubAgentStopReason = (
|
|
414
|
+
"invalid_result" if batch_handler.submission_attempted else "max_turns"
|
|
415
|
+
)
|
|
416
|
+
elif response.stop_reason in {
|
|
417
|
+
"model_error",
|
|
418
|
+
"context_overflow",
|
|
419
|
+
"repeated_tool_call",
|
|
420
|
+
}:
|
|
421
|
+
stop_reason = response.stop_reason
|
|
422
|
+
else:
|
|
423
|
+
stop_reason = "invalid_result"
|
|
424
|
+
|
|
425
|
+
summary = {
|
|
426
|
+
"max_turns": "SubAgent reached its maximum number of turns.",
|
|
427
|
+
"invalid_result": "SubAgent did not submit a valid structured result.",
|
|
428
|
+
"model_error": "SubAgent model request failed.",
|
|
429
|
+
"context_overflow": "SubAgent context exceeded its configured budget.",
|
|
430
|
+
"repeated_tool_call": "SubAgent repeated the same tool call too many times.",
|
|
431
|
+
}[stop_reason]
|
|
432
|
+
return _failed_result(
|
|
433
|
+
run_id=run_id,
|
|
434
|
+
task=task,
|
|
435
|
+
status="failed",
|
|
436
|
+
stop_reason=stop_reason,
|
|
437
|
+
summary=summary,
|
|
438
|
+
error=(
|
|
439
|
+
_omitted_model_content(response.content, fallback=summary)
|
|
440
|
+
if stop_reason == "invalid_result"
|
|
441
|
+
else _safe_text(response.content, fallback=summary)
|
|
442
|
+
),
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def _failed_result(
|
|
447
|
+
*,
|
|
448
|
+
run_id: str,
|
|
449
|
+
task: SubAgentTask,
|
|
450
|
+
status: SubAgentEnvelopeStatus,
|
|
451
|
+
stop_reason: SubAgentStopReason,
|
|
452
|
+
summary: str,
|
|
453
|
+
error: str,
|
|
454
|
+
) -> SubAgentResult:
|
|
455
|
+
return SubAgentResult(
|
|
456
|
+
run_id=run_id,
|
|
457
|
+
role=task.role,
|
|
458
|
+
status=status,
|
|
459
|
+
stop_reason=stop_reason,
|
|
460
|
+
summary=summary,
|
|
461
|
+
error=error,
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def _context_stats(runner: AgentRunner | None) -> SubAgentModelContextStats | None:
|
|
466
|
+
if runner is None or runner.last_model_context is None:
|
|
467
|
+
return None
|
|
468
|
+
context = runner.last_model_context
|
|
469
|
+
return SubAgentModelContextStats(
|
|
470
|
+
estimated_input_tokens=context.estimate.estimated_input_tokens,
|
|
471
|
+
max_input_tokens=context.estimate.max_input_tokens,
|
|
472
|
+
selected_message_count=context.selected_message_count,
|
|
473
|
+
original_message_count=context.original_message_count,
|
|
474
|
+
compressed_tool_result_count=context.compressed_tool_result_count,
|
|
475
|
+
memory_stats=context.memory_stats,
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def _task_message(task: SubAgentTask) -> str:
|
|
480
|
+
return (
|
|
481
|
+
"Complete this delegated task and submit the role-specific structured result.\n\n"
|
|
482
|
+
"<delegated_task_json>\n"
|
|
483
|
+
+ json.dumps(task.model_dump(), ensure_ascii=False, indent=2)
|
|
484
|
+
+ "\n</delegated_task_json>"
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def _memory_query(task: SubAgentTask) -> str:
|
|
489
|
+
return "\n".join(
|
|
490
|
+
part
|
|
491
|
+
for part in (
|
|
492
|
+
task.objective,
|
|
493
|
+
task.context,
|
|
494
|
+
" ".join(task.scope_paths),
|
|
495
|
+
)
|
|
496
|
+
if part
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def _safe_error(error: Exception) -> str:
|
|
501
|
+
return _safe_text(
|
|
502
|
+
f"{type(error).__name__}: {error}",
|
|
503
|
+
fallback=f"{type(error).__name__} with no safe message.",
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def _omitted_model_content(content: str, *, fallback: str) -> str:
|
|
508
|
+
normalized = content.strip()
|
|
509
|
+
if not normalized:
|
|
510
|
+
return fallback
|
|
511
|
+
digest = hashlib.sha256(normalized.encode("utf-8")).hexdigest()
|
|
512
|
+
return (
|
|
513
|
+
f"{fallback} Raw assistant content was not returned to the parent "
|
|
514
|
+
f"({len(normalized)} characters, sha256={digest})."
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def _safe_text(content: str, *, fallback: str) -> str:
|
|
519
|
+
normalized = content.strip()
|
|
520
|
+
if not normalized:
|
|
521
|
+
return fallback
|
|
522
|
+
if len(normalized) <= MAX_RUNTIME_ERROR_CHARS:
|
|
523
|
+
return normalized
|
|
524
|
+
digest = hashlib.sha256(normalized.encode("utf-8")).hexdigest()
|
|
525
|
+
return (
|
|
526
|
+
f"{fallback} Original message omitted because it exceeded the runtime "
|
|
527
|
+
f"error limit ({len(normalized)} characters, sha256={digest})."
|
|
528
|
+
)
|