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/tools/skill.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Skill tools: load_skill pins an SOP; a skill's own tools run its scripts."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
import sys
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from copy import deepcopy
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from pydantic import Field
|
|
12
|
+
|
|
13
|
+
from ..messages import ToolDefinition
|
|
14
|
+
from .base import Params, Tool, ToolFailure
|
|
15
|
+
from .process import COMMAND_HEAD, COMMAND_TAIL, run_process
|
|
16
|
+
|
|
17
|
+
FAILURE_TAIL = 2000 # Characters of stdout and stderr kept in a failure message.
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class LoadSkillParams(Params):
|
|
21
|
+
name: str = Field(min_length=1, max_length=64)
|
|
22
|
+
arguments: str = Field(default="", max_length=4000)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class LoadSkill(Tool):
|
|
26
|
+
name = "load_skill"
|
|
27
|
+
permission_kind = "load_skill" # No approval; it only reads the skill's own files.
|
|
28
|
+
read_only = True # Offered in plan mode as well.
|
|
29
|
+
description = (
|
|
30
|
+
"加载一个 Skill(可复用的操作规程)。系统提示的 Skill 一节列出了可加载的名称和描述;"
|
|
31
|
+
"用户的请求与某个描述相符时,先调用本工具,再按加载后的规程执行。"
|
|
32
|
+
"加载后规程出现在 <active-skills> 中并在本会话中持续有效,工具结果不重复规程全文;"
|
|
33
|
+
"Skill 的专属工具(skill__ 开头)同时变为可用。arguments 是传给规程的参数,可省略。"
|
|
34
|
+
)
|
|
35
|
+
prompt_summary = "加载与请求相符的 Skill 规程"
|
|
36
|
+
prompt_guidelines = (
|
|
37
|
+
"只加载 Skill 一节中列出的名称;已在 <active-skills> 中的 Skill 不需要再次加载,除非要换用新的参数。",
|
|
38
|
+
)
|
|
39
|
+
params_model = LoadSkillParams
|
|
40
|
+
|
|
41
|
+
def __init__(self, root, loader: Callable[[str, str], tuple[str, dict]]):
|
|
42
|
+
super().__init__(root)
|
|
43
|
+
self.loader = loader
|
|
44
|
+
|
|
45
|
+
async def execute(self, params: LoadSkillParams):
|
|
46
|
+
return self.loader(params.name, params.arguments)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _tail(text: str) -> str:
|
|
50
|
+
return text if len(text) <= FAILURE_TAIL else "…" + text[-FAILURE_TAIL:]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class SkillTool(Tool):
|
|
54
|
+
"""A tool from tool.json; argv without a shell, arguments as JSON on stdin, approval every call."""
|
|
55
|
+
|
|
56
|
+
permission_kind = "skill"
|
|
57
|
+
read_only = False
|
|
58
|
+
|
|
59
|
+
def __init__(self, root: Path, skill, spec):
|
|
60
|
+
from ..mcp_client import check_schema
|
|
61
|
+
|
|
62
|
+
super().__init__(root)
|
|
63
|
+
self.skill_name, self.short_name = skill.name, spec.name
|
|
64
|
+
self.directory = skill.directory.resolve()
|
|
65
|
+
self.name = spec.full_name
|
|
66
|
+
self.description = f"Skill {skill.name} 的专属工具。{spec.description}"
|
|
67
|
+
self.prompt_summary = f"Skill {skill.name} 的专属工具 {spec.name}"
|
|
68
|
+
self.schema = deepcopy(spec.input_schema)
|
|
69
|
+
self.validator = check_schema(self.schema)
|
|
70
|
+
self.command = spec.command
|
|
71
|
+
self.timeout = spec.timeout
|
|
72
|
+
|
|
73
|
+
def definition(self) -> ToolDefinition:
|
|
74
|
+
return ToolDefinition(self.name, self.description, deepcopy(self.schema), self.prompt_summary)
|
|
75
|
+
|
|
76
|
+
def validate_arguments(self, raw):
|
|
77
|
+
from ..mcp_client import json_arguments
|
|
78
|
+
|
|
79
|
+
return json_arguments(self.validator, raw, "Skill 工具")
|
|
80
|
+
|
|
81
|
+
def argv(self) -> list[str]:
|
|
82
|
+
"""${SKILL_DIR} and ${PYTHON} substituted."""
|
|
83
|
+
return [
|
|
84
|
+
part.replace("${SKILL_DIR}", str(self.directory)).replace("${PYTHON}", sys.executable)
|
|
85
|
+
for part in self.command
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
def _executable(self, first: str) -> str:
|
|
89
|
+
if os.sep in first or "/" in first:
|
|
90
|
+
if Path(first).is_file():
|
|
91
|
+
return first
|
|
92
|
+
elif found := shutil.which(first):
|
|
93
|
+
return found
|
|
94
|
+
raise ToolFailure("command_not_found", f"找不到 Skill 工具的命令:{first}。未执行。")
|
|
95
|
+
|
|
96
|
+
async def execute(self, params):
|
|
97
|
+
argv = self.argv()
|
|
98
|
+
argv[0] = self._executable(argv[0])
|
|
99
|
+
env = {
|
|
100
|
+
**os.environ,
|
|
101
|
+
"MOSSCODE_SKILL_DIR": str(self.directory),
|
|
102
|
+
"MOSSCODE_PROJECT_ROOT": str(self.root),
|
|
103
|
+
"PYTHONUTF8": "1",
|
|
104
|
+
"PYTHONIOENCODING": "utf-8",
|
|
105
|
+
}
|
|
106
|
+
data = json.dumps(params.model_dump(), ensure_ascii=False).encode("utf-8")
|
|
107
|
+
details = await run_process(
|
|
108
|
+
argv, self.root, check=False, limit=COMMAND_HEAD, tail=COMMAND_TAIL, env=env, input=data
|
|
109
|
+
)
|
|
110
|
+
details = {**details, "skill": self.skill_name, "tool": self.short_name}
|
|
111
|
+
if details["exit_code"] != 0:
|
|
112
|
+
parts = [f"Skill 工具 {self.name} 退出码为 {details['exit_code']}。"]
|
|
113
|
+
if details["stdout"]:
|
|
114
|
+
parts.append("stdout 末尾:\n" + _tail(details["stdout"]))
|
|
115
|
+
if details["stderr"]:
|
|
116
|
+
parts.append("stderr 末尾:\n" + _tail(details["stderr"]))
|
|
117
|
+
raise ToolFailure("nonzero_exit", "\n".join(parts), **details)
|
|
118
|
+
return details["stdout"] or "Skill 工具完成,无输出。", details
|
mosscode/tools/team.py
ADDED
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
"""The lead's team tools and a member's message, task and plan tools."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from typing import TYPE_CHECKING, Literal
|
|
8
|
+
|
|
9
|
+
from pydantic import Field
|
|
10
|
+
|
|
11
|
+
from ..teams.lock import TeamError
|
|
12
|
+
from ..teams.mailbox import BODY_LIMIT, SUMMARY_LIMIT
|
|
13
|
+
from .base import Params, Tool, ToolFailure
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from ..teams.member import MemberRunner, TeamAccess
|
|
17
|
+
from ..teams.runtime import TeamRuntime
|
|
18
|
+
|
|
19
|
+
LEAD_TOOLS = (
|
|
20
|
+
"team_create",
|
|
21
|
+
"team_spawn",
|
|
22
|
+
"team_delete",
|
|
23
|
+
"team_merge",
|
|
24
|
+
"team_task_create",
|
|
25
|
+
"team_task_get",
|
|
26
|
+
"team_task_list",
|
|
27
|
+
"team_task_update",
|
|
28
|
+
"send_message",
|
|
29
|
+
)
|
|
30
|
+
TEAM_TOOLS = frozenset(LEAD_TOOLS) # GLOBAL_BAN for sub-agents and forks.
|
|
31
|
+
|
|
32
|
+
MESSAGE_TYPES = Literal["text", "plan_approval_response", "shutdown_request", "shutdown_response"]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class TeamTool(Tool):
|
|
36
|
+
permission_kind = "team" # Team bookkeeping needs no approval; members' own calls are checked one by one.
|
|
37
|
+
plan_allowed = True
|
|
38
|
+
|
|
39
|
+
async def run(self, function, *args, **kwargs):
|
|
40
|
+
try:
|
|
41
|
+
return await asyncio.to_thread(function, *args, **kwargs)
|
|
42
|
+
except TeamError as exc:
|
|
43
|
+
raise ToolFailure(exc.code, exc.message) from None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def failure(exc: TeamError) -> ToolFailure:
|
|
47
|
+
return ToolFailure(exc.code, exc.message)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# Lead -----------------------------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class TeamCreateParams(Params):
|
|
54
|
+
name: str = Field(min_length=1, max_length=32)
|
|
55
|
+
description: str = Field(default="", max_length=500)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class TeamCreate(TeamTool):
|
|
59
|
+
name = "team_create"
|
|
60
|
+
description = (
|
|
61
|
+
"新建一个团队并设为当前团队;同名团队已存在时打开它。团队由你(lead)和至多 4 个长期存在的成员组成,"
|
|
62
|
+
"成员各自在自己的 git 工作树中工作,通过消息和共享任务协作。"
|
|
63
|
+
)
|
|
64
|
+
prompt_summary = "新建或打开团队"
|
|
65
|
+
params_model = TeamCreateParams
|
|
66
|
+
|
|
67
|
+
def __init__(self, root, runtime: TeamRuntime):
|
|
68
|
+
super().__init__(root)
|
|
69
|
+
self.runtime = runtime
|
|
70
|
+
|
|
71
|
+
async def execute(self, params: TeamCreateParams):
|
|
72
|
+
try:
|
|
73
|
+
return await self.runtime.create(params.name.strip(), params.description)
|
|
74
|
+
except TeamError as exc:
|
|
75
|
+
raise failure(exc) from None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class TeamSpawnParams(Params):
|
|
79
|
+
name: str = Field(min_length=1, max_length=32)
|
|
80
|
+
agent_type: str = Field(default="general", max_length=64)
|
|
81
|
+
prompt: str = Field(min_length=1, max_length=BODY_LIMIT)
|
|
82
|
+
require_plan_approval: bool = False
|
|
83
|
+
isolation: Literal["worktree", "none"] = "worktree"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class TeamSpawn(TeamTool):
|
|
87
|
+
name = "team_spawn"
|
|
88
|
+
timeout = 120.0 # Creating a worktree runs git.
|
|
89
|
+
description = (
|
|
90
|
+
"在当前团队中创建一个成员并开始它的第一轮。agent_type 是 Agent 定义名(默认 general),"
|
|
91
|
+
"prompt 是它的初始任务,要写清楚背景、范围和交付方式。默认在独立的 git 工作树中工作;"
|
|
92
|
+
"isolation: none 让它在主目录工作,只适合只读的角色。require_plan_approval 为 true 时,"
|
|
93
|
+
"成员先以计划模式调查并提交计划,由你批准后才能动手。"
|
|
94
|
+
)
|
|
95
|
+
prompt_summary = "创建团队成员"
|
|
96
|
+
params_model = TeamSpawnParams
|
|
97
|
+
|
|
98
|
+
def __init__(self, root, runtime: TeamRuntime):
|
|
99
|
+
super().__init__(root)
|
|
100
|
+
self.runtime = runtime
|
|
101
|
+
|
|
102
|
+
async def execute(self, params: TeamSpawnParams):
|
|
103
|
+
try:
|
|
104
|
+
return await self.runtime.spawn(
|
|
105
|
+
params.name.strip(),
|
|
106
|
+
params.agent_type.strip(),
|
|
107
|
+
params.prompt,
|
|
108
|
+
require_plan_approval=params.require_plan_approval,
|
|
109
|
+
isolation=params.isolation,
|
|
110
|
+
)
|
|
111
|
+
except TeamError as exc:
|
|
112
|
+
raise failure(exc) from None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class TeamDeleteParams(Params):
|
|
116
|
+
force: bool = False
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class TeamDelete(TeamTool):
|
|
120
|
+
name = "team_delete"
|
|
121
|
+
plan_allowed = False
|
|
122
|
+
description = (
|
|
123
|
+
"删除当前团队:记录、邮箱、任务和成员对话。有成员在运行时需要 force: true,运行中的成员会先被停止。"
|
|
124
|
+
"成员的工作树和分支保留,用 /worktree 管理。"
|
|
125
|
+
)
|
|
126
|
+
prompt_summary = "删除当前团队"
|
|
127
|
+
params_model = TeamDeleteParams
|
|
128
|
+
|
|
129
|
+
def __init__(self, root, runtime: TeamRuntime):
|
|
130
|
+
super().__init__(root)
|
|
131
|
+
self.runtime = runtime
|
|
132
|
+
|
|
133
|
+
async def execute(self, params: TeamDeleteParams):
|
|
134
|
+
try:
|
|
135
|
+
return await self.runtime.delete(force=params.force)
|
|
136
|
+
except TeamError as exc:
|
|
137
|
+
raise failure(exc) from None
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class TeamMergeParams(Params):
|
|
141
|
+
member: str = Field(min_length=1, max_length=32)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class TeamMerge(TeamTool):
|
|
145
|
+
name = "team_merge"
|
|
146
|
+
plan_allowed = False
|
|
147
|
+
timeout = None # It waits for the user's answer.
|
|
148
|
+
description = (
|
|
149
|
+
"把一个成员工作树分支上的提交合并到主目录的当前分支(git merge --no-ff)。合并前会检查主目录和成员的状态,"
|
|
150
|
+
"并请用户确认。有多个成员时按任务依赖的顺序逐个合并;冲突时合并会被放弃,按返回的下一步让成员处理后再合并。"
|
|
151
|
+
)
|
|
152
|
+
prompt_summary = "合并成员的分支"
|
|
153
|
+
params_model = TeamMergeParams
|
|
154
|
+
|
|
155
|
+
def __init__(self, root, runtime: TeamRuntime):
|
|
156
|
+
super().__init__(root)
|
|
157
|
+
self.runtime = runtime
|
|
158
|
+
|
|
159
|
+
async def execute(self, params: TeamMergeParams):
|
|
160
|
+
try:
|
|
161
|
+
return await self.runtime.merge(params.member.strip())
|
|
162
|
+
except TeamError as exc:
|
|
163
|
+
raise failure(exc) from None
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
# Shared tasks (lead and members) ---------------------------------------------------------------------
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class TaskCreateParams(Params):
|
|
170
|
+
subject: str = Field(min_length=1, max_length=200)
|
|
171
|
+
description: str = Field(default="", max_length=4000)
|
|
172
|
+
blocked_by: list[str] = Field(default_factory=list, max_length=50)
|
|
173
|
+
owner: str = Field(default="", max_length=32)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class TaskGetParams(Params):
|
|
177
|
+
id: str = Field(min_length=1, max_length=10)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class TaskListParams(Params):
|
|
181
|
+
pass
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class TaskUpdateParams(Params):
|
|
185
|
+
id: str = Field(min_length=1, max_length=10)
|
|
186
|
+
status: Literal["pending", "in_progress", "completed"] | None = None
|
|
187
|
+
owner: str | None = Field(default=None, max_length=32)
|
|
188
|
+
subject: str | None = Field(default=None, max_length=200)
|
|
189
|
+
description: str | None = Field(default=None, max_length=4000)
|
|
190
|
+
add_blocked_by: list[str] = Field(default_factory=list, max_length=50)
|
|
191
|
+
remove_blocked_by: list[str] = Field(default_factory=list, max_length=50)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class SharedTaskTool(TeamTool):
|
|
195
|
+
"""access() is the lead's current team (no_team without one) or the member's own team."""
|
|
196
|
+
|
|
197
|
+
def __init__(self, root, access: Callable[[], TeamAccess]):
|
|
198
|
+
super().__init__(root)
|
|
199
|
+
self.access = access
|
|
200
|
+
|
|
201
|
+
def _access(self) -> TeamAccess:
|
|
202
|
+
try:
|
|
203
|
+
return self.access()
|
|
204
|
+
except TeamError as exc:
|
|
205
|
+
raise failure(exc) from None
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class TeamTaskCreate(SharedTaskTool):
|
|
209
|
+
name = "team_task_create"
|
|
210
|
+
description = (
|
|
211
|
+
"在当前团队中新建共享任务。blocked_by 列出它依赖的任务编号,依赖都完成后它才就绪;"
|
|
212
|
+
"owner 可以直接指派给已创建的成员;成员还没创建时先留空,之后用 team_task_update 指派。"
|
|
213
|
+
)
|
|
214
|
+
prompt_summary = "新建团队任务"
|
|
215
|
+
params_model = TaskCreateParams
|
|
216
|
+
|
|
217
|
+
async def execute(self, params: TaskCreateParams):
|
|
218
|
+
access = self._access()
|
|
219
|
+
task = await self.run(
|
|
220
|
+
access.task_create, params.subject, params.description, params.blocked_by, params.owner.strip()
|
|
221
|
+
)
|
|
222
|
+
waits = f",依赖 {'、'.join(task.blocked_by)}" if task.blocked_by else ""
|
|
223
|
+
owner = f",负责人 {task.owner}" if task.owner else ""
|
|
224
|
+
return f"已新建任务 {task.id}:{task.subject}{waits}{owner}。", {"team": access.team, "task": task.id}
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
class TeamTaskGet(SharedTaskTool):
|
|
228
|
+
name = "team_task_get"
|
|
229
|
+
read_only = True
|
|
230
|
+
description = "查看当前团队的一个任务:状态、负责人、依赖、被依赖和说明。"
|
|
231
|
+
prompt_summary = "查看团队任务"
|
|
232
|
+
params_model = TaskGetParams
|
|
233
|
+
|
|
234
|
+
async def execute(self, params: TaskGetParams):
|
|
235
|
+
from ..teams.member import task_detail
|
|
236
|
+
|
|
237
|
+
access = self._access()
|
|
238
|
+
board = access.store.board
|
|
239
|
+
|
|
240
|
+
def read():
|
|
241
|
+
return board.get(params.id.strip()), board.list()
|
|
242
|
+
|
|
243
|
+
task, tasks = await self.run(read)
|
|
244
|
+
return task_detail(task, tasks), {"team": access.team, "task": task.id}
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
class TeamTaskList(SharedTaskTool):
|
|
248
|
+
name = "team_task_list"
|
|
249
|
+
read_only = True
|
|
250
|
+
description = "列出当前团队的全部任务:编号、状态、负责人、标题,以及是否就绪或在等待哪些任务。"
|
|
251
|
+
prompt_summary = "列出团队任务"
|
|
252
|
+
params_model = TaskListParams
|
|
253
|
+
|
|
254
|
+
async def execute(self, params: TaskListParams):
|
|
255
|
+
from ..teams.member import task_listing
|
|
256
|
+
|
|
257
|
+
access = self._access()
|
|
258
|
+
tasks = await self.run(access.store.board.list)
|
|
259
|
+
return task_listing(tasks), {"team": access.team, "tasks": len(tasks)}
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
class TeamTaskUpdate(SharedTaskTool):
|
|
263
|
+
name = "team_task_update"
|
|
264
|
+
description = (
|
|
265
|
+
"修改当前团队的一个任务。领取任务时把 owner 设为自己、status 设为 in_progress;完成后设为 completed。"
|
|
266
|
+
"状态只能向前推进。"
|
|
267
|
+
)
|
|
268
|
+
prompt_summary = "修改团队任务"
|
|
269
|
+
params_model = TaskUpdateParams
|
|
270
|
+
|
|
271
|
+
async def execute(self, params: TaskUpdateParams):
|
|
272
|
+
from ..teams.board import STATUS_LABELS
|
|
273
|
+
|
|
274
|
+
access = self._access()
|
|
275
|
+
changes = {
|
|
276
|
+
key: value
|
|
277
|
+
for key, value in (
|
|
278
|
+
("status", params.status),
|
|
279
|
+
("owner", params.owner),
|
|
280
|
+
("subject", params.subject),
|
|
281
|
+
("description", params.description),
|
|
282
|
+
)
|
|
283
|
+
if value is not None
|
|
284
|
+
}
|
|
285
|
+
updated = await self.run(
|
|
286
|
+
access.task_update,
|
|
287
|
+
params.id.strip(),
|
|
288
|
+
add_blocked_by=params.add_blocked_by,
|
|
289
|
+
remove_blocked_by=params.remove_blocked_by,
|
|
290
|
+
**changes,
|
|
291
|
+
)
|
|
292
|
+
task = updated.task
|
|
293
|
+
text = f"任务 {task.id}:{STATUS_LABELS.get(task.status, task.status)},负责人 {task.owner or '未指派'}。"
|
|
294
|
+
if updated.ready:
|
|
295
|
+
text += "现在就绪:" + "、".join(f"{item.id}({item.owner or '未指派'})" for item in updated.ready) + "。"
|
|
296
|
+
return text, {"team": access.team, "task": task.id, "status": task.status}
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
# Messages -------------------------------------------------------------------------------------------
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
class SendParams(Params):
|
|
303
|
+
to: str = Field(min_length=1, max_length=32)
|
|
304
|
+
type: MESSAGE_TYPES = "text"
|
|
305
|
+
body: str = Field(default="", max_length=BODY_LIMIT)
|
|
306
|
+
summary: str = Field(default="", max_length=SUMMARY_LIMIT)
|
|
307
|
+
request_id: str = Field(default="", max_length=40)
|
|
308
|
+
approve: bool | None = None
|
|
309
|
+
feedback: str = Field(default="", max_length=4000)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def extra_fields(params: SendParams) -> dict:
|
|
313
|
+
"""Each type's own fields; a missing one is invalid_message."""
|
|
314
|
+
if params.type == "plan_approval_response":
|
|
315
|
+
if params.approve is None:
|
|
316
|
+
raise ToolFailure("invalid_message", "plan_approval_response 需要 approve(true 或 false)。")
|
|
317
|
+
return {"request_id": params.request_id, "approve": params.approve, "feedback": params.feedback}
|
|
318
|
+
if params.type == "shutdown_request":
|
|
319
|
+
return {"reason": params.body or params.feedback}
|
|
320
|
+
if params.type == "shutdown_response":
|
|
321
|
+
if params.approve is None:
|
|
322
|
+
raise ToolFailure("invalid_message", "shutdown_response 需要 approve(true 或 false)。")
|
|
323
|
+
if not params.request_id:
|
|
324
|
+
raise ToolFailure("invalid_message", "shutdown_response 需要对应 shutdown_request 的 request_id。")
|
|
325
|
+
return {"request_id": params.request_id, "approve": params.approve, "reason": params.feedback or params.body}
|
|
326
|
+
return {}
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def sent(targets: list[str], kind: str) -> str:
|
|
330
|
+
return f"已发送 {kind} 给 {'、'.join(targets)}。"
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
class SendMessage(TeamTool):
|
|
334
|
+
name = "send_message"
|
|
335
|
+
description = (
|
|
336
|
+
"给团队中的其他人发消息:to 是成员名、lead 或 *(广播给除你以外的全部成员,只限 text)。"
|
|
337
|
+
"text 消息必须有一句话的 summary,正文写在 body。对方会在它的下一轮开始时收到。"
|
|
338
|
+
"plan_approval_response(只限 lead)回复成员的计划:写上 request_id、approve 和 feedback;"
|
|
339
|
+
"shutdown_request(只限 lead)请成员停止;shutdown_response(只限成员)回复停止请求。"
|
|
340
|
+
)
|
|
341
|
+
prompt_summary = "给团队成员或 lead 发消息"
|
|
342
|
+
params_model = SendParams
|
|
343
|
+
|
|
344
|
+
def __init__(self, root, runtime: TeamRuntime):
|
|
345
|
+
super().__init__(root)
|
|
346
|
+
self.runtime = runtime
|
|
347
|
+
|
|
348
|
+
async def execute(self, params: SendParams):
|
|
349
|
+
extra = extra_fields(params)
|
|
350
|
+
try:
|
|
351
|
+
targets, revived = await self.runtime.message(
|
|
352
|
+
params.to.strip(), params.type, params.body, params.summary, **extra
|
|
353
|
+
)
|
|
354
|
+
except TeamError as exc:
|
|
355
|
+
raise failure(exc) from None
|
|
356
|
+
text = sent(targets, params.type)
|
|
357
|
+
if revived:
|
|
358
|
+
text += (
|
|
359
|
+
";".join(f"{name} 原已停止,已从记录恢复({self.runtime.revived_with(name)})" for name in revived)
|
|
360
|
+
+ ",它会读到这条消息。"
|
|
361
|
+
)
|
|
362
|
+
return text, {"to": targets, "type": params.type, "revived": revived}
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
class MemberSendMessage(SendMessage):
|
|
366
|
+
def __init__(self, root, access: TeamAccess, runner: MemberRunner):
|
|
367
|
+
Tool.__init__(self, root)
|
|
368
|
+
self.access, self.runner = access, runner
|
|
369
|
+
|
|
370
|
+
async def execute(self, params: SendParams):
|
|
371
|
+
extra = extra_fields(params)
|
|
372
|
+
try:
|
|
373
|
+
targets = await self.access.asend(params.to.strip(), params.type, params.body, params.summary, **extra)
|
|
374
|
+
except TeamError as exc:
|
|
375
|
+
raise failure(exc) from None
|
|
376
|
+
if params.type == "shutdown_response":
|
|
377
|
+
self.runner.shutdown = bool(params.approve) # True stops it after this turn.
|
|
378
|
+
if params.approve:
|
|
379
|
+
return sent(targets, params.type) + "这一轮结束后你将停止;现在收尾并结束这一轮。", {
|
|
380
|
+
"to": targets,
|
|
381
|
+
"type": params.type,
|
|
382
|
+
}
|
|
383
|
+
return sent(targets, params.type), {"to": targets, "type": params.type}
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
class MemberPlanParams(Params):
|
|
387
|
+
plan: str = Field(min_length=1, max_length=20000)
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
class MemberExitPlan(Tool):
|
|
391
|
+
"""Same parameters as the main agent's; the plan goes to the lead, not the user."""
|
|
392
|
+
|
|
393
|
+
name = "exit_plan_mode"
|
|
394
|
+
plan_only = True
|
|
395
|
+
description = (
|
|
396
|
+
"计划模式下调查完成后调用,提交 Markdown 格式的实施计划给 lead 审批。提交后结束这一轮;"
|
|
397
|
+
"lead 的答复会作为下一条消息到达,批准后你才能写入、编辑和执行命令。"
|
|
398
|
+
)
|
|
399
|
+
prompt_summary = "提交计划给 lead 审批"
|
|
400
|
+
params_model = MemberPlanParams
|
|
401
|
+
|
|
402
|
+
def __init__(self, root, runner: MemberRunner):
|
|
403
|
+
super().__init__(root)
|
|
404
|
+
self.runner = runner
|
|
405
|
+
|
|
406
|
+
async def execute(self, params: MemberPlanParams):
|
|
407
|
+
try:
|
|
408
|
+
request_id = await asyncio.to_thread(self.runner.submit_plan, params.plan)
|
|
409
|
+
except TeamError as exc:
|
|
410
|
+
raise failure(exc) from None
|
|
411
|
+
self.runner.access.wake(["lead"])
|
|
412
|
+
return (
|
|
413
|
+
f"计划已提交给 lead(request_id={request_id}),等待审批;收到回复前不要继续,现在结束这一轮。",
|
|
414
|
+
{"request_id": request_id},
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
# Registries -----------------------------------------------------------------------------------------
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def lead_tools(root, runtime: TeamRuntime) -> tuple[Tool, ...]:
|
|
422
|
+
access = runtime.access
|
|
423
|
+
return (
|
|
424
|
+
TeamCreate(root, runtime),
|
|
425
|
+
TeamSpawn(root, runtime),
|
|
426
|
+
TeamDelete(root, runtime),
|
|
427
|
+
TeamMerge(root, runtime),
|
|
428
|
+
TeamTaskCreate(root, access),
|
|
429
|
+
TeamTaskGet(root, access),
|
|
430
|
+
TeamTaskList(root, access),
|
|
431
|
+
TeamTaskUpdate(root, access),
|
|
432
|
+
SendMessage(root, runtime),
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def member_tools(root, access: TeamAccess, runner: MemberRunner) -> tuple[Tool, ...]:
|
|
437
|
+
own = lambda: access # noqa: E731
|
|
438
|
+
tools: list[Tool] = [
|
|
439
|
+
MemberSendMessage(root, access, runner),
|
|
440
|
+
TeamTaskCreate(root, own),
|
|
441
|
+
TeamTaskGet(root, own),
|
|
442
|
+
TeamTaskList(root, own),
|
|
443
|
+
TeamTaskUpdate(root, own),
|
|
444
|
+
]
|
|
445
|
+
if runner.require_plan_approval:
|
|
446
|
+
tools.append(MemberExitPlan(root, runner))
|
|
447
|
+
return tuple(tools)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Windows kill-on-close Job Object for one command invocation."""
|
|
2
|
+
|
|
3
|
+
import ctypes
|
|
4
|
+
from ctypes import wintypes
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class BasicLimit(ctypes.Structure):
|
|
8
|
+
_fields_ = [
|
|
9
|
+
("ProcessTime", ctypes.c_int64),
|
|
10
|
+
("JobTime", ctypes.c_int64),
|
|
11
|
+
("Flags", wintypes.DWORD),
|
|
12
|
+
("MinWorkingSet", ctypes.c_size_t),
|
|
13
|
+
("MaxWorkingSet", ctypes.c_size_t),
|
|
14
|
+
("ActiveProcesses", wintypes.DWORD),
|
|
15
|
+
("Affinity", ctypes.c_size_t),
|
|
16
|
+
("PriorityClass", wintypes.DWORD),
|
|
17
|
+
("SchedulingClass", wintypes.DWORD),
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class IOCounters(ctypes.Structure):
|
|
22
|
+
_fields_ = [
|
|
23
|
+
(name, ctypes.c_uint64)
|
|
24
|
+
for name in ("ReadOperations", "WriteOperations", "OtherOperations", "ReadBytes", "WriteBytes", "OtherBytes")
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ExtendedLimit(ctypes.Structure):
|
|
29
|
+
_fields_ = [
|
|
30
|
+
("Basic", BasicLimit),
|
|
31
|
+
("IO", IOCounters),
|
|
32
|
+
("ProcessMemory", ctypes.c_size_t),
|
|
33
|
+
("JobMemory", ctypes.c_size_t),
|
|
34
|
+
("PeakProcessMemory", ctypes.c_size_t),
|
|
35
|
+
("PeakJobMemory", ctypes.c_size_t),
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class WindowsJob:
|
|
40
|
+
def __init__(self, pid: int):
|
|
41
|
+
api = ctypes.WinDLL("kernel32", use_last_error=True)
|
|
42
|
+
api.CreateJobObjectW.argtypes = [ctypes.c_void_p, wintypes.LPCWSTR]
|
|
43
|
+
api.CreateJobObjectW.restype = wintypes.HANDLE
|
|
44
|
+
api.SetInformationJobObject.argtypes = [wintypes.HANDLE, ctypes.c_int, ctypes.c_void_p, wintypes.DWORD]
|
|
45
|
+
api.SetInformationJobObject.restype = wintypes.BOOL
|
|
46
|
+
api.OpenProcess.argtypes = [wintypes.DWORD, wintypes.BOOL, wintypes.DWORD]
|
|
47
|
+
api.OpenProcess.restype = wintypes.HANDLE
|
|
48
|
+
api.AssignProcessToJobObject.argtypes = [wintypes.HANDLE, wintypes.HANDLE]
|
|
49
|
+
api.AssignProcessToJobObject.restype = wintypes.BOOL
|
|
50
|
+
api.CloseHandle.argtypes = [wintypes.HANDLE]
|
|
51
|
+
api.CloseHandle.restype = wintypes.BOOL
|
|
52
|
+
self.api = api
|
|
53
|
+
self.handle = api.CreateJobObjectW(None, None)
|
|
54
|
+
if not self.handle:
|
|
55
|
+
raise ctypes.WinError(ctypes.get_last_error())
|
|
56
|
+
limits = ExtendedLimit()
|
|
57
|
+
limits.Basic.Flags = 0x2000 # JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE
|
|
58
|
+
process = None
|
|
59
|
+
try:
|
|
60
|
+
if not api.SetInformationJobObject(self.handle, 9, ctypes.byref(limits), ctypes.sizeof(limits)):
|
|
61
|
+
raise ctypes.WinError(ctypes.get_last_error())
|
|
62
|
+
process = api.OpenProcess(0x0101, False, pid) # SET_QUOTA | TERMINATE
|
|
63
|
+
if not process or not api.AssignProcessToJobObject(self.handle, process):
|
|
64
|
+
raise ctypes.WinError(ctypes.get_last_error())
|
|
65
|
+
except OSError:
|
|
66
|
+
self.close()
|
|
67
|
+
raise
|
|
68
|
+
finally:
|
|
69
|
+
if process:
|
|
70
|
+
api.CloseHandle(process)
|
|
71
|
+
|
|
72
|
+
def close(self):
|
|
73
|
+
if self.handle:
|
|
74
|
+
self.api.CloseHandle(self.handle)
|
|
75
|
+
self.handle = None
|