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.
Files changed (91) hide show
  1. mosscode/__init__.py +10 -0
  2. mosscode/__main__.py +3 -0
  3. mosscode/agent.py +486 -0
  4. mosscode/agents/__init__.py +249 -0
  5. mosscode/agents/builtin/explore.md +16 -0
  6. mosscode/agents/builtin/general.md +12 -0
  7. mosscode/agents/builtin/plan.md +16 -0
  8. mosscode/agents/runtime.py +685 -0
  9. mosscode/agents/tasks.py +193 -0
  10. mosscode/appearance.py +184 -0
  11. mosscode/builtin_commands.py +1363 -0
  12. mosscode/cli.py +547 -0
  13. mosscode/commands.py +254 -0
  14. mosscode/compaction.py +259 -0
  15. mosscode/config.py +337 -0
  16. mosscode/context.py +102 -0
  17. mosscode/conversation.py +1046 -0
  18. mosscode/coordinator.py +54 -0
  19. mosscode/doctor.py +303 -0
  20. mosscode/hooks/__init__.py +22 -0
  21. mosscode/hooks/actions.py +151 -0
  22. mosscode/hooks/conditions.py +170 -0
  23. mosscode/hooks/config.py +404 -0
  24. mosscode/hooks/engine.py +401 -0
  25. mosscode/hooks/events.py +48 -0
  26. mosscode/launcher.py +177 -0
  27. mosscode/mcp_client.py +460 -0
  28. mosscode/mcp_config.py +216 -0
  29. mosscode/member_ui.py +289 -0
  30. mosscode/memory.py +291 -0
  31. mosscode/memory_extract.py +245 -0
  32. mosscode/messages.py +216 -0
  33. mosscode/paths.py +31 -0
  34. mosscode/permissions.py +733 -0
  35. mosscode/prompts.py +483 -0
  36. mosscode/providers.py +587 -0
  37. mosscode/scrollback.py +106 -0
  38. mosscode/security.py +282 -0
  39. mosscode/serialization.py +121 -0
  40. mosscode/session.py +563 -0
  41. mosscode/setup.py +248 -0
  42. mosscode/skills/__init__.py +455 -0
  43. mosscode/skills/builtin/commit/SKILL.md +15 -0
  44. mosscode/skills/builtin/review/SKILL.md +15 -0
  45. mosscode/skills/builtin/test/SKILL.md +13 -0
  46. mosscode/skills/builtin/test/scripts/detect.py +96 -0
  47. mosscode/skills/builtin/test/tool.json +9 -0
  48. mosscode/teams/__init__.py +216 -0
  49. mosscode/teams/backends.py +259 -0
  50. mosscode/teams/board.py +230 -0
  51. mosscode/teams/lock.py +173 -0
  52. mosscode/teams/mailbox.py +172 -0
  53. mosscode/teams/member.py +427 -0
  54. mosscode/teams/merge.py +207 -0
  55. mosscode/teams/pane.py +20 -0
  56. mosscode/teams/pane_app.py +167 -0
  57. mosscode/teams/process.py +371 -0
  58. mosscode/teams/runtime.py +1355 -0
  59. mosscode/teams/view.py +182 -0
  60. mosscode/tool_budget.py +86 -0
  61. mosscode/tools/__init__.py +38 -0
  62. mosscode/tools/agent.py +127 -0
  63. mosscode/tools/base.py +384 -0
  64. mosscode/tools/diffing.py +16 -0
  65. mosscode/tools/execution.py +99 -0
  66. mosscode/tools/files.py +287 -0
  67. mosscode/tools/memory.py +65 -0
  68. mosscode/tools/plan.py +30 -0
  69. mosscode/tools/process.py +216 -0
  70. mosscode/tools/search.py +229 -0
  71. mosscode/tools/search_worker.py +243 -0
  72. mosscode/tools/shell.py +121 -0
  73. mosscode/tools/skill.py +118 -0
  74. mosscode/tools/team.py +447 -0
  75. mosscode/tools/windows_job.py +75 -0
  76. mosscode/tui.py +2325 -0
  77. mosscode/usage.py +68 -0
  78. mosscode/windows_inline.py +148 -0
  79. mosscode/worktree/__init__.py +29 -0
  80. mosscode/worktree/changes.py +59 -0
  81. mosscode/worktree/git.py +125 -0
  82. mosscode/worktree/manager.py +462 -0
  83. mosscode/worktree/meta.py +82 -0
  84. mosscode/worktree/names.py +52 -0
  85. mosscode/worktree/setup.py +119 -0
  86. mosscode-0.1.0.dist-info/METADATA +92 -0
  87. mosscode-0.1.0.dist-info/RECORD +91 -0
  88. mosscode-0.1.0.dist-info/WHEEL +5 -0
  89. mosscode-0.1.0.dist-info/entry_points.txt +3 -0
  90. mosscode-0.1.0.dist-info/licenses/LICENSE +21 -0
  91. mosscode-0.1.0.dist-info/top_level.txt +1 -0
@@ -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