@pontia/pi-client-plugin 0.6.6 → 0.6.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pontia/pi-client-plugin",
3
- "version": "0.6.6",
3
+ "version": "0.6.7",
4
4
  "description": "Pi extension that connects coding-agent sessions to Pontia.",
5
5
  "keywords": [
6
6
  "pi-package",
package/src/index.ts CHANGED
@@ -38,6 +38,7 @@ import {
38
38
  assistantTextFromMessage,
39
39
  errorMessageFromAgentEnd,
40
40
  lastAssistantTextFromMessages,
41
+ thinkingDeltaFromEvent,
41
42
  } from "./pi-message.js";
42
43
  import { loadProfileSystemPrompt } from "./profile.js";
43
44
  import { EventReporter, type EventReportResult } from "./reporter.js";
@@ -865,6 +866,8 @@ export function createPontiaPiExtension(
865
866
  if (!activeTurn || activeTurn.ended) return;
866
867
  await reportContextUsageFromHookEvent(event, ctx);
867
868
 
869
+ const thinking = thinkingDeltaFromEvent(event);
870
+ if (thinking) activeTurn.liveOutput.appendReasoning(thinking);
868
871
  const delta = assistantDeltaFromEvent(event);
869
872
  if (delta) activeTurn.liveOutput.appendText(delta);
870
873
  const toolCall = completeToolCallFromMessageUpdate(event);
@@ -5,6 +5,7 @@ import { asRecord, optionalString } from "./values.js";
5
5
 
6
6
  const DEFAULT_BATCH_DELAY_MS = 75;
7
7
  const RETRY_DELAY_MS = 500;
8
+ const MAX_REASONING_BYTES = 1024 * 1024;
8
9
 
9
10
  export type ManagedToolUse = {
10
11
  tool_name: string;
@@ -31,6 +32,7 @@ export interface LiveToolResult {
31
32
 
32
33
  export type LiveOutputItem =
33
34
  | { kind: "assistant_text"; item_id: string; text: string }
35
+ | { kind: "reasoning"; item_id: string; text: string; truncated: boolean }
34
36
  | {
35
37
  kind: "tool_call";
36
38
  item_id: string;
@@ -49,6 +51,7 @@ export type LiveOutputUpdate =
49
51
  | { type: "tool_output_delta"; call_id: string; delta: string; truncated: boolean }
50
52
  | { type: "tool_output_updated"; call_id: string; text: string; truncated: boolean }
51
53
  | { type: "assistant_text_delta"; item_id: string; delta: string }
54
+ | { type: "reasoning_delta"; item_id: string; delta: string; truncated: boolean }
52
55
  | {
53
56
  type: "tool_call";
54
57
  item_id: string;
@@ -66,6 +69,7 @@ export interface CompleteToolCall {
66
69
 
67
70
  export interface LiveOutputPublisherLike {
68
71
  appendText(delta: string): void;
72
+ appendReasoning(delta: string): void;
69
73
  appendToolCall(toolCall: CompleteToolCall): void;
70
74
  toolExecution(event: unknown): void;
71
75
  close(): Promise<void>;
@@ -123,6 +127,39 @@ export class LiveOutputPublisher implements LiveOutputPublisherLike {
123
127
  this.enqueue({ type: "assistant_text_delta", item_id: item.item_id, delta });
124
128
  }
125
129
 
130
+ appendReasoning(delta: string): void {
131
+ if (this.closing || this.disabled || delta.length === 0) return;
132
+ const reasoning = this.items.filter(
133
+ (item): item is Extract<LiveOutputItem, { kind: "reasoning" }> => item.kind === "reasoning",
134
+ );
135
+ // Once reasoning is incomplete, later reasoning would hide the gap.
136
+ if (reasoning.some((item) => item.truncated)) return;
137
+ const used = reasoning.reduce((size, item) => size + byteLength(item.text), 0);
138
+ const retained = truncateUtf8(delta, MAX_REASONING_BYTES - used);
139
+ const truncated = retained !== delta;
140
+ if (retained.length === 0) {
141
+ // No room for content: the marker can only be carried by a snapshot. A
142
+ // zero budget implies earlier reasoning already holds it.
143
+ reasoning.at(-1)!.truncated = true;
144
+ this.resync();
145
+ return;
146
+ }
147
+ let item = this.items.at(-1);
148
+ if (item?.kind !== "reasoning") {
149
+ if (this.items.length >= 256) return;
150
+ item = {
151
+ kind: "reasoning",
152
+ item_id: this.nextItemId("reasoning"),
153
+ text: "",
154
+ truncated: false,
155
+ };
156
+ this.items.push(item);
157
+ }
158
+ item.text += retained;
159
+ item.truncated = truncated;
160
+ this.enqueue({ type: "reasoning_delta", item_id: item.item_id, delta: retained, truncated });
161
+ }
162
+
126
163
  appendToolCall(toolCall: CompleteToolCall): void {
127
164
  if (
128
165
  this.closing ||
@@ -276,6 +313,12 @@ export class LiveOutputPublisher implements LiveOutputPublisherLike {
276
313
  this.scheduleFlush(this.batchDelayMs);
277
314
  }
278
315
 
316
+ private resync(): void {
317
+ this.sequence += 1;
318
+ this.needsSnapshot = true;
319
+ this.scheduleFlush(this.batchDelayMs);
320
+ }
321
+
279
322
  private nextItemId(prefix: string): string {
280
323
  this.itemSequence += 1;
281
324
  return `${prefix}_${this.itemSequence}`;
@@ -362,6 +405,14 @@ export class LiveOutputPublisher implements LiveOutputPublisherLike {
362
405
  }) - MAX_RPC_FRAME_BYTES;
363
406
  if (excess() <= 0) return;
364
407
  let changed = false;
408
+ // Reasoning yields first. Trim the latest reasoning first so the visible
409
+ // reasoning stays a contiguous prefix.
410
+ for (const item of [...this.items].reverse()) {
411
+ if (item.kind !== "reasoning" || excess() <= 0 || !item.text) continue;
412
+ item.text = truncateText(item.text, Math.max(0, escapedTextBytes(item.text) - excess()));
413
+ item.truncated = true;
414
+ changed = true;
415
+ }
365
416
  for (const item of this.items) {
366
417
  if (item.kind !== "tool_call") continue;
367
418
  if (item.output && excess() > 0 && item.output.text) {
@@ -474,6 +525,17 @@ function resultBytes(result?: LiveToolResult): number {
474
525
  function escapedTextBytes(text: string): number {
475
526
  return jsonBytes(text) - 2;
476
527
  }
528
+ function truncateUtf8(text: string, budget: number): string {
529
+ if (byteLength(text) <= budget) return text;
530
+ let end = 0;
531
+ let bytes = 0;
532
+ for (const char of text) {
533
+ bytes += byteLength(char);
534
+ if (bytes > budget) break;
535
+ end += char.length;
536
+ }
537
+ return text.slice(0, end);
538
+ }
477
539
  function truncateText(text: string, budget: number): string {
478
540
  if (escapedTextBytes(text) <= budget) return text;
479
541
  let end = 0;
package/src/pi-message.ts CHANGED
@@ -20,6 +20,15 @@ export function assistantTextFromMessage(message: unknown): string | undefined {
20
20
  return textFromContent(record.content);
21
21
  }
22
22
 
23
+ export function thinkingDeltaFromEvent(event: unknown): string | undefined {
24
+ if (!event || typeof event !== "object") return undefined;
25
+ const streamEvent = (event as Record<string, unknown>).assistantMessageEvent;
26
+ if (!streamEvent || typeof streamEvent !== "object") return undefined;
27
+ const record = streamEvent as Record<string, unknown>;
28
+ if (record.type !== "thinking_delta") return undefined;
29
+ return typeof record.delta === "string" && record.delta.length > 0 ? record.delta : undefined;
30
+ }
31
+
23
32
  export function assistantDeltaFromEvent(event: unknown): string | undefined {
24
33
  if (!event || typeof event !== "object") return undefined;
25
34
  const record = event as Record<string, unknown>;