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/teams/view.py
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""A member's conversation as short lines — the same in the lead's member view
|
|
2
|
+
(read from its transcript) and in the member's own window (from the events of its turns)."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import re
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from rich.text import Text
|
|
11
|
+
|
|
12
|
+
from ..appearance import ERROR_COLOR, OPERATION_COLOR, PARAMETER_COLOR
|
|
13
|
+
from ..messages import Completed, Message, ProviderError, RunStopped, ToolCall, ToolProgress, ToolResult
|
|
14
|
+
from ..permissions import safe_display
|
|
15
|
+
from ..serialization import message_from_json
|
|
16
|
+
from .member import TYPE_LABELS
|
|
17
|
+
|
|
18
|
+
# render_incoming's head of each message: "[来自 lead · 消息 · id=m-1] 摘要:…".
|
|
19
|
+
HEAD = re.compile(r"^\[来自 (\S+) · (.+?) · id=[^\]]*\]\s*(.*)$")
|
|
20
|
+
COMPACTED = "(较早的对话已压缩)"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def incoming_line(sender: str, label: str, summary: str) -> Text:
|
|
24
|
+
line = Text(f"← {sender} · {label}", style="bold")
|
|
25
|
+
if summary:
|
|
26
|
+
line.append(f":{summary[:200]}")
|
|
27
|
+
return line
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def call_line(call: ToolCall) -> Text:
|
|
31
|
+
line = Text("● ", style=OPERATION_COLOR)
|
|
32
|
+
line.append(call.name, style="bold")
|
|
33
|
+
line.append(" " + safe_display(call.arguments, 160), style=PARAMETER_COLOR)
|
|
34
|
+
return line
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def result_line(result: ToolResult) -> Text:
|
|
38
|
+
first = (result.output or "").strip().splitlines()[:1]
|
|
39
|
+
style = ERROR_COLOR if result.is_error else "dim"
|
|
40
|
+
return Text(" └ " + safe_display(first[0] if first else "完成", 200), style=style)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _incoming(text: str) -> list[Text]:
|
|
44
|
+
lines, rows = text.splitlines(), []
|
|
45
|
+
for index, line in enumerate(lines):
|
|
46
|
+
match = HEAD.match(line)
|
|
47
|
+
if match is None:
|
|
48
|
+
continue
|
|
49
|
+
sender, label, rest = match.groups()
|
|
50
|
+
rest = rest.removeprefix("摘要:").strip()
|
|
51
|
+
if not rest: # The body starts on the next line.
|
|
52
|
+
rest = next((item.strip() for item in lines[index + 1 :] if item.strip()), "")
|
|
53
|
+
rest = "" if HEAD.match(rest) or rest.startswith("</incoming") else rest
|
|
54
|
+
rows.append(incoming_line(sender, label, rest))
|
|
55
|
+
return rows
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def message_lines(message: Message) -> list[Text]:
|
|
59
|
+
"""One committed message of the transcript, without its tool calls (Feed pairs them with their results)."""
|
|
60
|
+
if message.role == "tool":
|
|
61
|
+
return [result_line(result) for result in message.tool_results]
|
|
62
|
+
if message.role == "user":
|
|
63
|
+
if message.synthetic == "compact":
|
|
64
|
+
return [Text(COMPACTED, style="dim")]
|
|
65
|
+
text = message.text.strip()
|
|
66
|
+
if text.startswith("<incoming-messages>"):
|
|
67
|
+
return _incoming(text)
|
|
68
|
+
first = text.splitlines()[0] if text else ""
|
|
69
|
+
return [Text("❯ " + safe_display(first, 200), style="dim")] if first else []
|
|
70
|
+
return [Text(message.text.strip())] if message.text.strip() else []
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class Feed:
|
|
74
|
+
"""A member's conversation as lines, from its transcript (lead) or its turn's events (own window).
|
|
75
|
+
|
|
76
|
+
A tool call is written together with its result, so the results of parallel calls sit under their own
|
|
77
|
+
calls; until then the call is in `running`, shown apart from the log as the lead shows its own."""
|
|
78
|
+
|
|
79
|
+
def __init__(self) -> None:
|
|
80
|
+
self.waiting: dict[str, ToolCall] = {}
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def running(self) -> list[Text]:
|
|
84
|
+
return [call_line(call) for call in self.waiting.values()]
|
|
85
|
+
|
|
86
|
+
def running_text(self) -> Text:
|
|
87
|
+
text = Text("\n").join(self.running)
|
|
88
|
+
text.no_wrap, text.overflow = True, "ellipsis"
|
|
89
|
+
return text
|
|
90
|
+
|
|
91
|
+
def flush(self) -> list[Text]:
|
|
92
|
+
"""Calls whose results never came (an interrupted turn) are written as they were."""
|
|
93
|
+
lines, self.waiting = self.running, {}
|
|
94
|
+
return lines
|
|
95
|
+
|
|
96
|
+
def result(self, result: ToolResult) -> list[Text]:
|
|
97
|
+
call = self.waiting.pop(result.call_id, None)
|
|
98
|
+
return ([call_line(call)] if call is not None else []) + [result_line(result)]
|
|
99
|
+
|
|
100
|
+
def message(self, message: Message) -> list[Text]:
|
|
101
|
+
if message.role == "tool":
|
|
102
|
+
return [line for result in message.tool_results for line in self.result(result)]
|
|
103
|
+
lines = self.flush() + message_lines(message)
|
|
104
|
+
self.waiting.update((call.id, call) for call in message.tool_calls)
|
|
105
|
+
return lines
|
|
106
|
+
|
|
107
|
+
def record(self, record: dict) -> list[Text]:
|
|
108
|
+
kind = record.get("type")
|
|
109
|
+
if kind == "message":
|
|
110
|
+
try:
|
|
111
|
+
message = message_from_json(record.get("message"))
|
|
112
|
+
except (ValueError, TypeError, KeyError):
|
|
113
|
+
return []
|
|
114
|
+
return self.message(message)
|
|
115
|
+
if kind == "compact":
|
|
116
|
+
return self.flush() + [Text(COMPACTED, style="dim")]
|
|
117
|
+
return []
|
|
118
|
+
|
|
119
|
+
def event(self, event) -> list[Text]:
|
|
120
|
+
"""A member's own window, as its turn runs."""
|
|
121
|
+
if isinstance(event, list): # The messages of a new turn.
|
|
122
|
+
return self.flush() + [
|
|
123
|
+
incoming_line(
|
|
124
|
+
message.sender,
|
|
125
|
+
TYPE_LABELS.get(message.type, message.type),
|
|
126
|
+
message.summary or (message.body.strip().splitlines() or [""])[0],
|
|
127
|
+
)
|
|
128
|
+
for message in event
|
|
129
|
+
]
|
|
130
|
+
if isinstance(event, ToolProgress):
|
|
131
|
+
if event.phase == "running":
|
|
132
|
+
self.waiting[event.call.id] = event.call
|
|
133
|
+
elif event.phase == "finished" and event.result is not None:
|
|
134
|
+
return self.result(event.result)
|
|
135
|
+
elif isinstance(event, Completed) and event.message.text:
|
|
136
|
+
return self.flush() + [Text(event.message.text)]
|
|
137
|
+
elif isinstance(event, RunStopped):
|
|
138
|
+
lines = self.flush()
|
|
139
|
+
return lines + (
|
|
140
|
+
[Text(event.notice, style=ERROR_COLOR)] if event.reason not in {"completed", "max_tokens"} else []
|
|
141
|
+
)
|
|
142
|
+
elif isinstance(event, ProviderError):
|
|
143
|
+
return self.flush() + [Text(str(event), style=ERROR_COLOR)]
|
|
144
|
+
return []
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class TranscriptTail:
|
|
148
|
+
"""The records appended to a transcript since the last read. A line still being written stays
|
|
149
|
+
for the next read; a line that cannot be parsed is skipped."""
|
|
150
|
+
|
|
151
|
+
def __init__(self, path: Path):
|
|
152
|
+
self.path = path
|
|
153
|
+
self.offset = 0
|
|
154
|
+
self._partial = b""
|
|
155
|
+
|
|
156
|
+
def read(self) -> list[dict]:
|
|
157
|
+
"""Blocking. OSError when the file cannot be read; [] while it does not exist yet."""
|
|
158
|
+
try:
|
|
159
|
+
size = self.path.stat().st_size
|
|
160
|
+
except FileNotFoundError:
|
|
161
|
+
return []
|
|
162
|
+
if size < self.offset: # Replaced by a shorter file: start over.
|
|
163
|
+
self.offset, self._partial = 0, b""
|
|
164
|
+
if size == self.offset:
|
|
165
|
+
return []
|
|
166
|
+
with self.path.open("rb") as stream:
|
|
167
|
+
stream.seek(self.offset)
|
|
168
|
+
data = stream.read(size - self.offset)
|
|
169
|
+
self.offset += len(data)
|
|
170
|
+
data = self._partial + data
|
|
171
|
+
*complete, self._partial = data.split(b"\n")
|
|
172
|
+
records = []
|
|
173
|
+
for raw in complete:
|
|
174
|
+
if not raw.strip():
|
|
175
|
+
continue
|
|
176
|
+
try:
|
|
177
|
+
record = json.loads(raw)
|
|
178
|
+
except (ValueError, UnicodeError):
|
|
179
|
+
continue
|
|
180
|
+
if isinstance(record, dict):
|
|
181
|
+
records.append(record)
|
|
182
|
+
return records
|
mosscode/tool_budget.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Layer 1: oversized tool results go to the session directory; the history keeps a preview.
|
|
2
|
+
|
|
3
|
+
Decided once, before a result enters the history, and never revisited, so requests already sent
|
|
4
|
+
keep a byte-identical prefix for prompt caching.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import replace
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from .context import estimate_text
|
|
12
|
+
from .messages import Message, ToolResult
|
|
13
|
+
from .session import Session
|
|
14
|
+
|
|
15
|
+
RESULT_TOKENS = 12_500
|
|
16
|
+
MESSAGE_TOKENS = 50_000
|
|
17
|
+
HEAD, TAIL = 2_000, 1_000
|
|
18
|
+
# read_file is paged and reads files already on disk; storing it would loop on the stored copy.
|
|
19
|
+
EXEMPT = frozenset({"read_file"})
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def preview(text: str, tokens: int, path: Path | None) -> str:
|
|
23
|
+
size = f"输出过大(约 {tokens} Token,{len(text)} 字符)"
|
|
24
|
+
if path is None:
|
|
25
|
+
lead = f"{size},本次未能保存完整内容。"
|
|
26
|
+
else:
|
|
27
|
+
lead = (
|
|
28
|
+
f"{size},完整内容已保存到:\n{path}\n"
|
|
29
|
+
"需要完整内容时用 read_file 读取这个文件(可用 offset 和 limit 分页)。"
|
|
30
|
+
)
|
|
31
|
+
omitted = len(text) - HEAD - TAIL
|
|
32
|
+
return (
|
|
33
|
+
f"{lead}\n以下是开头 {HEAD} 字符和末尾 {TAIL} 字符:\n{text[:HEAD]}\n"
|
|
34
|
+
f"…(中间省略 {omitted} 字符)…\n{text[-TAIL:]}"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def store(text: str, details: dict[str, Any], session: Session | None, call_id: str) -> tuple[str, dict[str, Any]]:
|
|
39
|
+
tokens = estimate_text(text)
|
|
40
|
+
path = None
|
|
41
|
+
if session is not None:
|
|
42
|
+
try:
|
|
43
|
+
path = session.persist_result(call_id, text)
|
|
44
|
+
except OSError:
|
|
45
|
+
path = None
|
|
46
|
+
extra = {"persisted": str(path)} if path else {"truncated": True}
|
|
47
|
+
extra["budgeted"] = True # Already a preview: never stored again.
|
|
48
|
+
return preview(text, tokens, path), {**details, **extra}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def bound_result(
|
|
52
|
+
name: str, call_id: str, text: str, details: dict[str, Any], session: Session | None
|
|
53
|
+
) -> tuple[str, dict[str, Any]]:
|
|
54
|
+
"""One result over the threshold is stored."""
|
|
55
|
+
if name in EXEMPT or estimate_text(text) <= RESULT_TOKENS:
|
|
56
|
+
return text, details
|
|
57
|
+
return store(text, details, session, call_id)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def bound_message(message: Message, session: Session | None) -> Message:
|
|
61
|
+
"""Results of one batch over the combined budget are stored, largest first."""
|
|
62
|
+
results = list(message.tool_results)
|
|
63
|
+
sizes = [estimate_text(r.model_text()) for r in results]
|
|
64
|
+
total = sum(sizes)
|
|
65
|
+
if total <= MESSAGE_TOKENS:
|
|
66
|
+
return message
|
|
67
|
+
candidates = sorted(
|
|
68
|
+
(i for i, r in enumerate(results) if r.name not in EXEMPT and not r.details.get("budgeted")),
|
|
69
|
+
key=lambda i: sizes[i],
|
|
70
|
+
reverse=True,
|
|
71
|
+
)
|
|
72
|
+
for index in candidates:
|
|
73
|
+
if total <= MESSAGE_TOKENS:
|
|
74
|
+
break
|
|
75
|
+
result = results[index]
|
|
76
|
+
output, details = store(result.output, result.details, session, result.call_id)
|
|
77
|
+
results[index] = replace(result, output=output, details=details)
|
|
78
|
+
updated = estimate_text(results[index].model_text())
|
|
79
|
+
total += updated - sizes[index]
|
|
80
|
+
sizes[index] = updated
|
|
81
|
+
return replace(message, tool_results=tuple(results))
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def persisted_note(result: ToolResult) -> str:
|
|
85
|
+
path = result.details.get("persisted")
|
|
86
|
+
return f"完整结果已存盘:{path}" if path else ""
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from ..permissions import PermissionPolicy
|
|
4
|
+
from .base import ToolRegistry
|
|
5
|
+
from .files import EditFile, FileState, ReadFile, WriteFile
|
|
6
|
+
from .plan import ExitPlanMode
|
|
7
|
+
from .process import ExecuteCommand
|
|
8
|
+
from .search import Glob, Grep
|
|
9
|
+
from .shell import Shell
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def create_default_registry(
|
|
13
|
+
root: Path | None = None,
|
|
14
|
+
*,
|
|
15
|
+
permission_mode: str | None = None,
|
|
16
|
+
load_permissions: bool = True,
|
|
17
|
+
permissions: PermissionPolicy | None = None,
|
|
18
|
+
shell: Shell | str | None = "auto",
|
|
19
|
+
) -> ToolRegistry:
|
|
20
|
+
root = Path.cwd() if root is None else root
|
|
21
|
+
policy = permissions or PermissionPolicy(root, mode=permission_mode, load=load_permissions)
|
|
22
|
+
state = FileState() # Shared so edits can see what read_file returned.
|
|
23
|
+
tools = (
|
|
24
|
+
ReadFile(root, state),
|
|
25
|
+
WriteFile(root, state),
|
|
26
|
+
EditFile(root, state),
|
|
27
|
+
ExecuteCommand(root, shell),
|
|
28
|
+
Glob(root),
|
|
29
|
+
Grep(root),
|
|
30
|
+
ExitPlanMode(root),
|
|
31
|
+
)
|
|
32
|
+
return ToolRegistry(tools, permissions=policy)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def shell_label(registry: ToolRegistry | None) -> str | None:
|
|
36
|
+
"""The execute_command shell's name, e.g. "Git Bash", for prompts and the UI."""
|
|
37
|
+
shell = getattr(registry.get("execute_command"), "shell", None) if registry is not None else None
|
|
38
|
+
return shell.label if isinstance(shell, Shell) else None
|
mosscode/tools/agent.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""The agent tool and the two task tools of the main conversation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from typing import TYPE_CHECKING, Literal
|
|
7
|
+
|
|
8
|
+
from pydantic import Field
|
|
9
|
+
|
|
10
|
+
from .base import CURRENT_CALL, Params, Tool, ToolFailure
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from ..agents.runtime import SubAgents
|
|
14
|
+
|
|
15
|
+
WAIT_LIMIT = 300
|
|
16
|
+
PREVIEW = 4000 # Characters of a running task's last reply shown by task_output.
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class AgentParams(Params):
|
|
20
|
+
description: str = Field(min_length=1, max_length=60)
|
|
21
|
+
prompt: str = Field(min_length=1, max_length=40000)
|
|
22
|
+
agent_type: str = Field(default="", max_length=64)
|
|
23
|
+
run_in_background: bool = False
|
|
24
|
+
isolation: Literal["none", "worktree"] = "none"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class AgentTool(Tool):
|
|
28
|
+
name = "agent"
|
|
29
|
+
permission_kind = "agent" # No approval: every call the sub-agent makes is checked on its own.
|
|
30
|
+
plan_allowed = True # In plan mode its sub-agents are in plan mode too.
|
|
31
|
+
timeout = None # Foreground sub-agents end on their own, or move to the background.
|
|
32
|
+
description = (
|
|
33
|
+
"把一段可以独立完成的工作交给子 Agent,你只收到它的结论。系统提示的“子 Agent”一节列出了可用的类型。"
|
|
34
|
+
"agent_type 写类型名时,子 Agent 从空白对话开始,prompt 必须写清楚完整的任务、背景和期望的结果;"
|
|
35
|
+
"省略 agent_type 时为 Fork:它继承当前对话的全部内容,总在后台运行,完成后你会收到 <task-notification>。"
|
|
36
|
+
"description 是 3–10 个字的任务标签。isolation 为 worktree 时,子 Agent 在独立的 git 工作树中运行。"
|
|
37
|
+
)
|
|
38
|
+
prompt_summary = "把独立的子任务交给子 Agent"
|
|
39
|
+
prompt_guidelines = (
|
|
40
|
+
"适合委派:需要大量搜索或阅读、但只需要结论的调查;互不依赖的多个子任务(在一次回复中同时发起,它们会并行运行)。",
|
|
41
|
+
"不适合委派:一两次工具调用就能完成的事;需要与用户来回确认的事。",
|
|
42
|
+
"子 Agent 不能向用户提问;它的结论不会直接显示给用户,需要时由你转述。",
|
|
43
|
+
"run_in_background 为 true 时立即返回任务 id;不要在等待通知期间重复做同一件事,需要结果时用 task_output。",
|
|
44
|
+
"会改动文件、且可能与你或其他子 Agent 的改动冲突的任务(例如并行的实验性修改),用 isolation: worktree 让它在独立的"
|
|
45
|
+
"工作树中进行;有改动时工作树会保留,结果中给出分支名,由你决定是否合并。Fork 不能隔离。",
|
|
46
|
+
)
|
|
47
|
+
params_model = AgentParams
|
|
48
|
+
|
|
49
|
+
def __init__(self, root, runner: SubAgents):
|
|
50
|
+
super().__init__(root)
|
|
51
|
+
self.runner = runner
|
|
52
|
+
|
|
53
|
+
async def execute(self, params: AgentParams):
|
|
54
|
+
call = CURRENT_CALL.get()
|
|
55
|
+
return await self.runner.call(
|
|
56
|
+
params.description.strip(),
|
|
57
|
+
params.prompt,
|
|
58
|
+
params.agent_type.strip(),
|
|
59
|
+
params.run_in_background,
|
|
60
|
+
call.id if call is not None else "",
|
|
61
|
+
isolation=params.isolation,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class TaskOutputParams(Params):
|
|
66
|
+
task_id: str = Field(min_length=1, max_length=20)
|
|
67
|
+
wait: int = Field(default=0, ge=0, le=WAIT_LIMIT)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class TaskOutput(Tool):
|
|
71
|
+
name = "task_output"
|
|
72
|
+
permission_kind = "agent"
|
|
73
|
+
read_only = True
|
|
74
|
+
timeout = WAIT_LIMIT + 10
|
|
75
|
+
description = (
|
|
76
|
+
"查看后台任务(子 Agent)的状态与结果。wait 为等待完成的秒数(最多 300),0 表示立即返回;"
|
|
77
|
+
"任务完成时你也会自动收到 <task-notification>,一般不需要轮询。"
|
|
78
|
+
)
|
|
79
|
+
prompt_summary = "查看后台任务的状态与结果"
|
|
80
|
+
params_model = TaskOutputParams
|
|
81
|
+
|
|
82
|
+
def __init__(self, root, runner: SubAgents):
|
|
83
|
+
super().__init__(root)
|
|
84
|
+
self.runner = runner
|
|
85
|
+
|
|
86
|
+
async def execute(self, params: TaskOutputParams):
|
|
87
|
+
task = self.runner.tasks.get(params.task_id.strip())
|
|
88
|
+
if task is None:
|
|
89
|
+
raise ToolFailure("unknown_task", f"没有任务 {params.task_id}。")
|
|
90
|
+
if task.status == "running" and params.wait and task.job is not None:
|
|
91
|
+
await asyncio.wait({task.job}, timeout=params.wait)
|
|
92
|
+
from ..agents.runtime import stats
|
|
93
|
+
from ..agents.tasks import STATUS_LABELS
|
|
94
|
+
|
|
95
|
+
head = f"任务 {task.id}({task.label}):{STATUS_LABELS[task.status]},{stats(task)}。"
|
|
96
|
+
if task.status == "running":
|
|
97
|
+
last = f"最近一次工具调用:{task.last_tool}。" if task.last_tool else "尚未调用工具。"
|
|
98
|
+
return f"{head}\n{last}", {"task": task.id, "status": task.status}
|
|
99
|
+
return f"{head}\n\n{task.result or ''}", {"task": task.id, "status": task.status}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class TaskStopParams(Params):
|
|
103
|
+
task_id: str = Field(min_length=1, max_length=20)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class TaskStop(Tool):
|
|
107
|
+
name = "task_stop"
|
|
108
|
+
permission_kind = "agent"
|
|
109
|
+
plan_allowed = True
|
|
110
|
+
description = "停止一个运行中的后台任务(子 Agent)。停止后任务记为已取消,不再收到它的通知。"
|
|
111
|
+
prompt_summary = "停止后台任务"
|
|
112
|
+
params_model = TaskStopParams
|
|
113
|
+
|
|
114
|
+
def __init__(self, root, runner: SubAgents):
|
|
115
|
+
super().__init__(root)
|
|
116
|
+
self.runner = runner
|
|
117
|
+
|
|
118
|
+
async def execute(self, params: TaskStopParams):
|
|
119
|
+
task_id = params.task_id.strip()
|
|
120
|
+
task = self.runner.tasks.get(task_id)
|
|
121
|
+
if task is None:
|
|
122
|
+
raise ToolFailure("unknown_task", f"没有任务 {task_id}。")
|
|
123
|
+
if not await self.runner.tasks.stop(task_id):
|
|
124
|
+
from ..agents.tasks import STATUS_LABELS
|
|
125
|
+
|
|
126
|
+
return f"任务 {task_id} 已结束({STATUS_LABELS[task.status]}),无需停止。", {"task": task_id}
|
|
127
|
+
return f"已停止任务 {task_id}。", {"task": task_id, "stopped": True}
|