c0de-agent 1.12.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 +51 -23
- package/dist/cli/commands/acp.js +26 -8
- package/dist/cli/commands/chat.js +70 -6
- package/dist/cli/commands/config.d.ts +2 -0
- package/dist/cli/commands/config.js +46 -0
- package/dist/cli/commands/serve.js +11 -2
- package/dist/cli/commands/sessions.js +51 -4
- 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 +92 -12
- package/dist/cli/modes/acp.js +39 -8
- package/dist/cli/modes/print.d.ts +3 -0
- package/dist/cli/modes/print.js +71 -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/config.d.ts +43 -1
- package/dist/core/config.js +163 -14
- package/dist/core/loop/segment.d.ts +3 -1
- package/dist/core/loop/segment.js +24 -1
- package/dist/core/loop/subagent.js +65 -32
- package/dist/core/loop.d.ts +7 -0
- package/dist/core/loop.js +90 -6
- package/dist/core/slash.d.ts +10 -0
- package/dist/core/slash.js +150 -20
- package/dist/core/types.d.ts +20 -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 +356 -1
- package/dist/db/schema.js +57 -3
- package/dist/kanban/index.d.ts +2 -1
- package/dist/kanban/index.js +1 -1
- package/dist/kanban/store.d.ts +84 -2
- package/dist/kanban/store.js +358 -25
- package/dist/llm/registry.d.ts +7 -1
- package/dist/llm/registry.js +7 -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 +23 -0
- package/dist/project/project.js +65 -0
- package/dist/project/trust.d.ts +65 -0
- package/dist/project/trust.js +328 -0
- package/dist/server/agent-manager.d.ts +6 -2
- package/dist/server/agent-manager.js +4 -2
- package/dist/server/app.js +45 -30
- package/dist/server/auth-manager.d.ts +15 -4
- package/dist/server/auth-manager.js +41 -24
- package/dist/server/context.js +11 -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 +5 -0
- package/dist/server/permission/interactive.js +4 -1
- package/dist/server/permission/store.d.ts +2 -0
- package/dist/server/permission/store.js +8 -0
- package/dist/server/routes/auth.js +35 -9
- package/dist/server/routes/chat.d.ts +16 -7
- package/dist/server/routes/chat.js +483 -28
- package/dist/server/routes/commands.d.ts +2 -2
- package/dist/server/routes/commands.js +4 -6
- package/dist/server/routes/config.js +81 -4
- package/dist/server/routes/files.js +120 -24
- package/dist/server/routes/kanban.js +152 -22
- package/dist/server/routes/permissions.js +7 -0
- package/dist/server/routes/project.js +76 -22
- package/dist/server/routes/provider.js +6 -0
- package/dist/server/routes/session.js +177 -13
- package/dist/server/routes/tool.js +24 -5
- package/dist/server/routes/update.js +58 -2
- 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 +12 -1
- package/dist/server/server.js +194 -33
- 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 +9 -0
- package/dist/session/branch.d.ts +2 -1
- package/dist/session/branch.js +75 -10
- package/dist/session/context.js +28 -2
- package/dist/session/index.d.ts +1 -1
- package/dist/session/index.js +1 -1
- package/dist/session/session.d.ts +67 -10
- package/dist/session/session.js +180 -67
- package/dist/session/types.d.ts +2 -0
- package/dist/session/usage.d.ts +62 -0
- package/dist/session/usage.js +194 -0
- package/dist/shared/types/agent.d.ts +19 -3
- package/dist/shared/types/config.d.ts +59 -4
- package/dist/shared/types/kanban.d.ts +4 -2
- package/dist/shared/types/message.d.ts +14 -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 +40 -4
- 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/scheduler.js +5 -2
- package/dist/update/snapshot.d.ts +17 -3
- package/dist/update/snapshot.js +3 -2
- package/dist/update/updater.d.ts +2 -0
- package/dist/update/updater.js +2 -1
- package/dist/update/version.d.ts +3 -0
- package/dist/update/version.js +9 -3
- 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/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 +35 -0
- package/package.json +24 -23
- package/dist/core/agents/discovery.d.ts +0 -6
- package/dist/core/agents/discovery.js +0 -96
package/dist/core/config.d.ts
CHANGED
|
@@ -1,10 +1,43 @@
|
|
|
1
1
|
import type { AgentsConfig, CompactionConfig, Config, MCPServerConfig, PermissionConfig, SecurityConfig, ToolMetricsConfig, UpdateConfig, WebSearchConfig } from '../shared/types/config.js';
|
|
2
|
+
/**
|
|
3
|
+
* 全局配置根目录:默认 `~/.c0de`;C0DE_CONFIG_DIR 环境变量可整体重定向
|
|
4
|
+
* (相对路径按进程 cwd 解析),全局工作流/插件同样落于此根下。
|
|
5
|
+
* 与 C0DE_DB_DIR 同口径——开发隔离场景把全局配置从安装版默认位置切到独立目录。
|
|
6
|
+
*/
|
|
7
|
+
declare function resolveGlobalConfigDir(): string;
|
|
2
8
|
declare const DEFAULT_CONFIG: Config;
|
|
3
9
|
declare function mergeConfig(...configs: (Partial<Config> | undefined)[]): Config;
|
|
10
|
+
/**
|
|
11
|
+
* usage 下「全局口径」预算键——仅 global 作用域生效。项目作用域写入这些键
|
|
12
|
+
* 会被忽略(作用域收敛):此前「项目配置里设全局预算无意义但会生效」既造成
|
|
13
|
+
* 双预算语义混乱,又会随 git clone 传播、未被信任门禁拦截。收敛后项目作用域
|
|
14
|
+
* 的这两键不再进入任何合并视图,也不随 clone 影响本机护栏。
|
|
15
|
+
*/
|
|
16
|
+
declare const GLOBAL_ONLY_USAGE_KEYS: readonly ["globalMonthlyBudgetUsd", "globalMonthlyTokenBudget"];
|
|
17
|
+
/** 从项目作用域原始配置中剥离「服务端/全局」键(security 顶层键 + usage 全局口径预算键)。
|
|
18
|
+
* 浅拷贝,不改入参;无剥离键时原样返回(保持身份,调用方可安全比较)。 */
|
|
19
|
+
declare function stripProjectServerKeys(project: Partial<Config> | undefined): Partial<Config> | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* 返回项目作用域**原始文件**中出现的全局口径预算键名(strip 前的原始内容)。
|
|
22
|
+
* 供设置页告警展示:这些键来自旧版本或手动编辑,加载时已被剥离、不再生效。
|
|
23
|
+
*/
|
|
24
|
+
declare function projectGlobalOnlyUsageKeys(projectDir?: string): string[];
|
|
25
|
+
/**
|
|
26
|
+
* 返回项目作用域**原始文件**中出现的 security 子键名(strip 前的原始内容)。
|
|
27
|
+
* 供设置页告警展示:security 是服务端全局参数,随项目配置出现时已被剥离、不再生效。
|
|
28
|
+
*/
|
|
29
|
+
declare function projectSecurityKeys(projectDir?: string): string[];
|
|
30
|
+
/**
|
|
31
|
+
* 读取项目作用域原始文件内容(不剥离 security/全局键)。
|
|
32
|
+
* 供「文件原文」敏感的检查使用——被剥离的键仍可能携带明文密钥(如 security.token),
|
|
33
|
+
* 配置落在 git 仓库内同样要防误提交,不能因键不生效而漏掉警告。
|
|
34
|
+
*/
|
|
35
|
+
declare function loadProjectRawScope(projectDir?: string): Partial<Config> | undefined;
|
|
4
36
|
/**
|
|
5
37
|
* 读取 global/project 两个作用域的**原始文件内容**(不经 DEFAULT 合并)。
|
|
6
38
|
* 供配置持久化使用:写回某个作用域时只落该作用域应有的键,
|
|
7
39
|
* 避免把合并结果(含默认值与另一作用域的配置)整体序列化进文件。
|
|
40
|
+
* 项目作用域经 stripProjectServerKeys 收敛——security 与全局口径预算键只会从 global 读。
|
|
8
41
|
*/
|
|
9
42
|
declare function loadConfigScopes(projectDir?: string): {
|
|
10
43
|
global: Partial<Config> | undefined;
|
|
@@ -31,6 +64,15 @@ declare function mergeRaw(...cfgs: (Record<string, unknown> | undefined)[]): Rec
|
|
|
31
64
|
* - 写入后 chmod 600:配置文件可能含 token/加密密钥,默认 0644 同机可读。 */
|
|
32
65
|
declare function saveConfigScoped(scope: 'global' | 'project', projectDir: string | undefined, data: Record<string, unknown>): Promise<void>;
|
|
33
66
|
declare function loadConfig(projectDir?: string): Promise<Config>;
|
|
67
|
+
/**
|
|
68
|
+
* P0-1:配置迁移告警(返回值,供 Web 设置页 warnings 展示;CLI 侧 console.warn
|
|
69
|
+
* 复用)。检测语义翻转/易混淆键:
|
|
70
|
+
* - tools.enabled: [] —— 旧版含义「启用全部」已改为「禁用全部」(fail-closed),
|
|
71
|
+
* 老用户升级后会静默失去全部工具,必须在 Web 界面可见(不只落在 stderr)。
|
|
72
|
+
* - slashCommands.enabled: [] —— 旧版含义「全部启用」已同步改为「禁用全部」
|
|
73
|
+
* (与 tools.enabled 语义统一),升级后斜杠命令将全部失效,须显式引导。
|
|
74
|
+
*/
|
|
75
|
+
export declare function collectConfigMigrationWarnings(scope: 'global' | 'project', data: Record<string, unknown> | undefined): string[];
|
|
34
76
|
/** Config 全部顶层键(DEFAULT_CONFIG + 可选键)。未知顶层键校验与告警共用。 */
|
|
35
77
|
declare const KNOWN_CONFIG_KEYS: Set<string>;
|
|
36
78
|
/** 收集对象中不属于 Config 顶层键的未知键。 */
|
|
@@ -38,4 +80,4 @@ declare function collectUnknownConfigKeys(data: Record<string, unknown> | undefi
|
|
|
38
80
|
/** 加载配置时对未知顶层键告警(拼写错误/旧版键静默失效的唯一提示)。 */
|
|
39
81
|
declare function warnUnknownConfigKeys(scope: 'global' | 'project', data: Record<string, unknown> | undefined): void;
|
|
40
82
|
export type { AgentsConfig, CompactionConfig, Config, MCPServerConfig, PermissionConfig, SecurityConfig, ToolMetricsConfig, UpdateConfig, WebSearchConfig, };
|
|
41
|
-
export { applyScopedPatch, collectUnknownConfigKeys, DEFAULT_CONFIG, KNOWN_CONFIG_KEYS, loadConfig, loadConfigScopes, mergeConfig, mergeRaw, saveConfigScoped, warnUnknownConfigKeys, };
|
|
83
|
+
export { applyScopedPatch, collectUnknownConfigKeys, DEFAULT_CONFIG, GLOBAL_ONLY_USAGE_KEYS, KNOWN_CONFIG_KEYS, loadConfig, loadConfigScopes, loadProjectRawScope, mergeConfig, mergeRaw, projectGlobalOnlyUsageKeys, projectSecurityKeys, resolveGlobalConfigDir, saveConfigScoped, stripProjectServerKeys, warnUnknownConfigKeys, };
|
package/dist/core/config.js
CHANGED
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
|
-
import { join } from 'node:path';
|
|
3
|
+
import { isAbsolute, join, resolve } from 'node:path';
|
|
4
4
|
import { encryptSecret, isEncryptedSecret } from './secret.js';
|
|
5
5
|
const GLOBAL_CONFIG_DIR = '.c0de';
|
|
6
6
|
const CONFIG_FILENAME = 'config.json';
|
|
7
|
+
/**
|
|
8
|
+
* 全局配置根目录:默认 `~/.c0de`;C0DE_CONFIG_DIR 环境变量可整体重定向
|
|
9
|
+
* (相对路径按进程 cwd 解析),全局工作流/插件同样落于此根下。
|
|
10
|
+
* 与 C0DE_DB_DIR 同口径——开发隔离场景把全局配置从安装版默认位置切到独立目录。
|
|
11
|
+
*/
|
|
12
|
+
function resolveGlobalConfigDir() {
|
|
13
|
+
const envDir = process.env.C0DE_CONFIG_DIR?.trim();
|
|
14
|
+
if (envDir)
|
|
15
|
+
return isAbsolute(envDir) ? envDir : resolve(envDir);
|
|
16
|
+
return join(homedir(), GLOBAL_CONFIG_DIR);
|
|
17
|
+
}
|
|
7
18
|
const DEFAULT_CONFIG = {
|
|
8
19
|
providers: [],
|
|
9
20
|
defaultProvider: 'openai',
|
|
@@ -17,9 +28,10 @@ const DEFAULT_CONFIG = {
|
|
|
17
28
|
keepRecentTokens: 4000,
|
|
18
29
|
midTurnEnabled: false,
|
|
19
30
|
},
|
|
20
|
-
// enabled
|
|
21
|
-
//
|
|
22
|
-
|
|
31
|
+
// enabled 语义(P1-1 修复「空=全部」安全陷阱):
|
|
32
|
+
// ['*'](通配)= 启用全部注册工具(默认);[] = 无工具(fail-closed);
|
|
33
|
+
// 非空名单 = 默认工具集。CLI print 与 Web chat 统一经 resolveEnabledToolNames 解析。
|
|
34
|
+
tools: { enabled: ['*'], disabled: [] },
|
|
23
35
|
plugins: { enabled: [] },
|
|
24
36
|
mcpServers: [],
|
|
25
37
|
slashCommands: {
|
|
@@ -28,8 +40,15 @@ const DEFAULT_CONFIG = {
|
|
|
28
40
|
toolMetrics: { enabled: true, threshold: 0.8, minSamples: 5 },
|
|
29
41
|
security: { authEnabled: true, allowedOrigins: [] },
|
|
30
42
|
websearch: { provider: 'auto' },
|
|
31
|
-
agents: {
|
|
32
|
-
permission: { defaultMode: 'default' },
|
|
43
|
+
agents: { subagentConcurrency: 3 },
|
|
44
|
+
permission: { defaultMode: 'default', timeoutAction: 'pause' },
|
|
45
|
+
usage: {
|
|
46
|
+
monthlyBudgetUsd: 0,
|
|
47
|
+
globalMonthlyBudgetUsd: 0,
|
|
48
|
+
monthlyTokenBudget: 0,
|
|
49
|
+
globalMonthlyTokenBudget: 0,
|
|
50
|
+
budgetAction: 'warn',
|
|
51
|
+
},
|
|
33
52
|
update: { enabled: true, intervalMs: 60 * 60 * 1000, initialDelayMs: 10_000 },
|
|
34
53
|
theme: 'system',
|
|
35
54
|
};
|
|
@@ -70,15 +89,95 @@ function readJsonIfExists(path) {
|
|
|
70
89
|
return undefined;
|
|
71
90
|
}
|
|
72
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* usage 下「全局口径」预算键——仅 global 作用域生效。项目作用域写入这些键
|
|
94
|
+
* 会被忽略(作用域收敛):此前「项目配置里设全局预算无意义但会生效」既造成
|
|
95
|
+
* 双预算语义混乱,又会随 git clone 传播、未被信任门禁拦截。收敛后项目作用域
|
|
96
|
+
* 的这两键不再进入任何合并视图,也不随 clone 影响本机护栏。
|
|
97
|
+
*/
|
|
98
|
+
const GLOBAL_ONLY_USAGE_KEYS = ['globalMonthlyBudgetUsd', 'globalMonthlyTokenBudget'];
|
|
99
|
+
/**
|
|
100
|
+
* 项目作用域中「服务端/全局生效、随 git clone 传播有安全风险」的顶层键:整体剥离。
|
|
101
|
+
* `security` 是服务级信任边界(鉴权开关 / token / CORS origin / 首设备窗口)——单进程
|
|
102
|
+
* 多项目架构下不存在「按项目」的安全参数。此前项目配置可静默改写服务端安全参数
|
|
103
|
+
* (如 authEnabled:false、security.token 静态弱 token、allowedOrigins 放宽),且这些键
|
|
104
|
+
* 不在信任门禁的风险 kind(trust.ts summarizeProjectRisk)内,克隆仓库即可无确认生效。
|
|
105
|
+
* 收敛为 global-only:security 仅从全局作用域读取。
|
|
106
|
+
*/
|
|
107
|
+
const PROJECT_SERVER_ONLY_KEYS = ['security'];
|
|
108
|
+
/** 从项目作用域原始配置中剥离「服务端/全局」键(security 顶层键 + usage 全局口径预算键)。
|
|
109
|
+
* 浅拷贝,不改入参;无剥离键时原样返回(保持身份,调用方可安全比较)。 */
|
|
110
|
+
function stripProjectServerKeys(project) {
|
|
111
|
+
if (!project)
|
|
112
|
+
return project;
|
|
113
|
+
let stripped = false;
|
|
114
|
+
const next = { ...project };
|
|
115
|
+
// 顶层「服务端/全局」键整体移除(见 PROJECT_SERVER_ONLY_KEYS)。
|
|
116
|
+
for (const k of PROJECT_SERVER_ONLY_KEYS) {
|
|
117
|
+
if (k in next) {
|
|
118
|
+
delete next[k];
|
|
119
|
+
stripped = true;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// usage 下全局口径预算键移除(仅 global 作用域生效)。
|
|
123
|
+
const usage = project.usage;
|
|
124
|
+
if (typeof usage === 'object' && usage !== null && !Array.isArray(usage)) {
|
|
125
|
+
const u = { ...usage };
|
|
126
|
+
for (const k of GLOBAL_ONLY_USAGE_KEYS) {
|
|
127
|
+
if (k in u) {
|
|
128
|
+
delete u[k];
|
|
129
|
+
stripped = true;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
next.usage = u;
|
|
133
|
+
}
|
|
134
|
+
if (!stripped)
|
|
135
|
+
return project;
|
|
136
|
+
return next;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* 返回项目作用域**原始文件**中出现的全局口径预算键名(strip 前的原始内容)。
|
|
140
|
+
* 供设置页告警展示:这些键来自旧版本或手动编辑,加载时已被剥离、不再生效。
|
|
141
|
+
*/
|
|
142
|
+
function projectGlobalOnlyUsageKeys(projectDir) {
|
|
143
|
+
const raw = readJsonIfExists(join(projectDir ?? process.cwd(), '.c0de', CONFIG_FILENAME));
|
|
144
|
+
const usage = raw?.usage;
|
|
145
|
+
if (typeof usage !== 'object' || usage === null || Array.isArray(usage))
|
|
146
|
+
return [];
|
|
147
|
+
return GLOBAL_ONLY_USAGE_KEYS.filter((k) => k in usage);
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* 返回项目作用域**原始文件**中出现的 security 子键名(strip 前的原始内容)。
|
|
151
|
+
* 供设置页告警展示:security 是服务端全局参数,随项目配置出现时已被剥离、不再生效。
|
|
152
|
+
*/
|
|
153
|
+
function projectSecurityKeys(projectDir) {
|
|
154
|
+
const raw = readJsonIfExists(join(projectDir ?? process.cwd(), '.c0de', CONFIG_FILENAME));
|
|
155
|
+
const sec = raw?.security;
|
|
156
|
+
if (typeof sec !== 'object' || sec === null || Array.isArray(sec))
|
|
157
|
+
return [];
|
|
158
|
+
return Object.keys(sec);
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* 读取项目作用域原始文件内容(不剥离 security/全局键)。
|
|
162
|
+
* 供「文件原文」敏感的检查使用——被剥离的键仍可能携带明文密钥(如 security.token),
|
|
163
|
+
* 配置落在 git 仓库内同样要防误提交,不能因键不生效而漏掉警告。
|
|
164
|
+
*/
|
|
165
|
+
function loadProjectRawScope(projectDir) {
|
|
166
|
+
return readJsonIfExists(join(projectDir ?? process.cwd(), '.c0de', CONFIG_FILENAME));
|
|
167
|
+
}
|
|
73
168
|
/**
|
|
74
169
|
* 读取 global/project 两个作用域的**原始文件内容**(不经 DEFAULT 合并)。
|
|
75
170
|
* 供配置持久化使用:写回某个作用域时只落该作用域应有的键,
|
|
76
171
|
* 避免把合并结果(含默认值与另一作用域的配置)整体序列化进文件。
|
|
172
|
+
* 项目作用域经 stripProjectServerKeys 收敛——security 与全局口径预算键只会从 global 读。
|
|
77
173
|
*/
|
|
78
174
|
function loadConfigScopes(projectDir) {
|
|
79
|
-
const globalPath = join(
|
|
175
|
+
const globalPath = join(resolveGlobalConfigDir(), CONFIG_FILENAME);
|
|
80
176
|
const projectPath = join(projectDir ?? process.cwd(), '.c0de', CONFIG_FILENAME);
|
|
81
|
-
return {
|
|
177
|
+
return {
|
|
178
|
+
global: readJsonIfExists(globalPath),
|
|
179
|
+
project: stripProjectServerKeys(readJsonIfExists(projectPath)),
|
|
180
|
+
};
|
|
82
181
|
}
|
|
83
182
|
/**
|
|
84
183
|
* 把 patch 应用到某个作用域的原始配置(scoped patch,null=删除):
|
|
@@ -145,9 +244,7 @@ function mergeRaw(...cfgs) {
|
|
|
145
244
|
* CLI config set 与 /config 斜杠命令共用此路径,保证「apiKey 不明文落盘」全链路成立。
|
|
146
245
|
* - 写入后 chmod 600:配置文件可能含 token/加密密钥,默认 0644 同机可读。 */
|
|
147
246
|
async function saveConfigScoped(scope, projectDir, data) {
|
|
148
|
-
const dir = scope === 'global'
|
|
149
|
-
? join(homedir(), GLOBAL_CONFIG_DIR)
|
|
150
|
-
: join(projectDir ?? process.cwd(), '.c0de');
|
|
247
|
+
const dir = scope === 'global' ? resolveGlobalConfigDir() : join(projectDir ?? process.cwd(), '.c0de');
|
|
151
248
|
if (!existsSync(dir))
|
|
152
249
|
mkdirSync(dir, { recursive: true });
|
|
153
250
|
const path = join(dir, CONFIG_FILENAME);
|
|
@@ -194,14 +291,66 @@ function redactSensitiveOnSave(data) {
|
|
|
194
291
|
return out;
|
|
195
292
|
}
|
|
196
293
|
async function loadConfig(projectDir) {
|
|
197
|
-
const globalPath = join(
|
|
294
|
+
const globalPath = join(resolveGlobalConfigDir(), CONFIG_FILENAME);
|
|
198
295
|
const projectPath = join(projectDir ?? process.cwd(), '.c0de', CONFIG_FILENAME);
|
|
199
296
|
const global = readJsonIfExists(globalPath);
|
|
200
|
-
|
|
297
|
+
// 与 loadConfigScopes 同口径剥离:security 与 usage 全局口径预算键不进合并视图
|
|
298
|
+
// (此前 loadConfig 不剥离,服务启动目录项目的 security/全局预算键会静默生效——
|
|
299
|
+
// 安全键可被克隆仓库无确认改写,全局预算可被项目作用域注入)。
|
|
300
|
+
const projectRaw = readJsonIfExists(projectPath);
|
|
301
|
+
const project = stripProjectServerKeys(projectRaw);
|
|
302
|
+
// 项目配置遗留 security 键(旧版本/手动编辑/克隆仓库自带)→ CLI 侧明示已忽略,
|
|
303
|
+
// 与 Web 设置页 securityWarnings 同口径(stderr 可见,不必等到打开设置页)。
|
|
304
|
+
const rawSecurity = projectRaw?.security;
|
|
305
|
+
if (typeof rawSecurity === 'object' && rawSecurity !== null && !Array.isArray(rawSecurity)) {
|
|
306
|
+
const keys = Object.keys(rawSecurity);
|
|
307
|
+
if (keys.length > 0) {
|
|
308
|
+
console.warn(`[config] 项目配置含 security 键(${keys.join('、')},服务端全局参数),已忽略——` +
|
|
309
|
+
`请在全局配置(${join(resolveGlobalConfigDir(), CONFIG_FILENAME)})或 c0de config set --global 设置。`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
201
312
|
warnUnknownConfigKeys('global', global);
|
|
202
313
|
warnUnknownConfigKeys('project', project);
|
|
314
|
+
for (const [scope, data] of [
|
|
315
|
+
['global', global],
|
|
316
|
+
['project', project],
|
|
317
|
+
]) {
|
|
318
|
+
for (const w of collectConfigMigrationWarnings(scope, data))
|
|
319
|
+
console.warn(`[config] ${w}`);
|
|
320
|
+
}
|
|
203
321
|
return mergeConfig(global, project);
|
|
204
322
|
}
|
|
323
|
+
/**
|
|
324
|
+
* P0-1:配置迁移告警(返回值,供 Web 设置页 warnings 展示;CLI 侧 console.warn
|
|
325
|
+
* 复用)。检测语义翻转/易混淆键:
|
|
326
|
+
* - tools.enabled: [] —— 旧版含义「启用全部」已改为「禁用全部」(fail-closed),
|
|
327
|
+
* 老用户升级后会静默失去全部工具,必须在 Web 界面可见(不只落在 stderr)。
|
|
328
|
+
* - slashCommands.enabled: [] —— 旧版含义「全部启用」已同步改为「禁用全部」
|
|
329
|
+
* (与 tools.enabled 语义统一),升级后斜杠命令将全部失效,须显式引导。
|
|
330
|
+
*/
|
|
331
|
+
export function collectConfigMigrationWarnings(scope, data) {
|
|
332
|
+
if (!data)
|
|
333
|
+
return [];
|
|
334
|
+
const scopeLabel = scope === 'global' ? '全局' : '项目';
|
|
335
|
+
const out = [];
|
|
336
|
+
const tools = data.tools;
|
|
337
|
+
if (typeof tools === 'object' && tools !== null && !Array.isArray(tools)) {
|
|
338
|
+
const enabled = tools.enabled;
|
|
339
|
+
if (Array.isArray(enabled) && enabled.length === 0) {
|
|
340
|
+
out.push(`${scopeLabel}配置的 tools.enabled 为空数组:旧版含义「启用全部」已改为「禁用全部」——` +
|
|
341
|
+
`如需启用全部,请改为 ["*"],或删除该键恢复默认值。`);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
const slash = data.slashCommands;
|
|
345
|
+
if (typeof slash === 'object' && slash !== null && !Array.isArray(slash)) {
|
|
346
|
+
const enabled = slash.enabled;
|
|
347
|
+
if (Array.isArray(enabled) && enabled.length === 0) {
|
|
348
|
+
out.push(`${scopeLabel}配置的 slashCommands.enabled 为空数组:旧版含义「全部启用」已改为「禁用全部」` +
|
|
349
|
+
`(与 tools.enabled 语义统一)——如需启用全部,请改为 ["*"],或删除该键恢复默认值。`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return out;
|
|
353
|
+
}
|
|
205
354
|
/** Config 全部顶层键(DEFAULT_CONFIG + 可选键)。未知顶层键校验与告警共用。 */
|
|
206
355
|
const KNOWN_CONFIG_KEYS = new Set([
|
|
207
356
|
...Object.keys(DEFAULT_CONFIG),
|
|
@@ -222,4 +371,4 @@ function warnUnknownConfigKeys(scope, data) {
|
|
|
222
371
|
console.warn(`[config] ${scope} 配置包含未知键:${unknown.join(', ')}。` +
|
|
223
372
|
`这些键不会生效——请检查拼写,或移除过时配置。有效顶层键:${[...KNOWN_CONFIG_KEYS].join(', ')}`);
|
|
224
373
|
}
|
|
225
|
-
export { applyScopedPatch, collectUnknownConfigKeys, DEFAULT_CONFIG, KNOWN_CONFIG_KEYS, loadConfig, loadConfigScopes, mergeConfig, mergeRaw, saveConfigScoped, warnUnknownConfigKeys, };
|
|
374
|
+
export { applyScopedPatch, collectUnknownConfigKeys, DEFAULT_CONFIG, GLOBAL_ONLY_USAGE_KEYS, KNOWN_CONFIG_KEYS, loadConfig, loadConfigScopes, loadProjectRawScope, mergeConfig, mergeRaw, projectGlobalOnlyUsageKeys, projectSecurityKeys, resolveGlobalConfigDir, saveConfigScoped, stripProjectServerKeys, warnUnknownConfigKeys, };
|
|
@@ -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
|
}
|
|
@@ -57,41 +57,74 @@ export async function runSubAgent(deps, parent, request) {
|
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
const childCwd = worktreePath ?? deps.cwd;
|
|
60
|
-
//
|
|
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 槽位泄漏。
|
|
61
118
|
const runBody = async () => {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const childDepth = parentDepth + 1;
|
|
65
|
-
const declaredTools = def.tools ?? parent.config.tools;
|
|
66
|
-
const maxRec = def.maxRecursion ?? 0;
|
|
67
|
-
const baseTools = childDepth > maxRec ? declaredTools.filter((t) => t !== 'task') : declaredTools;
|
|
68
|
-
const childTools = Array.from(new Set([...baseTools, 'yield']));
|
|
69
|
-
const childConfig = {
|
|
70
|
-
...parent.config,
|
|
71
|
-
systemPrompt: def.systemPrompt,
|
|
72
|
-
// 子 agent 走整段 systemPrompt 替换,清除父的 role override 避免干扰
|
|
73
|
-
agentRolePrompt: undefined,
|
|
74
|
-
tools: childTools,
|
|
75
|
-
...(def.model ? { model: def.model } : {}),
|
|
76
|
-
...(request.model ? { model: request.model } : {}),
|
|
77
|
-
};
|
|
78
|
-
// 子 agent 的 deps:覆盖 cwd(worktree)+ 注入 yield 收集器 + 递归深度
|
|
79
|
-
const childDeps = {
|
|
80
|
-
...deps,
|
|
81
|
-
cwd: childCwd,
|
|
82
|
-
_subagentYieldCollector: (data) => {
|
|
83
|
-
yielded.push(data);
|
|
84
|
-
},
|
|
85
|
-
_subagentDepth: childDepth,
|
|
86
|
-
};
|
|
87
|
-
const childState = await createAgent(childSession, childConfig, childDeps);
|
|
88
|
-
// abort 链接:父 abort 则子 abort
|
|
89
|
-
if (parent.abortController.signal.aborted) {
|
|
90
|
-
childState.abortController.abort();
|
|
119
|
+
try {
|
|
120
|
+
return await runChildBody(childState, childDeps, childSession, title, baseline, worktreePath);
|
|
91
121
|
}
|
|
92
|
-
|
|
93
|
-
|
|
122
|
+
finally {
|
|
123
|
+
unregisterChild?.();
|
|
94
124
|
}
|
|
125
|
+
};
|
|
126
|
+
// 子 agent loop 执行体:运行 loop → worktree delta 回传 → 发射结束事件。
|
|
127
|
+
const runChildBody = async (childState, childDeps, childSession, title, baseline, worktreePath) => {
|
|
95
128
|
// 运行子 agent loop
|
|
96
129
|
const childPrompt = request.context
|
|
97
130
|
? `CONTEXT\n${request.context}\n\nASSIGNMENT\n${request.prompt}`
|
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),
|
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 };
|