c0de-agent 0.0.0 → 1.0.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/LICENSE +21 -0
- package/README.md +101 -0
- package/dist/cli/commands/acp.d.ts +19 -0
- package/dist/cli/commands/acp.js +50 -0
- package/dist/cli/commands/chat.d.ts +13 -0
- package/dist/cli/commands/chat.js +26 -0
- package/dist/cli/commands/config.d.ts +17 -0
- package/dist/cli/commands/config.js +77 -0
- package/dist/cli/commands/init.d.ts +9 -0
- package/dist/cli/commands/init.js +14 -0
- package/dist/cli/commands/plugin.d.ts +11 -0
- package/dist/cli/commands/plugin.js +31 -0
- package/dist/cli/commands/serve.d.ts +18 -0
- package/dist/cli/commands/serve.js +35 -0
- package/dist/cli/commands/update.d.ts +24 -0
- package/dist/cli/commands/update.js +40 -0
- package/dist/cli/deps.d.ts +19 -0
- package/dist/cli/deps.js +57 -0
- package/dist/cli/index.d.ts +8 -0
- package/dist/cli/index.js +149 -0
- package/dist/cli/modes/acp.d.ts +18 -0
- package/dist/cli/modes/acp.js +57 -0
- package/dist/cli/modes/print.d.ts +16 -0
- package/dist/cli/modes/print.js +30 -0
- package/dist/cli/parser.d.ts +19 -0
- package/dist/cli/parser.js +28 -0
- package/dist/cli/utils/output.d.ts +11 -0
- package/dist/cli/utils/output.js +23 -0
- package/dist/cli/utils/prompt.d.ts +12 -0
- package/dist/cli/utils/prompt.js +18 -0
- package/dist/core/agent.d.ts +12 -0
- package/dist/core/agent.js +95 -0
- package/dist/core/agents/builtin.d.ts +6 -0
- package/dist/core/agents/builtin.js +76 -0
- package/dist/core/agents/discovery.d.ts +6 -0
- package/dist/core/agents/discovery.js +96 -0
- package/dist/core/agents/index.d.ts +6 -0
- package/dist/core/agents/index.js +4 -0
- package/dist/core/agents/parallel.d.ts +13 -0
- package/dist/core/agents/parallel.js +37 -0
- package/dist/core/agents/registry.d.ts +4 -0
- package/dist/core/agents/registry.js +23 -0
- package/dist/core/agents/types.d.ts +37 -0
- package/dist/core/agents/types.js +1 -0
- package/dist/core/compact.d.ts +11 -0
- package/dist/core/compact.js +31 -0
- package/dist/core/config.d.ts +7 -0
- package/dist/core/config.js +84 -0
- package/dist/core/context.d.ts +23 -0
- package/dist/core/context.js +93 -0
- package/dist/core/index.d.ts +13 -0
- package/dist/core/index.js +11 -0
- package/dist/core/loop.d.ts +25 -0
- package/dist/core/loop.js +651 -0
- package/dist/core/metrics.d.ts +27 -0
- package/dist/core/metrics.js +94 -0
- package/dist/core/prompt-registry.d.ts +21 -0
- package/dist/core/prompt-registry.js +200 -0
- package/dist/core/prompt.d.ts +11 -0
- package/dist/core/prompt.js +13 -0
- package/dist/core/secret.d.ts +12 -0
- package/dist/core/secret.js +51 -0
- package/dist/core/slash.d.ts +15 -0
- package/dist/core/slash.js +114 -0
- package/dist/core/steering.d.ts +8 -0
- package/dist/core/steering.js +15 -0
- package/dist/core/title.d.ts +42 -0
- package/dist/core/title.js +115 -0
- package/dist/core/tool-exec.d.ts +20 -0
- package/dist/core/tool-exec.js +64 -0
- package/dist/core/types.d.ts +95 -0
- package/dist/core/types.js +1 -0
- package/dist/core/worktree.d.ts +24 -0
- package/dist/core/worktree.js +68 -0
- package/dist/dap/index.d.ts +6 -0
- package/dist/dap/index.js +4 -0
- package/dist/dap/protocol.d.ts +40 -0
- package/dist/dap/protocol.js +132 -0
- package/dist/dap/session.d.ts +23 -0
- package/dist/dap/session.js +130 -0
- package/dist/dap/transport.d.ts +8 -0
- package/dist/dap/transport.js +30 -0
- package/dist/dap/types.d.ts +50 -0
- package/dist/dap/types.js +2 -0
- package/dist/db/client.d.ts +22 -0
- package/dist/db/client.js +39 -0
- package/dist/db/index.d.ts +6 -0
- package/dist/db/index.js +4 -0
- package/dist/db/migrate.d.ts +9 -0
- package/dist/db/migrate.js +15 -0
- package/dist/db/schema.d.ts +965 -0
- package/dist/db/schema.js +112 -0
- package/dist/db/types.d.ts +11 -0
- package/dist/db/types.js +1 -0
- package/dist/llm/index.d.ts +13 -0
- package/dist/llm/index.js +8 -0
- package/dist/llm/protocols/openai-compat.d.ts +139 -0
- package/dist/llm/protocols/openai-compat.js +214 -0
- package/dist/llm/protocols/utils/index.d.ts +2 -0
- package/dist/llm/protocols/utils/index.js +2 -0
- package/dist/llm/protocols/utils/lifecycle.d.ts +23 -0
- package/dist/llm/protocols/utils/lifecycle.js +46 -0
- package/dist/llm/protocols/utils/tool-stream.d.ts +46 -0
- package/dist/llm/protocols/utils/tool-stream.js +94 -0
- package/dist/llm/provider-error.d.ts +5 -0
- package/dist/llm/provider-error.js +33 -0
- package/dist/llm/provider.d.ts +32 -0
- package/dist/llm/provider.js +191 -0
- package/dist/llm/registry.d.ts +44 -0
- package/dist/llm/registry.js +107 -0
- package/dist/llm/retry.d.ts +37 -0
- package/dist/llm/retry.js +82 -0
- package/dist/llm/routing.d.ts +36 -0
- package/dist/llm/routing.js +53 -0
- package/dist/llm/schema/errors.d.ts +88 -0
- package/dist/llm/schema/errors.js +33 -0
- package/dist/llm/schema/events.d.ts +135 -0
- package/dist/llm/schema/events.js +35 -0
- package/dist/llm/schema/ids.d.ts +22 -0
- package/dist/llm/schema/ids.js +3 -0
- package/dist/llm/schema/index.d.ts +5 -0
- package/dist/llm/schema/index.js +5 -0
- package/dist/llm/schema/messages.d.ts +97 -0
- package/dist/llm/schema/messages.js +18 -0
- package/dist/llm/schema/options.d.ts +54 -0
- package/dist/llm/schema/options.js +54 -0
- package/dist/llm/token.d.ts +2 -0
- package/dist/llm/token.js +13 -0
- package/dist/llm/transport.d.ts +31 -0
- package/dist/llm/transport.js +165 -0
- package/dist/mcp/index.d.ts +1 -0
- package/dist/mcp/index.js +3 -0
- package/dist/plugins/builtin.d.ts +6 -0
- package/dist/plugins/builtin.js +54 -0
- package/dist/plugins/hooks.d.ts +3 -0
- package/dist/plugins/hooks.js +79 -0
- package/dist/plugins/index.d.ts +9 -0
- package/dist/plugins/index.js +8 -0
- package/dist/plugins/init.d.ts +23 -0
- package/dist/plugins/init.js +30 -0
- package/dist/plugins/lifecycle.d.ts +6 -0
- package/dist/plugins/lifecycle.js +74 -0
- package/dist/plugins/loader.d.ts +16 -0
- package/dist/plugins/loader.js +68 -0
- package/dist/plugins/logger.d.ts +4 -0
- package/dist/plugins/logger.js +31 -0
- package/dist/plugins/registry.d.ts +7 -0
- package/dist/plugins/registry.js +24 -0
- package/dist/plugins/types.d.ts +113 -0
- package/dist/plugins/types.js +1 -0
- package/dist/project/detect.d.ts +4 -0
- package/dist/project/detect.js +131 -0
- package/dist/project/index.d.ts +4 -0
- package/dist/project/index.js +2 -0
- package/dist/project/project.d.ts +23 -0
- package/dist/project/project.js +87 -0
- package/dist/project/resolve.d.ts +8 -0
- package/dist/project/resolve.js +81 -0
- package/dist/server/agent-manager.d.ts +32 -0
- package/dist/server/agent-manager.js +54 -0
- package/dist/server/app.d.ts +5 -0
- package/dist/server/app.js +75 -0
- package/dist/server/context.d.ts +20 -0
- package/dist/server/context.js +47 -0
- package/dist/server/dev.d.ts +11 -0
- package/dist/server/dev.js +83 -0
- package/dist/server/index.d.ts +11 -0
- package/dist/server/index.js +7 -0
- package/dist/server/middleware/auth.d.ts +15 -0
- package/dist/server/middleware/auth.js +29 -0
- package/dist/server/middleware/cors.d.ts +16 -0
- package/dist/server/middleware/cors.js +44 -0
- package/dist/server/middleware/error.d.ts +15 -0
- package/dist/server/middleware/error.js +11 -0
- package/dist/server/permission/interactive.d.ts +24 -0
- package/dist/server/permission/interactive.js +56 -0
- package/dist/server/permission/store.d.ts +30 -0
- package/dist/server/permission/store.js +47 -0
- package/dist/server/routes/agent.d.ts +4 -0
- package/dist/server/routes/agent.js +19 -0
- package/dist/server/routes/catalog.d.ts +42 -0
- package/dist/server/routes/catalog.js +194 -0
- package/dist/server/routes/chat.d.ts +8 -0
- package/dist/server/routes/chat.js +282 -0
- package/dist/server/routes/commands.d.ts +5 -0
- package/dist/server/routes/commands.js +17 -0
- package/dist/server/routes/config.d.ts +4 -0
- package/dist/server/routes/config.js +27 -0
- package/dist/server/routes/files.d.ts +4 -0
- package/dist/server/routes/files.js +183 -0
- package/dist/server/routes/filesystem.d.ts +29 -0
- package/dist/server/routes/filesystem.js +108 -0
- package/dist/server/routes/health.d.ts +3 -0
- package/dist/server/routes/health.js +13 -0
- package/dist/server/routes/permissions.d.ts +5 -0
- package/dist/server/routes/permissions.js +23 -0
- package/dist/server/routes/project.d.ts +4 -0
- package/dist/server/routes/project.js +51 -0
- package/dist/server/routes/provider.d.ts +16 -0
- package/dist/server/routes/provider.js +66 -0
- package/dist/server/routes/session.d.ts +4 -0
- package/dist/server/routes/session.js +130 -0
- package/dist/server/routes/tool.d.ts +4 -0
- package/dist/server/routes/tool.js +28 -0
- package/dist/server/routes/update.d.ts +14 -0
- package/dist/server/routes/update.js +49 -0
- package/dist/server/server.d.ts +43 -0
- package/dist/server/server.js +197 -0
- package/dist/server/types.d.ts +73 -0
- package/dist/server/types.js +2 -0
- package/dist/server/util/safe-path.d.ts +7 -0
- package/dist/server/util/safe-path.js +7 -0
- package/dist/session/archive.d.ts +15 -0
- package/dist/session/archive.js +129 -0
- package/dist/session/branch.d.ts +9 -0
- package/dist/session/branch.js +63 -0
- package/dist/session/compaction.d.ts +27 -0
- package/dist/session/compaction.js +192 -0
- package/dist/session/context.d.ts +16 -0
- package/dist/session/context.js +173 -0
- package/dist/session/index.d.ts +10 -0
- package/dist/session/index.js +10 -0
- package/dist/session/message.d.ts +22 -0
- package/dist/session/message.js +145 -0
- package/dist/session/session.d.ts +37 -0
- package/dist/session/session.js +144 -0
- package/dist/session/snapshot.d.ts +11 -0
- package/dist/session/snapshot.js +60 -0
- package/dist/session/squash.d.ts +9 -0
- package/dist/session/squash.js +88 -0
- package/dist/session/token.d.ts +10 -0
- package/dist/session/token.js +33 -0
- package/dist/session/types.d.ts +122 -0
- package/dist/session/types.js +1 -0
- package/dist/shared/index.d.ts +2 -0
- package/dist/shared/index.js +1 -0
- package/dist/shared/types/agent.d.ts +202 -0
- package/dist/shared/types/agent.js +1 -0
- package/dist/shared/types/base.d.ts +32 -0
- package/dist/shared/types/base.js +1 -0
- package/dist/shared/types/config.d.ts +104 -0
- package/dist/shared/types/config.js +1 -0
- package/dist/shared/types/index.d.ts +6 -0
- package/dist/shared/types/index.js +1 -0
- package/dist/shared/types/llm.d.ts +122 -0
- package/dist/shared/types/llm.js +1 -0
- package/dist/shared/types/message.d.ts +71 -0
- package/dist/shared/types/message.js +1 -0
- package/dist/shared/types/tool.d.ts +134 -0
- package/dist/shared/types/tool.js +1 -0
- package/dist/shared/utils/path.d.ts +15 -0
- package/dist/shared/utils/path.js +23 -0
- package/dist/shared/utils.d.ts +5 -0
- package/dist/shared/utils.js +10 -0
- package/dist/tools/builtin/bash.d.ts +12 -0
- package/dist/tools/builtin/bash.js +121 -0
- package/dist/tools/builtin/dap.d.ts +23 -0
- package/dist/tools/builtin/dap.js +231 -0
- package/dist/tools/builtin/edit.d.ts +12 -0
- package/dist/tools/builtin/edit.js +151 -0
- package/dist/tools/builtin/glob.d.ts +11 -0
- package/dist/tools/builtin/glob.js +120 -0
- package/dist/tools/builtin/grep.d.ts +6 -0
- package/dist/tools/builtin/grep.js +141 -0
- package/dist/tools/builtin/read.d.ts +6 -0
- package/dist/tools/builtin/read.js +73 -0
- package/dist/tools/builtin/resolvers.d.ts +12 -0
- package/dist/tools/builtin/resolvers.js +79 -0
- package/dist/tools/builtin/task.d.ts +9 -0
- package/dist/tools/builtin/task.js +100 -0
- package/dist/tools/builtin/write.d.ts +6 -0
- package/dist/tools/builtin/write.js +36 -0
- package/dist/tools/builtin/yield.d.ts +9 -0
- package/dist/tools/builtin/yield.js +40 -0
- package/dist/tools/executor.d.ts +9 -0
- package/dist/tools/executor.js +54 -0
- package/dist/tools/hashline/index.d.ts +2 -0
- package/dist/tools/hashline/index.js +3 -0
- package/dist/tools/hashline/patch.d.ts +48 -0
- package/dist/tools/hashline/patch.js +201 -0
- package/dist/tools/index.d.ts +30 -0
- package/dist/tools/index.js +55 -0
- package/dist/tools/permission.d.ts +13 -0
- package/dist/tools/permission.js +39 -0
- package/dist/tools/registry.d.ts +15 -0
- package/dist/tools/registry.js +54 -0
- package/dist/tools/resolver.d.ts +15 -0
- package/dist/tools/resolver.js +47 -0
- package/dist/tools/truncate.d.ts +8 -0
- package/dist/tools/truncate.js +50 -0
- package/dist/tools/types.d.ts +101 -0
- package/dist/tools/types.js +1 -0
- package/dist/tools/validate.d.ts +8 -0
- package/dist/tools/validate.js +130 -0
- package/dist/tools/websearch/fetch.d.ts +11 -0
- package/dist/tools/websearch/fetch.js +21 -0
- package/dist/tools/websearch/index.d.ts +19 -0
- package/dist/tools/websearch/index.js +78 -0
- package/dist/tools/websearch/providers/brave.d.ts +5 -0
- package/dist/tools/websearch/providers/brave.js +53 -0
- package/dist/tools/websearch/providers/duckduckgo.d.ts +3 -0
- package/dist/tools/websearch/providers/duckduckgo.js +75 -0
- package/dist/tools/websearch/providers/tavily.d.ts +11 -0
- package/dist/tools/websearch/providers/tavily.js +89 -0
- package/dist/tools/websearch/types.d.ts +51 -0
- package/dist/tools/websearch/types.js +18 -0
- package/dist/tools/websearch/websearch.d.ts +10 -0
- package/dist/tools/websearch/websearch.js +48 -0
- package/dist/update/index.d.ts +10 -0
- package/dist/update/index.js +6 -0
- package/dist/update/ipc.d.ts +15 -0
- package/dist/update/ipc.js +41 -0
- package/dist/update/scheduler.d.ts +34 -0
- package/dist/update/scheduler.js +57 -0
- package/dist/update/snapshot.d.ts +39 -0
- package/dist/update/snapshot.js +99 -0
- package/dist/update/updater.d.ts +48 -0
- package/dist/update/updater.js +83 -0
- package/dist/update/version.d.ts +18 -0
- package/dist/update/version.js +49 -0
- package/drizzle/0000_magical_nightshade.sql +55 -0
- package/drizzle/0001_chemical_doctor_faustus.sql +13 -0
- package/drizzle/0002_amusing_micromacro.sql +12 -0
- package/drizzle/0003_regular_purifiers.sql +2 -0
- package/drizzle/meta/0000_snapshot.json +448 -0
- package/drizzle/meta/0001_snapshot.json +539 -0
- package/drizzle/meta/0002_snapshot.json +633 -0
- package/drizzle/meta/0003_snapshot.json +645 -0
- package/drizzle/meta/_journal.json +34 -0
- package/package.json +100 -8
|
@@ -0,0 +1,651 @@
|
|
|
1
|
+
import { chatStream as llmChatStream } from '../llm/provider.js';
|
|
2
|
+
import { resolveRoute } from '../llm/registry.js';
|
|
3
|
+
import { isLLMError } from '../llm/schema/errors.js';
|
|
4
|
+
import { detectProjectInfo } from '../project/detect.js';
|
|
5
|
+
import { entriesToChatMessages, getSessionContext } from '../session/context.js';
|
|
6
|
+
import { appendMessage, getMessages } from '../session/message.js';
|
|
7
|
+
import { createSession, saveLLMSegments, segmentFingerprint } from '../session/session.js';
|
|
8
|
+
import { estimateTokens } from '../session/token.js';
|
|
9
|
+
import { generateId } from '../shared/index.js';
|
|
10
|
+
import { createAgent, runAgent } from './agent.js';
|
|
11
|
+
import { createSummarizer, runCompaction } from './compact.js';
|
|
12
|
+
import { calibrateEstimate, estimateBudget, shouldCompact } from './context.js';
|
|
13
|
+
import { DEFAULT_EDIT_MODE, getToolMetrics, inferToolMode, recordToolMetrics, selectBestMode, } from './metrics.js';
|
|
14
|
+
import { buildDynamicPrompt, createPromptRegistry, registerPromptSection, } from './prompt-registry.js';
|
|
15
|
+
import { drainSteering } from './steering.js';
|
|
16
|
+
import { executeToolCalls } from './tool-exec.js';
|
|
17
|
+
import { applyPatchToParent, captureBaseline, captureDeltaPatch, createWorktree, removeWorktree, } from './worktree.js';
|
|
18
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
19
|
+
async function waitForResume(state) {
|
|
20
|
+
while (true) {
|
|
21
|
+
await sleep(100);
|
|
22
|
+
if (state.status._tag !== 'paused')
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** 运行一个按类型派发的子 agent(spec: multi-agent-design §4.5)。
|
|
27
|
+
*
|
|
28
|
+
* Host 端实现:查 agentRegistry 获取 AgentDefinition → 创建隔离子 session(agentType 记录)
|
|
29
|
+
* → 构建子 agent(专属 prompt + 受限工具集 + yield)→ 运行到 yield 或完成 → 返回结果。
|
|
30
|
+
* 发射 subagent_start/subagent_end 事件供父 agent 转发(spec §4.5 step 7)。
|
|
31
|
+
* abort 链接父→子。maxRecursion 控制子 agent 能否再递归派生 task(spec §4.5 step 4)。
|
|
32
|
+
* def.isolated 时在 git worktree 中运行,结束后把 delta 自动 apply 回父仓库(spec §4.6)。
|
|
33
|
+
* request.background 时 fork 异步运行,立即返回 running(spec §4.7)。 */
|
|
34
|
+
export async function runSubAgent(deps, parent, request) {
|
|
35
|
+
// 1. 查 agent 类型
|
|
36
|
+
if (!deps.agentRegistry) {
|
|
37
|
+
return { _tag: 'error', error: 'task tool unavailable: no agent registry is wired' };
|
|
38
|
+
}
|
|
39
|
+
const def = deps.agentRegistry.get(request.agentType);
|
|
40
|
+
if (!def) {
|
|
41
|
+
return {
|
|
42
|
+
_tag: 'error',
|
|
43
|
+
error: `Unknown agent type: ${request.agentType} is not a valid agent type`,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const title = request.description?.trim() ||
|
|
47
|
+
`Sub-agent (${request.agentType}): ${request.prompt.slice(0, 60)}`;
|
|
48
|
+
const childId = generateId();
|
|
49
|
+
const yielded = [];
|
|
50
|
+
// 发射 subagent_start 事件(spec §4.5 step 7)
|
|
51
|
+
deps._subagentEventSink?.({
|
|
52
|
+
_tag: 'subagent_start',
|
|
53
|
+
childId,
|
|
54
|
+
agentType: request.agentType,
|
|
55
|
+
description: request.description ?? '',
|
|
56
|
+
background: request.background ?? false,
|
|
57
|
+
});
|
|
58
|
+
// 2. 创建子 session(记录 agentType)
|
|
59
|
+
let childSession;
|
|
60
|
+
try {
|
|
61
|
+
childSession = await createSession(deps.db, title, parent.session.projectId ?? undefined, request.agentType);
|
|
62
|
+
}
|
|
63
|
+
catch (e) {
|
|
64
|
+
return { _tag: 'error', error: e instanceof Error ? e.message : String(e) };
|
|
65
|
+
}
|
|
66
|
+
// 3. worktree 隔离(isolated agent):失败回退共享 cwd
|
|
67
|
+
let worktreePath;
|
|
68
|
+
let baseline;
|
|
69
|
+
if (def.isolated) {
|
|
70
|
+
try {
|
|
71
|
+
baseline = await captureBaseline(deps.cwd);
|
|
72
|
+
worktreePath = await createWorktree(deps.cwd, `subagent-${childSession.id}`);
|
|
73
|
+
}
|
|
74
|
+
catch (e) {
|
|
75
|
+
console.warn(`[subagent] worktree creation failed, falling back to shared cwd: ${e instanceof Error ? e.message : e}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const childCwd = worktreePath ?? deps.cwd;
|
|
79
|
+
// 实际运行子 agent 的内部函数(sync 与 background 共用)
|
|
80
|
+
const runBody = async () => {
|
|
81
|
+
// 4. 构建子 agent 配置:工具集隔离 + 模型覆盖 + 递归限制 + yield
|
|
82
|
+
const parentDepth = deps._subagentDepth ?? 0;
|
|
83
|
+
const childDepth = parentDepth + 1;
|
|
84
|
+
const declaredTools = def.tools ?? parent.config.tools;
|
|
85
|
+
const maxRec = def.maxRecursion ?? 0;
|
|
86
|
+
const baseTools = childDepth > maxRec ? declaredTools.filter((t) => t !== 'task') : declaredTools;
|
|
87
|
+
const childTools = Array.from(new Set([...baseTools, 'yield']));
|
|
88
|
+
const childConfig = {
|
|
89
|
+
...parent.config,
|
|
90
|
+
systemPrompt: def.systemPrompt,
|
|
91
|
+
// 子 agent 走整段 systemPrompt 替换,清除父的 role override 避免干扰
|
|
92
|
+
agentRolePrompt: undefined,
|
|
93
|
+
tools: childTools,
|
|
94
|
+
...(def.model ? { model: def.model } : {}),
|
|
95
|
+
...(request.model ? { model: request.model } : {}),
|
|
96
|
+
};
|
|
97
|
+
// 子 agent 的 deps:覆盖 cwd(worktree)+ 注入 yield 收集器 + 递归深度
|
|
98
|
+
const childDeps = {
|
|
99
|
+
...deps,
|
|
100
|
+
cwd: childCwd,
|
|
101
|
+
_subagentYieldCollector: (data) => {
|
|
102
|
+
yielded.push(data);
|
|
103
|
+
},
|
|
104
|
+
_subagentDepth: childDepth,
|
|
105
|
+
};
|
|
106
|
+
const childState = await createAgent(childSession, childConfig, childDeps);
|
|
107
|
+
// abort 链接:父 abort 则子 abort
|
|
108
|
+
if (parent.abortController.signal.aborted) {
|
|
109
|
+
childState.abortController.abort();
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
parent.abortController.signal.addEventListener('abort', () => childState.abortController.abort(), { once: true });
|
|
113
|
+
}
|
|
114
|
+
// 运行子 agent loop
|
|
115
|
+
const childPrompt = request.context
|
|
116
|
+
? `CONTEXT\n${request.context}\n\nASSIGNMENT\n${request.prompt}`
|
|
117
|
+
: request.prompt;
|
|
118
|
+
const text = [];
|
|
119
|
+
let errMsg = null;
|
|
120
|
+
try {
|
|
121
|
+
for await (const ev of runAgent(childState, [{ _tag: 'text', text: childPrompt }], childDeps)) {
|
|
122
|
+
if (ev._tag === 'text_delta') {
|
|
123
|
+
text.push(ev.text);
|
|
124
|
+
}
|
|
125
|
+
else if (ev._tag === 'error') {
|
|
126
|
+
const e = ev.error;
|
|
127
|
+
errMsg = e._tag === 'unexpected' || e._tag === 'provider' ? e.message : e._tag;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch (e) {
|
|
132
|
+
errMsg = e instanceof Error ? e.message : String(e);
|
|
133
|
+
}
|
|
134
|
+
// 5. worktree 回传:仅成功时把 delta apply 回父仓库(spec §4.6);无论成败都清理 worktree
|
|
135
|
+
if (baseline && worktreePath) {
|
|
136
|
+
if (errMsg === null) {
|
|
137
|
+
try {
|
|
138
|
+
const patch = await captureDeltaPatch(worktreePath, baseline);
|
|
139
|
+
await applyPatchToParent(deps.cwd, patch, `agent(isolated): ${title}`);
|
|
140
|
+
}
|
|
141
|
+
catch (e) {
|
|
142
|
+
console.warn(`[subagent] worktree apply failed: ${e instanceof Error ? e.message : e}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
removeWorktree(deps.cwd, worktreePath);
|
|
146
|
+
}
|
|
147
|
+
const success = errMsg === null;
|
|
148
|
+
// 发射 subagent_end 事件(spec §4.5 step 7)
|
|
149
|
+
deps._subagentEventSink?.({
|
|
150
|
+
_tag: 'subagent_end',
|
|
151
|
+
childId,
|
|
152
|
+
agentType: request.agentType,
|
|
153
|
+
success,
|
|
154
|
+
...(success ? { output: text.join('') } : {}),
|
|
155
|
+
});
|
|
156
|
+
if (errMsg !== null) {
|
|
157
|
+
return { _tag: 'error', error: errMsg, sessionId: childSession.id };
|
|
158
|
+
}
|
|
159
|
+
const data = yielded.length > 0 ? (yielded.length === 1 ? yielded[0] : yielded) : undefined;
|
|
160
|
+
return {
|
|
161
|
+
_tag: 'success',
|
|
162
|
+
output: text.join(''),
|
|
163
|
+
sessionId: childSession.id,
|
|
164
|
+
...(data !== undefined ? { data } : {}),
|
|
165
|
+
};
|
|
166
|
+
};
|
|
167
|
+
// 6. background 模式:fork 异步运行,立即返回 running;完成时向父 session 注入合成通知
|
|
168
|
+
if (request.background) {
|
|
169
|
+
const jobId = childSession.id;
|
|
170
|
+
void runBody()
|
|
171
|
+
.then((result) => {
|
|
172
|
+
const success = result._tag === 'success';
|
|
173
|
+
const output = success ? result.output : result.error;
|
|
174
|
+
const tag = success ? 'task_result' : 'task_error';
|
|
175
|
+
const synthetic = `<task id="${childSession.id}" state="${success ? 'completed' : 'failed'}">\n<${tag}>\n${output}\n</${tag}>\n</task>`;
|
|
176
|
+
void appendMessage(deps.db, parent.session.id, {
|
|
177
|
+
role: 'user',
|
|
178
|
+
content: [{ _tag: 'text', text: synthetic }],
|
|
179
|
+
}).catch(() => { });
|
|
180
|
+
})
|
|
181
|
+
.catch(() => { });
|
|
182
|
+
return { _tag: 'running', jobId, sessionId: childSession.id };
|
|
183
|
+
}
|
|
184
|
+
return runBody();
|
|
185
|
+
}
|
|
186
|
+
function toolResultToContent(toolCallId, toolName, result) {
|
|
187
|
+
return [{ _tag: 'tool_result', id: toolCallId, tool: toolName, output: result }];
|
|
188
|
+
}
|
|
189
|
+
/** 入参是否为协议层/loop 标记的解析失败(携带 _parseError / _raw 容错标记)。
|
|
190
|
+
* 这类入参是后端专用、只反馈给模型重试的,绝不能进入持久化消息或渲染层。 */
|
|
191
|
+
function isParseErrorInput(input) {
|
|
192
|
+
if (input === null || typeof input !== 'object' || Array.isArray(input))
|
|
193
|
+
return false;
|
|
194
|
+
const obj = input;
|
|
195
|
+
return '_parseError' in obj || '_raw' in obj;
|
|
196
|
+
}
|
|
197
|
+
export async function* agentLoop(state, deps) {
|
|
198
|
+
const maxTurns = state.config.maxTurns ?? 50;
|
|
199
|
+
const streamFn = deps.chatStream ?? llmChatStream;
|
|
200
|
+
for (let turn = 0; turn < maxTurns; turn++) {
|
|
201
|
+
if (state.abortController.signal.aborted) {
|
|
202
|
+
yield { _tag: 'error', error: { _tag: 'aborted' } };
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
// subagent 事件缓冲:runSubAgent 通过 sink 推入事件,executeToolCalls 后 yield 出去
|
|
206
|
+
const subagentEvents = [];
|
|
207
|
+
const eventSink = (ev) => {
|
|
208
|
+
subagentEvents.push(ev);
|
|
209
|
+
};
|
|
210
|
+
if (state.status._tag === 'paused') {
|
|
211
|
+
state.status = { _tag: 'running', turnCount: turn };
|
|
212
|
+
yield { _tag: 'status_change', status: state.status };
|
|
213
|
+
await waitForResume(state);
|
|
214
|
+
if (state.status._tag !== 'running')
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
state.status = { _tag: 'running', turnCount: turn };
|
|
218
|
+
const steering = drainSteering(state);
|
|
219
|
+
const { entries, snapshots } = await getSessionContext(deps.db, state.session.id);
|
|
220
|
+
let chatMessages = entriesToChatMessages(entries, snapshots);
|
|
221
|
+
for (const s of steering) {
|
|
222
|
+
chatMessages.push({ role: 'system', content: s });
|
|
223
|
+
}
|
|
224
|
+
if (deps.hookRunner) {
|
|
225
|
+
const hookResult = await deps.hookRunner.runHooks('message:before', {
|
|
226
|
+
messages: chatMessages,
|
|
227
|
+
});
|
|
228
|
+
if (hookResult === false) {
|
|
229
|
+
yield {
|
|
230
|
+
_tag: 'error',
|
|
231
|
+
error: { _tag: 'unexpected', message: 'Aborted by message:before hook' },
|
|
232
|
+
};
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
chatMessages = hookResult.messages;
|
|
236
|
+
}
|
|
237
|
+
const promptCtx = {
|
|
238
|
+
tools: state.tools,
|
|
239
|
+
config: state.config,
|
|
240
|
+
projectInfo: detectProjectInfo(deps.cwd),
|
|
241
|
+
skills: [],
|
|
242
|
+
};
|
|
243
|
+
// spec §16.5:edit 工具模式偏好。仅当该轮启用 edit 且某模式历史成功率
|
|
244
|
+
// 显著优于默认时,向 system prompt 追加偏好提示,引导模型选择高成功率模式。
|
|
245
|
+
let modeHint = '';
|
|
246
|
+
if (deps.config.toolMetrics.enabled) {
|
|
247
|
+
const hasEdit = state.tools.some((t) => t.name === 'edit');
|
|
248
|
+
if (hasEdit) {
|
|
249
|
+
try {
|
|
250
|
+
const ms = await getToolMetrics(deps.db, state.config.model, 'edit');
|
|
251
|
+
const best = selectBestMode(ms, DEFAULT_EDIT_MODE, {
|
|
252
|
+
threshold: deps.config.toolMetrics.threshold,
|
|
253
|
+
minSamples: deps.config.toolMetrics.minSamples,
|
|
254
|
+
});
|
|
255
|
+
if (best !== DEFAULT_EDIT_MODE) {
|
|
256
|
+
modeHint = `\n\n# Tool-mode preference\nFor the \`edit\` tool prefer the \`${best}\` mode (content-hash-anchored patch) — your historical success rate is highest with it for this model.`;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
// metrics 查询失败非致命,跳过偏好注入
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
// primary agent 的 role prompt 覆盖 role section(保留工具/项目等动态段)。
|
|
265
|
+
// 仅当未走 config.systemPrompt 整段替换时生效。
|
|
266
|
+
const baseReg = deps.promptRegistry ?? createPromptRegistry();
|
|
267
|
+
if (state.config.agentRolePrompt) {
|
|
268
|
+
registerPromptSection(baseReg, {
|
|
269
|
+
id: 'role',
|
|
270
|
+
content: state.config.agentRolePrompt,
|
|
271
|
+
priority: 0,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
const systemPrompt = (state.config.systemPrompt ?? buildDynamicPrompt(baseReg, promptCtx)) + modeHint;
|
|
275
|
+
const tools = state.tools.map((t) => ({
|
|
276
|
+
name: t.name,
|
|
277
|
+
description: t.description,
|
|
278
|
+
parameters: t.parameters,
|
|
279
|
+
}));
|
|
280
|
+
let request = {
|
|
281
|
+
model: state.config.model,
|
|
282
|
+
messages: chatMessages,
|
|
283
|
+
tools: tools.length > 0 ? tools : undefined,
|
|
284
|
+
stream: true,
|
|
285
|
+
system: systemPrompt,
|
|
286
|
+
...(state.config.maxTokens !== undefined ? { maxTokens: state.config.maxTokens } : {}),
|
|
287
|
+
...(state.config.temperature !== undefined ? { temperature: state.config.temperature } : {}),
|
|
288
|
+
};
|
|
289
|
+
if (deps.hookRunner) {
|
|
290
|
+
const hookResult = await deps.hookRunner.runHooks('provider:before', { request });
|
|
291
|
+
if (hookResult === false) {
|
|
292
|
+
yield {
|
|
293
|
+
_tag: 'error',
|
|
294
|
+
error: { _tag: 'unexpected', message: 'Aborted by provider:before hook' },
|
|
295
|
+
};
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
request = hookResult.request;
|
|
299
|
+
}
|
|
300
|
+
// 总是收集完整 chunk 序列:既供 hookRunner 触发 provider:after,
|
|
301
|
+
// 也是 LLMDetail.responseChunks 的来源(用于调用详情展示)。
|
|
302
|
+
const collectedChunks = [];
|
|
303
|
+
const collectedText = [];
|
|
304
|
+
const collectedThinking = [];
|
|
305
|
+
const collectedToolCalls = new Map();
|
|
306
|
+
const toolCallArgs = new Map();
|
|
307
|
+
let collectedUsage = null;
|
|
308
|
+
let hadError = false;
|
|
309
|
+
// 非正常停止原因(length=被 max_tokens 截断, content_filter=被内容过滤)。
|
|
310
|
+
// 若在无 tool_call 的完成分支里仍非 null,说明回答被截断/过滤而非正常说完。
|
|
311
|
+
let truncated = null;
|
|
312
|
+
const requestStartTime = Date.now();
|
|
313
|
+
let firstTokenTime = null;
|
|
314
|
+
try {
|
|
315
|
+
for await (const chunk of streamFn({
|
|
316
|
+
registry: deps.llmRegistry,
|
|
317
|
+
signal: state.abortController.signal,
|
|
318
|
+
}, request, { provider: state.config.provider, model: state.config.model })) {
|
|
319
|
+
if (firstTokenTime === null && chunk._tag !== 'done') {
|
|
320
|
+
firstTokenTime = Date.now();
|
|
321
|
+
}
|
|
322
|
+
collectedChunks.push(chunk);
|
|
323
|
+
if (state.abortController.signal.aborted) {
|
|
324
|
+
yield { _tag: 'error', error: { _tag: 'aborted' } };
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
switch (chunk._tag) {
|
|
328
|
+
case 'text':
|
|
329
|
+
collectedText.push(chunk.text);
|
|
330
|
+
yield { _tag: 'text_delta', text: chunk.text };
|
|
331
|
+
break;
|
|
332
|
+
case 'tool_call_start':
|
|
333
|
+
// 仅登记,不立即发射 AgentEvent。此时入参尚未到达(流式 delta 累积中),
|
|
334
|
+
// 过早发射空入参的 tool_call_start 会让前端渲染 "Glob · " 等半成品卡,
|
|
335
|
+
// 且该 part 的入参之后也不会被纠正。tool_call_start 改在入参解析完成后、
|
|
336
|
+
// 携带真实入参时统一发射(见本轮流结束后的处理)。
|
|
337
|
+
collectedToolCalls.set(chunk.id, {
|
|
338
|
+
id: chunk.id,
|
|
339
|
+
tool: chunk.name,
|
|
340
|
+
input: {},
|
|
341
|
+
});
|
|
342
|
+
toolCallArgs.set(chunk.id, '');
|
|
343
|
+
break;
|
|
344
|
+
case 'tool_call_delta': {
|
|
345
|
+
const existing = toolCallArgs.get(chunk.id) ?? '';
|
|
346
|
+
toolCallArgs.set(chunk.id, existing + chunk.argumentsDelta);
|
|
347
|
+
break;
|
|
348
|
+
}
|
|
349
|
+
case 'tool_call_end': {
|
|
350
|
+
const id = chunk.id;
|
|
351
|
+
const finalArgs = chunk.argumentsFinal ?? toolCallArgs.get(id) ?? '{}';
|
|
352
|
+
let parsed = {};
|
|
353
|
+
try {
|
|
354
|
+
parsed = JSON.parse(finalArgs);
|
|
355
|
+
}
|
|
356
|
+
catch (e) {
|
|
357
|
+
// 与协议层 finishAll 的容错标记保持一致:同时带 _parseError(可读)与 _raw。
|
|
358
|
+
parsed = {
|
|
359
|
+
_parseError: e instanceof Error ? e.message : String(e),
|
|
360
|
+
_raw: finalArgs,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
const tc = collectedToolCalls.get(id);
|
|
364
|
+
if (tc)
|
|
365
|
+
tc.input = parsed;
|
|
366
|
+
break;
|
|
367
|
+
}
|
|
368
|
+
case 'thinking':
|
|
369
|
+
collectedThinking.push(chunk.text);
|
|
370
|
+
yield { _tag: 'thinking', text: chunk.text };
|
|
371
|
+
break;
|
|
372
|
+
case 'usage':
|
|
373
|
+
collectedUsage = {
|
|
374
|
+
inputTokens: chunk.inputTokens,
|
|
375
|
+
outputTokens: chunk.outputTokens,
|
|
376
|
+
cacheRead: chunk.cacheRead,
|
|
377
|
+
};
|
|
378
|
+
yield {
|
|
379
|
+
_tag: 'usage',
|
|
380
|
+
input: chunk.inputTokens,
|
|
381
|
+
output: chunk.outputTokens,
|
|
382
|
+
cacheRead: chunk.cacheRead,
|
|
383
|
+
};
|
|
384
|
+
break;
|
|
385
|
+
case 'done':
|
|
386
|
+
if (chunk.finishReason === 'length' || chunk.finishReason === 'content-filter') {
|
|
387
|
+
truncated = chunk.finishReason;
|
|
388
|
+
}
|
|
389
|
+
break;
|
|
390
|
+
case 'error':
|
|
391
|
+
yield {
|
|
392
|
+
_tag: 'error',
|
|
393
|
+
error: {
|
|
394
|
+
_tag: 'provider',
|
|
395
|
+
message: chunk.error.message,
|
|
396
|
+
retryable: chunk.error.retryable ?? false,
|
|
397
|
+
},
|
|
398
|
+
};
|
|
399
|
+
hadError = true;
|
|
400
|
+
break;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
catch (err) {
|
|
405
|
+
const message = err instanceof Error
|
|
406
|
+
? err.message
|
|
407
|
+
: isLLMError(err)
|
|
408
|
+
? err.message
|
|
409
|
+
: typeof err === 'object' && err !== null && 'message' in err
|
|
410
|
+
? String(err.message)
|
|
411
|
+
: String(err);
|
|
412
|
+
yield {
|
|
413
|
+
_tag: 'error',
|
|
414
|
+
error: {
|
|
415
|
+
_tag: 'unexpected',
|
|
416
|
+
message,
|
|
417
|
+
},
|
|
418
|
+
};
|
|
419
|
+
state.status = {
|
|
420
|
+
_tag: 'stopped',
|
|
421
|
+
reason: 'error',
|
|
422
|
+
error: {
|
|
423
|
+
_tag: 'unexpected',
|
|
424
|
+
message,
|
|
425
|
+
},
|
|
426
|
+
};
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
if (deps.hookRunner) {
|
|
430
|
+
await deps.hookRunner.fireHooks('provider:after', { request, chunks: collectedChunks });
|
|
431
|
+
}
|
|
432
|
+
const totalLatency = Date.now() - requestStartTime;
|
|
433
|
+
let contextWindow;
|
|
434
|
+
let computedCost = 0;
|
|
435
|
+
try {
|
|
436
|
+
const { capabilities } = resolveRoute(deps.llmRegistry, state.config.provider, state.config.model);
|
|
437
|
+
contextWindow = capabilities.contextWindow;
|
|
438
|
+
const inputTokens = collectedUsage?.inputTokens ?? 0;
|
|
439
|
+
const outputTokens = collectedUsage?.outputTokens ?? 0;
|
|
440
|
+
computedCost =
|
|
441
|
+
(inputTokens / 1000) * capabilities.costPer1kInput +
|
|
442
|
+
(outputTokens / 1000) * capabilities.costPer1kOutput;
|
|
443
|
+
}
|
|
444
|
+
catch {
|
|
445
|
+
// provider 未注册或模型未知:保留 contextWindow=undefined、cost=0
|
|
446
|
+
}
|
|
447
|
+
// —— 段管理:判断是否开新段 ——
|
|
448
|
+
const fp = segmentFingerprint(systemPrompt, tools);
|
|
449
|
+
const activeSeg = state.segments[state.segments.length - 1];
|
|
450
|
+
const pendingTrigger = state.pendingSegmentTrigger;
|
|
451
|
+
let trigger;
|
|
452
|
+
if (pendingTrigger) {
|
|
453
|
+
trigger = pendingTrigger;
|
|
454
|
+
state.pendingSegmentTrigger = undefined;
|
|
455
|
+
}
|
|
456
|
+
else if (!activeSeg) {
|
|
457
|
+
trigger = 'initial';
|
|
458
|
+
}
|
|
459
|
+
else if (activeSeg.model !== state.config.model) {
|
|
460
|
+
trigger = 'model_change';
|
|
461
|
+
}
|
|
462
|
+
else if (activeSeg.fingerprint !== fp) {
|
|
463
|
+
trigger = activeSeg.systemPrompt !== systemPrompt ? 'system_prompt_change' : 'tools_change';
|
|
464
|
+
}
|
|
465
|
+
else {
|
|
466
|
+
trigger = 'user_confirmed'; // 占位,实际不会开段
|
|
467
|
+
}
|
|
468
|
+
const needSegment = !!pendingTrigger ||
|
|
469
|
+
!activeSeg ||
|
|
470
|
+
activeSeg.model !== state.config.model ||
|
|
471
|
+
activeSeg.fingerprint !== fp;
|
|
472
|
+
let currentSeg;
|
|
473
|
+
if (!needSegment && activeSeg) {
|
|
474
|
+
currentSeg = activeSeg;
|
|
475
|
+
}
|
|
476
|
+
else {
|
|
477
|
+
currentSeg = {
|
|
478
|
+
id: generateId(),
|
|
479
|
+
fingerprint: fp,
|
|
480
|
+
provider: state.config.provider,
|
|
481
|
+
model: state.config.model,
|
|
482
|
+
systemPrompt,
|
|
483
|
+
tools,
|
|
484
|
+
startedAt: requestStartTime,
|
|
485
|
+
trigger,
|
|
486
|
+
agentName: state.config.agentName,
|
|
487
|
+
...(contextWindow !== undefined ? { contextWindow } : {}),
|
|
488
|
+
calls: [],
|
|
489
|
+
};
|
|
490
|
+
state.segments.push(currentSeg);
|
|
491
|
+
}
|
|
492
|
+
// —— 段内轻量 call ——
|
|
493
|
+
const call = {
|
|
494
|
+
id: generateId(),
|
|
495
|
+
timestamp: requestStartTime,
|
|
496
|
+
usage: {
|
|
497
|
+
input: collectedUsage?.inputTokens ?? 0,
|
|
498
|
+
output: collectedUsage?.outputTokens ?? 0,
|
|
499
|
+
...(collectedUsage?.cacheRead !== undefined ? { cacheRead: collectedUsage.cacheRead } : {}),
|
|
500
|
+
},
|
|
501
|
+
latency: {
|
|
502
|
+
firstToken: firstTokenTime ? firstTokenTime - requestStartTime : totalLatency,
|
|
503
|
+
total: totalLatency,
|
|
504
|
+
},
|
|
505
|
+
cost: computedCost,
|
|
506
|
+
...(collectedThinking.length > 0 ? { thinking: collectedThinking.join('') } : {}),
|
|
507
|
+
responseText: collectedText.join(''),
|
|
508
|
+
...(truncated ? { finishReason: truncated } : {}),
|
|
509
|
+
};
|
|
510
|
+
currentSeg.calls.push(call);
|
|
511
|
+
await saveLLMSegments(deps.db, state.session.id, state.segments);
|
|
512
|
+
yield { _tag: 'llm_detail' };
|
|
513
|
+
if (hadError) {
|
|
514
|
+
state.status = { _tag: 'stopped', reason: 'error' };
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
// 过滤掉无效 tool call(id 或工具名为空)。部分输出不规范的 provider
|
|
518
|
+
// 会把单个 tool call 的 arguments 流式片段拆成多个独立 delta,每片
|
|
519
|
+
// id/name 为空——这些碎片无法执行,且其空 id 在下一轮发回 provider 时
|
|
520
|
+
// 触发 "invalid tool_call_id"。这里在持久化/执行前将其丢弃。
|
|
521
|
+
const validToolCalls = Array.from(collectedToolCalls.values()).filter((c) => c.id.length > 0 && c.tool.length > 0);
|
|
522
|
+
// 区分入参解析成功 / 失败的调用。解析失败(模型输出不完整 JSON,常见于流被截断)
|
|
523
|
+
// 的调用对系统完全透明:不执行、不持久化、不发前端事件(见下方工具执行块说明),
|
|
524
|
+
// 仅保留解析成功的调用供执行与持久化,避免 _raw/_parseError 容错标记泄漏。
|
|
525
|
+
const validCalls = validToolCalls.filter((tc) => !isParseErrorInput(tc.input));
|
|
526
|
+
const assistantContent = [];
|
|
527
|
+
if (collectedText.length > 0) {
|
|
528
|
+
assistantContent.push({ _tag: 'text', text: collectedText.join('') });
|
|
529
|
+
}
|
|
530
|
+
// 仅持久化解析成功的调用(携带真实入参)。解析失败的入参是容错标记,不能落库。
|
|
531
|
+
for (const tc of validCalls) {
|
|
532
|
+
assistantContent.push({
|
|
533
|
+
_tag: 'tool_call',
|
|
534
|
+
id: tc.id,
|
|
535
|
+
tool: tc.tool,
|
|
536
|
+
input: tc.input,
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
if (assistantContent.length > 0) {
|
|
540
|
+
const savedMsg = await appendMessage(deps.db, state.session.id, {
|
|
541
|
+
role: 'assistant',
|
|
542
|
+
content: assistantContent,
|
|
543
|
+
});
|
|
544
|
+
if (deps.hookRunner) {
|
|
545
|
+
await deps.hookRunner.fireHooks('message:after', { message: savedMsg });
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
// 工具调用卡只在入参解析完成后、携带真实入参时才向前端发射 tool_call_start。
|
|
549
|
+
// 这样前端拿到的第一帧就是可渲染的完整入参,不再出现空 pattern 半成品卡。
|
|
550
|
+
// 解析失败的调用不发射 start,其 tool_call_end 在前端无匹配 part 会被忽略,
|
|
551
|
+
// 因此解析失败在 UI 中不可见(模型会立即重试),错误仅反馈给模型并落库。
|
|
552
|
+
for (const tc of validCalls) {
|
|
553
|
+
yield { _tag: 'tool_call_start', id: tc.id, tool: tc.tool, input: tc.input };
|
|
554
|
+
}
|
|
555
|
+
// 仅执行解析成功的工具调用。解析失败的调用(isParseErrorInput 为真)对系统完全透明:
|
|
556
|
+
// 不执行、不持久化 tool result、不发 tool_call_start/end。其入参是 _parseError/_raw
|
|
557
|
+
// 容错标记,既不能执行也不能落库;若持久化为 orphan tool 消息(无对应 assistant
|
|
558
|
+
// tool_call),context.ts 的 sanitizeToolPairs 会在重建上下文时将其丢弃——即模型
|
|
559
|
+
// 永远收不到这个"错误反馈",徒增一次注定被忽略的 DB 写。故让 parse-error 对模型
|
|
560
|
+
// 不可见:模型下轮基于已持久化的 assistant 文本/有效 tool_call 重新生成,通常能
|
|
561
|
+
// 修正一次性的流截断错误。
|
|
562
|
+
if (validCalls.length > 0) {
|
|
563
|
+
const toolExecStart = Date.now();
|
|
564
|
+
const results = await executeToolCalls(deps.toolRegistry, deps.permission, {
|
|
565
|
+
cwd: deps.cwd,
|
|
566
|
+
session: { id: state.session.id, cwd: deps.cwd },
|
|
567
|
+
abort: state.abortController.signal,
|
|
568
|
+
...(deps.urlRegistry ? { urlRegistry: deps.urlRegistry } : {}),
|
|
569
|
+
...(deps.debugSpawn ? { debugSpawn: deps.debugSpawn } : {}),
|
|
570
|
+
runSubAgent: (req) => runSubAgent({ ...deps, _subagentEventSink: eventSink }, state, req),
|
|
571
|
+
...(deps._subagentYieldCollector ? { collectYield: deps._subagentYieldCollector } : {}),
|
|
572
|
+
}, validCalls, deps.hookRunner);
|
|
573
|
+
const toolLatency = Date.now() - toolExecStart;
|
|
574
|
+
const metricsEnabled = deps.config.toolMetrics.enabled;
|
|
575
|
+
for (const { id, result } of results) {
|
|
576
|
+
yield { _tag: 'tool_call_end', id, result };
|
|
577
|
+
const tc = validCalls.find((c) => c.id === id);
|
|
578
|
+
if (tc) {
|
|
579
|
+
await appendMessage(deps.db, state.session.id, {
|
|
580
|
+
role: 'tool',
|
|
581
|
+
content: toolResultToContent(id, tc.tool, result),
|
|
582
|
+
});
|
|
583
|
+
// spec §16.5:记录工具执行结果供后续模式评估。
|
|
584
|
+
// fire-and-forget:记录失败绝不阻塞 agent 主流程。
|
|
585
|
+
if (metricsEnabled) {
|
|
586
|
+
const mode = inferToolMode(tc.tool, tc.input);
|
|
587
|
+
const success = result._tag === 'success' || result._tag === 'truncated';
|
|
588
|
+
void recordToolMetrics(deps.db, state.config.model, tc.tool, mode, success, toolLatency).catch(() => { });
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
// yield 在本轮工具执行中收集的 subagent 事件(subagent_start/end)
|
|
594
|
+
for (const ev of subagentEvents) {
|
|
595
|
+
yield ev;
|
|
596
|
+
}
|
|
597
|
+
if (validToolCalls.length === 0) {
|
|
598
|
+
// finish_reason=length/content_filter 表示响应被截断或被内容过滤,而非正常说完。
|
|
599
|
+
// 若当作 completed,被截断的半截回答会静默成功(用户看到“中断但无报错”)。
|
|
600
|
+
if (truncated !== null) {
|
|
601
|
+
const message = truncated === 'length'
|
|
602
|
+
? 'Response truncated: the model hit max_tokens before finishing (finish_reason=length)'
|
|
603
|
+
: 'Response filtered by content policy (finish_reason=content_filter)';
|
|
604
|
+
yield { _tag: 'error', error: { _tag: 'unexpected', message } };
|
|
605
|
+
state.status = { _tag: 'stopped', reason: 'error', error: { _tag: 'unexpected', message } };
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
state.status = { _tag: 'stopped', reason: 'completed' };
|
|
609
|
+
yield { _tag: 'done' };
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
const latestMessages = await getMessages(deps.db, state.session.id);
|
|
613
|
+
// 校准估算系数:本轮真实 input token(system prompt + 工具描述 + 消息序列全口径)
|
|
614
|
+
// 反算 estimateTokens 的系统性系数,反哺 used/fitToBudget 的裁剪与压缩判断。
|
|
615
|
+
if (collectedUsage && collectedUsage.inputTokens > 0) {
|
|
616
|
+
const estimatedRequest = estimateTokens(systemPrompt) +
|
|
617
|
+
tools.reduce((sum, t) => sum + estimateTokens(JSON.stringify(t)), 0) +
|
|
618
|
+
estimateTokens(JSON.stringify(chatMessages));
|
|
619
|
+
state.calibrationFactor = calibrateEstimate(state.calibrationFactor, estimatedRequest, collectedUsage.inputTokens);
|
|
620
|
+
}
|
|
621
|
+
state.tokenBudget.used = estimateBudget(latestMessages, state.calibrationFactor);
|
|
622
|
+
state.messages = latestMessages;
|
|
623
|
+
if (shouldCompact(latestMessages, state.tokenBudget, deps.config.compaction)) {
|
|
624
|
+
const summarizer = state.compactionModel
|
|
625
|
+
? createSummarizer(deps.llmRegistry, state.compactionModel.provider, state.compactionModel.model, { signal: state.abortController.signal })
|
|
626
|
+
: createSummarizer(deps.llmRegistry, state.config.provider, state.config.model, {
|
|
627
|
+
signal: state.abortController.signal,
|
|
628
|
+
});
|
|
629
|
+
try {
|
|
630
|
+
await runCompaction(deps.db, state.session.id, summarizer, {
|
|
631
|
+
keepRecentTokens: deps.config.compaction.keepRecentTokens,
|
|
632
|
+
});
|
|
633
|
+
// 压缩改写了消息历史 → 标记下一轮强制开新段(段边界)
|
|
634
|
+
state.pendingSegmentTrigger = 'compaction';
|
|
635
|
+
state.tokenBudget.used = estimateBudget(await getMessages(deps.db, state.session.id), state.calibrationFactor);
|
|
636
|
+
}
|
|
637
|
+
catch {
|
|
638
|
+
// Compaction failure is non-fatal
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
yield {
|
|
643
|
+
_tag: 'error',
|
|
644
|
+
error: { _tag: 'max_turns', maxTurns },
|
|
645
|
+
};
|
|
646
|
+
state.status = {
|
|
647
|
+
_tag: 'stopped',
|
|
648
|
+
reason: 'error',
|
|
649
|
+
error: { _tag: 'max_turns', maxTurns },
|
|
650
|
+
};
|
|
651
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { DB } from '../db/client.js';
|
|
2
|
+
import type { ModelToolMetrics } from '../shared/types/tool.js';
|
|
3
|
+
/** 非 edit 工具的占位模式(这些工具只有一种实现,无需选择)。 */
|
|
4
|
+
export declare const DEFAULT_TOOL_MODE = "default";
|
|
5
|
+
/** edit 工具在数据不足或历史不达标时使用的保守默认(spec §16.5)。 */
|
|
6
|
+
export declare const DEFAULT_EDIT_MODE = "diff";
|
|
7
|
+
/** 推断某次工具调用使用的模式。
|
|
8
|
+
* - edit:入参含 `patch` → hashline;否则 diff(search/replace)。
|
|
9
|
+
* - 其他工具:固定 default(单实现,不参与模式选择)。 */
|
|
10
|
+
export declare function inferToolMode(tool: string, input: unknown): string;
|
|
11
|
+
/** 查某 (model, tool) 所有已记录模式的 metrics。 */
|
|
12
|
+
export declare function getToolMetrics(handle: DB, model: string, tool: string): Promise<ModelToolMetrics[]>;
|
|
13
|
+
/**
|
|
14
|
+
* 记录一次工具调用的结果(upsert)。avgLatencyMs 用增量滚动平均更新。
|
|
15
|
+
* PGLite 单进程,select-then-update 无并发竞争。
|
|
16
|
+
*/
|
|
17
|
+
export declare function recordToolMetrics(handle: DB, model: string, tool: string, mode: string, success: boolean, latencyMs: number): Promise<void>;
|
|
18
|
+
/**
|
|
19
|
+
* 根据历史成功率选择最优工具模式(spec §16.5)。
|
|
20
|
+
* - 样本不足(attempts < minSamples)→ defaultMode
|
|
21
|
+
* - 成功率 >= threshold 的模式中取成功率最高(并列取样本最多者)
|
|
22
|
+
* - 无达标模式 → defaultMode
|
|
23
|
+
*/
|
|
24
|
+
export declare function selectBestMode(metrics: ModelToolMetrics[], defaultMode: string, opts?: {
|
|
25
|
+
threshold?: number;
|
|
26
|
+
minSamples?: number;
|
|
27
|
+
}): string;
|