@plurnk/plurnk-service 1.7.0 → 1.9.0
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/.env.defaults +49 -42
- package/INSTALL.md +48 -11
- package/README.md +12 -2
- package/SPEC.md +1166 -475
- package/digest-sql/channel-dispositions/dispositions.sql +15 -0
- package/digest-sql/channel-state/state.sql +6 -0
- package/digest-sql/curation/curation.sql +5 -0
- package/digest-sql/derivations/derivations.sql +4 -0
- package/digest-sql/embeddings/embeddings.sql +4 -0
- package/digest-sql/entry-dispositions/dispositions.sql +15 -0
- package/digest-sql/entry-state/state.sql +6 -0
- package/digest-sql/token-counts/token-counts.sql +2 -0
- package/dist/Paths.d.ts +0 -4
- package/dist/Paths.d.ts.map +1 -1
- package/dist/Paths.js +1 -21
- package/dist/Paths.js.map +1 -1
- package/dist/build-info.json +1 -1
- package/dist/content/read-projector.d.ts.map +1 -1
- package/dist/content/read-projector.js +4 -3
- package/dist/content/read-projector.js.map +1 -1
- package/dist/core/BudgetReadout.d.ts.map +1 -1
- package/dist/core/BudgetReadout.js +12 -22
- package/dist/core/BudgetReadout.js.map +1 -1
- package/dist/core/ChannelWrite.d.ts +8 -3
- package/dist/core/ChannelWrite.d.ts.map +1 -1
- package/dist/core/ChannelWrite.js +40 -8
- package/dist/core/ChannelWrite.js.map +1 -1
- package/dist/core/ChannelWrite.sql +19 -5
- package/dist/core/CoreSchemeServices.d.ts +7 -3
- package/dist/core/CoreSchemeServices.d.ts.map +1 -1
- package/dist/core/CoreSchemeServices.js +8 -1
- package/dist/core/CoreSchemeServices.js.map +1 -1
- package/dist/core/Dispatcher.d.ts +15 -6
- package/dist/core/Dispatcher.d.ts.map +1 -1
- package/dist/core/Dispatcher.js +352 -306
- package/dist/core/Dispatcher.js.map +1 -1
- package/dist/core/Engine.d.ts +22 -10
- package/dist/core/Engine.d.ts.map +1 -1
- package/dist/core/Engine.js +86 -46
- package/dist/core/Engine.js.map +1 -1
- package/dist/core/Engine.sql +202 -177
- package/dist/core/EntryAddressBinding.d.ts +32 -0
- package/dist/core/EntryAddressBinding.d.ts.map +1 -0
- package/dist/core/EntryAddressBinding.js +93 -0
- package/dist/core/EntryAddressBinding.js.map +1 -0
- package/dist/core/ExecutorRegistry.d.ts +12 -6
- package/dist/core/ExecutorRegistry.d.ts.map +1 -1
- package/dist/core/ExecutorRegistry.js +34 -31
- package/dist/core/ExecutorRegistry.js.map +1 -1
- package/dist/core/HostPaths.d.ts +29 -0
- package/dist/core/HostPaths.d.ts.map +1 -0
- package/dist/core/HostPaths.js +74 -0
- package/dist/core/HostPaths.js.map +1 -0
- package/dist/core/LegacyHome.d.ts +10 -0
- package/dist/core/LegacyHome.d.ts.map +1 -0
- package/dist/core/LegacyHome.js +170 -0
- package/dist/core/LegacyHome.js.map +1 -0
- package/dist/core/LogBody.d.ts +1 -1
- package/dist/core/LogBody.d.ts.map +1 -1
- package/dist/core/LogBody.js +21 -3
- package/dist/core/LogBody.js.map +1 -1
- package/dist/core/LogEntryProjection.js +1 -1
- package/dist/core/LogEntryProjection.js.map +1 -1
- package/dist/core/LogVisibility.d.ts +28 -0
- package/dist/core/LogVisibility.d.ts.map +1 -0
- package/dist/core/LogVisibility.js +215 -0
- package/dist/core/LogVisibility.js.map +1 -0
- package/dist/core/LoopLifecycle.d.ts +1 -0
- package/dist/core/LoopLifecycle.d.ts.map +1 -1
- package/dist/core/LoopLifecycle.js +8 -0
- package/dist/core/LoopLifecycle.js.map +1 -1
- package/dist/core/LoopLifecycle.sql +10 -0
- package/dist/core/OperatorConfig.d.ts +6 -0
- package/dist/core/OperatorConfig.d.ts.map +1 -0
- package/dist/core/OperatorConfig.js +95 -0
- package/dist/core/OperatorConfig.js.map +1 -0
- package/dist/core/OverflowTurn.d.ts +11 -0
- package/dist/core/OverflowTurn.d.ts.map +1 -0
- package/dist/core/OverflowTurn.js +81 -0
- package/dist/core/OverflowTurn.js.map +1 -0
- package/dist/core/Owner.js +3 -3
- package/dist/core/Owner.js.map +1 -1
- package/dist/core/PacketBuilder.d.ts +7 -15
- package/dist/core/PacketBuilder.d.ts.map +1 -1
- package/dist/core/PacketBuilder.js +64 -127
- package/dist/core/PacketBuilder.js.map +1 -1
- package/dist/core/ProblemLog.js +1 -1
- package/dist/core/ProblemLog.js.map +1 -1
- package/dist/core/ProposalLifecycle.d.ts +4 -1
- package/dist/core/ProposalLifecycle.d.ts.map +1 -1
- package/dist/core/ProposalLifecycle.js +86 -47
- package/dist/core/ProposalLifecycle.js.map +1 -1
- package/dist/core/ProviderInstantiate.d.ts +9 -5
- package/dist/core/ProviderInstantiate.d.ts.map +1 -1
- package/dist/core/ProviderInstantiate.js +99 -104
- package/dist/core/ProviderInstantiate.js.map +1 -1
- package/dist/core/ReasoningEvent.d.ts +13 -0
- package/dist/core/ReasoningEvent.d.ts.map +1 -0
- package/dist/core/ReasoningEvent.js +6 -0
- package/dist/core/ReasoningEvent.js.map +1 -0
- package/dist/core/ResourceMutations.d.ts +15 -19
- package/dist/core/ResourceMutations.d.ts.map +1 -1
- package/dist/core/ResourceMutations.js +118 -41
- package/dist/core/ResourceMutations.js.map +1 -1
- package/dist/core/SchemeRegistry.d.ts +12 -12
- package/dist/core/SchemeRegistry.d.ts.map +1 -1
- package/dist/core/SchemeRegistry.js +58 -57
- package/dist/core/SchemeRegistry.js.map +1 -1
- package/dist/core/ToolResources.d.ts +19 -0
- package/dist/core/ToolResources.d.ts.map +1 -0
- package/dist/core/ToolResources.js +123 -0
- package/dist/core/ToolResources.js.map +1 -0
- package/dist/core/Turn.d.ts +27 -0
- package/dist/core/Turn.d.ts.map +1 -0
- package/dist/core/Turn.js +43 -0
- package/dist/core/Turn.js.map +1 -0
- package/dist/core/Turn.sql +72 -0
- package/dist/core/TurnOps.d.ts +7 -0
- package/dist/core/TurnOps.d.ts.map +1 -0
- package/dist/core/TurnOps.js +73 -0
- package/dist/core/TurnOps.js.map +1 -0
- package/dist/core/TurnRunner.d.ts +56 -5
- package/dist/core/TurnRunner.d.ts.map +1 -1
- package/dist/core/TurnRunner.js +1163 -929
- package/dist/core/TurnRunner.js.map +1 -1
- package/dist/core/WorkerName.d.ts +2 -1
- package/dist/core/WorkerName.d.ts.map +1 -1
- package/dist/core/WorkerName.js +2 -1
- package/dist/core/WorkerName.js.map +1 -1
- package/dist/core/WorkerName.sql +17 -2
- package/dist/core/WorkspaceGate.js +1 -1
- package/dist/core/WorkspaceGate.js.map +1 -1
- package/dist/core/caps/CapsResolve.d.ts +2 -2
- package/dist/core/caps/CapsResolve.d.ts.map +1 -1
- package/dist/core/caps/CapsResolve.js +5 -7
- package/dist/core/caps/CapsResolve.js.map +1 -1
- package/dist/core/caps/DbChannelCaps.d.ts +1 -1
- package/dist/core/caps/DbChannelCaps.d.ts.map +1 -1
- package/dist/core/caps/DbChannelCaps.js +13 -11
- package/dist/core/caps/DbChannelCaps.js.map +1 -1
- package/dist/core/caps/DbEntryCaps.d.ts +5 -5
- package/dist/core/caps/DbEntryCaps.d.ts.map +1 -1
- package/dist/core/caps/DbEntryCaps.js +27 -30
- package/dist/core/caps/DbEntryCaps.js.map +1 -1
- package/dist/core/caps/DbNotifyCaps.d.ts +1 -1
- package/dist/core/caps/DbNotifyCaps.d.ts.map +1 -1
- package/dist/core/caps/DbNotifyCaps.js +5 -3
- package/dist/core/caps/DbNotifyCaps.js.map +1 -1
- package/dist/core/caps/DbSubscriptionCaps.d.ts +3 -3
- package/dist/core/caps/DbSubscriptionCaps.d.ts.map +1 -1
- package/dist/core/caps/DbSubscriptionCaps.js +16 -26
- package/dist/core/caps/DbSubscriptionCaps.js.map +1 -1
- package/dist/core/caps/SchemeCtxImpl.d.ts +6 -4
- package/dist/core/caps/SchemeCtxImpl.d.ts.map +1 -1
- package/dist/core/caps/SchemeCtxImpl.js +37 -5
- package/dist/core/caps/SchemeCtxImpl.js.map +1 -1
- package/dist/core/env-defaults.d.ts.map +1 -1
- package/dist/core/env-defaults.js +7 -5
- package/dist/core/env-defaults.js.map +1 -1
- package/dist/core/file-creation-policy.d.ts +9 -0
- package/dist/core/file-creation-policy.d.ts.map +1 -0
- package/dist/core/file-creation-policy.js +25 -0
- package/dist/core/file-creation-policy.js.map +1 -0
- package/dist/core/fork.d.ts +4 -1
- package/dist/core/fork.d.ts.map +1 -1
- package/dist/core/fork.js +39 -16
- package/dist/core/fork.js.map +1 -1
- package/dist/core/fork.sql +83 -40
- package/dist/core/git-env.d.ts +0 -1
- package/dist/core/git-env.d.ts.map +1 -1
- package/dist/core/git-env.js +0 -3
- package/dist/core/git-env.js.map +1 -1
- package/dist/core/git-membership.d.ts +19 -1
- package/dist/core/git-membership.d.ts.map +1 -1
- package/dist/core/git-membership.js +233 -74
- package/dist/core/git-membership.js.map +1 -1
- package/dist/core/git-state.d.ts.map +1 -1
- package/dist/core/git-state.js +13 -14
- package/dist/core/git-state.js.map +1 -1
- package/dist/core/operation-target-groups.d.ts +3 -0
- package/dist/core/operation-target-groups.d.ts.map +1 -0
- package/dist/core/operation-target-groups.js +53 -0
- package/dist/core/operation-target-groups.js.map +1 -0
- package/dist/core/packet-inject.d.ts +0 -1
- package/dist/core/packet-inject.d.ts.map +1 -1
- package/dist/core/packet-inject.js +6 -13
- package/dist/core/packet-inject.js.map +1 -1
- package/dist/core/packet-wire.d.ts +4 -0
- package/dist/core/packet-wire.d.ts.map +1 -1
- package/dist/core/packet-wire.js +226 -53
- package/dist/core/packet-wire.js.map +1 -1
- package/dist/core/plurnk-uri.d.ts +14 -3
- package/dist/core/plurnk-uri.d.ts.map +1 -1
- package/dist/core/plurnk-uri.js +62 -24
- package/dist/core/plurnk-uri.js.map +1 -1
- package/dist/core/scheme-types.d.ts +1 -0
- package/dist/core/scheme-types.d.ts.map +1 -1
- package/dist/core/teaching.d.ts.map +1 -1
- package/dist/core/teaching.js +4 -5
- package/dist/core/teaching.js.map +1 -1
- package/dist/core/worker-ops.sql +10 -6
- package/dist/core/worker-settings.d.ts +10 -0
- package/dist/core/worker-settings.d.ts.map +1 -0
- package/dist/core/worker-settings.js +32 -0
- package/dist/core/worker-settings.js.map +1 -0
- package/dist/core/workspace-settings.d.ts +2 -11
- package/dist/core/workspace-settings.d.ts.map +1 -1
- package/dist/core/workspace-settings.js +7 -47
- package/dist/core/workspace-settings.js.map +1 -1
- package/dist/digest/Digest.d.ts.map +1 -1
- package/dist/digest/Digest.js +164 -92
- package/dist/digest/Digest.js.map +1 -1
- package/dist/digest/digest.sql +9 -3
- package/dist/observe/genai.d.ts +9 -0
- package/dist/observe/genai.d.ts.map +1 -0
- package/dist/observe/genai.js +31 -0
- package/dist/observe/genai.js.map +1 -0
- package/dist/observe/spans.d.ts +3 -3
- package/dist/observe/spans.d.ts.map +1 -1
- package/dist/observe/spans.js +20 -5
- package/dist/observe/spans.js.map +1 -1
- package/dist/schemes/EffectPolicy.d.ts +3 -0
- package/dist/schemes/EffectPolicy.d.ts.map +1 -1
- package/dist/schemes/EffectPolicy.js +38 -6
- package/dist/schemes/EffectPolicy.js.map +1 -1
- package/dist/schemes/Exec.d.ts +3 -2
- package/dist/schemes/Exec.d.ts.map +1 -1
- package/dist/schemes/Exec.js +109 -34
- package/dist/schemes/Exec.js.map +1 -1
- package/dist/schemes/ExecOutputScheme.d.ts.map +1 -1
- package/dist/schemes/ExecOutputScheme.js +5 -4
- package/dist/schemes/ExecOutputScheme.js.map +1 -1
- package/dist/schemes/File.d.ts.map +1 -1
- package/dist/schemes/File.js +170 -80
- package/dist/schemes/File.js.map +1 -1
- package/dist/schemes/Log.d.ts +6 -2
- package/dist/schemes/Log.d.ts.map +1 -1
- package/dist/schemes/Log.js +73 -22
- package/dist/schemes/Log.js.map +1 -1
- package/dist/schemes/Log.sql +25 -7
- package/dist/schemes/Prompt.d.ts +2 -3
- package/dist/schemes/Prompt.d.ts.map +1 -1
- package/dist/schemes/Prompt.js +5 -9
- package/dist/schemes/Prompt.js.map +1 -1
- package/dist/schemes/QuestionTool.d.ts +10 -0
- package/dist/schemes/QuestionTool.d.ts.map +1 -0
- package/dist/schemes/QuestionTool.js +81 -0
- package/dist/schemes/QuestionTool.js.map +1 -0
- package/dist/schemes/Skill.d.ts.map +1 -1
- package/dist/schemes/Skill.js +6 -1
- package/dist/schemes/Skill.js.map +1 -1
- package/dist/schemes/Worker.d.ts.map +1 -1
- package/dist/schemes/Worker.js +97 -34
- package/dist/schemes/Worker.js.map +1 -1
- package/dist/schemes/_entry-crud.d.ts +5 -5
- package/dist/schemes/_entry-crud.d.ts.map +1 -1
- package/dist/schemes/_entry-crud.js +22 -17
- package/dist/schemes/_entry-crud.js.map +1 -1
- package/dist/schemes/_entry-crud.sql +77 -24
- package/dist/schemes/_entry-find.d.ts +8 -5
- package/dist/schemes/_entry-find.d.ts.map +1 -1
- package/dist/schemes/_entry-find.js +121 -40
- package/dist/schemes/_entry-find.js.map +1 -1
- package/dist/schemes/_entry-find.sql +16 -11
- package/dist/schemes/_entry-graph.sql +2 -6
- package/dist/schemes/_entry-manifest.d.ts +2 -2
- package/dist/schemes/_entry-manifest.d.ts.map +1 -1
- package/dist/schemes/_entry-manifest.js +18 -6
- package/dist/schemes/_entry-manifest.js.map +1 -1
- package/dist/schemes/_entry-ops.d.ts +6 -5
- package/dist/schemes/_entry-ops.d.ts.map +1 -1
- package/dist/schemes/_entry-ops.js +39 -46
- package/dist/schemes/_entry-ops.js.map +1 -1
- package/dist/schemes/_entry-ops.sql +9 -6
- package/dist/schemes/_entry-send.d.ts +2 -2
- package/dist/schemes/_entry-send.d.ts.map +1 -1
- package/dist/schemes/_entry-send.js +16 -14
- package/dist/schemes/_entry-send.js.map +1 -1
- package/dist/schemes/_search-index.d.ts.map +1 -1
- package/dist/schemes/_search-index.js +39 -25
- package/dist/schemes/_search-index.js.map +1 -1
- package/dist/server/Daemon.d.ts +98 -15
- package/dist/server/Daemon.d.ts.map +1 -1
- package/dist/server/Daemon.js +864 -348
- package/dist/server/Daemon.js.map +1 -1
- package/dist/server/DaemonModule.d.ts +84 -19
- package/dist/server/DaemonModule.d.ts.map +1 -1
- package/dist/server/DrainSupervisor.d.ts +15 -10
- package/dist/server/DrainSupervisor.d.ts.map +1 -1
- package/dist/server/DrainSupervisor.js +70 -19
- package/dist/server/DrainSupervisor.js.map +1 -1
- package/dist/server/Functionality.d.ts +44 -0
- package/dist/server/Functionality.d.ts.map +1 -0
- package/dist/server/Functionality.js +422 -0
- package/dist/server/Functionality.js.map +1 -0
- package/dist/server/FunctionalityManager.d.ts +22 -0
- package/dist/server/FunctionalityManager.d.ts.map +1 -0
- package/dist/server/FunctionalityManager.js +121 -0
- package/dist/server/FunctionalityManager.js.map +1 -0
- package/dist/server/SkillsFunctionality.d.ts +61 -0
- package/dist/server/SkillsFunctionality.d.ts.map +1 -0
- package/dist/server/SkillsFunctionality.js +449 -0
- package/dist/server/SkillsFunctionality.js.map +1 -0
- package/dist/server/WorkerCapabilities.d.ts +21 -0
- package/dist/server/WorkerCapabilities.d.ts.map +1 -0
- package/dist/server/WorkerCapabilities.js +214 -0
- package/dist/server/WorkerCapabilities.js.map +1 -0
- package/dist/server/WorkerResidency.d.ts +40 -0
- package/dist/server/WorkerResidency.d.ts.map +1 -0
- package/dist/server/WorkerResidency.js +283 -0
- package/dist/server/WorkerResidency.js.map +1 -0
- package/dist/server/client-input.d.ts +6 -2
- package/dist/server/client-input.d.ts.map +1 -1
- package/dist/server/client-input.js +72 -32
- package/dist/server/client-input.js.map +1 -1
- package/dist/server/dispatch-as-plurnk.d.ts +2 -2
- package/dist/server/dispatch-as-plurnk.d.ts.map +1 -1
- package/dist/server/dispatch-as-plurnk.js +109 -23
- package/dist/server/dispatch-as-plurnk.js.map +1 -1
- package/dist/server/drain.sql +33 -13
- package/dist/server/envelope.d.ts +7 -2
- package/dist/server/envelope.d.ts.map +1 -1
- package/dist/server/envelope.js +12 -6
- package/dist/server/envelope.js.map +1 -1
- package/dist/server/envelope.sql +36 -4
- package/dist/server/lifecycle-recovery.sql +24 -17
- package/dist/server/logEntry.d.ts +2 -1
- package/dist/server/logEntry.d.ts.map +1 -1
- package/dist/server/logEntry.js +3 -2
- package/dist/server/logEntry.js.map +1 -1
- package/dist/server/logEntry.sql +7 -0
- package/dist/server/loop-model.d.ts +2 -2
- package/dist/server/loop-model.d.ts.map +1 -1
- package/dist/server/loop-model.js +23 -33
- package/dist/server/loop-model.js.map +1 -1
- package/dist/server/loopDocs.d.ts +2 -1
- package/dist/server/loopDocs.d.ts.map +1 -1
- package/dist/server/loopDocs.js +88 -21
- package/dist/server/loopDocs.js.map +1 -1
- package/dist/server/loopDocs.sql +14 -8
- package/dist/server/model-catalog.d.ts +3 -0
- package/dist/server/model-catalog.d.ts.map +1 -0
- package/dist/server/model-catalog.js +83 -0
- package/dist/server/model-catalog.js.map +1 -0
- package/dist/server/model-route.d.ts +7 -0
- package/dist/server/model-route.d.ts.map +1 -0
- package/dist/server/model-route.js +47 -0
- package/dist/server/model-route.js.map +1 -0
- package/dist/server/module-discovery.d.ts +13 -0
- package/dist/server/module-discovery.d.ts.map +1 -0
- package/dist/server/module-discovery.js +103 -0
- package/dist/server/module-discovery.js.map +1 -0
- package/dist/server/seam-entry-read.sql +9 -7
- package/dist/server/seam-log-read.sql +1 -1
- package/dist/server/seam-loop.sql +10 -1
- package/dist/server/seam-proposal-list.sql +4 -2
- package/dist/server/worker-capabilities.sql +19 -0
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +193 -124
- package/dist/service.js.map +1 -1
- package/migrations/001_schema.sql +905 -196
- package/package.json +40 -38
- package/dist/core/ExecutableTools.d.ts +0 -11
- package/dist/core/ExecutableTools.d.ts.map +0 -1
- package/dist/core/ExecutableTools.js +0 -49
- package/dist/core/ExecutableTools.js.map +0 -1
- package/dist/core/JournalTurn.d.ts +0 -9
- package/dist/core/JournalTurn.d.ts.map +0 -1
- package/dist/core/JournalTurn.js +0 -14
- package/dist/core/JournalTurn.js.map +0 -1
- package/dist/core/JournalTurn.sql +0 -10
- package/dist/core/git-iso.d.ts +0 -13
- package/dist/core/git-iso.d.ts.map +0 -1
- package/dist/core/git-iso.js +0 -267
- package/dist/core/git-iso.js.map +0 -1
- package/dist/core/search-gate.d.ts +0 -17
- package/dist/core/search-gate.d.ts.map +0 -1
- package/dist/core/search-gate.js +0 -77
- package/dist/core/search-gate.js.map +0 -1
- package/dist/digest/Digest.sql +0 -1
- package/dist/server/workspace-capabilities.sql +0 -19
package/dist/server/Daemon.js
CHANGED
|
@@ -8,27 +8,36 @@ import ExecutorRegistry from "../core/ExecutorRegistry.js";
|
|
|
8
8
|
import SchemeRegistry from "../core/SchemeRegistry.js";
|
|
9
9
|
import { Mimetypes } from "@plurnk/plurnk-mimetypes";
|
|
10
10
|
import { RuntimeDeclaration } from "@plurnk/plurnk-execs";
|
|
11
|
+
import { projectModelRoute, routeForSpec, specForRoute } from "./model-route.js";
|
|
12
|
+
import { discoverDaemonModules } from "./module-discovery.js";
|
|
13
|
+
import WorkerSettingsReader from "../core/worker-settings.js";
|
|
14
|
+
import EffectPolicy from "../schemes/EffectPolicy.js";
|
|
15
|
+
import QuestionTool, { questionRuntimeDecl } from "../schemes/QuestionTool.js";
|
|
11
16
|
import DrainSupervisor from "./DrainSupervisor.js";
|
|
12
17
|
import { parsePath, Validator, } from "@plurnk/plurnk-contracts";
|
|
13
18
|
import LogEntry from "./logEntry.js";
|
|
14
19
|
import Envelope from "./envelope.js";
|
|
15
20
|
import ClientInput from "./client-input.js";
|
|
16
|
-
import
|
|
17
|
-
import
|
|
21
|
+
import Turn from "../core/Turn.js";
|
|
22
|
+
import SkillsFunctionality from "./SkillsFunctionality.js";
|
|
18
23
|
import GitMembership from "../core/git-membership.js";
|
|
19
24
|
import Fork from "../core/fork.js";
|
|
20
25
|
import WorkerName from "../core/WorkerName.js";
|
|
26
|
+
import WorkerControlAddress from "../core/WorkerControlAddress.js";
|
|
21
27
|
import LoopLifecycle from "../core/LoopLifecycle.js";
|
|
22
28
|
import { promptLoopPrefix } from "../core/plurnk-uri.js";
|
|
23
29
|
import { contentWeight } from "../core/content-weight.js";
|
|
24
|
-
import { parseAliasesFromEnv,
|
|
30
|
+
import { parseAliasesFromEnv, resolveActiveRoute, UnsupportedReasoningPolicyError, } from "@plurnk/plurnk-providers";
|
|
25
31
|
import ProviderInstantiate from "../core/ProviderInstantiate.js";
|
|
26
|
-
import {
|
|
32
|
+
import { resolveLoopRoute } from "./loop-model.js";
|
|
27
33
|
import LoopFlagsReader from "../core/LoopFlagsReader.js";
|
|
28
34
|
import Results, { OperationFailureError } from "../core/results.js";
|
|
29
35
|
import WorkspaceGate from "../core/WorkspaceGate.js";
|
|
36
|
+
import WorkerResidency from "./WorkerResidency.js";
|
|
30
37
|
import BranchBatches from "./BranchBatches.js";
|
|
38
|
+
import Functionality from "./Functionality.js";
|
|
31
39
|
import { observed, observedSync } from "../observe/spans.js";
|
|
40
|
+
import { listModelCatalog } from "./model-catalog.js";
|
|
32
41
|
const clientActionFailure = (error) => {
|
|
33
42
|
if (error instanceof OperationFailureError)
|
|
34
43
|
return error.result;
|
|
@@ -40,6 +49,9 @@ const clientActionFailure = (error) => {
|
|
|
40
49
|
};
|
|
41
50
|
const daemonFailure = (owner, code, status, detail, extensions = {}) => new OperationFailureError(Results.failure(owner, code, status, detail, {}, extensions));
|
|
42
51
|
const entryReadResult = (result) => Validator.assertEntryReadResult(result);
|
|
52
|
+
const modelRouteLabel = (route) => route.alias === undefined
|
|
53
|
+
? `model route '${route.provider}/${route.model}'`
|
|
54
|
+
: `provider alias '${route.alias}' (${route.provider}/${route.model})`;
|
|
43
55
|
export default class Daemon {
|
|
44
56
|
#db;
|
|
45
57
|
#engine;
|
|
@@ -54,16 +66,17 @@ export default class Daemon {
|
|
|
54
66
|
#nodeModulesPath;
|
|
55
67
|
#discoveryCwd;
|
|
56
68
|
#started = false; // {§module-lifecycle}: one discovery/module boot; no listener
|
|
57
|
-
#capabilitiesPublished = false;
|
|
58
69
|
#modules = [];
|
|
59
70
|
#moduleClosers = [];
|
|
60
71
|
#moduleActions = new Map();
|
|
61
|
-
#
|
|
72
|
+
#residency;
|
|
73
|
+
#functionality;
|
|
74
|
+
#skills;
|
|
62
75
|
// {§methods-event-subscribe} — the broadcast's in-process event source. A transport
|
|
63
76
|
// module (plurnk-agui) subscribes and fans out to its OWN clients; core emits, never owns
|
|
64
77
|
// client transport or connection state.
|
|
65
78
|
#eventSubscribers = new Set();
|
|
66
|
-
constructor({ db, schemes, mimetypes, provider, nodeModulesPath, }) {
|
|
79
|
+
constructor({ db, schemes, mimetypes, provider, nodeModulesPath, skills, }) {
|
|
67
80
|
this.#db = db;
|
|
68
81
|
this.#lifecycle = new LoopLifecycle(db);
|
|
69
82
|
this.#schemes = schemes ?? new SchemeRegistry();
|
|
@@ -83,7 +96,7 @@ export default class Daemon {
|
|
|
83
96
|
defaultMimetype: "text/markdown",
|
|
84
97
|
discoverOptions: { cwd: this.#discoveryCwd },
|
|
85
98
|
});
|
|
86
|
-
const bootSpec =
|
|
99
|
+
const bootSpec = resolveActiveRoute();
|
|
87
100
|
if (this.#provider !== null && bootSpec !== null) {
|
|
88
101
|
ProviderInstantiate.registerInstance(this.#provider, bootSpec);
|
|
89
102
|
}
|
|
@@ -94,6 +107,26 @@ export default class Daemon {
|
|
|
94
107
|
});
|
|
95
108
|
return row !== undefined;
|
|
96
109
|
});
|
|
110
|
+
// {§module-worker-residency} — residency, activation/cooling, and
|
|
111
|
+
// capability replacement live in their one owner; the Daemon delegates.
|
|
112
|
+
this.#residency = new WorkerResidency({
|
|
113
|
+
db,
|
|
114
|
+
engine: () => this.#engine,
|
|
115
|
+
workspaceGate: this.#workspaceGate,
|
|
116
|
+
normalizeRuntime: (registration) => this.#normalizeRuntime(registration),
|
|
117
|
+
});
|
|
118
|
+
// {§functionality-coordinator} — the one owner above every family
|
|
119
|
+
// adapter; Core hosts it, modules register adapters through the seam.
|
|
120
|
+
this.#functionality = new Functionality({
|
|
121
|
+
registerModuleAction: (registration) => this.registerModuleAction(registration),
|
|
122
|
+
registerWorkerCapabilityProvider: (owner, provider) => this.registerWorkerCapabilityProvider(owner, provider),
|
|
123
|
+
readWorkerModuleState: (workerId, owner) => this.readWorkerModuleState(workerId, owner),
|
|
124
|
+
replaceWorkerCapabilities: (replacement, options) => this.replaceWorkerCapabilities(replacement, options),
|
|
125
|
+
retainWorker: (workerId) => this.#residency.retain(workerId),
|
|
126
|
+
});
|
|
127
|
+
// {§skills-functionality} — Core's own family: standard Agent Skills.
|
|
128
|
+
this.#skills = new SkillsFunctionality({ db, ...skills });
|
|
129
|
+
this.#skills.attach(this.#functionality.register(this.#skills));
|
|
97
130
|
this.#branchBatches = new BranchBatches(db, this.#workspaceGate, {
|
|
98
131
|
settleWorkspace: async (workspaceId) => this.#engine.drainWorkspaceDerivations(workspaceId),
|
|
99
132
|
createChild: async ({ workspaceId, parentWorkerId, parentLoopId, op, name, prompt, flags, origin }) => {
|
|
@@ -101,19 +134,31 @@ export default class Daemon {
|
|
|
101
134
|
const providerSpec = parentPolicy.childProviderSpec ?? parentPolicy.providerSpec;
|
|
102
135
|
const workerName = WorkerName.assert(name);
|
|
103
136
|
const workerId = op === "FORK"
|
|
104
|
-
? await Fork.fork(this.#db, parentWorkerId, workerName)
|
|
137
|
+
? await Fork.fork(this.#db, parentWorkerId, workerName, (scheme) => this.#schemes.entryInheritanceForStoredScheme(scheme, parentWorkerId))
|
|
105
138
|
: (await this.#db.fork_insert_worker.get({
|
|
106
139
|
workspace_id: workspaceId,
|
|
107
140
|
name: workerName,
|
|
108
141
|
parent_worker_id: parentWorkerId,
|
|
109
142
|
origin,
|
|
143
|
+
fork_snapshot: 0,
|
|
110
144
|
}))?.id;
|
|
111
145
|
if (workerId === undefined)
|
|
112
146
|
throw new Error("Branch worker insert returned no row");
|
|
147
|
+
// {§worker-model-selection} — lineage inheritance by value: the branch
|
|
148
|
+
// child begins with the spawning loop's effective spawn model.
|
|
149
|
+
await this.#db.worker_generation_policy_update.run({
|
|
150
|
+
id: workerId,
|
|
151
|
+
model_route_id: await routeForSpec(this.#db, providerSpec),
|
|
152
|
+
spawn_model_route_id: null,
|
|
153
|
+
reasoning_policy: parentPolicy.reasoningPolicy,
|
|
154
|
+
});
|
|
155
|
+
const source = await this.#workerPromptSource(workspaceId, parentWorkerId, workerId);
|
|
113
156
|
const loopId = await this.#drains.enqueueFreshLoop({
|
|
114
157
|
workerId,
|
|
115
158
|
prompt,
|
|
159
|
+
...(source === undefined ? {} : { source }),
|
|
116
160
|
providerSpec,
|
|
161
|
+
reasoningPolicy: parentPolicy.reasoningPolicy,
|
|
117
162
|
childProviderSpec: parentPolicy.childProviderSpec,
|
|
118
163
|
flags,
|
|
119
164
|
});
|
|
@@ -145,28 +190,56 @@ export default class Daemon {
|
|
|
145
190
|
// model-independent. Request-shaped token facts stay provider-owned.
|
|
146
191
|
weigh: contentWeight,
|
|
147
192
|
streamEventNotify: (workspaceId, event) => this.notifyStreamEvent(workspaceId, event),
|
|
193
|
+
reasoningEventNotify: (workspaceId, event) => this.notifyReasoningEvent(workspaceId, event),
|
|
148
194
|
wakeWorkerNotify: (payload) => this.#drains.notifyWakeWorker(payload),
|
|
149
195
|
// worker:// loop-start primitive — spawn/fork/irc deliver through
|
|
150
196
|
// Daemon.inject (active sister → fold; idle → enqueue + drain). The
|
|
151
197
|
// daemon owns provider + the law-file system prompt; the worker scheme
|
|
152
198
|
// handler carries neither. Fire-and-forget: the returned drain runs
|
|
153
199
|
// independently (the sister is its own worker). {§machine-processes}
|
|
154
|
-
injectWorker: async ({ workspaceId, workerId, prompt, flags, parentLoopId }) => {
|
|
200
|
+
injectWorker: async ({ workspaceId, workerId, sourceWorkerId, prompt, flags, parentLoopId }) => {
|
|
201
|
+
await this.#assertModelWorker(workspaceId, workerId);
|
|
202
|
+
const source = await this.#workerPromptSource(workspaceId, sourceWorkerId, workerId);
|
|
155
203
|
const systemPrompt = await readFile(Paths.instructionsSystem, "utf8");
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
204
|
+
let providerSpec;
|
|
205
|
+
let childProviderSpec;
|
|
206
|
+
let reasoningPolicy;
|
|
207
|
+
if (parentLoopId === undefined) {
|
|
208
|
+
// {§worker-model-selection} — the voice door addresses an
|
|
209
|
+
// existing worker. Its own durable generation policy is the
|
|
210
|
+
// receiver's identity; the daemon default and sender do not
|
|
211
|
+
// overwrite it merely because a new loop must be minted.
|
|
212
|
+
const targetPolicy = await this.#resolveWorkerModel(workerId, undefined);
|
|
213
|
+
if (targetPolicy === null) {
|
|
214
|
+
throw new Error(`injectWorker: worker ${workerId} has no resolvable model route`);
|
|
215
|
+
}
|
|
216
|
+
providerSpec = targetPolicy.providerSpec;
|
|
217
|
+
reasoningPolicy = targetPolicy.reasoningPolicy;
|
|
218
|
+
childProviderSpec = await this.#resolveWorkerSpawnModel(workerId, undefined);
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
const parentPolicy = await this.#providerPolicyForLoop(parentLoopId);
|
|
222
|
+
providerSpec = parentPolicy.childProviderSpec ?? parentPolicy.providerSpec;
|
|
223
|
+
reasoningPolicy = parentPolicy.reasoningPolicy;
|
|
224
|
+
// {§worker-model-selection} — lineage inheritance by value:
|
|
225
|
+
// the child begins with the spawning loop's effective spawn
|
|
226
|
+
// model and no separate override of its own.
|
|
227
|
+
childProviderSpec = null;
|
|
228
|
+
await this.#db.worker_generation_policy_update.run({
|
|
229
|
+
id: workerId,
|
|
230
|
+
model_route_id: await routeForSpec(this.#db, providerSpec),
|
|
231
|
+
spawn_model_route_id: null,
|
|
232
|
+
reasoning_policy: reasoningPolicy,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
164
235
|
const { action, loopId } = await this.inject({
|
|
165
236
|
workspaceId,
|
|
166
237
|
workerId,
|
|
167
238
|
prompt,
|
|
239
|
+
...(source === undefined ? {} : { source }),
|
|
168
240
|
providerSpec,
|
|
169
|
-
|
|
241
|
+
reasoningPolicy,
|
|
242
|
+
childProviderSpec,
|
|
170
243
|
systemPrompt,
|
|
171
244
|
...(flags === undefined ? {} : { flags }),
|
|
172
245
|
});
|
|
@@ -175,7 +248,15 @@ export default class Daemon {
|
|
|
175
248
|
branchWorker: async (args) => this.#branchBatches.enqueue(args),
|
|
176
249
|
branchCompletionGate: async (workerId) => this.#branchBatches.completionGate(workerId),
|
|
177
250
|
acquireWorkspaceTurn: async (workspaceId, workerId) => this.#workspaceGate.acquireTurn(workspaceId, workerId),
|
|
178
|
-
|
|
251
|
+
// {§skills-hotload} — filesystem installers operate out of band.
|
|
252
|
+
// Republish under the workspace turn gate before packet assembly so
|
|
253
|
+
// the first subsequent model turn sees their exact result.
|
|
254
|
+
workspaceTurnStarting: async ({ workspaceId, workerId }) => {
|
|
255
|
+
await this.#skills.refreshIfChanged({ workspaceId, workerId });
|
|
256
|
+
},
|
|
257
|
+
workspaceTurnCompleted: async ({ turnId }) => {
|
|
258
|
+
await this.#branchBatches.sealTurn(turnId);
|
|
259
|
+
},
|
|
179
260
|
// worker:// KILL (terminate) — cancel the addressed worker subtree and
|
|
180
261
|
// tear down its held streams before the operation completes.
|
|
181
262
|
cancelWorker: async (workerId, reason) => this.#drains.cancelWorkerTree(workerId, reason),
|
|
@@ -185,10 +266,15 @@ export default class Daemon {
|
|
|
185
266
|
this.#drains = new DrainSupervisor({
|
|
186
267
|
db,
|
|
187
268
|
lifecycle: this.#lifecycle,
|
|
188
|
-
injectPrompt: (workerId, prompt, openPaths) => this.#engine.inject(workerId, prompt, openPaths),
|
|
189
|
-
assertInjectionCompatibility: async ({ workerId, loopId, providerSpec, childProviderSpec, turnCeiling, flags, }) => {
|
|
269
|
+
injectPrompt: (workerId, prompt, openPaths, source) => this.#engine.inject(workerId, prompt, openPaths, source),
|
|
270
|
+
assertInjectionCompatibility: async ({ workerId, loopId, providerSpec, providerSpecExplicit, reasoningPolicy, childProviderSpec, turnCeiling, flags, }) => {
|
|
190
271
|
await this.#assertFoldPosture(workerId, flags, loopId);
|
|
191
|
-
|
|
272
|
+
// An omitted selector keeps the loop's durable provider; only an
|
|
273
|
+
// explicit selection is checked against it (a deliberate switch).
|
|
274
|
+
if (providerSpecExplicit !== false) {
|
|
275
|
+
await this.#assertLoopProvider(loopId, providerSpec);
|
|
276
|
+
}
|
|
277
|
+
await this.#assertLoopReasoningPolicy(loopId, reasoningPolicy);
|
|
192
278
|
if (childProviderSpec !== undefined) {
|
|
193
279
|
await this.#assertLoopChildProvider(loopId, childProviderSpec);
|
|
194
280
|
}
|
|
@@ -196,22 +282,34 @@ export default class Daemon {
|
|
|
196
282
|
},
|
|
197
283
|
reconcilePrompts: (workerId, endedLoopId) => this.#reconcileOrphanedPrompts(workerId, endedLoopId),
|
|
198
284
|
runLoop: async ({ workspaceId, workerId, loopId, maxTurns, prompt, systemPrompt, signal, onDispatch, }) => {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
285
|
+
// The drain callback is the final provider/model boundary. Every
|
|
286
|
+
// admission path, including boot recovery, terminates here.
|
|
287
|
+
await this.#assertModelWorker(workspaceId, workerId);
|
|
288
|
+
const releaseCapabilities = await this.#residency.acquire(workspaceId, workerId);
|
|
289
|
+
try {
|
|
290
|
+
// Worker settings can change while Functionality remains
|
|
291
|
+
// resident. Reconcile the worker-private discovery surface
|
|
292
|
+
// at the same pre-inference boundary used for activation.
|
|
293
|
+
await this.#residency.reconcile(workspaceId, workerId);
|
|
294
|
+
const { provider, childProvider } = await this.#providersForLoop(loopId);
|
|
295
|
+
return await this.#engine.runLoop({
|
|
296
|
+
provider,
|
|
297
|
+
childProvider,
|
|
298
|
+
workspaceId,
|
|
299
|
+
workerId,
|
|
300
|
+
loopId,
|
|
301
|
+
maxTurns,
|
|
302
|
+
messages: [
|
|
303
|
+
{ role: "system", content: systemPrompt },
|
|
304
|
+
{ role: "user", content: prompt },
|
|
305
|
+
],
|
|
306
|
+
signal,
|
|
307
|
+
onDispatch,
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
finally {
|
|
311
|
+
releaseCapabilities();
|
|
312
|
+
}
|
|
215
313
|
},
|
|
216
314
|
loopUsage: (loopId) => this.#engine.loopUsage(loopId),
|
|
217
315
|
loopAttributions: (loopId) => this.#engine.loopAttributions(loopId),
|
|
@@ -220,7 +318,6 @@ export default class Daemon {
|
|
|
220
318
|
this.#engine.parkDeadlines.delete(loopId);
|
|
221
319
|
return deadline;
|
|
222
320
|
},
|
|
223
|
-
settleSearch: (target, status) => this.#engine.searchGate.settle(target, status),
|
|
224
321
|
cancelSubscription: (subscriptionId) => this.#engine.cancelSubscription(subscriptionId),
|
|
225
322
|
hasActiveStreams: (workerId) => this.#workerHasActiveStreams(workerId),
|
|
226
323
|
readSystemPrompt: () => readFile(Paths.instructionsSystem, "utf8"),
|
|
@@ -279,60 +376,31 @@ export default class Daemon {
|
|
|
279
376
|
async runLoop(args) {
|
|
280
377
|
const workspaceId = ClientInput.assertId("runLoop", "workspaceId", args.workspaceId);
|
|
281
378
|
const workerId = ClientInput.assertId("runLoop", "workerId", args.workerId);
|
|
379
|
+
await this.#assertModelWorker(workspaceId, workerId);
|
|
282
380
|
const prompt = ClientInput.assertPrompt("runLoop", args.prompt);
|
|
381
|
+
const source = ClientInput.assertOptionalSource("runLoop", args.source);
|
|
283
382
|
const requestedMaxTurns = ClientInput.assertMaxTurns("runLoop", args.maxTurns);
|
|
284
383
|
const openPaths = ClientInput.assertOpenPaths("runLoop", args.openPaths);
|
|
285
|
-
const
|
|
286
|
-
const
|
|
287
|
-
const childAlias = ClientInput.assertOptionalChildAlias("runLoop", args.childAlias);
|
|
288
|
-
const childModel = ClientInput.assertOptionalSelector("runLoop", "childModel", args.childModel);
|
|
289
|
-
if (childAlias === null && childModel !== undefined) {
|
|
290
|
-
throw daemonFailure("daemon:input", "child-provider-conflict", 400, "childModel cannot accompany an inherited child provider policy.", { field: "childModel", recovery: "Omit childModel or select a child alias.", retryable: false });
|
|
291
|
-
}
|
|
384
|
+
const selector = ClientInput.assertOptionalSelector("runLoop", "selector", args.selector);
|
|
385
|
+
const childSelector = ClientInput.assertOptionalChildSelector("runLoop", args.childSelector);
|
|
292
386
|
const flags = ClientInput.normalizeLoopFlags("runLoop", args.flags);
|
|
293
|
-
// {§
|
|
294
|
-
//
|
|
295
|
-
//
|
|
296
|
-
//
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
387
|
+
// {§worker-model-selection} — the worker owns the model. An explicit selector
|
|
388
|
+
// persists onto the worker; an omitted selector resolves the worker's durable model
|
|
389
|
+
// (seeded once from the daemon default). The loop then snapshots the resolved route.
|
|
390
|
+
// A continuation's omitted selector must still keep the loop's durable provider.
|
|
391
|
+
const selectorExplicit = selector !== undefined;
|
|
392
|
+
const generationPolicy = await this.#resolveWorkerModel(workerId, selector);
|
|
393
|
+
if (generationPolicy === null) {
|
|
394
|
+
throw new OperationFailureError(Results.failure("daemon:provider", "not-configured", 501, "No provider is configured for this worker.", {}, {
|
|
300
395
|
stage: "provider-selection",
|
|
301
396
|
recovery: "Select a configured model provider.",
|
|
302
397
|
retryable: false,
|
|
303
398
|
}));
|
|
304
399
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
: childAlias;
|
|
309
|
-
const childSelection = configuredChildAlias === null
|
|
310
|
-
|| (configuredChildAlias === undefined && childModel === undefined)
|
|
311
|
-
? null
|
|
312
|
-
: await this.#resolveLoopProvider(configuredChildAlias, childModel);
|
|
400
|
+
const { providerSpec: selection, reasoningPolicy } = generationPolicy;
|
|
401
|
+
// {§methods-loop-run-child-provider} — the worker's persistent spawn override.
|
|
402
|
+
const childSelection = await this.#resolveWorkerSpawnModel(workerId, childSelector);
|
|
313
403
|
const systemPrompt = await readFile(Paths.instructionsSystem, "utf8");
|
|
314
|
-
// {§machine-processes} — the model NEVER runs in a client-origin worker (its packets would carry
|
|
315
|
-
// client-action rows). The module resolves the model worker via ensureModelWorker and passes it (or a
|
|
316
|
-
// fork); a client worker here is a caller error, refused loudly rather than silently rehomed.
|
|
317
|
-
const target = await this.#db.envelope_get_worker_by_id.get({ id: workerId });
|
|
318
|
-
if (target === undefined) {
|
|
319
|
-
throw daemonFailure("daemon:worker", "worker-not-found", 404, `Worker ${workerId} does not exist.`, { workerId });
|
|
320
|
-
}
|
|
321
|
-
if (target.workspace_id !== workspaceId) {
|
|
322
|
-
throw daemonFailure("daemon:worker", "workspace-mismatch", 409, `Worker ${workerId} does not belong to workspace ${workspaceId}.`, {
|
|
323
|
-
workerId,
|
|
324
|
-
workspaceId,
|
|
325
|
-
actualWorkspaceId: target.workspace_id,
|
|
326
|
-
retryable: false,
|
|
327
|
-
});
|
|
328
|
-
}
|
|
329
|
-
if (target.origin === "client") {
|
|
330
|
-
throw daemonFailure("daemon:worker", "model-worker-required", 409, `Worker ${workerId} is not a model worker.`, {
|
|
331
|
-
workerId,
|
|
332
|
-
recovery: "Select or create a model worker for this loop.",
|
|
333
|
-
retryable: false,
|
|
334
|
-
});
|
|
335
|
-
}
|
|
336
404
|
// {§operator-config-max-turns-ceiling} — the operator ceiling clamps a per-call maxTurns; a
|
|
337
405
|
// seam caller must not bypass operator policy (inject only DEFAULTS from env, never clamps).
|
|
338
406
|
const ceiling = Number(process.env.PLURNK_SERVICE_MAX_TURNS ?? "-1");
|
|
@@ -346,76 +414,187 @@ export default class Daemon {
|
|
|
346
414
|
workspaceId,
|
|
347
415
|
workerId,
|
|
348
416
|
prompt,
|
|
417
|
+
...(source === undefined ? {} : { source }),
|
|
349
418
|
...(flags !== undefined ? { flags } : {}),
|
|
350
419
|
...(openPaths !== undefined ? { openPaths } : {}),
|
|
351
420
|
turnCeiling,
|
|
352
421
|
providerSpec: selection,
|
|
422
|
+
providerSpecExplicit: selectorExplicit,
|
|
423
|
+
reasoningPolicy,
|
|
353
424
|
childProviderSpec: childSelection,
|
|
354
425
|
systemPrompt,
|
|
355
426
|
});
|
|
356
427
|
return { status: 100, action, loopId, ...(turnSeq !== undefined ? { turnSeq } : {}) };
|
|
357
428
|
}
|
|
358
|
-
// {§methods-loop-run-model} — resolve
|
|
359
|
-
//
|
|
360
|
-
//
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
const requested = resolveLoopAlias(alias, model, parseAliasesFromEnv());
|
|
429
|
+
// {§methods-loop-run-model} — resolve one alias-or-route selector to a cached
|
|
430
|
+
// Provider; absent uses the boot default. An unknown alias or malformed exact
|
|
431
|
+
// route throws legibly rather than silently running the wrong model.
|
|
432
|
+
async #resolveLoopProvider(selector, reasoningPolicy) {
|
|
433
|
+
const requested = resolveLoopRoute(selector, parseAliasesFromEnv());
|
|
364
434
|
if (requested === null && this.#provider === null)
|
|
365
435
|
return null;
|
|
366
|
-
const spec = requested ??
|
|
436
|
+
const spec = requested ?? resolveActiveRoute();
|
|
367
437
|
if (spec === null) {
|
|
368
|
-
throw daemonFailure("daemon:provider", "active-
|
|
438
|
+
throw daemonFailure("daemon:provider", "active-model-unresolved", 500, "The active provider has no resolvable model route.", { stage: "provider-selection", retryable: false });
|
|
369
439
|
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
440
|
+
await this.#providerForPolicy(spec, reasoningPolicy);
|
|
441
|
+
return spec;
|
|
442
|
+
}
|
|
443
|
+
// Resolve eagerly so runLoop fails before enqueue when the selected route
|
|
444
|
+
// and durable reasoning policy cannot compose. The drain later retrieves
|
|
445
|
+
// this cached handle from the loop's immutable snapshot.
|
|
446
|
+
async #providerForPolicy(spec, reasoningPolicy) {
|
|
373
447
|
try {
|
|
374
|
-
await ProviderInstantiate.instantiateProvider(spec);
|
|
448
|
+
const provider = await ProviderInstantiate.instantiateProvider(spec, process.env, reasoningPolicy);
|
|
449
|
+
ProviderInstantiate.validateGrammarConfiguration(provider, process.env, reasoningPolicy);
|
|
450
|
+
return provider;
|
|
375
451
|
}
|
|
376
452
|
catch (cause) {
|
|
377
453
|
if (cause instanceof OperationFailureError)
|
|
378
454
|
throw cause;
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
455
|
+
if (cause instanceof UnsupportedReasoningPolicyError) {
|
|
456
|
+
throw daemonFailure("daemon:provider", "reasoning-policy-unsupported", 409, `${modelRouteLabel(spec)} does not support reasoning policy '${cause.policy}'.`, {
|
|
457
|
+
...(spec.alias === undefined ? {} : { alias: spec.alias }),
|
|
458
|
+
provider: spec.provider,
|
|
459
|
+
model: spec.model,
|
|
460
|
+
reasoningPolicy: cause.policy,
|
|
461
|
+
supportedReasoningPolicies: cause.supported,
|
|
462
|
+
stage: "provider-selection",
|
|
463
|
+
recovery: "Select one of the provider's supported reasoning policies.",
|
|
464
|
+
retryable: false,
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
console.error(`${modelRouteLabel(spec)} could not be instantiated:`, cause);
|
|
468
|
+
throw daemonFailure("daemon:provider", "provider-unavailable", 503, `${modelRouteLabel(spec)} is unavailable.`, {
|
|
469
|
+
...(spec.alias === undefined ? {} : { alias: spec.alias }),
|
|
382
470
|
provider: spec.provider,
|
|
383
471
|
model: spec.model,
|
|
384
472
|
stage: "provider-selection",
|
|
385
473
|
retryable: false,
|
|
386
474
|
});
|
|
387
475
|
}
|
|
388
|
-
return spec;
|
|
389
476
|
}
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
477
|
+
// {§worker-model-selection} — a model worker owns one durable model. An explicit
|
|
478
|
+
// selector persists onto the worker; an omitted selector resolves the worker's
|
|
479
|
+
// durable model, seeded once from the daemon default. A deliberately modelless
|
|
480
|
+
// daemon leaves the worker unset until an explicit selection arrives.
|
|
481
|
+
async #resolveWorkerModel(workerId, selector) {
|
|
482
|
+
const worker = await this.#db.worker_generation_policy_read.get({ id: workerId });
|
|
483
|
+
if (worker === undefined)
|
|
484
|
+
throw new Error(`worker ${workerId}: model route row missing`);
|
|
485
|
+
if (selector !== undefined) {
|
|
486
|
+
const spec = await this.#resolveLoopProvider(selector, worker.reasoning_policy ?? undefined);
|
|
487
|
+
if (spec === null)
|
|
488
|
+
return null;
|
|
489
|
+
const reasoningPolicy = worker.reasoning_policy
|
|
490
|
+
?? ProviderInstantiate.configuredReasoningPolicy(spec);
|
|
491
|
+
if (worker.spawn_model_route_id !== null) {
|
|
492
|
+
const spawnSpec = await specForRoute(this.#db, worker.spawn_model_route_id);
|
|
493
|
+
if (spawnSpec === null)
|
|
494
|
+
throw new Error(`worker ${workerId}: spawn model route is missing`);
|
|
495
|
+
await this.#providerForPolicy(spawnSpec, reasoningPolicy);
|
|
496
|
+
}
|
|
497
|
+
await this.#db.worker_generation_policy_update.run({
|
|
498
|
+
id: workerId,
|
|
499
|
+
model_route_id: await routeForSpec(this.#db, spec),
|
|
500
|
+
spawn_model_route_id: worker.spawn_model_route_id,
|
|
501
|
+
reasoning_policy: reasoningPolicy,
|
|
502
|
+
});
|
|
503
|
+
return { providerSpec: spec, reasoningPolicy };
|
|
394
504
|
}
|
|
395
|
-
|
|
396
|
-
|
|
505
|
+
if (worker.model_route_id !== null) {
|
|
506
|
+
if (worker.reasoning_policy === null) {
|
|
507
|
+
throw new Error(`worker ${workerId}: durable model has no reasoning policy`);
|
|
508
|
+
}
|
|
509
|
+
const spec = await specForRoute(this.#db, worker.model_route_id);
|
|
510
|
+
if (spec === null)
|
|
511
|
+
throw new Error(`worker ${workerId}: model route is missing`);
|
|
512
|
+
await this.#providerForPolicy(spec, worker.reasoning_policy);
|
|
513
|
+
return { providerSpec: spec, reasoningPolicy: worker.reasoning_policy };
|
|
397
514
|
}
|
|
398
|
-
if (
|
|
515
|
+
if (this.#provider === null)
|
|
399
516
|
return null;
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
517
|
+
const spec = resolveActiveRoute();
|
|
518
|
+
if (spec !== null) {
|
|
519
|
+
const reasoningPolicy = ProviderInstantiate.configuredReasoningPolicy(spec);
|
|
520
|
+
await this.#providerForPolicy(spec, reasoningPolicy);
|
|
521
|
+
if (worker.spawn_model_route_id !== null) {
|
|
522
|
+
const spawnSpec = await specForRoute(this.#db, worker.spawn_model_route_id);
|
|
523
|
+
if (spawnSpec === null)
|
|
524
|
+
throw new Error(`worker ${workerId}: spawn model route is missing`);
|
|
525
|
+
await this.#providerForPolicy(spawnSpec, reasoningPolicy);
|
|
526
|
+
}
|
|
527
|
+
await this.#db.worker_generation_policy_update.run({
|
|
528
|
+
id: workerId,
|
|
529
|
+
model_route_id: await routeForSpec(this.#db, spec),
|
|
530
|
+
spawn_model_route_id: worker.spawn_model_route_id,
|
|
531
|
+
reasoning_policy: reasoningPolicy,
|
|
532
|
+
});
|
|
533
|
+
return { providerSpec: spec, reasoningPolicy };
|
|
405
534
|
}
|
|
406
|
-
return
|
|
535
|
+
return null;
|
|
407
536
|
}
|
|
537
|
+
// {§worker-model-selection} — the persistent spawn override. An explicit child
|
|
538
|
+
// selector persists onto the worker (null clears it back to inherit); an omitted
|
|
539
|
+
// selector resolves the persisted override, seeded once from the operator's
|
|
540
|
+
// PLURNK_MODEL_CHILD default.
|
|
541
|
+
async #resolveWorkerSpawnModel(workerId, childSelector) {
|
|
542
|
+
const worker = await this.#db.worker_generation_policy_read.get({ id: workerId });
|
|
543
|
+
if (worker === undefined)
|
|
544
|
+
throw new Error(`worker ${workerId}: model route row missing`);
|
|
545
|
+
if (childSelector !== undefined) {
|
|
546
|
+
const spec = childSelector === null
|
|
547
|
+
? null
|
|
548
|
+
: await this.#resolveLoopProvider(childSelector, worker.reasoning_policy ?? undefined);
|
|
549
|
+
await this.#db.worker_generation_policy_update.run({
|
|
550
|
+
id: workerId,
|
|
551
|
+
model_route_id: worker.model_route_id,
|
|
552
|
+
spawn_model_route_id: spec === null ? null : await routeForSpec(this.#db, spec),
|
|
553
|
+
reasoning_policy: worker.reasoning_policy,
|
|
554
|
+
});
|
|
555
|
+
return spec;
|
|
556
|
+
}
|
|
557
|
+
if (worker.spawn_model_route_id !== null) {
|
|
558
|
+
const spec = await specForRoute(this.#db, worker.spawn_model_route_id);
|
|
559
|
+
if (spec === null)
|
|
560
|
+
throw new Error(`worker ${workerId}: spawn model route is missing`);
|
|
561
|
+
if (worker.reasoning_policy !== null) {
|
|
562
|
+
await this.#providerForPolicy(spec, worker.reasoning_policy);
|
|
563
|
+
}
|
|
564
|
+
return spec;
|
|
565
|
+
}
|
|
566
|
+
const configured = process.env.PLURNK_MODEL_CHILD;
|
|
567
|
+
if (configured === undefined || configured.length === 0)
|
|
568
|
+
return null;
|
|
569
|
+
const spec = await this.#resolveLoopProvider(configured, worker.reasoning_policy ?? undefined);
|
|
570
|
+
if (spec !== null) {
|
|
571
|
+
await this.#db.worker_generation_policy_update.run({
|
|
572
|
+
id: workerId,
|
|
573
|
+
model_route_id: worker.model_route_id,
|
|
574
|
+
spawn_model_route_id: await routeForSpec(this.#db, spec),
|
|
575
|
+
reasoning_policy: worker.reasoning_policy,
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
return spec;
|
|
579
|
+
}
|
|
580
|
+
// {§worker-model-selection} — the loop's durable snapshot is the immutable route ids,
|
|
581
|
+
// resolved through model_routes at the claim boundary; never re-resolved through the
|
|
582
|
+
// alias cascade (a changed alias declaration must not rewrite history).
|
|
408
583
|
async #providerPolicyForLoop(loopId) {
|
|
409
|
-
const row = await this.#db.
|
|
584
|
+
const row = await this.#db.drain_loop_generation_policy.get({ loop_id: loopId });
|
|
410
585
|
if (row === undefined)
|
|
411
586
|
throw new Error(`loop ${loopId}: provider selection row is missing`);
|
|
412
|
-
const providerSpec = this.#
|
|
587
|
+
const providerSpec = await specForRoute(this.#db, row.model_route_id);
|
|
413
588
|
if (providerSpec === null) {
|
|
414
|
-
throw new Error(`loop ${loopId}: persisted provider selection is missing
|
|
589
|
+
throw new Error(`loop ${loopId}: persisted provider selection is missing — refusing boot-default substitution`);
|
|
590
|
+
}
|
|
591
|
+
if (row.reasoning_policy === null) {
|
|
592
|
+
throw new Error(`loop ${loopId}: persisted reasoning policy is missing`);
|
|
415
593
|
}
|
|
416
594
|
return {
|
|
417
595
|
providerSpec,
|
|
418
|
-
childProviderSpec: this.#
|
|
596
|
+
childProviderSpec: await specForRoute(this.#db, row.spawn_model_route_id),
|
|
597
|
+
reasoningPolicy: row.reasoning_policy,
|
|
419
598
|
};
|
|
420
599
|
}
|
|
421
600
|
async #providerSpecForLoop(loopId) {
|
|
@@ -423,20 +602,20 @@ export default class Daemon {
|
|
|
423
602
|
}
|
|
424
603
|
async #providersForLoop(loopId) {
|
|
425
604
|
const policy = await this.#providerPolicyForLoop(loopId);
|
|
426
|
-
const provider = await
|
|
605
|
+
const provider = await this.#providerForPolicy(policy.providerSpec, policy.reasoningPolicy);
|
|
427
606
|
const childProvider = policy.childProviderSpec === null
|
|
428
607
|
? provider
|
|
429
|
-
: await
|
|
608
|
+
: await this.#providerForPolicy(policy.childProviderSpec, policy.reasoningPolicy);
|
|
430
609
|
return { provider, childProvider };
|
|
431
610
|
}
|
|
432
611
|
async #assertLoopProvider(loopId, requested) {
|
|
433
612
|
const selected = await this.#providerSpecForLoop(loopId);
|
|
434
613
|
if (JSON.stringify(selected) !== JSON.stringify(requested)) {
|
|
435
|
-
throw daemonFailure("daemon:provider", "loop-provider-conflict", 409, `Loop ${loopId} uses
|
|
614
|
+
throw daemonFailure("daemon:provider", "loop-provider-conflict", 409, `Loop ${loopId} uses ${modelRouteLabel(selected)}, not ${modelRouteLabel(requested)}.`, {
|
|
436
615
|
loopId,
|
|
437
|
-
selectedAlias: selected.alias,
|
|
616
|
+
...(selected.alias === undefined ? {} : { selectedAlias: selected.alias }),
|
|
438
617
|
selectedModel: `${selected.provider}/${selected.model}`,
|
|
439
|
-
requestedAlias: requested.alias,
|
|
618
|
+
...(requested.alias === undefined ? {} : { requestedAlias: requested.alias }),
|
|
440
619
|
requestedModel: `${requested.provider}/${requested.model}`,
|
|
441
620
|
stage: "loop-injection",
|
|
442
621
|
recovery: "Cancel or conclude the loop before selecting another provider.",
|
|
@@ -444,13 +623,28 @@ export default class Daemon {
|
|
|
444
623
|
});
|
|
445
624
|
}
|
|
446
625
|
}
|
|
626
|
+
async #assertLoopReasoningPolicy(loopId, requested) {
|
|
627
|
+
const selected = (await this.#providerPolicyForLoop(loopId)).reasoningPolicy;
|
|
628
|
+
if (selected !== requested) {
|
|
629
|
+
throw daemonFailure("daemon:provider", "loop-reasoning-policy-conflict", 409, `Loop ${loopId} uses reasoning policy '${selected}', not '${requested}'.`, {
|
|
630
|
+
loopId,
|
|
631
|
+
selectedReasoningPolicy: selected,
|
|
632
|
+
requestedReasoningPolicy: requested,
|
|
633
|
+
stage: "loop-injection",
|
|
634
|
+
recovery: "Cancel or conclude the loop before selecting another reasoning policy.",
|
|
635
|
+
retryable: false,
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
}
|
|
447
639
|
async #assertLoopChildProvider(loopId, requested) {
|
|
448
640
|
const selected = (await this.#providerPolicyForLoop(loopId)).childProviderSpec;
|
|
449
641
|
if (JSON.stringify(selected) !== JSON.stringify(requested)) {
|
|
450
642
|
throw daemonFailure("daemon:provider", "loop-child-provider-conflict", 409, `Loop ${loopId} already has a different child provider policy.`, {
|
|
451
643
|
loopId,
|
|
452
644
|
selectedChildAlias: selected?.alias ?? null,
|
|
645
|
+
selectedChildModel: selected === null ? null : `${selected.provider}/${selected.model}`,
|
|
453
646
|
requestedChildAlias: requested?.alias ?? null,
|
|
647
|
+
requestedChildModel: requested === null ? null : `${requested.provider}/${requested.model}`,
|
|
454
648
|
stage: "loop-injection",
|
|
455
649
|
recovery: "Cancel or conclude the loop before changing its child provider policy.",
|
|
456
650
|
retryable: false,
|
|
@@ -476,69 +670,316 @@ export default class Daemon {
|
|
|
476
670
|
}
|
|
477
671
|
// {§methods-model-worker} — the workspace's model worker (created on first use), distinct from the client
|
|
478
672
|
// worker so the model's packets never carry client-action rows. The module binds its threads to this.
|
|
479
|
-
|
|
480
|
-
|
|
673
|
+
// Optional worker settings ({§worker-settings}) ride the client's per-run declaration: merged in on
|
|
674
|
+
// creation AND on every subsequent ensure, so a client can change its mind between loops.
|
|
675
|
+
async ensureModelWorker(workspaceId, settings) {
|
|
676
|
+
const checked = ClientInput.assertId("worker.ensure-model", "workspaceId", workspaceId);
|
|
677
|
+
const created = await Envelope.ensureModelWorker(this.#db, checked);
|
|
678
|
+
if (settings !== undefined)
|
|
679
|
+
await this.#mergeWorkerSettings(created, settings);
|
|
680
|
+
return created;
|
|
681
|
+
}
|
|
682
|
+
// {§worker-settings} — merge known keys into the worker's behavioral-rules bag.
|
|
683
|
+
// Validated at the boundary; unprovided keys keep their durable value.
|
|
684
|
+
async #mergeWorkerSettings(workerId, settings) {
|
|
685
|
+
const current = await WorkerSettingsReader.read(this.#db, workerId);
|
|
686
|
+
const merged = {
|
|
687
|
+
requestUserInput: settings.requestUserInput ?? current.requestUserInput,
|
|
688
|
+
};
|
|
689
|
+
await this.#db.worker_settings_update.run({ id: workerId, settings: JSON.stringify(merged) });
|
|
690
|
+
}
|
|
691
|
+
// {§worker-settings} — project the worker's behavioral rules for a client-interface surface.
|
|
692
|
+
async readWorkerSettings(args) {
|
|
693
|
+
const workspaceId = ClientInput.assertId("worker.settings.get", "workspaceId", args.workspaceId);
|
|
694
|
+
const workerId = ClientInput.assertId("worker.settings.get", "workerId", args.workerId);
|
|
695
|
+
await this.#assertWorkerOwned(workspaceId, workerId);
|
|
696
|
+
const settings = await WorkerSettingsReader.read(this.#db, workerId);
|
|
697
|
+
return { requestUserInput: settings.requestUserInput };
|
|
698
|
+
}
|
|
699
|
+
// {§worker-settings} — persist known behavioral-rule keys; unknown keys are
|
|
700
|
+
// rejected at the input boundary, never persisted.
|
|
701
|
+
async setWorkerSettings(args) {
|
|
702
|
+
const workspaceId = ClientInput.assertId("worker.settings.set", "workspaceId", args.workspaceId);
|
|
703
|
+
const workerId = ClientInput.assertId("worker.settings.set", "workerId", args.workerId);
|
|
704
|
+
await this.#assertWorkerOwned(workspaceId, workerId);
|
|
705
|
+
ClientInput.parseWorkerSettings(args.settings);
|
|
706
|
+
await this.#mergeWorkerSettings(workerId, args.settings);
|
|
707
|
+
const settings = await WorkerSettingsReader.read(this.#db, workerId);
|
|
708
|
+
return { requestUserInput: settings.requestUserInput };
|
|
709
|
+
}
|
|
710
|
+
// {§worker-model-selection} — project a worker's durable model and spawn override
|
|
711
|
+
// for a client-interface get/set surface. Ownership is checked at this boundary.
|
|
712
|
+
async readWorkerModel(args) {
|
|
713
|
+
const workspaceId = ClientInput.assertId("worker.model.get", "workspaceId", args.workspaceId);
|
|
714
|
+
const workerId = ClientInput.assertId("worker.model.get", "workerId", args.workerId);
|
|
715
|
+
await this.#assertWorkerOwned(workspaceId, workerId);
|
|
716
|
+
const row = await this.#db.worker_generation_policy_read.get({ id: workerId });
|
|
717
|
+
if (row === undefined)
|
|
718
|
+
throw new Error(`worker ${workerId}: model route row missing`);
|
|
719
|
+
const modelSpec = await specForRoute(this.#db, row.model_route_id);
|
|
720
|
+
const spawnModelSpec = await specForRoute(this.#db, row.spawn_model_route_id);
|
|
721
|
+
return {
|
|
722
|
+
model: modelSpec === null ? null : projectModelRoute(modelSpec),
|
|
723
|
+
spawnModel: spawnModelSpec === null ? null : projectModelRoute(spawnModelSpec),
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
// {§worker-model-selection} — persist an explicit model selection onto the worker
|
|
727
|
+
// and return the resolved spec. An unresolvable selector fails loud here.
|
|
728
|
+
async setWorkerModel(args) {
|
|
729
|
+
const workspaceId = ClientInput.assertId("worker.model.set", "workspaceId", args.workspaceId);
|
|
730
|
+
const workerId = ClientInput.assertId("worker.model.set", "workerId", args.workerId);
|
|
731
|
+
await this.#assertWorkerOwned(workspaceId, workerId);
|
|
732
|
+
await this.#assertWorkerSelectable(workerId);
|
|
733
|
+
const selector = ClientInput.assertSelector("worker.model.set", "selector", args.selector);
|
|
734
|
+
const policy = await this.#resolveWorkerModel(workerId, selector);
|
|
735
|
+
if (policy === null) {
|
|
736
|
+
throw new OperationFailureError(Results.failure("daemon:provider", "not-configured", 501, "No provider is configured for this worker.", {}, { stage: "provider-selection", recovery: "Select a configured model provider.", retryable: false }));
|
|
737
|
+
}
|
|
738
|
+
return projectModelRoute(policy.providerSpec);
|
|
739
|
+
}
|
|
740
|
+
async #reasoningSupportForWorker(workerId, row, providerSpec, policy) {
|
|
741
|
+
const provider = await this.#providerForPolicy(providerSpec, policy);
|
|
742
|
+
if (row.spawn_model_route_id === null)
|
|
743
|
+
return provider.supportedReasoningPolicies;
|
|
744
|
+
const spawnSpec = await specForRoute(this.#db, row.spawn_model_route_id);
|
|
745
|
+
if (spawnSpec === null)
|
|
746
|
+
throw new Error(`worker ${workerId}: spawn model route is missing`);
|
|
747
|
+
const spawnProvider = await this.#providerForPolicy(spawnSpec, policy);
|
|
748
|
+
return provider.supportedReasoningPolicies.filter((candidate) => spawnProvider.supportedReasoningPolicies.includes(candidate));
|
|
749
|
+
}
|
|
750
|
+
// {§worker-reasoning-policy} — the worker's reasoning policy is durable and
|
|
751
|
+
// provider-relative. A model-less worker remains explicitly uninitialized.
|
|
752
|
+
async readWorkerReasoning(args) {
|
|
753
|
+
const workspaceId = ClientInput.assertId("worker.reasoning.get", "workspaceId", args.workspaceId);
|
|
754
|
+
const workerId = ClientInput.assertId("worker.reasoning.get", "workerId", args.workerId);
|
|
755
|
+
await this.#assertWorkerOwned(workspaceId, workerId);
|
|
756
|
+
await this.#resolveWorkerModel(workerId, undefined);
|
|
757
|
+
const row = await this.#db.worker_generation_policy_read.get({ id: workerId });
|
|
758
|
+
if (row === undefined)
|
|
759
|
+
throw new Error(`worker ${workerId}: generation policy row missing`);
|
|
760
|
+
if (row.model_route_id === null) {
|
|
761
|
+
if (row.reasoning_policy !== null) {
|
|
762
|
+
throw new Error(`worker ${workerId}: reasoning policy exists without a model route`);
|
|
763
|
+
}
|
|
764
|
+
return { policy: null, supportedPolicies: [] };
|
|
765
|
+
}
|
|
766
|
+
if (row.reasoning_policy === null) {
|
|
767
|
+
throw new Error(`worker ${workerId}: durable model has no reasoning policy`);
|
|
768
|
+
}
|
|
769
|
+
const spec = await specForRoute(this.#db, row.model_route_id);
|
|
770
|
+
if (spec === null)
|
|
771
|
+
throw new Error(`worker ${workerId}: model route is missing`);
|
|
772
|
+
return {
|
|
773
|
+
policy: row.reasoning_policy,
|
|
774
|
+
supportedPolicies: await this.#reasoningSupportForWorker(workerId, row, spec, row.reasoning_policy),
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
// {§worker-reasoning-policy} — mutate between loops, validate against both
|
|
778
|
+
// the worker model and its optional spawn model, then persist atomically.
|
|
779
|
+
async setWorkerReasoning(args) {
|
|
780
|
+
const workspaceId = ClientInput.assertId("worker.reasoning.set", "workspaceId", args.workspaceId);
|
|
781
|
+
const workerId = ClientInput.assertId("worker.reasoning.set", "workerId", args.workerId);
|
|
782
|
+
let policy;
|
|
783
|
+
try {
|
|
784
|
+
policy = Validator.assertReasoningPolicy(args.policy);
|
|
785
|
+
}
|
|
786
|
+
catch (cause) {
|
|
787
|
+
throw daemonFailure("daemon:input", "reasoning-policy-invalid", 400, cause instanceof Error ? cause.message : "The reasoning policy is invalid.", { field: "policy", retryable: false });
|
|
788
|
+
}
|
|
789
|
+
await this.#assertWorkerOwned(workspaceId, workerId);
|
|
790
|
+
await this.#assertWorkerSelectable(workerId);
|
|
791
|
+
await this.#resolveWorkerModel(workerId, undefined);
|
|
792
|
+
const row = await this.#db.worker_generation_policy_read.get({ id: workerId });
|
|
793
|
+
if (row === undefined)
|
|
794
|
+
throw new Error(`worker ${workerId}: generation policy row missing`);
|
|
795
|
+
const spec = await specForRoute(this.#db, row.model_route_id);
|
|
796
|
+
if (spec === null) {
|
|
797
|
+
throw daemonFailure("daemon:provider", "worker-model-unset", 409, `Worker ${workerId} has no model against which to validate a reasoning policy.`, {
|
|
798
|
+
workerId,
|
|
799
|
+
stage: "provider-selection",
|
|
800
|
+
recovery: "Select a model before selecting its reasoning policy.",
|
|
801
|
+
retryable: false,
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
const supportedPolicies = await this.#reasoningSupportForWorker(workerId, row, spec, policy);
|
|
805
|
+
await this.#db.worker_generation_policy_update.run({
|
|
806
|
+
id: workerId,
|
|
807
|
+
model_route_id: row.model_route_id,
|
|
808
|
+
spawn_model_route_id: row.spawn_model_route_id,
|
|
809
|
+
reasoning_policy: policy,
|
|
810
|
+
});
|
|
811
|
+
return { policy, supportedPolicies };
|
|
812
|
+
}
|
|
813
|
+
// {§worker-model-selection} — persist the worker's spawn override; a null
|
|
814
|
+
// selector means inherit (clears the override).
|
|
815
|
+
async setWorkerSpawnModel(args) {
|
|
816
|
+
const workspaceId = ClientInput.assertId("worker.child.set", "workspaceId", args.workspaceId);
|
|
817
|
+
const workerId = ClientInput.assertId("worker.child.set", "workerId", args.workerId);
|
|
818
|
+
await this.#assertWorkerOwned(workspaceId, workerId);
|
|
819
|
+
await this.#assertWorkerSelectable(workerId);
|
|
820
|
+
const selector = ClientInput.assertChildSelector("worker.child.set", args.selector);
|
|
821
|
+
const spec = await this.#resolveWorkerSpawnModel(workerId, selector);
|
|
822
|
+
return spec === null ? null : projectModelRoute(spec);
|
|
823
|
+
}
|
|
824
|
+
// {§worker-model-selection} — a selection must not mutate underneath active
|
|
825
|
+
// work: a live or parked loop is a precise 409, never a silent retroactive
|
|
826
|
+
// switch of the immutable loop snapshot.
|
|
827
|
+
async #assertWorkerSelectable(workerId) {
|
|
828
|
+
if (await this.#drains.hasLiveWork(workerId)) {
|
|
829
|
+
throw daemonFailure("daemon:worker", "worker-loop-active", 409, `Worker ${workerId} has a live or parked loop; select a model after concluding or cancelling it.`, {
|
|
830
|
+
workerId,
|
|
831
|
+
stage: "model-selection",
|
|
832
|
+
recovery: "Conclude or cancel the active loop before selecting a model.",
|
|
833
|
+
retryable: false,
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
async #assertWorkerOwned(workspaceId, workerId) {
|
|
838
|
+
const owner = await this.#db.envelope_get_worker_by_id.get({ id: workerId });
|
|
839
|
+
if (owner === undefined) {
|
|
840
|
+
throw daemonFailure("daemon:worker", "worker-not-found", 404, `Worker ${workerId} does not exist.`, { workerId });
|
|
841
|
+
}
|
|
842
|
+
if (owner.workspace_id !== workspaceId) {
|
|
843
|
+
throw daemonFailure("daemon:worker", "workspace-mismatch", 409, `Worker ${workerId} does not belong to workspace ${workspaceId}.`, { workerId, workspaceId, actualWorkspaceId: owner.workspace_id });
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
// {§machine-processes} — provider inference is an exclusive capability of a
|
|
847
|
+
// model worker. The same assertion is used by early client admissions and
|
|
848
|
+
// the final drain callback, so recovery and internal callers cannot bypass it.
|
|
849
|
+
async #assertModelWorker(workspaceId, workerId) {
|
|
850
|
+
const worker = await this.#db.envelope_get_worker_by_id.get({ id: workerId });
|
|
851
|
+
if (worker === undefined) {
|
|
852
|
+
throw daemonFailure("daemon:worker", "worker-not-found", 404, `Worker ${workerId} does not exist.`, { workerId });
|
|
853
|
+
}
|
|
854
|
+
if (worker.workspace_id !== workspaceId) {
|
|
855
|
+
throw daemonFailure("daemon:worker", "workspace-mismatch", 409, `Worker ${workerId} does not belong to workspace ${workspaceId}.`, {
|
|
856
|
+
workerId,
|
|
857
|
+
workspaceId,
|
|
858
|
+
actualWorkspaceId: worker.workspace_id,
|
|
859
|
+
retryable: false,
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
if (worker.origin !== "model") {
|
|
863
|
+
throw daemonFailure("daemon:worker", "model-worker-required", 409, `Worker ${workerId} is not a model worker.`, {
|
|
864
|
+
workerId,
|
|
865
|
+
actualOrigin: worker.origin,
|
|
866
|
+
recovery: "Select or create a model worker for this loop.",
|
|
867
|
+
retryable: false,
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
async #workerPromptSource(workspaceId, sourceWorkerId, targetWorkerId) {
|
|
872
|
+
if (sourceWorkerId === targetWorkerId)
|
|
873
|
+
return undefined;
|
|
874
|
+
const source = await this.#db.envelope_get_worker_by_id.get({ id: sourceWorkerId });
|
|
875
|
+
if (source === undefined || source.workspace_id !== workspaceId) {
|
|
876
|
+
throw new Error(`Worker prompt source ${sourceWorkerId} does not belong to workspace ${workspaceId}.`);
|
|
877
|
+
}
|
|
878
|
+
return WorkerControlAddress.render(source.name);
|
|
481
879
|
}
|
|
482
|
-
// {§methods-op-mirror} — execute parsed ops on behalf of a client
|
|
880
|
+
// {§methods-op-mirror} — execute parsed ops on behalf of a client as a
|
|
483
881
|
// client-origin turn (the log is core's, a client op is a first-class citizen), dispatched through
|
|
484
882
|
// the engine, then emitted as log/entry on the event source. One seam op backs the whole op_*
|
|
485
883
|
// family (read/edit/copy/find/fold/look/move/open/send/exec); the module parses at its edge with the
|
|
486
884
|
// grammar package and hands over the statement, then fans the emitted entry out to its own clients.
|
|
885
|
+
// {§functionality-model-mutation} — await every queued Functionality
|
|
886
|
+
// publication (a Worker's accepted mutation publishes at its turn boundary).
|
|
887
|
+
async settleFunctionality(workerId) {
|
|
888
|
+
await this.#functionality.settle(workerId);
|
|
889
|
+
}
|
|
487
890
|
async dispatchAsClient(args) {
|
|
488
891
|
const workspaceId = ClientInput.assertId("operation.dispatch", "workspaceId", args.workspaceId);
|
|
489
892
|
const workerId = ClientInput.assertId("operation.dispatch", "workerId", args.workerId);
|
|
893
|
+
const functionalityWorkerId = ClientInput.assertId("operation.dispatch", "functionalityWorkerId", args.functionalityWorkerId);
|
|
490
894
|
const { statement } = args;
|
|
491
|
-
|
|
895
|
+
await this.#assertWorkerOwned(workspaceId, functionalityWorkerId);
|
|
896
|
+
const releaseCapabilities = await this.#residency.acquire(workspaceId, functionalityWorkerId);
|
|
492
897
|
try {
|
|
493
|
-
const
|
|
494
|
-
|
|
495
|
-
|
|
898
|
+
const clientLoopId = await Envelope.ensureClientLoop(this.#db, workerId);
|
|
899
|
+
try {
|
|
900
|
+
const result = await this.#dispatchClientStatement({ workspaceId, workerId, functionalityWorkerId, loopId: clientLoopId, statement });
|
|
901
|
+
await Envelope.closeClientLoop(this.#db, clientLoopId, { status: 200 });
|
|
902
|
+
return result;
|
|
903
|
+
}
|
|
904
|
+
catch (error) {
|
|
905
|
+
await Envelope.closeClientLoop(this.#db, clientLoopId, clientActionFailure(error));
|
|
906
|
+
throw error;
|
|
907
|
+
}
|
|
496
908
|
}
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
throw error;
|
|
909
|
+
finally {
|
|
910
|
+
releaseCapabilities();
|
|
500
911
|
}
|
|
501
912
|
}
|
|
502
|
-
// The client-interface action contract: one AG-UI action owns one
|
|
503
|
-
// regardless of how many statements op.parse produced.
|
|
504
|
-
// keep
|
|
505
|
-
//
|
|
913
|
+
// The client-interface action contract: one AG-UI action owns one administrative
|
|
914
|
+
// loop, regardless of how many statements op.parse produced. Each statement is one
|
|
915
|
+
// ordinary operation turn; a proposal may keep that turn and loop open across
|
|
916
|
+
// interrupt/resume until settlement.
|
|
506
917
|
async dispatchClientAction(args) {
|
|
507
918
|
const workspaceId = ClientInput.assertId("operation.dispatch-batch", "workspaceId", args.workspaceId);
|
|
508
919
|
const workerId = ClientInput.assertId("operation.dispatch-batch", "workerId", args.workerId);
|
|
920
|
+
const functionalityWorkerId = ClientInput.assertId("operation.dispatch-batch", "functionalityWorkerId", args.functionalityWorkerId);
|
|
509
921
|
const { statements } = args;
|
|
510
922
|
if (statements.length === 0)
|
|
511
923
|
return [];
|
|
512
|
-
|
|
924
|
+
await this.#assertWorkerOwned(workspaceId, functionalityWorkerId);
|
|
925
|
+
// {§actor-boundary-attached-functionality} — the attached Worker's
|
|
926
|
+
// Functionality is what the client operation executes in.
|
|
927
|
+
const releaseCapabilities = await this.#residency.acquire(workspaceId, functionalityWorkerId);
|
|
513
928
|
try {
|
|
514
|
-
const
|
|
515
|
-
|
|
516
|
-
results
|
|
929
|
+
const clientLoopId = await Envelope.ensureClientLoop(this.#db, workerId);
|
|
930
|
+
try {
|
|
931
|
+
const results = [];
|
|
932
|
+
for (const statement of statements) {
|
|
933
|
+
results.push(await this.#dispatchClientStatement({ workspaceId, workerId, functionalityWorkerId, loopId: clientLoopId, statement }));
|
|
934
|
+
}
|
|
935
|
+
await Envelope.closeClientLoop(this.#db, clientLoopId, { status: 200 });
|
|
936
|
+
return results;
|
|
937
|
+
}
|
|
938
|
+
catch (error) {
|
|
939
|
+
await Envelope.closeClientLoop(this.#db, clientLoopId, clientActionFailure(error));
|
|
940
|
+
throw error;
|
|
517
941
|
}
|
|
518
|
-
await Envelope.closeClientLoop(this.#db, clientLoopId, { status: 200 });
|
|
519
|
-
return results;
|
|
520
942
|
}
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
throw error;
|
|
943
|
+
finally {
|
|
944
|
+
releaseCapabilities();
|
|
524
945
|
}
|
|
525
946
|
}
|
|
526
947
|
async #dispatchClientStatement(args) {
|
|
527
|
-
const { workspaceId, workerId, loopId, statement } = args;
|
|
948
|
+
const { workspaceId, workerId, functionalityWorkerId, loopId, statement } = args;
|
|
528
949
|
const release = await this.#workspaceGate.acquireTurn(workspaceId, workerId);
|
|
529
950
|
try {
|
|
530
|
-
const { id: turnId } = await
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
origin: "client", onDispatch: (logEntryId) => { entryIds.push(logEntryId); },
|
|
951
|
+
const { id: turnId } = await Turn.open(this.#db, {
|
|
952
|
+
loopId,
|
|
953
|
+
producer: "client",
|
|
954
|
+
kind: "operation",
|
|
535
955
|
});
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
const
|
|
539
|
-
|
|
956
|
+
let turnOpen = true;
|
|
957
|
+
try {
|
|
958
|
+
const entryIds = [];
|
|
959
|
+
const result = await this.#engine.dispatch({
|
|
960
|
+
statement, workspaceId, workerId, functionalityWorkerId, loopId, turnId, sequence: 1,
|
|
961
|
+
origin: "client", onDispatch: (logEntryId) => { entryIds.push(logEntryId); },
|
|
962
|
+
});
|
|
963
|
+
await Turn.complete(this.#db, turnId, result.status);
|
|
964
|
+
turnOpen = false;
|
|
965
|
+
await this.#branchBatches.sealTurn(turnId);
|
|
966
|
+
for (const logEntryId of entryIds) {
|
|
967
|
+
const entry = await LogEntry.fetchLogEntry(this.#db, logEntryId);
|
|
968
|
+
this.#broadcast({ workspaceId }, "log/entry", { entry });
|
|
969
|
+
}
|
|
970
|
+
return result;
|
|
971
|
+
}
|
|
972
|
+
catch (cause) {
|
|
973
|
+
if (turnOpen) {
|
|
974
|
+
try {
|
|
975
|
+
await Turn.complete(this.#db, turnId, 500);
|
|
976
|
+
}
|
|
977
|
+
catch (completionCause) {
|
|
978
|
+
throw new AggregateError([cause, completionCause], `client operation turn ${turnId} failed and could not be completed`);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
throw cause;
|
|
540
982
|
}
|
|
541
|
-
return result;
|
|
542
983
|
}
|
|
543
984
|
finally {
|
|
544
985
|
release();
|
|
@@ -553,20 +994,30 @@ export default class Daemon {
|
|
|
553
994
|
async look(args) {
|
|
554
995
|
const workspaceId = ClientInput.assertId("operation.look", "workspaceId", args.workspaceId);
|
|
555
996
|
const workerId = ClientInput.assertId("operation.look", "workerId", args.workerId);
|
|
997
|
+
const functionalityWorkerId = ClientInput.assertId("operation.look", "functionalityWorkerId", args.functionalityWorkerId);
|
|
556
998
|
const { statement } = args;
|
|
557
|
-
|
|
558
|
-
const
|
|
999
|
+
await this.#assertWorkerOwned(workspaceId, functionalityWorkerId);
|
|
1000
|
+
const releaseCapabilities = await this.#residency.acquire(workspaceId, functionalityWorkerId);
|
|
559
1001
|
try {
|
|
560
|
-
const
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
1002
|
+
const releaseWorkspace = await this.#workspaceGate.acquireTurn(workspaceId, workerId);
|
|
1003
|
+
try {
|
|
1004
|
+
const clientLoopId = await Envelope.ensureClientLoop(this.#db, workerId);
|
|
1005
|
+
try {
|
|
1006
|
+
const result = await this.#engine.look({ statement, workspaceId, workerId, functionalityWorkerId, loopId: clientLoopId });
|
|
1007
|
+
await Envelope.closeClientLoop(this.#db, clientLoopId, { status: 200 });
|
|
1008
|
+
return result;
|
|
1009
|
+
}
|
|
1010
|
+
catch (error) {
|
|
1011
|
+
await Envelope.closeClientLoop(this.#db, clientLoopId, clientActionFailure(error));
|
|
1012
|
+
throw error;
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
finally {
|
|
1016
|
+
releaseWorkspace();
|
|
1017
|
+
}
|
|
567
1018
|
}
|
|
568
1019
|
finally {
|
|
569
|
-
|
|
1020
|
+
releaseCapabilities();
|
|
570
1021
|
}
|
|
571
1022
|
}
|
|
572
1023
|
// {§methods-log-read} — a workspace's journal, the module's primary render input. The worker is
|
|
@@ -630,7 +1081,7 @@ export default class Daemon {
|
|
|
630
1081
|
// {§methods} — the module's render surface beyond the journal. Thin delegations
|
|
631
1082
|
// into core's envelope / membership / provider machinery; the module fans the results into its own views.
|
|
632
1083
|
listProviders() {
|
|
633
|
-
const active =
|
|
1084
|
+
const active = resolveActiveRoute();
|
|
634
1085
|
return {
|
|
635
1086
|
aliases: parseAliasesFromEnv().map((a) => {
|
|
636
1087
|
const isActive = active !== null && active.alias === a.alias;
|
|
@@ -641,6 +1092,11 @@ export default class Daemon {
|
|
|
641
1092
|
}),
|
|
642
1093
|
};
|
|
643
1094
|
}
|
|
1095
|
+
// {§model-catalog} The catalog is a bounded, worldless local projection.
|
|
1096
|
+
// It performs no provider I/O and never changes the worker's selection.
|
|
1097
|
+
listModels(query) {
|
|
1098
|
+
return listModelCatalog(query);
|
|
1099
|
+
}
|
|
644
1100
|
// {§client-display-capabilities} Core composes the installed family
|
|
645
1101
|
// declarations; interface modules expose this contracts-owned wire without
|
|
646
1102
|
// inventing presentation policy. `exec` is operation machinery, not an
|
|
@@ -665,8 +1121,60 @@ export default class Daemon {
|
|
|
665
1121
|
return Validator.assertClientDisplayCapabilities([...schemes, ...mimetypes]);
|
|
666
1122
|
}
|
|
667
1123
|
listWorkspaces() { return Envelope.listWorkspaces(this.#db); }
|
|
668
|
-
listWorkers(workspaceId) {
|
|
669
|
-
|
|
1124
|
+
listWorkers(workspaceId, query = {}) {
|
|
1125
|
+
const origin = query.origin;
|
|
1126
|
+
if (origin !== undefined && !["model", "client", "_plurnk"].includes(origin)) {
|
|
1127
|
+
throw daemonFailure("daemon:worker", "origin-invalid", 400, `Worker origin '${String(origin)}' is invalid.`, { origin, retryable: false });
|
|
1128
|
+
}
|
|
1129
|
+
const parentWorkerId = query.parentWorkerId === undefined
|
|
1130
|
+
? undefined
|
|
1131
|
+
: query.parentWorkerId === null
|
|
1132
|
+
? null
|
|
1133
|
+
: ClientInput.assertId("workspace.workers", "parentWorkerId", query.parentWorkerId);
|
|
1134
|
+
// {§methods-worker-list} — an omitted parent filter must stay absent:
|
|
1135
|
+
// the envelope keys the filter on the property's presence, not its value.
|
|
1136
|
+
return Envelope.listWorkersForWorkspace(this.#db, ClientInput.assertId("workspace.workers", "workspaceId", workspaceId), {
|
|
1137
|
+
...(origin === undefined ? {} : { origin }),
|
|
1138
|
+
...(parentWorkerId === undefined ? {} : { parentWorkerId }),
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
async readWorker(args) {
|
|
1142
|
+
const workspaceId = ClientInput.assertId("worker.read", "workspaceId", args.workspaceId);
|
|
1143
|
+
const hasId = Object.hasOwn(args.identity, "id");
|
|
1144
|
+
const hasName = Object.hasOwn(args.identity, "name");
|
|
1145
|
+
if (hasId === hasName) {
|
|
1146
|
+
throw daemonFailure("daemon:worker", "identity-invalid", 400, "Worker observation requires exactly one id or name.", { retryable: false });
|
|
1147
|
+
}
|
|
1148
|
+
const row = hasId
|
|
1149
|
+
? await this.#db.envelope_get_worker_by_id.get({
|
|
1150
|
+
id: ClientInput.assertId("worker.read", "id", args.identity.id),
|
|
1151
|
+
})
|
|
1152
|
+
: await this.#db.envelope_get_worker_by_name.get({
|
|
1153
|
+
workspace_id: workspaceId,
|
|
1154
|
+
name: ClientInput.assertOptionalWorkerName("worker.read", "name", args.identity.name),
|
|
1155
|
+
});
|
|
1156
|
+
if (row === undefined || row.workspace_id !== workspaceId)
|
|
1157
|
+
return null;
|
|
1158
|
+
const { workspace_id: _workspaceId, ...projection } = row;
|
|
1159
|
+
return projection;
|
|
1160
|
+
}
|
|
1161
|
+
async listWorkerLoops(args) {
|
|
1162
|
+
const workspaceId = ClientInput.assertId("worker.loops", "workspaceId", args.workspaceId);
|
|
1163
|
+
const workerId = ClientInput.assertId("worker.loops", "workerId", args.workerId);
|
|
1164
|
+
await this.#assertWorkerOwned(workspaceId, workerId);
|
|
1165
|
+
const rows = await this.#db.application_list_worker_loops.all({ worker_id: workerId });
|
|
1166
|
+
return rows.map((row) => ({
|
|
1167
|
+
id: row.id,
|
|
1168
|
+
workerId: row.workerId,
|
|
1169
|
+
sequence: row.sequence,
|
|
1170
|
+
status: row.status,
|
|
1171
|
+
prompt: row.prompt,
|
|
1172
|
+
promptSource: row.promptSource,
|
|
1173
|
+
terminatedAt: row.terminatedAt,
|
|
1174
|
+
terminalResult: row.terminalResult === null
|
|
1175
|
+
? null
|
|
1176
|
+
: Validator.assertOperationResult(JSON.parse(row.terminalResult)),
|
|
1177
|
+
}));
|
|
670
1178
|
}
|
|
671
1179
|
// {§methods-workspace-prompts}: root-conversation loop seeds, newest-first.
|
|
672
1180
|
listPrompts(workspaceId, limit) {
|
|
@@ -693,7 +1201,7 @@ export default class Daemon {
|
|
|
693
1201
|
}
|
|
694
1202
|
// {§methods-workspace-create}: the module owns protocol decoding; core validates the typed seam
|
|
695
1203
|
// inputs and owns the envelope, its reserved-name + name-uniqueness invariants,
|
|
696
|
-
// membership resolution
|
|
1204
|
+
// membership resolution and the workspace/created emit. No connection state
|
|
697
1205
|
// (which client is on which workspace) lives here — that's the module's.
|
|
698
1206
|
async createWorkspace(args) {
|
|
699
1207
|
// The seam fails hard on malformed semantic input so every module inherits one wall:
|
|
@@ -713,25 +1221,18 @@ export default class Daemon {
|
|
|
713
1221
|
}
|
|
714
1222
|
if (constraints.length > 0)
|
|
715
1223
|
await GitMembership.resolveGitMembership(this.#db, envelope.workspaceId, undefined);
|
|
716
|
-
for (const provider of this.#workspaceCapabilityProviders.values()) {
|
|
717
|
-
await provider.hydrate(envelope.workspaceId);
|
|
718
|
-
}
|
|
719
|
-
await LoopDocs.materialize(this.#engine, this.#db, envelope.workspaceId);
|
|
720
|
-
void this.#engine.warmWorkspaceDerivations(envelope.workspaceId).catch(() => { });
|
|
721
1224
|
this.#broadcast("all", "workspace/created", { id: envelope.workspaceId, name: envelope.workspaceName, projectRoot: envelope.projectRoot });
|
|
722
1225
|
return envelope;
|
|
723
1226
|
});
|
|
724
1227
|
}
|
|
725
1228
|
async attachWorkspace(args) {
|
|
726
|
-
// attachToWorkspace owns the reserved-name + worker-ownership invariants; the seam just delegates
|
|
1229
|
+
// attachToWorkspace owns the reserved-name + worker-ownership invariants; the seam just delegates.
|
|
727
1230
|
const workspaceId = ClientInput.assertId("workspace.attach", "workspaceId", args.workspaceId);
|
|
728
1231
|
const workerId = args.workerId === undefined
|
|
729
1232
|
? undefined
|
|
730
1233
|
: ClientInput.assertId("workspace.attach", "workerId", args.workerId);
|
|
731
1234
|
const workerName = ClientInput.assertOptionalWorkerName("workspace.attach", "workerName", args.workerName);
|
|
732
|
-
|
|
733
|
-
void this.#engine.warmWorkspaceDerivations(envelope.workspaceId).catch(() => { });
|
|
734
|
-
return envelope;
|
|
1235
|
+
return Envelope.attachToWorkspace(this.#db, workspaceId, { workerId, workerName });
|
|
735
1236
|
}
|
|
736
1237
|
async renameWorkspace(workspaceId, name) {
|
|
737
1238
|
const checkedWorkspaceId = ClientInput.assertId("workspace.rename", "workspaceId", workspaceId);
|
|
@@ -750,7 +1251,7 @@ export default class Daemon {
|
|
|
750
1251
|
// Members may have just landed — begin warming now, but return the constraint response
|
|
751
1252
|
// immediately so prompts do not wait for the complete derivation corpus.
|
|
752
1253
|
void this.#engine.warmWorkspaceDerivations(checkedWorkspaceId).catch(() => { });
|
|
753
|
-
return { effect, glob };
|
|
1254
|
+
return { effect, glob, source: "explicit" };
|
|
754
1255
|
}
|
|
755
1256
|
finally {
|
|
756
1257
|
release();
|
|
@@ -797,8 +1298,10 @@ export default class Daemon {
|
|
|
797
1298
|
retryable: false,
|
|
798
1299
|
});
|
|
799
1300
|
}
|
|
800
|
-
const
|
|
1301
|
+
const releaseCapabilities = await this.#residency.acquire(workspaceId, workerId);
|
|
1302
|
+
let releaseWorkspace;
|
|
801
1303
|
try {
|
|
1304
|
+
releaseWorkspace = await this.#workspaceGate.acquireTurn(workspaceId, workerId);
|
|
802
1305
|
let parsed;
|
|
803
1306
|
try {
|
|
804
1307
|
parsed = parsePath(args.target);
|
|
@@ -849,6 +1352,7 @@ export default class Daemon {
|
|
|
849
1352
|
workspace_id: workspaceId,
|
|
850
1353
|
owner_id: location.ownerId,
|
|
851
1354
|
scheme: location.scheme,
|
|
1355
|
+
authority: location.authority,
|
|
852
1356
|
pathname: location.pathname,
|
|
853
1357
|
});
|
|
854
1358
|
if (row === undefined) {
|
|
@@ -896,7 +1400,8 @@ export default class Daemon {
|
|
|
896
1400
|
});
|
|
897
1401
|
}
|
|
898
1402
|
finally {
|
|
899
|
-
|
|
1403
|
+
releaseWorkspace?.();
|
|
1404
|
+
releaseCapabilities();
|
|
900
1405
|
}
|
|
901
1406
|
}
|
|
902
1407
|
// {§methods-conversation-worker}: a fresh conversation is a model-origin root worker with an empty private log.
|
|
@@ -915,14 +1420,17 @@ export default class Daemon {
|
|
|
915
1420
|
}
|
|
916
1421
|
}
|
|
917
1422
|
const worker = await Envelope.createModelWorker(this.#db, workspaceId, name);
|
|
1423
|
+
if (args.settings !== undefined) {
|
|
1424
|
+
await this.#mergeWorkerSettings(worker.id, args.settings);
|
|
1425
|
+
}
|
|
918
1426
|
return { workerId: worker.id, workerName: worker.name };
|
|
919
1427
|
}
|
|
920
1428
|
// {§worker-scheme-fork} — branch a worker's log while sharing the workspace world.
|
|
921
1429
|
// Core owns the workspace check and immutable worker-name admission.
|
|
922
1430
|
async forkWorker(args) {
|
|
923
|
-
const workspaceId = ClientInput.assertId("
|
|
924
|
-
const workerId = ClientInput.assertId("
|
|
925
|
-
const name = ClientInput.assertOptionalWorkerName("
|
|
1431
|
+
const workspaceId = ClientInput.assertId("run.fork", "workspaceId", args.workspaceId);
|
|
1432
|
+
const workerId = ClientInput.assertId("run.fork", "workerId", args.workerId);
|
|
1433
|
+
const name = ClientInput.assertOptionalWorkerName("run.fork", "name", args.name);
|
|
926
1434
|
const owner = await this.#db.envelope_get_worker_by_id.get({ id: workerId });
|
|
927
1435
|
if (owner === undefined) {
|
|
928
1436
|
throw daemonFailure("daemon:worker", "worker-not-found", 404, `Worker ${workerId} does not exist.`, { workerId });
|
|
@@ -941,30 +1449,23 @@ export default class Daemon {
|
|
|
941
1449
|
throw daemonFailure("daemon:worker", "name-conflict", 409, `Worker name '${name}' is already in use in workspace ${workspaceId}.`, { workspaceId, name, recovery: "Choose another worker name.", retryable: false });
|
|
942
1450
|
}
|
|
943
1451
|
}
|
|
944
|
-
const branchWorkerId = await Fork.fork(this.#db, workerId, name);
|
|
1452
|
+
const branchWorkerId = await Fork.fork(this.#db, workerId, name, (scheme) => this.#schemes.entryInheritanceForStoredScheme(scheme, workerId));
|
|
945
1453
|
const branch = await this.#db.envelope_get_worker_by_id.get({ id: branchWorkerId });
|
|
946
1454
|
return { workerId: branchWorkerId, workerName: branch?.name ?? null, parentWorkerId: workerId };
|
|
947
1455
|
}
|
|
948
1456
|
async registerRuntimes(registrations) {
|
|
949
1457
|
const normalized = registrations.map((registration) => this.#normalizeRuntime(registration));
|
|
950
1458
|
this.#engine.registerRuntimes(normalized);
|
|
951
|
-
|
|
952
|
-
for (const workspace of await Envelope.listWorkspaces(this.#db)) {
|
|
953
|
-
await LoopDocs.materialize(this.#engine, this.#db, workspace.id);
|
|
954
|
-
}
|
|
955
|
-
}
|
|
1459
|
+
await this.#residency.rematerializeActive();
|
|
956
1460
|
}
|
|
957
1461
|
async registerRuntime(registration) {
|
|
958
1462
|
await this.registerRuntimes([registration]);
|
|
959
1463
|
}
|
|
960
1464
|
async registerScheme(name, handler) {
|
|
961
1465
|
this.#schemes.register(name, handler);
|
|
962
|
-
if (this.#
|
|
1466
|
+
if (this.#residency.published)
|
|
963
1467
|
await this.#schemes.ready();
|
|
964
|
-
|
|
965
|
-
await LoopDocs.materialize(this.#engine, this.#db, workspace.id);
|
|
966
|
-
}
|
|
967
|
-
}
|
|
1468
|
+
await this.#residency.rematerializeActive();
|
|
968
1469
|
}
|
|
969
1470
|
#normalizeRuntime({ namespaceOwner, decl, executor, availability, scheme }) {
|
|
970
1471
|
if (typeof namespaceOwner !== "string" || namespaceOwner.trim().length === 0) {
|
|
@@ -977,8 +1478,11 @@ export default class Daemon {
|
|
|
977
1478
|
executor,
|
|
978
1479
|
namespaceOwner: { kind: "module", name: namespaceOwner },
|
|
979
1480
|
glyph: runtime.glyph ?? "",
|
|
1481
|
+
summary: runtime.summary,
|
|
980
1482
|
invocation: runtime.invocation,
|
|
981
|
-
|
|
1483
|
+
details: runtime.details ?? "",
|
|
1484
|
+
...(runtime.resourcesPath === undefined ? {} : { resourcesPath: runtime.resourcesPath }),
|
|
1485
|
+
...(runtime.expandTools === undefined ? {} : { expandTools: runtime.expandTools }),
|
|
982
1486
|
available: availability.available,
|
|
983
1487
|
detail: availability.detail,
|
|
984
1488
|
},
|
|
@@ -986,12 +1490,26 @@ export default class Daemon {
|
|
|
986
1490
|
};
|
|
987
1491
|
}
|
|
988
1492
|
registerModuleAction(registration) {
|
|
989
|
-
const { name, scope, handler } = registration;
|
|
1493
|
+
const { name, scope, inputSchema, outputSchema, handler } = registration;
|
|
990
1494
|
if (name.length === 0)
|
|
991
1495
|
throw new Error("registerModuleAction: action name must not be empty");
|
|
992
|
-
if (scope !== "worldless" && scope !== "workspace") {
|
|
1496
|
+
if (scope !== "worldless" && scope !== "workspace" && scope !== "worker") {
|
|
993
1497
|
throw new Error(`module action '${name}' has invalid scope '${String(scope)}'`);
|
|
994
1498
|
}
|
|
1499
|
+
if (inputSchema === null || typeof inputSchema !== "object" || Array.isArray(inputSchema)) {
|
|
1500
|
+
throw new Error(`module action '${name}' has no input schema`);
|
|
1501
|
+
}
|
|
1502
|
+
if (outputSchema === null || typeof outputSchema !== "object" || Array.isArray(outputSchema)) {
|
|
1503
|
+
throw new Error(`module action '${name}' has no output schema`);
|
|
1504
|
+
}
|
|
1505
|
+
for (const [role, schema] of [["input", inputSchema], ["output", outputSchema]]) {
|
|
1506
|
+
try {
|
|
1507
|
+
Validator.validateJsonSchemaInstance(schema, {});
|
|
1508
|
+
}
|
|
1509
|
+
catch (cause) {
|
|
1510
|
+
throw new Error(`module action '${name}' has an invalid ${role} schema`, { cause });
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
995
1513
|
if (typeof handler !== "function")
|
|
996
1514
|
throw new Error(`module action '${name}' has no handler`);
|
|
997
1515
|
if (this.#moduleActions.has(name))
|
|
@@ -1000,7 +1518,12 @@ export default class Daemon {
|
|
|
1000
1518
|
}
|
|
1001
1519
|
listModuleActions() {
|
|
1002
1520
|
return [...this.#moduleActions.values()]
|
|
1003
|
-
.map(({ name, scope }) => ({
|
|
1521
|
+
.map(({ name, scope, inputSchema, outputSchema }) => ({
|
|
1522
|
+
name,
|
|
1523
|
+
scope,
|
|
1524
|
+
inputSchema,
|
|
1525
|
+
outputSchema,
|
|
1526
|
+
}))
|
|
1004
1527
|
.toSorted((left, right) => left.name.localeCompare(right.name));
|
|
1005
1528
|
}
|
|
1006
1529
|
async invokeModuleAction(name, params, context) {
|
|
@@ -1010,129 +1533,38 @@ export default class Daemon {
|
|
|
1010
1533
|
if (registration.scope !== context.scope) {
|
|
1011
1534
|
throw new Error(`module action '${name}' requires ${registration.scope} context, not ${context.scope}`);
|
|
1012
1535
|
}
|
|
1536
|
+
let releaseCapabilities;
|
|
1013
1537
|
if (context.scope === "workspace") {
|
|
1014
1538
|
ClientInput.assertId(`module action '${name}'`, "workspaceId", context.workspaceId);
|
|
1015
1539
|
}
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
if (this.#workspaceCapabilityProviders.has(namespaceOwner)) {
|
|
1023
|
-
throw new Error(`workspace capability provider '${namespaceOwner}' is already registered`);
|
|
1024
|
-
}
|
|
1025
|
-
this.#workspaceCapabilityProviders.set(namespaceOwner, provider);
|
|
1026
|
-
}
|
|
1027
|
-
async readWorkspaceModuleState(workspaceId, namespaceOwner) {
|
|
1028
|
-
const checkedWorkspaceId = ClientInput.assertId("workspace module state", "workspaceId", workspaceId);
|
|
1029
|
-
if (namespaceOwner.trim().length === 0) {
|
|
1030
|
-
throw new Error("workspace module state requires a non-empty namespace owner");
|
|
1031
|
-
}
|
|
1032
|
-
const row = await this.#db.workspace_module_state_get.get({
|
|
1033
|
-
workspace_id: checkedWorkspaceId,
|
|
1034
|
-
namespace_owner: namespaceOwner,
|
|
1035
|
-
});
|
|
1036
|
-
return row === undefined ? null : JSON.parse(row.state);
|
|
1037
|
-
}
|
|
1038
|
-
async replaceWorkspaceCapabilities({ workspaceId, namespaceOwner, state, runtimes, }) {
|
|
1039
|
-
const checkedWorkspaceId = ClientInput.assertId("workspace capability replacement", "workspaceId", workspaceId);
|
|
1040
|
-
if (namespaceOwner.trim().length === 0) {
|
|
1041
|
-
throw new Error("workspace capability replacement requires a non-empty namespace owner");
|
|
1042
|
-
}
|
|
1043
|
-
const workspace = await this.#db.envelope_get_workspace.get({ id: checkedWorkspaceId });
|
|
1044
|
-
if (workspace === undefined) {
|
|
1045
|
-
throw daemonFailure("daemon:workspace-capability", "workspace-not-found", 404, `Workspace ${checkedWorkspaceId} does not exist.`, { workspaceId: checkedWorkspaceId, retryable: false });
|
|
1046
|
-
}
|
|
1047
|
-
const encoded = state === null ? null : JSON.stringify(state);
|
|
1048
|
-
if (state !== null && encoded === undefined) {
|
|
1049
|
-
throw daemonFailure("daemon:workspace-capability", "state-not-json", 400, "Workspace module state is not JSON-serializable.", { namespaceOwner, retryable: false });
|
|
1050
|
-
}
|
|
1051
|
-
if (encoded !== null)
|
|
1052
|
-
JSON.parse(encoded);
|
|
1053
|
-
const normalized = runtimes.map((registration) => {
|
|
1054
|
-
if (registration.namespaceOwner !== namespaceOwner) {
|
|
1055
|
-
throw new Error(`workspace runtime owner '${registration.namespaceOwner}' does not match '${namespaceOwner}'`);
|
|
1540
|
+
else if (context.scope === "worker") {
|
|
1541
|
+
const workspaceId = ClientInput.assertId(`module action '${name}'`, "workspaceId", context.workspaceId);
|
|
1542
|
+
const workerId = ClientInput.assertId(`module action '${name}'`, "workerId", context.workerId);
|
|
1543
|
+
const worker = await this.#db.envelope_get_worker_by_id.get({ id: workerId });
|
|
1544
|
+
if (worker === undefined || worker.workspace_id !== workspaceId) {
|
|
1545
|
+
throw daemonFailure("daemon:module-action", "worker-not-found", 404, `Worker ${workerId} does not exist in workspace ${workspaceId}.`, { workspaceId, workerId, retryable: false });
|
|
1056
1546
|
}
|
|
1057
|
-
|
|
1058
|
-
});
|
|
1059
|
-
const gate = this.#workspaceGate.tryExclusive(checkedWorkspaceId);
|
|
1060
|
-
if (gate === null) {
|
|
1061
|
-
throw daemonFailure("daemon:workspace-capability", "workspace-busy", 409, `Workspace ${checkedWorkspaceId} is running an operation or another capability change.`, {
|
|
1062
|
-
workspaceId: checkedWorkspaceId,
|
|
1063
|
-
namespaceOwner,
|
|
1064
|
-
recovery: "Settle the current operation and retry the capability change.",
|
|
1065
|
-
retryable: true,
|
|
1066
|
-
});
|
|
1547
|
+
releaseCapabilities = await this.#residency.acquire(workspaceId, workerId);
|
|
1067
1548
|
}
|
|
1068
|
-
await gate.acquired;
|
|
1069
|
-
const prior = await this.#db.workspace_module_state_get.get({
|
|
1070
|
-
workspace_id: checkedWorkspaceId,
|
|
1071
|
-
namespace_owner: namespaceOwner,
|
|
1072
|
-
});
|
|
1073
|
-
let rollbackRuntimes;
|
|
1074
|
-
let stateChanged = false;
|
|
1075
1549
|
try {
|
|
1076
|
-
|
|
1077
|
-
if (encoded === null) {
|
|
1078
|
-
await this.#db.workspace_module_state_delete.run({
|
|
1079
|
-
workspace_id: checkedWorkspaceId,
|
|
1080
|
-
namespace_owner: namespaceOwner,
|
|
1081
|
-
});
|
|
1082
|
-
}
|
|
1083
|
-
else {
|
|
1084
|
-
await this.#db.workspace_module_state_put.run({
|
|
1085
|
-
workspace_id: checkedWorkspaceId,
|
|
1086
|
-
namespace_owner: namespaceOwner,
|
|
1087
|
-
state: encoded,
|
|
1088
|
-
});
|
|
1089
|
-
}
|
|
1090
|
-
stateChanged = true;
|
|
1091
|
-
rollbackRuntimes = commitRuntimes();
|
|
1092
|
-
if (this.#capabilitiesPublished) {
|
|
1093
|
-
await LoopDocs.materialize(this.#engine, this.#db, checkedWorkspaceId);
|
|
1094
|
-
}
|
|
1095
|
-
}
|
|
1096
|
-
catch (cause) {
|
|
1097
|
-
rollbackRuntimes?.();
|
|
1098
|
-
const rollbackErrors = [];
|
|
1099
|
-
if (stateChanged) {
|
|
1100
|
-
try {
|
|
1101
|
-
if (prior === undefined) {
|
|
1102
|
-
await this.#db.workspace_module_state_delete.run({
|
|
1103
|
-
workspace_id: checkedWorkspaceId,
|
|
1104
|
-
namespace_owner: namespaceOwner,
|
|
1105
|
-
});
|
|
1106
|
-
}
|
|
1107
|
-
else {
|
|
1108
|
-
await this.#db.workspace_module_state_put.run({
|
|
1109
|
-
workspace_id: checkedWorkspaceId,
|
|
1110
|
-
namespace_owner: namespaceOwner,
|
|
1111
|
-
state: prior.state,
|
|
1112
|
-
});
|
|
1113
|
-
}
|
|
1114
|
-
}
|
|
1115
|
-
catch (rollbackCause) {
|
|
1116
|
-
rollbackErrors.push(rollbackCause);
|
|
1117
|
-
}
|
|
1118
|
-
if (this.#capabilitiesPublished) {
|
|
1119
|
-
try {
|
|
1120
|
-
await LoopDocs.materialize(this.#engine, this.#db, checkedWorkspaceId);
|
|
1121
|
-
}
|
|
1122
|
-
catch (rollbackCause) {
|
|
1123
|
-
rollbackErrors.push(rollbackCause);
|
|
1124
|
-
}
|
|
1125
|
-
}
|
|
1126
|
-
}
|
|
1127
|
-
if (rollbackErrors.length > 0) {
|
|
1128
|
-
throw new AggregateError([cause, ...rollbackErrors], "Workspace capability replacement and rollback failed");
|
|
1129
|
-
}
|
|
1130
|
-
throw cause;
|
|
1550
|
+
return await registration.handler(params, context);
|
|
1131
1551
|
}
|
|
1132
1552
|
finally {
|
|
1133
|
-
|
|
1553
|
+
releaseCapabilities?.();
|
|
1134
1554
|
}
|
|
1135
1555
|
}
|
|
1556
|
+
registerWorkerCapabilityProvider(namespaceOwner, provider) {
|
|
1557
|
+
this.#residency.registerProvider(namespaceOwner, provider);
|
|
1558
|
+
}
|
|
1559
|
+
registerFunctionalityAdapter(adapter) {
|
|
1560
|
+
return this.#functionality.register(adapter);
|
|
1561
|
+
}
|
|
1562
|
+
async readWorkerModuleState(workerId, namespaceOwner) {
|
|
1563
|
+
return this.#residency.readModuleState(workerId, namespaceOwner);
|
|
1564
|
+
}
|
|
1565
|
+
async replaceWorkerCapabilities(replacement, options = {}) {
|
|
1566
|
+
await this.#residency.replace(replacement, options);
|
|
1567
|
+
}
|
|
1136
1568
|
get engine() { return this.#engine; }
|
|
1137
1569
|
get provider() { return this.#provider; }
|
|
1138
1570
|
get schemes() { return this.#schemes; }
|
|
@@ -1158,6 +1590,24 @@ export default class Daemon {
|
|
|
1158
1590
|
// shell is the default runtime, so its executor must boot usable.
|
|
1159
1591
|
const executors = await ExecutorRegistry.build({ defaultRuntime: "sh", cwd: this.#discoveryCwd });
|
|
1160
1592
|
this.#engine.setExecutors(executors);
|
|
1593
|
+
this.#engine.setFunctionalityDocuments((workerId) => this.#functionality.documents(workerId));
|
|
1594
|
+
// {§question-tool} — the native request-user-input runtime, process-wide;
|
|
1595
|
+
// per-worker admission gates its doc visibility and dispatch ({§worker-settings}).
|
|
1596
|
+
await this.#engine.registerRuntimes([{
|
|
1597
|
+
tag: "question",
|
|
1598
|
+
entry: {
|
|
1599
|
+
executor: new QuestionTool({ runtime: "question", glyph: "❓" }),
|
|
1600
|
+
namespaceOwner: { kind: "module", name: "core" },
|
|
1601
|
+
glyph: "❓",
|
|
1602
|
+
summary: questionRuntimeDecl.summary,
|
|
1603
|
+
invocation: questionRuntimeDecl.invocation,
|
|
1604
|
+
details: questionRuntimeDecl.details ?? "",
|
|
1605
|
+
available: true,
|
|
1606
|
+
detail: "in-process",
|
|
1607
|
+
},
|
|
1608
|
+
}]);
|
|
1609
|
+
// {§effect-policy-tunable} — invalid operator policy fails boot, not the first EXEC.
|
|
1610
|
+
EffectPolicy.validateConfiguration();
|
|
1161
1611
|
// {§exec} — mint a scheme per runtime tag so exec output entries address by tag
|
|
1162
1612
|
// authority (sh:///l/t/s). The "exec" scheme stays for the EXEC op dispatch.
|
|
1163
1613
|
this.#schemes.registerRuntimeSchemes(executors);
|
|
@@ -1165,25 +1615,24 @@ export default class Daemon {
|
|
|
1165
1615
|
// (agnostic, by plurnk.kind:"scheme"). They light up http://, etc. with
|
|
1166
1616
|
// no further engine change — #run wraps their context in SchemeCtxImpl ({§plugin-discovery}).
|
|
1167
1617
|
await this.#schemes.discoverExternal(this.#discoveryCwd);
|
|
1618
|
+
// {§module-discovery} — third-party daemon-module composition: trusted
|
|
1619
|
+
// packages declaring `plurnk.kind: "module"` register beside the
|
|
1620
|
+
// service's explicit composition before any module setup runs.
|
|
1621
|
+
const discoveredModules = await discoverDaemonModules({ cwd: this.#discoveryCwd });
|
|
1622
|
+
for (const packageName of discoveredModules.skipped) {
|
|
1623
|
+
console.warn(`module discovery: '${packageName}' is discovered but untrusted (PLURNK_PLUGINS_TRUSTED_ONLY); not registered`);
|
|
1624
|
+
}
|
|
1625
|
+
for (const module of discoveredModules.modules) {
|
|
1626
|
+
this.#modules.push(module);
|
|
1627
|
+
}
|
|
1168
1628
|
const setupSeam = this;
|
|
1169
1629
|
for (const module of this.#modules) {
|
|
1170
1630
|
if (module.close !== undefined)
|
|
1171
1631
|
this.#moduleClosers.push(module);
|
|
1172
1632
|
await module.setup?.(setupSeam);
|
|
1173
1633
|
}
|
|
1174
|
-
for (const workspace of await Envelope.listWorkspaces(this.#db)) {
|
|
1175
|
-
for (const provider of this.#workspaceCapabilityProviders.values()) {
|
|
1176
|
-
await provider.hydrate(workspace.id);
|
|
1177
|
-
}
|
|
1178
|
-
}
|
|
1179
1634
|
await this.#schemes.ready();
|
|
1180
|
-
|
|
1181
|
-
// Installed capabilities and operator configuration are now fully known; model loops
|
|
1182
|
-
// consume this workspace state but never republish it.
|
|
1183
|
-
for (const workspace of await Envelope.listWorkspaces(this.#db)) {
|
|
1184
|
-
await LoopDocs.materialize(this.#engine, this.#db, workspace.id);
|
|
1185
|
-
}
|
|
1186
|
-
this.#capabilitiesPublished = true;
|
|
1635
|
+
this.#residency.publish();
|
|
1187
1636
|
await this.#recoverLifecycle();
|
|
1188
1637
|
// {§module-lifecycle} — the daemon opens no transport. Modules start their listeners only
|
|
1189
1638
|
// after capability publication and durable lifecycle recovery are complete.
|
|
@@ -1198,6 +1647,7 @@ export default class Daemon {
|
|
|
1198
1647
|
await this.#db.recovery_fail_active_loops.run({});
|
|
1199
1648
|
await this.#db.recovery_settle_open_provider_requests.run({});
|
|
1200
1649
|
await this.#db.recovery_fail_open_model_calls.run({});
|
|
1650
|
+
await this.#db.recovery_fail_open_turns.run({});
|
|
1201
1651
|
await this.#db.recovery_fail_ownerless_proposals.run({});
|
|
1202
1652
|
await this.#db.recovery_remove_ownerless_client_interactions.run({});
|
|
1203
1653
|
await this.#db.recovery_error_orphan_subscription_channels.run({});
|
|
@@ -1205,11 +1655,24 @@ export default class Daemon {
|
|
|
1205
1655
|
await this.#db.recovery_resume_unblocked_parks.run({});
|
|
1206
1656
|
await this.#branchBatches.recover();
|
|
1207
1657
|
const orphanSources = await this.#db.recovery_orphan_prompt_sources.all({});
|
|
1658
|
+
const invalidOrphan = orphanSources.find(({ origin }) => origin !== "model");
|
|
1659
|
+
if (invalidOrphan !== undefined) {
|
|
1660
|
+
throw new Error(`Orphaned provider work belongs to ${invalidOrphan.origin} worker ${invalidOrphan.worker_id}; only model workers may enter inference.`);
|
|
1661
|
+
}
|
|
1208
1662
|
for (const source of orphanSources) {
|
|
1209
1663
|
await this.#drains.reconcileOrphanedPrompts(source.worker_id, source.loop_id);
|
|
1210
1664
|
}
|
|
1211
1665
|
const systemPrompt = await readFile(Paths.instructionsSystem, "utf8");
|
|
1212
1666
|
const queued = await this.#db.recovery_queued_workers.all({});
|
|
1667
|
+
const invalidQueued = queued.find(({ origin }) => origin !== "model");
|
|
1668
|
+
if (invalidQueued !== undefined) {
|
|
1669
|
+
throw new Error(`Queued provider work belongs to ${invalidQueued.origin} worker ${invalidQueued.worker_id}; only model workers may enter inference.`);
|
|
1670
|
+
}
|
|
1671
|
+
const parked = await this.#db.recovery_parked_workers.all({});
|
|
1672
|
+
const invalidParked = parked.find(({ origin }) => origin !== "model");
|
|
1673
|
+
if (invalidParked !== undefined) {
|
|
1674
|
+
throw new Error(`Parked provider work belongs to ${invalidParked.origin} worker ${invalidParked.worker_id}; only model workers may enter inference.`);
|
|
1675
|
+
}
|
|
1213
1676
|
for (const row of queued) {
|
|
1214
1677
|
const started = await this.#drains.ensureDrain({
|
|
1215
1678
|
workspaceId: row.workspace_id,
|
|
@@ -1217,18 +1680,52 @@ export default class Daemon {
|
|
|
1217
1680
|
systemPrompt,
|
|
1218
1681
|
});
|
|
1219
1682
|
started?.drainPromise.catch((err) => {
|
|
1220
|
-
|
|
1683
|
+
if (this.#started) {
|
|
1684
|
+
console.error(`recovered drain failed for worker ${row.worker_id}:`, err);
|
|
1685
|
+
}
|
|
1221
1686
|
});
|
|
1222
1687
|
}
|
|
1223
|
-
const parked = await this.#db.recovery_parked_workers.all({});
|
|
1224
1688
|
for (const row of parked) {
|
|
1225
1689
|
await this.#drains.schedulePollWake(row.workspace_id, row.worker_id, systemPrompt);
|
|
1226
1690
|
}
|
|
1227
1691
|
}
|
|
1692
|
+
// {§crash-only-stop} — the settle deadline. Default 30s; an operator can
|
|
1693
|
+
// raise it for slow hosts or lower it for tests. 0 is refused (an unbounded
|
|
1694
|
+
// stop is exactly the wedge this exists to prevent).
|
|
1695
|
+
static #stopDeadlineMs() {
|
|
1696
|
+
const raw = Number(process.env.PLURNK_SERVICE_STOP_TIMEOUT_MS ?? 30_000);
|
|
1697
|
+
if (!Number.isSafeInteger(raw) || raw <= 0) {
|
|
1698
|
+
throw new Error(`PLURNK_SERVICE_STOP_TIMEOUT_MS must be a positive integer; got ${JSON.stringify(process.env.PLURNK_SERVICE_STOP_TIMEOUT_MS)}.`);
|
|
1699
|
+
}
|
|
1700
|
+
return raw;
|
|
1701
|
+
}
|
|
1228
1702
|
async stop() {
|
|
1229
1703
|
if (!this.#started)
|
|
1230
1704
|
return;
|
|
1231
1705
|
this.#started = false;
|
|
1706
|
+
// Stop every producer before refusing capability acquisition. A drain
|
|
1707
|
+
// already between queue claim and activation must observe its own abort,
|
|
1708
|
+
// not misclassify orderly shutdown as a capability failure.
|
|
1709
|
+
const derivationAbort = new DOMException("daemon stopping", "AbortError");
|
|
1710
|
+
this.#engine.cancelAllProposals("daemon_stopping");
|
|
1711
|
+
this.#engine.cancelDerivations(derivationAbort);
|
|
1712
|
+
this.#branchBatches.beginStop();
|
|
1713
|
+
this.#drains.beginStop("daemon_stopping");
|
|
1714
|
+
this.#residency.beginStop();
|
|
1715
|
+
const stopDeadlineMs = Daemon.#stopDeadlineMs();
|
|
1716
|
+
const deadline = Date.now() + stopDeadlineMs;
|
|
1717
|
+
const settle = (label, wait) => new Promise((resolve) => {
|
|
1718
|
+
let settled = false;
|
|
1719
|
+
const finish = (result) => {
|
|
1720
|
+
if (settled)
|
|
1721
|
+
return;
|
|
1722
|
+
settled = true;
|
|
1723
|
+
clearTimeout(timer);
|
|
1724
|
+
resolve(result);
|
|
1725
|
+
};
|
|
1726
|
+
const timer = setTimeout(() => finish({ status: "rejected", reason: new Error(`stop deadline exceeded waiting for ${label}`) }), Math.max(0, deadline - Date.now()));
|
|
1727
|
+
Promise.resolve().then(wait).then((value) => finish({ status: "fulfilled", value }), (reason) => finish({ status: "rejected", reason }));
|
|
1728
|
+
});
|
|
1232
1729
|
// Stop accepting external work immediately, but do not await listener
|
|
1233
1730
|
// closure before cancelling active workers: an SSE connection may itself be
|
|
1234
1731
|
// waiting for the worker cancellation that follows.
|
|
@@ -1248,32 +1745,39 @@ export default class Daemon {
|
|
|
1248
1745
|
// Settle the stopped world FIRST: a drain paused at a pending proposal awaits a resolution
|
|
1249
1746
|
// that will never arrive once clients are gone — allSettled(drains) below would deadlock
|
|
1250
1747
|
// the stop forever (a daemon with a pending HITL proposal could not shut down).
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
await this.#branchBatches.idle();
|
|
1257
|
-
const
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
const
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
const
|
|
1748
|
+
// {§crash-only-stop} — the settle sequence is DEADLINE-BOUNDED: a child
|
|
1749
|
+
// that never closes (a wedged MCP server, a stuck stream) must not hang
|
|
1750
|
+
// the daemon forever. Past the deadline the waits are abandoned; the
|
|
1751
|
+
// process may exit with the WAL in place (SQLite recovers) rather than
|
|
1752
|
+
// leak as a live-but-wedged tree.
|
|
1753
|
+
const branchResult = await settle("branch batches idle", () => this.#branchBatches.idle());
|
|
1754
|
+
const drainResult = await settle("drains idle", () => this.#drains.idle());
|
|
1755
|
+
// A boundary publication queued behind the last turn settles before
|
|
1756
|
+
// modules close and before the DB goes away ({§functionality-publication}).
|
|
1757
|
+
const functionalityResult = await settle("functionality publications", () => this.#functionality.settle());
|
|
1758
|
+
const moduleResult = await settle("modules close", async () => {
|
|
1759
|
+
const results = await moduleClose;
|
|
1760
|
+
if (results.some((r) => r.status === "rejected"))
|
|
1761
|
+
throw new AggregateError(results.filter((r) => r.status === "rejected").map((r) => r.reason), "module close failed");
|
|
1762
|
+
});
|
|
1763
|
+
const streamingResult = await settle("streaming schemes idle", () => this.#drainStreamingSchemes());
|
|
1764
|
+
const derivationResult = await settle("derivation drain", () => this.#engine.drainDerivations(derivationAbort));
|
|
1765
|
+
const mimetypeResult = this.#ownsMimetypes
|
|
1766
|
+
? await settle("mimetypes dispose", () => this.#mimetypes.dispose())
|
|
1767
|
+
: null;
|
|
1768
|
+
const schemeResult = await settle("schemes close", () => this.#schemes.close());
|
|
1267
1769
|
// Streaming and scheme closure are the last producers of synchronous
|
|
1268
1770
|
// conclusion notifications. Join the supervisor-owned async tails only
|
|
1269
1771
|
// after those producers settle, before the caller may close SQLite.
|
|
1270
|
-
const
|
|
1772
|
+
const wakeResult = await settle("drains idle (wake)", () => this.#drains.idle());
|
|
1271
1773
|
const closeErrors = [
|
|
1272
|
-
|
|
1774
|
+
moduleResult,
|
|
1775
|
+
functionalityResult,
|
|
1776
|
+
branchResult,
|
|
1273
1777
|
drainResult,
|
|
1274
1778
|
streamingResult,
|
|
1275
1779
|
derivationResult,
|
|
1276
|
-
...
|
|
1780
|
+
...(mimetypeResult === null ? [] : [mimetypeResult]),
|
|
1277
1781
|
schemeResult,
|
|
1278
1782
|
wakeResult,
|
|
1279
1783
|
]
|
|
@@ -1299,6 +1803,10 @@ export default class Daemon {
|
|
|
1299
1803
|
notifyStreamEvent(workspaceId, event) {
|
|
1300
1804
|
this.#broadcast({ workspaceId }, "stream/event", event);
|
|
1301
1805
|
}
|
|
1806
|
+
/** Emit transient readable reasoning for an in-flight model call. */
|
|
1807
|
+
notifyReasoningEvent(workspaceId, event) {
|
|
1808
|
+
this.#broadcast({ workspaceId }, "reasoning/event", event);
|
|
1809
|
+
}
|
|
1302
1810
|
/**
|
|
1303
1811
|
* Emit a transient notice scoped to the workspace containing the loop.
|
|
1304
1812
|
*/
|
|
@@ -1344,9 +1852,11 @@ export default class Daemon {
|
|
|
1344
1852
|
worker_id: workerId,
|
|
1345
1853
|
sequence: seqRow.next,
|
|
1346
1854
|
prompt: first.body,
|
|
1855
|
+
prompt_source: first.prompt_source,
|
|
1347
1856
|
flags: first.flags,
|
|
1348
|
-
|
|
1349
|
-
|
|
1857
|
+
model_route_id: first.model_route_id,
|
|
1858
|
+
spawn_model_route_id: first.spawn_model_route_id,
|
|
1859
|
+
reasoning_policy: first.reasoning_policy,
|
|
1350
1860
|
max_turns: first.max_turns,
|
|
1351
1861
|
open_paths: first.open_paths ?? "[]",
|
|
1352
1862
|
orphan_source_loop_id: endedLoopId,
|
|
@@ -1369,6 +1879,12 @@ export default class Daemon {
|
|
|
1369
1879
|
cancelDrain(workerId, reason = "user_cancelled") {
|
|
1370
1880
|
return this.#drains.cancel(workerId, reason);
|
|
1371
1881
|
}
|
|
1882
|
+
async cancelWorker(args) {
|
|
1883
|
+
const workspaceId = ClientInput.assertId("worker.cancel", "workspaceId", args.workspaceId);
|
|
1884
|
+
const workerId = ClientInput.assertId("worker.cancel", "workerId", args.workerId);
|
|
1885
|
+
await this.#assertWorkerOwned(workspaceId, workerId);
|
|
1886
|
+
await this.#drains.cancelWorkerTree(workerId, args.reason ?? "user_cancelled");
|
|
1887
|
+
}
|
|
1372
1888
|
// Process-local stream activity is supplied to the drain owner through the
|
|
1373
1889
|
// scheme registry rather than exposing that registry across the boundary.
|
|
1374
1890
|
#workerHasActiveStreams(workerId) {
|