c0de-agent 1.11.0 → 1.13.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 +63 -16
- package/dist/cli/commands/acp.js +26 -8
- package/dist/cli/commands/chat.js +151 -1
- package/dist/cli/commands/config.d.ts +2 -0
- package/dist/cli/commands/config.js +63 -3
- package/dist/cli/commands/serve.js +11 -2
- package/dist/cli/commands/sessions.js +109 -3
- package/dist/cli/commands/trust.d.ts +11 -0
- package/dist/cli/commands/trust.js +31 -0
- package/dist/cli/commands/update.js +4 -0
- package/dist/cli/deps.d.ts +9 -6
- package/dist/cli/deps.js +66 -19
- package/dist/cli/index.d.ts +4 -1
- package/dist/cli/index.js +110 -11
- package/dist/cli/modes/acp.js +39 -8
- package/dist/cli/modes/print.d.ts +3 -0
- package/dist/cli/modes/print.js +78 -8
- package/dist/core/agent.d.ts +1 -1
- package/dist/core/agent.js +9 -4
- package/dist/core/agents/builtin.js +2 -2
- package/dist/core/agents/index.d.ts +0 -1
- package/dist/core/agents/index.js +0 -1
- package/dist/core/compact.d.ts +2 -0
- package/dist/core/compact.js +1 -0
- package/dist/core/config.d.ts +53 -2
- package/dist/core/config.js +235 -19
- package/dist/core/loop/compaction.d.ts +2 -1
- package/dist/core/loop/compaction.js +19 -6
- package/dist/core/loop/segment.d.ts +3 -1
- package/dist/core/loop/segment.js +24 -1
- package/dist/core/loop/stream-collect.js +9 -1
- package/dist/core/loop/subagent.js +88 -37
- package/dist/core/loop.d.ts +7 -0
- package/dist/core/loop.js +90 -6
- package/dist/core/redact.d.ts +11 -0
- package/dist/core/redact.js +49 -0
- package/dist/core/slash.d.ts +10 -0
- package/dist/core/slash.js +237 -65
- package/dist/core/title.js +7 -1
- package/dist/core/types.d.ts +22 -1
- package/dist/core/workflows/context.js +44 -4
- package/dist/core/workflows/discovery.d.ts +2 -2
- package/dist/core/workflows/discovery.js +5 -7
- package/dist/core/workflows/index.d.ts +2 -1
- package/dist/core/workflows/index.js +2 -1
- package/dist/core/workflows/registry.d.ts +13 -3
- package/dist/core/workflows/registry.js +14 -9
- package/dist/core/workflows/resolve.d.ts +22 -0
- package/dist/core/workflows/resolve.js +23 -0
- package/dist/core/workflows/runtime.d.ts +6 -1
- package/dist/core/workflows/runtime.js +19 -2
- package/dist/db/schema.d.ts +373 -1
- package/dist/db/schema.js +60 -4
- package/dist/kanban/index.d.ts +2 -1
- package/dist/kanban/index.js +1 -1
- package/dist/kanban/store.d.ts +87 -1
- package/dist/kanban/store.js +429 -22
- package/dist/llm/index.d.ts +1 -1
- package/dist/llm/index.js +1 -1
- package/dist/llm/provider.js +1 -1
- package/dist/llm/registry.d.ts +7 -1
- package/dist/llm/registry.js +7 -1
- package/dist/llm/retry.d.ts +5 -2
- package/dist/llm/retry.js +6 -5
- package/dist/llm/routing.d.ts +24 -1
- package/dist/llm/routing.js +29 -1
- package/dist/plugins/init.d.ts +7 -0
- package/dist/plugins/init.js +22 -1
- package/dist/plugins/loader.d.ts +7 -1
- package/dist/plugins/loader.js +12 -4
- package/dist/project/index.d.ts +1 -1
- package/dist/project/index.js +1 -1
- package/dist/project/project.d.ts +37 -0
- package/dist/project/project.js +142 -1
- package/dist/project/trust.d.ts +65 -0
- package/dist/project/trust.js +328 -0
- package/dist/server/agent-manager.d.ts +15 -2
- package/dist/server/agent-manager.js +18 -3
- package/dist/server/app.js +45 -30
- package/dist/server/auth-manager.d.ts +26 -9
- package/dist/server/auth-manager.js +78 -42
- package/dist/server/context.js +14 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/middleware/auth.d.ts +11 -1
- package/dist/server/middleware/auth.js +25 -3
- package/dist/server/permission/interactive.d.ts +11 -3
- package/dist/server/permission/interactive.js +23 -4
- package/dist/server/permission/store.d.ts +14 -2
- package/dist/server/permission/store.js +39 -3
- package/dist/server/registry-config.d.ts +14 -0
- package/dist/server/registry-config.js +44 -0
- package/dist/server/routes/auth.js +66 -10
- package/dist/server/routes/chat.d.ts +21 -0
- package/dist/server/routes/chat.js +685 -52
- package/dist/server/routes/commands.d.ts +4 -2
- package/dist/server/routes/commands.js +9 -4
- package/dist/server/routes/config.js +180 -23
- package/dist/server/routes/files.js +142 -28
- package/dist/server/routes/kanban.js +218 -26
- package/dist/server/routes/permissions.d.ts +0 -4
- package/dist/server/routes/permissions.js +140 -7
- package/dist/server/routes/project.js +132 -29
- package/dist/server/routes/provider.js +54 -3
- package/dist/server/routes/session.js +463 -23
- package/dist/server/routes/tool.js +24 -5
- package/dist/server/routes/update.d.ts +11 -5
- package/dist/server/routes/update.js +145 -29
- package/dist/server/routes/usage.d.ts +4 -0
- package/dist/server/routes/usage.js +133 -0
- package/dist/server/routes/workflows.js +476 -87
- package/dist/server/server.d.ts +16 -13
- package/dist/server/server.js +364 -107
- package/dist/server/terminal/pty-manager.d.ts +12 -0
- package/dist/server/terminal/pty-manager.js +63 -7
- package/dist/server/types.d.ts +16 -0
- package/dist/session/archive.d.ts +3 -1
- package/dist/session/archive.js +13 -1
- package/dist/session/branch.d.ts +2 -1
- package/dist/session/branch.js +92 -9
- package/dist/session/context.js +28 -2
- package/dist/session/import.d.ts +44 -0
- package/dist/session/import.js +83 -0
- package/dist/session/index.d.ts +1 -1
- package/dist/session/index.js +1 -1
- package/dist/session/jobs.d.ts +12 -0
- package/dist/session/jobs.js +53 -0
- package/dist/session/message.d.ts +7 -1
- package/dist/session/message.js +21 -1
- package/dist/session/session.d.ts +186 -12
- package/dist/session/session.js +577 -33
- package/dist/session/snapshot.d.ts +5 -2
- package/dist/session/snapshot.js +10 -2
- package/dist/session/types.d.ts +13 -1
- package/dist/session/usage.d.ts +62 -0
- package/dist/session/usage.js +194 -0
- package/dist/shared/types/agent.d.ts +29 -2
- package/dist/shared/types/config.d.ts +64 -5
- package/dist/shared/types/kanban.d.ts +17 -2
- package/dist/shared/types/message.d.ts +23 -0
- package/dist/tools/builtin/kanban.js +13 -2
- package/dist/tools/index.d.ts +14 -3
- package/dist/tools/index.js +23 -5
- package/dist/tools/websearch/index.d.ts +5 -1
- package/dist/tools/websearch/index.js +46 -7
- package/dist/tools/websearch/providers/brave.js +2 -2
- package/dist/tools/websearch/providers/duckduckgo.js +2 -1
- package/dist/tools/websearch/providers/tavily.js +2 -2
- package/dist/tools/websearch/types.d.ts +5 -1
- package/dist/tools/websearch/types.js +7 -1
- package/dist/update/index.d.ts +3 -3
- package/dist/update/index.js +2 -2
- package/dist/update/ipc.d.ts +13 -2
- package/dist/update/ipc.js +36 -2
- package/dist/update/scheduler.js +5 -2
- package/dist/update/snapshot.d.ts +18 -3
- package/dist/update/snapshot.js +5 -2
- package/dist/update/updater.d.ts +17 -1
- package/dist/update/updater.js +47 -18
- package/dist/update/version.d.ts +3 -0
- package/dist/update/version.js +9 -3
- package/drizzle/0008_tranquil_lily_hollister.sql +1 -0
- package/drizzle/0010_usage_events_ledger.sql +17 -0
- package/drizzle/0011_slow_slapstick.sql +12 -0
- package/drizzle/0012_chief_timeslip.sql +1 -0
- package/drizzle/0013_swift_zeigeist.sql +1 -0
- package/drizzle/0014_flimsy_legion.sql +1 -0
- package/drizzle/meta/0008_snapshot.json +875 -0
- package/drizzle/meta/0010_snapshot.json +1010 -0
- package/drizzle/meta/0011_snapshot.json +1055 -0
- package/drizzle/meta/0012_snapshot.json +1062 -0
- package/drizzle/meta/0013_snapshot.json +1068 -0
- package/drizzle/meta/0014_snapshot.json +1075 -0
- package/drizzle/meta/_journal.json +42 -0
- package/package.json +42 -42
- package/dist/core/agents/discovery.d.ts +0 -6
- package/dist/core/agents/discovery.js +0 -96
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { buildFallbackChain } from '../../llm/routing.js';
|
|
1
2
|
import { getMessages } from '../../session/message.js';
|
|
2
3
|
import { createSummarizer, runCompaction } from '../compact.js';
|
|
3
4
|
import { estimateBudget, shouldCompact } from '../context.js';
|
|
@@ -13,7 +14,13 @@ import { estimateBudget, shouldCompact } from '../context.js';
|
|
|
13
14
|
export async function* compactContext(state, deps) {
|
|
14
15
|
// summarizer 优先用 compactionModel 覆盖,否则回退当前会话 provider/model。
|
|
15
16
|
const cm = state.compactionModel;
|
|
16
|
-
const
|
|
17
|
+
const provider = cm ? cm.provider : state.config.provider;
|
|
18
|
+
const model = cm ? cm.model : state.config.model;
|
|
19
|
+
const fallback = buildFallbackChain(deps.config, provider, model);
|
|
20
|
+
const summarizer = createSummarizer(deps.llmRegistry, provider, model, {
|
|
21
|
+
signal: state.abortController.signal,
|
|
22
|
+
...(fallback ? { fallback } : {}),
|
|
23
|
+
});
|
|
17
24
|
const result = await runCompaction(deps.db, state.session.id, summarizer, {
|
|
18
25
|
keepRecentTokens: deps.config.compaction.keepRecentTokens,
|
|
19
26
|
});
|
|
@@ -77,15 +84,19 @@ export async function recoverFromOverflow(state, deps) {
|
|
|
77
84
|
* 死锁在下一轮用户输入(agentLoop 重入)时重置。
|
|
78
85
|
* - mid-turn:midTurnEnabled 单独 opt-in(默认关闭)时按 enabled:true 阈值静默压缩,
|
|
79
86
|
* 复用 shouldCompact 逻辑但不发 error/warning,压缩后刷新内存消息视图。
|
|
80
|
-
* 两者都静默消费 compactContext 的 text_delta
|
|
87
|
+
* 两者都静默消费 compactContext 的 text_delta 通知,但把 compaction_done 透传给前端,
|
|
88
|
+
* 让用户在上下文被改写时得到提示(可在归档面板查看原始内容)。 */
|
|
81
89
|
export async function* runCompactionIfNeeded(state, deps, latestMessages) {
|
|
82
90
|
// 死锁时跳过自动压缩:上一轮压缩已证明无法释放足够空间,重试只会无限循环。
|
|
83
91
|
if (!state.compactionDeadEnd &&
|
|
84
92
|
shouldCompact(latestMessages, state.tokenBudget, deps.config.compaction)) {
|
|
85
93
|
// 自动压缩:复用 compactContext;失败非致命,仅记录警告不中断主循环。
|
|
86
94
|
try {
|
|
87
|
-
for await (const
|
|
88
|
-
//
|
|
95
|
+
for await (const ev of compactContext(state, deps)) {
|
|
96
|
+
// 静默消费 text_delta 通知,但把 compaction_done 透传给前端——
|
|
97
|
+
// 上下文被改写,用户应被告知(可在归档面板查看原始内容),而非悄然失忆。
|
|
98
|
+
if (ev._tag === 'compaction_done')
|
|
99
|
+
yield ev;
|
|
89
100
|
}
|
|
90
101
|
// 进度保护:compactContext 已重算 state.tokenBudget.used。若压缩后仍超阈值
|
|
91
102
|
// (典型成因:keepRecentTokens 本身已超 historyBudget),压缩无法再释放足够
|
|
@@ -124,8 +135,10 @@ export async function* runCompactionIfNeeded(state, deps, latestMessages) {
|
|
|
124
135
|
// 中轮压缩静默执行:compactContext 内部已发 text_delta 通知,
|
|
125
136
|
// 不再 yield error/warning(与 turn-end 自动压缩的进度保护语义不同)。
|
|
126
137
|
try {
|
|
127
|
-
for await (const
|
|
128
|
-
//
|
|
138
|
+
for await (const ev of compactContext(state, deps)) {
|
|
139
|
+
// 静默消费 text_delta 通知,透传 compaction_done(与 turn-end 一致)。
|
|
140
|
+
if (ev._tag === 'compaction_done')
|
|
141
|
+
yield ev;
|
|
129
142
|
}
|
|
130
143
|
// 压缩改写了消息历史 → 刷新内存视图,供下一轮上下文重建使用
|
|
131
144
|
state.messages = await getMessages(deps.db, state.session.id);
|
|
@@ -5,4 +5,6 @@ import type { StreamCollectResult } from './stream-collect.js';
|
|
|
5
5
|
/** 段管理:判断段边界(pending trigger / model / systemPrompt / tools 变化),
|
|
6
6
|
* 构造本轮 LLMCall 并 push 到当前段,持久化 segments。直接修改 state.segments(引用语义)。
|
|
7
7
|
* trigger 判定用于新段元数据;needSegment 决定是否真正开新段(否则复用 activeSeg)。 */
|
|
8
|
-
export declare function manageSegment(state: AgentState, deps: LoopDeps, systemPrompt: string, tools: ChatTool[], requestStartTime: number, totalLatency: number, collectedUsage: StreamCollectResult['usage'], collectedText: string[], collectedThinking: string[], truncated: FinishReason | null, firstTokenTime: number | null, contextWindow: number | undefined,
|
|
8
|
+
export declare function manageSegment(state: AgentState, deps: LoopDeps, systemPrompt: string, tools: ChatTool[], requestStartTime: number, totalLatency: number, collectedUsage: StreamCollectResult['usage'], collectedText: string[], collectedThinking: string[], truncated: FinishReason | null, firstTokenTime: number | null, contextWindow: number | undefined,
|
|
9
|
+
/** null = 价格未知(loop 层已判定),按 $0 计入并由聚合层计数提示。 */
|
|
10
|
+
computedCost: number | null): Promise<void>;
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import { usageEvents } from '../../db/schema.js';
|
|
1
2
|
import { saveLLMSegments, segmentFingerprint } from '../../session/session.js';
|
|
2
3
|
import { generateId } from '../../shared/index.js';
|
|
3
4
|
/** 段管理:判断段边界(pending trigger / model / systemPrompt / tools 变化),
|
|
4
5
|
* 构造本轮 LLMCall 并 push 到当前段,持久化 segments。直接修改 state.segments(引用语义)。
|
|
5
6
|
* trigger 判定用于新段元数据;needSegment 决定是否真正开新段(否则复用 activeSeg)。 */
|
|
6
|
-
export async function manageSegment(state, deps, systemPrompt, tools, requestStartTime, totalLatency, collectedUsage, collectedText, collectedThinking, truncated, firstTokenTime, contextWindow,
|
|
7
|
+
export async function manageSegment(state, deps, systemPrompt, tools, requestStartTime, totalLatency, collectedUsage, collectedText, collectedThinking, truncated, firstTokenTime, contextWindow,
|
|
8
|
+
/** null = 价格未知(loop 层已判定),按 $0 计入并由聚合层计数提示。 */
|
|
9
|
+
computedCost) {
|
|
7
10
|
// —— 段管理:判断是否开新段 ——
|
|
8
11
|
const fp = segmentFingerprint(systemPrompt, tools);
|
|
9
12
|
const activeSeg = state.segments[state.segments.length - 1];
|
|
@@ -69,4 +72,24 @@ export async function manageSegment(state, deps, systemPrompt, tools, requestSta
|
|
|
69
72
|
};
|
|
70
73
|
currentSeg.calls.push(call);
|
|
71
74
|
await saveLLMSegments(deps.db, state.session.id, state.segments);
|
|
75
|
+
// P2 成本账本:每个 LLM 调用同时落独立 usage_events 表(与 session 生命周期解耦——
|
|
76
|
+
// 会话物理清除后花费记录仍在,账本不随回收站到期/彻底删除而消失)。
|
|
77
|
+
await deps.db.db
|
|
78
|
+
.insert(usageEvents)
|
|
79
|
+
.values({
|
|
80
|
+
callId: call.id,
|
|
81
|
+
sessionId: state.session.id,
|
|
82
|
+
projectId: state.session.projectId ?? null,
|
|
83
|
+
provider: state.config.provider,
|
|
84
|
+
model: state.config.model,
|
|
85
|
+
inputTokens: call.usage.input,
|
|
86
|
+
outputTokens: call.usage.output,
|
|
87
|
+
cacheRead: call.usage.cacheRead ?? 0,
|
|
88
|
+
cost: computedCost,
|
|
89
|
+
timestamp: call.timestamp,
|
|
90
|
+
})
|
|
91
|
+
.catch((err) => {
|
|
92
|
+
// 账本写入失败不致命(会话元数据仍含 segments 备份);告警便于排查。
|
|
93
|
+
console.warn(`[loop] usage_events 写入失败(成本账本将缺失本次调用):${err instanceof Error ? err.message : String(err)}`);
|
|
94
|
+
});
|
|
72
95
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isContextOverflowFailure } from '../../llm/provider-error.js';
|
|
2
|
+
import { buildFallbackChain } from '../../llm/routing.js';
|
|
2
3
|
import { isLLMError } from '../../llm/schema/errors.js';
|
|
3
4
|
import { recoverFromOverflow } from './compaction.js';
|
|
4
5
|
/** 入参是否为协议层/loop 标记的解析失败(携带 _parseError / _raw 容错标记)。
|
|
@@ -60,10 +61,17 @@ export async function* collectStreamChunks(state, deps, streamFn, request, overf
|
|
|
60
61
|
...over,
|
|
61
62
|
});
|
|
62
63
|
try {
|
|
64
|
+
// config.fallback 接线:enabled 时按配置重试参数 + 声明同模型的其他 provider 回退。
|
|
65
|
+
// 未启用时保持内置默认(3 次重试、无跨 provider 回退),行为与以往一致。
|
|
66
|
+
const fallback = buildFallbackChain(deps.config, state.config.provider, state.config.model);
|
|
63
67
|
for await (const chunk of streamFn({
|
|
64
68
|
registry: deps.llmRegistry,
|
|
65
69
|
signal: state.abortController.signal,
|
|
66
|
-
}, request, {
|
|
70
|
+
}, request, {
|
|
71
|
+
provider: state.config.provider,
|
|
72
|
+
model: state.config.model,
|
|
73
|
+
...(fallback ? { fallback } : {}),
|
|
74
|
+
})) {
|
|
67
75
|
if (firstTokenTime === null && chunk._tag !== 'done') {
|
|
68
76
|
firstTokenTime = Date.now();
|
|
69
77
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { appendMessage } from '../../session/message.js';
|
|
2
|
-
import { createSession } from '../../session/session.js';
|
|
2
|
+
import { createSession, updateSessionLastRun } from '../../session/session.js';
|
|
3
3
|
import { generateId } from '../../shared/index.js';
|
|
4
4
|
import { createAgent, runAgent } from '../agent.js';
|
|
5
5
|
import { applyPatchToParent, captureBaseline, captureDeltaPatch, createWorktree, removeWorktree, } from '../worktree.js';
|
|
@@ -35,10 +35,11 @@ export async function runSubAgent(deps, parent, request) {
|
|
|
35
35
|
description: request.description ?? '',
|
|
36
36
|
background: request.background ?? false,
|
|
37
37
|
});
|
|
38
|
-
// 2. 创建子 session(记录 agentType
|
|
38
|
+
// 2. 创建子 session(记录 agentType;parentId 挂到父会话——树内嵌套、
|
|
39
|
+
// 删除父会话时级联,不再是游离根节点)
|
|
39
40
|
let childSession;
|
|
40
41
|
try {
|
|
41
|
-
childSession = await createSession(deps.db, title, parent.session.projectId ?? undefined, request.agentType);
|
|
42
|
+
childSession = await createSession(deps.db, title, parent.session.projectId ?? undefined, request.agentType, parent.session.source ?? undefined, parent.session.id);
|
|
42
43
|
}
|
|
43
44
|
catch (e) {
|
|
44
45
|
return { _tag: 'error', error: e instanceof Error ? e.message : String(e) };
|
|
@@ -56,41 +57,74 @@ export async function runSubAgent(deps, parent, request) {
|
|
|
56
57
|
}
|
|
57
58
|
}
|
|
58
59
|
const childCwd = worktreePath ?? deps.cwd;
|
|
59
|
-
//
|
|
60
|
+
// 4. 构建子 agent 配置:工具集隔离 + 模型覆盖 + 递归限制 + yield
|
|
61
|
+
const parentDepth = deps._subagentDepth ?? 0;
|
|
62
|
+
const childDepth = parentDepth + 1;
|
|
63
|
+
// P1/P2-5:子 agent 工具集无条件取交集(def.tools ∩ 父工具集)——父工具集
|
|
64
|
+
// 已统一为 resolveEnabledToolNames 的配置解析结果(chat 主 run、CLI print、
|
|
65
|
+
// 工作流 runner 三处同口径),config.tools.enabled 的 fail-closed 语义对一切
|
|
66
|
+
// 派生路径成立:全禁时交集为空,子 agent 仅持 yield,写/执行能力不再被
|
|
67
|
+
// def 固定名单绕过。此前 length>0 例外为工作流 runner 恒 tools=[] 而设,
|
|
68
|
+
// 子 agent 因而拿到全套固定名单——现 runner 传解析结果,例外不再需要。
|
|
69
|
+
const declaredTools = def.tools
|
|
70
|
+
? def.tools.filter((t) => parent.config.tools.includes(t))
|
|
71
|
+
: parent.config.tools;
|
|
72
|
+
const maxRec = def.maxRecursion ?? 0;
|
|
73
|
+
const baseTools = childDepth > maxRec ? declaredTools.filter((t) => t !== 'task') : declaredTools;
|
|
74
|
+
const childTools = Array.from(new Set([...baseTools, 'yield']));
|
|
75
|
+
const childConfig = {
|
|
76
|
+
...parent.config,
|
|
77
|
+
systemPrompt: def.systemPrompt,
|
|
78
|
+
// 子 agent 走整段 systemPrompt 替换,清除父的 role override 避免干扰
|
|
79
|
+
agentRolePrompt: undefined,
|
|
80
|
+
tools: childTools,
|
|
81
|
+
...(def.model ? { model: def.model } : {}),
|
|
82
|
+
...(request.model ? { model: request.model } : {}),
|
|
83
|
+
};
|
|
84
|
+
// 子 agent 的 deps:覆盖 cwd(worktree)+ 注入 yield 收集器 + 递归深度
|
|
85
|
+
const childDeps = {
|
|
86
|
+
...deps,
|
|
87
|
+
cwd: childCwd,
|
|
88
|
+
_subagentYieldCollector: (data) => {
|
|
89
|
+
yielded.push(data);
|
|
90
|
+
},
|
|
91
|
+
_subagentDepth: childDepth,
|
|
92
|
+
};
|
|
93
|
+
const childState = await createAgent(childSession, childConfig, childDeps);
|
|
94
|
+
// 继承父 agent 的「预算已确认超支」标记:父 run 已因预算暂停、用户点「恢复」
|
|
95
|
+
// 继续后,子 agent 不应再因同一预算超支提前中止(用户已知情继续);父未超支
|
|
96
|
+
// 时标记为 undefined,子 agent 自跑其 loop 仍会按轮次独立检查预算。
|
|
97
|
+
childState.budgetPauseTriggered = parent.budgetPauseTriggered;
|
|
98
|
+
// abort 链接:父 abort 则子 abort
|
|
99
|
+
if (parent.abortController.signal.aborted) {
|
|
100
|
+
childState.abortController.abort();
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
parent.abortController.signal.addEventListener('abort', () => childState.abortController.abort(), { once: true });
|
|
104
|
+
}
|
|
105
|
+
// P1:把子 run(同步 + 后台)注册进宿主 run 跟踪器(Web=agentManager)。
|
|
106
|
+
// 此前子 run 完全游离:暂停(权限超时/用户/预算)、热更新 pauseAll、
|
|
107
|
+
// 删除父会话的中止级联、更新影响面列表全部绕过后台子 agent。
|
|
108
|
+
// 注册必须先于 dispatch:background 路径「返回 running」时子 run 已可被控制。
|
|
109
|
+
const unregisterChild = deps.registerChildRun?.({
|
|
110
|
+
sessionId: childSession.id,
|
|
111
|
+
parentSessionId: parent.session.id,
|
|
112
|
+
...(request.background ? { jobId: childSession.id } : {}),
|
|
113
|
+
state: childState,
|
|
114
|
+
deps: childDeps,
|
|
115
|
+
});
|
|
116
|
+
// 实际运行子 agent 的内部函数(sync 与 background 共用)。
|
|
117
|
+
// 无论正常完成/出错/被中止,finally 中注销宿主注册,避免 run 槽位泄漏。
|
|
60
118
|
const runBody = async () => {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const childDepth = parentDepth + 1;
|
|
64
|
-
const declaredTools = def.tools ?? parent.config.tools;
|
|
65
|
-
const maxRec = def.maxRecursion ?? 0;
|
|
66
|
-
const baseTools = childDepth > maxRec ? declaredTools.filter((t) => t !== 'task') : declaredTools;
|
|
67
|
-
const childTools = Array.from(new Set([...baseTools, 'yield']));
|
|
68
|
-
const childConfig = {
|
|
69
|
-
...parent.config,
|
|
70
|
-
systemPrompt: def.systemPrompt,
|
|
71
|
-
// 子 agent 走整段 systemPrompt 替换,清除父的 role override 避免干扰
|
|
72
|
-
agentRolePrompt: undefined,
|
|
73
|
-
tools: childTools,
|
|
74
|
-
...(def.model ? { model: def.model } : {}),
|
|
75
|
-
...(request.model ? { model: request.model } : {}),
|
|
76
|
-
};
|
|
77
|
-
// 子 agent 的 deps:覆盖 cwd(worktree)+ 注入 yield 收集器 + 递归深度
|
|
78
|
-
const childDeps = {
|
|
79
|
-
...deps,
|
|
80
|
-
cwd: childCwd,
|
|
81
|
-
_subagentYieldCollector: (data) => {
|
|
82
|
-
yielded.push(data);
|
|
83
|
-
},
|
|
84
|
-
_subagentDepth: childDepth,
|
|
85
|
-
};
|
|
86
|
-
const childState = await createAgent(childSession, childConfig, childDeps);
|
|
87
|
-
// abort 链接:父 abort 则子 abort
|
|
88
|
-
if (parent.abortController.signal.aborted) {
|
|
89
|
-
childState.abortController.abort();
|
|
119
|
+
try {
|
|
120
|
+
return await runChildBody(childState, childDeps, childSession, title, baseline, worktreePath);
|
|
90
121
|
}
|
|
91
|
-
|
|
92
|
-
|
|
122
|
+
finally {
|
|
123
|
+
unregisterChild?.();
|
|
93
124
|
}
|
|
125
|
+
};
|
|
126
|
+
// 子 agent loop 执行体:运行 loop → worktree delta 回传 → 发射结束事件。
|
|
127
|
+
const runChildBody = async (childState, childDeps, childSession, title, baseline, worktreePath) => {
|
|
94
128
|
// 运行子 agent loop
|
|
95
129
|
const childPrompt = request.context
|
|
96
130
|
? `CONTEXT\n${request.context}\n\nASSIGNMENT\n${request.prompt}`
|
|
@@ -147,13 +181,30 @@ export async function runSubAgent(deps, parent, request) {
|
|
|
147
181
|
// 6. background 模式:fork 异步运行,立即返回 running;完成时向父 session 注入合成通知
|
|
148
182
|
if (request.background) {
|
|
149
183
|
const jobId = childSession.id;
|
|
184
|
+
// P2:落库 running 标记——进程崩溃后 markDeadBackgroundJobs 可识别悬空任务
|
|
185
|
+
// 并给父会话发失败通知(此前仅内存 jobId,重启即静默丢失)。
|
|
186
|
+
// 此写 await 完成再返回:保证「返回 running」时 DB 已记录;同时避免与
|
|
187
|
+
// 子 loop 的并发写入叠加。
|
|
188
|
+
await updateSessionLastRun(deps.db, childSession.id, {
|
|
189
|
+
status: 'running',
|
|
190
|
+
agentName: request.agentType,
|
|
191
|
+
startedAt: Date.now(),
|
|
192
|
+
}).catch(() => { });
|
|
150
193
|
void runBody()
|
|
151
|
-
.then((result) => {
|
|
194
|
+
.then(async (result) => {
|
|
195
|
+
// 无论成败都置 completed:任务已终结,状态体现在合成通知里。
|
|
196
|
+
// 顺序 await(不并发发起):PGLite WASM 对同实例并发查询会忙等,
|
|
197
|
+
// 与 appendMessage 并发叠加曾导致 100% CPU 自旋(loop.test.ts 复现)。
|
|
198
|
+
await updateSessionLastRun(deps.db, childSession.id, {
|
|
199
|
+
status: 'completed',
|
|
200
|
+
agentName: request.agentType,
|
|
201
|
+
startedAt: Date.now(),
|
|
202
|
+
}).catch(() => { });
|
|
152
203
|
const success = result._tag === 'success';
|
|
153
204
|
const output = success ? result.output : result.error;
|
|
154
205
|
const tag = success ? 'task_result' : 'task_error';
|
|
155
206
|
const synthetic = `<task id="${childSession.id}" state="${success ? 'completed' : 'failed'}">\n<${tag}>\n${output}\n</${tag}>\n</task>`;
|
|
156
|
-
|
|
207
|
+
await appendMessage(deps.db, parent.session.id, {
|
|
157
208
|
role: 'user',
|
|
158
209
|
content: [{ _tag: 'text', text: synthetic }],
|
|
159
210
|
}).catch((e) => {
|
package/dist/core/loop.d.ts
CHANGED
|
@@ -12,6 +12,13 @@ type LoopDeps = AgentDependencies & {
|
|
|
12
12
|
readonly _subagentEventSink?: (event: AgentEvent) => void;
|
|
13
13
|
/** 当前递归深度(0=顶层主 agent)。用于 maxRecursion 控制(spec §4.5 step 4)。 */
|
|
14
14
|
readonly _subagentDepth?: number;
|
|
15
|
+
/** P3 成本护栏:Web 会话「启用预算检查」的门禁。具体 pause/abort 由
|
|
16
|
+
* budgetOverageParts 按超支轴配置动作判定(abort > pause),此处仅表启用。
|
|
17
|
+
* 由 chat 路由按 usage 任一轴动作为 'pause'/'abort' 注入。 */
|
|
18
|
+
readonly budgetPause?: boolean;
|
|
19
|
+
/** 预算护栏的 CLI 变体:无恢复 UI,超支时**中止 run**并产出 error(而非暂停挂起)。
|
|
20
|
+
* 由 CLI deps 组装(buildAgentDeps)按 usage 动作注入,替代 budgetPause(恒 abort)。 */
|
|
21
|
+
readonly budgetAbort?: boolean;
|
|
15
22
|
};
|
|
16
23
|
export type { LoopDeps };
|
|
17
24
|
export declare function agentLoop(state: AgentState, deps: LoopDeps): AsyncGenerator<AgentEvent>;
|
package/dist/core/loop.js
CHANGED
|
@@ -3,8 +3,9 @@ import { resolveRoute } from '../llm/registry.js';
|
|
|
3
3
|
import { detectProjectInfo } from '../project/detect.js';
|
|
4
4
|
import { entriesToChatMessages, getSessionContext } from '../session/context.js';
|
|
5
5
|
import { getMessages } from '../session/message.js';
|
|
6
|
-
import { updateSessionLastRun } from '../session/session.js';
|
|
6
|
+
import { markBudgetPause, updateSessionLastRun } from '../session/session.js';
|
|
7
7
|
import { estimateTokens } from '../session/token.js';
|
|
8
|
+
import { budgetOverageParts } from '../session/usage.js';
|
|
8
9
|
import { calibrateEstimate, createTokenBudget, estimateBudget } from './context.js';
|
|
9
10
|
import { runCompactionIfNeeded } from './loop/compaction.js';
|
|
10
11
|
import { persistAssistantAndTools } from './loop/persist.js';
|
|
@@ -22,6 +23,8 @@ const FALLBACK_CONTEXT_WINDOW = 32_000;
|
|
|
22
23
|
/** 每个 agent 已解析的有效 contextWindow 缓存(真实值或保守估计)。
|
|
23
24
|
* 避免每轮重复解析/告警,并防止估计值随每轮 input token 抖动导致预算基准漂移。 */
|
|
24
25
|
const effectiveContextWindowCache = new WeakMap();
|
|
26
|
+
/** 已记录「金额口径低估」告警的 agent(每 run 至多记一次,防逐轮刷屏)。 */
|
|
27
|
+
const budgetWarnedStates = new WeakSet();
|
|
25
28
|
/**
|
|
26
29
|
* 解析本轮有效 contextWindow,确保 shouldCompact 始终有合理基准:
|
|
27
30
|
* - provider 已声明(capabilities.contextWindow 为正数)→ 原样采用,不改变正常配置行为。
|
|
@@ -85,6 +88,82 @@ export async function* agentLoop(state, deps) {
|
|
|
85
88
|
if (state.status._tag === 'paused')
|
|
86
89
|
return; // 等待期间被 abort 等终结
|
|
87
90
|
}
|
|
91
|
+
// 预算护栏:金额 + token 双口径(token 兜底价格未知的自建网关/未登记模型),
|
|
92
|
+
// 每轮 LLM 请求前检查。每 run 至多触发一次(budgetPauseTriggered),恢复后
|
|
93
|
+
// 用户已知情继续。deps.budgetPause 由 Web 路由按配置注入;deps.budgetAbort
|
|
94
|
+
// 由 CLI 组装注入(无恢复 UI,超支中止而非暂停挂起)。
|
|
95
|
+
// 顶层 agent:超支 → 暂停(Web,可恢复)/ 中止(CLI,产出 error)。
|
|
96
|
+
// 子 agent:无恢复 UI,超支 → 提前中止自身(把超支作为错误回报父 agent,父
|
|
97
|
+
// run 下一轮检查将暂停)——收紧子 agent 单轮内 fan-out 的超支粒度。
|
|
98
|
+
if ((deps.budgetPause === true || deps.budgetAbort === true) && !state.budgetPauseTriggered) {
|
|
99
|
+
try {
|
|
100
|
+
const { parts, action, warnings } = await budgetOverageParts(deps.db, deps.config.usage, state.session.projectId);
|
|
101
|
+
if (parts.length > 0) {
|
|
102
|
+
state.budgetPauseTriggered = true;
|
|
103
|
+
const reason = `月度预算超支(${parts.join(';')})${warnings.length > 0 ? `;注意:${warnings.join(';')}` : ''}`;
|
|
104
|
+
if (deps._subagentDepth === undefined) {
|
|
105
|
+
// CLI(budgetAbort)恒中止;Web 按超支轴配置动作判定(abort > pause)。
|
|
106
|
+
if (deps.budgetAbort === true || action === 'abort') {
|
|
107
|
+
state.status = {
|
|
108
|
+
_tag: 'stopped',
|
|
109
|
+
reason: 'error',
|
|
110
|
+
error: { _tag: 'unexpected', message: reason },
|
|
111
|
+
};
|
|
112
|
+
yield { _tag: 'error', error: { _tag: 'unexpected', message: reason } };
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
state.status = { _tag: 'paused', pauseReason: reason };
|
|
116
|
+
yield { _tag: 'status_change', status: state.status };
|
|
117
|
+
await updateSessionLastRun(deps.db, state.session.id, {
|
|
118
|
+
status: 'paused',
|
|
119
|
+
agentName: state.config.agentName,
|
|
120
|
+
provider: state.config.provider,
|
|
121
|
+
model: state.config.model,
|
|
122
|
+
startedAt: state.lastRunStartedAt ?? Date.now(),
|
|
123
|
+
}).catch(() => { });
|
|
124
|
+
// P2-3:暂停原因落盘——热更新/重启后 run 重建,消费此标记还原
|
|
125
|
+
// budgetPauseTriggered,避免同一超支原因二次暂停打断用户。
|
|
126
|
+
await markBudgetPause(deps.db, state.session.id, reason).catch(() => { });
|
|
127
|
+
await waitForResume(state);
|
|
128
|
+
if (state.status._tag === 'paused')
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
state.status = {
|
|
133
|
+
_tag: 'stopped',
|
|
134
|
+
reason: 'error',
|
|
135
|
+
error: { _tag: 'unexpected', message: reason },
|
|
136
|
+
};
|
|
137
|
+
yield { _tag: 'error', error: { _tag: 'unexpected', message: reason } };
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
else if (warnings.length > 0 && !budgetWarnedStates.has(state)) {
|
|
142
|
+
// P2-2:未超支但金额口径不可靠(价格未知调用无 token 兜底)——每 run
|
|
143
|
+
// 记日志一次,不阻断。用户主动看日志即可发现「预算未真正兜底」。
|
|
144
|
+
budgetWarnedStates.add(state);
|
|
145
|
+
console.warn(`[usage] 预算金额口径低估告警:${warnings.join(';')}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
// 账本查询失败:记录告警。'warn'/'pause' 路径 fail-open(可用性优先);
|
|
150
|
+
// 'abort' 路径(用户显式要求硬封顶)保守 fail-closed——账本不可用即无法
|
|
151
|
+
// 保证「绝不超限」,宁可本轮中止并明示,而非静默放行继续烧钱。
|
|
152
|
+
console.error('[usage] 预算护栏账本查询失败:', err);
|
|
153
|
+
const abortSeverity = deps.config.usage.budgetAction === 'abort' ||
|
|
154
|
+
deps.config.usage.tokenBudgetAction === 'abort';
|
|
155
|
+
if (abortSeverity) {
|
|
156
|
+
const reason = '月度预算账本查询失败且预算动作为 abort(硬封顶):无法确认是否超支,出于安全中止本轮';
|
|
157
|
+
state.status = {
|
|
158
|
+
_tag: 'stopped',
|
|
159
|
+
reason: 'error',
|
|
160
|
+
error: { _tag: 'unexpected', message: reason },
|
|
161
|
+
};
|
|
162
|
+
yield { _tag: 'error', error: { _tag: 'unexpected', message: reason } };
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
88
167
|
state.status = { _tag: 'running', turnCount: turn };
|
|
89
168
|
const steering = drainSteering(state);
|
|
90
169
|
const { entries, snapshots } = await getSessionContext(deps.db, state.session.id, deps.cwd);
|
|
@@ -198,18 +277,23 @@ export async function* agentLoop(state, deps) {
|
|
|
198
277
|
}
|
|
199
278
|
const totalLatency = Date.now() - requestStartTime;
|
|
200
279
|
let contextWindow;
|
|
201
|
-
|
|
280
|
+
// H2:价格未知时 cost=null(聚合按 $0 计 + 显式计数),不再静默记 0。
|
|
281
|
+
// 未知判定:provider 未注册/解析失败,或模型价目全为 0(未声明价格的
|
|
282
|
+
// 自建网关模型回退到 DEFAULT_MODEL_CAPABILITIES 的 0 价)。
|
|
283
|
+
let computedCost = null;
|
|
202
284
|
try {
|
|
203
285
|
const { capabilities } = resolveRoute(deps.llmRegistry, state.config.provider, state.config.model);
|
|
204
286
|
contextWindow = capabilities.contextWindow;
|
|
205
287
|
const inputTokens = collected.usage?.inputTokens ?? 0;
|
|
206
288
|
const outputTokens = collected.usage?.outputTokens ?? 0;
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
(
|
|
289
|
+
if (capabilities.costPer1kInput > 0 || capabilities.costPer1kOutput > 0) {
|
|
290
|
+
computedCost =
|
|
291
|
+
(inputTokens / 1000) * capabilities.costPer1kInput +
|
|
292
|
+
(outputTokens / 1000) * capabilities.costPer1kOutput;
|
|
293
|
+
}
|
|
210
294
|
}
|
|
211
295
|
catch {
|
|
212
|
-
// provider
|
|
296
|
+
// provider 未注册:保留 contextWindow=undefined、cost=null
|
|
213
297
|
}
|
|
214
298
|
// —— contextWindow 兜底 + token 预算同步 ——
|
|
215
299
|
// provider 未声明 contextWindow 时回填保守估计(见 resolveEffectiveContextWindow),
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 敏感字段脱敏工具:/config 斜杠命令与 `c0de config get` 展示配置时使用,
|
|
3
|
+
* 防止 apiKey / token 等明文出现在聊天界面或终端输出中。
|
|
4
|
+
*/
|
|
5
|
+
export declare function isSecretKey(key: string): boolean;
|
|
6
|
+
/** 掩码敏感值:短值整体掩码;长值保留首尾各 4 位。非字符串统一返回 ****。 */
|
|
7
|
+
export declare function maskSecret(value: unknown): unknown;
|
|
8
|
+
/** 递归脱敏:键名命中敏感模式的字段被掩码,其余结构原样保留。 */
|
|
9
|
+
export declare function redactSecrets(value: unknown, key?: string): unknown;
|
|
10
|
+
/** 判断对象树中是否含非空敏感值(用于「配置文件含密钥」类警告)。 */
|
|
11
|
+
export declare function containsSecrets(value: unknown): boolean;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 敏感字段脱敏工具:/config 斜杠命令与 `c0de config get` 展示配置时使用,
|
|
3
|
+
* 防止 apiKey / token 等明文出现在聊天界面或终端输出中。
|
|
4
|
+
*/
|
|
5
|
+
/** 敏感字段名模式(大小写不敏感,匹配完整键名)。 */
|
|
6
|
+
const SECRET_KEY_RE = /(api[_-]?key|token|password|passwd|secret|authorization|auth[_-]?token)$/i;
|
|
7
|
+
export function isSecretKey(key) {
|
|
8
|
+
return SECRET_KEY_RE.test(key);
|
|
9
|
+
}
|
|
10
|
+
/** 掩码敏感值:短值整体掩码;长值保留首尾各 4 位。非字符串统一返回 ****。 */
|
|
11
|
+
export function maskSecret(value) {
|
|
12
|
+
if (typeof value !== 'string')
|
|
13
|
+
return '****';
|
|
14
|
+
if (value.length <= 8)
|
|
15
|
+
return '****';
|
|
16
|
+
return `${value.slice(0, 4)}****${value.slice(-4)}`;
|
|
17
|
+
}
|
|
18
|
+
/** 递归脱敏:键名命中敏感模式的字段被掩码,其余结构原样保留。 */
|
|
19
|
+
export function redactSecrets(value, key) {
|
|
20
|
+
if (typeof key === 'string' && isSecretKey(key))
|
|
21
|
+
return maskSecret(value);
|
|
22
|
+
if (Array.isArray(value))
|
|
23
|
+
return value.map((v) => redactSecrets(v));
|
|
24
|
+
if (value !== null && typeof value === 'object') {
|
|
25
|
+
const out = {};
|
|
26
|
+
for (const [k, v] of Object.entries(value)) {
|
|
27
|
+
out[k] = redactSecrets(v, k);
|
|
28
|
+
}
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
/** 判断对象树中是否含非空敏感值(用于「配置文件含密钥」类警告)。 */
|
|
34
|
+
export function containsSecrets(value) {
|
|
35
|
+
if (Array.isArray(value))
|
|
36
|
+
return value.some((v) => containsSecrets(v));
|
|
37
|
+
if (value !== null && typeof value === 'object') {
|
|
38
|
+
for (const [k, v] of Object.entries(value)) {
|
|
39
|
+
if (isSecretKey(k)) {
|
|
40
|
+
if (typeof v === 'string' ? v.length > 0 : v != null)
|
|
41
|
+
return true;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (containsSecrets(v))
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return false;
|
|
49
|
+
}
|
package/dist/core/slash.d.ts
CHANGED
|
@@ -10,6 +10,16 @@ type SlashRegistry = {
|
|
|
10
10
|
register: (cmd: SlashCommand) => void;
|
|
11
11
|
};
|
|
12
12
|
declare function createSlashRegistry(): SlashRegistry;
|
|
13
|
+
/**
|
|
14
|
+
* P0-2:统一斜杠命令启用判定(Web chat / CLI chat / 命令列表 / /help 共用)。
|
|
15
|
+
* 语义(fail-closed,与 tools.enabled 对齐):
|
|
16
|
+
* - enabled 含 '*'(通配)→ 全部启用。
|
|
17
|
+
* - enabled 为显式名单 → 仅名单内启用(名称兼容带/不带前缀斜杠)。
|
|
18
|
+
* - enabled 为空数组/缺失 → 全部禁用(旧版含义「空=全部启用」,升级告警见
|
|
19
|
+
* collectConfigMigrationWarnings——两处同形键不再语义相反)。
|
|
20
|
+
* `enabled` 已含 '/' 前缀或裸名均可。
|
|
21
|
+
*/
|
|
22
|
+
export declare function isSlashCommandEnabled(enabled: string[] | undefined, name: string): boolean;
|
|
13
23
|
declare const builtinCommands: SlashCommand[];
|
|
14
24
|
export type { SlashRegistry };
|
|
15
25
|
export { builtinCommands, createSlashRegistry, parseSlashInput };
|