@ian-pascoe/pi-codemode 0.2.0 → 0.3.1
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 +50 -13
- package/package.json +1 -1
- package/src/codemode-console-output.ts +11 -0
- package/src/codemode-observer-ui.ts +15 -36
- package/src/codemode-session-coordinator.ts +127 -90
- package/src/codemode-tool-catalog.ts +4 -6
- package/src/codemode-tool-contract.ts +37 -7
- package/src/codemode-tool-rendering.ts +71 -27
- package/src/codemode-worker-protocol.ts +141 -33
- package/src/codemode-worker.ts +283 -18
- package/src/pi-codemode-extension.ts +17 -21
- package/src/pi-tool-bridge.ts +5 -38
|
@@ -2,6 +2,7 @@ import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
|
2
2
|
import type { Usage } from "@earendil-works/pi-ai";
|
|
3
3
|
import { truncateHead } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { transformCodeModeCell } from "./codemode-cell-transform.js";
|
|
5
|
+
import type { CodeModeConsoleEntry } from "./codemode-console-output.js";
|
|
5
6
|
import { CodeModeWorkerProcess } from "./codemode-deno-process.js";
|
|
6
7
|
import { formatCodeModePresentationData } from "./codemode-presentation-output.js";
|
|
7
8
|
import type { CodeModeRuntime, CodeModeTimerHandle } from "./codemode-runtime.js";
|
|
@@ -35,9 +36,6 @@ const CODEMODE_RESULT_PRESENTATION_MAX_LINES = 2_000;
|
|
|
35
36
|
const CODEMODE_WATCHDOG_GRACE_MS = 100;
|
|
36
37
|
const INVALID_CODEMODE_SESSION_ID = "invalid-session-id";
|
|
37
38
|
|
|
38
|
-
/** Branded identifier for one retained CodeMode Session. */
|
|
39
|
-
export type CodeModeSessionId = string & { readonly CodeModeSessionId: unique symbol };
|
|
40
|
-
|
|
41
39
|
/** Observer state of one settled CodeMode Cell. */
|
|
42
40
|
export type CodeModeObservedCellState = "completed" | "failed" | "cancelled" | "timed_out";
|
|
43
41
|
|
|
@@ -70,7 +68,7 @@ export type CodeModeObservedLastCell = {
|
|
|
70
68
|
|
|
71
69
|
/** Immutable read-only facts for one CodeMode Session with at least one Cell. */
|
|
72
70
|
export type CodeModeObservedSession = {
|
|
73
|
-
readonly sessionId:
|
|
71
|
+
readonly sessionId: string;
|
|
74
72
|
readonly lifecycle: "running" | "idle" | "terminal";
|
|
75
73
|
/** Number of Cells started in this CodeMode Session. */
|
|
76
74
|
readonly cell_count: number;
|
|
@@ -88,7 +86,7 @@ export type CodeModeObserverSnapshot = {
|
|
|
88
86
|
|
|
89
87
|
/** Read-only facts for one live CodeMode Session, ordered by admission priority. */
|
|
90
88
|
export type CodeModeListedSession = {
|
|
91
|
-
readonly sessionId:
|
|
89
|
+
readonly sessionId: string;
|
|
92
90
|
readonly state: "running" | "idle";
|
|
93
91
|
readonly cellCount: number;
|
|
94
92
|
/** Parent wall-clock time of the latest execution-visible transition in Unix-epoch milliseconds. */
|
|
@@ -97,7 +95,7 @@ export type CodeModeListedSession = {
|
|
|
97
95
|
|
|
98
96
|
/** One idle CodeMode worker failure not represented by an active Cell result. */
|
|
99
97
|
export type CodeModeUnexpectedFailure = {
|
|
100
|
-
readonly sessionId:
|
|
98
|
+
readonly sessionId: string;
|
|
101
99
|
readonly message: string;
|
|
102
100
|
};
|
|
103
101
|
|
|
@@ -122,7 +120,7 @@ export type CodeModeNestedToolUpdate = AgentToolResult<unknown>;
|
|
|
122
120
|
|
|
123
121
|
/** One complete guest batch plus its Cell-scoped cancellation and update capabilities. */
|
|
124
122
|
export type CodeModeNestedToolBatch = {
|
|
125
|
-
readonly sessionId:
|
|
123
|
+
readonly sessionId: string;
|
|
126
124
|
readonly batchId: string;
|
|
127
125
|
readonly calls: readonly CodeModeNestedToolCall[];
|
|
128
126
|
readonly signal: AbortSignal;
|
|
@@ -187,14 +185,14 @@ type MutableCodeModeOuterToolMetadata = {
|
|
|
187
185
|
|
|
188
186
|
type CreateActiveCodeModeCellOptions = {
|
|
189
187
|
onUpdate?: (update: CodeModeNestedToolUpdate) => void;
|
|
190
|
-
reclaimedSessionId?:
|
|
188
|
+
reclaimedSessionId?: string;
|
|
191
189
|
};
|
|
192
190
|
|
|
193
191
|
type ActiveCodeModeCell = {
|
|
194
192
|
readonly cellId: string;
|
|
195
193
|
readonly ordinal: number;
|
|
196
194
|
readonly startedAtMs: number;
|
|
197
|
-
readonly reclaimedSessionId?:
|
|
195
|
+
readonly reclaimedSessionId?: string;
|
|
198
196
|
readonly abortController: AbortController;
|
|
199
197
|
readonly completion: Promise<CodeModeResult>;
|
|
200
198
|
readonly resolveCompletion: (result: CodeModeResult) => void;
|
|
@@ -213,7 +211,7 @@ type ActiveCodeModeCell = {
|
|
|
213
211
|
|
|
214
212
|
type LiveCodeModeSession = {
|
|
215
213
|
readonly state: "live";
|
|
216
|
-
readonly sessionId:
|
|
214
|
+
readonly sessionId: string;
|
|
217
215
|
readonly worker: CodeModeWorkerProcess;
|
|
218
216
|
lastAccess: number;
|
|
219
217
|
lastActivityAtMs: number;
|
|
@@ -227,7 +225,7 @@ type LiveCodeModeSession = {
|
|
|
227
225
|
|
|
228
226
|
type TerminalCodeModeSession = {
|
|
229
227
|
readonly state: "terminal";
|
|
230
|
-
readonly sessionId:
|
|
228
|
+
readonly sessionId: string;
|
|
231
229
|
lastAccess: number;
|
|
232
230
|
readonly lastActivityAtMs: number;
|
|
233
231
|
readonly cellCount: number;
|
|
@@ -242,23 +240,50 @@ type CodeModeSessionRecord = LiveCodeModeSession | TerminalCodeModeSession;
|
|
|
242
240
|
type LocateCodeModeSessionResult =
|
|
243
241
|
| {
|
|
244
242
|
readonly record: CodeModeSessionRecord;
|
|
245
|
-
readonly reclaimedSessionId?:
|
|
243
|
+
readonly reclaimedSessionId?: string;
|
|
246
244
|
}
|
|
247
245
|
| { readonly failure: CodeModeResult };
|
|
248
246
|
|
|
249
247
|
type FatalCodeModeSessionFailure = {
|
|
250
248
|
readonly code: Extract<CodeModeErrorCode, "timeout" | "cancellation" | "termination" | "runtime">;
|
|
251
249
|
readonly message: string;
|
|
250
|
+
readonly console?: readonly CodeModeConsoleEntry[];
|
|
252
251
|
};
|
|
253
252
|
|
|
254
|
-
type
|
|
255
|
-
|
|
256
|
-
|
|
253
|
+
type ParsestringResult = { readonly ok: true; readonly value: string } | { readonly ok: false };
|
|
254
|
+
|
|
255
|
+
function parsestring(value: string): ParsestringResult {
|
|
256
|
+
return value.length === 0 ? { ok: false } : { ok: true, value };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Return the shortest prefix of the identifier that no candidate shares. */
|
|
260
|
+
export function shortestUniqueCodeModeSessionPrefix(
|
|
261
|
+
identifier: string,
|
|
262
|
+
candidates: readonly string[],
|
|
263
|
+
): string {
|
|
264
|
+
let length = Math.min(8, identifier.length);
|
|
265
|
+
while (
|
|
266
|
+
length < identifier.length &&
|
|
267
|
+
candidates.some((candidate) => candidate.startsWith(identifier.slice(0, length)))
|
|
268
|
+
) {
|
|
269
|
+
length += 1;
|
|
270
|
+
}
|
|
271
|
+
return identifier.slice(0, length);
|
|
272
|
+
}
|
|
257
273
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
274
|
+
/** Render one elapsed duration as ms, seconds, or minutes + padded seconds. */
|
|
275
|
+
export function formatCodeModeDuration(elapsedMs: number): string {
|
|
276
|
+
if (elapsedMs < 1_000) return `${elapsedMs}ms`;
|
|
277
|
+
if (elapsedMs < 60_000) return `${(elapsedMs / 1_000).toFixed(1)}s`;
|
|
278
|
+
const seconds = Math.floor(elapsedMs / 1_000);
|
|
279
|
+
return `${Math.floor(seconds / 60)}m ${String(seconds % 60).padStart(2, "0")}s`;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** Round one elapsed measurement and clamp it to a safe non-negative integer. */
|
|
283
|
+
export function boundedCodeModeElapsedMs(startedAtMs: number, observedAtMs: number): number {
|
|
284
|
+
const elapsed = Math.round(observedAtMs - startedAtMs);
|
|
285
|
+
if (!Number.isFinite(elapsed)) return 0;
|
|
286
|
+
return Math.min(Number.MAX_SAFE_INTEGER, Math.max(0, elapsed));
|
|
262
287
|
}
|
|
263
288
|
|
|
264
289
|
function invalidCodeModeSessionResult(): CodeModeSessionOperationResult {
|
|
@@ -275,12 +300,11 @@ function emptyMetadataAccumulator(): CodeModeMetadataAccumulator {
|
|
|
275
300
|
return { addedToolNames: new Set(), terminate: false };
|
|
276
301
|
}
|
|
277
302
|
|
|
278
|
-
|
|
303
|
+
/** Sums two Usage values; optional fields appear only when either side carries them. */
|
|
304
|
+
export function addCodeModeUsage(left: Usage | undefined, right: Usage | undefined): Usage {
|
|
305
|
+
if (right === undefined) throw new Error("Pi CodeMode usage: missing increment");
|
|
279
306
|
if (left === undefined) {
|
|
280
|
-
return {
|
|
281
|
-
...right,
|
|
282
|
-
cost: { ...right.cost },
|
|
283
|
-
};
|
|
307
|
+
return { ...right, cost: { ...right.cost } };
|
|
284
308
|
}
|
|
285
309
|
const combined: Usage = {
|
|
286
310
|
input: left.input + right.input,
|
|
@@ -310,7 +334,7 @@ function mergeCodeModeOuterMetadata(
|
|
|
310
334
|
metadata: CodeModeOuterToolMetadata,
|
|
311
335
|
): void {
|
|
312
336
|
if (metadata.usage !== undefined) {
|
|
313
|
-
accumulator.usage =
|
|
337
|
+
accumulator.usage = addCodeModeUsage(accumulator.usage, metadata.usage);
|
|
314
338
|
}
|
|
315
339
|
for (const name of metadata.addedToolNames ?? []) accumulator.addedToolNames.add(name);
|
|
316
340
|
if (metadata.terminate === true) accumulator.terminate = true;
|
|
@@ -331,7 +355,7 @@ function finalizeCodeModeMetadata(
|
|
|
331
355
|
|
|
332
356
|
/** Owns bounded CodeMode Session records and one isolated Deno process per live Session. */
|
|
333
357
|
export class CodeModeSessionCoordinator {
|
|
334
|
-
private readonly records = new Map<
|
|
358
|
+
private readonly records = new Map<string, CodeModeSessionRecord>();
|
|
335
359
|
private readonly runtime: CodeModeRuntime;
|
|
336
360
|
private accessSequence = 0;
|
|
337
361
|
private cellSequence = 0;
|
|
@@ -357,7 +381,7 @@ export class CodeModeSessionCoordinator {
|
|
|
357
381
|
): Promise<CodeModeSessionOperationResult> {
|
|
358
382
|
if (this.shuttingDown) {
|
|
359
383
|
const candidateSessionId = input.sessionId ?? this.runtime.createSessionId();
|
|
360
|
-
const parsedSessionId =
|
|
384
|
+
const parsedSessionId = parsestring(candidateSessionId);
|
|
361
385
|
if (!parsedSessionId.ok) return invalidCodeModeSessionResult();
|
|
362
386
|
return {
|
|
363
387
|
result: createCodeModeFailure(
|
|
@@ -373,7 +397,7 @@ export class CodeModeSessionCoordinator {
|
|
|
373
397
|
let located: LocateCodeModeSessionResult;
|
|
374
398
|
if (input.sessionId === undefined) located = await this.createLiveSession();
|
|
375
399
|
else {
|
|
376
|
-
const parsedSessionId =
|
|
400
|
+
const parsedSessionId = parsestring(input.sessionId);
|
|
377
401
|
if (!parsedSessionId.ok) return invalidCodeModeSessionResult();
|
|
378
402
|
located = this.findSession(parsedSessionId.value);
|
|
379
403
|
}
|
|
@@ -446,7 +470,7 @@ export class CodeModeSessionCoordinator {
|
|
|
446
470
|
|
|
447
471
|
/** Polls the latest retained Cell result without consuming that public result. */
|
|
448
472
|
result(sessionIdValue: string): CodeModeSessionOperationResult {
|
|
449
|
-
const parsedSessionId =
|
|
473
|
+
const parsedSessionId = parsestring(sessionIdValue);
|
|
450
474
|
if (!parsedSessionId.ok) return invalidCodeModeSessionResult();
|
|
451
475
|
const sessionId = parsedSessionId.value;
|
|
452
476
|
const record = this.records.get(sessionId);
|
|
@@ -495,25 +519,33 @@ export class CodeModeSessionCoordinator {
|
|
|
495
519
|
|
|
496
520
|
/** Return the shortest currently unique Session prefix, or the full unknown historical ID. */
|
|
497
521
|
formatSessionPrefix(sessionIdValue: string): string {
|
|
498
|
-
const parsed =
|
|
522
|
+
const parsed = parsestring(sessionIdValue);
|
|
499
523
|
if (!parsed.ok || !this.records.has(parsed.value)) return sessionIdValue;
|
|
500
|
-
const
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
524
|
+
const others = [...this.records.keys()].filter((candidate) => candidate !== parsed.value);
|
|
525
|
+
return shortestUniqueCodeModeSessionPrefix(sessionIdValue, others);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
private closedCellPresentation(
|
|
529
|
+
cellState: CodeModePresentationSnapshot["cell_state"],
|
|
530
|
+
): CodeModePresentationSnapshot {
|
|
531
|
+
return {
|
|
532
|
+
version: 1,
|
|
533
|
+
cell_state: cellState,
|
|
534
|
+
session_state: "closed",
|
|
535
|
+
elapsed_ms: 0,
|
|
536
|
+
active_tool_names: [],
|
|
537
|
+
active_tool_count: 0,
|
|
538
|
+
nested_tool_count: 0,
|
|
539
|
+
succeeded_nested_tool_count: 0,
|
|
540
|
+
failed_nested_tool_count: 0,
|
|
541
|
+
nested_tools: [],
|
|
542
|
+
omitted_nested_tool_count: 0,
|
|
543
|
+
};
|
|
512
544
|
}
|
|
513
545
|
|
|
514
546
|
/** Force-terminates one live CodeMode Session and retains its cancellation result. */
|
|
515
547
|
async cancel(sessionIdValue: string): Promise<CodeModeSessionOperationResult> {
|
|
516
|
-
const parsedSessionId =
|
|
548
|
+
const parsedSessionId = parsestring(sessionIdValue);
|
|
517
549
|
if (!parsedSessionId.ok) return invalidCodeModeSessionResult();
|
|
518
550
|
const sessionId = parsedSessionId.value;
|
|
519
551
|
const record = this.records.get(sessionId);
|
|
@@ -535,19 +567,7 @@ export class CodeModeSessionCoordinator {
|
|
|
535
567
|
});
|
|
536
568
|
} else {
|
|
537
569
|
record.lastActivityAtMs = this.runtime.now();
|
|
538
|
-
record.latestPresentation =
|
|
539
|
-
version: 1,
|
|
540
|
-
cell_state: "cancelled",
|
|
541
|
-
session_state: "closed",
|
|
542
|
-
elapsed_ms: 0,
|
|
543
|
-
active_tool_names: [],
|
|
544
|
-
active_tool_count: 0,
|
|
545
|
-
nested_tool_count: 0,
|
|
546
|
-
succeeded_nested_tool_count: 0,
|
|
547
|
-
failed_nested_tool_count: 0,
|
|
548
|
-
nested_tools: [],
|
|
549
|
-
omitted_nested_tool_count: 0,
|
|
550
|
-
};
|
|
570
|
+
record.latestPresentation = this.closedCellPresentation("cancelled");
|
|
551
571
|
this.replaceWithTerminal(
|
|
552
572
|
record,
|
|
553
573
|
createCodeModeFailure(sessionId, "cancellation", "CodeMode Session was cancelled"),
|
|
@@ -564,7 +584,7 @@ export class CodeModeSessionCoordinator {
|
|
|
564
584
|
}
|
|
565
585
|
|
|
566
586
|
/** Releases every live Deno process; repeated shutdown calls share one completion. */
|
|
567
|
-
shutdown(
|
|
587
|
+
shutdown(): Promise<void> {
|
|
568
588
|
if (this.shutdownPromise !== undefined) return this.shutdownPromise;
|
|
569
589
|
this.shuttingDown = true;
|
|
570
590
|
this.shutdownPromise = this.shutdownAllSessions();
|
|
@@ -594,7 +614,7 @@ export class CodeModeSessionCoordinator {
|
|
|
594
614
|
}
|
|
595
615
|
|
|
596
616
|
private async createLiveSession(): Promise<LocateCodeModeSessionResult> {
|
|
597
|
-
const parsedSessionId =
|
|
617
|
+
const parsedSessionId = parsestring(this.runtime.createSessionId());
|
|
598
618
|
if (!parsedSessionId.ok) {
|
|
599
619
|
return {
|
|
600
620
|
failure: createCodeModeFailure(
|
|
@@ -618,7 +638,7 @@ export class CodeModeSessionCoordinator {
|
|
|
618
638
|
const liveRecords = [...this.records.values()].filter(
|
|
619
639
|
(record): record is LiveCodeModeSession => record.state === "live",
|
|
620
640
|
);
|
|
621
|
-
let reclaimedSessionId:
|
|
641
|
+
let reclaimedSessionId: string | undefined;
|
|
622
642
|
if (liveRecords.length >= this.options.maxSessions) {
|
|
623
643
|
const reclaimedRecord = liveRecords
|
|
624
644
|
.filter((record) => record.currentCell === undefined)
|
|
@@ -698,7 +718,7 @@ export class CodeModeSessionCoordinator {
|
|
|
698
718
|
return reclaimedSessionId === undefined ? { record } : { record, reclaimedSessionId };
|
|
699
719
|
}
|
|
700
720
|
|
|
701
|
-
private findSession(sessionId:
|
|
721
|
+
private findSession(sessionId: string): LocateCodeModeSessionResult {
|
|
702
722
|
const record = this.records.get(sessionId);
|
|
703
723
|
return record === undefined
|
|
704
724
|
? { failure: createCodeModeFailure(sessionId, "unknown", "Unknown CodeMode Session") }
|
|
@@ -794,10 +814,7 @@ export class CodeModeSessionCoordinator {
|
|
|
794
814
|
}
|
|
795
815
|
}
|
|
796
816
|
|
|
797
|
-
private handleWorkerResponse(
|
|
798
|
-
sessionId: CodeModeSessionId,
|
|
799
|
-
response: CodeModeWorkerResponse,
|
|
800
|
-
): void {
|
|
817
|
+
private handleWorkerResponse(sessionId: string, response: CodeModeWorkerResponse): void {
|
|
801
818
|
const record = this.records.get(sessionId);
|
|
802
819
|
if (record?.state !== "live" || record.currentCell === undefined) return;
|
|
803
820
|
const cell = record.currentCell;
|
|
@@ -810,7 +827,11 @@ export class CodeModeSessionCoordinator {
|
|
|
810
827
|
return;
|
|
811
828
|
}
|
|
812
829
|
if (response.resultJson === undefined) {
|
|
813
|
-
this.settleReusableCell(
|
|
830
|
+
this.settleReusableCell(
|
|
831
|
+
record,
|
|
832
|
+
cell,
|
|
833
|
+
createCodeModeSuccess(sessionId, undefined, response.console),
|
|
834
|
+
);
|
|
814
835
|
return;
|
|
815
836
|
}
|
|
816
837
|
const data = this.parseJsonString(response.resultJson, { allowUndefined: true });
|
|
@@ -818,11 +839,15 @@ export class CodeModeSessionCoordinator {
|
|
|
818
839
|
this.settleReusableCell(
|
|
819
840
|
record,
|
|
820
841
|
cell,
|
|
821
|
-
createCodeModeFailure(sessionId, "serialization", data.message),
|
|
842
|
+
createCodeModeFailure(sessionId, "serialization", data.message, response.console),
|
|
822
843
|
);
|
|
823
844
|
return;
|
|
824
845
|
}
|
|
825
|
-
this.settleReusableCell(
|
|
846
|
+
this.settleReusableCell(
|
|
847
|
+
record,
|
|
848
|
+
cell,
|
|
849
|
+
createCodeModeSuccess(sessionId, data.value, response.console),
|
|
850
|
+
);
|
|
826
851
|
return;
|
|
827
852
|
}
|
|
828
853
|
if (response.type === "cell-error") {
|
|
@@ -834,15 +859,25 @@ export class CodeModeSessionCoordinator {
|
|
|
834
859
|
return;
|
|
835
860
|
}
|
|
836
861
|
if (response.error.code === "runtime") {
|
|
837
|
-
|
|
862
|
+
const failure = {
|
|
838
863
|
code: response.error.code,
|
|
839
864
|
message: response.error.message,
|
|
840
|
-
}
|
|
865
|
+
} as const;
|
|
866
|
+
this.fatalizeSession(
|
|
867
|
+
record,
|
|
868
|
+
cell,
|
|
869
|
+
response.console === undefined ? failure : { ...failure, console: response.console },
|
|
870
|
+
);
|
|
841
871
|
} else {
|
|
842
872
|
this.settleReusableCell(
|
|
843
873
|
record,
|
|
844
874
|
cell,
|
|
845
|
-
createCodeModeFailure(
|
|
875
|
+
createCodeModeFailure(
|
|
876
|
+
sessionId,
|
|
877
|
+
response.error.code,
|
|
878
|
+
response.error.message,
|
|
879
|
+
response.console,
|
|
880
|
+
),
|
|
846
881
|
);
|
|
847
882
|
}
|
|
848
883
|
return;
|
|
@@ -1170,10 +1205,19 @@ export class CodeModeSessionCoordinator {
|
|
|
1170
1205
|
): void {
|
|
1171
1206
|
if (!this.isCurrentCell(record, cell)) return;
|
|
1172
1207
|
this.clearCellResources(cell);
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
:
|
|
1208
|
+
let retainedResult = result;
|
|
1209
|
+
if (result.result === "success" && cell.reclaimedSessionId !== undefined) {
|
|
1210
|
+
if (result.console === undefined) {
|
|
1211
|
+
retainedResult = { ...result, reclaimedSessionId: cell.reclaimedSessionId };
|
|
1212
|
+
} else {
|
|
1213
|
+
const { console: consoleEntries, ...resultWithoutConsole } = result;
|
|
1214
|
+
retainedResult = {
|
|
1215
|
+
...resultWithoutConsole,
|
|
1216
|
+
reclaimedSessionId: cell.reclaimedSessionId,
|
|
1217
|
+
console: consoleEntries,
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1177
1221
|
const presentation = this.settledCellPresentation(cell, retainedResult, "live");
|
|
1178
1222
|
const settledAtMs = this.runtime.now();
|
|
1179
1223
|
record.latestResult = retainedResult;
|
|
@@ -1199,7 +1243,12 @@ export class CodeModeSessionCoordinator {
|
|
|
1199
1243
|
if (failure.code === "termination") cell.metadata.terminate = true;
|
|
1200
1244
|
cell.abortController.abort();
|
|
1201
1245
|
this.clearCellResources(cell);
|
|
1202
|
-
const result = createCodeModeFailure(
|
|
1246
|
+
const result = createCodeModeFailure(
|
|
1247
|
+
record.sessionId,
|
|
1248
|
+
failure.code,
|
|
1249
|
+
failure.message,
|
|
1250
|
+
failure.console,
|
|
1251
|
+
);
|
|
1203
1252
|
const settledAtMs = this.runtime.now();
|
|
1204
1253
|
record.latestPresentation = this.cellPresentation(
|
|
1205
1254
|
cell,
|
|
@@ -1247,26 +1296,14 @@ export class CodeModeSessionCoordinator {
|
|
|
1247
1296
|
return stop;
|
|
1248
1297
|
}
|
|
1249
1298
|
|
|
1250
|
-
private handleWorkerFailure(sessionId:
|
|
1299
|
+
private handleWorkerFailure(sessionId: string, message: string): void {
|
|
1251
1300
|
const record = this.records.get(sessionId);
|
|
1252
1301
|
if (record?.state !== "live") return;
|
|
1253
1302
|
if (record.currentCell !== undefined) {
|
|
1254
1303
|
this.fatalizeSession(record, record.currentCell, { code: "runtime", message });
|
|
1255
1304
|
} else {
|
|
1256
1305
|
record.lastActivityAtMs = this.runtime.now();
|
|
1257
|
-
record.latestPresentation =
|
|
1258
|
-
version: 1,
|
|
1259
|
-
cell_state: "failed",
|
|
1260
|
-
session_state: "closed",
|
|
1261
|
-
elapsed_ms: 0,
|
|
1262
|
-
active_tool_names: [],
|
|
1263
|
-
active_tool_count: 0,
|
|
1264
|
-
nested_tool_count: 0,
|
|
1265
|
-
succeeded_nested_tool_count: 0,
|
|
1266
|
-
failed_nested_tool_count: 0,
|
|
1267
|
-
nested_tools: [],
|
|
1268
|
-
omitted_nested_tool_count: 0,
|
|
1269
|
-
};
|
|
1306
|
+
record.latestPresentation = this.closedCellPresentation("failed");
|
|
1270
1307
|
this.replaceWithTerminal(record, createCodeModeFailure(sessionId, "runtime", message));
|
|
1271
1308
|
try {
|
|
1272
1309
|
this.options.onUnexpectedFailure?.(Object.freeze({ sessionId, message }));
|
|
@@ -1387,7 +1424,7 @@ export class CodeModeSessionCoordinator {
|
|
|
1387
1424
|
}
|
|
1388
1425
|
}
|
|
1389
1426
|
|
|
1390
|
-
private retainTerminalFailure(sessionId:
|
|
1427
|
+
private retainTerminalFailure(sessionId: string, failure: CodeModeResult): void {
|
|
1391
1428
|
this.records.set(sessionId, {
|
|
1392
1429
|
state: "terminal",
|
|
1393
1430
|
sessionId,
|
|
@@ -95,10 +95,6 @@ function schemaRecord(value: CodeModeJsonValue | undefined): CodeModeJsonObject
|
|
|
95
95
|
return value !== undefined && isCodeModeJsonObject(value) ? value : undefined;
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
function jsonLiteral(value: CodeModeJsonValue): string | undefined {
|
|
99
|
-
return JSON.stringify(value);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
98
|
function quotedName(name: string): string {
|
|
103
99
|
return JSON.stringify(name);
|
|
104
100
|
}
|
|
@@ -139,9 +135,11 @@ function schemaType(
|
|
|
139
135
|
|
|
140
136
|
const constant = record.const;
|
|
141
137
|
if (Object.hasOwn(record, "const") && constant !== undefined) {
|
|
142
|
-
return
|
|
138
|
+
return JSON.stringify(constant) ?? "unknown";
|
|
143
139
|
}
|
|
144
|
-
const enumValues = Array.isArray(record.enum)
|
|
140
|
+
const enumValues = Array.isArray(record.enum)
|
|
141
|
+
? record.enum.map((value) => JSON.stringify(value))
|
|
142
|
+
: undefined;
|
|
145
143
|
if (
|
|
146
144
|
enumValues !== undefined &&
|
|
147
145
|
enumValues.length > 0 &&
|
|
@@ -8,6 +8,11 @@ import type {
|
|
|
8
8
|
import type { Usage } from "@earendil-works/pi-ai";
|
|
9
9
|
import { type Static, Type } from "typebox";
|
|
10
10
|
import { Value } from "typebox/value";
|
|
11
|
+
import {
|
|
12
|
+
CODEMODE_CONSOLE_METHODS,
|
|
13
|
+
type CodeModeConsoleEntry,
|
|
14
|
+
type CodeModeConsoleMethod,
|
|
15
|
+
} from "./codemode-console-output.js";
|
|
11
16
|
|
|
12
17
|
const CODEMODE_TOOL_NAMES = {
|
|
13
18
|
execute: "codemode_execute",
|
|
@@ -170,6 +175,15 @@ const CodeModeErrorCodeSchema = Type.Unsafe<CodeModeErrorCode>({
|
|
|
170
175
|
type: "string",
|
|
171
176
|
enum: [...CODEMODE_ERROR_CODES],
|
|
172
177
|
});
|
|
178
|
+
const CodeModeConsoleMethodSchema = Type.Unsafe<CodeModeConsoleMethod>({
|
|
179
|
+
type: "string",
|
|
180
|
+
enum: [...CODEMODE_CONSOLE_METHODS],
|
|
181
|
+
});
|
|
182
|
+
const CodeModeConsoleEntrySchema = Type.Object(
|
|
183
|
+
{ method: CodeModeConsoleMethodSchema, text: Type.String() },
|
|
184
|
+
{ additionalProperties: false },
|
|
185
|
+
);
|
|
186
|
+
const CodeModeConsoleOutputSchema = Type.Array(CodeModeConsoleEntrySchema, { minItems: 1 });
|
|
173
187
|
|
|
174
188
|
/** Stable error retained by a failed CodeMode result. */
|
|
175
189
|
export const CodeModeErrorSchema = Type.Object(
|
|
@@ -186,6 +200,7 @@ const CodeModeSuccessSchema = Type.Object(
|
|
|
186
200
|
sessionId: SessionIdSchema,
|
|
187
201
|
data: Type.Optional(CodeModeJsonValueSchema),
|
|
188
202
|
reclaimedSessionId: Type.Optional(SessionIdSchema),
|
|
203
|
+
console: Type.Optional(CodeModeConsoleOutputSchema),
|
|
189
204
|
},
|
|
190
205
|
{ additionalProperties: false },
|
|
191
206
|
);
|
|
@@ -225,6 +240,7 @@ const CodeModeFailedSchema = Type.Object(
|
|
|
225
240
|
result: Type.Literal("failed"),
|
|
226
241
|
sessionId: SessionIdSchema,
|
|
227
242
|
error: CodeModeErrorSchema,
|
|
243
|
+
console: Type.Optional(CodeModeConsoleOutputSchema),
|
|
228
244
|
},
|
|
229
245
|
{ additionalProperties: false },
|
|
230
246
|
);
|
|
@@ -245,6 +261,7 @@ const CodeModeSuccessDetailsSchema = Type.Object(
|
|
|
245
261
|
sessionId: SessionIdSchema,
|
|
246
262
|
data: Type.Optional(CodeModeJsonValueSchema),
|
|
247
263
|
reclaimedSessionId: Type.Optional(SessionIdSchema),
|
|
264
|
+
console: Type.Optional(CodeModeConsoleOutputSchema),
|
|
248
265
|
presentation: Type.Optional(CodeModePresentationSnapshotSchema),
|
|
249
266
|
},
|
|
250
267
|
{ additionalProperties: false },
|
|
@@ -262,6 +279,7 @@ const CodeModeFailedDetailsSchema = Type.Object(
|
|
|
262
279
|
result: Type.Literal("failed"),
|
|
263
280
|
sessionId: SessionIdSchema,
|
|
264
281
|
error: CodeModeErrorSchema,
|
|
282
|
+
console: Type.Optional(CodeModeConsoleOutputSchema),
|
|
265
283
|
presentation: Type.Optional(CodeModePresentationSnapshotSchema),
|
|
266
284
|
},
|
|
267
285
|
{ additionalProperties: false },
|
|
@@ -276,11 +294,19 @@ export const CodeModeResultDetailsSchema = Type.Union([
|
|
|
276
294
|
/** Schema-derived details retained by one session-scoped CodeMode operation. */
|
|
277
295
|
export type CodeModeResultDetails = Static<typeof CodeModeResultDetailsSchema>;
|
|
278
296
|
|
|
279
|
-
/** A
|
|
280
|
-
export function createCodeModeSuccess(
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
297
|
+
/** A success with optional data and non-empty Cell Console output; empty Console lists are omitted. */
|
|
298
|
+
export function createCodeModeSuccess(
|
|
299
|
+
sessionId: string,
|
|
300
|
+
data?: CodeModeJsonValue,
|
|
301
|
+
consoleEntries?: readonly CodeModeConsoleEntry[],
|
|
302
|
+
): CodeModeResult {
|
|
303
|
+
const result =
|
|
304
|
+
data === undefined
|
|
305
|
+
? { result: "success" as const, sessionId }
|
|
306
|
+
: { result: "success" as const, sessionId, data };
|
|
307
|
+
return consoleEntries === undefined || consoleEntries.length === 0
|
|
308
|
+
? result
|
|
309
|
+
: { ...result, console: [...consoleEntries] };
|
|
284
310
|
}
|
|
285
311
|
|
|
286
312
|
/** A polling result for a live Cell. */
|
|
@@ -288,13 +314,17 @@ export function createCodeModePending(sessionId: string): CodeModeResult {
|
|
|
288
314
|
return { result: "pending", sessionId };
|
|
289
315
|
}
|
|
290
316
|
|
|
291
|
-
/** A stable expected failure
|
|
317
|
+
/** A stable expected failure with non-empty Cell Console output; empty Console lists are omitted. */
|
|
292
318
|
export function createCodeModeFailure(
|
|
293
319
|
sessionId: string,
|
|
294
320
|
code: CodeModeErrorCode,
|
|
295
321
|
message: string,
|
|
322
|
+
consoleEntries?: readonly CodeModeConsoleEntry[],
|
|
296
323
|
): CodeModeResult {
|
|
297
|
-
|
|
324
|
+
const result = { result: "failed" as const, sessionId, error: { code, message } };
|
|
325
|
+
return consoleEntries === undefined || consoleEntries.length === 0
|
|
326
|
+
? result
|
|
327
|
+
: { ...result, console: [...consoleEntries] };
|
|
298
328
|
}
|
|
299
329
|
|
|
300
330
|
/** A bounded JSON compatibility parse that never invokes getters or `toJSON`. */
|