@ian-pascoe/pi-codemode 0.1.0

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/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export { default } from "./pi-codemode-extension.js";
@@ -0,0 +1,157 @@
1
+ import type { AgentTool } from "@earendil-works/pi-agent-core";
2
+ import { AgentSession, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
3
+ import { Type } from "typebox";
4
+ import { Value } from "typebox/value";
5
+
6
+ type PiAgentSessionPrivateFields = {
7
+ readonly _toolRegistry: unknown;
8
+ };
9
+
10
+ const PiCallableSchema = Type.Function([], Type.Unknown());
11
+ const PiToolWrapperSchema = Type.Object(
12
+ {
13
+ name: Type.String(),
14
+ label: Type.String(),
15
+ description: Type.String(),
16
+ parameters: Type.Object({}, { additionalProperties: true }),
17
+ execute: PiCallableSchema,
18
+ prepareArguments: Type.Optional(PiCallableSchema),
19
+ executionMode: Type.Optional(
20
+ Type.Union([Type.Literal("parallel"), Type.Literal("sequential")]),
21
+ ),
22
+ },
23
+ { additionalProperties: true },
24
+ );
25
+ const PiToolRegistryEntrySchema = Type.Tuple([Type.String(), PiToolWrapperSchema]);
26
+ const PiAgentSessionCapabilitiesSchema = Type.Object(
27
+ {
28
+ getActiveToolNames: PiCallableSchema,
29
+ setActiveToolsByName: PiCallableSchema,
30
+ settingsManager: Type.Object(
31
+ {
32
+ getGlobalSettings: PiCallableSchema,
33
+ getProjectSettings: PiCallableSchema,
34
+ isProjectTrusted: PiCallableSchema,
35
+ },
36
+ { additionalProperties: true },
37
+ ),
38
+ agent: Type.Object(
39
+ {
40
+ beforeToolCall: PiCallableSchema,
41
+ afterToolCall: PiCallableSchema,
42
+ },
43
+ { additionalProperties: true },
44
+ ),
45
+ },
46
+ { additionalProperties: true },
47
+ );
48
+
49
+ /** Capabilities proven against the pinned Pi AgentSession before CodeMode changes tool exposure. */
50
+ export interface CapturedPiAgentSession {
51
+ readonly agent: AgentSession["agent"];
52
+ readonly session: AgentSession;
53
+ readonly settingsManager: AgentSession["settingsManager"];
54
+ /** Reads Pi's replaceable exact wrapped-tool registry fresh on every call. */
55
+ getToolRegistry(): ReadonlyMap<string, AgentTool>;
56
+ }
57
+
58
+ /** Expected capture or version-capability failure at Pi session startup. */
59
+ export type CapturePiAgentSessionResult =
60
+ | { readonly ok: true; readonly capabilities: CapturedPiAgentSession }
61
+ | { readonly ok: false; readonly warning: string };
62
+
63
+ function piAgentSessionPrivateFields(session: AgentSession): PiAgentSessionPrivateFields {
64
+ const sessionObject: object = session;
65
+ // SAFETY: AgentSession identity and every consumed private field are runtime-gated in this sole compatibility boundary.
66
+ return sessionObject as PiAgentSessionPrivateFields;
67
+ }
68
+
69
+ function hasCallableSessionCapabilities(session: AgentSession): boolean {
70
+ return Value.Check(PiAgentSessionCapabilitiesSchema, session);
71
+ }
72
+
73
+ function isExecutablePiToolRegistry(
74
+ // oxlint-disable-next-line anti-slop/no-unknown-parameters -- SAFETY: This private Pi compatibility parser validates the version-pinned registry before exposing its wrapped handlers; pi-tool-bridge.test.ts covers capability loss.
75
+ value: unknown,
76
+ ): value is ReadonlyMap<string, AgentTool> {
77
+ if (!(value instanceof Map)) return false;
78
+ try {
79
+ for (const entry of value) {
80
+ if (!Value.Check(PiToolRegistryEntrySchema, entry) || entry[1].name !== entry[0]) {
81
+ return false;
82
+ }
83
+ }
84
+ return true;
85
+ } catch {
86
+ return false;
87
+ }
88
+ }
89
+
90
+ function captureFailure(message: string): CapturePiAgentSessionResult {
91
+ return {
92
+ ok: false,
93
+ warning: `Pi CodeMode disabled: ${message}`,
94
+ };
95
+ }
96
+
97
+ /** Captures the owning AgentSession through Pi's synchronous getAllTools delegation and restores its exact descriptor. */
98
+ export function capturePiAgentSession(
99
+ pi: Pick<ExtensionAPI, "getAllTools">,
100
+ ): CapturePiAgentSessionResult {
101
+ const prototype = AgentSession.prototype;
102
+ const descriptor = Object.getOwnPropertyDescriptor(prototype, "getAllTools");
103
+ if (descriptor === undefined || !Value.Check(PiCallableSchema, descriptor.value)) {
104
+ return captureFailure("AgentSession.getAllTools is not the tested data method");
105
+ }
106
+
107
+ const originalGetAllTools = descriptor.value;
108
+ let capturedSession: AgentSession | undefined;
109
+ Object.defineProperty(prototype, "getAllTools", {
110
+ ...descriptor,
111
+ value(this: AgentSession) {
112
+ // oxlint-disable-next-line typescript/no-this-alias -- SAFETY: Capturing the exact synchronous receiver is the approved transient AgentSession discovery mechanism; pi-tool-bridge.test.ts verifies descriptor restoration.
113
+ capturedSession = this;
114
+ return originalGetAllTools.call(this);
115
+ },
116
+ });
117
+
118
+ try {
119
+ pi.getAllTools();
120
+ } catch (cause) {
121
+ return captureFailure(
122
+ `getAllTools capture failed: ${cause instanceof Error ? cause.message : String(cause)}`,
123
+ );
124
+ } finally {
125
+ Object.defineProperty(prototype, "getAllTools", descriptor);
126
+ }
127
+
128
+ if (!(capturedSession instanceof AgentSession)) {
129
+ return captureFailure("getAllTools did not delegate to an AgentSession");
130
+ }
131
+ if (!hasCallableSessionCapabilities(capturedSession)) {
132
+ return captureFailure("AgentSession does not expose the pinned public capabilities");
133
+ }
134
+
135
+ const privateFields = piAgentSessionPrivateFields(capturedSession);
136
+ if (!isExecutablePiToolRegistry(privateFields._toolRegistry)) {
137
+ return captureFailure("AgentSession._toolRegistry is not the pinned executable wrapper map");
138
+ }
139
+
140
+ return {
141
+ ok: true,
142
+ capabilities: {
143
+ agent: capturedSession.agent,
144
+ session: capturedSession,
145
+ settingsManager: capturedSession.settingsManager,
146
+ getToolRegistry() {
147
+ const currentRegistry = privateFields._toolRegistry;
148
+ if (!isExecutablePiToolRegistry(currentRegistry)) {
149
+ throw new Error(
150
+ "Pi CodeMode capability lost: AgentSession._toolRegistry is no longer an executable wrapper map",
151
+ );
152
+ }
153
+ return currentRegistry;
154
+ },
155
+ },
156
+ };
157
+ }
@@ -0,0 +1,469 @@
1
+ import type { AgentToolResult } from "@earendil-works/pi-agent-core";
2
+ import type { AssistantMessage } from "@earendil-works/pi-ai";
3
+ import type {
4
+ ExtensionAPI,
5
+ ExtensionContext,
6
+ ExtensionFactory,
7
+ } from "@earendil-works/pi-coding-agent";
8
+ import { renderCodeModeToolCatalogue } from "./codemode-tool-catalog.js";
9
+ import { CodeModeObserverUiController } from "./codemode-observer-ui.js";
10
+ import {
11
+ CodeModeSessionCoordinator,
12
+ type CodeModeNestedToolBatch,
13
+ type CodeModeNestedToolBatchResult,
14
+ type CodeModeNestedToolResult,
15
+ } from "./codemode-session-coordinator.js";
16
+ import { CODEMODE_SYSTEM_RUNTIME } from "./codemode-runtime.js";
17
+ import { createCodeModeSessionFiles, type CodeModeSessionFiles } from "./codemode-session-files.js";
18
+ import {
19
+ createCodeModeFailure,
20
+ createCodeModePending,
21
+ isCodeModeJsonObject,
22
+ type CodeModeJsonValue,
23
+ type CodeModeResultDetails,
24
+ type CodeModeToolOperations,
25
+ } from "./codemode-tool-contract.js";
26
+ import { createRenderedCodeModeToolDefinitions } from "./codemode-tool-rendering.js";
27
+ import {
28
+ decideCodeModeToolExposure,
29
+ installCodeModeToolExposure,
30
+ type CodeModeToolExposureDecision,
31
+ type InstalledCodeModeToolExposure,
32
+ } from "./codemode-tool-exposure.js";
33
+ import { capturePiAgentSession, type CapturedPiAgentSession } from "./pi-agent-session-capture.js";
34
+ import { resolveCodeModeSettings } from "./pi-codemode-settings.js";
35
+ import {
36
+ executePiToolBridgeBatch,
37
+ type PiToolBridgeCall,
38
+ type PiToolBridgeValue,
39
+ } from "./pi-tool-bridge.js";
40
+
41
+ const CODEMODE_EXECUTE_DESCRIPTION =
42
+ "Execute a TypeScript Cell in a persistent isolated Deno CodeMode Session. Top-level declarations become Notebook Bindings. Use the read-only tools object for registered Pi tools.";
43
+
44
+ type PiCodeModeGeneration = {
45
+ readonly captured: CapturedPiAgentSession;
46
+ readonly context: ExtensionContext;
47
+ readonly coordinator: CodeModeSessionCoordinator;
48
+ readonly observer: CodeModeObserverUiController;
49
+ readonly sessionFiles: CodeModeSessionFiles;
50
+ readonly operations: CodeModeToolOperations;
51
+ exposure?: InstalledCodeModeToolExposure;
52
+ decision: CodeModeToolExposureDecision;
53
+ executeDescription: string;
54
+ active: boolean;
55
+ toolsRegistered: boolean;
56
+ synchronizing: boolean;
57
+ synchronizationPending: boolean;
58
+ catalogueWarningShown: boolean;
59
+ };
60
+
61
+ function catalogueDescription(catalogue: string): string {
62
+ return `${CODEMODE_EXECUTE_DESCRIPTION}\n\nCurrent CodeMode tool declarations:\n\n\`\`\`ts\n${catalogue}\`\`\``;
63
+ }
64
+
65
+ function renderGenerationCatalogue(
66
+ captured: CapturedPiAgentSession,
67
+ decision: CodeModeToolExposureDecision,
68
+ ) {
69
+ const registry = captured.getToolRegistry();
70
+ return renderCodeModeToolCatalogue(
71
+ decision.codeModeNames.flatMap((name) => {
72
+ const tool = registry.get(name);
73
+ return tool === undefined
74
+ ? []
75
+ : [{ name, description: tool.description, inputSchema: tool.parameters }];
76
+ }),
77
+ );
78
+ }
79
+
80
+ function latestCodeModeAssistantMessage(
81
+ captured: CapturedPiAgentSession,
82
+ ): AssistantMessage | undefined {
83
+ for (const message of captured.agent.state.messages.toReversed()) {
84
+ if (
85
+ message.role === "assistant" &&
86
+ message.content.some(
87
+ (content) =>
88
+ content.type === "toolCall" &&
89
+ (content.name === "codemode_execute" ||
90
+ content.name === "codemode_result" ||
91
+ content.name === "codemode_cancel"),
92
+ )
93
+ ) {
94
+ return message;
95
+ }
96
+ }
97
+ return undefined;
98
+ }
99
+
100
+ function unavailableNestedResult(
101
+ callId: string,
102
+ code: string,
103
+ message: string,
104
+ ): CodeModeNestedToolResult {
105
+ return { callId, outcome: "error", error: { code, message } };
106
+ }
107
+
108
+ function codeModeNestedBridgeValue(value: PiToolBridgeValue): CodeModeJsonValue {
109
+ const content: CodeModeJsonValue[] = value.content.map((entry) =>
110
+ entry.type === "text"
111
+ ? { type: "text", text: entry.text }
112
+ : { type: "image", data: entry.data, mimeType: entry.mimeType },
113
+ );
114
+ return value.details === undefined ? { content } : { content, details: value.details };
115
+ }
116
+
117
+ /** Owns Pi CodeMode startup, exposure/catalogue synchronization, and resource shutdown. */
118
+ class PiCodeModeLifecycleController {
119
+ private generation: PiCodeModeGeneration | undefined;
120
+
121
+ /** Creates inert lifecycle wiring around one Pi extension registration interface. */
122
+ constructor(private readonly pi: ExtensionAPI) {}
123
+
124
+ /** Registers inert lifecycle handlers; no process or public tool exists before session start. */
125
+ register(): void {
126
+ this.pi.on("session_start", async (_event, context) => this.startSession(context));
127
+ this.pi.on("before_agent_start", () => this.synchronizeCurrentGeneration());
128
+ this.pi.on("tool_execution_end", () => this.synchronizeCurrentGeneration());
129
+ this.pi.on("session_shutdown", async (event) => this.shutdownSession(event.reason));
130
+ }
131
+
132
+ private async startSession(context: ExtensionContext): Promise<void> {
133
+ await this.shutdownSession("replacement");
134
+ const capturedResult = capturePiAgentSession(this.pi);
135
+ if (!capturedResult.ok) {
136
+ this.notifyWarning(context, capturedResult.warning);
137
+ return;
138
+ }
139
+ const captured = capturedResult.capabilities;
140
+ const settings = resolveCodeModeSettings(captured.settingsManager);
141
+ if (!settings.enabled) {
142
+ this.notifyWarning(context, `Pi CodeMode disabled: ${settings.warning}`);
143
+ return;
144
+ }
145
+
146
+ const registryNames = [...captured.getToolRegistry().keys()];
147
+ const initialDecision = decideCodeModeToolExposure(
148
+ registryNames,
149
+ captured.session.getActiveToolNames(),
150
+ settings.rules,
151
+ );
152
+ const initialCatalogue = renderGenerationCatalogue(captured, initialDecision);
153
+ if (!initialCatalogue.ok) {
154
+ this.notifyWarning(
155
+ context,
156
+ "Pi CodeMode disabled: registered tool names exceed the 1 MiB catalogue limit",
157
+ );
158
+ return;
159
+ }
160
+
161
+ let sessionFiles: CodeModeSessionFiles;
162
+ try {
163
+ sessionFiles = await createCodeModeSessionFiles(context.sessionManager.getSessionDir());
164
+ } catch (cause) {
165
+ this.notifyWarning(
166
+ context,
167
+ `Pi CodeMode disabled: ${cause instanceof Error ? cause.message : String(cause)}`,
168
+ );
169
+ return;
170
+ }
171
+
172
+ let generation: PiCodeModeGeneration;
173
+ const observer = new CodeModeObserverUiController(context, CODEMODE_SYSTEM_RUNTIME);
174
+ const coordinator = new CodeModeSessionCoordinator({
175
+ maxSessions: settings.maxSessions,
176
+ runtime: CODEMODE_SYSTEM_RUNTIME,
177
+ resultSpillWriter: sessionFiles,
178
+ onSnapshotChange: (snapshot) => observer.onSnapshotChange(snapshot),
179
+ onUnexpectedFailure: (failure) => observer.onUnexpectedFailure(failure),
180
+ getToolNames: () =>
181
+ generation.active && this.generation === generation
182
+ ? generation.decision.codeModeNames
183
+ : [],
184
+ executeToolBatch: (batch) => this.executeNestedToolBatch(generation, batch),
185
+ });
186
+ const operations: CodeModeToolOperations = {
187
+ execute: async (input, signal, onUpdate) => {
188
+ if (!generation.active || this.generation !== generation) {
189
+ return {
190
+ result: createCodeModeFailure(
191
+ input.sessionId ?? "inactive",
192
+ "runtime",
193
+ "Pi CodeMode session generation is inactive",
194
+ ),
195
+ };
196
+ }
197
+ return coordinator.execute(
198
+ input,
199
+ signal,
200
+ onUpdate === undefined
201
+ ? undefined
202
+ : (update) => {
203
+ // SAFETY: executeNestedToolBatch is the only update producer and replaces nested details with a schema-valid CodeMode pending result.
204
+ onUpdate(update as AgentToolResult<CodeModeResultDetails>);
205
+ },
206
+ );
207
+ },
208
+ result: async (input) => coordinator.result(input.sessionId),
209
+ cancel: async (input) => coordinator.cancel(input.sessionId),
210
+ };
211
+ generation = {
212
+ captured,
213
+ context,
214
+ coordinator,
215
+ observer,
216
+ sessionFiles,
217
+ operations,
218
+ decision: initialDecision,
219
+ executeDescription: catalogueDescription(initialCatalogue.text),
220
+ active: true,
221
+ toolsRegistered: false,
222
+ synchronizing: false,
223
+ synchronizationPending: false,
224
+ catalogueWarningShown: false,
225
+ };
226
+ this.generation = generation;
227
+
228
+ try {
229
+ generation.exposure = installCodeModeToolExposure(
230
+ captured.session,
231
+ () => captured.getToolRegistry().keys(),
232
+ settings.rules,
233
+ (decision) => {
234
+ generation.decision = decision;
235
+ this.synchronizeGeneration(generation);
236
+ },
237
+ (decision) => this.acceptExposureDecision(generation, decision),
238
+ );
239
+ } catch (cause) {
240
+ generation.active = false;
241
+ try {
242
+ observer.dispose();
243
+ } catch {
244
+ // Observer cleanup is presentation-only; execution resources still require release.
245
+ }
246
+ await coordinator.shutdown("startup failure");
247
+ await sessionFiles.close();
248
+ if (this.generation === generation) this.generation = undefined;
249
+ this.notifyWarning(
250
+ context,
251
+ `Pi CodeMode disabled: ${cause instanceof Error ? cause.message : String(cause)}`,
252
+ );
253
+ return;
254
+ }
255
+
256
+ for (const definition of createRenderedCodeModeToolDefinitions(
257
+ operations,
258
+ generation.executeDescription,
259
+ (sessionId) => coordinator.formatSessionPrefix(sessionId),
260
+ )) {
261
+ this.pi.registerTool(definition);
262
+ }
263
+ generation.toolsRegistered = true;
264
+ this.synchronizeGeneration(generation);
265
+ }
266
+
267
+ private acceptExposureDecision(
268
+ generation: PiCodeModeGeneration,
269
+ decision: CodeModeToolExposureDecision,
270
+ ): boolean {
271
+ if (!generation.active || this.generation !== generation) return false;
272
+ const catalogue = renderGenerationCatalogue(generation.captured, decision);
273
+ if (catalogue.ok) return true;
274
+ if (!generation.catalogueWarningShown) {
275
+ generation.catalogueWarningShown = true;
276
+ this.notifyWarning(
277
+ generation.context,
278
+ "Pi CodeMode retained its previous exposure because registered tool names exceed the 1 MiB catalogue limit",
279
+ );
280
+ }
281
+ return false;
282
+ }
283
+
284
+ private synchronizeCurrentGeneration(): void {
285
+ const generation = this.generation;
286
+ if (generation !== undefined) this.synchronizeGeneration(generation);
287
+ }
288
+
289
+ private synchronizeGeneration(generation: PiCodeModeGeneration): void {
290
+ if (!generation.active || this.generation !== generation) return;
291
+ if (generation.synchronizing) {
292
+ generation.synchronizationPending = true;
293
+ return;
294
+ }
295
+ generation.synchronizing = true;
296
+ try {
297
+ do {
298
+ generation.synchronizationPending = false;
299
+ const decision = generation.exposure?.getDecision() ?? generation.decision;
300
+ const catalogue = renderGenerationCatalogue(generation.captured, decision);
301
+ if (!catalogue.ok) continue;
302
+ generation.decision = decision;
303
+ const description = catalogueDescription(catalogue.text);
304
+ if (description === generation.executeDescription) continue;
305
+ generation.executeDescription = description;
306
+ if (!generation.toolsRegistered) continue;
307
+ const executeDefinition = createRenderedCodeModeToolDefinitions(
308
+ generation.operations,
309
+ description,
310
+ (sessionId) => generation.coordinator.formatSessionPrefix(sessionId),
311
+ )[0];
312
+ if (executeDefinition !== undefined) this.pi.registerTool(executeDefinition);
313
+ } while (generation.synchronizationPending);
314
+ } finally {
315
+ generation.synchronizing = false;
316
+ }
317
+ }
318
+
319
+ private async executeNestedToolBatch(
320
+ generation: PiCodeModeGeneration,
321
+ batch: CodeModeNestedToolBatch,
322
+ ): Promise<CodeModeNestedToolBatchResult> {
323
+ if (!generation.active || this.generation !== generation) {
324
+ return {
325
+ results: batch.calls.map((call) =>
326
+ unavailableNestedResult(
327
+ call.callId,
328
+ "cancellation",
329
+ "Pi CodeMode session generation is inactive",
330
+ ),
331
+ ),
332
+ };
333
+ }
334
+
335
+ const exposedNames = new Set(generation.decision.codeModeNames);
336
+ const registry = generation.captured.getToolRegistry();
337
+ const earlyResults = new Map<string, CodeModeNestedToolResult>();
338
+ const bridgeCalls: PiToolBridgeCall[] = [];
339
+ for (const call of batch.calls) {
340
+ if (!exposedNames.has(call.toolName) || !registry.has(call.toolName)) {
341
+ earlyResults.set(
342
+ call.callId,
343
+ unavailableNestedResult(
344
+ call.callId,
345
+ "unknown-tool",
346
+ `Pi CodeMode tool is not currently exposed: ${call.toolName}`,
347
+ ),
348
+ );
349
+ } else {
350
+ if (!isCodeModeJsonObject(call.input)) {
351
+ earlyResults.set(
352
+ call.callId,
353
+ unavailableNestedResult(
354
+ call.callId,
355
+ "validation",
356
+ `Pi CodeMode tool input must be an object: ${call.toolName}`,
357
+ ),
358
+ );
359
+ } else {
360
+ bridgeCalls.push({ callId: call.callId, name: call.toolName, input: call.input });
361
+ }
362
+ }
363
+ }
364
+
365
+ const bridgeCaptured: CapturedPiAgentSession = {
366
+ agent: generation.captured.agent,
367
+ session: generation.captured.session,
368
+ settingsManager: generation.captured.settingsManager,
369
+ getToolRegistry: () => {
370
+ const currentExposedNames = new Set(generation.decision.codeModeNames);
371
+ return new Map(
372
+ [...generation.captured.getToolRegistry()].filter(([name]) =>
373
+ currentExposedNames.has(name),
374
+ ),
375
+ );
376
+ },
377
+ };
378
+ const outerAssistantMessage = latestCodeModeAssistantMessage(generation.captured);
379
+ const terminationController = new AbortController();
380
+ const bridgeOptions = {
381
+ calls: bridgeCalls,
382
+ now: CODEMODE_SYSTEM_RUNTIME.now,
383
+ signal: AbortSignal.any([batch.signal, terminationController.signal]),
384
+ onTerminate: () => terminationController.abort(),
385
+ };
386
+ if (outerAssistantMessage !== undefined) {
387
+ Object.assign(bridgeOptions, { outerAssistantMessage });
388
+ }
389
+ if (batch.onUpdate !== undefined) {
390
+ Object.assign(bridgeOptions, {
391
+ onUpdate: (_callId: string, update: AgentToolResult<unknown>) => {
392
+ const outerUpdate: AgentToolResult<CodeModeResultDetails> = {
393
+ content: update.content,
394
+ details: createCodeModePending(batch.sessionId),
395
+ };
396
+ batch.onUpdate?.(outerUpdate);
397
+ },
398
+ });
399
+ }
400
+ const bridged = await executePiToolBridgeBatch(bridgeCaptured, bridgeOptions);
401
+ const bridgedResults = new Map<string, CodeModeNestedToolResult>(
402
+ bridged.calls.map((outcome) => [
403
+ outcome.callId,
404
+ outcome.ok
405
+ ? {
406
+ callId: outcome.callId,
407
+ outcome: "success",
408
+ result: codeModeNestedBridgeValue(outcome.value),
409
+ }
410
+ : {
411
+ callId: outcome.callId,
412
+ outcome: "error",
413
+ error: { code: outcome.error.code, message: outcome.error.message },
414
+ },
415
+ ]),
416
+ );
417
+ const results = batch.calls.map(
418
+ (call) =>
419
+ earlyResults.get(call.callId) ??
420
+ bridgedResults.get(call.callId) ??
421
+ unavailableNestedResult(
422
+ call.callId,
423
+ "runtime",
424
+ "Pi CodeMode nested tool returned no result",
425
+ ),
426
+ );
427
+ const batchResult = { results, presentation: bridged.presentation };
428
+ if (bridged.usage !== undefined) Object.assign(batchResult, { usage: bridged.usage });
429
+ if (bridged.addedToolNames.length > 0) {
430
+ Object.assign(batchResult, { addedToolNames: bridged.addedToolNames });
431
+ }
432
+ if (bridged.terminate) Object.assign(batchResult, { terminate: true });
433
+ return batchResult;
434
+ }
435
+
436
+ private async shutdownSession(reason: string): Promise<void> {
437
+ const generation = this.generation;
438
+ if (generation === undefined || !generation.active) return;
439
+ generation.active = false;
440
+ try {
441
+ generation.observer.dispose();
442
+ } catch {
443
+ // Observer cleanup is presentation-only; execution resources still require release.
444
+ }
445
+ try {
446
+ await generation.coordinator.shutdown(reason);
447
+ } finally {
448
+ try {
449
+ await generation.sessionFiles.close();
450
+ } finally {
451
+ generation.exposure?.restore();
452
+ if (this.generation === generation) this.generation = undefined;
453
+ }
454
+ }
455
+ }
456
+
457
+ private notifyWarning(context: ExtensionContext, message: string): void {
458
+ context.ui.notify(message, "warning");
459
+ }
460
+ }
461
+
462
+ /** Creates the source-TypeScript CodeMode extension without startup side effects. */
463
+ export function createPiCodeModeExtension(): ExtensionFactory {
464
+ return (pi) => new PiCodeModeLifecycleController(pi).register();
465
+ }
466
+
467
+ const piCodeModeExtension = createPiCodeModeExtension();
468
+
469
+ export default piCodeModeExtension;