@tea-agent/loop-agent 0.43.0-next.1 → 0.43.0-next.11

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 (207) hide show
  1. package/CHANGELOG.md +144 -9
  2. package/dist/application/dag/run-dag.js +5 -0
  3. package/dist/application/evaluation/budget.js +19 -1
  4. package/dist/application/task-lifecycle/advance.js +17 -4
  5. package/dist/application/task-lifecycle/observe.js +43 -29
  6. package/dist/application/task-lifecycle/plan-transitions.js +5 -4
  7. package/dist/build-stamp.json +3 -3
  8. package/dist/executors/dag-pi-executor.js +1237 -586
  9. package/dist/executors/pi-executor.js +22 -1
  10. package/dist/executors/pi-sdk-executor.js +140 -39
  11. package/dist/executors/shell-executor.js +8 -4
  12. package/dist/shared/frontend-execution-policy.js +23 -0
  13. package/dist/task/config-types.js +4 -0
  14. package/dist/task/source-prepare/ledger-reconciliation.js +2 -2
  15. package/dist/task/source-prepare/ledger-review.js +6 -9
  16. package/dist/task/source-prepare/semantic-intake.js +16 -26
  17. package/dist/task/source-prepare/source-fidelity-pi.js +28 -9
  18. package/dist/task/source-references.js +48 -23
  19. package/dist/worker/console/chat/assistant-content.js +23 -2
  20. package/dist/worker/console/chat/browser-policy.js +113 -0
  21. package/dist/worker/console/chat/browser-routes.js +140 -0
  22. package/dist/worker/console/chat/explore-tools.js +13 -0
  23. package/dist/worker/console/chat/pi-runtime/custom-tools/ask-user-question.js +44 -0
  24. package/dist/worker/console/chat/pi-runtime/custom-tools/explore-tools.js +202 -0
  25. package/dist/worker/console/chat/pi-runtime/custom-tools/goal-tools.js +79 -0
  26. package/dist/worker/console/chat/pi-runtime/custom-tools/operator-tools.js +86 -0
  27. package/dist/worker/console/chat/pi-runtime/custom-tools/todo-write.js +38 -0
  28. package/dist/worker/console/chat/pi-runtime/inspection-types.js +28 -0
  29. package/dist/worker/console/chat/pi-runtime/inspection.js +439 -0
  30. package/dist/worker/console/chat/pi-runtime/sdk-bindings.js +153 -0
  31. package/dist/worker/console/chat/pi-runtime/sdk-session.js +5 -0
  32. package/dist/worker/console/chat/pi-runtime.js +207 -1178
  33. package/dist/worker/console/chat/routes.js +208 -71
  34. package/dist/worker/console/chat/scm-routes.js +217 -0
  35. package/dist/worker/console/chat/scm-service.js +283 -0
  36. package/dist/worker/console/chat/scm-tools.js +111 -0
  37. package/dist/worker/console/chat/sdd-data-alignment.js +65 -9
  38. package/dist/worker/console/chat/session-catalog.js +32 -0
  39. package/dist/worker/console/chat/session-mode-view.js +49 -0
  40. package/dist/worker/console/chat/session-mode.js +219 -0
  41. package/dist/worker/console/chat/session-store.js +22 -6
  42. package/dist/worker/console/chat/shortcuts.js +6 -0
  43. package/dist/worker/console/chat/terminal-routes.js +216 -0
  44. package/dist/worker/console/chat/terminal-sessions.js +348 -0
  45. package/dist/worker/console/chat/terminal-tools.js +211 -0
  46. package/dist/worker/console/chat/tool-preview.js +11 -0
  47. package/dist/worker/console/chat/tools.js +2 -0
  48. package/dist/worker/console/server.js +2 -28
  49. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-B0Q0nClf.js → abnfDiagram-N423BO3Z-CTSK0xbU.js} +1 -1
  50. package/dist/worker/console/static/assets/{arc-DCPjC19G.js → arc-_2MfctFP.js} +1 -1
  51. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CP5n9jmG.js → architectureDiagram-T3A2C74G-DsDe2wTg.js} +1 -1
  52. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-CmaWi_Wg.js → blockDiagram-VBNYF7ZC-Dtz_779U.js} +1 -1
  53. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-Ct5tcMfZ.js → c4Diagram-5PPSVZJV-LaULi-gw.js} +1 -1
  54. package/dist/worker/console/static/assets/channel-CqS4yBWT.js +1 -0
  55. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-C0osm1Zf.js → chunk-2GRJ4B5K-DuHBqqX8.js} +1 -1
  56. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-CDWviED7.js → chunk-2Q5K7J3B-BZgZs1dn.js} +1 -1
  57. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-mtSk1-j7.js → chunk-5RXB4S5H-tzwmlIO_.js} +1 -1
  58. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-D5J9PHC7.js → chunk-5VM5RSS4-CeYrLkSn.js} +1 -1
  59. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-Begg4WAa.js → chunk-6Q2QTUOP-C92cgilm.js} +1 -1
  60. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-sF1AEy7T.js → chunk-GF5L2VYU-XOXpGC0F.js} +1 -1
  61. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BqbsWXZb.js → chunk-JWPE2WC7-CGHbJyCH.js} +1 -1
  62. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BKJrPcJ6.js → chunk-KBJHAD2P-Bs2-LTzf.js} +1 -1
  63. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-CcwNMRho.js → chunk-RYQCIY6F-rN7up7E4.js} +1 -1
  64. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-BEJi3iIs.js → chunk-XXDRQBXY-B_fWjKpR.js} +1 -1
  65. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DVOxNOq0.js +1 -0
  66. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DVOxNOq0.js +1 -0
  67. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-DRLgTvVu.js → cose-bilkent-JH36ORCC-DE3SdJvB.js} +1 -1
  68. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BoAVG3cs.js → cynefin-VYW2F7L2-B-4yuRAX.js} +1 -1
  69. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Bzol06hR.js → cynefinDiagram-MW4NZA55-TiCPFuO8.js} +1 -1
  70. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-UmliJM77.js → dagre-VZM6K2ZE-7crzLMzA.js} +1 -1
  71. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-BWLcJkFo.js → diagram-7IWD3JNH-Cvr-x6Fd.js} +1 -1
  72. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-CgApOm-O.js → diagram-B4RE2ZJO-BzcRHUjH.js} +1 -1
  73. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-Bh3RgTLs.js → diagram-LBJQPF4R-BjkAXilh.js} +1 -1
  74. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-Dsh-D5nE.js → diagram-Q27KOJAE-BkzyATK7.js} +1 -1
  75. package/dist/worker/console/static/assets/{diagram-UB23O5K3-Dkbbpcpb.js → diagram-UB23O5K3-rZuTarYP.js} +1 -1
  76. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-3EBaA3t3.js → ebnfDiagram-BXEA7PRR-CAYENaN1.js} +1 -1
  77. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-Bq2Dc5ok.js → erDiagram-JOGREHBK-DCEam0Uq.js} +1 -1
  78. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-De7y55ha.js → flowDiagram-UKHOOZJN-e_dH4wVO.js} +1 -1
  79. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-fLiDbQRh.js → ganttDiagram-PKOTCBZU-CvZa2zOK.js} +1 -1
  80. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Du617mp1.js → gitGraphDiagram-DS77QQ5N-CbT5TuKp.js} +1 -1
  81. package/dist/worker/console/static/assets/index-CUPeoE8W.js +468 -0
  82. package/dist/worker/console/static/assets/index-SAANIK8g.css +1 -0
  83. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-BwmlF-XP.js → infoDiagram-6WML65LV-BWzHQa_Z.js} +1 -1
  84. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-zsGtS59u.js → ishikawaDiagram-WSZJBQD7-BTjlovtU.js} +1 -1
  85. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DTTPTe3f.js → journeyDiagram-NVQOT4AX-DkvQoCc1.js} +1 -1
  86. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BkXg9aP-.js → kanban-definition-27J2QSJJ-CHSzGi3W.js} +1 -1
  87. package/dist/worker/console/static/assets/{linear-7U2ue5IE.js → linear-DZMkTCWH.js} +1 -1
  88. package/dist/worker/console/static/assets/{mermaid.core-BUuGHmWO.js → mermaid.core-BIMhty7A.js} +5 -5
  89. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-7q6fX0Sv.js → mindmap-definition-FAOFIHXS-Dx-tyMl3.js} +1 -1
  90. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-CkQPJN55.js → pegDiagram-VL7TDLO6-C6G34rbY.js} +1 -1
  91. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-CCal_tfX.js → pieDiagram-7S7Q4E2Y-CHK9XOfm.js} +1 -1
  92. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-B7bpxyAZ.js → quadrantDiagram-CIZ2JOQS-CDARkI0w.js} +1 -1
  93. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-CJmww7D-.js → railroadDiagram-AXF67PYL-DiuUe0ZQ.js} +1 -1
  94. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-D6ldWJEv.js → requirementDiagram-LRYGKXZP-RE61UR56.js} +1 -1
  95. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-VGh33I9e.js → sankeyDiagram-W5VNT64P-KDNLHMEG.js} +1 -1
  96. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-CCkDrIjU.js → sequenceDiagram-SI44F4Z6-CeVgXnD9.js} +1 -1
  97. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-NC8f5Otb.js → sizeCapture-X5ZJPWSS-CXwGyjjG.js} +1 -1
  98. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-_92ezZdF.js → stateDiagram-OKZ733FA-BjTfozab.js} +1 -1
  99. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BXoYsyr2.js +1 -0
  100. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-BdUCGtbP.js → swimlanes-SLNWSIFB-BaXOB8GT.js} +2 -2
  101. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DfKliS4e.js +8 -0
  102. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CdD1H8ia.js → timeline-definition-Z64GVDOM-C3r2hqUU.js} +1 -1
  103. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-Dkdk3oFo.js → vennDiagram-T6HMQDX7-MWKgl1S9.js} +1 -1
  104. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CKu2uPPY.js → wardleyDiagram-T6FBY63Y-C9iFcZPk.js} +1 -1
  105. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-ClqAnuG8.js → xychartDiagram-ELKLHX3M-DqnM24Jp.js} +1 -1
  106. package/dist/worker/console/static/index.html +2 -2
  107. package/dist/worker/console/static-src/operator-chat/active-turn-tail.js +10 -0
  108. package/dist/worker/console/static-src/operator-chat/chat-link.js +94 -0
  109. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +122 -10
  110. package/dist/worker/console/static-src/operator-chat/compaction-message.js +7 -0
  111. package/dist/worker/console/static-src/operator-chat/pending-user-message.js +27 -15
  112. package/dist/worker/console/static-src/operator-chat/process-label.js +41 -0
  113. package/dist/worker/console/static-src/operator-chat/turn-group-equality.js +13 -0
  114. package/dist/worker/console/static-src/operator-chat/turn-process-disclosure.js +10 -0
  115. package/dist/worker/console/static-src/operator-chat/use-searchable-hidden.js +51 -0
  116. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +44 -24
  117. package/dist/worker/console/static-src/operator-chat/useChatStream.js +18 -2
  118. package/dist/worker/console/static-src/operator-chat/useChatThread.js +41 -8
  119. package/dist/worker/console/static-src/operator-chat/useComposer.js +26 -11
  120. package/dist/worker/console/static-src/operator-chat/user-turn-anchor-equality.js +21 -0
  121. package/dist/worker/console/workspace-context.js +15 -1
  122. package/dist/worker/observe/node-transparency.js +81 -72
  123. package/dist/worker/observe/routes.js +20 -1
  124. package/dist/worker/observe/static/dag-inspector-humanize.js +3 -0
  125. package/dist/worker/observe/static/dom.js +20 -1
  126. package/dist/worker/observe/static/format-pool.d.ts +2 -0
  127. package/dist/worker/observe/static/format-pool.js +6 -0
  128. package/dist/worker/observe/static/format.js +7 -0
  129. package/dist/worker/observe/static/inspect-workspace.js +34 -7
  130. package/dist/worker/observe/static/inspector-submission.js +32 -0
  131. package/dist/worker/observe/static/kpi.js +1 -0
  132. package/dist/worker/observe/static/relations.js +2 -0
  133. package/dist/worker/observe/static/router.js +13 -0
  134. package/dist/worker/observe/static/run-processing.js +2 -0
  135. package/dist/worker/observe/static/shell-chrome.js +36 -3
  136. package/dist/worker/observe/static/state.js +35 -2
  137. package/dist/worker/observe/static/styles.css +263 -39
  138. package/dist/worker/observe/static/task-failure-labels.d.ts +4 -0
  139. package/dist/worker/observe/static/task-failure-labels.js +67 -0
  140. package/dist/worker/observe/static/task-history.js +12 -0
  141. package/dist/worker/observe/static/views/batch.js +6 -13
  142. package/dist/worker/observe/static/views/dag-graph.js +50 -3
  143. package/dist/worker/observe/static/views/dag-inspector.js +742 -268
  144. package/dist/worker/observe/static/views/dag-trajectory.js +3 -0
  145. package/dist/worker/observe/static/views/dag.d.ts +6 -0
  146. package/dist/worker/observe/static/views/dag.js +49 -11
  147. package/dist/worker/observe/static/views/dags.js +2 -0
  148. package/dist/worker/observe/static/views/dashboard.js +21 -12
  149. package/dist/worker/observe/static/views/failures.js +21 -11
  150. package/dist/worker/observe/static/views/feature.js +11 -29
  151. package/dist/worker/observe/static/views/pool.js +37 -28
  152. package/dist/worker/observe/static/views/run.js +48 -5
  153. package/dist/worker/observe/static/views/session-timeline.js +189 -240
  154. package/dist/worker/observe/static/views/task.js +81 -62
  155. package/dist/workflows/dag/budget-enforcement.js +53 -3
  156. package/dist/workflows/dag/frontend-capacity.js +9 -0
  157. package/dist/workflows/dag/frontend-closeout.js +1 -1
  158. package/dist/workflows/dag/frontend-design-policy.js +2 -1
  159. package/dist/workflows/dag/frontend-durable-tools.js +193 -0
  160. package/dist/workflows/dag/frontend-execution-groups.js +24 -0
  161. package/dist/workflows/dag/frontend-implementation-contract.js +13 -0
  162. package/dist/workflows/dag/frontend-input-projection.js +76 -0
  163. package/dist/workflows/dag/frontend-plan-completeness.js +186 -0
  164. package/dist/workflows/dag/frontend-plan-recovery-policy.js +18 -0
  165. package/dist/workflows/dag/frontend-plan-render.js +10 -3
  166. package/dist/workflows/dag/frontend-prewrite-gate.js +3 -3
  167. package/dist/workflows/dag/frontend-recovery-controller.js +7 -7
  168. package/dist/workflows/dag/frontend-recovery-lineage.js +13 -0
  169. package/dist/workflows/dag/frontend-recovery-run.js +4 -0
  170. package/dist/workflows/dag/frontend-review-context.js +2 -1
  171. package/dist/workflows/dag/frontend-review-scopes.js +139 -0
  172. package/dist/workflows/dag/frontend-session-budget.js +249 -0
  173. package/dist/workflows/dag/frontend-shadow-dual-write.js +20 -2
  174. package/dist/workflows/dag/frontend-shape-facts.js +12 -2
  175. package/dist/workflows/dag/frontend-shape.js +30 -1
  176. package/dist/workflows/dag/frontend-test-execution-evidence.js +3 -2
  177. package/dist/workflows/dag/frontend-typed-event-store.js +11 -0
  178. package/dist/workflows/dag/frontend-verification-trace.js +1 -1
  179. package/dist/workflows/dag/init-hybrid.js +65 -167
  180. package/dist/workflows/dag/node-execution.js +38 -155
  181. package/dist/workflows/dag/prompt.js +4 -0
  182. package/dist/workflows/dag/rerun-plan.js +7 -1
  183. package/dist/workflows/dag/runner.js +26 -1
  184. package/dist/workflows/dag/types.js +6 -0
  185. package/docs/operations/README.md +1 -0
  186. package/docs/templates/frontend-design-contract.md +4 -4
  187. package/docs/templates/frontend-implementation-contract.schema.json +34 -2
  188. package/docs/templates/frontend-implementation-dag.json +14 -13
  189. package/package.json +6 -3
  190. package/skills/frontend-bounded-implement/SKILL.md +3 -3
  191. package/skills/frontend-bounded-implement/references/code-standards.md +3 -3
  192. package/skills/frontend-contract/SKILL.md +2 -1
  193. package/skills/frontend-contract/references/contract-protocol.md +22 -4
  194. package/skills/frontend-design-review/SKILL.md +22 -16
  195. package/skills/frontend-design-review/references/review-checklist.md +7 -7
  196. package/skills/frontend-plan/SKILL.md +21 -2
  197. package/skills/frontend-plan/references/decision-contract.md +42 -23
  198. package/skills/frontend-review/SKILL.md +22 -29
  199. package/skills/frontend-review/references/review-findings.md +24 -43
  200. package/skills/frontend-scout/references/scout-evidence.md +4 -0
  201. package/dist/worker/console/static/assets/channel-DAS07MdS.js +0 -1
  202. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Cwk-5jiW.js +0 -1
  203. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Cwk-5jiW.js +0 -1
  204. package/dist/worker/console/static/assets/index-C0O48S_P.js +0 -449
  205. package/dist/worker/console/static/assets/index-CzKf4U8P.css +0 -1
  206. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DyJg5gzW.js +0 -1
  207. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-B1cw_0Uy.js +0 -8
@@ -26,25 +26,35 @@
26
26
  * `createDefaultPiSdkBindings()`.
27
27
  */
28
28
  import { createHash, randomBytes } from "node:crypto";
29
- import { access, mkdir, rm, stat, writeFile } from "node:fs/promises";
29
+ import { stat } from "node:fs/promises";
30
30
  import path from "node:path";
31
31
  import { extractAssistantContentBlocks, extractAssistantText, isAssistantSdkMessage, } from "./assistant-content.js";
32
32
  import { parseProviderError, } from "./provider-error.js";
33
33
  import { projectCompactSnapshot, } from "./chat-event-store.js";
34
34
  import { extractUsageSample } from "./usage.js";
35
- import { OPERATOR_CHAT_ALLOWED_TOOLS, OPERATOR_CHAT_BUILTIN_CUSTOM_TOOLS, authorizeOperatorChatTool, assertNoWriteToolInList, } from "./tools.js";
35
+ import { OPERATOR_CHAT_ALLOWED_TOOLS, authorizeOperatorChatTool, assertNoWriteToolInList, } from "./tools.js";
36
+ import { authorizeChatSessionTool, DEFAULT_CONSOLE_CHAT_SESSION_MODE, chatActiveToolBaseline, composeChatSystemPrompt, composeChatSystemPromptParts, computeChatActiveToolNames, shouldLoadWorkflowBusinessTools, } from "./session-mode.js";
36
37
  import { buildAgentCustomTool, ExploreOrchestrator, } from "./subagents/index.js";
37
38
  import { createOperatorChatResourceLoader, } from "./resource-loader.js";
38
- import { applySkillAutoInvocationPreferences, isAutoInvocationEnabled, loadResourcePreferences, skillResourceId, } from "./resource-preferences-store.js";
39
- import { buildModelCallableToolSchemas } from "./tool-adapter.js";
39
+ import { applySkillAutoInvocationPreferences, loadResourcePreferences, } from "./resource-preferences-store.js";
40
40
  import { applyProcessCompactionOverride, createCompactionController, createContextPressureExtension, createSiftBridgeState, housekeepSiftStash, isConsoleSiftEnabled, parseCompactionPolicy, parseSiftBridgeConfig, projectPromptPressure, deriveEffectiveWindow, projectSafeTelemetry, emptyContextPressureCounters, } from "../../../shared/pi-context-pressure/index.js";
41
41
  import { incrementCounter as incrementPressureCounter } from "../../../shared/pi-context-pressure/telemetry.js";
42
42
  import { resolveDefaultChatModel, resolveLowChatModel, } from "./model-resolver.js";
43
43
  import { filterActiveInterviewTools } from "../interview/tools.js";
44
- import { RUNTIME_CONTEXT_TEXT_MAX, redactRuntimeText, } from "./runtime-context.js";
44
+ import { redactRuntimeText } from "./runtime-context.js";
45
45
  import { buildTodoContinuationContext, } from "./todos.js";
46
+ import { buildPersistentTerminalTools } from "./terminal-tools.js";
47
+ import { buildScmTools } from "./scm-tools.js";
46
48
  import { buildGoalContinuationContext, } from "./goals.js";
47
49
  import { injectWizardLocalHeaders, } from "./sdd-data-alignment.js";
50
+ import { buildCustomOperatorTools } from "./pi-runtime/custom-tools/operator-tools.js";
51
+ import { buildAskUserQuestionTool } from "./pi-runtime/custom-tools/ask-user-question.js";
52
+ import { buildTodoWriteTool } from "./pi-runtime/custom-tools/todo-write.js";
53
+ import { buildGoalTools } from "./pi-runtime/custom-tools/goal-tools.js";
54
+ import { buildExploreCustomTools } from "./pi-runtime/custom-tools/explore-tools.js";
55
+ import { THINKING_LEVELS, RuntimeSnapshotUnavailableError, } from "./pi-runtime/inspection-types.js";
56
+ import { collectRuntimeInventories, collectSlashCommands, projectRuntimeSnapshot, } from "./pi-runtime/inspection.js";
57
+ import { createDefaultPiSdkBindings, safeGetAgentDir, } from "./pi-runtime/sdk-bindings.js";
48
58
  /**
49
59
  * Base system prompt fragment every General Operator Chat session receives.
50
60
  *
@@ -75,15 +85,6 @@ export const OPERATOR_CHAT_SYSTEM_PROMPT_BASE = [
75
85
  "Continue until success, user stop, or no safe recovery remains; then stop and report failure/evidence. Never create a new task for a transient failure or replace repair-pi with direct edits.",
76
86
  "Narrate long-DAG progress briefly at start, meaningful phase changes/findings, verification failure/recovery, prolonged quiet and terminal state: say what finished and what is next. Never narrate unchanged operationWait polls or heartbeats, or claim beyond structured facts.",
77
87
  ].join("\n");
78
- /** Compose the inspectable system prompt actually injected into Operator Chat. */
79
- export function composeOperatorChatSystemPrompt(input) {
80
- return [
81
- OPERATOR_CHAT_SYSTEM_PROMPT_BASE,
82
- ...(input.systemPromptSuffix?.trim()
83
- ? [input.systemPromptSuffix.trim()]
84
- : []),
85
- ].join("\n\n");
86
- }
87
88
  /**
88
89
  * Built-in SDK tool names registered for every Chat session (ADR 0011).
89
90
  * Full Pi builtins: read/write/edit/bash/grep/find/ls. safe-* custom tools
@@ -112,27 +113,8 @@ export const OPERATOR_CHAT_SAFE_EXPLORE_TOOL_IDS = Object.freeze([
112
113
  "git-diff",
113
114
  ]);
114
115
  export function OPERATOR_CHAT_ACTIVE_TOOL_NAMES() {
115
- return [
116
- ...OPERATOR_CHAT_ALLOWED_TOOLS,
117
- ...OPERATOR_CHAT_BUILTIN_EXPLORE_TOOLS,
118
- ...OPERATOR_CHAT_SAFE_EXPLORE_TOOL_IDS,
119
- ...OPERATOR_CHAT_BUILTIN_CUSTOM_TOOLS,
120
- ];
116
+ return chatActiveToolBaseline("workflow");
121
117
  }
122
- /**
123
- * Non-Pi write / coding channels that must never be activated in Operator
124
- * Chat even when a user/project extension registers them (ADR 0011 / 0012).
125
- * Mirrors the NON_PI_WRITE_CHANNELS guard in tools.ts for the Gate 2 pin.
126
- */
127
- const OPERATOR_CHAT_EXTENSION_DENIED_TOOL_IDS = new Set([
128
- "apply_patch",
129
- "apply-patch",
130
- "full-tools",
131
- "full_tools",
132
- "coding-chat",
133
- "coding_chat",
134
- "shell",
135
- ].map((t) => t.toLowerCase()));
136
118
  /**
137
119
  * Gate 2 active-tool set for a live session (ADR 0012): the operator-chat
138
120
  * baseline (operator actions + Pi builtins + safe explore) PLUS extension
@@ -143,24 +125,8 @@ const OPERATOR_CHAT_EXTENSION_DENIED_TOOL_IDS = new Set([
143
125
  * coding-chat). Unknown/denied names are never activated; Gate 3 still
144
126
  * re-authorizes every tool call per invocation.
145
127
  */
146
- export function computeOperatorChatActiveToolNames(session) {
147
- const baseline = OPERATOR_CHAT_ACTIVE_TOOL_NAMES();
148
- if (!session?.getAllTools)
149
- return baseline;
150
- const baselineLower = new Set(baseline.map((t) => t.toLowerCase()));
151
- const extras = [];
152
- for (const tool of session.getAllTools()) {
153
- const name = tool?.name;
154
- if (!name || typeof name !== "string")
155
- continue;
156
- const lower = name.toLowerCase();
157
- if (baselineLower.has(lower))
158
- continue;
159
- if (OPERATOR_CHAT_EXTENSION_DENIED_TOOL_IDS.has(lower))
160
- continue;
161
- extras.push(name);
162
- }
163
- return extras.length > 0 ? [...baseline, ...extras] : baseline;
128
+ export function computeOperatorChatActiveToolNames(session, sessionMode = DEFAULT_CONSOLE_CHAT_SESSION_MODE) {
129
+ return computeChatActiveToolNames(session, sessionMode);
164
130
  }
165
131
  const OPERATOR_CHAT_SESSION_EXCLUDE_TOOLS = Object.freeze([
166
132
  "apply_patch",
@@ -216,8 +182,8 @@ async function bindSessionExtensions(session) {
216
182
  return;
217
183
  await session.bindExtensions(createConsoleExtensionBindings());
218
184
  }
219
- function pinOperatorChatTools(session) {
220
- session.setActiveToolsByName(computeOperatorChatActiveToolNames(session));
185
+ function pinOperatorChatTools(session, sessionMode = DEFAULT_CONSOLE_CHAT_SESSION_MODE) {
186
+ session.setActiveToolsByName(computeChatActiveToolNames(session, sessionMode));
221
187
  }
222
188
  /**
223
189
  * Pi has no hook result that merely appends instructions to its built-in
@@ -319,592 +285,8 @@ export function createWizardLocalHeadersExtension(options) {
319
285
  },
320
286
  };
321
287
  }
322
- /**
323
- * Build Pi custom ToolDefinition objects for every whitelisted operator action.
324
- * Each tool's execute() routes into the operator action dispatcher (Gate 3).
325
- * Requires the `typebox` + `defineTool` packages (both bundled with the SDK).
326
- */
327
- async function buildCustomOperatorTools(options) {
328
- // typebox is a transitive dep of @earendil-works/pi-coding-agent; Node's
329
- // resolver hoists to the SDK's nested copy. defineTool is re-exported by SDK.
330
- const [{ Type }, { defineTool }] = await Promise.all([
331
- import("typebox"),
332
- import("@earendil-works/pi-coding-agent"),
333
- ]);
334
- const schemas = buildModelCallableToolSchemas();
335
- const ctx = options.actionContext;
336
- return schemas.map((schema) => {
337
- // TypeBox schema is built dynamically; cast through unknown to the builder
338
- // since Static<TParams> is structural and cannot be statically inferred here.
339
- const parameters = Type.Object(schema.inputParams.reduce((acc, param) => {
340
- // Carry the registry's param description into the tool schema so
341
- // the model sees parameter semantics, not just names/types.
342
- const desc = param.description?.trim()
343
- ? { description: param.description }
344
- : {};
345
- let t;
346
- switch (param.type) {
347
- case "string":
348
- t = Type.String(desc);
349
- break;
350
- case "boolean":
351
- t = Type.Boolean(desc);
352
- break;
353
- case "number":
354
- t = Type.Number(desc);
355
- break;
356
- case "array":
357
- t = Type.Array(Type.String({}), Object.keys(desc).length ? desc : {});
358
- break;
359
- default:
360
- t = Type.Object({}, { additionalProperties: true });
361
- }
362
- acc[param.name] = param.required ? t : Type.Optional(t);
363
- return acc;
364
- }, {}), { additionalProperties: false });
365
- return defineTool({
366
- name: schema.toolId,
367
- label: schema.toolId,
368
- description: `${schema.description} (kind=${schema.kind}; cli: ${schema.cli})`,
369
- promptSnippet: schema.description,
370
- parameters: parameters,
371
- async execute(toolCallId, params) {
372
- const decision = authorizeOperatorChatTool(schema.toolId);
373
- if (!decision.ok) {
374
- return {
375
- content: [
376
- {
377
- type: "text",
378
- text: JSON.stringify({
379
- error: decision.message,
380
- code: decision.code,
381
- }),
382
- },
383
- ],
384
- details: { error: decision.message, code: decision.code },
385
- };
386
- }
387
- // Defer require to avoid circular import at module load.
388
- const { dispatchChatToolCall } = await import("./session-store.js");
389
- const result = await dispatchChatToolCall(ctx, decision.toolId, params, toolCallId);
390
- const payload = result.ok
391
- ? result.result
392
- : { error: result.message, code: result.errorCode };
393
- return {
394
- content: [{ type: "text", text: JSON.stringify(payload) }],
395
- details: payload,
396
- };
397
- },
398
- });
399
- });
400
- }
401
- const ASK_USER_QUESTION_DESCRIPTION = "Ask the user concise questions when a confirmation, choice, or missing detail is required before proceeding. " +
402
- "The tool pauses until the user answers. Use it instead of writing questions as ordinary assistant text. " +
403
- "Send one or more questions with stable ids; recommended options should be first and end with (Recommended).";
404
- async function buildAskUserQuestionTool(options) {
405
- const [{ Type }, { defineTool }] = await Promise.all([
406
- import("typebox"),
407
- import("@earendil-works/pi-coding-agent"),
408
- ]);
409
- return defineTool({
410
- name: "ask_user_question",
411
- label: "Ask user",
412
- description: ASK_USER_QUESTION_DESCRIPTION,
413
- promptSnippet: ASK_USER_QUESTION_DESCRIPTION,
414
- parameters: Type.Object({
415
- questions: Type.Array(Type.Object({
416
- id: Type.String({ description: "Stable question id echoed in the answer." }),
417
- question: Type.String({ description: "Specific user-facing question." }),
418
- header: Type.Optional(Type.String({ description: "Optional short heading." })),
419
- detail: Type.Optional(Type.String({ description: "Optional Markdown context shown below the question." })),
420
- options: Type.Optional(Type.Array(Type.Object({
421
- label: Type.String({ description: "Short user-facing option label." }),
422
- description: Type.Optional(Type.String({ description: "One sentence explaining impact or tradeoff." })),
423
- }, { additionalProperties: false }))),
424
- multi_select: Type.Optional(Type.Boolean({ description: "Allow more than one option." })),
425
- }, { additionalProperties: false }), { minItems: 1, maxItems: 20 }),
426
- }, { additionalProperties: false }),
427
- async execute(toolCallId, params, signal) {
428
- const answer = await options.registry.ask({
429
- sessionId: options.sessionId,
430
- toolCallId,
431
- questions: params.questions,
432
- signal,
433
- });
434
- return {
435
- content: [{ type: "text", text: JSON.stringify(answer) }],
436
- details: answer,
437
- };
438
- },
439
- });
440
- }
441
- 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 (no partial updates, no per-item edits). Before starting multi-step work, add one todo per concrete step. Keep at least one todo `in_progress` while work remains. Mark a todo `completed` immediately when it is done (do not batch completions) and resend the full list as progress or DAG state advances. Skip the list for simple Q&A, read-only lookups, and single-step tasks. Use it for multi-step implementation and whenever you prepare, run, or supervise a DAG — call `todo_write` before entering the DAG tool chain. Phrase DAG todos as user-facing phases users can understand, not copies of internal DAG nodes. Statuses: `pending` / `in_progress` / `completed`.";
442
- async function buildTodoWriteTool(options) {
443
- const [{ Type }, { defineTool }] = await Promise.all([
444
- import("typebox"),
445
- import("@earendil-works/pi-coding-agent"),
446
- ]);
447
- return defineTool({
448
- name: "todo_write",
449
- label: "Update todo list",
450
- description: TODO_WRITE_DESCRIPTION,
451
- promptSnippet: TODO_WRITE_DESCRIPTION,
452
- parameters: Type.Object({
453
- todos: Type.Array(Type.Object({
454
- content: Type.String({ description: "Short imperative task description." }),
455
- status: Type.Union([
456
- Type.Literal("pending"),
457
- Type.Literal("in_progress"),
458
- Type.Literal("completed"),
459
- ]),
460
- }, { additionalProperties: false })),
461
- }, { additionalProperties: false }),
462
- async execute(toolCallId, params) {
463
- const result = options.registry.write(options.sessionId, toolCallId, params.todos);
464
- return {
465
- content: [{ type: "text", text: JSON.stringify(result) }],
466
- details: result,
467
- };
468
- },
469
- });
470
- }
471
- const GET_GOAL_DESCRIPTION = "Read the current session's durable cross-turn Goal, including revision, status, round limit, and whether this Console process is authorized to continue it.";
472
- const CREATE_GOAL_DESCRIPTION = "Create a durable cross-turn Goal only when the user explicitly asks to start or create a Goal. Never silently upgrade an ordinary request. A session may have only one unfinished Goal.";
473
- const UPDATE_GOAL_DESCRIPTION = "Finish or block the current durable Goal using its exact goalId and revision. Set complete only when the objective is genuinely achieved and no required work remains. Set blocked only when safe progress cannot continue; include the blocker.";
474
- async function buildGoalTools(options) {
475
- const [{ Type }, { defineTool }] = await Promise.all([
476
- import("typebox"),
477
- import("@earendil-works/pi-coding-agent"),
478
- ]);
479
- const textResult = (value) => ({
480
- content: [{ type: "text", text: JSON.stringify(value) }],
481
- details: value,
482
- });
483
- return [
484
- defineTool({
485
- name: "get_goal",
486
- label: "Get goal",
487
- description: GET_GOAL_DESCRIPTION,
488
- promptSnippet: GET_GOAL_DESCRIPTION,
489
- parameters: Type.Object({}, { additionalProperties: false }),
490
- async execute() {
491
- return textResult({ goal: options.registry.get(options.sessionId) });
492
- },
493
- }),
494
- defineTool({
495
- name: "create_goal",
496
- label: "Create goal",
497
- description: CREATE_GOAL_DESCRIPTION,
498
- promptSnippet: CREATE_GOAL_DESCRIPTION,
499
- parameters: Type.Object({
500
- objective: Type.String({
501
- description: "Concrete final objective and completion standard explicitly requested by the user.",
502
- }),
503
- }, { additionalProperties: false }),
504
- async execute(_toolCallId, params) {
505
- if (!options.registry.consumeModelCreateGrant(options.sessionId)) {
506
- return textResult({
507
- ok: false,
508
- code: "GOAL_EXPLICIT_REQUEST_REQUIRED",
509
- message: "create_goal requires an explicit user request to create or start a Goal",
510
- });
511
- }
512
- return textResult(options.registry.create({
513
- sessionId: options.sessionId,
514
- objective: params.objective,
515
- }));
516
- },
517
- }),
518
- defineTool({
519
- name: "update_goal",
520
- label: "Update goal",
521
- description: UPDATE_GOAL_DESCRIPTION,
522
- promptSnippet: UPDATE_GOAL_DESCRIPTION,
523
- parameters: Type.Object({
524
- goalId: Type.String(),
525
- revision: Type.Number({ minimum: 1 }),
526
- status: Type.Union([
527
- Type.Literal("complete"),
528
- Type.Literal("blocked"),
529
- ]),
530
- blocker: Type.Optional(Type.String()),
531
- }, { additionalProperties: false }),
532
- async execute(_toolCallId, params) {
533
- return textResult(options.registry.update({
534
- sessionId: options.sessionId,
535
- goalId: params.goalId,
536
- expectedRevision: params.revision,
537
- phase: params.status,
538
- blocker: params.blocker,
539
- arm: false,
540
- }));
541
- },
542
- }),
543
- ];
544
- }
545
- /**
546
- * Build the safe explore custom tools (roadmap M0-A / G16): safe-read,
547
- * safe-grep, gitStatus, gitDiff. These replace the bare SDK bash/read/grep so
548
- * the model can probe the repo WITHOUT write escapes (echo redirect, tee,
549
- * sed -i, node -e) and WITHOUT reading sensitive files (env files, key/pem,
550
- * auth.json, sessions dir) — and every returned byte is secret-scrubbed
551
- * before reaching the model context.
552
- *
553
- * gitStatus/gitDiff use a FIXED argv (typed, no shell) so they cannot be
554
- * turned into an arbitrary-shell vector.
555
- */
556
- async function buildExploreCustomTools(options) {
557
- const [{ Type }, { defineTool }] = await Promise.all([
558
- import("typebox"),
559
- import("@earendil-works/pi-coding-agent"),
560
- ]);
561
- const { execFileSync } = await import("node:child_process");
562
- const repoRoot = options.repoRoot;
563
- const { safeReadFile, safeGrep, scrubSecrets } = await import("./explore-tools.js");
564
- function safeGit(args) {
565
- try {
566
- const stdout = execFileSync("git", args, {
567
- cwd: repoRoot,
568
- encoding: "utf8",
569
- // Fixed argv, no shell, bounded output.
570
- maxBuffer: 512 * 1024,
571
- timeout: 10_000,
572
- windowsHide: true,
573
- });
574
- return { ok: true, stdout };
575
- }
576
- catch (e) {
577
- return {
578
- ok: false,
579
- code: "GIT_FAILED",
580
- message: e instanceof Error ? e.message : String(e),
581
- };
582
- }
583
- }
584
- const safeReadTool = defineTool({
585
- name: "safe-read",
586
- label: "safe-read",
587
- description: "Read a repo-relative file with sensitive-file denylist (.env*/*.key/*.pem/.git/**/auth.json/sessions/**) and secret scrubbing. Absolute paths and .. are rejected.",
588
- promptSnippet: "safe-read: read a repo file (safe; secrets scrubbed)",
589
- parameters: Type.Object({
590
- path: Type.String({ description: "repo-relative file path" }),
591
- }, { additionalProperties: false }),
592
- async execute(_toolCallId, params) {
593
- const raw = String(params.path ?? "");
594
- try {
595
- const result = await safeReadFile(repoRoot, raw);
596
- return {
597
- content: [{ type: "text", text: JSON.stringify(result) }],
598
- details: result,
599
- };
600
- }
601
- catch (e) {
602
- const err = e;
603
- return {
604
- content: [
605
- {
606
- type: "text",
607
- text: JSON.stringify({
608
- error: err.message ?? String(e),
609
- code: err.code ?? "READ_FAILED",
610
- }),
611
- },
612
- ],
613
- details: {
614
- error: err.message ?? String(e),
615
- code: err.code ?? "READ_FAILED",
616
- },
617
- };
618
- }
619
- },
620
- });
621
- const safeGrepTool = defineTool({
622
- name: "safe-grep",
623
- label: "safe-grep",
624
- description: "Line search across the repo working tree with sensitive-file denylist and secret scrubbing. pattern is a JS regex source; optional glob filters paths.",
625
- promptSnippet: "safe-grep: search repo files (safe; secrets scrubbed)",
626
- parameters: Type.Object({
627
- pattern: Type.String({ description: "regex source (no shell)" }),
628
- caseInsensitive: Type.Optional(Type.Boolean()),
629
- glob: Type.Optional(Type.String({ description: "optional glob filter e.g. *.ts" })),
630
- }, { additionalProperties: false }),
631
- async execute(_toolCallId, params) {
632
- try {
633
- const result = await safeGrep(repoRoot, String(params.pattern ?? ""), {
634
- caseInsensitive: Boolean(params.caseInsensitive),
635
- glob: params.glob ? String(params.glob) : undefined,
636
- });
637
- return {
638
- content: [{ type: "text", text: JSON.stringify(result) }],
639
- details: result,
640
- };
641
- }
642
- catch (e) {
643
- const err = e;
644
- return {
645
- content: [
646
- {
647
- type: "text",
648
- text: JSON.stringify({
649
- error: err.message ?? String(e),
650
- code: err.code ?? "GREP_FAILED",
651
- }),
652
- },
653
- ],
654
- details: {
655
- error: err.message ?? String(e),
656
- code: err.code ?? "GREP_FAILED",
657
- },
658
- };
659
- }
660
- },
661
- });
662
- const gitStatusTool = defineTool({
663
- name: "git-status",
664
- label: "git-status",
665
- description: "Typed `git status --porcelain` (fixed argv, no shell). Returns scrubbed working-tree status.",
666
- promptSnippet: "git-status: working tree status (read-only)",
667
- parameters: Type.Object({}, { additionalProperties: false }),
668
- async execute() {
669
- const r = safeGit(["status", "--porcelain"]);
670
- if (!r.ok) {
671
- return {
672
- content: [
673
- {
674
- type: "text",
675
- text: JSON.stringify({ error: r.message, code: r.code }),
676
- },
677
- ],
678
- details: { error: r.message, code: r.code },
679
- };
680
- }
681
- const { scrubbed, redactions } = scrubSecrets(r.stdout);
682
- const payload = { status: scrubbed, redactions };
683
- return {
684
- content: [{ type: "text", text: JSON.stringify(payload) }],
685
- details: payload,
686
- };
687
- },
688
- });
689
- const gitDiffTool = defineTool({
690
- name: "git-diff",
691
- label: "git-diff",
692
- description: "Typed `git diff` (fixed argv: --stat or a single -- path; no shell). Returns scrubbed diff. path must be repo-relative and not sensitive.",
693
- promptSnippet: "git-diff: diff (read-only, scrubbed)",
694
- parameters: Type.Object({
695
- path: Type.Optional(Type.String({
696
- description: "optional repo-relative path to diff (default: whole tree --stat)",
697
- })),
698
- }, { additionalProperties: false }),
699
- async execute(_toolCallId, params) {
700
- let rawPath = params.path ? String(params.path) : undefined;
701
- // If a path is supplied, validate it via the read-path guard so git diff
702
- // cannot be aimed at sensitive files (.env* / auth.json / sessions).
703
- if (rawPath) {
704
- const { guardReadPath } = await import("./explore-tools.js");
705
- const g = await guardReadPath(repoRoot, rawPath);
706
- if (!g.ok) {
707
- return {
708
- content: [
709
- {
710
- type: "text",
711
- text: JSON.stringify({ error: g.message, code: g.code }),
712
- },
713
- ],
714
- details: { error: g.message, code: g.code },
715
- };
716
- }
717
- rawPath = g.repoRelative;
718
- }
719
- const args = rawPath ? ["diff", "--", rawPath] : ["diff", "--stat"];
720
- const r = safeGit(args);
721
- if (!r.ok) {
722
- return {
723
- content: [
724
- {
725
- type: "text",
726
- text: JSON.stringify({ error: r.message, code: r.code }),
727
- },
728
- ],
729
- details: { error: r.message, code: r.code },
730
- };
731
- }
732
- const { scrubbed, redactions } = scrubSecrets(r.stdout);
733
- const payload = { diff: scrubbed, redactions };
734
- return {
735
- content: [{ type: "text", text: JSON.stringify(payload) }],
736
- details: payload,
737
- };
738
- },
739
- });
740
- return [safeReadTool, safeGrepTool, gitStatusTool, gitDiffTool];
741
- }
742
- export const THINKING_LEVELS = [
743
- "off",
744
- "minimal",
745
- "low",
746
- "medium",
747
- "high",
748
- "xhigh",
749
- "max",
750
- ];
751
- /**
752
- * Structured critical-failure error for snapshot projection (RF-02). Model
753
- * enumeration or system prompt projection failures throw this so consuming
754
- * routes can return 503 PI_RUNTIME_UNAVAILABLE instead of a fake-empty 200.
755
- * Partial ResourceLoader / package-inventory failures are NOT critical — they
756
- * keep the 200 snapshot and surface as structured diagnostics instead.
757
- */
758
- export class RuntimeSnapshotUnavailableError extends Error {
759
- code = "PI_RUNTIME_UNAVAILABLE";
760
- constructor(message) {
761
- super(message);
762
- this.name = "RuntimeSnapshotUnavailableError";
763
- }
764
- }
765
- /**
766
- * Browser-safe projection of a live runtime snapshot (design §2.2 / RF-03).
767
- *
768
- * Every browser-visible string runs the shared secret-scrub chain
769
- * (scrubSecrets + ghp_/Bearer redaction via redactRuntimeText) and a fixed
770
- * per-field length limit: 200 (names/spec/source/origin), 400 (paths/errors/
771
- * diagnostic messages), 600 (skill descriptions), 80 (package versions);
772
- * truncation uses the fixed `…[truncated N chars …]` marker. The server-only
773
- * `raw` prompt and the RAW-based `sha256`/`characters` pass through untouched,
774
- * and real empty lists stay empty.
775
- *
776
- * Pure + deterministic and exported so the route harness reuses the exact
777
- * production implementation (same code path as getRuntimeSnapshot).
778
- */
779
- export function projectRuntimeSnapshot(input) {
780
- const field = (value, max) => value === undefined ? undefined : redactRuntimeText(value, max);
781
- const projectSkill = (skill) => ({
782
- name: field(skill.name, 200) ?? "",
783
- description: field(skill.description, 600) ?? "",
784
- path: field(skill.path, 400) ?? "",
785
- ...(skill.baseDir ? { baseDir: field(skill.baseDir, 400) } : {}),
786
- scope: skill.scope,
787
- source: field(skill.source, 200) ?? "",
788
- resourceId: field(skill.resourceId, 200) ?? "",
789
- autoInvocationEnabled: skill.autoInvocationEnabled,
790
- diagnostics: skill.diagnostics.map((diag) => field(diag, 400) ?? ""),
791
- });
792
- const projectExtension = (ext) => ({
793
- name: field(ext.name, 200) ?? "",
794
- path: field(ext.path, 400) ?? "",
795
- ...(ext.resolvedPath
796
- ? { resolvedPath: field(ext.resolvedPath, 400) }
797
- : {}),
798
- source: field(ext.source, 200) ?? "",
799
- scope: ext.scope,
800
- ...(ext.origin ? { origin: field(ext.origin, 200) } : {}),
801
- enabled: ext.enabled,
802
- ...(ext.error ? { error: field(ext.error, 400) } : {}),
803
- });
804
- const projectPackage = (pkg) => ({
805
- spec: field(pkg.spec, 200) ?? "",
806
- scope: pkg.scope,
807
- enabled: pkg.enabled,
808
- ...(pkg.version ? { version: field(pkg.version, 80) } : {}),
809
- ...(pkg.configuredVersion
810
- ? { configuredVersion: field(pkg.configuredVersion, 80) }
811
- : {}),
812
- ...(pkg.packageName ? { packageName: field(pkg.packageName, 200) } : {}),
813
- ...(pkg.path ? { path: field(pkg.path, 400) } : {}),
814
- ...(pkg.installedPath ? { installedPath: field(pkg.installedPath, 400) } : {}),
815
- ...(pkg.cwd ? { cwd: field(pkg.cwd, 400) } : {}),
816
- resolved: pkg.resolved.map((resource) => ({
817
- type: resource.type,
818
- name: field(resource.name, 200) ?? "",
819
- path: field(resource.path, 400) ?? "",
820
- source: field(resource.source, 200) ?? "",
821
- scope: resource.scope,
822
- })),
823
- diagnostics: pkg.diagnostics.map((diag) => field(diag, 400) ?? ""),
824
- });
825
- const projectMcpTool = (tool) => ({
826
- name: field(tool.name, 200) ?? "",
827
- description: field(tool.description, 600) ?? "",
828
- ...(tool.server ? { server: field(tool.server, 200) } : {}),
829
- source: field(tool.source, 200) ?? "",
830
- scope: tool.scope,
831
- ...(tool.path ? { path: field(tool.path, 400) } : {}),
832
- active: tool.active,
833
- });
834
- const projectMcp = (mcp) => ({
835
- servers: mcp.servers.map((server) => ({
836
- name: field(server.name, 200) ?? "",
837
- scope: server.scope,
838
- status: server.status,
839
- enabled: server.enabled,
840
- ...(server.transport ? { transport: field(server.transport, 80) } : {}),
841
- ...(server.command ? { command: field(server.command, 400) } : {}),
842
- ...(server.url ? { url: field(server.url, 400) } : {}),
843
- ...(server.configPath ? { configPath: field(server.configPath, 400) } : {}),
844
- tools: server.tools.map(projectMcpTool),
845
- })),
846
- unassignedTools: mcp.unassignedTools.map(projectMcpTool),
847
- });
848
- const projectDiagnostic = (diag) => ({
849
- type: diag.type,
850
- ...(diag.code ? { code: diag.code } : {}),
851
- message: field(diag.message, 400) ?? "",
852
- ...(diag.path ? { path: field(diag.path, 400) } : {}),
853
- ...(diag.retryable !== undefined
854
- ? { retryable: diag.retryable }
855
- : {}),
856
- });
857
- return {
858
- sessionId: input.sessionId,
859
- revision: input.revision,
860
- loadedAt: input.loadedAt,
861
- selection: {
862
- ...(input.selection.model
863
- ? {
864
- model: {
865
- provider: input.selection.model.provider,
866
- id: input.selection.model.id,
867
- ...(input.selection.model.name
868
- ? { name: field(input.selection.model.name, 200) }
869
- : {}),
870
- },
871
- }
872
- : {}),
873
- thinkingSelection: input.selection.thinkingSelection,
874
- ...(input.selection.effectiveThinkingLevel
875
- ? { effectiveThinkingLevel: input.selection.effectiveThinkingLevel }
876
- : {}),
877
- },
878
- models: input.models.map((model) => ({
879
- provider: model.provider,
880
- id: model.id,
881
- ...(model.name ? { name: field(model.name, 200) } : {}),
882
- ...(model.hasCredentials !== undefined
883
- ? { hasCredentials: model.hasCredentials }
884
- : {}),
885
- })),
886
- thinkingLevels: input.thinkingLevels.map((option) => ({ ...option })),
887
- systemPrompt: {
888
- ...(input.systemPrompt.raw !== undefined
889
- ? { raw: input.systemPrompt.raw }
890
- : {}),
891
- text: redactRuntimeText(input.systemPrompt.text, RUNTIME_CONTEXT_TEXT_MAX),
892
- sha256: input.systemPrompt.sha256,
893
- characters: input.systemPrompt.characters,
894
- },
895
- activeTools: [...input.activeTools],
896
- contextFiles: input.contextFiles.map((file) => ({
897
- path: field(file.path, 400) ?? "",
898
- characters: file.characters,
899
- })),
900
- skills: input.skills.map(projectSkill),
901
- extensions: input.extensions.map(projectExtension),
902
- packages: input.packages.map(projectPackage),
903
- mcp: projectMcp(input.mcp ?? { servers: [], unassignedTools: [] }),
904
- diagnostics: input.diagnostics.map(projectDiagnostic),
905
- hasBash: input.hasBash,
906
- };
907
- }
288
+ export { THINKING_LEVELS, RuntimeSnapshotUnavailableError, projectRuntimeSnapshot, };
289
+ export { createDefaultPiSdkBindings } from "./pi-runtime/sdk-bindings.js";
908
290
  export { extractAssistantContentBlocks, extractAssistantText, isAssistantSdkMessage };
909
291
  function readSessionManagerId(manager) {
910
292
  if (!manager || typeof manager !== "object")
@@ -1021,12 +403,14 @@ export class ConsolePiRuntime {
1021
403
  sessionInit = new Map();
1022
404
  /** Resolved model binding after materialization (create may return before this is known). */
1023
405
  sessionModels = new Map();
406
+ /** Durable session mode; record is the source of truth, this is the live handle cache. */
407
+ sessionModes = new Map();
1024
408
  /** SessionIds disposed while materialization was still in flight. */
1025
409
  disposedSessions = new Set();
1026
410
  compactionControllers = new Map();
1027
411
  siftStates = new Map();
1028
412
  /** Session-less tool definition caches (no AgentSession state). */
1029
- operatorCustomToolsPromise;
413
+ operatorCustomToolsByMode = new Map();
1030
414
  exploreCustomToolsPromise;
1031
415
  exploreOrchestrator;
1032
416
  bindings;
@@ -1054,6 +438,9 @@ export class ConsolePiRuntime {
1054
438
  },
1055
439
  });
1056
440
  }
441
+ async syncAimaxAuthToken() {
442
+ await this.options.sddDataAlignment?.auth?.getToken();
443
+ }
1057
444
  /** Absolute path for repo-scoped resource preferences (may be undefined in unit tests). */
1058
445
  get resourcePreferencesPath() {
1059
446
  return this.options.resourcePreferencesPath;
@@ -1082,10 +469,20 @@ export class ConsolePiRuntime {
1082
469
  * after a PATCH sees the latest resource-preferences.json (not create-time
1083
470
  * closure state).
1084
471
  */
472
+ sessionModeOf(sessionId) {
473
+ if (!sessionId)
474
+ return DEFAULT_CONSOLE_CHAT_SESSION_MODE;
475
+ return this.sessionModes.get(sessionId) ?? DEFAULT_CONSOLE_CHAT_SESSION_MODE;
476
+ }
477
+ rememberSessionMode(sessionId, sessionMode = DEFAULT_CONSOLE_CHAT_SESSION_MODE) {
478
+ this.sessionModes.set(sessionId, sessionMode);
479
+ return sessionMode;
480
+ }
1085
481
  async buildResourceLoaderOptions(base = {}, sessionId) {
1086
482
  const inheritedFactories = Array.isArray(base.extensionFactories)
1087
483
  ? base.extensionFactories
1088
484
  : [];
485
+ const sessionMode = this.sessionModeOf(sessionId);
1089
486
  const todoContextExtension = sessionId && this.options.todoRegistry
1090
487
  ? createTodoCompactionContextExtension({
1091
488
  sessionId,
@@ -1341,8 +738,10 @@ export class ConsolePiRuntime {
1341
738
  * `initializing: true`; dependent routes wait on the same promise.
1342
739
  */
1343
740
  async createSession(init) {
741
+ await this.syncAimaxAuthToken();
1344
742
  const sessionId = `operator-chat-${randomBytes(12).toString("hex")}`;
1345
743
  this.disposedSessions.delete(sessionId);
744
+ const sessionMode = this.rememberSessionMode(sessionId, init?.sessionMode ?? DEFAULT_CONSOLE_CHAT_SESSION_MODE);
1346
745
  const sessionManager = await this.bindings.createSessionManager({
1347
746
  cwd: this.options.cwd,
1348
747
  sessionDir: this.options.sessionDir,
@@ -1368,6 +767,7 @@ export class ConsolePiRuntime {
1368
767
  sessionManager,
1369
768
  model: init?.model,
1370
769
  systemPromptSuffix: init?.systemPromptSuffix,
770
+ sessionMode,
1371
771
  // Create path must resolve default model from the SAME services object
1372
772
  // used for formal materialization (no throwaway createServices).
1373
773
  resolveDefaultModel: !init?.model,
@@ -1377,7 +777,8 @@ export class ConsolePiRuntime {
1377
777
  sessionFile,
1378
778
  createdAt: new Date().toISOString(),
1379
779
  model: init?.model,
1380
- activeTools: OPERATOR_CHAT_ACTIVE_TOOL_NAMES(),
780
+ activeTools: chatActiveToolBaseline(sessionMode),
781
+ sessionMode,
1381
782
  initializing: true,
1382
783
  };
1383
784
  }
@@ -1435,28 +836,33 @@ export class ConsolePiRuntime {
1435
836
  this.sessionInit.set(sessionId, { status: "initializing", promise });
1436
837
  return promise;
1437
838
  }
1438
- async materializeSession(sessionId, init) {
1439
- if (this.disposedSessions.has(sessionId))
1440
- return;
839
+ /**
840
+ * Shared session preparation for create/reopen/fork: config capture, prompt
841
+ * composition, resource loading, model resolution, tool assembly, SDK session
842
+ * create, extension bind, tool pin, compaction override. Lifecycle maps stay
843
+ * owned by the caller.
844
+ */
845
+ async prepareLiveSession(init) {
846
+ const aborted = () => Boolean(init.abortIfDisposed && this.disposedSessions.has(init.sessionId));
847
+ if (aborted())
848
+ return undefined;
1441
849
  const agentDir = await safeGetAgentDir(this.bindings);
1442
850
  // Capture before services parse models.json. A concurrent edit can therefore
1443
851
  // cause at most one harmless extra refresh, never a missed next-turn refresh.
1444
852
  const modelConfigFile = await captureModelConfigFile(agentDir);
1445
- const appendSystemPrompt = [
1446
- OPERATOR_CHAT_SYSTEM_PROMPT_BASE,
1447
- ...(init.systemPromptSuffix ? [init.systemPromptSuffix] : []),
1448
- ];
1449
- // Single services construction for formal materialization (+ optional
1450
- // default-model resolve from the same modelRuntime). Repo-scoped skill
1451
- // auto-invocation prefs inject skillsOverride without touching SKILL.md.
853
+ const appendSystemPrompt = composeChatSystemPromptParts({
854
+ sessionMode: init.sessionMode,
855
+ workflowPrompt: OPERATOR_CHAT_SYSTEM_PROMPT_BASE,
856
+ systemPromptSuffix: init.systemPromptSuffix,
857
+ });
1452
858
  await this.refreshSkillPrefsCache().catch(() => undefined);
1453
859
  const { services } = await this.bindings.createServices({
1454
860
  cwd: this.options.cwd,
1455
861
  agentDir,
1456
- resourceLoaderOptions: await this.buildResourceLoaderOptions({ appendSystemPrompt }, sessionId),
862
+ resourceLoaderOptions: await this.buildResourceLoaderOptions({ appendSystemPrompt }, init.sessionId),
1457
863
  });
1458
- if (this.disposedSessions.has(sessionId))
1459
- return;
864
+ if (aborted())
865
+ return undefined;
1460
866
  const modelRuntime = services.modelRuntime;
1461
867
  let model = init.model;
1462
868
  if (!model && init.resolveDefaultModel) {
@@ -1470,9 +876,9 @@ export class ConsolePiRuntime {
1470
876
  }) ?? undefined)
1471
877
  : undefined;
1472
878
  assertNoWriteToolInList(OPERATOR_CHAT_ALLOWED_TOOLS);
1473
- const customTools = await this.loadCustomTools(sessionId);
1474
- if (this.disposedSessions.has(sessionId))
1475
- return;
879
+ const customTools = await this.loadCustomTools(init.sessionId);
880
+ if (aborted())
881
+ return undefined;
1476
882
  // pi-web parity: do NOT pass `tools`. A closed allowlist becomes
1477
883
  // SDK allowedToolNames and filters extension MCP tools out of the
1478
884
  // registry before Gate 2 can re-add them. Operator/safe customTools
@@ -1486,35 +892,59 @@ export class ConsolePiRuntime {
1486
892
  ...(customTools.length > 0 ? { customTools } : {}),
1487
893
  ...(resolvedModel ? { model: resolvedModel } : {}),
1488
894
  });
1489
- if (this.disposedSessions.has(sessionId)) {
895
+ if (aborted()) {
1490
896
  try {
1491
897
  session.dispose();
1492
898
  }
1493
899
  catch {
1494
900
  // ignore late dispose cleanup
1495
901
  }
1496
- return;
902
+ return undefined;
1497
903
  }
1498
904
  await bindSessionExtensions(session);
1499
- pinOperatorChatTools(session);
905
+ pinOperatorChatTools(session, init.sessionMode);
906
+ this.applySessionCompactionOverride(init.sessionId, services, session);
907
+ return {
908
+ session,
909
+ modelRuntime,
910
+ modelConfigFile,
911
+ model,
912
+ services: {
913
+ resourceLoader: services.resourceLoader,
914
+ settingsManager: services.settingsManager,
915
+ },
916
+ };
917
+ }
918
+ async materializeSession(sessionId, init) {
919
+ if (this.disposedSessions.has(sessionId))
920
+ return;
921
+ await this.syncAimaxAuthToken();
922
+ const sessionMode = this.rememberSessionMode(sessionId, init.sessionMode ?? this.sessionModeOf(sessionId));
923
+ const prepared = await this.prepareLiveSession({
924
+ sessionId,
925
+ sessionManager: init.sessionManager,
926
+ model: init.model,
927
+ systemPromptSuffix: init.systemPromptSuffix,
928
+ sessionMode,
929
+ resolveDefaultModel: init.resolveDefaultModel,
930
+ abortIfDisposed: true,
931
+ });
932
+ if (!prepared)
933
+ return;
1500
934
  // Register then re-check dispose race: a concurrent dispose must win.
1501
- this.sessions.set(sessionId, session);
935
+ this.sessions.set(sessionId, prepared.session);
1502
936
  this.sessionManagers.set(sessionId, init.sessionManager);
1503
- this.modelRuntimes.set(sessionId, modelRuntime);
1504
- this.modelConfigFiles.set(sessionId, modelConfigFile);
1505
- this.sessionModels.set(sessionId, model);
1506
- this.serviceScopes.set(sessionId, {
1507
- resourceLoader: services.resourceLoader,
1508
- settingsManager: services.settingsManager,
1509
- });
1510
- this.applySessionCompactionOverride(sessionId, services, session);
937
+ this.modelRuntimes.set(sessionId, prepared.modelRuntime);
938
+ this.modelConfigFiles.set(sessionId, prepared.modelConfigFile);
939
+ this.sessionModels.set(sessionId, prepared.model);
940
+ this.serviceScopes.set(sessionId, prepared.services);
1511
941
  this.revisions.set(sessionId, 1);
1512
942
  if (!this.thinkingSelections.has(sessionId)) {
1513
943
  this.thinkingSelections.set(sessionId, "auto");
1514
944
  }
1515
945
  if (this.disposedSessions.has(sessionId)) {
1516
946
  try {
1517
- session.dispose();
947
+ prepared.session.dispose();
1518
948
  }
1519
949
  catch {
1520
950
  // ignore
@@ -1524,6 +954,7 @@ export class ConsolePiRuntime {
1524
954
  this.modelRuntimes.delete(sessionId);
1525
955
  this.modelConfigFiles.delete(sessionId);
1526
956
  this.sessionModels.delete(sessionId);
957
+ this.sessionModes.delete(sessionId);
1527
958
  this.serviceScopes.delete(sessionId);
1528
959
  this.revisions.delete(sessionId);
1529
960
  this.thinkingSelections.delete(sessionId);
@@ -1531,8 +962,12 @@ export class ConsolePiRuntime {
1531
962
  }
1532
963
  /** Cached session-less custom tool definitions (operator + explore). */
1533
964
  async loadCustomTools(sessionId) {
965
+ const sessionMode = this.sessionModeOf(sessionId);
966
+ const loadWorkflowTools = shouldLoadWorkflowBusinessTools(sessionMode);
1534
967
  const [operatorCustomTools, exploreCustomTools] = await Promise.all([
1535
- this.loadOperatorCustomTools(),
968
+ loadWorkflowTools
969
+ ? this.loadOperatorCustomTools(sessionMode)
970
+ : Promise.resolve([]),
1536
971
  this.loadExploreCustomTools(),
1537
972
  ]);
1538
973
  const questionTool = this.options.questionRegistry
@@ -1542,11 +977,31 @@ export class ConsolePiRuntime {
1542
977
  })
1543
978
  : undefined;
1544
979
  const todoTool = this.options.todoRegistry
1545
- ? await buildTodoWriteTool({ sessionId, registry: this.options.todoRegistry })
980
+ ? await buildTodoWriteTool({
981
+ sessionId,
982
+ registry: this.options.todoRegistry,
983
+ sessionMode,
984
+ })
1546
985
  : undefined;
1547
986
  const goalTools = this.options.goalRegistry
1548
987
  ? await buildGoalTools({ sessionId, registry: this.options.goalRegistry })
1549
988
  : [];
989
+ const terminalTools = this.options.terminalRegistry
990
+ ? await buildPersistentTerminalTools({
991
+ ownerSessionId: sessionId,
992
+ registry: this.options.terminalRegistry,
993
+ defaultCwd: this.options.cwd,
994
+ }).catch((error) => {
995
+ process.stderr.write(`[console] chat terminal-tools build failed: ${error instanceof Error ? error.message : String(error)}\n`);
996
+ return [];
997
+ })
998
+ : [];
999
+ const scmTools = this.options.scmService
1000
+ ? await buildScmTools({ scm: this.options.scmService }).catch((error) => {
1001
+ process.stderr.write(`[console] chat scm-tools build failed: ${error instanceof Error ? error.message : String(error)}\n`);
1002
+ return [];
1003
+ })
1004
+ : [];
1550
1005
  const agentTool = await buildAgentCustomTool({
1551
1006
  parentSessionId: sessionId,
1552
1007
  orchestrator: this.exploreOrchestrator,
@@ -1571,23 +1026,28 @@ export class ConsolePiRuntime {
1571
1026
  ...(questionTool ? [questionTool] : []),
1572
1027
  ...(todoTool ? [todoTool] : []),
1573
1028
  ...goalTools,
1029
+ ...terminalTools,
1030
+ ...scmTools,
1574
1031
  ...(agentTool ? [agentTool] : []),
1575
1032
  ];
1576
1033
  }
1577
- loadOperatorCustomTools() {
1034
+ loadOperatorCustomTools(sessionMode) {
1578
1035
  if (!this.options.actionContext)
1579
1036
  return Promise.resolve([]);
1580
- if (!this.operatorCustomToolsPromise) {
1581
- this.operatorCustomToolsPromise = buildCustomOperatorTools({
1582
- actionContext: this.options.actionContext,
1583
- }).catch((error) => {
1584
- process.stderr.write(`[console] chat custom-tools build failed: ${error instanceof Error ? error.message : String(error)}\n`);
1585
- // Allow a later create/reopen to retry after a transient failure.
1586
- this.operatorCustomToolsPromise = undefined;
1587
- return [];
1588
- });
1589
- }
1590
- return this.operatorCustomToolsPromise;
1037
+ const cached = this.operatorCustomToolsByMode.get(sessionMode);
1038
+ if (cached)
1039
+ return cached;
1040
+ const promise = buildCustomOperatorTools({
1041
+ actionContext: this.options.actionContext,
1042
+ sessionMode,
1043
+ }).catch((error) => {
1044
+ process.stderr.write(`[console] chat custom-tools build failed: ${error instanceof Error ? error.message : String(error)}\n`);
1045
+ // Allow a later create/reopen to retry after a transient failure.
1046
+ this.operatorCustomToolsByMode.delete(sessionMode);
1047
+ return [];
1048
+ });
1049
+ this.operatorCustomToolsByMode.set(sessionMode, promise);
1050
+ return promise;
1591
1051
  }
1592
1052
  loadExploreCustomTools() {
1593
1053
  if (!this.exploreCustomToolsPromise) {
@@ -1605,6 +1065,7 @@ export class ConsolePiRuntime {
1605
1065
  if (!this.bindings.forkSessionManager)
1606
1066
  throw new Error("SESSION_FORK_UNSUPPORTED");
1607
1067
  const sessionId = `operator-chat-${randomBytes(12).toString("hex")}`;
1068
+ const sessionMode = this.rememberSessionMode(sessionId, init.sessionMode ?? DEFAULT_CONSOLE_CHAT_SESSION_MODE);
1608
1069
  const sessionManager = await this.bindings.forkSessionManager({
1609
1070
  sourcePath: init.sourceSessionFile,
1610
1071
  targetCwd: this.options.cwd,
@@ -1615,8 +1076,8 @@ export class ConsolePiRuntime {
1615
1076
  sessionId,
1616
1077
  sessionManager,
1617
1078
  model: init.model,
1079
+ sessionMode,
1618
1080
  });
1619
- built.session.setActiveToolsByName(computeOperatorChatActiveToolNames(built.session));
1620
1081
  this.sessions.set(sessionId, built.session);
1621
1082
  this.sessionManagers.set(sessionId, sessionManager);
1622
1083
  this.modelRuntimes.set(sessionId, built.modelRuntime);
@@ -1632,6 +1093,7 @@ export class ConsolePiRuntime {
1632
1093
  createdAt: new Date().toISOString(),
1633
1094
  model: init.model,
1634
1095
  activeTools: built.session.getActiveToolNames(),
1096
+ sessionMode,
1635
1097
  };
1636
1098
  }
1637
1099
  async branchSession(input) {
@@ -1810,15 +1272,22 @@ export class ConsolePiRuntime {
1810
1272
  async getRuntimeSystemPrompt(sessionId) {
1811
1273
  await this.ensureSessionReady(sessionId).catch(() => undefined);
1812
1274
  const session = this.sessions.get(sessionId);
1813
- if (!session)
1814
- return composeOperatorChatSystemPrompt({});
1275
+ if (!session) {
1276
+ return composeChatSystemPrompt({
1277
+ sessionMode: this.sessionModeOf(sessionId),
1278
+ workflowPrompt: OPERATOR_CHAT_SYSTEM_PROMPT_BASE,
1279
+ });
1280
+ }
1815
1281
  const fromState = session.state?.systemPrompt;
1816
1282
  const fromGetter = session.systemPrompt;
1817
1283
  const loader = this.serviceScopes.get(sessionId)?.resourceLoader;
1818
1284
  return (fromState ||
1819
1285
  fromGetter ||
1820
1286
  loader?.getSystemPrompt?.() ||
1821
- composeOperatorChatSystemPrompt({}));
1287
+ composeChatSystemPrompt({
1288
+ sessionMode: this.sessionModeOf(sessionId),
1289
+ workflowPrompt: OPERATOR_CHAT_SYSTEM_PROMPT_BASE,
1290
+ }));
1822
1291
  }
1823
1292
  /**
1824
1293
  * Read the RAW system prompt for the snapshot, converting any projection
@@ -1911,6 +1380,7 @@ export class ConsolePiRuntime {
1911
1380
  if (services)
1912
1381
  this.applySessionCompactionOverride(sessionId, services, session);
1913
1382
  await this.refreshModelConfiguration(sessionId, "force");
1383
+ pinOperatorChatTools(session, this.sessionModeOf(sessionId));
1914
1384
  }
1915
1385
  catch (error) {
1916
1386
  return {
@@ -1941,91 +1411,14 @@ export class ConsolePiRuntime {
1941
1411
  * Never returns prompt/skill file bodies.
1942
1412
  */
1943
1413
  async listSlashCommands(sessionId) {
1944
- const scrub = (value) => {
1945
- const raw = String(value ?? "");
1946
- return raw
1947
- .replace(/\/Users\/[^\s]+/gi, "~")
1948
- .replace(/[A-Za-z]:\\Users\\[^\s]+/gi, "~")
1949
- .slice(0, 240);
1950
- };
1951
1414
  // Fail soft at the HTTP layer: bubble readiness errors so routes can attach
1952
1415
  // a compact warning while the UI keeps local Operator/Pi Web commands.
1953
1416
  await this.ensureSessionReady(sessionId);
1954
1417
  const session = this.sessions.get(sessionId);
1955
1418
  if (!session)
1956
1419
  return { commands: [] };
1957
- // Same-name conflict priority (AC-3): extension > skill > prompt.
1958
- // Enum order must not decide the winner — prompt-before-skill still loses.
1959
- const SOURCE_RANK = {
1960
- extension: 0,
1961
- skill: 1,
1962
- prompt: 2,
1963
- };
1964
- const byKey = new Map();
1965
- const sourceWarnings = [];
1966
- const push = (command, label, description, source) => {
1967
- const name = command.startsWith("/") ? command : `/${command}`;
1968
- const key = name.toLowerCase();
1969
- if (!key || key === "/")
1970
- return;
1971
- const next = {
1972
- command: name,
1973
- label: (label || name).slice(0, 80),
1974
- description: scrub(description),
1975
- source,
1976
- };
1977
- const existing = byKey.get(key);
1978
- if (!existing || SOURCE_RANK[source] < SOURCE_RANK[existing.source]) {
1979
- byKey.set(key, next);
1980
- }
1981
- };
1982
- try {
1983
- const cmds = session.extensionRunner?.getRegisteredCommands?.() ?? [];
1984
- for (const cmd of cmds) {
1985
- const name = cmd.invocationName || cmd.name;
1986
- if (!name)
1987
- continue;
1988
- push(String(name), String(cmd.name || name), String(cmd.description || ""), "extension");
1989
- }
1990
- }
1991
- catch (error) {
1992
- // Source isolation: keep other sources; surface compact warning (AC-3).
1993
- sourceWarnings.push(`extension: ${scrub(error instanceof Error ? error.message : String(error))}`);
1994
- }
1995
- try {
1996
- // Enumerate prompts before skills on purpose: priority must still let skill win.
1997
- for (const tpl of session.promptTemplates ?? []) {
1998
- if (!tpl?.name)
1999
- continue;
2000
- push(String(tpl.name), String(tpl.name), String(tpl.description || ""), "prompt");
2001
- }
2002
- }
2003
- catch (error) {
2004
- sourceWarnings.push(`prompt: ${scrub(error instanceof Error ? error.message : String(error))}`);
2005
- }
2006
- try {
2007
- const services = this.serviceScopes.get(sessionId);
2008
- const resourceLoader = services?.resourceLoader;
2009
- const skills = resourceLoader?.getSkills?.()?.skills ?? [];
2010
- for (const skill of skills) {
2011
- if (!skill?.name)
2012
- continue;
2013
- const command = skill.name.startsWith("skill:")
2014
- ? `/${skill.name}`
2015
- : `/skill:${skill.name}`;
2016
- push(command, skill.name, String(skill.description || ""), "skill");
2017
- }
2018
- }
2019
- catch (error) {
2020
- sourceWarnings.push(`skill: ${scrub(error instanceof Error ? error.message : String(error))}`);
2021
- }
2022
- const warning = sourceWarnings.length > 0
2023
- ? sourceWarnings.join("; ").slice(0, 160)
2024
- : undefined;
2025
- return {
2026
- commands: [...byKey.values()],
2027
- ...(warning ? { warning } : {}),
2028
- };
1420
+ const resourceLoader = this.serviceScopes.get(sessionId)?.resourceLoader;
1421
+ return collectSlashCommands({ session, resourceLoader });
2029
1422
  }
2030
1423
  async getRuntimeSnapshot(sessionId, options) {
2031
1424
  try {
@@ -2069,9 +1462,6 @@ export class ConsolePiRuntime {
2069
1462
  throw new RuntimeSnapshotUnavailableError(`model enumeration failed: ${error instanceof Error ? error.message : String(error)}`);
2070
1463
  }
2071
1464
  }
2072
- // Thinking options: auto is always offered; explicit levels only when the
2073
- // runtime session supports setThinkingLevel (xhigh/max ship with the SDK
2074
- // and therefore surface only when this runtime provides them).
2075
1465
  const supportedLevels = typeof session.setThinkingLevel === "function"
2076
1466
  ? [...THINKING_LEVELS]
2077
1467
  : [];
@@ -2079,220 +1469,25 @@ export class ConsolePiRuntime {
2079
1469
  { value: "auto" },
2080
1470
  ...supportedLevels.map((value) => ({ value })),
2081
1471
  ];
2082
- // Resource inventories from the session ResourceLoader (ADR 0012 open
2083
- // surface). Missing loader → real empty lists, never fabricated entries.
2084
- // Each subface is isolated (RF-02): a throwing subface keeps the other
2085
- // inventories AND surfaces a structured retryable diagnostic instead of
2086
- // silently dropping the whole resource projection.
2087
- const contextFiles = [];
2088
- const skills = [];
2089
- const extensions = [];
2090
- const diagnostics = [];
2091
- const resolvedResources = [];
2092
- const resourceFailure = (subface, error) => {
2093
- diagnostics.push({
2094
- type: "error",
2095
- code: "RESOURCE_LOAD_FAILED",
2096
- message: `${subface} inventory failed: ${error instanceof Error ? error.message : String(error)}`,
2097
- retryable: true,
2098
- });
2099
- };
1472
+ let skillPrefs = { schemaVersion: 1, skills: {} };
1473
+ let skillPrefsError;
2100
1474
  if (resourceLoader) {
2101
- try {
2102
- const agentsFiles = resourceLoader.getAgentsFiles?.();
2103
- for (const file of agentsFiles?.agentsFiles ?? []) {
2104
- if (!file.path)
2105
- continue;
2106
- contextFiles.push({
2107
- path: file.path,
2108
- characters: file.content?.length ?? 0,
2109
- });
2110
- }
2111
- }
2112
- catch (error) {
2113
- resourceFailure("context files", error);
2114
- }
2115
- // Preferences are the source of truth for UI auto-invocation state;
2116
- // loader disableModelInvocation may also reflect frontmatter. Prefer
2117
- // explicit preference (default on) when projecting autoInvocationEnabled.
2118
- let skillPrefs = { schemaVersion: 1, skills: {} };
2119
1475
  try {
2120
1476
  skillPrefs = await this.loadSkillAutoInvocationPreferences();
2121
1477
  }
2122
1478
  catch (error) {
2123
- diagnostics.push({
2124
- type: "error",
2125
- code: "PI_RESOURCE_PREFERENCES_MALFORMED",
2126
- message: error instanceof Error ? error.message : String(error),
2127
- retryable: true,
2128
- });
2129
- }
2130
- try {
2131
- const skillsResult = resourceLoader.getSkills?.();
2132
- for (const skill of skillsResult?.skills ?? []) {
2133
- const resourceId = skillResourceId({
2134
- name: skill.name,
2135
- filePath: skill.filePath,
2136
- });
2137
- // Preference off always means autoInvocationEnabled=false even if
2138
- // loader already applied disableModelInvocation. Frontmatter-only
2139
- // disable (no preference) also surfaces as autoInvocationEnabled=false.
2140
- const prefEnabled = isAutoInvocationEnabled(skillPrefs, resourceId);
2141
- const autoInvocationEnabled = prefEnabled && skill.disableModelInvocation !== true;
2142
- skills.push({
2143
- name: skill.name,
2144
- description: skill.description ?? "",
2145
- path: skill.filePath,
2146
- ...(skill.baseDir ? { baseDir: skill.baseDir } : {}),
2147
- scope: skill.sourceInfo?.scope ?? "unknown",
2148
- source: skill.sourceInfo?.source ?? "",
2149
- resourceId,
2150
- autoInvocationEnabled,
2151
- diagnostics: [],
2152
- });
2153
- resolvedResources.push({
2154
- type: "skill",
2155
- name: skill.name,
2156
- path: skill.filePath,
2157
- source: skill.sourceInfo?.source ?? "",
2158
- scope: skill.sourceInfo?.scope ?? "unknown",
2159
- });
2160
- }
2161
- for (const diag of skillsResult?.diagnostics ?? []) {
2162
- if (!diag?.message)
2163
- continue;
2164
- diagnostics.push({
2165
- type: diag.type ?? "warning",
2166
- message: diag.message,
2167
- ...(diag.path ? { path: diag.path } : {}),
2168
- });
2169
- }
2170
- }
2171
- catch (error) {
2172
- resourceFailure("skills", error);
2173
- }
2174
- try {
2175
- const extResult = resourceLoader.getExtensions?.();
2176
- for (const ext of extResult?.extensions ?? []) {
2177
- const path = ext.resolvedPath ?? ext.path ?? "";
2178
- extensions.push({
2179
- name: ext.sourceInfo?.source ?? ext.path ?? "extension",
2180
- path,
2181
- ...(ext.resolvedPath ? { resolvedPath: ext.resolvedPath } : {}),
2182
- source: ext.sourceInfo?.source ?? "",
2183
- scope: ext.sourceInfo?.scope ?? "unknown",
2184
- ...(ext.sourceInfo?.origin ? { origin: ext.sourceInfo.origin } : {}),
2185
- enabled: true,
2186
- });
2187
- resolvedResources.push({
2188
- type: "extension",
2189
- name: ext.sourceInfo?.source ?? ext.path ?? "extension",
2190
- path,
2191
- source: ext.sourceInfo?.source ?? "",
2192
- scope: ext.sourceInfo?.scope ?? "unknown",
2193
- });
2194
- }
2195
- for (const err of extResult?.errors ?? []) {
2196
- if (!err?.error)
2197
- continue;
2198
- diagnostics.push({
2199
- type: "error",
2200
- message: err.error,
2201
- ...(err.path ? { path: err.path } : {}),
2202
- });
2203
- }
2204
- }
2205
- catch (error) {
2206
- resourceFailure("extensions", error);
1479
+ skillPrefsError = error;
2207
1480
  }
2208
- try {
2209
- const promptsResult = resourceLoader.getPrompts?.();
2210
- for (const prompt of promptsResult?.prompts ?? []) {
2211
- resolvedResources.push({
2212
- type: "prompt",
2213
- name: prompt.name,
2214
- path: prompt.filePath,
2215
- source: prompt.sourceInfo?.source ?? "",
2216
- scope: prompt.sourceInfo?.scope ?? "unknown",
2217
- });
2218
- }
2219
- for (const diag of promptsResult?.diagnostics ?? []) {
2220
- if (!diag?.message)
2221
- continue;
2222
- diagnostics.push({
2223
- type: diag.type ?? "warning",
2224
- message: diag.message,
2225
- ...(diag.path ? { path: diag.path } : {}),
2226
- });
2227
- }
2228
- }
2229
- catch (error) {
2230
- resourceFailure("prompts", error);
2231
- }
2232
- try {
2233
- const themesResult = resourceLoader.getThemes?.();
2234
- for (const theme of themesResult?.themes ?? []) {
2235
- if (!theme.path)
2236
- continue;
2237
- resolvedResources.push({
2238
- type: "theme",
2239
- name: theme.name ?? theme.path,
2240
- path: theme.path,
2241
- source: "",
2242
- scope: "unknown",
2243
- });
2244
- }
2245
- for (const diag of themesResult?.diagnostics ?? []) {
2246
- if (!diag?.message)
2247
- continue;
2248
- diagnostics.push({
2249
- type: diag.type ?? "warning",
2250
- message: diag.message,
2251
- ...(diag.path ? { path: diag.path } : {}),
2252
- });
2253
- }
2254
- }
2255
- catch (error) {
2256
- resourceFailure("themes", error);
2257
- }
2258
- }
2259
- // Packages: Pi settings specs + resolved resource metadata. Fields the SDK
2260
- // does not expose stay absent (real missing, never fabricated). A
2261
- // readPackageInventory failure is a PARTIAL failure (RF-02): the snapshot
2262
- // stays 200 with a structured retryable diagnostic — never a silent
2263
- // fake-empty package list.
2264
- const { readPackageInventory } = await import("./pi-console-config.js");
2265
- let packages = [];
2266
- try {
2267
- packages = (await readPackageInventory(await safeGetAgentDir(this.bindings), this.options.cwd, resolvedResources));
2268
- }
2269
- catch (error) {
2270
- packages = [];
2271
- diagnostics.push({
2272
- type: "error",
2273
- code: "PACKAGE_INVENTORY_FAILED",
2274
- message: error instanceof Error ? error.message : String(error),
2275
- retryable: true,
2276
- });
2277
- }
2278
- const { readMcpInventory } = await import("./mcp-inventory.js");
2279
- let mcp = { servers: [], unassignedTools: [] };
2280
- try {
2281
- mcp = await readMcpInventory({
2282
- agentDir: await safeGetAgentDir(this.bindings),
2283
- cwd: this.options.cwd,
2284
- tools: session.getAllTools?.() ?? [],
2285
- activeToolNames: session.getActiveToolNames(),
2286
- });
2287
- }
2288
- catch (error) {
2289
- diagnostics.push({
2290
- type: "error",
2291
- code: "MCP_INVENTORY_FAILED",
2292
- message: error instanceof Error ? error.message : String(error),
2293
- retryable: true,
2294
- });
2295
1481
  }
1482
+ const inventories = await collectRuntimeInventories({
1483
+ resourceLoader,
1484
+ skillPrefs,
1485
+ skillPrefsError,
1486
+ agentDir: await safeGetAgentDir(this.bindings),
1487
+ cwd: this.options.cwd,
1488
+ tools: session.getAllTools?.() ?? [],
1489
+ activeToolNames: session.getActiveToolNames(),
1490
+ });
2296
1491
  return projectRuntimeSnapshot({
2297
1492
  sessionId,
2298
1493
  revision: this.revisions.get(sessionId) ?? 0,
@@ -2317,19 +1512,17 @@ export class ConsolePiRuntime {
2317
1512
  thinkingLevels,
2318
1513
  systemPrompt: {
2319
1514
  ...(options?.includeRaw ? { raw: rawSystemPrompt } : {}),
2320
- // The RAW prompt flows into the projection: text is scrubbed +
2321
- // length-limited while sha256/characters stay RAW-based (design §2.2).
2322
1515
  text: rawSystemPrompt,
2323
1516
  sha256: createHash("sha256").update(rawSystemPrompt).digest("hex"),
2324
1517
  characters: rawSystemPrompt.length,
2325
1518
  },
2326
1519
  activeTools: session.getActiveToolNames(),
2327
- contextFiles,
2328
- skills,
2329
- extensions,
2330
- packages,
2331
- mcp,
2332
- diagnostics,
1520
+ contextFiles: inventories.contextFiles,
1521
+ skills: inventories.skills,
1522
+ extensions: inventories.extensions,
1523
+ packages: inventories.packages,
1524
+ mcp: inventories.mcp,
1525
+ diagnostics: inventories.diagnostics,
2333
1526
  hasBash: true,
2334
1527
  });
2335
1528
  }
@@ -2347,7 +1540,9 @@ export class ConsolePiRuntime {
2347
1540
  * than silently creating a fresh empty session and losing prior context.
2348
1541
  */
2349
1542
  async reopenSession(init) {
1543
+ await this.syncAimaxAuthToken();
2350
1544
  const sessionId = init.sessionId;
1545
+ const sessionMode = this.rememberSessionMode(sessionId, init.sessionMode ?? this.sessionModeOf(sessionId));
2351
1546
  // Wait for in-flight create materialization rather than racing reopen.
2352
1547
  const initState = this.sessionInit.get(sessionId);
2353
1548
  if (initState?.status === "initializing") {
@@ -2424,6 +1619,7 @@ export class ConsolePiRuntime {
2424
1619
  sessionManager,
2425
1620
  model: init.model,
2426
1621
  systemPromptSuffix: init.systemPromptSuffix,
1622
+ sessionMode,
2427
1623
  });
2428
1624
  const session = built.session;
2429
1625
  // Prefer the SessionManager id (from the JSONL header). Some SDK
@@ -2446,7 +1642,6 @@ export class ConsolePiRuntime {
2446
1642
  message: `reopened Pi sessionId mismatch: expected ${sessionId}, got ${reportedId}`,
2447
1643
  };
2448
1644
  }
2449
- session.setActiveToolsByName(computeOperatorChatActiveToolNames(session));
2450
1645
  this.sessions.set(sessionId, session);
2451
1646
  this.sessionManagers.set(sessionId, sessionManager);
2452
1647
  this.modelRuntimes.set(sessionId, built.modelRuntime);
@@ -2464,6 +1659,7 @@ export class ConsolePiRuntime {
2464
1659
  createdAt: new Date().toISOString(),
2465
1660
  model: init.model,
2466
1661
  activeTools: session.getActiveToolNames(),
1662
+ sessionMode,
2467
1663
  },
2468
1664
  };
2469
1665
  }
@@ -2495,6 +1691,7 @@ export class ConsolePiRuntime {
2495
1691
  createdAt: new Date().toISOString(),
2496
1692
  model,
2497
1693
  activeTools: session.getActiveToolNames(),
1694
+ sessionMode: this.sessionModeOf(sessionId),
2498
1695
  };
2499
1696
  }
2500
1697
  /**
@@ -2503,46 +1700,22 @@ export class ConsolePiRuntime {
2503
1700
  * before AgentSession is ready. Tool definitions reuse runtime-level caches.
2504
1701
  */
2505
1702
  async buildSessionWithServices(init) {
2506
- const agentDir = await safeGetAgentDir(this.bindings);
2507
- const modelConfigFile = await captureModelConfigFile(agentDir);
2508
- const appendSystemPrompt = [
2509
- OPERATOR_CHAT_SYSTEM_PROMPT_BASE,
2510
- ...(init.systemPromptSuffix ? [init.systemPromptSuffix] : []),
2511
- ];
2512
- await this.refreshSkillPrefsCache().catch(() => undefined);
2513
- const { services } = await this.bindings.createServices({
2514
- cwd: this.options.cwd,
2515
- agentDir,
2516
- resourceLoaderOptions: await this.buildResourceLoaderOptions({ appendSystemPrompt }, init.sessionId),
2517
- });
2518
- const modelRuntime = services.modelRuntime;
2519
- const resolvedModel = init.model
2520
- ? (this.bindings.resolveModel({
2521
- modelRuntime,
2522
- provider: init.model.provider,
2523
- modelId: init.model.modelId,
2524
- }) ?? undefined)
2525
- : undefined;
2526
- assertNoWriteToolInList(OPERATOR_CHAT_ALLOWED_TOOLS);
2527
- const customTools = await this.loadCustomTools(init.sessionId);
2528
- const { session } = await this.bindings.createSessionFromServices({
2529
- services,
1703
+ const sessionMode = this.rememberSessionMode(init.sessionId, init.sessionMode ?? this.sessionModeOf(init.sessionId));
1704
+ const prepared = await this.prepareLiveSession({
1705
+ sessionId: init.sessionId,
2530
1706
  sessionManager: init.sessionManager,
2531
- excludeTools: [...OPERATOR_CHAT_SESSION_EXCLUDE_TOOLS],
2532
- ...(customTools.length > 0 ? { customTools } : {}),
2533
- ...(resolvedModel ? { model: resolvedModel } : {}),
1707
+ model: init.model,
1708
+ systemPromptSuffix: init.systemPromptSuffix,
1709
+ sessionMode,
2534
1710
  });
2535
- await bindSessionExtensions(session);
2536
- pinOperatorChatTools(session);
2537
- this.applySessionCompactionOverride(init.sessionId, services, session);
1711
+ if (!prepared) {
1712
+ throw new Error(`chat session preparation aborted: ${init.sessionId}`);
1713
+ }
2538
1714
  return {
2539
- session,
2540
- modelRuntime,
2541
- modelConfigFile,
2542
- services: {
2543
- resourceLoader: services.resourceLoader,
2544
- settingsManager: services.settingsManager,
2545
- },
1715
+ session: prepared.session,
1716
+ modelRuntime: prepared.modelRuntime,
1717
+ modelConfigFile: prepared.modelConfigFile,
1718
+ services: prepared.services,
2546
1719
  };
2547
1720
  }
2548
1721
  /** Re-open a previously created session (cross-request). */
@@ -2602,7 +1775,7 @@ export class ConsolePiRuntime {
2602
1775
  }
2603
1776
  /**
2604
1777
  * Send a prompt and stream events. Gate 3: each tool invocation is
2605
- * re-authorized by authorizeOperatorChatTool before the dispatcher runs it
1778
+ * re-authorized by authorizeChatSessionTool(mode) before the dispatcher runs it
2606
1779
  * (the dispatcher is wired by the HTTP layer, see chat-session.ts).
2607
1780
  *
2608
1781
  * Prompt ownership is acquired synchronously before the first await so two
@@ -2665,10 +1838,11 @@ export class ConsolePiRuntime {
2665
1838
  // existing closed interview allow/deny registry. The next ordinary turn
2666
1839
  // explicitly restores the normal Operator Chat set (baseline + extension
2667
1840
  // tools, ADR 0012).
2668
- const requestedTools = OPERATOR_CHAT_ACTIVE_TOOL_NAMES();
2669
- const activeTools = options?.mode === "requirement-interview"
1841
+ const sessionMode = this.sessionModeOf(sessionId);
1842
+ const requestedTools = chatActiveToolBaseline(sessionMode);
1843
+ const activeTools = options?.mode === "requirement-interview" && sessionMode === "workflow"
2670
1844
  ? filterActiveInterviewTools(requestedTools).allowed
2671
- : computeOperatorChatActiveToolNames(session);
1845
+ : computeChatActiveToolNames(session, sessionMode);
2672
1846
  session.setActiveToolsByName(activeTools);
2673
1847
  // Final pre-prompt fence: abort after tool pin must still skip session.prompt.
2674
1848
  if (options?.signal?.aborted) {
@@ -3006,6 +2180,7 @@ export class ConsolePiRuntime {
3006
2180
  this.modelRuntimes.delete(sessionId);
3007
2181
  this.modelConfigFiles.delete(sessionId);
3008
2182
  this.sessionModels.delete(sessionId);
2183
+ this.sessionModes.delete(sessionId);
3009
2184
  this.serviceScopes.delete(sessionId);
3010
2185
  this.revisions.delete(sessionId);
3011
2186
  this.thinkingSelections.delete(sessionId);
@@ -3173,155 +2348,9 @@ async function waitForIdle(session, options) {
3173
2348
  await new Promise((r) => setTimeout(r, 50));
3174
2349
  }
3175
2350
  }
3176
- async function safeGetAgentDir(bindings) {
3177
- try {
3178
- return await bindings.getAgentDir();
3179
- }
3180
- catch {
3181
- // Fall back to a Console-app-data scoped agent dir so we never accidentally
3182
- // touch the user's real ~/.pi/agent config if getAgentDir throws.
3183
- return path.join(process.cwd(), ".harness", "console", "pi-agent-dir");
3184
- }
3185
- }
3186
- /**
3187
- * Cached SDK module loader. The pi-coding-agent package is ESM-only, so we
3188
- * must use dynamic import() (not require). Cached to avoid repeated imports.
3189
- */
3190
- let cachedSdk;
3191
- async function loadSdk() {
3192
- if (!cachedSdk) {
3193
- cachedSdk = import("@earendil-works/pi-coding-agent");
3194
- }
3195
- return cachedSdk;
3196
- }
3197
- /**
3198
- * Default production bindings backed by the real Pi SDK (services mode).
3199
- * Dynamically imported so the worker bundle does not hard-fail if the SDK is
3200
- * absent at module load (discovered lazily by pi-readiness instead).
3201
- */
3202
- export function createDefaultPiSdkBindings() {
3203
- return {
3204
- getAgentDir: async () => {
3205
- const sdk = (await loadSdk());
3206
- return sdk.getAgentDir();
3207
- },
3208
- createServices: async (opts) => {
3209
- const sdk = (await loadSdk());
3210
- // createAgentSessionServices returns the full AgentSessionServices object
3211
- // directly (cwd/agentDir/modelRuntime/settingsManager/resourceLoader).
3212
- // Wrap it so our type contract is satisfied.
3213
- const result = await sdk.createAgentSessionServices({
3214
- cwd: opts.cwd,
3215
- agentDir: opts.agentDir,
3216
- resourceLoaderOptions: opts.resourceLoaderOptions,
3217
- });
3218
- return { services: result };
3219
- },
3220
- createSessionFromServices: async (opts) => {
3221
- const sdk = (await loadSdk());
3222
- return sdk.createAgentSessionFromServices({
3223
- services: opts.services,
3224
- sessionManager: opts.sessionManager,
3225
- ...(opts.tools ? { tools: opts.tools } : {}),
3226
- ...(opts.noTools ? { noTools: opts.noTools } : {}),
3227
- ...(opts.excludeTools ? { excludeTools: opts.excludeTools } : {}),
3228
- ...(opts.customTools ? { customTools: opts.customTools } : {}),
3229
- ...(opts.model ? { model: opts.model } : {}),
3230
- });
3231
- },
3232
- createSessionManager: async (opts) => {
3233
- const sdk = (await loadSdk());
3234
- return sdk.SessionManager.create(opts.cwd, opts.sessionDir, {
3235
- id: opts.sessionId,
3236
- });
3237
- },
3238
- materializeSessionShell: async (opts) => {
3239
- const sdk = (await loadSdk());
3240
- const manager = opts.sessionManager;
3241
- if (typeof manager.setSessionFile !== "function") {
3242
- throw new Error("Pi SessionManager cannot materialize a session file");
3243
- }
3244
- let created = false;
3245
- try {
3246
- await mkdir(path.dirname(opts.sessionFile), { recursive: true });
3247
- // Match Pi's documented session header. Rebinding through the public
3248
- // SessionManager API marks it flushed, so Pi appends the first user
3249
- // entry instead of trying to create the already-present file later.
3250
- await writeFile(opts.sessionFile, `${JSON.stringify({
3251
- type: "session",
3252
- version: typeof sdk.CURRENT_SESSION_VERSION === "number"
3253
- ? sdk.CURRENT_SESSION_VERSION
3254
- : 3,
3255
- id: opts.sessionId,
3256
- timestamp: new Date().toISOString(),
3257
- cwd: opts.cwd,
3258
- })}\n`, { encoding: "utf8", flag: "wx", mode: 0o600 });
3259
- created = true;
3260
- manager.setSessionFile(opts.sessionFile);
3261
- }
3262
- catch (error) {
3263
- if (created)
3264
- await rm(opts.sessionFile, { force: true }).catch(() => undefined);
3265
- throw error;
3266
- }
3267
- },
3268
- createInMemorySessionManager: async (opts) => {
3269
- const sdk = (await loadSdk());
3270
- return sdk.SessionManager.inMemory(opts.cwd);
3271
- },
3272
- forkSessionManager: async (opts) => {
3273
- const sdk = (await loadSdk());
3274
- return sdk.SessionManager.forkFrom(opts.sourcePath, opts.targetCwd, opts.targetSessionDir, { id: opts.targetSessionId });
3275
- },
3276
- openSessionManager: async (opts) => {
3277
- const sdk = (await loadSdk());
3278
- try {
3279
- // Pi SessionManager.open(missingPath) does NOT throw — it creates a
3280
- // fresh in-memory session with a random UUID. That would reopen the
3281
- // wrong conversation under the durable Console sessionId. Fail closed
3282
- // before open when the persisted file is absent.
3283
- await access(opts.sessionFile);
3284
- }
3285
- catch (error) {
3286
- const code = error && typeof error === "object" && "code" in error
3287
- ? String(error.code ?? "")
3288
- : "";
3289
- if (code === "ENOENT") {
3290
- throw Object.assign(new Error(`SESSION_FILE_MISSING: ${opts.sessionFile}`), { code: "SESSION_FILE_MISSING", cause: error });
3291
- }
3292
- // File exists but is unreadable (permissions / I/O) — caller maps
3293
- // undefined to 503 rather than silently creating a fresh session.
3294
- return undefined;
3295
- }
3296
- return sdk.SessionManager.open(opts.sessionFile);
3297
- },
3298
- resolveModel: (opts) => {
3299
- const rt = opts.modelRuntime;
3300
- if (typeof rt?.getModel !== "function")
3301
- return undefined;
3302
- try {
3303
- return rt.getModel(opts.provider, opts.modelId);
3304
- }
3305
- catch {
3306
- return undefined;
3307
- }
3308
- },
3309
- listAvailableModels: (opts) => {
3310
- const rt = opts.modelRuntime;
3311
- const available = rt?.snapshot?.available ?? [];
3312
- const configured = rt?.snapshot?.configuredProviders;
3313
- const isConfigured = (provider) => configured instanceof Set ? configured.has(provider) : false;
3314
- return available.map((m) => ({
3315
- id: m.id,
3316
- name: m.name,
3317
- provider: m.provider,
3318
- hasCredentials: isConfigured(m.provider),
3319
- }));
3320
- },
3321
- };
3322
- }
3323
2351
  /**
3324
- * Re-export the tool gate for the dispatcher layer so the per-tool-call gate
3325
- * (Gate 3) lives in exactly one place.
2352
+ * Re-export the tool gates for the dispatcher layer so the per-tool-call gate
2353
+ * (Gate 3) lives in exactly one place. authorizeChatSessionTool is the
2354
+ * mode-aware gate; the legacy name stays for workflow-only callers.
3326
2355
  */
3327
- export { authorizeOperatorChatTool, assertNoWriteToolInList, OPERATOR_CHAT_ALLOWED_TOOLS, };
2356
+ export { authorizeChatSessionTool, authorizeOperatorChatTool, assertNoWriteToolInList, OPERATOR_CHAT_ALLOWED_TOOLS, };