@love-moon/ai-sdk 0.15.2 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # @love-moon/ai-sdk
2
2
 
3
+ ## 0.16.0
4
+
5
+ ### Patch Changes
6
+
7
+ - 5c81d06: Claude turns that wait on background subagents now report their full token
8
+ usage. Such a turn emits one result per segment and the task card used to show
9
+ only the last segment; it now sums the result's `modelUsage`, which covers
10
+ every segment, every subagent and small side calls. `conductor serve-ai`'s
11
+ `prompt_tokens` / `completion_tokens` come from the same usage, so they now
12
+ include those too. A turn cut off after an earlier segment keeps that
13
+ segment's usage.
14
+ - b69d665: Codex turns that spawn sub-agents now include the sub-agents' tokens in the
15
+ turn usage. Their `thread/tokenUsage/updated` notifications carry the
16
+ sub-agent's own thread id and were dropped with its other events. A
17
+ sub-agent's replayed total is not counted, and whatever it spends after the
18
+ parent turn ends goes to the next turn.
19
+
3
20
  ## 0.15.2
4
21
 
5
22
  ## 0.15.1
@@ -32,12 +32,15 @@ function createTurnError(message, extras = {}) {
32
32
  }
33
33
  return error;
34
34
  }
35
- /** Per-turn usage summed from streamed assistant messages (one entry per API response). */
36
- function sumStreamedUsage(usageByMessageId) {
35
+ /**
36
+ * Per-turn usage summed from streamed assistant messages (one entry per API
37
+ * response), on top of `base` (what an earlier result of the turn covered).
38
+ */
39
+ function sumStreamedUsage(usageByMessageId, base = null) {
37
40
  if (!usageByMessageId.size) {
38
- return null;
41
+ return base;
39
42
  }
40
- const total = {};
43
+ const total = { ...base };
41
44
  for (const usage of usageByMessageId.values()) {
42
45
  for (const key of ["input_tokens", "cache_creation_input_tokens", "cache_read_input_tokens", "output_tokens"]) {
43
46
  const value = Number(usage?.[key]);
@@ -48,6 +51,31 @@ function sumStreamedUsage(usageByMessageId) {
48
51
  }
49
52
  return total;
50
53
  }
54
+ /**
55
+ * A finished turn's usage. A turn that waits on background tasks emits one result per
56
+ * segment, and `usage` covers only the last one; `modelUsage` accumulates every API call
57
+ * the query's process made (all segments and subagents), so prefer its sum.
58
+ */
59
+ function resultUsage(resultMessage) {
60
+ const total = {};
61
+ for (const entry of Object.values(resultMessage.modelUsage || {})) {
62
+ for (const [from, to] of [
63
+ ["inputTokens", "input_tokens"],
64
+ ["cacheCreationInputTokens", "cache_creation_input_tokens"],
65
+ ["cacheReadInputTokens", "cache_read_input_tokens"],
66
+ ["outputTokens", "output_tokens"],
67
+ ]) {
68
+ const value = Number(entry?.[from]);
69
+ if (Number.isFinite(value)) {
70
+ total[to] = (total[to] || 0) + value;
71
+ }
72
+ }
73
+ }
74
+ if (Object.keys(total).length) {
75
+ return total;
76
+ }
77
+ return resultMessage.usage ? { ...resultMessage.usage } : null;
78
+ }
51
79
  function normalizeClaudeBackend(backend) {
52
80
  const normalized = String(backend || "").trim().toLowerCase();
53
81
  if (normalized === "claude-code") {
@@ -853,6 +881,8 @@ export class ClaudeAgentSdkSession extends EventEmitter {
853
881
  this.updateSessionInfo(message.session_id || message.sessionId);
854
882
  this.lastResult = message;
855
883
  currentTurn.resultMessage = message;
884
+ // Its modelUsage covers everything streamed so far.
885
+ currentTurn.usageByMessageId.clear();
856
886
  return;
857
887
  default:
858
888
  return;
@@ -982,7 +1012,7 @@ export class ClaudeAgentSdkSession extends EventEmitter {
982
1012
  ? [...resultMessage.permission_denials]
983
1013
  : [],
984
1014
  // A failed turn still spent tokens.
985
- usage: resultMessage.usage ? { ...resultMessage.usage } : null,
1015
+ usage: resultUsage(resultMessage),
986
1016
  });
987
1017
  }
988
1018
  const structuredOutput = jsonSchema && resultMessage.structured_output && typeof resultMessage.structured_output === "object"
@@ -1007,7 +1037,7 @@ export class ClaudeAgentSdkSession extends EventEmitter {
1007
1037
  }, onProgress);
1008
1038
  return {
1009
1039
  text: responseText,
1010
- usage: resultMessage.usage ? { ...resultMessage.usage } : null,
1040
+ usage: resultUsage(resultMessage),
1011
1041
  items: currentTurn.items,
1012
1042
  events: [],
1013
1043
  provider: this.backend,
@@ -1027,8 +1057,9 @@ export class ClaudeAgentSdkSession extends EventEmitter {
1027
1057
  }
1028
1058
  catch (error) {
1029
1059
  // A failed turn still spent tokens; an interrupted query ends without a
1030
- // result, so fall back to the usage streamed so far.
1031
- const usage = error?.usage ?? sumStreamedUsage(currentTurn.usageByMessageId);
1060
+ // (final) result, so fall back to the last result plus what streamed since.
1061
+ const usage = error?.usage ??
1062
+ sumStreamedUsage(currentTurn.usageByMessageId, currentTurn.resultMessage ? resultUsage(currentTurn.resultMessage) : null);
1032
1063
  if (error?.reason === "turn_timeout") {
1033
1064
  await this.interruptCurrentTurn();
1034
1065
  }
@@ -46,6 +46,10 @@ export class CodexAppServerSession extends EventEmitter<[never]> {
46
46
  totalTokens: number;
47
47
  } | null;
48
48
  turnTokensSeen: boolean;
49
+ subThreadTokenTotals: Map<any, any>;
50
+ subThreadTokenBaselines: Map<any, any>;
51
+ subThreadTokensReported: Map<any, any>;
52
+ subThreadTurnIds: Map<any, any>;
49
53
  currentTurnStatus: any;
50
54
  currentTurnActivityAt: number;
51
55
  now: any;
@@ -160,6 +164,12 @@ export class CodexAppServerSession extends EventEmitter<[never]> {
160
164
  applySessionConfigured(params: any): void;
161
165
  /** Start counting a turn's tokens from the thread total known right now. */
162
166
  beginTurnTokenAccounting(): void;
167
+ /**
168
+ * Track a spawned sub-agent thread's total. Its first update fixes its
169
+ * baseline: `total - last`, or the whole total when it is a replay (a turn id
170
+ * we never saw start), like the main thread's after thread/resume.
171
+ */
172
+ noteSubThreadTokenUsage(threadId: any, { turnId, tokenUsage }?: {}): void;
163
173
  /**
164
174
  * Thread token usage plus `turnTotalTokens` (and its `turnInputTokens` /
165
175
  * `turnCachedInputTokens` split): tokens spent since the current turn (or
@@ -88,6 +88,15 @@ function subtractTokenCounts(a, b) {
88
88
  : {}),
89
89
  };
90
90
  }
91
+ function addTokenCounts(a, b) {
92
+ const split = a.inputTokens !== undefined && b.inputTokens !== undefined;
93
+ return {
94
+ totalTokens: a.totalTokens + b.totalTokens,
95
+ ...(split
96
+ ? { inputTokens: a.inputTokens + b.inputTokens, cachedInputTokens: a.cachedInputTokens + b.cachedInputTokens }
97
+ : {}),
98
+ };
99
+ }
91
100
  /**
92
101
  * Adds tokens spent outside the reported attempt (an overflowed first try) to a
93
102
  * turn result or error. Only the total grows: the input split, and so the cache
@@ -276,6 +285,13 @@ export class CodexAppServerSession extends EventEmitter {
276
285
  // its own usage has arrived since; see snapshotTokenUsage().
277
286
  this.turnTokenBaseline = null;
278
287
  this.turnTokensSeen = false;
288
+ // Spawned sub-agent threads report their own thread totals: the latest one
289
+ // per thread, the part already counted (or not theirs to count), what the
290
+ // last snapshot reported, and each thread's running turn id.
291
+ this.subThreadTokenTotals = new Map();
292
+ this.subThreadTokenBaselines = new Map();
293
+ this.subThreadTokensReported = new Map();
294
+ this.subThreadTurnIds = new Map();
279
295
  this.currentTurnStatus = null;
280
296
  this.currentTurnActivityAt = 0;
281
297
  this.now = typeof options.now === "function" ? options.now : () => Date.now();
@@ -533,6 +549,28 @@ export class CodexAppServerSession extends EventEmitter {
533
549
  beginTurnTokenAccounting() {
534
550
  this.turnTokenBaseline = readTokenCounts(this.tokenUsage?.total);
535
551
  this.turnTokensSeen = false;
552
+ // What the previous turn reported is counted; sub-agent spend after that
553
+ // (including between turns) goes to this one.
554
+ for (const [threadId, reported] of this.subThreadTokensReported) {
555
+ this.subThreadTokenBaselines.set(threadId, reported);
556
+ }
557
+ this.subThreadTokensReported = new Map();
558
+ }
559
+ /**
560
+ * Track a spawned sub-agent thread's total. Its first update fixes its
561
+ * baseline: `total - last`, or the whole total when it is a replay (a turn id
562
+ * we never saw start), like the main thread's after thread/resume.
563
+ */
564
+ noteSubThreadTokenUsage(threadId, { turnId, tokenUsage } = {}) {
565
+ const total = readTokenCounts(tokenUsage?.total);
566
+ if (!total) {
567
+ return;
568
+ }
569
+ if (!this.subThreadTokenBaselines.has(threadId)) {
570
+ const replay = !turnId || turnId !== this.subThreadTurnIds.get(threadId);
571
+ this.subThreadTokenBaselines.set(threadId, replay ? total : subtractTokenCounts(total, readTokenCounts(tokenUsage.last)));
572
+ }
573
+ this.subThreadTokenTotals.set(threadId, total);
536
574
  }
537
575
  /**
538
576
  * Thread token usage plus `turnTotalTokens` (and its `turnInputTokens` /
@@ -545,9 +583,17 @@ export class CodexAppServerSession extends EventEmitter {
545
583
  if (!this.tokenUsage) {
546
584
  return null;
547
585
  }
548
- const turn = this.turnTokensSeen
586
+ let turn = this.turnTokensSeen
549
587
  ? subtractTokenCounts(readTokenCounts(this.tokenUsage.total), this.turnTokenBaseline)
550
588
  : null;
589
+ for (const [threadId, baseline] of this.subThreadTokenBaselines) {
590
+ const latest = this.subThreadTokenTotals.get(threadId);
591
+ const spent = subtractTokenCounts(latest, baseline);
592
+ if (spent) {
593
+ turn = turn ? addTokenCounts(turn, spent) : spent;
594
+ this.subThreadTokensReported.set(threadId, latest);
595
+ }
596
+ }
551
597
  return {
552
598
  ...this.tokenUsage,
553
599
  ...(turn
@@ -625,6 +671,10 @@ export class CodexAppServerSession extends EventEmitter {
625
671
  this.tokenUsage = null;
626
672
  this.turnTokenBaseline = null;
627
673
  this.turnTokensSeen = false;
674
+ this.subThreadTokenTotals = new Map();
675
+ this.subThreadTokenBaselines = new Map();
676
+ this.subThreadTokensReported = new Map();
677
+ this.subThreadTurnIds = new Map();
628
678
  this.booted = false;
629
679
  this.bootPromise = null;
630
680
  }
@@ -914,6 +964,12 @@ export class CodexAppServerSession extends EventEmitter {
914
964
  const threadId = typeof params?.threadId === "string" ? params.threadId.trim() : "";
915
965
  if (threadId && this.sessionId && threadId !== this.sessionId) {
916
966
  this.touchTurnActivity();
967
+ if (method === "turn/started" && typeof params?.turn?.id === "string") {
968
+ this.subThreadTurnIds.set(threadId, params.turn.id);
969
+ }
970
+ else if (method === "thread/tokenUsage/updated") {
971
+ this.noteSubThreadTokenUsage(threadId, params);
972
+ }
917
973
  return;
918
974
  }
919
975
  switch (method) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@love-moon/ai-sdk",
3
- "version": "0.15.2",
3
+ "version": "0.16.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/lovemoon-ai/conductor.git"
@@ -53,12 +53,12 @@
53
53
  "zod": "^4.1.5"
54
54
  },
55
55
  "optionalDependencies": {
56
- "@love-moon/chat-web": "^0.15.2"
56
+ "@love-moon/chat-web": "^0.16.0"
57
57
  },
58
58
  "devDependencies": {
59
59
  "@types/node": "^22.10.2",
60
60
  "tsx": "^4.20.6",
61
61
  "typescript": "^5.6.3"
62
62
  },
63
- "gitCommitId": "f795a61"
63
+ "gitCommitId": "77239b9"
64
64
  }