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.
- mindcode/__init__.py +5 -0
- mindcode/__main__.py +4 -0
- mindcode/__pycache__/__init__.cpython-313.pyc +0 -0
- mindcode/__pycache__/__main__.cpython-313.pyc +0 -0
- mindcode/__pycache__/_version.cpython-313.pyc +0 -0
- mindcode/__pycache__/config.cpython-313.pyc +0 -0
- mindcode/__pycache__/policy.cpython-313.pyc +0 -0
- mindcode/__pycache__/remote.cpython-313.pyc +0 -0
- mindcode/__pycache__/render.cpython-313.pyc +0 -0
- mindcode/__pycache__/runtime.cpython-313.pyc +0 -0
- mindcode/__pycache__/tasking.cpython-313.pyc +0 -0
- mindcode/__pycache__/terminal_core.cpython-313.pyc +0 -0
- mindcode/_version.py +11 -0
- mindcode/approval.py +105 -0
- mindcode/bench/__init__.py +19 -0
- mindcode/bench/__pycache__/__init__.cpython-313.pyc +0 -0
- mindcode/bench/__pycache__/fake_tools.cpython-313.pyc +0 -0
- mindcode/bench/__pycache__/jsonutil.cpython-313.pyc +0 -0
- mindcode/bench/__pycache__/paths.cpython-313.pyc +0 -0
- mindcode/bench/__pycache__/predictions.cpython-313.pyc +0 -0
- mindcode/bench/__pycache__/report.cpython-313.pyc +0 -0
- mindcode/bench/__pycache__/runner.cpython-313.pyc +0 -0
- mindcode/bench/__pycache__/schema.cpython-313.pyc +0 -0
- mindcode/bench/__pycache__/trace.cpython-313.pyc +0 -0
- mindcode/bench/__pycache__/workspace.cpython-313.pyc +0 -0
- mindcode/bench/adapters/__init__.py +22 -0
- mindcode/bench/adapters/__pycache__/__init__.cpython-313.pyc +0 -0
- mindcode/bench/adapters/__pycache__/base.cpython-313.pyc +0 -0
- mindcode/bench/adapters/__pycache__/mini_bfcl.cpython-313.pyc +0 -0
- mindcode/bench/adapters/__pycache__/mini_gaia.cpython-313.pyc +0 -0
- mindcode/bench/adapters/__pycache__/mini_terminal.cpython-313.pyc +0 -0
- mindcode/bench/adapters/__pycache__/swe_bench.cpython-313.pyc +0 -0
- mindcode/bench/adapters/__pycache__/terminal_bench.cpython-313.pyc +0 -0
- mindcode/bench/adapters/base.py +45 -0
- mindcode/bench/adapters/mini_bfcl.py +18 -0
- mindcode/bench/adapters/mini_gaia.py +18 -0
- mindcode/bench/adapters/mini_terminal.py +18 -0
- mindcode/bench/adapters/swe_bench.py +86 -0
- mindcode/bench/adapters/terminal_bench.py +107 -0
- mindcode/bench/fake_tools.py +73 -0
- mindcode/bench/jsonutil.py +20 -0
- mindcode/bench/paths.py +10 -0
- mindcode/bench/predictions.py +65 -0
- mindcode/bench/report.py +48 -0
- mindcode/bench/runner.py +250 -0
- mindcode/bench/schema.py +78 -0
- mindcode/bench/scorers/__pycache__/base.cpython-313.pyc +0 -0
- mindcode/bench/scorers/__pycache__/composite.cpython-313.pyc +0 -0
- mindcode/bench/scorers/__pycache__/exact.cpython-313.pyc +0 -0
- mindcode/bench/scorers/__pycache__/json_call.cpython-313.pyc +0 -0
- mindcode/bench/scorers/__pycache__/swe.cpython-313.pyc +0 -0
- mindcode/bench/scorers/__pycache__/terminal.cpython-313.pyc +0 -0
- mindcode/bench/scorers/__pycache__/terminal_bench.cpython-313.pyc +0 -0
- mindcode/bench/scorers/base.py +22 -0
- mindcode/bench/scorers/composite.py +57 -0
- mindcode/bench/scorers/exact.py +30 -0
- mindcode/bench/scorers/json_call.py +44 -0
- mindcode/bench/scorers/swe.py +29 -0
- mindcode/bench/scorers/terminal.py +43 -0
- mindcode/bench/scorers/terminal_bench.py +25 -0
- mindcode/bench/trace.py +34 -0
- mindcode/bench/workspace.py +127 -0
- mindcode/cli/__init__.py +112 -0
- mindcode/cli/__pycache__/__init__.cpython-313.pyc +0 -0
- mindcode/cli/__pycache__/_shared.cpython-313.pyc +0 -0
- mindcode/cli/_shared.py +43 -0
- mindcode/cli/commands/__init__.py +1 -0
- mindcode/cli/commands/__pycache__/__init__.cpython-313.pyc +0 -0
- mindcode/cli/commands/__pycache__/bench.cpython-313.pyc +0 -0
- mindcode/cli/commands/__pycache__/chat.cpython-313.pyc +0 -0
- mindcode/cli/commands/__pycache__/config_cmd.cpython-313.pyc +0 -0
- mindcode/cli/commands/__pycache__/remote.cpython-313.pyc +0 -0
- mindcode/cli/commands/__pycache__/shell.cpython-313.pyc +0 -0
- mindcode/cli/commands/__pycache__/status.cpython-313.pyc +0 -0
- mindcode/cli/commands/__pycache__/task.cpython-313.pyc +0 -0
- mindcode/cli/commands/__pycache__/terminal.cpython-313.pyc +0 -0
- mindcode/cli/commands/bench.py +365 -0
- mindcode/cli/commands/chat.py +86 -0
- mindcode/cli/commands/config_cmd.py +197 -0
- mindcode/cli/commands/remote.py +146 -0
- mindcode/cli/commands/shell.py +108 -0
- mindcode/cli/commands/status.py +37 -0
- mindcode/cli/commands/task.py +417 -0
- mindcode/cli/commands/terminal.py +124 -0
- mindcode/cli/shell/__init__.py +5 -0
- mindcode/cli/shell/__pycache__/__init__.cpython-313.pyc +0 -0
- mindcode/cli/shell/__pycache__/completion.cpython-313.pyc +0 -0
- mindcode/cli/shell/__pycache__/menu.cpython-313.pyc +0 -0
- mindcode/cli/shell/__pycache__/repl.cpython-313.pyc +0 -0
- mindcode/cli/shell/__pycache__/slash.cpython-313.pyc +0 -0
- mindcode/cli/shell/__pycache__/startup.cpython-313.pyc +0 -0
- mindcode/cli/shell/completion.py +72 -0
- mindcode/cli/shell/menu.py +88 -0
- mindcode/cli/shell/repl.py +597 -0
- mindcode/cli/shell/slash.py +480 -0
- mindcode/cli/shell/startup.py +55 -0
- mindcode/cli/shell/tui.py +897 -0
- mindcode/config.py +173 -0
- mindcode/mcp.py +205 -0
- mindcode/policy.py +173 -0
- mindcode/remote.py +216 -0
- mindcode/render.py +458 -0
- mindcode/runtime.py +541 -0
- mindcode/skills.py +154 -0
- mindcode/subagents.py +43 -0
- mindcode/tasking.py +281 -0
- mindcode/terminal/__init__.py +1 -0
- mindcode/terminal/__main__.py +17 -0
- mindcode/terminal/__pycache__/__init__.cpython-313.pyc +0 -0
- mindcode/terminal/__pycache__/__main__.cpython-313.pyc +0 -0
- mindcode/terminal_core.py +264 -0
- mindcode-0.2.0.dist-info/METADATA +244 -0
- mindcode-0.2.0.dist-info/RECORD +116 -0
- mindcode-0.2.0.dist-info/WHEEL +5 -0
- mindcode-0.2.0.dist-info/entry_points.txt +2 -0
- 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()
|