@tea-agent/loop-agent 0.43.0-next.17 → 0.43.0-next.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -140
- package/dist/application/task-lifecycle/advance.js +4 -17
- package/dist/application/task-lifecycle/observe.js +29 -43
- package/dist/application/task-lifecycle/plan-transitions.js +4 -5
- package/dist/build-stamp.json +3 -3
- package/dist/commands/client-recovery.js +55 -20
- package/dist/commands/new-task.js +5 -0
- package/dist/commands/status.js +57 -0
- package/dist/commands/task-contract.js +269 -0
- package/dist/executors/dag-pi-executor.js +230 -269
- package/dist/executors/pi-executor.js +0 -2
- package/dist/executors/pi-sdk-executor.js +39 -77
- package/dist/executors/shell-executor.js +2 -3
- package/dist/executors/shell-write-guard.js +32 -27
- package/dist/shared/pi-context-pressure/extension.js +16 -26
- package/dist/shared/pi-retry-settings.js +0 -40
- package/dist/shared/prompts.js +26 -0
- package/dist/task/config.js +2 -0
- package/dist/task/contract/validate-draft.js +106 -0
- package/dist/task/goal.js +3 -0
- package/dist/task/index.js +12 -0
- package/dist/task/lifecycle.js +1 -0
- package/dist/task/operator/capabilities.js +6 -0
- package/dist/task/operator/envelope.js +2 -0
- package/dist/task/operator/index.js +5 -0
- package/dist/task/operator/registry.js +2 -0
- package/dist/task/operator/types.js +1 -0
- package/dist/task/paths.js +1 -0
- package/dist/task/source-prepare/source-fidelity-pi.js +2 -2
- package/dist/task/source-references.js +23 -48
- package/dist/task/source-state.js +1 -0
- package/dist/task/subagent-guidance.js +1 -0
- package/dist/worker/console/chat/assistant-content.js +2 -23
- package/dist/worker/console/chat/explore-tools.js +0 -13
- package/dist/worker/console/chat/pi-runtime.js +1182 -221
- package/dist/worker/console/chat/routes.js +71 -213
- package/dist/worker/console/chat/sdd-data-alignment.js +9 -65
- package/dist/worker/console/chat/session-catalog.js +0 -32
- package/dist/worker/console/chat/session-store.js +6 -22
- package/dist/worker/console/chat/shortcuts.js +0 -6
- package/dist/worker/console/chat/sift-bridge.js +1 -0
- package/dist/worker/console/chat/tool-preview.js +0 -11
- package/dist/worker/console/chat/tools.js +0 -6
- package/dist/worker/console/console-update-and-init.js +19 -1
- package/dist/worker/console/console-update-runtime.js +20 -5
- package/dist/worker/console/index.js +20 -0
- package/dist/worker/console/operator-actions.js +1 -13
- package/dist/worker/console/routes.js +0 -8
- package/dist/worker/console/server.js +29 -11
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C4NXyedy.js → abnfDiagram-N423BO3Z-hUmM_GRf.js} +1 -1
- package/dist/worker/console/static/assets/{arc-NZ3qXvBI.js → arc-Bt-xuieW.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-COZSRvml.js → architectureDiagram-T3A2C74G-DTqkL4DJ.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-Ck0Gxi9C.js → blockDiagram-VBNYF7ZC-Cy0MNnzV.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BX5HQ4Ox.js → c4Diagram-5PPSVZJV-DDdlT0Pd.js} +1 -1
- package/dist/worker/console/static/assets/channel-BpzRnZmU.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-CoviemSL.js → chunk-2GRJ4B5K-3JawwNZ6.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-DbOdOkbZ.js → chunk-2Q5K7J3B-DoTp7Cri.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BsG2pmdQ.js → chunk-5RXB4S5H-CcirhZaS.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-DGdvrNm8.js → chunk-5VM5RSS4-VrG21gnP.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BAy7Yd6y.js → chunk-6Q2QTUOP-DDOgIS70.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-D9UA7hUb.js → chunk-GF5L2VYU-CBjmgvJ_.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-DPz7SZ8V.js → chunk-JWPE2WC7-CCQSht8M.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BmuEOIh7.js → chunk-KBJHAD2P-BpGMkUUV.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-CIhBWnd8.js → chunk-RYQCIY6F-DNDT41QL.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-CScR6mDD.js → chunk-XXDRQBXY-euXnDFbn.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BhlMVqCc.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BhlMVqCc.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BjpfWaUr.js → cose-bilkent-JH36ORCC-BJ6NAeql.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BFQohJgT.js → cynefin-VYW2F7L2-F5JKIvIq.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-D3gzRJnS.js → cynefinDiagram-MW4NZA55-Cwmkl_Gi.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-5eojkhoA.js → dagre-VZM6K2ZE-BmCZ8815.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-CpD8LWu_.js → diagram-7IWD3JNH-zWA3hUnX.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-B8_4qJ2a.js → diagram-B4RE2ZJO-BzA-888F.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-HOsXf0eu.js → diagram-LBJQPF4R-tPNheoch.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BqwRwiWP.js → diagram-Q27KOJAE-Cdi7Fyqp.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-BRVCNZe1.js → diagram-UB23O5K3-DFK9VBf1.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Cvb1TgGh.js → ebnfDiagram-BXEA7PRR-xCUr1kpg.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DS0O0qG9.js → erDiagram-JOGREHBK-BANQHY_A.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-BHIRvRr3.js → flowDiagram-UKHOOZJN-VHJYEMkP.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DcCJ1JWx.js → ganttDiagram-PKOTCBZU-DiyZQ0C6.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-B9ia5ony.js → gitGraphDiagram-DS77QQ5N-CZtIeBBa.js} +1 -1
- package/dist/worker/console/static/assets/index-BlN6ytLr.css +1 -0
- package/dist/worker/console/static/assets/index-CA-jfWlE.js +449 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-3ogpgegn.js → infoDiagram-6WML65LV-BuSspC0X.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-4tgpQ5SL.js → ishikawaDiagram-WSZJBQD7-DRJZyPFy.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DNfgHqFx.js → journeyDiagram-NVQOT4AX-DIfXIVuE.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-Dz7ngC8z.js → kanban-definition-27J2QSJJ-CT10_7Lt.js} +1 -1
- package/dist/worker/console/static/assets/{linear-DI7Jv62E.js → linear-ufemj74H.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-CQA4Nzmo.js → mermaid.core-DEQe8-_m.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-CtsMc8Mf.js → mindmap-definition-FAOFIHXS-BRLUrbrC.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-ByppE9yH.js → pegDiagram-VL7TDLO6-BgibKtiF.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-B5PtgFgt.js → pieDiagram-7S7Q4E2Y-D3eyfdV9.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-Cq4p0k2K.js → quadrantDiagram-CIZ2JOQS-DXeTk5Px.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-DjfkyljN.js → railroadDiagram-AXF67PYL-BgXdFEe7.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-DzynNRSl.js → requirementDiagram-LRYGKXZP-CseC2A7R.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-Cj7TCpTJ.js → sankeyDiagram-W5VNT64P-D80PFzfJ.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-C73C6ykA.js → sequenceDiagram-SI44F4Z6-BWzHW7LS.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-BiNLgN2x.js → sizeCapture-X5ZJPWSS-xWQYTLAn.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-UwvjS64-.js → stateDiagram-OKZ733FA-CLxtpZeL.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-CiVoq4G-.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-CC7DH98r.js → swimlanes-SLNWSIFB-DwugtmWV.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BkdgdpfR.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-LSBXTR70.js → timeline-definition-Z64GVDOM-DHEOwHIe.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BAuxWwGP.js → vennDiagram-T6HMQDX7-pmbZKlBK.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y--E5_gjZx.js → wardleyDiagram-T6FBY63Y-CivHoM5q.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-dJWNmyzF.js → xychartDiagram-ELKLHX3M-BfHIE7o6.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +10 -122
- package/dist/worker/console/static-src/operator-chat/compaction-message.js +0 -7
- package/dist/worker/console/static-src/operator-chat/pending-user-message.js +15 -35
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +62 -136
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +68 -159
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +56 -75
- package/dist/worker/console/static-src/operator-chat/useComposer.js +78 -206
- package/dist/worker/console/workspace-context.js +1 -21
- package/dist/worker/observe/static/router.js +1 -1
- package/dist/worker/observe/static/styles.css +4 -7
- package/dist/worker/observe/static/views/dag-inspector.js +19 -12
- package/dist/worker/observe/static/views/dag.js +2 -2
- package/dist/worker/scheduler/index.js +25 -0
- package/dist/workflows/dag/facts.js +4 -0
- package/dist/workflows/dag/frontend-closeout.js +1 -1
- package/dist/workflows/dag/frontend-design-policy.js +1 -2
- package/dist/workflows/dag/frontend-implementation-contract.js +4 -8
- package/dist/workflows/dag/frontend-prewrite-gate.js +3 -3
- package/dist/workflows/dag/frontend-repair.js +76 -55
- package/dist/workflows/dag/frontend-review-context.js +1 -2
- package/dist/workflows/dag/frontend-review-scopes.js +12 -34
- package/dist/workflows/dag/frontend-shape-facts.js +2 -12
- package/dist/workflows/dag/frontend-verification-trace.js +42 -8
- package/dist/workflows/dag/index.js +6 -0
- package/dist/workflows/dag/init-hybrid.js +160 -97
- package/dist/workflows/dynamic/index.js +7 -0
- package/docs/architecture/runtime-boundaries.md +3 -3
- package/docs/templates/frontend-implementation-dag.json +8 -9
- package/package.json +3 -7
- package/skills/frontend-bounded-implement/SKILL.md +3 -4
- package/skills/frontend-bounded-implement/references/code-standards.md +3 -3
- package/skills/frontend-contract/references/contract-protocol.md +1 -3
- package/skills/frontend-design-review/SKILL.md +7 -12
- package/skills/frontend-design-review/references/review-checklist.md +7 -7
- package/skills/frontend-plan/SKILL.md +0 -19
- package/skills/frontend-plan/references/decision-contract.md +19 -26
- package/skills/frontend-review/SKILL.md +18 -12
- package/skills/frontend-review/references/review-findings.md +43 -24
- package/dist/shared/pi-provider-recovery.js +0 -182
- package/dist/worker/console/chat/browser-automation.js +0 -363
- package/dist/worker/console/chat/browser-policy.js +0 -113
- package/dist/worker/console/chat/browser-routes.js +0 -162
- package/dist/worker/console/chat/pi-runtime/custom-tools/ask-user-question.js +0 -44
- package/dist/worker/console/chat/pi-runtime/custom-tools/browser-tools.js +0 -89
- package/dist/worker/console/chat/pi-runtime/custom-tools/explore-tools.js +0 -202
- package/dist/worker/console/chat/pi-runtime/custom-tools/goal-tools.js +0 -79
- package/dist/worker/console/chat/pi-runtime/custom-tools/operator-tools.js +0 -86
- package/dist/worker/console/chat/pi-runtime/custom-tools/todo-write.js +0 -38
- package/dist/worker/console/chat/pi-runtime/inspection-types.js +0 -28
- package/dist/worker/console/chat/pi-runtime/inspection.js +0 -439
- package/dist/worker/console/chat/pi-runtime/sdk-bindings.js +0 -153
- package/dist/worker/console/chat/pi-runtime/sdk-session.js +0 -5
- package/dist/worker/console/chat/scm-routes.js +0 -217
- package/dist/worker/console/chat/scm-service.js +0 -283
- package/dist/worker/console/chat/scm-tools.js +0 -111
- package/dist/worker/console/chat/session-mode-view.js +0 -49
- package/dist/worker/console/chat/session-mode.js +0 -220
- package/dist/worker/console/chat/terminal-routes.js +0 -216
- package/dist/worker/console/chat/terminal-sessions.js +0 -348
- package/dist/worker/console/chat/terminal-tools.js +0 -211
- package/dist/worker/console/static/assets/channel-BxdTlb_5.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-_ByA1Nf8.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-_ByA1Nf8.js +0 -1
- package/dist/worker/console/static/assets/index-B9goN3S5.css +0 -1
- package/dist/worker/console/static/assets/index-wXlQohNs.js +0 -469
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-nLgMZ0D3.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BcYqTady.js +0 -8
- package/dist/worker/console/static-src/operator-chat/active-turn-tail.js +0 -10
- package/dist/worker/console/static-src/operator-chat/chat-link.js +0 -94
- package/dist/worker/console/static-src/operator-chat/chat-scroll-position.js +0 -24
- package/dist/worker/console/static-src/operator-chat/composer-session-ledger.js +0 -192
- package/dist/worker/console/static-src/operator-chat/process-label.js +0 -41
- package/dist/worker/console/static-src/operator-chat/turn-group-equality.js +0 -13
- package/dist/worker/console/static-src/operator-chat/turn-process-disclosure.js +0 -10
- package/dist/worker/console/static-src/operator-chat/use-searchable-hidden.js +0 -51
- package/dist/worker/console/static-src/operator-chat/user-turn-anchor-equality.js +0 -21
- package/dist/worker/console/workspace-initialization.js +0 -209
- package/dist/workflows/dag/frontend-capacity.js +0 -9
- package/dist/workflows/dag/frontend-plan-completeness.js +0 -186
- package/dist/workflows/dag/frontend-plan-recovery-policy.js +0 -18
|
@@ -26,38 +26,25 @@
|
|
|
26
26
|
* `createDefaultPiSdkBindings()`.
|
|
27
27
|
*/
|
|
28
28
|
import { createHash, randomBytes } from "node:crypto";
|
|
29
|
-
import { stat } from "node:fs/promises";
|
|
29
|
+
import { access, mkdir, rm, stat, writeFile } 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, authorizeOperatorChatTool, assertNoWriteToolInList, } from "./tools.js";
|
|
36
|
-
import { authorizeChatSessionTool, DEFAULT_CONSOLE_CHAT_SESSION_MODE, chatActiveToolBaseline, composeChatSystemPrompt, composeChatSystemPromptParts, computeChatActiveToolNames, shouldLoadWorkflowBusinessTools, } from "./session-mode.js";
|
|
35
|
+
import { OPERATOR_CHAT_ALLOWED_TOOLS, OPERATOR_CHAT_BUILTIN_CUSTOM_TOOLS, authorizeOperatorChatTool, assertNoWriteToolInList, } from "./tools.js";
|
|
37
36
|
import { buildAgentCustomTool, ExploreOrchestrator, } from "./subagents/index.js";
|
|
38
37
|
import { createOperatorChatResourceLoader, } from "./resource-loader.js";
|
|
39
|
-
import { applySkillAutoInvocationPreferences, loadResourcePreferences, } from "./resource-preferences-store.js";
|
|
40
|
-
import {
|
|
38
|
+
import { applySkillAutoInvocationPreferences, isAutoInvocationEnabled, loadResourcePreferences, skillResourceId, } from "./resource-preferences-store.js";
|
|
39
|
+
import { buildModelCallableToolSchemas } from "./tool-adapter.js";
|
|
41
40
|
import { applyProcessCompactionOverride, createCompactionController, createContextPressureExtension, createSiftBridgeState, housekeepSiftStash, isConsoleSiftEnabled, parseCompactionPolicy, parseSiftBridgeConfig, projectPromptPressure, deriveEffectiveWindow, projectSafeTelemetry, emptyContextPressureCounters, } from "../../../shared/pi-context-pressure/index.js";
|
|
42
41
|
import { incrementCounter as incrementPressureCounter } from "../../../shared/pi-context-pressure/telemetry.js";
|
|
43
42
|
import { resolveDefaultChatModel, resolveLowChatModel, } from "./model-resolver.js";
|
|
44
43
|
import { filterActiveInterviewTools } from "../interview/tools.js";
|
|
45
|
-
import { redactRuntimeText } from "./runtime-context.js";
|
|
46
|
-
import { BrowserAutomationService } from "./browser-automation.js";
|
|
44
|
+
import { RUNTIME_CONTEXT_TEXT_MAX, redactRuntimeText, } from "./runtime-context.js";
|
|
47
45
|
import { buildTodoContinuationContext, } from "./todos.js";
|
|
48
|
-
import { buildPersistentTerminalTools } from "./terminal-tools.js";
|
|
49
|
-
import { buildScmTools } from "./scm-tools.js";
|
|
50
46
|
import { buildGoalContinuationContext, } from "./goals.js";
|
|
51
47
|
import { injectWizardLocalHeaders, } from "./sdd-data-alignment.js";
|
|
52
|
-
import { buildCustomOperatorTools } from "./pi-runtime/custom-tools/operator-tools.js";
|
|
53
|
-
import { buildBrowserTools } from "./pi-runtime/custom-tools/browser-tools.js";
|
|
54
|
-
import { buildAskUserQuestionTool } from "./pi-runtime/custom-tools/ask-user-question.js";
|
|
55
|
-
import { buildTodoWriteTool } from "./pi-runtime/custom-tools/todo-write.js";
|
|
56
|
-
import { buildGoalTools } from "./pi-runtime/custom-tools/goal-tools.js";
|
|
57
|
-
import { buildExploreCustomTools } from "./pi-runtime/custom-tools/explore-tools.js";
|
|
58
|
-
import { THINKING_LEVELS, RuntimeSnapshotUnavailableError, } from "./pi-runtime/inspection-types.js";
|
|
59
|
-
import { collectRuntimeInventories, collectSlashCommands, projectRuntimeSnapshot, } from "./pi-runtime/inspection.js";
|
|
60
|
-
import { createDefaultPiSdkBindings, safeGetAgentDir, } from "./pi-runtime/sdk-bindings.js";
|
|
61
48
|
/**
|
|
62
49
|
* Base system prompt fragment every General Operator Chat session receives.
|
|
63
50
|
*
|
|
@@ -80,7 +67,7 @@ import { createDefaultPiSdkBindings, safeGetAgentDir, } from "./pi-runtime/sdk-b
|
|
|
80
67
|
*/
|
|
81
68
|
export const OPERATOR_CHAT_SYSTEM_PROMPT_BASE = [
|
|
82
69
|
"You are General Operator Chat.",
|
|
83
|
-
"Use operator tools first (dagReport, dagRerunPlan, dagRerun). Pi read/write/edit/bash/grep/find/ls are available; obey repository AGENTS.md. apply_patch, full-tools, shell and coding-chat are denied. Prefer safe-read/safe-grep for probes.
|
|
70
|
+
"Use operator tools first (dagReport, dagRerunPlan, dagRerun). Pi read/write/edit/bash/grep/find/ls are available; obey repository AGENTS.md. apply_patch, full-tools, shell and coding-chat are denied. Prefer safe-read/safe-grep for probes.",
|
|
84
71
|
"No harness and asked to init: `loop-agent init instructions --repo-root .`, then `loop-agent init --repo-root . --profile full --merge`; verify with doctor, inspect and docs audit. Updates: `loop-agent init upgrade --repo-root . --json` until stable; `loop-agent init check-update` is read-only.",
|
|
85
72
|
"Long DAGs: prepareDagExecution → runDag → operationId. Never foreground-Bash task advance, tail/head pipes or nohup/Start-Process/start; do not end on accepted/queued/running or an operationId — NOT completion. Supervise operationGet/status/dagReport/dagDoctor/operationWait at 60s → 90s → 120s → 180s; reset to 60s on state change; 30-60s re-checks when stall suspected. operationWait MUST use afterSeq=previous nextSeq (recommendedNextCall). Heartbeats: not meaningful progress. Run id ≠ task id.",
|
|
86
73
|
"Failure: terminal runSummary first. With progress wait; else dagRerunPlan(fromNode=primaryFailure.nodeId); eligible → dagRerun: reruns the failed subgraph, keeps prior successes. Ineligible: Worker → workerTaskRetry; standalone → standaloneTaskRerun after contract adopt (regenerates the whole task, not node continue). Do not call dagResume; do not re-advance a run-failed task. Pre-run identical state twice: stop and report; never loop identical bash/read/grep. Approve only bounded writeSet inside allowedPaths, outside forbiddenPaths, with structured verification.",
|
|
@@ -88,6 +75,15 @@ export const OPERATOR_CHAT_SYSTEM_PROMPT_BASE = [
|
|
|
88
75
|
"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.",
|
|
89
76
|
"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.",
|
|
90
77
|
].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
|
+
}
|
|
91
87
|
/**
|
|
92
88
|
* Built-in SDK tool names registered for every Chat session (ADR 0011).
|
|
93
89
|
* Full Pi builtins: read/write/edit/bash/grep/find/ls. safe-* custom tools
|
|
@@ -116,8 +112,27 @@ export const OPERATOR_CHAT_SAFE_EXPLORE_TOOL_IDS = Object.freeze([
|
|
|
116
112
|
"git-diff",
|
|
117
113
|
]);
|
|
118
114
|
export function OPERATOR_CHAT_ACTIVE_TOOL_NAMES() {
|
|
119
|
-
return
|
|
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
|
+
];
|
|
120
121
|
}
|
|
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()));
|
|
121
136
|
/**
|
|
122
137
|
* Gate 2 active-tool set for a live session (ADR 0012): the operator-chat
|
|
123
138
|
* baseline (operator actions + Pi builtins + safe explore) PLUS extension
|
|
@@ -128,8 +143,24 @@ export function OPERATOR_CHAT_ACTIVE_TOOL_NAMES() {
|
|
|
128
143
|
* coding-chat). Unknown/denied names are never activated; Gate 3 still
|
|
129
144
|
* re-authorizes every tool call per invocation.
|
|
130
145
|
*/
|
|
131
|
-
export function computeOperatorChatActiveToolNames(session
|
|
132
|
-
|
|
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;
|
|
133
164
|
}
|
|
134
165
|
const OPERATOR_CHAT_SESSION_EXCLUDE_TOOLS = Object.freeze([
|
|
135
166
|
"apply_patch",
|
|
@@ -185,8 +216,8 @@ async function bindSessionExtensions(session) {
|
|
|
185
216
|
return;
|
|
186
217
|
await session.bindExtensions(createConsoleExtensionBindings());
|
|
187
218
|
}
|
|
188
|
-
function pinOperatorChatTools(session
|
|
189
|
-
session.setActiveToolsByName(
|
|
219
|
+
function pinOperatorChatTools(session) {
|
|
220
|
+
session.setActiveToolsByName(computeOperatorChatActiveToolNames(session));
|
|
190
221
|
}
|
|
191
222
|
/**
|
|
192
223
|
* Pi has no hook result that merely appends instructions to its built-in
|
|
@@ -288,8 +319,592 @@ export function createWizardLocalHeadersExtension(options) {
|
|
|
288
319
|
},
|
|
289
320
|
};
|
|
290
321
|
}
|
|
291
|
-
|
|
292
|
-
|
|
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
|
+
}
|
|
293
908
|
export { extractAssistantContentBlocks, extractAssistantText, isAssistantSdkMessage };
|
|
294
909
|
function readSessionManagerId(manager) {
|
|
295
910
|
if (!manager || typeof manager !== "object")
|
|
@@ -406,23 +1021,19 @@ export class ConsolePiRuntime {
|
|
|
406
1021
|
sessionInit = new Map();
|
|
407
1022
|
/** Resolved model binding after materialization (create may return before this is known). */
|
|
408
1023
|
sessionModels = new Map();
|
|
409
|
-
/** Durable session mode; record is the source of truth, this is the live handle cache. */
|
|
410
|
-
sessionModes = new Map();
|
|
411
1024
|
/** SessionIds disposed while materialization was still in flight. */
|
|
412
1025
|
disposedSessions = new Set();
|
|
413
1026
|
compactionControllers = new Map();
|
|
414
1027
|
siftStates = new Map();
|
|
415
1028
|
/** Session-less tool definition caches (no AgentSession state). */
|
|
416
|
-
|
|
1029
|
+
operatorCustomToolsPromise;
|
|
417
1030
|
exploreCustomToolsPromise;
|
|
418
1031
|
exploreOrchestrator;
|
|
419
1032
|
bindings;
|
|
420
|
-
browserAutomation;
|
|
421
1033
|
constructor(options) {
|
|
422
1034
|
this.options = options;
|
|
423
1035
|
this.loader = createOperatorChatResourceLoader();
|
|
424
1036
|
this.bindings = options.bindings ?? createDefaultPiSdkBindings();
|
|
425
|
-
this.browserAutomation = options.browserAutomation ?? new BrowserAutomationService();
|
|
426
1037
|
this.exploreOrchestrator = new ExploreOrchestrator({
|
|
427
1038
|
cwd: this.options.cwd,
|
|
428
1039
|
bindings: this.bindings,
|
|
@@ -443,9 +1054,6 @@ export class ConsolePiRuntime {
|
|
|
443
1054
|
},
|
|
444
1055
|
});
|
|
445
1056
|
}
|
|
446
|
-
async syncAimaxAuthToken() {
|
|
447
|
-
await this.options.sddDataAlignment?.auth?.getToken();
|
|
448
|
-
}
|
|
449
1057
|
/** Absolute path for repo-scoped resource preferences (may be undefined in unit tests). */
|
|
450
1058
|
get resourcePreferencesPath() {
|
|
451
1059
|
return this.options.resourcePreferencesPath;
|
|
@@ -474,15 +1082,6 @@ export class ConsolePiRuntime {
|
|
|
474
1082
|
* after a PATCH sees the latest resource-preferences.json (not create-time
|
|
475
1083
|
* closure state).
|
|
476
1084
|
*/
|
|
477
|
-
sessionModeOf(sessionId) {
|
|
478
|
-
if (!sessionId)
|
|
479
|
-
return DEFAULT_CONSOLE_CHAT_SESSION_MODE;
|
|
480
|
-
return this.sessionModes.get(sessionId) ?? DEFAULT_CONSOLE_CHAT_SESSION_MODE;
|
|
481
|
-
}
|
|
482
|
-
rememberSessionMode(sessionId, sessionMode = DEFAULT_CONSOLE_CHAT_SESSION_MODE) {
|
|
483
|
-
this.sessionModes.set(sessionId, sessionMode);
|
|
484
|
-
return sessionMode;
|
|
485
|
-
}
|
|
486
1085
|
async buildResourceLoaderOptions(base = {}, sessionId) {
|
|
487
1086
|
const inheritedFactories = Array.isArray(base.extensionFactories)
|
|
488
1087
|
? base.extensionFactories
|
|
@@ -582,11 +1181,9 @@ export class ConsolePiRuntime {
|
|
|
582
1181
|
policy,
|
|
583
1182
|
controller: this.getCompactionController(sessionId),
|
|
584
1183
|
sift,
|
|
585
|
-
normalizeTransientProviderErrors: true,
|
|
586
1184
|
});
|
|
587
1185
|
}
|
|
588
|
-
|
|
589
|
-
applyProcessRetryOverride(services?.settingsManager);
|
|
1186
|
+
applySessionCompactionOverride(sessionId, services, session) {
|
|
590
1187
|
const manager = services?.settingsManager;
|
|
591
1188
|
const window = session?.getSessionStats?.()?.contextUsage?.contextWindow;
|
|
592
1189
|
applyProcessCompactionOverride(manager, {
|
|
@@ -604,7 +1201,7 @@ export class ConsolePiRuntime {
|
|
|
604
1201
|
const window = stats?.contextUsage?.contextWindow;
|
|
605
1202
|
const tokens = stats?.contextUsage?.tokens;
|
|
606
1203
|
if (typeof window === "number" && window > 0) {
|
|
607
|
-
this.
|
|
1204
|
+
this.applySessionCompactionOverride(sessionId, this.serviceScopes.get(sessionId) ?? {}, session);
|
|
608
1205
|
}
|
|
609
1206
|
const projection = projectPromptPressure({
|
|
610
1207
|
currentContextTokens: tokens,
|
|
@@ -744,10 +1341,8 @@ export class ConsolePiRuntime {
|
|
|
744
1341
|
* `initializing: true`; dependent routes wait on the same promise.
|
|
745
1342
|
*/
|
|
746
1343
|
async createSession(init) {
|
|
747
|
-
await this.syncAimaxAuthToken();
|
|
748
1344
|
const sessionId = `operator-chat-${randomBytes(12).toString("hex")}`;
|
|
749
1345
|
this.disposedSessions.delete(sessionId);
|
|
750
|
-
const sessionMode = this.rememberSessionMode(sessionId, init?.sessionMode ?? DEFAULT_CONSOLE_CHAT_SESSION_MODE);
|
|
751
1346
|
const sessionManager = await this.bindings.createSessionManager({
|
|
752
1347
|
cwd: this.options.cwd,
|
|
753
1348
|
sessionDir: this.options.sessionDir,
|
|
@@ -773,7 +1368,6 @@ export class ConsolePiRuntime {
|
|
|
773
1368
|
sessionManager,
|
|
774
1369
|
model: init?.model,
|
|
775
1370
|
systemPromptSuffix: init?.systemPromptSuffix,
|
|
776
|
-
sessionMode,
|
|
777
1371
|
// Create path must resolve default model from the SAME services object
|
|
778
1372
|
// used for formal materialization (no throwaway createServices).
|
|
779
1373
|
resolveDefaultModel: !init?.model,
|
|
@@ -783,8 +1377,7 @@ export class ConsolePiRuntime {
|
|
|
783
1377
|
sessionFile,
|
|
784
1378
|
createdAt: new Date().toISOString(),
|
|
785
1379
|
model: init?.model,
|
|
786
|
-
activeTools:
|
|
787
|
-
sessionMode,
|
|
1380
|
+
activeTools: OPERATOR_CHAT_ACTIVE_TOOL_NAMES(),
|
|
788
1381
|
initializing: true,
|
|
789
1382
|
};
|
|
790
1383
|
}
|
|
@@ -842,33 +1435,28 @@ export class ConsolePiRuntime {
|
|
|
842
1435
|
this.sessionInit.set(sessionId, { status: "initializing", promise });
|
|
843
1436
|
return promise;
|
|
844
1437
|
}
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
* create, extension bind, tool pin, compaction override. Lifecycle maps stay
|
|
849
|
-
* owned by the caller.
|
|
850
|
-
*/
|
|
851
|
-
async prepareLiveSession(init) {
|
|
852
|
-
const aborted = () => Boolean(init.abortIfDisposed && this.disposedSessions.has(init.sessionId));
|
|
853
|
-
if (aborted())
|
|
854
|
-
return undefined;
|
|
1438
|
+
async materializeSession(sessionId, init) {
|
|
1439
|
+
if (this.disposedSessions.has(sessionId))
|
|
1440
|
+
return;
|
|
855
1441
|
const agentDir = await safeGetAgentDir(this.bindings);
|
|
856
1442
|
// Capture before services parse models.json. A concurrent edit can therefore
|
|
857
1443
|
// cause at most one harmless extra refresh, never a missed next-turn refresh.
|
|
858
1444
|
const modelConfigFile = await captureModelConfigFile(agentDir);
|
|
859
|
-
const appendSystemPrompt =
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
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.
|
|
864
1452
|
await this.refreshSkillPrefsCache().catch(() => undefined);
|
|
865
1453
|
const { services } = await this.bindings.createServices({
|
|
866
1454
|
cwd: this.options.cwd,
|
|
867
1455
|
agentDir,
|
|
868
|
-
resourceLoaderOptions: await this.buildResourceLoaderOptions({ appendSystemPrompt },
|
|
1456
|
+
resourceLoaderOptions: await this.buildResourceLoaderOptions({ appendSystemPrompt }, sessionId),
|
|
869
1457
|
});
|
|
870
|
-
if (
|
|
871
|
-
return
|
|
1458
|
+
if (this.disposedSessions.has(sessionId))
|
|
1459
|
+
return;
|
|
872
1460
|
const modelRuntime = services.modelRuntime;
|
|
873
1461
|
let model = init.model;
|
|
874
1462
|
if (!model && init.resolveDefaultModel) {
|
|
@@ -882,9 +1470,9 @@ export class ConsolePiRuntime {
|
|
|
882
1470
|
}) ?? undefined)
|
|
883
1471
|
: undefined;
|
|
884
1472
|
assertNoWriteToolInList(OPERATOR_CHAT_ALLOWED_TOOLS);
|
|
885
|
-
const customTools = await this.loadCustomTools(
|
|
886
|
-
if (
|
|
887
|
-
return
|
|
1473
|
+
const customTools = await this.loadCustomTools(sessionId);
|
|
1474
|
+
if (this.disposedSessions.has(sessionId))
|
|
1475
|
+
return;
|
|
888
1476
|
// pi-web parity: do NOT pass `tools`. A closed allowlist becomes
|
|
889
1477
|
// SDK allowedToolNames and filters extension MCP tools out of the
|
|
890
1478
|
// registry before Gate 2 can re-add them. Operator/safe customTools
|
|
@@ -898,59 +1486,35 @@ export class ConsolePiRuntime {
|
|
|
898
1486
|
...(customTools.length > 0 ? { customTools } : {}),
|
|
899
1487
|
...(resolvedModel ? { model: resolvedModel } : {}),
|
|
900
1488
|
});
|
|
901
|
-
if (
|
|
1489
|
+
if (this.disposedSessions.has(sessionId)) {
|
|
902
1490
|
try {
|
|
903
1491
|
session.dispose();
|
|
904
1492
|
}
|
|
905
1493
|
catch {
|
|
906
1494
|
// ignore late dispose cleanup
|
|
907
1495
|
}
|
|
908
|
-
return
|
|
1496
|
+
return;
|
|
909
1497
|
}
|
|
910
1498
|
await bindSessionExtensions(session);
|
|
911
|
-
pinOperatorChatTools(session
|
|
912
|
-
this.applySessionRecoveryOverrides(init.sessionId, services, session);
|
|
913
|
-
return {
|
|
914
|
-
session,
|
|
915
|
-
modelRuntime,
|
|
916
|
-
modelConfigFile,
|
|
917
|
-
model,
|
|
918
|
-
services: {
|
|
919
|
-
resourceLoader: services.resourceLoader,
|
|
920
|
-
settingsManager: services.settingsManager,
|
|
921
|
-
},
|
|
922
|
-
};
|
|
923
|
-
}
|
|
924
|
-
async materializeSession(sessionId, init) {
|
|
925
|
-
if (this.disposedSessions.has(sessionId))
|
|
926
|
-
return;
|
|
927
|
-
await this.syncAimaxAuthToken();
|
|
928
|
-
const sessionMode = this.rememberSessionMode(sessionId, init.sessionMode ?? this.sessionModeOf(sessionId));
|
|
929
|
-
const prepared = await this.prepareLiveSession({
|
|
930
|
-
sessionId,
|
|
931
|
-
sessionManager: init.sessionManager,
|
|
932
|
-
model: init.model,
|
|
933
|
-
systemPromptSuffix: init.systemPromptSuffix,
|
|
934
|
-
sessionMode,
|
|
935
|
-
resolveDefaultModel: init.resolveDefaultModel,
|
|
936
|
-
abortIfDisposed: true,
|
|
937
|
-
});
|
|
938
|
-
if (!prepared)
|
|
939
|
-
return;
|
|
1499
|
+
pinOperatorChatTools(session);
|
|
940
1500
|
// Register then re-check dispose race: a concurrent dispose must win.
|
|
941
|
-
this.sessions.set(sessionId,
|
|
1501
|
+
this.sessions.set(sessionId, session);
|
|
942
1502
|
this.sessionManagers.set(sessionId, init.sessionManager);
|
|
943
|
-
this.modelRuntimes.set(sessionId,
|
|
944
|
-
this.modelConfigFiles.set(sessionId,
|
|
945
|
-
this.sessionModels.set(sessionId,
|
|
946
|
-
this.serviceScopes.set(sessionId,
|
|
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);
|
|
947
1511
|
this.revisions.set(sessionId, 1);
|
|
948
1512
|
if (!this.thinkingSelections.has(sessionId)) {
|
|
949
1513
|
this.thinkingSelections.set(sessionId, "auto");
|
|
950
1514
|
}
|
|
951
1515
|
if (this.disposedSessions.has(sessionId)) {
|
|
952
1516
|
try {
|
|
953
|
-
|
|
1517
|
+
session.dispose();
|
|
954
1518
|
}
|
|
955
1519
|
catch {
|
|
956
1520
|
// ignore
|
|
@@ -960,7 +1524,6 @@ export class ConsolePiRuntime {
|
|
|
960
1524
|
this.modelRuntimes.delete(sessionId);
|
|
961
1525
|
this.modelConfigFiles.delete(sessionId);
|
|
962
1526
|
this.sessionModels.delete(sessionId);
|
|
963
|
-
this.sessionModes.delete(sessionId);
|
|
964
1527
|
this.serviceScopes.delete(sessionId);
|
|
965
1528
|
this.revisions.delete(sessionId);
|
|
966
1529
|
this.thinkingSelections.delete(sessionId);
|
|
@@ -968,17 +1531,10 @@ export class ConsolePiRuntime {
|
|
|
968
1531
|
}
|
|
969
1532
|
/** Cached session-less custom tool definitions (operator + explore). */
|
|
970
1533
|
async loadCustomTools(sessionId) {
|
|
971
|
-
const sessionMode = this.sessionModeOf(sessionId);
|
|
972
|
-
const loadWorkflowTools = shouldLoadWorkflowBusinessTools(sessionMode);
|
|
973
1534
|
const [operatorCustomTools, exploreCustomTools] = await Promise.all([
|
|
974
|
-
|
|
975
|
-
? this.loadOperatorCustomTools(sessionMode)
|
|
976
|
-
: Promise.resolve([]),
|
|
1535
|
+
this.loadOperatorCustomTools(),
|
|
977
1536
|
this.loadExploreCustomTools(),
|
|
978
1537
|
]);
|
|
979
|
-
const browserTools = await buildBrowserTools({
|
|
980
|
-
automation: this.browserAutomation,
|
|
981
|
-
});
|
|
982
1538
|
const questionTool = this.options.questionRegistry
|
|
983
1539
|
? await buildAskUserQuestionTool({
|
|
984
1540
|
sessionId,
|
|
@@ -986,31 +1542,11 @@ export class ConsolePiRuntime {
|
|
|
986
1542
|
})
|
|
987
1543
|
: undefined;
|
|
988
1544
|
const todoTool = this.options.todoRegistry
|
|
989
|
-
? await buildTodoWriteTool({
|
|
990
|
-
sessionId,
|
|
991
|
-
registry: this.options.todoRegistry,
|
|
992
|
-
sessionMode,
|
|
993
|
-
})
|
|
1545
|
+
? await buildTodoWriteTool({ sessionId, registry: this.options.todoRegistry })
|
|
994
1546
|
: undefined;
|
|
995
1547
|
const goalTools = this.options.goalRegistry
|
|
996
1548
|
? await buildGoalTools({ sessionId, registry: this.options.goalRegistry })
|
|
997
1549
|
: [];
|
|
998
|
-
const terminalTools = this.options.terminalRegistry
|
|
999
|
-
? await buildPersistentTerminalTools({
|
|
1000
|
-
ownerSessionId: sessionId,
|
|
1001
|
-
registry: this.options.terminalRegistry,
|
|
1002
|
-
defaultCwd: this.options.cwd,
|
|
1003
|
-
}).catch((error) => {
|
|
1004
|
-
process.stderr.write(`[console] chat terminal-tools build failed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
1005
|
-
return [];
|
|
1006
|
-
})
|
|
1007
|
-
: [];
|
|
1008
|
-
const scmTools = this.options.scmService
|
|
1009
|
-
? await buildScmTools({ scm: this.options.scmService }).catch((error) => {
|
|
1010
|
-
process.stderr.write(`[console] chat scm-tools build failed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
1011
|
-
return [];
|
|
1012
|
-
})
|
|
1013
|
-
: [];
|
|
1014
1550
|
const agentTool = await buildAgentCustomTool({
|
|
1015
1551
|
parentSessionId: sessionId,
|
|
1016
1552
|
orchestrator: this.exploreOrchestrator,
|
|
@@ -1035,29 +1571,23 @@ export class ConsolePiRuntime {
|
|
|
1035
1571
|
...(questionTool ? [questionTool] : []),
|
|
1036
1572
|
...(todoTool ? [todoTool] : []),
|
|
1037
1573
|
...goalTools,
|
|
1038
|
-
...terminalTools,
|
|
1039
|
-
...scmTools,
|
|
1040
|
-
...browserTools,
|
|
1041
1574
|
...(agentTool ? [agentTool] : []),
|
|
1042
1575
|
];
|
|
1043
1576
|
}
|
|
1044
|
-
loadOperatorCustomTools(
|
|
1577
|
+
loadOperatorCustomTools() {
|
|
1045
1578
|
if (!this.options.actionContext)
|
|
1046
1579
|
return Promise.resolve([]);
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
});
|
|
1059
|
-
this.operatorCustomToolsByMode.set(sessionMode, promise);
|
|
1060
|
-
return promise;
|
|
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;
|
|
1061
1591
|
}
|
|
1062
1592
|
loadExploreCustomTools() {
|
|
1063
1593
|
if (!this.exploreCustomToolsPromise) {
|
|
@@ -1075,7 +1605,6 @@ export class ConsolePiRuntime {
|
|
|
1075
1605
|
if (!this.bindings.forkSessionManager)
|
|
1076
1606
|
throw new Error("SESSION_FORK_UNSUPPORTED");
|
|
1077
1607
|
const sessionId = `operator-chat-${randomBytes(12).toString("hex")}`;
|
|
1078
|
-
const sessionMode = this.rememberSessionMode(sessionId, init.sessionMode ?? DEFAULT_CONSOLE_CHAT_SESSION_MODE);
|
|
1079
1608
|
const sessionManager = await this.bindings.forkSessionManager({
|
|
1080
1609
|
sourcePath: init.sourceSessionFile,
|
|
1081
1610
|
targetCwd: this.options.cwd,
|
|
@@ -1086,8 +1615,8 @@ export class ConsolePiRuntime {
|
|
|
1086
1615
|
sessionId,
|
|
1087
1616
|
sessionManager,
|
|
1088
1617
|
model: init.model,
|
|
1089
|
-
sessionMode,
|
|
1090
1618
|
});
|
|
1619
|
+
built.session.setActiveToolsByName(computeOperatorChatActiveToolNames(built.session));
|
|
1091
1620
|
this.sessions.set(sessionId, built.session);
|
|
1092
1621
|
this.sessionManagers.set(sessionId, sessionManager);
|
|
1093
1622
|
this.modelRuntimes.set(sessionId, built.modelRuntime);
|
|
@@ -1103,7 +1632,6 @@ export class ConsolePiRuntime {
|
|
|
1103
1632
|
createdAt: new Date().toISOString(),
|
|
1104
1633
|
model: init.model,
|
|
1105
1634
|
activeTools: built.session.getActiveToolNames(),
|
|
1106
|
-
sessionMode,
|
|
1107
1635
|
};
|
|
1108
1636
|
}
|
|
1109
1637
|
async branchSession(input) {
|
|
@@ -1282,22 +1810,15 @@ export class ConsolePiRuntime {
|
|
|
1282
1810
|
async getRuntimeSystemPrompt(sessionId) {
|
|
1283
1811
|
await this.ensureSessionReady(sessionId).catch(() => undefined);
|
|
1284
1812
|
const session = this.sessions.get(sessionId);
|
|
1285
|
-
if (!session)
|
|
1286
|
-
return
|
|
1287
|
-
sessionMode: this.sessionModeOf(sessionId),
|
|
1288
|
-
workflowPrompt: OPERATOR_CHAT_SYSTEM_PROMPT_BASE,
|
|
1289
|
-
});
|
|
1290
|
-
}
|
|
1813
|
+
if (!session)
|
|
1814
|
+
return composeOperatorChatSystemPrompt({});
|
|
1291
1815
|
const fromState = session.state?.systemPrompt;
|
|
1292
1816
|
const fromGetter = session.systemPrompt;
|
|
1293
1817
|
const loader = this.serviceScopes.get(sessionId)?.resourceLoader;
|
|
1294
1818
|
return (fromState ||
|
|
1295
1819
|
fromGetter ||
|
|
1296
1820
|
loader?.getSystemPrompt?.() ||
|
|
1297
|
-
|
|
1298
|
-
sessionMode: this.sessionModeOf(sessionId),
|
|
1299
|
-
workflowPrompt: OPERATOR_CHAT_SYSTEM_PROMPT_BASE,
|
|
1300
|
-
}));
|
|
1821
|
+
composeOperatorChatSystemPrompt({}));
|
|
1301
1822
|
}
|
|
1302
1823
|
/**
|
|
1303
1824
|
* Read the RAW system prompt for the snapshot, converting any projection
|
|
@@ -1388,9 +1909,8 @@ export class ConsolePiRuntime {
|
|
|
1388
1909
|
await session.reload();
|
|
1389
1910
|
const services = this.serviceScopes.get(sessionId);
|
|
1390
1911
|
if (services)
|
|
1391
|
-
this.
|
|
1912
|
+
this.applySessionCompactionOverride(sessionId, services, session);
|
|
1392
1913
|
await this.refreshModelConfiguration(sessionId, "force");
|
|
1393
|
-
pinOperatorChatTools(session, this.sessionModeOf(sessionId));
|
|
1394
1914
|
}
|
|
1395
1915
|
catch (error) {
|
|
1396
1916
|
return {
|
|
@@ -1421,14 +1941,91 @@ export class ConsolePiRuntime {
|
|
|
1421
1941
|
* Never returns prompt/skill file bodies.
|
|
1422
1942
|
*/
|
|
1423
1943
|
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
|
+
};
|
|
1424
1951
|
// Fail soft at the HTTP layer: bubble readiness errors so routes can attach
|
|
1425
1952
|
// a compact warning while the UI keeps local Operator/Pi Web commands.
|
|
1426
1953
|
await this.ensureSessionReady(sessionId);
|
|
1427
1954
|
const session = this.sessions.get(sessionId);
|
|
1428
1955
|
if (!session)
|
|
1429
1956
|
return { commands: [] };
|
|
1430
|
-
|
|
1431
|
-
|
|
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
|
+
};
|
|
1432
2029
|
}
|
|
1433
2030
|
async getRuntimeSnapshot(sessionId, options) {
|
|
1434
2031
|
try {
|
|
@@ -1472,6 +2069,9 @@ export class ConsolePiRuntime {
|
|
|
1472
2069
|
throw new RuntimeSnapshotUnavailableError(`model enumeration failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1473
2070
|
}
|
|
1474
2071
|
}
|
|
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).
|
|
1475
2075
|
const supportedLevels = typeof session.setThinkingLevel === "function"
|
|
1476
2076
|
? [...THINKING_LEVELS]
|
|
1477
2077
|
: [];
|
|
@@ -1479,25 +2079,220 @@ export class ConsolePiRuntime {
|
|
|
1479
2079
|
{ value: "auto" },
|
|
1480
2080
|
...supportedLevels.map((value) => ({ value })),
|
|
1481
2081
|
];
|
|
1482
|
-
|
|
1483
|
-
|
|
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
|
+
};
|
|
1484
2100
|
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: {} };
|
|
1485
2119
|
try {
|
|
1486
2120
|
skillPrefs = await this.loadSkillAutoInvocationPreferences();
|
|
1487
2121
|
}
|
|
1488
2122
|
catch (error) {
|
|
1489
|
-
|
|
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);
|
|
2207
|
+
}
|
|
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);
|
|
1490
2257
|
}
|
|
1491
2258
|
}
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
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
|
+
}
|
|
1501
2296
|
return projectRuntimeSnapshot({
|
|
1502
2297
|
sessionId,
|
|
1503
2298
|
revision: this.revisions.get(sessionId) ?? 0,
|
|
@@ -1522,17 +2317,19 @@ export class ConsolePiRuntime {
|
|
|
1522
2317
|
thinkingLevels,
|
|
1523
2318
|
systemPrompt: {
|
|
1524
2319
|
...(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).
|
|
1525
2322
|
text: rawSystemPrompt,
|
|
1526
2323
|
sha256: createHash("sha256").update(rawSystemPrompt).digest("hex"),
|
|
1527
2324
|
characters: rawSystemPrompt.length,
|
|
1528
2325
|
},
|
|
1529
2326
|
activeTools: session.getActiveToolNames(),
|
|
1530
|
-
contextFiles
|
|
1531
|
-
skills
|
|
1532
|
-
extensions
|
|
1533
|
-
packages
|
|
1534
|
-
mcp
|
|
1535
|
-
diagnostics
|
|
2327
|
+
contextFiles,
|
|
2328
|
+
skills,
|
|
2329
|
+
extensions,
|
|
2330
|
+
packages,
|
|
2331
|
+
mcp,
|
|
2332
|
+
diagnostics,
|
|
1536
2333
|
hasBash: true,
|
|
1537
2334
|
});
|
|
1538
2335
|
}
|
|
@@ -1550,9 +2347,7 @@ export class ConsolePiRuntime {
|
|
|
1550
2347
|
* than silently creating a fresh empty session and losing prior context.
|
|
1551
2348
|
*/
|
|
1552
2349
|
async reopenSession(init) {
|
|
1553
|
-
await this.syncAimaxAuthToken();
|
|
1554
2350
|
const sessionId = init.sessionId;
|
|
1555
|
-
const sessionMode = this.rememberSessionMode(sessionId, init.sessionMode ?? this.sessionModeOf(sessionId));
|
|
1556
2351
|
// Wait for in-flight create materialization rather than racing reopen.
|
|
1557
2352
|
const initState = this.sessionInit.get(sessionId);
|
|
1558
2353
|
if (initState?.status === "initializing") {
|
|
@@ -1629,7 +2424,6 @@ export class ConsolePiRuntime {
|
|
|
1629
2424
|
sessionManager,
|
|
1630
2425
|
model: init.model,
|
|
1631
2426
|
systemPromptSuffix: init.systemPromptSuffix,
|
|
1632
|
-
sessionMode,
|
|
1633
2427
|
});
|
|
1634
2428
|
const session = built.session;
|
|
1635
2429
|
// Prefer the SessionManager id (from the JSONL header). Some SDK
|
|
@@ -1652,6 +2446,7 @@ export class ConsolePiRuntime {
|
|
|
1652
2446
|
message: `reopened Pi sessionId mismatch: expected ${sessionId}, got ${reportedId}`,
|
|
1653
2447
|
};
|
|
1654
2448
|
}
|
|
2449
|
+
session.setActiveToolsByName(computeOperatorChatActiveToolNames(session));
|
|
1655
2450
|
this.sessions.set(sessionId, session);
|
|
1656
2451
|
this.sessionManagers.set(sessionId, sessionManager);
|
|
1657
2452
|
this.modelRuntimes.set(sessionId, built.modelRuntime);
|
|
@@ -1669,7 +2464,6 @@ export class ConsolePiRuntime {
|
|
|
1669
2464
|
createdAt: new Date().toISOString(),
|
|
1670
2465
|
model: init.model,
|
|
1671
2466
|
activeTools: session.getActiveToolNames(),
|
|
1672
|
-
sessionMode,
|
|
1673
2467
|
},
|
|
1674
2468
|
};
|
|
1675
2469
|
}
|
|
@@ -1701,7 +2495,6 @@ export class ConsolePiRuntime {
|
|
|
1701
2495
|
createdAt: new Date().toISOString(),
|
|
1702
2496
|
model,
|
|
1703
2497
|
activeTools: session.getActiveToolNames(),
|
|
1704
|
-
sessionMode: this.sessionModeOf(sessionId),
|
|
1705
2498
|
};
|
|
1706
2499
|
}
|
|
1707
2500
|
/**
|
|
@@ -1710,22 +2503,46 @@ export class ConsolePiRuntime {
|
|
|
1710
2503
|
* before AgentSession is ready. Tool definitions reuse runtime-level caches.
|
|
1711
2504
|
*/
|
|
1712
2505
|
async buildSessionWithServices(init) {
|
|
1713
|
-
const
|
|
1714
|
-
const
|
|
1715
|
-
|
|
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,
|
|
1716
2530
|
sessionManager: init.sessionManager,
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
2531
|
+
excludeTools: [...OPERATOR_CHAT_SESSION_EXCLUDE_TOOLS],
|
|
2532
|
+
...(customTools.length > 0 ? { customTools } : {}),
|
|
2533
|
+
...(resolvedModel ? { model: resolvedModel } : {}),
|
|
1720
2534
|
});
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
2535
|
+
await bindSessionExtensions(session);
|
|
2536
|
+
pinOperatorChatTools(session);
|
|
2537
|
+
this.applySessionCompactionOverride(init.sessionId, services, session);
|
|
1724
2538
|
return {
|
|
1725
|
-
session
|
|
1726
|
-
modelRuntime
|
|
1727
|
-
modelConfigFile
|
|
1728
|
-
services:
|
|
2539
|
+
session,
|
|
2540
|
+
modelRuntime,
|
|
2541
|
+
modelConfigFile,
|
|
2542
|
+
services: {
|
|
2543
|
+
resourceLoader: services.resourceLoader,
|
|
2544
|
+
settingsManager: services.settingsManager,
|
|
2545
|
+
},
|
|
1729
2546
|
};
|
|
1730
2547
|
}
|
|
1731
2548
|
/** Re-open a previously created session (cross-request). */
|
|
@@ -1785,7 +2602,7 @@ export class ConsolePiRuntime {
|
|
|
1785
2602
|
}
|
|
1786
2603
|
/**
|
|
1787
2604
|
* Send a prompt and stream events. Gate 3: each tool invocation is
|
|
1788
|
-
* re-authorized by
|
|
2605
|
+
* re-authorized by authorizeOperatorChatTool before the dispatcher runs it
|
|
1789
2606
|
* (the dispatcher is wired by the HTTP layer, see chat-session.ts).
|
|
1790
2607
|
*
|
|
1791
2608
|
* Prompt ownership is acquired synchronously before the first await so two
|
|
@@ -1848,11 +2665,10 @@ export class ConsolePiRuntime {
|
|
|
1848
2665
|
// existing closed interview allow/deny registry. The next ordinary turn
|
|
1849
2666
|
// explicitly restores the normal Operator Chat set (baseline + extension
|
|
1850
2667
|
// tools, ADR 0012).
|
|
1851
|
-
const
|
|
1852
|
-
const
|
|
1853
|
-
const activeTools = options?.mode === "requirement-interview" && sessionMode === "workflow"
|
|
2668
|
+
const requestedTools = OPERATOR_CHAT_ACTIVE_TOOL_NAMES();
|
|
2669
|
+
const activeTools = options?.mode === "requirement-interview"
|
|
1854
2670
|
? filterActiveInterviewTools(requestedTools).allowed
|
|
1855
|
-
:
|
|
2671
|
+
: computeOperatorChatActiveToolNames(session);
|
|
1856
2672
|
session.setActiveToolsByName(activeTools);
|
|
1857
2673
|
// Final pre-prompt fence: abort after tool pin must still skip session.prompt.
|
|
1858
2674
|
if (options?.signal?.aborted) {
|
|
@@ -2190,7 +3006,6 @@ export class ConsolePiRuntime {
|
|
|
2190
3006
|
this.modelRuntimes.delete(sessionId);
|
|
2191
3007
|
this.modelConfigFiles.delete(sessionId);
|
|
2192
3008
|
this.sessionModels.delete(sessionId);
|
|
2193
|
-
this.sessionModes.delete(sessionId);
|
|
2194
3009
|
this.serviceScopes.delete(sessionId);
|
|
2195
3010
|
this.revisions.delete(sessionId);
|
|
2196
3011
|
this.thinkingSelections.delete(sessionId);
|
|
@@ -2358,9 +3173,155 @@ async function waitForIdle(session, options) {
|
|
|
2358
3173
|
await new Promise((r) => setTimeout(r, 50));
|
|
2359
3174
|
}
|
|
2360
3175
|
}
|
|
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
|
+
}
|
|
2361
3323
|
/**
|
|
2362
|
-
* Re-export the tool
|
|
2363
|
-
* (Gate 3) lives in exactly one place.
|
|
2364
|
-
* mode-aware gate; the legacy name stays for workflow-only callers.
|
|
3324
|
+
* Re-export the tool gate for the dispatcher layer so the per-tool-call gate
|
|
3325
|
+
* (Gate 3) lives in exactly one place.
|
|
2365
3326
|
*/
|
|
2366
|
-
export {
|
|
3327
|
+
export { authorizeOperatorChatTool, assertNoWriteToolInList, OPERATOR_CHAT_ALLOWED_TOOLS, };
|