evolclaw 3.2.0 → 3.4.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 +53 -0
- package/README.md +7 -4
- package/dist/agents/{resolve.js → baseagent.js} +34 -5
- package/dist/agents/claude-runner.js +120 -31
- package/dist/agents/codex-app-server-client.js +364 -0
- package/dist/agents/codex-runner.js +1152 -140
- package/dist/agents/gemini-runner.js +2 -2
- package/dist/agents/runner-types.js +58 -0
- package/dist/aun/aid/store.js +1 -1
- package/dist/aun/outbox.js +14 -2
- package/dist/aun/storage/download.js +1 -1
- package/dist/aun/storage/upload.js +13 -1
- package/dist/channels/aun.js +869 -358
- package/dist/channels/dingtalk.js +77 -140
- package/dist/channels/feishu.js +125 -154
- 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-command.js +591 -0
- package/dist/cli/agent.js +23 -8
- package/dist/cli/aun-commands.js +1444 -0
- package/dist/cli/ctl-command.js +78 -0
- package/dist/cli/daemon-commands.js +2707 -0
- package/dist/cli/index.js +23 -4905
- package/dist/cli/init.js +33 -6
- package/dist/cli/model.js +1 -1
- package/dist/cli/restart-monitor.js +539 -0
- package/dist/cli/stats.js +558 -0
- package/dist/cli/version.js +87 -0
- package/dist/cli/watch-logs.js +33 -0
- package/dist/cli/watch-msg.js +5 -2
- package/dist/config-store.js +12 -6
- package/dist/core/channel-loader.js +88 -83
- package/dist/core/command/command-handler.js +1189 -0
- package/dist/core/command/menu-handler.js +1478 -0
- package/dist/core/command/slash-gate.js +142 -0
- package/dist/core/command/slash-handler.js +2090 -0
- package/dist/core/evolagent-registry.js +82 -0
- package/dist/core/evolagent.js +17 -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 +91 -51
- package/dist/core/message/items-formatter.js +9 -1
- package/dist/core/message/message-bridge.js +73 -24
- package/dist/core/message/message-log.js +1 -0
- package/dist/core/message/message-processor.js +432 -94
- package/dist/core/message/message-queue.js +70 -2
- package/dist/core/message/pending-hints.js +232 -0
- package/dist/core/model/model-catalog.js +1 -1
- package/dist/core/model/model-scope.js +2 -2
- package/dist/core/permission.js +25 -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 +86 -26
- 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 +334 -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/data/error-dict.json +7 -0
- package/dist/{agents → eck}/manifest-engine.js +20 -1
- package/dist/{agents → eck}/message-renderer.js +24 -1
- package/dist/index.js +174 -9
- package/dist/ipc.js +116 -1
- package/dist/utils/cross-platform.js +58 -5
- package/dist/utils/ecweb-launch.js +49 -0
- package/dist/utils/ecweb-pair.js +20 -0
- package/dist/utils/error-utils.js +18 -5
- package/dist/utils/npm-ops.js +38 -8
- package/dist/utils/stats.js +77 -6
- 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_message_manifest.json +30 -3
- package/kits/rules/05-venue.md +1 -1
- package/kits/templates/message-fragments/inject-default.md +2 -0
- package/package.json +5 -6
- package/dist/agents/baseagent-normalize.js +0 -19
- package/dist/core/command-handler.js +0 -3876
- 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,58 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v3.4.0 (2026-06-12)
|
|
4
|
+
|
|
5
|
+
### New Features
|
|
6
|
+
|
|
7
|
+
- **CLI 模块化** — 将4640行 `command-handler.ts` 和5131行 `cli/index.ts` 拆分为8个专注模块(`command/`、`cli/`子目录),claude-agent-sdk 升级至 ^0.3.170,净减约10000行
|
|
8
|
+
- **ECWeb Monitor 视图** — 新增实时监控页面:进程级 CPU/内存采样(1s 后台循环)、全局统计、per-agent 摘要;IPC 新增 `monitor-snapshot` 处理器
|
|
9
|
+
- **Agent 运行时控制** — ECWeb 支持对每个 agent 执行 start/stop/mute/unmute/queue-clear,stop 中断进行中的模型调用,mute 暂停队列消费同时保留入队消息
|
|
10
|
+
- **`ec watch logs` 多选** — 新增日志类型多选菜单,支持按类型(session/tool/error等)筛选实时聚合日志;`watch.logTypes` 配置默认可选集
|
|
11
|
+
- **AUN 结构化出站 payload** — task status 改走 `notify`(`event/app.task.status`)不入消息历史;activity 逐条结构化(`type:'activity'` + `item`);notice/error 结构化;`ref_message_id`/`initiator`/`thread_id` 统一透传
|
|
12
|
+
- **Context-aware auto-compact** — 根据 DB 中上次 model call 的实际 context token 记录决策压缩时机,在下一任务开始前执行;DB 新增 `context_tokens`/`max_tokens`/`auto_compact_tokens` 字段
|
|
13
|
+
- **Codex Edit events 统一 diff** — fileChange 映射为 `Edit` tool_use 事件并附带 unified diff,permission 层直接渲染,不重新计算
|
|
14
|
+
- **AUN 群命令 mention 过滤** — broadcast 指令强制要求 @ 触发;`action_card_reply` 归属由消息上下文精确判定
|
|
15
|
+
- **群话题创建权限** — 由 AUN `group.get_admins` 实时查询,仅 owner/admin 可建话题,fail-closed;无权时静默丢弃避免 agent 互相拒绝循环
|
|
16
|
+
- **`/baseagent scope` 参数** — 支持 `session`/`default`/`both` 三档控制切换范围
|
|
17
|
+
- **Session topic rename** — 支持 `/rename` 重命名当前话题会话
|
|
18
|
+
- **evolclaw-web 自动升级** — ECWeb 启动时检测并自动升级新版本,对标 fastaun 升级机制
|
|
19
|
+
- **Agent displayName 解析** — 从 agent.md 本地缓存 + 异步网络拉取 displayName,ECWeb 展示更友好
|
|
20
|
+
|
|
21
|
+
### Improvements
|
|
22
|
+
|
|
23
|
+
- **用户中断归类** — 新消息/`/stop`/撤回触发的流中断独立为 `task:interrupted` 事件,不计入 `task:error`
|
|
24
|
+
- **统一出站响应投递** — AUN 渠道所有出站路径收敛到 `adapter.send`,消除渠道间重复分支
|
|
25
|
+
- **Feishu Pin→CheckMark 两阶段 ack** — 收到消息先加 Pin(排队中),runner 开始时升级为 CheckMark 并移除 Pin,视觉无空窗
|
|
26
|
+
- **dispatchModeOverride 分离** — 动态覆盖与持久化 `dispatchMode` 解耦,避免一次性覆盖污染会话配置
|
|
27
|
+
- **ECWeb token TTL 延长至30天** — 支持滑动续期;端口被占时杀旧进程而非漂移到 port+1
|
|
28
|
+
- **`evolclaw status` 展示 ECWeb** — 状态命令新增 ECWeb 进程与 HTTP 就绪状态
|
|
29
|
+
|
|
30
|
+
### Bug Fixes
|
|
31
|
+
|
|
32
|
+
- **话题回复上下文丢失** — `ctl send/file` 在 Feishu 话题内未透传 `replyToMessageId`/`replyInThread`,回复落到主会话气泡
|
|
33
|
+
- **文件标记提前暴露** — 定时 flush(非最终)触发时 `[SEND_FILE:...]` 未过滤,原样发给用户
|
|
34
|
+
- **AUN inbound replyContext 缺字段** — 入站消息未填充 `peerId`/`replyToMessageId`,导致 task.status `initiator` 为空
|
|
35
|
+
- **Codex SSE idle 重连误报错** — SSE 超时重连的空 error 事件被当作任务错误处理
|
|
36
|
+
- **JSON parse error 自动重试** — API 返回 JSON 解析异常时触发指数退避重试
|
|
37
|
+
- **ECWeb 启动就绪检测** — HTTP 探测根路径判断就绪,避免进程存活但 HTTP 未就绪的误判
|
|
38
|
+
|
|
39
|
+
## v3.3.0 (2026-06-10)
|
|
40
|
+
|
|
41
|
+
### New Features
|
|
42
|
+
|
|
43
|
+
- **Thread 会话继承 baseagent** — 创建 thread 会话时自动继承父会话的 `agentId` 及 baseagent 配置,多线程场景无需重新切换
|
|
44
|
+
- **Codex runner 增强** — 新增 CLI 版本检测、streaming delta 支持、server 可用性检查;codex-app-server-client 补充类型定义
|
|
45
|
+
- **Menu 话题管理** — command-handler 新增话题菜单的权限判定(`canReadTopics`/`canDeleteTopic`)与格式化(`buildTopicMenuItem`/`resolveMenuChatType`)
|
|
46
|
+
|
|
47
|
+
### Improvements
|
|
48
|
+
|
|
49
|
+
- **Channel plugin 接口统一** — `ChannelPlugin` 从 `isEnabled/createChannel/createChannels` 收敛为单一 `createInstance(inst, ctx)` 单实例模型,新增 `ChannelBuildContext` 与 `showActivities` 共享策略;六个渠道(aun/feishu/wechat/dingtalk/qqbot/wecom)同步迁移
|
|
50
|
+
- **ECWeb 控制台重构** — control source 拆分为 `system`(evolclaw/fastaun/evolclaw-web 三包版本与健康检查)和 `triggers`(定时任务管理),前端联动更新
|
|
51
|
+
- **Trigger 失败统计** — Trigger 新增 `failCount`/`lastResult` 字段,scheduler/manager/parser 同步
|
|
52
|
+
- **Runner 类型模块化** — 抽出 `runner-types.ts` 统一共享类型,消除各 runner 重复声明
|
|
53
|
+
- **Agent AID 展示** — `AgentInfo` 新增 `aid` 字段,`ec agent` 命令展示 agent AID
|
|
54
|
+
- **缓存模块更名** — `read-cache` 重命名为 `daemon-file-cache`,语义更清晰
|
|
55
|
+
|
|
3
56
|
## v3.2.0 (2026-06-05)
|
|
4
57
|
|
|
5
58
|
### New Features
|
package/README.md
CHANGED
|
@@ -42,7 +42,7 @@ EvolClaw 是一个轻量级 AI Agent 网关系统。它为 Claude Code / Codex
|
|
|
42
42
|
|
|
43
43
|
1. **消息渠道层** (`src/channels/`) - Feishu + WeChat + DingTalk + QQBot + WeCom + AUN 网络
|
|
44
44
|
2. **消息队列层** (`src/core/message/message-queue.ts`) - 会话级串行处理 + 中断支持
|
|
45
|
-
3. **命令处理层** (`src/core/command
|
|
45
|
+
3. **命令处理层** (`src/core/command/`) - 斜杠命令处理(slash-handler / menu-handler / command-handler)
|
|
46
46
|
4. **消息处理层** (`src/core/message/message-processor.ts`) - 统一事件处理引擎
|
|
47
47
|
5. **会话管理层** (`src/core/session/session-manager.ts`) - 多项目会话管理
|
|
48
48
|
6. **交互路由层** (`src/core/interaction-router.ts`) - 卡片交互回调注册与路由
|
|
@@ -190,6 +190,11 @@ evolclaw/
|
|
|
190
190
|
│ │ └── gemini-runner.ts # Gemini CLI 封装
|
|
191
191
|
│ ├── aun/ # AUN 协议工具
|
|
192
192
|
│ ├── core/
|
|
193
|
+
│ │ ├── command/
|
|
194
|
+
│ │ │ ├── command-handler.ts # 命令派发入口
|
|
195
|
+
│ │ │ ├── slash-handler.ts # 斜杠命令实现
|
|
196
|
+
│ │ │ ├── menu-handler.ts # Menu 协议处理
|
|
197
|
+
│ │ │ └── slash-gate.ts # 权限前置拦截
|
|
193
198
|
│ │ ├── message/
|
|
194
199
|
│ │ │ ├── message-bridge.ts # 渠道 ↔ 核心消息桥
|
|
195
200
|
│ │ │ ├── message-processor.ts # 统一消息处理引擎
|
|
@@ -202,7 +207,6 @@ evolclaw/
|
|
|
202
207
|
│ │ │ ├── session-fs-store.ts # 文件系统存储原语
|
|
203
208
|
│ │ │ └── session-manager.ts # 会话管理(多项目支持)
|
|
204
209
|
│ │ ├── trigger/ # 触发器引擎
|
|
205
|
-
│ │ ├── command-handler.ts # 斜杠命令处理
|
|
206
210
|
│ │ ├── evolagent.ts # EvolAgent 实体
|
|
207
211
|
│ │ ├── evolagent-registry.ts # Agent 注册表(扫描/路由/热重载)
|
|
208
212
|
│ │ ├── interaction-router.ts # 卡片交互回调路由
|
|
@@ -250,7 +254,6 @@ evolclaw/
|
|
|
250
254
|
- `/perm [mode]` - 查看或切换权限模式(auto / edit / default / readonly)
|
|
251
255
|
|
|
252
256
|
**系统管理**:
|
|
253
|
-
- `/clear` - 清空对话历史
|
|
254
257
|
- `/compact` - 压缩会话上下文
|
|
255
258
|
- `/rewind <turn>` - 回退会话到指定轮次
|
|
256
259
|
- `/stop` - 中断当前任务
|
|
@@ -296,7 +299,7 @@ v3.2 新增进程级身份标识。启动时自动生成 `ec+5位数字.agentid.
|
|
|
296
299
|
## 技术栈
|
|
297
300
|
|
|
298
301
|
- **运行时**:Node.js >= 22 + TypeScript(ES modules)
|
|
299
|
-
- **AI
|
|
302
|
+
- **AI 后端**:@anthropic-ai/claude-agent-sdk >= 0.3.170、Codex CLI app-server、Gemini CLI
|
|
300
303
|
- **消息渠道**:飞书(@larksuiteoapi/node-sdk)、微信(ClawBot ilink API)、钉钉(dingtalk-stream)、QQ频道(pure-qqbot)、企业微信(AI Bot API)、AUN 网络
|
|
301
304
|
- **数据存储**:文件系统(per-chat 目录) + JSONL(CLI 共用)
|
|
302
305
|
- **测试框架**: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, isClaudeContextUsageModel, isOneMillionContextModel, realContextWindowForModel, autoCompactWindowForModel } from './runner-types.js';
|
|
14
|
+
export { hasCompact, hasModelSwitcher, hasPermissionController } from './runner-types.js';
|
|
13
15
|
// ── 模型别名解析 ──
|
|
14
16
|
// SDK 内置的别名表可能落后于代理实际可用的最新模型,
|
|
15
17
|
// 因此优先从 {baseUrl}/models 动态获取各系列最新版本,失败则回退静态表。
|
|
@@ -97,8 +99,6 @@ function resolveModelAlias(model, baseUrl) {
|
|
|
97
99
|
// 回退静态表
|
|
98
100
|
return STATIC_MODEL_ALIASES[model] || model;
|
|
99
101
|
}
|
|
100
|
-
/** 支持 1M 上下文窗口的模型 ID 前缀(SDK 通过 `[1m]` 后缀启用)。 */
|
|
101
|
-
const ONE_M_CONTEXT_PREFIXES = ['claude-opus-4-8', 'claude-sonnet-4-6'];
|
|
102
102
|
/**
|
|
103
103
|
* 为支持 1M 上下文的模型追加 `[1m]` 后缀——仅在交给 SDK query() 时调用。
|
|
104
104
|
* 目录与校验层始终使用不带后缀的基础 ID,避免与网关 /models 返回值(无 `[1m]`)冲突。
|
|
@@ -106,14 +106,10 @@ const ONE_M_CONTEXT_PREFIXES = ['claude-opus-4-8', 'claude-sonnet-4-6'];
|
|
|
106
106
|
function applyContextWindow(modelId) {
|
|
107
107
|
if (/\[1m\]$/.test(modelId))
|
|
108
108
|
return modelId; // 已带后缀
|
|
109
|
-
if (
|
|
109
|
+
if (isOneMillionContextModel(modelId))
|
|
110
110
|
return `${modelId}[1m]`;
|
|
111
111
|
return modelId;
|
|
112
112
|
}
|
|
113
|
-
/** 根据 SDK model 串(含 [1m] 后缀)返回合适的 autoCompactWindow 值。 */
|
|
114
|
-
function contextWindowFor(sdkModel) {
|
|
115
|
-
return /\[1m\]$/.test(sdkModel) ? 900000 : 200000;
|
|
116
|
-
}
|
|
117
113
|
/** 解析别名 + 追加 1M 后缀,得到最终交给 SDK 的 model 串。 */
|
|
118
114
|
function resolveSdkModel(model, baseUrl) {
|
|
119
115
|
return applyContextWindow(resolveModelAlias(model, baseUrl));
|
|
@@ -168,19 +164,9 @@ class MessageStream {
|
|
|
168
164
|
}
|
|
169
165
|
}
|
|
170
166
|
}
|
|
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
167
|
export class AgentRunner {
|
|
182
168
|
name = 'claude';
|
|
183
|
-
capabilities = { clear: true, compact: true, fork: true };
|
|
169
|
+
capabilities = { clear: true, compact: true, fork: true, askUserQuestion: true, planApproval: true, fileRewind: 'checkpoint' };
|
|
184
170
|
apiKey;
|
|
185
171
|
model;
|
|
186
172
|
effort;
|
|
@@ -383,7 +369,6 @@ export class AgentRunner {
|
|
|
383
369
|
},
|
|
384
370
|
channelId: permCtx.channelId,
|
|
385
371
|
sessionId,
|
|
386
|
-
expiresAt: Date.now() + 5 * 60 * 1000,
|
|
387
372
|
};
|
|
388
373
|
}
|
|
389
374
|
else {
|
|
@@ -411,11 +396,11 @@ export class AgentRunner {
|
|
|
411
396
|
},
|
|
412
397
|
channelId: permCtx.channelId,
|
|
413
398
|
sessionId,
|
|
414
|
-
expiresAt: Date.now() + 5 * 60 * 1000,
|
|
415
399
|
};
|
|
416
400
|
}
|
|
417
401
|
let cardSent = false;
|
|
418
402
|
try {
|
|
403
|
+
await permCtx.flushPending?.();
|
|
419
404
|
const envelope = buildEnvelope({
|
|
420
405
|
taskId: permCtx.taskId,
|
|
421
406
|
channel: permCtx.channel ?? permCtx.adapter.channelName,
|
|
@@ -433,6 +418,7 @@ export class AgentRunner {
|
|
|
433
418
|
logger.warn(`[AgentRunner] AskUserQuestion card send failed for q${i}:`, err);
|
|
434
419
|
}
|
|
435
420
|
if (!cardSent) {
|
|
421
|
+
await permCtx.flushPending?.();
|
|
436
422
|
const firstLabel = q.options[0]?.label || '';
|
|
437
423
|
answers[q.question] = q.multiSelect ? [firstLabel] : firstLabel;
|
|
438
424
|
if (sendPrompt) {
|
|
@@ -530,7 +516,7 @@ export class AgentRunner {
|
|
|
530
516
|
else {
|
|
531
517
|
resolve(action.trim());
|
|
532
518
|
}
|
|
533
|
-
}, {
|
|
519
|
+
}, { initiatorId: permCtx.userId, fallbackCommand: 'ask' });
|
|
534
520
|
});
|
|
535
521
|
answers[q.question] = answer;
|
|
536
522
|
}
|
|
@@ -606,6 +592,7 @@ export class AgentRunner {
|
|
|
606
592
|
},
|
|
607
593
|
};
|
|
608
594
|
try {
|
|
595
|
+
await permCtx.flushPending?.();
|
|
609
596
|
const envelope = buildEnvelope({
|
|
610
597
|
taskId: permCtx.taskId,
|
|
611
598
|
channel: permCtx.channel ?? permCtx.adapter.channelName,
|
|
@@ -663,6 +650,7 @@ export class AgentRunner {
|
|
|
663
650
|
buttonArgMap: { approve: '1', reject: '2' },
|
|
664
651
|
},
|
|
665
652
|
};
|
|
653
|
+
await permCtx.flushPending?.();
|
|
666
654
|
await sendPrompt(renderActionAsText(fallbackInteraction));
|
|
667
655
|
permCtx.interactionRouter.unmarkWaiting(sessionId);
|
|
668
656
|
return new Promise((resolve) => {
|
|
@@ -674,11 +662,12 @@ export class AgentRunner {
|
|
|
674
662
|
else {
|
|
675
663
|
resolve({ behavior: 'allow', updatedInput: input, decisionClassification: 'user_temporary' });
|
|
676
664
|
}
|
|
677
|
-
}, {
|
|
665
|
+
}, { initiatorId: permCtx.userId, fallbackCommand: 'ask' });
|
|
678
666
|
});
|
|
679
667
|
}
|
|
680
668
|
// 无交互能力,发提示后直接 allow
|
|
681
669
|
permCtx?.interactionRouter?.unmarkWaiting(sessionId);
|
|
670
|
+
await permCtx.flushPending?.();
|
|
682
671
|
await sendPrompt('📋 计划审批\nAI 已完成规划,自动批准执行。');
|
|
683
672
|
return { behavior: 'allow', updatedInput: input, decisionClassification: 'user_temporary' };
|
|
684
673
|
}
|
|
@@ -692,6 +681,9 @@ export class AgentRunner {
|
|
|
692
681
|
const toolUseNames = new Map();
|
|
693
682
|
let turnCount = 0;
|
|
694
683
|
const seenMessageIds = new Set();
|
|
684
|
+
let lastModelCall;
|
|
685
|
+
// 流式收集各次大模型调用(fallback:SDK iterations 为空时使用)
|
|
686
|
+
const collectedCalls = [];
|
|
695
687
|
try {
|
|
696
688
|
for await (const event of sdkStream) {
|
|
697
689
|
// 提取 session_id(任意 SDK 事件都可能携带)
|
|
@@ -700,6 +692,38 @@ export class AgentRunner {
|
|
|
700
692
|
this.updateSessionId(sessionId, event.session_id);
|
|
701
693
|
yield { type: 'session_id', sessionId: event.session_id };
|
|
702
694
|
}
|
|
695
|
+
if (event.type === 'stream_event') {
|
|
696
|
+
const streamEvent = event.event;
|
|
697
|
+
if (streamEvent?.type === 'message_start' && streamEvent.message?.usage) {
|
|
698
|
+
lastModelCall = {
|
|
699
|
+
uuid: event.uuid,
|
|
700
|
+
model: streamEvent.message.model,
|
|
701
|
+
tokenUsage: streamEvent.message.usage,
|
|
702
|
+
};
|
|
703
|
+
// 流式收集:每个 message_start = 一次新的大模型调用
|
|
704
|
+
collectedCalls.push({
|
|
705
|
+
call_index: collectedCalls.length,
|
|
706
|
+
model: streamEvent.message.model ?? callModel ?? this.model,
|
|
707
|
+
request_id: event.request_id,
|
|
708
|
+
tokenUsage: { ...streamEvent.message.usage },
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
else if (streamEvent?.type === 'message_delta' && streamEvent.usage) {
|
|
712
|
+
lastModelCall = {
|
|
713
|
+
...lastModelCall,
|
|
714
|
+
uuid: lastModelCall?.uuid ?? event.uuid,
|
|
715
|
+
tokenUsage: {
|
|
716
|
+
...(lastModelCall?.tokenUsage ?? {}),
|
|
717
|
+
...streamEvent.usage,
|
|
718
|
+
},
|
|
719
|
+
};
|
|
720
|
+
// 将 message_delta 的 usage 合并进当前(最后一次)收集的调用
|
|
721
|
+
const last = collectedCalls[collectedCalls.length - 1];
|
|
722
|
+
if (last)
|
|
723
|
+
last.tokenUsage = { ...last.tokenUsage, ...streamEvent.usage };
|
|
724
|
+
}
|
|
725
|
+
continue;
|
|
726
|
+
}
|
|
703
727
|
// system: compact_boundary → compact
|
|
704
728
|
if (event.type === 'system' && event.subtype === 'compact_boundary') {
|
|
705
729
|
yield {
|
|
@@ -730,6 +754,18 @@ export class AgentRunner {
|
|
|
730
754
|
seenMessageIds.add(msgId);
|
|
731
755
|
turnCount++;
|
|
732
756
|
}
|
|
757
|
+
if (event.message.usage) {
|
|
758
|
+
lastModelCall = {
|
|
759
|
+
...lastModelCall,
|
|
760
|
+
messageId: event.message.id,
|
|
761
|
+
requestId: event.request_id,
|
|
762
|
+
model: event.message.model,
|
|
763
|
+
tokenUsage: {
|
|
764
|
+
...event.message.usage,
|
|
765
|
+
...(lastModelCall?.tokenUsage ?? {}),
|
|
766
|
+
},
|
|
767
|
+
};
|
|
768
|
+
}
|
|
733
769
|
// 统计本轮 base agent 全部输出字符数(text + tool_use input)
|
|
734
770
|
let turnOutputChars = 0;
|
|
735
771
|
for (const content of event.message.content) {
|
|
@@ -790,19 +826,69 @@ export class AgentRunner {
|
|
|
790
826
|
const cleanResult = typeof event.result === 'string'
|
|
791
827
|
? event.result.replace(/<thinking>[\s\S]*?<\/thinking>\s*/g, '').trim()
|
|
792
828
|
: event.result;
|
|
793
|
-
// 从 usage
|
|
829
|
+
// 从 usage 求当前上下文占用。
|
|
830
|
+
// Claude:input_tokens 是净输入(不含 cache),三项求和 = 实际上下文长度。
|
|
831
|
+
// 非 Claude(DeepSeek/OpenAI 兼容):cache_read 是服务端 KV cache 不占上下文窗口,
|
|
832
|
+
// input_tokens 本身就是完整的上下文输入量。
|
|
794
833
|
const u = event.usage;
|
|
795
|
-
const
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
const
|
|
834
|
+
const effectiveModel = callModel ?? this.model;
|
|
835
|
+
const isClaudeModel = isClaudeContextUsageModel(effectiveModel);
|
|
836
|
+
const totalTokens = contextTokensForUsage(u, !!isClaudeModel);
|
|
837
|
+
const contextWindowTokens = realContextWindowForModel(sdkModel);
|
|
838
|
+
const autoCompactTokens = autoCompactWindowForModel(sdkModel);
|
|
799
839
|
const contextUsage = totalTokens > 0 ? {
|
|
800
840
|
totalTokens,
|
|
801
|
-
maxTokens,
|
|
802
|
-
percentage: Math.round((totalTokens /
|
|
841
|
+
maxTokens: contextWindowTokens,
|
|
842
|
+
percentage: Math.round((totalTokens / contextWindowTokens) * 100),
|
|
843
|
+
autoCompactTokens,
|
|
803
844
|
model: callModel ?? this.model,
|
|
804
845
|
effort: callEffort ?? this.effort,
|
|
805
846
|
} : undefined;
|
|
847
|
+
if (lastModelCall?.tokenUsage) {
|
|
848
|
+
const lastUsageForContext = usageForContext(lastModelCall.tokenUsage);
|
|
849
|
+
const lastTotalTokens = contextTokensForUsage(lastUsageForContext, !!isClaudeModel);
|
|
850
|
+
lastModelCall = {
|
|
851
|
+
...lastModelCall,
|
|
852
|
+
contextUsage: lastTotalTokens > 0 ? {
|
|
853
|
+
totalTokens: lastTotalTokens,
|
|
854
|
+
maxTokens: contextWindowTokens,
|
|
855
|
+
percentage: Math.round((lastTotalTokens / contextWindowTokens) * 100),
|
|
856
|
+
autoCompactTokens,
|
|
857
|
+
model: callModel ?? this.model,
|
|
858
|
+
effort: callEffort ?? this.effort,
|
|
859
|
+
} : undefined,
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
const contextUsageForCall = (usage) => {
|
|
863
|
+
const callTotalTokens = contextTokensForUsage(usageForContext(usage), !!isClaudeModel);
|
|
864
|
+
return callTotalTokens > 0 ? {
|
|
865
|
+
totalTokens: callTotalTokens,
|
|
866
|
+
maxTokens: contextWindowTokens,
|
|
867
|
+
percentage: Math.round((callTotalTokens / contextWindowTokens) * 100),
|
|
868
|
+
autoCompactTokens,
|
|
869
|
+
model: callModel ?? this.model,
|
|
870
|
+
effort: callEffort ?? this.effort,
|
|
871
|
+
} : undefined;
|
|
872
|
+
};
|
|
873
|
+
// 组装 modelCalls:优先 SDK iterations,fallback 流式收集,兜底降级单行。
|
|
874
|
+
const callModel_ = callModel ?? this.model;
|
|
875
|
+
let modelCalls;
|
|
876
|
+
const iterArr = Array.isArray(u?.iterations) && u.iterations.length > 0 ? u.iterations : null;
|
|
877
|
+
if (iterArr) {
|
|
878
|
+
modelCalls = iterArr.map((it, i) => ({
|
|
879
|
+
call_index: i, model: callModel_, tokenUsage: it, contextUsage: contextUsageForCall(it),
|
|
880
|
+
}));
|
|
881
|
+
}
|
|
882
|
+
else if (collectedCalls.length > 0) {
|
|
883
|
+
modelCalls = collectedCalls.map(call => ({
|
|
884
|
+
...call,
|
|
885
|
+
contextUsage: contextUsageForCall(call.tokenUsage),
|
|
886
|
+
}));
|
|
887
|
+
}
|
|
888
|
+
else if (u) {
|
|
889
|
+
// 降级:无逐次数据,写一条累计行
|
|
890
|
+
modelCalls = [{ call_index: 0, model: callModel_, tokenUsage: u, contextUsage: contextUsageForCall(u), degraded: true }];
|
|
891
|
+
}
|
|
806
892
|
yield {
|
|
807
893
|
type: 'complete',
|
|
808
894
|
result: cleanResult,
|
|
@@ -817,6 +903,8 @@ export class AgentRunner {
|
|
|
817
903
|
numTurns: event.num_turns,
|
|
818
904
|
tokenUsage: event.usage,
|
|
819
905
|
contextUsage,
|
|
906
|
+
lastModelCall,
|
|
907
|
+
modelCalls,
|
|
820
908
|
};
|
|
821
909
|
// result 是 SDK 流的终结事件,不再等待后续(防止 interrupt 后流不关闭导致挂起)
|
|
822
910
|
return;
|
|
@@ -1030,11 +1118,12 @@ export class AgentRunner {
|
|
|
1030
1118
|
model: sdkModel,
|
|
1031
1119
|
...(callEffort ? { effort: callEffort } : {}),
|
|
1032
1120
|
...(this.claudeExecutablePath ? { pathToClaudeCodeExecutable: this.claudeExecutablePath } : {}),
|
|
1033
|
-
autoCompactWindow:
|
|
1121
|
+
autoCompactWindow: autoCompactWindowForModel(sdkModel),
|
|
1034
1122
|
advisorModel: 'haiku',
|
|
1035
1123
|
canUseTool: canUseToolCallback,
|
|
1036
1124
|
permissionMode: sdkPermissionMode,
|
|
1037
1125
|
persistSession: true,
|
|
1126
|
+
includePartialMessages: true,
|
|
1038
1127
|
enableFileCheckpointing: true,
|
|
1039
1128
|
hooks: {
|
|
1040
1129
|
PreCompact: [{ matcher: '.*', hooks: [preCompactHook] }],
|