@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 +48 -12
- package/package.json +6 -6
- package/src/codemode-observer-ui.ts +4 -2
- package/src/codemode-session-coordinator.ts +196 -78
- package/src/codemode-tool-contract.ts +71 -7
- package/src/codemode-tool-rendering.ts +125 -32
- package/src/pi-codemode-extension.ts +15 -8
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
|
-
|
|
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
|
-
| {
|
|
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
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
|
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.
|
|
212
|
-
|
|
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.
|
|
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
|
-
|
|
229
|
-
|
|
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
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
if (
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
554
|
-
|
|
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
|
-
"
|
|
558
|
-
"CodeMode
|
|
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
|
|
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
|
|
705
|
+
: { record };
|
|
596
706
|
}
|
|
597
707
|
|
|
598
708
|
private createActiveCell(
|
|
599
709
|
record: LiveCodeModeSession,
|
|
600
|
-
options:
|
|
710
|
+
options: CreateActiveCodeModeCellOptions,
|
|
601
711
|
): ActiveCodeModeCell {
|
|
602
712
|
const completion = Promise.withResolvers<CodeModeResult>();
|
|
603
|
-
const
|
|
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
|
|
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 =
|
|
1179
|
+
record.latestResult = retainedResult;
|
|
1062
1180
|
record.latestPresentation = presentation;
|
|
1063
|
-
record.lastCell = this.observeSettledCell(cell,
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
41
|
-
export type CodeModeRenderedToolName =
|
|
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
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
-
: "
|
|
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 =
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
323
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
}
|