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.
Files changed (121) hide show
  1. mycode/__init__.py +0 -0
  2. mycode/adapters/__init__.py +21 -0
  3. mycode/adapters/jsonl.py +692 -0
  4. mycode/agent/__init__.py +25 -0
  5. mycode/agent/events.py +111 -0
  6. mycode/agent/outcome.py +103 -0
  7. mycode/agent/progress.py +373 -0
  8. mycode/agent/runner.py +1481 -0
  9. mycode/application/__init__.py +38 -0
  10. mycode/application/agent_session.py +367 -0
  11. mycode/application/events.py +59 -0
  12. mycode/application/runtime.py +211 -0
  13. mycode/application/sessions.py +180 -0
  14. mycode/cli.py +840 -0
  15. mycode/config.py +355 -0
  16. mycode/context/__init__.py +1 -0
  17. mycode/context/artifacts.py +672 -0
  18. mycode/context/budget.py +752 -0
  19. mycode/context/builder.py +112 -0
  20. mycode/context/compact.py +795 -0
  21. mycode/context/tool_result_format.py +199 -0
  22. mycode/context/tool_result_retention.py +261 -0
  23. mycode/conversation.py +78 -0
  24. mycode/error_handling.py +481 -0
  25. mycode/event_format.py +147 -0
  26. mycode/instructions.py +285 -0
  27. mycode/llm.py +771 -0
  28. mycode/mcp/__init__.py +41 -0
  29. mycode/mcp/client.py +44 -0
  30. mycode/mcp/config.py +207 -0
  31. mycode/mcp/errors.py +302 -0
  32. mycode/mcp/manager.py +339 -0
  33. mycode/mcp/models.py +20 -0
  34. mycode/mcp/result_adapter.py +58 -0
  35. mycode/mcp/tool_adapter.py +145 -0
  36. mycode/mcp/trust.py +313 -0
  37. mycode/memory.py +570 -0
  38. mycode/memory_context.py +245 -0
  39. mycode/messages.py +63 -0
  40. mycode/observability.py +28 -0
  41. mycode/permissions.py +262 -0
  42. mycode/persistence/__init__.py +1 -0
  43. mycode/persistence/filesystem.py +291 -0
  44. mycode/persistence/project_storage.py +208 -0
  45. mycode/persistence/session_lock.py +138 -0
  46. mycode/persistence/session_store.py +503 -0
  47. mycode/presentation/__init__.py +1 -0
  48. mycode/presentation/cli/__init__.py +14 -0
  49. mycode/presentation/cli/confirmer.py +116 -0
  50. mycode/presentation/cli/mcp_trust.py +61 -0
  51. mycode/presentation/cli/presenter.py +320 -0
  52. mycode/presentation/cli/session_menu.py +146 -0
  53. mycode/presentation/cli/subagent_observer.py +124 -0
  54. mycode/presentation/command_format.py +90 -0
  55. mycode/presentation/commands.py +95 -0
  56. mycode/presentation/tui/__init__.py +6 -0
  57. mycode/presentation/tui/app.py +1351 -0
  58. mycode/presentation/tui/interactions.py +253 -0
  59. mycode/presentation/tui/presenter.py +266 -0
  60. mycode/presentation/tui/screens.py +305 -0
  61. mycode/presentation/tui/widgets.py +214 -0
  62. mycode/project.py +22 -0
  63. mycode/prompts.py +181 -0
  64. mycode/reasoning.py +40 -0
  65. mycode/session.py +86 -0
  66. mycode/skills/__init__.py +27 -0
  67. mycode/skills/builtin/database-recovery/SKILL.md +138 -0
  68. mycode/skills/builtin/database-recovery/references/sqlite.md +235 -0
  69. mycode/skills/registry.py +295 -0
  70. mycode/skills/state.py +68 -0
  71. mycode/subagents/__init__.py +1 -0
  72. mycode/subagents/audit.py +212 -0
  73. mycode/subagents/concurrency.py +124 -0
  74. mycode/subagents/contracts.py +421 -0
  75. mycode/subagents/delegate.py +80 -0
  76. mycode/subagents/delegation.py +128 -0
  77. mycode/subagents/lifecycle.py +86 -0
  78. mycode/subagents/limits.py +7 -0
  79. mycode/subagents/observability.py +150 -0
  80. mycode/subagents/persistence.py +152 -0
  81. mycode/subagents/profiles.py +184 -0
  82. mycode/subagents/prompts.py +67 -0
  83. mycode/subagents/results.py +178 -0
  84. mycode/subagents/runtime.py +528 -0
  85. mycode/subagents/snapshots.py +211 -0
  86. mycode/subagents/tool_batch.py +260 -0
  87. mycode/tools/__init__.py +81 -0
  88. mycode/tools/base.py +222 -0
  89. mycode/tools/bounds.py +14 -0
  90. mycode/tools/command_executor.py +167 -0
  91. mycode/tools/command_output.py +166 -0
  92. mycode/tools/command_risk.py +596 -0
  93. mycode/tools/defaults.py +59 -0
  94. mycode/tools/edit_file.py +524 -0
  95. mycode/tools/file_mutation.py +30 -0
  96. mycode/tools/glob.py +247 -0
  97. mycode/tools/grep.py +324 -0
  98. mycode/tools/ignore.py +122 -0
  99. mycode/tools/inspect_changes.py +269 -0
  100. mycode/tools/load_skill.py +92 -0
  101. mycode/tools/memory.py +264 -0
  102. mycode/tools/path_permissions.py +78 -0
  103. mycode/tools/patterns.py +48 -0
  104. mycode/tools/permission_metadata.py +27 -0
  105. mycode/tools/process_tree.py +166 -0
  106. mycode/tools/read_file.py +242 -0
  107. mycode/tools/read_skill_resource.py +93 -0
  108. mycode/tools/registry.py +279 -0
  109. mycode/tools/run_command.py +237 -0
  110. mycode/tools/run_skill_script.py +206 -0
  111. mycode/tools/run_validation.py +107 -0
  112. mycode/tools/submit_result.py +93 -0
  113. mycode/tools/text.py +15 -0
  114. mycode/tools/validation_command.py +377 -0
  115. mycode/tools/workspace.py +33 -0
  116. mycode/tools/write_file.py +169 -0
  117. mycode_coding_agent-0.1.0.dist-info/METADATA +244 -0
  118. mycode_coding_agent-0.1.0.dist-info/RECORD +121 -0
  119. mycode_coding_agent-0.1.0.dist-info/WHEEL +4 -0
  120. mycode_coding_agent-0.1.0.dist-info/entry_points.txt +2 -0
  121. 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() + "..."