@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.
@@ -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: CodeModeSessionId;
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: CodeModeSessionId;
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: CodeModeSessionId;
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: CodeModeSessionId;
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?: CodeModeSessionId;
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?: CodeModeSessionId;
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: CodeModeSessionId;
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: CodeModeSessionId;
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?: CodeModeSessionId;
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 ParseCodeModeSessionIdResult =
255
- | { readonly ok: true; readonly value: CodeModeSessionId }
256
- | { readonly ok: false };
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
- function parseCodeModeSessionId(value: string): ParseCodeModeSessionIdResult {
259
- if (value.length === 0) return { ok: false };
260
- // SAFETY: This parser establishes the only CodeMode Session ID invariant (non-empty) before applying the domain brand.
261
- return { ok: true, value: value as CodeModeSessionId };
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
- function combineCodeModeUsage(left: Usage | undefined, right: Usage): Usage {
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 = combineCodeModeUsage(accumulator.usage, metadata.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<CodeModeSessionId, CodeModeSessionRecord>();
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 = parseCodeModeSessionId(candidateSessionId);
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 = parseCodeModeSessionId(input.sessionId);
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 = parseCodeModeSessionId(sessionIdValue);
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 = parseCodeModeSessionId(sessionIdValue);
522
+ const parsed = parsestring(sessionIdValue);
499
523
  if (!parsed.ok || !this.records.has(parsed.value)) return sessionIdValue;
500
- const sessionIds = [...this.records.keys()];
501
- let length = Math.min(8, sessionIdValue.length);
502
- while (
503
- length < sessionIdValue.length &&
504
- sessionIds.some(
505
- (candidate) =>
506
- candidate !== parsed.value && candidate.startsWith(sessionIdValue.slice(0, length)),
507
- )
508
- ) {
509
- length += 1;
510
- }
511
- return sessionIdValue.slice(0, length);
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 = parseCodeModeSessionId(sessionIdValue);
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(_reason: string): Promise<void> {
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 = parseCodeModeSessionId(this.runtime.createSessionId());
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: CodeModeSessionId | undefined;
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: CodeModeSessionId): LocateCodeModeSessionResult {
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(record, cell, createCodeModeSuccess(sessionId));
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(record, cell, createCodeModeSuccess(sessionId, data.value));
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
- this.fatalizeSession(record, cell, {
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(sessionId, response.error.code, response.error.message),
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
- const retainedResult =
1174
- result.result === "success" && cell.reclaimedSessionId !== undefined
1175
- ? { ...result, reclaimedSessionId: cell.reclaimedSessionId }
1176
- : result;
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(record.sessionId, failure.code, failure.message);
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: CodeModeSessionId, message: string): void {
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: CodeModeSessionId, failure: CodeModeResult): void {
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 jsonLiteral(constant) ?? "unknown";
138
+ return JSON.stringify(constant) ?? "unknown";
143
139
  }
144
- const enumValues = Array.isArray(record.enum) ? record.enum.map(jsonLiteral) : undefined;
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 successful result with optional JSON data. */
280
- export function createCodeModeSuccess(sessionId: string, data?: CodeModeJsonValue): CodeModeResult {
281
- return data === undefined
282
- ? { result: "success", sessionId }
283
- : { result: "success", sessionId, data };
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 result. */
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
- return { result: "failed", sessionId, error: { code, message } };
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`. */