@ai-zen/agents-sdk 1.0.0-alpha.22 → 1.0.0-alpha.23
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.
- package/CHANGELOG.md +216 -184
- package/README.md +13 -0
- package/README.zh.md +13 -0
- package/dist/agent-plugins/AutoMigratePlugin.js +1 -1
- package/dist/agent-plugins/UnknownToolHintPlugin.js +4 -4
- package/dist/config/constants.js +24 -24
- package/dist/config/factory-skills.js +56 -56
- package/dist/runtime/HandoffGuardPlugin.js +2 -2
- package/dist/runtime/HandoffService.js +22 -22
- package/dist/runtime/createAgent.js +2 -2
- package/dist/runtime/createModel.js +3 -3
- package/dist/scope/PermissionEvaluator.js +1 -1
- package/dist/scope/Scope.js +1 -1
- package/dist/scope/plugins/builtin/BatchEditTool.js +8 -8
- package/dist/scope/plugins/builtin/CopyTool.js +4 -4
- package/dist/scope/plugins/builtin/CwdTool.js +1 -1
- package/dist/scope/plugins/builtin/DownloadFileTool.js +8 -8
- package/dist/scope/plugins/builtin/EditTool.js +9 -9
- package/dist/scope/plugins/builtin/ExecTool.js +8 -8
- package/dist/scope/plugins/builtin/ExistTool.js +2 -2
- package/dist/scope/plugins/builtin/FindTextTool.js +10 -10
- package/dist/scope/plugins/builtin/GenerateImageTool.js +13 -13
- package/dist/scope/plugins/builtin/GlobTool.js +4 -4
- package/dist/scope/plugins/builtin/InspectFileTool.js +7 -7
- package/dist/scope/plugins/builtin/LsTool.js +5 -5
- package/dist/scope/plugins/builtin/MkdirTool.js +3 -3
- package/dist/scope/plugins/builtin/ReadTool.js +8 -8
- package/dist/scope/plugins/builtin/RenameTool.js +3 -3
- package/dist/scope/plugins/builtin/RmTool.js +3 -3
- package/dist/scope/plugins/builtin/SleepTool.js +6 -6
- package/dist/scope/plugins/builtin/SpawnTool.js +9 -9
- package/dist/scope/plugins/builtin/ViewImageTool.js +12 -12
- package/dist/scope/plugins/builtin/WriteTool.js +3 -3
- package/dist/scope/plugins/mcp/McpConnectionManager.js +7 -7
- package/dist/scope/plugins/mcp/discover.js +1 -1
- package/dist/scope/plugins/mcp/mcpTools.js +29 -29
- package/dist/scope/plugins/skills/discover.js +7 -7
- package/dist/scope/plugins/skills/skillTools.js +17 -17
- package/dist/scope/plugins/subagents/subAgentTools.js +2 -2
- package/dist/scope/plugins/usertools/discover.js +2 -2
- package/dist/shared/content-blocks.js +9 -9
- package/dist/shared/errors.js +3 -3
- package/package.json +10 -1
package/README.md
CHANGED
|
@@ -136,6 +136,19 @@ Interrupt strategies live in the config: `AppConfig.abortedMessageStrategy` (`ke
|
|
|
136
136
|
| `ContextGuardPlugin` | Context safety guard — before each request, throws `ContextOverflowError` (interrupting the conversation) when the previous round's `usage.prompt_tokens` exceeds `maxTokens × ratio` (default 1.5), preventing context runaway from reading oversized files |
|
|
137
137
|
| `UnknownToolHintPlugin` | Smarter unknown-tool hints — when the LLM calls a nonexistent tool, guides it to `call_mcp_tool` / points out permission issues based on MCP config (opt-in via `agent.use`) |
|
|
138
138
|
|
|
139
|
+
## Language
|
|
140
|
+
|
|
141
|
+
The SDK is **English-only**: every model-facing and user-facing string — tool schema descriptions, factory prompts, runtime-injected guidance, tool results, error messages, factory catalog data — is a fixed English literal. There is no locale field, no message catalog, and no runtime language switch. UI language belongs entirely to each consumer (the CLI's `i18n/`, the Desktop's language setting).
|
|
142
|
+
|
|
143
|
+
This is a performance boundary, not a convenience choice — **SDK behaviour and performance must not vary by the user's language**:
|
|
144
|
+
|
|
145
|
+
- **Thresholds are absolute.** `Model.tokenThresholds.handoff` / `guard` and `AppConfig.maxToolOutput` are absolute token / character counts. A localized tool schema is a different token count, so handoff and context-guard would trip at *different amounts of real content* for the same `config.json` — same config, different behaviour per language.
|
|
146
|
+
- **Schema cost is paid every turn.** Description text is re-sent with every request, so a token delta is a standing cost in request size, context usage and spend rather than a one-off.
|
|
147
|
+
- **Cache prefixes stop being comparable.** Prompt caching matches on the prefix; letting language into the SDK multiplies the prefix shapes of one code path and removes the single variable that benchmarks rely on.
|
|
148
|
+
- **Diagnosis needs one shape.** Performance data, bug reproductions and tool-call success rates have to be comparable across users; once behaviour drifts with locale, every conclusion needs a "what language are you on?" qualifier first.
|
|
149
|
+
|
|
150
|
+
Following the user's language is handled by one language-neutral instruction in the factory system prompt — `Always reply in the same language the user writes in.` — which adapts per **message** rather than per installation, and needs no locale plumbing in the SDK. Full rationale: [`docs/sdk-design-v1.md` §1](docs/sdk-design-v1.md).
|
|
151
|
+
|
|
139
152
|
## Design Principles
|
|
140
153
|
|
|
141
154
|
See the project-root [`PRINCIPLES.md`](../../PRINCIPLES.md):
|
package/README.zh.md
CHANGED
|
@@ -135,6 +135,19 @@ await scope.dispose(); // 断开 MCP、释放插件资源
|
|
|
135
135
|
| `ContextGuardPlugin` | 上下文安全护栏 — 每轮发请求前检测上一轮 `usage.prompt_tokens`,超过 `maxTokens × ratio`(默认 1.5,即 +50%)时抛 `ContextOverflowError` 中断对话,防止读入超大文件导致上下文失控 |
|
|
136
136
|
| `UnknownToolHintPlugin` | 未知工具智能提示 — LLM 调用不存在的工具时,根据 MCP 配置引导使用 `call_mcp_tool` / 提示权限问题(调用方显式 `agent.use` 注册) |
|
|
137
137
|
|
|
138
|
+
## 语言
|
|
139
|
+
|
|
140
|
+
SDK 是 **English-only**:所有面向模型与面向用户的文案(工具 schema 描述、出厂提示词、运行期注入语、工具返回值、错误信息、出厂 catalog 数据)都是**固定英文字面量**。没有 locale 字段、没有语言包、没有运行期切换。界面语言完全属于各端(CLI 的 `i18n/`、Desktop 的语言设置)。
|
|
141
|
+
|
|
142
|
+
这是**性能边界**,不是图省事 —— **SDK 的行为与性能不得随用户语言变化**:
|
|
143
|
+
|
|
144
|
+
- **阈值是绝对量。** `Model.tokenThresholds.handoff` / `guard` 与 `AppConfig.maxToolOutput` 都以 token / 字符的绝对值为判据。工具 schema 换个语言就是另一组 token 数 —— 交接与上下文阻断会在**不同的实际内容量**上触发。同一份 `config.json` 在不同语言下跑出不同行为。
|
|
145
|
+
- **schema 成本每轮都付。** 描述文字随每次请求重发,token 差异是摊在每一轮上的持续开销,会改变请求体积、上下文占用与费用,而不是一次性成本。
|
|
146
|
+
- **缓存前缀不再可比。** prompt 缓存按前缀匹配;语言一旦进入 SDK,同一条代码路径就有了多个前缀形态,基准测试失去单一变量。
|
|
147
|
+
- **基准与排查需要单一形态。** 性能数据、bug 复现、工具调用成功率都要能横向比较;行为若随 locale 漂移,任何结论都得先问「你是什么语言」。
|
|
148
|
+
|
|
149
|
+
「模型跟随用户语言」由一条语言中立的 system prompt 指令承担 —— `Always reply in the same language the user writes in.` —— 按**每条消息**自适应,比按安装实例绑定 locale 更准确,也不需要 SDK 参与。完整论述见 [`docs/sdk-design-v1.md` §1](docs/sdk-design-v1.md)。
|
|
150
|
+
|
|
138
151
|
## 设计原则
|
|
139
152
|
|
|
140
153
|
参见项目根 [`PRINCIPLES.md`](../../PRINCIPLES.md):
|
|
@@ -43,7 +43,7 @@ export class AutoMigratePlugin {
|
|
|
43
43
|
await service.migrate({ agent, promptTokens, handoffTokens, mode: mode ?? undefined });
|
|
44
44
|
}
|
|
45
45
|
catch (err) {
|
|
46
|
-
getLogger().error(`[autoMigrate]
|
|
46
|
+
getLogger().error(`[autoMigrate] migration failed: ${err?.message ?? err}`);
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
}
|
|
@@ -19,16 +19,16 @@ export class UnknownToolHintPlugin {
|
|
|
19
19
|
this.scope = options.scope;
|
|
20
20
|
}
|
|
21
21
|
onUnknownTool(ctx) {
|
|
22
|
-
const toolName = ctx.toolCall.function?.name ?? "
|
|
22
|
+
const toolName = ctx.toolCall.function?.name ?? "unknown";
|
|
23
23
|
const mcpPlugin = this.scope.getPluginById(McpScopePlugin.ID);
|
|
24
24
|
const hasMcpConfig = mcpPlugin?.hasConfig() ?? false;
|
|
25
25
|
const hasCallMcpTool = ctx.availableTools.some((t) => t.function.name === "call_mcp_tool");
|
|
26
26
|
if (hasMcpConfig && !hasCallMcpTool) {
|
|
27
|
-
return
|
|
27
|
+
return `Tool "${toolName}" does not exist. An MCP server is configured, but the call_mcp_tool permission is disabled — adjust permissions if you need to use MCP tools.`;
|
|
28
28
|
}
|
|
29
29
|
if (hasMcpConfig && hasCallMcpTool) {
|
|
30
|
-
return
|
|
30
|
+
return `Tool "${toolName}" does not exist. To call MCP tools, use call_mcp_tool.`;
|
|
31
31
|
}
|
|
32
|
-
return
|
|
32
|
+
return `Tool "${toolName}" does not exist.`;
|
|
33
33
|
}
|
|
34
34
|
}
|
package/dist/config/constants.js
CHANGED
|
@@ -14,15 +14,15 @@ export const DEFAULT_MODEL_ID = "deepseek-flash";
|
|
|
14
14
|
/** 默认 Agent 定义(不含时间戳) */
|
|
15
15
|
export const DEFAULT_AGENT_DEFINITION = {
|
|
16
16
|
id: DEFAULT_AGENT_ID,
|
|
17
|
-
name: "
|
|
18
|
-
description: "
|
|
17
|
+
name: "Default Assistant",
|
|
18
|
+
description: "The default AI assistant, suited for everyday Q&A and task execution.",
|
|
19
19
|
// 模型写在 Agent 定义上,是运行期唯一真相源(无 config.defaultModel 兜底)
|
|
20
20
|
modelId: DEFAULT_MODEL_ID,
|
|
21
21
|
messages: [
|
|
22
|
-
Message.System(
|
|
22
|
+
Message.System(`You are a witty, rigorous and reliable assistant. Always reply in the same language the user writes in. Current operating system: ${process.platform}.
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
1. When user input is needed for a decision, ask only one question at a time.
|
|
25
|
+
2. This session runs on Node.js — you can write Node.js scripts to carry out complex tasks and run them via the exec tool.`),
|
|
26
26
|
],
|
|
27
27
|
// 出厂默认 Agent:custom 为 false,SDK 初始化时会同步出厂提示词(用户设为 true 则跳过)
|
|
28
28
|
custom: false,
|
|
@@ -41,10 +41,10 @@ export const DEFAULT_SUBAGENT_ID = "sub-agent-default";
|
|
|
41
41
|
/** 默认 SubAgent 定义(不含时间戳) */
|
|
42
42
|
export const DEFAULT_SUBAGENT_DEFINITION = {
|
|
43
43
|
id: DEFAULT_SUBAGENT_ID,
|
|
44
|
-
name: "
|
|
45
|
-
description: "
|
|
44
|
+
name: "General Assistant",
|
|
45
|
+
description: "A general-purpose sub-agent that is good at completing all kinds of tasks independently.",
|
|
46
46
|
messages: [
|
|
47
|
-
Message.System("
|
|
47
|
+
Message.System("You are a general-purpose assistant sub-agent, delegated by a parent agent to independently complete a specific task. Based on the given task description, proactively call your tools (file read/write, command execution, search, etc.) to analyze and complete the task. When the task is done, return the result directly; there is no need to explain your thinking process.\n\n⚠️ Mandatory requirements:\n1. If anything in the task description is unclear, vague or missing (including but not limited to the specific goal, file paths, constraints, expected deliverables, etc.), you MUST refuse to execute immediately and explicitly list what is unclear or missing, asking the parent agent to provide more complete task context. Do not assume, guess or make up any information on your own.\n2. If the task description contains contradictory information (such as mutually conflicting requirements, inconsistent file paths, contradictory constraints, etc.), you MUST point out the contradiction promptly and require the parent agent to clarify before executing."),
|
|
48
48
|
Message.User("{{task}}"),
|
|
49
49
|
],
|
|
50
50
|
permissions: {
|
|
@@ -55,11 +55,11 @@ export const DEFAULT_SUBAGENT_DEFINITION = {
|
|
|
55
55
|
},
|
|
56
56
|
function: {
|
|
57
57
|
name: "sub_agent_default",
|
|
58
|
-
description: "
|
|
58
|
+
description: "General-purpose sub-agent that can independently complete all kinds of tasks. ⚠️ When delegating a task you must provide complete task context, including all necessary background information, file paths, specific requirements and constraints; any unclear information will cause the sub-agent to refuse execution and ask for more information.",
|
|
59
59
|
parameters: {
|
|
60
60
|
type: "object",
|
|
61
61
|
properties: {
|
|
62
|
-
task: { type: "string", description: "
|
|
62
|
+
task: { type: "string", description: "Complete task description; must include all necessary background, goals, constraints and context. Any unclear information will cause the sub-agent to refuse execution." },
|
|
63
63
|
},
|
|
64
64
|
required: ["task"],
|
|
65
65
|
additionalProperties: false,
|
|
@@ -77,21 +77,21 @@ export const DEFAULT_APP_CONFIG = {
|
|
|
77
77
|
name: "OpenAI",
|
|
78
78
|
apiKey: "",
|
|
79
79
|
baseUrl: "https://api.openai.com/v1",
|
|
80
|
-
description: "OpenAI API
|
|
80
|
+
description: "OpenAI API endpoint",
|
|
81
81
|
},
|
|
82
82
|
{
|
|
83
83
|
id: "bigmodelcn",
|
|
84
|
-
name: "BigModelCN (
|
|
84
|
+
name: "BigModelCN (Zhipu AI)",
|
|
85
85
|
apiKey: "",
|
|
86
86
|
baseUrl: "https://open.bigmodel.cn/api/paas/v4",
|
|
87
|
-
description: "
|
|
87
|
+
description: "Zhipu AI large-model endpoint",
|
|
88
88
|
},
|
|
89
89
|
{
|
|
90
90
|
id: "deepseek",
|
|
91
91
|
name: "DeepSeek",
|
|
92
92
|
apiKey: "",
|
|
93
93
|
baseUrl: "https://api.deepseek.com/v1",
|
|
94
|
-
description: "DeepSeek API
|
|
94
|
+
description: "DeepSeek API endpoint (supports the Files API: local files are uploaded and then referenced by file_id)",
|
|
95
95
|
// 唯一声明了 Files API 能力的出厂端点;其余端点本地文件走 base64 内联。
|
|
96
96
|
// 判断依据是能力位而非端点 id,用户自建端点只要声明即可享受上传。
|
|
97
97
|
capabilities: { filesApi: true },
|
|
@@ -128,7 +128,7 @@ export const DEFAULT_APP_CONFIG = {
|
|
|
128
128
|
defaultParams: {},
|
|
129
129
|
vision: true,
|
|
130
130
|
reasoningPreset: "openai",
|
|
131
|
-
description: "OpenAI
|
|
131
|
+
description: "OpenAI flagship (2026-09): advanced analysis, software engineering, deep research and scientific work",
|
|
132
132
|
},
|
|
133
133
|
{
|
|
134
134
|
id: "gpt-6.1-sol",
|
|
@@ -139,7 +139,7 @@ export const DEFAULT_APP_CONFIG = {
|
|
|
139
139
|
defaultParams: {},
|
|
140
140
|
vision: true,
|
|
141
141
|
reasoningPreset: "openai",
|
|
142
|
-
description: "OpenAI
|
|
142
|
+
description: "OpenAI latest flagship (2026-09-29): agentic coding, computer operation and document-intensive professional tasks",
|
|
143
143
|
},
|
|
144
144
|
{
|
|
145
145
|
id: "gpt-6-luna",
|
|
@@ -150,7 +150,7 @@ export const DEFAULT_APP_CONFIG = {
|
|
|
150
150
|
defaultParams: {},
|
|
151
151
|
vision: true,
|
|
152
152
|
reasoningPreset: "openai",
|
|
153
|
-
description: "OpenAI GPT-6
|
|
153
|
+
description: "OpenAI GPT-6 series fast, low-cost model: suited for high-throughput, low-latency scenarios",
|
|
154
154
|
},
|
|
155
155
|
// ---------- 智谱 BigModel ----------
|
|
156
156
|
{
|
|
@@ -161,7 +161,7 @@ export const DEFAULT_APP_CONFIG = {
|
|
|
161
161
|
tokenThresholds: { handoff: 250_000, guard: 375_000 },
|
|
162
162
|
defaultParams: { thinking: { type: "enabled" }, reasoning_effort: "max" },
|
|
163
163
|
reasoningPreset: "zhipu-5.3",
|
|
164
|
-
description: "
|
|
164
|
+
description: "Zhipu latest flagship: complex software engineering and long-horizon agent tasks (thinking always on, 1M context)",
|
|
165
165
|
},
|
|
166
166
|
{
|
|
167
167
|
id: "glm-5.3-flash",
|
|
@@ -172,7 +172,7 @@ export const DEFAULT_APP_CONFIG = {
|
|
|
172
172
|
defaultParams: { thinking: { type: "enabled" }, reasoning_effort: "max" },
|
|
173
173
|
vision: true,
|
|
174
174
|
reasoningPreset: "zhipu-5.3",
|
|
175
|
-
description: "
|
|
175
|
+
description: "Zhipu native multimodal model (image/video/file): visual coding, 1M context",
|
|
176
176
|
},
|
|
177
177
|
{
|
|
178
178
|
id: "glm-5.3-flashx",
|
|
@@ -183,7 +183,7 @@ export const DEFAULT_APP_CONFIG = {
|
|
|
183
183
|
defaultParams: { thinking: { type: "enabled" }, reasoning_effort: "max" },
|
|
184
184
|
vision: true,
|
|
185
185
|
reasoningPreset: "zhipu-5.3",
|
|
186
|
-
description: "GLM-5.3-Flash
|
|
186
|
+
description: "GLM-5.3-Flash high-speed edition (200 tokens/s), native multimodal",
|
|
187
187
|
},
|
|
188
188
|
{
|
|
189
189
|
id: "glm-4.7-flash",
|
|
@@ -193,7 +193,7 @@ export const DEFAULT_APP_CONFIG = {
|
|
|
193
193
|
// 该模型真实窗口仅 200K,阈值下调至 100K(约窗口的 50%),留足 response 空间
|
|
194
194
|
tokenThresholds: { handoff: 100_000, guard: 150_000 },
|
|
195
195
|
defaultParams: {},
|
|
196
|
-
description: "
|
|
196
|
+
description: "Zhipu free text model: 200K context, suited for lightweight tasks",
|
|
197
197
|
},
|
|
198
198
|
// ---------- DeepSeek ----------
|
|
199
199
|
{
|
|
@@ -205,7 +205,7 @@ export const DEFAULT_APP_CONFIG = {
|
|
|
205
205
|
defaultParams: { thinking: { type: "enabled" } },
|
|
206
206
|
vision: true,
|
|
207
207
|
reasoningPreset: "deepseek",
|
|
208
|
-
description: "DeepSeek
|
|
208
|
+
description: "DeepSeek flagship (2026-09-10): 552B MoE asymmetric architecture, native multimodal, 1M context",
|
|
209
209
|
},
|
|
210
210
|
],
|
|
211
211
|
imageModels: [
|
|
@@ -260,13 +260,13 @@ export const DEFAULT_MCP_CONFIG = {
|
|
|
260
260
|
type: "stdio",
|
|
261
261
|
command: "npx",
|
|
262
262
|
args: ["-y", "@ai-zen/socket-pty", "mcp"],
|
|
263
|
-
description: "
|
|
263
|
+
description: "Manageable pseudo-terminal (pty): spawn/read/wait/write/resize/status/kill",
|
|
264
264
|
},
|
|
265
265
|
"chrome-devtools": {
|
|
266
266
|
type: "stdio",
|
|
267
267
|
command: "npx",
|
|
268
268
|
args: ["-y", "chrome-devtools-mcp@latest"],
|
|
269
|
-
description: "Chrome DevTools
|
|
269
|
+
description: "Chrome DevTools browser debugging and automation (Google chrome-devtools-mcp): page navigation, performance tracing, network and console inspection, screenshots, etc.",
|
|
270
270
|
},
|
|
271
271
|
},
|
|
272
272
|
};
|
|
@@ -19,94 +19,94 @@ export const HANDOFF_SKILL_ID = "handoff";
|
|
|
19
19
|
* 它是**保险丝**,不是第二真相源:正常路径下模板一律来自 SKILL.md。
|
|
20
20
|
*/
|
|
21
21
|
export const FALLBACK_HANDOFF_TEMPLATE = [
|
|
22
|
-
"#
|
|
22
|
+
"# Handoff Document",
|
|
23
23
|
"",
|
|
24
|
-
"
|
|
25
|
-
"
|
|
24
|
+
"Stop work immediately: implement no further features, do not continue unfinished tasks, and do not ask the user questions.",
|
|
25
|
+
"Your only task is: verify the actual current state → write the handoff document to the path given by the handoff parameters.",
|
|
26
26
|
"",
|
|
27
|
-
"## 💬
|
|
28
|
-
"## 🎯
|
|
29
|
-
"## 📊
|
|
30
|
-
"## ✅
|
|
31
|
-
"## 📋
|
|
32
|
-
"## 🧠
|
|
33
|
-
"## ⚠️
|
|
34
|
-
"## 📁
|
|
35
|
-
"## 🔔
|
|
27
|
+
"## 💬 Conversation Breakpoint",
|
|
28
|
+
"## 🎯 Task Goal",
|
|
29
|
+
"## 📊 Current State",
|
|
30
|
+
"## ✅ Completed",
|
|
31
|
+
"## 📋 Not Completed",
|
|
32
|
+
"## 🧠 Key Decisions",
|
|
33
|
+
"## ⚠️ Constraints and Preferences",
|
|
34
|
+
"## 📁 File Index",
|
|
35
|
+
"## 🔔 First Step for the Next Agent",
|
|
36
36
|
"",
|
|
37
|
-
"
|
|
37
|
+
'For anything you cannot verify, write "⚠️ Unverified"; speculation is forbidden.',
|
|
38
38
|
].join("\n");
|
|
39
39
|
/** 出厂 Skill:交接(handoff) */
|
|
40
40
|
const HANDOFF_SKILL = `---
|
|
41
41
|
name: handoff
|
|
42
|
-
description:
|
|
42
|
+
description: Write a task handoff document — when you stop work and leave the conversation, put the task goal, the actual current state, key decisions and next steps into a document that someone else can pick up directly.
|
|
43
43
|
metadata:
|
|
44
44
|
custom: "false"
|
|
45
45
|
---
|
|
46
46
|
|
|
47
|
-
#
|
|
47
|
+
# Handoff Document Writing Standard
|
|
48
48
|
|
|
49
|
-
##
|
|
49
|
+
## When It Applies
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
When the context is about to overflow and trigger a task migration, when the user explicitly asks you to "write a handoff document", or when you are asked to "hand over".
|
|
52
52
|
|
|
53
|
-
##
|
|
53
|
+
## Iron Rules (violating any of them counts as failure)
|
|
54
54
|
|
|
55
|
-
1.
|
|
56
|
-
2.
|
|
57
|
-
3.
|
|
58
|
-
4.
|
|
59
|
-
|
|
60
|
-
5.
|
|
55
|
+
1. **Stop work immediately**: implement no further features, do not continue unfinished tasks, do not fix bugs in passing, do not refactor.
|
|
56
|
+
2. **Do not ask the user questions**, do not discuss approaches, do not ask for confirmation.
|
|
57
|
+
3. Do only two things: **verify the actual current state** → **write the document**.
|
|
58
|
+
4. Read-only operations are allowed (read / ls / glob / find_text / inspect_file / cwd, plus read-only commands such as git status, git diff, git log).
|
|
59
|
+
Modifying the workspace is forbidden: do not change source code, do not npm install, do not commit, do not push. The only file you are allowed to write is the handoff document itself.
|
|
60
|
+
5. **Write it in one pass**, do not rewrite it over and over; do not read large files; do not paste long code into the document.
|
|
61
61
|
|
|
62
|
-
##
|
|
62
|
+
## Writing the File
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
The path is given by the handoff parameters — **use it verbatim**; do not rename it, do not move it to another directory, do not omit it.
|
|
65
|
+
After writing with the write tool, read it back once with the read tool to confirm the content is complete (skip this step if you have no read capability).
|
|
66
66
|
|
|
67
|
-
##
|
|
67
|
+
## Document Structure (write in this order, keep the headings exactly as they are)
|
|
68
68
|
|
|
69
|
-
## 💬
|
|
70
|
-
|
|
69
|
+
## 💬 Conversation Breakpoint
|
|
70
|
+
What the user last asked for, and what you last replied with or delivered. One paragraph each; do not restate the whole conversation.
|
|
71
71
|
|
|
72
|
-
## 🎯
|
|
73
|
-
|
|
72
|
+
## 🎯 Task Goal
|
|
73
|
+
What the user actually wants (intent and acceptance criteria), not a checklist of the steps you performed.
|
|
74
74
|
|
|
75
|
-
## 📊
|
|
76
|
-
|
|
75
|
+
## 📊 Current State
|
|
76
|
+
Branch and commit, uncommitted changes, whether tests pass, whether any process/port is running, whether dependencies are installed.
|
|
77
77
|
|
|
78
|
-
## ✅
|
|
79
|
-
|
|
78
|
+
## ✅ Completed
|
|
79
|
+
For each item write: what was done + the output path (file/directory) + how to verify that it really took effect.
|
|
80
80
|
|
|
81
|
-
## 📋
|
|
82
|
-
|
|
81
|
+
## 📋 Not Completed
|
|
82
|
+
Order by priority; for each item write "the smallest next action", down to a specific file or runnable command.
|
|
83
83
|
|
|
84
|
-
## 🧠
|
|
85
|
-
|
|
84
|
+
## 🧠 Key Decisions
|
|
85
|
+
Why it was done this way (the trade-off reasoning); and the dead ends you walked into — so the next person does not repeat them.
|
|
86
86
|
|
|
87
|
-
## ⚠️
|
|
88
|
-
|
|
87
|
+
## ⚠️ Constraints and Preferences
|
|
88
|
+
Rules the user stressed, project conventions, parts that must not be changed, styles that must be followed.
|
|
89
89
|
|
|
90
|
-
## 📁
|
|
91
|
-
|
|
90
|
+
## 📁 File Index
|
|
91
|
+
Path + purpose + whether it changed this time (added / modified / read only).
|
|
92
92
|
|
|
93
|
-
## 🔔
|
|
94
|
-
3
|
|
93
|
+
## 🔔 First Step for the Next Agent
|
|
94
|
+
At most 3 commands or actions that can be run directly, so the next person can start working immediately.
|
|
95
95
|
|
|
96
|
-
##
|
|
96
|
+
## State Verification Checklist
|
|
97
97
|
|
|
98
|
-
- \`git status\` / \`git diff --stat
|
|
99
|
-
-
|
|
100
|
-
-
|
|
101
|
-
-
|
|
98
|
+
- \`git status\` / \`git diff --stat\`: current branch, whether there are uncommitted changes
|
|
99
|
+
- The latest test result: if you ran tests, write a summary of the result; if you did not, write "not run" — **do not write "it should pass"**
|
|
100
|
+
- Key files involved in the handoff: use ls / read to confirm they really exist on disk
|
|
101
|
+
- Running services and ports: \`ps\` / \`lsof -i\`, or the project's own start script
|
|
102
102
|
|
|
103
|
-
> ⚠️
|
|
103
|
+
> ⚠️ If the handoff parameters state that tools are unavailable: write "⚠️ Unverified" for every item above; **speculation is forbidden** about specific paths, progress and test status.
|
|
104
104
|
|
|
105
|
-
##
|
|
105
|
+
## Writing Requirements
|
|
106
106
|
|
|
107
|
-
-
|
|
108
|
-
-
|
|
109
|
-
-
|
|
107
|
+
- Every sentence must let the next person "start working immediately"; empty phrases such as "continue development", "polish the features", "do some optimization" are forbidden.
|
|
108
|
+
- For information you can no longer determine in the current context, write "needs user confirmation on XXX"; do not fabricate it.
|
|
109
|
+
- Keep the whole document within 300 lines.
|
|
110
110
|
`;
|
|
111
111
|
/** 全部出厂 Skill(顺序无意义,按 id 去重) */
|
|
112
112
|
export const FACTORY_SKILLS = [
|
|
@@ -55,10 +55,10 @@ export class HandoffGuardPlugin {
|
|
|
55
55
|
if (typeof raw === "string" && resolvePath(this.cwd, raw) === handoffPath) {
|
|
56
56
|
return;
|
|
57
57
|
}
|
|
58
|
-
return
|
|
58
|
+
return `During the handoff round the only allowed write is the handoff document: ${handoffPath} (pass that path to write exactly as given), start writing now.`;
|
|
59
59
|
}
|
|
60
60
|
if (HandoffGuardPlugin.WRITE_TOOLS.has(name)) {
|
|
61
|
-
return
|
|
61
|
+
return `Modifying the workspace is not allowed during the handoff round (${name}): your only task is to write the handoff document to ${handoffPath}.`;
|
|
62
62
|
}
|
|
63
63
|
return;
|
|
64
64
|
}
|
|
@@ -16,7 +16,7 @@ import { HandoffGuardPlugin } from "./HandoffGuardPlugin.js";
|
|
|
16
16
|
*/
|
|
17
17
|
export class HandoffAbortedError extends Error {
|
|
18
18
|
constructor() {
|
|
19
|
-
super("
|
|
19
|
+
super("Handoff cancelled");
|
|
20
20
|
this.name = "HandoffAbortedError";
|
|
21
21
|
}
|
|
22
22
|
}
|
|
@@ -115,14 +115,14 @@ export class HandoffService {
|
|
|
115
115
|
const { skillContent, handoffPath, toolsAvailable } = params;
|
|
116
116
|
const tail = toolsAvailable
|
|
117
117
|
? [
|
|
118
|
-
"
|
|
119
|
-
"
|
|
118
|
+
"Start writing now: use the write tool to put the complete document at the path given above (use the path exactly as given, do not rename it and do not move it to another directory).",
|
|
119
|
+
"After writing, read it back once with the read tool to confirm it is complete, then reply with a single confirmation of no more than 40 words, **do not restate the document content**.",
|
|
120
120
|
]
|
|
121
121
|
: [
|
|
122
|
-
"
|
|
122
|
+
"Output the complete document body directly: no explanations, do not wrap it in a code block, the caller will persist it to disk itself.",
|
|
123
123
|
];
|
|
124
124
|
return [
|
|
125
|
-
"
|
|
125
|
+
"You are now to write a task handoff document for this conversation. The following handoff rules are mandatory.",
|
|
126
126
|
"",
|
|
127
127
|
"---",
|
|
128
128
|
"",
|
|
@@ -130,11 +130,11 @@ export class HandoffService {
|
|
|
130
130
|
"",
|
|
131
131
|
"---",
|
|
132
132
|
"",
|
|
133
|
-
"##
|
|
134
|
-
`-
|
|
135
|
-
`-
|
|
136
|
-
? "
|
|
137
|
-
: "
|
|
133
|
+
"## Handoff parameters",
|
|
134
|
+
`- Handoff document absolute path: ${handoffPath}`,
|
|
135
|
+
`- Tools available: ${toolsAvailable
|
|
136
|
+
? "yes (read / exec, etc., use them to verify the actual state)"
|
|
137
|
+
: "no (you cannot inspect the disk, so for any state information always write '⚠️ Unverified', never speculate)"}`,
|
|
138
138
|
"",
|
|
139
139
|
...tail,
|
|
140
140
|
].join("\n");
|
|
@@ -147,10 +147,10 @@ export class HandoffService {
|
|
|
147
147
|
*/
|
|
148
148
|
static createPostMessages(handoffPath) {
|
|
149
149
|
const content = [
|
|
150
|
-
"
|
|
150
|
+
"The previous conversation has been handed off, the handoff document is saved at:",
|
|
151
151
|
handoffPath,
|
|
152
152
|
"",
|
|
153
|
-
"
|
|
153
|
+
"Read that file first with the read tool, and once you understand the context and the unfinished work, continue helping the user with the task.",
|
|
154
154
|
].join("\n");
|
|
155
155
|
return [Message.User(content)];
|
|
156
156
|
}
|
|
@@ -213,7 +213,7 @@ export class HandoffService {
|
|
|
213
213
|
/** 串行化 + 防重入:交接与迁移共用同一把锁 */
|
|
214
214
|
async withBusy(fn) {
|
|
215
215
|
if (this._busy)
|
|
216
|
-
throw new Error("
|
|
216
|
+
throw new Error("A handoff is already in progress, wait for it to finish");
|
|
217
217
|
this._busy = true;
|
|
218
218
|
try {
|
|
219
219
|
return await fn();
|
|
@@ -298,13 +298,13 @@ export class HandoffService {
|
|
|
298
298
|
let handoffDoc;
|
|
299
299
|
let historyText = "";
|
|
300
300
|
if (mode === "self" && !this.supportsSelfHandoff(agent)) {
|
|
301
|
-
this.logger.warn("[handoff] agent
|
|
301
|
+
this.logger.warn("[handoff] agent has no write tool or send capability, falling back to standalone summary mode");
|
|
302
302
|
mode = "summary";
|
|
303
303
|
}
|
|
304
304
|
if (mode === "self") {
|
|
305
305
|
const ok = await this.runSelfHandoff(agent, handoffPath, signal);
|
|
306
306
|
if (!ok) {
|
|
307
|
-
this.logger.warn("[handoff]
|
|
307
|
+
this.logger.warn("[handoff] self handoff failed, falling back to standalone summary mode");
|
|
308
308
|
mode = "summary";
|
|
309
309
|
}
|
|
310
310
|
}
|
|
@@ -375,10 +375,10 @@ export class HandoffService {
|
|
|
375
375
|
this.throwIfAborted(signal);
|
|
376
376
|
if (await this.isHandoffFileReady(handoffPath))
|
|
377
377
|
return true;
|
|
378
|
-
this.logger.warn(`[handoff]
|
|
378
|
+
this.logger.warn(`[handoff] handoff document missing or too small, retrying once: ${handoffPath}`);
|
|
379
379
|
await agent.send([
|
|
380
|
-
|
|
381
|
-
"
|
|
380
|
+
`The handoff document does not exist or is too small to deliver: ${handoffPath}`,
|
|
381
|
+
"Use the write tool right now to write the **complete** handoff document to that path (use the path exactly as given) and do nothing else.",
|
|
382
382
|
].join("\n"));
|
|
383
383
|
this.throwIfAborted(signal);
|
|
384
384
|
return await this.isHandoffFileReady(handoffPath);
|
|
@@ -388,7 +388,7 @@ export class HandoffService {
|
|
|
388
388
|
await this.discardPartialFile(handoffPath, existedBefore);
|
|
389
389
|
throw new HandoffAbortedError();
|
|
390
390
|
}
|
|
391
|
-
this.logger.error(`[handoff]
|
|
391
|
+
this.logger.error(`[handoff] self handoff round failed: ${err?.message ?? err}`);
|
|
392
392
|
return false;
|
|
393
393
|
}
|
|
394
394
|
finally {
|
|
@@ -421,7 +421,7 @@ export class HandoffService {
|
|
|
421
421
|
}, signal ? { signal } : undefined);
|
|
422
422
|
const handoffDoc = response.choices[0]?.message?.content;
|
|
423
423
|
if (!handoffDoc || !handoffDoc.trim()) {
|
|
424
|
-
throw new Error("
|
|
424
|
+
throw new Error("Failed to generate the handoff document");
|
|
425
425
|
}
|
|
426
426
|
return handoffDoc;
|
|
427
427
|
}
|
|
@@ -446,7 +446,7 @@ export class HandoffService {
|
|
|
446
446
|
if (skill?.content?.trim())
|
|
447
447
|
return skill.content;
|
|
448
448
|
}
|
|
449
|
-
this.logger.warn(`[handoff]
|
|
449
|
+
this.logger.warn(`[handoff] ${HANDOFF_SKILL_ID} skill not found, using the built-in fallback template (paths: ${this.skillDirs.join(", ") || "none configured"})`);
|
|
450
450
|
return FALLBACK_HANDOFF_TEMPLATE;
|
|
451
451
|
}
|
|
452
452
|
/** 守卫按 agent 注册一次(`Agent.use()` 无移除 API,故复用实例 + 开关) */
|
|
@@ -466,7 +466,7 @@ export class HandoffService {
|
|
|
466
466
|
await hook(ctx);
|
|
467
467
|
}
|
|
468
468
|
catch (err) {
|
|
469
|
-
this.logger.error(`[handoff]
|
|
469
|
+
this.logger.error(`[handoff] hook execution failed: ${err?.message ?? err}`);
|
|
470
470
|
}
|
|
471
471
|
}
|
|
472
472
|
}
|
|
@@ -14,10 +14,10 @@ import { createModel } from "./createModel.js";
|
|
|
14
14
|
export async function createAgent(scope, agentId, options = {}) {
|
|
15
15
|
const definition = await new AgentRepository(scope.agentsDir).read(agentId);
|
|
16
16
|
if (!definition)
|
|
17
|
-
throw new Error(`Agent "${agentId}"
|
|
17
|
+
throw new Error(`Agent "${agentId}" not found`);
|
|
18
18
|
const modelId = definition.modelId;
|
|
19
19
|
if (!modelId) {
|
|
20
|
-
throw new Error(`Agent "${agentId}"
|
|
20
|
+
throw new Error(`Agent "${agentId}" has no model bound (modelId is missing). Assign a model to it in the config`);
|
|
21
21
|
}
|
|
22
22
|
const { client, model, modelConfig } = createModel(scope, modelId, definition.modelParams);
|
|
23
23
|
const tools = scope.buildTools(definition, {
|
|
@@ -26,12 +26,12 @@ export function createModel(scope, modelId, overrides) {
|
|
|
26
26
|
const config = scope.config;
|
|
27
27
|
const modelConfig = config.models.find((m) => m.id === modelId);
|
|
28
28
|
if (!modelConfig)
|
|
29
|
-
throw new Error(
|
|
29
|
+
throw new Error(`Model "${modelId}" not found`);
|
|
30
30
|
const endpointConfig = config.endpoints.find((e) => e.id === modelConfig.endpointId);
|
|
31
31
|
if (!endpointConfig)
|
|
32
|
-
throw new Error(
|
|
32
|
+
throw new Error(`Endpoint "${modelConfig.endpointId}" is not configured`);
|
|
33
33
|
if (!endpointConfig.apiKey) {
|
|
34
|
-
throw new Error(
|
|
34
|
+
throw new Error(`API key for endpoint "${endpointConfig.name}" is not set`);
|
|
35
35
|
}
|
|
36
36
|
const client = new OpenAI({
|
|
37
37
|
apiKey: endpointConfig.apiKey,
|
package/dist/scope/Scope.js
CHANGED
|
@@ -57,7 +57,7 @@ export class Scope {
|
|
|
57
57
|
*/
|
|
58
58
|
use(...plugins) {
|
|
59
59
|
if (this._initialized) {
|
|
60
|
-
throw new Error("Scope.use()
|
|
60
|
+
throw new Error("Scope.use() must be called before init()");
|
|
61
61
|
}
|
|
62
62
|
for (const plugin of plugins) {
|
|
63
63
|
this._plugins.push(plugin);
|