@plannotator/pi-extension 0.15.0 → 0.15.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/README.md +1 -1
- package/generated/ai/base-session.ts +95 -0
- package/generated/ai/context.ts +212 -0
- package/generated/ai/endpoints.ts +309 -0
- package/generated/ai/index.ts +106 -0
- package/generated/ai/provider.ts +104 -0
- package/generated/ai/providers/claude-agent-sdk.ts +441 -0
- package/generated/ai/providers/codex-sdk.ts +430 -0
- package/generated/ai/providers/opencode-sdk.ts +491 -0
- package/generated/ai/providers/pi-events.ts +111 -0
- package/generated/ai/providers/pi-sdk-node.ts +377 -0
- package/generated/ai/providers/pi-sdk.ts +442 -0
- package/generated/ai/session-manager.ts +196 -0
- package/generated/ai/types.ts +370 -0
- package/generated/resolve-file.ts +28 -0
- package/index.ts +74 -45
- package/package.json +2 -2
- package/plannotator.html +70 -70
- package/review-editor.html +2 -2
- package/server/serverAnnotate.ts +2 -1
- package/server/serverReview.ts +5 -5
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
// @generated — DO NOT EDIT. Source: packages/ai/providers/opencode-sdk.ts
|
|
2
|
+
/**
|
|
3
|
+
* OpenCode provider — bridges Plannotator's AI layer with OpenCode's agent server.
|
|
4
|
+
*
|
|
5
|
+
* Uses @opencode-ai/sdk to spawn `opencode serve` and communicate via HTTP + SSE.
|
|
6
|
+
* One server per provider, shared across all sessions. The user must have the
|
|
7
|
+
* `opencode` CLI installed and authenticated.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { BaseSession } from "../base-session.ts";
|
|
11
|
+
import { buildSystemPrompt } from "../context.ts";
|
|
12
|
+
import type {
|
|
13
|
+
AIMessage,
|
|
14
|
+
AIProvider,
|
|
15
|
+
AIProviderCapabilities,
|
|
16
|
+
AISession,
|
|
17
|
+
CreateSessionOptions,
|
|
18
|
+
OpenCodeConfig,
|
|
19
|
+
} from "../types.ts";
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Constants
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
const PROVIDER_NAME = "opencode-sdk";
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// SDK import cache — resolve once, reuse across all sessions
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
// biome-ignore lint/suspicious/noExplicitAny: SDK types not available at compile time
|
|
32
|
+
let sdk: any = null;
|
|
33
|
+
|
|
34
|
+
async function getSDK() {
|
|
35
|
+
if (!sdk) {
|
|
36
|
+
sdk = await import("@opencode-ai/sdk");
|
|
37
|
+
}
|
|
38
|
+
return sdk;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Provider
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
export class OpenCodeProvider implements AIProvider {
|
|
46
|
+
readonly name = PROVIDER_NAME;
|
|
47
|
+
readonly capabilities: AIProviderCapabilities = {
|
|
48
|
+
fork: true,
|
|
49
|
+
resume: true,
|
|
50
|
+
streaming: true,
|
|
51
|
+
tools: true,
|
|
52
|
+
};
|
|
53
|
+
models?: Array<{ id: string; label: string; default?: boolean }>;
|
|
54
|
+
|
|
55
|
+
private config: OpenCodeConfig;
|
|
56
|
+
// biome-ignore lint/suspicious/noExplicitAny: SDK types not available at compile time
|
|
57
|
+
private server: { url: string; close: () => void } | null = null;
|
|
58
|
+
// biome-ignore lint/suspicious/noExplicitAny: SDK types not available at compile time
|
|
59
|
+
private client: any = null;
|
|
60
|
+
private startPromise: Promise<void> | null = null;
|
|
61
|
+
|
|
62
|
+
constructor(config: OpenCodeConfig) {
|
|
63
|
+
this.config = config;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Lazy-spawn the OpenCode server and create the HTTP client. */
|
|
67
|
+
async ensureServer(): Promise<void> {
|
|
68
|
+
if (this.server && this.client) return;
|
|
69
|
+
this.startPromise ??= this.doStart().catch((err) => {
|
|
70
|
+
this.startPromise = null;
|
|
71
|
+
throw err;
|
|
72
|
+
});
|
|
73
|
+
return this.startPromise;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private async doStart(): Promise<void> {
|
|
77
|
+
const { createOpencodeServer, createOpencodeClient } = await getSDK();
|
|
78
|
+
|
|
79
|
+
this.server = await createOpencodeServer({
|
|
80
|
+
hostname: this.config.hostname ?? "127.0.0.1",
|
|
81
|
+
...(this.config.port != null && { port: this.config.port }),
|
|
82
|
+
timeout: 15_000,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
this.client = createOpencodeClient({
|
|
86
|
+
baseUrl: this.server!.url,
|
|
87
|
+
directory: this.config.cwd ?? process.cwd(),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async createSession(options: CreateSessionOptions): Promise<AISession> {
|
|
92
|
+
await this.ensureServer();
|
|
93
|
+
|
|
94
|
+
const result = await this.client.session.create({
|
|
95
|
+
query: { directory: options.cwd ?? this.config.cwd ?? process.cwd() },
|
|
96
|
+
});
|
|
97
|
+
const sessionData = result.data;
|
|
98
|
+
|
|
99
|
+
const session = new OpenCodeSession({
|
|
100
|
+
sessionId: sessionData.id,
|
|
101
|
+
systemPrompt: buildSystemPrompt(options.context),
|
|
102
|
+
client: this.client,
|
|
103
|
+
model: options.model,
|
|
104
|
+
parentSessionId: null,
|
|
105
|
+
});
|
|
106
|
+
return session;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async forkSession(options: CreateSessionOptions): Promise<AISession> {
|
|
110
|
+
await this.ensureServer();
|
|
111
|
+
|
|
112
|
+
const parentId = options.context.parent?.sessionId;
|
|
113
|
+
if (!parentId) {
|
|
114
|
+
throw new Error("Fork requires a parent session ID.");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const result = await this.client.session.fork({
|
|
118
|
+
path: { id: parentId },
|
|
119
|
+
});
|
|
120
|
+
const sessionData = result.data;
|
|
121
|
+
|
|
122
|
+
return new OpenCodeSession({
|
|
123
|
+
sessionId: sessionData.id,
|
|
124
|
+
systemPrompt: buildSystemPrompt(options.context),
|
|
125
|
+
client: this.client,
|
|
126
|
+
model: options.model,
|
|
127
|
+
parentSessionId: parentId,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async resumeSession(sessionId: string): Promise<AISession> {
|
|
132
|
+
await this.ensureServer();
|
|
133
|
+
|
|
134
|
+
// Verify session exists
|
|
135
|
+
await this.client.session.get({ path: { id: sessionId } });
|
|
136
|
+
|
|
137
|
+
return new OpenCodeSession({
|
|
138
|
+
sessionId,
|
|
139
|
+
systemPrompt: null,
|
|
140
|
+
client: this.client,
|
|
141
|
+
model: undefined,
|
|
142
|
+
parentSessionId: null,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
dispose(): void {
|
|
147
|
+
if (this.server) {
|
|
148
|
+
this.server.close();
|
|
149
|
+
this.server = null;
|
|
150
|
+
this.client = null;
|
|
151
|
+
this.startPromise = null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Fetch available models from OpenCode. Call before registering the provider. */
|
|
156
|
+
async fetchModels(): Promise<void> {
|
|
157
|
+
try {
|
|
158
|
+
await this.ensureServer();
|
|
159
|
+
|
|
160
|
+
const result = await this.client.provider.list({
|
|
161
|
+
query: { directory: this.config.cwd ?? process.cwd() },
|
|
162
|
+
});
|
|
163
|
+
const data = result.data;
|
|
164
|
+
const connected = new Set(data.connected as string[]);
|
|
165
|
+
const allProviders = data.all as Array<{
|
|
166
|
+
id: string;
|
|
167
|
+
models: Record<string, { id: string; providerID: string; name: string }>;
|
|
168
|
+
}>;
|
|
169
|
+
|
|
170
|
+
const models: Array<{ id: string; label: string; default?: boolean }> = [];
|
|
171
|
+
for (const provider of allProviders) {
|
|
172
|
+
if (!connected.has(provider.id)) continue;
|
|
173
|
+
for (const model of Object.values(provider.models)) {
|
|
174
|
+
models.push({
|
|
175
|
+
id: `${model.providerID}/${model.id}`,
|
|
176
|
+
label: model.name ?? model.id,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (models.length > 0) {
|
|
182
|
+
// Mark first model as default
|
|
183
|
+
models[0].default = true;
|
|
184
|
+
this.models = models;
|
|
185
|
+
}
|
|
186
|
+
} catch {
|
|
187
|
+
// OpenCode not configured or no models available
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
// Session
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
|
|
196
|
+
interface SessionConfig {
|
|
197
|
+
sessionId: string;
|
|
198
|
+
systemPrompt: string | null;
|
|
199
|
+
// biome-ignore lint/suspicious/noExplicitAny: SDK types not available at compile time
|
|
200
|
+
client: any;
|
|
201
|
+
/** Model in "providerID/modelID" format. */
|
|
202
|
+
model?: string;
|
|
203
|
+
parentSessionId: string | null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
class OpenCodeSession extends BaseSession {
|
|
207
|
+
private config: SessionConfig;
|
|
208
|
+
|
|
209
|
+
constructor(config: SessionConfig) {
|
|
210
|
+
super({
|
|
211
|
+
parentSessionId: config.parentSessionId,
|
|
212
|
+
initialId: config.sessionId,
|
|
213
|
+
});
|
|
214
|
+
this.config = config;
|
|
215
|
+
this._resolvedId = config.sessionId;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async *query(prompt: string): AsyncIterable<AIMessage> {
|
|
219
|
+
const started = this.startQuery();
|
|
220
|
+
if (!started) {
|
|
221
|
+
yield BaseSession.BUSY_ERROR;
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const { gen } = started;
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
// Build model param if specified
|
|
228
|
+
let modelParam: { providerID: string; modelID: string } | undefined;
|
|
229
|
+
if (this.config.model) {
|
|
230
|
+
const [providerID, ...rest] = this.config.model.split("/");
|
|
231
|
+
const modelID = rest.join("/");
|
|
232
|
+
if (providerID && modelID) {
|
|
233
|
+
modelParam = { providerID, modelID };
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Subscribe to SSE events
|
|
238
|
+
const { stream } = await this.config.client.event.subscribe();
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
// Send prompt asynchronously
|
|
242
|
+
try {
|
|
243
|
+
await this.config.client.session.promptAsync({
|
|
244
|
+
path: { id: this.config.sessionId },
|
|
245
|
+
body: {
|
|
246
|
+
...(!this._firstQuerySent &&
|
|
247
|
+
this.config.systemPrompt && {
|
|
248
|
+
system: this.config.systemPrompt,
|
|
249
|
+
}),
|
|
250
|
+
...(modelParam && { model: modelParam }),
|
|
251
|
+
parts: [{ type: "text", text: prompt }],
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
} catch (err) {
|
|
255
|
+
yield {
|
|
256
|
+
type: "error",
|
|
257
|
+
error: `OpenCode rejected prompt: ${err instanceof Error ? err.message : String(err)}`,
|
|
258
|
+
code: "opencode_prompt_rejected",
|
|
259
|
+
};
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
this._firstQuerySent = true;
|
|
263
|
+
|
|
264
|
+
// Drain SSE events filtered by session ID
|
|
265
|
+
for await (const event of stream) {
|
|
266
|
+
const eventType = event.type as string;
|
|
267
|
+
const props = event.properties as Record<string, unknown> | undefined;
|
|
268
|
+
if (!props) continue;
|
|
269
|
+
|
|
270
|
+
// Filter: only events for our session
|
|
271
|
+
const eventSessionId =
|
|
272
|
+
(props.sessionID as string) ??
|
|
273
|
+
((props.info as Record<string, unknown>)?.sessionID as string) ??
|
|
274
|
+
((props.part as Record<string, unknown>)?.sessionID as string);
|
|
275
|
+
if (eventSessionId && eventSessionId !== this.config.sessionId) continue;
|
|
276
|
+
|
|
277
|
+
const mapped = mapOpenCodeEvent(eventType, props, this.id);
|
|
278
|
+
for (const msg of mapped) {
|
|
279
|
+
yield msg;
|
|
280
|
+
if (msg.type === "result" || (msg.type === "error" && isTerminalEvent(eventType))) {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
} finally {
|
|
286
|
+
stream.return?.();
|
|
287
|
+
}
|
|
288
|
+
} catch (err) {
|
|
289
|
+
yield {
|
|
290
|
+
type: "error",
|
|
291
|
+
error: err instanceof Error ? err.message : String(err),
|
|
292
|
+
code: "provider_error",
|
|
293
|
+
};
|
|
294
|
+
} finally {
|
|
295
|
+
this.endQuery(gen);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
abort(): void {
|
|
300
|
+
this.config.client.session
|
|
301
|
+
.abort({ path: { id: this.config.sessionId } })
|
|
302
|
+
.catch(() => {});
|
|
303
|
+
super.abort();
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
respondToPermission(
|
|
307
|
+
requestId: string,
|
|
308
|
+
allow: boolean,
|
|
309
|
+
_message?: string,
|
|
310
|
+
): void {
|
|
311
|
+
this.config.client
|
|
312
|
+
.postSessionIdPermissionsPermissionId({
|
|
313
|
+
path: { id: this.config.sessionId, permissionID: requestId },
|
|
314
|
+
body: { response: allow ? "once" : "reject" },
|
|
315
|
+
})
|
|
316
|
+
.catch(() => {});
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// ---------------------------------------------------------------------------
|
|
321
|
+
// Event mapping
|
|
322
|
+
// ---------------------------------------------------------------------------
|
|
323
|
+
|
|
324
|
+
/** Returns true for events that should terminate the query when mapped to an error. */
|
|
325
|
+
function isTerminalEvent(eventType: string): boolean {
|
|
326
|
+
return eventType === "session.error" || eventType === "session.status";
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Map an OpenCode SSE event to AIMessage[].
|
|
331
|
+
*
|
|
332
|
+
* Key events:
|
|
333
|
+
* message.part.delta → text_delta (streaming text)
|
|
334
|
+
* message.part.updated → tool_use / tool_result (tool lifecycle)
|
|
335
|
+
* permission.updated → permission_request
|
|
336
|
+
* session.status → result (when idle)
|
|
337
|
+
* message.updated → error (when message has error)
|
|
338
|
+
*/
|
|
339
|
+
export function mapOpenCodeEvent(
|
|
340
|
+
eventType: string,
|
|
341
|
+
props: Record<string, unknown>,
|
|
342
|
+
sessionId: string,
|
|
343
|
+
): AIMessage[] {
|
|
344
|
+
switch (eventType) {
|
|
345
|
+
case "message.part.delta": {
|
|
346
|
+
const field = props.field as string;
|
|
347
|
+
const delta = props.delta as string;
|
|
348
|
+
if (field === "text" && delta) {
|
|
349
|
+
return [{ type: "text_delta", delta }];
|
|
350
|
+
}
|
|
351
|
+
return [];
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
case "message.part.updated": {
|
|
355
|
+
const part = props.part as Record<string, unknown>;
|
|
356
|
+
if (!part) return [];
|
|
357
|
+
|
|
358
|
+
const partType = part.type as string;
|
|
359
|
+
|
|
360
|
+
if (partType === "tool") {
|
|
361
|
+
const state = part.state as Record<string, unknown>;
|
|
362
|
+
if (!state) return [];
|
|
363
|
+
|
|
364
|
+
const status = state.status as string;
|
|
365
|
+
const callID = (part.callID as string) ?? (part.id as string);
|
|
366
|
+
const toolName = part.tool as string;
|
|
367
|
+
|
|
368
|
+
switch (status) {
|
|
369
|
+
case "running":
|
|
370
|
+
return [
|
|
371
|
+
{
|
|
372
|
+
type: "tool_use",
|
|
373
|
+
toolName: toolName ?? "unknown",
|
|
374
|
+
toolInput: (state.input as Record<string, unknown>) ?? {},
|
|
375
|
+
toolUseId: callID,
|
|
376
|
+
},
|
|
377
|
+
];
|
|
378
|
+
|
|
379
|
+
case "completed": {
|
|
380
|
+
const output = (state.output as string) ?? "";
|
|
381
|
+
return [
|
|
382
|
+
{
|
|
383
|
+
type: "tool_result",
|
|
384
|
+
toolUseId: callID,
|
|
385
|
+
result: output,
|
|
386
|
+
},
|
|
387
|
+
];
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
case "error": {
|
|
391
|
+
const error = (state.error as string) ?? "Tool execution failed";
|
|
392
|
+
return [
|
|
393
|
+
{
|
|
394
|
+
type: "tool_result",
|
|
395
|
+
toolUseId: callID,
|
|
396
|
+
result: `[Error] ${error}`,
|
|
397
|
+
},
|
|
398
|
+
];
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
default:
|
|
402
|
+
return [];
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return [];
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
case "permission.updated": {
|
|
410
|
+
const id = props.id as string;
|
|
411
|
+
const permType = props.type as string;
|
|
412
|
+
const title = props.title as string;
|
|
413
|
+
const callID = props.callID as string;
|
|
414
|
+
const metadata = (props.metadata as Record<string, unknown>) ?? {};
|
|
415
|
+
|
|
416
|
+
return [
|
|
417
|
+
{
|
|
418
|
+
type: "permission_request",
|
|
419
|
+
requestId: id,
|
|
420
|
+
toolName: permType ?? "unknown",
|
|
421
|
+
toolInput: metadata,
|
|
422
|
+
title: title ?? permType,
|
|
423
|
+
toolUseId: callID ?? id,
|
|
424
|
+
},
|
|
425
|
+
];
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
case "session.status": {
|
|
429
|
+
const status = props.status as Record<string, unknown>;
|
|
430
|
+
if (status?.type === "idle") {
|
|
431
|
+
return [
|
|
432
|
+
{
|
|
433
|
+
type: "result",
|
|
434
|
+
sessionId,
|
|
435
|
+
success: true,
|
|
436
|
+
},
|
|
437
|
+
];
|
|
438
|
+
}
|
|
439
|
+
return [];
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
case "session.error": {
|
|
443
|
+
const error = props.error as Record<string, unknown>;
|
|
444
|
+
const message =
|
|
445
|
+
(error?.message as string) ?? (props.message as string) ?? "Session error";
|
|
446
|
+
return [
|
|
447
|
+
{
|
|
448
|
+
type: "error",
|
|
449
|
+
error: message,
|
|
450
|
+
code: "opencode_session_error",
|
|
451
|
+
},
|
|
452
|
+
];
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
case "message.updated": {
|
|
456
|
+
const info = props.info as Record<string, unknown>;
|
|
457
|
+
if (!info) return [];
|
|
458
|
+
|
|
459
|
+
const msgError = info.error as Record<string, unknown>;
|
|
460
|
+
if (msgError) {
|
|
461
|
+
const errorData = msgError.data as Record<string, unknown>;
|
|
462
|
+
const message =
|
|
463
|
+
(errorData?.message as string) ??
|
|
464
|
+
(msgError.name as string) ??
|
|
465
|
+
"Message error";
|
|
466
|
+
return [
|
|
467
|
+
{
|
|
468
|
+
type: "error",
|
|
469
|
+
error: message,
|
|
470
|
+
code: "opencode_message_error",
|
|
471
|
+
},
|
|
472
|
+
];
|
|
473
|
+
}
|
|
474
|
+
return [];
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
default:
|
|
478
|
+
return [];
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// ---------------------------------------------------------------------------
|
|
483
|
+
// Factory registration
|
|
484
|
+
// ---------------------------------------------------------------------------
|
|
485
|
+
|
|
486
|
+
import { registerProviderFactory } from "../provider.ts";
|
|
487
|
+
|
|
488
|
+
registerProviderFactory(
|
|
489
|
+
PROVIDER_NAME,
|
|
490
|
+
async (config) => new OpenCodeProvider(config as OpenCodeConfig),
|
|
491
|
+
);
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// @generated — DO NOT EDIT. Source: packages/ai/providers/pi-events.ts
|
|
2
|
+
/**
|
|
3
|
+
* Pi event mapping — shared between Bun and Node.js Pi providers.
|
|
4
|
+
*
|
|
5
|
+
* Pure function, no runtime-specific dependencies.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { AIMessage } from "../types.ts";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Map a Pi AgentEvent (received as JSONL) to AIMessage[].
|
|
12
|
+
*
|
|
13
|
+
* Pi event hierarchy:
|
|
14
|
+
* agent_start > turn_start > message_start > message_update* > message_end
|
|
15
|
+
* > tool_execution_start > tool_execution_end > turn_end > agent_end
|
|
16
|
+
*
|
|
17
|
+
* We extract:
|
|
18
|
+
* - text_delta from message_update.assistantMessageEvent
|
|
19
|
+
* - tool_use from toolcall_end
|
|
20
|
+
* - tool_result from tool_execution_end
|
|
21
|
+
* - result from agent_end
|
|
22
|
+
*/
|
|
23
|
+
export function mapPiEvent(
|
|
24
|
+
event: Record<string, unknown>,
|
|
25
|
+
sessionId: string,
|
|
26
|
+
): AIMessage[] {
|
|
27
|
+
const eventType = event.type as string;
|
|
28
|
+
|
|
29
|
+
switch (eventType) {
|
|
30
|
+
case "message_update": {
|
|
31
|
+
const ame = event.assistantMessageEvent as
|
|
32
|
+
| Record<string, unknown>
|
|
33
|
+
| undefined;
|
|
34
|
+
if (!ame) return [];
|
|
35
|
+
|
|
36
|
+
const subType = ame.type as string;
|
|
37
|
+
|
|
38
|
+
switch (subType) {
|
|
39
|
+
case "text_delta":
|
|
40
|
+
return [{ type: "text_delta", delta: ame.delta as string }];
|
|
41
|
+
|
|
42
|
+
case "toolcall_end": {
|
|
43
|
+
const tc = ame.toolCall as Record<string, unknown>;
|
|
44
|
+
if (!tc) return [];
|
|
45
|
+
return [
|
|
46
|
+
{
|
|
47
|
+
type: "tool_use",
|
|
48
|
+
toolName: tc.name as string,
|
|
49
|
+
toolInput: (tc.arguments as Record<string, unknown>) ?? {},
|
|
50
|
+
toolUseId: tc.id as string,
|
|
51
|
+
},
|
|
52
|
+
];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
case "error": {
|
|
56
|
+
const partial = ame.error as Record<string, unknown> | undefined;
|
|
57
|
+
const errorMessage =
|
|
58
|
+
(partial?.errorMessage as string) ?? "Stream error";
|
|
59
|
+
return [
|
|
60
|
+
{ type: "error", error: errorMessage, code: "pi_stream_error" },
|
|
61
|
+
];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
default:
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
case "tool_execution_end": {
|
|
70
|
+
const result = event.result;
|
|
71
|
+
const isError = event.isError as boolean;
|
|
72
|
+
const resultStr =
|
|
73
|
+
result == null
|
|
74
|
+
? ""
|
|
75
|
+
: typeof result === "string"
|
|
76
|
+
? result
|
|
77
|
+
: JSON.stringify(result);
|
|
78
|
+
|
|
79
|
+
return [
|
|
80
|
+
{
|
|
81
|
+
type: "tool_result",
|
|
82
|
+
toolUseId: event.toolCallId as string,
|
|
83
|
+
result: isError
|
|
84
|
+
? `[Error] ${resultStr || "Tool execution failed"}`
|
|
85
|
+
: resultStr,
|
|
86
|
+
},
|
|
87
|
+
];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
case "agent_end":
|
|
91
|
+
return [
|
|
92
|
+
{
|
|
93
|
+
type: "result",
|
|
94
|
+
sessionId,
|
|
95
|
+
success: true,
|
|
96
|
+
},
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
case "process_exited":
|
|
100
|
+
return [
|
|
101
|
+
{
|
|
102
|
+
type: "error",
|
|
103
|
+
error: "Pi process exited unexpectedly.",
|
|
104
|
+
code: "pi_process_exit",
|
|
105
|
+
},
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
default:
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
}
|