@caupulican/pi-adaptative 0.81.29 → 0.81.30
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 +18 -0
- package/dist/bundled-resources/skills/harness-self-adaptation/SKILL.md +327 -0
- package/dist/bundled-resources/skills/harness-self-adaptation/references/adaptation-layers.md +89 -0
- package/dist/core/agent-session.d.ts +11 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +16 -14
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/autonomy/approval-gate.d.ts +1 -1
- package/dist/core/autonomy/approval-gate.d.ts.map +1 -1
- package/dist/core/autonomy/approval-gate.js +5 -1
- package/dist/core/autonomy/approval-gate.js.map +1 -1
- package/dist/core/autonomy/lane-tool-surface.d.ts +4 -0
- package/dist/core/autonomy/lane-tool-surface.d.ts.map +1 -1
- package/dist/core/autonomy/lane-tool-surface.js +31 -4
- package/dist/core/autonomy/lane-tool-surface.js.map +1 -1
- package/dist/core/autonomy/lane-tracker.d.ts +1 -0
- package/dist/core/autonomy/lane-tracker.d.ts.map +1 -1
- package/dist/core/autonomy/lane-tracker.js +11 -0
- package/dist/core/autonomy/lane-tracker.js.map +1 -1
- package/dist/core/autonomy/risk-assessment.d.ts.map +1 -1
- package/dist/core/autonomy/risk-assessment.js +1 -1
- package/dist/core/autonomy/risk-assessment.js.map +1 -1
- package/dist/core/background-lane-controller.d.ts +12 -0
- package/dist/core/background-lane-controller.d.ts.map +1 -1
- package/dist/core/background-lane-controller.js +114 -19
- package/dist/core/background-lane-controller.js.map +1 -1
- package/dist/core/context/context-artifacts.d.ts.map +1 -1
- package/dist/core/context/context-artifacts.js +7 -6
- package/dist/core/context/context-artifacts.js.map +1 -1
- package/dist/core/context/context-store-retention.d.ts +12 -0
- package/dist/core/context/context-store-retention.d.ts.map +1 -0
- package/dist/core/context/context-store-retention.js +161 -0
- package/dist/core/context/context-store-retention.js.map +1 -0
- package/dist/core/context-gc.d.ts.map +1 -1
- package/dist/core/context-gc.js +19 -4
- package/dist/core/context-gc.js.map +1 -1
- package/dist/core/context-pipeline.d.ts +13 -12
- package/dist/core/context-pipeline.d.ts.map +1 -1
- package/dist/core/context-pipeline.js +128 -33
- package/dist/core/context-pipeline.js.map +1 -1
- package/dist/core/cost/cost-summary.d.ts +7 -1
- package/dist/core/cost/cost-summary.d.ts.map +1 -1
- package/dist/core/cost/cost-summary.js +28 -17
- package/dist/core/cost/cost-summary.js.map +1 -1
- package/dist/core/cost/daily-usage.d.ts +7 -3
- package/dist/core/cost/daily-usage.d.ts.map +1 -1
- package/dist/core/cost/daily-usage.js +10 -5
- package/dist/core/cost/daily-usage.js.map +1 -1
- package/dist/core/delegation/worker-runner.d.ts.map +1 -1
- package/dist/core/delegation/worker-runner.js +25 -9
- package/dist/core/delegation/worker-runner.js.map +1 -1
- package/dist/core/export-html/template.js +2 -1
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +52 -2
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +21 -19
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +5 -0
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/failure-corpus.d.ts.map +1 -1
- package/dist/core/failure-corpus.js +36 -13
- package/dist/core/failure-corpus.js.map +1 -1
- package/dist/core/local-runtime-controller.d.ts.map +1 -1
- package/dist/core/local-runtime-controller.js +20 -2
- package/dist/core/local-runtime-controller.js.map +1 -1
- package/dist/core/memory-controller.d.ts +2 -0
- package/dist/core/memory-controller.d.ts.map +1 -1
- package/dist/core/memory-controller.js +18 -0
- package/dist/core/memory-controller.js.map +1 -1
- package/dist/core/models/local-runtime.d.ts.map +1 -1
- package/dist/core/models/local-runtime.js +15 -10
- package/dist/core/models/local-runtime.js.map +1 -1
- package/dist/core/reflection-controller.d.ts +3 -1
- package/dist/core/reflection-controller.d.ts.map +1 -1
- package/dist/core/reflection-controller.js +8 -5
- package/dist/core/reflection-controller.js.map +1 -1
- package/dist/core/runtime-builder.d.ts +2 -0
- package/dist/core/runtime-builder.d.ts.map +1 -1
- package/dist/core/runtime-builder.js.map +1 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +1 -1
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-analytics.d.ts +13 -7
- package/dist/core/session-analytics.d.ts.map +1 -1
- package/dist/core/session-analytics.js +155 -112
- package/dist/core/session-analytics.js.map +1 -1
- package/dist/core/tool-recovery-log-records.d.ts.map +1 -1
- package/dist/core/tool-recovery-log-records.js +54 -9
- package/dist/core/tool-recovery-log-records.js.map +1 -1
- package/dist/core/tool-recovery-logger.d.ts +2 -0
- package/dist/core/tool-recovery-logger.d.ts.map +1 -1
- package/dist/core/tool-recovery-logger.js +32 -13
- package/dist/core/tool-recovery-logger.js.map +1 -1
- package/dist/core/tool-recovery-stats.d.ts +15 -0
- package/dist/core/tool-recovery-stats.d.ts.map +1 -0
- package/dist/core/tool-recovery-stats.js +206 -0
- package/dist/core/tool-recovery-stats.js.map +1 -0
- package/dist/core/tools/delegate-status.d.ts.map +1 -1
- package/dist/core/tools/delegate-status.js +15 -6
- package/dist/core/tools/delegate-status.js.map +1 -1
- package/dist/core/tools/delegate.d.ts +3 -0
- package/dist/core/tools/delegate.d.ts.map +1 -1
- package/dist/core/tools/delegate.js +7 -2
- package/dist/core/tools/delegate.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +1 -1
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/bash-execution.d.ts +2 -1
- package/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/bash-execution.js +12 -1
- package/dist/modes/interactive/components/bash-execution.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts +1 -0
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +19 -9
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +12 -2
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/components/tool-panel-registry.d.ts +1 -0
- package/dist/modes/interactive/components/tool-panel-registry.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-panel-registry.js +18 -2
- package/dist/modes/interactive/components/tool-panel-registry.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +9 -3
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/rpc/jsonl.d.ts.map +1 -1
- package/dist/modes/rpc/jsonl.js +9 -28
- package/dist/modes/rpc/jsonl.js.map +1 -1
- package/dist/utils/streaming-lines.d.ts +20 -0
- package/dist/utils/streaming-lines.d.ts.map +1 -0
- package/dist/utils/streaming-lines.js +79 -0
- package/dist/utils/streaming-lines.js.map +1 -0
- package/docs/extensions.md +2 -0
- package/docs/index.md +1 -1
- package/docs/self-adaptation.md +7 -0
- package/docs/settings.md +5 -1
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/npm-shrinkwrap.json +12 -12
- package/package.json +4 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"local-runtime-controller.d.ts","sourceRoot":"","sources":["../../src/core/local-runtime-controller.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,KAAK,EAAE,GAAG,EAAE,gBAAgB,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AACtE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC5D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAC7D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAEhE,OAAO,EACN,KAAK,gBAAgB,EACrB,aAAa,EAEb,mBAAmB,EACnB,MAAM,2BAA2B,CAAC;AAiCnC,MAAM,WAAW,0BAA0B;IAC1C,oGAAkG;IAClG,QAAQ,EAAE,MAAM,CAAC;IACjB,mGAAmG;IACnG,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;IACpC;8BAC0B;IAC1B,uBAAuB,IAAI,gBAAgB,GAAG,SAAS,CAAC;IACxD,mGAAiG;IACjG,YAAY,IAAI,kBAAkB,GAAG,SAAS,CAAC;IAC/C,0GAA0G;IAC1G,IAAI,CAAC,KAAK,EAAE,iBAAiB,GAAG,IAAI,CAAC;IACrC;6CACyC;IACzC,0BAA0B,CAAC,IAAI,EAAE,QAAQ,GAAG,WAAW,GAAG,KAAK,CAAC,GAAG,CAAC,GAAG,SAAS,CAAC;IACjF,uEAAuE;IACvE,WAAW,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC;CACvC;AAED,qBAAa,sBAAsB;IAClC,qGAAqG;IACrG,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAoC;IAC9D,uGAAuG;IACvG,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAA0C;IAChF;uEACmE;IACnE,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAqB;IAClD,gFAAgF;IAChF,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAA8C;IACjF,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAA+B;IAEhE,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA6B;IAElD,YAAY,IAAI,EAAE,0BAA0B,EAE3C;IAED;;;;;OAKG;IACH,eAAe,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,aAAa,CAQ/C;IAED,sBAAsB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,mBAAmB,CAc7E;IAED;yEACqE;IACrE,qBAAqB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAElD;IAED,OAAO,CAAC,2BAA2B;IAInC,OAAO,CAAC,sBAAsB;IAI9B;;;;OAIG;IACH,OAAO,CAAC,eAAe;IAIvB,OAAO,CAAC,0BAA0B;YAWpB,oBAAoB;YAuCpB,0BAA0B;IA2BxC,OAAO,CAAC,sBAAsB;IAO9B,OAAO,CAAC,eAAe;IAmBvB;;;OAGG;IACG,wBAAwB,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAW/D;IAED;;;;OAIG;IACG,0BAA0B,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAoBjE;IAED;;;;;OAKG;IACG,qBAAqB,CAC1B,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,GACf,OAAO,CAAC;QAAE,KAAK,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAyCtE;IAEK,4BAA4B,CACjC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,GACf,OAAO,CAAC;QAAE,KAAK,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CA6BtE;YAgBa,2BAA2B;YAsC3B,iCAAiC;IA+C/C;;;;;;;;;;;;;;;;OAgBG;IACG,qBAAqB,CAC1B,QAAQ,EAAE;QAAE,QAAQ,EAAE,aAAa,CAAC;QAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAA;KAAE,GAAG,SAAS,GAClE,OAAO,CAAC;QAAE,QAAQ,EAAE,aAAa,CAAC;QAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAA;KAAE,GAAG,SAAS,CAAC,CAuErE;CACD","sourcesContent":["/**\n * Local-runtime (Ollama) lifecycle controller.\n *\n * Extracted verbatim from agent-session.ts (god-file decomposition). Owns the cached, per-server\n * {@link OllamaRuntime} instances, the \"confirmed up this session\" flag, and the router's readiness\n * gate for a turn routed to a local (`ollama`) model — including the #31 install-on-consent flow and\n * the #27 graceful tier-escalation fallback. Takes narrow deps (agent dir, a last-assistant-message\n * accessor, the session's UI context/event emitter, and the router's own tier resolver) rather than\n * the whole AgentSession.\n */\n\nimport { totalmem } from \"node:os\";\nimport type { Api, AssistantMessage, Model } from \"@caupulican/pi-ai\";\nimport type { AgentSessionEvent } from \"./agent-session.ts\";\nimport type { RouteDecision } from \"./autonomy/contracts.ts\";\nimport type { ExtensionUIContext } from \"./extensions/index.ts\";\nimport { HF_TRANSFORMERS_PROVIDER, OLLAMA_PROVIDER } from \"./models/local-registration.ts\";\nimport {\n\ttype LocalRuntimeDeps,\n\tOllamaRuntime,\n\tresolveTransformersBaseUrl,\n\tTransformersRuntime,\n} from \"./models/local-runtime.ts\";\nimport { matchesInstalledLocalModel } from \"./models/model-ref.ts\";\nimport {\n\tOllamaRuntimeResidencyAdapter,\n\ttype RuntimeEvictionRecord,\n\ttype RuntimeResidencyAdapter,\n\tRuntimeResidencyArbiter,\n\tTransformersRuntimeResidencyAdapter,\n} from \"./models/runtime-arbiter.ts\";\n\n/** User-facing router tiers in ascending order — \"learning\" is never selected for a user turn, so\n * it has no place in the escalation ladder (#27's ensureRouteModelReady walks this forward only). */\nconst MODEL_ROUTER_TIER_ORDER: readonly (\"cheap\" | \"medium\" | \"expensive\")[] = [\"cheap\", \"medium\", \"expensive\"];\n\n/** How long the #31 \"install ollama now?\" confirm waits before auto-dismissing (same as a \"No\") —\n * long enough to read and decide, short enough that an unattended session doesn't hang a turn on it. */\nconst OLLAMA_INSTALL_CONFIRM_TIMEOUT_MS = 30_000;\n\nfunction parseWrongOllamaStoreReason(\n\treason: string,\n): { activePath: string; modelCount: number; ownedPath: string } | undefined {\n\tif (!reason.startsWith(\"wrong_store:\")) return undefined;\n\tconst parts = reason.slice(\"wrong_store:\".length).split(\":\");\n\tif (parts.length < 3) return undefined;\n\tconst [activePath, countText, ...ownedPathParts] = parts;\n\tconst modelCount = Number(countText);\n\treturn {\n\t\tactivePath,\n\t\tmodelCount: Number.isFinite(modelCount) ? modelCount : 0,\n\t\townedPath: ownedPathParts.join(\":\"),\n\t};\n}\n\nexport interface LocalRuntimeControllerDeps {\n\t/** Root directory OllamaRuntime instances are scoped under — fixed for the session's lifetime. */\n\tagentDir: string;\n\t/** Test-injectable seams for OllamaRuntime's own fetch/spawn/exists calls; unset in production. */\n\tlocalRuntimeDeps?: LocalRuntimeDeps;\n\t/** The session's last assistant message, to detect a just-failed local call and drop a stale\n\t * \"confirmed up\" flag. */\n\tgetLastAssistantMessage(): AssistantMessage | undefined;\n\t/** The session's live interactive UI context, if any — undefined in headless/RPC/print modes. */\n\tgetUIContext(): ExtensionUIContext | undefined;\n\t/** Emits a session event (only ever `warning` / `routing_start` / `routing_end` from this controller). */\n\temit(event: AgentSessionEvent): void;\n\t/** Resolves the model configured for a router tier, respecting configured auth — owned by the\n\t * router itself, not this controller. */\n\tresolveConfiguredTierModel(tier: \"medium\" | \"expensive\"): Model<Api> | undefined;\n\t/** `${provider}/${id}` label for a model, for warning/confirm text. */\n\tformatModel(model: Model<Api>): string;\n}\n\nexport class LocalRuntimeController {\n\t/** Lazy, cached by baseUrl so the router path and any other caller share one instance per server. */\n\tprivate readonly _runtimes = new Map<string, OllamaRuntime>();\n\t/** Lazy, cached by model+baseUrl so the router and `/models` share one sidecar handle per HF model. */\n\tprivate readonly _transformersRuntimes = new Map<string, TransformersRuntime>();\n\t/** Server URLs confirmed reachable THIS session — skips the health-check round trip on every\n\t * local-routed turn once warm. Keyed the same way as _runtimes. */\n\tprivate readonly _confirmedUp = new Set<string>();\n\t/** All live runtime adapters participate in one session-wide residency view. */\n\tprivate readonly _residencyAdapters = new Map<string, RuntimeResidencyAdapter>();\n\tprivate readonly _recentEvictions: RuntimeEvictionRecord[] = [];\n\n\tprivate readonly deps: LocalRuntimeControllerDeps;\n\n\tconstructor(deps: LocalRuntimeControllerDeps) {\n\t\tthis.deps = deps;\n\t}\n\n\t/**\n\t * Shared {@link OllamaRuntime} for a given server, lazily created and cached by baseUrl so every\n\t * caller — the router's readiness gate below and any host UI's own model-lifecycle commands\n\t * (e.g. `/models`) — sees and can stop the SAME pi-managed process instead of each tracking its\n\t * own untracked child.\n\t */\n\tgetLocalRuntime(baseUrl?: string): OllamaRuntime {\n\t\tconst key = baseUrl ?? \"default\";\n\t\tlet runtime = this._runtimes.get(key);\n\t\tif (!runtime) {\n\t\t\truntime = new OllamaRuntime({ agentDir: this.deps.agentDir, baseUrl, deps: this.deps.localRuntimeDeps });\n\t\t\tthis._runtimes.set(key, runtime);\n\t\t}\n\t\treturn runtime;\n\t}\n\n\tgetTransformersRuntime(modelId: string, baseUrl?: string): TransformersRuntime {\n\t\tconst resolvedBaseUrl = baseUrl?.replace(/\\/$/, \"\") ?? resolveTransformersBaseUrl(modelId);\n\t\tconst key = `${modelId}\\0${resolvedBaseUrl}`;\n\t\tlet runtime = this._transformersRuntimes.get(key);\n\t\tif (!runtime) {\n\t\t\truntime = new TransformersRuntime({\n\t\t\t\tagentDir: this.deps.agentDir,\n\t\t\t\tmodelId,\n\t\t\t\tbaseUrl: resolvedBaseUrl,\n\t\t\t\tdeps: this.deps.localRuntimeDeps,\n\t\t\t});\n\t\t\tthis._transformersRuntimes.set(key, runtime);\n\t\t}\n\t\treturn runtime;\n\t}\n\n\t/** models.json registers a local model's baseUrl as `<server>/v1` (OpenAI-compat); the runtime's\n\t * own health/boot endpoints are on the Ollama-native server root. */\n\tderiveOllamaServerUrl(modelBaseUrl: string): string {\n\t\treturn modelBaseUrl.replace(/\\/v1\\/?$/, \"\");\n\t}\n\n\tprivate deriveOpenAICompatServerUrl(modelBaseUrl: string): string {\n\t\treturn modelBaseUrl.replace(/\\/v1\\/?$/, \"\");\n\t}\n\n\tprivate isManagedLocalProvider(provider: string): boolean {\n\t\treturn provider === OLLAMA_PROVIDER || provider === HF_TRANSFORMERS_PROVIDER;\n\t}\n\n\t/**\n\t * If the last assistant message in this session was an error from THIS exact local server, a\n\t * cached \"confirmed up\" flag would be stale (the server may have died mid-session) — drop it so\n\t * the next ensure-check is a real one instead of trusting stale state.\n\t */\n\tprivate confirmationKey(model: Model<Api>, serverUrl: string): string {\n\t\treturn model.provider === HF_TRANSFORMERS_PROVIDER ? `${serverUrl}\\0${model.id}` : serverUrl;\n\t}\n\n\tprivate invalidateIfLastCallFailed(model: Model<Api>, serverUrl: string): void {\n\t\tconst lastAssistant = this.deps.getLastAssistantMessage();\n\t\tif (\n\t\t\tlastAssistant?.stopReason === \"error\" &&\n\t\t\tlastAssistant.provider === model.provider &&\n\t\t\tlastAssistant.model === model.id\n\t\t) {\n\t\t\tthis._confirmedUp.delete(this.confirmationKey(model, serverUrl));\n\t\t}\n\t}\n\n\tprivate async ensureOllamaResident(\n\t\tmodel: Model<Api>,\n\t\truntime: OllamaRuntime,\n\t\tknownSizeBytes?: number,\n\t): Promise<{ ok: boolean; reason?: string }> {\n\t\tlet bytes = knownSizeBytes ?? 0;\n\t\tif (knownSizeBytes === undefined) {\n\t\t\ttry {\n\t\t\t\tconst installed = await runtime.list();\n\t\t\t\tbytes = installed.find((entry) => matchesInstalledLocalModel(model.id, entry.name))?.sizeBytes ?? 0;\n\t\t\t} catch {\n\t\t\t\t// Residency is best-effort; a list failure falls back to a zero-byte admission request.\n\t\t\t}\n\t\t}\n\t\tconst serverUrl = this.deriveOllamaServerUrl(model.baseUrl);\n\t\tconst adapterId = `ollama:${serverUrl}`;\n\t\tthis._residencyAdapters.set(adapterId, new OllamaRuntimeResidencyAdapter(adapterId, runtime));\n\t\tconst arbiter = this.createResidencyArbiter();\n\t\ttry {\n\t\t\tconst nowMs = Date.now();\n\t\t\tconst plan = await arbiter.ensureResident(adapterId, {\n\t\t\t\tmodel: model.id,\n\t\t\t\tbytes,\n\t\t\t\trole: \"active\",\n\t\t\t\tpriority: 100,\n\t\t\t\tnowMs,\n\t\t\t\tpinActiveModel: model.id,\n\t\t\t\trecentEvictions: this._recentEvictions,\n\t\t\t\t// Cold Ollama loads can legitimately take several minutes. The actual adaptive stream\n\t\t\t\t// owns that wait; readiness must not front-run it with a 60-second empty generation.\n\t\t\t\tloadModel: false,\n\t\t\t});\n\t\t\tthis.recordEvictions(plan.evict, model.id, nowMs, adapterId);\n\t\t\treturn plan.status === \"fits\" ? { ok: true } : { ok: false, reason: `residency_refused:${plan.reason}` };\n\t\t} catch (error) {\n\t\t\treturn { ok: false, reason: `residency_error:${error instanceof Error ? error.message : String(error)}` };\n\t\t}\n\t}\n\n\tprivate async ensureTransformersResident(\n\t\tmodel: Model<Api>,\n\t\truntime: TransformersRuntime,\n\t): Promise<{ ok: boolean; reason?: string }> {\n\t\tconst serverUrl = this.deriveOpenAICompatServerUrl(model.baseUrl);\n\t\tconst adapterId = `transformers:${model.id}:${serverUrl}`;\n\t\tthis._residencyAdapters.set(adapterId, new TransformersRuntimeResidencyAdapter(adapterId, runtime, model.id, 0));\n\t\tconst arbiter = this.createResidencyArbiter();\n\t\ttry {\n\t\t\tconst nowMs = Date.now();\n\t\t\tconst plan = await arbiter.ensureResident(adapterId, {\n\t\t\t\tmodel: model.id,\n\t\t\t\tbytes: 0,\n\t\t\t\trole: \"active\",\n\t\t\t\tpriority: 100,\n\t\t\t\tnowMs,\n\t\t\t\tpinActiveModel: model.id,\n\t\t\t\trecentEvictions: this._recentEvictions,\n\t\t\t\tloadModel: false,\n\t\t\t});\n\t\t\tthis.recordEvictions(plan.evict, model.id, nowMs, adapterId);\n\t\t\treturn plan.status === \"fits\" ? { ok: true } : { ok: false, reason: `residency_refused:${plan.reason}` };\n\t\t} catch (error) {\n\t\t\treturn { ok: false, reason: `residency_error:${error instanceof Error ? error.message : String(error)}` };\n\t\t}\n\t}\n\n\tprivate createResidencyArbiter(): RuntimeResidencyArbiter {\n\t\treturn new RuntimeResidencyArbiter({\n\t\t\tbudgetBytes: Math.floor(totalmem() * 0.85),\n\t\t\tadapters: [...this._residencyAdapters.values()],\n\t\t});\n\t}\n\n\tprivate recordEvictions(\n\t\tevicted: readonly { adapterId: string; model: string }[],\n\t\tloaded: string,\n\t\tatMs: number,\n\t\tloadedAdapterId: string,\n\t): void {\n\t\tfor (const resident of evicted) {\n\t\t\tthis._recentEvictions.push({\n\t\t\t\tevicted: resident.model,\n\t\t\t\tloaded,\n\t\t\t\tatMs,\n\t\t\t\tevictedAdapterId: resident.adapterId,\n\t\t\t\tloadedAdapterId,\n\t\t\t});\n\t\t}\n\t\tconst cutoff = atMs - 5 * 60_000;\n\t\twhile (this._recentEvictions[0]?.atMs < cutoff) this._recentEvictions.shift();\n\t}\n\n\t/**\n\t * Readiness gate for every isolated/background completion. Unlike the foreground route gate it\n\t * never prompts or changes tiers: lanes either use the configured model or fail visibly.\n\t */\n\tasync ensureIsolatedModelReady(model: Model<Api>): Promise<void> {\n\t\tif (!this.isManagedLocalProvider(model.provider)) return;\n\t\tconst readiness =\n\t\t\tmodel.provider === OLLAMA_PROVIDER\n\t\t\t\t? await this.ensureLocalModelReady(model)\n\t\t\t\t: await this.ensureTransformersModelReady(model);\n\t\tif (readiness.ready) return;\n\t\tconst guide = readiness.installGuide?.length ? `\\n${readiness.installGuide.join(\"\\n\")}` : \"\";\n\t\tthrow new Error(\n\t\t\t`Managed local model ${this.deps.formatModel(model)} is unavailable for isolated execution (${readiness.reason}).${guide}`,\n\t\t);\n\t}\n\n\t/**\n\t * Readiness gate for a manually selected/default foreground model when no router route owns the\n\t * turn. Interactive sessions may offer the same managed-install consent as routed turns; the\n\t * selected model is never silently replaced.\n\t */\n\tasync ensureForegroundModelReady(model: Model<Api>): Promise<void> {\n\t\tif (!this.isManagedLocalProvider(model.provider)) return;\n\t\tlet readiness: {\n\t\t\tready: boolean;\n\t\t\treason: string;\n\t\t\tinstallGuide?: string[];\n\t\t\tinstallAttemptError?: string;\n\t\t} =\n\t\t\tmodel.provider === OLLAMA_PROVIDER\n\t\t\t\t? await this.ensureLocalModelReady(model)\n\t\t\t\t: await this.ensureTransformersModelReady(model);\n\t\tif (!readiness.ready) {\n\t\t\treadiness =\n\t\t\t\tmodel.provider === OLLAMA_PROVIDER\n\t\t\t\t\t? await this.maybeInstallOllamaOnConsent(model, readiness)\n\t\t\t\t\t: await this.maybeInstallTransformersOnConsent(model, readiness);\n\t\t}\n\t\tif (readiness.ready) return;\n\t\tconst detail = readiness.installAttemptError ?? readiness.installGuide?.join(\"\\n\") ?? readiness.reason;\n\t\tthrow new Error(`Managed local model ${this.deps.formatModel(model)} is unavailable: ${detail}`);\n\t}\n\n\t/**\n\t * Ensure a routed managed-local model is actually reachable before the turn calls it. No-op (and\n\t * free) for non-local/API models. Caches a \"confirmed up this session\" flag per server (and per\n\t * Transformers model) so steady-state routing pays the health-check round trip once; invalidated\n\t * above when a prior local call failed so a dead sidecar gets re-detected instead of trusted.\n\t */\n\tasync ensureLocalModelReady(\n\t\tmodel: Model<Api>,\n\t): Promise<{ ready: boolean; reason: string; installGuide?: string[] }> {\n\t\tif (model.provider !== OLLAMA_PROVIDER) {\n\t\t\treturn { ready: true, reason: \"not_local\" };\n\t\t}\n\t\tconst serverUrl = this.deriveOllamaServerUrl(model.baseUrl);\n\t\tconst confirmedKey = this.confirmationKey(model, serverUrl);\n\t\tthis.invalidateIfLastCallFailed(model, serverUrl);\n\t\tif (this._confirmedUp.has(confirmedKey)) {\n\t\t\treturn { ready: true, reason: \"confirmed_up_cached\" };\n\t\t}\n\t\tconst runtime = this.getLocalRuntime(serverUrl);\n\t\tconst status = await runtime.detect();\n\t\tif (status.serverUp) {\n\t\t\t// Server ownership is not a capability boundary. Reuse a configured user/system server\n\t\t\t// when it exposes the requested model; this preserves its accelerator settings and warm\n\t\t\t// cache instead of forcing a slower duplicate Pi-owned process/store.\n\t\t\tconst installedEntry = status.serverModels.find((entry) => matchesInstalledLocalModel(model.id, entry.name));\n\t\t\tif (!installedEntry) {\n\t\t\t\treturn {\n\t\t\t\t\tready: false,\n\t\t\t\t\treason: `model_missing_on_server:${model.id}:${status.activeStore?.path ?? \"external/unknown\"}`,\n\t\t\t\t};\n\t\t\t}\n\t\t\tconst resident = await this.ensureOllamaResident(model, runtime, installedEntry.sizeBytes);\n\t\t\tif (!resident.ok) return { ready: false, reason: resident.reason ?? \"residency_refused\" };\n\t\t\tthis._confirmedUp.add(confirmedKey);\n\t\t\treturn {\n\t\t\t\tready: true,\n\t\t\t\treason: status.managedByPi ? \"already_running_managed\" : \"already_running_configured_server\",\n\t\t\t};\n\t\t}\n\t\tif (!status.binaryPath) {\n\t\t\treturn { ready: false, reason: \"binary_missing\", installGuide: runtime.installGuide() };\n\t\t}\n\t\tconst started = await runtime.start();\n\t\tif (started.started) {\n\t\t\tconst resident = await this.ensureOllamaResident(model, runtime);\n\t\t\tif (!resident.ok) return { ready: false, reason: resident.reason ?? \"residency_refused\" };\n\t\t\tthis._confirmedUp.add(confirmedKey);\n\t\t}\n\t\treturn { ready: started.started, reason: started.reason };\n\t}\n\n\tasync ensureTransformersModelReady(\n\t\tmodel: Model<Api>,\n\t): Promise<{ ready: boolean; reason: string; installGuide?: string[] }> {\n\t\tif (model.provider !== HF_TRANSFORMERS_PROVIDER) {\n\t\t\treturn { ready: true, reason: \"not_transformers\" };\n\t\t}\n\t\tconst serverUrl = this.deriveOpenAICompatServerUrl(model.baseUrl);\n\t\tconst confirmedKey = this.confirmationKey(model, serverUrl);\n\t\tthis.invalidateIfLastCallFailed(model, serverUrl);\n\t\tif (this._confirmedUp.has(confirmedKey)) {\n\t\t\treturn { ready: true, reason: \"confirmed_up_cached\" };\n\t\t}\n\t\tconst runtime = this.getTransformersRuntime(model.id, serverUrl);\n\t\tconst status = await runtime.detect();\n\t\tif (status.serverUp) {\n\t\t\tconst resident = await this.ensureTransformersResident(model, runtime);\n\t\t\tif (!resident.ok) return { ready: false, reason: resident.reason ?? \"residency_refused\" };\n\t\t\tthis._confirmedUp.add(confirmedKey);\n\t\t\treturn { ready: true, reason: \"already_running\" };\n\t\t}\n\t\tif (!status.runtimeInstalled) {\n\t\t\treturn { ready: false, reason: \"runtime_missing\", installGuide: runtime.installGuide() };\n\t\t}\n\t\tconst started = await runtime.start();\n\t\tif (started.started || started.reason === \"already_running\") {\n\t\t\tconst resident = await this.ensureTransformersResident(model, runtime);\n\t\t\tif (!resident.ok) return { ready: false, reason: resident.reason ?? \"residency_refused\" };\n\t\t\tthis._confirmedUp.add(confirmedKey);\n\t\t\treturn { ready: true, reason: started.reason };\n\t\t}\n\t\treturn { ready: false, reason: started.reason };\n\t}\n\n\t/**\n\t * #31: the ONE case a routed local model's unreadiness can be fixed automatically is a missing\n\t * ollama binary — an unreachable server can't be helped by installing, so that reason is left to\n\t * the graceful-fallback warning below unchanged. Only offered when there's an interactive UI to\n\t * ask through: headless/RPC/print sessions have no UI context and fall straight through, same as\n\t * declining or timing out (both resolve confirm() to false). Reverses \"pi never runs installers\n\t * itself\" specifically for this one path — the user is asked first, the download is pi's own\n\t * (never curl|sh), and it lands in pi's own runtimes dir (see OllamaRuntime.installManaged).\n\t *\n\t * Pauses/resumes the routing working-indicator around the confirm dialog itself (re-emitting\n\t * routing_end/routing_start — both already idempotent, see interactive-mode.ts's handlers) so an\n\t * animated spinner doesn't fight a dialog the user is trying to read and answer; the indicator\n\t * comes back for the download/extract that follows a \"yes\", which is genuine processing feedback.\n\t */\n\tprivate async maybeInstallOllamaOnConsent(\n\t\tmodel: Model<Api>,\n\t\treadiness: { ready: boolean; reason: string; installGuide?: string[] },\n\t): Promise<{ ready: boolean; reason: string; installGuide?: string[]; installAttemptError?: string }> {\n\t\tconst ui = this.deps.getUIContext();\n\t\tif (!ui || readiness.ready || readiness.reason !== \"binary_missing\") return readiness;\n\n\t\tconst modelLabel = this.deps.formatModel(model);\n\t\tthis.deps.emit({ type: \"routing_end\" });\n\t\tlet confirmed: boolean;\n\t\ttry {\n\t\t\tconfirmed = await ui.confirm(\n\t\t\t\t\"Install Ollama?\",\n\t\t\t\t`Ollama isn't installed, so the local model \"${modelLabel}\" can't run. Pi can download and ` +\n\t\t\t\t\t\"install it now (a large one-time download, possibly over 1 GB depending on your platform) \" +\n\t\t\t\t\t\"into its own runtimes folder — never curl|sh, never touching anything outside pi's own \" +\n\t\t\t\t\t\"directory. Install it now?\",\n\t\t\t\t{ timeout: OLLAMA_INSTALL_CONFIRM_TIMEOUT_MS },\n\t\t\t);\n\t\t} finally {\n\t\t\tthis.deps.emit({ type: \"routing_start\" });\n\t\t}\n\t\tif (!confirmed) return readiness;\n\n\t\tconst serverUrl = this.deriveOllamaServerUrl(model.baseUrl);\n\t\tconst runtime = this.getLocalRuntime(serverUrl);\n\t\tlet installResult: { ok: boolean; error?: string };\n\t\ttry {\n\t\t\tinstallResult = await runtime.installManaged((status) => ui.setStatus(\"ollama-install\", status));\n\t\t} finally {\n\t\t\tui.setStatus(\"ollama-install\", undefined);\n\t\t}\n\t\tif (!installResult.ok) {\n\t\t\treturn { ready: false, reason: \"install_failed\", installAttemptError: installResult.error };\n\t\t}\n\t\treturn this.ensureLocalModelReady(model);\n\t}\n\n\tprivate async maybeInstallTransformersOnConsent(\n\t\tmodel: Model<Api>,\n\t\treadiness: { ready: boolean; reason: string; installGuide?: string[] },\n\t): Promise<{ ready: boolean; reason: string; installGuide?: string[]; installAttemptError?: string }> {\n\t\tconst ui = this.deps.getUIContext();\n\t\tif (!ui || readiness.ready || readiness.reason !== \"runtime_missing\") return readiness;\n\n\t\tconst modelLabel = this.deps.formatModel(model);\n\t\tthis.deps.emit({ type: \"routing_end\" });\n\t\tlet confirmed: boolean;\n\t\ttry {\n\t\t\tconfirmed = await ui.confirm(\n\t\t\t\t\"Install Transformers runtime?\",\n\t\t\t\t`The Hugging Face model \"${modelLabel}\" needs a pi-managed Python venv with Transformers ` +\n\t\t\t\t\t\"and CPU PyTorch before it can run. Pi will install those packages into its own runtimes \" +\n\t\t\t\t\t\"folder, download the model into a pi-owned Hugging Face cache, and leave system Python, \" +\n\t\t\t\t\t\"your Ollama models, and your user HF cache untouched. Install it now?\",\n\t\t\t\t{ timeout: OLLAMA_INSTALL_CONFIRM_TIMEOUT_MS },\n\t\t\t);\n\t\t} finally {\n\t\t\tthis.deps.emit({ type: \"routing_start\" });\n\t\t}\n\t\tif (!confirmed) return readiness;\n\n\t\tconst serverUrl = this.deriveOpenAICompatServerUrl(model.baseUrl);\n\t\tconst runtime = this.getTransformersRuntime(model.id, serverUrl);\n\t\tlet installResult: { ok: boolean; error?: string };\n\t\ttry {\n\t\t\tinstallResult = await runtime.installManaged((status) => ui.setStatus(\"transformers-install\", status));\n\t\t} finally {\n\t\t\tui.setStatus(\"transformers-install\", undefined);\n\t\t}\n\t\tif (!installResult.ok) {\n\t\t\treturn { ready: false, reason: \"install_failed\", installAttemptError: installResult.error };\n\t\t}\n\t\tlet downloadResult: { ok: boolean; error?: string };\n\t\ttry {\n\t\t\tdownloadResult = await runtime.downloadModel((status) => ui.setStatus(\"transformers-download\", status));\n\t\t} finally {\n\t\t\tui.setStatus(\"transformers-download\", undefined);\n\t\t}\n\t\tif (!downloadResult.ok) {\n\t\t\treturn { ready: false, reason: \"download_failed\", installAttemptError: downloadResult.error };\n\t\t}\n\t\treturn this.ensureTransformersModelReady(model);\n\t}\n\n\t/**\n\t * Router-swap gate (#27): a turn routed to a local model (any tier, including an executor-direct\n\t * route — both carry tier \"cheap\") must not dead-end the turn just because ollama isn't up.\n\t * Never a SILENT swap: every fallback is announced in a warning that states (i) the local model\n\t * was unavailable and WHY — binary missing surfaces the install guide inline; any other reason\n\t * gets a \"check that ollama is running\" hint — and (ii) which tier is now handling the turn, so\n\t * the cost shift is never a surprise. Escalates cheap -> medium -> expensive, skipping any\n\t * unconfigured intermediate tier, reusing the router's own existing \"model unavailable\"\n\t * resolution (resolveConfiguredTierModel) rather than inventing a new fallback mechanism.\n\t * Escalation is bounded: tier strictly increases each hop, so it terminates within two hops.\n\t *\n\t * Before the warning/escalation below: #31's consent gate gets one shot at fixing a missing\n\t * binary interactively (see maybeInstallOllamaOnConsent) — declining, timing out, running\n\t * headless, or the install attempt itself failing all fall through here unchanged, just with an\n\t * honest reason (an install that failed is worded as a failed install, not re-labeled as if\n\t * nothing was ever tried).\n\t */\n\tasync ensureRouteModelReady(\n\t\tresolved: { decision: RouteDecision; model: Model<Api> } | undefined,\n\t): Promise<{ decision: RouteDecision; model: Model<Api> } | undefined> {\n\t\tlet current = resolved;\n\t\twhile (current && this.isManagedLocalProvider(current.model.provider)) {\n\t\t\tlet readiness: { ready: boolean; reason: string; installGuide?: string[]; installAttemptError?: string } =\n\t\t\t\tcurrent.model.provider === OLLAMA_PROVIDER\n\t\t\t\t\t? await this.ensureLocalModelReady(current.model)\n\t\t\t\t\t: await this.ensureTransformersModelReady(current.model);\n\t\t\tif (!readiness.ready) {\n\t\t\t\treadiness =\n\t\t\t\t\tcurrent.model.provider === OLLAMA_PROVIDER\n\t\t\t\t\t\t? await this.maybeInstallOllamaOnConsent(current.model, readiness)\n\t\t\t\t\t\t: await this.maybeInstallTransformersOnConsent(current.model, readiness);\n\t\t\t}\n\t\t\tif (readiness.ready) return current;\n\n\t\t\t// Walk the remaining tiers in order (never back down to cheap) and take the first one that\n\t\t\t// actually resolves — an unconfigured intermediate tier (e.g. no mediumModel set) must be\n\t\t\t// skipped, not treated as \"no fallback available\".\n\t\t\tconst startIndex = MODEL_ROUTER_TIER_ORDER.indexOf(current.decision.tier as \"cheap\" | \"medium\" | \"expensive\");\n\t\t\tlet escalated: { tier: \"medium\" | \"expensive\"; model: Model<Api> } | undefined;\n\t\t\tfor (let i = startIndex + 1; startIndex !== -1 && i < MODEL_ROUTER_TIER_ORDER.length; i++) {\n\t\t\t\tconst tier = MODEL_ROUTER_TIER_ORDER[i] as \"medium\" | \"expensive\";\n\t\t\t\tconst model = this.deps.resolveConfiguredTierModel(tier);\n\t\t\t\tif (model) {\n\t\t\t\t\tescalated = { tier, model };\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst modelLabel = this.deps.formatModel(current.model);\n\t\t\tconst localRuntimeName = current.model.provider === OLLAMA_PROVIDER ? \"ollama\" : \"Transformers\";\n\t\t\tconst wrongStore = parseWrongOllamaStoreReason(readiness.reason);\n\t\t\tconst whyText = readiness.installAttemptError\n\t\t\t\t? `pi tried to install it just now, but the install attempt failed: ${readiness.installAttemptError}`\n\t\t\t\t: readiness.installGuide\n\t\t\t\t\t? [\n\t\t\t\t\t\t\tcurrent.model.provider === OLLAMA_PROVIDER\n\t\t\t\t\t\t\t\t? \"the ollama binary is not installed.\"\n\t\t\t\t\t\t\t\t: \"the pi-managed Transformers runtime is not installed.\",\n\t\t\t\t\t\t\t...readiness.installGuide,\n\t\t\t\t\t\t].join(\"\\n\")\n\t\t\t\t\t: wrongStore\n\t\t\t\t\t\t? `an Ollama server is already running with store ${wrongStore.activePath} (${wrongStore.modelCount} model(s)); pi's owned store is ${wrongStore.ownedPath}. Stop the other server or run /models import, then retry.`\n\t\t\t\t\t\t: current.model.provider === OLLAMA_PROVIDER\n\t\t\t\t\t\t\t? `its ${localRuntimeName} server is not reachable (${readiness.reason}) — check that ollama is running.`\n\t\t\t\t\t\t\t: `its ${localRuntimeName} server is not reachable (${readiness.reason}) — check that the runtime is running.`;\n\t\t\tconst fallbackText = escalated\n\t\t\t\t? `Falling back to the ${escalated.tier} tier for this turn.`\n\t\t\t\t: \"No other tier is configured — falling back to the session's default model.\";\n\t\t\tthis.deps.emit({\n\t\t\t\ttype: \"warning\",\n\t\t\t\tmessage: `Local model \"${modelLabel}\" is unavailable: ${whyText}\\n${fallbackText}`,\n\t\t\t});\n\n\t\t\tif (!escalated) return undefined; // no higher tier resolves — caller falls back to the session default\n\t\t\tcurrent = {\n\t\t\t\tmodel: escalated.model,\n\t\t\t\tdecision: {\n\t\t\t\t\t...current.decision,\n\t\t\t\t\ttier: escalated.tier,\n\t\t\t\t\tfallbackFrom: current.decision.tier,\n\t\t\t\t\treasonCode: \"local_model_not_ready_fallback\",\n\t\t\t\t\treasons: [\n\t\t\t\t\t\t...current.decision.reasons,\n\t\t\t\t\t\t`Local model not ready (${readiness.reason}); escalated to ${escalated.tier}`,\n\t\t\t\t\t],\n\t\t\t\t\tmodel: this.deps.formatModel(escalated.model),\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\treturn current;\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"local-runtime-controller.d.ts","sourceRoot":"","sources":["../../src/core/local-runtime-controller.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Local-runtime (Ollama) lifecycle controller.\n *\n * Extracted verbatim from agent-session.ts (god-file decomposition). Owns the cached, per-server\n * {@link OllamaRuntime} instances, the \"confirmed up this session\" flag, and the router's readiness\n * gate for a turn routed to a local (`ollama`) model — including the #31 install-on-consent flow and\n * the #27 graceful tier-escalation fallback. Takes narrow deps (agent dir, a last-assistant-message\n * accessor, the session's UI context/event emitter, and the router's own tier resolver) rather than\n * the whole AgentSession.\n */\n\nimport { totalmem } from \"node:os\";\nimport type { Api, AssistantMessage, Model } from \"@caupulican/pi-ai\";\nimport type { AgentSessionEvent } from \"./agent-session.ts\";\nimport type { RouteDecision } from \"./autonomy/contracts.ts\";\nimport type { ExtensionUIContext } from \"./extensions/index.ts\";\nimport { HF_TRANSFORMERS_PROVIDER, OLLAMA_PROVIDER } from \"./models/local-registration.ts\";\nimport {\n\ttype LocalRuntimeDeps,\n\tOllamaRuntime,\n\tresolveTransformersBaseUrl,\n\tTransformersRuntime,\n} from \"./models/local-runtime.ts\";\nimport { matchesInstalledLocalModel } from \"./models/model-ref.ts\";\nimport {\n\tOllamaRuntimeResidencyAdapter,\n\ttype RuntimeEvictionRecord,\n\ttype RuntimeResidencyAdapter,\n\tRuntimeResidencyArbiter,\n\tTransformersRuntimeResidencyAdapter,\n} from \"./models/runtime-arbiter.ts\";\n\n/** User-facing router tiers in ascending order — \"learning\" is never selected for a user turn, so\n * it has no place in the escalation ladder (#27's ensureRouteModelReady walks this forward only). */\nconst MODEL_ROUTER_TIER_ORDER: readonly (\"cheap\" | \"medium\" | \"expensive\")[] = [\"cheap\", \"medium\", \"expensive\"];\n\n/** How long the #31 \"install ollama now?\" confirm waits before auto-dismissing (same as a \"No\") —\n * long enough to read and decide, short enough that an unattended session doesn't hang a turn on it. */\nconst OLLAMA_INSTALL_CONFIRM_TIMEOUT_MS = 30_000;\n\nfunction parseWrongOllamaStoreReason(\n\treason: string,\n): { activePath: string; modelCount: number; ownedPath: string } | undefined {\n\tif (!reason.startsWith(\"wrong_store:\")) return undefined;\n\tconst parts = reason.slice(\"wrong_store:\".length).split(\":\");\n\tif (parts.length < 3) return undefined;\n\tconst [activePath, countText, ...ownedPathParts] = parts;\n\tconst modelCount = Number(countText);\n\treturn {\n\t\tactivePath,\n\t\tmodelCount: Number.isFinite(modelCount) ? modelCount : 0,\n\t\townedPath: ownedPathParts.join(\":\"),\n\t};\n}\n\nexport interface LocalRuntimeControllerDeps {\n\t/** Root directory OllamaRuntime instances are scoped under — fixed for the session's lifetime. */\n\tagentDir: string;\n\t/** Test-injectable seams for OllamaRuntime's own fetch/spawn/exists calls; unset in production. */\n\tlocalRuntimeDeps?: LocalRuntimeDeps;\n\t/** The session's last assistant message, to detect a just-failed local call and drop a stale\n\t * \"confirmed up\" flag. */\n\tgetLastAssistantMessage(): AssistantMessage | undefined;\n\t/** The session's live interactive UI context, if any — undefined in headless/RPC/print modes. */\n\tgetUIContext(): ExtensionUIContext | undefined;\n\t/** Emits a session event (only ever `warning` / `routing_start` / `routing_end` from this controller). */\n\temit(event: AgentSessionEvent): void;\n\t/** Resolves the model configured for a router tier, respecting configured auth — owned by the\n\t * router itself, not this controller. */\n\tresolveConfiguredTierModel(tier: \"medium\" | \"expensive\"): Model<Api> | undefined;\n\t/** `${provider}/${id}` label for a model, for warning/confirm text. */\n\tformatModel(model: Model<Api>): string;\n}\n\nexport class LocalRuntimeController {\n\t/** Lazy, cached by baseUrl so the router path and any other caller share one instance per server. */\n\tprivate readonly _runtimes = new Map<string, OllamaRuntime>();\n\t/** Lazy, cached by model+baseUrl so the router and `/models` share one sidecar handle per HF model. */\n\tprivate readonly _transformersRuntimes = new Map<string, TransformersRuntime>();\n\t/** Server URLs confirmed reachable THIS session — skips the health-check round trip on every\n\t * local-routed turn once warm. Keyed the same way as _runtimes. */\n\tprivate readonly _confirmedUp = new Set<string>();\n\t/** All live runtime adapters participate in one session-wide residency view. */\n\tprivate readonly _residencyAdapters = new Map<string, RuntimeResidencyAdapter>();\n\tprivate readonly _recentEvictions: RuntimeEvictionRecord[] = [];\n\n\tprivate readonly deps: LocalRuntimeControllerDeps;\n\n\tconstructor(deps: LocalRuntimeControllerDeps) {\n\t\tthis.deps = deps;\n\t}\n\n\t/**\n\t * Shared {@link OllamaRuntime} for a given server, lazily created and cached by baseUrl so every\n\t * caller — the router's readiness gate below and any host UI's own model-lifecycle commands\n\t * (e.g. `/models`) — sees and can stop the SAME pi-managed process instead of each tracking its\n\t * own untracked child.\n\t */\n\tgetLocalRuntime(baseUrl?: string): OllamaRuntime {\n\t\tconst key = baseUrl ?? \"default\";\n\t\tlet runtime = this._runtimes.get(key);\n\t\tif (!runtime) {\n\t\t\truntime = new OllamaRuntime({ agentDir: this.deps.agentDir, baseUrl, deps: this.deps.localRuntimeDeps });\n\t\t\tthis._runtimes.set(key, runtime);\n\t\t}\n\t\treturn runtime;\n\t}\n\n\tgetTransformersRuntime(modelId: string, baseUrl?: string): TransformersRuntime {\n\t\tconst resolvedBaseUrl = baseUrl?.replace(/\\/$/, \"\") ?? resolveTransformersBaseUrl(modelId);\n\t\tconst key = `${modelId}\\0${resolvedBaseUrl}`;\n\t\tlet runtime = this._transformersRuntimes.get(key);\n\t\tif (!runtime) {\n\t\t\truntime = new TransformersRuntime({\n\t\t\t\tagentDir: this.deps.agentDir,\n\t\t\t\tmodelId,\n\t\t\t\tbaseUrl: resolvedBaseUrl,\n\t\t\t\tdeps: this.deps.localRuntimeDeps,\n\t\t\t});\n\t\t\tthis._transformersRuntimes.set(key, runtime);\n\t\t}\n\t\treturn runtime;\n\t}\n\n\t/** models.json registers a local model's baseUrl as `<server>/v1` (OpenAI-compat); the runtime's\n\t * own health/boot endpoints are on the Ollama-native server root. */\n\tderiveOllamaServerUrl(modelBaseUrl: string): string {\n\t\treturn modelBaseUrl.replace(/\\/v1\\/?$/, \"\");\n\t}\n\n\tprivate deriveOpenAICompatServerUrl(modelBaseUrl: string): string {\n\t\treturn modelBaseUrl.replace(/\\/v1\\/?$/, \"\");\n\t}\n\n\tprivate isManagedLocalProvider(provider: string): boolean {\n\t\treturn provider === OLLAMA_PROVIDER || provider === HF_TRANSFORMERS_PROVIDER;\n\t}\n\n\t/**\n\t * If the last assistant message in this session was an error from THIS exact local server, a\n\t * cached \"confirmed up\" flag would be stale (the server may have died mid-session) — drop it so\n\t * the next ensure-check is a real one instead of trusting stale state.\n\t */\n\tprivate confirmationKey(model: Model<Api>, serverUrl: string): string {\n\t\treturn model.provider === HF_TRANSFORMERS_PROVIDER ? `${serverUrl}\\0${model.id}` : serverUrl;\n\t}\n\n\tprivate invalidateIfLastCallFailed(model: Model<Api>, serverUrl: string): void {\n\t\tconst lastAssistant = this.deps.getLastAssistantMessage();\n\t\tif (\n\t\t\tlastAssistant?.stopReason === \"error\" &&\n\t\t\tlastAssistant.provider === model.provider &&\n\t\t\tlastAssistant.model === model.id\n\t\t) {\n\t\t\tthis._confirmedUp.delete(this.confirmationKey(model, serverUrl));\n\t\t}\n\t}\n\n\tprivate async ensureOllamaResident(\n\t\tmodel: Model<Api>,\n\t\truntime: OllamaRuntime,\n\t\tknownSizeBytes?: number,\n\t): Promise<{ ok: boolean; reason?: string }> {\n\t\tlet bytes = knownSizeBytes ?? 0;\n\t\tif (knownSizeBytes === undefined) {\n\t\t\ttry {\n\t\t\t\tconst installed = await runtime.list();\n\t\t\t\tbytes = installed.find((entry) => matchesInstalledLocalModel(model.id, entry.name))?.sizeBytes ?? 0;\n\t\t\t} catch {\n\t\t\t\t// Residency is best-effort; a list failure falls back to a zero-byte admission request.\n\t\t\t}\n\t\t}\n\t\tconst serverUrl = this.deriveOllamaServerUrl(model.baseUrl);\n\t\tconst adapterId = `ollama:${serverUrl}`;\n\t\tthis._residencyAdapters.set(adapterId, new OllamaRuntimeResidencyAdapter(adapterId, runtime));\n\t\tconst arbiter = this.createResidencyArbiter();\n\t\ttry {\n\t\t\tconst nowMs = Date.now();\n\t\t\tconst plan = await arbiter.ensureResident(adapterId, {\n\t\t\t\tmodel: model.id,\n\t\t\t\tbytes,\n\t\t\t\trole: \"active\",\n\t\t\t\tpriority: 100,\n\t\t\t\tnowMs,\n\t\t\t\tpinActiveModel: model.id,\n\t\t\t\trecentEvictions: this._recentEvictions,\n\t\t\t\t// Cold Ollama loads can legitimately take several minutes. The actual adaptive stream\n\t\t\t\t// owns that wait; readiness must not front-run it with a 60-second empty generation.\n\t\t\t\tloadModel: false,\n\t\t\t});\n\t\t\tthis.recordEvictions(plan.evict, model.id, nowMs, adapterId);\n\t\t\treturn plan.status === \"fits\" ? { ok: true } : { ok: false, reason: `residency_refused:${plan.reason}` };\n\t\t} catch (error) {\n\t\t\treturn { ok: false, reason: `residency_error:${error instanceof Error ? error.message : String(error)}` };\n\t\t}\n\t}\n\n\tprivate async ensureTransformersResident(\n\t\tmodel: Model<Api>,\n\t\truntime: TransformersRuntime,\n\t): Promise<{ ok: boolean; reason?: string }> {\n\t\tconst serverUrl = this.deriveOpenAICompatServerUrl(model.baseUrl);\n\t\tconst adapterId = `transformers:${model.id}:${serverUrl}`;\n\t\tthis._residencyAdapters.set(adapterId, new TransformersRuntimeResidencyAdapter(adapterId, runtime, model.id, 0));\n\t\tconst arbiter = this.createResidencyArbiter();\n\t\ttry {\n\t\t\tconst nowMs = Date.now();\n\t\t\tconst plan = await arbiter.ensureResident(adapterId, {\n\t\t\t\tmodel: model.id,\n\t\t\t\tbytes: 0,\n\t\t\t\trole: \"active\",\n\t\t\t\tpriority: 100,\n\t\t\t\tnowMs,\n\t\t\t\tpinActiveModel: model.id,\n\t\t\t\trecentEvictions: this._recentEvictions,\n\t\t\t\tloadModel: false,\n\t\t\t});\n\t\t\tthis.recordEvictions(plan.evict, model.id, nowMs, adapterId);\n\t\t\treturn plan.status === \"fits\" ? { ok: true } : { ok: false, reason: `residency_refused:${plan.reason}` };\n\t\t} catch (error) {\n\t\t\treturn { ok: false, reason: `residency_error:${error instanceof Error ? error.message : String(error)}` };\n\t\t}\n\t}\n\n\tprivate createResidencyArbiter(): RuntimeResidencyArbiter {\n\t\treturn new RuntimeResidencyArbiter({\n\t\t\tbudgetBytes: Math.floor(totalmem() * 0.85),\n\t\t\tadapters: [...this._residencyAdapters.values()],\n\t\t});\n\t}\n\n\tprivate recordEvictions(\n\t\tevicted: readonly { adapterId: string; model: string }[],\n\t\tloaded: string,\n\t\tatMs: number,\n\t\tloadedAdapterId: string,\n\t): void {\n\t\tfor (const resident of evicted) {\n\t\t\tthis._recentEvictions.push({\n\t\t\t\tevicted: resident.model,\n\t\t\t\tloaded,\n\t\t\t\tatMs,\n\t\t\t\tevictedAdapterId: resident.adapterId,\n\t\t\t\tloadedAdapterId,\n\t\t\t});\n\t\t}\n\t\tconst cutoff = atMs - 5 * 60_000;\n\t\twhile (this._recentEvictions[0]?.atMs < cutoff) this._recentEvictions.shift();\n\t}\n\n\t/**\n\t * Readiness gate for every isolated/background completion. Unlike the foreground route gate it\n\t * never prompts or changes tiers: lanes either use the configured model or fail visibly.\n\t */\n\tasync ensureIsolatedModelReady(model: Model<Api>): Promise<void> {\n\t\tif (!this.isManagedLocalProvider(model.provider)) return;\n\t\tconst readiness =\n\t\t\tmodel.provider === OLLAMA_PROVIDER\n\t\t\t\t? await this.ensureLocalModelReady(model)\n\t\t\t\t: await this.ensureTransformersModelReady(model);\n\t\tif (readiness.ready) return;\n\t\tconst guide = readiness.installGuide?.length ? `\\n${readiness.installGuide.join(\"\\n\")}` : \"\";\n\t\tthrow new Error(\n\t\t\t`Managed local model ${this.deps.formatModel(model)} is unavailable for isolated execution (${readiness.reason}).${guide}`,\n\t\t);\n\t}\n\n\t/**\n\t * Readiness gate for a manually selected/default foreground model when no router route owns the\n\t * turn. Interactive sessions may offer the same managed-install consent as routed turns; the\n\t * selected model is never silently replaced.\n\t */\n\tasync ensureForegroundModelReady(model: Model<Api>): Promise<void> {\n\t\tif (!this.isManagedLocalProvider(model.provider)) return;\n\t\tlet readiness: {\n\t\t\tready: boolean;\n\t\t\treason: string;\n\t\t\tinstallGuide?: string[];\n\t\t\tinstallAttemptError?: string;\n\t\t} =\n\t\t\tmodel.provider === OLLAMA_PROVIDER\n\t\t\t\t? await this.ensureLocalModelReady(model)\n\t\t\t\t: await this.ensureTransformersModelReady(model);\n\t\tif (!readiness.ready) {\n\t\t\treadiness =\n\t\t\t\tmodel.provider === OLLAMA_PROVIDER\n\t\t\t\t\t? await this.maybeInstallOllamaOnConsent(model, readiness)\n\t\t\t\t\t: await this.maybeInstallTransformersOnConsent(model, readiness);\n\t\t}\n\t\tif (readiness.ready) return;\n\t\tconst detail = readiness.installAttemptError ?? readiness.installGuide?.join(\"\\n\") ?? readiness.reason;\n\t\tthrow new Error(`Managed local model ${this.deps.formatModel(model)} is unavailable: ${detail}`);\n\t}\n\n\t/**\n\t * Ensure a routed managed-local model is actually reachable before the turn calls it. No-op (and\n\t * free) for non-local/API models. Caches a \"confirmed up this session\" flag per server (and per\n\t * Transformers model) so steady-state routing pays the health-check round trip once; invalidated\n\t * above when a prior local call failed so a dead sidecar gets re-detected instead of trusted.\n\t */\n\tasync ensureLocalModelReady(\n\t\tmodel: Model<Api>,\n\t): Promise<{ ready: boolean; reason: string; installGuide?: string[] }> {\n\t\tif (model.provider !== OLLAMA_PROVIDER) {\n\t\t\treturn { ready: true, reason: \"not_local\" };\n\t\t}\n\t\tconst serverUrl = this.deriveOllamaServerUrl(model.baseUrl);\n\t\tconst confirmedKey = this.confirmationKey(model, serverUrl);\n\t\tthis.invalidateIfLastCallFailed(model, serverUrl);\n\t\tif (this._confirmedUp.has(confirmedKey)) {\n\t\t\treturn { ready: true, reason: \"confirmed_up_cached\" };\n\t\t}\n\t\tconst runtime = this.getLocalRuntime(serverUrl);\n\t\tconst status = await runtime.detect();\n\t\tif (status.serverUp) {\n\t\t\t// Server ownership is not a capability boundary. Reuse a configured user/system server\n\t\t\t// when it exposes the requested model; this preserves its accelerator settings and warm\n\t\t\t// cache instead of forcing a slower duplicate Pi-owned process/store.\n\t\t\tconst installedEntry = status.serverModels.find((entry) => matchesInstalledLocalModel(model.id, entry.name));\n\t\t\tif (!installedEntry) {\n\t\t\t\treturn {\n\t\t\t\t\tready: false,\n\t\t\t\t\treason: `model_missing_on_server:${model.id}:${status.activeStore?.path ?? \"external/unknown\"}`,\n\t\t\t\t};\n\t\t\t}\n\t\t\tconst resident = await this.ensureOllamaResident(model, runtime, installedEntry.sizeBytes);\n\t\t\tif (!resident.ok) return { ready: false, reason: resident.reason ?? \"residency_refused\" };\n\t\t\tthis._confirmedUp.add(confirmedKey);\n\t\t\treturn {\n\t\t\t\tready: true,\n\t\t\t\treason: status.managedByPi ? \"already_running_managed\" : \"already_running_configured_server\",\n\t\t\t};\n\t\t}\n\t\tif (!status.binaryPath) {\n\t\t\treturn { ready: false, reason: \"binary_missing\", installGuide: runtime.installGuide() };\n\t\t}\n\t\tconst started = await runtime.start();\n\t\tif (started.started) {\n\t\t\tlet installedEntry: { name: string; sizeBytes: number } | undefined;\n\t\t\ttry {\n\t\t\t\tconst installed = await runtime.list();\n\t\t\t\tinstalledEntry = installed.find((entry) => matchesInstalledLocalModel(model.id, entry.name));\n\t\t\t} catch (error) {\n\t\t\t\truntime.stop();\n\t\t\t\treturn {\n\t\t\t\t\tready: false,\n\t\t\t\t\treason: `model_list_failed_after_start:${error instanceof Error ? error.message : String(error)}`,\n\t\t\t\t};\n\t\t\t}\n\t\t\tif (!installedEntry) {\n\t\t\t\truntime.stop();\n\t\t\t\treturn { ready: false, reason: `model_missing_on_started_server:${model.id}` };\n\t\t\t}\n\t\t\tconst resident = await this.ensureOllamaResident(model, runtime, installedEntry.sizeBytes);\n\t\t\tif (!resident.ok) {\n\t\t\t\truntime.stop();\n\t\t\t\treturn { ready: false, reason: resident.reason ?? \"residency_refused\" };\n\t\t\t}\n\t\t\tthis._confirmedUp.add(confirmedKey);\n\t\t}\n\t\treturn { ready: started.started, reason: started.reason };\n\t}\n\n\tasync ensureTransformersModelReady(\n\t\tmodel: Model<Api>,\n\t): Promise<{ ready: boolean; reason: string; installGuide?: string[] }> {\n\t\tif (model.provider !== HF_TRANSFORMERS_PROVIDER) {\n\t\t\treturn { ready: true, reason: \"not_transformers\" };\n\t\t}\n\t\tconst serverUrl = this.deriveOpenAICompatServerUrl(model.baseUrl);\n\t\tconst confirmedKey = this.confirmationKey(model, serverUrl);\n\t\tthis.invalidateIfLastCallFailed(model, serverUrl);\n\t\tif (this._confirmedUp.has(confirmedKey)) {\n\t\t\treturn { ready: true, reason: \"confirmed_up_cached\" };\n\t\t}\n\t\tconst runtime = this.getTransformersRuntime(model.id, serverUrl);\n\t\tconst status = await runtime.detect();\n\t\tif (status.serverUp) {\n\t\t\tconst resident = await this.ensureTransformersResident(model, runtime);\n\t\t\tif (!resident.ok) return { ready: false, reason: resident.reason ?? \"residency_refused\" };\n\t\t\tthis._confirmedUp.add(confirmedKey);\n\t\t\treturn { ready: true, reason: \"already_running\" };\n\t\t}\n\t\tif (!status.runtimeInstalled) {\n\t\t\treturn { ready: false, reason: \"runtime_missing\", installGuide: runtime.installGuide() };\n\t\t}\n\t\tconst started = await runtime.start();\n\t\tif (started.started || started.reason === \"already_running\") {\n\t\t\tconst resident = await this.ensureTransformersResident(model, runtime);\n\t\t\tif (!resident.ok) return { ready: false, reason: resident.reason ?? \"residency_refused\" };\n\t\t\tthis._confirmedUp.add(confirmedKey);\n\t\t\treturn { ready: true, reason: started.reason };\n\t\t}\n\t\treturn { ready: false, reason: started.reason };\n\t}\n\n\t/**\n\t * #31: the ONE case a routed local model's unreadiness can be fixed automatically is a missing\n\t * ollama binary — an unreachable server can't be helped by installing, so that reason is left to\n\t * the graceful-fallback warning below unchanged. Only offered when there's an interactive UI to\n\t * ask through: headless/RPC/print sessions have no UI context and fall straight through, same as\n\t * declining or timing out (both resolve confirm() to false). Reverses \"pi never runs installers\n\t * itself\" specifically for this one path — the user is asked first, the download is pi's own\n\t * (never curl|sh), and it lands in pi's own runtimes dir (see OllamaRuntime.installManaged).\n\t *\n\t * Pauses/resumes the routing working-indicator around the confirm dialog itself (re-emitting\n\t * routing_end/routing_start — both already idempotent, see interactive-mode.ts's handlers) so an\n\t * animated spinner doesn't fight a dialog the user is trying to read and answer; the indicator\n\t * comes back for the download/extract that follows a \"yes\", which is genuine processing feedback.\n\t */\n\tprivate async maybeInstallOllamaOnConsent(\n\t\tmodel: Model<Api>,\n\t\treadiness: { ready: boolean; reason: string; installGuide?: string[] },\n\t): Promise<{ ready: boolean; reason: string; installGuide?: string[]; installAttemptError?: string }> {\n\t\tconst ui = this.deps.getUIContext();\n\t\tif (!ui || readiness.ready || readiness.reason !== \"binary_missing\") return readiness;\n\n\t\tconst modelLabel = this.deps.formatModel(model);\n\t\tthis.deps.emit({ type: \"routing_end\" });\n\t\tlet confirmed: boolean;\n\t\ttry {\n\t\t\tconfirmed = await ui.confirm(\n\t\t\t\t\"Install Ollama?\",\n\t\t\t\t`Ollama isn't installed, so the local model \"${modelLabel}\" can't run. Pi can download and ` +\n\t\t\t\t\t\"install it now (a large one-time download, possibly over 1 GB depending on your platform) \" +\n\t\t\t\t\t\"into its own runtimes folder — never curl|sh, never touching anything outside pi's own \" +\n\t\t\t\t\t\"directory. Install it now?\",\n\t\t\t\t{ timeout: OLLAMA_INSTALL_CONFIRM_TIMEOUT_MS },\n\t\t\t);\n\t\t} finally {\n\t\t\tthis.deps.emit({ type: \"routing_start\" });\n\t\t}\n\t\tif (!confirmed) return readiness;\n\n\t\tconst serverUrl = this.deriveOllamaServerUrl(model.baseUrl);\n\t\tconst runtime = this.getLocalRuntime(serverUrl);\n\t\tlet installResult: { ok: boolean; error?: string };\n\t\ttry {\n\t\t\tinstallResult = await runtime.installManaged((status) => ui.setStatus(\"ollama-install\", status));\n\t\t} finally {\n\t\t\tui.setStatus(\"ollama-install\", undefined);\n\t\t}\n\t\tif (!installResult.ok) {\n\t\t\treturn { ready: false, reason: \"install_failed\", installAttemptError: installResult.error };\n\t\t}\n\t\treturn this.ensureLocalModelReady(model);\n\t}\n\n\tprivate async maybeInstallTransformersOnConsent(\n\t\tmodel: Model<Api>,\n\t\treadiness: { ready: boolean; reason: string; installGuide?: string[] },\n\t): Promise<{ ready: boolean; reason: string; installGuide?: string[]; installAttemptError?: string }> {\n\t\tconst ui = this.deps.getUIContext();\n\t\tif (!ui || readiness.ready || readiness.reason !== \"runtime_missing\") return readiness;\n\n\t\tconst modelLabel = this.deps.formatModel(model);\n\t\tthis.deps.emit({ type: \"routing_end\" });\n\t\tlet confirmed: boolean;\n\t\ttry {\n\t\t\tconfirmed = await ui.confirm(\n\t\t\t\t\"Install Transformers runtime?\",\n\t\t\t\t`The Hugging Face model \"${modelLabel}\" needs a pi-managed Python venv with Transformers ` +\n\t\t\t\t\t\"and CPU PyTorch before it can run. Pi will install those packages into its own runtimes \" +\n\t\t\t\t\t\"folder, download the model into a pi-owned Hugging Face cache, and leave system Python, \" +\n\t\t\t\t\t\"your Ollama models, and your user HF cache untouched. Install it now?\",\n\t\t\t\t{ timeout: OLLAMA_INSTALL_CONFIRM_TIMEOUT_MS },\n\t\t\t);\n\t\t} finally {\n\t\t\tthis.deps.emit({ type: \"routing_start\" });\n\t\t}\n\t\tif (!confirmed) return readiness;\n\n\t\tconst serverUrl = this.deriveOpenAICompatServerUrl(model.baseUrl);\n\t\tconst runtime = this.getTransformersRuntime(model.id, serverUrl);\n\t\tlet installResult: { ok: boolean; error?: string };\n\t\ttry {\n\t\t\tinstallResult = await runtime.installManaged((status) => ui.setStatus(\"transformers-install\", status));\n\t\t} finally {\n\t\t\tui.setStatus(\"transformers-install\", undefined);\n\t\t}\n\t\tif (!installResult.ok) {\n\t\t\treturn { ready: false, reason: \"install_failed\", installAttemptError: installResult.error };\n\t\t}\n\t\tlet downloadResult: { ok: boolean; error?: string };\n\t\ttry {\n\t\t\tdownloadResult = await runtime.downloadModel((status) => ui.setStatus(\"transformers-download\", status));\n\t\t} finally {\n\t\t\tui.setStatus(\"transformers-download\", undefined);\n\t\t}\n\t\tif (!downloadResult.ok) {\n\t\t\treturn { ready: false, reason: \"download_failed\", installAttemptError: downloadResult.error };\n\t\t}\n\t\treturn this.ensureTransformersModelReady(model);\n\t}\n\n\t/**\n\t * Router-swap gate (#27): a turn routed to a local model (any tier, including an executor-direct\n\t * route — both carry tier \"cheap\") must not dead-end the turn just because ollama isn't up.\n\t * Never a SILENT swap: every fallback is announced in a warning that states (i) the local model\n\t * was unavailable and WHY — binary missing surfaces the install guide inline; any other reason\n\t * gets a \"check that ollama is running\" hint — and (ii) which tier is now handling the turn, so\n\t * the cost shift is never a surprise. Escalates cheap -> medium -> expensive, skipping any\n\t * unconfigured intermediate tier, reusing the router's own existing \"model unavailable\"\n\t * resolution (resolveConfiguredTierModel) rather than inventing a new fallback mechanism.\n\t * Escalation is bounded: tier strictly increases each hop, so it terminates within two hops.\n\t *\n\t * Before the warning/escalation below: #31's consent gate gets one shot at fixing a missing\n\t * binary interactively (see maybeInstallOllamaOnConsent) — declining, timing out, running\n\t * headless, or the install attempt itself failing all fall through here unchanged, just with an\n\t * honest reason (an install that failed is worded as a failed install, not re-labeled as if\n\t * nothing was ever tried).\n\t */\n\tasync ensureRouteModelReady(\n\t\tresolved: { decision: RouteDecision; model: Model<Api> } | undefined,\n\t): Promise<{ decision: RouteDecision; model: Model<Api> } | undefined> {\n\t\tlet current = resolved;\n\t\twhile (current && this.isManagedLocalProvider(current.model.provider)) {\n\t\t\tlet readiness: { ready: boolean; reason: string; installGuide?: string[]; installAttemptError?: string } =\n\t\t\t\tcurrent.model.provider === OLLAMA_PROVIDER\n\t\t\t\t\t? await this.ensureLocalModelReady(current.model)\n\t\t\t\t\t: await this.ensureTransformersModelReady(current.model);\n\t\t\tif (!readiness.ready) {\n\t\t\t\treadiness =\n\t\t\t\t\tcurrent.model.provider === OLLAMA_PROVIDER\n\t\t\t\t\t\t? await this.maybeInstallOllamaOnConsent(current.model, readiness)\n\t\t\t\t\t\t: await this.maybeInstallTransformersOnConsent(current.model, readiness);\n\t\t\t}\n\t\t\tif (readiness.ready) return current;\n\n\t\t\t// Walk the remaining tiers in order (never back down to cheap) and take the first one that\n\t\t\t// actually resolves — an unconfigured intermediate tier (e.g. no mediumModel set) must be\n\t\t\t// skipped, not treated as \"no fallback available\".\n\t\t\tconst startIndex = MODEL_ROUTER_TIER_ORDER.indexOf(current.decision.tier as \"cheap\" | \"medium\" | \"expensive\");\n\t\t\tlet escalated: { tier: \"medium\" | \"expensive\"; model: Model<Api> } | undefined;\n\t\t\tfor (let i = startIndex + 1; startIndex !== -1 && i < MODEL_ROUTER_TIER_ORDER.length; i++) {\n\t\t\t\tconst tier = MODEL_ROUTER_TIER_ORDER[i] as \"medium\" | \"expensive\";\n\t\t\t\tconst model = this.deps.resolveConfiguredTierModel(tier);\n\t\t\t\tif (model) {\n\t\t\t\t\tescalated = { tier, model };\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst modelLabel = this.deps.formatModel(current.model);\n\t\t\tconst localRuntimeName = current.model.provider === OLLAMA_PROVIDER ? \"ollama\" : \"Transformers\";\n\t\t\tconst wrongStore = parseWrongOllamaStoreReason(readiness.reason);\n\t\t\tconst whyText = readiness.installAttemptError\n\t\t\t\t? `pi tried to install it just now, but the install attempt failed: ${readiness.installAttemptError}`\n\t\t\t\t: readiness.installGuide\n\t\t\t\t\t? [\n\t\t\t\t\t\t\tcurrent.model.provider === OLLAMA_PROVIDER\n\t\t\t\t\t\t\t\t? \"the ollama binary is not installed.\"\n\t\t\t\t\t\t\t\t: \"the pi-managed Transformers runtime is not installed.\",\n\t\t\t\t\t\t\t...readiness.installGuide,\n\t\t\t\t\t\t].join(\"\\n\")\n\t\t\t\t\t: wrongStore\n\t\t\t\t\t\t? `an Ollama server is already running with store ${wrongStore.activePath} (${wrongStore.modelCount} model(s)); pi's owned store is ${wrongStore.ownedPath}. Stop the other server or run /models import, then retry.`\n\t\t\t\t\t\t: current.model.provider === OLLAMA_PROVIDER\n\t\t\t\t\t\t\t? `its ${localRuntimeName} server is not reachable (${readiness.reason}) — check that ollama is running.`\n\t\t\t\t\t\t\t: `its ${localRuntimeName} server is not reachable (${readiness.reason}) — check that the runtime is running.`;\n\t\t\tconst fallbackText = escalated\n\t\t\t\t? `Falling back to the ${escalated.tier} tier for this turn.`\n\t\t\t\t: \"No other tier is configured — falling back to the session's default model.\";\n\t\t\tthis.deps.emit({\n\t\t\t\ttype: \"warning\",\n\t\t\t\tmessage: `Local model \"${modelLabel}\" is unavailable: ${whyText}\\n${fallbackText}`,\n\t\t\t});\n\n\t\t\tif (!escalated) return undefined; // no higher tier resolves — caller falls back to the session default\n\t\t\tcurrent = {\n\t\t\t\tmodel: escalated.model,\n\t\t\t\tdecision: {\n\t\t\t\t\t...current.decision,\n\t\t\t\t\ttier: escalated.tier,\n\t\t\t\t\tfallbackFrom: current.decision.tier,\n\t\t\t\t\treasonCode: \"local_model_not_ready_fallback\",\n\t\t\t\t\treasons: [\n\t\t\t\t\t\t...current.decision.reasons,\n\t\t\t\t\t\t`Local model not ready (${readiness.reason}); escalated to ${escalated.tier}`,\n\t\t\t\t\t],\n\t\t\t\t\tmodel: this.deps.formatModel(escalated.model),\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\treturn current;\n\t}\n}\n"]}
|
|
@@ -265,9 +265,27 @@ export class LocalRuntimeController {
|
|
|
265
265
|
}
|
|
266
266
|
const started = await runtime.start();
|
|
267
267
|
if (started.started) {
|
|
268
|
-
|
|
269
|
-
|
|
268
|
+
let installedEntry;
|
|
269
|
+
try {
|
|
270
|
+
const installed = await runtime.list();
|
|
271
|
+
installedEntry = installed.find((entry) => matchesInstalledLocalModel(model.id, entry.name));
|
|
272
|
+
}
|
|
273
|
+
catch (error) {
|
|
274
|
+
runtime.stop();
|
|
275
|
+
return {
|
|
276
|
+
ready: false,
|
|
277
|
+
reason: `model_list_failed_after_start:${error instanceof Error ? error.message : String(error)}`,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
if (!installedEntry) {
|
|
281
|
+
runtime.stop();
|
|
282
|
+
return { ready: false, reason: `model_missing_on_started_server:${model.id}` };
|
|
283
|
+
}
|
|
284
|
+
const resident = await this.ensureOllamaResident(model, runtime, installedEntry.sizeBytes);
|
|
285
|
+
if (!resident.ok) {
|
|
286
|
+
runtime.stop();
|
|
270
287
|
return { ready: false, reason: resident.reason ?? "residency_refused" };
|
|
288
|
+
}
|
|
271
289
|
this._confirmedUp.add(confirmedKey);
|
|
272
290
|
}
|
|
273
291
|
return { ready: started.started, reason: started.reason };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"local-runtime-controller.js","sourceRoot":"","sources":["../../src/core/local-runtime-controller.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Local-runtime (Ollama) lifecycle controller.\n *\n * Extracted verbatim from agent-session.ts (god-file decomposition). Owns the cached, per-server\n * {@link OllamaRuntime} instances, the \"confirmed up this session\" flag, and the router's readiness\n * gate for a turn routed to a local (`ollama`) model — including the #31 install-on-consent flow and\n * the #27 graceful tier-escalation fallback. Takes narrow deps (agent dir, a last-assistant-message\n * accessor, the session's UI context/event emitter, and the router's own tier resolver) rather than\n * the whole AgentSession.\n */\n\nimport { totalmem } from \"node:os\";\nimport type { Api, AssistantMessage, Model } from \"@caupulican/pi-ai\";\nimport type { AgentSessionEvent } from \"./agent-session.ts\";\nimport type { RouteDecision } from \"./autonomy/contracts.ts\";\nimport type { ExtensionUIContext } from \"./extensions/index.ts\";\nimport { HF_TRANSFORMERS_PROVIDER, OLLAMA_PROVIDER } from \"./models/local-registration.ts\";\nimport {\n\ttype LocalRuntimeDeps,\n\tOllamaRuntime,\n\tresolveTransformersBaseUrl,\n\tTransformersRuntime,\n} from \"./models/local-runtime.ts\";\nimport { matchesInstalledLocalModel } from \"./models/model-ref.ts\";\nimport {\n\tOllamaRuntimeResidencyAdapter,\n\ttype RuntimeEvictionRecord,\n\ttype RuntimeResidencyAdapter,\n\tRuntimeResidencyArbiter,\n\tTransformersRuntimeResidencyAdapter,\n} from \"./models/runtime-arbiter.ts\";\n\n/** User-facing router tiers in ascending order — \"learning\" is never selected for a user turn, so\n * it has no place in the escalation ladder (#27's ensureRouteModelReady walks this forward only). */\nconst MODEL_ROUTER_TIER_ORDER: readonly (\"cheap\" | \"medium\" | \"expensive\")[] = [\"cheap\", \"medium\", \"expensive\"];\n\n/** How long the #31 \"install ollama now?\" confirm waits before auto-dismissing (same as a \"No\") —\n * long enough to read and decide, short enough that an unattended session doesn't hang a turn on it. */\nconst OLLAMA_INSTALL_CONFIRM_TIMEOUT_MS = 30_000;\n\nfunction parseWrongOllamaStoreReason(\n\treason: string,\n): { activePath: string; modelCount: number; ownedPath: string } | undefined {\n\tif (!reason.startsWith(\"wrong_store:\")) return undefined;\n\tconst parts = reason.slice(\"wrong_store:\".length).split(\":\");\n\tif (parts.length < 3) return undefined;\n\tconst [activePath, countText, ...ownedPathParts] = parts;\n\tconst modelCount = Number(countText);\n\treturn {\n\t\tactivePath,\n\t\tmodelCount: Number.isFinite(modelCount) ? modelCount : 0,\n\t\townedPath: ownedPathParts.join(\":\"),\n\t};\n}\n\nexport interface LocalRuntimeControllerDeps {\n\t/** Root directory OllamaRuntime instances are scoped under — fixed for the session's lifetime. */\n\tagentDir: string;\n\t/** Test-injectable seams for OllamaRuntime's own fetch/spawn/exists calls; unset in production. */\n\tlocalRuntimeDeps?: LocalRuntimeDeps;\n\t/** The session's last assistant message, to detect a just-failed local call and drop a stale\n\t * \"confirmed up\" flag. */\n\tgetLastAssistantMessage(): AssistantMessage | undefined;\n\t/** The session's live interactive UI context, if any — undefined in headless/RPC/print modes. */\n\tgetUIContext(): ExtensionUIContext | undefined;\n\t/** Emits a session event (only ever `warning` / `routing_start` / `routing_end` from this controller). */\n\temit(event: AgentSessionEvent): void;\n\t/** Resolves the model configured for a router tier, respecting configured auth — owned by the\n\t * router itself, not this controller. */\n\tresolveConfiguredTierModel(tier: \"medium\" | \"expensive\"): Model<Api> | undefined;\n\t/** `${provider}/${id}` label for a model, for warning/confirm text. */\n\tformatModel(model: Model<Api>): string;\n}\n\nexport class LocalRuntimeController {\n\t/** Lazy, cached by baseUrl so the router path and any other caller share one instance per server. */\n\tprivate readonly _runtimes = new Map<string, OllamaRuntime>();\n\t/** Lazy, cached by model+baseUrl so the router and `/models` share one sidecar handle per HF model. */\n\tprivate readonly _transformersRuntimes = new Map<string, TransformersRuntime>();\n\t/** Server URLs confirmed reachable THIS session — skips the health-check round trip on every\n\t * local-routed turn once warm. Keyed the same way as _runtimes. */\n\tprivate readonly _confirmedUp = new Set<string>();\n\t/** All live runtime adapters participate in one session-wide residency view. */\n\tprivate readonly _residencyAdapters = new Map<string, RuntimeResidencyAdapter>();\n\tprivate readonly _recentEvictions: RuntimeEvictionRecord[] = [];\n\n\tprivate readonly deps: LocalRuntimeControllerDeps;\n\n\tconstructor(deps: LocalRuntimeControllerDeps) {\n\t\tthis.deps = deps;\n\t}\n\n\t/**\n\t * Shared {@link OllamaRuntime} for a given server, lazily created and cached by baseUrl so every\n\t * caller — the router's readiness gate below and any host UI's own model-lifecycle commands\n\t * (e.g. `/models`) — sees and can stop the SAME pi-managed process instead of each tracking its\n\t * own untracked child.\n\t */\n\tgetLocalRuntime(baseUrl?: string): OllamaRuntime {\n\t\tconst key = baseUrl ?? \"default\";\n\t\tlet runtime = this._runtimes.get(key);\n\t\tif (!runtime) {\n\t\t\truntime = new OllamaRuntime({ agentDir: this.deps.agentDir, baseUrl, deps: this.deps.localRuntimeDeps });\n\t\t\tthis._runtimes.set(key, runtime);\n\t\t}\n\t\treturn runtime;\n\t}\n\n\tgetTransformersRuntime(modelId: string, baseUrl?: string): TransformersRuntime {\n\t\tconst resolvedBaseUrl = baseUrl?.replace(/\\/$/, \"\") ?? resolveTransformersBaseUrl(modelId);\n\t\tconst key = `${modelId}\\0${resolvedBaseUrl}`;\n\t\tlet runtime = this._transformersRuntimes.get(key);\n\t\tif (!runtime) {\n\t\t\truntime = new TransformersRuntime({\n\t\t\t\tagentDir: this.deps.agentDir,\n\t\t\t\tmodelId,\n\t\t\t\tbaseUrl: resolvedBaseUrl,\n\t\t\t\tdeps: this.deps.localRuntimeDeps,\n\t\t\t});\n\t\t\tthis._transformersRuntimes.set(key, runtime);\n\t\t}\n\t\treturn runtime;\n\t}\n\n\t/** models.json registers a local model's baseUrl as `<server>/v1` (OpenAI-compat); the runtime's\n\t * own health/boot endpoints are on the Ollama-native server root. */\n\tderiveOllamaServerUrl(modelBaseUrl: string): string {\n\t\treturn modelBaseUrl.replace(/\\/v1\\/?$/, \"\");\n\t}\n\n\tprivate deriveOpenAICompatServerUrl(modelBaseUrl: string): string {\n\t\treturn modelBaseUrl.replace(/\\/v1\\/?$/, \"\");\n\t}\n\n\tprivate isManagedLocalProvider(provider: string): boolean {\n\t\treturn provider === OLLAMA_PROVIDER || provider === HF_TRANSFORMERS_PROVIDER;\n\t}\n\n\t/**\n\t * If the last assistant message in this session was an error from THIS exact local server, a\n\t * cached \"confirmed up\" flag would be stale (the server may have died mid-session) — drop it so\n\t * the next ensure-check is a real one instead of trusting stale state.\n\t */\n\tprivate confirmationKey(model: Model<Api>, serverUrl: string): string {\n\t\treturn model.provider === HF_TRANSFORMERS_PROVIDER ? `${serverUrl}\\0${model.id}` : serverUrl;\n\t}\n\n\tprivate invalidateIfLastCallFailed(model: Model<Api>, serverUrl: string): void {\n\t\tconst lastAssistant = this.deps.getLastAssistantMessage();\n\t\tif (\n\t\t\tlastAssistant?.stopReason === \"error\" &&\n\t\t\tlastAssistant.provider === model.provider &&\n\t\t\tlastAssistant.model === model.id\n\t\t) {\n\t\t\tthis._confirmedUp.delete(this.confirmationKey(model, serverUrl));\n\t\t}\n\t}\n\n\tprivate async ensureOllamaResident(\n\t\tmodel: Model<Api>,\n\t\truntime: OllamaRuntime,\n\t\tknownSizeBytes?: number,\n\t): Promise<{ ok: boolean; reason?: string }> {\n\t\tlet bytes = knownSizeBytes ?? 0;\n\t\tif (knownSizeBytes === undefined) {\n\t\t\ttry {\n\t\t\t\tconst installed = await runtime.list();\n\t\t\t\tbytes = installed.find((entry) => matchesInstalledLocalModel(model.id, entry.name))?.sizeBytes ?? 0;\n\t\t\t} catch {\n\t\t\t\t// Residency is best-effort; a list failure falls back to a zero-byte admission request.\n\t\t\t}\n\t\t}\n\t\tconst serverUrl = this.deriveOllamaServerUrl(model.baseUrl);\n\t\tconst adapterId = `ollama:${serverUrl}`;\n\t\tthis._residencyAdapters.set(adapterId, new OllamaRuntimeResidencyAdapter(adapterId, runtime));\n\t\tconst arbiter = this.createResidencyArbiter();\n\t\ttry {\n\t\t\tconst nowMs = Date.now();\n\t\t\tconst plan = await arbiter.ensureResident(adapterId, {\n\t\t\t\tmodel: model.id,\n\t\t\t\tbytes,\n\t\t\t\trole: \"active\",\n\t\t\t\tpriority: 100,\n\t\t\t\tnowMs,\n\t\t\t\tpinActiveModel: model.id,\n\t\t\t\trecentEvictions: this._recentEvictions,\n\t\t\t\t// Cold Ollama loads can legitimately take several minutes. The actual adaptive stream\n\t\t\t\t// owns that wait; readiness must not front-run it with a 60-second empty generation.\n\t\t\t\tloadModel: false,\n\t\t\t});\n\t\t\tthis.recordEvictions(plan.evict, model.id, nowMs, adapterId);\n\t\t\treturn plan.status === \"fits\" ? { ok: true } : { ok: false, reason: `residency_refused:${plan.reason}` };\n\t\t} catch (error) {\n\t\t\treturn { ok: false, reason: `residency_error:${error instanceof Error ? error.message : String(error)}` };\n\t\t}\n\t}\n\n\tprivate async ensureTransformersResident(\n\t\tmodel: Model<Api>,\n\t\truntime: TransformersRuntime,\n\t): Promise<{ ok: boolean; reason?: string }> {\n\t\tconst serverUrl = this.deriveOpenAICompatServerUrl(model.baseUrl);\n\t\tconst adapterId = `transformers:${model.id}:${serverUrl}`;\n\t\tthis._residencyAdapters.set(adapterId, new TransformersRuntimeResidencyAdapter(adapterId, runtime, model.id, 0));\n\t\tconst arbiter = this.createResidencyArbiter();\n\t\ttry {\n\t\t\tconst nowMs = Date.now();\n\t\t\tconst plan = await arbiter.ensureResident(adapterId, {\n\t\t\t\tmodel: model.id,\n\t\t\t\tbytes: 0,\n\t\t\t\trole: \"active\",\n\t\t\t\tpriority: 100,\n\t\t\t\tnowMs,\n\t\t\t\tpinActiveModel: model.id,\n\t\t\t\trecentEvictions: this._recentEvictions,\n\t\t\t\tloadModel: false,\n\t\t\t});\n\t\t\tthis.recordEvictions(plan.evict, model.id, nowMs, adapterId);\n\t\t\treturn plan.status === \"fits\" ? { ok: true } : { ok: false, reason: `residency_refused:${plan.reason}` };\n\t\t} catch (error) {\n\t\t\treturn { ok: false, reason: `residency_error:${error instanceof Error ? error.message : String(error)}` };\n\t\t}\n\t}\n\n\tprivate createResidencyArbiter(): RuntimeResidencyArbiter {\n\t\treturn new RuntimeResidencyArbiter({\n\t\t\tbudgetBytes: Math.floor(totalmem() * 0.85),\n\t\t\tadapters: [...this._residencyAdapters.values()],\n\t\t});\n\t}\n\n\tprivate recordEvictions(\n\t\tevicted: readonly { adapterId: string; model: string }[],\n\t\tloaded: string,\n\t\tatMs: number,\n\t\tloadedAdapterId: string,\n\t): void {\n\t\tfor (const resident of evicted) {\n\t\t\tthis._recentEvictions.push({\n\t\t\t\tevicted: resident.model,\n\t\t\t\tloaded,\n\t\t\t\tatMs,\n\t\t\t\tevictedAdapterId: resident.adapterId,\n\t\t\t\tloadedAdapterId,\n\t\t\t});\n\t\t}\n\t\tconst cutoff = atMs - 5 * 60_000;\n\t\twhile (this._recentEvictions[0]?.atMs < cutoff) this._recentEvictions.shift();\n\t}\n\n\t/**\n\t * Readiness gate for every isolated/background completion. Unlike the foreground route gate it\n\t * never prompts or changes tiers: lanes either use the configured model or fail visibly.\n\t */\n\tasync ensureIsolatedModelReady(model: Model<Api>): Promise<void> {\n\t\tif (!this.isManagedLocalProvider(model.provider)) return;\n\t\tconst readiness =\n\t\t\tmodel.provider === OLLAMA_PROVIDER\n\t\t\t\t? await this.ensureLocalModelReady(model)\n\t\t\t\t: await this.ensureTransformersModelReady(model);\n\t\tif (readiness.ready) return;\n\t\tconst guide = readiness.installGuide?.length ? `\\n${readiness.installGuide.join(\"\\n\")}` : \"\";\n\t\tthrow new Error(\n\t\t\t`Managed local model ${this.deps.formatModel(model)} is unavailable for isolated execution (${readiness.reason}).${guide}`,\n\t\t);\n\t}\n\n\t/**\n\t * Readiness gate for a manually selected/default foreground model when no router route owns the\n\t * turn. Interactive sessions may offer the same managed-install consent as routed turns; the\n\t * selected model is never silently replaced.\n\t */\n\tasync ensureForegroundModelReady(model: Model<Api>): Promise<void> {\n\t\tif (!this.isManagedLocalProvider(model.provider)) return;\n\t\tlet readiness: {\n\t\t\tready: boolean;\n\t\t\treason: string;\n\t\t\tinstallGuide?: string[];\n\t\t\tinstallAttemptError?: string;\n\t\t} =\n\t\t\tmodel.provider === OLLAMA_PROVIDER\n\t\t\t\t? await this.ensureLocalModelReady(model)\n\t\t\t\t: await this.ensureTransformersModelReady(model);\n\t\tif (!readiness.ready) {\n\t\t\treadiness =\n\t\t\t\tmodel.provider === OLLAMA_PROVIDER\n\t\t\t\t\t? await this.maybeInstallOllamaOnConsent(model, readiness)\n\t\t\t\t\t: await this.maybeInstallTransformersOnConsent(model, readiness);\n\t\t}\n\t\tif (readiness.ready) return;\n\t\tconst detail = readiness.installAttemptError ?? readiness.installGuide?.join(\"\\n\") ?? readiness.reason;\n\t\tthrow new Error(`Managed local model ${this.deps.formatModel(model)} is unavailable: ${detail}`);\n\t}\n\n\t/**\n\t * Ensure a routed managed-local model is actually reachable before the turn calls it. No-op (and\n\t * free) for non-local/API models. Caches a \"confirmed up this session\" flag per server (and per\n\t * Transformers model) so steady-state routing pays the health-check round trip once; invalidated\n\t * above when a prior local call failed so a dead sidecar gets re-detected instead of trusted.\n\t */\n\tasync ensureLocalModelReady(\n\t\tmodel: Model<Api>,\n\t): Promise<{ ready: boolean; reason: string; installGuide?: string[] }> {\n\t\tif (model.provider !== OLLAMA_PROVIDER) {\n\t\t\treturn { ready: true, reason: \"not_local\" };\n\t\t}\n\t\tconst serverUrl = this.deriveOllamaServerUrl(model.baseUrl);\n\t\tconst confirmedKey = this.confirmationKey(model, serverUrl);\n\t\tthis.invalidateIfLastCallFailed(model, serverUrl);\n\t\tif (this._confirmedUp.has(confirmedKey)) {\n\t\t\treturn { ready: true, reason: \"confirmed_up_cached\" };\n\t\t}\n\t\tconst runtime = this.getLocalRuntime(serverUrl);\n\t\tconst status = await runtime.detect();\n\t\tif (status.serverUp) {\n\t\t\t// Server ownership is not a capability boundary. Reuse a configured user/system server\n\t\t\t// when it exposes the requested model; this preserves its accelerator settings and warm\n\t\t\t// cache instead of forcing a slower duplicate Pi-owned process/store.\n\t\t\tconst installedEntry = status.serverModels.find((entry) => matchesInstalledLocalModel(model.id, entry.name));\n\t\t\tif (!installedEntry) {\n\t\t\t\treturn {\n\t\t\t\t\tready: false,\n\t\t\t\t\treason: `model_missing_on_server:${model.id}:${status.activeStore?.path ?? \"external/unknown\"}`,\n\t\t\t\t};\n\t\t\t}\n\t\t\tconst resident = await this.ensureOllamaResident(model, runtime, installedEntry.sizeBytes);\n\t\t\tif (!resident.ok) return { ready: false, reason: resident.reason ?? \"residency_refused\" };\n\t\t\tthis._confirmedUp.add(confirmedKey);\n\t\t\treturn {\n\t\t\t\tready: true,\n\t\t\t\treason: status.managedByPi ? \"already_running_managed\" : \"already_running_configured_server\",\n\t\t\t};\n\t\t}\n\t\tif (!status.binaryPath) {\n\t\t\treturn { ready: false, reason: \"binary_missing\", installGuide: runtime.installGuide() };\n\t\t}\n\t\tconst started = await runtime.start();\n\t\tif (started.started) {\n\t\t\tconst resident = await this.ensureOllamaResident(model, runtime);\n\t\t\tif (!resident.ok) return { ready: false, reason: resident.reason ?? \"residency_refused\" };\n\t\t\tthis._confirmedUp.add(confirmedKey);\n\t\t}\n\t\treturn { ready: started.started, reason: started.reason };\n\t}\n\n\tasync ensureTransformersModelReady(\n\t\tmodel: Model<Api>,\n\t): Promise<{ ready: boolean; reason: string; installGuide?: string[] }> {\n\t\tif (model.provider !== HF_TRANSFORMERS_PROVIDER) {\n\t\t\treturn { ready: true, reason: \"not_transformers\" };\n\t\t}\n\t\tconst serverUrl = this.deriveOpenAICompatServerUrl(model.baseUrl);\n\t\tconst confirmedKey = this.confirmationKey(model, serverUrl);\n\t\tthis.invalidateIfLastCallFailed(model, serverUrl);\n\t\tif (this._confirmedUp.has(confirmedKey)) {\n\t\t\treturn { ready: true, reason: \"confirmed_up_cached\" };\n\t\t}\n\t\tconst runtime = this.getTransformersRuntime(model.id, serverUrl);\n\t\tconst status = await runtime.detect();\n\t\tif (status.serverUp) {\n\t\t\tconst resident = await this.ensureTransformersResident(model, runtime);\n\t\t\tif (!resident.ok) return { ready: false, reason: resident.reason ?? \"residency_refused\" };\n\t\t\tthis._confirmedUp.add(confirmedKey);\n\t\t\treturn { ready: true, reason: \"already_running\" };\n\t\t}\n\t\tif (!status.runtimeInstalled) {\n\t\t\treturn { ready: false, reason: \"runtime_missing\", installGuide: runtime.installGuide() };\n\t\t}\n\t\tconst started = await runtime.start();\n\t\tif (started.started || started.reason === \"already_running\") {\n\t\t\tconst resident = await this.ensureTransformersResident(model, runtime);\n\t\t\tif (!resident.ok) return { ready: false, reason: resident.reason ?? \"residency_refused\" };\n\t\t\tthis._confirmedUp.add(confirmedKey);\n\t\t\treturn { ready: true, reason: started.reason };\n\t\t}\n\t\treturn { ready: false, reason: started.reason };\n\t}\n\n\t/**\n\t * #31: the ONE case a routed local model's unreadiness can be fixed automatically is a missing\n\t * ollama binary — an unreachable server can't be helped by installing, so that reason is left to\n\t * the graceful-fallback warning below unchanged. Only offered when there's an interactive UI to\n\t * ask through: headless/RPC/print sessions have no UI context and fall straight through, same as\n\t * declining or timing out (both resolve confirm() to false). Reverses \"pi never runs installers\n\t * itself\" specifically for this one path — the user is asked first, the download is pi's own\n\t * (never curl|sh), and it lands in pi's own runtimes dir (see OllamaRuntime.installManaged).\n\t *\n\t * Pauses/resumes the routing working-indicator around the confirm dialog itself (re-emitting\n\t * routing_end/routing_start — both already idempotent, see interactive-mode.ts's handlers) so an\n\t * animated spinner doesn't fight a dialog the user is trying to read and answer; the indicator\n\t * comes back for the download/extract that follows a \"yes\", which is genuine processing feedback.\n\t */\n\tprivate async maybeInstallOllamaOnConsent(\n\t\tmodel: Model<Api>,\n\t\treadiness: { ready: boolean; reason: string; installGuide?: string[] },\n\t): Promise<{ ready: boolean; reason: string; installGuide?: string[]; installAttemptError?: string }> {\n\t\tconst ui = this.deps.getUIContext();\n\t\tif (!ui || readiness.ready || readiness.reason !== \"binary_missing\") return readiness;\n\n\t\tconst modelLabel = this.deps.formatModel(model);\n\t\tthis.deps.emit({ type: \"routing_end\" });\n\t\tlet confirmed: boolean;\n\t\ttry {\n\t\t\tconfirmed = await ui.confirm(\n\t\t\t\t\"Install Ollama?\",\n\t\t\t\t`Ollama isn't installed, so the local model \"${modelLabel}\" can't run. Pi can download and ` +\n\t\t\t\t\t\"install it now (a large one-time download, possibly over 1 GB depending on your platform) \" +\n\t\t\t\t\t\"into its own runtimes folder — never curl|sh, never touching anything outside pi's own \" +\n\t\t\t\t\t\"directory. Install it now?\",\n\t\t\t\t{ timeout: OLLAMA_INSTALL_CONFIRM_TIMEOUT_MS },\n\t\t\t);\n\t\t} finally {\n\t\t\tthis.deps.emit({ type: \"routing_start\" });\n\t\t}\n\t\tif (!confirmed) return readiness;\n\n\t\tconst serverUrl = this.deriveOllamaServerUrl(model.baseUrl);\n\t\tconst runtime = this.getLocalRuntime(serverUrl);\n\t\tlet installResult: { ok: boolean; error?: string };\n\t\ttry {\n\t\t\tinstallResult = await runtime.installManaged((status) => ui.setStatus(\"ollama-install\", status));\n\t\t} finally {\n\t\t\tui.setStatus(\"ollama-install\", undefined);\n\t\t}\n\t\tif (!installResult.ok) {\n\t\t\treturn { ready: false, reason: \"install_failed\", installAttemptError: installResult.error };\n\t\t}\n\t\treturn this.ensureLocalModelReady(model);\n\t}\n\n\tprivate async maybeInstallTransformersOnConsent(\n\t\tmodel: Model<Api>,\n\t\treadiness: { ready: boolean; reason: string; installGuide?: string[] },\n\t): Promise<{ ready: boolean; reason: string; installGuide?: string[]; installAttemptError?: string }> {\n\t\tconst ui = this.deps.getUIContext();\n\t\tif (!ui || readiness.ready || readiness.reason !== \"runtime_missing\") return readiness;\n\n\t\tconst modelLabel = this.deps.formatModel(model);\n\t\tthis.deps.emit({ type: \"routing_end\" });\n\t\tlet confirmed: boolean;\n\t\ttry {\n\t\t\tconfirmed = await ui.confirm(\n\t\t\t\t\"Install Transformers runtime?\",\n\t\t\t\t`The Hugging Face model \"${modelLabel}\" needs a pi-managed Python venv with Transformers ` +\n\t\t\t\t\t\"and CPU PyTorch before it can run. Pi will install those packages into its own runtimes \" +\n\t\t\t\t\t\"folder, download the model into a pi-owned Hugging Face cache, and leave system Python, \" +\n\t\t\t\t\t\"your Ollama models, and your user HF cache untouched. Install it now?\",\n\t\t\t\t{ timeout: OLLAMA_INSTALL_CONFIRM_TIMEOUT_MS },\n\t\t\t);\n\t\t} finally {\n\t\t\tthis.deps.emit({ type: \"routing_start\" });\n\t\t}\n\t\tif (!confirmed) return readiness;\n\n\t\tconst serverUrl = this.deriveOpenAICompatServerUrl(model.baseUrl);\n\t\tconst runtime = this.getTransformersRuntime(model.id, serverUrl);\n\t\tlet installResult: { ok: boolean; error?: string };\n\t\ttry {\n\t\t\tinstallResult = await runtime.installManaged((status) => ui.setStatus(\"transformers-install\", status));\n\t\t} finally {\n\t\t\tui.setStatus(\"transformers-install\", undefined);\n\t\t}\n\t\tif (!installResult.ok) {\n\t\t\treturn { ready: false, reason: \"install_failed\", installAttemptError: installResult.error };\n\t\t}\n\t\tlet downloadResult: { ok: boolean; error?: string };\n\t\ttry {\n\t\t\tdownloadResult = await runtime.downloadModel((status) => ui.setStatus(\"transformers-download\", status));\n\t\t} finally {\n\t\t\tui.setStatus(\"transformers-download\", undefined);\n\t\t}\n\t\tif (!downloadResult.ok) {\n\t\t\treturn { ready: false, reason: \"download_failed\", installAttemptError: downloadResult.error };\n\t\t}\n\t\treturn this.ensureTransformersModelReady(model);\n\t}\n\n\t/**\n\t * Router-swap gate (#27): a turn routed to a local model (any tier, including an executor-direct\n\t * route — both carry tier \"cheap\") must not dead-end the turn just because ollama isn't up.\n\t * Never a SILENT swap: every fallback is announced in a warning that states (i) the local model\n\t * was unavailable and WHY — binary missing surfaces the install guide inline; any other reason\n\t * gets a \"check that ollama is running\" hint — and (ii) which tier is now handling the turn, so\n\t * the cost shift is never a surprise. Escalates cheap -> medium -> expensive, skipping any\n\t * unconfigured intermediate tier, reusing the router's own existing \"model unavailable\"\n\t * resolution (resolveConfiguredTierModel) rather than inventing a new fallback mechanism.\n\t * Escalation is bounded: tier strictly increases each hop, so it terminates within two hops.\n\t *\n\t * Before the warning/escalation below: #31's consent gate gets one shot at fixing a missing\n\t * binary interactively (see maybeInstallOllamaOnConsent) — declining, timing out, running\n\t * headless, or the install attempt itself failing all fall through here unchanged, just with an\n\t * honest reason (an install that failed is worded as a failed install, not re-labeled as if\n\t * nothing was ever tried).\n\t */\n\tasync ensureRouteModelReady(\n\t\tresolved: { decision: RouteDecision; model: Model<Api> } | undefined,\n\t): Promise<{ decision: RouteDecision; model: Model<Api> } | undefined> {\n\t\tlet current = resolved;\n\t\twhile (current && this.isManagedLocalProvider(current.model.provider)) {\n\t\t\tlet readiness: { ready: boolean; reason: string; installGuide?: string[]; installAttemptError?: string } =\n\t\t\t\tcurrent.model.provider === OLLAMA_PROVIDER\n\t\t\t\t\t? await this.ensureLocalModelReady(current.model)\n\t\t\t\t\t: await this.ensureTransformersModelReady(current.model);\n\t\t\tif (!readiness.ready) {\n\t\t\t\treadiness =\n\t\t\t\t\tcurrent.model.provider === OLLAMA_PROVIDER\n\t\t\t\t\t\t? await this.maybeInstallOllamaOnConsent(current.model, readiness)\n\t\t\t\t\t\t: await this.maybeInstallTransformersOnConsent(current.model, readiness);\n\t\t\t}\n\t\t\tif (readiness.ready) return current;\n\n\t\t\t// Walk the remaining tiers in order (never back down to cheap) and take the first one that\n\t\t\t// actually resolves — an unconfigured intermediate tier (e.g. no mediumModel set) must be\n\t\t\t// skipped, not treated as \"no fallback available\".\n\t\t\tconst startIndex = MODEL_ROUTER_TIER_ORDER.indexOf(current.decision.tier as \"cheap\" | \"medium\" | \"expensive\");\n\t\t\tlet escalated: { tier: \"medium\" | \"expensive\"; model: Model<Api> } | undefined;\n\t\t\tfor (let i = startIndex + 1; startIndex !== -1 && i < MODEL_ROUTER_TIER_ORDER.length; i++) {\n\t\t\t\tconst tier = MODEL_ROUTER_TIER_ORDER[i] as \"medium\" | \"expensive\";\n\t\t\t\tconst model = this.deps.resolveConfiguredTierModel(tier);\n\t\t\t\tif (model) {\n\t\t\t\t\tescalated = { tier, model };\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst modelLabel = this.deps.formatModel(current.model);\n\t\t\tconst localRuntimeName = current.model.provider === OLLAMA_PROVIDER ? \"ollama\" : \"Transformers\";\n\t\t\tconst wrongStore = parseWrongOllamaStoreReason(readiness.reason);\n\t\t\tconst whyText = readiness.installAttemptError\n\t\t\t\t? `pi tried to install it just now, but the install attempt failed: ${readiness.installAttemptError}`\n\t\t\t\t: readiness.installGuide\n\t\t\t\t\t? [\n\t\t\t\t\t\t\tcurrent.model.provider === OLLAMA_PROVIDER\n\t\t\t\t\t\t\t\t? \"the ollama binary is not installed.\"\n\t\t\t\t\t\t\t\t: \"the pi-managed Transformers runtime is not installed.\",\n\t\t\t\t\t\t\t...readiness.installGuide,\n\t\t\t\t\t\t].join(\"\\n\")\n\t\t\t\t\t: wrongStore\n\t\t\t\t\t\t? `an Ollama server is already running with store ${wrongStore.activePath} (${wrongStore.modelCount} model(s)); pi's owned store is ${wrongStore.ownedPath}. Stop the other server or run /models import, then retry.`\n\t\t\t\t\t\t: current.model.provider === OLLAMA_PROVIDER\n\t\t\t\t\t\t\t? `its ${localRuntimeName} server is not reachable (${readiness.reason}) — check that ollama is running.`\n\t\t\t\t\t\t\t: `its ${localRuntimeName} server is not reachable (${readiness.reason}) — check that the runtime is running.`;\n\t\t\tconst fallbackText = escalated\n\t\t\t\t? `Falling back to the ${escalated.tier} tier for this turn.`\n\t\t\t\t: \"No other tier is configured — falling back to the session's default model.\";\n\t\t\tthis.deps.emit({\n\t\t\t\ttype: \"warning\",\n\t\t\t\tmessage: `Local model \"${modelLabel}\" is unavailable: ${whyText}\\n${fallbackText}`,\n\t\t\t});\n\n\t\t\tif (!escalated) return undefined; // no higher tier resolves — caller falls back to the session default\n\t\t\tcurrent = {\n\t\t\t\tmodel: escalated.model,\n\t\t\t\tdecision: {\n\t\t\t\t\t...current.decision,\n\t\t\t\t\ttier: escalated.tier,\n\t\t\t\t\tfallbackFrom: current.decision.tier,\n\t\t\t\t\treasonCode: \"local_model_not_ready_fallback\",\n\t\t\t\t\treasons: [\n\t\t\t\t\t\t...current.decision.reasons,\n\t\t\t\t\t\t`Local model not ready (${readiness.reason}); escalated to ${escalated.tier}`,\n\t\t\t\t\t],\n\t\t\t\t\tmodel: this.deps.formatModel(escalated.model),\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\treturn current;\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"local-runtime-controller.js","sourceRoot":"","sources":["../../src/core/local-runtime-controller.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Local-runtime (Ollama) lifecycle controller.\n *\n * Extracted verbatim from agent-session.ts (god-file decomposition). Owns the cached, per-server\n * {@link OllamaRuntime} instances, the \"confirmed up this session\" flag, and the router's readiness\n * gate for a turn routed to a local (`ollama`) model — including the #31 install-on-consent flow and\n * the #27 graceful tier-escalation fallback. Takes narrow deps (agent dir, a last-assistant-message\n * accessor, the session's UI context/event emitter, and the router's own tier resolver) rather than\n * the whole AgentSession.\n */\n\nimport { totalmem } from \"node:os\";\nimport type { Api, AssistantMessage, Model } from \"@caupulican/pi-ai\";\nimport type { AgentSessionEvent } from \"./agent-session.ts\";\nimport type { RouteDecision } from \"./autonomy/contracts.ts\";\nimport type { ExtensionUIContext } from \"./extensions/index.ts\";\nimport { HF_TRANSFORMERS_PROVIDER, OLLAMA_PROVIDER } from \"./models/local-registration.ts\";\nimport {\n\ttype LocalRuntimeDeps,\n\tOllamaRuntime,\n\tresolveTransformersBaseUrl,\n\tTransformersRuntime,\n} from \"./models/local-runtime.ts\";\nimport { matchesInstalledLocalModel } from \"./models/model-ref.ts\";\nimport {\n\tOllamaRuntimeResidencyAdapter,\n\ttype RuntimeEvictionRecord,\n\ttype RuntimeResidencyAdapter,\n\tRuntimeResidencyArbiter,\n\tTransformersRuntimeResidencyAdapter,\n} from \"./models/runtime-arbiter.ts\";\n\n/** User-facing router tiers in ascending order — \"learning\" is never selected for a user turn, so\n * it has no place in the escalation ladder (#27's ensureRouteModelReady walks this forward only). */\nconst MODEL_ROUTER_TIER_ORDER: readonly (\"cheap\" | \"medium\" | \"expensive\")[] = [\"cheap\", \"medium\", \"expensive\"];\n\n/** How long the #31 \"install ollama now?\" confirm waits before auto-dismissing (same as a \"No\") —\n * long enough to read and decide, short enough that an unattended session doesn't hang a turn on it. */\nconst OLLAMA_INSTALL_CONFIRM_TIMEOUT_MS = 30_000;\n\nfunction parseWrongOllamaStoreReason(\n\treason: string,\n): { activePath: string; modelCount: number; ownedPath: string } | undefined {\n\tif (!reason.startsWith(\"wrong_store:\")) return undefined;\n\tconst parts = reason.slice(\"wrong_store:\".length).split(\":\");\n\tif (parts.length < 3) return undefined;\n\tconst [activePath, countText, ...ownedPathParts] = parts;\n\tconst modelCount = Number(countText);\n\treturn {\n\t\tactivePath,\n\t\tmodelCount: Number.isFinite(modelCount) ? modelCount : 0,\n\t\townedPath: ownedPathParts.join(\":\"),\n\t};\n}\n\nexport interface LocalRuntimeControllerDeps {\n\t/** Root directory OllamaRuntime instances are scoped under — fixed for the session's lifetime. */\n\tagentDir: string;\n\t/** Test-injectable seams for OllamaRuntime's own fetch/spawn/exists calls; unset in production. */\n\tlocalRuntimeDeps?: LocalRuntimeDeps;\n\t/** The session's last assistant message, to detect a just-failed local call and drop a stale\n\t * \"confirmed up\" flag. */\n\tgetLastAssistantMessage(): AssistantMessage | undefined;\n\t/** The session's live interactive UI context, if any — undefined in headless/RPC/print modes. */\n\tgetUIContext(): ExtensionUIContext | undefined;\n\t/** Emits a session event (only ever `warning` / `routing_start` / `routing_end` from this controller). */\n\temit(event: AgentSessionEvent): void;\n\t/** Resolves the model configured for a router tier, respecting configured auth — owned by the\n\t * router itself, not this controller. */\n\tresolveConfiguredTierModel(tier: \"medium\" | \"expensive\"): Model<Api> | undefined;\n\t/** `${provider}/${id}` label for a model, for warning/confirm text. */\n\tformatModel(model: Model<Api>): string;\n}\n\nexport class LocalRuntimeController {\n\t/** Lazy, cached by baseUrl so the router path and any other caller share one instance per server. */\n\tprivate readonly _runtimes = new Map<string, OllamaRuntime>();\n\t/** Lazy, cached by model+baseUrl so the router and `/models` share one sidecar handle per HF model. */\n\tprivate readonly _transformersRuntimes = new Map<string, TransformersRuntime>();\n\t/** Server URLs confirmed reachable THIS session — skips the health-check round trip on every\n\t * local-routed turn once warm. Keyed the same way as _runtimes. */\n\tprivate readonly _confirmedUp = new Set<string>();\n\t/** All live runtime adapters participate in one session-wide residency view. */\n\tprivate readonly _residencyAdapters = new Map<string, RuntimeResidencyAdapter>();\n\tprivate readonly _recentEvictions: RuntimeEvictionRecord[] = [];\n\n\tprivate readonly deps: LocalRuntimeControllerDeps;\n\n\tconstructor(deps: LocalRuntimeControllerDeps) {\n\t\tthis.deps = deps;\n\t}\n\n\t/**\n\t * Shared {@link OllamaRuntime} for a given server, lazily created and cached by baseUrl so every\n\t * caller — the router's readiness gate below and any host UI's own model-lifecycle commands\n\t * (e.g. `/models`) — sees and can stop the SAME pi-managed process instead of each tracking its\n\t * own untracked child.\n\t */\n\tgetLocalRuntime(baseUrl?: string): OllamaRuntime {\n\t\tconst key = baseUrl ?? \"default\";\n\t\tlet runtime = this._runtimes.get(key);\n\t\tif (!runtime) {\n\t\t\truntime = new OllamaRuntime({ agentDir: this.deps.agentDir, baseUrl, deps: this.deps.localRuntimeDeps });\n\t\t\tthis._runtimes.set(key, runtime);\n\t\t}\n\t\treturn runtime;\n\t}\n\n\tgetTransformersRuntime(modelId: string, baseUrl?: string): TransformersRuntime {\n\t\tconst resolvedBaseUrl = baseUrl?.replace(/\\/$/, \"\") ?? resolveTransformersBaseUrl(modelId);\n\t\tconst key = `${modelId}\\0${resolvedBaseUrl}`;\n\t\tlet runtime = this._transformersRuntimes.get(key);\n\t\tif (!runtime) {\n\t\t\truntime = new TransformersRuntime({\n\t\t\t\tagentDir: this.deps.agentDir,\n\t\t\t\tmodelId,\n\t\t\t\tbaseUrl: resolvedBaseUrl,\n\t\t\t\tdeps: this.deps.localRuntimeDeps,\n\t\t\t});\n\t\t\tthis._transformersRuntimes.set(key, runtime);\n\t\t}\n\t\treturn runtime;\n\t}\n\n\t/** models.json registers a local model's baseUrl as `<server>/v1` (OpenAI-compat); the runtime's\n\t * own health/boot endpoints are on the Ollama-native server root. */\n\tderiveOllamaServerUrl(modelBaseUrl: string): string {\n\t\treturn modelBaseUrl.replace(/\\/v1\\/?$/, \"\");\n\t}\n\n\tprivate deriveOpenAICompatServerUrl(modelBaseUrl: string): string {\n\t\treturn modelBaseUrl.replace(/\\/v1\\/?$/, \"\");\n\t}\n\n\tprivate isManagedLocalProvider(provider: string): boolean {\n\t\treturn provider === OLLAMA_PROVIDER || provider === HF_TRANSFORMERS_PROVIDER;\n\t}\n\n\t/**\n\t * If the last assistant message in this session was an error from THIS exact local server, a\n\t * cached \"confirmed up\" flag would be stale (the server may have died mid-session) — drop it so\n\t * the next ensure-check is a real one instead of trusting stale state.\n\t */\n\tprivate confirmationKey(model: Model<Api>, serverUrl: string): string {\n\t\treturn model.provider === HF_TRANSFORMERS_PROVIDER ? `${serverUrl}\\0${model.id}` : serverUrl;\n\t}\n\n\tprivate invalidateIfLastCallFailed(model: Model<Api>, serverUrl: string): void {\n\t\tconst lastAssistant = this.deps.getLastAssistantMessage();\n\t\tif (\n\t\t\tlastAssistant?.stopReason === \"error\" &&\n\t\t\tlastAssistant.provider === model.provider &&\n\t\t\tlastAssistant.model === model.id\n\t\t) {\n\t\t\tthis._confirmedUp.delete(this.confirmationKey(model, serverUrl));\n\t\t}\n\t}\n\n\tprivate async ensureOllamaResident(\n\t\tmodel: Model<Api>,\n\t\truntime: OllamaRuntime,\n\t\tknownSizeBytes?: number,\n\t): Promise<{ ok: boolean; reason?: string }> {\n\t\tlet bytes = knownSizeBytes ?? 0;\n\t\tif (knownSizeBytes === undefined) {\n\t\t\ttry {\n\t\t\t\tconst installed = await runtime.list();\n\t\t\t\tbytes = installed.find((entry) => matchesInstalledLocalModel(model.id, entry.name))?.sizeBytes ?? 0;\n\t\t\t} catch {\n\t\t\t\t// Residency is best-effort; a list failure falls back to a zero-byte admission request.\n\t\t\t}\n\t\t}\n\t\tconst serverUrl = this.deriveOllamaServerUrl(model.baseUrl);\n\t\tconst adapterId = `ollama:${serverUrl}`;\n\t\tthis._residencyAdapters.set(adapterId, new OllamaRuntimeResidencyAdapter(adapterId, runtime));\n\t\tconst arbiter = this.createResidencyArbiter();\n\t\ttry {\n\t\t\tconst nowMs = Date.now();\n\t\t\tconst plan = await arbiter.ensureResident(adapterId, {\n\t\t\t\tmodel: model.id,\n\t\t\t\tbytes,\n\t\t\t\trole: \"active\",\n\t\t\t\tpriority: 100,\n\t\t\t\tnowMs,\n\t\t\t\tpinActiveModel: model.id,\n\t\t\t\trecentEvictions: this._recentEvictions,\n\t\t\t\t// Cold Ollama loads can legitimately take several minutes. The actual adaptive stream\n\t\t\t\t// owns that wait; readiness must not front-run it with a 60-second empty generation.\n\t\t\t\tloadModel: false,\n\t\t\t});\n\t\t\tthis.recordEvictions(plan.evict, model.id, nowMs, adapterId);\n\t\t\treturn plan.status === \"fits\" ? { ok: true } : { ok: false, reason: `residency_refused:${plan.reason}` };\n\t\t} catch (error) {\n\t\t\treturn { ok: false, reason: `residency_error:${error instanceof Error ? error.message : String(error)}` };\n\t\t}\n\t}\n\n\tprivate async ensureTransformersResident(\n\t\tmodel: Model<Api>,\n\t\truntime: TransformersRuntime,\n\t): Promise<{ ok: boolean; reason?: string }> {\n\t\tconst serverUrl = this.deriveOpenAICompatServerUrl(model.baseUrl);\n\t\tconst adapterId = `transformers:${model.id}:${serverUrl}`;\n\t\tthis._residencyAdapters.set(adapterId, new TransformersRuntimeResidencyAdapter(adapterId, runtime, model.id, 0));\n\t\tconst arbiter = this.createResidencyArbiter();\n\t\ttry {\n\t\t\tconst nowMs = Date.now();\n\t\t\tconst plan = await arbiter.ensureResident(adapterId, {\n\t\t\t\tmodel: model.id,\n\t\t\t\tbytes: 0,\n\t\t\t\trole: \"active\",\n\t\t\t\tpriority: 100,\n\t\t\t\tnowMs,\n\t\t\t\tpinActiveModel: model.id,\n\t\t\t\trecentEvictions: this._recentEvictions,\n\t\t\t\tloadModel: false,\n\t\t\t});\n\t\t\tthis.recordEvictions(plan.evict, model.id, nowMs, adapterId);\n\t\t\treturn plan.status === \"fits\" ? { ok: true } : { ok: false, reason: `residency_refused:${plan.reason}` };\n\t\t} catch (error) {\n\t\t\treturn { ok: false, reason: `residency_error:${error instanceof Error ? error.message : String(error)}` };\n\t\t}\n\t}\n\n\tprivate createResidencyArbiter(): RuntimeResidencyArbiter {\n\t\treturn new RuntimeResidencyArbiter({\n\t\t\tbudgetBytes: Math.floor(totalmem() * 0.85),\n\t\t\tadapters: [...this._residencyAdapters.values()],\n\t\t});\n\t}\n\n\tprivate recordEvictions(\n\t\tevicted: readonly { adapterId: string; model: string }[],\n\t\tloaded: string,\n\t\tatMs: number,\n\t\tloadedAdapterId: string,\n\t): void {\n\t\tfor (const resident of evicted) {\n\t\t\tthis._recentEvictions.push({\n\t\t\t\tevicted: resident.model,\n\t\t\t\tloaded,\n\t\t\t\tatMs,\n\t\t\t\tevictedAdapterId: resident.adapterId,\n\t\t\t\tloadedAdapterId,\n\t\t\t});\n\t\t}\n\t\tconst cutoff = atMs - 5 * 60_000;\n\t\twhile (this._recentEvictions[0]?.atMs < cutoff) this._recentEvictions.shift();\n\t}\n\n\t/**\n\t * Readiness gate for every isolated/background completion. Unlike the foreground route gate it\n\t * never prompts or changes tiers: lanes either use the configured model or fail visibly.\n\t */\n\tasync ensureIsolatedModelReady(model: Model<Api>): Promise<void> {\n\t\tif (!this.isManagedLocalProvider(model.provider)) return;\n\t\tconst readiness =\n\t\t\tmodel.provider === OLLAMA_PROVIDER\n\t\t\t\t? await this.ensureLocalModelReady(model)\n\t\t\t\t: await this.ensureTransformersModelReady(model);\n\t\tif (readiness.ready) return;\n\t\tconst guide = readiness.installGuide?.length ? `\\n${readiness.installGuide.join(\"\\n\")}` : \"\";\n\t\tthrow new Error(\n\t\t\t`Managed local model ${this.deps.formatModel(model)} is unavailable for isolated execution (${readiness.reason}).${guide}`,\n\t\t);\n\t}\n\n\t/**\n\t * Readiness gate for a manually selected/default foreground model when no router route owns the\n\t * turn. Interactive sessions may offer the same managed-install consent as routed turns; the\n\t * selected model is never silently replaced.\n\t */\n\tasync ensureForegroundModelReady(model: Model<Api>): Promise<void> {\n\t\tif (!this.isManagedLocalProvider(model.provider)) return;\n\t\tlet readiness: {\n\t\t\tready: boolean;\n\t\t\treason: string;\n\t\t\tinstallGuide?: string[];\n\t\t\tinstallAttemptError?: string;\n\t\t} =\n\t\t\tmodel.provider === OLLAMA_PROVIDER\n\t\t\t\t? await this.ensureLocalModelReady(model)\n\t\t\t\t: await this.ensureTransformersModelReady(model);\n\t\tif (!readiness.ready) {\n\t\t\treadiness =\n\t\t\t\tmodel.provider === OLLAMA_PROVIDER\n\t\t\t\t\t? await this.maybeInstallOllamaOnConsent(model, readiness)\n\t\t\t\t\t: await this.maybeInstallTransformersOnConsent(model, readiness);\n\t\t}\n\t\tif (readiness.ready) return;\n\t\tconst detail = readiness.installAttemptError ?? readiness.installGuide?.join(\"\\n\") ?? readiness.reason;\n\t\tthrow new Error(`Managed local model ${this.deps.formatModel(model)} is unavailable: ${detail}`);\n\t}\n\n\t/**\n\t * Ensure a routed managed-local model is actually reachable before the turn calls it. No-op (and\n\t * free) for non-local/API models. Caches a \"confirmed up this session\" flag per server (and per\n\t * Transformers model) so steady-state routing pays the health-check round trip once; invalidated\n\t * above when a prior local call failed so a dead sidecar gets re-detected instead of trusted.\n\t */\n\tasync ensureLocalModelReady(\n\t\tmodel: Model<Api>,\n\t): Promise<{ ready: boolean; reason: string; installGuide?: string[] }> {\n\t\tif (model.provider !== OLLAMA_PROVIDER) {\n\t\t\treturn { ready: true, reason: \"not_local\" };\n\t\t}\n\t\tconst serverUrl = this.deriveOllamaServerUrl(model.baseUrl);\n\t\tconst confirmedKey = this.confirmationKey(model, serverUrl);\n\t\tthis.invalidateIfLastCallFailed(model, serverUrl);\n\t\tif (this._confirmedUp.has(confirmedKey)) {\n\t\t\treturn { ready: true, reason: \"confirmed_up_cached\" };\n\t\t}\n\t\tconst runtime = this.getLocalRuntime(serverUrl);\n\t\tconst status = await runtime.detect();\n\t\tif (status.serverUp) {\n\t\t\t// Server ownership is not a capability boundary. Reuse a configured user/system server\n\t\t\t// when it exposes the requested model; this preserves its accelerator settings and warm\n\t\t\t// cache instead of forcing a slower duplicate Pi-owned process/store.\n\t\t\tconst installedEntry = status.serverModels.find((entry) => matchesInstalledLocalModel(model.id, entry.name));\n\t\t\tif (!installedEntry) {\n\t\t\t\treturn {\n\t\t\t\t\tready: false,\n\t\t\t\t\treason: `model_missing_on_server:${model.id}:${status.activeStore?.path ?? \"external/unknown\"}`,\n\t\t\t\t};\n\t\t\t}\n\t\t\tconst resident = await this.ensureOllamaResident(model, runtime, installedEntry.sizeBytes);\n\t\t\tif (!resident.ok) return { ready: false, reason: resident.reason ?? \"residency_refused\" };\n\t\t\tthis._confirmedUp.add(confirmedKey);\n\t\t\treturn {\n\t\t\t\tready: true,\n\t\t\t\treason: status.managedByPi ? \"already_running_managed\" : \"already_running_configured_server\",\n\t\t\t};\n\t\t}\n\t\tif (!status.binaryPath) {\n\t\t\treturn { ready: false, reason: \"binary_missing\", installGuide: runtime.installGuide() };\n\t\t}\n\t\tconst started = await runtime.start();\n\t\tif (started.started) {\n\t\t\tlet installedEntry: { name: string; sizeBytes: number } | undefined;\n\t\t\ttry {\n\t\t\t\tconst installed = await runtime.list();\n\t\t\t\tinstalledEntry = installed.find((entry) => matchesInstalledLocalModel(model.id, entry.name));\n\t\t\t} catch (error) {\n\t\t\t\truntime.stop();\n\t\t\t\treturn {\n\t\t\t\t\tready: false,\n\t\t\t\t\treason: `model_list_failed_after_start:${error instanceof Error ? error.message : String(error)}`,\n\t\t\t\t};\n\t\t\t}\n\t\t\tif (!installedEntry) {\n\t\t\t\truntime.stop();\n\t\t\t\treturn { ready: false, reason: `model_missing_on_started_server:${model.id}` };\n\t\t\t}\n\t\t\tconst resident = await this.ensureOllamaResident(model, runtime, installedEntry.sizeBytes);\n\t\t\tif (!resident.ok) {\n\t\t\t\truntime.stop();\n\t\t\t\treturn { ready: false, reason: resident.reason ?? \"residency_refused\" };\n\t\t\t}\n\t\t\tthis._confirmedUp.add(confirmedKey);\n\t\t}\n\t\treturn { ready: started.started, reason: started.reason };\n\t}\n\n\tasync ensureTransformersModelReady(\n\t\tmodel: Model<Api>,\n\t): Promise<{ ready: boolean; reason: string; installGuide?: string[] }> {\n\t\tif (model.provider !== HF_TRANSFORMERS_PROVIDER) {\n\t\t\treturn { ready: true, reason: \"not_transformers\" };\n\t\t}\n\t\tconst serverUrl = this.deriveOpenAICompatServerUrl(model.baseUrl);\n\t\tconst confirmedKey = this.confirmationKey(model, serverUrl);\n\t\tthis.invalidateIfLastCallFailed(model, serverUrl);\n\t\tif (this._confirmedUp.has(confirmedKey)) {\n\t\t\treturn { ready: true, reason: \"confirmed_up_cached\" };\n\t\t}\n\t\tconst runtime = this.getTransformersRuntime(model.id, serverUrl);\n\t\tconst status = await runtime.detect();\n\t\tif (status.serverUp) {\n\t\t\tconst resident = await this.ensureTransformersResident(model, runtime);\n\t\t\tif (!resident.ok) return { ready: false, reason: resident.reason ?? \"residency_refused\" };\n\t\t\tthis._confirmedUp.add(confirmedKey);\n\t\t\treturn { ready: true, reason: \"already_running\" };\n\t\t}\n\t\tif (!status.runtimeInstalled) {\n\t\t\treturn { ready: false, reason: \"runtime_missing\", installGuide: runtime.installGuide() };\n\t\t}\n\t\tconst started = await runtime.start();\n\t\tif (started.started || started.reason === \"already_running\") {\n\t\t\tconst resident = await this.ensureTransformersResident(model, runtime);\n\t\t\tif (!resident.ok) return { ready: false, reason: resident.reason ?? \"residency_refused\" };\n\t\t\tthis._confirmedUp.add(confirmedKey);\n\t\t\treturn { ready: true, reason: started.reason };\n\t\t}\n\t\treturn { ready: false, reason: started.reason };\n\t}\n\n\t/**\n\t * #31: the ONE case a routed local model's unreadiness can be fixed automatically is a missing\n\t * ollama binary — an unreachable server can't be helped by installing, so that reason is left to\n\t * the graceful-fallback warning below unchanged. Only offered when there's an interactive UI to\n\t * ask through: headless/RPC/print sessions have no UI context and fall straight through, same as\n\t * declining or timing out (both resolve confirm() to false). Reverses \"pi never runs installers\n\t * itself\" specifically for this one path — the user is asked first, the download is pi's own\n\t * (never curl|sh), and it lands in pi's own runtimes dir (see OllamaRuntime.installManaged).\n\t *\n\t * Pauses/resumes the routing working-indicator around the confirm dialog itself (re-emitting\n\t * routing_end/routing_start — both already idempotent, see interactive-mode.ts's handlers) so an\n\t * animated spinner doesn't fight a dialog the user is trying to read and answer; the indicator\n\t * comes back for the download/extract that follows a \"yes\", which is genuine processing feedback.\n\t */\n\tprivate async maybeInstallOllamaOnConsent(\n\t\tmodel: Model<Api>,\n\t\treadiness: { ready: boolean; reason: string; installGuide?: string[] },\n\t): Promise<{ ready: boolean; reason: string; installGuide?: string[]; installAttemptError?: string }> {\n\t\tconst ui = this.deps.getUIContext();\n\t\tif (!ui || readiness.ready || readiness.reason !== \"binary_missing\") return readiness;\n\n\t\tconst modelLabel = this.deps.formatModel(model);\n\t\tthis.deps.emit({ type: \"routing_end\" });\n\t\tlet confirmed: boolean;\n\t\ttry {\n\t\t\tconfirmed = await ui.confirm(\n\t\t\t\t\"Install Ollama?\",\n\t\t\t\t`Ollama isn't installed, so the local model \"${modelLabel}\" can't run. Pi can download and ` +\n\t\t\t\t\t\"install it now (a large one-time download, possibly over 1 GB depending on your platform) \" +\n\t\t\t\t\t\"into its own runtimes folder — never curl|sh, never touching anything outside pi's own \" +\n\t\t\t\t\t\"directory. Install it now?\",\n\t\t\t\t{ timeout: OLLAMA_INSTALL_CONFIRM_TIMEOUT_MS },\n\t\t\t);\n\t\t} finally {\n\t\t\tthis.deps.emit({ type: \"routing_start\" });\n\t\t}\n\t\tif (!confirmed) return readiness;\n\n\t\tconst serverUrl = this.deriveOllamaServerUrl(model.baseUrl);\n\t\tconst runtime = this.getLocalRuntime(serverUrl);\n\t\tlet installResult: { ok: boolean; error?: string };\n\t\ttry {\n\t\t\tinstallResult = await runtime.installManaged((status) => ui.setStatus(\"ollama-install\", status));\n\t\t} finally {\n\t\t\tui.setStatus(\"ollama-install\", undefined);\n\t\t}\n\t\tif (!installResult.ok) {\n\t\t\treturn { ready: false, reason: \"install_failed\", installAttemptError: installResult.error };\n\t\t}\n\t\treturn this.ensureLocalModelReady(model);\n\t}\n\n\tprivate async maybeInstallTransformersOnConsent(\n\t\tmodel: Model<Api>,\n\t\treadiness: { ready: boolean; reason: string; installGuide?: string[] },\n\t): Promise<{ ready: boolean; reason: string; installGuide?: string[]; installAttemptError?: string }> {\n\t\tconst ui = this.deps.getUIContext();\n\t\tif (!ui || readiness.ready || readiness.reason !== \"runtime_missing\") return readiness;\n\n\t\tconst modelLabel = this.deps.formatModel(model);\n\t\tthis.deps.emit({ type: \"routing_end\" });\n\t\tlet confirmed: boolean;\n\t\ttry {\n\t\t\tconfirmed = await ui.confirm(\n\t\t\t\t\"Install Transformers runtime?\",\n\t\t\t\t`The Hugging Face model \"${modelLabel}\" needs a pi-managed Python venv with Transformers ` +\n\t\t\t\t\t\"and CPU PyTorch before it can run. Pi will install those packages into its own runtimes \" +\n\t\t\t\t\t\"folder, download the model into a pi-owned Hugging Face cache, and leave system Python, \" +\n\t\t\t\t\t\"your Ollama models, and your user HF cache untouched. Install it now?\",\n\t\t\t\t{ timeout: OLLAMA_INSTALL_CONFIRM_TIMEOUT_MS },\n\t\t\t);\n\t\t} finally {\n\t\t\tthis.deps.emit({ type: \"routing_start\" });\n\t\t}\n\t\tif (!confirmed) return readiness;\n\n\t\tconst serverUrl = this.deriveOpenAICompatServerUrl(model.baseUrl);\n\t\tconst runtime = this.getTransformersRuntime(model.id, serverUrl);\n\t\tlet installResult: { ok: boolean; error?: string };\n\t\ttry {\n\t\t\tinstallResult = await runtime.installManaged((status) => ui.setStatus(\"transformers-install\", status));\n\t\t} finally {\n\t\t\tui.setStatus(\"transformers-install\", undefined);\n\t\t}\n\t\tif (!installResult.ok) {\n\t\t\treturn { ready: false, reason: \"install_failed\", installAttemptError: installResult.error };\n\t\t}\n\t\tlet downloadResult: { ok: boolean; error?: string };\n\t\ttry {\n\t\t\tdownloadResult = await runtime.downloadModel((status) => ui.setStatus(\"transformers-download\", status));\n\t\t} finally {\n\t\t\tui.setStatus(\"transformers-download\", undefined);\n\t\t}\n\t\tif (!downloadResult.ok) {\n\t\t\treturn { ready: false, reason: \"download_failed\", installAttemptError: downloadResult.error };\n\t\t}\n\t\treturn this.ensureTransformersModelReady(model);\n\t}\n\n\t/**\n\t * Router-swap gate (#27): a turn routed to a local model (any tier, including an executor-direct\n\t * route — both carry tier \"cheap\") must not dead-end the turn just because ollama isn't up.\n\t * Never a SILENT swap: every fallback is announced in a warning that states (i) the local model\n\t * was unavailable and WHY — binary missing surfaces the install guide inline; any other reason\n\t * gets a \"check that ollama is running\" hint — and (ii) which tier is now handling the turn, so\n\t * the cost shift is never a surprise. Escalates cheap -> medium -> expensive, skipping any\n\t * unconfigured intermediate tier, reusing the router's own existing \"model unavailable\"\n\t * resolution (resolveConfiguredTierModel) rather than inventing a new fallback mechanism.\n\t * Escalation is bounded: tier strictly increases each hop, so it terminates within two hops.\n\t *\n\t * Before the warning/escalation below: #31's consent gate gets one shot at fixing a missing\n\t * binary interactively (see maybeInstallOllamaOnConsent) — declining, timing out, running\n\t * headless, or the install attempt itself failing all fall through here unchanged, just with an\n\t * honest reason (an install that failed is worded as a failed install, not re-labeled as if\n\t * nothing was ever tried).\n\t */\n\tasync ensureRouteModelReady(\n\t\tresolved: { decision: RouteDecision; model: Model<Api> } | undefined,\n\t): Promise<{ decision: RouteDecision; model: Model<Api> } | undefined> {\n\t\tlet current = resolved;\n\t\twhile (current && this.isManagedLocalProvider(current.model.provider)) {\n\t\t\tlet readiness: { ready: boolean; reason: string; installGuide?: string[]; installAttemptError?: string } =\n\t\t\t\tcurrent.model.provider === OLLAMA_PROVIDER\n\t\t\t\t\t? await this.ensureLocalModelReady(current.model)\n\t\t\t\t\t: await this.ensureTransformersModelReady(current.model);\n\t\t\tif (!readiness.ready) {\n\t\t\t\treadiness =\n\t\t\t\t\tcurrent.model.provider === OLLAMA_PROVIDER\n\t\t\t\t\t\t? await this.maybeInstallOllamaOnConsent(current.model, readiness)\n\t\t\t\t\t\t: await this.maybeInstallTransformersOnConsent(current.model, readiness);\n\t\t\t}\n\t\t\tif (readiness.ready) return current;\n\n\t\t\t// Walk the remaining tiers in order (never back down to cheap) and take the first one that\n\t\t\t// actually resolves — an unconfigured intermediate tier (e.g. no mediumModel set) must be\n\t\t\t// skipped, not treated as \"no fallback available\".\n\t\t\tconst startIndex = MODEL_ROUTER_TIER_ORDER.indexOf(current.decision.tier as \"cheap\" | \"medium\" | \"expensive\");\n\t\t\tlet escalated: { tier: \"medium\" | \"expensive\"; model: Model<Api> } | undefined;\n\t\t\tfor (let i = startIndex + 1; startIndex !== -1 && i < MODEL_ROUTER_TIER_ORDER.length; i++) {\n\t\t\t\tconst tier = MODEL_ROUTER_TIER_ORDER[i] as \"medium\" | \"expensive\";\n\t\t\t\tconst model = this.deps.resolveConfiguredTierModel(tier);\n\t\t\t\tif (model) {\n\t\t\t\t\tescalated = { tier, model };\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst modelLabel = this.deps.formatModel(current.model);\n\t\t\tconst localRuntimeName = current.model.provider === OLLAMA_PROVIDER ? \"ollama\" : \"Transformers\";\n\t\t\tconst wrongStore = parseWrongOllamaStoreReason(readiness.reason);\n\t\t\tconst whyText = readiness.installAttemptError\n\t\t\t\t? `pi tried to install it just now, but the install attempt failed: ${readiness.installAttemptError}`\n\t\t\t\t: readiness.installGuide\n\t\t\t\t\t? [\n\t\t\t\t\t\t\tcurrent.model.provider === OLLAMA_PROVIDER\n\t\t\t\t\t\t\t\t? \"the ollama binary is not installed.\"\n\t\t\t\t\t\t\t\t: \"the pi-managed Transformers runtime is not installed.\",\n\t\t\t\t\t\t\t...readiness.installGuide,\n\t\t\t\t\t\t].join(\"\\n\")\n\t\t\t\t\t: wrongStore\n\t\t\t\t\t\t? `an Ollama server is already running with store ${wrongStore.activePath} (${wrongStore.modelCount} model(s)); pi's owned store is ${wrongStore.ownedPath}. Stop the other server or run /models import, then retry.`\n\t\t\t\t\t\t: current.model.provider === OLLAMA_PROVIDER\n\t\t\t\t\t\t\t? `its ${localRuntimeName} server is not reachable (${readiness.reason}) — check that ollama is running.`\n\t\t\t\t\t\t\t: `its ${localRuntimeName} server is not reachable (${readiness.reason}) — check that the runtime is running.`;\n\t\t\tconst fallbackText = escalated\n\t\t\t\t? `Falling back to the ${escalated.tier} tier for this turn.`\n\t\t\t\t: \"No other tier is configured — falling back to the session's default model.\";\n\t\t\tthis.deps.emit({\n\t\t\t\ttype: \"warning\",\n\t\t\t\tmessage: `Local model \"${modelLabel}\" is unavailable: ${whyText}\\n${fallbackText}`,\n\t\t\t});\n\n\t\t\tif (!escalated) return undefined; // no higher tier resolves — caller falls back to the session default\n\t\t\tcurrent = {\n\t\t\t\tmodel: escalated.model,\n\t\t\t\tdecision: {\n\t\t\t\t\t...current.decision,\n\t\t\t\t\ttier: escalated.tier,\n\t\t\t\t\tfallbackFrom: current.decision.tier,\n\t\t\t\t\treasonCode: \"local_model_not_ready_fallback\",\n\t\t\t\t\treasons: [\n\t\t\t\t\t\t...current.decision.reasons,\n\t\t\t\t\t\t`Local model not ready (${readiness.reason}); escalated to ${escalated.tier}`,\n\t\t\t\t\t],\n\t\t\t\t\tmodel: this.deps.formatModel(escalated.model),\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\treturn current;\n\t}\n}\n"]}
|
|
@@ -145,6 +145,8 @@ export declare class MemoryController {
|
|
|
145
145
|
shouldAttemptRecall(text: string): boolean;
|
|
146
146
|
/** Legacy recall prefetch with the same hard-off and explicit external-egress policy as context retrieval. */
|
|
147
147
|
prefetchRecall(query: string): Promise<string>;
|
|
148
|
+
/** Bounded, read-only memory view for an explicitly authorized delegated worker. */
|
|
149
|
+
readMemoryForLane(query: string): Promise<string>;
|
|
148
150
|
/** R4: score whether the agent actually used an injected recall page, so the recall gate can adapt. */
|
|
149
151
|
recordRecallOutcome(recallText: string, queryText: string, responseText: string): void;
|
|
150
152
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"memory-controller.d.ts","sourceRoot":"","sources":["../../src/core/memory-controller.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Memory controller: the session's plug-and-play memory subsystem — the read-only OKF retrieval\n * provider, the bounded prompt-evidence surfacing pilot, cross-session recall effectiveness, and the\n * live {@link MemoryManager} (bundled file-store + transcript-recall providers plus any extension\n * contributions).\n *\n * Extracted verbatim from agent-session.ts (god-file decomposition). Owns the lazily-built OKF\n * provider, the latest retrieval/prompt-inclusion reports, the recreated-on-reload MemoryManager, the\n * recall {@link EffectivenessTracker}, and the extension-contributed pending providers. Everything\n * else it needs — settings, the current turn index, agent/workspace dirs, the session id, the\n * child-session flag, and the tool-registry refresh — is reached through narrow deps accessors rather\n * than the whole AgentSession.\n *\n * Context-transform boundary (deliberate): {@link runMemoryRetrieval} and\n * {@link maybeAppendMemoryEvidenceBlock} are invoked from the session's context transform as one-line\n * delegations. This controller deliberately imports no compaction/context-pipeline internals — it only\n * ever reads settings and builds the retrieval report + the bounded evidence block, so the transform\n * stays the single owner of the pass ordering.\n */\n\nimport { join } from \"node:path\";\nimport type { AgentMessage, CustomMessage } from \"@caupulican/pi-agent-core\";\nimport { collectCurrentWorkMemory } from \"./context/current-work-memory.ts\";\nimport { createFileStoreMemoryProvider } from \"./context/file-store-memory-provider.ts\";\nimport { shouldQueryLongTermMemory } from \"./context/long-term-memory-trigger.ts\";\nimport {\n\tdefaultMemoryPromptInclusionReport,\n\ttype MemoryPromptInclusionReport,\n\ttype MemoryRetrievalDiagnostics,\n\tsanitizeMemoryRetrievalReportForDiagnostics,\n} from \"./context/memory-diagnostics.ts\";\nimport { type MemoryPromptBudget, resolveMemoryPromptBudget } from \"./context/memory-prompt-budget.ts\";\nimport {\n\ttype MemoryProvider as ContextMemoryProvider,\n\tDEFAULT_EXTERNAL_MEMORY_EGRESS_POLICY,\n\tDEFAULT_LOCAL_MEMORY_EGRESS_POLICY,\n} from \"./context/memory-provider-contract.ts\";\nimport { type MemoryRetrievalReport, retrieveMemoryForContext } from \"./context/memory-retrieval.ts\";\nimport { composeTieredMemoryPromptBlock, type MemoryTierCandidate } from \"./context/memory-tier-composer.ts\";\nimport { createOkfMemoryProvider } from \"./context/okf-memory-provider.ts\";\nimport type { GoalState } from \"./goals/goal-state.ts\";\nimport { EffectivenessTracker } from \"./memory/effectiveness-tracker.ts\";\nimport { MemoryManager } from \"./memory/memory-manager.ts\";\nimport type { MemoryProvider } from \"./memory/memory-provider.ts\";\nimport { FileStoreProvider } from \"./memory/providers/file-store.ts\";\nimport { TranscriptRecallProvider } from \"./memory/providers/transcript-recall.ts\";\nimport { wrapUntrustedText } from \"./security/untrusted-boundary.ts\";\nimport type { SettingsManager } from \"./settings-manager.ts\";\n\n/**\n * Text of the most recent user message, or \"\" if there is none (e.g. goal-continuation\n * turns with no new user input). An empty query is still valid: standing user preferences may\n * surface from the file-store retrieval fallback when the static memory prompt cannot fit,\n * while long-term providers remain gated by shouldQueryLongTermMemory().\n */\nfunction latestUserMessageText(messages: AgentMessage[]): string {\n\tfor (let index = messages.length - 1; index >= 0; index--) {\n\t\tconst message = messages[index];\n\t\tif (message.role !== \"user\") continue;\n\t\tif (typeof message.content === \"string\") return message.content;\n\t\tconst parts: string[] = [];\n\t\tfor (const part of message.content) {\n\t\t\tif (part.type === \"text\") parts.push(part.text);\n\t\t}\n\t\treturn parts.join(\"\\n\");\n\t}\n\treturn \"\";\n}\n\nfunction emptyMemoryRetrievalReport(maxResults: number): MemoryRetrievalReport {\n\treturn { request: { query: \"\", maxResults }, providerReports: [], results: [], contextItems: [] };\n}\n\nconst ENABLED_EXTERNAL_MEMORY_EGRESS_POLICY = {\n\t...DEFAULT_EXTERNAL_MEMORY_EGRESS_POLICY,\n\tenabled: true,\n\tallowedScopes: DEFAULT_LOCAL_MEMORY_EGRESS_POLICY.allowedScopes,\n\tallowExternalEgress: true,\n} as const;\n\nexport interface MemoryControllerDeps {\n\t/** Memory-retrieval + prompt-inclusion settings (default-on gates for retrieval and surfacing). */\n\tgetSettingsManager(): SettingsManager;\n\t/** Current turn index, stamped into a retrieval request's `createdAtTurn`. */\n\tgetTurnIndex(): number;\n\t/** Agent root — the durable OKF memory docs live under `<agentDir>/okf-memory`. */\n\tgetAgentDir(): string;\n\t/** Workspace root, passed to provider initialization. */\n\tgetCwd(): string;\n\t/** This session's id, passed to provider initialization. */\n\tgetSessionId(): string;\n\t/** Child sessions gate durable memory writes; passed to provider initialization. */\n\tisChildSession(): boolean;\n\t/** Re-derive the tool registry after (re)init so the newly-surfaced memory tools take effect. */\n\trefreshToolRegistry(): void;\n\t/** Active model context window, used to cap prompt-visible memory. */\n\tgetContextWindow(): number | undefined;\n\t/** Latest active goal state, used for short-term current-work memory. */\n\tgetGoalState(): GoalState | undefined;\n}\n\n/** Extension-contributed memory state staged across an atomic runtime reload. */\nexport interface MemoryControllerReloadSnapshot {\n\tpendingMemoryProviders: MemoryProvider[];\n\tpendingContextMemoryProviders: ContextMemoryProvider[];\n\tmemoryOkfProvider: ContextMemoryProvider | undefined;\n\tfileStoreMemoryProvider: ContextMemoryProvider | undefined;\n}\n\nexport class MemoryController {\n\tprivate _memoryOkfProvider: ContextMemoryProvider | undefined = undefined;\n\tprivate _fileStoreMemoryProvider: ContextMemoryProvider | undefined = undefined;\n\tprivate _latestMemoryRetrievalReport: MemoryRetrievalReport | undefined = undefined;\n\tprivate _latestMemoryPromptInclusionReport: MemoryPromptInclusionReport | undefined = undefined;\n\t/** Plug-and-play memory subsystem. Recreated on each (re)initialize so reload is safe. */\n\tprivate _memoryManager: MemoryManager = new MemoryManager();\n\t/** R4: tracks whether injected recall is actually used, to adapt the recall gate. */\n\tprivate readonly _effectivenessTracker = new EffectivenessTracker();\n\t/** Memory providers registered by extensions via pi.registerMemoryProvider, applied on (re)init. */\n\tprivate _pendingMemoryProviders: MemoryProvider[] = [];\n\t/** Context-memory providers registered by extensions via pi.registerContextMemoryProvider. */\n\tprivate _pendingContextMemoryProviders: ContextMemoryProvider[] = [];\n\n\tprivate readonly deps: MemoryControllerDeps;\n\n\tconstructor(deps: MemoryControllerDeps) {\n\t\tthis.deps = deps;\n\t}\n\n\t/** The live memory manager. Callers reach prefetch / tool-definitions / markers / shutdown through it. */\n\tgetMemoryManager(): MemoryManager {\n\t\treturn this._memoryManager;\n\t}\n\n\t/**\n\t * Fixed path for this slice's local Pi OKF memory documents, shared across sessions\n\t * under this agentDir (not session-scoped, unlike tool-artifacts/context-gc, since OKF\n\t * memory represents durable cross-session knowledge, not a per-session capture). Not\n\t * yet user-configurable -- see the memory-retrieval settings doc comment.\n\t */\n\tprivate _memoryOkfDir(): string {\n\t\treturn join(this.deps.getAgentDir(), \"okf-memory\");\n\t}\n\n\t/**\n\t * Session-scoped, read-only local OKF memory provider. Lazily created ONLY when memory\n\t * retrieval is enabled (see `runMemoryRetrieval`) -- never force-created, so a session\n\t * with the setting off never touches `_memoryOkfDir()` at all (no directory access, no\n\t * creation; `createOkfMemoryProvider` itself never writes/mkdirs either way).\n\t */\n\tprivate _getMemoryOkfProvider(): ContextMemoryProvider {\n\t\tthis._memoryOkfProvider ??= createOkfMemoryProvider({ rootDir: this._memoryOkfDir() });\n\t\treturn this._memoryOkfProvider;\n\t}\n\n\tprivate _getFileStoreMemoryProvider(): ContextMemoryProvider {\n\t\tthis._fileStoreMemoryProvider ??= createFileStoreMemoryProvider({\n\t\t\tmemoryFilePath: join(this.deps.getAgentDir(), \"MEMORY.md\"),\n\t\t\tuserFilePath: join(this.deps.getAgentDir(), \"USER.md\"),\n\t\t});\n\t\treturn this._fileStoreMemoryProvider;\n\t}\n\n\tprivate _memoryBudget(configuredMaxResults: number) {\n\t\treturn resolveMemoryPromptBudget({\n\t\t\tcontextWindow: this.deps.getContextWindow(),\n\t\t\tconfiguredMaxResults,\n\t\t});\n\t}\n\n\tprivate _shouldQueryFileStoreFallback(budget: MemoryPromptBudget): boolean {\n\t\t// MEMORY.md/USER.md are already injected through the static file-store prompt on\n\t\t// normal windows, so querying them again would duplicate provider-visible memory.\n\t\t// In compact windows the static block can be omitted because it does not fit; only\n\t\t// then use the retrieval view to surface a few budget-pruned lines.\n\t\tif (!budget.enabled || !budget.compact) return false;\n\t\treturn this._memoryManager.buildSystemPromptBlock(budget).trim().length === 0;\n\t}\n\n\t/**\n\t * Observe-only local memory retrieval (see context/memory-retrieval.ts and\n\t * context/okf-memory-provider.ts): default-on, but settings-gated. When disabled,\n\t * never constructs built-in context-memory providers (no directory access under\n\t * `_memoryOkfDir()` at all) and returns an empty report -- fully fail-closed. When enabled,\n\t * queries local, read-only providers with the latest user message text (empty if there is\n\t * none, e.g. a goal-continuation turn) under `DEFAULT_LOCAL_MEMORY_EGRESS_POLICY`.\n\t * Retrieved items are only ever stored in the report; nothing here touches `messages`,\n\t * the transcript, or the provider-visible prompt. Never throws into a live turn: any\n\t * failure (including a provider search error) degrades to an empty report.\n\t */\n\tasync runMemoryRetrieval(messages: AgentMessage[]): Promise<MemoryRetrievalReport> {\n\t\ttry {\n\t\t\tconst settings = this.deps.getSettingsManager().getMemoryRetrievalSettings();\n\t\t\tif (!settings.enabled) {\n\t\t\t\tconst report = emptyMemoryRetrievalReport(settings.maxResults);\n\t\t\t\tthis._latestMemoryRetrievalReport = report;\n\t\t\t\treturn report;\n\t\t\t}\n\t\t\tconst query = latestUserMessageText(messages);\n\t\t\tconst budget = this._memoryBudget(settings.maxResults);\n\t\t\tconst currentWork = collectCurrentWorkMemory({ goalState: this.deps.getGoalState() });\n\t\t\tconst longTermDecision = shouldQueryLongTermMemory({\n\t\t\t\tlatestUserText: query,\n\t\t\t\tgoalState: this.deps.getGoalState(),\n\t\t\t\tbudget,\n\t\t\t\tcurrentWorkCandidateCount: currentWork.length,\n\t\t\t});\n\t\t\tconst providers = this._shouldQueryFileStoreFallback(budget) ? [this._getFileStoreMemoryProvider()] : [];\n\t\t\tif (longTermDecision.shouldQuery) {\n\t\t\t\tproviders.push(this._getMemoryOkfProvider(), ...this._pendingContextMemoryProviders);\n\t\t\t}\n\t\t\tconst maxResults = budget.enabled ? Math.min(settings.maxResults, budget.maxResults) : settings.maxResults;\n\t\t\tconst report = await retrieveMemoryForContext(\n\t\t\t\tproviders,\n\t\t\t\t{ query, maxResults },\n\t\t\t\t{\n\t\t\t\t\tcreatedAtTurn: this.deps.getTurnIndex(),\n\t\t\t\t\tmaxResults,\n\t\t\t\t\tdefaultLocalPolicy: DEFAULT_LOCAL_MEMORY_EGRESS_POLICY,\n\t\t\t\t\tdefaultExternalPolicy: settings.allowExternalEgress\n\t\t\t\t\t\t? ENABLED_EXTERNAL_MEMORY_EGRESS_POLICY\n\t\t\t\t\t\t: DEFAULT_EXTERNAL_MEMORY_EGRESS_POLICY,\n\t\t\t\t},\n\t\t\t);\n\t\t\tthis._latestMemoryRetrievalReport = report;\n\t\t\treturn report;\n\t\t} catch {\n\t\t\tconst report = emptyMemoryRetrievalReport(0);\n\t\t\tthis._latestMemoryRetrievalReport = report;\n\t\t\treturn report;\n\t\t}\n\t}\n\n\t/** Read-only inspection of the latest memory-retrieval report, for tests/debugging. */\n\tgetMemoryRetrievalReport(): MemoryRetrievalReport {\n\t\treturn this._latestMemoryRetrievalReport ?? emptyMemoryRetrievalReport(0);\n\t}\n\n\tprivate _candidateForContextItem(\n\t\titem: MemoryRetrievalReport[\"contextItems\"][number],\n\t): MemoryTierCandidate | undefined {\n\t\tconst summary = item.summary?.trim();\n\t\tif (!summary) return undefined;\n\t\tconst ref = item.primaryRef?.type === \"memory\" ? item.primaryRef.ref : undefined;\n\t\tconst sourceLabel =\n\t\t\tref?.providerId === \"pi-file-store\" && ref.kind === \"user_preference\"\n\t\t\t\t? \"rule:user\"\n\t\t\t\t: `memory:${ref?.providerId ?? item.source}`;\n\t\tconst tier = ref?.kind === \"user_preference\" ? \"standing\" : \"long_term\";\n\t\treturn {\n\t\t\tid: item.id,\n\t\t\ttier,\n\t\t\tsourceLabel,\n\t\t\tsummary,\n\t\t\tscore: 0.5,\n\t\t\tevidenceRefs: item.evidenceRefs,\n\t\t};\n\t}\n\n\tprivate _memoryCandidates(report: MemoryRetrievalReport): MemoryTierCandidate[] {\n\t\treturn [\n\t\t\t...collectCurrentWorkMemory({ goalState: this.deps.getGoalState() }),\n\t\t\t...report.contextItems.flatMap((item) => {\n\t\t\t\tconst candidate = this._candidateForContextItem(item);\n\t\t\t\treturn candidate === undefined ? [] : [candidate];\n\t\t\t}),\n\t\t];\n\t}\n\n\t/**\n\t * Bounded prompt-surfacing for local memory evidence (see context/memory-tier-composer.ts):\n\t * default-on, but gated on TWO settings (`enabled` AND `includeInPrompt`) plus at least one\n\t * current-work, standing, or retrieved memory candidate -- the first\n\t * two are belt-and-suspenders on top of the fact that `runMemoryRetrieval` already\n\t * leaves `contextItems` empty whenever `enabled` is false, regardless of\n\t * `includeInPrompt`. Reuses the `report` this pass's `runMemoryRetrieval` call already\n\t * computed -- never re-queries the provider here.\n\t *\n\t * Appends exactly one ephemeral `custom`/\"memory_evidence\" message wrapped by\n\t * `wrapUntrustedText` (the same nonce-fenced boundary + always-on system-prompt rule\n\t * used for other untrusted content) to the END of `messages`. This is purely additive\n\t * (never mutates an existing message) and purely transient: `messages` here is the\n\t * array about to be sent to the provider, not `this.agent.state.messages` or anything\n\t * persisted via `sessionManager` -- so the injected message can never reach the\n\t * transcript, regardless of how many times this pass runs.\n\t *\n\t * Also records a `MemoryPromptInclusionReport` (context/memory-diagnostics.ts) at each\n\t * branch below, for context_audit's diagnostic surface only -- this is pure bookkeeping\n\t * alongside the existing branches, not a new branch/condition: the messages returned\n\t * are unchanged by this recording.\n\t */\n\tmaybeAppendMemoryEvidenceBlock(messages: AgentMessage[], report: MemoryRetrievalReport): AgentMessage[] {\n\t\ttry {\n\t\t\tconst settings = this.deps.getSettingsManager().getMemoryRetrievalSettings();\n\t\t\tconst candidates = this._memoryCandidates(report);\n\t\t\tconst base = {\n\t\t\t\tenabled: settings.enabled,\n\t\t\t\tincludeInPrompt: settings.includeInPrompt,\n\t\t\t\tselectedItemCount: candidates.length,\n\t\t\t};\n\t\t\tif (!settings.enabled) {\n\t\t\t\tthis._latestMemoryPromptInclusionReport = {\n\t\t\t\t\t...base,\n\t\t\t\t\tstatus: \"disabled\",\n\t\t\t\t\tincludedCount: 0,\n\t\t\t\t\tomittedCount: 0,\n\t\t\t\t\tblockChars: 0,\n\t\t\t\t};\n\t\t\t\treturn messages;\n\t\t\t}\n\t\t\tif (!settings.includeInPrompt) {\n\t\t\t\tthis._latestMemoryPromptInclusionReport = {\n\t\t\t\t\t...base,\n\t\t\t\t\tstatus: \"include_disabled\",\n\t\t\t\t\tincludedCount: 0,\n\t\t\t\t\tomittedCount: 0,\n\t\t\t\t\tblockChars: 0,\n\t\t\t\t};\n\t\t\t\treturn messages;\n\t\t\t}\n\t\t\tif (candidates.length === 0) {\n\t\t\t\tthis._latestMemoryPromptInclusionReport = {\n\t\t\t\t\t...base,\n\t\t\t\t\tstatus: \"no_results\",\n\t\t\t\t\tincludedCount: 0,\n\t\t\t\t\tomittedCount: 0,\n\t\t\t\t\tblockChars: 0,\n\t\t\t\t};\n\t\t\t\treturn messages;\n\t\t\t}\n\n\t\t\tconst budget = this._memoryBudget(settings.maxResults);\n\t\t\tconst block = composeTieredMemoryPromptBlock(candidates, budget);\n\t\t\tif (!block.text) {\n\t\t\t\tthis._latestMemoryPromptInclusionReport = {\n\t\t\t\t\t...base,\n\t\t\t\t\tstatus: \"empty_block\",\n\t\t\t\t\tincludedCount: block.includedCount,\n\t\t\t\t\tomittedCount: block.omittedCount,\n\t\t\t\t\tblockChars: 0,\n\t\t\t\t};\n\t\t\t\treturn messages;\n\t\t\t}\n\n\t\t\tconst wrapped = wrapUntrustedText(block.text, \"memory:tiered\");\n\t\t\tconst evidenceMessage: CustomMessage = {\n\t\t\t\trole: \"custom\",\n\t\t\t\tcustomType: \"memory_evidence\",\n\t\t\t\tcontent: [{ type: \"text\", text: wrapped }],\n\t\t\t\tdisplay: false,\n\t\t\t\ttimestamp: Date.now(),\n\t\t\t};\n\t\t\tthis._latestMemoryPromptInclusionReport = {\n\t\t\t\t...base,\n\t\t\t\tstatus: \"included\",\n\t\t\t\tincludedCount: block.includedCount,\n\t\t\t\tomittedCount: block.omittedCount,\n\t\t\t\tblockChars: wrapped.length,\n\t\t\t\tsourceLabel: \"memory:tiered\",\n\t\t\t};\n\t\t\treturn [...messages, evidenceMessage];\n\t\t} catch {\n\t\t\t// `base` may not exist yet if the throw happened before it was computed (e.g.\n\t\t\t// settings access or `report.contextItems` itself threw), so this branch cannot\n\t\t\t// rely on it -- fall back to safe, fixed defaults rather than risk referencing\n\t\t\t// a partially-evaluated value.\n\t\t\tthis._latestMemoryPromptInclusionReport = {\n\t\t\t\tenabled: false,\n\t\t\t\tincludeInPrompt: false,\n\t\t\t\tselectedItemCount: 0,\n\t\t\t\tstatus: \"failed\",\n\t\t\t\tincludedCount: 0,\n\t\t\t\tomittedCount: 0,\n\t\t\t\tblockChars: 0,\n\t\t\t};\n\t\t\treturn messages;\n\t\t}\n\t}\n\n\t/** Read-only inspection of the latest memory-prompt-inclusion decision, for tests/debugging and context_audit. */\n\tgetMemoryPromptInclusionReport(): MemoryPromptInclusionReport {\n\t\treturn this._latestMemoryPromptInclusionReport ?? defaultMemoryPromptInclusionReport();\n\t}\n\n\t/**\n\t * Combines the already-stored, no-arg latest reports (never re-queries the provider or\n\t * touches the OKF directory) into the safe, allow-list-projected shape context_audit\n\t * exposes. See context/memory-diagnostics.ts for why this projection is allow-list\n\t * based rather than a spread-then-delete of the raw report.\n\t */\n\tgetMemoryAuditDiagnostics(): {\n\t\tretrieval: MemoryRetrievalDiagnostics;\n\t\tpromptInclusion: MemoryPromptInclusionReport;\n\t} {\n\t\tconst settings = this.deps.getSettingsManager().getMemoryRetrievalSettings();\n\t\treturn {\n\t\t\tretrieval: sanitizeMemoryRetrievalReportForDiagnostics(this.getMemoryRetrievalReport(), settings),\n\t\t\tpromptInclusion: this.getMemoryPromptInclusionReport(),\n\t\t};\n\t}\n\n\t/**\n\t * Zero-I/O gate for cross-session recall (R3): skip trivial turns (short acks, slash commands) so\n\t * recall only runs when it could plausibly help. The provider's similarity cutoff is the real\n\t * filter — this just avoids the index query on turns that obviously don't warrant it.\n\t */\n\tshouldAttemptRecall(text: string): boolean {\n\t\tif (!this.deps.getSettingsManager().getMemoryRetrievalSettings().enabled) return false;\n\t\tconst t = text.trim();\n\t\tif (t.length < 12 || t.startsWith(\"/\")) return false;\n\t\tconst words = t.split(/\\s+/).filter((w) => w.length >= 3);\n\t\t// R4 adaptive gate: if recall has rarely been used lately (enough samples to trust the signal),\n\t\t// raise the bar so we only recall on clearly substantial turns — and relax it again once recall\n\t\t// starts paying off. Never fully disabled, so the loop can recover.\n\t\tconst recallRarelyUseful =\n\t\t\tthis._effectivenessTracker.sampleCount >= 5 && this._effectivenessTracker.usefulLately() < 0.15;\n\t\treturn words.length >= (recallRarelyUseful ? 6 : 3);\n\t}\n\n\t/** Legacy recall prefetch with the same hard-off and explicit external-egress policy as context retrieval. */\n\tasync prefetchRecall(query: string): Promise<string> {\n\t\tconst settings = this.deps.getSettingsManager().getMemoryRetrievalSettings();\n\t\tif (!settings.enabled) return \"\";\n\t\treturn this._memoryManager.prefetch(query, {\n\t\t\texternalEgressPolicy: settings.allowExternalEgress\n\t\t\t\t? ENABLED_EXTERNAL_MEMORY_EGRESS_POLICY\n\t\t\t\t: DEFAULT_EXTERNAL_MEMORY_EGRESS_POLICY,\n\t\t});\n\t}\n\n\t/** R4: score whether the agent actually used an injected recall page, so the recall gate can adapt. */\n\trecordRecallOutcome(recallText: string, queryText: string, responseText: string): void {\n\t\tthis._effectivenessTracker.recordRecallOutcome(recallText, queryText, responseText);\n\t}\n\n\t/**\n\t * (Re)build the memory subsystem: a fresh MemoryManager (reload-safe), register the bundled\n\t * file-store + any extension-contributed providers, initialize, then surface the memory tools and\n\t * the frozen system-prompt block. Best-effort: never throws into the session lifecycle.\n\t */\n\tasync initialize(): Promise<void> {\n\t\ttry {\n\t\t\t// Release the previous generation's providers (locks/handles) before recreating, so a\n\t\t\t// reload does not orphan the old MemoryManager. No-op on first init / for file-store.\n\t\t\tawait this._memoryManager.shutdownAll().catch(() => {});\n\t\t\tconst manager = new MemoryManager();\n\t\t\tmanager.registerProvider(new FileStoreProvider());\n\t\t\t// Bundled read-only cross-session recall (R3): indexes past-session transcripts and answers\n\t\t\t// prefetch() with a <memory_context> page. Never writes.\n\t\t\tmanager.registerProvider(new TranscriptRecallProvider());\n\t\t\tfor (const provider of this._pendingMemoryProviders) {\n\t\t\t\ttry {\n\t\t\t\t\tmanager.registerProvider(provider);\n\t\t\t\t} catch {\n\t\t\t\t\t// Duplicate name or reserved-tool collision — skip this provider, keep the rest.\n\t\t\t\t}\n\t\t\t}\n\t\t\tthis._memoryManager = manager;\n\t\t\tawait manager.initializeAll(this.deps.getSessionId(), {\n\t\t\t\tagentDir: this.deps.getAgentDir(),\n\t\t\t\tcwd: this.deps.getCwd(),\n\t\t\t\tisChildSession: this.deps.isChildSession(),\n\t\t\t});\n\t\t\t// Surface memory tools + the frozen memory block now that providers are initialized.\n\t\t\t// refreshToolRegistry() ends in setActiveToolsByName(), which rebuilds AND assigns the\n\t\t\t// system prompt (including the memory block), so no explicit _rebuildSystemPrompt is needed.\n\t\t\tthis.deps.refreshToolRegistry();\n\t\t} catch (error) {\n\t\t\tconsole.error(\"Memory subsystem init failed:\", error instanceof Error ? error.message : String(error));\n\t\t}\n\t}\n\n\t/** Register a memory provider contributed by an extension; applied on the next memory (re)init. */\n\tregisterMemoryProvider(provider: MemoryProvider): void {\n\t\tif (!this._pendingMemoryProviders.some((p) => p.name === provider.name)) {\n\t\t\tthis._pendingMemoryProviders.push(provider);\n\t\t}\n\t}\n\n\t/** Register a retrieval-style context memory provider contributed by an extension. */\n\tregisterContextMemoryProvider(provider: ContextMemoryProvider): void {\n\t\tif (!this._pendingContextMemoryProviders.some((p) => p.id === provider.id)) {\n\t\t\tthis._pendingContextMemoryProviders.push(provider);\n\t\t}\n\t}\n\n\tcreateReloadSnapshot(): MemoryControllerReloadSnapshot {\n\t\treturn {\n\t\t\tpendingMemoryProviders: [...this._pendingMemoryProviders],\n\t\t\tpendingContextMemoryProviders: [...this._pendingContextMemoryProviders],\n\t\t\tmemoryOkfProvider: this._memoryOkfProvider,\n\t\t\tfileStoreMemoryProvider: this._fileStoreMemoryProvider,\n\t\t};\n\t}\n\n\trestoreReloadSnapshot(snapshot: MemoryControllerReloadSnapshot): void {\n\t\tthis._pendingMemoryProviders = [...snapshot.pendingMemoryProviders];\n\t\tthis._pendingContextMemoryProviders = [...snapshot.pendingContextMemoryProviders];\n\t\tthis._memoryOkfProvider = snapshot.memoryOkfProvider;\n\t\tthis._fileStoreMemoryProvider = snapshot.fileStoreMemoryProvider;\n\t}\n\n\t/** Reload starts memory providers fresh; loaded extensions re-register before the next `initialize()`. */\n\tclearPendingProviders(): void {\n\t\tthis._pendingMemoryProviders = [];\n\t\tthis._pendingContextMemoryProviders = [];\n\t\tthis._memoryOkfProvider = undefined;\n\t\tthis._fileStoreMemoryProvider = undefined;\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"memory-controller.d.ts","sourceRoot":"","sources":["../../src/core/memory-controller.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Memory controller: the session's plug-and-play memory subsystem — the read-only OKF retrieval\n * provider, the bounded prompt-evidence surfacing pilot, cross-session recall effectiveness, and the\n * live {@link MemoryManager} (bundled file-store + transcript-recall providers plus any extension\n * contributions).\n *\n * Extracted verbatim from agent-session.ts (god-file decomposition). Owns the lazily-built OKF\n * provider, the latest retrieval/prompt-inclusion reports, the recreated-on-reload MemoryManager, the\n * recall {@link EffectivenessTracker}, and the extension-contributed pending providers. Everything\n * else it needs — settings, the current turn index, agent/workspace dirs, the session id, the\n * child-session flag, and the tool-registry refresh — is reached through narrow deps accessors rather\n * than the whole AgentSession.\n *\n * Context-transform boundary (deliberate): {@link runMemoryRetrieval} and\n * {@link maybeAppendMemoryEvidenceBlock} are invoked from the session's context transform as one-line\n * delegations. This controller deliberately imports no compaction/context-pipeline internals — it only\n * ever reads settings and builds the retrieval report + the bounded evidence block, so the transform\n * stays the single owner of the pass ordering.\n */\n\nimport { join } from \"node:path\";\nimport type { AgentMessage, CustomMessage } from \"@caupulican/pi-agent-core\";\nimport { collectCurrentWorkMemory } from \"./context/current-work-memory.ts\";\nimport { createFileStoreMemoryProvider } from \"./context/file-store-memory-provider.ts\";\nimport { shouldQueryLongTermMemory } from \"./context/long-term-memory-trigger.ts\";\nimport {\n\tdefaultMemoryPromptInclusionReport,\n\ttype MemoryPromptInclusionReport,\n\ttype MemoryRetrievalDiagnostics,\n\tsanitizeMemoryRetrievalReportForDiagnostics,\n} from \"./context/memory-diagnostics.ts\";\nimport { type MemoryPromptBudget, resolveMemoryPromptBudget } from \"./context/memory-prompt-budget.ts\";\nimport {\n\ttype MemoryProvider as ContextMemoryProvider,\n\tDEFAULT_EXTERNAL_MEMORY_EGRESS_POLICY,\n\tDEFAULT_LOCAL_MEMORY_EGRESS_POLICY,\n} from \"./context/memory-provider-contract.ts\";\nimport { type MemoryRetrievalReport, retrieveMemoryForContext } from \"./context/memory-retrieval.ts\";\nimport { composeTieredMemoryPromptBlock, type MemoryTierCandidate } from \"./context/memory-tier-composer.ts\";\nimport { createOkfMemoryProvider } from \"./context/okf-memory-provider.ts\";\nimport type { GoalState } from \"./goals/goal-state.ts\";\nimport { EffectivenessTracker } from \"./memory/effectiveness-tracker.ts\";\nimport { MemoryManager } from \"./memory/memory-manager.ts\";\nimport type { MemoryProvider } from \"./memory/memory-provider.ts\";\nimport { FileStoreProvider } from \"./memory/providers/file-store.ts\";\nimport { TranscriptRecallProvider } from \"./memory/providers/transcript-recall.ts\";\nimport { wrapUntrustedText } from \"./security/untrusted-boundary.ts\";\nimport type { SettingsManager } from \"./settings-manager.ts\";\n\n/**\n * Text of the most recent user message, or \"\" if there is none (e.g. goal-continuation\n * turns with no new user input). An empty query is still valid: standing user preferences may\n * surface from the file-store retrieval fallback when the static memory prompt cannot fit,\n * while long-term providers remain gated by shouldQueryLongTermMemory().\n */\nfunction latestUserMessageText(messages: AgentMessage[]): string {\n\tfor (let index = messages.length - 1; index >= 0; index--) {\n\t\tconst message = messages[index];\n\t\tif (message.role !== \"user\") continue;\n\t\tif (typeof message.content === \"string\") return message.content;\n\t\tconst parts: string[] = [];\n\t\tfor (const part of message.content) {\n\t\t\tif (part.type === \"text\") parts.push(part.text);\n\t\t}\n\t\treturn parts.join(\"\\n\");\n\t}\n\treturn \"\";\n}\n\nfunction emptyMemoryRetrievalReport(maxResults: number): MemoryRetrievalReport {\n\treturn { request: { query: \"\", maxResults }, providerReports: [], results: [], contextItems: [] };\n}\n\nconst ENABLED_EXTERNAL_MEMORY_EGRESS_POLICY = {\n\t...DEFAULT_EXTERNAL_MEMORY_EGRESS_POLICY,\n\tenabled: true,\n\tallowedScopes: DEFAULT_LOCAL_MEMORY_EGRESS_POLICY.allowedScopes,\n\tallowExternalEgress: true,\n} as const;\n\nexport interface MemoryControllerDeps {\n\t/** Memory-retrieval + prompt-inclusion settings (default-on gates for retrieval and surfacing). */\n\tgetSettingsManager(): SettingsManager;\n\t/** Current turn index, stamped into a retrieval request's `createdAtTurn`. */\n\tgetTurnIndex(): number;\n\t/** Agent root — the durable OKF memory docs live under `<agentDir>/okf-memory`. */\n\tgetAgentDir(): string;\n\t/** Workspace root, passed to provider initialization. */\n\tgetCwd(): string;\n\t/** This session's id, passed to provider initialization. */\n\tgetSessionId(): string;\n\t/** Child sessions gate durable memory writes; passed to provider initialization. */\n\tisChildSession(): boolean;\n\t/** Re-derive the tool registry after (re)init so the newly-surfaced memory tools take effect. */\n\trefreshToolRegistry(): void;\n\t/** Active model context window, used to cap prompt-visible memory. */\n\tgetContextWindow(): number | undefined;\n\t/** Latest active goal state, used for short-term current-work memory. */\n\tgetGoalState(): GoalState | undefined;\n}\n\n/** Extension-contributed memory state staged across an atomic runtime reload. */\nexport interface MemoryControllerReloadSnapshot {\n\tpendingMemoryProviders: MemoryProvider[];\n\tpendingContextMemoryProviders: ContextMemoryProvider[];\n\tmemoryOkfProvider: ContextMemoryProvider | undefined;\n\tfileStoreMemoryProvider: ContextMemoryProvider | undefined;\n}\n\nexport class MemoryController {\n\tprivate _memoryOkfProvider: ContextMemoryProvider | undefined = undefined;\n\tprivate _fileStoreMemoryProvider: ContextMemoryProvider | undefined = undefined;\n\tprivate _latestMemoryRetrievalReport: MemoryRetrievalReport | undefined = undefined;\n\tprivate _latestMemoryPromptInclusionReport: MemoryPromptInclusionReport | undefined = undefined;\n\t/** Plug-and-play memory subsystem. Recreated on each (re)initialize so reload is safe. */\n\tprivate _memoryManager: MemoryManager = new MemoryManager();\n\t/** R4: tracks whether injected recall is actually used, to adapt the recall gate. */\n\tprivate readonly _effectivenessTracker = new EffectivenessTracker();\n\t/** Memory providers registered by extensions via pi.registerMemoryProvider, applied on (re)init. */\n\tprivate _pendingMemoryProviders: MemoryProvider[] = [];\n\t/** Context-memory providers registered by extensions via pi.registerContextMemoryProvider. */\n\tprivate _pendingContextMemoryProviders: ContextMemoryProvider[] = [];\n\n\tprivate readonly deps: MemoryControllerDeps;\n\n\tconstructor(deps: MemoryControllerDeps) {\n\t\tthis.deps = deps;\n\t}\n\n\t/** The live memory manager. Callers reach prefetch / tool-definitions / markers / shutdown through it. */\n\tgetMemoryManager(): MemoryManager {\n\t\treturn this._memoryManager;\n\t}\n\n\t/**\n\t * Fixed path for this slice's local Pi OKF memory documents, shared across sessions\n\t * under this agentDir (not session-scoped, unlike tool-artifacts/context-gc, since OKF\n\t * memory represents durable cross-session knowledge, not a per-session capture). Not\n\t * yet user-configurable -- see the memory-retrieval settings doc comment.\n\t */\n\tprivate _memoryOkfDir(): string {\n\t\treturn join(this.deps.getAgentDir(), \"okf-memory\");\n\t}\n\n\t/**\n\t * Session-scoped, read-only local OKF memory provider. Lazily created ONLY when memory\n\t * retrieval is enabled (see `runMemoryRetrieval`) -- never force-created, so a session\n\t * with the setting off never touches `_memoryOkfDir()` at all (no directory access, no\n\t * creation; `createOkfMemoryProvider` itself never writes/mkdirs either way).\n\t */\n\tprivate _getMemoryOkfProvider(): ContextMemoryProvider {\n\t\tthis._memoryOkfProvider ??= createOkfMemoryProvider({ rootDir: this._memoryOkfDir() });\n\t\treturn this._memoryOkfProvider;\n\t}\n\n\tprivate _getFileStoreMemoryProvider(): ContextMemoryProvider {\n\t\tthis._fileStoreMemoryProvider ??= createFileStoreMemoryProvider({\n\t\t\tmemoryFilePath: join(this.deps.getAgentDir(), \"MEMORY.md\"),\n\t\t\tuserFilePath: join(this.deps.getAgentDir(), \"USER.md\"),\n\t\t});\n\t\treturn this._fileStoreMemoryProvider;\n\t}\n\n\tprivate _memoryBudget(configuredMaxResults: number) {\n\t\treturn resolveMemoryPromptBudget({\n\t\t\tcontextWindow: this.deps.getContextWindow(),\n\t\t\tconfiguredMaxResults,\n\t\t});\n\t}\n\n\tprivate _shouldQueryFileStoreFallback(budget: MemoryPromptBudget): boolean {\n\t\t// MEMORY.md/USER.md are already injected through the static file-store prompt on\n\t\t// normal windows, so querying them again would duplicate provider-visible memory.\n\t\t// In compact windows the static block can be omitted because it does not fit; only\n\t\t// then use the retrieval view to surface a few budget-pruned lines.\n\t\tif (!budget.enabled || !budget.compact) return false;\n\t\treturn this._memoryManager.buildSystemPromptBlock(budget).trim().length === 0;\n\t}\n\n\t/**\n\t * Observe-only local memory retrieval (see context/memory-retrieval.ts and\n\t * context/okf-memory-provider.ts): default-on, but settings-gated. When disabled,\n\t * never constructs built-in context-memory providers (no directory access under\n\t * `_memoryOkfDir()` at all) and returns an empty report -- fully fail-closed. When enabled,\n\t * queries local, read-only providers with the latest user message text (empty if there is\n\t * none, e.g. a goal-continuation turn) under `DEFAULT_LOCAL_MEMORY_EGRESS_POLICY`.\n\t * Retrieved items are only ever stored in the report; nothing here touches `messages`,\n\t * the transcript, or the provider-visible prompt. Never throws into a live turn: any\n\t * failure (including a provider search error) degrades to an empty report.\n\t */\n\tasync runMemoryRetrieval(messages: AgentMessage[]): Promise<MemoryRetrievalReport> {\n\t\ttry {\n\t\t\tconst settings = this.deps.getSettingsManager().getMemoryRetrievalSettings();\n\t\t\tif (!settings.enabled) {\n\t\t\t\tconst report = emptyMemoryRetrievalReport(settings.maxResults);\n\t\t\t\tthis._latestMemoryRetrievalReport = report;\n\t\t\t\treturn report;\n\t\t\t}\n\t\t\tconst query = latestUserMessageText(messages);\n\t\t\tconst budget = this._memoryBudget(settings.maxResults);\n\t\t\tconst currentWork = collectCurrentWorkMemory({ goalState: this.deps.getGoalState() });\n\t\t\tconst longTermDecision = shouldQueryLongTermMemory({\n\t\t\t\tlatestUserText: query,\n\t\t\t\tgoalState: this.deps.getGoalState(),\n\t\t\t\tbudget,\n\t\t\t\tcurrentWorkCandidateCount: currentWork.length,\n\t\t\t});\n\t\t\tconst providers = this._shouldQueryFileStoreFallback(budget) ? [this._getFileStoreMemoryProvider()] : [];\n\t\t\tif (longTermDecision.shouldQuery) {\n\t\t\t\tproviders.push(this._getMemoryOkfProvider(), ...this._pendingContextMemoryProviders);\n\t\t\t}\n\t\t\tconst maxResults = budget.enabled ? Math.min(settings.maxResults, budget.maxResults) : settings.maxResults;\n\t\t\tconst report = await retrieveMemoryForContext(\n\t\t\t\tproviders,\n\t\t\t\t{ query, maxResults },\n\t\t\t\t{\n\t\t\t\t\tcreatedAtTurn: this.deps.getTurnIndex(),\n\t\t\t\t\tmaxResults,\n\t\t\t\t\tdefaultLocalPolicy: DEFAULT_LOCAL_MEMORY_EGRESS_POLICY,\n\t\t\t\t\tdefaultExternalPolicy: settings.allowExternalEgress\n\t\t\t\t\t\t? ENABLED_EXTERNAL_MEMORY_EGRESS_POLICY\n\t\t\t\t\t\t: DEFAULT_EXTERNAL_MEMORY_EGRESS_POLICY,\n\t\t\t\t},\n\t\t\t);\n\t\t\tthis._latestMemoryRetrievalReport = report;\n\t\t\treturn report;\n\t\t} catch {\n\t\t\tconst report = emptyMemoryRetrievalReport(0);\n\t\t\tthis._latestMemoryRetrievalReport = report;\n\t\t\treturn report;\n\t\t}\n\t}\n\n\t/** Read-only inspection of the latest memory-retrieval report, for tests/debugging. */\n\tgetMemoryRetrievalReport(): MemoryRetrievalReport {\n\t\treturn this._latestMemoryRetrievalReport ?? emptyMemoryRetrievalReport(0);\n\t}\n\n\tprivate _candidateForContextItem(\n\t\titem: MemoryRetrievalReport[\"contextItems\"][number],\n\t): MemoryTierCandidate | undefined {\n\t\tconst summary = item.summary?.trim();\n\t\tif (!summary) return undefined;\n\t\tconst ref = item.primaryRef?.type === \"memory\" ? item.primaryRef.ref : undefined;\n\t\tconst sourceLabel =\n\t\t\tref?.providerId === \"pi-file-store\" && ref.kind === \"user_preference\"\n\t\t\t\t? \"rule:user\"\n\t\t\t\t: `memory:${ref?.providerId ?? item.source}`;\n\t\tconst tier = ref?.kind === \"user_preference\" ? \"standing\" : \"long_term\";\n\t\treturn {\n\t\t\tid: item.id,\n\t\t\ttier,\n\t\t\tsourceLabel,\n\t\t\tsummary,\n\t\t\tscore: 0.5,\n\t\t\tevidenceRefs: item.evidenceRefs,\n\t\t};\n\t}\n\n\tprivate _memoryCandidates(report: MemoryRetrievalReport): MemoryTierCandidate[] {\n\t\treturn [\n\t\t\t...collectCurrentWorkMemory({ goalState: this.deps.getGoalState() }),\n\t\t\t...report.contextItems.flatMap((item) => {\n\t\t\t\tconst candidate = this._candidateForContextItem(item);\n\t\t\t\treturn candidate === undefined ? [] : [candidate];\n\t\t\t}),\n\t\t];\n\t}\n\n\t/**\n\t * Bounded prompt-surfacing for local memory evidence (see context/memory-tier-composer.ts):\n\t * default-on, but gated on TWO settings (`enabled` AND `includeInPrompt`) plus at least one\n\t * current-work, standing, or retrieved memory candidate -- the first\n\t * two are belt-and-suspenders on top of the fact that `runMemoryRetrieval` already\n\t * leaves `contextItems` empty whenever `enabled` is false, regardless of\n\t * `includeInPrompt`. Reuses the `report` this pass's `runMemoryRetrieval` call already\n\t * computed -- never re-queries the provider here.\n\t *\n\t * Appends exactly one ephemeral `custom`/\"memory_evidence\" message wrapped by\n\t * `wrapUntrustedText` (the same nonce-fenced boundary + always-on system-prompt rule\n\t * used for other untrusted content) to the END of `messages`. This is purely additive\n\t * (never mutates an existing message) and purely transient: `messages` here is the\n\t * array about to be sent to the provider, not `this.agent.state.messages` or anything\n\t * persisted via `sessionManager` -- so the injected message can never reach the\n\t * transcript, regardless of how many times this pass runs.\n\t *\n\t * Also records a `MemoryPromptInclusionReport` (context/memory-diagnostics.ts) at each\n\t * branch below, for context_audit's diagnostic surface only -- this is pure bookkeeping\n\t * alongside the existing branches, not a new branch/condition: the messages returned\n\t * are unchanged by this recording.\n\t */\n\tmaybeAppendMemoryEvidenceBlock(messages: AgentMessage[], report: MemoryRetrievalReport): AgentMessage[] {\n\t\ttry {\n\t\t\tconst settings = this.deps.getSettingsManager().getMemoryRetrievalSettings();\n\t\t\tconst candidates = this._memoryCandidates(report);\n\t\t\tconst base = {\n\t\t\t\tenabled: settings.enabled,\n\t\t\t\tincludeInPrompt: settings.includeInPrompt,\n\t\t\t\tselectedItemCount: candidates.length,\n\t\t\t};\n\t\t\tif (!settings.enabled) {\n\t\t\t\tthis._latestMemoryPromptInclusionReport = {\n\t\t\t\t\t...base,\n\t\t\t\t\tstatus: \"disabled\",\n\t\t\t\t\tincludedCount: 0,\n\t\t\t\t\tomittedCount: 0,\n\t\t\t\t\tblockChars: 0,\n\t\t\t\t};\n\t\t\t\treturn messages;\n\t\t\t}\n\t\t\tif (!settings.includeInPrompt) {\n\t\t\t\tthis._latestMemoryPromptInclusionReport = {\n\t\t\t\t\t...base,\n\t\t\t\t\tstatus: \"include_disabled\",\n\t\t\t\t\tincludedCount: 0,\n\t\t\t\t\tomittedCount: 0,\n\t\t\t\t\tblockChars: 0,\n\t\t\t\t};\n\t\t\t\treturn messages;\n\t\t\t}\n\t\t\tif (candidates.length === 0) {\n\t\t\t\tthis._latestMemoryPromptInclusionReport = {\n\t\t\t\t\t...base,\n\t\t\t\t\tstatus: \"no_results\",\n\t\t\t\t\tincludedCount: 0,\n\t\t\t\t\tomittedCount: 0,\n\t\t\t\t\tblockChars: 0,\n\t\t\t\t};\n\t\t\t\treturn messages;\n\t\t\t}\n\n\t\t\tconst budget = this._memoryBudget(settings.maxResults);\n\t\t\tconst block = composeTieredMemoryPromptBlock(candidates, budget);\n\t\t\tif (!block.text) {\n\t\t\t\tthis._latestMemoryPromptInclusionReport = {\n\t\t\t\t\t...base,\n\t\t\t\t\tstatus: \"empty_block\",\n\t\t\t\t\tincludedCount: block.includedCount,\n\t\t\t\t\tomittedCount: block.omittedCount,\n\t\t\t\t\tblockChars: 0,\n\t\t\t\t};\n\t\t\t\treturn messages;\n\t\t\t}\n\n\t\t\tconst wrapped = wrapUntrustedText(block.text, \"memory:tiered\");\n\t\t\tconst evidenceMessage: CustomMessage = {\n\t\t\t\trole: \"custom\",\n\t\t\t\tcustomType: \"memory_evidence\",\n\t\t\t\tcontent: [{ type: \"text\", text: wrapped }],\n\t\t\t\tdisplay: false,\n\t\t\t\ttimestamp: Date.now(),\n\t\t\t};\n\t\t\tthis._latestMemoryPromptInclusionReport = {\n\t\t\t\t...base,\n\t\t\t\tstatus: \"included\",\n\t\t\t\tincludedCount: block.includedCount,\n\t\t\t\tomittedCount: block.omittedCount,\n\t\t\t\tblockChars: wrapped.length,\n\t\t\t\tsourceLabel: \"memory:tiered\",\n\t\t\t};\n\t\t\treturn [...messages, evidenceMessage];\n\t\t} catch {\n\t\t\t// `base` may not exist yet if the throw happened before it was computed (e.g.\n\t\t\t// settings access or `report.contextItems` itself threw), so this branch cannot\n\t\t\t// rely on it -- fall back to safe, fixed defaults rather than risk referencing\n\t\t\t// a partially-evaluated value.\n\t\t\tthis._latestMemoryPromptInclusionReport = {\n\t\t\t\tenabled: false,\n\t\t\t\tincludeInPrompt: false,\n\t\t\t\tselectedItemCount: 0,\n\t\t\t\tstatus: \"failed\",\n\t\t\t\tincludedCount: 0,\n\t\t\t\tomittedCount: 0,\n\t\t\t\tblockChars: 0,\n\t\t\t};\n\t\t\treturn messages;\n\t\t}\n\t}\n\n\t/** Read-only inspection of the latest memory-prompt-inclusion decision, for tests/debugging and context_audit. */\n\tgetMemoryPromptInclusionReport(): MemoryPromptInclusionReport {\n\t\treturn this._latestMemoryPromptInclusionReport ?? defaultMemoryPromptInclusionReport();\n\t}\n\n\t/**\n\t * Combines the already-stored, no-arg latest reports (never re-queries the provider or\n\t * touches the OKF directory) into the safe, allow-list-projected shape context_audit\n\t * exposes. See context/memory-diagnostics.ts for why this projection is allow-list\n\t * based rather than a spread-then-delete of the raw report.\n\t */\n\tgetMemoryAuditDiagnostics(): {\n\t\tretrieval: MemoryRetrievalDiagnostics;\n\t\tpromptInclusion: MemoryPromptInclusionReport;\n\t} {\n\t\tconst settings = this.deps.getSettingsManager().getMemoryRetrievalSettings();\n\t\treturn {\n\t\t\tretrieval: sanitizeMemoryRetrievalReportForDiagnostics(this.getMemoryRetrievalReport(), settings),\n\t\t\tpromptInclusion: this.getMemoryPromptInclusionReport(),\n\t\t};\n\t}\n\n\t/**\n\t * Zero-I/O gate for cross-session recall (R3): skip trivial turns (short acks, slash commands) so\n\t * recall only runs when it could plausibly help. The provider's similarity cutoff is the real\n\t * filter — this just avoids the index query on turns that obviously don't warrant it.\n\t */\n\tshouldAttemptRecall(text: string): boolean {\n\t\tif (!this.deps.getSettingsManager().getMemoryRetrievalSettings().enabled) return false;\n\t\tconst t = text.trim();\n\t\tif (t.length < 12 || t.startsWith(\"/\")) return false;\n\t\tconst words = t.split(/\\s+/).filter((w) => w.length >= 3);\n\t\t// R4 adaptive gate: if recall has rarely been used lately (enough samples to trust the signal),\n\t\t// raise the bar so we only recall on clearly substantial turns — and relax it again once recall\n\t\t// starts paying off. Never fully disabled, so the loop can recover.\n\t\tconst recallRarelyUseful =\n\t\t\tthis._effectivenessTracker.sampleCount >= 5 && this._effectivenessTracker.usefulLately() < 0.15;\n\t\treturn words.length >= (recallRarelyUseful ? 6 : 3);\n\t}\n\n\t/** Legacy recall prefetch with the same hard-off and explicit external-egress policy as context retrieval. */\n\tasync prefetchRecall(query: string): Promise<string> {\n\t\tconst settings = this.deps.getSettingsManager().getMemoryRetrievalSettings();\n\t\tif (!settings.enabled) return \"\";\n\t\treturn this._memoryManager.prefetch(query, {\n\t\t\texternalEgressPolicy: settings.allowExternalEgress\n\t\t\t\t? ENABLED_EXTERNAL_MEMORY_EGRESS_POLICY\n\t\t\t\t: DEFAULT_EXTERNAL_MEMORY_EGRESS_POLICY,\n\t\t});\n\t}\n\n\t/** Bounded, read-only memory view for an explicitly authorized delegated worker. */\n\tasync readMemoryForLane(query: string): Promise<string> {\n\t\tconst settings = this.deps.getSettingsManager().getMemoryRetrievalSettings();\n\t\tif (!settings.enabled) return \"Memory retrieval is disabled by policy.\";\n\t\tconst budget = this._memoryBudget(settings.maxResults);\n\t\tconst staticBlock = this._memoryManager\n\t\t\t.buildSystemPromptBlockFresh(budget)\n\t\t\t.replace(\n\t\t\t\t\"[System Note: Below is a snapshot of your persistent memory. You can update these using the 'memory' tool.]\",\n\t\t\t\t\"[Read-only snapshot for a delegated worker.]\",\n\t\t\t);\n\t\tconst recalled = await this.prefetchRecall(query);\n\t\tconst combined = [staticBlock, recalled]\n\t\t\t.filter((part) => part.trim().length > 0)\n\t\t\t.join(\"\\n\\n\")\n\t\t\t.slice(0, 8000);\n\t\treturn combined.length > 0\n\t\t\t? wrapUntrustedText(combined, \"worker-memory\")\n\t\t\t: \"No relevant standing memory was found.\";\n\t}\n\n\t/** R4: score whether the agent actually used an injected recall page, so the recall gate can adapt. */\n\trecordRecallOutcome(recallText: string, queryText: string, responseText: string): void {\n\t\tthis._effectivenessTracker.recordRecallOutcome(recallText, queryText, responseText);\n\t}\n\n\t/**\n\t * (Re)build the memory subsystem: a fresh MemoryManager (reload-safe), register the bundled\n\t * file-store + any extension-contributed providers, initialize, then surface the memory tools and\n\t * the frozen system-prompt block. Best-effort: never throws into the session lifecycle.\n\t */\n\tasync initialize(): Promise<void> {\n\t\ttry {\n\t\t\t// Release the previous generation's providers (locks/handles) before recreating, so a\n\t\t\t// reload does not orphan the old MemoryManager. No-op on first init / for file-store.\n\t\t\tawait this._memoryManager.shutdownAll().catch(() => {});\n\t\t\tconst manager = new MemoryManager();\n\t\t\tmanager.registerProvider(new FileStoreProvider());\n\t\t\t// Bundled read-only cross-session recall (R3): indexes past-session transcripts and answers\n\t\t\t// prefetch() with a <memory_context> page. Never writes.\n\t\t\tmanager.registerProvider(new TranscriptRecallProvider());\n\t\t\tfor (const provider of this._pendingMemoryProviders) {\n\t\t\t\ttry {\n\t\t\t\t\tmanager.registerProvider(provider);\n\t\t\t\t} catch {\n\t\t\t\t\t// Duplicate name or reserved-tool collision — skip this provider, keep the rest.\n\t\t\t\t}\n\t\t\t}\n\t\t\tthis._memoryManager = manager;\n\t\t\tawait manager.initializeAll(this.deps.getSessionId(), {\n\t\t\t\tagentDir: this.deps.getAgentDir(),\n\t\t\t\tcwd: this.deps.getCwd(),\n\t\t\t\tisChildSession: this.deps.isChildSession(),\n\t\t\t});\n\t\t\t// Surface memory tools + the frozen memory block now that providers are initialized.\n\t\t\t// refreshToolRegistry() ends in setActiveToolsByName(), which rebuilds AND assigns the\n\t\t\t// system prompt (including the memory block), so no explicit _rebuildSystemPrompt is needed.\n\t\t\tthis.deps.refreshToolRegistry();\n\t\t} catch (error) {\n\t\t\tconsole.error(\"Memory subsystem init failed:\", error instanceof Error ? error.message : String(error));\n\t\t}\n\t}\n\n\t/** Register a memory provider contributed by an extension; applied on the next memory (re)init. */\n\tregisterMemoryProvider(provider: MemoryProvider): void {\n\t\tif (!this._pendingMemoryProviders.some((p) => p.name === provider.name)) {\n\t\t\tthis._pendingMemoryProviders.push(provider);\n\t\t}\n\t}\n\n\t/** Register a retrieval-style context memory provider contributed by an extension. */\n\tregisterContextMemoryProvider(provider: ContextMemoryProvider): void {\n\t\tif (!this._pendingContextMemoryProviders.some((p) => p.id === provider.id)) {\n\t\t\tthis._pendingContextMemoryProviders.push(provider);\n\t\t}\n\t}\n\n\tcreateReloadSnapshot(): MemoryControllerReloadSnapshot {\n\t\treturn {\n\t\t\tpendingMemoryProviders: [...this._pendingMemoryProviders],\n\t\t\tpendingContextMemoryProviders: [...this._pendingContextMemoryProviders],\n\t\t\tmemoryOkfProvider: this._memoryOkfProvider,\n\t\t\tfileStoreMemoryProvider: this._fileStoreMemoryProvider,\n\t\t};\n\t}\n\n\trestoreReloadSnapshot(snapshot: MemoryControllerReloadSnapshot): void {\n\t\tthis._pendingMemoryProviders = [...snapshot.pendingMemoryProviders];\n\t\tthis._pendingContextMemoryProviders = [...snapshot.pendingContextMemoryProviders];\n\t\tthis._memoryOkfProvider = snapshot.memoryOkfProvider;\n\t\tthis._fileStoreMemoryProvider = snapshot.fileStoreMemoryProvider;\n\t}\n\n\t/** Reload starts memory providers fresh; loaded extensions re-register before the next `initialize()`. */\n\tclearPendingProviders(): void {\n\t\tthis._pendingMemoryProviders = [];\n\t\tthis._pendingContextMemoryProviders = [];\n\t\tthis._memoryOkfProvider = undefined;\n\t\tthis._fileStoreMemoryProvider = undefined;\n\t}\n}\n"]}
|
|
@@ -360,6 +360,24 @@ export class MemoryController {
|
|
|
360
360
|
: DEFAULT_EXTERNAL_MEMORY_EGRESS_POLICY,
|
|
361
361
|
});
|
|
362
362
|
}
|
|
363
|
+
/** Bounded, read-only memory view for an explicitly authorized delegated worker. */
|
|
364
|
+
async readMemoryForLane(query) {
|
|
365
|
+
const settings = this.deps.getSettingsManager().getMemoryRetrievalSettings();
|
|
366
|
+
if (!settings.enabled)
|
|
367
|
+
return "Memory retrieval is disabled by policy.";
|
|
368
|
+
const budget = this._memoryBudget(settings.maxResults);
|
|
369
|
+
const staticBlock = this._memoryManager
|
|
370
|
+
.buildSystemPromptBlockFresh(budget)
|
|
371
|
+
.replace("[System Note: Below is a snapshot of your persistent memory. You can update these using the 'memory' tool.]", "[Read-only snapshot for a delegated worker.]");
|
|
372
|
+
const recalled = await this.prefetchRecall(query);
|
|
373
|
+
const combined = [staticBlock, recalled]
|
|
374
|
+
.filter((part) => part.trim().length > 0)
|
|
375
|
+
.join("\n\n")
|
|
376
|
+
.slice(0, 8000);
|
|
377
|
+
return combined.length > 0
|
|
378
|
+
? wrapUntrustedText(combined, "worker-memory")
|
|
379
|
+
: "No relevant standing memory was found.";
|
|
380
|
+
}
|
|
363
381
|
/** R4: score whether the agent actually used an injected recall page, so the recall gate can adapt. */
|
|
364
382
|
recordRecallOutcome(recallText, queryText, responseText) {
|
|
365
383
|
this._effectivenessTracker.recordRecallOutcome(recallText, queryText, responseText);
|