mocode-ai 1.3.9 → 1.4.0
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/README.md +78 -70
- package/README.zh-CN.md +81 -73
- package/bin/mocode-agent-host.js +1 -1
- package/dist/agent/core.js +226 -293
- package/dist/agent/index.js +14 -10
- package/dist/agent/mode.js +3 -3
- package/dist/agent/runtime-context.js +45 -0
- package/dist/agent/spawn.js +101 -86
- package/dist/agent/tool-helpers.js +134 -0
- package/dist/agent/trace-state.js +118 -0
- package/dist/agents/coordinator.js +6 -1
- package/dist/changeset/index.js +9 -8
- package/dist/config/index.js +109 -77
- package/dist/config/presets.js +3 -1
- package/dist/config/profiles.js +125 -0
- package/dist/context/age-aware.js +2 -4
- package/dist/context/artifacts.js +6 -13
- package/dist/context/budget.js +6 -8
- package/dist/context/classifier.js +1 -2
- package/dist/context/encoders/code.js +2 -6
- package/dist/context/encoders/command.js +1 -3
- package/dist/context/encoders/search.js +1 -3
- package/dist/context/index.js +1 -1
- package/dist/context/lifecycle.js +5 -3
- package/dist/context/pipeline.js +1 -1
- package/dist/context/relevance.js +2 -4
- package/dist/context/token-calibration.js +13 -18
- package/dist/host/stdio.js +73 -12
- package/dist/i18n/index.js +96 -44
- package/dist/index.js +1 -1
- package/dist/llm/index.js +39 -23
- package/dist/llm/provider.js +23 -0
- package/dist/llm/providers/anthropic.js +12 -18
- package/dist/llm/tool-schema.js +48 -0
- package/dist/mcp/client.js +70 -25
- package/dist/mcp/config.js +1 -3
- package/dist/mcp/index.js +3 -1
- package/dist/memory/reflect.js +3 -6
- package/dist/memory/store.js +10 -12
- package/dist/permissions/index.js +66 -22
- package/dist/pet/bridge.js +2 -2
- package/dist/repl/commands/appearance.js +72 -0
- package/dist/repl/commands/compact.js +114 -0
- package/dist/repl/commands/context.js +17 -0
- package/dist/repl/commands/image.js +71 -0
- package/dist/repl/commands/memory.js +51 -0
- package/dist/repl/commands/mode.js +47 -0
- package/dist/repl/commands/model.js +435 -0
- package/dist/repl/commands/pet.js +70 -0
- package/dist/repl/commands/registry.js +46 -0
- package/dist/repl/commands/session.js +102 -0
- package/dist/repl/commands/skill.js +95 -0
- package/dist/repl/commands/system.js +202 -0
- package/dist/repl/commands/tool-group.js +233 -0
- package/dist/repl/commands/types.js +4 -0
- package/dist/repl/commands.js +324 -0
- package/dist/repl/index.js +5 -2577
- package/dist/repl/message-format.js +151 -0
- package/dist/repl/running-input.js +147 -0
- package/dist/repl/runtime.js +816 -0
- package/dist/repl/status-bar.js +161 -0
- package/dist/rollback/index.js +44 -14
- package/dist/runtime/browser-manager.js +1 -1
- package/dist/runtime/dev-server-manager.js +1 -1
- package/dist/runtime/input-injector.js +313 -0
- package/dist/runtime/screen-capture.js +89 -0
- package/dist/runtime/screen-pipeline.js +223 -0
- package/dist/sandbox/index.js +1 -1
- package/dist/sandbox/policy.js +8 -2
- package/dist/session/compact.js +22 -34
- package/dist/session/index.js +2 -2
- package/dist/session/notes.js +39 -5
- package/dist/session/persist.js +14 -8
- package/dist/session/scheduler.js +3 -4
- package/dist/session/trace-metrics.js +6 -3
- package/dist/skills/activation.js +10 -5
- package/dist/skills/discover.js +5 -17
- package/dist/skills/index.js +1 -1
- package/dist/skills/runner.js +28 -44
- package/dist/skills/toolmap.js +5 -2
- package/dist/tools/builtins/browser.js +17 -3
- package/dist/tools/builtins/computer.js +322 -0
- package/dist/tools/builtins/dev-server.js +1 -3
- package/dist/tools/builtins/edit-file.js +21 -7
- package/dist/tools/builtins/grep.js +1 -1
- package/dist/tools/builtins/index.js +28 -25
- package/dist/tools/builtins/memory-forget.js +1 -1
- package/dist/tools/builtins/memory-graph.js +4 -7
- package/dist/tools/builtins/memory-list.js +2 -6
- package/dist/tools/builtins/memory-save.js +8 -2
- package/dist/tools/builtins/memory-search.js +4 -4
- package/dist/tools/builtins/note-append.js +14 -3
- package/dist/tools/builtins/plan-update.js +40 -9
- package/dist/tools/builtins/run-command.js +8 -8
- package/dist/tools/builtins/screenshot.js +11 -87
- package/dist/tools/builtins/task.js +29 -40
- package/dist/tools/builtins/use-skill.js +1 -1
- package/dist/tools/builtins/view-image.js +5 -3
- package/dist/tools/builtins/web-fetch.js +1 -3
- package/dist/tools/builtins/web-search.js +3 -7
- package/dist/tools/builtins/write-file.js +4 -2
- package/dist/tools/constants.js +50 -37
- package/dist/tools/policy.js +221 -0
- package/dist/tools/registry.js +40 -19
- package/dist/tools/resource-lock.js +4 -2
- package/dist/tools/router.js +143 -0
- package/dist/tools/validation.js +6 -5
- package/dist/ui/batch.js +63 -23
- package/dist/ui/clipboard.js +17 -5
- package/dist/ui/content.js +166 -13
- package/dist/ui/diff.js +61 -19
- package/dist/ui/fuzzy.js +35 -2
- package/dist/ui/intervention.js +2 -4
- package/dist/ui/layout-internal/content-write.js +587 -0
- package/dist/ui/layout-internal/core.js +590 -0
- package/dist/ui/layout-internal/input-paint.js +365 -0
- package/dist/ui/layout-internal/screen.js +128 -0
- package/dist/ui/layout-internal/scroll.js +152 -0
- package/dist/ui/layout-internal/selection.js +229 -0
- package/dist/ui/layout-internal/state.js +82 -0
- package/dist/ui/layout-internal/statusbar.js +351 -0
- package/dist/ui/layout-types.js +1 -0
- package/dist/ui/layout.js +3 -2288
- package/dist/ui/markdown.js +10 -2
- package/dist/ui/prompt-internal/editor.js +945 -0
- package/dist/ui/prompt-internal/paste.js +52 -0
- package/dist/ui/prompt-internal/pickers.js +519 -0
- package/dist/ui/prompt-internal/types.js +1 -0
- package/dist/ui/prompt.js +4 -1506
- package/dist/ui/render.js +87 -2
- package/dist/verification/affected.js +11 -5
- package/dist/verification/discovery.js +1 -3
- package/dist/verification/profile.js +11 -3
- package/package.json +11 -2
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { installBuiltinTools } from '../registry.js';
|
|
1
2
|
import { readFileTool } from './read-file.js';
|
|
2
3
|
import { viewImageTool } from './view-image.js';
|
|
3
4
|
import { screenshotTool } from './screenshot.js';
|
|
@@ -22,30 +23,15 @@ import { memoryUpdateTool } from './memory-update.js';
|
|
|
22
23
|
import { memoryForgetTool } from './memory-forget.js';
|
|
23
24
|
import { memoryGraphTool } from './memory-graph.js';
|
|
24
25
|
import { subAgentTool } from './task.js';
|
|
26
|
+
import { computerTool } from './computer.js';
|
|
25
27
|
/**
|
|
26
28
|
* 所有内置工具,按注册顺序排列。
|
|
27
29
|
* 加新工具:在本目录新建 `xxx.ts` 导出一个 Tool,再在下面数组里加一行。无需改 agent / llm。
|
|
28
30
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* —— 这是有意为之,避免切开关瞬间把已发出请求的工具列表打乱。
|
|
33
|
-
*
|
|
34
|
-
* 注:这里直接读 env(MEMORY_ENABLED)而不是调 config.isMemoryEnabled(),因为本模块可能在
|
|
35
|
-
* config 单例尚未初始化时被其它模块拉起(import 链路:tools/registry → builtinTools,
|
|
36
|
-
* config 单例字段 getter 在 getPlanDisabledTools 等调用链路上 lazy 求值)。
|
|
31
|
+
* memory_* 始终注册进 registry(JSONL store 仍为懒加载);主 Agent 是否看见它们由每 turn
|
|
32
|
+
* ToolPolicy 的 memory-read / memory-write 簇决定,MEMORY_ENABLED=false 可作 capability veto。
|
|
33
|
+
* 未传 ToolPolicy 的旧嵌入调用仍走 legacy profile 过滤。
|
|
37
34
|
*/
|
|
38
|
-
const _memoryEnabledAtBoot = process.env.MEMORY_ENABLED === 'true';
|
|
39
|
-
const _memoryTools = _memoryEnabledAtBoot
|
|
40
|
-
? [
|
|
41
|
-
memorySaveTool,
|
|
42
|
-
memorySearchTool,
|
|
43
|
-
memoryListTool,
|
|
44
|
-
memoryUpdateTool,
|
|
45
|
-
memoryForgetTool,
|
|
46
|
-
memoryGraphTool,
|
|
47
|
-
]
|
|
48
|
-
: [];
|
|
49
35
|
const pathResource = (args) => typeof args.path === 'string' && args.path ? [`file:${args.path}`] : ['workspace'];
|
|
50
36
|
const workspaceResource = () => ['workspace'];
|
|
51
37
|
const memoryResource = () => ['memory-store'];
|
|
@@ -53,7 +39,12 @@ const CAPABILITIES = {
|
|
|
53
39
|
read_file: { effect: 'read', concurrency: 'parallel', resources: pathResource },
|
|
54
40
|
view_image: { effect: 'read', concurrency: 'parallel', resources: pathResource },
|
|
55
41
|
screenshot: { effect: 'process', concurrency: 'serial', resources: workspaceResource, supportsAbort: true },
|
|
56
|
-
write_file: {
|
|
42
|
+
write_file: {
|
|
43
|
+
effect: 'write',
|
|
44
|
+
concurrency: 'resource-locked',
|
|
45
|
+
resources: pathResource,
|
|
46
|
+
delegatesResourceLocks: true,
|
|
47
|
+
},
|
|
57
48
|
edit_file: { effect: 'write', concurrency: 'resource-locked', resources: pathResource, delegatesResourceLocks: true },
|
|
58
49
|
run_command: { effect: 'process', concurrency: 'serial', resources: workspaceResource, supportsAbort: true },
|
|
59
50
|
// 后台进程与浏览器会话跨调用存活;串行执行避免同一页面/服务被并发操作。
|
|
@@ -78,13 +69,15 @@ const CAPABILITIES = {
|
|
|
78
69
|
memory_forget: { effect: 'write', concurrency: 'serial', resources: memoryResource },
|
|
79
70
|
// memory_graph:search/neighbors/stats 只读、add 写,统一按写处理走串行(调用不频繁,简化)。
|
|
80
71
|
memory_graph: { effect: 'write', concurrency: 'serial', resources: memoryResource },
|
|
81
|
-
//
|
|
72
|
+
// computer 桌面操控:桌面状态全局唯一,任何两个 computer 调用都不允许并发;
|
|
73
|
+
// effect=process(不写工作区文件,不触发 rollback/diff 追踪),supportsAbort 中断长 wait/拖拽。
|
|
74
|
+
computer: { effect: 'process', concurrency: 'serial', resources: () => ['desktop'], supportsAbort: true },
|
|
75
|
+
// sub-agent 与主 agent 同权,直接写工作区(无 overlay)。编排器本身不持锁——锁由嵌套工具
|
|
76
|
+
// 各自获取,否则子 agent 内的 run_command 会等父持有的 workspace 锁而自锁。
|
|
77
|
+
// 并发批排除见 tool-helpers.isResourceLockedCall:多个子 agent 逐个串行,不同时改工作区。
|
|
82
78
|
'sub-agent': {
|
|
83
79
|
effect: 'write',
|
|
84
80
|
concurrency: 'resource-locked',
|
|
85
|
-
resources: (args) => args.mode === 'write' && Array.isArray(args.writeSet) && args.writeSet.length
|
|
86
|
-
? args.writeSet.map((item) => `file:${String(item)}`)
|
|
87
|
-
: args.mode === 'write' ? ['workspace'] : [],
|
|
88
81
|
delegatesResourceLocks: true,
|
|
89
82
|
supportsAbort: true,
|
|
90
83
|
},
|
|
@@ -107,11 +100,21 @@ const rawBuiltinTools = [
|
|
|
107
100
|
askHumanTool,
|
|
108
101
|
planUpdateTool,
|
|
109
102
|
noteAppendTool,
|
|
110
|
-
|
|
103
|
+
memorySaveTool,
|
|
104
|
+
memorySearchTool,
|
|
105
|
+
memoryListTool,
|
|
106
|
+
memoryUpdateTool,
|
|
107
|
+
memoryForgetTool,
|
|
108
|
+
memoryGraphTool,
|
|
111
109
|
subAgentTool,
|
|
110
|
+
computerTool,
|
|
112
111
|
];
|
|
113
112
|
/** 所有内置工具均携带显式能力;新增工具遗漏声明时 registry 会保守串行。 */
|
|
114
113
|
export const builtinTools = rawBuiltinTools.map((tool) => ({
|
|
115
114
|
...tool,
|
|
116
115
|
capabilities: CAPABILITIES[tool.name],
|
|
117
116
|
}));
|
|
117
|
+
// 自注册:registry 不再顶层 import builtins(破模块循环,见 registry.ts 顶部注释),改为本模块
|
|
118
|
+
// 初始化时把官方默认工具包注入注册表。registry→builtins 边已断,此处调 installBuiltinTools 时
|
|
119
|
+
// registry 模块必已完成求值,无 TDZ 风险。装配方(CLI/stdio host/eval/测试)import 本模块即完成装配。
|
|
120
|
+
installBuiltinTools(builtinTools);
|
|
@@ -3,7 +3,7 @@ import { forgetEntry } from '../../memory/store.js';
|
|
|
3
3
|
// 遗忘:默认归档(archived,从索引/默认搜索隐藏,可复活);mode=delete 硬删。pinned 拒删。
|
|
4
4
|
export const memoryForgetTool = {
|
|
5
5
|
name: 'memory_forget',
|
|
6
|
-
description:
|
|
6
|
+
description: "Forget a memory entry: default archive (hidden, revivable via memory_update); mode=delete hard-deletes. Pinned entries can't be deleted (unpin first).",
|
|
7
7
|
risk: 'confirm',
|
|
8
8
|
parameters: {
|
|
9
9
|
type: 'object',
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { addTriple, graphStats, neighborsOf, pathBetween
|
|
1
|
+
import { addTriple, graphStats, neighborsOf, pathBetween } from '../../memory/graph.js';
|
|
2
2
|
// ---------- memory_graph ----------
|
|
3
3
|
// 知识图谱维护工具:邻居遍历(1-3 跳,可 relation 过滤)、两点最短路径、手工加三元组、看图统计。
|
|
4
4
|
// 关键词搜索已并入 memory_search(条目 + 图谱事实一次返回),本工具不再提供 search。
|
|
@@ -7,19 +7,16 @@ import { addTriple, graphStats, neighborsOf, pathBetween, } from '../../memory/g
|
|
|
7
7
|
function fmtEdges(edges) {
|
|
8
8
|
if (edges.length === 0)
|
|
9
9
|
return '(无边)';
|
|
10
|
-
return edges
|
|
11
|
-
.map((e) => `${e.src} --[${e.relation}]--> ${e.dst}${e.fact ? ` (${e.fact})` : ''}`)
|
|
12
|
-
.join('\n');
|
|
10
|
+
return edges.map((e) => `${e.src} --[${e.relation}]--> ${e.dst}${e.fact ? ` (${e.fact})` : ''}`).join('\n');
|
|
13
11
|
}
|
|
14
12
|
function fmtEntities(entities) {
|
|
15
13
|
if (entities.length === 0)
|
|
16
14
|
return '(无实体)';
|
|
17
|
-
return entities
|
|
18
|
-
.map((e) => `- ${e.id}: ${e.name}${e.summary ? ` — ${e.summary}` : ''} [${e.scope}]`)
|
|
19
|
-
.join('\n');
|
|
15
|
+
return entities.map((e) => `- ${e.id}: ${e.name}${e.summary ? ` — ${e.summary}` : ''} [${e.scope}]`).join('\n');
|
|
20
16
|
}
|
|
21
17
|
export const memoryGraphTool = {
|
|
22
18
|
name: 'memory_graph',
|
|
19
|
+
risk: 'confirm',
|
|
23
20
|
description: 'Maintain/explore the knowledge-graph memory layer (entities + temporal triples). Keyword search lives in memory_search. ' +
|
|
24
21
|
'action=neighbors: BFS 1-3 hops around an entity (optional relation filter to follow one edge type); ' +
|
|
25
22
|
'action=path: shortest path between two entities (bidirectional BFS, max 6 hops); ' +
|
|
@@ -21,14 +21,10 @@ export const memoryListTool = {
|
|
|
21
21
|
async execute(args) {
|
|
22
22
|
const items = listEntries({
|
|
23
23
|
type: typeof args.type === 'string' ? args.type : undefined,
|
|
24
|
-
status: typeof args.status === 'string'
|
|
25
|
-
? args.status
|
|
26
|
-
: undefined,
|
|
24
|
+
status: typeof args.status === 'string' ? args.status : undefined,
|
|
27
25
|
});
|
|
28
26
|
if (items.length === 0)
|
|
29
27
|
return '(无记忆条目)';
|
|
30
|
-
return items
|
|
31
|
-
.map((i) => `- ${i.id}: ${i.name} — ${i.summary} (${i.type}, ${i.status})`)
|
|
32
|
-
.join('\n');
|
|
28
|
+
return items.map((i) => `- ${i.id}: ${i.name} — ${i.summary} (${i.type}, ${i.status})`).join('\n');
|
|
33
29
|
},
|
|
34
30
|
};
|
|
@@ -11,7 +11,10 @@ export const memorySaveTool = {
|
|
|
11
11
|
parameters: {
|
|
12
12
|
type: 'object',
|
|
13
13
|
properties: {
|
|
14
|
-
name: {
|
|
14
|
+
name: {
|
|
15
|
+
type: 'string',
|
|
16
|
+
description: 'Short unique title (converted to id; rejected if it collides with an existing one)',
|
|
17
|
+
},
|
|
15
18
|
summary: { type: 'string', description: 'One-line summary (goes into the index; keep it short)' },
|
|
16
19
|
body: { type: 'string', description: 'Full content (details/context/evidence)' },
|
|
17
20
|
type: {
|
|
@@ -32,7 +35,10 @@ export const memorySaveTool = {
|
|
|
32
35
|
type: 'object',
|
|
33
36
|
properties: {
|
|
34
37
|
src: { type: 'string', description: 'Source entity name (defaults to the memory name)' },
|
|
35
|
-
relation: {
|
|
38
|
+
relation: {
|
|
39
|
+
type: 'string',
|
|
40
|
+
description: 'Relation, snake_case, e.g. depends_on / decided_by / conflicts_with',
|
|
41
|
+
},
|
|
36
42
|
dst: { type: 'string', description: 'Target entity name' },
|
|
37
43
|
fact: { type: 'string', description: 'Optional one-line statement for the edge' },
|
|
38
44
|
},
|
|
@@ -31,9 +31,7 @@ export const memorySearchTool = {
|
|
|
31
31
|
return '错误:缺少 query。';
|
|
32
32
|
const r = searchEntries(query, {
|
|
33
33
|
type: typeof args.type === 'string' ? args.type : undefined,
|
|
34
|
-
status: typeof args.status === 'string'
|
|
35
|
-
? args.status
|
|
36
|
-
: undefined,
|
|
34
|
+
status: typeof args.status === 'string' ? args.status : undefined,
|
|
37
35
|
limit: typeof args.limit === 'number' ? args.limit : undefined,
|
|
38
36
|
});
|
|
39
37
|
const entryText = r
|
|
@@ -47,7 +45,9 @@ export const memorySearchTool = {
|
|
|
47
45
|
const lines = g.edges
|
|
48
46
|
.slice(0, GRAPH_FACTS_LIMIT)
|
|
49
47
|
.map((e) => `${e.src} --[${e.relation}]--> ${e.dst}${e.fact ? ` (${e.fact})` : ''}`);
|
|
50
|
-
const more = g.edges.length > GRAPH_FACTS_LIMIT
|
|
48
|
+
const more = g.edges.length > GRAPH_FACTS_LIMIT
|
|
49
|
+
? `\n…(共 ${g.edges.length} 条,其余用 memory_graph action=neighbors 展开)`
|
|
50
|
+
: '';
|
|
51
51
|
graphText = `\n\n## 知识图谱事实\n${lines.join('\n')}${more}`;
|
|
52
52
|
}
|
|
53
53
|
}
|
|
@@ -17,7 +17,10 @@ function err(message) {
|
|
|
17
17
|
}
|
|
18
18
|
/** 归一化 section:兼容单复数、下划线/空格/连字符、大小写偏差。 */
|
|
19
19
|
function normalizeSection(raw) {
|
|
20
|
-
const s = String(raw ?? '')
|
|
20
|
+
const s = String(raw ?? '')
|
|
21
|
+
.trim()
|
|
22
|
+
.toLowerCase()
|
|
23
|
+
.replace(/[-\s]+/g, '_');
|
|
21
24
|
if (NOTE_SECTION_KEYS.includes(s))
|
|
22
25
|
return s;
|
|
23
26
|
// 单数/别名归一
|
|
@@ -86,11 +89,19 @@ export const noteAppendTool = {
|
|
|
86
89
|
const tag = typeof args.tag === 'string' && args.tag.trim() ? args.tag.trim() : undefined;
|
|
87
90
|
const result = appendNoteToSection(section, entry, tag);
|
|
88
91
|
if ('error' in result) {
|
|
89
|
-
return {
|
|
92
|
+
return {
|
|
93
|
+
status: 'error',
|
|
94
|
+
code: 'EXECUTION_ERROR',
|
|
95
|
+
retryable: false,
|
|
96
|
+
output: `错误:写入 notes.md 失败: ${result.error}`,
|
|
97
|
+
};
|
|
90
98
|
}
|
|
91
99
|
// note_append 写内部 notes.md,不作为用户代码 mutation 上报 changedFiles(与 plan_update 一致)。
|
|
92
100
|
const titleMap = {
|
|
93
|
-
findings: 'Findings',
|
|
101
|
+
findings: 'Findings',
|
|
102
|
+
decisions: 'Decisions',
|
|
103
|
+
open_questions: 'Open Questions',
|
|
104
|
+
risks: 'Risks',
|
|
94
105
|
};
|
|
95
106
|
const rendered = tag ? `- **[${tag}]** ${entry}` : `- ${entry}`;
|
|
96
107
|
return {
|
|
@@ -5,7 +5,9 @@ function err(message) {
|
|
|
5
5
|
}
|
|
6
6
|
/** 归一化模型常见的 status 变体,降低调用摩擦(无损兼容,不补造业务值)。 */
|
|
7
7
|
function normalizeStatus(raw) {
|
|
8
|
-
const s = String(raw ?? 'pending')
|
|
8
|
+
const s = String(raw ?? 'pending')
|
|
9
|
+
.trim()
|
|
10
|
+
.toLowerCase();
|
|
9
11
|
if (['in_progress', 'in-progress', 'inprogress', 'doing', 'current', 'active', 'wip'].includes(s))
|
|
10
12
|
return 'in_progress';
|
|
11
13
|
if (['completed', 'complete', 'done', 'finished', 'checked'].includes(s))
|
|
@@ -19,7 +21,8 @@ export const planUpdateTool = {
|
|
|
19
21
|
description: 'Record and update the session execution plan (the `## Plan:` block in `.mocode/sessions/<id>/notes.md`). ' +
|
|
20
22
|
'Use for any task with 3+ steps or context-loss risk. This REPLACES the whole plan each call, so always pass the full steps array. ' +
|
|
21
23
|
'Rules: at most one step may be in_progress; mark a step completed as soon as its work is done — do not batch updates to end of turn. ' +
|
|
22
|
-
'
|
|
24
|
+
'Give every step a short `title` (≤20 chars, e.g. "编写测试" / "修 status bar") that shows in the status bar, ' +
|
|
25
|
+
'plus a `content` that is self-contained enough to survive context compaction: name the target file/symbol, the change, and how to verify. ' +
|
|
23
26
|
'When every step is completed the plan auto-settles to `## Done:`. Creates notes.md if missing. Safe to call in PLAN mode (writes only the session notepad, never project files).',
|
|
24
27
|
risk: 'safe',
|
|
25
28
|
parameters: {
|
|
@@ -35,10 +38,15 @@ export const planUpdateTool = {
|
|
|
35
38
|
},
|
|
36
39
|
steps: {
|
|
37
40
|
type: 'array',
|
|
38
|
-
description: `Full replacement step list (1-${PLAN_MAX_STEPS}). At most one step may be in_progress
|
|
41
|
+
description: `Full replacement step list (1-${PLAN_MAX_STEPS}). At most one step may be in_progress. ` +
|
|
42
|
+
'Each step should carry both a short `title` and a detailed `content`.',
|
|
39
43
|
items: {
|
|
40
44
|
type: 'object',
|
|
41
45
|
properties: {
|
|
46
|
+
title: {
|
|
47
|
+
type: 'string',
|
|
48
|
+
description: 'Short scannable label (≤20 chars) shown in the status bar, e.g. "编写测试" / "修 status bar". Keep details in `content`.',
|
|
49
|
+
},
|
|
42
50
|
content: {
|
|
43
51
|
type: 'string',
|
|
44
52
|
description: 'Self-contained step: target file/symbol, the change, and how to verify it.',
|
|
@@ -61,7 +69,9 @@ export const planUpdateTool = {
|
|
|
61
69
|
required: ['steps'],
|
|
62
70
|
additionalProperties: false,
|
|
63
71
|
},
|
|
64
|
-
// 兼容模型的字段命名偏差:activeForm→active_form;step 用 text/task/description 代替 content
|
|
72
|
+
// 兼容模型的字段命名偏差:activeForm→active_form;step 用 text/task/description 代替 content;
|
|
73
|
+
// step 短标签用 label/heading/step_title/short_title 代替 title(必须 delete 原字段,
|
|
74
|
+
// 否则 items.additionalProperties=false 会在随后的 schema 校验里拒收)。
|
|
65
75
|
normalizeArguments(args) {
|
|
66
76
|
if (args.activeForm !== undefined && args.active_form === undefined) {
|
|
67
77
|
args.active_form = args.activeForm;
|
|
@@ -72,10 +82,22 @@ export const planUpdateTool = {
|
|
|
72
82
|
if (!s || typeof s !== 'object')
|
|
73
83
|
continue;
|
|
74
84
|
const step = s;
|
|
85
|
+
if (step.title === undefined) {
|
|
86
|
+
for (const alt of ['label', 'heading', 'step_title', 'short_title']) {
|
|
87
|
+
if (typeof step[alt] === 'string') {
|
|
88
|
+
step.title = step[alt];
|
|
89
|
+
delete step[alt];
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
75
94
|
if (step.content === undefined) {
|
|
76
95
|
for (const alt of ['text', 'task', 'description', 'title']) {
|
|
77
96
|
if (typeof step[alt] === 'string') {
|
|
78
97
|
step.content = step[alt];
|
|
98
|
+
// content 由 title 兜底填补时,不再把它当短标签——否则会渲染成 `**A** — A`。
|
|
99
|
+
if (alt === 'title')
|
|
100
|
+
step.title = undefined;
|
|
79
101
|
break;
|
|
80
102
|
}
|
|
81
103
|
}
|
|
@@ -108,10 +130,14 @@ export const planUpdateTool = {
|
|
|
108
130
|
}
|
|
109
131
|
if (status === 'in_progress')
|
|
110
132
|
inProgressCount++;
|
|
111
|
-
const activeForm = typeof raw?.active_form === 'string' && raw.active_form.trim()
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
133
|
+
const activeForm = typeof raw?.active_form === 'string' && raw.active_form.trim() ? raw.active_form.trim() : undefined;
|
|
134
|
+
const title = typeof raw?.title === 'string' && raw.title.trim() ? raw.title.trim() : undefined;
|
|
135
|
+
steps.push({
|
|
136
|
+
...(title ? { title } : {}),
|
|
137
|
+
content,
|
|
138
|
+
status,
|
|
139
|
+
...(activeForm ? { activeForm } : {}),
|
|
140
|
+
});
|
|
115
141
|
}
|
|
116
142
|
if (inProgressCount > 1) {
|
|
117
143
|
return err(`同一时刻只能有一个 in_progress 步骤,当前 ${inProgressCount} 个——请把其余改回 pending。`);
|
|
@@ -128,7 +154,12 @@ export const planUpdateTool = {
|
|
|
128
154
|
const plan = { title, ...(goal ? { goal } : {}), steps };
|
|
129
155
|
const result = writePlanToNotes(plan);
|
|
130
156
|
if ('error' in result) {
|
|
131
|
-
return {
|
|
157
|
+
return {
|
|
158
|
+
status: 'error',
|
|
159
|
+
code: 'EXECUTION_ERROR',
|
|
160
|
+
retryable: false,
|
|
161
|
+
output: `错误:写入 notes.md 失败: ${result.error}`,
|
|
162
|
+
};
|
|
132
163
|
}
|
|
133
164
|
const done = steps.filter((s) => s.status === 'completed').length;
|
|
134
165
|
const rendered = renderPlanSection(plan);
|
|
@@ -53,7 +53,6 @@ export async function runCommandRaw(command, timeout = 120000, signal, cwd) {
|
|
|
53
53
|
});
|
|
54
54
|
const output = new BoundedCommandOutput();
|
|
55
55
|
let finished = false;
|
|
56
|
-
let timer;
|
|
57
56
|
const killTree = () => {
|
|
58
57
|
try {
|
|
59
58
|
if (isWin) {
|
|
@@ -73,8 +72,7 @@ export async function runCommandRaw(command, timeout = 120000, signal, cwd) {
|
|
|
73
72
|
if (finished)
|
|
74
73
|
return;
|
|
75
74
|
finished = true;
|
|
76
|
-
|
|
77
|
-
clearTimeout(timer);
|
|
75
|
+
clearTimeout(timer);
|
|
78
76
|
signal?.removeEventListener('abort', onAbort);
|
|
79
77
|
done({ ...result, durationMs: Date.now() - startedAt });
|
|
80
78
|
};
|
|
@@ -83,6 +81,12 @@ export async function runCommandRaw(command, timeout = 120000, signal, cwd) {
|
|
|
83
81
|
finish({ status: 'aborted', exitCode: null, output: output.render().trim() });
|
|
84
82
|
};
|
|
85
83
|
const onChunk = (chunk) => output.append(chunk.toString('utf8'));
|
|
84
|
+
// 先起 timer 再挂事件:finish/onAbort 是闭包,里面要 clearTimeout(timer)。
|
|
85
|
+
// 把 timer 的初始化提到所有引用它的注册点之前,避免依赖"事件回调必然异步"这一前提。
|
|
86
|
+
const timer = setTimeout(() => {
|
|
87
|
+
killTree();
|
|
88
|
+
finish({ status: 'timed_out', exitCode: null, output: output.render().trim() });
|
|
89
|
+
}, timeout);
|
|
86
90
|
child.stdout.on('data', onChunk);
|
|
87
91
|
child.stderr.on('data', onChunk);
|
|
88
92
|
child.on('error', (error) => {
|
|
@@ -95,10 +99,6 @@ export async function runCommandRaw(command, timeout = 120000, signal, cwd) {
|
|
|
95
99
|
output: output.render().trim(),
|
|
96
100
|
});
|
|
97
101
|
});
|
|
98
|
-
timer = setTimeout(() => {
|
|
99
|
-
killTree();
|
|
100
|
-
finish({ status: 'timed_out', exitCode: null, output: output.render().trim() });
|
|
101
|
-
}, timeout);
|
|
102
102
|
if (signal) {
|
|
103
103
|
if (signal.aborted)
|
|
104
104
|
onAbort();
|
|
@@ -142,7 +142,7 @@ function commandOutcome(result) {
|
|
|
142
142
|
export const runCommandTool = {
|
|
143
143
|
name: 'run_command',
|
|
144
144
|
description: 'Run a shell command, merging stdout+stderr. Default timeout 120s. For tests, builds, git, etc.\n' +
|
|
145
|
-
|
|
145
|
+
"Multiple independent run_command calls may be issued in one response to save model round-trips; they execute serially, so do not depend one on another's output within the same message.",
|
|
146
146
|
risk: 'dangerous',
|
|
147
147
|
parameters: {
|
|
148
148
|
type: 'object',
|
|
@@ -1,89 +1,9 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
2
1
|
import { mkdir } from 'node:fs/promises';
|
|
3
2
|
import { dirname, extname } from 'node:path';
|
|
4
|
-
import { loadImageAttachment, MAX_INLINE_BYTES_DEFAULT
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const child = spawn(program, args, {
|
|
9
|
-
env,
|
|
10
|
-
windowsHide: true,
|
|
11
|
-
stdio: ['ignore', 'ignore', 'pipe'],
|
|
12
|
-
});
|
|
13
|
-
let stderr = '';
|
|
14
|
-
let finished = false;
|
|
15
|
-
const finish = (result) => {
|
|
16
|
-
if (finished)
|
|
17
|
-
return;
|
|
18
|
-
finished = true;
|
|
19
|
-
signal?.removeEventListener('abort', onAbort);
|
|
20
|
-
resolve(result);
|
|
21
|
-
};
|
|
22
|
-
const onAbort = () => {
|
|
23
|
-
child.kill();
|
|
24
|
-
finish({ status: 'aborted', detail: 'Screenshot capture was aborted.' });
|
|
25
|
-
};
|
|
26
|
-
child.stderr.on('data', (chunk) => {
|
|
27
|
-
stderr = (stderr + chunk.toString('utf8')).slice(-4000);
|
|
28
|
-
});
|
|
29
|
-
child.on('error', (error) => finish({ status: 'spawn_error', detail: error.message }));
|
|
30
|
-
child.on('close', (code) => finish({
|
|
31
|
-
status: code === 0 ? 'passed' : 'failed',
|
|
32
|
-
detail: stderr.trim() || `Capture process exited with code ${code ?? 'null'}.`,
|
|
33
|
-
}));
|
|
34
|
-
if (signal) {
|
|
35
|
-
if (signal.aborted)
|
|
36
|
-
onAbort();
|
|
37
|
-
else
|
|
38
|
-
signal.addEventListener('abort', onAbort, { once: true });
|
|
39
|
-
}
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
const WINDOWS_CAPTURE_SCRIPT = [
|
|
43
|
-
'Add-Type -AssemblyName System.Windows.Forms',
|
|
44
|
-
'Add-Type -AssemblyName System.Drawing',
|
|
45
|
-
"$bounds = if ($env:MOCODE_SCREENSHOT_TARGET -eq 'all') { [System.Windows.Forms.SystemInformation]::VirtualScreen } else { [System.Windows.Forms.Screen]::PrimaryScreen.Bounds }",
|
|
46
|
-
'$bitmap = [System.Drawing.Bitmap]::new($bounds.Width, $bounds.Height)',
|
|
47
|
-
'$graphics = [System.Drawing.Graphics]::FromImage($bitmap)',
|
|
48
|
-
'try {',
|
|
49
|
-
' $graphics.CopyFromScreen($bounds.X, $bounds.Y, 0, 0, $bounds.Size)',
|
|
50
|
-
' $bitmap.Save($env:MOCODE_SCREENSHOT_PATH, [System.Drawing.Imaging.ImageFormat]::Png)',
|
|
51
|
-
'} finally {',
|
|
52
|
-
' $graphics.Dispose()',
|
|
53
|
-
' $bitmap.Dispose()',
|
|
54
|
-
'}',
|
|
55
|
-
].join('\n');
|
|
56
|
-
async function captureDesktop(outputPath, target, signal) {
|
|
57
|
-
const env = {
|
|
58
|
-
...filterEnv(process.env),
|
|
59
|
-
MOCODE_SCREENSHOT_PATH: outputPath,
|
|
60
|
-
MOCODE_SCREENSHOT_TARGET: target,
|
|
61
|
-
};
|
|
62
|
-
if (process.platform === 'win32') {
|
|
63
|
-
return runCaptureProcess('powershell.exe', ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', WINDOWS_CAPTURE_SCRIPT], env, signal);
|
|
64
|
-
}
|
|
65
|
-
if (process.platform === 'darwin') {
|
|
66
|
-
const args = ['-x', ...(target === 'primary' ? ['-m'] : []), outputPath];
|
|
67
|
-
return runCaptureProcess('/usr/sbin/screencapture', args, env, signal);
|
|
68
|
-
}
|
|
69
|
-
const candidates = [
|
|
70
|
-
['gnome-screenshot', ['-f', outputPath]],
|
|
71
|
-
['grim', [outputPath]],
|
|
72
|
-
['scrot', [outputPath]],
|
|
73
|
-
['import', ['-window', 'root', outputPath]],
|
|
74
|
-
];
|
|
75
|
-
const failures = [];
|
|
76
|
-
for (const [program, args] of candidates) {
|
|
77
|
-
const result = await runCaptureProcess(program, args, env, signal);
|
|
78
|
-
if (result.status === 'passed' || result.status === 'aborted')
|
|
79
|
-
return result;
|
|
80
|
-
failures.push(`${program}: ${result.detail}`);
|
|
81
|
-
}
|
|
82
|
-
return {
|
|
83
|
-
status: 'failed',
|
|
84
|
-
detail: `No supported Linux screenshot command succeeded. ${failures.join(' | ')}`,
|
|
85
|
-
};
|
|
86
|
-
}
|
|
3
|
+
import { loadImageAttachment, MAX_INLINE_BYTES_DEFAULT } from '../../attachments/image.js';
|
|
4
|
+
import { jailResolve } from '../../sandbox/index.js';
|
|
5
|
+
// 平台捕获逻辑已抽到 runtime/screen-capture.ts(computer 工具闭环共用),本文件只做薄封装。
|
|
6
|
+
import { captureDesktop } from '../../runtime/screen-capture.js';
|
|
87
7
|
export const screenshotTool = {
|
|
88
8
|
name: 'screenshot',
|
|
89
9
|
description: 'Capture the desktop and inspect it as visual model input. Use this to diagnose visible UI state, dialogs, rendering problems, or applications that cannot be understood from source files alone. The user must approve each capture because screenshots may contain sensitive information.',
|
|
@@ -117,7 +37,9 @@ export const screenshotTool = {
|
|
|
117
37
|
requestedPath += '.png';
|
|
118
38
|
if (extname(requestedPath).toLowerCase() !== '.png') {
|
|
119
39
|
return {
|
|
120
|
-
status: 'error',
|
|
40
|
+
status: 'error',
|
|
41
|
+
code: 'INVALID_ARGUMENTS',
|
|
42
|
+
retryable: false,
|
|
121
43
|
output: 'Screenshot output path must use the .png extension.',
|
|
122
44
|
};
|
|
123
45
|
}
|
|
@@ -161,13 +83,15 @@ export const screenshotTool = {
|
|
|
161
83
|
code: 'OK',
|
|
162
84
|
retryable: false,
|
|
163
85
|
output: `Captured ${target} display to ${requestedPath} (${att.bytes} bytes). Visual content is attached to the next model request.`,
|
|
164
|
-
modelAttachments: [
|
|
86
|
+
modelAttachments: [
|
|
87
|
+
{
|
|
165
88
|
type: 'image',
|
|
166
89
|
name: att.name,
|
|
167
90
|
mime: att.mime,
|
|
168
91
|
dataUrl: att.dataUrl,
|
|
169
92
|
detail,
|
|
170
|
-
}
|
|
93
|
+
},
|
|
94
|
+
],
|
|
171
95
|
};
|
|
172
96
|
},
|
|
173
97
|
};
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
import { spawnAgent } from '../../agent/spawn.js';
|
|
2
2
|
import { MAX_OUTPUT } from '../constants.js';
|
|
3
3
|
import { t } from '../../i18n/index.js';
|
|
4
|
-
import {
|
|
4
|
+
import { isSubAgentHardDisabled } from '../../config/index.js';
|
|
5
5
|
// ---------- task ----------
|
|
6
|
-
// 派生子 agent 执行独立子任务。子 agent
|
|
7
|
-
//
|
|
6
|
+
// 派生子 agent 执行独立子任务。子 agent 与主 agent 完全同源:同一份系统提示、同一份工具
|
|
7
|
+
// schema、同一份对话前缀(命中前缀缓存),写操作直接落在工作区并进入主 agent 当前轮次的
|
|
8
|
+
// 同一回滚事务。它有自己的 history 分支(子任务的工具噪声不回灌主对话),最终摘要回灌主
|
|
9
|
+
// history 供主 agent 继续。
|
|
8
10
|
//
|
|
9
11
|
// 适用:分而治之的复杂任务 / 隔离上下文避免子任务工具噪声撑爆主窗口。
|
|
10
|
-
// 多个 task 在共享工作区期间由 capability scheduler 串行执行;隔离 workspace 落地后再开放并行写。
|
|
11
12
|
export const subAgentTool = {
|
|
12
13
|
name: 'sub-agent',
|
|
13
14
|
risk: 'dangerous',
|
|
14
15
|
description: [
|
|
15
|
-
'Spawn a
|
|
16
|
-
'
|
|
17
|
-
'Cannot recursively spawn sub-agents.',
|
|
16
|
+
'Spawn a worker with your own full capability set and conversation context for an isolated sub-task; only its structured result returns.',
|
|
17
|
+
'The worker sees everything you have already read and concluded, so it does not repeat exploration.',
|
|
18
18
|
].join(''),
|
|
19
19
|
parameters: {
|
|
20
20
|
type: 'object',
|
|
@@ -23,23 +23,10 @@ export const subAgentTool = {
|
|
|
23
23
|
type: 'string',
|
|
24
24
|
description: 'The sub-task instruction for the sub-agent. Be specific about what to investigate/implement and what to report back.',
|
|
25
25
|
},
|
|
26
|
-
tools: {
|
|
27
|
-
type: 'array',
|
|
28
|
-
items: { type: 'string' },
|
|
29
|
-
description: 'Optional whitelist for deliberate specialization. Omit it to preserve the worker full capability for its mode.',
|
|
30
|
-
},
|
|
31
26
|
maxSteps: {
|
|
32
27
|
type: 'number',
|
|
33
28
|
description: 'Optional loop-safety override. By default the worker uses the same step ceiling as the main agent; this is not a token budget.',
|
|
34
29
|
},
|
|
35
|
-
mode: {
|
|
36
|
-
type: 'string', enum: ['read', 'write'],
|
|
37
|
-
description: 'read tasks may run in parallel; write tasks use an isolated overlay and are merged by the coordinator.',
|
|
38
|
-
},
|
|
39
|
-
writeSet: {
|
|
40
|
-
type: 'array', items: { type: 'string' },
|
|
41
|
-
description: 'Known workspace-relative paths this task may write. Unknown write sets conservatively use the workspace lock.',
|
|
42
|
-
},
|
|
43
30
|
context: {
|
|
44
31
|
type: 'string',
|
|
45
32
|
description: 'Concise facts already learned by the main agent. Passing this avoids duplicate repository exploration.',
|
|
@@ -48,26 +35,28 @@ export const subAgentTool = {
|
|
|
48
35
|
required: ['prompt'],
|
|
49
36
|
},
|
|
50
37
|
async execute(args, ctx) {
|
|
51
|
-
if (
|
|
38
|
+
if (isSubAgentHardDisabled())
|
|
52
39
|
return t('task.disabled');
|
|
53
40
|
const prompt = String(args.prompt ?? '');
|
|
54
41
|
if (!prompt)
|
|
55
42
|
return t('task.missingPrompt');
|
|
56
|
-
const
|
|
57
|
-
? args.tools.map((t) => String(t))
|
|
58
|
-
: undefined;
|
|
59
|
-
const maxSteps = typeof args.maxSteps === 'number' && args.maxSteps > 0
|
|
60
|
-
? Math.floor(args.maxSteps)
|
|
61
|
-
: undefined;
|
|
62
|
-
const mode = args.mode === 'write' ? 'write' : 'read';
|
|
63
|
-
const writeSet = Array.isArray(args.writeSet) ? args.writeSet.map(String) : undefined;
|
|
43
|
+
const maxSteps = typeof args.maxSteps === 'number' && args.maxSteps > 0 ? Math.floor(args.maxSteps) : undefined;
|
|
64
44
|
const context = typeof args.context === 'string' ? args.context.slice(0, 4000) : undefined;
|
|
65
45
|
// 透传主 agent 的 abort signal:主 Ctrl+C 树杀子 agent(chat abort + 工具 abort)。
|
|
66
46
|
// callId 透传:子 agent 实时渲染据此挂靠主侧对应批次(并行派发时各行归位)。
|
|
67
|
-
|
|
47
|
+
// delegation 透传:子 agent 复用父 step 的系统提示/工具 schema/对话前缀,命中前缀缓存。
|
|
48
|
+
const result = await spawnAgent({
|
|
49
|
+
prompt,
|
|
50
|
+
maxSteps,
|
|
51
|
+
signal: ctx?.signal,
|
|
52
|
+
context,
|
|
53
|
+
callId: ctx?.callId,
|
|
54
|
+
parentAllowedToolNames: ctx?.allowedToolNames,
|
|
55
|
+
delegation: ctx?.delegation,
|
|
56
|
+
});
|
|
68
57
|
let output;
|
|
69
58
|
if (!result.completed) {
|
|
70
|
-
output = `[SubAgentResult status=${result.status} tokens=${result.usage.totalTokens}
|
|
59
|
+
output = `[SubAgentResult status=${result.status} tokens=${result.usage.totalTokens}]\n${result.summary ?? t('task.interrupted')}`;
|
|
71
60
|
}
|
|
72
61
|
else if (!result.summary) {
|
|
73
62
|
output = t('task.noSummary');
|
|
@@ -76,16 +65,16 @@ export const subAgentTool = {
|
|
|
76
65
|
// 摘要可能很长,截到 MAX_OUTPUT 保主 history 不爆。
|
|
77
66
|
const summary = [
|
|
78
67
|
result.summary,
|
|
79
|
-
`\n[SubAgentResult status=${result.status} tokens=${result.usage.totalTokens} prompt=${result.usage.promptTokens} completion=${result.usage.completionTokens} cached=${result.usage.cachedTokens}
|
|
80
|
-
]
|
|
81
|
-
|
|
82
|
-
|
|
68
|
+
`\n[SubAgentResult status=${result.status} tokens=${result.usage.totalTokens} prompt=${result.usage.promptTokens} completion=${result.usage.completionTokens} cached=${result.usage.cachedTokens}]`,
|
|
69
|
+
]
|
|
70
|
+
.filter(Boolean)
|
|
71
|
+
.join('\n');
|
|
72
|
+
output =
|
|
73
|
+
summary.length > MAX_OUTPUT
|
|
74
|
+
? summary.slice(0, MAX_OUTPUT) + `\n\n${t('task.summaryTruncated', { count: summary.length - MAX_OUTPUT })}`
|
|
75
|
+
: summary;
|
|
83
76
|
}
|
|
84
|
-
const code = result.status === 'aborted'
|
|
85
|
-
? 'ABORTED'
|
|
86
|
-
: result.status === 'conflict'
|
|
87
|
-
? 'CHANGE_CONFLICT'
|
|
88
|
-
: result.completed ? 'OK' : 'EXECUTION_ERROR';
|
|
77
|
+
const code = result.status === 'aborted' ? 'ABORTED' : result.completed ? 'OK' : 'EXECUTION_ERROR';
|
|
89
78
|
const outcome = {
|
|
90
79
|
status: result.status === 'aborted' ? 'aborted' : result.completed ? 'success' : 'error',
|
|
91
80
|
code,
|
|
@@ -55,7 +55,7 @@ export const useSkillTool = {
|
|
|
55
55
|
return `错误:无法读取 skill "${name}" 的文件 "${args.file}"(不存在 / 越界 / 过大)。`;
|
|
56
56
|
return `# Skill: ${name} · ${args.file}\n\n${content}`;
|
|
57
57
|
}
|
|
58
|
-
const body = await renderSkillBody(skill, args.args, ctx?.signal);
|
|
58
|
+
const body = await renderSkillBody(skill, args.args, ctx?.signal, ctx?.allowedToolNames);
|
|
59
59
|
if (body === null)
|
|
60
60
|
return `错误:未找到 skill "${name}" 的正文。用 /skills 查看可用 skill 列表。`;
|
|
61
61
|
// 激活 inline skill 的工具面约束(allowed/disallowed),本轮内生效。
|