@ian-pascoe/pi-codemode 0.1.0 → 0.2.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/README.md CHANGED
@@ -62,11 +62,38 @@ codemode_cancel({ sessionId: string });
62
62
  Stops the session process and frees its capacity. The cancel call succeeds;
63
63
  subsequent polling returns the retained `cancellation` failure.
64
64
 
65
- All three tools return:
65
+ ### `codemode_sessions`
66
+
67
+ ```ts
68
+ codemode_sessions({});
69
+ ```
70
+
71
+ Lists every live Session, with idle Sessions first in least-recently-used order,
72
+ then running Sessions in least-recently-used order. Listing does not refresh
73
+ Session recency.
74
+
75
+ ```ts
76
+ type CodeModeSessionsResult = {
77
+ result: "success";
78
+ sessions: Array<{
79
+ sessionId: string;
80
+ state: "idle" | "running";
81
+ cellCount: number;
82
+ lastActivityAtMs: number;
83
+ }>;
84
+ };
85
+ ```
86
+
87
+ The execute, result, and cancel tools return:
66
88
 
67
89
  ```ts
68
90
  type CodeModeResult =
69
- | { result: "success"; sessionId: string; data?: JsonValue }
91
+ | {
92
+ result: "success";
93
+ sessionId: string;
94
+ data?: JsonValue;
95
+ reclaimedSessionId?: string;
96
+ }
70
97
  | { result: "pending"; sessionId: string }
71
98
  | {
72
99
  result: "failed";
@@ -76,6 +103,7 @@ type CodeModeResult =
76
103
  | "unknown"
77
104
  | "busy"
78
105
  | "capacity"
106
+ | "eviction"
79
107
  | "script"
80
108
  | "serialization"
81
109
  | "timeout"
@@ -93,7 +121,7 @@ Snapshots in tool-result details for Transcript replay and the TUI.
93
121
 
94
122
  ## Transcript and Observer UI
95
123
 
96
- The CodeMode Transcript gives all three tools semantic collapsed and expanded
124
+ The CodeMode Transcript gives all four tools semantic collapsed and expanded
97
125
  rendering. Collapsed rows prioritize Cell lifecycle, a short Session ID, Cell
98
126
  Ordinal, returned-value shape, nested-tool count, and elapsed time. Expanded
99
127
  rows show the full Session ID, explicit call arguments, TypeScript source,
@@ -105,15 +133,17 @@ Status always uses a symbol and text together:
105
133
 
106
134
  ```text
107
135
  ◉ running ○ idle ✓ completed
108
- × failed ■ cancelled ! timed out
136
+ × failed ■ cancelled ■ reclaimed ! timed out
109
137
  ```
110
138
 
111
139
  Awaited Cells publish a presentation update immediately and once per second.
112
- Source display is limited to 200 lines or 50 KB. Returned-data display uses
113
- Pi's 2,000-line/50-KB limit; complete oversized data is written to a private
114
- Result Spill while the model-facing result remains unchanged. Result Spill
115
- files last for the live Pi session. Replayed history falls back to its retained
116
- bounded data when a prior spill is no longer available.
140
+ Collapsed calls show one highlighted line inline or the first eight highlighted
141
+ lines of a multi-line Cell. Expanded calls show the complete TypeScript source.
142
+ Returned-data display uses Pi's 2,000-line/50-KB limit; complete oversized data
143
+ is written to a private Result Spill while the model-facing result remains
144
+ unchanged. Result Spill files last for the live Pi session. Replayed history
145
+ falls back to its retained bounded data when a prior spill is no longer
146
+ available.
117
147
 
118
148
  In TUI mode, the read-only CodeMode Observer UI appears above the editor during
119
149
  Cell activity. It shows up to eight running, idle, or recently terminal
@@ -204,12 +234,18 @@ last match wins. Project `tools` replaces the global array, while project
204
234
 
205
235
  An unmatched active tool defaults to `direct-and-codemode`; an unmatched
206
236
  inactive tool remains unavailable. An explicit rule may expose an inactive tool
207
- or activate direct access. The three `codemode_*` tools are always direct-only.
237
+ or activate direct access. The four `codemode_*` tools are always direct-only.
208
238
  Pi's global allowed/excluded registry remains authoritative. Invalid fields or
209
239
  patterns disable CodeMode for that session without changing Pi's active tools.
210
240
 
211
- `maxSessions` defaults to 8 and counts only live Deno processes. Up to 64 recent
212
- worker-free terminal or failed-admission records remain pollable.
241
+ `maxSessions` defaults to 8 and counts only live Deno processes. When capacity
242
+ is full, a new Session gracefully stops the least-recently-used idle Session
243
+ before starting; its Notebook Bindings are discarded, the new success reports
244
+ `reclaimedSessionId`, and polling the old ID returns `eviction`. Running Sessions
245
+ are never reclaimed, so admission still returns `capacity` when every process is
246
+ busy. Executing or polling refreshes recency; listing and Observer rendering do
247
+ not. Up to 64 recent worker-free terminal or failed-admission records remain
248
+ pollable.
213
249
 
214
250
  ## Isolation and limits
215
251
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ian-pascoe/pi-codemode",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "private": false,
5
5
  "description": "Process-isolated persistent TypeScript tool composition for Pi",
6
6
  "keywords": [
@@ -32,10 +32,6 @@
32
32
  "access": "public",
33
33
  "provenance": true
34
34
  },
35
- "scripts": {
36
- "test": "vitest run --config ../../vitest.config.ts --root .",
37
- "typecheck": "tsc --noEmit -p tsconfig.json"
38
- },
39
35
  "dependencies": {
40
36
  "deno": "2.9.5",
41
37
  "minimatch": "^10.2.5",
@@ -55,5 +51,9 @@
55
51
  "extensions": [
56
52
  "./src/index.ts"
57
53
  ]
54
+ },
55
+ "scripts": {
56
+ "test": "vitest run --config ../../vitest.config.ts --root .",
57
+ "typecheck": "tsc --noEmit -p tsconfig.json"
58
58
  }
59
- }
59
+ }
@@ -72,7 +72,7 @@ export type CodeModeObserverRow = {
72
72
  /** Idle Session summary retained only during relevant Observer activity. */
73
73
  | { readonly state: "idle"; readonly cellCount: number }
74
74
  | {
75
- readonly state: "failed" | "cancelled" | "timed_out";
75
+ readonly state: "failed" | "cancelled" | "reclaimed" | "timed_out";
76
76
  /** Most recent one-based Cell Ordinal, or Session Cell count when no final Cell exists. */
77
77
  readonly cellOrdinal: number;
78
78
  }
@@ -102,6 +102,7 @@ const CODEMODE_OBSERVER_STATE_PRESENTATION = {
102
102
  idle: { symbol: "○", label: "idle", color: "muted" },
103
103
  failed: { symbol: "×", label: "failed", color: "error" },
104
104
  cancelled: { symbol: "■", label: "cancelled", color: "warning" },
105
+ reclaimed: { symbol: "■", label: "reclaimed", color: "warning" },
105
106
  timed_out: { symbol: "!", label: "timed out", color: "error" },
106
107
  } satisfies Record<CodeModeObserverState, CodeModeObserverStatePresentation>;
107
108
 
@@ -153,10 +154,11 @@ function boundedElapsedMilliseconds(nowMs: number, startedAtMs: number): number
153
154
 
154
155
  function codeModeTerminalObserverState(
155
156
  session: CodeModeObservedSession,
156
- ): Extract<CodeModeObserverState, "failed" | "cancelled" | "timed_out"> {
157
+ ): Extract<CodeModeObserverState, "failed" | "cancelled" | "reclaimed" | "timed_out"> {
157
158
  if (session.terminal_error_code === "cancellation" || session.last_cell?.state === "cancelled") {
158
159
  return "cancelled";
159
160
  }
161
+ if (session.terminal_error_code === "eviction") return "reclaimed";
160
162
  if (session.terminal_error_code === "timeout" || session.last_cell?.state === "timed_out") {
161
163
  return "timed_out";
162
164
  }
@@ -86,6 +86,15 @@ export type CodeModeObserverSnapshot = {
86
86
  readonly sessions: readonly CodeModeObservedSession[];
87
87
  };
88
88
 
89
+ /** Read-only facts for one live CodeMode Session, ordered by admission priority. */
90
+ export type CodeModeListedSession = {
91
+ readonly sessionId: CodeModeSessionId;
92
+ readonly state: "running" | "idle";
93
+ readonly cellCount: number;
94
+ /** Parent wall-clock time of the latest execution-visible transition in Unix-epoch milliseconds. */
95
+ readonly lastActivityAtMs: number;
96
+ };
97
+
89
98
  /** One idle CodeMode worker failure not represented by an active Cell result. */
90
99
  export type CodeModeUnexpectedFailure = {
91
100
  readonly sessionId: CodeModeSessionId;
@@ -176,10 +185,16 @@ type MutableCodeModeOuterToolMetadata = {
176
185
  terminate?: boolean;
177
186
  };
178
187
 
188
+ type CreateActiveCodeModeCellOptions = {
189
+ onUpdate?: (update: CodeModeNestedToolUpdate) => void;
190
+ reclaimedSessionId?: CodeModeSessionId;
191
+ };
192
+
179
193
  type ActiveCodeModeCell = {
180
194
  readonly cellId: string;
181
195
  readonly ordinal: number;
182
196
  readonly startedAtMs: number;
197
+ readonly reclaimedSessionId?: CodeModeSessionId;
183
198
  readonly abortController: AbortController;
184
199
  readonly completion: Promise<CodeModeResult>;
185
200
  readonly resolveCompletion: (result: CodeModeResult) => void;
@@ -224,10 +239,12 @@ type TerminalCodeModeSession = {
224
239
 
225
240
  type CodeModeSessionRecord = LiveCodeModeSession | TerminalCodeModeSession;
226
241
 
227
- type LocateCodeModeSessionResult = {
228
- readonly record?: CodeModeSessionRecord;
229
- readonly failure: CodeModeResult;
230
- };
242
+ type LocateCodeModeSessionResult =
243
+ | {
244
+ readonly record: CodeModeSessionRecord;
245
+ readonly reclaimedSessionId?: CodeModeSessionId;
246
+ }
247
+ | { readonly failure: CodeModeResult };
231
248
 
232
249
  type FatalCodeModeSessionFailure = {
233
250
  readonly code: Extract<CodeModeErrorCode, "timeout" | "cancellation" | "termination" | "runtime">;
@@ -325,6 +342,7 @@ export class CodeModeSessionCoordinator {
325
342
  (update: CodeModeNestedToolUpdate) => void
326
343
  >();
327
344
  private readonly pendingProcessStops = new Set<Promise<void>>();
345
+ private admissionQueue: Promise<void> = Promise.resolve();
328
346
 
329
347
  /** Creates one coordinator from its Pi bridge, limits, and parent runtime capabilities. */
330
348
  constructor(private readonly options: CodeModeSessionCoordinatorOptions) {
@@ -349,70 +367,80 @@ export class CodeModeSessionCoordinator {
349
367
  ),
350
368
  };
351
369
  }
352
- let located: LocateCodeModeSessionResult;
353
- if (input.sessionId === undefined) located = this.createLiveSession();
354
- else {
355
- const parsedSessionId = parseCodeModeSessionId(input.sessionId);
356
- if (!parsedSessionId.ok) return invalidCodeModeSessionResult();
357
- located = this.findSession(parsedSessionId.value);
358
- }
359
- if (located.record === undefined) return { result: located.failure };
360
- const record = located.record;
361
- this.touch(record);
362
- if (record.state === "terminal")
363
- return this.operationResult(
364
- record.latestResult,
365
- this.takeMetadata(record),
366
- record.latestPresentation,
367
- );
368
- if (record.currentCell !== undefined) {
369
- return {
370
- result: createCodeModeFailure(
371
- record.sessionId,
372
- "busy",
373
- "CodeMode Session already has an active Cell",
374
- ),
375
- };
376
- }
370
+ let releaseAdmission =
371
+ input.sessionId === undefined ? await this.acquireSessionAdmission() : undefined;
372
+ try {
373
+ let located: LocateCodeModeSessionResult;
374
+ if (input.sessionId === undefined) located = await this.createLiveSession();
375
+ else {
376
+ const parsedSessionId = parseCodeModeSessionId(input.sessionId);
377
+ if (!parsedSessionId.ok) return invalidCodeModeSessionResult();
378
+ located = this.findSession(parsedSessionId.value);
379
+ }
380
+ if ("failure" in located) return { result: located.failure };
381
+ const record = located.record;
382
+ this.touch(record);
383
+ if (record.state === "terminal")
384
+ return this.operationResult(
385
+ record.latestResult,
386
+ this.takeMetadata(record),
387
+ record.latestPresentation,
388
+ );
389
+ if (record.currentCell !== undefined) {
390
+ return {
391
+ result: createCodeModeFailure(
392
+ record.sessionId,
393
+ "busy",
394
+ "CodeMode Session already has an active Cell",
395
+ ),
396
+ };
397
+ }
377
398
 
378
- const shouldWait = input.wait !== false;
379
- const cell = this.createActiveCell(
380
- record,
381
- shouldWait && onUpdate !== undefined ? { onUpdate } : {},
382
- );
383
- const priorMetadata = this.takeMetadata(record);
384
- if (priorMetadata !== undefined) mergeCodeModeOuterMetadata(cell.metadata, priorMetadata);
385
- record.currentCell = cell;
386
- record.latestResult = createCodeModePending(record.sessionId);
387
- record.lastActivityAtMs = cell.startedAtMs;
388
- this.publishObserverSnapshot();
389
- this.emitCellProgress(record, cell);
390
- this.scheduleCellProgress(record, cell);
391
- void this.startCell(record, cell, input);
399
+ const shouldWait = input.wait !== false;
400
+ const cellOptions: CreateActiveCodeModeCellOptions = {};
401
+ if (shouldWait && onUpdate !== undefined) cellOptions.onUpdate = onUpdate;
402
+ if (located.reclaimedSessionId !== undefined) {
403
+ cellOptions.reclaimedSessionId = located.reclaimedSessionId;
404
+ }
405
+ const cell = this.createActiveCell(record, cellOptions);
406
+ const priorMetadata = this.takeMetadata(record);
407
+ if (priorMetadata !== undefined) mergeCodeModeOuterMetadata(cell.metadata, priorMetadata);
408
+ record.currentCell = cell;
409
+ record.latestResult = createCodeModePending(record.sessionId);
410
+ record.lastActivityAtMs = cell.startedAtMs;
411
+ this.publishObserverSnapshot();
412
+ this.emitCellProgress(record, cell);
413
+ this.scheduleCellProgress(record, cell);
414
+ void this.startCell(record, cell, input);
415
+ releaseAdmission?.();
416
+ releaseAdmission = undefined;
392
417
 
393
- const pending = createCodeModePending(record.sessionId);
394
- if (!shouldWait) {
395
- return this.operationResult(pending, undefined, this.runningCellPresentation(cell));
396
- }
397
- const abort = (): void => {
398
- this.fatalizeSession(record, cell, {
399
- code: "cancellation",
400
- message: "CodeMode Cell was cancelled",
401
- });
402
- };
403
- if (signal?.aborted === true) abort();
404
- else signal?.addEventListener("abort", abort, { once: true });
405
- try {
406
- const result = await cell.completion;
407
- const retainedRecord = this.records.get(record.sessionId) ?? record;
408
- return this.operationResult(
409
- result,
410
- this.takeMetadata(retainedRecord),
411
- retainedRecord.latestPresentation,
412
- );
418
+ const pending = createCodeModePending(record.sessionId);
419
+ if (!shouldWait) {
420
+ return this.operationResult(pending, undefined, this.runningCellPresentation(cell));
421
+ }
422
+ const abort = (): void => {
423
+ this.fatalizeSession(record, cell, {
424
+ code: "cancellation",
425
+ message: "CodeMode Cell was cancelled",
426
+ });
427
+ };
428
+ if (signal?.aborted === true) abort();
429
+ else signal?.addEventListener("abort", abort, { once: true });
430
+ try {
431
+ const result = await cell.completion;
432
+ const retainedRecord = this.records.get(record.sessionId) ?? record;
433
+ return this.operationResult(
434
+ result,
435
+ this.takeMetadata(retainedRecord),
436
+ retainedRecord.latestPresentation,
437
+ );
438
+ } finally {
439
+ cell.acceptsUpdates = false;
440
+ signal?.removeEventListener("abort", abort);
441
+ }
413
442
  } finally {
414
- cell.acceptsUpdates = false;
415
- signal?.removeEventListener("abort", abort);
443
+ releaseAdmission?.();
416
444
  }
417
445
  }
418
446
 
@@ -437,6 +465,26 @@ export class CodeModeSessionCoordinator {
437
465
  return this.operationResult(result, this.takeMetadata(record), record.latestPresentation);
438
466
  }
439
467
 
468
+ /** Lists all live Sessions with idle least-recently-used entries before running entries. */
469
+ listSessions(): readonly CodeModeListedSession[] {
470
+ const sessions = [...this.records.values()]
471
+ .filter((record): record is LiveCodeModeSession => record.state === "live")
472
+ .toSorted((left, right) => {
473
+ const stateOrder =
474
+ Number(left.currentCell !== undefined) - Number(right.currentCell !== undefined);
475
+ return stateOrder === 0 ? left.lastAccess - right.lastAccess : stateOrder;
476
+ })
477
+ .map((record) =>
478
+ Object.freeze({
479
+ sessionId: record.sessionId,
480
+ state: record.currentCell === undefined ? "idle" : "running",
481
+ cellCount: record.cellCount,
482
+ lastActivityAtMs: record.lastActivityAtMs,
483
+ } satisfies CodeModeListedSession),
484
+ );
485
+ return Object.freeze(sessions);
486
+ }
487
+
440
488
  /** Returns immutable, non-authoritative facts for the ephemeral CodeMode Observer UI. */
441
489
  inspectObserverSnapshot(): CodeModeObserverSnapshot {
442
490
  const sessions = [...this.records.values()]
@@ -537,7 +585,15 @@ export class CodeModeSessionCoordinator {
537
585
  await Promise.all(this.pendingProcessStops);
538
586
  }
539
587
 
540
- private createLiveSession(): LocateCodeModeSessionResult {
588
+ private async acquireSessionAdmission(): Promise<() => void> {
589
+ const previousAdmission = this.admissionQueue;
590
+ const admission = Promise.withResolvers<void>();
591
+ this.admissionQueue = previousAdmission.then(() => admission.promise);
592
+ await previousAdmission;
593
+ return admission.resolve;
594
+ }
595
+
596
+ private async createLiveSession(): Promise<LocateCodeModeSessionResult> {
541
597
  const parsedSessionId = parseCodeModeSessionId(this.runtime.createSessionId());
542
598
  if (!parsedSessionId.ok) {
543
599
  return {
@@ -549,13 +605,67 @@ export class CodeModeSessionCoordinator {
549
605
  };
550
606
  }
551
607
  const sessionId = parsedSessionId.value;
608
+ if (this.shuttingDown) {
609
+ const failure = createCodeModeFailure(
610
+ sessionId,
611
+ "runtime",
612
+ "CodeMode coordinator is shutting down",
613
+ );
614
+ this.retainTerminalFailure(sessionId, failure);
615
+ return { failure };
616
+ }
552
617
  this.evictTerminalRecords();
553
- const liveCount = [...this.records.values()].filter((record) => record.state === "live").length;
554
- if (liveCount >= this.options.maxSessions) {
618
+ const liveRecords = [...this.records.values()].filter(
619
+ (record): record is LiveCodeModeSession => record.state === "live",
620
+ );
621
+ let reclaimedSessionId: CodeModeSessionId | undefined;
622
+ if (liveRecords.length >= this.options.maxSessions) {
623
+ const reclaimedRecord = liveRecords
624
+ .filter((record) => record.currentCell === undefined)
625
+ .toSorted((left, right) => left.lastAccess - right.lastAccess)[0];
626
+ if (reclaimedRecord === undefined) {
627
+ const failure = createCodeModeFailure(
628
+ sessionId,
629
+ "capacity",
630
+ "CodeMode Session capacity is exhausted",
631
+ );
632
+ this.retainTerminalFailure(sessionId, failure);
633
+ return { failure };
634
+ }
635
+ reclaimedSessionId = reclaimedRecord.sessionId;
636
+ reclaimedRecord.lastActivityAtMs = this.runtime.now();
637
+ this.touch(reclaimedRecord);
638
+ if (reclaimedRecord.latestPresentation !== undefined) {
639
+ reclaimedRecord.latestPresentation = {
640
+ ...reclaimedRecord.latestPresentation,
641
+ session_state: "closed",
642
+ };
643
+ }
644
+ this.replaceWithTerminal(
645
+ reclaimedRecord,
646
+ createCodeModeFailure(
647
+ reclaimedRecord.sessionId,
648
+ "eviction",
649
+ "CodeMode Session was reclaimed to free capacity.",
650
+ ),
651
+ );
652
+ try {
653
+ await this.stopWorker(reclaimedRecord.worker, "shutdown");
654
+ } catch (cause) {
655
+ const message =
656
+ cause instanceof Error
657
+ ? `CodeMode Session reclamation failed: ${cause.message}`
658
+ : "CodeMode Session reclamation failed: Deno process did not stop cleanly";
659
+ const failure = createCodeModeFailure(sessionId, "runtime", message);
660
+ this.retainTerminalFailure(sessionId, failure);
661
+ return { failure };
662
+ }
663
+ }
664
+ if (this.shuttingDown) {
555
665
  const failure = createCodeModeFailure(
556
666
  sessionId,
557
- "capacity",
558
- "CodeMode Session capacity is exhausted",
667
+ "runtime",
668
+ "CodeMode coordinator is shutting down",
559
669
  );
560
670
  this.retainTerminalFailure(sessionId, failure);
561
671
  return { failure };
@@ -585,22 +695,22 @@ export class CodeModeSessionCoordinator {
585
695
  cellCount: 0,
586
696
  };
587
697
  this.records.set(sessionId, record);
588
- return { record, failure: createCodeModePending(sessionId) };
698
+ return reclaimedSessionId === undefined ? { record } : { record, reclaimedSessionId };
589
699
  }
590
700
 
591
701
  private findSession(sessionId: CodeModeSessionId): LocateCodeModeSessionResult {
592
702
  const record = this.records.get(sessionId);
593
703
  return record === undefined
594
704
  ? { failure: createCodeModeFailure(sessionId, "unknown", "Unknown CodeMode Session") }
595
- : { record, failure: createCodeModePending(sessionId) };
705
+ : { record };
596
706
  }
597
707
 
598
708
  private createActiveCell(
599
709
  record: LiveCodeModeSession,
600
- options: { readonly onUpdate?: (update: CodeModeNestedToolUpdate) => void },
710
+ options: CreateActiveCodeModeCellOptions,
601
711
  ): ActiveCodeModeCell {
602
712
  const completion = Promise.withResolvers<CodeModeResult>();
603
- const cell: ActiveCodeModeCell = {
713
+ const cellBase: Omit<ActiveCodeModeCell, "reclaimedSessionId"> = {
604
714
  cellId: `cell-${++this.cellSequence}`,
605
715
  ordinal: ++record.cellCount,
606
716
  startedAtMs: this.runtime.now(),
@@ -617,6 +727,10 @@ export class CodeModeSessionCoordinator {
617
727
  acceptsUpdates: options.onUpdate !== undefined,
618
728
  settled: false,
619
729
  };
730
+ const cell: ActiveCodeModeCell =
731
+ options.reclaimedSessionId === undefined
732
+ ? cellBase
733
+ : { ...cellBase, reclaimedSessionId: options.reclaimedSessionId };
620
734
  if (options.onUpdate !== undefined) this.activeUpdateCallbacks.set(cell, options.onUpdate);
621
735
  return cell;
622
736
  }
@@ -1056,18 +1170,22 @@ export class CodeModeSessionCoordinator {
1056
1170
  ): void {
1057
1171
  if (!this.isCurrentCell(record, cell)) return;
1058
1172
  this.clearCellResources(cell);
1059
- const presentation = this.settledCellPresentation(cell, result, "live");
1173
+ const retainedResult =
1174
+ result.result === "success" && cell.reclaimedSessionId !== undefined
1175
+ ? { ...result, reclaimedSessionId: cell.reclaimedSessionId }
1176
+ : result;
1177
+ const presentation = this.settledCellPresentation(cell, retainedResult, "live");
1060
1178
  const settledAtMs = this.runtime.now();
1061
- record.latestResult = result;
1179
+ record.latestResult = retainedResult;
1062
1180
  record.latestPresentation = presentation;
1063
- record.lastCell = this.observeSettledCell(cell, result, settledAtMs);
1181
+ record.lastCell = this.observeSettledCell(cell, retainedResult, settledAtMs);
1064
1182
  record.lastActivityAtMs = settledAtMs;
1065
1183
  const metadata = finalizeCodeModeMetadata(cell.metadata);
1066
1184
  if (metadata === undefined) delete record.availableMetadata;
1067
1185
  else record.availableMetadata = metadata;
1068
1186
  delete record.currentCell;
1069
1187
  cell.settled = true;
1070
- cell.resolveCompletion(result);
1188
+ cell.resolveCompletion(retainedResult);
1071
1189
  this.touch(record);
1072
1190
  this.publishObserverSnapshot();
1073
1191
  }
@@ -13,6 +13,7 @@ const CODEMODE_TOOL_NAMES = {
13
13
  execute: "codemode_execute",
14
14
  result: "codemode_result",
15
15
  cancel: "codemode_cancel",
16
+ sessions: "codemode_sessions",
16
17
  } as const;
17
18
  const RESERVED_CODEMODE_TOOL_NAMES = new Set<string>(Object.values(CODEMODE_TOOL_NAMES));
18
19
 
@@ -26,6 +27,7 @@ export const CODEMODE_ERROR_CODES = [
26
27
  "unknown",
27
28
  "busy",
28
29
  "capacity",
30
+ "eviction",
29
31
  "script",
30
32
  "serialization",
31
33
  "timeout",
@@ -116,12 +118,17 @@ export const CodeModeCancelParametersSchema = Type.Object(
116
118
  { additionalProperties: false },
117
119
  );
118
120
 
121
+ /** Strict empty arguments accepted by the read-only `codemode_sessions` tool. */
122
+ export const CodeModeSessionsParametersSchema = Type.Object({}, { additionalProperties: false });
123
+
119
124
  /** Parsed arguments for `codemode_execute`. */
120
125
  export type CodeModeExecuteParameters = Static<typeof CodeModeExecuteParametersSchema>;
121
126
  /** Parsed arguments for `codemode_result`. */
122
127
  export type CodeModeResultParameters = Static<typeof CodeModeResultParametersSchema>;
123
128
  /** Parsed arguments for `codemode_cancel`. */
124
129
  export type CodeModeCancelParameters = Static<typeof CodeModeCancelParametersSchema>;
130
+ /** Parsed arguments for the read-only `codemode_sessions` tool. */
131
+ export type CodeModeSessionsParameters = Static<typeof CodeModeSessionsParametersSchema>;
125
132
 
126
133
  /** A JSON object accepted in a successful CodeMode result. */
127
134
  export type CodeModeJsonObject = { readonly [key: string]: CodeModeJsonValue };
@@ -178,9 +185,34 @@ const CodeModeSuccessSchema = Type.Object(
178
185
  result: Type.Literal("success"),
179
186
  sessionId: SessionIdSchema,
180
187
  data: Type.Optional(CodeModeJsonValueSchema),
188
+ reclaimedSessionId: Type.Optional(SessionIdSchema),
181
189
  },
182
190
  { additionalProperties: false },
183
191
  );
192
+
193
+ /** One live CodeMode Session with its Unix-epoch last-activity time. */
194
+ export const CodeModeSessionListEntrySchema = Type.Object(
195
+ {
196
+ sessionId: SessionIdSchema,
197
+ state: Type.Union([Type.Literal("idle"), Type.Literal("running")]),
198
+ cellCount: NonNegativeSafeIntegerSchema,
199
+ lastActivityAtMs: NonNegativeSafeIntegerSchema,
200
+ },
201
+ { additionalProperties: false },
202
+ );
203
+
204
+ /** Idle-LRU-first, then running-LRU aggregate returned by `codemode_sessions`. */
205
+ export const CodeModeSessionsResultSchema = Type.Object(
206
+ {
207
+ result: Type.Literal("success"),
208
+ sessions: Type.Array(CodeModeSessionListEntrySchema),
209
+ },
210
+ { additionalProperties: false },
211
+ );
212
+
213
+ /** Schema-derived live Session list ordered by reclamation priority. */
214
+ export type CodeModeSessionsResult = Static<typeof CodeModeSessionsResultSchema>;
215
+
184
216
  const CodeModePendingSchema = Type.Object(
185
217
  {
186
218
  result: Type.Literal("pending"),
@@ -197,14 +229,14 @@ const CodeModeFailedSchema = Type.Object(
197
229
  { additionalProperties: false },
198
230
  );
199
231
 
200
- /** Schema-derived result union shared by all three public CodeMode tools. */
232
+ /** Schema-derived result union shared by the execute, result, and cancel tools. */
201
233
  export const CodeModeResultSchema = Type.Union([
202
234
  CodeModeSuccessSchema,
203
235
  CodeModePendingSchema,
204
236
  CodeModeFailedSchema,
205
237
  ]);
206
238
 
207
- /** Schema-derived result returned by every public CodeMode tool. */
239
+ /** Schema-derived result returned by one session-scoped CodeMode operation. */
208
240
  export type CodeModeResult = Static<typeof CodeModeResultSchema>;
209
241
 
210
242
  const CodeModeSuccessDetailsSchema = Type.Object(
@@ -212,6 +244,7 @@ const CodeModeSuccessDetailsSchema = Type.Object(
212
244
  result: Type.Literal("success"),
213
245
  sessionId: SessionIdSchema,
214
246
  data: Type.Optional(CodeModeJsonValueSchema),
247
+ reclaimedSessionId: Type.Optional(SessionIdSchema),
215
248
  presentation: Type.Optional(CodeModePresentationSnapshotSchema),
216
249
  },
217
250
  { additionalProperties: false },
@@ -240,7 +273,7 @@ export const CodeModeResultDetailsSchema = Type.Union([
240
273
  CodeModePendingDetailsSchema,
241
274
  CodeModeFailedDetailsSchema,
242
275
  ]);
243
- /** Schema-derived details retained by every public CodeMode tool. */
276
+ /** Schema-derived details retained by one session-scoped CodeMode operation. */
244
277
  export type CodeModeResultDetails = Static<typeof CodeModeResultDetailsSchema>;
245
278
 
246
279
  /** A successful result with optional JSON data. */
@@ -402,7 +435,7 @@ export type CodeModeToolOperationResult = {
402
435
  readonly presentation?: CodeModePresentationSnapshot;
403
436
  };
404
437
 
405
- /** Operations supplied by the session coordinator to build the three Pi tools. */
438
+ /** Operations supplied by the session coordinator to build the four Pi tools. */
406
439
  export interface CodeModeToolOperations {
407
440
  execute(
408
441
  input: CodeModeExecuteParameters,
@@ -412,6 +445,7 @@ export interface CodeModeToolOperations {
412
445
  ): Promise<CodeModeToolOperationResult>;
413
446
  result(input: CodeModeResultParameters): Promise<CodeModeToolOperationResult>;
414
447
  cancel(input: CodeModeCancelParameters): Promise<CodeModeToolOperationResult>;
448
+ sessions(): Promise<CodeModeSessionsResult>;
415
449
  }
416
450
 
417
451
  function structuredCodeModeResult(
@@ -438,9 +472,19 @@ type CodeModeToolDefinitions = readonly [
438
472
  ToolDefinition<typeof CodeModeExecuteParametersSchema, CodeModeResultDetails>,
439
473
  ToolDefinition<typeof CodeModeResultParametersSchema, CodeModeResultDetails>,
440
474
  ToolDefinition<typeof CodeModeCancelParametersSchema, CodeModeResultDetails>,
475
+ ToolDefinition<typeof CodeModeSessionsParametersSchema, CodeModeSessionsResult>,
441
476
  ];
442
477
 
443
- /** Creates the three stable Pi definitions while leaving admission and session policy to the coordinator. */
478
+ function structuredCodeModeSessionsResult(
479
+ result: CodeModeSessionsResult,
480
+ ): AgentToolResult<CodeModeSessionsResult> {
481
+ return {
482
+ content: [{ type: "text", text: JSON.stringify(result) }],
483
+ details: result,
484
+ };
485
+ }
486
+
487
+ /** Creates the four stable Pi definitions while leaving admission and session policy to the coordinator. */
444
488
  export function createCodeModeToolDefinitions(
445
489
  operations: CodeModeToolOperations,
446
490
  executeDescription = "Execute TypeScript in a persistent isolated Deno CodeMode Session.",
@@ -450,6 +494,13 @@ export function createCodeModeToolDefinitions(
450
494
  name: CODEMODE_TOOL_NAMES.execute,
451
495
  label: "CodeMode Execute",
452
496
  description: executeDescription,
497
+ promptSnippet:
498
+ "Batch, filter, and aggregate Pi tool calls in TypeScript with less latency and context usage.",
499
+ promptGuidelines: [
500
+ "Prefer codemode_execute when multiple Pi tool calls can be filtered, joined, aggregated, paginated, or used to drive later calls, or when one large result can be reduced before returning. Use direct parallel calls for a few small results needed verbatim.",
501
+ "Return only decision-relevant CodeMode data while preserving paths, line numbers, IDs, URLs, source names, and concise evidence needed for verification.",
502
+ "Reuse a CodeMode Session for related work. Prefer direct tools for simple one-off calls, full raw output, and confirmation-sensitive or destructive actions; use CodeMode mutations only when conditional sequencing is the point, and fall back to direct tools when the CodeMode boundary does not fit.",
503
+ ],
453
504
  parameters: CodeModeExecuteParametersSchema,
454
505
  executionMode: "sequential",
455
506
  async execute(_toolCallId, input, signal, onUpdate, context) {
@@ -469,12 +520,25 @@ export function createCodeModeToolDefinitions(
469
520
  const cancelTool: ToolDefinition<typeof CodeModeCancelParametersSchema, CodeModeResultDetails> = {
470
521
  name: CODEMODE_TOOL_NAMES.cancel,
471
522
  label: "CodeMode Cancel",
472
- description: "Cancel a live CodeMode session and retain its terminal result.",
523
+ description: "Cancel a live CodeMode Session, free its capacity, and retain its result.",
473
524
  parameters: CodeModeCancelParametersSchema,
474
525
  executionMode: "sequential",
475
526
  async execute(_toolCallId, input) {
476
527
  return structuredCodeModeResult(await operations.cancel(input));
477
528
  },
478
529
  };
479
- return [executeTool, resultTool, cancelTool];
530
+ const sessionsTool: ToolDefinition<
531
+ typeof CodeModeSessionsParametersSchema,
532
+ CodeModeSessionsResult
533
+ > = {
534
+ name: CODEMODE_TOOL_NAMES.sessions,
535
+ label: "List Sessions",
536
+ description: "List live CodeMode Sessions without changing their recency or state.",
537
+ parameters: CodeModeSessionsParametersSchema,
538
+ executionMode: "sequential",
539
+ async execute() {
540
+ return structuredCodeModeSessionsResult(await operations.sessions());
541
+ },
542
+ };
543
+ return [executeTool, resultTool, cancelTool, sessionsTool];
480
544
  }
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  getMarkdownTheme,
3
+ highlightCode,
3
4
  keyText,
4
5
  truncateHead,
5
6
  type AgentToolResult,
@@ -25,6 +26,8 @@ import {
25
26
  CodeModeExecuteParametersSchema,
26
27
  CodeModeResultDetailsSchema,
27
28
  CodeModeResultParametersSchema,
29
+ CodeModeSessionsParametersSchema,
30
+ CodeModeSessionsResultSchema,
28
31
  createCodeModeToolDefinitions,
29
32
  type CodeModeCancelParameters,
30
33
  type CodeModeErrorCode,
@@ -34,17 +37,24 @@ import {
34
37
  type CodeModePresentationSnapshot,
35
38
  type CodeModeResultDetails,
36
39
  type CodeModeResultParameters,
40
+ type CodeModeSessionsParameters,
41
+ type CodeModeSessionsResult,
37
42
  type CodeModeToolOperations,
38
43
  } from "./codemode-tool-contract.js";
39
44
 
40
- /** Names of the three CodeMode tools with semantic Transcript rendering. */
41
- export type CodeModeRenderedToolName = "codemode_execute" | "codemode_result" | "codemode_cancel";
45
+ /** Names of the four CodeMode tools with semantic Transcript rendering. */
46
+ export type CodeModeRenderedToolName =
47
+ | "codemode_execute"
48
+ | "codemode_result"
49
+ | "codemode_cancel"
50
+ | "codemode_sessions";
42
51
 
43
- /** Parsed arguments accepted by one of the three CodeMode Transcript renderers. */
52
+ /** Parsed arguments accepted by one of the four CodeMode Transcript renderers. */
44
53
  export type CodeModeRenderedToolParameters =
45
54
  | CodeModeExecuteParameters
46
55
  | CodeModeResultParameters
47
- | CodeModeCancelParameters;
56
+ | CodeModeCancelParameters
57
+ | CodeModeSessionsParameters;
48
58
 
49
59
  /** Theme operations used by CodeMode Transcript renderers. */
50
60
  export type CodeModeRenderTheme = Pick<Theme, "bold" | "fg">;
@@ -62,7 +72,7 @@ const CODEMODE_STATUS_PRESENTATION = {
62
72
  cancelled: { color: "warning", label: "■ cancelled" },
63
73
  timed_out: { color: "error", label: "! timed out" },
64
74
  } satisfies Record<CodeModeCellState, CodeModeStatusPresentation>;
65
- const CODEMODE_SCRIPT_MAX_LINES = 200;
75
+ const CODEMODE_COLLAPSED_SCRIPT_LINES = 8;
66
76
  const CODEMODE_PRESENTATION_MAX_BYTES = 50 * 1024;
67
77
  const CodeModeJsonStringSchema = Type.String();
68
78
 
@@ -82,14 +92,6 @@ function boundedCodeModePreview(text: string, width = 72): string {
82
92
  return `${sliceByColumn(singleLine, 0, width - 1, true).trimEnd()}…`;
83
93
  }
84
94
 
85
- function firstMeaningfulScriptLine(script: string): string | undefined {
86
- const line = sanitizeCodeModeText(script)
87
- .split("\n")
88
- .map((candidate) => candidate.trim())
89
- .find(Boolean);
90
- return line === undefined ? undefined : boundedCodeModePreview(line);
91
- }
92
-
93
95
  function parseCodeModeRenderedToolParameters(
94
96
  toolName: CodeModeRenderedToolName,
95
97
  parameters: CodeModeJsonValue,
@@ -100,7 +102,10 @@ function parseCodeModeRenderedToolParameters(
100
102
  if (toolName === "codemode_result") {
101
103
  return Value.Check(CodeModeResultParametersSchema, parameters) ? parameters : undefined;
102
104
  }
103
- return Value.Check(CodeModeCancelParametersSchema, parameters) ? parameters : undefined;
105
+ if (toolName === "codemode_cancel") {
106
+ return Value.Check(CodeModeCancelParametersSchema, parameters) ? parameters : undefined;
107
+ }
108
+ return Value.Check(CodeModeSessionsParametersSchema, parameters) ? parameters : undefined;
104
109
  }
105
110
 
106
111
  function shortCodeModeSessionId(sessionId: string): string {
@@ -139,6 +144,9 @@ function codeModeSessionLifecycle(
139
144
  toolName: CodeModeRenderedToolName,
140
145
  details: CodeModeResultDetails,
141
146
  ): "Session reusable" | "Session closed" | "No reusable Session" {
147
+ if (details.result === "failed" && details.error.code === "eviction") {
148
+ return "Session closed";
149
+ }
142
150
  if (details.presentation !== undefined) {
143
151
  return details.presentation.session_state === "live" ? "Session reusable" : "Session closed";
144
152
  }
@@ -192,6 +200,16 @@ function appendCodeModeBlock(container: Container, language: "ts" | "json", cont
192
200
  );
193
201
  }
194
202
 
203
+ function highlightedCodeModeSource(source: string): string {
204
+ return highlightCode(source, "typescript")
205
+ .map((line) => ` ${line}`)
206
+ .join("\n");
207
+ }
208
+
209
+ function appendHighlightedCodeModeSource(container: Container, source: string): void {
210
+ container.addChild(new Text(highlightedCodeModeSource(source), 0, 0));
211
+ }
212
+
195
213
  function boundedCodeModeText(text: string, maxLines: number): string {
196
214
  const safe = sanitizeCodeModeText(text);
197
215
  const truncated = truncateHead(safe, {
@@ -235,7 +253,10 @@ function renderCodeModeSummary(
235
253
  ): string {
236
254
  const presentation = details.presentation;
237
255
  const state = codeModeCellState(toolName, details);
238
- const status = CODEMODE_STATUS_PRESENTATION[state];
256
+ const status =
257
+ details.result === "failed" && details.error.code === "eviction"
258
+ ? { color: "warning" as const, label: "■ reclaimed" }
259
+ : CODEMODE_STATUS_PRESENTATION[state];
239
260
  const activeToolNames = presentation?.active_tool_names.slice(0, 3) ?? [];
240
261
  const omittedActiveToolCount = Math.max(
241
262
  0,
@@ -268,7 +289,7 @@ function renderCodeModeSummary(
268
289
  return parts.join(" ");
269
290
  }
270
291
 
271
- /** Render one CodeMode tool call as a semantic operation with bounded source detail. */
292
+ /** Render one CodeMode tool call with a bounded collapsed preview or complete expanded source. */
272
293
  export function renderCodeModeToolCall(
273
294
  toolName: CodeModeRenderedToolName,
274
295
  parameters: CodeModeJsonValue,
@@ -281,7 +302,9 @@ export function renderCodeModeToolCall(
281
302
  ? "Run Cell"
282
303
  : toolName === "codemode_result"
283
304
  ? "Poll"
284
- : "Cancel";
305
+ : toolName === "codemode_cancel"
306
+ ? "Cancel"
307
+ : "List Sessions";
285
308
  const parsedParameters = parseCodeModeRenderedToolParameters(toolName, parameters);
286
309
  const executeParameters =
287
310
  toolName === "codemode_execute" &&
@@ -289,11 +312,14 @@ export function renderCodeModeToolCall(
289
312
  "script" in parsedParameters
290
313
  ? parsedParameters
291
314
  : undefined;
292
- const sessionId = parsedParameters?.sessionId;
293
- const preview =
294
- executeParameters === undefined
295
- ? undefined
296
- : firstMeaningfulScriptLine(executeParameters.script);
315
+ const sessionId =
316
+ parsedParameters !== undefined && "sessionId" in parsedParameters
317
+ ? parsedParameters.sessionId
318
+ : undefined;
319
+ const source =
320
+ executeParameters === undefined ? undefined : sanitizeCodeModeText(executeParameters.script);
321
+ const oneLineSource = source !== undefined && !source.includes("\n") ? source : undefined;
322
+ const expansionHint = `${keyText("app.tools.expand")} to expand`;
297
323
  const container = new Container();
298
324
  container.addChild(
299
325
  new Text(
@@ -301,7 +327,12 @@ export function renderCodeModeToolCall(
301
327
  theme.fg("toolTitle", theme.bold("CodeMode")),
302
328
  theme.fg("accent", operation),
303
329
  theme.fg("muted", sessionId === undefined ? "new" : formatSessionPrefix(sessionId)),
304
- preview === undefined ? undefined : theme.fg("dim", preview),
330
+ !expanded && oneLineSource !== undefined
331
+ ? highlightCode(oneLineSource, "typescript")[0]
332
+ : undefined,
333
+ !expanded && oneLineSource !== undefined
334
+ ? theme.fg("dim", `· ${expansionHint}`)
335
+ : undefined,
305
336
  ]
306
337
  .filter((part): part is string => part !== undefined)
307
338
  .join(" "),
@@ -309,7 +340,17 @@ export function renderCodeModeToolCall(
309
340
  0,
310
341
  ),
311
342
  );
312
- if (!expanded) return container;
343
+ if (!expanded) {
344
+ if (source === undefined || oneLineSource !== undefined) return container;
345
+ const sourceLines = source.split("\n");
346
+ const visibleSource = sourceLines.slice(0, CODEMODE_COLLAPSED_SCRIPT_LINES).join("\n");
347
+ appendHighlightedCodeModeSource(container, visibleSource);
348
+ const omittedLines = Math.max(0, sourceLines.length - CODEMODE_COLLAPSED_SCRIPT_LINES);
349
+ const omitted =
350
+ omittedLines === 0 ? "" : `… ${pluralizedCodeModeCount(omittedLines, "line")} omitted · `;
351
+ container.addChild(new Text(theme.fg("dim", ` ${omitted}${expansionHint}`), 0, 0));
352
+ return container;
353
+ }
313
354
  container.addChild(new Spacer(1));
314
355
  if (sessionId !== undefined) appendCodeModeField(container, theme, "Session", sessionId);
315
356
  if (executeParameters === undefined) return container;
@@ -319,8 +360,41 @@ export function renderCodeModeToolCall(
319
360
  appendCodeModeField(container, theme, "Timeout", `${executeParameters.timeoutMs}ms`);
320
361
  container.addChild(new Spacer(1));
321
362
  container.addChild(new Text(theme.fg("muted", theme.bold("TypeScript")), 0, 0));
322
- const script = boundedCodeModeText(executeParameters.script, CODEMODE_SCRIPT_MAX_LINES);
323
- appendCodeModeBlock(container, "ts", script);
363
+ appendHighlightedCodeModeSource(container, source ?? "");
364
+ return container;
365
+ }
366
+
367
+ function renderCodeModeSessionsResult(
368
+ result: AgentToolResult<unknown>,
369
+ options: ToolRenderResultOptions,
370
+ theme: CodeModeRenderTheme,
371
+ ): Component {
372
+ if (!Value.Check(CodeModeSessionsResultSchema, result.details)) {
373
+ return renderCodeModeFallback(result, options, theme, false);
374
+ }
375
+ const sessions: CodeModeSessionsResult = result.details;
376
+ const summary = `${theme.fg("success", "✓")} ${pluralizedCodeModeCount(sessions.sessions.length, "session")}`;
377
+ if (!options.expanded) {
378
+ const hint = options.isPartial ? "" : ` · ${keyText("app.tools.expand")} to expand`;
379
+ return new Text(`${summary}${hint}`, 0, 0);
380
+ }
381
+ const container = new Container();
382
+ container.addChild(new Text(summary, 0, 0));
383
+ if (sessions.sessions.length === 0) {
384
+ container.addChild(new Spacer(1));
385
+ container.addChild(new Text(theme.fg("dim", "No live Sessions"), 0, 0));
386
+ return container;
387
+ }
388
+ container.addChild(new Spacer(1));
389
+ for (const session of sessions.sessions) {
390
+ container.addChild(
391
+ new Text(
392
+ `${theme.fg(session.state === "running" ? "accent" : "muted", session.state)} ${sanitizeCodeModeText(session.sessionId)} ${pluralizedCodeModeCount(session.cellCount, "cell")} ${session.lastActivityAtMs}`,
393
+ 0,
394
+ 0,
395
+ ),
396
+ );
397
+ }
324
398
  return container;
325
399
  }
326
400
 
@@ -330,10 +404,12 @@ export function renderCodeModeToolResult(
330
404
  result: AgentToolResult<unknown>,
331
405
  options: ToolRenderResultOptions,
332
406
  theme: CodeModeRenderTheme,
333
- parameters: CodeModeRenderedToolParameters,
334
407
  isError: boolean,
335
408
  formatSessionPrefix: CodeModeSessionPrefixFormatter = shortCodeModeSessionId,
336
409
  ): Component {
410
+ if (toolName === "codemode_sessions") {
411
+ return renderCodeModeSessionsResult(result, options, theme);
412
+ }
337
413
  if (!Value.Check(CodeModeResultDetailsSchema, result.details)) {
338
414
  return renderCodeModeFallback(result, options, theme, isError);
339
415
  }
@@ -409,13 +485,13 @@ export function renderCodeModeToolResult(
409
485
  return container;
410
486
  }
411
487
 
412
- /** Create the three CodeMode tools with semantic call and result Transcript renderers. */
488
+ /** Create the four CodeMode tools with semantic call and result Transcript renderers. */
413
489
  export function createRenderedCodeModeToolDefinitions(
414
490
  operations: CodeModeToolOperations,
415
491
  executeDescription?: string,
416
492
  formatSessionPrefix: CodeModeSessionPrefixFormatter = shortCodeModeSessionId,
417
493
  ): ReturnType<typeof createCodeModeToolDefinitions> {
418
- const [executeTool, resultTool, cancelTool] = createCodeModeToolDefinitions(
494
+ const [executeTool, resultTool, cancelTool, sessionsTool] = createCodeModeToolDefinitions(
419
495
  operations,
420
496
  executeDescription,
421
497
  );
@@ -436,7 +512,6 @@ export function createRenderedCodeModeToolDefinitions(
436
512
  result,
437
513
  options,
438
514
  theme,
439
- context.args,
440
515
  context.isError,
441
516
  formatSessionPrefix,
442
517
  ),
@@ -457,7 +532,6 @@ export function createRenderedCodeModeToolDefinitions(
457
532
  result,
458
533
  options,
459
534
  theme,
460
- context.args,
461
535
  context.isError,
462
536
  formatSessionPrefix,
463
537
  ),
@@ -478,7 +552,26 @@ export function createRenderedCodeModeToolDefinitions(
478
552
  result,
479
553
  options,
480
554
  theme,
481
- context.args,
555
+ context.isError,
556
+ formatSessionPrefix,
557
+ ),
558
+ },
559
+ {
560
+ ...sessionsTool,
561
+ renderCall: (_args, theme, context) =>
562
+ renderCodeModeToolCall(
563
+ "codemode_sessions",
564
+ {},
565
+ theme,
566
+ context.expanded,
567
+ formatSessionPrefix,
568
+ ),
569
+ renderResult: (result, options, theme, context) =>
570
+ renderCodeModeToolResult(
571
+ "codemode_sessions",
572
+ result,
573
+ options,
574
+ theme,
482
575
  context.isError,
483
576
  formatSessionPrefix,
484
577
  ),
@@ -39,7 +39,7 @@ import {
39
39
  } from "./pi-tool-bridge.js";
40
40
 
41
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.";
42
+ "Execute a TypeScript Cell in a persistent isolated Deno CodeMode Session. Reuse a Session ID to retain Notebook Bindings; a new Session reclaims the least-recently-used idle Session at capacity. Use the read-only tools object for registered Pi tools.";
43
43
 
44
44
  type PiCodeModeGeneration = {
45
45
  readonly captured: CapturedPiAgentSession;
@@ -207,6 +207,10 @@ class PiCodeModeLifecycleController {
207
207
  },
208
208
  result: async (input) => coordinator.result(input.sessionId),
209
209
  cancel: async (input) => coordinator.cancel(input.sessionId),
210
+ sessions: async () => ({
211
+ result: "success",
212
+ sessions: [...coordinator.listSessions()],
213
+ }),
210
214
  };
211
215
  generation = {
212
216
  captured,
@@ -253,13 +257,16 @@ class PiCodeModeLifecycleController {
253
257
  return;
254
258
  }
255
259
 
256
- for (const definition of createRenderedCodeModeToolDefinitions(
257
- operations,
258
- generation.executeDescription,
259
- (sessionId) => coordinator.formatSessionPrefix(sessionId),
260
- )) {
261
- this.pi.registerTool(definition);
262
- }
260
+ const [executeTool, resultTool, cancelTool, sessionsTool] =
261
+ createRenderedCodeModeToolDefinitions(
262
+ operations,
263
+ generation.executeDescription,
264
+ (sessionId) => coordinator.formatSessionPrefix(sessionId),
265
+ );
266
+ this.pi.registerTool(executeTool);
267
+ this.pi.registerTool(resultTool);
268
+ this.pi.registerTool(cancelTool);
269
+ this.pi.registerTool(sessionsTool);
263
270
  generation.toolsRegistered = true;
264
271
  this.synchronizeGeneration(generation);
265
272
  }