laohuang 0.8.7 → 0.8.8

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/dist/bin.js CHANGED
@@ -7,13 +7,177 @@ import { fileURLToPath } from "node:url";
7
7
 
8
8
  // ../../packages/session/session-context/dist/conversation-history.js
9
9
  import { randomUUID } from "node:crypto";
10
+
11
+ // ../../packages/session/session-context/dist/token-estimator.js
12
+ import { createHash } from "node:crypto";
13
+ var TEXT_BYTES_PER_TOKEN = 4;
14
+ var BLOCK_OVERHEAD = 4;
15
+ var MESSAGE_OVERHEAD = 4;
16
+ var DefaultTokenEstimator = class {
17
+ estimateMessages(messages) {
18
+ return messages.reduce((total, message) => total + this.estimateMessage(message), 0);
19
+ }
20
+ estimateTools(tools) {
21
+ if (tools.length === 0) {
22
+ return 0;
23
+ }
24
+ return this.estimateText(canonicalJson(tools.map(({ name, description, parameters }) => ({ name, description, parameters })))) + BLOCK_OVERHEAD;
25
+ }
26
+ estimateText(text2) {
27
+ return Math.ceil(Buffer.byteLength(text2, "utf8") / TEXT_BYTES_PER_TOKEN);
28
+ }
29
+ measure(input) {
30
+ const anchor = input.anchor ?? null;
31
+ if (anchor !== null && this.isValidAnchor(input, anchor)) {
32
+ const trailingEntries = input.entries.filter((entry) => entry.seq > anchor.throughSeq);
33
+ const trailingTokens = this.estimateMessages(messagesForEntries(trailingEntries));
34
+ return {
35
+ totalTokens: anchor.contextTokens + trailingTokens,
36
+ anchorTokens: anchor.contextTokens,
37
+ trailingTokens,
38
+ source: "provider_usage"
39
+ };
40
+ }
41
+ const totalTokens = this.estimateMessages(input.messages) + this.estimateTools(input.tools);
42
+ return {
43
+ totalTokens,
44
+ anchorTokens: 0,
45
+ trailingTokens: totalTokens,
46
+ source: "estimated"
47
+ };
48
+ }
49
+ estimateMessage(message) {
50
+ if (message.role === "system" && message.toolDefinitions !== void 0)
51
+ return 0;
52
+ if (message.role === "system" || message.role === "user") {
53
+ return MESSAGE_OVERHEAD + BLOCK_OVERHEAD + this.estimateText(message.content);
54
+ }
55
+ if (message.role === "tool-result") {
56
+ return MESSAGE_OVERHEAD + BLOCK_OVERHEAD + this.estimateText(message.content);
57
+ }
58
+ return MESSAGE_OVERHEAD + message.content.reduce((total, block) => {
59
+ if (block.type === "text" || block.type === "reasoning") {
60
+ return total + BLOCK_OVERHEAD + this.estimateText(block.text);
61
+ }
62
+ return total + BLOCK_OVERHEAD + this.estimateText(`${block.call.name}${canonicalJson(block.call.arguments)}`);
63
+ }, 0);
64
+ }
65
+ isValidAnchor(input, anchor) {
66
+ return anchor.provider === input.provider && anchor.model === input.model && anchor.systemFingerprint === input.systemFingerprint && anchor.projectInstructionsFingerprint === input.projectInstructionsFingerprint && anchor.toolsFingerprint === input.toolsFingerprint && input.entries.some((entry) => entry.id === anchor.throughEntryId && entry.seq === anchor.throughSeq);
67
+ }
68
+ };
69
+ function normalizeProviderUsage(usage) {
70
+ return usage.inputTokens + (usage.cacheReadTokens ?? 0) + (usage.cacheWriteTokens ?? 0) + usage.outputTokens;
71
+ }
72
+ function fingerprintContextPart(value) {
73
+ return createHash("sha256").update(canonicalJson(value)).digest("hex");
74
+ }
75
+ function canonicalJson(value) {
76
+ return JSON.stringify(sortStable(value));
77
+ }
78
+ function sortStable(value) {
79
+ if (Array.isArray(value)) {
80
+ return value.map(sortStable);
81
+ }
82
+ if (value !== null && typeof value === "object") {
83
+ const record = value;
84
+ const sorted = {};
85
+ for (const key of Object.keys(record).sort()) {
86
+ sorted[key] = sortStable(record[key]);
87
+ }
88
+ return sorted;
89
+ }
90
+ return value;
91
+ }
92
+ function messagesForEntries(entries) {
93
+ return entries.flatMap((entry) => {
94
+ if (entry.entryType === "tool_definitions" || entry.entryType === "tool_catalog")
95
+ return [entry.payload.message];
96
+ if (entry.entryType === "system_context") {
97
+ return [entry.payload.message];
98
+ }
99
+ if (entry.entryType === "project_instructions" || entry.entryType === "user_message" || entry.entryType === "reminder") {
100
+ return [entry.payload.message];
101
+ }
102
+ if (entry.entryType === "assistant_message") {
103
+ return [entry.payload.message];
104
+ }
105
+ if (entry.entryType === "tool_result") {
106
+ return [entry.payload.message];
107
+ }
108
+ return [];
109
+ });
110
+ }
111
+
112
+ // ../../packages/session/session-context/dist/context-usage.js
113
+ var ContextUsage = class {
114
+ #tokens = 0;
115
+ constructor(entries = []) {
116
+ for (const entry of entries)
117
+ this.append(entry);
118
+ }
119
+ get tokens() {
120
+ return this.#tokens;
121
+ }
122
+ /** Histories are append-only and ordered; process each committed entry once. */
123
+ append(entry) {
124
+ if (entry.entryType === "compaction") {
125
+ this.#tokens = null;
126
+ return;
127
+ }
128
+ if (entry.entryType === "assistant_message") {
129
+ const usage = validUsageTokens(entry.payload.usage);
130
+ if (usage !== null) {
131
+ this.#tokens = usage;
132
+ return;
133
+ }
134
+ }
135
+ if (this.#tokens === null)
136
+ return;
137
+ if (entry.entryType === "user_message" || entry.entryType === "assistant_message" || entry.entryType === "tool_result" || entry.entryType === "reminder") {
138
+ this.#tokens += estimateMessage(entry.payload.message);
139
+ }
140
+ }
141
+ };
142
+ function validUsageTokens(usage) {
143
+ if (usage === void 0 || usage === null)
144
+ return null;
145
+ const fields = [
146
+ usage.inputTokens,
147
+ usage.outputTokens,
148
+ usage.cacheReadTokens ?? 0,
149
+ usage.cacheWriteTokens ?? 0
150
+ ];
151
+ if (!fields.every((value) => Number.isSafeInteger(value) && value >= 0))
152
+ return null;
153
+ const total = normalizeProviderUsage(usage);
154
+ return Number.isSafeInteger(total) && total > 0 ? total : null;
155
+ }
156
+ function estimateMessage(message) {
157
+ if (message.role === "system")
158
+ return 0;
159
+ if (message.role !== "assistant")
160
+ return Math.ceil(message.content.length / 4);
161
+ const chars = message.content.reduce((sum, block) => {
162
+ if (block.type === "tool-call") {
163
+ return sum + block.call.name.length + JSON.stringify(block.call.arguments).length;
164
+ }
165
+ return sum + block.text.length;
166
+ }, 0);
167
+ return Math.ceil(chars / 4);
168
+ }
169
+
170
+ // ../../packages/session/session-context/dist/conversation-history.js
10
171
  var ConversationHistory = class _ConversationHistory {
11
172
  #journal;
12
173
  #entries;
13
174
  #staged = /* @__PURE__ */ new Map();
175
+ #contextUsage;
176
+ #listeners = /* @__PURE__ */ new Set();
14
177
  constructor(entries, journal) {
15
178
  this.#entries = [...entries];
16
179
  this.#journal = journal;
180
+ this.#contextUsage = new ContextUsage(entries);
17
181
  }
18
182
  static fromReplay(replay, journal) {
19
183
  const history = new _ConversationHistory(replay.items.filter((item) => item.kind === "entry"), journal);
@@ -126,9 +290,21 @@ var ConversationHistory = class _ConversationHistory {
126
290
  entries() {
127
291
  return [...this.#entries];
128
292
  }
293
+ get contextTokens() {
294
+ return this.#contextUsage.tokens;
295
+ }
296
+ onChange(listener) {
297
+ this.#listeners.add(listener);
298
+ return () => {
299
+ this.#listeners.delete(listener);
300
+ };
301
+ }
129
302
  append(input) {
130
303
  const entry = this.#journal.appendEntry(input);
131
304
  this.#entries.push(entry);
305
+ this.#contextUsage.append(entry);
306
+ for (const listener of this.#listeners)
307
+ listener();
132
308
  return entry;
133
309
  }
134
310
  };
@@ -517,104 +693,6 @@ function validateToolPairs(selected) {
517
693
  }
518
694
  }
519
695
 
520
- // ../../packages/session/session-context/dist/token-estimator.js
521
- import { createHash } from "node:crypto";
522
- var TEXT_BYTES_PER_TOKEN = 4;
523
- var BLOCK_OVERHEAD = 4;
524
- var MESSAGE_OVERHEAD = 4;
525
- var DefaultTokenEstimator = class {
526
- estimateMessages(messages) {
527
- return messages.reduce((total, message) => total + this.estimateMessage(message), 0);
528
- }
529
- estimateTools(tools) {
530
- if (tools.length === 0) {
531
- return 0;
532
- }
533
- return this.estimateText(canonicalJson(tools.map(({ name, description, parameters }) => ({ name, description, parameters })))) + BLOCK_OVERHEAD;
534
- }
535
- estimateText(text2) {
536
- return Math.ceil(Buffer.byteLength(text2, "utf8") / TEXT_BYTES_PER_TOKEN);
537
- }
538
- measure(input) {
539
- const anchor = input.anchor ?? null;
540
- if (anchor !== null && this.isValidAnchor(input, anchor)) {
541
- const trailingEntries = input.entries.filter((entry) => entry.seq > anchor.throughSeq);
542
- const trailingTokens = this.estimateMessages(messagesForEntries(trailingEntries));
543
- return {
544
- totalTokens: anchor.contextTokens + trailingTokens,
545
- anchorTokens: anchor.contextTokens,
546
- trailingTokens,
547
- source: "provider_usage"
548
- };
549
- }
550
- const totalTokens = this.estimateMessages(input.messages) + this.estimateTools(input.tools);
551
- return {
552
- totalTokens,
553
- anchorTokens: 0,
554
- trailingTokens: totalTokens,
555
- source: "estimated"
556
- };
557
- }
558
- estimateMessage(message) {
559
- if (message.role === "system" && message.toolDefinitions !== void 0)
560
- return 0;
561
- if (message.role === "system" || message.role === "user") {
562
- return MESSAGE_OVERHEAD + BLOCK_OVERHEAD + this.estimateText(message.content);
563
- }
564
- if (message.role === "tool-result") {
565
- return MESSAGE_OVERHEAD + BLOCK_OVERHEAD + this.estimateText(message.content);
566
- }
567
- return MESSAGE_OVERHEAD + message.content.reduce((total, block) => {
568
- if (block.type === "text" || block.type === "reasoning") {
569
- return total + BLOCK_OVERHEAD + this.estimateText(block.text);
570
- }
571
- return total + BLOCK_OVERHEAD + this.estimateText(`${block.call.name}${canonicalJson(block.call.arguments)}`);
572
- }, 0);
573
- }
574
- isValidAnchor(input, anchor) {
575
- return anchor.provider === input.provider && anchor.model === input.model && anchor.systemFingerprint === input.systemFingerprint && anchor.projectInstructionsFingerprint === input.projectInstructionsFingerprint && anchor.toolsFingerprint === input.toolsFingerprint && input.entries.some((entry) => entry.id === anchor.throughEntryId && entry.seq === anchor.throughSeq);
576
- }
577
- };
578
- function fingerprintContextPart(value) {
579
- return createHash("sha256").update(canonicalJson(value)).digest("hex");
580
- }
581
- function canonicalJson(value) {
582
- return JSON.stringify(sortStable(value));
583
- }
584
- function sortStable(value) {
585
- if (Array.isArray(value)) {
586
- return value.map(sortStable);
587
- }
588
- if (value !== null && typeof value === "object") {
589
- const record = value;
590
- const sorted = {};
591
- for (const key of Object.keys(record).sort()) {
592
- sorted[key] = sortStable(record[key]);
593
- }
594
- return sorted;
595
- }
596
- return value;
597
- }
598
- function messagesForEntries(entries) {
599
- return entries.flatMap((entry) => {
600
- if (entry.entryType === "tool_definitions" || entry.entryType === "tool_catalog")
601
- return [entry.payload.message];
602
- if (entry.entryType === "system_context") {
603
- return [entry.payload.message];
604
- }
605
- if (entry.entryType === "project_instructions" || entry.entryType === "user_message" || entry.entryType === "reminder") {
606
- return [entry.payload.message];
607
- }
608
- if (entry.entryType === "assistant_message") {
609
- return [entry.payload.message];
610
- }
611
- if (entry.entryType === "tool_result") {
612
- return [entry.payload.message];
613
- }
614
- return [];
615
- });
616
- }
617
-
618
696
  // ../../packages/session/session-context/dist/summary-prompt.js
619
697
  var COMPACTION_SYSTEM_PROMPT = "Summarize the conversation for continuation. Preserve user goals, explicit constraints, completed work, modified files, architectural decisions, tool outcomes, failures, current TODOs, and concrete state needed to continue. Do not include credentials. Do not invent results.";
620
698
 
@@ -8686,7 +8764,6 @@ var UIEventReducer = class _UIEventReducer {
8686
8764
  }
8687
8765
  if (kind === "model.request_started") {
8688
8766
  this.state.sessionState = "RUNNING_MODEL";
8689
- this.#updateContextUsage(payload);
8690
8767
  this.state.activeResponse = {
8691
8768
  requestId: correlationId,
8692
8769
  text: "",
@@ -8785,7 +8862,6 @@ var UIEventReducer = class _UIEventReducer {
8785
8862
  if (kind === "model.switched") {
8786
8863
  this.state.provider = String("provider" in payload ? payload.provider : this.state.provider);
8787
8864
  this.state.model = String("model" in payload ? payload.model : this.state.model);
8788
- this.#updateContextUsage(payload);
8789
8865
  return createUpdate(kind, { payload });
8790
8866
  }
8791
8867
  return null;
@@ -8800,7 +8876,7 @@ var UIEventReducer = class _UIEventReducer {
8800
8876
  }
8801
8877
  #updateContextUsage(payload) {
8802
8878
  if ("context_tokens" in payload) {
8803
- this.state.contextTokens = toInt(payload.context_tokens);
8879
+ this.state.contextTokens = payload.context_tokens === null ? null : toInt(payload.context_tokens);
8804
8880
  }
8805
8881
  if ("context_window" in payload) {
8806
8882
  this.state.contextWindow = toInt(payload.context_window);
@@ -9466,6 +9542,8 @@ function contextUsageLabel(tokens, contextWindow) {
9466
9542
  if (window <= 0) {
9467
9543
  return null;
9468
9544
  }
9545
+ if (tokens === null)
9546
+ return `context: ? (?/${formatCompactCount(window)})`;
9469
9547
  const used = normalizedCount(tokens);
9470
9548
  const ratio = Math.min(100, used / window * 100);
9471
9549
  const percent = ratio > 0 && ratio < 0.1 ? "<0.1" : formatScaled(ratio);
@@ -12335,12 +12413,15 @@ var TerminalUI = class {
12335
12413
  Object.assign(this.capabilities, capabilities);
12336
12414
  }
12337
12415
  setSessionId(sessionId) {
12416
+ if (sessionId !== this.#sessionId)
12417
+ this.state.contextTokens = 0;
12338
12418
  this.#sessionId = sessionId;
12339
12419
  this.#loop?.requestRender();
12340
12420
  }
12341
12421
  setContextUsage(tokens, contextWindow) {
12342
12422
  this.publishEvent({
12343
12423
  kind: "ui.context_usage",
12424
+ session_id: this.#sessionId,
12344
12425
  payload: { context_tokens: tokens, context_window: contextWindow }
12345
12426
  });
12346
12427
  }
@@ -12529,6 +12610,8 @@ var TerminalUI = class {
12529
12610
  }
12530
12611
  /** Reduce a projected event and apply its append-only block change. */
12531
12612
  applyProjectedEvent(event) {
12613
+ if (event.kind === "ui.context_usage" && event.session_id !== this.#sessionId)
12614
+ return;
12532
12615
  for (const projected of this.#displayPolicy.project(event)) {
12533
12616
  this.#applyDisplayEvent(projected);
12534
12617
  }
@@ -16562,7 +16645,8 @@ Authentication failed for ${this.options.provider}. Run /login ${this.options.pr
16562
16645
  });
16563
16646
  if (!history.commitAssistant(result.message, {
16564
16647
  requestId,
16565
- finishReason: result.finishReason
16648
+ finishReason: result.finishReason,
16649
+ ...toolCalls.length > 0 || textOf(result.message) !== "" ? { usage: result.usage } : {}
16566
16650
  })) {
16567
16651
  this.emit("model_response_aborted", {
16568
16652
  round: modelRound,
@@ -16767,7 +16851,8 @@ var HistoryCommitter = class {
16767
16851
  this.conversationHistory?.appendAssistant({
16768
16852
  message,
16769
16853
  requestId: metadata.requestId,
16770
- finishReason: metadata.finishReason
16854
+ finishReason: metadata.finishReason,
16855
+ ...metadata.usage === void 0 ? {} : { usage: metadata.usage }
16771
16856
  });
16772
16857
  }
16773
16858
  this.messages.push(message);
@@ -16778,7 +16863,8 @@ var HistoryCommitter = class {
16778
16863
  this.conversationHistory?.appendAssistant({
16779
16864
  message,
16780
16865
  requestId: metadata.requestId,
16781
- finishReason: metadata.finishReason
16866
+ finishReason: metadata.finishReason,
16867
+ ...metadata.usage === void 0 ? {} : { usage: metadata.usage }
16782
16868
  });
16783
16869
  }
16784
16870
  this.messages.push(message);
@@ -17385,24 +17471,22 @@ function createSessionRuntime(options) {
17385
17471
  if (notify === void 0) {
17386
17472
  return;
17387
17473
  }
17388
- const entries = options.sessionController.history?.entries() ?? [];
17389
- const context = new ContextBuilder().build({
17390
- entries,
17391
- currentProvider: route.provider,
17392
- currentModel: route.model
17393
- });
17394
- const estimator = new DefaultTokenEstimator();
17395
17474
  notify({
17396
- contextTokens: estimator.estimateMessages(context.messages) + estimator.estimateTools(projectTools(context.messages)),
17475
+ contextTokens: options.sessionController.history === null ? 0 : options.sessionController.history.contextTokens,
17397
17476
  contextWindow: selectedModel.contextWindow
17398
17477
  });
17399
17478
  };
17479
+ let unsubscribeHistory;
17400
17480
  const refreshSession = () => {
17401
17481
  const sessionId = options.sessionController.currentSessionId;
17402
17482
  const history = options.sessionController.history;
17403
17483
  if (sessionId === null || history === null) {
17404
17484
  throw new Error("no active session");
17405
17485
  }
17486
+ unsubscribeHistory?.();
17487
+ unsubscribeHistory = history.onChange(() => {
17488
+ if (options.sessionController.history === history) refreshContextUsage();
17489
+ });
17406
17490
  session.setSessionId(sessionId);
17407
17491
  options.presentation?.sessionChanged?.(sessionId);
17408
17492
  let entries = history.entries();
@@ -17470,6 +17554,7 @@ function createSessionRuntime(options) {
17470
17554
  compact: () => options.sessionController.compact(),
17471
17555
  refreshContextUsage,
17472
17556
  async close() {
17557
+ unsubscribeHistory?.();
17473
17558
  try {
17474
17559
  await recorder.close();
17475
17560
  } finally {