@xiaobuyu/nodesign 0.0.1 → 0.0.2
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 +156 -106
- package/bin/nodesign.js +0 -0
- package/package.json +1 -1
- package/server/api/README.md +45 -45
- package/server/api/_guard.js +75 -75
- package/server/api/admin.js +187 -187
- package/server/api/assets/docx-page.js +103 -103
- package/server/api/assets/helpers.js +22 -22
- package/server/api/assets/notes.js +144 -144
- package/server/api/assets.js +896 -896
- package/server/api/board.js +57 -57
- package/server/api/browse.js +138 -138
- package/server/api/canvas.js +444 -444
- package/server/api/chatai.js +324 -324
- package/server/api/exports/build-standalone.js +980 -980
- package/server/api/exports/cards.js +118 -118
- package/server/api/exports/docx-pdf.js +46 -46
- package/server/api/exports/export-page.js +142 -142
- package/server/api/exports/handoff.js +147 -147
- package/server/api/exports.js +690 -690
- package/server/api/instruction.js +75 -75
- package/server/api/me.js +116 -116
- package/server/api/memory.js +185 -185
- package/server/api/pending-changes.js +366 -366
- package/server/api/plugins.js +164 -164
- package/server/api/projects.js +220 -220
- package/server/api/publish.js +64 -64
- package/server/api/recent.js +73 -73
- package/server/api/sessions.js +589 -589
- package/server/api/skills.js +77 -77
- package/server/api/standalone-fit.js +168 -168
- package/server/api/turn-compose.js +180 -180
- package/server/api/turn-inflight.js +44 -44
- package/server/auth/README.md +17 -17
- package/server/auth/middleware.js +138 -138
- package/server/auth/origin-guard.js +71 -71
- package/server/auth/session.js +108 -108
- package/server/auth/tier.js +113 -113
- package/server/auth/users-store.js +348 -348
- package/server/edit/README.md +29 -29
- package/server/engine/README.md +113 -113
- package/server/engine/agent/agent-shared.js +550 -550
- package/server/engine/agent/auto-mode-default-hard-deny.txt +4 -4
- package/server/engine/agent/auto-mode-rules.js +78 -78
- package/server/engine/agent/context.js +418 -418
- package/server/engine/agent/events.js +464 -464
- package/server/engine/agent/hooks/canvas-validate.js +199 -199
- package/server/engine/agent/hooks/failure.js +120 -120
- package/server/engine/agent/hooks/file-events.js +75 -75
- package/server/engine/agent/hooks/lifecycle.js +233 -233
- package/server/engine/agent/hooks/post-canvas-focus.js +80 -80
- package/server/engine/agent/hooks/post-guidance.js +206 -206
- package/server/engine/agent/hooks/post-subagent-report.js +104 -104
- package/server/engine/agent/hooks/post-trim.js +58 -58
- package/server/engine/agent/hooks/pre-board-neighborhood.js +34 -34
- package/server/engine/agent/hooks/pre-defaults.js +84 -84
- package/server/engine/agent/hooks/pre-injectors.js +277 -277
- package/server/engine/agent/hooks/pre-performance-log-guard.js +78 -78
- package/server/engine/agent/hooks/pre-starter-files.js +61 -61
- package/server/engine/agent/hooks/pre-workspace-scope-guard.js +78 -78
- package/server/engine/agent/hooks/site-validate.js +94 -94
- package/server/engine/agent/hooks/tool-prompts.js +31 -31
- package/server/engine/agent/hooks/turn-state-memory.js +56 -56
- package/server/engine/agent/hooks/user-prompt-submit.js +215 -215
- package/server/engine/agent/hooks.js +354 -354
- package/server/engine/agent/init-contract.js +69 -69
- package/server/engine/agent/isolation.js +159 -159
- package/server/engine/agent/model-table.js +292 -292
- package/server/engine/agent/plugin-loader.js +266 -266
- package/server/engine/agent/prompts/nodesign-prelude.md +336 -336
- package/server/engine/agent/prompts/tools/ask-user-question-protocol.md +71 -71
- package/server/engine/agent/prompts/tools/direct-edit-protocol.md +98 -98
- package/server/engine/agent/prompts/tools/generate-image-cookbook.gemini-gateway.md +583 -583
- package/server/engine/agent/prompts/tools/generate-image-cookbook.md +483 -483
- package/server/engine/agent/prompts/tools/hybrid-reference.md +100 -100
- package/server/engine/agent/prompts/tools/paint-still-cookbook.md +270 -270
- package/server/engine/agent/prompts/tools/roll-film-cookbook.md +70 -70
- package/server/engine/agent/prompts/tools/site-reference.md +106 -106
- package/server/engine/agent/prompts/tools/tweaks-syntax.md +125 -125
- package/server/engine/agent/prompts/tools/vision-checker-dispatch.md +48 -48
- package/server/engine/agent/session-loop.js +1024 -1024
- package/server/engine/agent/session-model.js +164 -164
- package/server/engine/agent/skill.js +204 -204
- package/server/engine/agent/task-events.js +78 -78
- package/server/engine/agents/ds-extractor.md +132 -132
- package/server/engine/agents/explorer.md +190 -190
- package/server/engine/agents/index.js +230 -230
- package/server/engine/agents/schemas/design-system.json +156 -156
- package/server/engine/agents/schemas/tweak-schema.json +113 -113
- package/server/engine/agents/tweak-proposer.md +127 -127
- package/server/engine/agents/vision-checker.md +254 -254
- package/server/engine/browse/capture.js +422 -422
- package/server/engine/browse/card.js +137 -137
- package/server/engine/browse/handover.js +59 -59
- package/server/engine/browse/page-digest.js +119 -119
- package/server/engine/browse/refs.js +196 -196
- package/server/engine/browse/registry.js +307 -307
- package/server/engine/browse/screencast.js +170 -170
- package/server/engine/browse/state.js +121 -121
- package/server/engine/chatai/chat-log.js +79 -79
- package/server/engine/chatai/index.js +193 -193
- package/server/engine/chatai/openai-compat.js +152 -152
- package/server/engine/chatai/orchestrate.js +309 -309
- package/server/engine/chatai/perform.js +40 -40
- package/server/engine/chatai/summarize.js +79 -79
- package/server/engine/mcp/capability-gate.js +52 -52
- package/server/engine/mcp/index.js +339 -339
- package/server/engine/mcp/param-sanitizer.js +142 -142
- package/server/engine/mcp/tools/arrange-on-board.js +124 -124
- package/server/engine/mcp/tools/artifact-session.js +278 -278
- package/server/engine/mcp/tools/browse-computer.js +421 -421
- package/server/engine/mcp/tools/browse-find-batch.js +183 -183
- package/server/engine/mcp/tools/browse-screenshot.js +157 -157
- package/server/engine/mcp/tools/browse.js +480 -480
- package/server/engine/mcp/tools/build-docx.js +101 -101
- package/server/engine/mcp/tools/clear-pending-changes.js +99 -99
- package/server/engine/mcp/tools/create-on-board.js +155 -155
- package/server/engine/mcp/tools/crystallize-skill.js +168 -168
- package/server/engine/mcp/tools/explain-style.js +237 -237
- package/server/engine/mcp/tools/export-handoff.js +133 -133
- package/server/engine/mcp/tools/expose-tweaks.js +149 -149
- package/server/engine/mcp/tools/generate-image.js +842 -842
- package/server/engine/mcp/tools/get-computed-styles.js +164 -164
- package/server/engine/mcp/tools/get-pending-changes.js +205 -205
- package/server/engine/mcp/tools/h3box-ssh.js +84 -84
- package/server/engine/mcp/tools/helpers/acquire-page.js +82 -82
- package/server/engine/mcp/tools/helpers/motion-lab.js +554 -554
- package/server/engine/mcp/tools/helpers/motion-scroll.js +112 -112
- package/server/engine/mcp/tools/helpers/perception-page.js +215 -215
- package/server/engine/mcp/tools/helpers/reference-download.js +103 -103
- package/server/engine/mcp/tools/helpers/rembg-bridge.py +80 -80
- package/server/engine/mcp/tools/helpers/shot-pipeline.js +280 -280
- package/server/engine/mcp/tools/highlight.js +68 -68
- package/server/engine/mcp/tools/list-pages.js +222 -222
- package/server/engine/mcp/tools/lookup-tags.js +59 -59
- package/server/engine/mcp/tools/navigate-to-page.js +60 -60
- package/server/engine/mcp/tools/organize-board.js +64 -64
- package/server/engine/mcp/tools/paint-still.js +434 -434
- package/server/engine/mcp/tools/pin-to-board.js +130 -130
- package/server/engine/mcp/tools/preview-deck.js +130 -130
- package/server/engine/mcp/tools/profile-scroll.js +305 -305
- package/server/engine/mcp/tools/publish-site.js +102 -102
- package/server/engine/mcp/tools/query-elements.js +195 -195
- package/server/engine/mcp/tools/read-board.js +103 -103
- package/server/engine/mcp/tools/read-document.js +93 -93
- package/server/engine/mcp/tools/read-page.js +264 -264
- package/server/engine/mcp/tools/read-tavern-json.js +143 -143
- package/server/engine/mcp/tools/record-decision.js +140 -140
- package/server/engine/mcp/tools/relate-on-board.js +110 -110
- package/server/engine/mcp/tools/report-issue.js +115 -115
- package/server/engine/mcp/tools/roll-film.js +266 -266
- package/server/engine/mcp/tools/screenshot-docx.js +138 -138
- package/server/engine/mcp/tools/screenshot-url.js +195 -195
- package/server/engine/mcp/tools/screenshot.js +583 -583
- package/server/engine/mcp/tools/tier-gate.js +96 -96
- package/server/engine/mcp/tools/trace-motion.js +215 -215
- package/server/engine/mcp/tools/web-search.js +548 -548
- package/server/engine/motion/inventory.js +349 -349
- package/server/engine/perception/session.js +235 -235
- package/server/engine/plugins/nodesign/.claude-plugin/plugin.json +5 -5
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/SKILL.md +192 -192
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/canvas.template.html +667 -667
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/hybrid-grid.md +22 -22
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/image-led-cover.md +25 -25
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/portrait.md +23 -23
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/quote-backdrop.md +21 -21
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/section-divider.md +23 -23
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/text-led.md +21 -21
- package/server/engine/plugins/nodesign/skills/docx-craft/SKILL.md +162 -162
- package/server/engine/plugins/nodesign/skills/docx-craft/references/token-schema.md +407 -407
- package/server/engine/plugins/nodesign/skills/docx-craft//346/226/207/346/241/243.template.json +80 -80
- package/server/engine/plugins/nodesign/skills/rp-craft/SKILL.md +228 -228
- package/server/engine/plugins/nodesign/skills/rp-craft/patterns//346/274/224/345/207/272/351/241/265-/346/234/200/345/260/217/345/256/236/347/216/260.html +78 -78
- package/server/engine/plugins/nodesign/skills/rp-craft//346/274/224/345/207/272.template.js +268 -268
- package/server/engine/plugins/nodesign/skills/site-craft/SKILL.md +323 -323
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/build-lane.md +134 -134
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/cutout-collage.md +61 -61
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/mock-app.md +163 -163
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/page-transitions.md +86 -86
- package/server/engine/runs/active-runs.js +698 -698
- package/server/engine/runs/live-turn.js +252 -252
- package/server/engine/runs/store.js +364 -364
- package/server/engine/runs/turn-relay.js +217 -217
- package/server/engine/runtime/workspace.js +164 -164
- package/server/lib/artifact-file-path.js +80 -80
- package/server/lib/artifact-target.js +284 -284
- package/server/lib/asset-refs.js +239 -239
- package/server/lib/async-queue.js +104 -104
- package/server/lib/auto-relations.js +130 -130
- package/server/lib/binding-types.js +60 -60
- package/server/lib/board-kind-sizes.js +62 -62
- package/server/lib/board-relations.js +133 -133
- package/server/lib/browse-proxy.js +180 -180
- package/server/lib/canvas-id.js +36 -36
- package/server/lib/cover.js +227 -227
- package/server/lib/danbooru-tags.js +291 -291
- package/server/lib/doc-extract.js +156 -156
- package/server/lib/docx/build-from-source.js +260 -260
- package/server/lib/docx/build.js +490 -490
- package/server/lib/docx/dump-styles.js +287 -287
- package/server/lib/docx/fonts/nodesign-cjk.conf +164 -164
- package/server/lib/docx/merge-runs.js +129 -129
- package/server/lib/docx/numbering.js +218 -218
- package/server/lib/docx/order.js +152 -152
- package/server/lib/docx/rawzip.js +172 -172
- package/server/lib/docx/render.js +92 -92
- package/server/lib/docx/text-lint.js +225 -225
- package/server/lib/docx/tokens.js +335 -335
- package/server/lib/docx/units.js +29 -29
- package/server/lib/docx/xml.js +291 -291
- package/server/lib/docx-pages.js +187 -187
- package/server/lib/export-collect.js +289 -289
- package/server/lib/export-package.js +210 -210
- package/server/lib/html-srcset.js +145 -145
- package/server/lib/image-variant.js +460 -460
- package/server/lib/ingress/forward-openai-chat.js +305 -305
- package/server/lib/ingress/openai-chat.js +491 -491
- package/server/lib/ingress/session-notice.js +66 -66
- package/server/lib/ingress/session-routes.js +68 -68
- package/server/lib/ingress/slot-probe.js +130 -130
- package/server/lib/ingress/upstream-billing.js +52 -52
- package/server/lib/ingress/upstream-fail-streak.js +72 -72
- package/server/lib/ingress/upstream-truncation.js +48 -48
- package/server/lib/issues-store.js +182 -182
- package/server/lib/kinds/deck.js +72 -72
- package/server/lib/kinds/docx.js +283 -283
- package/server/lib/kinds/file-kinds.js +113 -113
- package/server/lib/kinds/index.js +202 -202
- package/server/lib/kinds/site.js +161 -161
- package/server/lib/model-ingress.js +543 -543
- package/server/lib/moderation.js +255 -255
- package/server/lib/notice-store.js +103 -103
- package/server/lib/plugin-install.js +104 -104
- package/server/lib/plugin-validator.js +721 -721
- package/server/lib/publish-store.js +115 -115
- package/server/lib/quick-summary.js +52 -52
- package/server/lib/quota.js +270 -270
- package/server/lib/rate-window.js +29 -29
- package/server/lib/reference-assets.js +92 -92
- package/server/lib/region-shot.js +135 -135
- package/server/lib/safe-path.js +73 -73
- package/server/lib/sdk-session.js +33 -33
- package/server/lib/showcase-store.js +92 -92
- package/server/lib/site-publish.js +465 -465
- package/server/lib/ssrf-guard.js +432 -432
- package/server/lib/task-scan.js +158 -158
- package/server/lib/tavern-json.js +154 -154
- package/server/lib/video-variant.js +237 -237
- package/server/lib/workspace-path.js +33 -33
- package/server/ops/fix-sdk-musl.mjs +40 -40
- package/server/ops/install-macos-fonts.sh +114 -114
- package/server/ops/macos-fonts.conf +336 -336
- package/server/ops/sandbox-shim/bwrap +57 -57
- package/server/projects/assets-summary.js +119 -119
- package/server/projects/auto-name.js +53 -53
- package/server/projects/board-store.js +640 -640
- package/server/projects/move-entry.js +103 -103
- package/server/projects/store.js +270 -270
- package/server/projects/ui-config.js +54 -54
- package/server/projects/workspace-templates.js +53 -53
- package/server/projects/workspace.js +1025 -1025
- package/server/runtime/local-config.js +177 -177
- package/server/runtime/profile.js +72 -72
- package/server/services/rembg-service.py +334 -334
- package/server/shared/README.md +25 -25
- package/server/shared/deck.js +54 -54
- package/server/shared/time.js +49 -49
- package/server/style-pipeline/README.md +53 -53
- package/server/ws/broker.js +36 -36
- package/server/ws/browse-channel.js +177 -177
- package/server/ws/index.js +386 -386
- package/web/README.md +89 -89
- package/web/dist/assets/BrowserWindow-BYCgizHw.js +11 -0
- package/web/dist/assets/{DeckWindow-4G0xX7P3.js → DeckWindow-pNydEM4L.js} +1 -1
- package/web/dist/assets/{DocxWindow-BXQsoPkh.js → DocxWindow-PPgzh3Gq.js} +1 -1
- package/web/dist/assets/{SiteWindow-D6eS-2gM.js → SiteWindow-DM0LQPCO.js} +1 -1
- package/web/dist/assets/{index-Cbl3ATvp.js → index-BHTBJlpY.js} +32 -32
- package/web/dist/assets/{pending-edit-apply-DTjo2geW.js → pending-edit-apply-C0I13CbD.js} +1 -1
- package/web/dist/assets/{rotate-cw-LMT3x1Zj.js → rotate-cw-B1UQ8Hld.js} +1 -1
- package/web/dist/assets/{square-dashed-mouse-pointer-CBoAkfhi.js → square-dashed-mouse-pointer-8lrgqmfv.js} +1 -1
- package/web/dist/index.html +17 -17
- package/web/dist/assets/BrowserWindow-Dc7J7CYc.js +0 -11
|
@@ -1,418 +1,418 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AgentContext — 一次 run 的运行时上下文(SDK 包装版)
|
|
3
|
-
*
|
|
4
|
-
* 形态变更(2026-04-29 战略转向):
|
|
5
|
-
* - 之前:自写 agent-loop + tool-registry,ctx 含 todoState / forTool() 给 tool execute 用
|
|
6
|
-
* - 现在:包 Claude Agent SDK 的 query(),工具由 SDK 内置 + 子进程跑,ctx 不再管 tool 执行
|
|
7
|
-
*
|
|
8
|
-
* 职责:
|
|
9
|
-
* - 串起 runId / skillId / EventBus / AbortController
|
|
10
|
-
* - 提供 workspace.* 包装供 skill loader / 业务逻辑读取沙盒
|
|
11
|
-
* - 暴露 emit() 把业务事件推到 EventBus(给 WS 桥接)
|
|
12
|
-
* - 跟踪 SDK 返回的 sessionId(从首条 message 提取,存 metadata)
|
|
13
|
-
* - 维护 counters(rounds / tokens / cost / errors)落 run.metadata
|
|
14
|
-
*
|
|
15
|
-
* 不再管:
|
|
16
|
-
* - todoState(SDK TodoWrite 工具自管)
|
|
17
|
-
* - forTool()(SDK 自己管 tool execute 上下文)
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
import { promises as fs } from 'fs';
|
|
21
|
-
import path from 'path';
|
|
22
|
-
import { ensureWorkspace, getWorkspaceRoot, readFile, writeFile, listDir, exists, safeResolve } from '../runtime/workspace.js';
|
|
23
|
-
import { EventBus } from './events.js';
|
|
24
|
-
import { repriceUsageDeltas } from './model-context.js';
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* 一个回合的计数器初值。**唯一形状真相**:AgentContext 构造函数与 session-loop 的
|
|
28
|
-
* startTurn(每个回合重置一次)都用它。
|
|
29
|
-
*
|
|
30
|
-
* ⛔ 08-21 夜血案:startTurn 里原来抄了一份字面量,08-21 深夜给构造函数加 `toolCharges`
|
|
31
|
-
* 时没同步过去 → startTurn 必定跑在任何工具之前 → `addToolCharge` 读 undefined 直接
|
|
32
|
-
* TypeError:generate_image 的 $0.20 一分没记,而且**成功出的图还会变成一次工具报错**。
|
|
33
|
-
* 收成一份就没有下一次。
|
|
34
|
-
*/
|
|
35
|
-
export function freshTurnCounters() {
|
|
36
|
-
return {
|
|
37
|
-
turns: 0, // SDK num_turns
|
|
38
|
-
toolCalls: 0,
|
|
39
|
-
toolFailures: 0,
|
|
40
|
-
compactBoundaries: 0,
|
|
41
|
-
apiRetries: 0,
|
|
42
|
-
durationMs: 0,
|
|
43
|
-
durationApiMs: 0,
|
|
44
|
-
totalCostUsd: 0,
|
|
45
|
-
inputTokens: 0,
|
|
46
|
-
outputTokens: 0,
|
|
47
|
-
cacheReadTokens: 0,
|
|
48
|
-
cacheCreateTokens: 0,
|
|
49
|
-
modelUsage: null, // 分模型本 turn 增量(absorbResult 差分产物)
|
|
50
|
-
toolCharges: {}, // 按件计价的工具花费(generate_image $0.20/张),absorbResult 末尾并进 modelUsage
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export class AgentContext {
|
|
55
|
-
/**
|
|
56
|
-
* @param {object} opts
|
|
57
|
-
* @param {string} opts.runId
|
|
58
|
-
* @param {string} opts.skillId
|
|
59
|
-
* @param {EventBus} [opts.eventBus]
|
|
60
|
-
* @param {AbortController} [opts.abortController]
|
|
61
|
-
* @param {object} [opts.metadata={}]
|
|
62
|
-
* @param {string} [opts.workspaceRoot] - 外部 workspace(如 per-project 目录);
|
|
63
|
-
* 传了就走它,否则用 runId 推路径(旧 smoke 兼容)
|
|
64
|
-
* @param {string} [opts.sessionId] - 当前 SDK session id;emit 时自动 enrich 到事件,
|
|
65
|
-
* 让 WS 端按 sid 过滤防跨 session 串扰
|
|
66
|
-
* @param {string} [opts.appModel] - NoDesign 上层真实 model(如 kimi-k2.6)。
|
|
67
|
-
* 区别于 sdkOptions.model(spoofing alias,让 SDK
|
|
68
|
-
* 内部 rawMaxTokens 算对)。hooks/events 算真实
|
|
69
|
-
* context window 时按 appModel 查映射表。
|
|
70
|
-
*/
|
|
71
|
-
constructor({ runId, skillId, eventBus, abortController, metadata = {}, workspaceRoot = null, sessionId = null, appModel = null }) {
|
|
72
|
-
if (!runId) throw new Error('AgentContext: runId required');
|
|
73
|
-
if (!skillId) throw new Error('AgentContext: skillId required');
|
|
74
|
-
|
|
75
|
-
this.runId = runId;
|
|
76
|
-
this.skillId = skillId;
|
|
77
|
-
this.sessionId = sessionId;
|
|
78
|
-
this.appModel = appModel;
|
|
79
|
-
this.eventBus = eventBus || new EventBus();
|
|
80
|
-
this.abortController = abortController || new AbortController();
|
|
81
|
-
this.metadata = metadata;
|
|
82
|
-
this._externalWorkspaceRoot = workspaceRoot;
|
|
83
|
-
|
|
84
|
-
// SDK 在 message 流里返回 session_id,首次见到时记下
|
|
85
|
-
this.sdkSessionId = null;
|
|
86
|
-
|
|
87
|
-
// result.modelUsage 的差分基准(2026-07-31)。modelUsage 是**会话累计值**
|
|
88
|
-
// (探针实测:turn2 的值 = turn1 + turn2),要拿到本 turn 增量必须对上一条
|
|
89
|
-
// result 做差。基准跟 ctx 同生命周期 = 跟 SDK query stream 同生命周期
|
|
90
|
-
// (runSession 每次 new AgentContext + new stream),stream 重启累计值归零、
|
|
91
|
-
// 基准也归零,差分天然对齐。**不随 turn 边界重置**。
|
|
92
|
-
this._modelUsageBase = null;
|
|
93
|
-
|
|
94
|
-
// 可观测计数器(落 run.metadata 用)—— 形状只有 freshTurnCounters 一份
|
|
95
|
-
this.counters = freshTurnCounters();
|
|
96
|
-
|
|
97
|
-
this.startedAt = Date.now();
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// ── 取消 ──
|
|
101
|
-
|
|
102
|
-
get signal() {
|
|
103
|
-
return this.abortController.signal;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* 取消 run。Phase 3c 加幂等保证 —— cancel 多次只触发一次 abort + 一次
|
|
108
|
-
* run.cancelled emit。两条调用路径都依赖此幂等:
|
|
109
|
-
* 1. cancelRun(active-runs.js)race window / interrupt 兜底时直接调
|
|
110
|
-
* 2. session-loop.js result 处理识别 terminal_reason: 'aborted_*' 时调(interrupt 路径)
|
|
111
|
-
*
|
|
112
|
-
* 没有这个 flag,interrupt 路径 + abort 兜底会双 emit run.cancelled,
|
|
113
|
-
* 前端 toast '已取消' 弹两次。
|
|
114
|
-
*/
|
|
115
|
-
cancel(reason = 'user_cancel') {
|
|
116
|
-
if (this._cancelled) return;
|
|
117
|
-
this._cancelled = true;
|
|
118
|
-
this.abortController.abort(reason);
|
|
119
|
-
this.emit({ type: 'run.cancelled', reason });
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
ensureNotAborted() {
|
|
123
|
-
if (this.signal.aborted) {
|
|
124
|
-
const err = new Error(`Run ${this.runId} aborted: ${this.signal.reason || 'unknown'}`);
|
|
125
|
-
err.code = 'AGENT_ABORTED';
|
|
126
|
-
throw err;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// ── 事件 ──
|
|
131
|
-
|
|
132
|
-
emit(event) {
|
|
133
|
-
if (!event || !event.type) throw new Error('emit: event.type required');
|
|
134
|
-
// sessionId 自动 enrich 让 WS 端按 sid 过滤事件,防多 session / 多 tab 跨 session
|
|
135
|
-
// 串扰(同 project bus 共享)。`...event` 在后保证调用方显式传的字段不被覆盖。
|
|
136
|
-
const enriched = {
|
|
137
|
-
runId: this.runId,
|
|
138
|
-
sessionId: this.sessionId,
|
|
139
|
-
ts: new Date().toISOString(),
|
|
140
|
-
...event,
|
|
141
|
-
};
|
|
142
|
-
this.eventBus.publish(enriched);
|
|
143
|
-
return enriched;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// ── workspace 包装 ──
|
|
147
|
-
// 外部 workspaceRoot 模式(P0 per-project 目录):直接走绝对路径,
|
|
148
|
-
// 由调用方负责 mkdir + git init(见 projects/workspace.js)。
|
|
149
|
-
// 旧 runId 模式(保留给 smoke 测试 / 单 run 沙盒):走 runtime/workspace.js 那套。
|
|
150
|
-
|
|
151
|
-
workspace = {
|
|
152
|
-
ensure: async () => {
|
|
153
|
-
if (this._externalWorkspaceRoot) {
|
|
154
|
-
await fs.mkdir(this._externalWorkspaceRoot, { recursive: true });
|
|
155
|
-
return this._externalWorkspaceRoot;
|
|
156
|
-
}
|
|
157
|
-
return ensureWorkspace(this.runId);
|
|
158
|
-
},
|
|
159
|
-
root: () => this._externalWorkspaceRoot || getWorkspaceRoot(this.runId),
|
|
160
|
-
exists: async (rel) => {
|
|
161
|
-
if (this._externalWorkspaceRoot) {
|
|
162
|
-
try {
|
|
163
|
-
await fs.access(path.resolve(this._externalWorkspaceRoot, rel));
|
|
164
|
-
return true;
|
|
165
|
-
} catch {
|
|
166
|
-
return false;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
return exists(this.runId, rel);
|
|
170
|
-
},
|
|
171
|
-
read: (rel) => readFile(this.runId, rel),
|
|
172
|
-
write: (rel, content) => writeFile(this.runId, rel, content),
|
|
173
|
-
list: (rel) => listDir(this.runId, rel),
|
|
174
|
-
resolve: (rel) => safeResolve(this.runId, rel),
|
|
175
|
-
};
|
|
176
|
-
|
|
177
|
-
// ── 跟踪 SDK 数据 ──
|
|
178
|
-
|
|
179
|
-
/** SDK 第一条 message 带 session_id,记下供 metadata / debug 用 */
|
|
180
|
-
recordSdkSession(sessionId) {
|
|
181
|
-
if (!this.sdkSessionId && sessionId) {
|
|
182
|
-
this.sdkSessionId = sessionId;
|
|
183
|
-
this.emit({ type: 'run.sdk.session', sessionId });
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* SDK SDKResultMessage 含全套统计;一次性吸收。
|
|
189
|
-
*
|
|
190
|
-
* 计量口径(2026-07-31 起):权威数据源是 result.modelUsage —— SDK 的分模型
|
|
191
|
-
* 总账,含子代理消耗,中断的 turn 也不丢(累计值持续增长,下一条 result 的
|
|
192
|
-
* 差分会捞回来)。差分出的本 turn 增量:
|
|
193
|
-
* - 按模型汇总进 counters.inputTokens 等主字段(配额真列的来源)
|
|
194
|
-
* - 分模型明细留在 counters.modelUsage,finishTurn 落 run_model_usage 表
|
|
195
|
-
* - totalCostUsd 同样取差分(result.total_cost_usd 是会话累计值,以前
|
|
196
|
-
* 原样落库导致 cost 列不能 sum —— 那是虚价,但至少语义要对)
|
|
197
|
-
* result.usage 只做 modelUsage 缺失时的兜底(usage 是本轮真增量,探针实测)。
|
|
198
|
-
*/
|
|
199
|
-
absorbResult(result) {
|
|
200
|
-
if (!result) return;
|
|
201
|
-
this.counters.turns = result.num_turns ?? this.counters.turns;
|
|
202
|
-
this.counters.durationMs = result.duration_ms ?? this.counters.durationMs;
|
|
203
|
-
this.counters.durationApiMs = result.duration_api_ms ?? this.counters.durationApiMs;
|
|
204
|
-
|
|
205
|
-
// reprice:仅 API 会话 —— key 从 SDK spoof alias 还原成 appModel、按表价
|
|
206
|
-
// 重算 costUsd(SDK 按 alias 的 Claude 价目表算,Kimi 时代虚高 30×)。
|
|
207
|
-
// 订阅会话原样过(alias 与真订阅模型名同形,必须以会话通路为准,见
|
|
208
|
-
// model-context.js repriceUsageDeltas 注释)。
|
|
209
|
-
const deltas = repriceUsageDeltas(this._diffModelUsage(result.modelUsage), this.appModel);
|
|
210
|
-
if (deltas) {
|
|
211
|
-
let inp = 0; let out = 0; let cr = 0; let cc = 0; let cost = 0;
|
|
212
|
-
for (const d of Object.values(deltas)) {
|
|
213
|
-
inp += d.inputTokens; out += d.outputTokens;
|
|
214
|
-
cr += d.cacheReadTokens; cc += d.cacheCreateTokens;
|
|
215
|
-
cost += d.costUsd;
|
|
216
|
-
}
|
|
217
|
-
this.counters.inputTokens = inp;
|
|
218
|
-
this.counters.outputTokens = out;
|
|
219
|
-
this.counters.cacheReadTokens = cr;
|
|
220
|
-
this.counters.cacheCreateTokens = cc;
|
|
221
|
-
this.counters.totalCostUsd = cost;
|
|
222
|
-
this.counters.modelUsage = deltas;
|
|
223
|
-
} else if (result.usage) {
|
|
224
|
-
this.counters.inputTokens = result.usage.input_tokens || 0;
|
|
225
|
-
this.counters.outputTokens = result.usage.output_tokens || 0;
|
|
226
|
-
this.counters.cacheReadTokens = result.usage.cache_read_input_tokens || 0;
|
|
227
|
-
this.counters.cacheCreateTokens = result.usage.cache_creation_input_tokens || 0;
|
|
228
|
-
this.counters.totalCostUsd = result.total_cost_usd ?? this.counters.totalCostUsd;
|
|
229
|
-
}
|
|
230
|
-
this._foldToolCharges();
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
/**
|
|
234
|
-
* 半截续接的账要**结转**(08-21 夜,fable 评审 P0)。
|
|
235
|
-
*
|
|
236
|
-
* 病:absorbResult 对 counters 是**赋值不是累加**(差分基准 _modelUsageBase 在第一个
|
|
237
|
-
* result 就前移了),而半截续接会让同一个 run 出现第二个 result → 第一轮的整份账
|
|
238
|
-
* (token、表价 cost、已经折进去的 generate_image 按件费、上游自报的真 cost)被第二轮
|
|
239
|
-
* 的增量整个覆盖 → 那一轮等于白跑,日限闸门(quota.usedCostToday 读 run_model_usage)少收。
|
|
240
|
-
* 真路径实测:续接的回合上游真打 2 发、落库只有 1 发的量。
|
|
241
|
-
*
|
|
242
|
-
* 用法:推续接消息**之前** takeCarry() 存一份,下一个 result 的 absorbResult /
|
|
243
|
-
* applyUpstreamBilling 跑完后 addCarry(carry) 加回去。多次续接会一路滚下去。
|
|
244
|
-
*
|
|
245
|
-
* ⚠️ 只加"可累加"的量:token / cost / 时长 / 工具计数。`turns` 不加 —— 它是 SDK 的
|
|
246
|
-
* num_turns,本身带累计语义,加了会双数。
|
|
247
|
-
*/
|
|
248
|
-
takeCarry() {
|
|
249
|
-
const c = this.counters;
|
|
250
|
-
return {
|
|
251
|
-
inputTokens: c.inputTokens || 0,
|
|
252
|
-
outputTokens: c.outputTokens || 0,
|
|
253
|
-
cacheReadTokens: c.cacheReadTokens || 0,
|
|
254
|
-
cacheCreateTokens: c.cacheCreateTokens || 0,
|
|
255
|
-
totalCostUsd: c.totalCostUsd || 0,
|
|
256
|
-
durationMs: c.durationMs || 0,
|
|
257
|
-
durationApiMs: c.durationApiMs || 0,
|
|
258
|
-
toolCalls: c.toolCalls || 0,
|
|
259
|
-
toolFailures: c.toolFailures || 0,
|
|
260
|
-
compactBoundaries: c.compactBoundaries || 0,
|
|
261
|
-
apiRetries: c.apiRetries || 0,
|
|
262
|
-
modelUsage: c.modelUsage ? JSON.parse(JSON.stringify(c.modelUsage)) : null,
|
|
263
|
-
};
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
addCarry(carry) {
|
|
267
|
-
if (!carry) return;
|
|
268
|
-
const c = this.counters;
|
|
269
|
-
for (const k of ['inputTokens', 'outputTokens', 'cacheReadTokens', 'cacheCreateTokens', 'totalCostUsd', 'durationMs', 'durationApiMs']) {
|
|
270
|
-
c[k] = (c[k] || 0) + (carry[k] || 0);
|
|
271
|
-
}
|
|
272
|
-
// toolCalls 等计数器**不是**每回合重置后单调累加的差分量,而是 handleSDKMessage 一路 ++ 的,
|
|
273
|
-
// 续接期间它们没被重置(同一个 run 同一个 ctx)→ 已经含了第一轮,别再加一遍。
|
|
274
|
-
if (carry.modelUsage) {
|
|
275
|
-
const usage = c.modelUsage && typeof c.modelUsage === 'object' ? c.modelUsage : {};
|
|
276
|
-
for (const [name, d] of Object.entries(carry.modelUsage)) {
|
|
277
|
-
const prev = usage[name] || { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreateTokens: 0, costUsd: 0 };
|
|
278
|
-
usage[name] = {
|
|
279
|
-
...prev,
|
|
280
|
-
inputTokens: (prev.inputTokens || 0) + (d.inputTokens || 0),
|
|
281
|
-
outputTokens: (prev.outputTokens || 0) + (d.outputTokens || 0),
|
|
282
|
-
cacheReadTokens: (prev.cacheReadTokens || 0) + (d.cacheReadTokens || 0),
|
|
283
|
-
cacheCreateTokens: (prev.cacheCreateTokens || 0) + (d.cacheCreateTokens || 0),
|
|
284
|
-
costUsd: (prev.costUsd || 0) + (d.costUsd || 0),
|
|
285
|
-
};
|
|
286
|
-
}
|
|
287
|
-
c.modelUsage = usage;
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
/** 按件计价的工具花费(generate_image 每张 $0.20):工具成功后记一笔,结账时并进 modelUsage 同一本账 */
|
|
292
|
-
addToolCharge(name, usd) {
|
|
293
|
-
const v = Number(usd);
|
|
294
|
-
if (!name || !Number.isFinite(v) || v <= 0) return;
|
|
295
|
-
this.counters.toolCharges[name] = (this.counters.toolCharges[name] || 0) + v;
|
|
296
|
-
}
|
|
297
|
-
_foldToolCharges() {
|
|
298
|
-
const charges = this.counters.toolCharges || {};
|
|
299
|
-
const names = Object.keys(charges);
|
|
300
|
-
if (!names.length) return;
|
|
301
|
-
const usage = this.counters.modelUsage && typeof this.counters.modelUsage === 'object' ? this.counters.modelUsage : {};
|
|
302
|
-
for (const name of names) {
|
|
303
|
-
const prev = usage[name] || { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreateTokens: 0, costUsd: 0 };
|
|
304
|
-
usage[name] = { ...prev, costUsd: (prev.costUsd || 0) + charges[name] };
|
|
305
|
-
}
|
|
306
|
-
this.counters.modelUsage = usage;
|
|
307
|
-
this.counters.totalCostUsd = Object.values(usage).reduce((s, d) => s + (d.costUsd || 0), 0);
|
|
308
|
-
this.counters.toolCharges = {};
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
/**
|
|
312
|
-
* 上游自报的费用覆盖(08-21 晚,lib/ingress/upstream-billing.js)。
|
|
313
|
-
* billing = { appModel → { costUsd, responses, promptTokens, completionTokens, cachedTokens } },
|
|
314
|
-
* 是 ingress 在本轮累加、session-loop 结账前取走的。规则:
|
|
315
|
-
* - 上游报的 cost **> 0**(真扣了余额)→ 覆盖 modelUsage[appModel].costUsd;token 数仍信 SDK 差分(口径不同)
|
|
316
|
-
* - 上游报 0 / 没报 → 保留表价算出来的数。OpenCode Go 订阅行(08-21 深夜实测)对订阅额度内的请求报 cost=0,
|
|
317
|
-
* 但它消耗的是全站共享的 Go 池子($12/5h),按表价记才能让每用户日限跟着受控;免费行表价本就是 0
|
|
318
|
-
* - SDK 没有该模型条目(CLI 失败 / 没覆盖到的 helper)→ 用上游 token 数补一条,按表价算(repriceUsageDeltas)
|
|
319
|
-
* 覆盖后重算 totalCostUsd。counters.costSource:'upstream'(上游真扣费覆盖过)| 'table'(只按表价)
|
|
320
|
-
*/
|
|
321
|
-
applyUpstreamBilling(billing) {
|
|
322
|
-
if (!billing || typeof billing !== 'object') return;
|
|
323
|
-
let touched = false; let overrode = false;
|
|
324
|
-
const usage = this.counters.modelUsage && typeof this.counters.modelUsage === 'object' ? this.counters.modelUsage : {};
|
|
325
|
-
for (const [appModel, acc] of Object.entries(billing)) {
|
|
326
|
-
if (!acc || !acc.responses) continue;
|
|
327
|
-
const reported = acc.costUsd != null && Number(acc.costUsd) > 0 ? Number(acc.costUsd) : null;
|
|
328
|
-
const prev = usage[appModel];
|
|
329
|
-
if (prev) {
|
|
330
|
-
if (reported != null) { usage[appModel] = { ...prev, costUsd: reported }; overrode = true; touched = true; }
|
|
331
|
-
continue;
|
|
332
|
-
}
|
|
333
|
-
// SDK 没这条(CLI 失败那类):用上游 token 数补一条,先按表价,再看上游有没有真扣费
|
|
334
|
-
const cached = acc.cachedTokens || 0;
|
|
335
|
-
const raw = { [appModel]: { inputTokens: Math.max(0, (acc.promptTokens || 0) - cached), outputTokens: acc.completionTokens || 0, cacheReadTokens: cached, cacheCreateTokens: 0, costUsd: 0 } };
|
|
336
|
-
const priced = repriceUsageDeltas(raw, this.appModel)?.[appModel] || raw[appModel];
|
|
337
|
-
usage[appModel] = reported != null ? { ...priced, costUsd: reported } : priced;
|
|
338
|
-
if (reported != null) overrode = true;
|
|
339
|
-
touched = true;
|
|
340
|
-
}
|
|
341
|
-
if (!touched) return;
|
|
342
|
-
this.counters.modelUsage = usage;
|
|
343
|
-
this.counters.totalCostUsd = Object.values(usage).reduce((s, d) => s + (d.costUsd || 0), 0);
|
|
344
|
-
this.counters.costSource = overrode ? 'upstream' : 'table';
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
/**
|
|
348
|
-
* 会话累计的 modelUsage → 本 turn 增量。返回 null 表示"没有可用的 modelUsage"
|
|
349
|
-
* (调用方退回 usage 兜底);返回 {}(空对象)表示"有 modelUsage 但本 turn
|
|
350
|
-
* 没有任何新消耗",此时主字段照常清零是正确语义。
|
|
351
|
-
* Math.max(0, ...) 防浮点 / 上游异常出负数;基准无论如何都更新成最新快照。
|
|
352
|
-
*/
|
|
353
|
-
_diffModelUsage(modelUsage) {
|
|
354
|
-
if (!modelUsage || typeof modelUsage !== 'object') return null;
|
|
355
|
-
const models = Object.keys(modelUsage);
|
|
356
|
-
if (models.length === 0) return null;
|
|
357
|
-
const base = this._modelUsageBase || {};
|
|
358
|
-
const deltas = {};
|
|
359
|
-
const snapshot = {};
|
|
360
|
-
for (const model of models) {
|
|
361
|
-
const u = modelUsage[model] || {};
|
|
362
|
-
const b = base[model] || {};
|
|
363
|
-
snapshot[model] = {
|
|
364
|
-
inputTokens: u.inputTokens || 0,
|
|
365
|
-
outputTokens: u.outputTokens || 0,
|
|
366
|
-
cacheReadInputTokens: u.cacheReadInputTokens || 0,
|
|
367
|
-
cacheCreationInputTokens: u.cacheCreationInputTokens || 0,
|
|
368
|
-
costUSD: u.costUSD || 0,
|
|
369
|
-
};
|
|
370
|
-
const d = {
|
|
371
|
-
inputTokens: Math.max(0, snapshot[model].inputTokens - (b.inputTokens || 0)),
|
|
372
|
-
outputTokens: Math.max(0, snapshot[model].outputTokens - (b.outputTokens || 0)),
|
|
373
|
-
cacheReadTokens: Math.max(0, snapshot[model].cacheReadInputTokens - (b.cacheReadInputTokens || 0)),
|
|
374
|
-
cacheCreateTokens: Math.max(0, snapshot[model].cacheCreationInputTokens - (b.cacheCreationInputTokens || 0)),
|
|
375
|
-
costUsd: Math.max(0, snapshot[model].costUSD - (b.costUSD || 0)),
|
|
376
|
-
};
|
|
377
|
-
if (d.inputTokens || d.outputTokens || d.cacheReadTokens || d.cacheCreateTokens || d.costUsd) {
|
|
378
|
-
deltas[model] = d;
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
// 基准里已有但本次 modelUsage 没出现的模型:保留旧值(SDK 不应该丢 key,
|
|
382
|
-
// 防御性合并,免得它真丢时差分把历史消耗又算一遍)
|
|
383
|
-
this._modelUsageBase = { ...base, ...snapshot };
|
|
384
|
-
return deltas;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
incrementTool(failed = false) {
|
|
388
|
-
this.counters.toolCalls += 1;
|
|
389
|
-
if (failed) this.counters.toolFailures += 1;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
/**
|
|
393
|
-
* 子代理收尾用量(SDK task_notification.usage)累积(2026-07-30)。
|
|
394
|
-
* **单独记账,不加进主 inputTokens/outputTokens** —— 主字段现在来自
|
|
395
|
-
* modelUsage 差分(SDK 的分模型总账,已含 sidechain),相加必然双重计数。
|
|
396
|
-
* 这两个字段只进 metadata 做可观测("这轮子代理烧了多少"),也是交叉验证
|
|
397
|
-
* 钩子:某轮 subagent 用量明显大于 modelUsage 差分总和 = 总账没含 sidechain
|
|
398
|
-
* 的信号(目前判断是含的,出现反例再查)。
|
|
399
|
-
*/
|
|
400
|
-
absorbSubagentUsage(usage) {
|
|
401
|
-
if (!usage || typeof usage !== 'object') return;
|
|
402
|
-
this.counters.subagentInputTokens = (this.counters.subagentInputTokens || 0) + (usage.input_tokens || 0);
|
|
403
|
-
this.counters.subagentOutputTokens = (this.counters.subagentOutputTokens || 0) + (usage.output_tokens || 0);
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
// ── observability ──
|
|
407
|
-
|
|
408
|
-
snapshot() {
|
|
409
|
-
return {
|
|
410
|
-
runId: this.runId,
|
|
411
|
-
skillId: this.skillId,
|
|
412
|
-
sdkSessionId: this.sdkSessionId,
|
|
413
|
-
counters: { ...this.counters },
|
|
414
|
-
elapsedMs: Date.now() - this.startedAt,
|
|
415
|
-
aborted: this.signal.aborted,
|
|
416
|
-
};
|
|
417
|
-
}
|
|
418
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* AgentContext — 一次 run 的运行时上下文(SDK 包装版)
|
|
3
|
+
*
|
|
4
|
+
* 形态变更(2026-04-29 战略转向):
|
|
5
|
+
* - 之前:自写 agent-loop + tool-registry,ctx 含 todoState / forTool() 给 tool execute 用
|
|
6
|
+
* - 现在:包 Claude Agent SDK 的 query(),工具由 SDK 内置 + 子进程跑,ctx 不再管 tool 执行
|
|
7
|
+
*
|
|
8
|
+
* 职责:
|
|
9
|
+
* - 串起 runId / skillId / EventBus / AbortController
|
|
10
|
+
* - 提供 workspace.* 包装供 skill loader / 业务逻辑读取沙盒
|
|
11
|
+
* - 暴露 emit() 把业务事件推到 EventBus(给 WS 桥接)
|
|
12
|
+
* - 跟踪 SDK 返回的 sessionId(从首条 message 提取,存 metadata)
|
|
13
|
+
* - 维护 counters(rounds / tokens / cost / errors)落 run.metadata
|
|
14
|
+
*
|
|
15
|
+
* 不再管:
|
|
16
|
+
* - todoState(SDK TodoWrite 工具自管)
|
|
17
|
+
* - forTool()(SDK 自己管 tool execute 上下文)
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { promises as fs } from 'fs';
|
|
21
|
+
import path from 'path';
|
|
22
|
+
import { ensureWorkspace, getWorkspaceRoot, readFile, writeFile, listDir, exists, safeResolve } from '../runtime/workspace.js';
|
|
23
|
+
import { EventBus } from './events.js';
|
|
24
|
+
import { repriceUsageDeltas } from './model-context.js';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 一个回合的计数器初值。**唯一形状真相**:AgentContext 构造函数与 session-loop 的
|
|
28
|
+
* startTurn(每个回合重置一次)都用它。
|
|
29
|
+
*
|
|
30
|
+
* ⛔ 08-21 夜血案:startTurn 里原来抄了一份字面量,08-21 深夜给构造函数加 `toolCharges`
|
|
31
|
+
* 时没同步过去 → startTurn 必定跑在任何工具之前 → `addToolCharge` 读 undefined 直接
|
|
32
|
+
* TypeError:generate_image 的 $0.20 一分没记,而且**成功出的图还会变成一次工具报错**。
|
|
33
|
+
* 收成一份就没有下一次。
|
|
34
|
+
*/
|
|
35
|
+
export function freshTurnCounters() {
|
|
36
|
+
return {
|
|
37
|
+
turns: 0, // SDK num_turns
|
|
38
|
+
toolCalls: 0,
|
|
39
|
+
toolFailures: 0,
|
|
40
|
+
compactBoundaries: 0,
|
|
41
|
+
apiRetries: 0,
|
|
42
|
+
durationMs: 0,
|
|
43
|
+
durationApiMs: 0,
|
|
44
|
+
totalCostUsd: 0,
|
|
45
|
+
inputTokens: 0,
|
|
46
|
+
outputTokens: 0,
|
|
47
|
+
cacheReadTokens: 0,
|
|
48
|
+
cacheCreateTokens: 0,
|
|
49
|
+
modelUsage: null, // 分模型本 turn 增量(absorbResult 差分产物)
|
|
50
|
+
toolCharges: {}, // 按件计价的工具花费(generate_image $0.20/张),absorbResult 末尾并进 modelUsage
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export class AgentContext {
|
|
55
|
+
/**
|
|
56
|
+
* @param {object} opts
|
|
57
|
+
* @param {string} opts.runId
|
|
58
|
+
* @param {string} opts.skillId
|
|
59
|
+
* @param {EventBus} [opts.eventBus]
|
|
60
|
+
* @param {AbortController} [opts.abortController]
|
|
61
|
+
* @param {object} [opts.metadata={}]
|
|
62
|
+
* @param {string} [opts.workspaceRoot] - 外部 workspace(如 per-project 目录);
|
|
63
|
+
* 传了就走它,否则用 runId 推路径(旧 smoke 兼容)
|
|
64
|
+
* @param {string} [opts.sessionId] - 当前 SDK session id;emit 时自动 enrich 到事件,
|
|
65
|
+
* 让 WS 端按 sid 过滤防跨 session 串扰
|
|
66
|
+
* @param {string} [opts.appModel] - NoDesign 上层真实 model(如 kimi-k2.6)。
|
|
67
|
+
* 区别于 sdkOptions.model(spoofing alias,让 SDK
|
|
68
|
+
* 内部 rawMaxTokens 算对)。hooks/events 算真实
|
|
69
|
+
* context window 时按 appModel 查映射表。
|
|
70
|
+
*/
|
|
71
|
+
constructor({ runId, skillId, eventBus, abortController, metadata = {}, workspaceRoot = null, sessionId = null, appModel = null }) {
|
|
72
|
+
if (!runId) throw new Error('AgentContext: runId required');
|
|
73
|
+
if (!skillId) throw new Error('AgentContext: skillId required');
|
|
74
|
+
|
|
75
|
+
this.runId = runId;
|
|
76
|
+
this.skillId = skillId;
|
|
77
|
+
this.sessionId = sessionId;
|
|
78
|
+
this.appModel = appModel;
|
|
79
|
+
this.eventBus = eventBus || new EventBus();
|
|
80
|
+
this.abortController = abortController || new AbortController();
|
|
81
|
+
this.metadata = metadata;
|
|
82
|
+
this._externalWorkspaceRoot = workspaceRoot;
|
|
83
|
+
|
|
84
|
+
// SDK 在 message 流里返回 session_id,首次见到时记下
|
|
85
|
+
this.sdkSessionId = null;
|
|
86
|
+
|
|
87
|
+
// result.modelUsage 的差分基准(2026-07-31)。modelUsage 是**会话累计值**
|
|
88
|
+
// (探针实测:turn2 的值 = turn1 + turn2),要拿到本 turn 增量必须对上一条
|
|
89
|
+
// result 做差。基准跟 ctx 同生命周期 = 跟 SDK query stream 同生命周期
|
|
90
|
+
// (runSession 每次 new AgentContext + new stream),stream 重启累计值归零、
|
|
91
|
+
// 基准也归零,差分天然对齐。**不随 turn 边界重置**。
|
|
92
|
+
this._modelUsageBase = null;
|
|
93
|
+
|
|
94
|
+
// 可观测计数器(落 run.metadata 用)—— 形状只有 freshTurnCounters 一份
|
|
95
|
+
this.counters = freshTurnCounters();
|
|
96
|
+
|
|
97
|
+
this.startedAt = Date.now();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ── 取消 ──
|
|
101
|
+
|
|
102
|
+
get signal() {
|
|
103
|
+
return this.abortController.signal;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 取消 run。Phase 3c 加幂等保证 —— cancel 多次只触发一次 abort + 一次
|
|
108
|
+
* run.cancelled emit。两条调用路径都依赖此幂等:
|
|
109
|
+
* 1. cancelRun(active-runs.js)race window / interrupt 兜底时直接调
|
|
110
|
+
* 2. session-loop.js result 处理识别 terminal_reason: 'aborted_*' 时调(interrupt 路径)
|
|
111
|
+
*
|
|
112
|
+
* 没有这个 flag,interrupt 路径 + abort 兜底会双 emit run.cancelled,
|
|
113
|
+
* 前端 toast '已取消' 弹两次。
|
|
114
|
+
*/
|
|
115
|
+
cancel(reason = 'user_cancel') {
|
|
116
|
+
if (this._cancelled) return;
|
|
117
|
+
this._cancelled = true;
|
|
118
|
+
this.abortController.abort(reason);
|
|
119
|
+
this.emit({ type: 'run.cancelled', reason });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
ensureNotAborted() {
|
|
123
|
+
if (this.signal.aborted) {
|
|
124
|
+
const err = new Error(`Run ${this.runId} aborted: ${this.signal.reason || 'unknown'}`);
|
|
125
|
+
err.code = 'AGENT_ABORTED';
|
|
126
|
+
throw err;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ── 事件 ──
|
|
131
|
+
|
|
132
|
+
emit(event) {
|
|
133
|
+
if (!event || !event.type) throw new Error('emit: event.type required');
|
|
134
|
+
// sessionId 自动 enrich 让 WS 端按 sid 过滤事件,防多 session / 多 tab 跨 session
|
|
135
|
+
// 串扰(同 project bus 共享)。`...event` 在后保证调用方显式传的字段不被覆盖。
|
|
136
|
+
const enriched = {
|
|
137
|
+
runId: this.runId,
|
|
138
|
+
sessionId: this.sessionId,
|
|
139
|
+
ts: new Date().toISOString(),
|
|
140
|
+
...event,
|
|
141
|
+
};
|
|
142
|
+
this.eventBus.publish(enriched);
|
|
143
|
+
return enriched;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ── workspace 包装 ──
|
|
147
|
+
// 外部 workspaceRoot 模式(P0 per-project 目录):直接走绝对路径,
|
|
148
|
+
// 由调用方负责 mkdir + git init(见 projects/workspace.js)。
|
|
149
|
+
// 旧 runId 模式(保留给 smoke 测试 / 单 run 沙盒):走 runtime/workspace.js 那套。
|
|
150
|
+
|
|
151
|
+
workspace = {
|
|
152
|
+
ensure: async () => {
|
|
153
|
+
if (this._externalWorkspaceRoot) {
|
|
154
|
+
await fs.mkdir(this._externalWorkspaceRoot, { recursive: true });
|
|
155
|
+
return this._externalWorkspaceRoot;
|
|
156
|
+
}
|
|
157
|
+
return ensureWorkspace(this.runId);
|
|
158
|
+
},
|
|
159
|
+
root: () => this._externalWorkspaceRoot || getWorkspaceRoot(this.runId),
|
|
160
|
+
exists: async (rel) => {
|
|
161
|
+
if (this._externalWorkspaceRoot) {
|
|
162
|
+
try {
|
|
163
|
+
await fs.access(path.resolve(this._externalWorkspaceRoot, rel));
|
|
164
|
+
return true;
|
|
165
|
+
} catch {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return exists(this.runId, rel);
|
|
170
|
+
},
|
|
171
|
+
read: (rel) => readFile(this.runId, rel),
|
|
172
|
+
write: (rel, content) => writeFile(this.runId, rel, content),
|
|
173
|
+
list: (rel) => listDir(this.runId, rel),
|
|
174
|
+
resolve: (rel) => safeResolve(this.runId, rel),
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
// ── 跟踪 SDK 数据 ──
|
|
178
|
+
|
|
179
|
+
/** SDK 第一条 message 带 session_id,记下供 metadata / debug 用 */
|
|
180
|
+
recordSdkSession(sessionId) {
|
|
181
|
+
if (!this.sdkSessionId && sessionId) {
|
|
182
|
+
this.sdkSessionId = sessionId;
|
|
183
|
+
this.emit({ type: 'run.sdk.session', sessionId });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* SDK SDKResultMessage 含全套统计;一次性吸收。
|
|
189
|
+
*
|
|
190
|
+
* 计量口径(2026-07-31 起):权威数据源是 result.modelUsage —— SDK 的分模型
|
|
191
|
+
* 总账,含子代理消耗,中断的 turn 也不丢(累计值持续增长,下一条 result 的
|
|
192
|
+
* 差分会捞回来)。差分出的本 turn 增量:
|
|
193
|
+
* - 按模型汇总进 counters.inputTokens 等主字段(配额真列的来源)
|
|
194
|
+
* - 分模型明细留在 counters.modelUsage,finishTurn 落 run_model_usage 表
|
|
195
|
+
* - totalCostUsd 同样取差分(result.total_cost_usd 是会话累计值,以前
|
|
196
|
+
* 原样落库导致 cost 列不能 sum —— 那是虚价,但至少语义要对)
|
|
197
|
+
* result.usage 只做 modelUsage 缺失时的兜底(usage 是本轮真增量,探针实测)。
|
|
198
|
+
*/
|
|
199
|
+
absorbResult(result) {
|
|
200
|
+
if (!result) return;
|
|
201
|
+
this.counters.turns = result.num_turns ?? this.counters.turns;
|
|
202
|
+
this.counters.durationMs = result.duration_ms ?? this.counters.durationMs;
|
|
203
|
+
this.counters.durationApiMs = result.duration_api_ms ?? this.counters.durationApiMs;
|
|
204
|
+
|
|
205
|
+
// reprice:仅 API 会话 —— key 从 SDK spoof alias 还原成 appModel、按表价
|
|
206
|
+
// 重算 costUsd(SDK 按 alias 的 Claude 价目表算,Kimi 时代虚高 30×)。
|
|
207
|
+
// 订阅会话原样过(alias 与真订阅模型名同形,必须以会话通路为准,见
|
|
208
|
+
// model-context.js repriceUsageDeltas 注释)。
|
|
209
|
+
const deltas = repriceUsageDeltas(this._diffModelUsage(result.modelUsage), this.appModel);
|
|
210
|
+
if (deltas) {
|
|
211
|
+
let inp = 0; let out = 0; let cr = 0; let cc = 0; let cost = 0;
|
|
212
|
+
for (const d of Object.values(deltas)) {
|
|
213
|
+
inp += d.inputTokens; out += d.outputTokens;
|
|
214
|
+
cr += d.cacheReadTokens; cc += d.cacheCreateTokens;
|
|
215
|
+
cost += d.costUsd;
|
|
216
|
+
}
|
|
217
|
+
this.counters.inputTokens = inp;
|
|
218
|
+
this.counters.outputTokens = out;
|
|
219
|
+
this.counters.cacheReadTokens = cr;
|
|
220
|
+
this.counters.cacheCreateTokens = cc;
|
|
221
|
+
this.counters.totalCostUsd = cost;
|
|
222
|
+
this.counters.modelUsage = deltas;
|
|
223
|
+
} else if (result.usage) {
|
|
224
|
+
this.counters.inputTokens = result.usage.input_tokens || 0;
|
|
225
|
+
this.counters.outputTokens = result.usage.output_tokens || 0;
|
|
226
|
+
this.counters.cacheReadTokens = result.usage.cache_read_input_tokens || 0;
|
|
227
|
+
this.counters.cacheCreateTokens = result.usage.cache_creation_input_tokens || 0;
|
|
228
|
+
this.counters.totalCostUsd = result.total_cost_usd ?? this.counters.totalCostUsd;
|
|
229
|
+
}
|
|
230
|
+
this._foldToolCharges();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* 半截续接的账要**结转**(08-21 夜,fable 评审 P0)。
|
|
235
|
+
*
|
|
236
|
+
* 病:absorbResult 对 counters 是**赋值不是累加**(差分基准 _modelUsageBase 在第一个
|
|
237
|
+
* result 就前移了),而半截续接会让同一个 run 出现第二个 result → 第一轮的整份账
|
|
238
|
+
* (token、表价 cost、已经折进去的 generate_image 按件费、上游自报的真 cost)被第二轮
|
|
239
|
+
* 的增量整个覆盖 → 那一轮等于白跑,日限闸门(quota.usedCostToday 读 run_model_usage)少收。
|
|
240
|
+
* 真路径实测:续接的回合上游真打 2 发、落库只有 1 发的量。
|
|
241
|
+
*
|
|
242
|
+
* 用法:推续接消息**之前** takeCarry() 存一份,下一个 result 的 absorbResult /
|
|
243
|
+
* applyUpstreamBilling 跑完后 addCarry(carry) 加回去。多次续接会一路滚下去。
|
|
244
|
+
*
|
|
245
|
+
* ⚠️ 只加"可累加"的量:token / cost / 时长 / 工具计数。`turns` 不加 —— 它是 SDK 的
|
|
246
|
+
* num_turns,本身带累计语义,加了会双数。
|
|
247
|
+
*/
|
|
248
|
+
takeCarry() {
|
|
249
|
+
const c = this.counters;
|
|
250
|
+
return {
|
|
251
|
+
inputTokens: c.inputTokens || 0,
|
|
252
|
+
outputTokens: c.outputTokens || 0,
|
|
253
|
+
cacheReadTokens: c.cacheReadTokens || 0,
|
|
254
|
+
cacheCreateTokens: c.cacheCreateTokens || 0,
|
|
255
|
+
totalCostUsd: c.totalCostUsd || 0,
|
|
256
|
+
durationMs: c.durationMs || 0,
|
|
257
|
+
durationApiMs: c.durationApiMs || 0,
|
|
258
|
+
toolCalls: c.toolCalls || 0,
|
|
259
|
+
toolFailures: c.toolFailures || 0,
|
|
260
|
+
compactBoundaries: c.compactBoundaries || 0,
|
|
261
|
+
apiRetries: c.apiRetries || 0,
|
|
262
|
+
modelUsage: c.modelUsage ? JSON.parse(JSON.stringify(c.modelUsage)) : null,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
addCarry(carry) {
|
|
267
|
+
if (!carry) return;
|
|
268
|
+
const c = this.counters;
|
|
269
|
+
for (const k of ['inputTokens', 'outputTokens', 'cacheReadTokens', 'cacheCreateTokens', 'totalCostUsd', 'durationMs', 'durationApiMs']) {
|
|
270
|
+
c[k] = (c[k] || 0) + (carry[k] || 0);
|
|
271
|
+
}
|
|
272
|
+
// toolCalls 等计数器**不是**每回合重置后单调累加的差分量,而是 handleSDKMessage 一路 ++ 的,
|
|
273
|
+
// 续接期间它们没被重置(同一个 run 同一个 ctx)→ 已经含了第一轮,别再加一遍。
|
|
274
|
+
if (carry.modelUsage) {
|
|
275
|
+
const usage = c.modelUsage && typeof c.modelUsage === 'object' ? c.modelUsage : {};
|
|
276
|
+
for (const [name, d] of Object.entries(carry.modelUsage)) {
|
|
277
|
+
const prev = usage[name] || { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreateTokens: 0, costUsd: 0 };
|
|
278
|
+
usage[name] = {
|
|
279
|
+
...prev,
|
|
280
|
+
inputTokens: (prev.inputTokens || 0) + (d.inputTokens || 0),
|
|
281
|
+
outputTokens: (prev.outputTokens || 0) + (d.outputTokens || 0),
|
|
282
|
+
cacheReadTokens: (prev.cacheReadTokens || 0) + (d.cacheReadTokens || 0),
|
|
283
|
+
cacheCreateTokens: (prev.cacheCreateTokens || 0) + (d.cacheCreateTokens || 0),
|
|
284
|
+
costUsd: (prev.costUsd || 0) + (d.costUsd || 0),
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
c.modelUsage = usage;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** 按件计价的工具花费(generate_image 每张 $0.20):工具成功后记一笔,结账时并进 modelUsage 同一本账 */
|
|
292
|
+
addToolCharge(name, usd) {
|
|
293
|
+
const v = Number(usd);
|
|
294
|
+
if (!name || !Number.isFinite(v) || v <= 0) return;
|
|
295
|
+
this.counters.toolCharges[name] = (this.counters.toolCharges[name] || 0) + v;
|
|
296
|
+
}
|
|
297
|
+
_foldToolCharges() {
|
|
298
|
+
const charges = this.counters.toolCharges || {};
|
|
299
|
+
const names = Object.keys(charges);
|
|
300
|
+
if (!names.length) return;
|
|
301
|
+
const usage = this.counters.modelUsage && typeof this.counters.modelUsage === 'object' ? this.counters.modelUsage : {};
|
|
302
|
+
for (const name of names) {
|
|
303
|
+
const prev = usage[name] || { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreateTokens: 0, costUsd: 0 };
|
|
304
|
+
usage[name] = { ...prev, costUsd: (prev.costUsd || 0) + charges[name] };
|
|
305
|
+
}
|
|
306
|
+
this.counters.modelUsage = usage;
|
|
307
|
+
this.counters.totalCostUsd = Object.values(usage).reduce((s, d) => s + (d.costUsd || 0), 0);
|
|
308
|
+
this.counters.toolCharges = {};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* 上游自报的费用覆盖(08-21 晚,lib/ingress/upstream-billing.js)。
|
|
313
|
+
* billing = { appModel → { costUsd, responses, promptTokens, completionTokens, cachedTokens } },
|
|
314
|
+
* 是 ingress 在本轮累加、session-loop 结账前取走的。规则:
|
|
315
|
+
* - 上游报的 cost **> 0**(真扣了余额)→ 覆盖 modelUsage[appModel].costUsd;token 数仍信 SDK 差分(口径不同)
|
|
316
|
+
* - 上游报 0 / 没报 → 保留表价算出来的数。OpenCode Go 订阅行(08-21 深夜实测)对订阅额度内的请求报 cost=0,
|
|
317
|
+
* 但它消耗的是全站共享的 Go 池子($12/5h),按表价记才能让每用户日限跟着受控;免费行表价本就是 0
|
|
318
|
+
* - SDK 没有该模型条目(CLI 失败 / 没覆盖到的 helper)→ 用上游 token 数补一条,按表价算(repriceUsageDeltas)
|
|
319
|
+
* 覆盖后重算 totalCostUsd。counters.costSource:'upstream'(上游真扣费覆盖过)| 'table'(只按表价)
|
|
320
|
+
*/
|
|
321
|
+
applyUpstreamBilling(billing) {
|
|
322
|
+
if (!billing || typeof billing !== 'object') return;
|
|
323
|
+
let touched = false; let overrode = false;
|
|
324
|
+
const usage = this.counters.modelUsage && typeof this.counters.modelUsage === 'object' ? this.counters.modelUsage : {};
|
|
325
|
+
for (const [appModel, acc] of Object.entries(billing)) {
|
|
326
|
+
if (!acc || !acc.responses) continue;
|
|
327
|
+
const reported = acc.costUsd != null && Number(acc.costUsd) > 0 ? Number(acc.costUsd) : null;
|
|
328
|
+
const prev = usage[appModel];
|
|
329
|
+
if (prev) {
|
|
330
|
+
if (reported != null) { usage[appModel] = { ...prev, costUsd: reported }; overrode = true; touched = true; }
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
// SDK 没这条(CLI 失败那类):用上游 token 数补一条,先按表价,再看上游有没有真扣费
|
|
334
|
+
const cached = acc.cachedTokens || 0;
|
|
335
|
+
const raw = { [appModel]: { inputTokens: Math.max(0, (acc.promptTokens || 0) - cached), outputTokens: acc.completionTokens || 0, cacheReadTokens: cached, cacheCreateTokens: 0, costUsd: 0 } };
|
|
336
|
+
const priced = repriceUsageDeltas(raw, this.appModel)?.[appModel] || raw[appModel];
|
|
337
|
+
usage[appModel] = reported != null ? { ...priced, costUsd: reported } : priced;
|
|
338
|
+
if (reported != null) overrode = true;
|
|
339
|
+
touched = true;
|
|
340
|
+
}
|
|
341
|
+
if (!touched) return;
|
|
342
|
+
this.counters.modelUsage = usage;
|
|
343
|
+
this.counters.totalCostUsd = Object.values(usage).reduce((s, d) => s + (d.costUsd || 0), 0);
|
|
344
|
+
this.counters.costSource = overrode ? 'upstream' : 'table';
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* 会话累计的 modelUsage → 本 turn 增量。返回 null 表示"没有可用的 modelUsage"
|
|
349
|
+
* (调用方退回 usage 兜底);返回 {}(空对象)表示"有 modelUsage 但本 turn
|
|
350
|
+
* 没有任何新消耗",此时主字段照常清零是正确语义。
|
|
351
|
+
* Math.max(0, ...) 防浮点 / 上游异常出负数;基准无论如何都更新成最新快照。
|
|
352
|
+
*/
|
|
353
|
+
_diffModelUsage(modelUsage) {
|
|
354
|
+
if (!modelUsage || typeof modelUsage !== 'object') return null;
|
|
355
|
+
const models = Object.keys(modelUsage);
|
|
356
|
+
if (models.length === 0) return null;
|
|
357
|
+
const base = this._modelUsageBase || {};
|
|
358
|
+
const deltas = {};
|
|
359
|
+
const snapshot = {};
|
|
360
|
+
for (const model of models) {
|
|
361
|
+
const u = modelUsage[model] || {};
|
|
362
|
+
const b = base[model] || {};
|
|
363
|
+
snapshot[model] = {
|
|
364
|
+
inputTokens: u.inputTokens || 0,
|
|
365
|
+
outputTokens: u.outputTokens || 0,
|
|
366
|
+
cacheReadInputTokens: u.cacheReadInputTokens || 0,
|
|
367
|
+
cacheCreationInputTokens: u.cacheCreationInputTokens || 0,
|
|
368
|
+
costUSD: u.costUSD || 0,
|
|
369
|
+
};
|
|
370
|
+
const d = {
|
|
371
|
+
inputTokens: Math.max(0, snapshot[model].inputTokens - (b.inputTokens || 0)),
|
|
372
|
+
outputTokens: Math.max(0, snapshot[model].outputTokens - (b.outputTokens || 0)),
|
|
373
|
+
cacheReadTokens: Math.max(0, snapshot[model].cacheReadInputTokens - (b.cacheReadInputTokens || 0)),
|
|
374
|
+
cacheCreateTokens: Math.max(0, snapshot[model].cacheCreationInputTokens - (b.cacheCreationInputTokens || 0)),
|
|
375
|
+
costUsd: Math.max(0, snapshot[model].costUSD - (b.costUSD || 0)),
|
|
376
|
+
};
|
|
377
|
+
if (d.inputTokens || d.outputTokens || d.cacheReadTokens || d.cacheCreateTokens || d.costUsd) {
|
|
378
|
+
deltas[model] = d;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
// 基准里已有但本次 modelUsage 没出现的模型:保留旧值(SDK 不应该丢 key,
|
|
382
|
+
// 防御性合并,免得它真丢时差分把历史消耗又算一遍)
|
|
383
|
+
this._modelUsageBase = { ...base, ...snapshot };
|
|
384
|
+
return deltas;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
incrementTool(failed = false) {
|
|
388
|
+
this.counters.toolCalls += 1;
|
|
389
|
+
if (failed) this.counters.toolFailures += 1;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* 子代理收尾用量(SDK task_notification.usage)累积(2026-07-30)。
|
|
394
|
+
* **单独记账,不加进主 inputTokens/outputTokens** —— 主字段现在来自
|
|
395
|
+
* modelUsage 差分(SDK 的分模型总账,已含 sidechain),相加必然双重计数。
|
|
396
|
+
* 这两个字段只进 metadata 做可观测("这轮子代理烧了多少"),也是交叉验证
|
|
397
|
+
* 钩子:某轮 subagent 用量明显大于 modelUsage 差分总和 = 总账没含 sidechain
|
|
398
|
+
* 的信号(目前判断是含的,出现反例再查)。
|
|
399
|
+
*/
|
|
400
|
+
absorbSubagentUsage(usage) {
|
|
401
|
+
if (!usage || typeof usage !== 'object') return;
|
|
402
|
+
this.counters.subagentInputTokens = (this.counters.subagentInputTokens || 0) + (usage.input_tokens || 0);
|
|
403
|
+
this.counters.subagentOutputTokens = (this.counters.subagentOutputTokens || 0) + (usage.output_tokens || 0);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// ── observability ──
|
|
407
|
+
|
|
408
|
+
snapshot() {
|
|
409
|
+
return {
|
|
410
|
+
runId: this.runId,
|
|
411
|
+
skillId: this.skillId,
|
|
412
|
+
sdkSessionId: this.sdkSessionId,
|
|
413
|
+
counters: { ...this.counters },
|
|
414
|
+
elapsedMs: Date.now() - this.startedAt,
|
|
415
|
+
aborted: this.signal.aborted,
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
}
|