@gamegeek-saikel/dsh-cost-meter 0.3.0 → 0.3.1
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 +1 -1
- package/README.zh-CN.md +1 -1
- package/lib/index.js +70 -20
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +2 -1
- package/lib/types/subagent-cost.d.ts +54 -15
- package/lib/types/subagent-cost.d.ts.map +1 -1
- package/lib/types/subagent-cost.js +92 -26
- package/package.json +1 -1
- package/src/index.ts +11 -1
- package/src/subagent-cost.ts +141 -28
package/README.md
CHANGED
|
@@ -47,7 +47,7 @@ Chat costs in DeepSeek pricing change over time (list prices, USD→CNY exchange
|
|
|
47
47
|
| Peak pricing | 2026-08-17 00:00 Beijing rollout; peak windows apply Monday–Friday only (the zh and en pages may state different timezones; fallback 09:00–12:00 / 14:00–18:00 Beijing), so Saturdays, Sundays, and all other hours are off-peak at half price |
|
|
48
48
|
| Cost formula | Uncached input + cache reads (hit rate) + cache writes (billed at uncached input rate) + output, per 1M tokens, CNY |
|
|
49
49
|
| Account balance | Official `GET /user/balance`, cached 60 s, single in-flight request, trust-fenced route |
|
|
50
|
-
| Subagent support | Enumerates the conversation's durable subagent tree through `subagents.listDescendants` (nested delegations, settled children, and cold subagent sessions included, with no depth cap); falls back to a BFS over the live agent tree when
|
|
50
|
+
| Subagent support | Enumerates the conversation's durable subagent tree through `subagents.listDescendants` (nested delegations, settled children, and cold subagent sessions included, with no depth cap); a ledger is read from the resident session's projection when available and otherwise cold-restored from the child's stored log (`sessionQuery.readSession` + `sessionProjections.restore`), so historical subagent spend is also counted after a host restart; falls back to a BFS over the live agent tree when neither service is mounted |
|
|
51
51
|
| Conversation total | Main session + every descendant subagent (any depth); subagent totals still render while the main session's own ledger is not materialized |
|
|
52
52
|
| UI surfaces | Composer dock · Cost tab · per-reply chip · header pill (live estimate) · settings card |
|
|
53
53
|
| Locale | Simplified Chinese (source) + English |
|
package/README.zh-CN.md
CHANGED
|
@@ -47,7 +47,7 @@ DeepSeek 的价格随时间变化(官方价目表、USD→CNY 汇率、2026-08
|
|
|
47
47
|
| 峰值定价 | 2026-08-17 00:00 北京生效;高峰窗口仅周一至周五(中英文页面时区可能不同,缺省 09:00–12:00 / 14:00–18:00 北京),因此周六、周日全天及其余时段均为半价闲时 |
|
|
48
48
|
| 成本公式 | 未命中输入 + 缓存命中(命中价)+ 缓存写入(按未命中输入价计)+ 输出,每百万 tokens,CNY |
|
|
49
49
|
| 账户余额 | 官方 `GET /user/balance`,缓存 60 秒,单飞请求,路由带信任围栏 |
|
|
50
|
-
| 子代理支持 | 沿 `subagents.listDescendants`
|
|
50
|
+
| 子代理支持 | 沿 `subagents.listDescendants` 枚举本会话持久子代理树(子代理再派生的孙代理、已结束或已冷启动的子会话都计入,无层数上限);账本优先取常驻会话的投影,未常驻的经 `sessionQuery.readSession` + `sessionProjections.restore` 从其持久日志冷读——因此主机重启后,旧会话的历史子代理花费同样会被补算;未挂载该服务时回退到活跃代理树 BFS |
|
|
51
51
|
| 对话总花费 | 主会话 + 全部后代子代理(任意层数);主会话自身账本尚未就绪时仍展示子代理合计 |
|
|
52
52
|
| UI 表面 | 输入框读数 · 花费标签页 · 回复小标签 · 头部胶囊(实时估算)· 设置卡 |
|
|
53
53
|
| 本地化 | 简体中文(键源)+ 英文 |
|
package/lib/index.js
CHANGED
|
@@ -2265,8 +2265,53 @@ const ZERO_TOTALS = {
|
|
|
2265
2265
|
unpricedSteps: 0,
|
|
2266
2266
|
steps: 0
|
|
2267
2267
|
};
|
|
2268
|
-
/**
|
|
2269
|
-
|
|
2268
|
+
/** How long a cold fold is reused before the persisted log is read again. */
|
|
2269
|
+
const COLD_LEDGER_TTL_MS = 3e4;
|
|
2270
|
+
/** How many cold folds stay cached across requests. */
|
|
2271
|
+
const COLD_LEDGER_CACHE_LIMIT = 256;
|
|
2272
|
+
/** Cross-request cold-ledger cache, keyed by session id, insertion-ordered. */
|
|
2273
|
+
const coldLedgerCache = /* @__PURE__ */ new Map();
|
|
2274
|
+
/**
|
|
2275
|
+
* Drop every cached cold fold. The cache is process-global and keyed by
|
|
2276
|
+
* session id; tests (and any caller that knowingly replaced a session log)
|
|
2277
|
+
* use this instead of compensating with synthetic ids.
|
|
2278
|
+
*/
|
|
2279
|
+
function resetColdLedgerCache() {
|
|
2280
|
+
coldLedgerCache.clear();
|
|
2281
|
+
}
|
|
2282
|
+
/**
|
|
2283
|
+
* Fold one stored subagent log into its ledger totals through the framework's
|
|
2284
|
+
* own cold-read recipe: an empty checkpoint at seq 0, every stored event, and
|
|
2285
|
+
* the exact inherited cut. Cached briefly so the route's poll cadence does not
|
|
2286
|
+
* refold unchanged logs.
|
|
2287
|
+
* @param sessionId - the subagent session to read.
|
|
2288
|
+
* @param query - the session query service.
|
|
2289
|
+
* @param projections - the projection registry (live + restore face).
|
|
2290
|
+
* @param projectionKey - the currency-specific ledger key.
|
|
2291
|
+
* @returns the restored totals, or undefined without a cold-read seam.
|
|
2292
|
+
*/
|
|
2293
|
+
async function readColdLedger(sessionId, query, projections, projectionKey) {
|
|
2294
|
+
if (query === void 0 || typeof query.readSession !== "function") return void 0;
|
|
2295
|
+
if (typeof projections.restore !== "function") return void 0;
|
|
2296
|
+
const cached = coldLedgerCache.get(sessionId);
|
|
2297
|
+
if (cached !== void 0 && Date.now() - cached.at < COLD_LEDGER_TTL_MS) return cached.totals;
|
|
2298
|
+
const log = await query.readSession(sessionId);
|
|
2299
|
+
const restored = projections.restore({}, log.events, 0, log.session, log.inheritedEventCount);
|
|
2300
|
+
const totals = restored.snapshot.values[projectionKey]?.totals;
|
|
2301
|
+
if (totals === void 0) return void 0;
|
|
2302
|
+
coldLedgerCache.set(sessionId, {
|
|
2303
|
+
totals,
|
|
2304
|
+
asOfSeq: Number(restored.snapshot.asOfSeq),
|
|
2305
|
+
at: Date.now()
|
|
2306
|
+
});
|
|
2307
|
+
if (coldLedgerCache.size > COLD_LEDGER_CACHE_LIMIT) {
|
|
2308
|
+
const oldest = coldLedgerCache.keys().next();
|
|
2309
|
+
if (!oldest.done) coldLedgerCache.delete(oldest.value);
|
|
2310
|
+
}
|
|
2311
|
+
return totals;
|
|
2312
|
+
}
|
|
2313
|
+
/** Resolve the ledger of one resident session, or undefined when it carries none. */
|
|
2314
|
+
function liveTotalsOf(session, projections, projectionKey) {
|
|
2270
2315
|
if (session === void 0) return void 0;
|
|
2271
2316
|
let snapshot;
|
|
2272
2317
|
try {
|
|
@@ -2276,10 +2321,7 @@ function totalsOf(session, projections, projectionKey) {
|
|
|
2276
2321
|
}
|
|
2277
2322
|
return snapshot.values[projectionKey]?.totals;
|
|
2278
2323
|
}
|
|
2279
|
-
/**
|
|
2280
|
-
* Index the live session store by id. Read once per request so the durable
|
|
2281
|
-
* listing resolves every row without a per-child store lookup.
|
|
2282
|
-
*/
|
|
2324
|
+
/** Index the live session store by id, so the durable walk resolves rows cheaply. */
|
|
2283
2325
|
function liveSessionsById(sessions) {
|
|
2284
2326
|
const index = /* @__PURE__ */ new Map();
|
|
2285
2327
|
for (const session of sessions.list()) {
|
|
@@ -2289,30 +2331,35 @@ function liveSessionsById(sessions) {
|
|
|
2289
2331
|
return index;
|
|
2290
2332
|
}
|
|
2291
2333
|
/**
|
|
2292
|
-
* Fold the durable subagent tree into cost rows
|
|
2293
|
-
*
|
|
2294
|
-
*
|
|
2295
|
-
* child.
|
|
2334
|
+
* Fold the durable subagent tree into cost rows: the live ledger when the
|
|
2335
|
+
* child is resident, else its persisted-log restore. A `diagnostic` row names
|
|
2336
|
+
* no interpreted child and is skipped.
|
|
2296
2337
|
* @param rootSessionId - the root conversation's session id.
|
|
2297
2338
|
* @param subagents - the subagents service (durable tree enumeration).
|
|
2298
2339
|
* @param sessions - the sessions service (live session store).
|
|
2299
2340
|
* @param projections - the sessionProjections service.
|
|
2300
2341
|
* @param projectionKey - the currency-specific ledger key.
|
|
2342
|
+
* @param query - the session query service (cold-ledger source), when mounted.
|
|
2301
2343
|
* @returns one entry per descendant that has a ledger.
|
|
2302
2344
|
* @throws whatever the listing throws; the caller decides the fallback.
|
|
2303
2345
|
*/
|
|
2304
|
-
async function collectDurableSubagentCosts(rootSessionId, subagents, sessions, projections, projectionKey) {
|
|
2346
|
+
async function collectDurableSubagentCosts(rootSessionId, subagents, sessions, projections, projectionKey, query) {
|
|
2305
2347
|
const entries = await subagents.listDescendants(rootSessionId);
|
|
2306
2348
|
if (entries.length === 0) return [];
|
|
2307
2349
|
const live = liveSessionsById(sessions);
|
|
2308
2350
|
const result = [];
|
|
2309
2351
|
const seen = /* @__PURE__ */ new Set();
|
|
2310
2352
|
for (const entry of entries) {
|
|
2311
|
-
if (entry.kind
|
|
2353
|
+
if (entry.kind === "diagnostic") continue;
|
|
2312
2354
|
const sessionId = String(entry.id);
|
|
2313
2355
|
if (sessionId.length === 0 || sessionId === rootSessionId || seen.has(sessionId)) continue;
|
|
2314
2356
|
seen.add(sessionId);
|
|
2315
|
-
|
|
2357
|
+
let totals = liveTotalsOf(live.get(sessionId), projections, projectionKey);
|
|
2358
|
+
if (totals === void 0) try {
|
|
2359
|
+
totals = await readColdLedger(sessionId, query, projections, projectionKey);
|
|
2360
|
+
} catch {
|
|
2361
|
+
totals = void 0;
|
|
2362
|
+
}
|
|
2316
2363
|
if (totals === void 0) continue;
|
|
2317
2364
|
result.push({
|
|
2318
2365
|
sessionId,
|
|
@@ -2350,7 +2397,7 @@ function collectRuntimeSubagentCosts(rootSessionId, agents, sessions, projection
|
|
|
2350
2397
|
if (!agents.isOwnedBy(candidateId, parent)) continue;
|
|
2351
2398
|
seen.add(candidateId);
|
|
2352
2399
|
queue.push(candidate);
|
|
2353
|
-
const totals =
|
|
2400
|
+
const totals = liveTotalsOf(sessions.get(candidateId), projections, projectionKey);
|
|
2354
2401
|
if (totals === void 0) continue;
|
|
2355
2402
|
result.push({
|
|
2356
2403
|
sessionId: candidateId,
|
|
@@ -2364,21 +2411,23 @@ function collectRuntimeSubagentCosts(rootSessionId, agents, sessions, projection
|
|
|
2364
2411
|
/**
|
|
2365
2412
|
* Collect every session-backed descendant of `rootSessionId` with its
|
|
2366
2413
|
* anchored cost totals: the durable subagent tree first (nested, settled, and
|
|
2367
|
-
* cold children included
|
|
2368
|
-
* the
|
|
2414
|
+
* cold children included — a resident child through its registered
|
|
2415
|
+
* projection, a stored one through the cold restore), then the live runtime
|
|
2416
|
+
* ownership walk for whatever the listing could not reach.
|
|
2369
2417
|
* @param rootSessionId - the root conversation's session id.
|
|
2370
2418
|
* @param agents - the agents service (live registry).
|
|
2371
2419
|
* @param sessions - the sessions service (session store).
|
|
2372
2420
|
* @param projections - the sessionProjections service.
|
|
2373
2421
|
* @param projectionKey - the currency-specific ledger key.
|
|
2374
2422
|
* @param subagents - the subagents service; omitted, only the runtime walk runs.
|
|
2423
|
+
* @param query - the session query service; omitted, cold children are skipped.
|
|
2375
2424
|
* @returns one entry per descendant with a priced or unpriced ledger; empty
|
|
2376
2425
|
* when the conversation has no subagents.
|
|
2377
2426
|
*/
|
|
2378
|
-
async function collectSubagentCosts(rootSessionId, agents, sessions, projections, projectionKey = "sessionCost", subagents) {
|
|
2427
|
+
async function collectSubagentCosts(rootSessionId, agents, sessions, projections, projectionKey = "sessionCost", subagents, query) {
|
|
2379
2428
|
let durable = [];
|
|
2380
2429
|
if (subagents !== void 0) try {
|
|
2381
|
-
durable = await collectDurableSubagentCosts(rootSessionId, subagents, sessions, projections, projectionKey);
|
|
2430
|
+
durable = await collectDurableSubagentCosts(rootSessionId, subagents, sessions, projections, projectionKey, query);
|
|
2382
2431
|
} catch {
|
|
2383
2432
|
durable = [];
|
|
2384
2433
|
}
|
|
@@ -2699,9 +2748,10 @@ async function apply(ctx, config) {
|
|
|
2699
2748
|
const agents = ctx.get("agents");
|
|
2700
2749
|
const sessionsStore = ctx.get("sessions");
|
|
2701
2750
|
const subagentTree = ctx.get("subagents");
|
|
2751
|
+
const sessionQuery = ctx.get("sessionQuery");
|
|
2702
2752
|
const pricebook = currency === "USD" ? pricebookUsd : pricebookCny;
|
|
2703
2753
|
const projectionKey = currency === "USD" ? "sessionCostUsd" : "sessionCost";
|
|
2704
|
-
const subagents = rootSessionId === void 0 || sessionsStore === void 0 ? [] : await collectSubagentCosts(rootSessionId, agents, sessionsStore, ctx.sessionProjections, projectionKey, subagentTree);
|
|
2754
|
+
const subagents = rootSessionId === void 0 || sessionsStore === void 0 ? [] : await collectSubagentCosts(rootSessionId, agents, sessionsStore, ctx.sessionProjections, projectionKey, subagentTree, sessionQuery);
|
|
2705
2755
|
return {
|
|
2706
2756
|
balance: await refresh(),
|
|
2707
2757
|
pricebook: pricebook.view(),
|
|
@@ -2745,4 +2795,4 @@ async function apply(ctx, config) {
|
|
|
2745
2795
|
ctx.effect(() => ctx.webServer.register(route), "cost-meter: /cost-meter route");
|
|
2746
2796
|
}
|
|
2747
2797
|
//#endregion
|
|
2748
|
-
export { Config, DEFAULT_ALIASES, DEFAULT_API_KEY_ENV, DEFAULT_CACHE_READ_DISCOUNT, DEFAULT_FX_RATE, DEFAULT_PRICING_REFRESH_HOURS, DEFAULT_REFRESH_MS, DEFAULT_SNAPSHOT_HISTORY_LIMIT, FALLBACK_CURRENT, FALLBACK_CURRENT_USD, FALLBACK_PEAK, FALLBACK_PEAK_USD, FX_API_URL, OPENROUTER_MODELS_URL, PEAK_PRICING_START_MS, PEAK_SCHEDULE_EN, PEAK_SCHEDULE_ZH, PRICEBOOK_DOMAIN, PRICEBOOK_DOMAIN_USD, PRICING_URL, PRICING_URL_EN, PUBLIC_BASE_URL, PricebookHandle, SETTINGS_NAMESPACE, apply, bandForTime, bucketOf, collectSubagentCosts, computePricebook, effectiveBucket, effectivePricing, effectiveRate, fetchBalance, fetchFxRate, fetchOpenRouter, fetchPricing, foldSessionCost, foldSessionCostIndex, initialPricebookState, inject, isLoopbackHostname, isPeakHour, isTrustedRequest, modelKeys, name, officialInputOf, parseCurrentTable, parseCurrentTableEn, parsePeakSchedule, parsePeakTable, parsePeakTableEn, pickBalanceInfo, priceAt, pricesEqual, pricingKeyOfModel, resolveApiKey, sessionCostIndexProjection, sessionCostProjection, snapshotForTime, stepCost, stripHtml, sumTotals, usageBuckets, viewSessionCost };
|
|
2798
|
+
export { Config, DEFAULT_ALIASES, DEFAULT_API_KEY_ENV, DEFAULT_CACHE_READ_DISCOUNT, DEFAULT_FX_RATE, DEFAULT_PRICING_REFRESH_HOURS, DEFAULT_REFRESH_MS, DEFAULT_SNAPSHOT_HISTORY_LIMIT, FALLBACK_CURRENT, FALLBACK_CURRENT_USD, FALLBACK_PEAK, FALLBACK_PEAK_USD, FX_API_URL, OPENROUTER_MODELS_URL, PEAK_PRICING_START_MS, PEAK_SCHEDULE_EN, PEAK_SCHEDULE_ZH, PRICEBOOK_DOMAIN, PRICEBOOK_DOMAIN_USD, PRICING_URL, PRICING_URL_EN, PUBLIC_BASE_URL, PricebookHandle, SETTINGS_NAMESPACE, apply, bandForTime, bucketOf, collectSubagentCosts, computePricebook, effectiveBucket, effectivePricing, effectiveRate, fetchBalance, fetchFxRate, fetchOpenRouter, fetchPricing, foldSessionCost, foldSessionCostIndex, initialPricebookState, inject, isLoopbackHostname, isPeakHour, isTrustedRequest, modelKeys, name, officialInputOf, parseCurrentTable, parseCurrentTableEn, parsePeakSchedule, parsePeakTable, parsePeakTableEn, pickBalanceInfo, priceAt, pricesEqual, pricingKeyOfModel, resetColdLedgerCache, resolveApiKey, sessionCostIndexProjection, sessionCostProjection, snapshotForTime, stepCost, stripHtml, sumTotals, usageBuckets, viewSessionCost };
|
package/lib/types/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,KAAK,EAAE,eAAe,EAAkB,MAAM,WAAW,CAAA;AAChE,OAAO,CAAC,MAAM,0BAA0B,CAAA;AAKxC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"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"}
|
package/lib/types/index.js
CHANGED
|
@@ -361,11 +361,12 @@ export async function apply(ctx, config) {
|
|
|
361
361
|
const agents = ctx.get('agents');
|
|
362
362
|
const sessionsStore = ctx.get('sessions');
|
|
363
363
|
const subagentTree = ctx.get('subagents');
|
|
364
|
+
const sessionQuery = ctx.get('sessionQuery');
|
|
364
365
|
const pricebook = currency === 'USD' ? pricebookUsd : pricebookCny;
|
|
365
366
|
const projectionKey = currency === 'USD' ? 'sessionCostUsd' : 'sessionCost';
|
|
366
367
|
const subagents = rootSessionId === undefined || sessionsStore === undefined
|
|
367
368
|
? []
|
|
368
|
-
: await collectSubagentCosts(rootSessionId, agents, sessionsStore, ctx.sessionProjections, projectionKey, subagentTree);
|
|
369
|
+
: await collectSubagentCosts(rootSessionId, agents, sessionsStore, ctx.sessionProjections, projectionKey, subagentTree, sessionQuery);
|
|
369
370
|
return {
|
|
370
371
|
balance: await refresh(),
|
|
371
372
|
pricebook: pricebook.view(),
|
|
@@ -5,14 +5,19 @@
|
|
|
5
5
|
* conversation spend (root + every nesting level) and a per-subagent
|
|
6
6
|
* breakdown.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
8
|
+
* Two seams, in this order:
|
|
9
|
+
*
|
|
10
|
+
* 1. **Enumeration** — the durable, session-backed subagent tree
|
|
11
|
+
* (`subagents.listDescendants`, the same listing the product's own subagent
|
|
12
|
+
* catalog uses): it walks `parentSession` lineage recorded on every session
|
|
13
|
+
* header, so a subagent that delegates further, a one-shot child, and a
|
|
14
|
+
* child whose Agent was unloaded are all found at any depth.
|
|
15
|
+
* 2. **Ledger read** — a resident child's registered projection, otherwise a
|
|
16
|
+
* COLD restore over its stored log through the session query service
|
|
17
|
+
* (`readSession` + `sessionProjections.restore`: the framework's own
|
|
18
|
+
* checkpoint-plus-tail recipe). A freshly restarted process holds no live
|
|
19
|
+
* session at all, which is exactly the case where counting only resident
|
|
20
|
+
* children reported zero subagents for a conversation full of them.
|
|
16
21
|
*
|
|
17
22
|
* The runtime ownership walk over the live agent registry (`agents.isOwnedBy`
|
|
18
23
|
* + `agents.list`) remains as a fallback for a deployment that mounts no
|
|
@@ -21,7 +26,7 @@
|
|
|
21
26
|
*
|
|
22
27
|
* @module @gamegeek-saikel/dsh-cost-meter/subagent-cost
|
|
23
28
|
*/
|
|
24
|
-
import type { Session } from '@deepseek-ai/dsh-session';
|
|
29
|
+
import type { Session, SessionEvent, SessionHeader } from '@deepseek-ai/dsh-session';
|
|
25
30
|
import type { ProjectionSnapshot } from '@deepseek-ai/dsh-session-projection';
|
|
26
31
|
import type { SessionCostTotals, SubagentCost } from './types.ts';
|
|
27
32
|
/** The subset of the agents service the fallback walk needs. */
|
|
@@ -41,9 +46,36 @@ export interface SubagentSessionsService {
|
|
|
41
46
|
get(id: string): Session | undefined;
|
|
42
47
|
list(): Session[];
|
|
43
48
|
}
|
|
44
|
-
/**
|
|
49
|
+
/**
|
|
50
|
+
* The subset of the sessionProjections service the aggregation needs: the live
|
|
51
|
+
* read face plus the cold `restore` face when the registry exposes it.
|
|
52
|
+
*/
|
|
45
53
|
export interface SubagentProjectionsService {
|
|
46
54
|
snapshot(session: Session): ProjectionSnapshot;
|
|
55
|
+
/**
|
|
56
|
+
* Restore one projection cut from stored events without a live session.
|
|
57
|
+
* @param checkpoint - persisted rows, or `{}` for a full fold.
|
|
58
|
+
* @param events - stored events from `baseSeq`, in seq order.
|
|
59
|
+
* @param baseSeq - the seq `events` starts at.
|
|
60
|
+
* @param header - the stored session header.
|
|
61
|
+
* @param inheritedEventCount - exact fork-inherited prefix length.
|
|
62
|
+
* @returns the restored snapshot.
|
|
63
|
+
*/
|
|
64
|
+
restore?(checkpoint: Record<string, unknown>, events: readonly SessionEvent[], baseSeq: unknown, header: SessionHeader, inheritedEventCount: unknown): {
|
|
65
|
+
snapshot: ProjectionSnapshot;
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The subset of the sessionQuery service the aggregation needs to read one
|
|
70
|
+
* stored subagent log. Only `readSession` is consulted; a deployment whose
|
|
71
|
+
* query engine does not answer it simply has no cold-ledger source.
|
|
72
|
+
*/
|
|
73
|
+
export interface SubagentQueryService {
|
|
74
|
+
readSession(sessionId: string): Promise<{
|
|
75
|
+
session: SessionHeader;
|
|
76
|
+
inheritedEventCount: unknown;
|
|
77
|
+
events: SessionEvent[];
|
|
78
|
+
}>;
|
|
47
79
|
}
|
|
48
80
|
/** One session-backed descendant row of the product's subagent listing. */
|
|
49
81
|
interface SubagentDescendantEntry {
|
|
@@ -56,27 +88,34 @@ interface SubagentDescendantEntry {
|
|
|
56
88
|
/**
|
|
57
89
|
* The subset of the subagents service the durable enumeration needs. The real
|
|
58
90
|
* service answers with branded `SessionId` values and takes one, so the
|
|
59
|
-
* interface stays permissive at both ends
|
|
60
|
-
* hands back into the service.
|
|
91
|
+
* interface stays permissive at both ends.
|
|
61
92
|
*/
|
|
62
93
|
export interface SubagentTreeService {
|
|
63
94
|
listDescendants(rootSessionId: string, signal?: AbortSignal): Promise<readonly SubagentDescendantEntry[]>;
|
|
64
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* Drop every cached cold fold. The cache is process-global and keyed by
|
|
98
|
+
* session id; tests (and any caller that knowingly replaced a session log)
|
|
99
|
+
* use this instead of compensating with synthetic ids.
|
|
100
|
+
*/
|
|
101
|
+
export declare function resetColdLedgerCache(): void;
|
|
65
102
|
/**
|
|
66
103
|
* Collect every session-backed descendant of `rootSessionId` with its
|
|
67
104
|
* anchored cost totals: the durable subagent tree first (nested, settled, and
|
|
68
|
-
* cold children included
|
|
69
|
-
* the
|
|
105
|
+
* cold children included — a resident child through its registered
|
|
106
|
+
* projection, a stored one through the cold restore), then the live runtime
|
|
107
|
+
* ownership walk for whatever the listing could not reach.
|
|
70
108
|
* @param rootSessionId - the root conversation's session id.
|
|
71
109
|
* @param agents - the agents service (live registry).
|
|
72
110
|
* @param sessions - the sessions service (session store).
|
|
73
111
|
* @param projections - the sessionProjections service.
|
|
74
112
|
* @param projectionKey - the currency-specific ledger key.
|
|
75
113
|
* @param subagents - the subagents service; omitted, only the runtime walk runs.
|
|
114
|
+
* @param query - the session query service; omitted, cold children are skipped.
|
|
76
115
|
* @returns one entry per descendant with a priced or unpriced ledger; empty
|
|
77
116
|
* when the conversation has no subagents.
|
|
78
117
|
*/
|
|
79
|
-
export declare function collectSubagentCosts(rootSessionId: string, agents: SubagentAgentsService | undefined, sessions: SubagentSessionsService, projections: SubagentProjectionsService, projectionKey?: 'sessionCost' | 'sessionCostUsd', subagents?: SubagentTreeService): Promise<SubagentCost[]>;
|
|
118
|
+
export declare function collectSubagentCosts(rootSessionId: string, agents: SubagentAgentsService | undefined, sessions: SubagentSessionsService, projections: SubagentProjectionsService, projectionKey?: 'sessionCost' | 'sessionCostUsd', subagents?: SubagentTreeService, query?: SubagentQueryService): Promise<SubagentCost[]>;
|
|
80
119
|
/** Sum several totals into one (empty list → the zero totals). */
|
|
81
120
|
export declare function sumTotals(totals: readonly SessionCostTotals[]): SessionCostTotals;
|
|
82
121
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"subagent-cost.d.ts","sourceRoot":"","sources":["../../src/subagent-cost.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"subagent-cost.d.ts","sourceRoot":"","sources":["../../src/subagent-cost.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAA;AACpF,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,qCAAqC,CAAA;AAC7E,OAAO,KAAK,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAEjE,gEAAgE;AAChE,MAAM,WAAW,qBAAqB;IACpC,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,GAAG,SAAS,CAAA;IACjD,SAAS,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAA;IAC3D,IAAI,IAAI;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,EAAE,CAAA;CAC/B;AAED,gEAAgE;AAChE,MAAM,WAAW,uBAAuB;IACtC,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,CAAA;IACpC,IAAI,IAAI,OAAO,EAAE,CAAA;CAClB;AAED;;;GAGG;AACH,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,OAAO,EAAE,OAAO,GAAG,kBAAkB,CAAA;IAC9C;;;;;;;;OAQG;IACH,OAAO,CAAC,CACN,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACnC,MAAM,EAAE,SAAS,YAAY,EAAE,EAC/B,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,aAAa,EACrB,mBAAmB,EAAE,OAAO,GAC3B;QAAE,QAAQ,EAAE,kBAAkB,CAAA;KAAE,CAAA;CACpC;AAED;;;;GAIG;AACH,MAAM,WAAW,oBAAoB;IACnC,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,aAAa,CAAC;QAAC,mBAAmB,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,YAAY,EAAE,CAAA;KAAE,CAAC,CAAA;CAC1H;AAED,2EAA2E;AAC3E,UAAU,uBAAuB;IAC/B,QAAQ,CAAC,IAAI,EAAE,OAAO,GAAG,YAAY,CAAA;IACrC,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAA;IACpB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;IAC3B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CACxB;AAED;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IAClC,eAAe,CAAC,aAAa,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,SAAS,uBAAuB,EAAE,CAAC,CAAA;CAC1G;AA8BD;;;;GAIG;AACH,wBAAgB,oBAAoB,IAAI,IAAI,CAE3C;AAyKD;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,oBAAoB,CACxC,aAAa,EAAE,MAAM,EACrB,MAAM,EAAE,qBAAqB,GAAG,SAAS,EACzC,QAAQ,EAAE,uBAAuB,EACjC,WAAW,EAAE,0BAA0B,EACvC,aAAa,GAAE,aAAa,GAAG,gBAAgC,EAC/D,SAAS,CAAC,EAAE,mBAAmB,EAC/B,KAAK,CAAC,EAAE,oBAAoB,GAC3B,OAAO,CAAC,YAAY,EAAE,CAAC,CAczB;AAED,kEAAkE;AAClE,wBAAgB,SAAS,CAAC,MAAM,EAAE,SAAS,iBAAiB,EAAE,GAAG,iBAAiB,CAYjF"}
|
|
@@ -5,14 +5,19 @@
|
|
|
5
5
|
* conversation spend (root + every nesting level) and a per-subagent
|
|
6
6
|
* breakdown.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
8
|
+
* Two seams, in this order:
|
|
9
|
+
*
|
|
10
|
+
* 1. **Enumeration** — the durable, session-backed subagent tree
|
|
11
|
+
* (`subagents.listDescendants`, the same listing the product's own subagent
|
|
12
|
+
* catalog uses): it walks `parentSession` lineage recorded on every session
|
|
13
|
+
* header, so a subagent that delegates further, a one-shot child, and a
|
|
14
|
+
* child whose Agent was unloaded are all found at any depth.
|
|
15
|
+
* 2. **Ledger read** — a resident child's registered projection, otherwise a
|
|
16
|
+
* COLD restore over its stored log through the session query service
|
|
17
|
+
* (`readSession` + `sessionProjections.restore`: the framework's own
|
|
18
|
+
* checkpoint-plus-tail recipe). A freshly restarted process holds no live
|
|
19
|
+
* session at all, which is exactly the case where counting only resident
|
|
20
|
+
* children reported zero subagents for a conversation full of them.
|
|
16
21
|
*
|
|
17
22
|
* The runtime ownership walk over the live agent registry (`agents.isOwnedBy`
|
|
18
23
|
* + `agents.list`) remains as a fallback for a deployment that mounts no
|
|
@@ -30,8 +35,58 @@ const ZERO_TOTALS = {
|
|
|
30
35
|
unpricedSteps: 0,
|
|
31
36
|
steps: 0,
|
|
32
37
|
};
|
|
33
|
-
/**
|
|
34
|
-
|
|
38
|
+
/** How long a cold fold is reused before the persisted log is read again. */
|
|
39
|
+
const COLD_LEDGER_TTL_MS = 30_000;
|
|
40
|
+
/** How many cold folds stay cached across requests. */
|
|
41
|
+
const COLD_LEDGER_CACHE_LIMIT = 256;
|
|
42
|
+
/** Cross-request cold-ledger cache, keyed by session id, insertion-ordered. */
|
|
43
|
+
const coldLedgerCache = new Map();
|
|
44
|
+
/**
|
|
45
|
+
* Drop every cached cold fold. The cache is process-global and keyed by
|
|
46
|
+
* session id; tests (and any caller that knowingly replaced a session log)
|
|
47
|
+
* use this instead of compensating with synthetic ids.
|
|
48
|
+
*/
|
|
49
|
+
export function resetColdLedgerCache() {
|
|
50
|
+
coldLedgerCache.clear();
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Fold one stored subagent log into its ledger totals through the framework's
|
|
54
|
+
* own cold-read recipe: an empty checkpoint at seq 0, every stored event, and
|
|
55
|
+
* the exact inherited cut. Cached briefly so the route's poll cadence does not
|
|
56
|
+
* refold unchanged logs.
|
|
57
|
+
* @param sessionId - the subagent session to read.
|
|
58
|
+
* @param query - the session query service.
|
|
59
|
+
* @param projections - the projection registry (live + restore face).
|
|
60
|
+
* @param projectionKey - the currency-specific ledger key.
|
|
61
|
+
* @returns the restored totals, or undefined without a cold-read seam.
|
|
62
|
+
*/
|
|
63
|
+
async function readColdLedger(sessionId, query, projections, projectionKey) {
|
|
64
|
+
if (query === undefined || typeof query.readSession !== 'function')
|
|
65
|
+
return undefined;
|
|
66
|
+
if (typeof projections.restore !== 'function')
|
|
67
|
+
return undefined;
|
|
68
|
+
const cached = coldLedgerCache.get(sessionId);
|
|
69
|
+
if (cached !== undefined && Date.now() - cached.at < COLD_LEDGER_TTL_MS)
|
|
70
|
+
return cached.totals;
|
|
71
|
+
const log = await query.readSession(sessionId);
|
|
72
|
+
// The stored log starts at its own seq 0, so an empty checkpoint and
|
|
73
|
+
// `baseSeq` 0 fold every registered unit from `init` across the whole log —
|
|
74
|
+
// the exact same `apply` path a live session runs.
|
|
75
|
+
const restored = projections.restore({}, log.events, 0, log.session, log.inheritedEventCount);
|
|
76
|
+
const value = restored.snapshot.values[projectionKey];
|
|
77
|
+
const totals = value?.totals;
|
|
78
|
+
if (totals === undefined)
|
|
79
|
+
return undefined;
|
|
80
|
+
coldLedgerCache.set(sessionId, { totals, asOfSeq: Number(restored.snapshot.asOfSeq), at: Date.now() });
|
|
81
|
+
if (coldLedgerCache.size > COLD_LEDGER_CACHE_LIMIT) {
|
|
82
|
+
const oldest = coldLedgerCache.keys().next();
|
|
83
|
+
if (!oldest.done)
|
|
84
|
+
coldLedgerCache.delete(oldest.value);
|
|
85
|
+
}
|
|
86
|
+
return totals;
|
|
87
|
+
}
|
|
88
|
+
/** Resolve the ledger of one resident session, or undefined when it carries none. */
|
|
89
|
+
function liveTotalsOf(session, projections, projectionKey) {
|
|
35
90
|
if (session === undefined)
|
|
36
91
|
return undefined;
|
|
37
92
|
let snapshot;
|
|
@@ -44,10 +99,7 @@ function totalsOf(session, projections, projectionKey) {
|
|
|
44
99
|
}
|
|
45
100
|
return snapshot.values[projectionKey]?.totals;
|
|
46
101
|
}
|
|
47
|
-
/**
|
|
48
|
-
* Index the live session store by id. Read once per request so the durable
|
|
49
|
-
* listing resolves every row without a per-child store lookup.
|
|
50
|
-
*/
|
|
102
|
+
/** Index the live session store by id, so the durable walk resolves rows cheaply. */
|
|
51
103
|
function liveSessionsById(sessions) {
|
|
52
104
|
const index = new Map();
|
|
53
105
|
for (const session of sessions.list()) {
|
|
@@ -58,19 +110,19 @@ function liveSessionsById(sessions) {
|
|
|
58
110
|
return index;
|
|
59
111
|
}
|
|
60
112
|
/**
|
|
61
|
-
* Fold the durable subagent tree into cost rows
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
* child.
|
|
113
|
+
* Fold the durable subagent tree into cost rows: the live ledger when the
|
|
114
|
+
* child is resident, else its persisted-log restore. A `diagnostic` row names
|
|
115
|
+
* no interpreted child and is skipped.
|
|
65
116
|
* @param rootSessionId - the root conversation's session id.
|
|
66
117
|
* @param subagents - the subagents service (durable tree enumeration).
|
|
67
118
|
* @param sessions - the sessions service (live session store).
|
|
68
119
|
* @param projections - the sessionProjections service.
|
|
69
120
|
* @param projectionKey - the currency-specific ledger key.
|
|
121
|
+
* @param query - the session query service (cold-ledger source), when mounted.
|
|
70
122
|
* @returns one entry per descendant that has a ledger.
|
|
71
123
|
* @throws whatever the listing throws; the caller decides the fallback.
|
|
72
124
|
*/
|
|
73
|
-
async function collectDurableSubagentCosts(rootSessionId, subagents, sessions, projections, projectionKey) {
|
|
125
|
+
async function collectDurableSubagentCosts(rootSessionId, subagents, sessions, projections, projectionKey, query) {
|
|
74
126
|
const entries = await subagents.listDescendants(rootSessionId);
|
|
75
127
|
if (entries.length === 0)
|
|
76
128
|
return [];
|
|
@@ -78,13 +130,25 @@ async function collectDurableSubagentCosts(rootSessionId, subagents, sessions, p
|
|
|
78
130
|
const result = [];
|
|
79
131
|
const seen = new Set();
|
|
80
132
|
for (const entry of entries) {
|
|
81
|
-
|
|
133
|
+
// A `diagnostic` row names a candidate the product's listing could not
|
|
134
|
+
// interpret; only interpreted children carry costs.
|
|
135
|
+
if (entry.kind === 'diagnostic')
|
|
82
136
|
continue;
|
|
83
137
|
const sessionId = String(entry.id);
|
|
84
138
|
if (sessionId.length === 0 || sessionId === rootSessionId || seen.has(sessionId))
|
|
85
139
|
continue;
|
|
86
140
|
seen.add(sessionId);
|
|
87
|
-
|
|
141
|
+
let totals = liveTotalsOf(live.get(sessionId), projections, projectionKey);
|
|
142
|
+
if (totals === undefined) {
|
|
143
|
+
try {
|
|
144
|
+
totals = await readColdLedger(sessionId, query, projections, projectionKey);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
// One unreadable log (damaged, pruned, or a cancelled read) must not
|
|
148
|
+
// hide the other children's costs.
|
|
149
|
+
totals = undefined;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
88
152
|
if (totals === undefined)
|
|
89
153
|
continue;
|
|
90
154
|
result.push({
|
|
@@ -126,7 +190,7 @@ function collectRuntimeSubagentCosts(rootSessionId, agents, sessions, projection
|
|
|
126
190
|
continue;
|
|
127
191
|
seen.add(candidateId);
|
|
128
192
|
queue.push(candidate);
|
|
129
|
-
const totals =
|
|
193
|
+
const totals = liveTotalsOf(sessions.get(candidateId), projections, projectionKey);
|
|
130
194
|
if (totals === undefined)
|
|
131
195
|
continue;
|
|
132
196
|
result.push({ sessionId: candidateId, depth: -1, totals });
|
|
@@ -137,22 +201,24 @@ function collectRuntimeSubagentCosts(rootSessionId, agents, sessions, projection
|
|
|
137
201
|
/**
|
|
138
202
|
* Collect every session-backed descendant of `rootSessionId` with its
|
|
139
203
|
* anchored cost totals: the durable subagent tree first (nested, settled, and
|
|
140
|
-
* cold children included
|
|
141
|
-
* the
|
|
204
|
+
* cold children included — a resident child through its registered
|
|
205
|
+
* projection, a stored one through the cold restore), then the live runtime
|
|
206
|
+
* ownership walk for whatever the listing could not reach.
|
|
142
207
|
* @param rootSessionId - the root conversation's session id.
|
|
143
208
|
* @param agents - the agents service (live registry).
|
|
144
209
|
* @param sessions - the sessions service (session store).
|
|
145
210
|
* @param projections - the sessionProjections service.
|
|
146
211
|
* @param projectionKey - the currency-specific ledger key.
|
|
147
212
|
* @param subagents - the subagents service; omitted, only the runtime walk runs.
|
|
213
|
+
* @param query - the session query service; omitted, cold children are skipped.
|
|
148
214
|
* @returns one entry per descendant with a priced or unpriced ledger; empty
|
|
149
215
|
* when the conversation has no subagents.
|
|
150
216
|
*/
|
|
151
|
-
export async function collectSubagentCosts(rootSessionId, agents, sessions, projections, projectionKey = 'sessionCost', subagents) {
|
|
217
|
+
export async function collectSubagentCosts(rootSessionId, agents, sessions, projections, projectionKey = 'sessionCost', subagents, query) {
|
|
152
218
|
let durable = [];
|
|
153
219
|
if (subagents !== undefined) {
|
|
154
220
|
try {
|
|
155
|
-
durable = await collectDurableSubagentCosts(rootSessionId, subagents, sessions, projections, projectionKey);
|
|
221
|
+
durable = await collectDurableSubagentCosts(rootSessionId, subagents, sessions, projections, projectionKey, query);
|
|
156
222
|
}
|
|
157
223
|
catch {
|
|
158
224
|
// A listing failure (no session query, a cancelled read, corrupt log)
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gamegeek-saikel/dsh-cost-meter",
|
|
3
3
|
"description": "Cost tracking plugin for the DeepSeek Harness Web GUI — snapshot-anchored per-turn pricing, account balance, and live cost estimates.",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@11.7.0",
|
|
7
7
|
"engines": {
|
package/src/index.ts
CHANGED
|
@@ -40,6 +40,7 @@ import { sessionCostIndexProjection } from './session-cost-index.ts'
|
|
|
40
40
|
import {
|
|
41
41
|
collectSubagentCosts,
|
|
42
42
|
type SubagentAgentsService,
|
|
43
|
+
type SubagentQueryService,
|
|
43
44
|
type SubagentSessionsService,
|
|
44
45
|
type SubagentTreeService,
|
|
45
46
|
} from './subagent-cost.ts'
|
|
@@ -460,11 +461,20 @@ export async function apply(ctx: Context, config?: Config): Promise<void> {
|
|
|
460
461
|
const agents = ctx.get('agents') as SubagentAgentsService | undefined
|
|
461
462
|
const sessionsStore = ctx.get('sessions') as SubagentSessionsService | undefined
|
|
462
463
|
const subagentTree = ctx.get('subagents') as SubagentTreeService | undefined
|
|
464
|
+
const sessionQuery = ctx.get('sessionQuery') as SubagentQueryService | undefined
|
|
463
465
|
const pricebook = currency === 'USD' ? pricebookUsd : pricebookCny
|
|
464
466
|
const projectionKey = currency === 'USD' ? 'sessionCostUsd' : 'sessionCost'
|
|
465
467
|
const subagents = rootSessionId === undefined || sessionsStore === undefined
|
|
466
468
|
? []
|
|
467
|
-
: await collectSubagentCosts(
|
|
469
|
+
: await collectSubagentCosts(
|
|
470
|
+
rootSessionId,
|
|
471
|
+
agents,
|
|
472
|
+
sessionsStore,
|
|
473
|
+
ctx.sessionProjections,
|
|
474
|
+
projectionKey,
|
|
475
|
+
subagentTree,
|
|
476
|
+
sessionQuery,
|
|
477
|
+
)
|
|
468
478
|
return {
|
|
469
479
|
balance: await refresh(),
|
|
470
480
|
pricebook: pricebook.view(),
|
package/src/subagent-cost.ts
CHANGED
|
@@ -5,14 +5,19 @@
|
|
|
5
5
|
* conversation spend (root + every nesting level) and a per-subagent
|
|
6
6
|
* breakdown.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
8
|
+
* Two seams, in this order:
|
|
9
|
+
*
|
|
10
|
+
* 1. **Enumeration** — the durable, session-backed subagent tree
|
|
11
|
+
* (`subagents.listDescendants`, the same listing the product's own subagent
|
|
12
|
+
* catalog uses): it walks `parentSession` lineage recorded on every session
|
|
13
|
+
* header, so a subagent that delegates further, a one-shot child, and a
|
|
14
|
+
* child whose Agent was unloaded are all found at any depth.
|
|
15
|
+
* 2. **Ledger read** — a resident child's registered projection, otherwise a
|
|
16
|
+
* COLD restore over its stored log through the session query service
|
|
17
|
+
* (`readSession` + `sessionProjections.restore`: the framework's own
|
|
18
|
+
* checkpoint-plus-tail recipe). A freshly restarted process holds no live
|
|
19
|
+
* session at all, which is exactly the case where counting only resident
|
|
20
|
+
* children reported zero subagents for a conversation full of them.
|
|
16
21
|
*
|
|
17
22
|
* The runtime ownership walk over the live agent registry (`agents.isOwnedBy`
|
|
18
23
|
* + `agents.list`) remains as a fallback for a deployment that mounts no
|
|
@@ -22,7 +27,7 @@
|
|
|
22
27
|
* @module @gamegeek-saikel/dsh-cost-meter/subagent-cost
|
|
23
28
|
*/
|
|
24
29
|
|
|
25
|
-
import type { Session } from '@deepseek-ai/dsh-session'
|
|
30
|
+
import type { Session, SessionEvent, SessionHeader } from '@deepseek-ai/dsh-session'
|
|
26
31
|
import type { ProjectionSnapshot } from '@deepseek-ai/dsh-session-projection'
|
|
27
32
|
import type { SessionCostTotals, SubagentCost } from './types.ts'
|
|
28
33
|
|
|
@@ -39,9 +44,37 @@ export interface SubagentSessionsService {
|
|
|
39
44
|
list(): Session[]
|
|
40
45
|
}
|
|
41
46
|
|
|
42
|
-
/**
|
|
47
|
+
/**
|
|
48
|
+
* The subset of the sessionProjections service the aggregation needs: the live
|
|
49
|
+
* read face plus the cold `restore` face when the registry exposes it.
|
|
50
|
+
*/
|
|
43
51
|
export interface SubagentProjectionsService {
|
|
44
52
|
snapshot(session: Session): ProjectionSnapshot
|
|
53
|
+
/**
|
|
54
|
+
* Restore one projection cut from stored events without a live session.
|
|
55
|
+
* @param checkpoint - persisted rows, or `{}` for a full fold.
|
|
56
|
+
* @param events - stored events from `baseSeq`, in seq order.
|
|
57
|
+
* @param baseSeq - the seq `events` starts at.
|
|
58
|
+
* @param header - the stored session header.
|
|
59
|
+
* @param inheritedEventCount - exact fork-inherited prefix length.
|
|
60
|
+
* @returns the restored snapshot.
|
|
61
|
+
*/
|
|
62
|
+
restore?(
|
|
63
|
+
checkpoint: Record<string, unknown>,
|
|
64
|
+
events: readonly SessionEvent[],
|
|
65
|
+
baseSeq: unknown,
|
|
66
|
+
header: SessionHeader,
|
|
67
|
+
inheritedEventCount: unknown,
|
|
68
|
+
): { snapshot: ProjectionSnapshot }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The subset of the sessionQuery service the aggregation needs to read one
|
|
73
|
+
* stored subagent log. Only `readSession` is consulted; a deployment whose
|
|
74
|
+
* query engine does not answer it simply has no cold-ledger source.
|
|
75
|
+
*/
|
|
76
|
+
export interface SubagentQueryService {
|
|
77
|
+
readSession(sessionId: string): Promise<{ session: SessionHeader; inheritedEventCount: unknown; events: SessionEvent[] }>
|
|
45
78
|
}
|
|
46
79
|
|
|
47
80
|
/** One session-backed descendant row of the product's subagent listing. */
|
|
@@ -56,8 +89,7 @@ interface SubagentDescendantEntry {
|
|
|
56
89
|
/**
|
|
57
90
|
* The subset of the subagents service the durable enumeration needs. The real
|
|
58
91
|
* service answers with branded `SessionId` values and takes one, so the
|
|
59
|
-
* interface stays permissive at both ends
|
|
60
|
-
* hands back into the service.
|
|
92
|
+
* interface stays permissive at both ends.
|
|
61
93
|
*/
|
|
62
94
|
export interface SubagentTreeService {
|
|
63
95
|
listDescendants(rootSessionId: string, signal?: AbortSignal): Promise<readonly SubagentDescendantEntry[]>
|
|
@@ -73,8 +105,75 @@ const ZERO_TOTALS: SessionCostTotals = {
|
|
|
73
105
|
steps: 0,
|
|
74
106
|
}
|
|
75
107
|
|
|
76
|
-
/**
|
|
77
|
-
|
|
108
|
+
/** One cold session's folded ledger, with the log watermark it describes. */
|
|
109
|
+
interface ColdLedger {
|
|
110
|
+
/** Totals of the restored cut. */
|
|
111
|
+
readonly totals: SessionCostTotals
|
|
112
|
+
/** The restored cut's watermark (never read today; kept for diagnostics). */
|
|
113
|
+
readonly asOfSeq: number
|
|
114
|
+
/** Epoch millis when this fold was computed. */
|
|
115
|
+
readonly at: number
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** How long a cold fold is reused before the persisted log is read again. */
|
|
119
|
+
const COLD_LEDGER_TTL_MS = 30_000
|
|
120
|
+
/** How many cold folds stay cached across requests. */
|
|
121
|
+
const COLD_LEDGER_CACHE_LIMIT = 256
|
|
122
|
+
|
|
123
|
+
/** Cross-request cold-ledger cache, keyed by session id, insertion-ordered. */
|
|
124
|
+
const coldLedgerCache = new Map<string, ColdLedger>()
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Drop every cached cold fold. The cache is process-global and keyed by
|
|
128
|
+
* session id; tests (and any caller that knowingly replaced a session log)
|
|
129
|
+
* use this instead of compensating with synthetic ids.
|
|
130
|
+
*/
|
|
131
|
+
export function resetColdLedgerCache(): void {
|
|
132
|
+
coldLedgerCache.clear()
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Fold one stored subagent log into its ledger totals through the framework's
|
|
137
|
+
* own cold-read recipe: an empty checkpoint at seq 0, every stored event, and
|
|
138
|
+
* the exact inherited cut. Cached briefly so the route's poll cadence does not
|
|
139
|
+
* refold unchanged logs.
|
|
140
|
+
* @param sessionId - the subagent session to read.
|
|
141
|
+
* @param query - the session query service.
|
|
142
|
+
* @param projections - the projection registry (live + restore face).
|
|
143
|
+
* @param projectionKey - the currency-specific ledger key.
|
|
144
|
+
* @returns the restored totals, or undefined without a cold-read seam.
|
|
145
|
+
*/
|
|
146
|
+
async function readColdLedger(
|
|
147
|
+
sessionId: string,
|
|
148
|
+
query: SubagentQueryService | undefined,
|
|
149
|
+
projections: SubagentProjectionsService,
|
|
150
|
+
projectionKey: 'sessionCost' | 'sessionCostUsd',
|
|
151
|
+
): Promise<SessionCostTotals | undefined> {
|
|
152
|
+
if (query === undefined || typeof query.readSession !== 'function') return undefined
|
|
153
|
+
if (typeof projections.restore !== 'function') return undefined
|
|
154
|
+
|
|
155
|
+
const cached = coldLedgerCache.get(sessionId)
|
|
156
|
+
if (cached !== undefined && Date.now() - cached.at < COLD_LEDGER_TTL_MS) return cached.totals
|
|
157
|
+
|
|
158
|
+
const log = await query.readSession(sessionId)
|
|
159
|
+
// The stored log starts at its own seq 0, so an empty checkpoint and
|
|
160
|
+
// `baseSeq` 0 fold every registered unit from `init` across the whole log —
|
|
161
|
+
// the exact same `apply` path a live session runs.
|
|
162
|
+
const restored = projections.restore({}, log.events, 0, log.session, log.inheritedEventCount)
|
|
163
|
+
const value = restored.snapshot.values[projectionKey] as { totals?: SessionCostTotals } | undefined
|
|
164
|
+
const totals = value?.totals
|
|
165
|
+
if (totals === undefined) return undefined
|
|
166
|
+
|
|
167
|
+
coldLedgerCache.set(sessionId, { totals, asOfSeq: Number(restored.snapshot.asOfSeq), at: Date.now() })
|
|
168
|
+
if (coldLedgerCache.size > COLD_LEDGER_CACHE_LIMIT) {
|
|
169
|
+
const oldest = coldLedgerCache.keys().next()
|
|
170
|
+
if (!oldest.done) coldLedgerCache.delete(oldest.value)
|
|
171
|
+
}
|
|
172
|
+
return totals
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Resolve the ledger of one resident session, or undefined when it carries none. */
|
|
176
|
+
function liveTotalsOf(
|
|
78
177
|
session: Session | undefined,
|
|
79
178
|
projections: SubagentProjectionsService,
|
|
80
179
|
projectionKey: 'sessionCost' | 'sessionCostUsd',
|
|
@@ -90,10 +189,7 @@ function totalsOf(
|
|
|
90
189
|
return snapshot.values[projectionKey]?.totals
|
|
91
190
|
}
|
|
92
191
|
|
|
93
|
-
/**
|
|
94
|
-
* Index the live session store by id. Read once per request so the durable
|
|
95
|
-
* listing resolves every row without a per-child store lookup.
|
|
96
|
-
*/
|
|
192
|
+
/** Index the live session store by id, so the durable walk resolves rows cheaply. */
|
|
97
193
|
function liveSessionsById(sessions: SubagentSessionsService): Map<string, Session> {
|
|
98
194
|
const index = new Map<string, Session>()
|
|
99
195
|
for (const session of sessions.list()) {
|
|
@@ -104,15 +200,15 @@ function liveSessionsById(sessions: SubagentSessionsService): Map<string, Sessio
|
|
|
104
200
|
}
|
|
105
201
|
|
|
106
202
|
/**
|
|
107
|
-
* Fold the durable subagent tree into cost rows
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
* child.
|
|
203
|
+
* Fold the durable subagent tree into cost rows: the live ledger when the
|
|
204
|
+
* child is resident, else its persisted-log restore. A `diagnostic` row names
|
|
205
|
+
* no interpreted child and is skipped.
|
|
111
206
|
* @param rootSessionId - the root conversation's session id.
|
|
112
207
|
* @param subagents - the subagents service (durable tree enumeration).
|
|
113
208
|
* @param sessions - the sessions service (live session store).
|
|
114
209
|
* @param projections - the sessionProjections service.
|
|
115
210
|
* @param projectionKey - the currency-specific ledger key.
|
|
211
|
+
* @param query - the session query service (cold-ledger source), when mounted.
|
|
116
212
|
* @returns one entry per descendant that has a ledger.
|
|
117
213
|
* @throws whatever the listing throws; the caller decides the fallback.
|
|
118
214
|
*/
|
|
@@ -122,6 +218,7 @@ async function collectDurableSubagentCosts(
|
|
|
122
218
|
sessions: SubagentSessionsService,
|
|
123
219
|
projections: SubagentProjectionsService,
|
|
124
220
|
projectionKey: 'sessionCost' | 'sessionCostUsd',
|
|
221
|
+
query: SubagentQueryService | undefined,
|
|
125
222
|
): Promise<SubagentCost[]> {
|
|
126
223
|
const entries = await subagents.listDescendants(rootSessionId)
|
|
127
224
|
if (entries.length === 0) return []
|
|
@@ -130,12 +227,25 @@ async function collectDurableSubagentCosts(
|
|
|
130
227
|
const result: SubagentCost[] = []
|
|
131
228
|
const seen = new Set<string>()
|
|
132
229
|
for (const entry of entries) {
|
|
133
|
-
|
|
230
|
+
// A `diagnostic` row names a candidate the product's listing could not
|
|
231
|
+
// interpret; only interpreted children carry costs.
|
|
232
|
+
if (entry.kind === 'diagnostic') continue
|
|
134
233
|
const sessionId = String(entry.id)
|
|
135
234
|
if (sessionId.length === 0 || sessionId === rootSessionId || seen.has(sessionId)) continue
|
|
136
235
|
seen.add(sessionId)
|
|
137
|
-
|
|
236
|
+
|
|
237
|
+
let totals = liveTotalsOf(live.get(sessionId), projections, projectionKey)
|
|
238
|
+
if (totals === undefined) {
|
|
239
|
+
try {
|
|
240
|
+
totals = await readColdLedger(sessionId, query, projections, projectionKey)
|
|
241
|
+
} catch {
|
|
242
|
+
// One unreadable log (damaged, pruned, or a cancelled read) must not
|
|
243
|
+
// hide the other children's costs.
|
|
244
|
+
totals = undefined
|
|
245
|
+
}
|
|
246
|
+
}
|
|
138
247
|
if (totals === undefined) continue
|
|
248
|
+
|
|
139
249
|
result.push({
|
|
140
250
|
sessionId,
|
|
141
251
|
...(entry.parentId === undefined ? {} : { parentId: String(entry.parentId) }),
|
|
@@ -181,7 +291,7 @@ function collectRuntimeSubagentCosts(
|
|
|
181
291
|
if (!agents.isOwnedBy(candidateId, parent)) continue
|
|
182
292
|
seen.add(candidateId)
|
|
183
293
|
queue.push(candidate)
|
|
184
|
-
const totals =
|
|
294
|
+
const totals = liveTotalsOf(sessions.get(candidateId), projections, projectionKey)
|
|
185
295
|
if (totals === undefined) continue
|
|
186
296
|
result.push({ sessionId: candidateId, depth: -1, totals })
|
|
187
297
|
}
|
|
@@ -192,14 +302,16 @@ function collectRuntimeSubagentCosts(
|
|
|
192
302
|
/**
|
|
193
303
|
* Collect every session-backed descendant of `rootSessionId` with its
|
|
194
304
|
* anchored cost totals: the durable subagent tree first (nested, settled, and
|
|
195
|
-
* cold children included
|
|
196
|
-
* the
|
|
305
|
+
* cold children included — a resident child through its registered
|
|
306
|
+
* projection, a stored one through the cold restore), then the live runtime
|
|
307
|
+
* ownership walk for whatever the listing could not reach.
|
|
197
308
|
* @param rootSessionId - the root conversation's session id.
|
|
198
309
|
* @param agents - the agents service (live registry).
|
|
199
310
|
* @param sessions - the sessions service (session store).
|
|
200
311
|
* @param projections - the sessionProjections service.
|
|
201
312
|
* @param projectionKey - the currency-specific ledger key.
|
|
202
313
|
* @param subagents - the subagents service; omitted, only the runtime walk runs.
|
|
314
|
+
* @param query - the session query service; omitted, cold children are skipped.
|
|
203
315
|
* @returns one entry per descendant with a priced or unpriced ledger; empty
|
|
204
316
|
* when the conversation has no subagents.
|
|
205
317
|
*/
|
|
@@ -210,11 +322,12 @@ export async function collectSubagentCosts(
|
|
|
210
322
|
projections: SubagentProjectionsService,
|
|
211
323
|
projectionKey: 'sessionCost' | 'sessionCostUsd' = 'sessionCost',
|
|
212
324
|
subagents?: SubagentTreeService,
|
|
325
|
+
query?: SubagentQueryService,
|
|
213
326
|
): Promise<SubagentCost[]> {
|
|
214
327
|
let durable: SubagentCost[] = []
|
|
215
328
|
if (subagents !== undefined) {
|
|
216
329
|
try {
|
|
217
|
-
durable = await collectDurableSubagentCosts(rootSessionId, subagents, sessions, projections, projectionKey)
|
|
330
|
+
durable = await collectDurableSubagentCosts(rootSessionId, subagents, sessions, projections, projectionKey, query)
|
|
218
331
|
} catch {
|
|
219
332
|
// A listing failure (no session query, a cancelled read, corrupt log)
|
|
220
333
|
// must not hide the subagents the live registry can still prove.
|