@pontia/pi-client-plugin 0.5.0 → 0.5.2
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/control-socket.ts +19 -7
- package/src/events.ts +0 -14
- package/src/history.ts +143 -0
- package/src/index.ts +8 -32
- package/src/pi-message.ts +0 -17
- package/src/rpc-error.ts +7 -0
package/package.json
CHANGED
package/src/control-socket.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { RpcError } from "./rpc-error.js";
|
|
2
|
+
export { RpcError } from "./rpc-error.js";
|
|
3
|
+
import { readHistory, type HistoryContext } from "./history.js";
|
|
1
4
|
import { createConnection, type Socket } from "node:net";
|
|
2
5
|
import { isAbsolute, join } from "node:path";
|
|
3
6
|
|
|
@@ -35,13 +38,6 @@ export interface PiConnection {
|
|
|
35
38
|
registered(identity: ControlIdentity): void;
|
|
36
39
|
close(): Promise<void>;
|
|
37
40
|
}
|
|
38
|
-
export class RpcError extends Error {
|
|
39
|
-
readonly code: number;
|
|
40
|
-
constructor(code: number, message: string) {
|
|
41
|
-
super(message);
|
|
42
|
-
this.code = code;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
41
|
|
|
46
42
|
export function piSocketPath(pontiaHome: string): string {
|
|
47
43
|
const path = join(pontiaHome, "state", "pi", "rpc.sock");
|
|
@@ -68,6 +64,7 @@ class RpcSocket {
|
|
|
68
64
|
private onSubmit: (input: ControlInput) => void;
|
|
69
65
|
private models?: ModelControl;
|
|
70
66
|
private lifecycle?: LifecycleControl;
|
|
67
|
+
private history?: () => HistoryContext;
|
|
71
68
|
private onReplay?: (inboxMessageId: string) => void;
|
|
72
69
|
|
|
73
70
|
constructor(
|
|
@@ -76,12 +73,14 @@ class RpcSocket {
|
|
|
76
73
|
models?: ModelControl,
|
|
77
74
|
onReplay?: (inboxMessageId: string) => void,
|
|
78
75
|
lifecycle?: LifecycleControl,
|
|
76
|
+
history?: () => HistoryContext,
|
|
79
77
|
) {
|
|
80
78
|
this.socket = socket;
|
|
81
79
|
this.onSubmit = onSubmit;
|
|
82
80
|
this.models = models;
|
|
83
81
|
this.onReplay = onReplay;
|
|
84
82
|
this.lifecycle = lifecycle;
|
|
83
|
+
this.history = history;
|
|
85
84
|
socket.on("data", (chunk: Buffer) => this.receive(chunk));
|
|
86
85
|
socket.on("error", (error) => this.fail(error));
|
|
87
86
|
socket.on("close", () =>
|
|
@@ -181,6 +180,17 @@ class RpcSocket {
|
|
|
181
180
|
error(-32602, "Expected named parameters");
|
|
182
181
|
return;
|
|
183
182
|
}
|
|
183
|
+
if (message.method === "history.read" && this.history) {
|
|
184
|
+
try {
|
|
185
|
+
respond({ result: readHistory(this.history(), params) });
|
|
186
|
+
} catch (failure) {
|
|
187
|
+
error(
|
|
188
|
+
failure instanceof RpcError ? failure.code : -32009,
|
|
189
|
+
failure instanceof Error ? failure.message : String(failure),
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
184
194
|
if (message.method === "ping") {
|
|
185
195
|
respond({ result: { pong: true } });
|
|
186
196
|
return;
|
|
@@ -299,6 +309,7 @@ export async function connectPi(
|
|
|
299
309
|
models?: ModelControl,
|
|
300
310
|
onReplay?: (inboxMessageId: string) => void,
|
|
301
311
|
lifecycle?: LifecycleControl,
|
|
312
|
+
history?: () => HistoryContext,
|
|
302
313
|
): Promise<PiConnection> {
|
|
303
314
|
const path = piSocketPath(pontiaHome);
|
|
304
315
|
let identity: ControlIdentity | undefined;
|
|
@@ -369,6 +380,7 @@ export async function connectPi(
|
|
|
369
380
|
reportingOnly ? undefined : models,
|
|
370
381
|
reportingOnly ? undefined : onReplay,
|
|
371
382
|
reportingOnly ? undefined : lifecycle,
|
|
383
|
+
reportingOnly ? undefined : history,
|
|
372
384
|
);
|
|
373
385
|
peers.add(peer);
|
|
374
386
|
socket.once("close", () => {
|
package/src/events.ts
CHANGED
|
@@ -8,7 +8,6 @@ export type InternalEventType =
|
|
|
8
8
|
| "session.ready"
|
|
9
9
|
| "session.exited"
|
|
10
10
|
| "session.model_updated"
|
|
11
|
-
| "session.message_updated"
|
|
12
11
|
| "session.context_usage_updated"
|
|
13
12
|
| "turn.started"
|
|
14
13
|
| "turn.output"
|
|
@@ -32,8 +31,6 @@ export interface ContextUsageObservation {
|
|
|
32
31
|
model: string | null;
|
|
33
32
|
}
|
|
34
33
|
|
|
35
|
-
export type SessionMessageUpdatedReason = "append" | "update" | "final";
|
|
36
|
-
|
|
37
34
|
export type PiTopologyEntryKind =
|
|
38
35
|
| "user_message"
|
|
39
36
|
| "assistant_message"
|
|
@@ -76,17 +73,6 @@ function turnFact(
|
|
|
76
73
|
return { session_id: context.sessionId, turn_id: context.turnId, type, data };
|
|
77
74
|
}
|
|
78
75
|
|
|
79
|
-
export function buildSessionMessageUpdatedEvent(
|
|
80
|
-
context: TurnContext,
|
|
81
|
-
reason: SessionMessageUpdatedReason,
|
|
82
|
-
): InternalEvent {
|
|
83
|
-
return {
|
|
84
|
-
session_id: context.sessionId,
|
|
85
|
-
type: "session.message_updated",
|
|
86
|
-
data: { reason },
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
|
|
90
76
|
export function buildSessionContextUsageUpdatedEvent(
|
|
91
77
|
context: TurnContext,
|
|
92
78
|
usage: ContextUsagePayload,
|
package/src/history.ts
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { RpcError } from "./rpc-error.js";
|
|
2
|
+
|
|
3
|
+
const PAGE_BYTES = 1024 * 1024;
|
|
4
|
+
export const MAX_HISTORY_ENTRIES = 100_000;
|
|
5
|
+
export interface HistoryContext {
|
|
6
|
+
generation: number;
|
|
7
|
+
sessionManager: {
|
|
8
|
+
getSessionId(): string;
|
|
9
|
+
getEntries(): unknown[];
|
|
10
|
+
getLeafId(): string | null;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
interface Snapshot {
|
|
14
|
+
session_id: string;
|
|
15
|
+
generation: number;
|
|
16
|
+
entry_count: number;
|
|
17
|
+
upper_entry_id: string | null;
|
|
18
|
+
leaf_id: string | null;
|
|
19
|
+
}
|
|
20
|
+
const invalid = (message: string): never => {
|
|
21
|
+
throw new RpcError(-32602, message);
|
|
22
|
+
};
|
|
23
|
+
const token = (value: unknown): string => Buffer.from(JSON.stringify(value)).toString("base64url");
|
|
24
|
+
function decode(value: unknown): Record<string, unknown> {
|
|
25
|
+
if (typeof value !== "string" || value.length > 4096)
|
|
26
|
+
return invalid("Invalid history continuation");
|
|
27
|
+
try {
|
|
28
|
+
const parsed = JSON.parse(Buffer.from(value, "base64url").toString());
|
|
29
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed) || token(parsed) !== value)
|
|
30
|
+
return invalid("Invalid history continuation");
|
|
31
|
+
return parsed;
|
|
32
|
+
} catch {
|
|
33
|
+
return invalid("Invalid history continuation");
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const identity = (value: unknown): value is string =>
|
|
37
|
+
typeof value === "string" && !!value.trim() && value.length <= 512;
|
|
38
|
+
|
|
39
|
+
/** Reads the current canonical manager; tokens contain identities, never file positions. */
|
|
40
|
+
export function readHistory(context: HistoryContext, params: Record<string, unknown>): object {
|
|
41
|
+
if (Object.keys(params).some((key) => !["page_size", "snapshot", "continuation"].includes(key)))
|
|
42
|
+
return invalid("Unknown history parameter");
|
|
43
|
+
const size = params.page_size === undefined ? 64 : params.page_size;
|
|
44
|
+
if (!Number.isInteger(size) || (size as number) < 1 || (size as number) > 128)
|
|
45
|
+
return invalid("Invalid history page size");
|
|
46
|
+
const manager = context.sessionManager;
|
|
47
|
+
const sessionId = manager.getSessionId();
|
|
48
|
+
if (!identity(sessionId)) return invalid("Invalid native Session identity");
|
|
49
|
+
const entries = manager.getEntries() as Record<string, unknown>[];
|
|
50
|
+
if (!Array.isArray(entries) || entries.length > MAX_HISTORY_ENTRIES)
|
|
51
|
+
return invalid("History snapshot exceeds entry limit");
|
|
52
|
+
let snapshot: Snapshot;
|
|
53
|
+
if (params.snapshot === undefined) {
|
|
54
|
+
if (params.continuation !== undefined) return invalid("Continuation requires snapshot");
|
|
55
|
+
snapshot = {
|
|
56
|
+
session_id: sessionId,
|
|
57
|
+
generation: context.generation,
|
|
58
|
+
entry_count: entries.length,
|
|
59
|
+
upper_entry_id: entries.length ? (entries[entries.length - 1].id as string) : null,
|
|
60
|
+
leaf_id: manager.getLeafId(),
|
|
61
|
+
};
|
|
62
|
+
} else {
|
|
63
|
+
const parsed = decode(params.snapshot);
|
|
64
|
+
if (
|
|
65
|
+
Object.keys(parsed).sort().join() !==
|
|
66
|
+
"entry_count,generation,leaf_id,session_id,upper_entry_id" ||
|
|
67
|
+
!Number.isInteger(parsed.entry_count) ||
|
|
68
|
+
(parsed.entry_count as number) < 0 ||
|
|
69
|
+
(parsed.entry_count as number) > MAX_HISTORY_ENTRIES ||
|
|
70
|
+
parsed.session_id !== sessionId ||
|
|
71
|
+
parsed.generation !== context.generation ||
|
|
72
|
+
!(parsed.upper_entry_id === null || identity(parsed.upper_entry_id)) ||
|
|
73
|
+
!(parsed.leaf_id === null || identity(parsed.leaf_id))
|
|
74
|
+
)
|
|
75
|
+
return invalid("History snapshot identity changed");
|
|
76
|
+
snapshot = parsed as unknown as Snapshot;
|
|
77
|
+
}
|
|
78
|
+
const upper =
|
|
79
|
+
snapshot.upper_entry_id === null
|
|
80
|
+
? -1
|
|
81
|
+
: entries.findIndex((entry) => entry?.id === snapshot.upper_entry_id);
|
|
82
|
+
if (
|
|
83
|
+
snapshot.upper_entry_id !== null &&
|
|
84
|
+
entries.filter((entry) => entry?.id === snapshot.upper_entry_id).length !== 1
|
|
85
|
+
)
|
|
86
|
+
return invalid("Duplicate history upper identity");
|
|
87
|
+
if (snapshot.upper_entry_id !== null && upper < 0) return invalid("Invalid history upper bound");
|
|
88
|
+
if (upper + 1 !== snapshot.entry_count) return invalid("History snapshot entry count changed");
|
|
89
|
+
const ids = new Set<string>();
|
|
90
|
+
for (let index = 0; index <= upper; index++) {
|
|
91
|
+
const entry = entries[index];
|
|
92
|
+
if (
|
|
93
|
+
!entry ||
|
|
94
|
+
!identity(entry.id) ||
|
|
95
|
+
ids.has(entry.id) ||
|
|
96
|
+
!identity(entry.type) ||
|
|
97
|
+
!identity(entry.timestamp) ||
|
|
98
|
+
!(entry.parentId === null || identity(entry.parentId)) ||
|
|
99
|
+
(entry.parentId !== null && !ids.has(entry.parentId as string))
|
|
100
|
+
)
|
|
101
|
+
return invalid("Invalid history entry identity or parent");
|
|
102
|
+
ids.add(entry.id);
|
|
103
|
+
}
|
|
104
|
+
if (snapshot.leaf_id !== null && !ids.has(snapshot.leaf_id))
|
|
105
|
+
return invalid("History leaf is outside snapshot");
|
|
106
|
+
const snapshotToken = token(snapshot);
|
|
107
|
+
let start = 0;
|
|
108
|
+
if (params.continuation !== undefined) {
|
|
109
|
+
const continuation = decode(params.continuation);
|
|
110
|
+
if (
|
|
111
|
+
Object.keys(continuation).sort().join() !== "after,snapshot" ||
|
|
112
|
+
continuation.snapshot !== snapshotToken ||
|
|
113
|
+
!identity(continuation.after)
|
|
114
|
+
)
|
|
115
|
+
return invalid("Invalid history continuation");
|
|
116
|
+
const after = entries.findIndex((entry) => entry?.id === continuation.after);
|
|
117
|
+
if (after < 0 || after >= upper) return invalid("History continuation outside snapshot");
|
|
118
|
+
start = after + 1;
|
|
119
|
+
} else if (params.snapshot !== undefined) return invalid("Snapshot requires continuation");
|
|
120
|
+
const page: unknown[] = [];
|
|
121
|
+
let bytes = 0;
|
|
122
|
+
for (let index = start; index <= upper && page.length < (size as number); index++) {
|
|
123
|
+
const entry = entries[index];
|
|
124
|
+
const encoded = JSON.stringify(entry);
|
|
125
|
+
const length = Buffer.byteLength(encoded);
|
|
126
|
+
if (length > PAGE_BYTES - 8192) return invalid("History entry exceeds response budget");
|
|
127
|
+
if (bytes + length > PAGE_BYTES - 8192) break;
|
|
128
|
+
page.push(entry);
|
|
129
|
+
bytes += length + 1;
|
|
130
|
+
}
|
|
131
|
+
const end = start + page.length;
|
|
132
|
+
const continuation =
|
|
133
|
+
end <= upper ? token({ snapshot: snapshotToken, after: entries[end - 1].id }) : null;
|
|
134
|
+
return {
|
|
135
|
+
session_id: sessionId,
|
|
136
|
+
snapshot: snapshotToken,
|
|
137
|
+
entry_count: snapshot.entry_count,
|
|
138
|
+
upper_entry_id: snapshot.upper_entry_id,
|
|
139
|
+
leaf_id: snapshot.leaf_id,
|
|
140
|
+
entries: page,
|
|
141
|
+
continuation,
|
|
142
|
+
};
|
|
143
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
2
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import type { HistoryContext } from "./history.js";
|
|
3
4
|
import { connectPi, RpcError, type PiConnection, type ControlInput } from "./control-socket.js";
|
|
4
5
|
import { defaultHookLogFile, type EnvLike, type TurnContext } from "./context.js";
|
|
5
6
|
import { appendDiagnostic, type DiagnosticEntry } from "./diagnostics.js";
|
|
@@ -7,7 +8,6 @@ import { pontiaHomeFromEnv } from "./discovery.js";
|
|
|
7
8
|
import {
|
|
8
9
|
buildSessionContextUsageUpdatedEvent,
|
|
9
10
|
buildSessionExitedEvent,
|
|
10
|
-
buildSessionMessageUpdatedEvent,
|
|
11
11
|
buildSessionReadyEvent,
|
|
12
12
|
buildTurnCompletedEvent,
|
|
13
13
|
buildTurnFailedEvent,
|
|
@@ -18,7 +18,6 @@ import {
|
|
|
18
18
|
type InternalEvent,
|
|
19
19
|
type PiTopologyContext,
|
|
20
20
|
type PiTopologyEntryKind,
|
|
21
|
-
type SessionMessageUpdatedReason,
|
|
22
21
|
} from "./events.js";
|
|
23
22
|
import { asRecord, optionalString } from "./values.js";
|
|
24
23
|
import {
|
|
@@ -37,7 +36,6 @@ import {
|
|
|
37
36
|
assistantDeltaFromEvent,
|
|
38
37
|
assistantTextFromMessage,
|
|
39
38
|
errorMessageFromAgentEnd,
|
|
40
|
-
isTranscriptBoundaryMessageUpdate,
|
|
41
39
|
lastAssistantTextFromMessages,
|
|
42
40
|
} from "./pi-message.js";
|
|
43
41
|
import { loadProfileSystemPrompt } from "./profile.js";
|
|
@@ -340,6 +338,13 @@ export function createPontiaPiExtension(
|
|
|
340
338
|
ctx.shutdown();
|
|
341
339
|
},
|
|
342
340
|
},
|
|
341
|
+
() => {
|
|
342
|
+
lifecycleContext();
|
|
343
|
+
const manager = piContext!.sessionManager;
|
|
344
|
+
if (manager.getSessionId() !== boundSessionContext?.clientSessionKey)
|
|
345
|
+
throw new Error("Pi history native Session identity changed");
|
|
346
|
+
return { generation, sessionManager: manager } satisfies HistoryContext;
|
|
347
|
+
},
|
|
343
348
|
);
|
|
344
349
|
if (generation !== controlGeneration) {
|
|
345
350
|
await socket.close();
|
|
@@ -475,21 +480,6 @@ export function createPontiaPiExtension(
|
|
|
475
480
|
},
|
|
476
481
|
});
|
|
477
482
|
|
|
478
|
-
async function scheduleMessageRefresh(reason: SessionMessageUpdatedReason): Promise<void> {
|
|
479
|
-
if (!activeTurn || activeTurn.ended) return;
|
|
480
|
-
await activeTurn.reporter.report(
|
|
481
|
-
activeTurn.context,
|
|
482
|
-
buildSessionMessageUpdatedEvent(activeTurn.context, reason),
|
|
483
|
-
);
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
async function reportFinalMessageRefresh(state: ActiveTurnState): Promise<void> {
|
|
487
|
-
await state.reporter.report(
|
|
488
|
-
state.context,
|
|
489
|
-
buildSessionMessageUpdatedEvent(state.context, "final"),
|
|
490
|
-
);
|
|
491
|
-
}
|
|
492
|
-
|
|
493
483
|
async function reportContextUsageFromHookEvent(event: unknown, ctx?: unknown): Promise<void> {
|
|
494
484
|
if (!activeTurn || activeTurn.ended) return;
|
|
495
485
|
const observation = contextUsageFromPiHook(event, ctx);
|
|
@@ -836,16 +826,6 @@ export function createPontiaPiExtension(
|
|
|
836
826
|
} else if (delta) {
|
|
837
827
|
activeTurn.output += delta;
|
|
838
828
|
}
|
|
839
|
-
|
|
840
|
-
if (isTranscriptBoundaryMessageUpdate(event)) await scheduleMessageRefresh("update");
|
|
841
|
-
});
|
|
842
|
-
|
|
843
|
-
pi.on("tool_execution_start", async (event) => {
|
|
844
|
-
if (!event.parentToolCallId) await scheduleMessageRefresh("update");
|
|
845
|
-
});
|
|
846
|
-
|
|
847
|
-
pi.on("tool_execution_end", async (event) => {
|
|
848
|
-
if (!event.parentToolCallId) await scheduleMessageRefresh("update");
|
|
849
829
|
});
|
|
850
830
|
|
|
851
831
|
pi.on("message_end", async (event, ctx) => {
|
|
@@ -855,7 +835,6 @@ export function createPontiaPiExtension(
|
|
|
855
835
|
(event as unknown as Record<string, unknown> | undefined)?.message,
|
|
856
836
|
);
|
|
857
837
|
if (fullText) activeTurn.output = fullText;
|
|
858
|
-
await scheduleMessageRefresh("append");
|
|
859
838
|
});
|
|
860
839
|
|
|
861
840
|
pi.on("agent_settled", (_event, ctx) => {
|
|
@@ -878,7 +857,6 @@ export function createPontiaPiExtension(
|
|
|
878
857
|
state.context,
|
|
879
858
|
buildTurnInterruptedEvent(state.context, terminalLeafId),
|
|
880
859
|
);
|
|
881
|
-
await reportFinalMessageRefresh(state);
|
|
882
860
|
return;
|
|
883
861
|
}
|
|
884
862
|
|
|
@@ -888,7 +866,6 @@ export function createPontiaPiExtension(
|
|
|
888
866
|
state.context,
|
|
889
867
|
buildTurnFailedEvent(state.context, failureMessage, terminalLeafId),
|
|
890
868
|
);
|
|
891
|
-
await reportFinalMessageRefresh(state);
|
|
892
869
|
return;
|
|
893
870
|
}
|
|
894
871
|
|
|
@@ -912,7 +889,6 @@ export function createPontiaPiExtension(
|
|
|
912
889
|
state.context,
|
|
913
890
|
buildTurnCompletedEvent(state.context, terminalLeafId),
|
|
914
891
|
);
|
|
915
|
-
await reportFinalMessageRefresh(state);
|
|
916
892
|
} finally {
|
|
917
893
|
await state.liveOutput.close();
|
|
918
894
|
}
|
package/src/pi-message.ts
CHANGED
|
@@ -36,23 +36,6 @@ export function assistantDeltaFromEvent(event: unknown): string | undefined {
|
|
|
36
36
|
return undefined;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
const transcriptBoundaryStreamEventTypes = new Set([
|
|
40
|
-
"thinking_start",
|
|
41
|
-
"thinking_end",
|
|
42
|
-
"text_start",
|
|
43
|
-
"text_end",
|
|
44
|
-
"toolcall_start",
|
|
45
|
-
"toolcall_end",
|
|
46
|
-
]);
|
|
47
|
-
|
|
48
|
-
export function isTranscriptBoundaryMessageUpdate(event: unknown): boolean {
|
|
49
|
-
if (!event || typeof event !== "object") return false;
|
|
50
|
-
const streamEvent = (event as Record<string, unknown>).assistantMessageEvent;
|
|
51
|
-
if (!streamEvent || typeof streamEvent !== "object") return false;
|
|
52
|
-
const type = (streamEvent as Record<string, unknown>).type;
|
|
53
|
-
return typeof type === "string" && transcriptBoundaryStreamEventTypes.has(type);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
39
|
interface PiAgentEndEventLike {
|
|
57
40
|
messages: unknown[];
|
|
58
41
|
}
|