@oh-my-pi/pi-coding-agent 14.1.0 → 14.1.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.
Files changed (82) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/package.json +8 -8
  3. package/src/async/job-manager.ts +43 -10
  4. package/src/commit/agentic/tools/analyze-file.ts +1 -2
  5. package/src/config/mcp-schema.json +1 -1
  6. package/src/config/model-equivalence.ts +1 -0
  7. package/src/config/model-registry.ts +63 -34
  8. package/src/config/model-resolver.ts +111 -15
  9. package/src/config/settings-schema.ts +4 -3
  10. package/src/config/settings.ts +1 -1
  11. package/src/cursor.ts +64 -23
  12. package/src/edit/index.ts +254 -89
  13. package/src/edit/modes/chunk.ts +336 -57
  14. package/src/edit/modes/hashline.ts +51 -26
  15. package/src/edit/modes/patch.ts +16 -10
  16. package/src/edit/modes/replace.ts +15 -7
  17. package/src/edit/renderer.ts +248 -94
  18. package/src/export/html/template.generated.ts +1 -1
  19. package/src/export/html/template.js +6 -4
  20. package/src/extensibility/custom-tools/types.ts +0 -3
  21. package/src/extensibility/extensions/loader.ts +16 -0
  22. package/src/extensibility/extensions/runner.ts +2 -7
  23. package/src/extensibility/extensions/types.ts +8 -4
  24. package/src/internal-urls/docs-index.generated.ts +3 -3
  25. package/src/ipy/executor.ts +447 -52
  26. package/src/ipy/kernel.ts +39 -13
  27. package/src/lsp/client.ts +54 -0
  28. package/src/lsp/index.ts +8 -0
  29. package/src/lsp/types.ts +6 -0
  30. package/src/main.ts +0 -1
  31. package/src/modes/acp/acp-agent.ts +4 -1
  32. package/src/modes/components/bash-execution.ts +16 -4
  33. package/src/modes/components/status-line/presets.ts +17 -6
  34. package/src/modes/components/status-line/segments.ts +15 -0
  35. package/src/modes/components/status-line-segment-editor.ts +1 -0
  36. package/src/modes/components/status-line.ts +7 -1
  37. package/src/modes/components/tool-execution.ts +145 -75
  38. package/src/modes/controllers/command-controller.ts +24 -1
  39. package/src/modes/controllers/event-controller.ts +4 -1
  40. package/src/modes/controllers/extension-ui-controller.ts +28 -5
  41. package/src/modes/controllers/input-controller.ts +9 -3
  42. package/src/modes/controllers/selector-controller.ts +4 -1
  43. package/src/modes/interactive-mode.ts +19 -3
  44. package/src/modes/print-mode.ts +13 -4
  45. package/src/modes/prompt-action-autocomplete.ts +3 -5
  46. package/src/modes/rpc/rpc-mode.ts +8 -2
  47. package/src/modes/shared.ts +2 -2
  48. package/src/modes/types.ts +1 -0
  49. package/src/modes/utils/ui-helpers.ts +1 -0
  50. package/src/prompts/tools/bash.md +2 -2
  51. package/src/prompts/tools/chunk-edit.md +191 -163
  52. package/src/prompts/tools/hashline.md +11 -11
  53. package/src/prompts/tools/patch.md +10 -5
  54. package/src/prompts/tools/{await.md → poll.md} +1 -1
  55. package/src/prompts/tools/read-chunk.md +3 -3
  56. package/src/prompts/tools/task.md +2 -2
  57. package/src/prompts/tools/vim.md +98 -0
  58. package/src/sdk.ts +754 -724
  59. package/src/session/agent-session.ts +164 -34
  60. package/src/session/session-manager.ts +50 -4
  61. package/src/slash-commands/builtin-registry.ts +17 -0
  62. package/src/task/executor.ts +4 -4
  63. package/src/task/index.ts +3 -5
  64. package/src/task/types.ts +2 -2
  65. package/src/tools/bash.ts +26 -8
  66. package/src/tools/find.ts +5 -2
  67. package/src/tools/grep.ts +77 -8
  68. package/src/tools/index.ts +48 -19
  69. package/src/tools/{await-tool.ts → poll-tool.ts} +36 -30
  70. package/src/tools/python.ts +293 -278
  71. package/src/tools/submit-result.ts +5 -2
  72. package/src/tools/todo-write.ts +8 -2
  73. package/src/tools/vim.ts +966 -0
  74. package/src/utils/edit-mode.ts +2 -1
  75. package/src/utils/session-color.ts +55 -0
  76. package/src/utils/title-generator.ts +15 -6
  77. package/src/vim/buffer.ts +309 -0
  78. package/src/vim/commands.ts +382 -0
  79. package/src/vim/engine.ts +2426 -0
  80. package/src/vim/parser.ts +151 -0
  81. package/src/vim/render.ts +252 -0
  82. package/src/vim/types.ts +197 -0
@@ -175,6 +175,7 @@ export class PythonTool implements AgentTool<typeof pythonSchema> {
175
175
  if (!this.session) {
176
176
  throw new ToolError("Python tool requires a session when not using proxy executor");
177
177
  }
178
+ const session = this.session;
178
179
 
179
180
  const { cells, timeout: rawTimeout = 30, cwd, reset } = params;
180
181
  // Clamp to reasonable range: 1s - 600s (10 min)
@@ -182,7 +183,10 @@ export class PythonTool implements AgentTool<typeof pythonSchema> {
182
183
  const timeoutMs = timeoutSec * 1000;
183
184
  const deadlineMs = Date.now() + timeoutMs;
184
185
  const timeoutSignal = AbortSignal.timeout(Math.max(0, deadlineMs - Date.now()));
185
- const combinedSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
186
+ const sessionAbortController = new AbortController();
187
+ const combinedSignal = signal
188
+ ? AbortSignal.any([signal, timeoutSignal, sessionAbortController.signal])
189
+ : AbortSignal.any([timeoutSignal, sessionAbortController.signal]);
186
190
  let outputSink: OutputSink | undefined;
187
191
  let outputSummary: OutputSummary | undefined;
188
192
  let outputDumped = false;
@@ -193,311 +197,322 @@ export class PythonTool implements AgentTool<typeof pythonSchema> {
193
197
  return outputSummary;
194
198
  };
195
199
 
196
- try {
197
- if (signal?.aborted) {
198
- throw new ToolAbortError();
199
- }
200
-
201
- const commandCwd = cwd ? resolveToCwd(cwd, this.session.cwd) : this.session.cwd;
202
- let cwdStat: fs.Stats;
200
+ const execution = (async (): Promise<AgentToolResult<PythonToolDetails | undefined>> => {
203
201
  try {
204
- cwdStat = await Bun.file(commandCwd).stat();
205
- } catch {
206
- throw new ToolError(`Working directory does not exist: ${commandCwd}`);
207
- }
208
- if (!cwdStat.isDirectory()) {
209
- throw new ToolError(`Working directory is not a directory: ${commandCwd}`);
210
- }
211
-
212
- const tailBuffer = new TailBuffer(DEFAULT_MAX_BYTES * 2);
213
- const jsonOutputs: unknown[] = [];
214
- const images: ImageContent[] = [];
215
- const statusEvents: PythonStatusEvent[] = [];
216
-
217
- const cellResults: PythonCellResult[] = cells.map((cell, index) => ({
218
- index,
219
- title: cell.title,
220
- code: cell.code,
221
- output: "",
222
- status: "pending",
223
- }));
224
- const cellOutputs: string[] = [];
225
-
226
- const appendTail = (text: string) => {
227
- tailBuffer.append(text);
228
- };
229
-
230
- const buildUpdateDetails = (): PythonToolDetails => {
231
- const details: PythonToolDetails = {
232
- cells: cellResults.map(cell => ({
233
- ...cell,
234
- statusEvents: cell.statusEvents ? [...cell.statusEvents] : undefined,
235
- })),
236
- };
237
- if (jsonOutputs.length > 0) {
238
- details.jsonOutputs = jsonOutputs;
239
- }
240
- if (images.length > 0) {
241
- details.images = images;
202
+ if (signal?.aborted) {
203
+ throw new ToolAbortError();
242
204
  }
243
- if (statusEvents.length > 0) {
244
- details.statusEvents = statusEvents;
245
- }
246
- return details;
247
- };
248
-
249
- const pushUpdate = () => {
250
- if (!onUpdate) return;
251
- const tailText = tailBuffer.text();
252
- onUpdate({
253
- content: [{ type: "text", text: tailText }],
254
- details: buildUpdateDetails(),
255
- });
256
- };
205
+ session.assertPythonExecutionAllowed?.();
257
206
 
258
- const sessionFile = this.session.getSessionFile?.() ?? undefined;
259
- const { path: artifactPath, id: artifactId } = (await this.session.allocateOutputArtifact?.("python")) ?? {};
260
- outputSink = new OutputSink({
261
- artifactPath,
262
- artifactId,
263
- onChunk: chunk => {
264
- appendTail(chunk);
265
- pushUpdate();
266
- },
267
- });
268
- const sessionId = sessionFile ? `session:${sessionFile}:cwd:${commandCwd}` : `cwd:${commandCwd}`;
269
-
270
- if (getPreludeDocs().length === 0) {
271
- const warmup = await warmPythonEnvironment(
272
- commandCwd,
273
- sessionId,
274
- this.session.settings.get("python.sharedGateway"),
275
- sessionFile ?? undefined,
276
- );
277
- if (!warmup.ok) {
278
- throw new ToolError(warmup.reason ?? "Python prelude helpers unavailable");
207
+ const commandCwd = cwd ? resolveToCwd(cwd, session.cwd) : session.cwd;
208
+ let cwdStat: fs.Stats;
209
+ try {
210
+ cwdStat = await Bun.file(commandCwd).stat();
211
+ } catch {
212
+ throw new ToolError(`Working directory does not exist: ${commandCwd}`);
213
+ }
214
+ if (!cwdStat.isDirectory()) {
215
+ throw new ToolError(`Working directory is not a directory: ${commandCwd}`);
279
216
  }
280
- }
281
-
282
- const baseExecutorOptions: Omit<PythonExecutorOptions, "reset"> = {
283
- cwd: commandCwd,
284
- deadlineMs,
285
- signal: combinedSignal,
286
- sessionId,
287
- kernelMode: this.session.settings.get("python.kernelMode"),
288
- useSharedGateway: this.session.settings.get("python.sharedGateway"),
289
- sessionFile: sessionFile ?? undefined,
290
- };
291
217
 
292
- for (let i = 0; i < cells.length; i++) {
293
- const cell = cells[i];
294
- const isFirstCell = i === 0;
295
- const cellResult = cellResults[i];
296
- cellResult.status = "running";
297
- cellResult.output = "";
298
- cellResult.statusEvents = undefined;
299
- cellResult.exitCode = undefined;
300
- cellResult.durationMs = undefined;
301
- pushUpdate();
302
-
303
- const executorOptions: PythonExecutorOptions = {
304
- ...baseExecutorOptions,
305
- reset: isFirstCell ? reset : false,
306
- onChunk: chunk => {
307
- outputSink!.push(chunk);
308
- },
218
+ const tailBuffer = new TailBuffer(DEFAULT_MAX_BYTES * 2);
219
+ const jsonOutputs: unknown[] = [];
220
+ const images: ImageContent[] = [];
221
+ const statusEvents: PythonStatusEvent[] = [];
222
+
223
+ const cellResults: PythonCellResult[] = cells.map((cell, index) => ({
224
+ index,
225
+ title: cell.title,
226
+ code: cell.code,
227
+ output: "",
228
+ status: "pending",
229
+ }));
230
+ const cellOutputs: string[] = [];
231
+
232
+ const appendTail = (text: string) => {
233
+ tailBuffer.append(text);
309
234
  };
310
235
 
311
- const startTime = Date.now();
312
- const result = await executePython(cell.code, executorOptions);
313
- const durationMs = Date.now() - startTime;
314
-
315
- const cellStatusEvents: PythonStatusEvent[] = [];
316
- let cellHasMarkdown = false;
317
- for (const output of result.displayOutputs) {
318
- if (output.type === "json") {
319
- jsonOutputs.push(output.data);
320
- }
321
- if (output.type === "image") {
322
- images.push({ type: "image", data: output.data, mimeType: output.mimeType });
236
+ const buildUpdateDetails = (): PythonToolDetails => {
237
+ const details: PythonToolDetails = {
238
+ cells: cellResults.map(cell => ({
239
+ ...cell,
240
+ statusEvents: cell.statusEvents ? [...cell.statusEvents] : undefined,
241
+ })),
242
+ };
243
+ if (jsonOutputs.length > 0) {
244
+ details.jsonOutputs = jsonOutputs;
323
245
  }
324
- if (output.type === "status") {
325
- statusEvents.push(output.event);
326
- cellStatusEvents.push(output.event);
246
+ if (images.length > 0) {
247
+ details.images = images;
327
248
  }
328
- if (output.type === "markdown") {
329
- cellHasMarkdown = true;
249
+ if (statusEvents.length > 0) {
250
+ details.statusEvents = statusEvents;
330
251
  }
331
- }
252
+ return details;
253
+ };
332
254
 
333
- const cellOutput = result.output.trim();
334
- cellResult.output = cellOutput;
335
- cellResult.exitCode = result.exitCode;
336
- cellResult.durationMs = durationMs;
337
- cellResult.statusEvents = cellStatusEvents.length > 0 ? cellStatusEvents : undefined;
338
- cellResult.hasMarkdown = cellHasMarkdown || undefined;
339
-
340
- let combinedCellOutput = "";
341
- if (cells.length > 1) {
342
- const cellHeader = `[${i + 1}/${cells.length}]`;
343
- const cellTitle = cell.title ? ` ${cell.title}` : "";
344
- if (cellOutput) {
345
- combinedCellOutput = `${cellHeader}${cellTitle}\n${cellOutput}`;
346
- } else {
347
- combinedCellOutput = `${cellHeader}${cellTitle} (ok)`;
255
+ const pushUpdate = () => {
256
+ if (!onUpdate) return;
257
+ const tailText = tailBuffer.text();
258
+ onUpdate({
259
+ content: [{ type: "text", text: tailText }],
260
+ details: buildUpdateDetails(),
261
+ });
262
+ };
263
+
264
+ const sessionFile = session.getSessionFile?.() ?? undefined;
265
+ const kernelOwnerId = session.getPythonKernelOwnerId?.() ?? undefined;
266
+ const { path: artifactPath, id: artifactId } = (await session.allocateOutputArtifact?.("python")) ?? {};
267
+ session.assertPythonExecutionAllowed?.();
268
+ outputSink = new OutputSink({
269
+ artifactPath,
270
+ artifactId,
271
+ onChunk: chunk => {
272
+ appendTail(chunk);
273
+ pushUpdate();
274
+ },
275
+ });
276
+ const sessionId = sessionFile ? `session:${sessionFile}:cwd:${commandCwd}` : `cwd:${commandCwd}`;
277
+
278
+ if (getPreludeDocs().length === 0) {
279
+ const warmup = await warmPythonEnvironment(
280
+ commandCwd,
281
+ sessionId,
282
+ session.settings.get("python.sharedGateway"),
283
+ sessionFile ?? undefined,
284
+ kernelOwnerId,
285
+ combinedSignal,
286
+ );
287
+ if (!warmup.ok) {
288
+ if (combinedSignal.aborted) throw new ToolAbortError();
289
+ throw new ToolError(warmup.reason ?? "Python prelude helpers unavailable");
348
290
  }
349
- cellOutputs.push(combinedCellOutput);
350
- } else if (cellOutput) {
351
- combinedCellOutput = cellOutput;
352
- cellOutputs.push(combinedCellOutput);
291
+ session.assertPythonExecutionAllowed?.();
353
292
  }
354
293
 
355
- if (combinedCellOutput) {
356
- const prefix = cellOutputs.length > 1 ? "\n\n" : "";
357
- appendTail(`${prefix}${combinedCellOutput}`);
358
- }
294
+ const baseExecutorOptions = {
295
+ cwd: commandCwd,
296
+ deadlineMs,
297
+ signal: combinedSignal,
298
+ sessionId,
299
+ kernelMode: session.settings.get("python.kernelMode"),
300
+ useSharedGateway: session.settings.get("python.sharedGateway"),
301
+ sessionFile: sessionFile ?? undefined,
302
+ kernelOwnerId,
303
+ };
359
304
 
360
- if (result.cancelled) {
361
- cellResult.status = "error";
305
+ for (let i = 0; i < cells.length; i++) {
306
+ const cell = cells[i];
307
+ const isFirstCell = i === 0;
308
+ const cellResult = cellResults[i];
309
+ cellResult.status = "running";
310
+ cellResult.output = "";
311
+ cellResult.statusEvents = undefined;
312
+ cellResult.exitCode = undefined;
313
+ cellResult.durationMs = undefined;
362
314
  pushUpdate();
363
- const errorMsg = result.output || "Command aborted";
364
- const combinedOutput = cellOutputs.join("\n\n");
365
- const outputText =
366
- cells.length > 1
367
- ? `${combinedOutput}\n\nCell ${i + 1} aborted: ${errorMsg}`
368
- : combinedOutput || errorMsg;
369
-
370
- const rawSummary = (await finalizeOutput()) ?? {
371
- output: "",
372
- truncated: false,
373
- totalLines: 0,
374
- totalBytes: 0,
375
- outputLines: 0,
376
- outputBytes: 0,
377
- };
378
- const outputLines = combinedOutput.length > 0 ? combinedOutput.split("\n").length : 0;
379
- const outputBytes = Buffer.byteLength(combinedOutput, "utf-8");
380
- const missingLines = Math.max(0, rawSummary.totalLines - rawSummary.outputLines);
381
- const missingBytes = Math.max(0, rawSummary.totalBytes - rawSummary.outputBytes);
382
- const summaryForMeta: OutputSummary = {
383
- output: combinedOutput,
384
- truncated: rawSummary.truncated,
385
- totalLines: outputLines + missingLines,
386
- totalBytes: outputBytes + missingBytes,
387
- outputLines,
388
- outputBytes,
389
- artifactId: rawSummary.artifactId,
390
- };
391
315
 
392
- const details: PythonToolDetails = {
393
- cells: cellResults,
394
- jsonOutputs: jsonOutputs.length > 0 ? jsonOutputs : undefined,
395
- images: images.length > 0 ? images : undefined,
396
- statusEvents: statusEvents.length > 0 ? statusEvents : undefined,
397
- isError: true,
316
+ const executorOptions: PythonExecutorOptions = {
317
+ ...baseExecutorOptions,
318
+ reset: isFirstCell ? reset : false,
319
+ onChunk: chunk => {
320
+ outputSink!.push(chunk);
321
+ },
398
322
  };
399
323
 
400
- return toolResult(details)
401
- .text(outputText)
402
- .truncationFromSummary(summaryForMeta, { direction: "tail" })
403
- .done();
404
- }
324
+ const startTime = Date.now();
325
+ const result = await executePython(cell.code, executorOptions);
326
+ const durationMs = Date.now() - startTime;
405
327
 
406
- if (result.exitCode !== 0 && result.exitCode !== undefined) {
407
- cellResult.status = "error";
408
- pushUpdate();
409
- const combinedOutput = cellOutputs.join("\n\n");
410
- const outputText =
411
- cells.length > 1
412
- ? `${combinedOutput}\n\nCell ${i + 1} failed (exit code ${result.exitCode}). Earlier cells succeeded—their state persists. Fix only cell ${i + 1}.`
413
- : combinedOutput
414
- ? `${combinedOutput}\n\nCommand exited with code ${result.exitCode}`
415
- : `Command exited with code ${result.exitCode}`;
416
-
417
- const rawSummary = (await finalizeOutput()) ?? {
418
- output: "",
419
- truncated: false,
420
- totalLines: 0,
421
- totalBytes: 0,
422
- outputLines: 0,
423
- outputBytes: 0,
424
- };
425
- const outputLines = combinedOutput.length > 0 ? combinedOutput.split("\n").length : 0;
426
- const outputBytes = Buffer.byteLength(combinedOutput, "utf-8");
427
- const missingLines = Math.max(0, rawSummary.totalLines - rawSummary.outputLines);
428
- const missingBytes = Math.max(0, rawSummary.totalBytes - rawSummary.outputBytes);
429
- const summaryForMeta: OutputSummary = {
430
- output: combinedOutput,
431
- truncated: rawSummary.truncated,
432
- totalLines: outputLines + missingLines,
433
- totalBytes: outputBytes + missingBytes,
434
- outputLines,
435
- outputBytes,
436
- artifactId: rawSummary.artifactId,
437
- };
328
+ const cellStatusEvents: PythonStatusEvent[] = [];
329
+ let cellHasMarkdown = false;
330
+ for (const output of result.displayOutputs) {
331
+ if (output.type === "json") {
332
+ jsonOutputs.push(output.data);
333
+ }
334
+ if (output.type === "image") {
335
+ images.push({ type: "image", data: output.data, mimeType: output.mimeType });
336
+ }
337
+ if (output.type === "status") {
338
+ statusEvents.push(output.event);
339
+ cellStatusEvents.push(output.event);
340
+ }
341
+ if (output.type === "markdown") {
342
+ cellHasMarkdown = true;
343
+ }
344
+ }
438
345
 
439
- const details: PythonToolDetails = {
440
- cells: cellResults,
441
- jsonOutputs: jsonOutputs.length > 0 ? jsonOutputs : undefined,
442
- images: images.length > 0 ? images : undefined,
443
- statusEvents: statusEvents.length > 0 ? statusEvents : undefined,
444
- isError: true,
445
- };
346
+ const cellOutput = result.output.trim();
347
+ cellResult.output = cellOutput;
348
+ cellResult.exitCode = result.exitCode;
349
+ cellResult.durationMs = durationMs;
350
+ cellResult.statusEvents = cellStatusEvents.length > 0 ? cellStatusEvents : undefined;
351
+ cellResult.hasMarkdown = cellHasMarkdown || undefined;
352
+
353
+ let combinedCellOutput = "";
354
+ if (cells.length > 1) {
355
+ const cellHeader = `[${i + 1}/${cells.length}]`;
356
+ const cellTitle = cell.title ? ` ${cell.title}` : "";
357
+ if (cellOutput) {
358
+ combinedCellOutput = `${cellHeader}${cellTitle}\n${cellOutput}`;
359
+ } else {
360
+ combinedCellOutput = `${cellHeader}${cellTitle} (ok)`;
361
+ }
362
+ cellOutputs.push(combinedCellOutput);
363
+ } else if (cellOutput) {
364
+ combinedCellOutput = cellOutput;
365
+ cellOutputs.push(combinedCellOutput);
366
+ }
446
367
 
447
- return toolResult(details)
448
- .text(outputText)
449
- .truncationFromSummary(summaryForMeta, { direction: "tail" })
450
- .done();
451
- }
368
+ if (combinedCellOutput) {
369
+ const prefix = cellOutputs.length > 1 ? "\n\n" : "";
370
+ appendTail(`${prefix}${combinedCellOutput}`);
371
+ }
452
372
 
453
- cellResult.status = "complete";
454
- pushUpdate();
455
- }
373
+ if (result.cancelled) {
374
+ cellResult.status = "error";
375
+ pushUpdate();
376
+ const errorMsg = result.output || "Command aborted";
377
+ const combinedOutput = cellOutputs.join("\n\n");
378
+ const outputText =
379
+ cells.length > 1
380
+ ? `${combinedOutput}\n\nCell ${i + 1} aborted: ${errorMsg}`
381
+ : combinedOutput || errorMsg;
382
+
383
+ const rawSummary = (await finalizeOutput()) ?? {
384
+ output: "",
385
+ truncated: false,
386
+ totalLines: 0,
387
+ totalBytes: 0,
388
+ outputLines: 0,
389
+ outputBytes: 0,
390
+ };
391
+ const outputLines = combinedOutput.length > 0 ? combinedOutput.split("\n").length : 0;
392
+ const outputBytes = Buffer.byteLength(combinedOutput, "utf-8");
393
+ const missingLines = Math.max(0, rawSummary.totalLines - rawSummary.outputLines);
394
+ const missingBytes = Math.max(0, rawSummary.totalBytes - rawSummary.outputBytes);
395
+ const summaryForMeta: OutputSummary = {
396
+ output: combinedOutput,
397
+ truncated: rawSummary.truncated,
398
+ totalLines: outputLines + missingLines,
399
+ totalBytes: outputBytes + missingBytes,
400
+ outputLines,
401
+ outputBytes,
402
+ artifactId: rawSummary.artifactId,
403
+ };
404
+
405
+ const details: PythonToolDetails = {
406
+ cells: cellResults,
407
+ jsonOutputs: jsonOutputs.length > 0 ? jsonOutputs : undefined,
408
+ images: images.length > 0 ? images : undefined,
409
+ statusEvents: statusEvents.length > 0 ? statusEvents : undefined,
410
+ isError: true,
411
+ };
412
+
413
+ return toolResult(details)
414
+ .text(outputText)
415
+ .truncationFromSummary(summaryForMeta, { direction: "tail" })
416
+ .done();
417
+ }
456
418
 
457
- const combinedOutput = cellOutputs.join("\n\n");
458
- const outputText =
459
- combinedOutput || (jsonOutputs.length > 0 || images.length > 0 ? "(no text output)" : "(no output)");
460
- const rawSummary = (await finalizeOutput()) ?? {
461
- output: "",
462
- truncated: false,
463
- totalLines: 0,
464
- totalBytes: 0,
465
- outputLines: 0,
466
- outputBytes: 0,
467
- };
468
- const outputLines = combinedOutput.length > 0 ? combinedOutput.split("\n").length : 0;
469
- const outputBytes = Buffer.byteLength(combinedOutput, "utf-8");
470
- const missingLines = Math.max(0, rawSummary.totalLines - rawSummary.outputLines);
471
- const missingBytes = Math.max(0, rawSummary.totalBytes - rawSummary.outputBytes);
472
- const summaryForMeta: OutputSummary = {
473
- output: combinedOutput,
474
- truncated: rawSummary.truncated,
475
- totalLines: outputLines + missingLines,
476
- totalBytes: outputBytes + missingBytes,
477
- outputLines,
478
- outputBytes,
479
- artifactId: rawSummary.artifactId,
480
- };
419
+ if (result.exitCode !== 0 && result.exitCode !== undefined) {
420
+ cellResult.status = "error";
421
+ pushUpdate();
422
+ const combinedOutput = cellOutputs.join("\n\n");
423
+ const outputText =
424
+ cells.length > 1
425
+ ? `${combinedOutput}\n\nCell ${i + 1} failed (exit code ${result.exitCode}). Earlier cells succeeded—their state persists. Fix only cell ${i + 1}.`
426
+ : combinedOutput
427
+ ? `${combinedOutput}\n\nCommand exited with code ${result.exitCode}`
428
+ : `Command exited with code ${result.exitCode}`;
429
+
430
+ const rawSummary = (await finalizeOutput()) ?? {
431
+ output: "",
432
+ truncated: false,
433
+ totalLines: 0,
434
+ totalBytes: 0,
435
+ outputLines: 0,
436
+ outputBytes: 0,
437
+ };
438
+ const outputLines = combinedOutput.length > 0 ? combinedOutput.split("\n").length : 0;
439
+ const outputBytes = Buffer.byteLength(combinedOutput, "utf-8");
440
+ const missingLines = Math.max(0, rawSummary.totalLines - rawSummary.outputLines);
441
+ const missingBytes = Math.max(0, rawSummary.totalBytes - rawSummary.outputBytes);
442
+ const summaryForMeta: OutputSummary = {
443
+ output: combinedOutput,
444
+ truncated: rawSummary.truncated,
445
+ totalLines: outputLines + missingLines,
446
+ totalBytes: outputBytes + missingBytes,
447
+ outputLines,
448
+ outputBytes,
449
+ artifactId: rawSummary.artifactId,
450
+ };
451
+
452
+ const details: PythonToolDetails = {
453
+ cells: cellResults,
454
+ jsonOutputs: jsonOutputs.length > 0 ? jsonOutputs : undefined,
455
+ images: images.length > 0 ? images : undefined,
456
+ statusEvents: statusEvents.length > 0 ? statusEvents : undefined,
457
+ isError: true,
458
+ };
459
+
460
+ return toolResult(details)
461
+ .text(outputText)
462
+ .truncationFromSummary(summaryForMeta, { direction: "tail" })
463
+ .done();
464
+ }
481
465
 
482
- const details: PythonToolDetails = {
483
- cells: cellResults,
484
- jsonOutputs: jsonOutputs.length > 0 ? jsonOutputs : undefined,
485
- images: images.length > 0 ? images : undefined,
486
- statusEvents: statusEvents.length > 0 ? statusEvents : undefined,
487
- };
466
+ cellResult.status = "complete";
467
+ pushUpdate();
468
+ }
488
469
 
489
- const resultBuilder = toolResult(details)
490
- .text(outputText)
491
- .truncationFromSummary(summaryForMeta, { direction: "tail" });
470
+ const combinedOutput = cellOutputs.join("\n\n");
471
+ const outputText =
472
+ combinedOutput || (jsonOutputs.length > 0 || images.length > 0 ? "(no text output)" : "(no output)");
473
+ const rawSummary = (await finalizeOutput()) ?? {
474
+ output: "",
475
+ truncated: false,
476
+ totalLines: 0,
477
+ totalBytes: 0,
478
+ outputLines: 0,
479
+ outputBytes: 0,
480
+ };
481
+ const outputLines = combinedOutput.length > 0 ? combinedOutput.split("\n").length : 0;
482
+ const outputBytes = Buffer.byteLength(combinedOutput, "utf-8");
483
+ const missingLines = Math.max(0, rawSummary.totalLines - rawSummary.outputLines);
484
+ const missingBytes = Math.max(0, rawSummary.totalBytes - rawSummary.outputBytes);
485
+ const summaryForMeta: OutputSummary = {
486
+ output: combinedOutput,
487
+ truncated: rawSummary.truncated,
488
+ totalLines: outputLines + missingLines,
489
+ totalBytes: outputBytes + missingBytes,
490
+ outputLines,
491
+ outputBytes,
492
+ artifactId: rawSummary.artifactId,
493
+ };
492
494
 
493
- return resultBuilder.done();
494
- } finally {
495
- if (!outputDumped) {
496
- try {
497
- await finalizeOutput();
498
- } catch {}
495
+ const details: PythonToolDetails = {
496
+ cells: cellResults,
497
+ jsonOutputs: jsonOutputs.length > 0 ? jsonOutputs : undefined,
498
+ images: images.length > 0 ? images : undefined,
499
+ statusEvents: statusEvents.length > 0 ? statusEvents : undefined,
500
+ };
501
+
502
+ const resultBuilder = toolResult(details)
503
+ .text(outputText)
504
+ .truncationFromSummary(summaryForMeta, { direction: "tail" });
505
+
506
+ return resultBuilder.done();
507
+ } finally {
508
+ if (!outputDumped) {
509
+ try {
510
+ await finalizeOutput();
511
+ } catch {}
512
+ }
499
513
  }
500
- }
514
+ })();
515
+ return await (session.trackPythonExecution?.(execution, sessionAbortController) ?? execution);
501
516
  }
502
517
  }
503
518
 
@@ -57,7 +57,8 @@ export class SubmitResultTool implements AgentTool<TSchema, SubmitResultDetails>
57
57
  readonly label = "Submit Result";
58
58
  readonly description =
59
59
  "Finish the task with structured JSON output. Call exactly once at the end of the task.\n\n" +
60
- "If you cannot complete the task, call with an error message payload.";
60
+ 'Pass `result: { data: <your output> }` for success, or `result: { error: "message" }` for failure.\n' +
61
+ "The `data`/`error` wrapper is required — do not put your output directly in `result`.";
61
62
  readonly parameters: TSchema;
62
63
  strict = true;
63
64
  lenientArgValidation = true;
@@ -171,7 +172,9 @@ export class SubmitResultTool implements AgentTool<TSchema, SubmitResultDetails>
171
172
  throw new Error("result cannot contain both data and error");
172
173
  }
173
174
  if (errorMessage === undefined && data === undefined) {
174
- throw new Error("result must contain either data or error");
175
+ throw new Error(
176
+ 'result must contain either `data` or `error`. Use `{result: {data: <your output>}}` for success or `{result: {error: "message"}}` for failure.',
177
+ );
175
178
  }
176
179
 
177
180
  const status = errorMessage !== undefined ? "aborted" : "success";
@@ -251,7 +251,9 @@ function applyOps(file: TodoFile, ops: TodoWriteParams["ops"]): { file: TodoFile
251
251
  case "update": {
252
252
  const task = findTask(file.phases, op.id);
253
253
  if (!task) {
254
- errors.push(`Task "${op.id}" not found`);
254
+ const totalTasks = file.phases.reduce((sum, p) => sum + p.tasks.length, 0);
255
+ const hint = totalTasks === 0 ? " (todo list is empty — was it replaced or not yet created?)" : "";
256
+ errors.push(`Task "${op.id}" not found${hint}`);
255
257
  break;
256
258
  }
257
259
  if (op.status !== undefined) task.status = op.status;
@@ -271,7 +273,11 @@ function applyOps(file: TodoFile, ops: TodoWriteParams["ops"]): { file: TodoFile
271
273
  break;
272
274
  }
273
275
  }
274
- if (!removed) errors.push(`Task "${op.id}" not found`);
276
+ if (!removed) {
277
+ const totalTasks = file.phases.reduce((sum, p) => sum + p.tasks.length, 0);
278
+ const hint = totalTasks === 0 ? " (todo list is empty)" : "";
279
+ errors.push(`Task "${op.id}" not found${hint}`);
280
+ }
275
281
  break;
276
282
  }
277
283
  }