mindcode 0.2.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 (116) hide show
  1. mindcode/__init__.py +5 -0
  2. mindcode/__main__.py +4 -0
  3. mindcode/__pycache__/__init__.cpython-313.pyc +0 -0
  4. mindcode/__pycache__/__main__.cpython-313.pyc +0 -0
  5. mindcode/__pycache__/_version.cpython-313.pyc +0 -0
  6. mindcode/__pycache__/config.cpython-313.pyc +0 -0
  7. mindcode/__pycache__/policy.cpython-313.pyc +0 -0
  8. mindcode/__pycache__/remote.cpython-313.pyc +0 -0
  9. mindcode/__pycache__/render.cpython-313.pyc +0 -0
  10. mindcode/__pycache__/runtime.cpython-313.pyc +0 -0
  11. mindcode/__pycache__/tasking.cpython-313.pyc +0 -0
  12. mindcode/__pycache__/terminal_core.cpython-313.pyc +0 -0
  13. mindcode/_version.py +11 -0
  14. mindcode/approval.py +105 -0
  15. mindcode/bench/__init__.py +19 -0
  16. mindcode/bench/__pycache__/__init__.cpython-313.pyc +0 -0
  17. mindcode/bench/__pycache__/fake_tools.cpython-313.pyc +0 -0
  18. mindcode/bench/__pycache__/jsonutil.cpython-313.pyc +0 -0
  19. mindcode/bench/__pycache__/paths.cpython-313.pyc +0 -0
  20. mindcode/bench/__pycache__/predictions.cpython-313.pyc +0 -0
  21. mindcode/bench/__pycache__/report.cpython-313.pyc +0 -0
  22. mindcode/bench/__pycache__/runner.cpython-313.pyc +0 -0
  23. mindcode/bench/__pycache__/schema.cpython-313.pyc +0 -0
  24. mindcode/bench/__pycache__/trace.cpython-313.pyc +0 -0
  25. mindcode/bench/__pycache__/workspace.cpython-313.pyc +0 -0
  26. mindcode/bench/adapters/__init__.py +22 -0
  27. mindcode/bench/adapters/__pycache__/__init__.cpython-313.pyc +0 -0
  28. mindcode/bench/adapters/__pycache__/base.cpython-313.pyc +0 -0
  29. mindcode/bench/adapters/__pycache__/mini_bfcl.cpython-313.pyc +0 -0
  30. mindcode/bench/adapters/__pycache__/mini_gaia.cpython-313.pyc +0 -0
  31. mindcode/bench/adapters/__pycache__/mini_terminal.cpython-313.pyc +0 -0
  32. mindcode/bench/adapters/__pycache__/swe_bench.cpython-313.pyc +0 -0
  33. mindcode/bench/adapters/__pycache__/terminal_bench.cpython-313.pyc +0 -0
  34. mindcode/bench/adapters/base.py +45 -0
  35. mindcode/bench/adapters/mini_bfcl.py +18 -0
  36. mindcode/bench/adapters/mini_gaia.py +18 -0
  37. mindcode/bench/adapters/mini_terminal.py +18 -0
  38. mindcode/bench/adapters/swe_bench.py +86 -0
  39. mindcode/bench/adapters/terminal_bench.py +107 -0
  40. mindcode/bench/fake_tools.py +73 -0
  41. mindcode/bench/jsonutil.py +20 -0
  42. mindcode/bench/paths.py +10 -0
  43. mindcode/bench/predictions.py +65 -0
  44. mindcode/bench/report.py +48 -0
  45. mindcode/bench/runner.py +250 -0
  46. mindcode/bench/schema.py +78 -0
  47. mindcode/bench/scorers/__pycache__/base.cpython-313.pyc +0 -0
  48. mindcode/bench/scorers/__pycache__/composite.cpython-313.pyc +0 -0
  49. mindcode/bench/scorers/__pycache__/exact.cpython-313.pyc +0 -0
  50. mindcode/bench/scorers/__pycache__/json_call.cpython-313.pyc +0 -0
  51. mindcode/bench/scorers/__pycache__/swe.cpython-313.pyc +0 -0
  52. mindcode/bench/scorers/__pycache__/terminal.cpython-313.pyc +0 -0
  53. mindcode/bench/scorers/__pycache__/terminal_bench.cpython-313.pyc +0 -0
  54. mindcode/bench/scorers/base.py +22 -0
  55. mindcode/bench/scorers/composite.py +57 -0
  56. mindcode/bench/scorers/exact.py +30 -0
  57. mindcode/bench/scorers/json_call.py +44 -0
  58. mindcode/bench/scorers/swe.py +29 -0
  59. mindcode/bench/scorers/terminal.py +43 -0
  60. mindcode/bench/scorers/terminal_bench.py +25 -0
  61. mindcode/bench/trace.py +34 -0
  62. mindcode/bench/workspace.py +127 -0
  63. mindcode/cli/__init__.py +112 -0
  64. mindcode/cli/__pycache__/__init__.cpython-313.pyc +0 -0
  65. mindcode/cli/__pycache__/_shared.cpython-313.pyc +0 -0
  66. mindcode/cli/_shared.py +43 -0
  67. mindcode/cli/commands/__init__.py +1 -0
  68. mindcode/cli/commands/__pycache__/__init__.cpython-313.pyc +0 -0
  69. mindcode/cli/commands/__pycache__/bench.cpython-313.pyc +0 -0
  70. mindcode/cli/commands/__pycache__/chat.cpython-313.pyc +0 -0
  71. mindcode/cli/commands/__pycache__/config_cmd.cpython-313.pyc +0 -0
  72. mindcode/cli/commands/__pycache__/remote.cpython-313.pyc +0 -0
  73. mindcode/cli/commands/__pycache__/shell.cpython-313.pyc +0 -0
  74. mindcode/cli/commands/__pycache__/status.cpython-313.pyc +0 -0
  75. mindcode/cli/commands/__pycache__/task.cpython-313.pyc +0 -0
  76. mindcode/cli/commands/__pycache__/terminal.cpython-313.pyc +0 -0
  77. mindcode/cli/commands/bench.py +365 -0
  78. mindcode/cli/commands/chat.py +86 -0
  79. mindcode/cli/commands/config_cmd.py +197 -0
  80. mindcode/cli/commands/remote.py +146 -0
  81. mindcode/cli/commands/shell.py +108 -0
  82. mindcode/cli/commands/status.py +37 -0
  83. mindcode/cli/commands/task.py +417 -0
  84. mindcode/cli/commands/terminal.py +124 -0
  85. mindcode/cli/shell/__init__.py +5 -0
  86. mindcode/cli/shell/__pycache__/__init__.cpython-313.pyc +0 -0
  87. mindcode/cli/shell/__pycache__/completion.cpython-313.pyc +0 -0
  88. mindcode/cli/shell/__pycache__/menu.cpython-313.pyc +0 -0
  89. mindcode/cli/shell/__pycache__/repl.cpython-313.pyc +0 -0
  90. mindcode/cli/shell/__pycache__/slash.cpython-313.pyc +0 -0
  91. mindcode/cli/shell/__pycache__/startup.cpython-313.pyc +0 -0
  92. mindcode/cli/shell/completion.py +72 -0
  93. mindcode/cli/shell/menu.py +88 -0
  94. mindcode/cli/shell/repl.py +597 -0
  95. mindcode/cli/shell/slash.py +480 -0
  96. mindcode/cli/shell/startup.py +55 -0
  97. mindcode/cli/shell/tui.py +897 -0
  98. mindcode/config.py +173 -0
  99. mindcode/mcp.py +205 -0
  100. mindcode/policy.py +173 -0
  101. mindcode/remote.py +216 -0
  102. mindcode/render.py +458 -0
  103. mindcode/runtime.py +541 -0
  104. mindcode/skills.py +154 -0
  105. mindcode/subagents.py +43 -0
  106. mindcode/tasking.py +281 -0
  107. mindcode/terminal/__init__.py +1 -0
  108. mindcode/terminal/__main__.py +17 -0
  109. mindcode/terminal/__pycache__/__init__.cpython-313.pyc +0 -0
  110. mindcode/terminal/__pycache__/__main__.cpython-313.pyc +0 -0
  111. mindcode/terminal_core.py +264 -0
  112. mindcode-0.2.0.dist-info/METADATA +244 -0
  113. mindcode-0.2.0.dist-info/RECORD +116 -0
  114. mindcode-0.2.0.dist-info/WHEEL +5 -0
  115. mindcode-0.2.0.dist-info/entry_points.txt +2 -0
  116. mindcode-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,597 @@
1
+ """Shell 类 — mindcode 全屏 TUI 的队列与 runtime 主循环。
2
+
3
+ Prompt Toolkit 的固定输入框只负责收集文本;process_loop 继续按原有顺序
4
+ 处理队列中的 slash 命令和 agent turn。
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ import time
11
+ from dataclasses import dataclass, field
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ from mindagent.core import (
16
+ ActionType,
17
+ AgentSession,
18
+ AgentRuntime,
19
+ LocalConversationStore,
20
+ SessionEnvironment,
21
+ SessionEventType,
22
+ SessionPolicy,
23
+ )
24
+ from ...config import (
25
+ CONFIG_FILE,
26
+ MindcodeConfig,
27
+ SESSIONS_DIR,
28
+ load_config,
29
+ save_config,
30
+ )
31
+ from ...approval import make_shell_approval
32
+ from ...policy import ApprovalMode, approval_mode_label
33
+ from ...render import RichRenderer
34
+ from ...runtime import build_master_worker_system, build_runtime
35
+ from ...subagents import MasterWorkerSystem
36
+ from .._shared import console
37
+ from .completion import MindcodeCompleter
38
+ from .slash import handle_slash_command
39
+ from .startup import build_welcome_banner
40
+ from .tui import MindcodeTui, Transcript, redirect_console_output
41
+
42
+
43
+ @dataclass
44
+ class _SessionErrorResult:
45
+ """Minimal result shape used when the Session actor rejects a command."""
46
+
47
+ run_id: str
48
+ error: str
49
+ final_answer: str | None = None
50
+ error_info: Any | None = None
51
+
52
+
53
+ @dataclass
54
+ class ShellState:
55
+ """Mutable state shared with slash command handlers."""
56
+
57
+ config_path: Path
58
+ config: MindcodeConfig
59
+ active_provider: str
60
+ active_model: str
61
+ runtime: AgentRuntime | None
62
+ subagent_system: MasterWorkerSystem | None
63
+ renderer: RichRenderer
64
+ store: LocalConversationStore
65
+ workspace: Path
66
+ session_id: str
67
+ approve_mode: ApprovalMode
68
+ stream: bool
69
+ subagents: bool = False
70
+ session: AgentSession | None = None
71
+ max_steps: int = 50
72
+ step_timeout_s: float | None = 300.0
73
+ total_timeout_s: float | None = 1800.0
74
+ history: list[dict[str, Any]] = field(default_factory=list)
75
+ last_run_id: str | None = None
76
+
77
+ async def rebuild_runtime(self) -> None:
78
+ provider = self.config.providers[self.active_provider]
79
+ approval = make_shell_approval(
80
+ lambda: getattr(self, "choose_option", None)
81
+ )
82
+ if self.subagents:
83
+ replacement = build_master_worker_system(
84
+ self.workspace,
85
+ provider_config=provider,
86
+ approve_mode=self.approve_mode,
87
+ model=self.active_model or None,
88
+ stream=self.stream,
89
+ event_handler=self.renderer,
90
+ max_steps=self.max_steps,
91
+ step_timeout_s=self.step_timeout_s,
92
+ total_timeout_s=self.total_timeout_s,
93
+ approval_callback=approval,
94
+ )
95
+ else:
96
+ replacement = build_runtime(
97
+ self.workspace,
98
+ provider_config=provider,
99
+ approve_mode=self.approve_mode,
100
+ model=self.active_model or None,
101
+ stream=self.stream,
102
+ event_handler=self.renderer,
103
+ max_steps=self.max_steps,
104
+ step_timeout_s=self.step_timeout_s,
105
+ total_timeout_s=self.total_timeout_s,
106
+ approval_callback=approval,
107
+ )
108
+ try:
109
+ await self.close()
110
+ except Exception:
111
+ if isinstance(replacement, MasterWorkerSystem):
112
+ await replacement.close()
113
+ else:
114
+ await replacement.close(close_dependencies=True)
115
+ raise
116
+ if isinstance(replacement, MasterWorkerSystem):
117
+ self.subagent_system = replacement
118
+ self.runtime = None
119
+ self.session = None
120
+ else:
121
+ self.runtime = replacement
122
+ self.subagent_system = None
123
+ self.session = self._create_session(replacement)
124
+
125
+ def _create_session(self, runtime: AgentRuntime) -> AgentSession:
126
+ return runtime.create_session(
127
+ session_id=self.session_id,
128
+ environment=SessionEnvironment(
129
+ workspace_root=self.workspace,
130
+ working_directory=self.workspace,
131
+ ),
132
+ policy=SessionPolicy(
133
+ allow_write=True,
134
+ allow_dangerous=True,
135
+ max_parallel_actions=1,
136
+ ),
137
+ conversation_store=self.store,
138
+ )
139
+
140
+ async def close(self) -> None:
141
+ if self.subagent_system is not None:
142
+ await self.subagent_system.close()
143
+ elif self.runtime is not None:
144
+ await self.runtime.close(close_dependencies=True)
145
+
146
+ def _save_config(self) -> None:
147
+ save_config(self.config)
148
+
149
+
150
+ class Shell:
151
+ """Mindcode 全屏终端交互会话。"""
152
+
153
+ def __init__(
154
+ self,
155
+ workspace: Path,
156
+ *,
157
+ approve_mode: ApprovalMode,
158
+ provider_name: str | None,
159
+ model: str | None,
160
+ session_id: str | None,
161
+ resume: str | None,
162
+ stream: bool,
163
+ subagents: bool = False,
164
+ max_steps: int = 50,
165
+ step_timeout_s: float | None = 300.0,
166
+ total_timeout_s: float | None = 1800.0,
167
+ ):
168
+ self.workspace = workspace
169
+ self.approve_mode = approve_mode
170
+ self.stream = stream
171
+ self.resume_id = resume
172
+ self.explicit_session_id = session_id
173
+
174
+ self.config = load_config(CONFIG_FILE)
175
+ active_provider = provider_name or self.config.default_provider
176
+ if not active_provider or active_provider not in self.config.providers:
177
+ raise ValueError(
178
+ f"无效 provider: {active_provider};可用: "
179
+ f"{', '.join(self.config.providers)}"
180
+ )
181
+ provider = self.config.providers[active_provider]
182
+ active_model = (
183
+ model
184
+ or provider.default_model
185
+ or (provider.models[0] if provider.models else "")
186
+ )
187
+
188
+ self.renderer = RichRenderer(console=console)
189
+ # TUI 在 run() 才创建,chooser 必须运行时动态读取;
190
+ # state 尚不存在,闭包延迟到审批调用时才求值。
191
+ approval = make_shell_approval(
192
+ lambda: getattr(self.state, "choose_option", None)
193
+ )
194
+ if subagents:
195
+ self.runtime = None
196
+ self.subagent_system = build_master_worker_system(
197
+ workspace,
198
+ provider_config=provider,
199
+ approve_mode=approve_mode,
200
+ model=active_model or None,
201
+ stream=stream,
202
+ event_handler=self.renderer,
203
+ max_steps=max_steps,
204
+ step_timeout_s=step_timeout_s,
205
+ total_timeout_s=total_timeout_s,
206
+ approval_callback=approval,
207
+ )
208
+ else:
209
+ self.runtime = build_runtime(
210
+ workspace,
211
+ provider_config=provider,
212
+ approve_mode=approve_mode,
213
+ model=active_model or None,
214
+ stream=stream,
215
+ event_handler=self.renderer,
216
+ max_steps=max_steps,
217
+ step_timeout_s=step_timeout_s,
218
+ total_timeout_s=total_timeout_s,
219
+ approval_callback=approval,
220
+ )
221
+ self.subagent_system = None
222
+
223
+ self.store = LocalConversationStore(SESSIONS_DIR)
224
+
225
+ if resume:
226
+ self.session_id = resume
227
+ self.history = self.store.load(resume)
228
+ if not self.history:
229
+ console.print(
230
+ f"[yellow]警告:[/yellow] session [cyan]{resume}[/cyan]"
231
+ " 没有可恢复的历史"
232
+ )
233
+ else:
234
+ self.session_id = (
235
+ session_id or f"sess-{int(time.time())}"
236
+ )
237
+ self.history = []
238
+
239
+ self.state = ShellState(
240
+ config_path=CONFIG_FILE,
241
+ config=self.config,
242
+ active_provider=active_provider,
243
+ active_model=active_model,
244
+ runtime=self.runtime,
245
+ subagent_system=self.subagent_system,
246
+ renderer=self.renderer,
247
+ store=self.store,
248
+ workspace=workspace,
249
+ session_id=self.session_id,
250
+ approve_mode=approve_mode,
251
+ stream=stream,
252
+ subagents=subagents,
253
+ max_steps=max_steps,
254
+ step_timeout_s=step_timeout_s,
255
+ total_timeout_s=total_timeout_s,
256
+ session=(
257
+ self.runtime.create_session(
258
+ session_id=self.session_id,
259
+ environment=SessionEnvironment(
260
+ workspace_root=workspace,
261
+ working_directory=workspace,
262
+ ),
263
+ policy=SessionPolicy(
264
+ allow_write=True,
265
+ allow_dangerous=True,
266
+ max_parallel_actions=1,
267
+ ),
268
+ conversation_store=self.store,
269
+ )
270
+ if self.runtime is not None
271
+ else None
272
+ ),
273
+ history=self.history,
274
+ )
275
+ # Slash completion remains the same; TUI only changes its visual host.
276
+ self.state.completer = MindcodeCompleter(self.state)
277
+
278
+ # 并发队列 + 忙标记
279
+ self._queue: asyncio.Queue[str | None] = asyncio.Queue()
280
+ self._busy: bool = False
281
+ self._tui: MindcodeTui | None = None
282
+
283
+ async def run(self) -> int:
284
+ self._tui = MindcodeTui(
285
+ self.state,
286
+ Transcript(),
287
+ on_submit=self._submit_from_tui,
288
+ on_exit=self._request_exit,
289
+ status_provider=self._status_text,
290
+ )
291
+ self.renderer.attach_transcript(self._tui.transcript)
292
+ self.state.choose_option = self._tui.choose
293
+ self._print_banner()
294
+ process_task = asyncio.create_task(self._process_loop())
295
+ tui_task = asyncio.create_task(self._tui.run())
296
+ try:
297
+ with redirect_console_output(self._tui.transcript):
298
+ await process_task
299
+ finally:
300
+ self._tui.exit()
301
+ try:
302
+ await tui_task
303
+ except asyncio.CancelledError:
304
+ pass
305
+ self.renderer.detach_transcript()
306
+ await self.state.close()
307
+ return 0
308
+
309
+ async def _submit_from_tui(self, text: str) -> None:
310
+ """Keep the existing queue semantics behind the new fixed composer."""
311
+ text = text.strip()
312
+ if not text:
313
+ return
314
+ if not text.startswith("/") and self._tui is not None:
315
+ # Echo immediately, including while a previous agent turn is
316
+ # running, so queued work remains visible to the user.
317
+ self._tui.add_user_message(text)
318
+ await self._queue.put(text)
319
+ if self._busy and self._tui is not None:
320
+ self._tui.transcript.add_system(
321
+ f"已加入队列(第 {self._queue.qsize()} 项)"
322
+ )
323
+
324
+ async def _request_exit(self) -> None:
325
+ await self._queue.put(None)
326
+
327
+ async def _process_loop(self) -> None:
328
+ """从队列取消息,依次处理。None 表示退出。"""
329
+ while True:
330
+ text = await self._queue.get()
331
+ if text is None:
332
+ console.print("[dim]bye[/dim]")
333
+ return
334
+ self._set_busy(True)
335
+ try:
336
+ signal = await self._dispatch(text)
337
+ if signal == "exit":
338
+ console.print("[dim]bye[/dim]")
339
+ return
340
+ except KeyboardInterrupt:
341
+ console.print("\n[yellow]interrupted[/yellow]")
342
+ finally:
343
+ self._set_busy(False)
344
+ self.store.save(self.state.session_id, self.state.history)
345
+
346
+ async def _dispatch(self, text: str) -> str | None:
347
+ """分发:slash 命令 vs agent turn。"""
348
+ if text.startswith("/"):
349
+ return await handle_slash_command(text, self.state)
350
+ await self._run_turn(text)
351
+ return None
352
+
353
+ def _print_banner(self) -> None:
354
+ provider = self.config.providers.get(self.state.active_provider)
355
+ if self._tui is not None:
356
+ self._tui.transcript.add_system(
357
+ "已就绪。输入消息开始对话;/help 查看命令;/exit 退出。"
358
+ )
359
+ if self.state.history:
360
+ self._tui.transcript.add_system(
361
+ f"已恢复 {len(self.state.history)} 条历史消息。"
362
+ )
363
+ if not provider or not provider.api_key:
364
+ self._tui.transcript.add_system(
365
+ "提示:当前 provider 还未配置 API key。"
366
+ )
367
+ return
368
+ console.print(
369
+ build_welcome_banner(
370
+ model=self.state.active_model,
371
+ provider=self.state.active_provider,
372
+ workspace=str(self.workspace),
373
+ session_id=self.state.session_id,
374
+ approve_mode=approval_mode_label(self.state.approve_mode),
375
+ api_key_set=bool(provider and provider.api_key),
376
+ )
377
+ )
378
+ if self.state.history:
379
+ console.print(
380
+ f" [dim]resumed {len(self.state.history)} messages[/dim]"
381
+ )
382
+
383
+ async def _run_turn(self, user_input: str) -> None:
384
+ self.renderer.reset()
385
+ if self.state.subagent_system is not None:
386
+ result = await self.state.subagent_system.run(
387
+ user_input,
388
+ messages=self.state.history,
389
+ session_environment=SessionEnvironment(
390
+ workspace_root=self.workspace,
391
+ ),
392
+ session_policy=SessionPolicy(
393
+ allowed_action_types=frozenset({ActionType.AGENT}),
394
+ allow_write=False,
395
+ allow_dangerous=False,
396
+ max_parallel_actions=1,
397
+ ),
398
+ )
399
+ self.state.last_run_id = result.run_id
400
+ self.state.history.extend(
401
+ [
402
+ {"role": "user", "content": user_input},
403
+ {
404
+ "role": "assistant",
405
+ "content": result.final_answer or result.error or "",
406
+ },
407
+ ]
408
+ )
409
+ else:
410
+ result = await self._run_session_turn(user_input)
411
+
412
+ if getattr(self, "_tui", None) is None:
413
+ console.print(f"[dim]run_id={result.run_id}[/dim]")
414
+
415
+ if result.error:
416
+ error_info = getattr(result, "error_info", None)
417
+ recoverable = (
418
+ error_info is not None
419
+ and getattr(error_info, "recoverable", False)
420
+ )
421
+ tag = "[yellow]pause›[/yellow]" if recoverable else "[red]error›[/red]"
422
+ console.print(f"\n{tag} {result.error}")
423
+ if not recoverable:
424
+ return
425
+ console.print("[dim]已保存进度,发送新消息可继续[/dim]")
426
+ return
427
+
428
+ self.renderer._flush_stream()
429
+ if not self.renderer._did_stream and result.final_answer:
430
+ render_final_answer = getattr(
431
+ self.renderer, "render_final_answer", None
432
+ )
433
+ if render_final_answer is not None:
434
+ render_final_answer(result.final_answer)
435
+ else:
436
+ console.print(
437
+ f"\n[bold cyan]answer›[/bold cyan] {result.final_answer}"
438
+ )
439
+
440
+ async def _run_session_turn(self, user_input: str) -> Any:
441
+ """Run through AgentSession and keep the legacy history view synced."""
442
+ session = await self._ensure_session()
443
+ checkpoint = session.pending_checkpoint
444
+ if checkpoint is None:
445
+ command_id = await session.submit_run(user_input)
446
+ else:
447
+ command_id = await session.continue_run(
448
+ checkpoint.checkpoint_id,
449
+ user_input=user_input,
450
+ )
451
+ while True:
452
+ event = await session.next_event()
453
+ if event.event_type == SessionEventType.RUN_ACCEPTED:
454
+ self.state.last_run_id = event.run_id
455
+ continue
456
+ if event.event_type == SessionEventType.APPROVAL_REQUIRED:
457
+ # 问答与审批暂共享 WAITING 通道:仅提示,不阻塞,继续等后续事件。
458
+ # 审批走 session.approve_run/reject_run(见 approve_current_run/
459
+ # reject_current_run),问答走 submit_user_input。
460
+ console.print(
461
+ f"[yellow]等待审批/输入,run_id={event.run_id}[/yellow]"
462
+ )
463
+ continue
464
+ if event.event_type == SessionEventType.APPROVAL_DECIDED:
465
+ decision = event.payload.get("decision", "decided")
466
+ console.print(
467
+ f"[dim]审批已决策({decision}),run_id={event.run_id},"
468
+ "等待 run 完成[/dim]"
469
+ )
470
+ continue
471
+ if (
472
+ event.event_type == SessionEventType.RUN_COMPLETED
473
+ and event.correlation_id == command_id
474
+ ):
475
+ result = event.payload.get("result")
476
+ if result is None:
477
+ reason = event.payload.get("error", "session run failed")
478
+ result = _SessionErrorResult(
479
+ run_id=event.run_id or self.state.last_run_id or "",
480
+ error=str(reason),
481
+ )
482
+ self.state.history[:] = session.messages
483
+ return result
484
+ if (
485
+ event.event_type == SessionEventType.COMMAND_REJECTED
486
+ and event.correlation_id == command_id
487
+ ):
488
+ reason = event.payload.get("reason", "session command rejected")
489
+ return _SessionErrorResult(
490
+ run_id=event.run_id or self.state.last_run_id or "",
491
+ error=str(reason),
492
+ )
493
+
494
+ async def approve_current_run(self, comment: str = "approved") -> str:
495
+ """审批通过当前 run:透调 session.approve_run(REPL 便捷调用点)。"""
496
+ session = self.state.session
497
+ if session is None or not self.state.last_run_id:
498
+ raise RuntimeError("当前没有可审批的 run")
499
+ return await session.approve_run(self.state.last_run_id, comment)
500
+
501
+ async def reject_current_run(self, reason: str = "rejected") -> str:
502
+ """审批拒绝当前 run:透调 session.reject_run(REPL 便捷调用点)。"""
503
+ session = self.state.session
504
+ if session is None or not self.state.last_run_id:
505
+ raise RuntimeError("当前没有可拒绝的 run")
506
+ return await session.reject_run(self.state.last_run_id, reason)
507
+
508
+ async def _ensure_session(self) -> AgentSession:
509
+ """Reconcile slash-command edits before submitting a new run."""
510
+ runtime = self.state.runtime
511
+ if runtime is None:
512
+ raise RuntimeError("当前 shell 没有可用 runtime")
513
+ session = self.state.session
514
+ if session is None:
515
+ self.state.session = self._new_session(runtime)
516
+ return self.state.session
517
+ if session.messages == self.state.history:
518
+ return session
519
+
520
+ # Slash commands mutate the compatibility history list. Persist that
521
+ # explicit product-layer edit, then replace the actor so its next run
522
+ # loads exactly the same transcript from ConversationStore.
523
+ self.state.store.save(self.state.session_id, self.state.history)
524
+ await runtime.close_session(session.session_id)
525
+ self.state.session = self._new_session(runtime)
526
+ return self.state.session
527
+
528
+ def _new_session(self, runtime: AgentRuntime) -> AgentSession:
529
+ return runtime.create_session(
530
+ session_id=self.state.session_id,
531
+ environment=SessionEnvironment(
532
+ workspace_root=self.state.workspace,
533
+ working_directory=self.state.workspace,
534
+ ),
535
+ policy=SessionPolicy(
536
+ allow_write=True,
537
+ allow_dangerous=True,
538
+ max_parallel_actions=1,
539
+ ),
540
+ conversation_store=self.state.store,
541
+ )
542
+
543
+ def _status_text(self) -> str:
544
+ return (
545
+ f"{self.state.active_provider}/{self.state.active_model} · "
546
+ f"{approval_mode_label(self.state.approve_mode)} · "
547
+ f"{self.state.workspace}"
548
+ )
549
+
550
+ def _set_busy(self, busy: bool) -> None:
551
+ self._busy = busy
552
+ if self._tui is not None:
553
+ self._tui.set_working(busy)
554
+
555
+
556
+ def run_shell(
557
+ workspace: Path,
558
+ *,
559
+ approve_mode: ApprovalMode,
560
+ provider_name: str | None,
561
+ model: str | None,
562
+ session_id: str | None,
563
+ resume: str | None,
564
+ stream: bool,
565
+ subagents: bool = False,
566
+ one_shot_prompt: str | None = None,
567
+ max_steps: int = 50,
568
+ step_timeout_s: float | None = 300.0,
569
+ total_timeout_s: float | None = 1800.0,
570
+ ) -> int:
571
+ """Synchronous entry: build Shell and run it (or one-shot)."""
572
+ shell = Shell(
573
+ workspace,
574
+ approve_mode=approve_mode,
575
+ provider_name=provider_name,
576
+ model=model,
577
+ session_id=session_id,
578
+ resume=resume,
579
+ stream=stream,
580
+ subagents=subagents,
581
+ max_steps=max_steps,
582
+ step_timeout_s=step_timeout_s,
583
+ total_timeout_s=total_timeout_s,
584
+ )
585
+
586
+ if one_shot_prompt:
587
+ return asyncio.run(_one_shot(shell, one_shot_prompt))
588
+ return asyncio.run(shell.run())
589
+
590
+
591
+ async def _one_shot(shell: Shell, prompt: str) -> int:
592
+ try:
593
+ await shell._run_turn(prompt)
594
+ shell.store.save(shell.state.session_id, shell.state.history)
595
+ return 0
596
+ finally:
597
+ await shell.state.close()