c0de-agent 1.13.1 → 1.14.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/dist/cli/commands/serve.js +5 -1
- package/dist/cli/deps.d.ts +2 -4
- package/dist/cli/deps.js +22 -36
- package/dist/cli/modes/print.js +27 -17
- package/dist/core/agent.js +59 -36
- package/dist/core/agents/parallel.js +4 -2
- package/dist/core/config-path.d.ts +3 -1
- package/dist/core/config-path.js +33 -3
- package/dist/core/config.js +81 -0
- package/dist/core/context.js +6 -3
- package/dist/core/loop/persist.js +4 -1
- package/dist/core/loop/subagent.d.ts +19 -0
- package/dist/core/loop/subagent.js +73 -7
- package/dist/core/loop.d.ts +4 -1
- package/dist/core/loop.js +22 -6
- package/dist/{server/registry-config.d.ts → core/provider-registry.d.ts} +10 -2
- package/dist/core/provider-registry.js +66 -0
- package/dist/core/redact.d.ts +2 -1
- package/dist/core/redact.js +4 -2
- package/dist/core/secret.d.ts +5 -0
- package/dist/core/secret.js +14 -0
- package/dist/core/skills.d.ts +2 -0
- package/dist/core/skills.js +61 -0
- package/dist/core/slash.js +31 -18
- package/dist/core/title.js +4 -2
- package/dist/core/todo-tags.d.ts +4 -3
- package/dist/core/todo-tags.js +20 -6
- package/dist/core/workflow.d.ts +4 -13
- package/dist/core/workflow.js +6 -6
- package/dist/core/workflows/context.js +31 -59
- package/dist/core/workflows/runtime.js +10 -8
- package/dist/dap/index.d.ts +1 -0
- package/dist/dap/index.js +1 -0
- package/dist/dap/protocol.d.ts +8 -3
- package/dist/dap/protocol.js +54 -18
- package/dist/dap/session.js +71 -12
- package/dist/dap/spawn.d.ts +11 -0
- package/dist/dap/spawn.js +48 -0
- package/dist/dap/transport.js +18 -0
- package/dist/kanban/index.d.ts +1 -1
- package/dist/kanban/index.js +1 -1
- package/dist/kanban/store.d.ts +8 -1
- package/dist/kanban/store.js +61 -13
- package/dist/llm/protocols/openai-compat.d.ts +7 -0
- package/dist/llm/protocols/openai-compat.js +19 -2
- package/dist/llm/protocols/utils/tool-stream.d.ts +12 -3
- package/dist/llm/protocols/utils/tool-stream.js +47 -23
- package/dist/llm/provider.js +18 -3
- package/dist/llm/retry.d.ts +1 -0
- package/dist/llm/retry.js +20 -7
- package/dist/llm/routing.js +14 -2
- package/dist/llm/schema/messages.d.ts +9 -2
- package/dist/llm/token.d.ts +9 -0
- package/dist/llm/token.js +10 -6
- package/dist/llm/transport.js +7 -2
- package/dist/mcp/client.d.ts +11 -0
- package/dist/mcp/client.js +70 -0
- package/dist/mcp/index.d.ts +46 -0
- package/dist/mcp/index.js +106 -0
- package/dist/mcp/tool-adapter.d.ts +14 -0
- package/dist/mcp/tool-adapter.js +65 -0
- package/dist/mcp/transport.d.ts +35 -0
- package/dist/mcp/transport.js +243 -0
- package/dist/mcp/types.d.ts +55 -0
- package/dist/mcp/types.js +3 -0
- package/dist/plugins/init.d.ts +2 -0
- package/dist/plugins/init.js +10 -1
- package/dist/project/project.js +50 -3
- package/dist/project/resolve.d.ts +7 -0
- package/dist/project/resolve.js +52 -5
- package/dist/server/permission/interactive.js +9 -4
- package/dist/server/permission/store.js +8 -2
- package/dist/server/routes/auth.js +28 -13
- package/dist/server/routes/chat.js +92 -26
- package/dist/server/routes/config.js +38 -10
- package/dist/server/routes/files.js +75 -16
- package/dist/server/routes/filesystem.js +9 -2
- package/dist/server/routes/kanban.js +146 -9
- package/dist/server/routes/permissions.js +32 -0
- package/dist/server/routes/project.js +15 -6
- package/dist/server/routes/provider.js +10 -5
- package/dist/server/routes/session.js +72 -26
- package/dist/server/routes/terminal.js +10 -2
- package/dist/server/routes/todo.js +18 -0
- package/dist/server/routes/tool.js +13 -2
- package/dist/server/routes/update.js +5 -1
- package/dist/server/routes/workflows.js +17 -6
- package/dist/server/server.d.ts +1 -1
- package/dist/server/server.js +31 -17
- package/dist/server/terminal/pty-manager.d.ts +3 -0
- package/dist/server/terminal/pty-manager.js +8 -3
- package/dist/server/types.d.ts +2 -0
- package/dist/server/util/json-body.d.ts +31 -0
- package/dist/server/util/json-body.js +38 -0
- package/dist/session/archive.js +4 -4
- package/dist/session/branch.d.ts +10 -1
- package/dist/session/branch.js +54 -9
- package/dist/session/compaction.d.ts +5 -0
- package/dist/session/compaction.js +15 -10
- package/dist/session/context.d.ts +7 -3
- package/dist/session/context.js +35 -9
- package/dist/session/import.js +53 -6
- package/dist/session/index.d.ts +2 -2
- package/dist/session/index.js +2 -2
- package/dist/session/message.d.ts +12 -3
- package/dist/session/message.js +44 -4
- package/dist/session/session.d.ts +11 -5
- package/dist/session/session.js +99 -88
- package/dist/session/shake.js +35 -25
- package/dist/session/squash.js +3 -4
- package/dist/session/token.d.ts +7 -8
- package/dist/session/token.js +19 -16
- package/dist/session/usage.js +4 -2
- package/dist/shared/index.d.ts +1 -0
- package/dist/shared/index.js +1 -0
- package/dist/shared/round.d.ts +12 -0
- package/dist/shared/round.js +18 -0
- package/dist/shared/timer.d.ts +23 -0
- package/dist/shared/timer.js +27 -0
- package/dist/shared/types/tool.d.ts +4 -0
- package/dist/shared/utils/markdown-code.d.ts +15 -0
- package/dist/shared/utils/markdown-code.js +44 -0
- package/dist/shared/utils/path.d.ts +4 -1
- package/dist/shared/utils/path.js +5 -2
- package/dist/shared/utils/string.d.ts +5 -0
- package/dist/shared/utils/string.js +52 -0
- package/dist/tools/builtin/bash.js +79 -10
- package/dist/tools/builtin/edit.js +67 -8
- package/dist/tools/builtin/glob.js +150 -14
- package/dist/tools/builtin/grep.js +53 -11
- package/dist/tools/builtin/kanban.d.ts +14 -0
- package/dist/tools/builtin/kanban.js +4 -1
- package/dist/tools/builtin/read.js +15 -2
- package/dist/tools/builtin/resolvers.d.ts +9 -2
- package/dist/tools/builtin/resolvers.js +20 -6
- package/dist/tools/builtin/task.js +107 -25
- package/dist/tools/builtin/todo.js +84 -9
- package/dist/tools/builtin/write.js +7 -1
- package/dist/tools/builtin/yield.js +9 -1
- package/dist/tools/executor.js +53 -1
- package/dist/tools/hashline/patch.js +114 -38
- package/dist/tools/truncate.js +45 -16
- package/dist/tools/validate.js +66 -20
- package/dist/tools/websearch/index.js +8 -4
- package/dist/update/index.d.ts +1 -1
- package/dist/update/index.js +1 -1
- package/dist/update/scheduler.js +9 -2
- package/dist/update/snapshot.d.ts +14 -4
- package/dist/update/snapshot.js +22 -22
- package/dist/update/updater.d.ts +16 -2
- package/dist/update/updater.js +39 -8
- package/dist/update/version.d.ts +3 -1
- package/dist/update/version.js +63 -9
- package/package.json +1 -1
- package/dist/server/registry-config.js +0 -44
|
@@ -21,7 +21,11 @@ async function runServeCommand(ctx) {
|
|
|
21
21
|
(ctx.banner ?? printStartupBanner)(url);
|
|
22
22
|
// 非回环监听警告:绑定 0.0.0.0/局域网 IP 时本机以外的主机均可访问,
|
|
23
23
|
// 安全性完全依赖 token + allowedOrigins——默认行为已收紧为 127.0.0.1。
|
|
24
|
-
|
|
24
|
+
// 判定必须区分「127. 前缀」与「点分 IPv4 字面量」:host.startsWith('127.')
|
|
25
|
+
// 会把以 127. 开头的**域名**(127.example.com 等,可解析到任意公网地址)
|
|
26
|
+
// 误判为回环,非回环绑定的安全警告被静默跳过。仅四段点分十进制且首段为
|
|
27
|
+
// 127 的串(127.0.0.0/8 全域回环,RFC 1122)算回环。
|
|
28
|
+
const isLoopback = host === 'localhost' || host === '::1' || /^127(\.\d{1,3}){3}$/.test(host);
|
|
25
29
|
if (!isLoopback) {
|
|
26
30
|
console.warn(`[c0de] ⚠ 监听地址为 ${host}(非回环):本机以外的主机均可访问此服务。\n` +
|
|
27
31
|
` 请确认 security.authEnabled 已开启、token 未泄漏;仅在可信网络使用,或在防火墙限制访问。`);
|
package/dist/cli/deps.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { LoopDeps } from '../core/loop.js';
|
|
2
|
+
import { buildRegistryFromConfig } from '../core/provider-registry.js';
|
|
2
3
|
import type { DB } from '../db/client.js';
|
|
3
|
-
import type { Registry } from '../llm/registry.js';
|
|
4
4
|
import type { Config } from '../shared/types/config.js';
|
|
5
5
|
import type { PermissionChecker } from '../tools/types.js';
|
|
6
6
|
/** 权限策略:
|
|
@@ -11,8 +11,6 @@ type PermissionStrategy = 'full-auto' | 'safe';
|
|
|
11
11
|
declare const fullyAutoApproveChecker: PermissionChecker;
|
|
12
12
|
/** 无白名单的非交互安全策略(保留导出兼容既有调用方)。 */
|
|
13
13
|
declare const nonInteractiveSafeChecker: PermissionChecker;
|
|
14
|
-
/** 把 config.providers 注册到新建的 LLM registry。 */
|
|
15
|
-
declare function buildLLMRegistry(config: Config): Registry;
|
|
16
14
|
type BuildDepsOptions = {
|
|
17
15
|
db: DB;
|
|
18
16
|
cwd: string;
|
|
@@ -32,4 +30,4 @@ declare function resolveCwdProjectTrusted(db: DB, cwd: string): Promise<boolean>
|
|
|
32
30
|
/** 组装完整 LoopDeps(默认 safe 放行 + 默认工具注册表)。 */
|
|
33
31
|
declare function buildAgentDeps(config: Config, opts: BuildDepsOptions): Promise<LoopDeps>;
|
|
34
32
|
export type { BuildDepsOptions, PermissionStrategy };
|
|
35
|
-
export { buildAgentDeps,
|
|
33
|
+
export { buildAgentDeps, buildRegistryFromConfig, fullyAutoApproveChecker, nonInteractiveSafeChecker, resolveCwdProjectTrusted, };
|
package/dist/cli/deps.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { loadConfigScopes } from '../core/config.js';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { buildRegistryFromConfig } from '../core/provider-registry.js';
|
|
3
|
+
import { discoverSkills } from '../core/skills.js';
|
|
4
|
+
import { createDebugSpawn } from '../dap/index.js';
|
|
5
|
+
import { collectScopedMCPServers, registerMCPServers } from '../mcp/index.js';
|
|
4
6
|
import { initPlugins } from '../plugins/index.js';
|
|
5
7
|
import { getByDirectory } from '../project/index.js';
|
|
6
8
|
import { projectTrustCurrent } from '../project/trust.js';
|
|
@@ -40,31 +42,6 @@ function createNonInteractiveChecker(allowTools) {
|
|
|
40
42
|
}
|
|
41
43
|
/** 无白名单的非交互安全策略(保留导出兼容既有调用方)。 */
|
|
42
44
|
const nonInteractiveSafeChecker = createNonInteractiveChecker();
|
|
43
|
-
/** 把 config.providers 注册到新建的 LLM registry。 */
|
|
44
|
-
function buildLLMRegistry(config) {
|
|
45
|
-
const registry = createRegistry();
|
|
46
|
-
for (const p of config.providers) {
|
|
47
|
-
registerProviderFromConfig(registry, p);
|
|
48
|
-
}
|
|
49
|
-
return registry;
|
|
50
|
-
}
|
|
51
|
-
function registerProviderFromConfig(registry, p) {
|
|
52
|
-
// 兼容 config.json 中以 _tag 标识 provider 的格式(name 缺失时回退到 _tag)
|
|
53
|
-
const name = p.name || p._tag;
|
|
54
|
-
if (!name || !p.baseURL)
|
|
55
|
-
return;
|
|
56
|
-
// baseURL 已含 /v1 时用 /chat/completions,避免 /v1/v1 双重前缀
|
|
57
|
-
const path = p.baseURL.replace(/\/+$/, '').endsWith('/v1') ? '/chat/completions' : undefined;
|
|
58
|
-
registerProvider(registry, {
|
|
59
|
-
name,
|
|
60
|
-
baseURL: p.baseURL,
|
|
61
|
-
apiKey: p.apiKey ? decryptSecret(p.apiKey) : p.apiKey,
|
|
62
|
-
...(path ? { path } : {}),
|
|
63
|
-
// 传递用户配置的 per-model capabilities(contextWindow 等),
|
|
64
|
-
// 否则 resolveRoute 回退到 DEFAULT_MODEL_CAPABILITIES,可能导致预算过小。
|
|
65
|
-
...(p.models ? { models: overrideToCapabilities(p.models) } : {}),
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
45
|
/** 解析权限 checker:显式策略优先,否则回退到 config.permission.defaultMode。
|
|
69
46
|
* 无 --allow 白名单时返回单例 nonInteractiveSafeChecker(保持对象同一性);有白名单
|
|
70
47
|
* 时按白名单新建 checker(--allow 只影响 ask 工具的定向放行)。 */
|
|
@@ -98,39 +75,48 @@ async function resolveCwdProjectTrusted(db, cwd) {
|
|
|
98
75
|
}
|
|
99
76
|
/** 组装完整 LoopDeps(默认 safe 放行 + 默认工具注册表)。 */
|
|
100
77
|
async function buildAgentDeps(config, opts) {
|
|
101
|
-
const llmRegistry =
|
|
78
|
+
const llmRegistry = buildRegistryFromConfig(config);
|
|
102
79
|
const toolRegistry = createDefaultRegistry(config);
|
|
103
80
|
// P0-2:项目插件仅在项目被显式信任后加载(c0de trust <dir>);内存库
|
|
104
81
|
// (--temp/锁冲突降级)不含项目记录 → 未信任,项目插件不加载。
|
|
105
82
|
// P0(代码面):已信任但指纹漂移(插件代码/MCP 参数/风险键变更)→ 不加载,
|
|
106
83
|
// 与聊天门禁同口径。
|
|
107
84
|
const projectTrusted = await resolveCwdProjectTrusted(opts.db, opts.cwd);
|
|
108
|
-
const { hookRunner } = await initPlugins({
|
|
85
|
+
const { hookRunner, config: resolvedConfig } = await initPlugins({
|
|
109
86
|
cwd: opts.cwd,
|
|
110
87
|
config,
|
|
111
88
|
toolRegistry,
|
|
112
89
|
llmRegistry,
|
|
113
90
|
projectTrusted,
|
|
114
91
|
});
|
|
92
|
+
// MCP 客户端(spec §6):全局作用域始终连接;项目作用域(随 git clone 传播的
|
|
93
|
+
// 任意命令执行面)仅项目已信任时连接——与插件加载同口径。stdio 子进程已
|
|
94
|
+
// unref:CLI 进程退出时 stdin 关闭,MCP 服务器自行退出。
|
|
95
|
+
await registerMCPServers(toolRegistry, collectScopedMCPServers(loadConfigScopes(opts.cwd), projectTrusted));
|
|
115
96
|
const deps = {
|
|
116
97
|
db: opts.db,
|
|
117
98
|
llmRegistry,
|
|
118
99
|
toolRegistry,
|
|
119
100
|
urlRegistry: createDefaultURLRegistry(),
|
|
120
101
|
hookRunner,
|
|
121
|
-
permission: resolvePermissionChecker(
|
|
122
|
-
config,
|
|
102
|
+
permission: resolvePermissionChecker(resolvedConfig, opts.permissionStrategy, opts.allowTools),
|
|
103
|
+
config: resolvedConfig,
|
|
123
104
|
cwd: opts.cwd,
|
|
105
|
+
// 技能发现(system prompt ## Loaded Skills 段数据源)。
|
|
106
|
+
skills: discoverSkills(opts.cwd),
|
|
107
|
+
// DAP(spec §21):CLI 宿主注入真实适配器 spawn——此前恒报「no debug adapter
|
|
108
|
+
// spawn is wired」。调试会话随进程结束自然消亡(transport 子进程非 unref)。
|
|
109
|
+
debugSpawn: createDebugSpawn(),
|
|
124
110
|
...(opts.chatStream ? { chatStream: opts.chatStream } : {}),
|
|
125
111
|
// P:预算护栏 CLI 变体——print/acp 无恢复 UI,超支中止 run(产出 error)而非
|
|
126
112
|
// 暂停永久挂起。金额与 token 任一动作='pause'/'abort' 即启用(每轮 LLM 请求前检查)。
|
|
127
|
-
...(
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
113
|
+
...(resolvedConfig.usage?.budgetAction === 'pause' ||
|
|
114
|
+
resolvedConfig.usage?.budgetAction === 'abort' ||
|
|
115
|
+
resolvedConfig.usage?.tokenBudgetAction === 'pause' ||
|
|
116
|
+
resolvedConfig.usage?.tokenBudgetAction === 'abort'
|
|
131
117
|
? { budgetAbort: true }
|
|
132
118
|
: {}),
|
|
133
119
|
};
|
|
134
120
|
return deps;
|
|
135
121
|
}
|
|
136
|
-
export { buildAgentDeps,
|
|
122
|
+
export { buildAgentDeps, buildRegistryFromConfig, fullyAutoApproveChecker, nonInteractiveSafeChecker, resolveCwdProjectTrusted, };
|
package/dist/cli/modes/print.js
CHANGED
|
@@ -99,29 +99,39 @@ async function runPrintMode(config, message, deps, opts = {}) {
|
|
|
99
99
|
const state = await createAgent(session, agentConfig, deps);
|
|
100
100
|
// P2-4:ACP abort 真中止——把外部信号桥接到该 run 的 abortController。
|
|
101
101
|
// abortAgent 会使 loop 在 turn/流边界 unwind,runAgent 随即结束。
|
|
102
|
+
// 监听器必须在 run 终结处摘除:{ once: true } 只在「真的触发 abort」时
|
|
103
|
+
// 自动摘除——正常完成的 run(绝大多数)从不触发 abort,闭包捕获整个
|
|
104
|
+
// agent state,经 ACP 的 currentAbort(持有上一个 controller)钉住到
|
|
105
|
+
// 下一次请求才释放(同型:executor/bash/permission checker 口径)。
|
|
106
|
+
const onAbort = () => {
|
|
107
|
+
abortAgent(state);
|
|
108
|
+
};
|
|
102
109
|
if (opts.abortSignal) {
|
|
103
110
|
if (opts.abortSignal.aborted) {
|
|
104
111
|
throw new Error('已中止');
|
|
105
112
|
}
|
|
106
|
-
opts.abortSignal.addEventListener('abort',
|
|
107
|
-
abortAgent(state);
|
|
108
|
-
}, { once: true });
|
|
109
|
-
}
|
|
110
|
-
const events = [];
|
|
111
|
-
for await (const event of runAgent(state, [{ _tag: 'text', text: message }], deps)) {
|
|
112
|
-
events.push(event);
|
|
113
|
-
opts.onEvent?.(event);
|
|
113
|
+
opts.abortSignal.addEventListener('abort', onAbort, { once: true });
|
|
114
114
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
115
|
+
try {
|
|
116
|
+
const events = [];
|
|
117
|
+
for await (const event of runAgent(state, [{ _tag: 'text', text: message }], deps)) {
|
|
118
|
+
events.push(event);
|
|
119
|
+
opts.onEvent?.(event);
|
|
120
|
+
}
|
|
121
|
+
// P2-4:中止后如实报错(ACP 客户端收到 error 响应,而非谎报 ok 的完成结果)。
|
|
122
|
+
if (opts.abortSignal?.aborted) {
|
|
123
|
+
throw new Error('已中止');
|
|
124
|
+
}
|
|
125
|
+
// 终态错误(unexpected,含预算中止)必须反馈给用户而非静默返回半截文本——
|
|
126
|
+
// 抛出后 dispatch 打印 message 并以非零码退出(预算超支中止等硬性护栏由此可见)。
|
|
127
|
+
const terminal = events.find((e) => e._tag === 'error' && e.error._tag === 'unexpected');
|
|
128
|
+
if (terminal && terminal._tag === 'error' && terminal.error._tag === 'unexpected') {
|
|
129
|
+
throw new Error(terminal.error.message);
|
|
130
|
+
}
|
|
131
|
+
return collectAssistantText(events);
|
|
118
132
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const terminal = events.find((e) => e._tag === 'error' && e.error._tag === 'unexpected');
|
|
122
|
-
if (terminal && terminal._tag === 'error' && terminal.error._tag === 'unexpected') {
|
|
123
|
-
throw new Error(terminal.error.message);
|
|
133
|
+
finally {
|
|
134
|
+
opts.abortSignal?.removeEventListener('abort', onAbort);
|
|
124
135
|
}
|
|
125
|
-
return collectAssistantText(events);
|
|
126
136
|
}
|
|
127
137
|
export { collectAssistantText, runPrintMode };
|
package/dist/core/agent.js
CHANGED
|
@@ -50,44 +50,67 @@ async function createAgent(session, config, deps) {
|
|
|
50
50
|
};
|
|
51
51
|
}
|
|
52
52
|
async function* runAgent(state, userInput, deps) {
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (p._tag === 'image' && q?._tag === 'image')
|
|
64
|
-
return p.mediaType === q.mediaType && p.data === q.data;
|
|
65
|
-
return false;
|
|
66
|
-
});
|
|
67
|
-
if (!isDupUser) {
|
|
68
|
-
await appendMessage(deps.db, state.session.id, {
|
|
69
|
-
role: 'user',
|
|
70
|
-
content: userInput,
|
|
71
|
-
});
|
|
53
|
+
// 生命周期 hook(spec §3.7):agent:start 广播每次 run 的启动配置;
|
|
54
|
+
// agent:end 无论完成/出错/中止都广播(finally 保证)。
|
|
55
|
+
// 主 run 与子 agent run 都经此处,插件可按 config.agentName 等区分。
|
|
56
|
+
if (deps.hookRunner) {
|
|
57
|
+
try {
|
|
58
|
+
await deps.hookRunner.fireHooks('agent:start', { config: state.config });
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// fireHooks 内部已吞 handler 异常;双保险避免启动广播失败阻断 run
|
|
62
|
+
}
|
|
72
63
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
.
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
64
|
+
try {
|
|
65
|
+
// 幂等重发保护:若 DB 末尾已是相同 user message(服务重启后续传场景),
|
|
66
|
+
// 跳过 append 避免重复。比较 text/image parts 全量内容——纯文本比较会在
|
|
67
|
+
// 「含图片消息重发」时误判为不同消息而追加重复条目(P1-2 修复)。
|
|
68
|
+
const last = state.messages[state.messages.length - 1];
|
|
69
|
+
const isDupUser = last?.role === 'user' &&
|
|
70
|
+
last.content.length === userInput.length &&
|
|
71
|
+
last.content.every((p, i) => {
|
|
72
|
+
const q = userInput[i];
|
|
73
|
+
if (p._tag === 'text' && q?._tag === 'text')
|
|
74
|
+
return p.text === q.text;
|
|
75
|
+
if (p._tag === 'image' && q?._tag === 'image')
|
|
76
|
+
return p.mediaType === q.mediaType && p.data === q.data;
|
|
77
|
+
return false;
|
|
78
|
+
});
|
|
79
|
+
if (!isDupUser) {
|
|
80
|
+
await appendMessage(deps.db, state.session.id, {
|
|
81
|
+
role: 'user',
|
|
82
|
+
content: userInput,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
// 标题生成用纯文本(join text parts),忽略 image/tool 等非文本 part。
|
|
86
|
+
const titleText = userInput
|
|
87
|
+
.filter((p) => p._tag === 'text')
|
|
88
|
+
.map((p) => (p._tag === 'text' ? p.text : ''))
|
|
89
|
+
.join('');
|
|
90
|
+
// 第一条用户消息后,后台为会话生成简短标题(fire-and-forget)。
|
|
91
|
+
// 条件:标题仍是默认占位 + 持久化前无任何消息(即首条消息)。
|
|
92
|
+
// 失败被吞掉,绝不阻塞主对话流。
|
|
93
|
+
if (state.session.title === DEFAULT_SESSION_TITLE && state.messages.length === 0) {
|
|
94
|
+
void generateSessionTitle({
|
|
95
|
+
db: deps.db,
|
|
96
|
+
llmRegistry: deps.llmRegistry,
|
|
97
|
+
config: deps.config,
|
|
98
|
+
...(deps.titleChatFn ? { chatFn: deps.titleChatFn } : {}),
|
|
99
|
+
}, state.session.id, titleText, state.config.provider, state.config.model).catch(() => { });
|
|
100
|
+
}
|
|
101
|
+
state.status = { _tag: 'running', turnCount: 0 };
|
|
102
|
+
yield* agentLoop(state, deps);
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
if (deps.hookRunner) {
|
|
106
|
+
try {
|
|
107
|
+
await deps.hookRunner.fireHooks('agent:end', {});
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
// fireHooks 内部已吞 handler 异常;双保险
|
|
111
|
+
}
|
|
112
|
+
}
|
|
88
113
|
}
|
|
89
|
-
state.status = { _tag: 'running', turnCount: 0 };
|
|
90
|
-
yield* agentLoop(state, deps);
|
|
91
114
|
}
|
|
92
115
|
function pauseAgent(state, reason) {
|
|
93
116
|
if (state.status._tag !== 'running')
|
|
@@ -19,8 +19,10 @@ async function mapWithConcurrencyLimit(items, concurrency, fn, signal) {
|
|
|
19
19
|
if (idx >= items.length)
|
|
20
20
|
return;
|
|
21
21
|
const item = items[idx];
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
// 退化元素(稀疏数组空洞 / 显式 undefined)是合法入参:照常交给 fn,
|
|
23
|
+
// 由 fn 决定如何映射——此前遇 undefined 直接 return,该 worker 永久
|
|
24
|
+
// 退出,并发为 1 时其后全部任务静默跳过(results 留洞,上层把被跳过
|
|
25
|
+
// 的任务误报为「run aborted 未启动」)。
|
|
24
26
|
results[idx] = await fn(item, idx, workerSignal);
|
|
25
27
|
}
|
|
26
28
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/** 段是否为原型链键(供合并路径跳过自持的 `__proto__` 键)。 */
|
|
2
|
+
declare function isPrototypeKey(key: string): boolean;
|
|
1
3
|
/** 按 a.b.c 点路径从对象取值;找不到抛错。 */
|
|
2
4
|
declare function getByPath(obj: unknown, path: string): unknown;
|
|
3
5
|
/** 按 a.b.c 点路径写值(创建中间对象)。 */
|
|
@@ -6,4 +8,4 @@ declare function setByPath(obj: Record<string, unknown>, path: string, value: un
|
|
|
6
8
|
declare function coerce(value: string): unknown;
|
|
7
9
|
/** 构造 a.b.c → { a: { b: { c: value } } } 的 patch 对象。 */
|
|
8
10
|
declare function setPathPatch(path: string, value: unknown): Record<string, unknown>;
|
|
9
|
-
export { coerce, getByPath, setByPath, setPathPatch };
|
|
11
|
+
export { coerce, getByPath, isPrototypeKey, setByPath, setPathPatch };
|
package/dist/core/config-path.js
CHANGED
|
@@ -1,9 +1,28 @@
|
|
|
1
1
|
// 配置点路径(a.b.c)读写工具:CLI `config set/get` 与斜杠 `/config` 共用。
|
|
2
|
+
/**
|
|
3
|
+
* 原型链键:`__proto__` / `constructor` / `prototype`。它们不可能是合法配置键
|
|
4
|
+
* (KNOWN_CONFIG_KEYS 不含),但点路径(用户/模型可控:`/config` 斜杠命令对点路径
|
|
5
|
+
* 无顶层键校验)与 JSON 配置文件键都可能携带,经普通赋值即改写原型链:
|
|
6
|
+
* setByPath 沿 `__proto__` 段继续下钻拿到的是 Object.prototype,末段直接写进
|
|
7
|
+
* 全局原型——进程内所有对象/数组凭空多出该属性(回显「已设置」,JSON.stringify
|
|
8
|
+
* 落盘时又静默丢弃)。写入/读取一律显式拒绝。
|
|
9
|
+
*/
|
|
10
|
+
const PROTOTYPE_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
11
|
+
/** 段是否为原型链键(供合并路径跳过自持的 `__proto__` 键)。 */
|
|
12
|
+
function isPrototypeKey(key) {
|
|
13
|
+
return PROTOTYPE_KEYS.has(key);
|
|
14
|
+
}
|
|
15
|
+
function assertNoPrototypeKey(path, segment) {
|
|
16
|
+
if (isPrototypeKey(segment)) {
|
|
17
|
+
throw new Error(`config: 键路径 "${path}" 含原型链段 "${segment}"——该段会改写原型链而非写入配置,已拒绝`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
2
20
|
/** 按 a.b.c 点路径从对象取值;找不到抛错。 */
|
|
3
21
|
function getByPath(obj, path) {
|
|
4
22
|
const parts = path.split('.');
|
|
5
23
|
let cur = obj;
|
|
6
24
|
for (const p of parts) {
|
|
25
|
+
assertNoPrototypeKey(path, p);
|
|
7
26
|
if (cur === null || typeof cur !== 'object')
|
|
8
27
|
throw new Error(`config: key "${path}" not found`);
|
|
9
28
|
cur = cur[p];
|
|
@@ -18,11 +37,14 @@ function setByPath(obj, path, value) {
|
|
|
18
37
|
let cur = obj;
|
|
19
38
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
20
39
|
const k = parts[i];
|
|
40
|
+
assertNoPrototypeKey(path, k);
|
|
21
41
|
const next = cur[k];
|
|
22
42
|
cur[k] = (next !== null && typeof next === 'object' ? next : {});
|
|
23
43
|
cur = cur[k];
|
|
24
44
|
}
|
|
25
|
-
|
|
45
|
+
const leaf = parts[parts.length - 1];
|
|
46
|
+
assertNoPrototypeKey(path, leaf);
|
|
47
|
+
cur[leaf] = value;
|
|
26
48
|
}
|
|
27
49
|
/** 把字符串解析为合适的标量类型。 */
|
|
28
50
|
function coerce(value) {
|
|
@@ -32,12 +54,20 @@ function coerce(value) {
|
|
|
32
54
|
return false;
|
|
33
55
|
if (/^-?\d+(\.\d+)?$/.test(value))
|
|
34
56
|
return Number(value);
|
|
57
|
+
let parsed;
|
|
35
58
|
try {
|
|
36
|
-
|
|
59
|
+
parsed = JSON.parse(value);
|
|
37
60
|
}
|
|
38
61
|
catch {
|
|
39
62
|
return value;
|
|
40
63
|
}
|
|
64
|
+
// JSON 数值文法允许 1e999——解析为 Infinity,JSON.stringify 落盘为 null:
|
|
65
|
+
// 「设置超大预算/超长间隔」变成「静默删除该键」(预算护栏关闭)。
|
|
66
|
+
// 显式拒绝,让 CLI /config 斜杠命令把错误反馈给用户/模型。
|
|
67
|
+
if (typeof parsed === 'number' && !Number.isFinite(parsed)) {
|
|
68
|
+
throw new Error(`config: 值 "${value}" 是非有限数值(JSON 无法表示,拒绝写入)`);
|
|
69
|
+
}
|
|
70
|
+
return parsed;
|
|
41
71
|
}
|
|
42
72
|
/** 构造 a.b.c → { a: { b: { c: value } } } 的 patch 对象。 */
|
|
43
73
|
function setPathPatch(path, value) {
|
|
@@ -45,4 +75,4 @@ function setPathPatch(path, value) {
|
|
|
45
75
|
setByPath(root, path, value);
|
|
46
76
|
return root;
|
|
47
77
|
}
|
|
48
|
-
export { coerce, getByPath, setByPath, setPathPatch };
|
|
78
|
+
export { coerce, getByPath, isPrototypeKey, setByPath, setPathPatch };
|
package/dist/core/config.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { isAbsolute, join, resolve } from 'node:path';
|
|
4
|
+
import { isPrototypeKey } from './config-path.js';
|
|
4
5
|
import { encryptSecret, isEncryptedSecret } from './secret.js';
|
|
5
6
|
const GLOBAL_CONFIG_DIR = '.c0de';
|
|
6
7
|
const CONFIG_FILENAME = 'config.json';
|
|
@@ -58,6 +59,10 @@ function mergeConfig(...configs) {
|
|
|
58
59
|
if (!cfg)
|
|
59
60
|
continue;
|
|
60
61
|
for (const key of Object.keys(cfg)) {
|
|
62
|
+
// 自持的原型链键(配置文件里手写/克隆仓库自带的 `"__proto__"`):赋值会改写
|
|
63
|
+
// 合并结果的原型而非写入键——合并视图凭空多出继承属性,且不可枚举/不可落盘。
|
|
64
|
+
if (isPrototypeKey(key))
|
|
65
|
+
continue;
|
|
61
66
|
const val = cfg[key];
|
|
62
67
|
if (val === undefined)
|
|
63
68
|
continue;
|
|
@@ -179,6 +184,68 @@ function loadConfigScopes(projectDir) {
|
|
|
179
184
|
project: stripProjectServerKeys(readJsonIfExists(projectPath)),
|
|
180
185
|
};
|
|
181
186
|
}
|
|
187
|
+
/**
|
|
188
|
+
* 递归断言 patch 树中不含非有限数值。JSON 数值文法允许 1e999/1e309——
|
|
189
|
+
* JSON.parse 解析为 ±Infinity,JSON.stringify 落盘为 null:写入通道回显
|
|
190
|
+
* 「已设置」,实际键被静默删除(预算护栏等安全相关键尤其致命)。
|
|
191
|
+
* 三个写入入口(CLI config set、/config 斜杠、REST PATCH)共用本检查,
|
|
192
|
+
* 抛错由各入口转成面向用户/模型的明确错误。
|
|
193
|
+
*/
|
|
194
|
+
function assertFiniteNumbers(value, path) {
|
|
195
|
+
if (typeof value === 'number') {
|
|
196
|
+
if (!Number.isFinite(value)) {
|
|
197
|
+
throw new Error(`${path} 含非有限数值 ${value}(JSON 无法表示,拒绝写入)`);
|
|
198
|
+
}
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (Array.isArray(value)) {
|
|
202
|
+
for (let i = 0; i < value.length; i++) {
|
|
203
|
+
assertFiniteNumbers(value[i], `${path}[${i}]`);
|
|
204
|
+
}
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (value !== null && typeof value === 'object') {
|
|
208
|
+
for (const [k, v] of Object.entries(value)) {
|
|
209
|
+
assertFiniteNumbers(v, `${path}.${k}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* 校验 providers 列表条目形状。providers 是配置写入的唯一「对象数组」字段,
|
|
215
|
+
* 三个写入入口(CLI config set、/config 斜杠、REST PATCH)都经 applyScopedPatch
|
|
216
|
+
* 汇聚——畸形条目此前被原样落盘/透传:
|
|
217
|
+
* - null 条目:REST PATCH 的 apiKey 加密映射处 TypeError 500;已落盘的 null
|
|
218
|
+
* 随后让 registry 重建(启动/同步)读 p.name 抛 TypeError,服务无法启动。
|
|
219
|
+
* - 字符串条目:静默持久化成毒化配置,registry 构建时被跳过——UI 显示已保存,
|
|
220
|
+
* provider 永不可用。
|
|
221
|
+
* - baseURL/apiKey 非字符串:registerProviderFromConfig 对 baseURL 调
|
|
222
|
+
* .replace/.endsWith 抛 TypeError(同步 registry 或下次启动击穿)。
|
|
223
|
+
* 显式拒绝,错误含条目下标供调用方定位。name 兼容注册表读取侧的 _tag 回退
|
|
224
|
+
* (旧版 config.json 格式),但两者都缺则拒绝。
|
|
225
|
+
*/
|
|
226
|
+
function assertValidProviderList(value, path) {
|
|
227
|
+
if (!Array.isArray(value)) {
|
|
228
|
+
throw new Error(`${path} 必须是 provider 对象数组(${JSON.stringify(value)})`);
|
|
229
|
+
}
|
|
230
|
+
for (let i = 0; i < value.length; i++) {
|
|
231
|
+
const p = value[i];
|
|
232
|
+
if (p === null || typeof p !== 'object' || Array.isArray(p)) {
|
|
233
|
+
throw new Error(`${path}[${i}] 必须是 provider 对象(收到 ${JSON.stringify(p)})`);
|
|
234
|
+
}
|
|
235
|
+
const e = p;
|
|
236
|
+
const name = typeof e.name === 'string' && e.name.length > 0 ? e.name : undefined;
|
|
237
|
+
const tag = typeof e._tag === 'string' && e._tag.length > 0 ? e._tag : undefined;
|
|
238
|
+
if (name === undefined && tag === undefined) {
|
|
239
|
+
throw new Error(`${path}[${i}] 缺少非空字符串 name(收到 ${JSON.stringify(p)})`);
|
|
240
|
+
}
|
|
241
|
+
if (e.baseURL !== undefined && typeof e.baseURL !== 'string') {
|
|
242
|
+
throw new Error(`${path}[${i}].baseURL 必须是字符串(收到 ${JSON.stringify(e.baseURL)})`);
|
|
243
|
+
}
|
|
244
|
+
if (e.apiKey !== undefined && typeof e.apiKey !== 'string') {
|
|
245
|
+
throw new Error(`${path}[${i}].apiKey 必须是字符串(收到 ${JSON.stringify(e.apiKey)})`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
182
249
|
/**
|
|
183
250
|
* 把 patch 应用到某个作用域的原始配置(scoped patch,null=删除):
|
|
184
251
|
* - 深合并:嵌套普通对象递归合并,数组整体替换(providers 等列表语义);
|
|
@@ -188,14 +255,23 @@ function loadConfigScopes(projectDir) {
|
|
|
188
255
|
* 不注入默认值。CLI config set / 服务端 PATCH /api/config 共用。
|
|
189
256
|
*/
|
|
190
257
|
function applyScopedPatch(base, patch) {
|
|
258
|
+
assertFiniteNumbers(patch, 'config');
|
|
191
259
|
const result = { ...(base ?? {}) };
|
|
192
260
|
for (const [key, val] of Object.entries(patch)) {
|
|
261
|
+
// 自持的原型链键(JSON.parse 的 `"__proto__"` 会建成自有键)经普通赋值改写
|
|
262
|
+
// result 的原型而非写入键:配置读起来像「没写」,键又被静默丢弃。跳过。
|
|
263
|
+
if (isPrototypeKey(key))
|
|
264
|
+
continue;
|
|
193
265
|
if (val === undefined)
|
|
194
266
|
continue;
|
|
195
267
|
if (val === null) {
|
|
196
268
|
delete result[key];
|
|
197
269
|
continue;
|
|
198
270
|
}
|
|
271
|
+
// providers 条目形状校验(唯一对象数组字段;畸形条目落盘即毒化配置)。
|
|
272
|
+
if (key === 'providers') {
|
|
273
|
+
assertValidProviderList(val, 'config.providers');
|
|
274
|
+
}
|
|
199
275
|
const current = result[key];
|
|
200
276
|
if (typeof val === 'object' &&
|
|
201
277
|
!Array.isArray(val) &&
|
|
@@ -221,6 +297,8 @@ function mergeRaw(...cfgs) {
|
|
|
221
297
|
if (!cfg)
|
|
222
298
|
continue;
|
|
223
299
|
for (const [key, val] of Object.entries(cfg)) {
|
|
300
|
+
if (isPrototypeKey(key))
|
|
301
|
+
continue;
|
|
224
302
|
if (val === undefined)
|
|
225
303
|
continue;
|
|
226
304
|
const current = result[key];
|
|
@@ -263,6 +341,9 @@ async function saveConfigScoped(scope, projectDir, data) {
|
|
|
263
341
|
function redactSensitiveOnSave(data) {
|
|
264
342
|
const out = {};
|
|
265
343
|
for (const [key, val] of Object.entries(data)) {
|
|
344
|
+
// 原型链键不落盘(赋值会改写 out 的原型,键本身也不会被序列化)。
|
|
345
|
+
if (isPrototypeKey(key))
|
|
346
|
+
continue;
|
|
266
347
|
if (key === 'providers' && Array.isArray(val)) {
|
|
267
348
|
out[key] = val.map((p) => {
|
|
268
349
|
if (p === null || typeof p !== 'object')
|
package/dist/core/context.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { estimateMessageTokens } from '../session/token.js';
|
|
2
2
|
// Token 预算分配(spec §3.6 策略1):
|
|
3
3
|
// reserved 20% — system prompt + 工具描述
|
|
4
4
|
// historyBudget 60% — 历史消息上限
|
|
@@ -21,11 +21,14 @@ function createTokenBudget(totalTokens, opts) {
|
|
|
21
21
|
keepRecent: opts?.keepRecent ?? Math.floor(totalTokens * KEEP_RECENT_RATIO),
|
|
22
22
|
};
|
|
23
23
|
}
|
|
24
|
-
/** 单条消息的原始 token 估算(不应用校准系数)。
|
|
24
|
+
/** 单条消息的原始 token 估算(不应用校准系数)。
|
|
25
|
+
* 全 content 部分口径(text/thinking/steering/tool_call/tool_result)——
|
|
26
|
+
* 此前只统计 text/thinking,纯工具消息按 0 token 计、预算与压缩判定脱节;
|
|
27
|
+
* 与 compaction 的 messageTokens(estimateMessageTokens)同源同口径。 */
|
|
25
28
|
function rawMessageTokens(m) {
|
|
26
29
|
if (m.tokenCount > 0)
|
|
27
30
|
return m.tokenCount;
|
|
28
|
-
return
|
|
31
|
+
return estimateMessageTokens(m.content);
|
|
29
32
|
}
|
|
30
33
|
function estimateBudget(messages, factor = 1.0) {
|
|
31
34
|
const raw = messages.reduce((sum, m) => sum + rawMessageTokens(m), 0);
|
|
@@ -2,7 +2,7 @@ import { createKanbanStore } from '../../kanban/index.js';
|
|
|
2
2
|
import { appendMessage } from '../../session/message.js';
|
|
3
3
|
import { inferToolMode, recordToolMetrics } from '../metrics.js';
|
|
4
4
|
import { executeToolCalls } from '../tool-exec.js';
|
|
5
|
-
import { runSubAgent } from './subagent.js';
|
|
5
|
+
import { runSubAgent, runSubAgents } from './subagent.js';
|
|
6
6
|
function toolResultToContent(toolCallId, toolName, result) {
|
|
7
7
|
return [{ _tag: 'tool_result', id: toolCallId, tool: toolName, output: result }];
|
|
8
8
|
}
|
|
@@ -64,6 +64,9 @@ export async function* persistAssistantAndTools(state, deps, collectedText, vali
|
|
|
64
64
|
...(deps.urlRegistry ? { urlRegistry: deps.urlRegistry } : {}),
|
|
65
65
|
...(deps.debugSpawn ? { debugSpawn: deps.debugSpawn } : {}),
|
|
66
66
|
runSubAgent: (req) => runSubAgent({ ...deps, _subagentEventSink: eventSink }, state, req),
|
|
67
|
+
// task 工具批量模式:宿主批量入口按 config.agents.subagentConcurrency 建并发池
|
|
68
|
+
// (与工作流 ctx.runSubagents 共用同一实现),子 agent 事件同走 eventSink。
|
|
69
|
+
runSubAgents: (reqs) => runSubAgents({ ...deps, _subagentEventSink: eventSink }, state, reqs),
|
|
67
70
|
...(deps._subagentYieldCollector ? { collectYield: deps._subagentYieldCollector } : {}),
|
|
68
71
|
// todo 工具状态通过 dependency-reversal hook 注入:get/set 直接读写
|
|
69
72
|
// state.todoPhases(in-memory),tool result 的 metadata.phases 充当
|
|
@@ -10,3 +10,22 @@ import type { LoopDeps } from '../loop.js';
|
|
|
10
10
|
* def.isolated 时在 git worktree 中运行,结束后把 delta 自动 apply 回父仓库(spec §4.6)。
|
|
11
11
|
* request.background 时 fork 异步运行,立即返回 running(spec §4.7)。 */
|
|
12
12
|
export declare function runSubAgent(deps: LoopDeps, parent: AgentState, request: SubAgentRequest): Promise<SubAgentResult>;
|
|
13
|
+
/** 归一化子 agent 并发上限:缺失/非有限/<1 回落默认 3,上限钳到 10。
|
|
14
|
+
* 与 ToolDef.timeout / clampTimerDelay 同口径——配置面的退化值不得让派发失控
|
|
15
|
+
* (0 或负数会让并发池退化成「一个都不跑」)。 */
|
|
16
|
+
export declare function normalizeSubagentConcurrency(value: number | undefined): number;
|
|
17
|
+
/** 单任务派发函数(批量派发的注入点:工作流宿主自带 runner 时无需经 deps 组装)。 */
|
|
18
|
+
type SubAgentRunner = (request: SubAgentRequest) => Promise<SubAgentResult>;
|
|
19
|
+
/**
|
|
20
|
+
* 批量派发子 agent(task 工具批量模式 / 工作流 ctx.runSubagents 共用)。
|
|
21
|
+
*
|
|
22
|
+
* - 并发上限取 config.agents.subagentConcurrency(默认 3)——此前 task 工具批量模式
|
|
23
|
+
* 逐个 await(墙钟时间线性叠加,与工具描述的 "Launch multiple agents concurrently"
|
|
24
|
+
* 相悖),工作流侧则硬编码 3(配置项形同虚设)。
|
|
25
|
+
* - 结果与入参顺序一一对应,绝不出现空洞:未启动的任务(abort)填 error 结果,
|
|
26
|
+
* 调用方无需再处理 undefined。
|
|
27
|
+
* - 单个任务失败隔离(抛错/error 结果都收敛为 error),不 fail-fast 终止兄弟任务。
|
|
28
|
+
* - abort(父 run 中止)取消尚未启动的任务。
|
|
29
|
+
*/
|
|
30
|
+
export declare function runSubAgents(deps: LoopDeps, parent: AgentState, requests: SubAgentRequest[], runOne?: SubAgentRunner): Promise<SubAgentResult[]>;
|
|
31
|
+
export {};
|