mindcode 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. mindcode/__init__.py +5 -0
  2. mindcode/__main__.py +4 -0
  3. mindcode/__pycache__/__init__.cpython-313.pyc +0 -0
  4. mindcode/__pycache__/__main__.cpython-313.pyc +0 -0
  5. mindcode/__pycache__/_version.cpython-313.pyc +0 -0
  6. mindcode/__pycache__/config.cpython-313.pyc +0 -0
  7. mindcode/__pycache__/policy.cpython-313.pyc +0 -0
  8. mindcode/__pycache__/remote.cpython-313.pyc +0 -0
  9. mindcode/__pycache__/render.cpython-313.pyc +0 -0
  10. mindcode/__pycache__/runtime.cpython-313.pyc +0 -0
  11. mindcode/__pycache__/tasking.cpython-313.pyc +0 -0
  12. mindcode/__pycache__/terminal_core.cpython-313.pyc +0 -0
  13. mindcode/_version.py +11 -0
  14. mindcode/approval.py +105 -0
  15. mindcode/bench/__init__.py +19 -0
  16. mindcode/bench/__pycache__/__init__.cpython-313.pyc +0 -0
  17. mindcode/bench/__pycache__/fake_tools.cpython-313.pyc +0 -0
  18. mindcode/bench/__pycache__/jsonutil.cpython-313.pyc +0 -0
  19. mindcode/bench/__pycache__/paths.cpython-313.pyc +0 -0
  20. mindcode/bench/__pycache__/predictions.cpython-313.pyc +0 -0
  21. mindcode/bench/__pycache__/report.cpython-313.pyc +0 -0
  22. mindcode/bench/__pycache__/runner.cpython-313.pyc +0 -0
  23. mindcode/bench/__pycache__/schema.cpython-313.pyc +0 -0
  24. mindcode/bench/__pycache__/trace.cpython-313.pyc +0 -0
  25. mindcode/bench/__pycache__/workspace.cpython-313.pyc +0 -0
  26. mindcode/bench/adapters/__init__.py +22 -0
  27. mindcode/bench/adapters/__pycache__/__init__.cpython-313.pyc +0 -0
  28. mindcode/bench/adapters/__pycache__/base.cpython-313.pyc +0 -0
  29. mindcode/bench/adapters/__pycache__/mini_bfcl.cpython-313.pyc +0 -0
  30. mindcode/bench/adapters/__pycache__/mini_gaia.cpython-313.pyc +0 -0
  31. mindcode/bench/adapters/__pycache__/mini_terminal.cpython-313.pyc +0 -0
  32. mindcode/bench/adapters/__pycache__/swe_bench.cpython-313.pyc +0 -0
  33. mindcode/bench/adapters/__pycache__/terminal_bench.cpython-313.pyc +0 -0
  34. mindcode/bench/adapters/base.py +45 -0
  35. mindcode/bench/adapters/mini_bfcl.py +18 -0
  36. mindcode/bench/adapters/mini_gaia.py +18 -0
  37. mindcode/bench/adapters/mini_terminal.py +18 -0
  38. mindcode/bench/adapters/swe_bench.py +86 -0
  39. mindcode/bench/adapters/terminal_bench.py +107 -0
  40. mindcode/bench/fake_tools.py +73 -0
  41. mindcode/bench/jsonutil.py +20 -0
  42. mindcode/bench/paths.py +10 -0
  43. mindcode/bench/predictions.py +65 -0
  44. mindcode/bench/report.py +48 -0
  45. mindcode/bench/runner.py +250 -0
  46. mindcode/bench/schema.py +78 -0
  47. mindcode/bench/scorers/__pycache__/base.cpython-313.pyc +0 -0
  48. mindcode/bench/scorers/__pycache__/composite.cpython-313.pyc +0 -0
  49. mindcode/bench/scorers/__pycache__/exact.cpython-313.pyc +0 -0
  50. mindcode/bench/scorers/__pycache__/json_call.cpython-313.pyc +0 -0
  51. mindcode/bench/scorers/__pycache__/swe.cpython-313.pyc +0 -0
  52. mindcode/bench/scorers/__pycache__/terminal.cpython-313.pyc +0 -0
  53. mindcode/bench/scorers/__pycache__/terminal_bench.cpython-313.pyc +0 -0
  54. mindcode/bench/scorers/base.py +22 -0
  55. mindcode/bench/scorers/composite.py +57 -0
  56. mindcode/bench/scorers/exact.py +30 -0
  57. mindcode/bench/scorers/json_call.py +44 -0
  58. mindcode/bench/scorers/swe.py +29 -0
  59. mindcode/bench/scorers/terminal.py +43 -0
  60. mindcode/bench/scorers/terminal_bench.py +25 -0
  61. mindcode/bench/trace.py +34 -0
  62. mindcode/bench/workspace.py +127 -0
  63. mindcode/cli/__init__.py +112 -0
  64. mindcode/cli/__pycache__/__init__.cpython-313.pyc +0 -0
  65. mindcode/cli/__pycache__/_shared.cpython-313.pyc +0 -0
  66. mindcode/cli/_shared.py +43 -0
  67. mindcode/cli/commands/__init__.py +1 -0
  68. mindcode/cli/commands/__pycache__/__init__.cpython-313.pyc +0 -0
  69. mindcode/cli/commands/__pycache__/bench.cpython-313.pyc +0 -0
  70. mindcode/cli/commands/__pycache__/chat.cpython-313.pyc +0 -0
  71. mindcode/cli/commands/__pycache__/config_cmd.cpython-313.pyc +0 -0
  72. mindcode/cli/commands/__pycache__/remote.cpython-313.pyc +0 -0
  73. mindcode/cli/commands/__pycache__/shell.cpython-313.pyc +0 -0
  74. mindcode/cli/commands/__pycache__/status.cpython-313.pyc +0 -0
  75. mindcode/cli/commands/__pycache__/task.cpython-313.pyc +0 -0
  76. mindcode/cli/commands/__pycache__/terminal.cpython-313.pyc +0 -0
  77. mindcode/cli/commands/bench.py +365 -0
  78. mindcode/cli/commands/chat.py +86 -0
  79. mindcode/cli/commands/config_cmd.py +197 -0
  80. mindcode/cli/commands/remote.py +146 -0
  81. mindcode/cli/commands/shell.py +108 -0
  82. mindcode/cli/commands/status.py +37 -0
  83. mindcode/cli/commands/task.py +417 -0
  84. mindcode/cli/commands/terminal.py +124 -0
  85. mindcode/cli/shell/__init__.py +5 -0
  86. mindcode/cli/shell/__pycache__/__init__.cpython-313.pyc +0 -0
  87. mindcode/cli/shell/__pycache__/completion.cpython-313.pyc +0 -0
  88. mindcode/cli/shell/__pycache__/menu.cpython-313.pyc +0 -0
  89. mindcode/cli/shell/__pycache__/repl.cpython-313.pyc +0 -0
  90. mindcode/cli/shell/__pycache__/slash.cpython-313.pyc +0 -0
  91. mindcode/cli/shell/__pycache__/startup.cpython-313.pyc +0 -0
  92. mindcode/cli/shell/completion.py +72 -0
  93. mindcode/cli/shell/menu.py +88 -0
  94. mindcode/cli/shell/repl.py +597 -0
  95. mindcode/cli/shell/slash.py +480 -0
  96. mindcode/cli/shell/startup.py +55 -0
  97. mindcode/cli/shell/tui.py +897 -0
  98. mindcode/config.py +173 -0
  99. mindcode/mcp.py +205 -0
  100. mindcode/policy.py +173 -0
  101. mindcode/remote.py +216 -0
  102. mindcode/render.py +458 -0
  103. mindcode/runtime.py +541 -0
  104. mindcode/skills.py +154 -0
  105. mindcode/subagents.py +43 -0
  106. mindcode/tasking.py +281 -0
  107. mindcode/terminal/__init__.py +1 -0
  108. mindcode/terminal/__main__.py +17 -0
  109. mindcode/terminal/__pycache__/__init__.cpython-313.pyc +0 -0
  110. mindcode/terminal/__pycache__/__main__.cpython-313.pyc +0 -0
  111. mindcode/terminal_core.py +264 -0
  112. mindcode-0.2.0.dist-info/METADATA +244 -0
  113. mindcode-0.2.0.dist-info/RECORD +116 -0
  114. mindcode-0.2.0.dist-info/WHEEL +5 -0
  115. mindcode-0.2.0.dist-info/entry_points.txt +2 -0
  116. mindcode-0.2.0.dist-info/top_level.txt +1 -0
mindcode/runtime.py ADDED
@@ -0,0 +1,541 @@
1
+ from __future__ import annotations
2
+
3
+ import difflib
4
+ from collections.abc import Iterable
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from mindagent.context import ContextConfig, ContextManager
9
+ from mindagent.core import (
10
+ ActionRisk,
11
+ ActionDispatcher,
12
+ ActionType,
13
+ AgentOrchestrator,
14
+ AgentRuntime,
15
+ RunConfig,
16
+ SessionPolicy,
17
+ SubAgentExecutor,
18
+ )
19
+ from mindagent.providers import ProviderReasoner, ProviderRouter
20
+ from mindagent.providers.openai import OpenAIProvider, OpenAIProviderParam
21
+ from mindagent.tools import (
22
+ ExecCommandTool,
23
+ FileEditTool,
24
+ FileReadTool,
25
+ FileWriteTool,
26
+ ToolExecutor,
27
+ ToolRegistry,
28
+ WorkspacePathResolver,
29
+ workspace_paths,
30
+ )
31
+ from mindagent.tools.base import BaseTool, ToolContext
32
+
33
+ from .config import ProviderConfig
34
+ from .mcp import MCPClient, adapt_mcp_tools
35
+ from .policy import ApprovalMode, make_policy
36
+ from .skills import Skill, SkillRegistry
37
+ from .subagents import MasterWorkerSystem
38
+
39
+
40
+ SYSTEM_PROMPT = (
41
+ "You are mindcode, an autonomous senior engineer working inside "
42
+ "the user's workspace.\n"
43
+ "\n"
44
+ "# General\n"
45
+ "- You run inside a fixed workspace root (injected below). Only "
46
+ "read/edit files and run commands within it. Do NOT search or operate "
47
+ "outside it (e.g. no `find`/`ls` on the home directory or parent "
48
+ "paths).\n"
49
+ "- If a dedicated tool exists for an action, prefer it over shell "
50
+ "commands (`file_read` over `cat`). Prefer `file_read` for file "
51
+ "contents and `exec_command` with `rg`/`grep` for searches; `rg` is "
52
+ "much faster than `grep`. An empty search result is not an error.\n"
53
+ "- The `exec_command` tool does not invoke a shell: pass `command` as "
54
+ "an argv array, never a string, and do not include standalone shell "
55
+ "operators such as |, &&, ;, >, or <. Split pipelines into separate "
56
+ "tool calls, or explicitly invoke ['sh', '-c', '...'] when a shell is "
57
+ "required.\n"
58
+ "- Mirror the user's language (e.g. reply in Chinese when the user "
59
+ "writes Chinese). Keep answers concise.\n"
60
+ "\n"
61
+ "# Autonomy and Persistence\n"
62
+ "- Bias to action: once the user gives a direction, proactively gather "
63
+ "context, implement, verify, and refine without waiting for prompts at "
64
+ "each step. Make reasonable assumptions instead of asking clarifying "
65
+ "questions; only stop to ask when truly blocked, and then ask a "
66
+ "targeted question plus state what you have ruled out.\n"
67
+ "- Persist end-to-end within the turn whenever feasible: do not stop "
68
+ "at analysis or partial fixes; carry changes through implementation, "
69
+ "verification, and a clear closing summary unless the user explicitly "
70
+ "pauses or redirects.\n"
71
+ "- Deliver working code, not just a plan. Never end a turn with only "
72
+ "a plan unless the user asked for one.\n"
73
+ "- Avoid excessive looping: if you re-read or re-edit the same files "
74
+ "without clear progress, stop and end the turn with a concise summary "
75
+ "and the explicit blocker.\n"
76
+ "- Keep reasoning summaries empty or under 12 words; never narrate "
77
+ "progress or greet the user there — act silently and report only in "
78
+ "the final answer.\n"
79
+ "\n"
80
+ "# Exploration and Reading\n"
81
+ "- Think first: before any tool call, decide ALL files/resources you "
82
+ "will need.\n"
83
+ "- Batch independent reads: issue independent `file_read`/search calls "
84
+ "together in one block instead of one-by-one. Only go sequential when "
85
+ "the next step truly depends on the previous result.\n"
86
+ "- Edits and commands stay serial: never batch "
87
+ "`file_write`/`file_edit`/`exec_command` calls that touch the same "
88
+ "files or depend on each other; verify each consequential change "
89
+ "before moving on.\n"
90
+ "\n"
91
+ "# Code Implementation\n"
92
+ "- Optimize for correctness, clarity, and reliability over speed: fix "
93
+ "the root cause, not the symptom; avoid risky shortcuts and "
94
+ "speculative changes.\n"
95
+ "- Conform to codebase conventions: follow existing patterns, helpers, "
96
+ "naming, and formatting; if you must diverge, state why.\n"
97
+ "- Inspect before editing: read enough surrounding context first, then "
98
+ "make precise, batched edits instead of many tiny patches. Always "
99
+ "cite file paths you touched.\n"
100
+ "- Run relevant checks after changes (tests, build, type-check per "
101
+ "repo conventions) and fix failures before finishing.\n"
102
+ "- You may be in a dirty worktree: NEVER revert, discard, or amend "
103
+ "changes you did not make unless explicitly requested. If unrelated "
104
+ "changes appear in files you touched, work with them; if they are "
105
+ "elsewhere, ignore them.\n"
106
+ "\n"
107
+ "# Final Message\n"
108
+ "- Be concise and scannable: lead with the outcome, then the minimal "
109
+ "context (what changed, where, why).\n"
110
+ "- Reference file paths instead of dumping file contents. Suggest "
111
+ "logical next steps (tests, commit) briefly."
112
+ )
113
+
114
+
115
+ MASTER_SYSTEM_PROMPT = (
116
+ "You are mindcode's task coordinator. Delegate all workspace "
117
+ "inspection, editing, and command execution to the registered coder "
118
+ "agent with the agent_delegate tool. Give the coder a bounded task, "
119
+ "file scope, and verification goal. Bias to action: decompose and "
120
+ "delegate immediately instead of asking the user for details you can "
121
+ "reasonably assume. Review the returned evidence before reporting a "
122
+ "result. Do not claim work completed without worker evidence. Keep "
123
+ "reasoning summaries empty or under 12 words; never narrate progress "
124
+ "there."
125
+ )
126
+
127
+
128
+ PROJECT_CONTEXT_SKILL = (
129
+ "Built-in skill: project context discovery.\n"
130
+ "For project-specific development, debugging, deployment, model analysis, "
131
+ "benchmarking, or task execution, first discover project instructions "
132
+ "progressively instead of guessing. Locate AGENTS.md with exec_command "
133
+ "(rg --files -g AGENTS.md, or find . -name AGENTS.md). If present, "
134
+ "read it with file_read before planning. Then look for environment "
135
+ "and execution context only when relevant: REMOTES.md. "
136
+ "Use REMOTES.md for runtime environment, services, logs, remote "
137
+ "machines, deployment paths, and common operations. Keep discovery "
138
+ "simple and stable: do not look for additional convention files unless "
139
+ "the user explicitly names them. Read only the files needed for the "
140
+ "current task. Do not preload broad documentation or scan outside the "
141
+ "workspace."
142
+ )
143
+
144
+
145
+ def build_system_prompt(workspace: Path) -> str:
146
+ """注入实际 workspace 路径与边界约束,避免模型跑到 workspace 之外。"""
147
+ return (
148
+ SYSTEM_PROMPT
149
+ + "\n\n"
150
+ + PROJECT_CONTEXT_SKILL
151
+ + f"\n\nWorkspace root: {workspace}.\n"
152
+ "Only read/edit files and run commands within this workspace. "
153
+ "Do NOT search or operate outside it (e.g. no `find`/`ls` on the "
154
+ "home directory or parent paths). Prefer file_read for file "
155
+ "contents and exec_command with rg/grep for searches; an empty "
156
+ "search result is not an error."
157
+ )
158
+
159
+
160
+ def build_master_system_prompt(workspace: Path) -> str:
161
+ return (
162
+ MASTER_SYSTEM_PROMPT
163
+ + f"\n\nWorkspace root: {workspace}."
164
+ + " Every agent_delegate call must set context.agent_id to 'coder' "
165
+ "and context.mode to 'await_result'."
166
+ )
167
+
168
+
169
+ class DiffingFileTool(BaseTool):
170
+ """Wrap a file-mutating tool to capture before/after content for diffs.
171
+
172
+ Reuses the wrapped tool's ToolDefinition so ToolRegistry still routes
173
+ by name and the LLM sees the identical schema.
174
+ """
175
+
176
+ def __init__(self, inner: BaseTool, workspace_root: str | Path):
177
+ self._inner = inner
178
+ self._fallback_resolver = WorkspacePathResolver(workspace_root)
179
+ self.definition = self._inner.definition
180
+
181
+ def assess_risk(
182
+ self,
183
+ arguments: dict[str, Any],
184
+ context: Any | None = None,
185
+ ) -> ActionRisk:
186
+ return self._inner.assess_risk(arguments, context)
187
+
188
+ async def execute(
189
+ self,
190
+ arguments: dict[str, Any],
191
+ context: ToolContext,
192
+ ) -> dict[str, Any]:
193
+ resolver = workspace_paths(context, self._fallback_resolver)
194
+ rel_path = arguments.get("path", "")
195
+ before = self._safe_read(resolver, rel_path)
196
+
197
+ result = await self._inner.execute(arguments, context)
198
+
199
+ after = self._safe_read(resolver, rel_path)
200
+ result["mindcode_diff"] = _unified_diff(before, after, rel_path)
201
+ result["mindcode_before"] = before
202
+ return result
203
+
204
+ @staticmethod
205
+ def _safe_read(
206
+ resolver: WorkspacePathResolver,
207
+ rel_path: str,
208
+ ) -> str:
209
+ if not rel_path:
210
+ return ""
211
+ try:
212
+ path = resolver.resolve(rel_path, must_exist=False)
213
+ except Exception:
214
+ return ""
215
+ if not path.is_file():
216
+ return ""
217
+ try:
218
+ return path.read_text(encoding="utf-8")
219
+ except Exception:
220
+ return ""
221
+
222
+
223
+ def _unified_diff(before: str, after: str, rel_path: str) -> str:
224
+ if before == after:
225
+ return ""
226
+ diff_lines = difflib.unified_diff(
227
+ before.splitlines(keepends=True),
228
+ after.splitlines(keepends=True),
229
+ fromfile=f"a/{rel_path}",
230
+ tofile=f"b/{rel_path}",
231
+ )
232
+ return "".join(diff_lines)
233
+
234
+
235
+ _SHELL_CONTROL_TOKENS = frozenset(
236
+ {
237
+ "|",
238
+ "||",
239
+ "|&",
240
+ "&&",
241
+ ";",
242
+ ";;",
243
+ "&",
244
+ ">",
245
+ ">>",
246
+ ">|",
247
+ "<",
248
+ "<<",
249
+ "<<<",
250
+ "<&",
251
+ ">&",
252
+ "&>",
253
+ "&>>",
254
+ "2>",
255
+ "2>>",
256
+ "2>&1",
257
+ }
258
+ )
259
+
260
+
261
+ class GuardedExecCommandTool(BaseTool):
262
+ """Reject shell syntax before delegating to direct argv execution."""
263
+
264
+ def __init__(self, inner: ExecCommandTool):
265
+ self._inner = inner
266
+ # Keep the exact definition so providers continue to see the
267
+ # mindagent exec_command schema.
268
+ self.definition = inner.definition
269
+
270
+ def assess_risk(
271
+ self,
272
+ arguments: dict[str, Any],
273
+ context: Any | None = None,
274
+ ) -> ActionRisk:
275
+ return self._inner.assess_risk(arguments, context)
276
+
277
+ async def execute(
278
+ self,
279
+ arguments: dict[str, Any],
280
+ context: ToolContext,
281
+ ) -> dict[str, Any]:
282
+ command = arguments.get("command")
283
+ if isinstance(command, list):
284
+ for argument in command:
285
+ if (
286
+ isinstance(argument, str)
287
+ and argument in _SHELL_CONTROL_TOKENS
288
+ ):
289
+ raise ValueError(
290
+ "exec_command 不经过 shell,不能把 shell 控制符 "
291
+ f"{argument!r} 作为独立参数传入;请拆分命令,"
292
+ "或显式使用 ['sh', '-c', '...'] 等合适的 argv。"
293
+ )
294
+ return await self._inner.execute(arguments, context)
295
+
296
+
297
+ def build_runtime(
298
+ workspace: Path,
299
+ *,
300
+ provider_config: ProviderConfig,
301
+ approve_mode: ApprovalMode,
302
+ model: str | None = None,
303
+ stream: bool = True,
304
+ event_handler: Any | None = None,
305
+ max_steps: int = 50,
306
+ step_timeout_s: float | None = 300.0,
307
+ total_timeout_s: float | None = 1800.0,
308
+ extra_tools: Iterable[BaseTool] | None = None,
309
+ skills: Iterable[Skill] | None = None,
310
+ mcp_clients: Iterable[MCPClient] | None = None,
311
+ approval_callback: Any | None = None,
312
+ ) -> AgentRuntime:
313
+ skill_registry = SkillRegistry(skills)
314
+ mcp_tools: list[BaseTool] = []
315
+ for client in mcp_clients or ():
316
+ mcp_tools.extend(adapt_mcp_tools(client))
317
+
318
+ registry = ToolRegistry(
319
+ [
320
+ FileReadTool(workspace),
321
+ DiffingFileTool(FileWriteTool(workspace), workspace),
322
+ DiffingFileTool(FileEditTool(workspace), workspace),
323
+ GuardedExecCommandTool(ExecCommandTool(workspace)),
324
+ *skill_registry.tools(),
325
+ *mcp_tools,
326
+ *(extra_tools or ()),
327
+ ]
328
+ )
329
+
330
+ # ``step_timeout_s=None`` disables the Runtime's ReAct step deadline; it
331
+ # must not turn into an arbitrary 330s provider deadline. Leave the
332
+ # provider timeout unset in that case so the Provider layer applies its
333
+ # own default request timeout.
334
+ provider_timeout_s = (
335
+ None
336
+ if step_timeout_s is None
337
+ else max(120.0, step_timeout_s + 30.0)
338
+ )
339
+ reasoner = _build_provider_reasoner(
340
+ provider_config,
341
+ model=model,
342
+ tools=registry.provider_schemas(),
343
+ action_risks=registry.action_risks(),
344
+ action_risk_resolver=registry.assess_risk,
345
+ stream=stream,
346
+ timeout_s=provider_timeout_s,
347
+ # 产品层单次模型请求期限,默认取 step_timeout_s;与传输层 timeout
348
+ # (OpenAIProviderParam.timeout) 正交,先到者为准。
349
+ request_timeout_s=step_timeout_s,
350
+ )
351
+
352
+ policy_engine = make_policy(
353
+ approve_mode,
354
+ approval_callback=approval_callback,
355
+ )
356
+
357
+ system_prompt = build_system_prompt(workspace)
358
+ skill_instructions = skill_registry.instructions()
359
+ if skill_instructions:
360
+ system_prompt += "\n\n" + skill_instructions
361
+
362
+ return AgentRuntime(
363
+ reasoner,
364
+ ToolExecutor(registry),
365
+ context_manager=ContextManager(
366
+ ContextConfig(system_prompt=system_prompt)
367
+ ),
368
+ policy_engine=policy_engine,
369
+ event_handler=event_handler,
370
+ config=RunConfig(
371
+ max_steps=max_steps,
372
+ step_timeout_s=step_timeout_s,
373
+ total_timeout_s=total_timeout_s,
374
+ # policy_timeout_s 已废弃:core 侧同步 policy 不再设 deadline,统一 None。
375
+ policy_timeout_s=None,
376
+ enable_heartbeat=False,
377
+ ),
378
+ )
379
+
380
+
381
+ def build_master_worker_system(
382
+ workspace: Path,
383
+ *,
384
+ provider_config: ProviderConfig,
385
+ approve_mode: ApprovalMode,
386
+ model: str | None = None,
387
+ stream: bool = True,
388
+ event_handler: Any | None = None,
389
+ max_steps: int = 50,
390
+ step_timeout_s: float | None = 300.0,
391
+ total_timeout_s: float | None = 1800.0,
392
+ skills: Iterable[Skill] | None = None,
393
+ mcp_clients: Iterable[MCPClient] | None = None,
394
+ approval_callback: Any | None = None,
395
+ ) -> MasterWorkerSystem:
396
+ """Build a restricted master and one workspace-capable coder worker."""
397
+ orchestrator = AgentOrchestrator()
398
+
399
+ async def worker_event_handler(event: Any) -> None:
400
+ await orchestrator.handle_agent_event(event)
401
+ if event_handler is not None:
402
+ await event_handler(event)
403
+
404
+ provider_timeout_s = (
405
+ None
406
+ if step_timeout_s is None
407
+ else max(120.0, step_timeout_s + 30.0)
408
+ )
409
+ worker = build_runtime(
410
+ workspace,
411
+ provider_config=provider_config,
412
+ approve_mode=approve_mode,
413
+ model=model,
414
+ stream=stream,
415
+ event_handler=worker_event_handler,
416
+ max_steps=max_steps,
417
+ step_timeout_s=step_timeout_s,
418
+ total_timeout_s=total_timeout_s,
419
+ skills=skills,
420
+ mcp_clients=mcp_clients,
421
+ approval_callback=approval_callback,
422
+ )
423
+ coder_tool_names = frozenset(
424
+ schema.get("function", {}).get("name")
425
+ for schema in worker.reasoner.tools
426
+ if schema.get("function", {}).get("name") not in {
427
+ ProviderReasoner.ASK_USER_TOOL,
428
+ ProviderReasoner.COMPLETE_TOOL,
429
+ }
430
+ )
431
+ orchestrator.register_agent(
432
+ "coder",
433
+ worker,
434
+ session_policy=SessionPolicy(
435
+ allowed_action_types=frozenset({ActionType.TOOL}),
436
+ allowed_tools=coder_tool_names,
437
+ allow_write=True,
438
+ allow_dangerous=False,
439
+ max_parallel_actions=1,
440
+ ),
441
+ )
442
+
443
+ delegate_modes = frozenset({"await_result"})
444
+ dispatcher = ActionDispatcher()
445
+ dispatcher.register(
446
+ ActionType.AGENT,
447
+ SubAgentExecutor(
448
+ orchestrator.run_child,
449
+ allowed_modes=delegate_modes,
450
+ ),
451
+ )
452
+ master_reasoner = _build_provider_reasoner(
453
+ provider_config,
454
+ model=model,
455
+ tools=SubAgentExecutor.tool_schemas(modes=delegate_modes),
456
+ action_types=SubAgentExecutor.action_types(),
457
+ stream=stream,
458
+ timeout_s=provider_timeout_s,
459
+ # 同 worker:产品层期限默认取 step_timeout_s,与传输层 timeout 正交。
460
+ request_timeout_s=step_timeout_s,
461
+ )
462
+ master = AgentRuntime(
463
+ master_reasoner,
464
+ dispatcher,
465
+ context_manager=ContextManager(
466
+ ContextConfig(system_prompt=build_master_system_prompt(workspace))
467
+ ),
468
+ policy_engine=make_policy(
469
+ approve_mode,
470
+ approval_callback=approval_callback,
471
+ ),
472
+ event_handler=event_handler,
473
+ config=RunConfig(
474
+ max_steps=max_steps,
475
+ step_timeout_s=step_timeout_s,
476
+ total_timeout_s=total_timeout_s,
477
+ # policy_timeout_s 已废弃:core 侧同步 policy 不再设 deadline,统一 None。
478
+ policy_timeout_s=None,
479
+ enable_heartbeat=False,
480
+ ),
481
+ )
482
+ orchestrator.register_agent(
483
+ "master",
484
+ master,
485
+ session_policy=SessionPolicy(
486
+ allowed_action_types=frozenset({ActionType.AGENT}),
487
+ allow_write=False,
488
+ allow_dangerous=False,
489
+ max_parallel_actions=1,
490
+ ),
491
+ )
492
+ return MasterWorkerSystem(orchestrator)
493
+
494
+
495
+ def _build_provider_reasoner(
496
+ provider_config: ProviderConfig,
497
+ *,
498
+ model: str | None,
499
+ tools: list[dict[str, Any]],
500
+ action_types: dict[str, ActionType] | None = None,
501
+ action_risks: dict[str, ActionRisk] | None = None,
502
+ action_risk_resolver: Any | None = None,
503
+ stream: bool,
504
+ timeout_s: float | None = None,
505
+ request_timeout_s: float | None = None,
506
+ ) -> ProviderReasoner:
507
+ model_name = provider_config.resolve_model(model)
508
+ if provider_config.protocol != "openai":
509
+ raise ValueError(
510
+ f"provider {provider_config.name} 使用 protocol="
511
+ f"{provider_config.protocol!r};当前 runtime 只支持 openai "
512
+ "compatible endpoint"
513
+ )
514
+ if not provider_config.base_url:
515
+ raise ValueError(
516
+ f"provider {provider_config.name} 缺少 base_url;"
517
+ f"编辑 ~/.cache/mindcode/config.toml"
518
+ )
519
+
520
+ # Pass a placeholder when api_key is empty so AsyncOpenAI can construct.
521
+ # The real error surfaces on the first chat() call; until then the user
522
+ # can fix it from the REPL via /api-key without restarting.
523
+ api_key = provider_config.api_key or "pending-api-key"
524
+ param = OpenAIProviderParam(
525
+ model=model_name,
526
+ api_key=api_key,
527
+ base_url=provider_config.base_url,
528
+ timeout=timeout_s if timeout_s is not None else 120.0,
529
+ )
530
+ provider = OpenAIProvider(param)
531
+ router = ProviderRouter([provider])
532
+
533
+ return ProviderReasoner(
534
+ router,
535
+ tools=tools,
536
+ action_types=action_types,
537
+ action_risks=action_risks,
538
+ action_risk_resolver=action_risk_resolver,
539
+ stream=stream,
540
+ request_timeout_s=request_timeout_s,
541
+ )
mindcode/skills.py ADDED
@@ -0,0 +1,154 @@
1
+ """MindCode-owned skill registration and loading.
2
+
3
+ Skills are intentionally small: an instruction block can be injected into the
4
+ runtime system prompt and may optionally contribute ordinary MindAgent tools.
5
+ The registry does not execute skills or depend on a plugin framework.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from collections.abc import Iterable, Iterator
11
+ from dataclasses import dataclass, field
12
+ from pathlib import Path
13
+
14
+ from mindagent.tools.base import BaseTool
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class Skill:
19
+ """A named instruction block with optional tools."""
20
+
21
+ name: str
22
+ instructions: str
23
+ tools: tuple[BaseTool, ...] = field(default_factory=tuple)
24
+
25
+ def __post_init__(self) -> None:
26
+ name = self.name.strip()
27
+ if not name:
28
+ raise ValueError("skill name 不能为空")
29
+ if not self.instructions.strip():
30
+ raise ValueError(f"skill {name!r} instructions 不能为空")
31
+ object.__setattr__(self, "name", name)
32
+ object.__setattr__(self, "instructions", self.instructions.strip())
33
+ object.__setattr__(self, "tools", tuple(self.tools))
34
+
35
+
36
+ class SkillRegistry:
37
+ """Deterministic in-memory skill registry owned by MindCode."""
38
+
39
+ def __init__(self, skills: Iterable[Skill] | None = None):
40
+ self._skills: dict[str, Skill] = {}
41
+ for skill in skills or ():
42
+ self.register(skill)
43
+
44
+ def register(self, skill: Skill) -> None:
45
+ if not isinstance(skill, Skill):
46
+ raise TypeError("只能注册 Skill 实例")
47
+ if skill.name in self._skills:
48
+ raise ValueError(f"Skill 已注册: {skill.name}")
49
+ self._skills[skill.name] = skill
50
+
51
+ def get(self, name: str) -> Skill:
52
+ try:
53
+ return self._skills[name]
54
+ except KeyError as exc:
55
+ raise KeyError(f"Skill 不存在: {name}") from exc
56
+
57
+ def __iter__(self) -> Iterator[Skill]:
58
+ return iter(self._skills.values())
59
+
60
+ def __len__(self) -> int:
61
+ return len(self._skills)
62
+
63
+ def instructions(self) -> str:
64
+ """Render registered instructions for a system prompt."""
65
+ return "\n\n".join(
66
+ f"Skill: {skill.name}\n{skill.instructions}"
67
+ for skill in self
68
+ )
69
+
70
+ def tools(self) -> list[BaseTool]:
71
+ """Return skill-provided tools in registration order."""
72
+ return [tool for skill in self for tool in skill.tools]
73
+
74
+
75
+ class SkillLoader:
76
+ """Load plain Markdown skill files without external dependencies.
77
+
78
+ A file named ``SKILL.md`` uses its parent directory name by default; other
79
+ Markdown files use their stem. An optional minimal front matter header is
80
+ supported for a custom name::
81
+
82
+ ---
83
+ name: repository-guidance
84
+ ---
85
+ ...instructions...
86
+ """
87
+
88
+ def load_file(self, path: str | Path, *, name: str | None = None) -> Skill:
89
+ source = Path(path)
90
+ text = source.read_text(encoding="utf-8")
91
+ front_name, body = _parse_front_matter(text)
92
+ skill_name = name or front_name or _default_name(source)
93
+ return Skill(skill_name, body)
94
+
95
+ def load_directory(
96
+ self,
97
+ path: str | Path,
98
+ *,
99
+ pattern: str = "SKILL.md",
100
+ ) -> list[Skill]:
101
+ root = Path(path)
102
+ if not root.is_dir():
103
+ raise ValueError(f"skill 目录不存在: {root}")
104
+ return [
105
+ self.load_file(source)
106
+ for source in sorted(root.rglob(pattern))
107
+ if source.is_file()
108
+ ]
109
+
110
+ def load_into(
111
+ self,
112
+ registry: SkillRegistry,
113
+ paths: Iterable[str | Path],
114
+ ) -> list[Skill]:
115
+ loaded: list[Skill] = []
116
+ for path in paths:
117
+ source = Path(path)
118
+ items = (
119
+ self.load_directory(source)
120
+ if source.is_dir()
121
+ else [self.load_file(source)]
122
+ )
123
+ for skill in items:
124
+ registry.register(skill)
125
+ loaded.append(skill)
126
+ return loaded
127
+
128
+
129
+ def _default_name(path: Path) -> str:
130
+ if path.name.lower() == "skill.md":
131
+ return path.parent.name
132
+ return path.stem
133
+
134
+
135
+ def _parse_front_matter(text: str) -> tuple[str | None, str]:
136
+ if not text.startswith("---\n"):
137
+ return None, text
138
+ marker = "\n---\n"
139
+ end = text.find(marker, 4)
140
+ if end < 0:
141
+ return None, text
142
+ header = text[4:end]
143
+ name: str | None = None
144
+ for line in header.splitlines():
145
+ key, separator, value = line.partition(":")
146
+ if separator and key.strip().lower() == "name":
147
+ value = value.strip()
148
+ if value:
149
+ name = value.strip("\"'")
150
+ break
151
+ return name, text[end + len(marker) :]
152
+
153
+
154
+ __all__ = ["Skill", "SkillLoader", "SkillRegistry"]