@tea-agent/loop-agent 0.40.0-next.4 → 0.40.0-next.6

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 (134) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/build-stamp.json +3 -3
  3. package/dist/executors/shell-executor.js +4 -1
  4. package/dist/infrastructure/harness/atomic-write.js +23 -0
  5. package/dist/worker/console/chat/chat-event-store.js +62 -4
  6. package/dist/worker/console/chat/context-panel.js +7 -0
  7. package/dist/worker/console/chat/deferred-turn.js +12 -0
  8. package/dist/worker/console/chat/pi-runtime.js +264 -16
  9. package/dist/worker/console/chat/provider-error.js +98 -0
  10. package/dist/worker/console/chat/routes.js +292 -29
  11. package/dist/worker/console/chat/session-stats.js +179 -0
  12. package/dist/worker/console/chat/session-store.js +93 -63
  13. package/dist/worker/console/chat/todos.js +115 -0
  14. package/dist/worker/console/chat/tool-preview.js +42 -0
  15. package/dist/worker/console/chat/turn-process.js +20 -55
  16. package/dist/worker/console/chat/user-questions.js +266 -0
  17. package/dist/worker/console/chat/workspace-landing.js +14 -0
  18. package/dist/worker/console/native-directory-picker.js +13 -0
  19. package/dist/worker/console/routes.js +309 -1
  20. package/dist/worker/console/security.js +4 -4
  21. package/dist/worker/console/server.js +529 -186
  22. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-CTx1TrNa.js → abnfDiagram-N423BO3Z-DGt5TRsk.js} +1 -1
  23. package/dist/worker/console/static/assets/{arc-B4i9cVxX.js → arc-DKBSJsXm.js} +1 -1
  24. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-BAXE8_Rl.js → architectureDiagram-T3A2C74G-BnhooRBs.js} +1 -1
  25. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-C5Xh59at.js → blockDiagram-VBNYF7ZC-Bm9qkrJP.js} +1 -1
  26. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-B_asOzAu.js → c4Diagram-5PPSVZJV-B2cFh4bO.js} +1 -1
  27. package/dist/worker/console/static/assets/channel-DSgFX07x.js +1 -0
  28. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-wRO_iRNc.js → chunk-2GRJ4B5K-DhvWRI03.js} +1 -1
  29. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-ByOaSh_G.js → chunk-2Q5K7J3B-chzTP_2w.js} +1 -1
  30. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DjLzudan.js → chunk-5RXB4S5H-SdWIR12m.js} +1 -1
  31. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-JCNGkXYT.js → chunk-5VM5RSS4-C2-8Eo_A.js} +1 -1
  32. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BhrPXXMZ.js → chunk-6Q2QTUOP-DvbE1yQW.js} +1 -1
  33. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-TJ5JuEBT.js → chunk-GF5L2VYU-CEenyPTz.js} +1 -1
  34. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-VVthn_mD.js → chunk-JWPE2WC7-CcaZrq0k.js} +1 -1
  35. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-D60-Mny4.js → chunk-KBJHAD2P-FGngOTto.js} +1 -1
  36. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-zXv7aVye.js → chunk-RYQCIY6F-jT_yX3sf.js} +1 -1
  37. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-By9VQoi1.js → chunk-XXDRQBXY-CjE9LQOM.js} +1 -1
  38. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-unJObRls.js +1 -0
  39. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-unJObRls.js +1 -0
  40. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-C4zQLe-M.js → cose-bilkent-JH36ORCC-GQWns1tb.js} +1 -1
  41. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-wh5N2ReN.js → cynefin-VYW2F7L2-CGRpxWNJ.js} +1 -1
  42. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Dlu5wIqs.js → cynefinDiagram-MW4NZA55-Cd3uFGE4.js} +1 -1
  43. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CrDgthc6.js → dagre-VZM6K2ZE-DmDHuUW0.js} +1 -1
  44. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-zN1clg27.js → diagram-7IWD3JNH-kAr--hF3.js} +1 -1
  45. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BN-o7J7-.js → diagram-B4RE2ZJO-woahL2Oh.js} +1 -1
  46. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-C8SuPt0d.js → diagram-LBJQPF4R-7syNVaNf.js} +1 -1
  47. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BoizuHbZ.js → diagram-Q27KOJAE-BELPXiaY.js} +1 -1
  48. package/dist/worker/console/static/assets/{diagram-UB23O5K3-BzUzXixG.js → diagram-UB23O5K3-CrPa4aXi.js} +1 -1
  49. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Ev3lA4Cf.js → ebnfDiagram-BXEA7PRR-B48u2VVe.js} +1 -1
  50. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-GALUzRbs.js → erDiagram-JOGREHBK-DhBTfwTF.js} +1 -1
  51. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CZJN65Sb.js → flowDiagram-UKHOOZJN-CAeZ5nDU.js} +1 -1
  52. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-JLB9OESl.js → ganttDiagram-PKOTCBZU-CkIFeFLN.js} +1 -1
  53. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-CcvCr4GD.js → gitGraphDiagram-DS77QQ5N-t0HGYDel.js} +1 -1
  54. package/dist/worker/console/static/assets/index-B4l5ag79.js +407 -0
  55. package/dist/worker/console/static/assets/index-CwVHWB71.css +1 -0
  56. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-HG5z7aVg.js → infoDiagram-6WML65LV-WM0HZhe4.js} +1 -1
  57. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-BHEu9chR.js → ishikawaDiagram-WSZJBQD7-CV5byHe0.js} +1 -1
  58. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-Cp_Up8-a.js → journeyDiagram-NVQOT4AX-lIyWkgnw.js} +1 -1
  59. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-DvVeY-em.js → kanban-definition-27J2QSJJ-BIH4YNRY.js} +1 -1
  60. package/dist/worker/console/static/assets/{linear-aGmcjgTn.js → linear-lQ0ZGrWc.js} +1 -1
  61. package/dist/worker/console/static/assets/{mermaid.core-Biyo-tqA.js → mermaid.core-BYdXtke8.js} +5 -5
  62. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-DyxY2pIb.js → mindmap-definition-FAOFIHXS-k93vkIa4.js} +1 -1
  63. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-CkdGuc50.js → pegDiagram-VL7TDLO6-Bm6AqvSy.js} +1 -1
  64. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-NNQI9xVR.js → pieDiagram-7S7Q4E2Y-DnJW6Xpf.js} +1 -1
  65. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-PAJkuz_G.js → quadrantDiagram-CIZ2JOQS-CMzm4ObL.js} +1 -1
  66. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-ZwrQod0O.js → railroadDiagram-AXF67PYL-fwFF5dFQ.js} +1 -1
  67. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-D3ZfrzM2.js → requirementDiagram-LRYGKXZP-tzl9VDUc.js} +1 -1
  68. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-DDGFmWk5.js → sankeyDiagram-W5VNT64P-B26V09DD.js} +1 -1
  69. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DO6mmXTA.js → sequenceDiagram-SI44F4Z6-D2npyCeP.js} +1 -1
  70. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DMbCQiJt.js → sizeCapture-X5ZJPWSS-CTJtkRU2.js} +1 -1
  71. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Di_YUEfC.js → stateDiagram-OKZ733FA-B8lk4Lzu.js} +1 -1
  72. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BmOcMTJE.js +1 -0
  73. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-Bn2PphNJ.js → swimlanes-SLNWSIFB-C3yP53hH.js} +2 -2
  74. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Beah-dq0.js +8 -0
  75. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DPh8pRsp.js → timeline-definition-Z64GVDOM-Bck7xjhe.js} +1 -1
  76. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-GMfl26pW.js → vennDiagram-T6HMQDX7-CCntr1w7.js} +1 -1
  77. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DTlmVrmF.js → wardleyDiagram-T6FBY63Y-DsCqCBZz.js} +1 -1
  78. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-YGBjKAS5.js → xychartDiagram-ELKLHX3M-Br2l90OM.js} +1 -1
  79. package/dist/worker/console/static/index.html +2 -2
  80. package/dist/worker/console/static-src/app/console-types.js +29 -4
  81. package/dist/worker/console/static-src/app/useConsoleShell.js +30 -12
  82. package/dist/worker/console/static-src/app/useOperatorActions.js +36 -6
  83. package/dist/worker/console/static-src/app/useRecoveryConsole.js +10 -11
  84. package/dist/worker/console/static-src/app/useRunProgress.js +2 -1
  85. package/dist/worker/console/static-src/app/useTaskWizard.js +2 -2
  86. package/dist/worker/console/static-src/night/useNightBoard.js +8 -5
  87. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +140 -8
  88. package/dist/worker/console/static-src/operator-chat/details-dag-actions.js +122 -0
  89. package/dist/worker/console/static-src/operator-chat/refs.js +3 -0
  90. package/dist/worker/console/static-src/operator-chat/runtime-selection-labels.js +27 -0
  91. package/dist/worker/console/static-src/operator-chat/turn-submission.js +3 -2
  92. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +168 -72
  93. package/dist/worker/console/static-src/operator-chat/useChatStream.js +78 -17
  94. package/dist/worker/console/static-src/operator-chat/useChatThread.js +184 -4
  95. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +63 -9
  96. package/dist/worker/console/static-src/operator-chat/useWorkspaceBrowserGate.js +4 -0
  97. package/dist/worker/console/static-src/operator-chat/workspace-layout-mode.js +3 -3
  98. package/dist/worker/console/static-src/shell/useWorkspaces.js +299 -0
  99. package/dist/worker/console/static-src/shell/workspace-route.js +337 -0
  100. package/dist/worker/console/workspace-context.js +234 -0
  101. package/dist/worker/console/workspace-registry.js +214 -0
  102. package/dist/worker/observe/server.js +4 -0
  103. package/dist/worker/observe/static/api.js +26 -6
  104. package/dist/worker/observe/static/app.js +18 -70
  105. package/dist/worker/observe/static/index.html +1 -1
  106. package/dist/worker/observe/static/inspect-workspace.js +307 -0
  107. package/dist/worker/observe/static/operator-chrome.css +145 -20
  108. package/dist/worker/observe/static/operator-chrome.d.ts +20 -3
  109. package/dist/worker/observe/static/operator-chrome.js +330 -100
  110. package/dist/worker/observe/static/router.d.ts +46 -0
  111. package/dist/worker/observe/static/router.js +25 -1
  112. package/dist/worker/observe/static/state.d.ts +50 -0
  113. package/dist/worker/observe/static/state.js +172 -0
  114. package/dist/worker/observe/static/views/dag-inspector.js +3 -2
  115. package/dist/worker/observe/static/views/dag.js +2 -1
  116. package/dist/worker/observe/static/views/dashboard.js +6 -4
  117. package/dist/worker/observe/static/views/night.js +2 -2
  118. package/dist/worker/observe/static/views/pool.js +1 -0
  119. package/dist/worker/observe/static/views/run.js +3 -1
  120. package/dist/worker/observe/static/views/task.js +1 -1
  121. package/dist/workflows/dag/backend-test-pytest-collection.js +11 -10
  122. package/dist/workflows/dag/interrupt-request.js +3 -3
  123. package/dist/workflows/dag/lifecycle.js +3 -3
  124. package/docs/README.md +1 -0
  125. package/docs/operations/local-development-environment.md +21 -0
  126. package/harness.json +3 -3
  127. package/package.json +3 -1
  128. package/dist/worker/console/static/assets/channel-Dj_4Moei.js +0 -1
  129. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C0YYAyCa.js +0 -1
  130. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C0YYAyCa.js +0 -1
  131. package/dist/worker/console/static/assets/index-CW88Fjbw.js +0 -406
  132. package/dist/worker/console/static/assets/index-DP0ZJl39.css +0 -1
  133. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DrdDvela.js +0 -1
  134. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DPkEAULl.js +0 -8
@@ -29,6 +29,7 @@ import { createHash, randomBytes } from "node:crypto";
29
29
  import { access } from "node:fs/promises";
30
30
  import path from "node:path";
31
31
  import { extractAssistantContentBlocks, extractAssistantText, isAssistantSdkMessage, } from "./assistant-content.js";
32
+ import { parseProviderError, } from "./provider-error.js";
32
33
  import { projectCompactSnapshot, } from "./chat-event-store.js";
33
34
  import { extractUsageSample } from "./usage.js";
34
35
  import { OPERATOR_CHAT_ALLOWED_TOOLS, authorizeOperatorChatTool, assertNoWriteToolInList, } from "./tools.js";
@@ -38,6 +39,7 @@ import { buildModelCallableToolSchemas } from "./tool-adapter.js";
38
39
  import { resolveDefaultChatModel, resolveLowChatModel, } from "./model-resolver.js";
39
40
  import { filterActiveInterviewTools } from "../interview/tools.js";
40
41
  import { RUNTIME_CONTEXT_TEXT_MAX, redactRuntimeText, } from "./runtime-context.js";
42
+ import { buildTodoContinuationContext, } from "./todos.js";
41
43
  /**
42
44
  * Base system prompt fragment every General Operator Chat session receives.
43
45
  *
@@ -210,6 +212,51 @@ async function bindSessionExtensions(session) {
210
212
  function pinOperatorChatTools(session) {
211
213
  session.setActiveToolsByName(computeOperatorChatActiveToolNames(session));
212
214
  }
215
+ /**
216
+ * Pi has no hook result that merely appends instructions to its built-in
217
+ * automatic compactor: session_before_compact must either cancel or replace
218
+ * the whole compaction result. Keep the SDK algorithm intact and inject the
219
+ * current Todo as ephemeral context only after a successful compaction. The
220
+ * next turn resets the marker and TodoListRegistry clears the list.
221
+ */
222
+ export function createTodoCompactionContextExtension(options) {
223
+ return {
224
+ name: "operator-chat-todo-compaction-context",
225
+ hidden: true,
226
+ factory: (api) => {
227
+ let compactedInCurrentTurn = false;
228
+ api.on("turn_start", () => {
229
+ compactedInCurrentTurn = false;
230
+ });
231
+ api.on("session_compact", () => {
232
+ compactedInCurrentTurn = true;
233
+ });
234
+ api.on("context", (rawEvent) => {
235
+ if (!compactedInCurrentTurn)
236
+ return undefined;
237
+ const context = buildTodoContinuationContext(options.registry.get(options.sessionId));
238
+ if (!context)
239
+ return undefined;
240
+ const messages = Array.isArray(rawEvent.messages)
241
+ ? rawEvent.messages
242
+ : [];
243
+ return {
244
+ messages: [
245
+ ...messages,
246
+ {
247
+ role: "custom",
248
+ customType: "operator-chat-todo-context",
249
+ content: context,
250
+ display: false,
251
+ details: { todos: options.registry.get(options.sessionId) },
252
+ timestamp: Date.now(),
253
+ },
254
+ ],
255
+ };
256
+ });
257
+ },
258
+ };
259
+ }
213
260
  /**
214
261
  * Build Pi custom ToolDefinition objects for every whitelisted operator action.
215
262
  * Each tool's execute() routes into the operator action dispatcher (Gate 3).
@@ -289,6 +336,76 @@ async function buildCustomOperatorTools(options) {
289
336
  });
290
337
  });
291
338
  }
339
+ const ASK_USER_QUESTION_DESCRIPTION = "Ask the user concise questions when a confirmation, choice, or missing detail is required before proceeding. " +
340
+ "The tool pauses until the user answers. Use it instead of writing questions as ordinary assistant text. " +
341
+ "Send one or more questions with stable ids; recommended options should be first and end with (Recommended).";
342
+ async function buildAskUserQuestionTool(options) {
343
+ const [{ Type }, { defineTool }] = await Promise.all([
344
+ import("typebox"),
345
+ import("@earendil-works/pi-coding-agent"),
346
+ ]);
347
+ return defineTool({
348
+ name: "ask_user_question",
349
+ label: "Ask user",
350
+ description: ASK_USER_QUESTION_DESCRIPTION,
351
+ promptSnippet: ASK_USER_QUESTION_DESCRIPTION,
352
+ parameters: Type.Object({
353
+ questions: Type.Array(Type.Object({
354
+ id: Type.String({ description: "Stable question id echoed in the answer." }),
355
+ question: Type.String({ description: "Specific user-facing question." }),
356
+ header: Type.Optional(Type.String({ description: "Optional short heading." })),
357
+ detail: Type.Optional(Type.String({ description: "Optional Markdown context shown below the question." })),
358
+ options: Type.Optional(Type.Array(Type.Object({
359
+ label: Type.String({ description: "Short user-facing option label." }),
360
+ description: Type.Optional(Type.String({ description: "One sentence explaining impact or tradeoff." })),
361
+ }, { additionalProperties: false }))),
362
+ multi_select: Type.Optional(Type.Boolean({ description: "Allow more than one option." })),
363
+ }, { additionalProperties: false }), { minItems: 1, maxItems: 20 }),
364
+ }, { additionalProperties: false }),
365
+ async execute(toolCallId, params, signal) {
366
+ const answer = await options.registry.ask({
367
+ sessionId: options.sessionId,
368
+ toolCallId,
369
+ questions: params.questions,
370
+ signal,
371
+ });
372
+ return {
373
+ content: [{ type: "text", text: JSON.stringify(answer) }],
374
+ details: answer,
375
+ };
376
+ },
377
+ });
378
+ }
379
+ const TODO_WRITE_DESCRIPTION = "Record and update a structured task list for the current work. Send the ENTIRE list every call — it REPLACES the previous list. Use it for multi-step work; skip trivial single-step tasks. Mark tasks pending, in_progress, or completed, and mark completed immediately when done.";
380
+ async function buildTodoWriteTool(options) {
381
+ const [{ Type }, { defineTool }] = await Promise.all([
382
+ import("typebox"),
383
+ import("@earendil-works/pi-coding-agent"),
384
+ ]);
385
+ return defineTool({
386
+ name: "todo_write",
387
+ label: "Update todo list",
388
+ description: TODO_WRITE_DESCRIPTION,
389
+ promptSnippet: TODO_WRITE_DESCRIPTION,
390
+ parameters: Type.Object({
391
+ todos: Type.Array(Type.Object({
392
+ content: Type.String({ description: "Short imperative task description." }),
393
+ status: Type.Union([
394
+ Type.Literal("pending"),
395
+ Type.Literal("in_progress"),
396
+ Type.Literal("completed"),
397
+ ]),
398
+ }, { additionalProperties: false })),
399
+ }, { additionalProperties: false }),
400
+ async execute(toolCallId, params) {
401
+ const result = options.registry.write(options.sessionId, toolCallId, params.todos);
402
+ return {
403
+ content: [{ type: "text", text: JSON.stringify(result) }],
404
+ details: result,
405
+ };
406
+ },
407
+ });
408
+ }
292
409
  /**
293
410
  * Build the safe explore custom tools (roadmap M0-A / G16): safe-read,
294
411
  * safe-grep, gitStatus, gitDiff. These replace the bare SDK bash/read/grep so
@@ -765,9 +882,26 @@ export class ConsolePiRuntime {
765
882
  * after a PATCH sees the latest resource-preferences.json (not create-time
766
883
  * closure state).
767
884
  */
768
- async buildResourceLoaderOptions(base = {}) {
885
+ async buildResourceLoaderOptions(base = {}, sessionId) {
886
+ const inheritedFactories = Array.isArray(base.extensionFactories)
887
+ ? base.extensionFactories
888
+ : [];
889
+ const todoContextExtension = sessionId && this.options.todoRegistry
890
+ ? createTodoCompactionContextExtension({
891
+ sessionId,
892
+ registry: this.options.todoRegistry,
893
+ })
894
+ : undefined;
769
895
  return {
770
896
  ...base,
897
+ ...(todoContextExtension
898
+ ? {
899
+ extensionFactories: [
900
+ ...inheritedFactories,
901
+ todoContextExtension,
902
+ ],
903
+ }
904
+ : {}),
771
905
  skillsOverride: (current) => {
772
906
  // Synchronous override surface: use last-known prefs via a sync
773
907
  // fail-closed path. Prefer async cache filled by ensurePrefs.
@@ -969,9 +1103,7 @@ export class ConsolePiRuntime {
969
1103
  const { services } = await this.bindings.createServices({
970
1104
  cwd: this.options.cwd,
971
1105
  agentDir,
972
- resourceLoaderOptions: await this.buildResourceLoaderOptions({
973
- appendSystemPrompt,
974
- }),
1106
+ resourceLoaderOptions: await this.buildResourceLoaderOptions({ appendSystemPrompt }, sessionId),
975
1107
  });
976
1108
  if (this.disposedSessions.has(sessionId))
977
1109
  return;
@@ -988,7 +1120,7 @@ export class ConsolePiRuntime {
988
1120
  }) ?? undefined)
989
1121
  : undefined;
990
1122
  assertNoWriteToolInList(OPERATOR_CHAT_ALLOWED_TOOLS);
991
- const customTools = await this.loadCustomTools();
1123
+ const customTools = await this.loadCustomTools(sessionId);
992
1124
  if (this.disposedSessions.has(sessionId))
993
1125
  return;
994
1126
  // pi-web parity: do NOT pass `tools`. A closed allowlist becomes
@@ -1045,12 +1177,26 @@ export class ConsolePiRuntime {
1045
1177
  }
1046
1178
  }
1047
1179
  /** Cached session-less custom tool definitions (operator + explore). */
1048
- async loadCustomTools() {
1180
+ async loadCustomTools(sessionId) {
1049
1181
  const [operatorCustomTools, exploreCustomTools] = await Promise.all([
1050
1182
  this.loadOperatorCustomTools(),
1051
1183
  this.loadExploreCustomTools(),
1052
1184
  ]);
1053
- return [...operatorCustomTools, ...exploreCustomTools];
1185
+ const questionTool = this.options.questionRegistry
1186
+ ? await buildAskUserQuestionTool({
1187
+ sessionId,
1188
+ registry: this.options.questionRegistry,
1189
+ })
1190
+ : undefined;
1191
+ const todoTool = this.options.todoRegistry
1192
+ ? await buildTodoWriteTool({ sessionId, registry: this.options.todoRegistry })
1193
+ : undefined;
1194
+ return [
1195
+ ...operatorCustomTools,
1196
+ ...exploreCustomTools,
1197
+ ...(questionTool ? [questionTool] : []),
1198
+ ...(todoTool ? [todoTool] : []),
1199
+ ];
1054
1200
  }
1055
1201
  loadOperatorCustomTools() {
1056
1202
  if (!this.options.actionContext)
@@ -1090,6 +1236,7 @@ export class ConsolePiRuntime {
1090
1236
  targetSessionId: sessionId,
1091
1237
  });
1092
1238
  const built = await this.buildSessionWithServices({
1239
+ sessionId,
1093
1240
  sessionManager,
1094
1241
  model: init.model,
1095
1242
  });
@@ -1817,6 +1964,7 @@ export class ConsolePiRuntime {
1817
1964
  }
1818
1965
  try {
1819
1966
  const built = await this.buildSessionWithServices({
1967
+ sessionId,
1820
1968
  sessionManager,
1821
1969
  model: init.model,
1822
1970
  systemPromptSuffix: init.systemPromptSuffix,
@@ -1907,9 +2055,7 @@ export class ConsolePiRuntime {
1907
2055
  const { services } = await this.bindings.createServices({
1908
2056
  cwd: this.options.cwd,
1909
2057
  agentDir,
1910
- resourceLoaderOptions: await this.buildResourceLoaderOptions({
1911
- appendSystemPrompt,
1912
- }),
2058
+ resourceLoaderOptions: await this.buildResourceLoaderOptions({ appendSystemPrompt }, init.sessionId),
1913
2059
  });
1914
2060
  const modelRuntime = services.modelRuntime;
1915
2061
  const resolvedModel = init.model
@@ -1920,7 +2066,7 @@ export class ConsolePiRuntime {
1920
2066
  }) ?? undefined)
1921
2067
  : undefined;
1922
2068
  assertNoWriteToolInList(OPERATOR_CHAT_ALLOWED_TOOLS);
1923
- const customTools = await this.loadCustomTools();
2069
+ const customTools = await this.loadCustomTools(init.sessionId);
1924
2070
  const { session } = await this.bindings.createSessionFromServices({
1925
2071
  services,
1926
2072
  sessionManager: init.sessionManager,
@@ -1943,6 +2089,16 @@ export class ConsolePiRuntime {
1943
2089
  open(sessionId) {
1944
2090
  return this.sessions.get(sessionId);
1945
2091
  }
2092
+ /** Inject one queued message into the currently running Pi turn. */
2093
+ async steer(sessionId, text, images) {
2094
+ const session = this.sessions.get(sessionId);
2095
+ if (!session || !this.promptOwners.has(sessionId) || session.isStreaming === false) {
2096
+ throw new Error("CHAT_TURN_NOT_ACTIVE");
2097
+ }
2098
+ if (typeof session.steer !== "function")
2099
+ throw new Error("CHAT_STEER_UNAVAILABLE");
2100
+ await session.steer(text, images);
2101
+ }
1946
2102
  hasSession(sessionId) {
1947
2103
  return this.sessions.has(sessionId);
1948
2104
  }
@@ -1997,6 +2153,12 @@ export class ConsolePiRuntime {
1997
2153
  }
1998
2154
  };
1999
2155
  try {
2156
+ if (options?.turnId && this.options.questionRegistry) {
2157
+ this.options.questionRegistry.setTurn(sessionId, options.turnId);
2158
+ }
2159
+ if (options?.turnId && this.options.todoRegistry) {
2160
+ this.options.todoRegistry.startTurn(sessionId, options.turnId);
2161
+ }
2000
2162
  // AC-FIX-002: if Stop already fired before readiness, never start model work.
2001
2163
  if (options?.signal?.aborted) {
2002
2164
  return;
@@ -2050,19 +2212,77 @@ export class ConsolePiRuntime {
2050
2212
  }
2051
2213
  };
2052
2214
  emitContextUsage();
2215
+ let stepStartedAt;
2216
+ let firstTokenAt;
2217
+ let pendingProviderError;
2218
+ let providerErrorEmitted = false;
2219
+ const emitPendingProviderError = () => {
2220
+ if (!pendingProviderError)
2221
+ return;
2222
+ onEvent(pendingProviderError);
2223
+ pendingProviderError = undefined;
2224
+ providerErrorEmitted = true;
2225
+ };
2053
2226
  unsub = session.subscribe((event) => {
2227
+ const observedAt = Date.now();
2054
2228
  const mapped = mapSdkEvent(sessionId, event);
2055
- if (mapped)
2056
- onEvent(mapped);
2229
+ if (mapped?.type === "error") {
2230
+ // Pi represents provider failures as an assistant message_end and may
2231
+ // retry internally. Hold the candidate until the SDK says the attempt
2232
+ // is terminal; a later successful message_end supersedes it.
2233
+ pendingProviderError = mapped;
2234
+ stepStartedAt = undefined;
2235
+ firstTokenAt = undefined;
2236
+ return;
2237
+ }
2238
+ if (mapped?.type === "message_end")
2239
+ pendingProviderError = undefined;
2240
+ if ((mapped?.type === "agent_end" && !mapped.willRetry) ||
2241
+ mapped?.type === "agent_settled") {
2242
+ emitPendingProviderError();
2243
+ }
2244
+ if (mapped?.type === "agent_start") {
2245
+ stepStartedAt = observedAt;
2246
+ firstTokenAt = undefined;
2247
+ }
2248
+ if (mapped?.type === "message_update" && firstTokenAt === undefined) {
2249
+ firstTokenAt = observedAt;
2250
+ }
2057
2251
  // Pi puts per-assistant-message usage on `message.usage` ({ input, output,
2058
2252
  // cacheRead, cacheWrite, totalTokens, cost }). Emit on message_end so
2059
2253
  // multi-step tool turns accumulate like pi-web SessionInfoBar; turn_end
2060
2254
  // repeats the final message and must not double-count.
2061
- if (event.type === "message_end") {
2255
+ if (mapped?.type === "message_end") {
2062
2256
  const usage = extractUsageSample(event);
2257
+ const timing = stepStartedAt === undefined
2258
+ ? undefined
2259
+ : {
2260
+ llmMs: Math.max(0, observedAt - stepStartedAt),
2261
+ ...(firstTokenAt === undefined
2262
+ ? {}
2263
+ : {
2264
+ ttftMs: Math.max(0, firstTokenAt - stepStartedAt),
2265
+ decodeMs: Math.max(0, observedAt - firstTokenAt),
2266
+ }),
2267
+ ...(usage?.outputTokens === undefined
2268
+ ? {}
2269
+ : { outputTokens: usage.outputTokens }),
2270
+ };
2271
+ onEvent({ ...mapped, ...(timing ? { timing } : {}) });
2063
2272
  if (usage)
2064
2273
  onEvent({ type: "usage", sessionId, usage });
2065
2274
  emitContextUsage();
2275
+ stepStartedAt = undefined;
2276
+ firstTokenAt = undefined;
2277
+ }
2278
+ else if (mapped) {
2279
+ onEvent(mapped);
2280
+ if (mapped.type === "tool_result") {
2281
+ // The next model request starts after the final completed tool. If
2282
+ // tools run concurrently, the last result naturally wins.
2283
+ stepStartedAt = observedAt;
2284
+ firstTokenAt = undefined;
2285
+ }
2066
2286
  }
2067
2287
  // Keep the context fill bar live through the turn (pi-web SessionInfoBar).
2068
2288
  if (event.type === "agent_start" ||
@@ -2094,6 +2314,7 @@ export class ConsolePiRuntime {
2094
2314
  // User abort is closed out as Turn state `aborted`; do not publish a
2095
2315
  // red ordinary error event for intentional Stop.
2096
2316
  if (!options?.signal?.aborted) {
2317
+ emitPendingProviderError();
2097
2318
  onEvent({ type: "agent_settled", sessionId });
2098
2319
  }
2099
2320
  }
@@ -2104,8 +2325,16 @@ export class ConsolePiRuntime {
2104
2325
  // User abort: swallow ordinary error publication (AC-FIX-003).
2105
2326
  return;
2106
2327
  }
2107
- const message = error instanceof Error ? error.message : String(error);
2108
- onEvent({ type: "error", sessionId, message });
2328
+ emitPendingProviderError();
2329
+ if (!providerErrorEmitted) {
2330
+ const providerError = parseProviderError(error);
2331
+ onEvent({
2332
+ type: "error",
2333
+ sessionId,
2334
+ message: providerError.message,
2335
+ providerError,
2336
+ });
2337
+ }
2109
2338
  throw error;
2110
2339
  }
2111
2340
  finally {
@@ -2114,6 +2343,8 @@ export class ConsolePiRuntime {
2114
2343
  }
2115
2344
  }
2116
2345
  finally {
2346
+ if (this.options.questionRegistry)
2347
+ this.options.questionRegistry.clearTurn(sessionId);
2117
2348
  this.promptOwners.delete(sessionId);
2118
2349
  }
2119
2350
  }
@@ -2243,6 +2474,23 @@ export class ConsolePiRuntime {
2243
2474
  }
2244
2475
  }
2245
2476
  function mapAssistantMessageEvent(type, sessionId, message) {
2477
+ const record = message !== null && typeof message === "object"
2478
+ ? message
2479
+ : undefined;
2480
+ if (type === "message_end" &&
2481
+ record?.stopReason === "error") {
2482
+ const content = extractAssistantContentBlocks(message);
2483
+ const providerError = parseProviderError(typeof record.errorMessage === "string"
2484
+ ? record.errorMessage
2485
+ : "Provider request failed");
2486
+ return {
2487
+ type: "error",
2488
+ sessionId,
2489
+ message: providerError.message,
2490
+ providerError,
2491
+ ...(content.length > 0 ? { content } : {}),
2492
+ };
2493
+ }
2246
2494
  const content = extractAssistantContentBlocks(message);
2247
2495
  return {
2248
2496
  type,
@@ -0,0 +1,98 @@
1
+ const MAX_RAW_CHARS = 8_000;
2
+ const MAX_MESSAGE_CHARS = 1_000;
3
+ function asRecord(value) {
4
+ return value !== null && typeof value === "object" && !Array.isArray(value)
5
+ ? value
6
+ : undefined;
7
+ }
8
+ function bounded(value, limit) {
9
+ return value.length <= limit ? value : `${value.slice(0, limit - 1)}…`;
10
+ }
11
+ function scrub(value) {
12
+ return value
13
+ .replace(/((?:authorization|api[-_]?key|access[-_]?token|refresh[-_]?token|secret)["']?\s*[:=]\s*["']?)([^"'\s,}]+)/gi, "$1[REDACTED]")
14
+ .replace(/\bBearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer [REDACTED]");
15
+ }
16
+ function parseEnvelope(raw) {
17
+ const candidates = [raw];
18
+ const start = raw.indexOf("{");
19
+ const end = raw.lastIndexOf("}");
20
+ if (start >= 0 && end > start)
21
+ candidates.push(raw.slice(start, end + 1));
22
+ for (const candidate of candidates) {
23
+ try {
24
+ const record = asRecord(JSON.parse(candidate));
25
+ if (record)
26
+ return record;
27
+ }
28
+ catch {
29
+ // Plain text and HTTP-prefixed provider errors are expected here.
30
+ }
31
+ }
32
+ return undefined;
33
+ }
34
+ function stringField(record, key) {
35
+ const value = record?.[key];
36
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
37
+ }
38
+ function codeField(record) {
39
+ const value = record?.code ?? record?.status ?? record?.statusCode;
40
+ if (typeof value === "number" && Number.isFinite(value))
41
+ return value;
42
+ if (typeof value === "string" && value.trim()) {
43
+ const trimmed = value.trim();
44
+ return /^\d{3}$/.test(trimmed) ? Number(trimmed) : trimmed;
45
+ }
46
+ return undefined;
47
+ }
48
+ function classify(input) {
49
+ const haystack = `${input.message}\n${input.raw}`.toLowerCase();
50
+ const status = typeof input.code === "number" ? input.code : undefined;
51
+ if (status === 401 ||
52
+ status === 403 ||
53
+ /认证|鉴权|unauthori[sz]ed|forbidden|authentication|invalid api[-_ ]?key/.test(haystack))
54
+ return "authentication";
55
+ if (/请求上下文过大|上下文.{0,12}(?:过大|太大|过长|超限)|context_length_exceeded|maximum context|context.{0,24}(?:length|window|limit|large|long)|too many tokens/.test(haystack))
56
+ return "context-overflow";
57
+ if (status === 429 ||
58
+ /rate.?limit|too many requests|请求.{0,8}(?:频繁|过多)|限流/.test(haystack))
59
+ return "rate-limit";
60
+ return "provider";
61
+ }
62
+ /** Convert an SDK/provider error into a bounded durable timeline projection. */
63
+ export function parseProviderError(value) {
64
+ const source = typeof value === "string"
65
+ ? value
66
+ : value instanceof Error
67
+ ? value.message
68
+ : String(value ?? "Provider request failed");
69
+ const raw = bounded(scrub(source.trim()), MAX_RAW_CHARS);
70
+ const envelope = parseEnvelope(raw);
71
+ const nested = asRecord(envelope?.error);
72
+ const firstLine = raw.split(/\r?\n/, 1)[0]?.trim();
73
+ const message = bounded(stringField(nested, "message") ??
74
+ stringField(envelope, "message") ??
75
+ (firstLine || undefined) ??
76
+ "Provider request failed", MAX_MESSAGE_CHARS);
77
+ const code = codeField(nested) ?? codeField(envelope);
78
+ const type = stringField(nested, "type") ?? stringField(envelope, "type");
79
+ return {
80
+ message,
81
+ raw,
82
+ ...(code === undefined ? {} : { code }),
83
+ ...(type ? { type } : {}),
84
+ category: classify({ message, raw, code }),
85
+ };
86
+ }
87
+ export function providerErrorTitle(category) {
88
+ switch (category) {
89
+ case "authentication":
90
+ return "认证失败";
91
+ case "context-overflow":
92
+ return "上下文过大";
93
+ case "rate-limit":
94
+ return "请求过于频繁";
95
+ default:
96
+ return "Provider 请求失败";
97
+ }
98
+ }