mosscode 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.
- mosscode/__init__.py +10 -0
- mosscode/__main__.py +3 -0
- mosscode/agent.py +486 -0
- mosscode/agents/__init__.py +249 -0
- mosscode/agents/builtin/explore.md +16 -0
- mosscode/agents/builtin/general.md +12 -0
- mosscode/agents/builtin/plan.md +16 -0
- mosscode/agents/runtime.py +685 -0
- mosscode/agents/tasks.py +193 -0
- mosscode/appearance.py +184 -0
- mosscode/builtin_commands.py +1363 -0
- mosscode/cli.py +547 -0
- mosscode/commands.py +254 -0
- mosscode/compaction.py +259 -0
- mosscode/config.py +337 -0
- mosscode/context.py +102 -0
- mosscode/conversation.py +1046 -0
- mosscode/coordinator.py +54 -0
- mosscode/doctor.py +303 -0
- mosscode/hooks/__init__.py +22 -0
- mosscode/hooks/actions.py +151 -0
- mosscode/hooks/conditions.py +170 -0
- mosscode/hooks/config.py +404 -0
- mosscode/hooks/engine.py +401 -0
- mosscode/hooks/events.py +48 -0
- mosscode/launcher.py +177 -0
- mosscode/mcp_client.py +460 -0
- mosscode/mcp_config.py +216 -0
- mosscode/member_ui.py +289 -0
- mosscode/memory.py +291 -0
- mosscode/memory_extract.py +245 -0
- mosscode/messages.py +216 -0
- mosscode/paths.py +31 -0
- mosscode/permissions.py +733 -0
- mosscode/prompts.py +483 -0
- mosscode/providers.py +587 -0
- mosscode/scrollback.py +106 -0
- mosscode/security.py +282 -0
- mosscode/serialization.py +121 -0
- mosscode/session.py +563 -0
- mosscode/setup.py +248 -0
- mosscode/skills/__init__.py +455 -0
- mosscode/skills/builtin/commit/SKILL.md +15 -0
- mosscode/skills/builtin/review/SKILL.md +15 -0
- mosscode/skills/builtin/test/SKILL.md +13 -0
- mosscode/skills/builtin/test/scripts/detect.py +96 -0
- mosscode/skills/builtin/test/tool.json +9 -0
- mosscode/teams/__init__.py +216 -0
- mosscode/teams/backends.py +259 -0
- mosscode/teams/board.py +230 -0
- mosscode/teams/lock.py +173 -0
- mosscode/teams/mailbox.py +172 -0
- mosscode/teams/member.py +427 -0
- mosscode/teams/merge.py +207 -0
- mosscode/teams/pane.py +20 -0
- mosscode/teams/pane_app.py +167 -0
- mosscode/teams/process.py +371 -0
- mosscode/teams/runtime.py +1355 -0
- mosscode/teams/view.py +182 -0
- mosscode/tool_budget.py +86 -0
- mosscode/tools/__init__.py +38 -0
- mosscode/tools/agent.py +127 -0
- mosscode/tools/base.py +384 -0
- mosscode/tools/diffing.py +16 -0
- mosscode/tools/execution.py +99 -0
- mosscode/tools/files.py +287 -0
- mosscode/tools/memory.py +65 -0
- mosscode/tools/plan.py +30 -0
- mosscode/tools/process.py +216 -0
- mosscode/tools/search.py +229 -0
- mosscode/tools/search_worker.py +243 -0
- mosscode/tools/shell.py +121 -0
- mosscode/tools/skill.py +118 -0
- mosscode/tools/team.py +447 -0
- mosscode/tools/windows_job.py +75 -0
- mosscode/tui.py +2325 -0
- mosscode/usage.py +68 -0
- mosscode/windows_inline.py +148 -0
- mosscode/worktree/__init__.py +29 -0
- mosscode/worktree/changes.py +59 -0
- mosscode/worktree/git.py +125 -0
- mosscode/worktree/manager.py +462 -0
- mosscode/worktree/meta.py +82 -0
- mosscode/worktree/names.py +52 -0
- mosscode/worktree/setup.py +119 -0
- mosscode-0.1.0.dist-info/METADATA +92 -0
- mosscode-0.1.0.dist-info/RECORD +91 -0
- mosscode-0.1.0.dist-info/WHEEL +5 -0
- mosscode-0.1.0.dist-info/entry_points.txt +3 -0
- mosscode-0.1.0.dist-info/licenses/LICENSE +21 -0
- mosscode-0.1.0.dist-info/top_level.txt +1 -0
mosscode/cli.py
ADDED
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import asyncio
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
from contextlib import aclosing
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from time import monotonic
|
|
9
|
+
|
|
10
|
+
from .agents import AgentCatalog
|
|
11
|
+
from .agents.tasks import Announcer
|
|
12
|
+
from .appearance import compact_notice
|
|
13
|
+
from .builtin_commands import BUILTIN, typed
|
|
14
|
+
from .commands import CommandRegistry, argument_problem, parse_input, unknown_note
|
|
15
|
+
from .config import ConfigError, find_config, load_settings
|
|
16
|
+
from .conversation import Conversation, Notice, Resumed
|
|
17
|
+
from .coordinator import decide as decide_coordinator
|
|
18
|
+
from .hooks import HookEngine
|
|
19
|
+
from .launcher import is_terminal, parse_arguments
|
|
20
|
+
from .mcp_client import MCPManager
|
|
21
|
+
from .mcp_config import MCPConfigError, load_mcp_settings
|
|
22
|
+
from .memory import MemoryStore
|
|
23
|
+
from .messages import (
|
|
24
|
+
CompactFailed,
|
|
25
|
+
CompactFinished,
|
|
26
|
+
CompactStarted,
|
|
27
|
+
Completed,
|
|
28
|
+
Delta,
|
|
29
|
+
Message,
|
|
30
|
+
ProviderError,
|
|
31
|
+
ResponseBoundary,
|
|
32
|
+
Retrying,
|
|
33
|
+
RunStopped,
|
|
34
|
+
ToolProgress,
|
|
35
|
+
UsageUpdate,
|
|
36
|
+
)
|
|
37
|
+
from .permissions import PermissionConfigError
|
|
38
|
+
from .providers import create_provider
|
|
39
|
+
from .session import Session, prune_all
|
|
40
|
+
from .skills import ForkRequest, SkillCatalog
|
|
41
|
+
from .tools import create_default_registry
|
|
42
|
+
from .tools.shell import ShellNotFound
|
|
43
|
+
from .tui import MossCodeApp, ProviderSelectApp, plan_text
|
|
44
|
+
from .usage import request_usage_text
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _headless_input(
|
|
48
|
+
text: str, commands: CommandRegistry = BUILTIN, notices: tuple[str, ...] = ()
|
|
49
|
+
) -> tuple[str, str | None, str | None]:
|
|
50
|
+
"""-p sends messages, prompt commands and skills; returns (prompt, command, skill) or exits with 2.
|
|
51
|
+
|
|
52
|
+
For a skill the prompt is its argument: the request is rendered once the tools are known.
|
|
53
|
+
"""
|
|
54
|
+
parsed = parse_input(text, commands)
|
|
55
|
+
if parsed.kind in {"empty", "message"}:
|
|
56
|
+
return (parsed.text if parsed.kind == "message" else text), None, None
|
|
57
|
+
if parsed.kind == "unknown":
|
|
58
|
+
problem = unknown_note(parsed, commands)
|
|
59
|
+
elif parsed.command.kind == "local":
|
|
60
|
+
problem = f"/{parsed.command.name} 只能在交互界面中使用。"
|
|
61
|
+
else:
|
|
62
|
+
problem = argument_problem(parsed.command, parsed.argument)
|
|
63
|
+
if problem:
|
|
64
|
+
for notice in notices: # Say why a skill is missing before the run ends here.
|
|
65
|
+
print(notice, file=sys.stderr)
|
|
66
|
+
print(problem, file=sys.stderr)
|
|
67
|
+
raise SystemExit(2)
|
|
68
|
+
if parsed.command.kind == "skill":
|
|
69
|
+
return parsed.argument, typed(parsed.command, parsed.argument), parsed.command.skill
|
|
70
|
+
return parsed.command.expand(parsed.argument), typed(parsed.command, parsed.argument), None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _skill_prompt(conversation: Conversation, skill: str | None, prompt: str, report):
|
|
74
|
+
"""Check the skill's tools once MCP is up, then send it like the TUI does; returns (prompt, fork)."""
|
|
75
|
+
if skill is None:
|
|
76
|
+
return prompt, None
|
|
77
|
+
found, rendered, notices = conversation.prepare_skill(skill, prompt)
|
|
78
|
+
for notice in notices:
|
|
79
|
+
report("notice", notice)
|
|
80
|
+
if found.mode != "fork":
|
|
81
|
+
return rendered, None
|
|
82
|
+
report("notice", f"在独立上下文中运行 Skill {found.name}")
|
|
83
|
+
return rendered, ForkRequest(found, rendered)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _arguments(arguments: str):
|
|
87
|
+
try:
|
|
88
|
+
return json.loads(arguments)
|
|
89
|
+
except ValueError:
|
|
90
|
+
return arguments
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _assistant(message: Message, usage, stop_reason: str | None) -> dict:
|
|
94
|
+
return {
|
|
95
|
+
"type": "assistant",
|
|
96
|
+
"text": message.text,
|
|
97
|
+
"tool_calls": [{"id": c.id, "name": c.name, "arguments": _arguments(c.arguments)} for c in message.tool_calls],
|
|
98
|
+
"stop_reason": stop_reason,
|
|
99
|
+
"usage": _usage_dict(usage),
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _usage_dict(usage) -> dict | None:
|
|
104
|
+
if usage is None:
|
|
105
|
+
return None
|
|
106
|
+
return {
|
|
107
|
+
"input_tokens": usage.input_tokens,
|
|
108
|
+
"output_tokens": usage.output_tokens,
|
|
109
|
+
"cached_input_tokens": usage.cached_input_tokens,
|
|
110
|
+
"cache_write_tokens": usage.cache_write_tokens,
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _emit(data: dict) -> None:
|
|
115
|
+
print(json.dumps(data, ensure_ascii=False), flush=True)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _compact_event(event) -> dict:
|
|
119
|
+
if isinstance(event, CompactStarted):
|
|
120
|
+
return {"type": "compact", "subtype": "started", "manual": event.manual}
|
|
121
|
+
if isinstance(event, CompactFinished):
|
|
122
|
+
return {
|
|
123
|
+
"type": "compact",
|
|
124
|
+
"subtype": "finished",
|
|
125
|
+
"removed": event.removed,
|
|
126
|
+
"summary_tokens": event.summary_tokens,
|
|
127
|
+
"duration_ms": round(event.elapsed * 1000),
|
|
128
|
+
"record": event.record,
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
"type": "compact",
|
|
132
|
+
"subtype": "failed",
|
|
133
|
+
"reason": event.reason,
|
|
134
|
+
"consecutive": event.consecutive,
|
|
135
|
+
"paused": event.paused,
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
async def _resume(conversation: Conversation, session_id: str | None, report) -> None:
|
|
140
|
+
"""Restore before the task; report(kind, value) prints notices and compaction progress."""
|
|
141
|
+
if session_id is None:
|
|
142
|
+
return
|
|
143
|
+
async with aclosing(conversation.resume(session_id)) as stream:
|
|
144
|
+
async for event in stream:
|
|
145
|
+
if isinstance(event, Resumed):
|
|
146
|
+
restored = event.restored
|
|
147
|
+
report("notice", f"已恢复会话 {restored.id} · {restored.title} · {event.messages} 条消息")
|
|
148
|
+
for notice in event.notices:
|
|
149
|
+
report("notice", notice)
|
|
150
|
+
for text in restored.unanswered:
|
|
151
|
+
# Never resent automatically; shown so the user can decide.
|
|
152
|
+
report("unanswered", text)
|
|
153
|
+
elif isinstance(event, Notice):
|
|
154
|
+
report("notice", event.text)
|
|
155
|
+
else:
|
|
156
|
+
report("compact", event)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _background_notices(conversation: Conversation, report) -> None:
|
|
160
|
+
"""Hook failures, background ones included, go to stderr (stream-json: a notice event)."""
|
|
161
|
+
conversation.on_memory = lambda event: report("notice", event.text) if isinstance(event, Notice) else None
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _task_notices(conversation: Conversation, report) -> None:
|
|
165
|
+
"""A line on stderr (stream-json: a notice event) when a background task starts or ends."""
|
|
166
|
+
conversation.tasks.on_change = Announcer(lambda text: report("notice", text))
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
async def _turns(conversation: Conversation, prompt: str, *, command: str | None, fork):
|
|
170
|
+
"""The task's turn, then one turn per batch of notifications until no task is left running."""
|
|
171
|
+
async with aclosing(conversation.reply(prompt, command=command, fork=fork)) as stream:
|
|
172
|
+
async for event in stream:
|
|
173
|
+
yield event
|
|
174
|
+
tasks = conversation.tasks
|
|
175
|
+
while True:
|
|
176
|
+
if conversation.notifications:
|
|
177
|
+
async with aclosing(conversation.notify()) as stream:
|
|
178
|
+
async for event in stream:
|
|
179
|
+
yield event
|
|
180
|
+
continue
|
|
181
|
+
if not tasks.running():
|
|
182
|
+
return
|
|
183
|
+
tasks.finished.clear()
|
|
184
|
+
await tasks.finished.wait()
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _hook_engine(args, registry, *, interactive: bool):
|
|
188
|
+
"""--no-hooks loads nothing; -p skips untrusted project files unless --trust-project-hooks."""
|
|
189
|
+
tool = registry.get("execute_command")
|
|
190
|
+
engine = HookEngine(
|
|
191
|
+
Path.cwd(),
|
|
192
|
+
shell=getattr(tool, "shell", None),
|
|
193
|
+
enabled=not args.no_hooks,
|
|
194
|
+
trust_project=args.trust_project_hooks,
|
|
195
|
+
)
|
|
196
|
+
notices = list(engine.problems)
|
|
197
|
+
if not interactive:
|
|
198
|
+
notices += [
|
|
199
|
+
f"项目 Hook 文件 {file.path} 未经确认,未启用;请在交互界面中启动一次并确认,或使用 --trust-project-hooks。"
|
|
200
|
+
for file in engine.untrusted
|
|
201
|
+
]
|
|
202
|
+
return engine, tuple(notices)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
async def _prompt(
|
|
206
|
+
conversation: Conversation,
|
|
207
|
+
prompt: str,
|
|
208
|
+
*,
|
|
209
|
+
show_usage: bool = False,
|
|
210
|
+
output_format: str = "text",
|
|
211
|
+
model: str = "",
|
|
212
|
+
resume: str | None = None,
|
|
213
|
+
command: str | None = None,
|
|
214
|
+
skill: str | None = None,
|
|
215
|
+
) -> dict | None:
|
|
216
|
+
"""Run one headless task; JSON formats return the result object instead of raising."""
|
|
217
|
+
if output_format == "text":
|
|
218
|
+
await _text_prompt(conversation, prompt, show_usage=show_usage, resume=resume, command=command, skill=skill)
|
|
219
|
+
return None
|
|
220
|
+
stream_json = output_format == "stream-json"
|
|
221
|
+
started = monotonic()
|
|
222
|
+
stopped: RunStopped | None = None
|
|
223
|
+
final = ""
|
|
224
|
+
error: str | None = None
|
|
225
|
+
|
|
226
|
+
def report(kind, value):
|
|
227
|
+
if kind == "compact":
|
|
228
|
+
if stream_json:
|
|
229
|
+
_emit(_compact_event(value))
|
|
230
|
+
elif stream_json:
|
|
231
|
+
_emit({"type": "notice", "text": value if kind == "notice" else "未得到回复的消息:" + value})
|
|
232
|
+
else:
|
|
233
|
+
print(value if kind == "notice" else "未得到回复的消息:" + value, file=sys.stderr, flush=True)
|
|
234
|
+
|
|
235
|
+
_background_notices(conversation, report)
|
|
236
|
+
_task_notices(conversation, report)
|
|
237
|
+
try:
|
|
238
|
+
try:
|
|
239
|
+
if stream_json:
|
|
240
|
+
_emit(
|
|
241
|
+
{
|
|
242
|
+
"type": "session",
|
|
243
|
+
"id": conversation.session_id if resume is None else resume,
|
|
244
|
+
"resumed": resume is not None,
|
|
245
|
+
}
|
|
246
|
+
)
|
|
247
|
+
await _resume(conversation, resume, report)
|
|
248
|
+
notices = await conversation.start()
|
|
249
|
+
for notice in notices:
|
|
250
|
+
if stream_json:
|
|
251
|
+
_emit({"type": "notice", "text": notice})
|
|
252
|
+
else:
|
|
253
|
+
print(notice, file=sys.stderr, flush=True)
|
|
254
|
+
prompt, fork = _skill_prompt(conversation, skill, prompt, report)
|
|
255
|
+
registry = conversation.registry
|
|
256
|
+
if stream_json:
|
|
257
|
+
_emit(
|
|
258
|
+
{
|
|
259
|
+
"type": "system",
|
|
260
|
+
"subtype": "init",
|
|
261
|
+
"cwd": str(Path.cwd()),
|
|
262
|
+
"model": model,
|
|
263
|
+
"permission_mode": registry.permissions.mode if registry else None,
|
|
264
|
+
"tools": [d.name for d in registry.definitions()] if registry else [],
|
|
265
|
+
}
|
|
266
|
+
)
|
|
267
|
+
usage = None
|
|
268
|
+
async with aclosing(_turns(conversation, prompt, command=command, fork=fork)) as stream:
|
|
269
|
+
async for event in stream:
|
|
270
|
+
if isinstance(event, UsageUpdate):
|
|
271
|
+
usage = event.usage
|
|
272
|
+
elif isinstance(event, RunStopped):
|
|
273
|
+
stopped = event
|
|
274
|
+
elif isinstance(event, Completed):
|
|
275
|
+
final = event.message.text
|
|
276
|
+
if not stream_json:
|
|
277
|
+
continue
|
|
278
|
+
if isinstance(event, ResponseBoundary):
|
|
279
|
+
_emit(_assistant(event.message, usage, "tool_use"))
|
|
280
|
+
elif isinstance(event, Completed):
|
|
281
|
+
_emit(_assistant(event.message, usage, event.stop_reason))
|
|
282
|
+
elif isinstance(event, ToolProgress) and event.phase == "finished":
|
|
283
|
+
result = event.result
|
|
284
|
+
_emit(
|
|
285
|
+
{
|
|
286
|
+
"type": "tool_result",
|
|
287
|
+
"id": event.call.id,
|
|
288
|
+
"name": event.call.name,
|
|
289
|
+
"is_error": result.is_error,
|
|
290
|
+
"code": result.code,
|
|
291
|
+
"output": result.output,
|
|
292
|
+
}
|
|
293
|
+
)
|
|
294
|
+
elif isinstance(event, Retrying):
|
|
295
|
+
_emit(
|
|
296
|
+
{
|
|
297
|
+
"type": "retry",
|
|
298
|
+
"attempt": event.attempt,
|
|
299
|
+
"max_attempts": event.max_attempts,
|
|
300
|
+
"delay": event.delay,
|
|
301
|
+
"reason": event.reason,
|
|
302
|
+
}
|
|
303
|
+
)
|
|
304
|
+
elif isinstance(event, (CompactStarted, CompactFinished, CompactFailed)):
|
|
305
|
+
_emit(_compact_event(event))
|
|
306
|
+
elif isinstance(event, Notice):
|
|
307
|
+
_emit({"type": "notice", "text": event.text})
|
|
308
|
+
except ProviderError as exc:
|
|
309
|
+
error = str(exc)
|
|
310
|
+
registry = conversation.registry
|
|
311
|
+
total = conversation.total_usage
|
|
312
|
+
subtype = "error" if error is not None else stopped.reason if stopped else "error"
|
|
313
|
+
result = {
|
|
314
|
+
"type": "result",
|
|
315
|
+
"subtype": subtype,
|
|
316
|
+
"is_error": subtype != "completed",
|
|
317
|
+
"result": final if subtype == "completed" else error or (stopped.notice if stopped else "没有完整回复。"),
|
|
318
|
+
"num_turns": stopped.iterations if stopped else 0,
|
|
319
|
+
"duration_ms": round((monotonic() - started) * 1000),
|
|
320
|
+
"usage": {
|
|
321
|
+
"input_tokens": total.input_tokens,
|
|
322
|
+
"output_tokens": total.output_tokens,
|
|
323
|
+
"cached_input_tokens": total.cached_input_tokens,
|
|
324
|
+
"cache_write_tokens": total.cache_write_tokens,
|
|
325
|
+
"requests": total.requests,
|
|
326
|
+
},
|
|
327
|
+
"compactions": conversation.compactions,
|
|
328
|
+
"session_id": conversation.session_id,
|
|
329
|
+
"model": model,
|
|
330
|
+
"permission_mode": registry.permissions.mode if registry else None,
|
|
331
|
+
}
|
|
332
|
+
_emit(result)
|
|
333
|
+
return result
|
|
334
|
+
finally:
|
|
335
|
+
await conversation.close()
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
async def _text_prompt(
|
|
339
|
+
conversation: Conversation,
|
|
340
|
+
prompt: str,
|
|
341
|
+
*,
|
|
342
|
+
show_usage: bool = False,
|
|
343
|
+
resume: str | None = None,
|
|
344
|
+
command: str | None = None,
|
|
345
|
+
skill: str | None = None,
|
|
346
|
+
) -> None:
|
|
347
|
+
def report(kind, value):
|
|
348
|
+
if kind == "compact":
|
|
349
|
+
print(
|
|
350
|
+
"正在压缩上下文…" if isinstance(value, CompactStarted) else compact_notice(value),
|
|
351
|
+
file=sys.stderr,
|
|
352
|
+
flush=True,
|
|
353
|
+
)
|
|
354
|
+
else:
|
|
355
|
+
print(value if kind == "notice" else "未得到回复的消息:" + value, file=sys.stderr, flush=True)
|
|
356
|
+
|
|
357
|
+
_background_notices(conversation, report)
|
|
358
|
+
_task_notices(conversation, report)
|
|
359
|
+
try:
|
|
360
|
+
await _resume(conversation, resume, report)
|
|
361
|
+
for notice in await conversation.start():
|
|
362
|
+
print(notice, file=sys.stderr, flush=True)
|
|
363
|
+
prompt, fork = _skill_prompt(conversation, skill, prompt, report)
|
|
364
|
+
async with aclosing(_turns(conversation, prompt, command=command, fork=fork)) as stream:
|
|
365
|
+
async for event in stream:
|
|
366
|
+
if isinstance(event, Delta) and event.kind == "text":
|
|
367
|
+
print(event.text, end="", flush=True)
|
|
368
|
+
elif isinstance(event, ResponseBoundary):
|
|
369
|
+
print()
|
|
370
|
+
elif (
|
|
371
|
+
isinstance(event, ToolProgress) and event.phase == "running" and event.call.name == "exit_plan_mode"
|
|
372
|
+
):
|
|
373
|
+
print(f"\n计划:\n{plan_text(event.call.arguments)}", flush=True)
|
|
374
|
+
elif isinstance(event, ToolProgress) and event.phase == "finished":
|
|
375
|
+
print(f"\n● {event.call.name} · {event.elapsed:.1f}s\n└ {event.result.output}", flush=True)
|
|
376
|
+
elif isinstance(event, RunStopped) and event.reason in {
|
|
377
|
+
"max_iterations",
|
|
378
|
+
"unknown_tools",
|
|
379
|
+
"max_tokens",
|
|
380
|
+
}:
|
|
381
|
+
print("\n" + event.notice, flush=True)
|
|
382
|
+
elif isinstance(event, Retrying):
|
|
383
|
+
print(
|
|
384
|
+
f"{event.reason},{event.delay:.1f} 秒后重试({event.attempt}/{event.max_attempts})",
|
|
385
|
+
file=sys.stderr,
|
|
386
|
+
flush=True,
|
|
387
|
+
)
|
|
388
|
+
elif isinstance(event, UsageUpdate) and show_usage:
|
|
389
|
+
print(request_usage_text(event.usage, event.iteration), file=sys.stderr, flush=True)
|
|
390
|
+
elif isinstance(event, CompactStarted):
|
|
391
|
+
print("正在压缩上下文…", file=sys.stderr, flush=True)
|
|
392
|
+
elif isinstance(event, (CompactFinished, CompactFailed)):
|
|
393
|
+
print(compact_notice(event), file=sys.stderr, flush=True)
|
|
394
|
+
elif isinstance(event, Notice):
|
|
395
|
+
print(event.text, file=sys.stderr, flush=True)
|
|
396
|
+
print()
|
|
397
|
+
finally:
|
|
398
|
+
await conversation.close()
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def _memory_options(settings, *, interactive: bool) -> dict:
|
|
402
|
+
"""Notes and the memory tool everywhere; background extraction only in the TUI."""
|
|
403
|
+
if not settings.memory:
|
|
404
|
+
return {}
|
|
405
|
+
options = {"memory": MemoryStore.for_project(Path.cwd()), "auto_memory": interactive and settings.auto_memory}
|
|
406
|
+
if options["auto_memory"] and settings.memory_provider:
|
|
407
|
+
options["memory_provider"] = create_provider(settings.select(settings.memory_provider))
|
|
408
|
+
return options
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def _config_path(args) -> Path | None:
|
|
412
|
+
"""No configuration in an interactive terminal starts the setup; None when it was left."""
|
|
413
|
+
if args.config is not None:
|
|
414
|
+
return Path(args.config)
|
|
415
|
+
try:
|
|
416
|
+
return find_config()
|
|
417
|
+
except ConfigError:
|
|
418
|
+
if args.prompt is not None or args.list_providers or not (is_terminal(sys.stdin) and is_terminal(sys.stdout)):
|
|
419
|
+
raise
|
|
420
|
+
from .setup import run_setup
|
|
421
|
+
|
|
422
|
+
return run_setup()
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def main(argv: list[str] | None = None) -> None:
|
|
426
|
+
"""The arguments as launcher.main parses them, without its terminal checks (tests call this)."""
|
|
427
|
+
run(parse_arguments(argv))
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def run(args: argparse.Namespace) -> None:
|
|
431
|
+
if args.team_dir is not None:
|
|
432
|
+
from .teams.process import run_member
|
|
433
|
+
|
|
434
|
+
raise SystemExit(run_member(args.team_dir, args.team_member, args.config, args.provider))
|
|
435
|
+
# Skills are scanned first so that -p "/<name>" parses.
|
|
436
|
+
skills = SkillCatalog.load(Path.cwd(), reserved=BUILTIN.names())
|
|
437
|
+
commands, skipped = BUILTIN.with_skills(skills)
|
|
438
|
+
prompt, command, skill = (
|
|
439
|
+
_headless_input(args.prompt, commands, skills.problems + skipped)
|
|
440
|
+
if args.prompt is not None
|
|
441
|
+
else (None, None, None)
|
|
442
|
+
)
|
|
443
|
+
try:
|
|
444
|
+
path = _config_path(args)
|
|
445
|
+
if path is None:
|
|
446
|
+
return
|
|
447
|
+
settings = load_settings(path)
|
|
448
|
+
if args.list_providers:
|
|
449
|
+
for item in settings.providers:
|
|
450
|
+
print(f"{item.name}: {item.protocol} / {item.model}")
|
|
451
|
+
return
|
|
452
|
+
config = settings.select(args.provider)
|
|
453
|
+
if (
|
|
454
|
+
args.prompt is None
|
|
455
|
+
and args.provider is None
|
|
456
|
+
and settings.default_provider is None
|
|
457
|
+
and len(settings.providers) > 1
|
|
458
|
+
):
|
|
459
|
+
config = ProviderSelectApp(settings).run(inline=True, mouse=False)
|
|
460
|
+
if config is None:
|
|
461
|
+
return
|
|
462
|
+
registry = create_default_registry(permission_mode=args.permission_mode, shell=settings.shell)
|
|
463
|
+
mcp_settings = load_mcp_settings(Path.cwd())
|
|
464
|
+
manager = MCPManager(Path.cwd(), mcp_settings) if mcp_settings.servers or mcp_settings.notices else None
|
|
465
|
+
prune_all()
|
|
466
|
+
budget = config.budget()
|
|
467
|
+
hooks, hook_notices = _hook_engine(args, registry, interactive=args.prompt is None)
|
|
468
|
+
notices = skills.problems + skipped + hook_notices
|
|
469
|
+
coordinator, coordinator_notice = decide_coordinator(
|
|
470
|
+
settings.coordinator_mode, os.environ, interactive=args.prompt is None
|
|
471
|
+
)
|
|
472
|
+
if coordinator_notice:
|
|
473
|
+
notices += (coordinator_notice,)
|
|
474
|
+
if not budget.configured:
|
|
475
|
+
notices += (f"{config.name} 未配置 context_window,按 128K 估算上下文。",)
|
|
476
|
+
conversation = Conversation(
|
|
477
|
+
create_provider(config),
|
|
478
|
+
settings.system_prompt,
|
|
479
|
+
registry,
|
|
480
|
+
mcp_manager=manager,
|
|
481
|
+
max_iterations=settings.max_iterations,
|
|
482
|
+
context_root=Path.cwd(),
|
|
483
|
+
budget=budget,
|
|
484
|
+
session=Session.new(Path.cwd()),
|
|
485
|
+
auto_compact=settings.auto_compact,
|
|
486
|
+
notices=notices,
|
|
487
|
+
persist=settings.persist_sessions,
|
|
488
|
+
skills=skills,
|
|
489
|
+
provider_configs=settings.providers,
|
|
490
|
+
provider_factory=create_provider,
|
|
491
|
+
provider_info=(config.name, config.protocol, config.model),
|
|
492
|
+
hooks=hooks,
|
|
493
|
+
agents=AgentCatalog.load(Path.cwd()),
|
|
494
|
+
agent_auto_background=settings.agent_auto_background,
|
|
495
|
+
agent_max_background=settings.agent_max_background,
|
|
496
|
+
worktree_links=settings.worktree_links,
|
|
497
|
+
worktree_stale_hours=settings.worktree_stale_hours,
|
|
498
|
+
teams=args.prompt is None,
|
|
499
|
+
team_max_members=settings.team_max_members,
|
|
500
|
+
teammate_mode=settings.teammate_mode,
|
|
501
|
+
team_launch={"config": settings.source, "provider": config.name},
|
|
502
|
+
coordinator=coordinator,
|
|
503
|
+
**_memory_options(settings, interactive=args.prompt is None),
|
|
504
|
+
)
|
|
505
|
+
resume = args.resume
|
|
506
|
+
if args.continue_session or resume:
|
|
507
|
+
resume = conversation.resolve_session(resume or None)
|
|
508
|
+
if args.prompt is not None:
|
|
509
|
+
if args.output_format != "text" and hasattr(sys.stdout, "reconfigure"):
|
|
510
|
+
# Machine-readable output is always UTF-8, whatever the console code page.
|
|
511
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
512
|
+
result = asyncio.run(
|
|
513
|
+
_prompt(
|
|
514
|
+
conversation,
|
|
515
|
+
prompt,
|
|
516
|
+
show_usage=args.show_usage,
|
|
517
|
+
output_format=args.output_format,
|
|
518
|
+
model=config.model,
|
|
519
|
+
resume=resume,
|
|
520
|
+
command=command,
|
|
521
|
+
skill=skill,
|
|
522
|
+
)
|
|
523
|
+
)
|
|
524
|
+
if result is not None and result["is_error"]:
|
|
525
|
+
raise SystemExit(1)
|
|
526
|
+
else:
|
|
527
|
+
if resume == "" and conversation.unavailable():
|
|
528
|
+
raise ProviderError(conversation.unavailable())
|
|
529
|
+
MossCodeApp(
|
|
530
|
+
conversation,
|
|
531
|
+
config.name,
|
|
532
|
+
settings=settings,
|
|
533
|
+
model=config.model,
|
|
534
|
+
resume=resume,
|
|
535
|
+
commands=commands,
|
|
536
|
+
initial_prompt=args.initial,
|
|
537
|
+
).run(
|
|
538
|
+
inline=True,
|
|
539
|
+
inline_no_clear=True,
|
|
540
|
+
mouse=False,
|
|
541
|
+
)
|
|
542
|
+
except (ConfigError, ProviderError, PermissionConfigError, MCPConfigError, ShellNotFound) as exc:
|
|
543
|
+
print(f"MossCode: {exc}", file=sys.stderr)
|
|
544
|
+
raise SystemExit(1) from None
|
|
545
|
+
except KeyboardInterrupt:
|
|
546
|
+
print("\n已取消。", file=sys.stderr)
|
|
547
|
+
raise SystemExit(130) from None
|