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,61 @@
|
|
|
1
|
+
"""Terminal presentation for the application MCP trust contract."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
|
|
7
|
+
from mycode.mcp.trust import MCPTrustRequest, MCPTrustWarning
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TerminalMCPTrustConfirmer:
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
input_func: Callable[[str], str] = input,
|
|
14
|
+
output_func: Callable[[str], None] = print,
|
|
15
|
+
) -> None:
|
|
16
|
+
self.input_func = input_func
|
|
17
|
+
self.output_func = output_func
|
|
18
|
+
|
|
19
|
+
def confirm(self, request: MCPTrustRequest) -> bool:
|
|
20
|
+
self._show_request(request)
|
|
21
|
+
try:
|
|
22
|
+
approved = self.input_func(
|
|
23
|
+
"MCP trust> 是否信任并启用这些项目级 MCP?[y/N] "
|
|
24
|
+
).strip().lower() in {"y", "yes"}
|
|
25
|
+
except EOFError:
|
|
26
|
+
approved = False
|
|
27
|
+
|
|
28
|
+
if not approved:
|
|
29
|
+
self.output_func("MCP trust> 未启用项目级 MCP")
|
|
30
|
+
return approved
|
|
31
|
+
|
|
32
|
+
def report_warning(self, warning: MCPTrustWarning) -> None:
|
|
33
|
+
if warning.code == "invalid_trust_store":
|
|
34
|
+
self.output_func(
|
|
35
|
+
"MCP trust> 警告:信任状态文件无效;"
|
|
36
|
+
"项目级 MCP 将按未信任处理"
|
|
37
|
+
)
|
|
38
|
+
elif warning.code == "persistence_failed":
|
|
39
|
+
self.output_func(
|
|
40
|
+
"MCP trust> 警告:信任状态未能保存;"
|
|
41
|
+
"本次仍会启用,下次将再次询问"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
def _show_request(self, request: MCPTrustRequest) -> None:
|
|
45
|
+
self.output_func("MCP trust> 项目配置请求启用以下 MCP Server:")
|
|
46
|
+
for server in request.servers:
|
|
47
|
+
self.output_func("")
|
|
48
|
+
self.output_func(f"server> {server.alias!r}")
|
|
49
|
+
self.output_func(f"transport> {server.transport}")
|
|
50
|
+
if server.transport == "stdio":
|
|
51
|
+
self.output_func(f"command> {server.command!r}")
|
|
52
|
+
self.output_func(f"args> {list(server.args)!r}")
|
|
53
|
+
if server.env_keys:
|
|
54
|
+
self.output_func(f"env keys> {list(server.env_keys)!r}")
|
|
55
|
+
continue
|
|
56
|
+
|
|
57
|
+
self.output_func(f"url template> {server.url_template!r}")
|
|
58
|
+
if server.destination is not None:
|
|
59
|
+
self.output_func(f"destination> {server.destination!r}")
|
|
60
|
+
if server.header_keys:
|
|
61
|
+
self.output_func(f"header keys> {list(server.header_keys)!r}")
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
from collections import deque
|
|
2
|
+
from collections.abc import Callable
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Literal
|
|
5
|
+
|
|
6
|
+
from mycode.agent.events import AgentEvent, AgentToolCall
|
|
7
|
+
from mycode.event_format import summarize_event_content, summarize_tool_arguments
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
CliDisplayMode = Literal["normal", "verbose", "debug"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
READ_ONLY_ACTIVITY_TOOLS = {
|
|
14
|
+
"glob",
|
|
15
|
+
"grep",
|
|
16
|
+
"inspect_changes",
|
|
17
|
+
"list_memories",
|
|
18
|
+
"read_artifact",
|
|
19
|
+
"read_file",
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class CliPresenter:
|
|
25
|
+
output: Callable[[str], None]
|
|
26
|
+
mode: CliDisplayMode = "normal"
|
|
27
|
+
_read_only_calls: list[AgentToolCall] = field(default_factory=list, init=False)
|
|
28
|
+
_pending_result_tools: deque[str] = field(default_factory=deque, init=False)
|
|
29
|
+
_continue_after_error: bool = field(default=False, init=False)
|
|
30
|
+
|
|
31
|
+
def flush(self) -> None:
|
|
32
|
+
if not self._read_only_calls:
|
|
33
|
+
return
|
|
34
|
+
self.output(_summarize_read_only_activity(self._read_only_calls))
|
|
35
|
+
self._read_only_calls.clear()
|
|
36
|
+
|
|
37
|
+
def show_agent_event(self, event: AgentEvent) -> None:
|
|
38
|
+
if event.type == "turn":
|
|
39
|
+
if event.turn_number is None or event.max_turns is None:
|
|
40
|
+
return
|
|
41
|
+
label = "轮次" if self.mode == "normal" else "turn"
|
|
42
|
+
self.output(f"{label}> {event.turn_number}/{event.max_turns}")
|
|
43
|
+
if event.content:
|
|
44
|
+
notice_label = "提醒" if self.mode == "normal" else "turn_notice"
|
|
45
|
+
self.output(f"{notice_label}> {event.content}")
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
if event.type == "context":
|
|
49
|
+
if self.mode != "normal":
|
|
50
|
+
self.output(f"context> {event.content}")
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
if event.type == "progress":
|
|
54
|
+
if self.mode == "debug" and event.progress is not None:
|
|
55
|
+
progress = event.progress
|
|
56
|
+
self.output(
|
|
57
|
+
"progress> "
|
|
58
|
+
f"stagnation_turns={progress.stagnation_turns} "
|
|
59
|
+
f"same_tool_repeat={progress.same_tool_repeat} "
|
|
60
|
+
f"same_result_repeat={progress.same_result_repeat} "
|
|
61
|
+
f"resource_repeat={progress.resource_repeat} "
|
|
62
|
+
f"convergence_guided={progress.convergence_guided} "
|
|
63
|
+
f"reason={progress.reason}"
|
|
64
|
+
)
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
if event.type == "artifact_warning":
|
|
68
|
+
self.flush()
|
|
69
|
+
if self.mode == "normal":
|
|
70
|
+
self.output(f"警告> 工具结果归档异常:{event.content}")
|
|
71
|
+
else:
|
|
72
|
+
self.output(f"artifact> warning: {event.content}")
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
if event.type == "model_retry" and event.model_retry is not None:
|
|
76
|
+
self.flush()
|
|
77
|
+
retry = event.model_retry
|
|
78
|
+
if self.mode == "normal":
|
|
79
|
+
self.output(
|
|
80
|
+
"重试> 模型连接异常,"
|
|
81
|
+
f"{retry.delay_seconds:.1f} 秒后重试当前模型轮次"
|
|
82
|
+
f"({retry.attempt}/{retry.max_retries})"
|
|
83
|
+
)
|
|
84
|
+
else:
|
|
85
|
+
self.output(
|
|
86
|
+
"model_retry> "
|
|
87
|
+
f"call_kind={retry.call_kind} "
|
|
88
|
+
f"attempt={retry.attempt}/{retry.max_retries} "
|
|
89
|
+
f"error_type={retry.error_type} "
|
|
90
|
+
f"error_code={retry.error_code} "
|
|
91
|
+
f"retryable={retry.retryable} "
|
|
92
|
+
f"delay_seconds={retry.delay_seconds:.1f} "
|
|
93
|
+
f"stream_started={retry.stream_started} "
|
|
94
|
+
f"partial_output_chars={retry.partial_output_chars}"
|
|
95
|
+
)
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
if event.type == "tool_call" and event.tool_call is not None:
|
|
99
|
+
self._pending_result_tools.append(event.tool_call.name)
|
|
100
|
+
if self.mode == "normal":
|
|
101
|
+
if self._continue_after_error:
|
|
102
|
+
self.flush()
|
|
103
|
+
self.output("活动> 上一步失败,正在尝试其他方式")
|
|
104
|
+
self._continue_after_error = False
|
|
105
|
+
if event.tool_call.name in READ_ONLY_ACTIVITY_TOOLS:
|
|
106
|
+
self._read_only_calls.append(event.tool_call)
|
|
107
|
+
return
|
|
108
|
+
self.flush()
|
|
109
|
+
if event.tool_call.name != "delegate_task":
|
|
110
|
+
self.output(_describe_activity(event.tool_call))
|
|
111
|
+
return
|
|
112
|
+
argument_summary = summarize_tool_arguments(
|
|
113
|
+
event.tool_call.name,
|
|
114
|
+
event.tool_call.arguments,
|
|
115
|
+
)
|
|
116
|
+
self.output(
|
|
117
|
+
f"tool_call> {event.tool_call.name}"
|
|
118
|
+
f"({argument_summary})"
|
|
119
|
+
)
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
if event.type == "tool_result" and event.tool_result is not None:
|
|
123
|
+
self.flush()
|
|
124
|
+
result = event.tool_result
|
|
125
|
+
status = "ok" if result.ok else "error"
|
|
126
|
+
tool_name = (
|
|
127
|
+
self._pending_result_tools.popleft()
|
|
128
|
+
if self._pending_result_tools
|
|
129
|
+
else str(result.metadata.get("tool_name", "工具"))
|
|
130
|
+
)
|
|
131
|
+
if result.metadata.get("tool_name") == "delegate_task":
|
|
132
|
+
metadata = result.metadata
|
|
133
|
+
child_status = metadata.get("child_status", status)
|
|
134
|
+
if self.mode == "normal" and result.ok and child_status == "completed":
|
|
135
|
+
return
|
|
136
|
+
if self.mode == "normal":
|
|
137
|
+
reason = metadata.get(
|
|
138
|
+
"child_stop_reason",
|
|
139
|
+
metadata.get("reason", "未知原因"),
|
|
140
|
+
)
|
|
141
|
+
self.output(
|
|
142
|
+
f"警告> SubAgent 未完成:角色={metadata.get('role', 'unknown')},"
|
|
143
|
+
f"状态={child_status},原因={reason}"
|
|
144
|
+
)
|
|
145
|
+
self._continue_after_error = True
|
|
146
|
+
return
|
|
147
|
+
details = [
|
|
148
|
+
f"role={metadata.get('role', 'unknown')}",
|
|
149
|
+
f"status={child_status}",
|
|
150
|
+
"stop="
|
|
151
|
+
f"{metadata.get('child_stop_reason', metadata.get('reason', 'unknown'))}",
|
|
152
|
+
]
|
|
153
|
+
self.output(f"tool_result> {status}: subagent " + " ".join(details))
|
|
154
|
+
return
|
|
155
|
+
if self.mode == "normal" and result.ok:
|
|
156
|
+
return
|
|
157
|
+
summary = summarize_event_content(
|
|
158
|
+
result.content if result.ok else result.error
|
|
159
|
+
)
|
|
160
|
+
if self.mode == "normal":
|
|
161
|
+
self.output(
|
|
162
|
+
f"警告> {_tool_display_name(tool_name)}失败:"
|
|
163
|
+
f"{_localize_error(summary)}"
|
|
164
|
+
)
|
|
165
|
+
self._continue_after_error = True
|
|
166
|
+
return
|
|
167
|
+
self.output(f"tool_result> {status}: {summary}")
|
|
168
|
+
return
|
|
169
|
+
|
|
170
|
+
if event.type == "error":
|
|
171
|
+
self.flush()
|
|
172
|
+
message = _localize_error(event.error or event.content)
|
|
173
|
+
if self.mode == "normal":
|
|
174
|
+
self.output(f"错误> {message}")
|
|
175
|
+
else:
|
|
176
|
+
self.output(f"error> {message}")
|
|
177
|
+
return
|
|
178
|
+
|
|
179
|
+
if event.type == "stop":
|
|
180
|
+
self.flush()
|
|
181
|
+
if event.stop_reason == "max_turns":
|
|
182
|
+
message = event.content or (
|
|
183
|
+
"本轮已达到步骤上限,未能生成最终答案。"
|
|
184
|
+
)
|
|
185
|
+
self.output(f"警告> {message}")
|
|
186
|
+
self.output(
|
|
187
|
+
"提示> 当前进度已保存;如需继续,直接输入“继续”。"
|
|
188
|
+
)
|
|
189
|
+
if self.mode == "debug":
|
|
190
|
+
self.output("stop> max_turns")
|
|
191
|
+
return
|
|
192
|
+
if self.mode == "normal":
|
|
193
|
+
stop_message = _normal_stop_message(event.stop_reason)
|
|
194
|
+
if stop_message is not None:
|
|
195
|
+
self.output(stop_message)
|
|
196
|
+
return
|
|
197
|
+
if self.mode == "debug" or event.stop_reason != "final_answer":
|
|
198
|
+
self.output(f"stop> {event.stop_reason}")
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _summarize_read_only_activity(calls: list[AgentToolCall]) -> str:
|
|
202
|
+
if len(calls) == 1:
|
|
203
|
+
return _describe_activity(calls[0])
|
|
204
|
+
if all(call.name == "read_file" for call in calls):
|
|
205
|
+
targets = [_read_file_target(call) for call in calls]
|
|
206
|
+
return f"活动> 正在读取 {len(targets)} 个文件:{', '.join(targets)}"
|
|
207
|
+
if all(call.name == "read_artifact" for call in calls):
|
|
208
|
+
return f"活动> 正在读取 {len(calls)} 份已保存的工具结果"
|
|
209
|
+
if all(call.name == "grep" for call in calls):
|
|
210
|
+
return f"活动> 正在执行 {len(calls)} 次代码搜索"
|
|
211
|
+
counts = {name: 0 for name in READ_ONLY_ACTIVITY_TOOLS}
|
|
212
|
+
for call in calls:
|
|
213
|
+
counts[call.name] += 1
|
|
214
|
+
summaries = []
|
|
215
|
+
labels = {
|
|
216
|
+
"read_file": "读取 {count} 个文件",
|
|
217
|
+
"grep": "搜索代码 {count} 次",
|
|
218
|
+
"glob": "查找文件 {count} 次",
|
|
219
|
+
"read_artifact": "读取 {count} 份已保存的工具结果",
|
|
220
|
+
"inspect_changes": "检查代码变更 {count} 次",
|
|
221
|
+
"list_memories": "读取长期记忆 {count} 次",
|
|
222
|
+
}
|
|
223
|
+
for name in (
|
|
224
|
+
"read_file",
|
|
225
|
+
"grep",
|
|
226
|
+
"glob",
|
|
227
|
+
"read_artifact",
|
|
228
|
+
"inspect_changes",
|
|
229
|
+
"list_memories",
|
|
230
|
+
):
|
|
231
|
+
if counts[name] > 0:
|
|
232
|
+
summaries.append(labels[name].format(count=counts[name]))
|
|
233
|
+
return "活动> 正在进行只读分析:" + "、".join(summaries)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _describe_activity(call: AgentToolCall) -> str:
|
|
237
|
+
arguments = call.arguments
|
|
238
|
+
if call.name == "read_file":
|
|
239
|
+
path = arguments.get("path", "未知文件")
|
|
240
|
+
start_line = arguments.get("start_line")
|
|
241
|
+
if isinstance(start_line, int) and start_line > 1:
|
|
242
|
+
return f"活动> 正在继续读取 {path}(从第 {start_line} 行开始)"
|
|
243
|
+
return f"活动> 正在读取文件:{path}"
|
|
244
|
+
if call.name == "read_artifact":
|
|
245
|
+
offset = arguments.get("offset_chars")
|
|
246
|
+
if isinstance(offset, int) and offset > 0:
|
|
247
|
+
return (
|
|
248
|
+
"活动> 正在继续读取已保存的工具结果"
|
|
249
|
+
f"(从第 {offset} 个字符开始)"
|
|
250
|
+
)
|
|
251
|
+
return "活动> 正在读取已保存的工具结果"
|
|
252
|
+
if call.name == "glob":
|
|
253
|
+
return f"活动> 正在查找文件:{arguments.get('pattern', '未知范围')}"
|
|
254
|
+
if call.name == "grep":
|
|
255
|
+
return (
|
|
256
|
+
"活动> 正在搜索代码:"
|
|
257
|
+
f"{arguments.get('path_pattern', '工作区内的文件')}"
|
|
258
|
+
)
|
|
259
|
+
if call.name == "inspect_changes":
|
|
260
|
+
return "活动> 正在检查代码变更"
|
|
261
|
+
if call.name == "list_memories":
|
|
262
|
+
return "活动> 正在读取长期记忆"
|
|
263
|
+
activity_names = {
|
|
264
|
+
"delete_memory": "正在删除长期记忆",
|
|
265
|
+
"edit_file": "正在编辑文件",
|
|
266
|
+
"run_command": "正在运行命令",
|
|
267
|
+
"save_memory": "正在保存长期记忆",
|
|
268
|
+
"write_file": "正在写入文件",
|
|
269
|
+
}
|
|
270
|
+
description = activity_names.get(call.name, f"正在执行 {call.name}")
|
|
271
|
+
return f"活动> {description}"
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _read_file_target(call: AgentToolCall) -> str:
|
|
275
|
+
path = str(call.arguments.get("path", "未知文件"))
|
|
276
|
+
start_line = call.arguments.get("start_line")
|
|
277
|
+
if isinstance(start_line, int) and start_line > 1:
|
|
278
|
+
return f"{path}(从第 {start_line} 行继续)"
|
|
279
|
+
return path
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _tool_display_name(name: str) -> str:
|
|
283
|
+
names = {
|
|
284
|
+
"glob": "查找文件",
|
|
285
|
+
"grep": "搜索代码",
|
|
286
|
+
"read_artifact": "读取已保存的工具结果",
|
|
287
|
+
"read_file": "读取文件",
|
|
288
|
+
}
|
|
289
|
+
return names.get(name, name)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _localize_error(message: str) -> str:
|
|
293
|
+
translations = {
|
|
294
|
+
"File not found: ": "未找到文件:",
|
|
295
|
+
"Directory not found: ": "未找到目录:",
|
|
296
|
+
"Model streaming failed: ": "模型流式请求失败:",
|
|
297
|
+
"Model request failed: ": "模型请求失败:",
|
|
298
|
+
}
|
|
299
|
+
for prefix, translated in translations.items():
|
|
300
|
+
if message.startswith(prefix):
|
|
301
|
+
return translated + message[len(prefix) :]
|
|
302
|
+
return message
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _normal_stop_message(stop_reason: str | None) -> str | None:
|
|
306
|
+
messages = {
|
|
307
|
+
"context_overflow": "错误> 当前上下文超过模型可用范围,本轮无法继续。",
|
|
308
|
+
"model_error": "提示> 本轮因模型调用错误而结束。",
|
|
309
|
+
"repeated_tool_call": "警告> 模型连续重复了相同的工具调用,本轮已停止。",
|
|
310
|
+
"tool_error": "警告> 本轮因工具执行错误而停止。",
|
|
311
|
+
}
|
|
312
|
+
return messages.get(stop_reason)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
__all__ = [
|
|
316
|
+
"CliDisplayMode",
|
|
317
|
+
"CliPresenter",
|
|
318
|
+
"summarize_event_content",
|
|
319
|
+
"summarize_tool_arguments",
|
|
320
|
+
]
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
|
|
5
|
+
from mycode.application.sessions import (
|
|
6
|
+
SessionStartRequest,
|
|
7
|
+
delete_project_session,
|
|
8
|
+
list_project_sessions,
|
|
9
|
+
)
|
|
10
|
+
from mycode.persistence.session_store import (
|
|
11
|
+
SessionRecord,
|
|
12
|
+
SessionStore,
|
|
13
|
+
SessionStoreError,
|
|
14
|
+
)
|
|
15
|
+
from mycode.project import ProjectIdentity
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def select_session_request(
|
|
19
|
+
store: SessionStore,
|
|
20
|
+
project: ProjectIdentity,
|
|
21
|
+
*,
|
|
22
|
+
input_func: Callable[[str], str] = input,
|
|
23
|
+
output_func: Callable[[str], None] = print,
|
|
24
|
+
) -> SessionStartRequest | None:
|
|
25
|
+
sessions = list_project_sessions(store, project)
|
|
26
|
+
output_func(f"session> 项目 {project.workspace_root}")
|
|
27
|
+
if not sessions:
|
|
28
|
+
output_func("session> 没有历史会话,正在创建新会话")
|
|
29
|
+
return SessionStartRequest(mode="new")
|
|
30
|
+
|
|
31
|
+
while True:
|
|
32
|
+
if sessions:
|
|
33
|
+
output_func("session> 请选择历史会话,或管理已有会话")
|
|
34
|
+
_output_numbered_sessions(sessions, output_func)
|
|
35
|
+
else:
|
|
36
|
+
output_func("session> 当前没有历史会话")
|
|
37
|
+
output_func("session> [N] 创建新会话")
|
|
38
|
+
if sessions:
|
|
39
|
+
output_func("session> [D] 永久删除会话")
|
|
40
|
+
output_func("session> [Q] 退出")
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
answer = input_func("session> ").strip()
|
|
44
|
+
except EOFError:
|
|
45
|
+
output_func("")
|
|
46
|
+
return None
|
|
47
|
+
normalized = answer.casefold()
|
|
48
|
+
if normalized in {"n", "new"}:
|
|
49
|
+
return SessionStartRequest(mode="new")
|
|
50
|
+
if normalized in {"d", "delete"} and sessions:
|
|
51
|
+
_delete_session_interactively(
|
|
52
|
+
store,
|
|
53
|
+
project,
|
|
54
|
+
sessions,
|
|
55
|
+
input_func=input_func,
|
|
56
|
+
output_func=output_func,
|
|
57
|
+
)
|
|
58
|
+
sessions = list_project_sessions(store, project)
|
|
59
|
+
continue
|
|
60
|
+
if normalized in {"q", "quit"}:
|
|
61
|
+
return None
|
|
62
|
+
if answer.isdigit():
|
|
63
|
+
index = int(answer)
|
|
64
|
+
if 1 <= index <= len(sessions):
|
|
65
|
+
selected = sessions[index - 1]
|
|
66
|
+
return SessionStartRequest(mode="resume", session_id=selected.id)
|
|
67
|
+
output_func("session> 选择无效,请重新输入")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _output_numbered_sessions(
|
|
71
|
+
sessions: list[SessionRecord],
|
|
72
|
+
output_func: Callable[[str], None],
|
|
73
|
+
) -> None:
|
|
74
|
+
for index, session in enumerate(sessions, start=1):
|
|
75
|
+
output_func(
|
|
76
|
+
f"session> [{index}] {session.title} "
|
|
77
|
+
f"({_session_status_label(session.status)}, {session.id[:8]})"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _delete_session_interactively(
|
|
82
|
+
store: SessionStore,
|
|
83
|
+
project: ProjectIdentity,
|
|
84
|
+
sessions: list[SessionRecord],
|
|
85
|
+
*,
|
|
86
|
+
input_func: Callable[[str], str],
|
|
87
|
+
output_func: Callable[[str], None],
|
|
88
|
+
) -> None:
|
|
89
|
+
output_func("session> 请选择要永久删除的会话")
|
|
90
|
+
_output_numbered_sessions(sessions, output_func)
|
|
91
|
+
output_func("session> [Q] 取消删除")
|
|
92
|
+
|
|
93
|
+
target: SessionRecord | None = None
|
|
94
|
+
while target is None:
|
|
95
|
+
try:
|
|
96
|
+
answer = input_func("session delete> ").strip()
|
|
97
|
+
except EOFError:
|
|
98
|
+
output_func("")
|
|
99
|
+
output_func("session> 已取消删除")
|
|
100
|
+
return
|
|
101
|
+
if answer.casefold() in {"q", "quit", "cancel"}:
|
|
102
|
+
output_func("session> 已取消删除")
|
|
103
|
+
return
|
|
104
|
+
if answer.isdigit():
|
|
105
|
+
index = int(answer)
|
|
106
|
+
if 1 <= index <= len(sessions):
|
|
107
|
+
target = sessions[index - 1]
|
|
108
|
+
break
|
|
109
|
+
output_func("session> 删除选项无效,请重新输入")
|
|
110
|
+
|
|
111
|
+
confirmation = f"DELETE {target.id}"
|
|
112
|
+
output_func(
|
|
113
|
+
f"session> 将永久删除 {target.title} "
|
|
114
|
+
f"({_session_status_label(target.status)}, {target.id})"
|
|
115
|
+
)
|
|
116
|
+
output_func(
|
|
117
|
+
"session> 这会删除该会话的消息、工具调用、Compact 状态、"
|
|
118
|
+
"SubAgent 记录和 artifact"
|
|
119
|
+
)
|
|
120
|
+
try:
|
|
121
|
+
answer = input_func(f"session> 输入 {confirmation} 进行确认:").strip()
|
|
122
|
+
except EOFError:
|
|
123
|
+
output_func("")
|
|
124
|
+
output_func("session> 已取消删除")
|
|
125
|
+
return
|
|
126
|
+
if answer != confirmation:
|
|
127
|
+
output_func("session> 已取消删除:确认文本不匹配")
|
|
128
|
+
return
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
removed = delete_project_session(store, project, target.id)
|
|
132
|
+
except SessionStoreError as error:
|
|
133
|
+
output_func(f"session> 当前无法删除:{error}")
|
|
134
|
+
return
|
|
135
|
+
if removed:
|
|
136
|
+
output_func(f"session> 已永久删除 {target.id}:{target.title}")
|
|
137
|
+
else:
|
|
138
|
+
output_func(f"session> 会话此前已经删除:{target.id}")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _session_status_label(status: str) -> str:
|
|
142
|
+
return {
|
|
143
|
+
"active": "进行中",
|
|
144
|
+
"closed": "已关闭",
|
|
145
|
+
"interrupted": "已中断",
|
|
146
|
+
}.get(status, status)
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
from mycode.presentation.cli.presenter import CliDisplayMode
|
|
6
|
+
from mycode.subagents.audit import SubAgentToolAudit
|
|
7
|
+
from mycode.subagents.contracts import SubAgentTask
|
|
8
|
+
from mycode.subagents.lifecycle import SubAgentStateTransition
|
|
9
|
+
from mycode.subagents.runtime import SubAgentExecution
|
|
10
|
+
from mycode.subagents.snapshots import SubAgentSnapshotMetadata
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
CLI_SUBAGENT_SUMMARY_CHARS = 160
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class CliSubAgentObserver:
|
|
18
|
+
output: Callable[[str], None]
|
|
19
|
+
mode: CliDisplayMode = "normal"
|
|
20
|
+
|
|
21
|
+
def on_state(
|
|
22
|
+
self,
|
|
23
|
+
task: SubAgentTask,
|
|
24
|
+
transition: SubAgentStateTransition,
|
|
25
|
+
) -> None:
|
|
26
|
+
run = transition.run_id[:8]
|
|
27
|
+
if transition.state == "running" and transition.reason == "run_started":
|
|
28
|
+
if self.mode == "normal":
|
|
29
|
+
self.output(f"activity> {transition.role.capitalize()} started")
|
|
30
|
+
return
|
|
31
|
+
self.output(
|
|
32
|
+
f"subagent> start role={transition.role} run={run} "
|
|
33
|
+
f"objective_chars={len(task.objective)} "
|
|
34
|
+
f"context_chars={len(task.context)} scope_paths={len(task.scope_paths)}"
|
|
35
|
+
)
|
|
36
|
+
return
|
|
37
|
+
if self.mode == "normal":
|
|
38
|
+
if transition.state in {"failed", "interrupted"}:
|
|
39
|
+
self.output(
|
|
40
|
+
f"activity> {transition.role.capitalize()} "
|
|
41
|
+
f"{transition.state}: {transition.reason}"
|
|
42
|
+
)
|
|
43
|
+
return
|
|
44
|
+
self.output(
|
|
45
|
+
f"subagent> role={transition.role} run={run} "
|
|
46
|
+
f"state={transition.state} reason={transition.reason}"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
def on_snapshot(
|
|
50
|
+
self,
|
|
51
|
+
task: SubAgentTask,
|
|
52
|
+
run_id: str,
|
|
53
|
+
snapshot: SubAgentSnapshotMetadata,
|
|
54
|
+
occurred_at: datetime,
|
|
55
|
+
) -> None:
|
|
56
|
+
if self.mode != "debug":
|
|
57
|
+
return
|
|
58
|
+
self.output(
|
|
59
|
+
f"subagent_context> role={task.role} run={run_id[:8]} "
|
|
60
|
+
f"snapshot={snapshot.combined_sha256[:12]} "
|
|
61
|
+
f"instructions={len(snapshot.instructions.sources)} "
|
|
62
|
+
f"memory_entries={len(snapshot.memory.selected_entries)}"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def on_tool_audit(
|
|
66
|
+
self,
|
|
67
|
+
task: SubAgentTask,
|
|
68
|
+
run_id: str,
|
|
69
|
+
audit: SubAgentToolAudit,
|
|
70
|
+
occurred_at: datetime,
|
|
71
|
+
) -> None:
|
|
72
|
+
if self.mode == "normal":
|
|
73
|
+
return
|
|
74
|
+
status = "ok" if audit.ok else "error"
|
|
75
|
+
details = [
|
|
76
|
+
f"subagent_tool> role={task.role}",
|
|
77
|
+
f"name={audit.tool_name}",
|
|
78
|
+
f"status={status}",
|
|
79
|
+
]
|
|
80
|
+
if self.mode == "debug":
|
|
81
|
+
details.insert(1, f"run={run_id[:8]}")
|
|
82
|
+
details.append(f"args={audit.arguments_sha256[:12]}")
|
|
83
|
+
details.append(f"output_chars={audit.output_chars}")
|
|
84
|
+
if audit.exit_code is not None:
|
|
85
|
+
details.append(f"exit_code={audit.exit_code}")
|
|
86
|
+
if audit.duration_ms is not None:
|
|
87
|
+
details.append(f"duration_ms={audit.duration_ms}")
|
|
88
|
+
if audit.truncated:
|
|
89
|
+
details.append("truncated=true")
|
|
90
|
+
if audit.reason is not None:
|
|
91
|
+
details.append(f"reason={audit.reason}")
|
|
92
|
+
self.output(" ".join(details))
|
|
93
|
+
|
|
94
|
+
def on_result(
|
|
95
|
+
self,
|
|
96
|
+
task: SubAgentTask,
|
|
97
|
+
execution: SubAgentExecution,
|
|
98
|
+
occurred_at: datetime,
|
|
99
|
+
) -> None:
|
|
100
|
+
if self.mode == "normal":
|
|
101
|
+
self.output(
|
|
102
|
+
f"activity> {execution.result.role.capitalize()} "
|
|
103
|
+
f"{execution.result.status}: {_summary(execution.result.summary)}"
|
|
104
|
+
)
|
|
105
|
+
return
|
|
106
|
+
usage = execution.token_usage
|
|
107
|
+
token_text = ""
|
|
108
|
+
if usage is not None:
|
|
109
|
+
token_text = f" tokens={usage.total_tokens}"
|
|
110
|
+
run_text = (
|
|
111
|
+
f" run={execution.result.run_id[:8]}" if self.mode == "debug" else ""
|
|
112
|
+
)
|
|
113
|
+
self.output(
|
|
114
|
+
f"subagent_result> role={execution.result.role}{run_text} "
|
|
115
|
+
f"status={execution.result.status} stop={execution.result.stop_reason}"
|
|
116
|
+
f"{token_text} summary={_summary(execution.result.summary)!r}"
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _summary(value: str) -> str:
|
|
121
|
+
normalized = " ".join(value.split())
|
|
122
|
+
if len(normalized) <= CLI_SUBAGENT_SUMMARY_CHARS:
|
|
123
|
+
return normalized
|
|
124
|
+
return normalized[: CLI_SUBAGENT_SUMMARY_CHARS - 3].rstrip() + "..."
|