@pontia/pi-client-plugin 0.6.5 → 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 +1 -1
- package/src/index.ts +3 -0
- package/src/live-output.ts +62 -0
- package/src/pi-message.ts +9 -0
package/package.json
CHANGED
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);
|
package/src/live-output.ts
CHANGED
|
@@ -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>;
|