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.
- mindcode/__init__.py +5 -0
- mindcode/__main__.py +4 -0
- mindcode/__pycache__/__init__.cpython-313.pyc +0 -0
- mindcode/__pycache__/__main__.cpython-313.pyc +0 -0
- mindcode/__pycache__/_version.cpython-313.pyc +0 -0
- mindcode/__pycache__/config.cpython-313.pyc +0 -0
- mindcode/__pycache__/policy.cpython-313.pyc +0 -0
- mindcode/__pycache__/remote.cpython-313.pyc +0 -0
- mindcode/__pycache__/render.cpython-313.pyc +0 -0
- mindcode/__pycache__/runtime.cpython-313.pyc +0 -0
- mindcode/__pycache__/tasking.cpython-313.pyc +0 -0
- mindcode/__pycache__/terminal_core.cpython-313.pyc +0 -0
- mindcode/_version.py +11 -0
- mindcode/approval.py +105 -0
- mindcode/bench/__init__.py +19 -0
- mindcode/bench/__pycache__/__init__.cpython-313.pyc +0 -0
- mindcode/bench/__pycache__/fake_tools.cpython-313.pyc +0 -0
- mindcode/bench/__pycache__/jsonutil.cpython-313.pyc +0 -0
- mindcode/bench/__pycache__/paths.cpython-313.pyc +0 -0
- mindcode/bench/__pycache__/predictions.cpython-313.pyc +0 -0
- mindcode/bench/__pycache__/report.cpython-313.pyc +0 -0
- mindcode/bench/__pycache__/runner.cpython-313.pyc +0 -0
- mindcode/bench/__pycache__/schema.cpython-313.pyc +0 -0
- mindcode/bench/__pycache__/trace.cpython-313.pyc +0 -0
- mindcode/bench/__pycache__/workspace.cpython-313.pyc +0 -0
- mindcode/bench/adapters/__init__.py +22 -0
- mindcode/bench/adapters/__pycache__/__init__.cpython-313.pyc +0 -0
- mindcode/bench/adapters/__pycache__/base.cpython-313.pyc +0 -0
- mindcode/bench/adapters/__pycache__/mini_bfcl.cpython-313.pyc +0 -0
- mindcode/bench/adapters/__pycache__/mini_gaia.cpython-313.pyc +0 -0
- mindcode/bench/adapters/__pycache__/mini_terminal.cpython-313.pyc +0 -0
- mindcode/bench/adapters/__pycache__/swe_bench.cpython-313.pyc +0 -0
- mindcode/bench/adapters/__pycache__/terminal_bench.cpython-313.pyc +0 -0
- mindcode/bench/adapters/base.py +45 -0
- mindcode/bench/adapters/mini_bfcl.py +18 -0
- mindcode/bench/adapters/mini_gaia.py +18 -0
- mindcode/bench/adapters/mini_terminal.py +18 -0
- mindcode/bench/adapters/swe_bench.py +86 -0
- mindcode/bench/adapters/terminal_bench.py +107 -0
- mindcode/bench/fake_tools.py +73 -0
- mindcode/bench/jsonutil.py +20 -0
- mindcode/bench/paths.py +10 -0
- mindcode/bench/predictions.py +65 -0
- mindcode/bench/report.py +48 -0
- mindcode/bench/runner.py +250 -0
- mindcode/bench/schema.py +78 -0
- mindcode/bench/scorers/__pycache__/base.cpython-313.pyc +0 -0
- mindcode/bench/scorers/__pycache__/composite.cpython-313.pyc +0 -0
- mindcode/bench/scorers/__pycache__/exact.cpython-313.pyc +0 -0
- mindcode/bench/scorers/__pycache__/json_call.cpython-313.pyc +0 -0
- mindcode/bench/scorers/__pycache__/swe.cpython-313.pyc +0 -0
- mindcode/bench/scorers/__pycache__/terminal.cpython-313.pyc +0 -0
- mindcode/bench/scorers/__pycache__/terminal_bench.cpython-313.pyc +0 -0
- mindcode/bench/scorers/base.py +22 -0
- mindcode/bench/scorers/composite.py +57 -0
- mindcode/bench/scorers/exact.py +30 -0
- mindcode/bench/scorers/json_call.py +44 -0
- mindcode/bench/scorers/swe.py +29 -0
- mindcode/bench/scorers/terminal.py +43 -0
- mindcode/bench/scorers/terminal_bench.py +25 -0
- mindcode/bench/trace.py +34 -0
- mindcode/bench/workspace.py +127 -0
- mindcode/cli/__init__.py +112 -0
- mindcode/cli/__pycache__/__init__.cpython-313.pyc +0 -0
- mindcode/cli/__pycache__/_shared.cpython-313.pyc +0 -0
- mindcode/cli/_shared.py +43 -0
- mindcode/cli/commands/__init__.py +1 -0
- mindcode/cli/commands/__pycache__/__init__.cpython-313.pyc +0 -0
- mindcode/cli/commands/__pycache__/bench.cpython-313.pyc +0 -0
- mindcode/cli/commands/__pycache__/chat.cpython-313.pyc +0 -0
- mindcode/cli/commands/__pycache__/config_cmd.cpython-313.pyc +0 -0
- mindcode/cli/commands/__pycache__/remote.cpython-313.pyc +0 -0
- mindcode/cli/commands/__pycache__/shell.cpython-313.pyc +0 -0
- mindcode/cli/commands/__pycache__/status.cpython-313.pyc +0 -0
- mindcode/cli/commands/__pycache__/task.cpython-313.pyc +0 -0
- mindcode/cli/commands/__pycache__/terminal.cpython-313.pyc +0 -0
- mindcode/cli/commands/bench.py +365 -0
- mindcode/cli/commands/chat.py +86 -0
- mindcode/cli/commands/config_cmd.py +197 -0
- mindcode/cli/commands/remote.py +146 -0
- mindcode/cli/commands/shell.py +108 -0
- mindcode/cli/commands/status.py +37 -0
- mindcode/cli/commands/task.py +417 -0
- mindcode/cli/commands/terminal.py +124 -0
- mindcode/cli/shell/__init__.py +5 -0
- mindcode/cli/shell/__pycache__/__init__.cpython-313.pyc +0 -0
- mindcode/cli/shell/__pycache__/completion.cpython-313.pyc +0 -0
- mindcode/cli/shell/__pycache__/menu.cpython-313.pyc +0 -0
- mindcode/cli/shell/__pycache__/repl.cpython-313.pyc +0 -0
- mindcode/cli/shell/__pycache__/slash.cpython-313.pyc +0 -0
- mindcode/cli/shell/__pycache__/startup.cpython-313.pyc +0 -0
- mindcode/cli/shell/completion.py +72 -0
- mindcode/cli/shell/menu.py +88 -0
- mindcode/cli/shell/repl.py +597 -0
- mindcode/cli/shell/slash.py +480 -0
- mindcode/cli/shell/startup.py +55 -0
- mindcode/cli/shell/tui.py +897 -0
- mindcode/config.py +173 -0
- mindcode/mcp.py +205 -0
- mindcode/policy.py +173 -0
- mindcode/remote.py +216 -0
- mindcode/render.py +458 -0
- mindcode/runtime.py +541 -0
- mindcode/skills.py +154 -0
- mindcode/subagents.py +43 -0
- mindcode/tasking.py +281 -0
- mindcode/terminal/__init__.py +1 -0
- mindcode/terminal/__main__.py +17 -0
- mindcode/terminal/__pycache__/__init__.cpython-313.pyc +0 -0
- mindcode/terminal/__pycache__/__main__.cpython-313.pyc +0 -0
- mindcode/terminal_core.py +264 -0
- mindcode-0.2.0.dist-info/METADATA +244 -0
- mindcode-0.2.0.dist-info/RECORD +116 -0
- mindcode-0.2.0.dist-info/WHEEL +5 -0
- mindcode-0.2.0.dist-info/entry_points.txt +2 -0
- 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"]
|