evolclaw 3.2.0 → 3.3.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/CHANGELOG.md +17 -0
- package/README.md +1 -2
- package/dist/agents/{resolve.js → baseagent.js} +34 -5
- package/dist/agents/claude-runner.js +120 -27
- package/dist/agents/codex-app-server-client.js +364 -0
- package/dist/agents/codex-runner.js +1069 -141
- package/dist/agents/gemini-runner.js +2 -2
- package/dist/agents/runner-types.js +28 -0
- package/dist/aun/aid/store.js +1 -1
- package/dist/aun/storage/download.js +1 -1
- package/dist/aun/storage/upload.js +13 -1
- package/dist/channels/aun.js +406 -293
- package/dist/channels/dingtalk.js +77 -140
- package/dist/channels/feishu.js +97 -150
- package/dist/channels/qqbot.js +75 -138
- package/dist/channels/wechat.js +75 -136
- package/dist/channels/wecom.js +75 -138
- package/dist/cli/agent.js +8 -5
- package/dist/cli/index.js +177 -44
- package/dist/cli/init.js +33 -6
- package/dist/cli/model.js +1 -1
- package/dist/cli/stats.js +558 -0
- package/dist/cli/version.js +87 -0
- package/dist/cli/watch-msg.js +5 -2
- package/dist/config-store.js +12 -6
- package/dist/core/channel-loader.js +84 -82
- package/dist/core/command-handler.js +473 -114
- package/dist/core/evolagent-registry.js +1 -0
- package/dist/core/evolagent.js +1 -1
- package/dist/core/interaction-router.js +8 -0
- package/dist/core/message/command-handler-agent-control.js +63 -1
- package/dist/core/message/im-renderer.js +35 -13
- package/dist/core/message/items-formatter.js +9 -1
- package/dist/core/message/message-bridge.js +49 -21
- package/dist/core/message/message-log.js +1 -0
- package/dist/core/message/message-processor.js +295 -35
- package/dist/core/message/message-queue.js +2 -2
- package/dist/core/message/pending-hints.js +232 -0
- package/dist/core/message/response-depth.js +56 -0
- package/dist/core/model/model-catalog.js +1 -1
- package/dist/core/model/model-scope.js +2 -2
- package/dist/core/permission.js +9 -12
- package/dist/core/relation/peer-identity.js +16 -1
- package/dist/core/session/adapters/codex-session-file-adapter.js +4 -2
- package/dist/core/session/session-manager.js +27 -13
- package/dist/core/session/session-title.js +26 -0
- package/dist/core/stats/billing.js +151 -0
- package/dist/core/stats/budget.js +93 -0
- package/dist/core/stats/db.js +314 -0
- package/dist/core/stats/eck-vars.js +84 -0
- package/dist/core/stats/index.js +10 -0
- package/dist/core/stats/normalizer.js +78 -0
- package/dist/core/stats/query.js +760 -0
- package/dist/core/stats/writer.js +115 -0
- package/dist/core/trigger/manager.js +34 -0
- package/dist/core/trigger/parser.js +9 -3
- package/dist/core/trigger/scheduler.js +20 -17
- package/dist/{agents → eck}/manifest-engine.js +20 -1
- package/dist/{agents → eck}/message-renderer.js +24 -1
- package/dist/index.js +130 -8
- package/dist/ipc.js +17 -1
- package/dist/utils/cross-platform.js +23 -5
- package/dist/utils/ecweb-pair.js +20 -0
- package/dist/utils/stats.js +14 -0
- package/kits/docs/evolclaw/INDEX.md +3 -1
- package/kits/docs/evolclaw/fs-architecture.md +1215 -0
- package/kits/docs/evolclaw/fs.md +131 -0
- package/kits/docs/evolclaw/group-fs.md +209 -0
- package/kits/docs/evolclaw/stats.md +70 -0
- package/kits/docs/venues/aun-group.md +29 -6
- package/kits/docs/venues/group.md +5 -4
- package/kits/eck_manifest.json +12 -0
- package/kits/eck_message_manifest.json +30 -3
- package/kits/rules/05-venue.md +1 -1
- package/kits/templates/message-fragments/inject-default.md +2 -0
- package/kits/templates/system-fragments/response-depth.md +16 -0
- package/package.json +4 -4
- package/dist/agents/baseagent-normalize.js +0 -19
- package/dist/core/relation/peer-key.js +0 -16
- package/dist/evolclaw-config.js +0 -11
- package/dist/utils/channel-helpers.js +0 -46
- /package/dist/core/{cache/file-cache.js → daemon-file-cache.js} +0 -0
- /package/dist/{agents → eck}/kit-renderer.js +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v3.3.0 (2026-06-10)
|
|
4
|
+
|
|
5
|
+
### New Features
|
|
6
|
+
|
|
7
|
+
- **Thread 会话继承 baseagent** — 创建 thread 会话时自动继承父会话的 `agentId` 及 baseagent 配置,多线程场景无需重新切换
|
|
8
|
+
- **Codex runner 增强** — 新增 CLI 版本检测、streaming delta 支持、server 可用性检查;codex-app-server-client 补充类型定义
|
|
9
|
+
- **Menu 话题管理** — command-handler 新增话题菜单的权限判定(`canReadTopics`/`canDeleteTopic`)与格式化(`buildTopicMenuItem`/`resolveMenuChatType`)
|
|
10
|
+
|
|
11
|
+
### Improvements
|
|
12
|
+
|
|
13
|
+
- **Channel plugin 接口统一** — `ChannelPlugin` 从 `isEnabled/createChannel/createChannels` 收敛为单一 `createInstance(inst, ctx)` 单实例模型,新增 `ChannelBuildContext` 与 `showActivities` 共享策略;六个渠道(aun/feishu/wechat/dingtalk/qqbot/wecom)同步迁移
|
|
14
|
+
- **ECWeb 控制台重构** — control source 拆分为 `system`(evolclaw/fastaun/evolclaw-web 三包版本与健康检查)和 `triggers`(定时任务管理),前端联动更新
|
|
15
|
+
- **Trigger 失败统计** — Trigger 新增 `failCount`/`lastResult` 字段,scheduler/manager/parser 同步
|
|
16
|
+
- **Runner 类型模块化** — 抽出 `runner-types.ts` 统一共享类型,消除各 runner 重复声明
|
|
17
|
+
- **Agent AID 展示** — `AgentInfo` 新增 `aid` 字段,`ec agent` 命令展示 agent AID
|
|
18
|
+
- **缓存模块更名** — `read-cache` 重命名为 `daemon-file-cache`,语义更清晰
|
|
19
|
+
|
|
3
20
|
## v3.2.0 (2026-06-05)
|
|
4
21
|
|
|
5
22
|
### New Features
|
package/README.md
CHANGED
|
@@ -250,7 +250,6 @@ evolclaw/
|
|
|
250
250
|
- `/perm [mode]` - 查看或切换权限模式(auto / edit / default / readonly)
|
|
251
251
|
|
|
252
252
|
**系统管理**:
|
|
253
|
-
- `/clear` - 清空对话历史
|
|
254
253
|
- `/compact` - 压缩会话上下文
|
|
255
254
|
- `/rewind <turn>` - 回退会话到指定轮次
|
|
256
255
|
- `/stop` - 中断当前任务
|
|
@@ -296,7 +295,7 @@ v3.2 新增进程级身份标识。启动时自动生成 `ec+5位数字.agentid.
|
|
|
296
295
|
## 技术栈
|
|
297
296
|
|
|
298
297
|
- **运行时**:Node.js >= 22 + TypeScript(ES modules)
|
|
299
|
-
- **AI
|
|
298
|
+
- **AI 后端**:@anthropic-ai/claude-agent-sdk >= 0.2.100、Codex CLI app-server、Gemini CLI
|
|
300
299
|
- **消息渠道**:飞书(@larksuiteoapi/node-sdk)、微信(ClawBot ilink API)、钉钉(dingtalk-stream)、QQ频道(pure-qqbot)、企业微信(AI Bot API)、AUN 网络
|
|
301
300
|
- **数据存储**:文件系统(per-chat 目录) + JSONL(CLI 共用)
|
|
302
301
|
- **测试框架**:Vitest
|
|
@@ -1,14 +1,37 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Baseagent credential
|
|
2
|
+
* Baseagent identity + credential resolution.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* 两部分:
|
|
5
|
+
* 1. normalizeBaseagent —— 把用户输入的各种别名(cc / claude-code / gemini cli …)
|
|
6
|
+
* 归一到 canonical 标识 + 展示名。
|
|
7
|
+
* 2. resolve*Config —— 各后端的凭证解析。输入是 Config 形态
|
|
8
|
+
* (`config.agents.<baseagent>` + override)。启动期由 index.ts 从
|
|
9
|
+
* primaryAgent.config.baseagents 构造一个 syntheticConfig 喂入;各 plugin 的
|
|
10
|
+
* createAgent 也各自构造 syntheticConfig。
|
|
7
11
|
*/
|
|
8
12
|
import fs from 'fs';
|
|
9
13
|
import path from 'path';
|
|
10
14
|
import os from 'os';
|
|
11
15
|
import { commandExists } from '../utils/cross-platform.js';
|
|
16
|
+
const BASEAGENT_ALIASES = {
|
|
17
|
+
claude: { canonical: 'claude', displayName: 'Claude Code' },
|
|
18
|
+
cc: { canonical: 'claude', displayName: 'Claude Code' },
|
|
19
|
+
'claude-code': { canonical: 'claude', displayName: 'Claude Code' },
|
|
20
|
+
'claude code': { canonical: 'claude', displayName: 'Claude Code' },
|
|
21
|
+
claudecode: { canonical: 'claude', displayName: 'Claude Code' },
|
|
22
|
+
codex: { canonical: 'codex', displayName: 'Codex' },
|
|
23
|
+
'codex-cli': { canonical: 'codex', displayName: 'Codex' },
|
|
24
|
+
'codex cli': { canonical: 'codex', displayName: 'Codex' },
|
|
25
|
+
gemini: { canonical: 'gemini', displayName: 'Gemini CLI' },
|
|
26
|
+
'gemini-cli': { canonical: 'gemini', displayName: 'Gemini CLI' },
|
|
27
|
+
'gemini cli': { canonical: 'gemini', displayName: 'Gemini CLI' },
|
|
28
|
+
geminicli: { canonical: 'gemini', displayName: 'Gemini CLI' },
|
|
29
|
+
hermes: { canonical: 'hermes', displayName: 'Hermes' },
|
|
30
|
+
};
|
|
31
|
+
export function normalizeBaseagent(input) {
|
|
32
|
+
const key = String(input || '').trim().toLowerCase().replace(/_/g, '-');
|
|
33
|
+
return BASEAGENT_ALIASES[key] || { canonical: 'unknown', displayName: input ? String(input) : 'Unknown' };
|
|
34
|
+
}
|
|
12
35
|
function loadClaudeSettings() {
|
|
13
36
|
try {
|
|
14
37
|
const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
|
|
@@ -109,7 +132,13 @@ export function resolveOpenaiConfig(config, override) {
|
|
|
109
132
|
|| config.agents?.codex?.effort
|
|
110
133
|
|| config.agents?.codex?.reasoning
|
|
111
134
|
|| undefined;
|
|
112
|
-
|
|
135
|
+
const enableRequestUserInput = override?.enableRequestUserInput
|
|
136
|
+
?? config.agents?.codex?.enableRequestUserInput
|
|
137
|
+
?? true;
|
|
138
|
+
const approvalsReviewer = override?.approvalsReviewer
|
|
139
|
+
?? config.agents?.codex?.approvalsReviewer
|
|
140
|
+
?? undefined;
|
|
141
|
+
return { apiKey, baseUrl, model, effort, enableRequestUserInput, approvalsReviewer };
|
|
113
142
|
}
|
|
114
143
|
export function resolveGoogleConfig(config, override) {
|
|
115
144
|
const googleCfg = config.agents?.gemini;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { query, forkSession as sdkForkSession, getSessionMessages as sdkGetSessionMessages } from '@anthropic-ai/claude-agent-sdk';
|
|
2
2
|
import { ensureDir } from '../utils/atomic-write.js';
|
|
3
|
-
import { resolveAnthropicConfig } from './
|
|
3
|
+
import { resolveAnthropicConfig } from './baseagent.js';
|
|
4
4
|
import { DEFAULT_PERMISSION_MODE } from '../types.js';
|
|
5
5
|
import { renderActionAsText } from '../core/interaction-router.js';
|
|
6
6
|
import { buildEnvelope, sendInteractionPayload } from '../core/message/message-processor.js';
|
|
@@ -10,6 +10,8 @@ import os from 'os';
|
|
|
10
10
|
import { logger } from '../utils/logger.js';
|
|
11
11
|
import { checkBlacklist, checkReadonly, summarizeToolInput } from '../core/permission.js';
|
|
12
12
|
import { encodePath } from '../utils/cross-platform.js';
|
|
13
|
+
import { contextTokensForUsage, usageForContext } from './runner-types.js';
|
|
14
|
+
export { hasCompact, hasModelSwitcher, hasPermissionController } from './runner-types.js';
|
|
13
15
|
// ── 模型别名解析 ──
|
|
14
16
|
// SDK 内置的别名表可能落后于代理实际可用的最新模型,
|
|
15
17
|
// 因此优先从 {baseUrl}/models 动态获取各系列最新版本,失败则回退静态表。
|
|
@@ -110,9 +112,21 @@ function applyContextWindow(modelId) {
|
|
|
110
112
|
return `${modelId}[1m]`;
|
|
111
113
|
return modelId;
|
|
112
114
|
}
|
|
113
|
-
/**
|
|
114
|
-
function
|
|
115
|
-
|
|
115
|
+
/** 真实上下文窗口大小:1M 模型 = 1000000,否则 200000 */
|
|
116
|
+
function realContextWindowFor(sdkModel) {
|
|
117
|
+
if (/\[1m\]$/.test(sdkModel))
|
|
118
|
+
return 1000000;
|
|
119
|
+
if (/deepseek-v4/i.test(sdkModel))
|
|
120
|
+
return 1000000; // deepseek-v4 系列原生 1M 窗口(无 [1m] 后缀)
|
|
121
|
+
return 200000;
|
|
122
|
+
}
|
|
123
|
+
/** autoCompact 触发阈值:1M 模型 = 900000(留 ~100k buffer),否则 200000 */
|
|
124
|
+
function autoCompactWindowFor(sdkModel) {
|
|
125
|
+
if (/\[1m\]$/.test(sdkModel))
|
|
126
|
+
return 900000;
|
|
127
|
+
if (/deepseek-v4/i.test(sdkModel))
|
|
128
|
+
return 900000;
|
|
129
|
+
return 200000;
|
|
116
130
|
}
|
|
117
131
|
/** 解析别名 + 追加 1M 后缀,得到最终交给 SDK 的 model 串。 */
|
|
118
132
|
function resolveSdkModel(model, baseUrl) {
|
|
@@ -168,19 +182,9 @@ class MessageStream {
|
|
|
168
182
|
}
|
|
169
183
|
}
|
|
170
184
|
}
|
|
171
|
-
// ── 类型守卫 ──
|
|
172
|
-
export function hasModelSwitcher(agent) {
|
|
173
|
-
return typeof agent.setModel === 'function' && typeof agent.listModels === 'function';
|
|
174
|
-
}
|
|
175
|
-
export function hasPermissionController(agent) {
|
|
176
|
-
return typeof agent.setMode === 'function' && typeof agent.listModes === 'function';
|
|
177
|
-
}
|
|
178
|
-
export function hasCompact(agent) {
|
|
179
|
-
return typeof agent.compact === 'function';
|
|
180
|
-
}
|
|
181
185
|
export class AgentRunner {
|
|
182
186
|
name = 'claude';
|
|
183
|
-
capabilities = { clear: true, compact: true, fork: true };
|
|
187
|
+
capabilities = { clear: true, compact: true, fork: true, askUserQuestion: true, planApproval: true, fileRewind: 'checkpoint' };
|
|
184
188
|
apiKey;
|
|
185
189
|
model;
|
|
186
190
|
effort;
|
|
@@ -383,7 +387,6 @@ export class AgentRunner {
|
|
|
383
387
|
},
|
|
384
388
|
channelId: permCtx.channelId,
|
|
385
389
|
sessionId,
|
|
386
|
-
expiresAt: Date.now() + 5 * 60 * 1000,
|
|
387
390
|
};
|
|
388
391
|
}
|
|
389
392
|
else {
|
|
@@ -411,11 +414,11 @@ export class AgentRunner {
|
|
|
411
414
|
},
|
|
412
415
|
channelId: permCtx.channelId,
|
|
413
416
|
sessionId,
|
|
414
|
-
expiresAt: Date.now() + 5 * 60 * 1000,
|
|
415
417
|
};
|
|
416
418
|
}
|
|
417
419
|
let cardSent = false;
|
|
418
420
|
try {
|
|
421
|
+
await permCtx.flushPending?.();
|
|
419
422
|
const envelope = buildEnvelope({
|
|
420
423
|
taskId: permCtx.taskId,
|
|
421
424
|
channel: permCtx.channel ?? permCtx.adapter.channelName,
|
|
@@ -433,6 +436,7 @@ export class AgentRunner {
|
|
|
433
436
|
logger.warn(`[AgentRunner] AskUserQuestion card send failed for q${i}:`, err);
|
|
434
437
|
}
|
|
435
438
|
if (!cardSent) {
|
|
439
|
+
await permCtx.flushPending?.();
|
|
436
440
|
const firstLabel = q.options[0]?.label || '';
|
|
437
441
|
answers[q.question] = q.multiSelect ? [firstLabel] : firstLabel;
|
|
438
442
|
if (sendPrompt) {
|
|
@@ -530,7 +534,7 @@ export class AgentRunner {
|
|
|
530
534
|
else {
|
|
531
535
|
resolve(action.trim());
|
|
532
536
|
}
|
|
533
|
-
}, {
|
|
537
|
+
}, { initiatorId: permCtx.userId, fallbackCommand: 'ask' });
|
|
534
538
|
});
|
|
535
539
|
answers[q.question] = answer;
|
|
536
540
|
}
|
|
@@ -606,6 +610,7 @@ export class AgentRunner {
|
|
|
606
610
|
},
|
|
607
611
|
};
|
|
608
612
|
try {
|
|
613
|
+
await permCtx.flushPending?.();
|
|
609
614
|
const envelope = buildEnvelope({
|
|
610
615
|
taskId: permCtx.taskId,
|
|
611
616
|
channel: permCtx.channel ?? permCtx.adapter.channelName,
|
|
@@ -663,6 +668,7 @@ export class AgentRunner {
|
|
|
663
668
|
buttonArgMap: { approve: '1', reject: '2' },
|
|
664
669
|
},
|
|
665
670
|
};
|
|
671
|
+
await permCtx.flushPending?.();
|
|
666
672
|
await sendPrompt(renderActionAsText(fallbackInteraction));
|
|
667
673
|
permCtx.interactionRouter.unmarkWaiting(sessionId);
|
|
668
674
|
return new Promise((resolve) => {
|
|
@@ -674,11 +680,12 @@ export class AgentRunner {
|
|
|
674
680
|
else {
|
|
675
681
|
resolve({ behavior: 'allow', updatedInput: input, decisionClassification: 'user_temporary' });
|
|
676
682
|
}
|
|
677
|
-
}, {
|
|
683
|
+
}, { initiatorId: permCtx.userId, fallbackCommand: 'ask' });
|
|
678
684
|
});
|
|
679
685
|
}
|
|
680
686
|
// 无交互能力,发提示后直接 allow
|
|
681
687
|
permCtx?.interactionRouter?.unmarkWaiting(sessionId);
|
|
688
|
+
await permCtx.flushPending?.();
|
|
682
689
|
await sendPrompt('📋 计划审批\nAI 已完成规划,自动批准执行。');
|
|
683
690
|
return { behavior: 'allow', updatedInput: input, decisionClassification: 'user_temporary' };
|
|
684
691
|
}
|
|
@@ -692,6 +699,9 @@ export class AgentRunner {
|
|
|
692
699
|
const toolUseNames = new Map();
|
|
693
700
|
let turnCount = 0;
|
|
694
701
|
const seenMessageIds = new Set();
|
|
702
|
+
let lastModelCall;
|
|
703
|
+
// 流式收集各次大模型调用(fallback:SDK iterations 为空时使用)
|
|
704
|
+
const collectedCalls = [];
|
|
695
705
|
try {
|
|
696
706
|
for await (const event of sdkStream) {
|
|
697
707
|
// 提取 session_id(任意 SDK 事件都可能携带)
|
|
@@ -700,6 +710,38 @@ export class AgentRunner {
|
|
|
700
710
|
this.updateSessionId(sessionId, event.session_id);
|
|
701
711
|
yield { type: 'session_id', sessionId: event.session_id };
|
|
702
712
|
}
|
|
713
|
+
if (event.type === 'stream_event') {
|
|
714
|
+
const streamEvent = event.event;
|
|
715
|
+
if (streamEvent?.type === 'message_start' && streamEvent.message?.usage) {
|
|
716
|
+
lastModelCall = {
|
|
717
|
+
uuid: event.uuid,
|
|
718
|
+
model: streamEvent.message.model,
|
|
719
|
+
tokenUsage: streamEvent.message.usage,
|
|
720
|
+
};
|
|
721
|
+
// 流式收集:每个 message_start = 一次新的大模型调用
|
|
722
|
+
collectedCalls.push({
|
|
723
|
+
call_index: collectedCalls.length,
|
|
724
|
+
model: streamEvent.message.model ?? callModel ?? this.model,
|
|
725
|
+
request_id: event.request_id,
|
|
726
|
+
tokenUsage: { ...streamEvent.message.usage },
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
else if (streamEvent?.type === 'message_delta' && streamEvent.usage) {
|
|
730
|
+
lastModelCall = {
|
|
731
|
+
...lastModelCall,
|
|
732
|
+
uuid: lastModelCall?.uuid ?? event.uuid,
|
|
733
|
+
tokenUsage: {
|
|
734
|
+
...(lastModelCall?.tokenUsage ?? {}),
|
|
735
|
+
...streamEvent.usage,
|
|
736
|
+
},
|
|
737
|
+
};
|
|
738
|
+
// 将 message_delta 的 usage 合并进当前(最后一次)收集的调用
|
|
739
|
+
const last = collectedCalls[collectedCalls.length - 1];
|
|
740
|
+
if (last)
|
|
741
|
+
last.tokenUsage = { ...last.tokenUsage, ...streamEvent.usage };
|
|
742
|
+
}
|
|
743
|
+
continue;
|
|
744
|
+
}
|
|
703
745
|
// system: compact_boundary → compact
|
|
704
746
|
if (event.type === 'system' && event.subtype === 'compact_boundary') {
|
|
705
747
|
yield {
|
|
@@ -730,6 +772,18 @@ export class AgentRunner {
|
|
|
730
772
|
seenMessageIds.add(msgId);
|
|
731
773
|
turnCount++;
|
|
732
774
|
}
|
|
775
|
+
if (event.message.usage) {
|
|
776
|
+
lastModelCall = {
|
|
777
|
+
...lastModelCall,
|
|
778
|
+
messageId: event.message.id,
|
|
779
|
+
requestId: event.request_id,
|
|
780
|
+
model: event.message.model,
|
|
781
|
+
tokenUsage: {
|
|
782
|
+
...event.message.usage,
|
|
783
|
+
...(lastModelCall?.tokenUsage ?? {}),
|
|
784
|
+
},
|
|
785
|
+
};
|
|
786
|
+
}
|
|
733
787
|
// 统计本轮 base agent 全部输出字符数(text + tool_use input)
|
|
734
788
|
let turnOutputChars = 0;
|
|
735
789
|
for (const content of event.message.content) {
|
|
@@ -790,19 +844,55 @@ export class AgentRunner {
|
|
|
790
844
|
const cleanResult = typeof event.result === 'string'
|
|
791
845
|
? event.result.replace(/<thinking>[\s\S]*?<\/thinking>\s*/g, '').trim()
|
|
792
846
|
: event.result;
|
|
793
|
-
// 从 usage
|
|
847
|
+
// 从 usage 求当前上下文占用。
|
|
848
|
+
// Claude:input_tokens 是净输入(不含 cache),三项求和 = 实际上下文长度。
|
|
849
|
+
// 非 Claude(DeepSeek/OpenAI 兼容):cache_read 是服务端 KV cache 不占上下文窗口,
|
|
850
|
+
// input_tokens 本身就是完整的上下文输入量。
|
|
794
851
|
const u = event.usage;
|
|
795
|
-
const
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
const
|
|
852
|
+
const effectiveModel = callModel ?? this.model;
|
|
853
|
+
const isClaudeModel = effectiveModel?.startsWith('claude');
|
|
854
|
+
const totalTokens = contextTokensForUsage(u, !!isClaudeModel);
|
|
855
|
+
const contextWindowTokens = sdkModel ? realContextWindowFor(sdkModel) : 200000;
|
|
856
|
+
const autoCompactTokens = sdkModel ? autoCompactWindowFor(sdkModel) : 200000;
|
|
799
857
|
const contextUsage = totalTokens > 0 ? {
|
|
800
858
|
totalTokens,
|
|
801
|
-
maxTokens,
|
|
802
|
-
percentage: Math.round((totalTokens /
|
|
859
|
+
maxTokens: contextWindowTokens,
|
|
860
|
+
percentage: Math.round((totalTokens / contextWindowTokens) * 100),
|
|
861
|
+
autoCompactTokens,
|
|
803
862
|
model: callModel ?? this.model,
|
|
804
863
|
effort: callEffort ?? this.effort,
|
|
805
864
|
} : undefined;
|
|
865
|
+
if (lastModelCall?.tokenUsage) {
|
|
866
|
+
const lastUsageForContext = usageForContext(lastModelCall.tokenUsage);
|
|
867
|
+
const lastTotalTokens = contextTokensForUsage(lastUsageForContext, !!isClaudeModel);
|
|
868
|
+
lastModelCall = {
|
|
869
|
+
...lastModelCall,
|
|
870
|
+
contextUsage: lastTotalTokens > 0 ? {
|
|
871
|
+
totalTokens: lastTotalTokens,
|
|
872
|
+
maxTokens: contextWindowTokens,
|
|
873
|
+
percentage: Math.round((lastTotalTokens / contextWindowTokens) * 100),
|
|
874
|
+
autoCompactTokens,
|
|
875
|
+
model: callModel ?? this.model,
|
|
876
|
+
effort: callEffort ?? this.effort,
|
|
877
|
+
} : undefined,
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
// 组装 modelCalls:优先 SDK iterations,fallback 流式收集,兜底降级单行。
|
|
881
|
+
const callModel_ = callModel ?? this.model;
|
|
882
|
+
let modelCalls;
|
|
883
|
+
const iterArr = Array.isArray(u?.iterations) && u.iterations.length > 0 ? u.iterations : null;
|
|
884
|
+
if (iterArr) {
|
|
885
|
+
modelCalls = iterArr.map((it, i) => ({
|
|
886
|
+
call_index: i, model: callModel_, tokenUsage: it,
|
|
887
|
+
}));
|
|
888
|
+
}
|
|
889
|
+
else if (collectedCalls.length > 0) {
|
|
890
|
+
modelCalls = collectedCalls;
|
|
891
|
+
}
|
|
892
|
+
else if (u) {
|
|
893
|
+
// 降级:无逐次数据,写一条累计行
|
|
894
|
+
modelCalls = [{ call_index: 0, model: callModel_, tokenUsage: u, degraded: true }];
|
|
895
|
+
}
|
|
806
896
|
yield {
|
|
807
897
|
type: 'complete',
|
|
808
898
|
result: cleanResult,
|
|
@@ -817,6 +907,8 @@ export class AgentRunner {
|
|
|
817
907
|
numTurns: event.num_turns,
|
|
818
908
|
tokenUsage: event.usage,
|
|
819
909
|
contextUsage,
|
|
910
|
+
lastModelCall,
|
|
911
|
+
modelCalls,
|
|
820
912
|
};
|
|
821
913
|
// result 是 SDK 流的终结事件,不再等待后续(防止 interrupt 后流不关闭导致挂起)
|
|
822
914
|
return;
|
|
@@ -1030,11 +1122,12 @@ export class AgentRunner {
|
|
|
1030
1122
|
model: sdkModel,
|
|
1031
1123
|
...(callEffort ? { effort: callEffort } : {}),
|
|
1032
1124
|
...(this.claudeExecutablePath ? { pathToClaudeCodeExecutable: this.claudeExecutablePath } : {}),
|
|
1033
|
-
autoCompactWindow:
|
|
1125
|
+
autoCompactWindow: autoCompactWindowFor(sdkModel),
|
|
1034
1126
|
advisorModel: 'haiku',
|
|
1035
1127
|
canUseTool: canUseToolCallback,
|
|
1036
1128
|
permissionMode: sdkPermissionMode,
|
|
1037
1129
|
persistSession: true,
|
|
1130
|
+
includePartialMessages: true,
|
|
1038
1131
|
enableFileCheckpointing: true,
|
|
1039
1132
|
hooks: {
|
|
1040
1133
|
PreCompact: [{ matcher: '.*', hooks: [preCompactHook] }],
|