@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,430 @@
|
|
|
1
|
+
// @generated — DO NOT EDIT. Source: packages/ai/providers/codex-sdk.ts
|
|
2
|
+
/**
|
|
3
|
+
* Codex SDK provider — bridges Plannotator's AI layer with OpenAI's Codex agent.
|
|
4
|
+
*
|
|
5
|
+
* Uses @openai/codex-sdk to create sessions that can:
|
|
6
|
+
* - Start fresh with Plannotator context as the system prompt
|
|
7
|
+
* - Fake-fork from a parent session (fresh thread + preamble, no real history)
|
|
8
|
+
* - Resume a previous thread by ID
|
|
9
|
+
* - Stream text deltas back to the UI in real time
|
|
10
|
+
*
|
|
11
|
+
* Sessions default to read-only sandbox mode for safety in inline chat.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { buildSystemPrompt, buildEffectivePrompt } from "../context.ts";
|
|
15
|
+
import { BaseSession } from "../base-session.ts";
|
|
16
|
+
import type {
|
|
17
|
+
AIProvider,
|
|
18
|
+
AIProviderCapabilities,
|
|
19
|
+
AISession,
|
|
20
|
+
AIMessage,
|
|
21
|
+
CreateSessionOptions,
|
|
22
|
+
CodexSDKConfig,
|
|
23
|
+
} from "../types.ts";
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Constants
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
const PROVIDER_NAME = "codex-sdk";
|
|
30
|
+
const DEFAULT_MODEL = "gpt-5.4";
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Provider
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
export class CodexSDKProvider implements AIProvider {
|
|
37
|
+
readonly name = PROVIDER_NAME;
|
|
38
|
+
readonly capabilities: AIProviderCapabilities = {
|
|
39
|
+
fork: false, // No real fork — faked with fresh thread + preamble
|
|
40
|
+
resume: true,
|
|
41
|
+
streaming: true,
|
|
42
|
+
tools: true,
|
|
43
|
+
};
|
|
44
|
+
readonly models = [
|
|
45
|
+
{ id: 'gpt-5.4', label: 'GPT-5.4', default: true },
|
|
46
|
+
{ id: 'gpt-5.4-mini', label: 'GPT-5.4 Mini' },
|
|
47
|
+
{ id: 'gpt-5.3-codex', label: 'GPT-5.3 Codex' },
|
|
48
|
+
{ id: 'gpt-5.3-codex-spark', label: 'GPT-5.3 Codex Spark' },
|
|
49
|
+
{ id: 'gpt-5.2-codex', label: 'GPT-5.2 Codex' },
|
|
50
|
+
{ id: 'gpt-5.2', label: 'GPT-5.2' },
|
|
51
|
+
] as const;
|
|
52
|
+
|
|
53
|
+
private config: CodexSDKConfig;
|
|
54
|
+
|
|
55
|
+
constructor(config: CodexSDKConfig) {
|
|
56
|
+
this.config = config;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async createSession(options: CreateSessionOptions): Promise<AISession> {
|
|
60
|
+
return new CodexSDKSession({
|
|
61
|
+
...this.baseConfig(options),
|
|
62
|
+
systemPrompt: buildSystemPrompt(options.context),
|
|
63
|
+
cwd: options.cwd ?? this.config.cwd ?? process.cwd(),
|
|
64
|
+
parentSessionId: null,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async forkSession(_options: CreateSessionOptions): Promise<AISession> {
|
|
69
|
+
throw new Error(
|
|
70
|
+
"Codex does not support session forking. " +
|
|
71
|
+
"The endpoint layer should fall back to createSession()."
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async resumeSession(sessionId: string): Promise<AISession> {
|
|
76
|
+
return new CodexSDKSession({
|
|
77
|
+
...this.baseConfig(),
|
|
78
|
+
systemPrompt: null,
|
|
79
|
+
cwd: this.config.cwd ?? process.cwd(),
|
|
80
|
+
parentSessionId: null,
|
|
81
|
+
resumeThreadId: sessionId,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
dispose(): void {
|
|
86
|
+
// No persistent resources to clean up
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private baseConfig(options?: CreateSessionOptions) {
|
|
90
|
+
return {
|
|
91
|
+
model: options?.model ?? this.config.model ?? DEFAULT_MODEL,
|
|
92
|
+
maxTurns: options?.maxTurns ?? 99,
|
|
93
|
+
sandboxMode: this.config.sandboxMode ?? "read-only" as const,
|
|
94
|
+
codexExecutablePath: this.config.codexExecutablePath,
|
|
95
|
+
reasoningEffort: options?.reasoningEffort,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// SDK import cache — resolve once, reuse across all sessions
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
// biome-ignore lint/suspicious/noExplicitAny: SDK type not available at compile time
|
|
105
|
+
let CodexClass: any = null;
|
|
106
|
+
|
|
107
|
+
async function getCodexClass() {
|
|
108
|
+
if (!CodexClass) {
|
|
109
|
+
// biome-ignore lint/suspicious/noExplicitAny: SDK exports vary between versions
|
|
110
|
+
const mod = await import("@openai/codex-sdk") as any;
|
|
111
|
+
CodexClass = mod.default ?? mod.Codex;
|
|
112
|
+
}
|
|
113
|
+
return CodexClass;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
// Session
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
interface SessionConfig {
|
|
121
|
+
systemPrompt: string | null;
|
|
122
|
+
model: string;
|
|
123
|
+
maxTurns: number;
|
|
124
|
+
sandboxMode: "read-only" | "workspace-write" | "danger-full-access";
|
|
125
|
+
cwd: string;
|
|
126
|
+
parentSessionId: string | null;
|
|
127
|
+
resumeThreadId?: string;
|
|
128
|
+
codexExecutablePath?: string;
|
|
129
|
+
reasoningEffort?: "minimal" | "low" | "medium" | "high" | "xhigh";
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
class CodexSDKSession extends BaseSession {
|
|
133
|
+
private config: SessionConfig;
|
|
134
|
+
// biome-ignore lint/suspicious/noExplicitAny: SDK types not available at compile time
|
|
135
|
+
private _codexInstance: any = null;
|
|
136
|
+
// biome-ignore lint/suspicious/noExplicitAny: SDK types not available at compile time
|
|
137
|
+
private _thread: any = null;
|
|
138
|
+
/** Tracks cumulative text length per item for delta extraction. */
|
|
139
|
+
private _itemTextOffsets = new Map<string, number>();
|
|
140
|
+
|
|
141
|
+
constructor(config: SessionConfig) {
|
|
142
|
+
super({
|
|
143
|
+
parentSessionId: config.parentSessionId,
|
|
144
|
+
initialId: config.resumeThreadId,
|
|
145
|
+
});
|
|
146
|
+
this.config = config;
|
|
147
|
+
// If resuming, treat the thread ID as already resolved
|
|
148
|
+
if (config.resumeThreadId) {
|
|
149
|
+
this._resolvedId = config.resumeThreadId;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async *query(prompt: string): AsyncIterable<AIMessage> {
|
|
154
|
+
const started = this.startQuery();
|
|
155
|
+
if (!started) { yield BaseSession.BUSY_ERROR; return; }
|
|
156
|
+
const { gen, signal } = started;
|
|
157
|
+
|
|
158
|
+
this._itemTextOffsets.clear();
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
const Codex = await getCodexClass();
|
|
162
|
+
|
|
163
|
+
// Lazy-create the Codex instance
|
|
164
|
+
if (!this._codexInstance) {
|
|
165
|
+
this._codexInstance = new Codex({
|
|
166
|
+
...(this.config.codexExecutablePath && { codexPathOverride: this.config.codexExecutablePath }),
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Lazy-create or resume the thread
|
|
171
|
+
if (!this._thread) {
|
|
172
|
+
if (this.config.resumeThreadId) {
|
|
173
|
+
this._thread = this._codexInstance.resumeThread(this.config.resumeThreadId, {
|
|
174
|
+
model: this.config.model,
|
|
175
|
+
workingDirectory: this.config.cwd,
|
|
176
|
+
sandboxMode: this.config.sandboxMode,
|
|
177
|
+
...(this.config.reasoningEffort && { modelReasoningEffort: this.config.reasoningEffort }),
|
|
178
|
+
});
|
|
179
|
+
} else {
|
|
180
|
+
this._thread = this._codexInstance.startThread({
|
|
181
|
+
model: this.config.model,
|
|
182
|
+
workingDirectory: this.config.cwd,
|
|
183
|
+
sandboxMode: this.config.sandboxMode,
|
|
184
|
+
...(this.config.reasoningEffort && { modelReasoningEffort: this.config.reasoningEffort }),
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const effectivePrompt = buildEffectivePrompt(
|
|
190
|
+
prompt,
|
|
191
|
+
this.config.systemPrompt,
|
|
192
|
+
this._firstQuerySent,
|
|
193
|
+
);
|
|
194
|
+
const streamed = await this._thread.runStreamed(effectivePrompt, {
|
|
195
|
+
signal,
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
this._firstQuerySent = true;
|
|
199
|
+
let turnFailed = false;
|
|
200
|
+
|
|
201
|
+
for await (const event of streamed.events) {
|
|
202
|
+
// ID resolution from thread.started
|
|
203
|
+
if (
|
|
204
|
+
!this._resolvedId &&
|
|
205
|
+
event.type === "thread.started" &&
|
|
206
|
+
typeof event.thread_id === "string"
|
|
207
|
+
) {
|
|
208
|
+
this.resolveId(event.thread_id);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (event.type === "turn.failed") {
|
|
212
|
+
turnFailed = true;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const mapped = mapCodexEvent(event, this._itemTextOffsets);
|
|
216
|
+
for (const msg of mapped) {
|
|
217
|
+
yield msg;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Emit synthetic result after stream ends
|
|
222
|
+
if (!turnFailed) {
|
|
223
|
+
yield {
|
|
224
|
+
type: "result",
|
|
225
|
+
sessionId: this.id,
|
|
226
|
+
success: true,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
} catch (err) {
|
|
230
|
+
yield {
|
|
231
|
+
type: "error",
|
|
232
|
+
error: err instanceof Error ? err.message : String(err),
|
|
233
|
+
code: "provider_error",
|
|
234
|
+
};
|
|
235
|
+
} finally {
|
|
236
|
+
this.endQuery(gen);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ---------------------------------------------------------------------------
|
|
243
|
+
// Event mapping
|
|
244
|
+
// ---------------------------------------------------------------------------
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Map a Codex SDK ThreadEvent to one or more AIMessages.
|
|
248
|
+
*
|
|
249
|
+
* The itemTextOffsets map tracks cumulative text length per item ID
|
|
250
|
+
* so we can extract true deltas from the cumulative text in item.updated events.
|
|
251
|
+
*/
|
|
252
|
+
function mapCodexEvent(
|
|
253
|
+
event: Record<string, unknown>,
|
|
254
|
+
itemTextOffsets: Map<string, number>,
|
|
255
|
+
): AIMessage[] {
|
|
256
|
+
const eventType = event.type as string;
|
|
257
|
+
|
|
258
|
+
switch (eventType) {
|
|
259
|
+
case "thread.started":
|
|
260
|
+
case "turn.started":
|
|
261
|
+
return [];
|
|
262
|
+
|
|
263
|
+
case "turn.completed":
|
|
264
|
+
return [];
|
|
265
|
+
|
|
266
|
+
case "turn.failed": {
|
|
267
|
+
const error = event.error as Record<string, unknown> | undefined;
|
|
268
|
+
return [{
|
|
269
|
+
type: "error",
|
|
270
|
+
error: (error?.message as string) ?? "Turn failed",
|
|
271
|
+
code: "turn_failed",
|
|
272
|
+
}];
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
case "error":
|
|
276
|
+
return [{
|
|
277
|
+
type: "error",
|
|
278
|
+
error: (event.message as string) ?? "Unknown error",
|
|
279
|
+
code: "codex_error",
|
|
280
|
+
}];
|
|
281
|
+
|
|
282
|
+
case "item.started":
|
|
283
|
+
case "item.updated":
|
|
284
|
+
case "item.completed":
|
|
285
|
+
return mapCodexItem(event, itemTextOffsets);
|
|
286
|
+
|
|
287
|
+
default:
|
|
288
|
+
return [{ type: "unknown", raw: event }];
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Map item-level events to AIMessages.
|
|
294
|
+
*/
|
|
295
|
+
function mapCodexItem(
|
|
296
|
+
event: Record<string, unknown>,
|
|
297
|
+
itemTextOffsets: Map<string, number>,
|
|
298
|
+
): AIMessage[] {
|
|
299
|
+
const item = event.item as Record<string, unknown>;
|
|
300
|
+
if (!item) return [{ type: "unknown", raw: event }];
|
|
301
|
+
|
|
302
|
+
const eventType = event.type as string;
|
|
303
|
+
const itemType = item.type as string;
|
|
304
|
+
const itemId = (item.id as string) ?? "";
|
|
305
|
+
const isStarted = eventType === "item.started";
|
|
306
|
+
const isCompleted = eventType === "item.completed";
|
|
307
|
+
|
|
308
|
+
switch (itemType) {
|
|
309
|
+
case "agent_message": {
|
|
310
|
+
const text = (item.text as string) ?? "";
|
|
311
|
+
|
|
312
|
+
if (isStarted) {
|
|
313
|
+
// Reset offset tracking for this item
|
|
314
|
+
itemTextOffsets.set(itemId, 0);
|
|
315
|
+
return [];
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (isCompleted) {
|
|
319
|
+
// Emit final complete text
|
|
320
|
+
itemTextOffsets.delete(itemId);
|
|
321
|
+
return text ? [{ type: "text", text }] : [];
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// item.updated — extract delta from cumulative text
|
|
325
|
+
const prevOffset = itemTextOffsets.get(itemId) ?? 0;
|
|
326
|
+
if (text.length > prevOffset) {
|
|
327
|
+
const delta = text.slice(prevOffset);
|
|
328
|
+
itemTextOffsets.set(itemId, text.length);
|
|
329
|
+
return [{ type: "text_delta", delta }];
|
|
330
|
+
}
|
|
331
|
+
return [];
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
case "command_execution": {
|
|
335
|
+
const messages: AIMessage[] = [];
|
|
336
|
+
if (isStarted) {
|
|
337
|
+
messages.push({
|
|
338
|
+
type: "tool_use",
|
|
339
|
+
toolName: "Bash",
|
|
340
|
+
toolInput: { command: item.command as string },
|
|
341
|
+
toolUseId: itemId,
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
if (isCompleted) {
|
|
345
|
+
const output = (item.aggregated_output as string) ?? "";
|
|
346
|
+
const exitCode = item.exit_code as number | undefined;
|
|
347
|
+
messages.push({
|
|
348
|
+
type: "tool_result",
|
|
349
|
+
toolUseId: itemId,
|
|
350
|
+
result: exitCode != null ? `${output}\n[exit code: ${exitCode}]` : output,
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
return messages;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
case "file_change": {
|
|
357
|
+
const changes = item.changes as Array<{ path: string; kind: string }> | undefined;
|
|
358
|
+
if (isStarted || isCompleted) {
|
|
359
|
+
return [{
|
|
360
|
+
type: "tool_use",
|
|
361
|
+
toolName: "FileChange",
|
|
362
|
+
toolInput: { changes: changes ?? [] },
|
|
363
|
+
toolUseId: itemId,
|
|
364
|
+
}];
|
|
365
|
+
}
|
|
366
|
+
return [];
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
case "mcp_tool_call": {
|
|
370
|
+
const messages: AIMessage[] = [];
|
|
371
|
+
if (isStarted) {
|
|
372
|
+
messages.push({
|
|
373
|
+
type: "tool_use",
|
|
374
|
+
toolName: `${item.server as string}/${item.tool as string}`,
|
|
375
|
+
toolInput: (item.arguments as Record<string, unknown>) ?? {},
|
|
376
|
+
toolUseId: itemId,
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
if (isCompleted) {
|
|
380
|
+
if (item.result != null) {
|
|
381
|
+
messages.push({
|
|
382
|
+
type: "tool_result",
|
|
383
|
+
toolUseId: itemId,
|
|
384
|
+
result: typeof item.result === "string" ? item.result : JSON.stringify(item.result),
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
if (item.error) {
|
|
388
|
+
const err = item.error as Record<string, unknown>;
|
|
389
|
+
messages.push({
|
|
390
|
+
type: "error",
|
|
391
|
+
error: (err.message as string) ?? "MCP tool call failed",
|
|
392
|
+
code: "mcp_error",
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return messages;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
case "error":
|
|
400
|
+
return [{
|
|
401
|
+
type: "error",
|
|
402
|
+
error: (item.message as string) ?? "Unknown error",
|
|
403
|
+
}];
|
|
404
|
+
|
|
405
|
+
case "reasoning":
|
|
406
|
+
case "web_search":
|
|
407
|
+
case "todo_list":
|
|
408
|
+
return [{ type: "unknown", raw: { eventType, item } }];
|
|
409
|
+
|
|
410
|
+
default:
|
|
411
|
+
return [{ type: "unknown", raw: { eventType, item } }];
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// ---------------------------------------------------------------------------
|
|
416
|
+
// Exported for testing
|
|
417
|
+
// ---------------------------------------------------------------------------
|
|
418
|
+
|
|
419
|
+
export { mapCodexEvent, mapCodexItem };
|
|
420
|
+
|
|
421
|
+
// ---------------------------------------------------------------------------
|
|
422
|
+
// Factory registration
|
|
423
|
+
// ---------------------------------------------------------------------------
|
|
424
|
+
|
|
425
|
+
import { registerProviderFactory } from "../provider.ts";
|
|
426
|
+
|
|
427
|
+
registerProviderFactory(
|
|
428
|
+
PROVIDER_NAME,
|
|
429
|
+
async (config) => new CodexSDKProvider(config as CodexSDKConfig)
|
|
430
|
+
);
|