@plurnk/plurnk-service 1.17.0 → 1.19.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 +95 -14
- package/README.md +5 -3
- package/SPEC.md +1756 -1442
- package/dist/Paths.js +3 -3
- package/dist/build-info.json +1 -1
- package/dist/content/body-preview.d.ts +1 -0
- package/dist/content/body-preview.d.ts.map +1 -1
- package/dist/content/body-preview.js +8 -10
- package/dist/content/body-preview.js.map +1 -1
- package/dist/content/edit-receipt.d.ts.map +1 -1
- package/dist/content/edit-receipt.js +30 -29
- package/dist/content/edit-receipt.js.map +1 -1
- package/dist/content/line-anchors.d.ts.map +1 -1
- package/dist/content/line-anchors.js +17 -5
- package/dist/content/line-anchors.js.map +1 -1
- package/dist/content/read-projector.d.ts +2 -2
- package/dist/content/read-projector.d.ts.map +1 -1
- package/dist/content/read-projector.js +81 -39
- package/dist/content/read-projector.js.map +1 -1
- package/dist/content/read-resolve.d.ts +1 -0
- package/dist/content/read-resolve.d.ts.map +1 -1
- package/dist/content/read-resolve.js +13 -10
- package/dist/content/read-resolve.js.map +1 -1
- package/dist/content/scope-format.d.ts +9 -0
- package/dist/content/scope-format.d.ts.map +1 -0
- package/dist/content/scope-format.js +28 -0
- package/dist/content/scope-format.js.map +1 -0
- package/dist/core/AdministrativeLoop.d.ts +8 -0
- package/dist/core/AdministrativeLoop.d.ts.map +1 -0
- package/dist/core/AdministrativeLoop.js +16 -0
- package/dist/core/AdministrativeLoop.js.map +1 -0
- package/dist/core/AdmittedTurnExecutor.d.ts +2 -2
- package/dist/core/AdmittedTurnExecutor.d.ts.map +1 -1
- package/dist/core/AdmittedTurnExecutor.js +26 -26
- package/dist/core/AdmittedTurnExecutor.js.map +1 -1
- package/dist/core/BareBatchRunner.d.ts +1 -2
- package/dist/core/BareBatchRunner.d.ts.map +1 -1
- package/dist/core/BareBatchRunner.js +7 -12
- package/dist/core/BareBatchRunner.js.map +1 -1
- package/dist/core/BudgetReadout.d.ts.map +1 -1
- package/dist/core/BudgetReadout.js +3 -4
- package/dist/core/BudgetReadout.js.map +1 -1
- package/dist/core/CapabilityPolicies.d.ts +4 -2
- package/dist/core/CapabilityPolicies.d.ts.map +1 -1
- package/dist/core/CapabilityPolicies.js +23 -2
- package/dist/core/CapabilityPolicies.js.map +1 -1
- package/dist/core/CapabilityResolver.d.ts +2 -2
- package/dist/core/CapabilityResolver.d.ts.map +1 -1
- package/dist/core/CapabilityResolver.js +46 -36
- package/dist/core/CapabilityResolver.js.map +1 -1
- package/dist/core/ChannelWrite.d.ts +4 -6
- package/dist/core/ChannelWrite.d.ts.map +1 -1
- package/dist/core/ChannelWrite.js +8 -8
- package/dist/core/ChannelWrite.js.map +1 -1
- package/dist/core/ChannelWrite.sql +17 -11
- package/dist/core/ClientInteractions.d.ts +11 -1
- package/dist/core/ClientInteractions.d.ts.map +1 -1
- package/dist/core/ClientInteractions.js +26 -2
- package/dist/core/ClientInteractions.js.map +1 -1
- package/dist/core/CoreSchemeServices.d.ts.map +1 -1
- package/dist/core/CoreSchemeServices.js +1 -0
- package/dist/core/CoreSchemeServices.js.map +1 -1
- package/dist/core/DataStatementRunner.d.ts.map +1 -1
- package/dist/core/DataStatementRunner.js +198 -148
- package/dist/core/DataStatementRunner.js.map +1 -1
- package/dist/core/Dispatcher.d.ts +18 -5
- package/dist/core/Dispatcher.d.ts.map +1 -1
- package/dist/core/Dispatcher.js +135 -114
- package/dist/core/Dispatcher.js.map +1 -1
- package/dist/core/Dispatcher.sql +11 -26
- package/dist/core/DurableStatement.d.ts +4 -1
- package/dist/core/DurableStatement.d.ts.map +1 -1
- package/dist/core/DurableStatement.js +4 -4
- package/dist/core/DurableStatement.js.map +1 -1
- package/dist/core/EditMutations.d.ts.map +1 -1
- package/dist/core/EditMutations.js +5 -2
- package/dist/core/EditMutations.js.map +1 -1
- package/dist/core/Engine.d.ts +11 -9
- package/dist/core/Engine.d.ts.map +1 -1
- package/dist/core/Engine.js +27 -65
- package/dist/core/Engine.js.map +1 -1
- package/dist/core/Engine.sql +2 -22
- package/dist/core/EnvFlags.d.ts.map +1 -1
- package/dist/core/EnvFlags.js +4 -2
- package/dist/core/EnvFlags.js.map +1 -1
- package/dist/core/ExecutionOutputs.sql +8 -0
- package/dist/core/ExecutorRegistry.d.ts +4 -0
- package/dist/core/ExecutorRegistry.d.ts.map +1 -1
- package/dist/core/ExecutorRegistry.js +3 -1
- package/dist/core/ExecutorRegistry.js.map +1 -1
- package/dist/core/KillHandler.d.ts.map +1 -1
- package/dist/core/KillHandler.js +7 -12
- package/dist/core/KillHandler.js.map +1 -1
- package/dist/core/Knob.d.ts +9 -0
- package/dist/core/Knob.d.ts.map +1 -0
- package/dist/core/Knob.js +50 -0
- package/dist/core/Knob.js.map +1 -0
- package/dist/core/LogBody.d.ts.map +1 -1
- package/dist/core/LogBody.js +8 -36
- package/dist/core/LogBody.js.map +1 -1
- package/dist/core/LogEntryProjection.d.ts.map +1 -1
- package/dist/core/LogEntryProjection.js +8 -10
- package/dist/core/LogEntryProjection.js.map +1 -1
- package/dist/core/LogVisibility.d.ts.map +1 -1
- package/dist/core/LogVisibility.js +2 -1
- package/dist/core/LogVisibility.js.map +1 -1
- package/dist/core/LogWriter.d.ts.map +1 -1
- package/dist/core/LogWriter.js +11 -9
- package/dist/core/LogWriter.js.map +1 -1
- package/dist/core/LoopDriver.d.ts +1 -1
- package/dist/core/LoopDriver.d.ts.map +1 -1
- package/dist/core/LoopDriver.js +24 -32
- package/dist/core/LoopDriver.js.map +1 -1
- package/dist/core/LoopLifecycle.d.ts +4 -17
- package/dist/core/LoopLifecycle.d.ts.map +1 -1
- package/dist/core/LoopLifecycle.js +20 -22
- package/dist/core/LoopLifecycle.js.map +1 -1
- package/dist/core/LoopLifecycle.sql +36 -49
- package/dist/core/LoopOutcome.d.ts +10 -0
- package/dist/core/LoopOutcome.d.ts.map +1 -0
- package/dist/core/LoopOutcome.js +28 -0
- package/dist/core/LoopOutcome.js.map +1 -0
- package/dist/core/LoopPolicies.d.ts +7 -0
- package/dist/core/LoopPolicies.d.ts.map +1 -0
- package/dist/core/LoopPolicies.js +37 -0
- package/dist/core/LoopPolicies.js.map +1 -0
- package/dist/core/MessageResources.d.ts +15 -0
- package/dist/core/MessageResources.d.ts.map +1 -0
- package/dist/core/MessageResources.js +46 -0
- package/dist/core/MessageResources.js.map +1 -0
- package/dist/core/MessageResources.sql +36 -0
- package/dist/core/ModelCall.d.ts.map +1 -1
- package/dist/core/ModelCall.js +2 -6
- package/dist/core/ModelCall.js.map +1 -1
- package/dist/core/ModelCall.sql +7 -36
- package/dist/core/OperatorConfig.d.ts +2 -1
- package/dist/core/OperatorConfig.d.ts.map +1 -1
- package/dist/core/OperatorConfig.js +14 -6
- package/dist/core/OperatorConfig.js.map +1 -1
- package/dist/core/PacketBuilder.d.ts.map +1 -1
- package/dist/core/PacketBuilder.js +20 -35
- package/dist/core/PacketBuilder.js.map +1 -1
- package/dist/core/PacketBuilder.sql +24 -6
- package/dist/core/PatternSelection.d.ts +1 -1
- package/dist/core/PatternSelection.d.ts.map +1 -1
- package/dist/core/ProposalLifecycle.d.ts +3 -1
- package/dist/core/ProposalLifecycle.d.ts.map +1 -1
- package/dist/core/ProposalLifecycle.js +93 -28
- package/dist/core/ProposalLifecycle.js.map +1 -1
- package/dist/core/ProviderInstantiate.d.ts +1 -0
- package/dist/core/ProviderInstantiate.d.ts.map +1 -1
- package/dist/core/ProviderInstantiate.js +12 -7
- package/dist/core/ProviderInstantiate.js.map +1 -1
- package/dist/core/ReasoningView.d.ts +1 -2
- package/dist/core/ReasoningView.d.ts.map +1 -1
- package/dist/core/ReasoningView.js +9 -20
- package/dist/core/ReasoningView.js.map +1 -1
- package/dist/core/ResourceBindings.d.ts.map +1 -1
- package/dist/core/ResourceBindings.js +5 -4
- package/dist/core/ResourceBindings.js.map +1 -1
- package/dist/core/ResourceMutations.d.ts +4 -4
- package/dist/core/ResourceMutations.d.ts.map +1 -1
- package/dist/core/ResourceMutations.js +2 -3
- package/dist/core/ResourceMutations.js.map +1 -1
- package/dist/core/ResourceSelector.d.ts +10 -3
- package/dist/core/ResourceSelector.d.ts.map +1 -1
- package/dist/core/ResourceSelector.js +52 -13
- package/dist/core/ResourceSelector.js.map +1 -1
- package/dist/core/ResourceTransfers.d.ts.map +1 -1
- package/dist/core/ResourceTransfers.js +4 -2
- package/dist/core/ResourceTransfers.js.map +1 -1
- package/dist/core/RuntimeWorker.d.ts.map +1 -1
- package/dist/core/RuntimeWorker.js +6 -1
- package/dist/core/RuntimeWorker.js.map +1 -1
- package/dist/core/RuntimeWorker.sql +7 -1
- package/dist/core/SchemeRegistry.d.ts +2 -0
- package/dist/core/SchemeRegistry.d.ts.map +1 -1
- package/dist/core/SchemeRegistry.js +12 -6
- package/dist/core/SchemeRegistry.js.map +1 -1
- package/dist/core/StoredPacket.d.ts +3 -1
- package/dist/core/StoredPacket.d.ts.map +1 -1
- package/dist/core/StoredPacket.js +7 -3
- package/dist/core/StoredPacket.js.map +1 -1
- package/dist/core/StrikeRail.d.ts +2 -2
- package/dist/core/StrikeRail.d.ts.map +1 -1
- package/dist/core/StrikeRail.js +15 -7
- package/dist/core/StrikeRail.js.map +1 -1
- package/dist/core/TerminalResult.d.ts +2 -2
- package/dist/core/TerminalResult.d.ts.map +1 -1
- package/dist/core/TerminalResult.js +15 -8
- package/dist/core/TerminalResult.js.map +1 -1
- package/dist/core/ToolInputSchema.d.ts.map +1 -1
- package/dist/core/ToolInputSchema.js +24 -8
- package/dist/core/ToolInputSchema.js.map +1 -1
- package/dist/core/ToolResources.d.ts +1 -0
- package/dist/core/ToolResources.d.ts.map +1 -1
- package/dist/core/ToolResources.js +50 -26
- package/dist/core/ToolResources.js.map +1 -1
- package/dist/core/Turn.d.ts +3 -1
- package/dist/core/Turn.d.ts.map +1 -1
- package/dist/core/Turn.js +4 -1
- package/dist/core/Turn.js.map +1 -1
- package/dist/core/Turn.sql +14 -2
- package/dist/core/TurnDispositionHandler.d.ts +13 -23
- package/dist/core/TurnDispositionHandler.d.ts.map +1 -1
- package/dist/core/TurnDispositionHandler.js +30 -180
- package/dist/core/TurnDispositionHandler.js.map +1 -1
- package/dist/core/TurnMaterialization.d.ts +9 -7
- package/dist/core/TurnMaterialization.d.ts.map +1 -1
- package/dist/core/TurnMaterialization.js +66 -46
- package/dist/core/TurnMaterialization.js.map +1 -1
- package/dist/core/TurnMaterialization.sql +23 -6
- package/dist/core/TurnOps.d.ts.map +1 -1
- package/dist/core/TurnOps.js +2 -2
- package/dist/core/TurnOps.js.map +1 -1
- package/dist/core/TurnRunner.d.ts +2 -8
- package/dist/core/TurnRunner.d.ts.map +1 -1
- package/dist/core/TurnRunner.js +1116 -1123
- package/dist/core/TurnRunner.js.map +1 -1
- package/dist/core/TurnRunner.sql +1 -24
- package/dist/core/TurnSources.sql +42 -11
- package/dist/core/WorkerControlHandler.d.ts.map +1 -1
- package/dist/core/WorkerControlHandler.js +23 -0
- package/dist/core/WorkerControlHandler.js.map +1 -1
- package/dist/core/WorkerName.d.ts +3 -6
- package/dist/core/WorkerName.d.ts.map +1 -1
- package/dist/core/WorkerName.js +10 -26
- package/dist/core/WorkerName.js.map +1 -1
- package/dist/core/WorkspaceGate.js +4 -4
- package/dist/core/WorkspaceGate.js.map +1 -1
- package/dist/core/ambient.sql +45 -19
- package/dist/core/attachments.d.ts +3 -1
- package/dist/core/attachments.d.ts.map +1 -1
- package/dist/core/attachments.js +4 -0
- package/dist/core/attachments.js.map +1 -1
- package/dist/core/caps/CoreInteractionCaps.d.ts +1 -1
- package/dist/core/caps/CoreInteractionCaps.d.ts.map +1 -1
- package/dist/core/caps/CoreInteractionCaps.js +4 -2
- package/dist/core/caps/CoreInteractionCaps.js.map +1 -1
- package/dist/core/caps/DbChannelCaps.js +6 -6
- package/dist/core/caps/DbChannelCaps.js.map +1 -1
- package/dist/core/caps/DbEntryCaps.js +1 -1
- package/dist/core/caps/DbEntryCaps.js.map +1 -1
- package/dist/core/caps/DbMessageCaps.d.ts +10 -0
- package/dist/core/caps/DbMessageCaps.d.ts.map +1 -0
- package/dist/core/caps/DbMessageCaps.js +39 -0
- package/dist/core/caps/DbMessageCaps.js.map +1 -0
- package/dist/core/caps/DbProjectionCaps.d.ts +2 -2
- package/dist/core/caps/DbProjectionCaps.d.ts.map +1 -1
- package/dist/core/caps/DbProjectionCaps.js +10 -4
- package/dist/core/caps/DbProjectionCaps.js.map +1 -1
- package/dist/core/caps/SchemeCtxImpl.d.ts +3 -1
- package/dist/core/caps/SchemeCtxImpl.d.ts.map +1 -1
- package/dist/core/caps/SchemeCtxImpl.js +11 -0
- package/dist/core/caps/SchemeCtxImpl.js.map +1 -1
- package/dist/core/env-catalog.d.ts +28 -0
- package/dist/core/env-catalog.d.ts.map +1 -0
- package/dist/core/env-catalog.js +99 -0
- package/dist/core/env-catalog.js.map +1 -0
- package/dist/core/file-materialization.js +1 -1
- package/dist/core/file-materialization.js.map +1 -1
- package/dist/core/fork.sql +7 -6
- package/dist/core/git-state.d.ts +3 -2
- package/dist/core/git-state.d.ts.map +1 -1
- package/dist/core/git-state.js +38 -21
- package/dist/core/git-state.js.map +1 -1
- package/dist/core/operation-target-groups.d.ts.map +1 -1
- package/dist/core/operation-target-groups.js +2 -1
- package/dist/core/operation-target-groups.js.map +1 -1
- package/dist/core/packet-inject.js +1 -1
- package/dist/core/packet-wire.d.ts +11 -0
- package/dist/core/packet-wire.d.ts.map +1 -1
- package/dist/core/packet-wire.js +382 -333
- package/dist/core/packet-wire.js.map +1 -1
- package/dist/core/plurnk-uri.d.ts +4 -5
- package/dist/core/plurnk-uri.d.ts.map +1 -1
- package/dist/core/plurnk-uri.js +10 -31
- package/dist/core/plurnk-uri.js.map +1 -1
- package/dist/core/scheme-types.d.ts +3 -1
- package/dist/core/scheme-types.d.ts.map +1 -1
- package/dist/core/scheme-types.js +1 -1
- package/dist/core/scheme-types.js.map +1 -1
- package/dist/core/teaching.d.ts.map +1 -1
- package/dist/core/teaching.js +2 -1
- package/dist/core/teaching.js.map +1 -1
- package/dist/core/types.d.ts +1 -2
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/core/worker-ops.sql +3 -8
- package/dist/digest/Digest.d.ts.map +1 -1
- package/dist/digest/Digest.js +37 -30
- package/dist/digest/Digest.js.map +1 -1
- package/dist/digest/DigestRender.d.ts +2 -0
- package/dist/digest/DigestRender.d.ts.map +1 -1
- package/dist/digest/DigestRender.js +97 -9
- package/dist/digest/DigestRender.js.map +1 -1
- package/dist/digest/DigestRequiem.d.ts.map +1 -1
- package/dist/digest/DigestRequiem.js +13 -15
- package/dist/digest/DigestRequiem.js.map +1 -1
- package/dist/digest/digest-db.d.ts +3 -0
- package/dist/digest/digest-db.d.ts.map +1 -0
- package/dist/digest/digest-db.js +68 -0
- package/dist/digest/digest-db.js.map +1 -0
- package/dist/digest/digest-paths.d.ts +6 -0
- package/dist/digest/digest-paths.d.ts.map +1 -0
- package/dist/digest/digest-paths.js +23 -0
- package/dist/digest/digest-paths.js.map +1 -0
- package/dist/digest/digest-rows.d.ts +21 -0
- package/dist/digest/digest-rows.d.ts.map +1 -1
- package/dist/digest/digest.sql +24 -5
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/index.js.map +1 -1
- package/dist/observe/genai.d.ts +2 -2
- package/dist/observe/genai.d.ts.map +1 -1
- package/dist/observe/genai.js +15 -5
- package/dist/observe/genai.js.map +1 -1
- package/dist/schemes/EffectPolicy.d.ts +0 -2
- package/dist/schemes/EffectPolicy.d.ts.map +1 -1
- package/dist/schemes/EffectPolicy.js +25 -36
- package/dist/schemes/EffectPolicy.js.map +1 -1
- package/dist/schemes/Exec.d.ts +4 -5
- package/dist/schemes/Exec.d.ts.map +1 -1
- package/dist/schemes/Exec.js +129 -82
- package/dist/schemes/Exec.js.map +1 -1
- package/dist/schemes/ExecOutputScheme.d.ts +6 -5
- package/dist/schemes/ExecOutputScheme.d.ts.map +1 -1
- package/dist/schemes/ExecOutputScheme.js +49 -16
- package/dist/schemes/ExecOutputScheme.js.map +1 -1
- package/dist/schemes/ExecScheduler.js +2 -2
- package/dist/schemes/ExecScheduler.js.map +1 -1
- package/dist/schemes/File.d.ts.map +1 -1
- package/dist/schemes/File.js +30 -21
- package/dist/schemes/File.js.map +1 -1
- package/dist/schemes/Log.d.ts +4 -4
- package/dist/schemes/Log.d.ts.map +1 -1
- package/dist/schemes/Log.js +30 -10
- package/dist/schemes/Log.js.map +1 -1
- package/dist/schemes/QuestionTool.d.ts.map +1 -1
- package/dist/schemes/QuestionTool.js +15 -2
- package/dist/schemes/QuestionTool.js.map +1 -1
- package/dist/schemes/TurnSource.d.ts +3 -2
- package/dist/schemes/TurnSource.d.ts.map +1 -1
- package/dist/schemes/TurnSource.js +87 -41
- package/dist/schemes/TurnSource.js.map +1 -1
- package/dist/schemes/Worker.d.ts.map +1 -1
- package/dist/schemes/Worker.js +22 -56
- package/dist/schemes/Worker.js.map +1 -1
- package/dist/schemes/_entry-crud.js +6 -6
- package/dist/schemes/_entry-crud.js.map +1 -1
- package/dist/schemes/_entry-crud.sql +37 -17
- package/dist/schemes/_entry-find.d.ts.map +1 -1
- package/dist/schemes/_entry-find.js +6 -5
- package/dist/schemes/_entry-find.js.map +1 -1
- package/dist/schemes/_entry-fts.d.ts.map +1 -1
- package/dist/schemes/_entry-fts.js +6 -3
- package/dist/schemes/_entry-fts.js.map +1 -1
- package/dist/schemes/_entry-fts.sql +25 -6
- package/dist/schemes/_entry-manifest.d.ts +1 -1
- package/dist/schemes/_entry-manifest.d.ts.map +1 -1
- package/dist/schemes/_entry-manifest.js +10 -17
- package/dist/schemes/_entry-manifest.js.map +1 -1
- package/dist/schemes/_entry-manifest.sql +1 -1
- package/dist/schemes/_entry-ops.js +4 -4
- package/dist/schemes/_entry-ops.js.map +1 -1
- package/dist/schemes/_entry-ops.sql +2 -2
- package/dist/schemes/_entry-readable.d.ts.map +1 -1
- package/dist/schemes/_entry-readable.js +5 -4
- package/dist/schemes/_entry-readable.js.map +1 -1
- package/dist/schemes/_entry-send.js +2 -2
- package/dist/schemes/_entry-send.js.map +1 -1
- package/dist/schemes/_search-exclusion.d.ts +1 -0
- package/dist/schemes/_search-exclusion.d.ts.map +1 -1
- package/dist/schemes/_search-exclusion.js +12 -0
- package/dist/schemes/_search-exclusion.js.map +1 -1
- package/dist/schemes/_search-index.d.ts +5 -1
- package/dist/schemes/_search-index.d.ts.map +1 -1
- package/dist/schemes/_search-index.js +99 -59
- package/dist/schemes/_search-index.js.map +1 -1
- package/dist/schemes/_search-index.sql +17 -0
- package/dist/schemes/exec-abort.d.ts.map +1 -1
- package/dist/schemes/exec-abort.js +5 -4
- package/dist/schemes/exec-abort.js.map +1 -1
- package/dist/schemes/exec-env.d.ts +3 -0
- package/dist/schemes/exec-env.d.ts.map +1 -1
- package/dist/schemes/exec-env.js +66 -15
- package/dist/schemes/exec-env.js.map +1 -1
- package/dist/schemes/exec-lifetime.d.ts +11 -0
- package/dist/schemes/exec-lifetime.d.ts.map +1 -0
- package/dist/schemes/exec-lifetime.js +29 -0
- package/dist/schemes/exec-lifetime.js.map +1 -0
- package/dist/schemes/exec-runtime.d.ts +1 -1
- package/dist/schemes/exec-runtime.d.ts.map +1 -1
- package/dist/schemes/exec-runtime.js +1 -1
- package/dist/schemes/exec-runtime.js.map +1 -1
- package/dist/server/ClientReads.d.ts.map +1 -1
- package/dist/server/ClientReads.js +5 -2
- package/dist/server/ClientReads.js.map +1 -1
- package/dist/server/Daemon.d.ts +31 -7
- package/dist/server/Daemon.d.ts.map +1 -1
- package/dist/server/Daemon.js +137 -60
- package/dist/server/Daemon.js.map +1 -1
- package/dist/server/DaemonModule.d.ts +25 -6
- package/dist/server/DaemonModule.d.ts.map +1 -1
- package/dist/server/DrainSupervisor.d.ts +13 -11
- package/dist/server/DrainSupervisor.d.ts.map +1 -1
- package/dist/server/DrainSupervisor.js +87 -98
- package/dist/server/DrainSupervisor.js.map +1 -1
- package/dist/server/EnvFunctionality.d.ts +52 -0
- package/dist/server/EnvFunctionality.d.ts.map +1 -0
- package/dist/server/EnvFunctionality.js +199 -0
- package/dist/server/EnvFunctionality.js.map +1 -0
- package/dist/server/Functionality.d.ts +4 -2
- package/dist/server/Functionality.d.ts.map +1 -1
- package/dist/server/Functionality.js +250 -106
- package/dist/server/Functionality.js.map +1 -1
- package/dist/server/FunctionalityManager.d.ts +5 -1
- package/dist/server/FunctionalityManager.d.ts.map +1 -1
- package/dist/server/FunctionalityManager.js +24 -8
- package/dist/server/FunctionalityManager.js.map +1 -1
- package/dist/server/HttpListener.d.ts +16 -0
- package/dist/server/HttpListener.d.ts.map +1 -0
- package/dist/server/HttpListener.js +81 -0
- package/dist/server/HttpListener.js.map +1 -0
- package/dist/server/MembersFunctionality.d.ts.map +1 -1
- package/dist/server/MembersFunctionality.js +12 -17
- package/dist/server/MembersFunctionality.js.map +1 -1
- package/dist/server/PlurnkSkill.d.ts.map +1 -1
- package/dist/server/PlurnkSkill.js +1 -0
- package/dist/server/PlurnkSkill.js.map +1 -1
- package/dist/server/Retention.d.ts +14 -0
- package/dist/server/Retention.d.ts.map +1 -1
- package/dist/server/Retention.js +52 -4
- package/dist/server/Retention.js.map +1 -1
- package/dist/server/Retention.sql +55 -1
- package/dist/server/ServiceModules.d.ts.map +1 -1
- package/dist/server/ServiceModules.js +2 -0
- package/dist/server/ServiceModules.js.map +1 -1
- package/dist/server/SkillsFunctionality.d.ts +2 -0
- package/dist/server/SkillsFunctionality.d.ts.map +1 -1
- package/dist/server/SkillsFunctionality.js +27 -17
- package/dist/server/SkillsFunctionality.js.map +1 -1
- package/dist/server/WorkspaceCapabilities.d.ts.map +1 -1
- package/dist/server/WorkspaceCapabilities.js +1 -0
- package/dist/server/WorkspaceCapabilities.js.map +1 -1
- package/dist/server/WorkspaceResidency.d.ts +2 -0
- package/dist/server/WorkspaceResidency.d.ts.map +1 -1
- package/dist/server/WorkspaceResidency.js +27 -0
- package/dist/server/WorkspaceResidency.js.map +1 -1
- package/dist/server/WorkspaceStorage.d.ts +8 -0
- package/dist/server/WorkspaceStorage.d.ts.map +1 -0
- package/dist/server/WorkspaceStorage.js +26 -0
- package/dist/server/WorkspaceStorage.js.map +1 -0
- package/dist/server/client-input.d.ts +4 -3
- package/dist/server/client-input.d.ts.map +1 -1
- package/dist/server/client-input.js +25 -27
- package/dist/server/client-input.js.map +1 -1
- package/dist/server/dispatch-as-plurnk.js +2 -2
- package/dist/server/dispatch-as-plurnk.js.map +1 -1
- package/dist/server/drain.sql +69 -132
- package/dist/server/envelope.d.ts +2 -3
- package/dist/server/envelope.d.ts.map +1 -1
- package/dist/server/envelope.js +7 -21
- package/dist/server/envelope.js.map +1 -1
- package/dist/server/envelope.sql +10 -6
- package/dist/server/exec-poll-backoff.js +3 -3
- package/dist/server/exec-poll-backoff.js.map +1 -1
- package/dist/server/lifecycle-recovery.sql +12 -38
- package/dist/server/logEntry.sql +2 -2
- package/dist/server/loopDocs.js +3 -3
- package/dist/server/loopDocs.sql +4 -2
- package/dist/server/model-catalog.d.ts.map +1 -1
- package/dist/server/model-catalog.js +2 -2
- package/dist/server/model-catalog.js.map +1 -1
- package/dist/server/seam-loop.sql +0 -1
- package/dist/server/workspace-capabilities.sql +45 -0
- package/dist/server/workspace-storage.sql +8 -0
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +37 -11
- package/dist/service.js.map +1 -1
- package/docs/copy-move.md +31 -0
- package/docs/env.md +67 -0
- package/docs/members.md +4 -4
- package/docs/skills.md +2 -2
- package/migrations/002_workers.sql +41 -17
- package/migrations/003_loops.sql +82 -28
- package/migrations/004_inference.sql +53 -12
- package/migrations/005_entries.sql +135 -14
- package/migrations/006_log.sql +79 -24
- package/migrations/007_subscriptions.sql +10 -15
- package/package.json +45 -36
- package/dist/core/PromptFrames.d.ts +0 -12
- package/dist/core/PromptFrames.d.ts.map +0 -1
- package/dist/core/PromptFrames.js +0 -28
- package/dist/core/PromptFrames.js.map +0 -1
- package/dist/schemes/Prompt.d.ts +0 -8
- package/dist/schemes/Prompt.d.ts.map +0 -1
- package/dist/schemes/Prompt.js +0 -25
- package/dist/schemes/Prompt.js.map +0 -1
package/dist/core/TurnRunner.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
var _a;
|
|
2
2
|
import NativeContent from "./NativeContent.js";
|
|
3
|
-
import {
|
|
3
|
+
import { PlurnkParser } from "@plurnk/plurnk-parser";
|
|
4
|
+
import { PathSyntax, PlurnkParseError, UNKNOWN_POSITION } from "@plurnk/plurnk-contracts";
|
|
5
|
+
import LoopPolicyReader from "./LoopPolicyReader.js";
|
|
4
6
|
import { setTimeout as delay } from "node:timers/promises";
|
|
5
7
|
import { aggregateProviderAccounting } from "@plurnk/plurnk-providers";
|
|
6
8
|
const comparePosition = (a, b) => a.line - b.line || a.column - b.column;
|
|
@@ -9,9 +11,7 @@ import GitMembership from "./git-membership.js";
|
|
|
9
11
|
import { acceptedKinds } from "./attachments.js";
|
|
10
12
|
import GitState from "./git-state.js";
|
|
11
13
|
import WorkspaceSettings from "./workspace-settings.js";
|
|
12
|
-
import {
|
|
13
|
-
import { generatedPathname, promptLoopPrefix } from "./plurnk-uri.js";
|
|
14
|
-
import PromptFrames from "./PromptFrames.js";
|
|
14
|
+
import { generatedPathname } from "./plurnk-uri.js";
|
|
15
15
|
import { readFile } from "node:fs/promises";
|
|
16
16
|
import { resolve as resolvePath } from "node:path";
|
|
17
17
|
import { homedir } from "node:os";
|
|
@@ -34,22 +34,13 @@ import Results, { OperationFailureError } from "./results.js";
|
|
|
34
34
|
import Turn from "./Turn.js";
|
|
35
35
|
import StoredPacket from "./StoredPacket.js";
|
|
36
36
|
import { observed, observedSync } from "../observe/spans.js";
|
|
37
|
-
import {
|
|
37
|
+
import { genAiRequestName, genAiRequestOptions, settleGenAiResponse } from "../observe/genai.js";
|
|
38
38
|
import { PROVIDER_CALLS, recordCounter } from "../observe/metrics.js";
|
|
39
39
|
import ModelCall, { ModelCallPersistenceError, ProviderAccountingIntegrityError } from "./ModelCall.js";
|
|
40
40
|
import WorkerName from "./WorkerName.js";
|
|
41
41
|
import TurnOps from "./TurnOps.js";
|
|
42
42
|
import CapabilityPolicies from "./CapabilityPolicies.js";
|
|
43
43
|
import CapabilityResolver from "./CapabilityResolver.js";
|
|
44
|
-
// {§prompt-address}: the same literal address in every observer's packet.
|
|
45
|
-
const promptTarget = (workerName, storage) => {
|
|
46
|
-
return {
|
|
47
|
-
kind: "url", raw: `prompt://${workerName}${storage}`,
|
|
48
|
-
scheme: "prompt", username: null, password: null,
|
|
49
|
-
hostname: workerName, port: null,
|
|
50
|
-
pathname: storage, query: null, fragment: null,
|
|
51
|
-
};
|
|
52
|
-
};
|
|
53
44
|
const regexLiteral = (value) => value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
54
45
|
const workerCatalogTarget = (namespace) => {
|
|
55
46
|
const pathname = generatedPathname(`/${namespace}/*.md`);
|
|
@@ -72,22 +63,6 @@ const assertOpenPaths = (value, source) => {
|
|
|
72
63
|
}
|
|
73
64
|
return value;
|
|
74
65
|
};
|
|
75
|
-
const parsePromptAttributes = (encoded, source) => {
|
|
76
|
-
const attributes = JSON.parse(encoded);
|
|
77
|
-
if (attributes === null || typeof attributes !== "object" || Array.isArray(attributes)) {
|
|
78
|
-
throw new TypeError(`${source}: expected a JSON object`);
|
|
79
|
-
}
|
|
80
|
-
return attributes;
|
|
81
|
-
};
|
|
82
|
-
const promptSourceFromAttributes = (attributes, source) => {
|
|
83
|
-
const value = attributes.source;
|
|
84
|
-
if (value === undefined)
|
|
85
|
-
return null;
|
|
86
|
-
if (typeof value !== "string" || value.length === 0) {
|
|
87
|
-
throw new TypeError(`${source}: source must be a non-empty string when present`);
|
|
88
|
-
}
|
|
89
|
-
return value;
|
|
90
|
-
};
|
|
91
66
|
// Per-emission action ceiling — OFF by default. `-1` (or unset/non-positive) = no cap:
|
|
92
67
|
// every generated op dispatches. Runaway degeneration is a sampler concern (repetition penalty),
|
|
93
68
|
// not grounds to drop already-generated work. A positive value is an operator ceiling a
|
|
@@ -147,14 +122,31 @@ const readMilliseconds = (key) => {
|
|
|
147
122
|
return value;
|
|
148
123
|
};
|
|
149
124
|
// {§provider-recovery} — how long one turn keeps re-issuing its provider call after a
|
|
150
|
-
// recoverable failure before the loop parks (0 parks at once),
|
|
151
|
-
//
|
|
125
|
+
// recoverable failure before the loop parks (0 parks at once), the first backoff delay, which
|
|
126
|
+
// doubles per failure, and that delay's ceiling.
|
|
152
127
|
const readProviderRecovery = () => readMilliseconds("PLURNK_SERVICE_PROVIDER_RECOVERY");
|
|
153
128
|
const readProviderRecoveryBackoff = () => readMilliseconds("PLURNK_SERVICE_PROVIDER_RECOVERY_BACKOFF");
|
|
129
|
+
const readProviderRecoveryBackoffMax = () => readMilliseconds("PLURNK_SERVICE_PROVIDER_RECOVERY_BACKOFF_MAX");
|
|
154
130
|
const RECOVERABLE_PROVIDER_FAILURES = new Set(["rate_limit", "network_failure", "deadline_exceeded", "resource_interrupted"]);
|
|
155
131
|
// The wall's abort reason — runLoop branches a mid-turn teardown to the 504 terminal on it.
|
|
156
132
|
export const LOOP_TIMEOUT_REASON = "loop_timeout";
|
|
157
|
-
|
|
133
|
+
// Every turn result carries the same shape; a diversion names only what differs.
|
|
134
|
+
const turnResult = ({ createdTurnIds, turnId }, status, facts = {}) => ({
|
|
135
|
+
createdTurnIds,
|
|
136
|
+
turnId,
|
|
137
|
+
producer: "model",
|
|
138
|
+
kind: "inference",
|
|
139
|
+
status,
|
|
140
|
+
outcomes: [],
|
|
141
|
+
fingerprint: "",
|
|
142
|
+
capacityHardStop: false,
|
|
143
|
+
providerParked: false,
|
|
144
|
+
emptyTurn: false,
|
|
145
|
+
emissionAttempts: 0,
|
|
146
|
+
emissionExhausted: false,
|
|
147
|
+
...facts,
|
|
148
|
+
});
|
|
149
|
+
class TurnRunner {
|
|
158
150
|
#db;
|
|
159
151
|
#schemes;
|
|
160
152
|
#mimetypes;
|
|
@@ -203,8 +195,8 @@ export default class TurnRunner {
|
|
|
203
195
|
this.#warmWorkspace = warmWorkspace;
|
|
204
196
|
this.#dispatch = dispatch;
|
|
205
197
|
this.#resolveWorkerProviderIdentity = resolveWorkerProviderIdentity;
|
|
206
|
-
this.#materialization = new TurnMaterialization({ db: this.#db, weighContent: this.#weighContent });
|
|
207
|
-
this.#bareBatch = new BareBatchRunner({ db: this.#db, providerAttributions: this.#providerAttributions.bind(this), providerFailure:
|
|
198
|
+
this.#materialization = new TurnMaterialization({ db: this.#db, weighContent: this.#weighContent, mimetypes: this.#mimetypes });
|
|
199
|
+
this.#bareBatch = new BareBatchRunner({ db: this.#db, providerAttributions: this.#providerAttributions.bind(this), providerFailure: _a.#providerFailure });
|
|
208
200
|
this.#admitted = new AdmittedTurnExecutor({ db: this.#db, schemes: this.#schemes, notices: this.#notices, problems: this.#problems, dispatcher: this.#dispatcher, bareBatch: this.#bareBatch });
|
|
209
201
|
}
|
|
210
202
|
async #recordInference(args) {
|
|
@@ -218,7 +210,6 @@ export default class TurnRunner {
|
|
|
218
210
|
workerId: args.workerId,
|
|
219
211
|
loopId: args.loopId,
|
|
220
212
|
packetCount: packetCount.count,
|
|
221
|
-
...taskTiming(packetCount),
|
|
222
213
|
});
|
|
223
214
|
}
|
|
224
215
|
// {§operator-grammar} — the operator's grammar text for this provider, or undefined when
|
|
@@ -244,8 +235,6 @@ export default class TurnRunner {
|
|
|
244
235
|
process.stderr.write(`plurnk-engine: operator grammar: ${alias || "(bare)"} → ${path} (${text.length} chars)\n`);
|
|
245
236
|
return text;
|
|
246
237
|
}
|
|
247
|
-
// A lineage's no-parent root; a root worker resolves to itself. Fail hard
|
|
248
|
-
// when corruption leaves a worker without one. {§worker-primary}
|
|
249
238
|
async #attemptAttributions(provider, context) {
|
|
250
239
|
const tags = Meta.composeAttributions(this.#schemes.attributions(context), this.#executors()?.attributions(context) ?? [], await this.#mimetypes.attributions(context), provider.attributions?.(context) ?? []);
|
|
251
240
|
return [...tags];
|
|
@@ -300,1110 +289,1076 @@ export default class TurnRunner {
|
|
|
300
289
|
}, (kind) => accepted.includes(kind));
|
|
301
290
|
return { messages, nativeInputs: [...nativeInputs] };
|
|
302
291
|
}
|
|
303
|
-
|
|
292
|
+
// One inference cycle: the six phases in order, over the records declared above.
|
|
293
|
+
// A diversion (curation overflow, a provider failure, an exhausted emission)
|
|
294
|
+
// completes the turn and returns; anything else that throws fails every turn
|
|
295
|
+
// this cycle opened.
|
|
296
|
+
async runTurn({ provider, childProvider = provider, messages, recap = "", workspaceId, workerId, loopId, signal, onDispatch, onSettled, turnNumber = 1, invalidEmissionRecoveryEntryId, }) {
|
|
297
|
+
const args = {
|
|
298
|
+
provider, childProvider, messages, recap, workspaceId, workerId, loopId, signal, onDispatch, onSettled,
|
|
299
|
+
turnNumber, invalidEmissionRecoveryEntryId,
|
|
300
|
+
};
|
|
301
|
+
const createdTurnIds = [];
|
|
302
|
+
try {
|
|
303
|
+
const container = await this.#openTurnContainer(args, createdTurnIds);
|
|
304
|
+
const gitStatus = await this.#deriveWorkspace(args, container.systemCtx);
|
|
305
|
+
if (container.initializationTurn !== null)
|
|
306
|
+
await this.#runInitializationTurn(args, container, container.initializationTurn);
|
|
307
|
+
const request = await this.#composeRequest(args, container, gitStatus);
|
|
308
|
+
const overflow = this.#packets.curationOverflow(request.packet);
|
|
309
|
+
if (overflow !== null)
|
|
310
|
+
return await this.#failCuration(request, overflow);
|
|
311
|
+
const attempts = await this.#prepareProviderAttempts(args, request);
|
|
312
|
+
let emission;
|
|
313
|
+
try {
|
|
314
|
+
emission = await this.#attemptProvider(args, request, attempts);
|
|
315
|
+
}
|
|
316
|
+
catch (error) {
|
|
317
|
+
return await this.#settleProviderFailure(error, args, request, attempts);
|
|
318
|
+
}
|
|
319
|
+
if (!emission.split.emissionValid)
|
|
320
|
+
return await this.#rejectExhaustedEmission(args, request, emission);
|
|
321
|
+
await this.#recordAdmittedEmission(args, request, emission);
|
|
322
|
+
return await this.#settleAdmittedTurn(args, request, emission);
|
|
323
|
+
}
|
|
324
|
+
catch (cause) {
|
|
325
|
+
const completionFailures = [];
|
|
326
|
+
for (const createdTurnId of createdTurnIds) {
|
|
327
|
+
try {
|
|
328
|
+
await Turn.failOpen(this.#db, createdTurnId);
|
|
329
|
+
}
|
|
330
|
+
catch (completionCause) {
|
|
331
|
+
completionFailures.push(completionCause);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if (completionFailures.length > 0) {
|
|
335
|
+
throw new AggregateError([cause, ...completionFailures], "turn execution failed and its open turn containers could not all be completed");
|
|
336
|
+
}
|
|
337
|
+
throw cause;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
// Phase 1 — the producer-neutral turn container. Every producer opens the same
|
|
341
|
+
// durable turn and completes it only after its ordered operations settle; model
|
|
342
|
+
// packets and provider metadata are optional inference evidence, not turn identity.
|
|
343
|
+
async #openTurnContainer(args, createdTurnIds) {
|
|
344
|
+
const { workspaceId, workerId, loopId, invalidEmissionRecoveryEntryId } = args;
|
|
304
345
|
const workerName = await WorkerName.forId(this.#db, workerId);
|
|
305
346
|
const transientOpenLogEntryId = typeof invalidEmissionRecoveryEntryId === "number"
|
|
306
347
|
? invalidEmissionRecoveryEntryId
|
|
307
348
|
: null;
|
|
308
|
-
// === Producer-neutral turn container ===
|
|
309
|
-
//
|
|
310
|
-
// Every producer opens the same durable turn and completes it only
|
|
311
|
-
// after its ordered operations settle. Model packets and provider
|
|
312
|
-
// metadata are optional inference evidence, not turn identity.
|
|
313
349
|
const initialSequence = await this.#db.engine_next_turn_sequence.get({ loop_id: loopId });
|
|
314
350
|
if (initialSequence === undefined)
|
|
315
351
|
throw new Error("Engine.runTurn: next turn sequence is unavailable");
|
|
316
352
|
// Turn-0 foists that belong to the Worker (catalog preview, AGENTS) gate
|
|
317
353
|
// on its first inference history, not on loop sequence: honest client or
|
|
318
354
|
// runtime administrative turns may precede the first provider exchange.
|
|
319
|
-
// Per-loop foists such as
|
|
320
|
-
// {§
|
|
321
|
-
// rather than the model-turn ordinal, prevents an overflow diversion
|
|
322
|
-
//
|
|
323
|
-
const
|
|
355
|
+
// Per-loop foists such as the initial message's publication
|
|
356
|
+
// ({§message-arrival}) fire once per loop: the inbox row's publication state,
|
|
357
|
+
// rather than the model-turn ordinal, prevents an overflow diversion from
|
|
358
|
+
// replaying a message and its automatic path READs.
|
|
359
|
+
const loopSequence = (await this.#db.engine_loop_sequence.get({ loop_id: loopId }))?.sequence ?? loopId;
|
|
324
360
|
const priorInference = await this.#db.engine_worker_has_inference_history.get({
|
|
325
361
|
worker_id: workerId,
|
|
326
362
|
});
|
|
327
363
|
const workerFirstInference = priorInference?.present === 0;
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
mimetypes: this.#mimetypes,
|
|
364
|
-
defaultChannelFor: (s) => this.#schemes.defaultChannelFor(s, workspaceId),
|
|
365
|
-
pushNotice: (notice) => this.#notices.push(workspaceId, workerId, loopId, notice),
|
|
366
|
-
requestInteraction: (request, signal = this.#loopSignal(loopId)) => this.#interactions.request(request, { workspaceId, workerId, loopId, turnId: contextTurnId }, signal),
|
|
364
|
+
// {§worker-initialization-entry} — turn zero is an ordinary `_plurnk`
|
|
365
|
+
// operation turn. Its model-facing zero is a phase label; durable turn
|
|
366
|
+
// coordinates remain one-based.
|
|
367
|
+
const initializationTurn = initialSequence.next === 1 && workerFirstInference
|
|
368
|
+
? await Turn.open(this.#db, { loopId, producer: "_plurnk", kind: "initialization" })
|
|
369
|
+
: null;
|
|
370
|
+
if (initializationTurn !== null)
|
|
371
|
+
createdTurnIds.push(initializationTurn.id);
|
|
372
|
+
const initializationPolicies = initializationTurn === null
|
|
373
|
+
? []
|
|
374
|
+
: (await CapabilityPolicies.layers(this.#db, workspaceId, loopId)).map((layer) => layer.policy);
|
|
375
|
+
const modelTurn = initializationTurn === null
|
|
376
|
+
? await Turn.open(this.#db, { loopId, producer: "model", kind: "inference" })
|
|
377
|
+
: null;
|
|
378
|
+
if (modelTurn !== null)
|
|
379
|
+
createdTurnIds.push(modelTurn.id);
|
|
380
|
+
// {§env-delta-log-pull} — creation atomically owns the ordinary worker
|
|
381
|
+
// baseline or fork snapshot. Packet assembly consumes, never invents,
|
|
382
|
+
// that durable observation boundary.
|
|
383
|
+
const ambientCursor = await this.#db.engine_initialize_ambient_cursor.get({ workspace_id: workspaceId, worker_id: workerId });
|
|
384
|
+
if (ambientCursor?.ambient_event_cursor === null || ambientCursor === undefined) {
|
|
385
|
+
throw new Error(`worker ${workerId} has no durable ambient observation boundary`);
|
|
386
|
+
}
|
|
387
|
+
const systemCtx = this.#schemeContext(args, initializationTurn?.id ?? modelTurn.id);
|
|
388
|
+
const initializationStatements = [];
|
|
389
|
+
// {§worker-initialization-entry} — the worker's first turn is the worked
|
|
390
|
+
// example itself: the actual orienting operations and ordinary NOTEs.
|
|
391
|
+
// {§turn0-agents-stunt} — the project AGENTS.md (materialized by LoopDocs as
|
|
392
|
+
// worker:///_plurnk/AGENTS.md) gets one foisted READ on the worker's first
|
|
393
|
+
// loop, so local repo guidance is visible turn-0 content. Global policy
|
|
394
|
+
// stays in the system prompt; nothing else is force-read.
|
|
395
|
+
if (initializationTurn !== null) {
|
|
396
|
+
initializationStatements.push({
|
|
397
|
+
op: "NOTE", aside: null, metadata: null, target: null, lineMarker: null,
|
|
398
|
+
body: `This turn surveys tooling and environment. The log records results; ops://${workerName}/${loopSequence}/${initializationTurn.sequence} contains the submitted OPs.`, position: UNKNOWN_POSITION,
|
|
367
399
|
});
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
source: loopRow.prompt_source, pathname: loopRow.prompt_pathname,
|
|
380
|
-
openPaths: assertOpenPaths(JSON.parse(loopRow.open_paths), `Loop ${loopId} open_paths`),
|
|
381
|
-
})),
|
|
382
|
-
openPaths: assertOpenPaths(JSON.parse(loopRow.open_paths), `Loop ${loopId} open_paths`),
|
|
383
|
-
}
|
|
384
|
-
: null;
|
|
385
|
-
const initializationStatements = [];
|
|
386
|
-
// {§worker-initialization-entry} — the worker's first turn is the worked
|
|
387
|
-
// example itself: the actual orienting operations and an ordinary TASK.
|
|
388
|
-
if (initializationTurn !== null) {
|
|
389
|
-
// {§turn0-agents-stunt} — the project AGENTS.md (materialized by LoopDocs as
|
|
390
|
-
// worker:///_plurnk/agents.md) gets one foisted READ on the worker's first
|
|
391
|
-
// loop, so local repo guidance is visible turn-0 content. Global policy
|
|
392
|
-
// stays in the system prompt; nothing else is force-read.
|
|
393
|
-
if (workerFirstInference) {
|
|
394
|
-
const agentsEntry = await this.#db.crud_find_workspace_entry.get({
|
|
395
|
-
workspace_id: workspaceId,
|
|
396
|
-
scheme: "worker",
|
|
397
|
-
authority: "",
|
|
398
|
-
pathname: generatedPathname("/agents.md"),
|
|
399
|
-
});
|
|
400
|
-
if (agentsEntry !== undefined) {
|
|
401
|
-
const agentsTarget = {
|
|
402
|
-
kind: "url", raw: "worker:///_plurnk/agents.md", scheme: "worker",
|
|
403
|
-
username: null, password: null, hostname: null, port: null,
|
|
404
|
-
pathname: generatedPathname("/agents.md"), query: null, fragment: null,
|
|
405
|
-
};
|
|
406
|
-
const agentsRead = {
|
|
407
|
-
op: "READ", aside: null, target: agentsTarget,
|
|
408
|
-
metadata: null, lineMarker: null, matcher: null, body: null, position: UNKNOWN_POSITION,
|
|
409
|
-
};
|
|
410
|
-
initializationStatements.push(agentsRead);
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
// The persistent search-index pass (_search-index.maintain) attaches
|
|
415
|
-
// every readable entry/log projection to complete graph/FTS derivations.
|
|
416
|
-
// NOT an action: no log entry, no sequence slot,
|
|
417
|
-
// not dispatched. There is no materialized manifest entry — the catalog
|
|
418
|
-
// is served on demand by FIND: recursive when asked, shallow-mapped below.
|
|
419
|
-
// SPEC {§membership} D4/D5 — git-ls-files workspace membership, resolved at
|
|
420
|
-
// prompt-composition (EMI is eager + exhaustive — git is the only bound). When the
|
|
421
|
-
// workspace's project_root is a git working tree, tracked files are
|
|
422
|
-
// members without a client `add`; active members are materialized
|
|
423
|
-
// (disk → body channel) so they appear in the catalog. No-ops
|
|
424
|
-
// on headless / non-git workspaces. Runs BEFORE the derivation pump so
|
|
425
|
-
// this turn's packet reflects them.
|
|
426
|
-
// Joining here is the correctness boundary: passive workspace creation and
|
|
427
|
-
// attachment spend nothing, while the model never runs against partial
|
|
428
|
-
// graph/vector coverage.
|
|
429
|
-
await this.#warmWorkspace(systemCtx, false);
|
|
430
|
-
// The warm materialized membership before deriving. This second pass is the
|
|
431
|
-
// ordinary cheap change detector and captures any drift that landed meanwhile.
|
|
432
|
-
const fsDivergences = await GitMembership.indexGitMembership(systemCtx);
|
|
433
|
-
// {§packet-git-status} — one post-reconciliation snapshot supplies both
|
|
434
|
-
// the compact packet summary and each causal file event's exact XY state.
|
|
435
|
-
const gitStatus = await GitState.status(this.#db, workspaceId, this.#loopSignal(loopId));
|
|
436
|
-
await this.#materialization.logFsFictions(workspaceId, fsDivergences, gitStatus);
|
|
437
|
-
// The refresh above may have changed bodies (including model/client edits
|
|
438
|
-
// since the startup warm). Re-derive to completion before packet/model
|
|
439
|
-
// construction. Membership is already current, so this pass does not
|
|
440
|
-
// consume the filesystem divergences a second time.
|
|
441
|
-
await this.#warmWorkspace(systemCtx, true, false);
|
|
442
|
-
// Turn-0 catalog preview (PLURNK_SERVICE_FILES_ITEMS, {§actor-boundary-catalog-preview}):
|
|
443
|
-
// Eight bodyless FIND surveys in the worker's packetless initialization turn establish the Agent
|
|
444
|
-
// Skills, the plurnk references, the enabled tools, agents, and members, then the project, commons,
|
|
445
|
-
// and named scratch, in that order.
|
|
446
|
-
// Their `init` classification lets the model curate this opening survey as one log set.
|
|
447
|
-
if (initializationTurn !== null) {
|
|
448
|
-
// {§operator-config-workspace-files-items} — workspace filesItems replaces the env default.
|
|
449
|
-
const { filesItems: workspaceMI } = await WorkspaceSettings.read(this.#db, workspaceId);
|
|
450
|
-
const filesItems = workspaceMI !== null ? normalizeFilesItems(workspaceMI) : readFilesItems();
|
|
451
|
-
if (filesItems !== null && workerFirstInference) { // {§actor-boundary-catalog-preview} — once per worker
|
|
452
|
-
const catalogSchemes = await this.#db.engine_scheme_catalog_summary.all({ workspace_id: workspaceId });
|
|
453
|
-
const fileItems = catalogSchemes.find(({ scheme }) => scheme === "file")?.shallow_items ?? 0;
|
|
454
|
-
const fileCap = filesItems > 0 && fileItems > 0 ? Math.min(filesItems, fileItems) : null;
|
|
455
|
-
// {§tools-resource-materialization} — expanded families project complete
|
|
456
|
-
// invocation blocks, paged through ordinary FIND result ranges.
|
|
457
|
-
const registry = this.#executors();
|
|
458
|
-
const plurnkCatalog = workerCatalogTarget("plurnk");
|
|
459
|
-
const toolsCatalog = workerCatalogTarget("tools");
|
|
460
|
-
const toolExpansions = [];
|
|
461
|
-
for (const tag of registry?.availableRuntimes(workspaceId) ?? []) {
|
|
462
|
-
const entry = registry?.entry(tag, workspaceId);
|
|
463
|
-
if (entry?.resourcesPath !== "/tools" || entry.expandTools !== true)
|
|
464
|
-
continue;
|
|
465
|
-
const tools = registry?.toolRegistry(tag, workspaceId);
|
|
466
|
-
const admittedTools = tools?.tools.filter((tool) => this.#capabilities.allowsRuntimeAcross(tag, tool.target, workspaceId, initializationPolicies)) ?? [];
|
|
467
|
-
if (admittedTools.length === 0)
|
|
468
|
-
continue;
|
|
469
|
-
const targetFilter = tools !== null && tools !== undefined && admittedTools.length !== tools.tools.length
|
|
470
|
-
? " \\((?:" + admittedTools.map((tool) => regexLiteral(PathSyntax.escapeTarget(tool.target))).join("|") + ")\\)"
|
|
471
|
-
: " ";
|
|
472
|
-
const pattern = "^(\x60{3,})" + regexLiteral(tag) + targetFilter + "[^\\n]*?(?:\\1|\\n[\\s\\S]*?\\n\\1)$";
|
|
473
|
-
toolExpansions.push({
|
|
474
|
-
statement: {
|
|
475
|
-
op: "FIND", aside: null,
|
|
476
|
-
target: { kind: "url", raw: `worker:///_plurnk/tools/${tag}.md`, scheme: "worker", username: null, password: null, hostname: null, port: null, pathname: generatedPathname(`/tools/${tag}.md`), query: null, fragment: null },
|
|
477
|
-
metadata: null,
|
|
478
|
-
matcher: { dialect: "regex", raw: `/${pattern.replaceAll("/", "\\/")}/m`, pattern, flags: "m" },
|
|
479
|
-
body: null, lineMarker: null, position: UNKNOWN_POSITION,
|
|
480
|
-
},
|
|
481
|
-
});
|
|
482
|
-
}
|
|
483
|
-
const surveys = [
|
|
484
|
-
{
|
|
485
|
-
statement: {
|
|
486
|
-
op: "FIND", aside: null,
|
|
487
|
-
target: { kind: "url", raw: "skill://*/SKILL.md", scheme: "skill", username: null, password: null, hostname: "*", port: null, pathname: "/SKILL.md", query: null, fragment: null },
|
|
488
|
-
metadata: null,
|
|
489
|
-
matcher: null, body: null, lineMarker: { marks: [1, -1] }, position: UNKNOWN_POSITION,
|
|
490
|
-
},
|
|
491
|
-
},
|
|
492
|
-
...(plurnkCatalog === null ? [] : [{
|
|
493
|
-
statement: {
|
|
494
|
-
op: "FIND", aside: null,
|
|
495
|
-
target: plurnkCatalog,
|
|
496
|
-
metadata: null,
|
|
497
|
-
matcher: null, body: null, lineMarker: { marks: [1, -1] }, position: UNKNOWN_POSITION,
|
|
498
|
-
},
|
|
499
|
-
}]),
|
|
500
|
-
...(toolsCatalog === null ? [] : [{
|
|
501
|
-
// {§tools-resource-materialization} — enabled tool families
|
|
502
|
-
// (MCP servers) survey at family level; each row's summary is
|
|
503
|
-
// the server one-liner or its flagship invocation form, so the
|
|
504
|
-
// discovery row itself orients. Servers named in
|
|
505
|
-
// PLURNK_MCP_EXPANDED add a second survey of their complete
|
|
506
|
-
// tool tree.
|
|
507
|
-
statement: {
|
|
508
|
-
op: "FIND", aside: null,
|
|
509
|
-
target: toolsCatalog,
|
|
510
|
-
metadata: null,
|
|
511
|
-
matcher: null, body: null, lineMarker: { marks: [1, -1] }, position: UNKNOWN_POSITION,
|
|
512
|
-
},
|
|
513
|
-
}]),
|
|
514
|
-
...toolExpansions,
|
|
515
|
-
{
|
|
516
|
-
// {§a2a-agents-catalog} — enabled outbound agents survey at
|
|
517
|
-
// alias level; each row's summary is the agent's identity line,
|
|
518
|
-
// the exact card stays pullable through READ a2a://<alias>.
|
|
519
|
-
statement: {
|
|
520
|
-
op: "FIND", aside: null,
|
|
521
|
-
target: { kind: "url", raw: "worker:///_plurnk/agents/*.md", scheme: "worker", username: null, password: null, hostname: null, port: null, pathname: generatedPathname("/agents/*.md"), query: null, fragment: null },
|
|
522
|
-
metadata: null,
|
|
523
|
-
matcher: null, body: null, lineMarker: { marks: [1, -1] }, position: UNKNOWN_POSITION,
|
|
524
|
-
},
|
|
525
|
-
},
|
|
526
|
-
{
|
|
527
|
-
// {§members-projection} — enabled members definitions survey at alias
|
|
528
|
-
// level; each row's summary is what its glob resolved to.
|
|
529
|
-
statement: {
|
|
530
|
-
op: "FIND", aside: null,
|
|
531
|
-
target: { kind: "url", raw: "worker:///_plurnk/members/*.md", scheme: "worker", username: null, password: null, hostname: null, port: null, pathname: generatedPathname("/members/*.md"), query: null, fragment: null },
|
|
532
|
-
metadata: null,
|
|
533
|
-
matcher: null, body: null, lineMarker: { marks: [1, -1] }, position: UNKNOWN_POSITION,
|
|
534
|
-
},
|
|
535
|
-
},
|
|
536
|
-
{
|
|
537
|
-
statement: {
|
|
538
|
-
op: "FIND", aside: "project root member files",
|
|
539
|
-
target: { kind: "local", raw: "*" },
|
|
540
|
-
metadata: null,
|
|
541
|
-
matcher: null,
|
|
542
|
-
body: null,
|
|
543
|
-
lineMarker: fileCap === null ? null : { marks: [1, fileCap] },
|
|
544
|
-
position: UNKNOWN_POSITION,
|
|
545
|
-
},
|
|
546
|
-
},
|
|
547
|
-
{
|
|
548
|
-
statement: {
|
|
549
|
-
op: "FIND", aside: "shared worker Extended Context",
|
|
550
|
-
target: { kind: "url", raw: "worker:///*", scheme: "worker", username: null, password: null, hostname: null, port: null, pathname: "/*", query: null, fragment: null },
|
|
551
|
-
metadata: null,
|
|
552
|
-
matcher: null, body: null, lineMarker: null, position: UNKNOWN_POSITION,
|
|
553
|
-
},
|
|
554
|
-
},
|
|
555
|
-
{
|
|
556
|
-
statement: {
|
|
557
|
-
op: "FIND", aside: "private worker Extended Context",
|
|
558
|
-
target: { kind: "url", raw: `worker://${workerName}/*`, scheme: "worker", username: null, password: null, hostname: workerName, port: null, pathname: "/*", query: null, fragment: null },
|
|
559
|
-
metadata: null,
|
|
560
|
-
matcher: null, body: null, lineMarker: null, position: UNKNOWN_POSITION,
|
|
561
|
-
},
|
|
562
|
-
},
|
|
563
|
-
];
|
|
564
|
-
initializationStatements.push(...surveys.map(({ statement }) => statement).filter(({ target }) => this.#schemes.get(target?.kind === "url" ? target.scheme : "file", workspaceId) !== undefined));
|
|
565
|
-
}
|
|
566
|
-
const task = {
|
|
567
|
-
op: "TASK", aside: null, target: null, metadata: null, lineMarker: null,
|
|
568
|
-
body: [{ content: "Address the prompt.", status: "in_progress" }],
|
|
569
|
-
position: UNKNOWN_POSITION,
|
|
400
|
+
const agentsEntry = await this.#db.crud_find_workspace_entry.get({
|
|
401
|
+
workspace_id: workspaceId,
|
|
402
|
+
scheme: "worker",
|
|
403
|
+
authority: "",
|
|
404
|
+
pathname: generatedPathname("/AGENTS.md"),
|
|
405
|
+
});
|
|
406
|
+
if (agentsEntry !== undefined) {
|
|
407
|
+
const agentsTarget = {
|
|
408
|
+
kind: "url", raw: "worker:///_plurnk/AGENTS.md", scheme: "worker",
|
|
409
|
+
username: null, password: null, hostname: null, port: null,
|
|
410
|
+
pathname: generatedPathname("/AGENTS.md"), query: null, fragment: null,
|
|
570
411
|
};
|
|
571
|
-
const
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
if (reasoningRead !== null)
|
|
577
|
-
initializationStatements.push(reasoningRead);
|
|
578
|
-
initializationStatements.push({
|
|
579
|
-
op: "READ", aside: "inspect this turn's emission", matcher: null, body: null, metadata: null,
|
|
580
|
-
target: {
|
|
581
|
-
kind: "url", raw: `ops://${pathname}`, scheme: "ops", pathname,
|
|
582
|
-
username: null, password: null, hostname: null, port: null, query: null, fragment: null,
|
|
583
|
-
},
|
|
584
|
-
lineMarker: { marks: [1, -1] }, position: UNKNOWN_POSITION,
|
|
585
|
-
});
|
|
586
|
-
// {§prompt-entry} — the prompt reaches the model as its `prompt` row in the first
|
|
587
|
-
// model turn; initialization does not READ it a second time.
|
|
588
|
-
initializationStatements.push(task);
|
|
589
|
-
const admittedInitializationStatements = initializationStatements.filter(initializationAdmits);
|
|
590
|
-
const source = TurnOps.renderInternal(admittedInitializationStatements);
|
|
591
|
-
const admitted = TurnOps.parseInternal(source);
|
|
592
|
-
const result = await this.executeAdmittedTurn({
|
|
593
|
-
statements: admitted,
|
|
594
|
-
source,
|
|
595
|
-
origin: "_plurnk",
|
|
596
|
-
workspaceId,
|
|
597
|
-
workerId,
|
|
598
|
-
loopId,
|
|
599
|
-
turnId: initializationTurn.id,
|
|
600
|
-
fromSequence: 1,
|
|
601
|
-
failOnOperationError: true,
|
|
602
|
-
signal: this.#loopSignal(loopId),
|
|
603
|
-
onDispatch,
|
|
604
|
-
onSettled,
|
|
605
|
-
});
|
|
606
|
-
if (result.status !== TURN_STATUS_IMPLICIT_CONTINUE) {
|
|
607
|
-
throw new Error(`initialization TASK returned ${result.status}; expected ${TURN_STATUS_IMPLICIT_CONTINUE}`);
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
// Initialization is a complete preceding turn, not a set of rows
|
|
611
|
-
// interleaved with the model boundary. The engine may now acquire the
|
|
612
|
-
// inference turn inside the same warmed workspace cycle.
|
|
613
|
-
if (modelTurn === null) {
|
|
614
|
-
modelTurn = await Turn.open(this.#db, { loopId, producer: "model", kind: "inference" });
|
|
615
|
-
createdTurnIds.push(modelTurn.id);
|
|
616
|
-
}
|
|
617
|
-
const seq = modelTurn.sequence;
|
|
618
|
-
const turnId = modelTurn.id;
|
|
619
|
-
systemCtx = systemContext(turnId);
|
|
620
|
-
// Pre-model writes. Each prompt the model has not seen yet becomes a
|
|
621
|
-
// `prompt` operation row whose target is its durable prompt:// entry.
|
|
622
|
-
// Model operations continue the same turn sequence after these rows.
|
|
623
|
-
let nextActionIndex = 1;
|
|
624
|
-
const turnOpenPaths = [];
|
|
625
|
-
if (promptPublication !== null) { // {§prompt-entry} — one durable publication per loop
|
|
626
|
-
turnOpenPaths.push(...promptPublication.openPaths);
|
|
627
|
-
const promptLogId = await this.#materialization.writePromptLog({
|
|
628
|
-
workerId,
|
|
629
|
-
loopId,
|
|
630
|
-
turnId,
|
|
631
|
-
sequence: nextActionIndex++,
|
|
632
|
-
target: promptPublication.path,
|
|
633
|
-
content: promptPublication.content,
|
|
634
|
-
source: promptPublication.source,
|
|
635
|
-
});
|
|
636
|
-
onDispatch?.(promptLogId);
|
|
637
|
-
await onSettled?.(promptLogId);
|
|
412
|
+
const agentsRead = {
|
|
413
|
+
op: "READ", aside: null, target: agentsTarget,
|
|
414
|
+
metadata: null, lineMarker: null, matcher: null, body: null, position: UNKNOWN_POSITION,
|
|
415
|
+
};
|
|
416
|
+
initializationStatements.push(agentsRead);
|
|
638
417
|
}
|
|
639
|
-
|
|
640
|
-
|
|
418
|
+
}
|
|
419
|
+
return {
|
|
420
|
+
workerName, transientOpenLogEntryId, loopSequence, createdTurnIds,
|
|
421
|
+
initializationTurn, initializationPolicies, initializationStatements,
|
|
422
|
+
modelTurn, systemCtx,
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
// The `_plurnk` scheme context for one turn. Threaded per turn, never engine
|
|
426
|
+
// state, so concurrent loops on different providers each read their own honest
|
|
427
|
+
// tokenizer values.
|
|
428
|
+
#schemeContext({ workspaceId, workerId, loopId }, turnId) {
|
|
429
|
+
return {
|
|
430
|
+
db: this.#db, workspaceId, workerId, loopId, turnId,
|
|
431
|
+
writer: "_plurnk",
|
|
432
|
+
signal: this.#loopSignal(loopId),
|
|
433
|
+
streamEventNotify: this.#streamEventNotify,
|
|
434
|
+
wakeWorkerNotify: this.#wakeWorkerNotify,
|
|
435
|
+
weigh: this.#weighContent,
|
|
436
|
+
mimetypes: this.#mimetypes,
|
|
437
|
+
defaultChannelFor: (s) => this.#schemes.defaultChannelFor(s, workspaceId),
|
|
438
|
+
pushNotice: (notice) => this.#notices.push(workspaceId, workerId, loopId, notice),
|
|
439
|
+
requestInteraction: (request, signal = this.#loopSignal(loopId)) => this.#interactions.request(request, { workspaceId, workerId, loopId, turnId }, signal),
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
// Phase 2 — membership and derivation to completion, then the one git snapshot
|
|
443
|
+
// every packet rebuild reads.
|
|
444
|
+
async #deriveWorkspace({ workspaceId, loopId }, systemCtx) {
|
|
445
|
+
// The persistent search-index pass (_search-index.maintain) attaches
|
|
446
|
+
// every readable entry/log projection to complete graph/FTS derivations.
|
|
447
|
+
// NOT an action: no log entry, no sequence slot,
|
|
448
|
+
// not dispatched. There is no materialized manifest entry — the catalog
|
|
449
|
+
// is served on demand by FIND: recursive when asked, shallow-mapped below.
|
|
450
|
+
// SPEC {§membership} D4/D5 — git-ls-files workspace membership, resolved at
|
|
451
|
+
// prompt-composition (EMI is eager + exhaustive — git is the only bound). When the
|
|
452
|
+
// workspace's project_root is a git working tree, tracked files are
|
|
453
|
+
// members without a client `add`; active members are materialized
|
|
454
|
+
// (disk → body channel) so they appear in the catalog. No-ops
|
|
455
|
+
// on headless / non-git workspaces. Runs BEFORE the derivation pump so
|
|
456
|
+
// this turn's packet reflects them.
|
|
457
|
+
// Joining here is the correctness boundary: passive workspace creation and
|
|
458
|
+
// attachment spend nothing, while the model never runs against partial
|
|
459
|
+
// graph/vector coverage.
|
|
460
|
+
await this.#warmWorkspace(systemCtx, false);
|
|
461
|
+
// The warm materialized membership before deriving. This second pass is the
|
|
462
|
+
// ordinary cheap change detector and captures any drift that landed meanwhile.
|
|
463
|
+
const fsDivergences = await GitMembership.indexGitMembership(systemCtx);
|
|
464
|
+
// {§packet-git-status} — one post-reconciliation snapshot supplies both
|
|
465
|
+
// the compact packet summary and each causal file event's exact XY state.
|
|
466
|
+
const gitStatus = await GitState.status(this.#db, workspaceId, this.#loopSignal(loopId));
|
|
467
|
+
await this.#materialization.logFsFictions(workspaceId, fsDivergences, gitStatus);
|
|
468
|
+
// The refresh above may have changed bodies (including model/client edits
|
|
469
|
+
// since the startup warm). Re-derive to completion before packet/model
|
|
470
|
+
// construction. Membership is already current, so this pass does not
|
|
471
|
+
// consume the filesystem divergences a second time.
|
|
472
|
+
await this.#warmWorkspace(systemCtx, true, false);
|
|
473
|
+
return gitStatus;
|
|
474
|
+
}
|
|
475
|
+
// {§worker-initialization-entry} — the worker's first turn is the worked example
|
|
476
|
+
// itself: the actual orienting operations and ordinary NOTEs, executed as a
|
|
477
|
+
// complete turn before the model boundary.
|
|
478
|
+
async #runInitializationTurn(args, container, initializationTurn) {
|
|
479
|
+
const { provider, workspaceId, workerId, loopId, onDispatch, onSettled } = args;
|
|
480
|
+
const { workerName, loopSequence, initializationStatements, initializationPolicies } = container;
|
|
481
|
+
// Turn-0 catalog preview (PLURNK_SERVICE_FILES_ITEMS, {§actor-boundary-catalog-preview}):
|
|
482
|
+
// Eight bodyless FIND surveys in the worker's packetless initialization turn establish the Agent
|
|
483
|
+
// Skills, the plurnk references, the enabled tools, agents, and members, then the project, commons,
|
|
484
|
+
// and named scratch, in that order.
|
|
485
|
+
// Their `init` classification lets the model curate this opening survey as one log set.
|
|
486
|
+
// {§operator-config-workspace-files-items} — workspace filesItems replaces the env default.
|
|
487
|
+
const { filesItems: workspaceMI } = await WorkspaceSettings.read(this.#db, workspaceId);
|
|
488
|
+
const filesItems = workspaceMI !== null ? normalizeFilesItems(workspaceMI) : readFilesItems();
|
|
489
|
+
if (filesItems !== null) { // {§actor-boundary-catalog-preview} — once per worker
|
|
490
|
+
initializationStatements.push(...await this.#catalogSurveys(args, container, filesItems));
|
|
491
|
+
}
|
|
492
|
+
const pathname = `/${loopSequence}/${initializationTurn.sequence}`;
|
|
493
|
+
const reasoning = ReasoningView.initialSource();
|
|
494
|
+
await Turn.recordSource(this.#db, initializationTurn.id, "reasoning", reasoning);
|
|
495
|
+
const reasoningRead = ReasoningView.initialRead(provider, workerName, loopSequence, initializationTurn.sequence);
|
|
496
|
+
if (reasoningRead !== null)
|
|
497
|
+
initializationStatements.push(reasoningRead);
|
|
498
|
+
initializationStatements.push({
|
|
499
|
+
op: "READ", aside: "inspect this turn's emission", matcher: null, body: null, metadata: null,
|
|
500
|
+
target: {
|
|
501
|
+
kind: "url", raw: `ops://${workerName}${pathname}`, scheme: "ops", pathname,
|
|
502
|
+
username: null, password: null, hostname: workerName, port: null, query: null, fragment: null,
|
|
503
|
+
},
|
|
504
|
+
lineMarker: { marks: [1, -1] }, position: UNKNOWN_POSITION,
|
|
505
|
+
});
|
|
506
|
+
// {§message-arrival} — the message reaches the model as an inbound SEND in the first
|
|
507
|
+
// model turn; initialization does not READ it a second time.
|
|
508
|
+
const admittedInitializationStatements = initializationStatements.filter((statement) => this.#capabilities.allowsAcross(statement, workspaceId, initializationPolicies));
|
|
509
|
+
const source = admittedInitializationStatements.length === 0 ? "" : TurnOps.renderInternal(admittedInitializationStatements);
|
|
510
|
+
const admitted = [...PlurnkParser.parseReasoningNotes(reasoning), ...(source.length === 0 ? [] : TurnOps.parseInternal(source))];
|
|
511
|
+
const result = await this.executeAdmittedTurn({
|
|
512
|
+
statements: admitted,
|
|
513
|
+
source,
|
|
514
|
+
origin: "_plurnk",
|
|
515
|
+
workspaceId,
|
|
516
|
+
workerId,
|
|
517
|
+
loopId,
|
|
518
|
+
turnId: initializationTurn.id,
|
|
519
|
+
fromSequence: 1,
|
|
520
|
+
failOnOperationError: true,
|
|
521
|
+
signal: this.#loopSignal(loopId),
|
|
522
|
+
onDispatch,
|
|
523
|
+
onSettled,
|
|
524
|
+
});
|
|
525
|
+
if (result.status !== 200) {
|
|
526
|
+
throw new Error(`initialization returned ${result.status}; expected 200`);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
// {§actor-boundary-catalog-preview} — the opening surveys, each admitted only when
|
|
530
|
+
// its scheme is registered for the workspace. A positive filesItems caps the
|
|
531
|
+
// project rows.
|
|
532
|
+
async #catalogSurveys({ workspaceId }, { workerName, initializationPolicies }, filesItems) {
|
|
533
|
+
const catalogSchemes = await this.#db.engine_scheme_catalog_summary.all({ workspace_id: workspaceId });
|
|
534
|
+
const fileItems = catalogSchemes.find(({ scheme }) => scheme === "file")?.shallow_items ?? 0;
|
|
535
|
+
const fileCap = filesItems > 0 && fileItems > 0 ? Math.min(filesItems, fileItems) : null;
|
|
536
|
+
const surveys = [
|
|
641
537
|
{
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
538
|
+
op: "FIND", aside: null,
|
|
539
|
+
target: { kind: "url", raw: "skill://*/SKILL.md", scheme: "skill", username: null, password: null, hostname: "*", port: null, pathname: "/SKILL.md", query: null, fragment: null },
|
|
540
|
+
metadata: null,
|
|
541
|
+
matcher: null, body: null, lineMarker: { marks: [1, -1] }, position: UNKNOWN_POSITION,
|
|
542
|
+
},
|
|
543
|
+
{
|
|
544
|
+
op: "FIND", aside: null,
|
|
545
|
+
target: workerCatalogTarget("plurnk"),
|
|
546
|
+
metadata: null,
|
|
547
|
+
matcher: null, body: null, lineMarker: { marks: [1, -1] }, position: UNKNOWN_POSITION,
|
|
548
|
+
},
|
|
549
|
+
{
|
|
550
|
+
// {§tools-resource-materialization} — enabled tool families
|
|
551
|
+
// (MCP servers) survey at family level; each row's summary is
|
|
552
|
+
// the server one-liner or its flagship invocation form, so the
|
|
553
|
+
// discovery row itself orients. Servers named in
|
|
554
|
+
// PLURNK_MCP_EXPANDED add a second survey of their complete
|
|
555
|
+
// tool tree.
|
|
556
|
+
op: "FIND", aside: null,
|
|
557
|
+
target: workerCatalogTarget("tools"),
|
|
558
|
+
metadata: null,
|
|
559
|
+
matcher: null, body: null, lineMarker: { marks: [1, -1] }, position: UNKNOWN_POSITION,
|
|
560
|
+
},
|
|
561
|
+
...this.#toolExpansions(workspaceId, initializationPolicies),
|
|
562
|
+
{
|
|
563
|
+
// {§a2a-catalog} — enabled outbound agents survey at
|
|
564
|
+
// alias level; each row's summary is the agent's identity line,
|
|
565
|
+
// the exact card stays pullable through READ a2a://<alias>.
|
|
566
|
+
op: "FIND", aside: null,
|
|
567
|
+
target: { kind: "url", raw: "worker:///_plurnk/a2a/*.md", scheme: "worker", username: null, password: null, hostname: null, port: null, pathname: generatedPathname("/a2a/*.md"), query: null, fragment: null },
|
|
568
|
+
metadata: null,
|
|
569
|
+
matcher: null, body: null, lineMarker: { marks: [1, -1] }, position: UNKNOWN_POSITION,
|
|
570
|
+
},
|
|
571
|
+
{
|
|
572
|
+
// {§members-projection} — enabled members definitions survey at alias
|
|
573
|
+
// level; each row's summary is what its glob resolved to.
|
|
574
|
+
op: "FIND", aside: null,
|
|
575
|
+
target: { kind: "url", raw: "worker:///_plurnk/members/*.md", scheme: "worker", username: null, password: null, hostname: null, port: null, pathname: generatedPathname("/members/*.md"), query: null, fragment: null },
|
|
576
|
+
metadata: null,
|
|
577
|
+
matcher: null, body: null, lineMarker: { marks: [1, -1] }, position: UNKNOWN_POSITION,
|
|
578
|
+
},
|
|
579
|
+
{
|
|
580
|
+
op: "FIND", aside: "project root member files",
|
|
581
|
+
target: { kind: "local", raw: "*" },
|
|
582
|
+
metadata: null,
|
|
583
|
+
matcher: null,
|
|
584
|
+
body: null,
|
|
585
|
+
lineMarker: fileCap === null ? null : { marks: [1, fileCap] },
|
|
586
|
+
position: UNKNOWN_POSITION,
|
|
587
|
+
},
|
|
588
|
+
{
|
|
589
|
+
op: "FIND", aside: "workspace knowledgebase entries",
|
|
590
|
+
target: { kind: "url", raw: "worker:///*", scheme: "worker", username: null, password: null, hostname: null, port: null, pathname: "/*", query: null, fragment: null },
|
|
591
|
+
metadata: null,
|
|
592
|
+
matcher: null, body: null, lineMarker: null, position: UNKNOWN_POSITION,
|
|
593
|
+
},
|
|
594
|
+
{
|
|
595
|
+
op: "FIND", aside: "worker knowledgebase entries",
|
|
596
|
+
target: { kind: "url", raw: `worker://${workerName}/*`, scheme: "worker", username: null, password: null, hostname: workerName, port: null, pathname: "/*", query: null, fragment: null },
|
|
597
|
+
metadata: null,
|
|
598
|
+
matcher: null, body: null, lineMarker: null, position: UNKNOWN_POSITION,
|
|
599
|
+
},
|
|
600
|
+
];
|
|
601
|
+
return surveys.filter(({ target }) => this.#schemes.get(target?.kind === "url" ? target.scheme : "file", workspaceId) !== undefined);
|
|
602
|
+
}
|
|
603
|
+
// {§tools-resource-materialization} — expanded families project complete
|
|
604
|
+
// invocation blocks, paged through ordinary FIND result ranges: one pattern FIND
|
|
605
|
+
// per family, naming the admitted tools when the policy narrows them.
|
|
606
|
+
#toolExpansions(workspaceId, policies) {
|
|
607
|
+
const registry = this.#executors();
|
|
608
|
+
const expansions = [];
|
|
609
|
+
for (const tag of registry?.availableRuntimes(workspaceId) ?? []) {
|
|
610
|
+
const entry = registry?.entry(tag, workspaceId);
|
|
611
|
+
if (entry?.resourcesPath !== "/tools" || entry.expandTools !== true)
|
|
612
|
+
continue;
|
|
613
|
+
const tools = registry?.toolRegistry(tag, workspaceId);
|
|
614
|
+
const admittedTools = tools?.tools.filter((tool) => this.#capabilities.allowsRuntimeAcross(tag, tool.target, workspaceId, policies)) ?? [];
|
|
615
|
+
if (admittedTools.length === 0)
|
|
616
|
+
continue;
|
|
617
|
+
const targetFilter = tools !== null && tools !== undefined && admittedTools.length !== tools.tools.length
|
|
618
|
+
? " \\((?:" + admittedTools.map((tool) => regexLiteral(PathSyntax.escapeTarget(tool.target))).join("|") + ")\\)"
|
|
619
|
+
: " ";
|
|
620
|
+
const pattern = "^(\x60{3,})" + regexLiteral(tag) + targetFilter + "[^\\n]*?(?:\\1|\\n[\\s\\S]*?\\n\\1)$";
|
|
621
|
+
expansions.push({
|
|
622
|
+
op: "FIND", aside: null,
|
|
623
|
+
target: { kind: "url", raw: `worker:///_plurnk/tools/${tag}.md`, scheme: "worker", username: null, password: null, hostname: null, port: null, pathname: generatedPathname(`/tools/${tag}.md`), query: null, fragment: null },
|
|
624
|
+
metadata: null,
|
|
625
|
+
matcher: { dialect: "regex", raw: `/${pattern.replaceAll("/", "\\/")}/m`, pattern, flags: "m" },
|
|
626
|
+
body: null, lineMarker: null, position: UNKNOWN_POSITION,
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
return expansions;
|
|
630
|
+
}
|
|
631
|
+
// Phase 3 — the inference turn and its request: the prompts the model has not
|
|
632
|
+
// seen, their open-path READs, the ambient and stream deltas, then the packet
|
|
633
|
+
// ({§packet-stored-shape}).
|
|
634
|
+
async #composeRequest(args, container, gitStatus) {
|
|
635
|
+
const { workspaceId, workerId, loopId } = args;
|
|
636
|
+
// Initialization is a complete preceding turn, not a set of rows
|
|
637
|
+
// interleaved with the model boundary. The engine may now acquire the
|
|
638
|
+
// inference turn inside the same warmed workspace cycle.
|
|
639
|
+
const modelTurn = container.modelTurn ?? await Turn.open(this.#db, { loopId, producer: "model", kind: "inference" });
|
|
640
|
+
if (container.modelTurn === null)
|
|
641
|
+
container.createdTurnIds.push(modelTurn.id);
|
|
642
|
+
const { id: turnId, sequence: seq } = modelTurn;
|
|
643
|
+
const systemCtx = this.#schemeContext(args, turnId);
|
|
644
|
+
const messages = await this.#publishMessages(args, turnId);
|
|
645
|
+
let nextActionIndex = await this.#readOpenPaths(args, turnId, messages.openPaths, messages.nextActionIndex);
|
|
646
|
+
// {§env-delta-log-pull} — materialize ambient observations before packet
|
|
647
|
+
// composition and reserve their action indices. {§exec-stream} owns the
|
|
648
|
+
// distinct byte-cursor path for this worker's streams.
|
|
649
|
+
// {§exec-lifetime} — a `"turn"` lifetime: reap the worker's open turn-scoped streams (necessarily
|
|
650
|
+
// from a prior turn — this runs before the turn's own spawns) so a `<0>` never survives into
|
|
651
|
+
// the subsequent turn. The terminal output then surfaces initially visible via the stream-delta path.
|
|
652
|
+
await this.#reapTurnScopedStreams(workerId);
|
|
653
|
+
const ambientEntries = await this.#materialization.materializeEnvironmentDeltas({ workspaceId, workerId, loopId, turnId, fromSequence: nextActionIndex });
|
|
654
|
+
nextActionIndex += ambientEntries.length;
|
|
655
|
+
const streamEntries = await this.#materialization.materializeStreamDeltas({ workspaceId, workerId, loopId, turnId, fromSequence: nextActionIndex });
|
|
656
|
+
nextActionIndex += streamEntries.length;
|
|
657
|
+
await Turn.observeCompletions(this.#db, turnId);
|
|
658
|
+
// {§notifications-log-entry-notify}: materialized observations are
|
|
659
|
+
// ordinary committed log rows, not packet-only content.
|
|
660
|
+
for (const id of [...ambientEntries, ...streamEntries]) {
|
|
661
|
+
args.onDispatch?.(id);
|
|
662
|
+
await args.onSettled?.(id);
|
|
663
|
+
}
|
|
664
|
+
// The post-reconciliation Git snapshot above is threaded into the packet
|
|
665
|
+
// and every budget rebuild; overflow never shells again.
|
|
666
|
+
// Notices are non-terminal observations, never operation-failure truth.
|
|
667
|
+
// Drain once and thread the same set through every overflow rebuild.
|
|
668
|
+
const notices = this.#notices.drain(loopId)
|
|
669
|
+
.filter((event) => event.level !== "info");
|
|
670
|
+
// Build the model request packet ({§packet-stored-shape}). The log build
|
|
671
|
+
// queries log_entries scoped to the worker, including this turn's newly
|
|
672
|
+
// published message arrivals.
|
|
673
|
+
const facts = { turnId, seq, gitStatus, notices, transientOpenLogEntryId: container.transientOpenLogEntryId, promptProjection: "automatic" };
|
|
674
|
+
let packet = await this.#buildPacket(args, facts);
|
|
675
|
+
// {§context-output-admission} — output admission changes no operation
|
|
676
|
+
// outcome, authored memory, or turn identity.
|
|
677
|
+
if (await this.#packets.admitOutput(packet, turnId))
|
|
678
|
+
packet = await this.#buildPacket(args, facts);
|
|
679
|
+
return { ...facts, createdTurnIds: container.createdTurnIds, loopSeq: container.loopSequence, systemCtx, nextActionIndex, packet };
|
|
680
|
+
}
|
|
681
|
+
// Pre-model writes. Each message the model has not seen yet becomes an inbound `SEND`
|
|
682
|
+
// row; model operations continue the same turn sequence after these rows. Returns the
|
|
683
|
+
// next action index and the messages' open paths.
|
|
684
|
+
// {§message-loop-containment}: the loop contains every message that arrived before this
|
|
685
|
+
// boundary. Publish each unpublished inbox row oldest-first, exactly once, as an inbound
|
|
686
|
+
// SEND row ({§message-arrival}); its selected paths ride along for the READs that follow.
|
|
687
|
+
async #publishMessages({ workerId, loopId, onDispatch, onSettled }, turnId) {
|
|
688
|
+
let nextActionIndex = 1;
|
|
689
|
+
const openPaths = [];
|
|
690
|
+
const unpublished = await this.#db.drain_unpublished_messages_for_loop.all({ loop_id: loopId });
|
|
691
|
+
for (const message of unpublished) {
|
|
692
|
+
openPaths.push(...assertOpenPaths(JSON.parse(message.open_paths), `Message ${message.id} open_paths`));
|
|
693
|
+
const logEntryId = await this.#materialization.writeArrivalLog({
|
|
694
|
+
workerId, loopId, turnId, sequence: nextActionIndex++, body: message.body, source: message.source, resource: message.path,
|
|
695
|
+
selfAddressed: message.source !== null && message.source === message.durable_path,
|
|
696
|
+
});
|
|
697
|
+
const published = await this.#db.drain_publish_message.get({ id: message.id, log_entry_id: logEntryId });
|
|
698
|
+
if (published === undefined)
|
|
699
|
+
throw new Error(`TurnRunner.#publishMessages: message ${message.id} was already published`);
|
|
700
|
+
onDispatch?.(logEntryId);
|
|
701
|
+
await onSettled?.(logEntryId);
|
|
702
|
+
}
|
|
703
|
+
return { nextActionIndex, openPaths };
|
|
704
|
+
}
|
|
705
|
+
// {§methods-loop-run-open-paths}: selected workspace paths belong to the message.
|
|
706
|
+
// Publish it, then dispatch ordinary core READs in that same turn;
|
|
707
|
+
// missing/non-member paths retain their normal 4xx. Returns the next action index.
|
|
708
|
+
async #readOpenPaths({ workspaceId, workerId, loopId, onDispatch, onSettled }, turnId, openPaths, fromSequence) {
|
|
709
|
+
let nextActionIndex = fromSequence;
|
|
710
|
+
for (const raw of openPaths) {
|
|
711
|
+
const pathname = raw.startsWith("/") ? raw : `/${raw}`;
|
|
712
|
+
const fileRead = {
|
|
713
|
+
op: "READ", aside: null, lineMarker: null, matcher: null,
|
|
714
|
+
target: {
|
|
715
|
+
kind: "url", raw: `file://${pathname}`, scheme: "file",
|
|
716
|
+
username: null, password: null, hostname: null, port: null,
|
|
717
|
+
pathname, query: null, fragment: null,
|
|
718
|
+
},
|
|
719
|
+
metadata: null,
|
|
720
|
+
body: null, position: UNKNOWN_POSITION,
|
|
721
|
+
};
|
|
722
|
+
await this.#dispatch({
|
|
723
|
+
statement: fileRead, workspaceId, workerId, loopId, turnId,
|
|
724
|
+
sequence: nextActionIndex, origin: "_plurnk", onDispatch, onSettled,
|
|
725
|
+
});
|
|
726
|
+
nextActionIndex++;
|
|
727
|
+
}
|
|
728
|
+
return nextActionIndex;
|
|
729
|
+
}
|
|
730
|
+
#buildPacket({ messages, recap, workspaceId, workerId, loopId, provider }, facts) {
|
|
731
|
+
return this.#packets.buildRequestPacket({
|
|
732
|
+
initialMessages: messages,
|
|
733
|
+
recap,
|
|
734
|
+
workspaceId,
|
|
735
|
+
workerId,
|
|
736
|
+
loopId,
|
|
737
|
+
currentTurnSeq: facts.seq,
|
|
738
|
+
provider,
|
|
739
|
+
gitStatus: facts.gitStatus,
|
|
740
|
+
notices: facts.notices,
|
|
741
|
+
transientOpenLogEntryId: facts.transientOpenLogEntryId,
|
|
742
|
+
promptProjection: facts.promptProjection,
|
|
743
|
+
turnId: facts.turnId,
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
// Retained context cannot fit: the turn completes on the curation failure and
|
|
747
|
+
// the loop rules the terminal.
|
|
748
|
+
async #failCuration(request, overflow) {
|
|
749
|
+
const curationFailure = curationOverflowFailure(overflow);
|
|
750
|
+
await Turn.complete(this.#db, request.turnId, curationFailure.status);
|
|
751
|
+
return turnResult(request, curationFailure.status, { curationFailure });
|
|
752
|
+
}
|
|
753
|
+
// Phase 4's bookkeeping: the wire request, the recovery knobs, the signal the
|
|
754
|
+
// provider sees, the client id and the worker's provider identity.
|
|
755
|
+
async #prepareProviderAttempts({ provider, workspaceId, workerId, loopId, signal }, request) {
|
|
756
|
+
const wire = await this.#wireMessages(request.packet, request.systemCtx, provider);
|
|
757
|
+
// {§provider-recovery} — this turn's recovery clock: the first recoverable provider
|
|
758
|
+
// failure starts it; the budget and backoff are the operator's.
|
|
759
|
+
const recoveryBudget = readProviderRecovery();
|
|
760
|
+
const recoveryBackoff = readProviderRecoveryBackoff();
|
|
761
|
+
const providerSignal = this.#loopSignal(loopId) ?? signal;
|
|
762
|
+
const { workerId: providerWorkerId } = await this.#resolveWorkerProviderIdentity(workerId);
|
|
763
|
+
return {
|
|
764
|
+
wire,
|
|
765
|
+
response: undefined,
|
|
766
|
+
split: undefined,
|
|
767
|
+
railGrammar: undefined,
|
|
768
|
+
railEvidence: undefined,
|
|
769
|
+
emissionAttempts: 0,
|
|
770
|
+
callInFlight: false,
|
|
771
|
+
modelCallSequence: 0,
|
|
772
|
+
currentEmissionAttempt: 0,
|
|
773
|
+
attemptId: null,
|
|
774
|
+
modelCall: null,
|
|
775
|
+
recoveryStartedAt: null,
|
|
776
|
+
recoveryFailures: 0,
|
|
777
|
+
parked: false,
|
|
778
|
+
attributions: [],
|
|
779
|
+
recoveryBudget,
|
|
780
|
+
recoveryBackoff,
|
|
781
|
+
signal: providerSignal,
|
|
782
|
+
providerWorkerId,
|
|
783
|
+
turnWireAccounting: [],
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
// Phase 4 — the provider attempt loop. Each iteration is one logical call: a
|
|
787
|
+
// recoverable failure or a capacity rebuild re-issues the same emission attempt,
|
|
788
|
+
// an invalid emission spends one, and a valid emission ends the loop
|
|
789
|
+
// ({§invalid-emission-attempts}).
|
|
790
|
+
async #attemptProvider(args, request, attempts) {
|
|
791
|
+
const { provider, workspaceId, workerId, loopId, signal } = args;
|
|
792
|
+
// {§turn-lifecycle}: bracket the complete provider-attempt window with liveness notices.
|
|
793
|
+
if (!signal?.aborted)
|
|
794
|
+
this.#notices.push(workspaceId, workerId, loopId, { source: "engine:turn", kind: "turn_awaiting_model", level: "info", message: "awaiting model response" });
|
|
795
|
+
attempts.railGrammar = await this.#operatorGrammar(provider);
|
|
796
|
+
const attemptLimit = readEmissionAttempts();
|
|
797
|
+
const strikeStreak = await this.#strikes.streak(loopId);
|
|
798
|
+
for (let attempt = 1; attempt <= attemptLimit;) {
|
|
799
|
+
const verdict = await this.#issueProviderCall(args, request, attempts, attempt, strikeStreak);
|
|
800
|
+
if (verdict === "admitted")
|
|
801
|
+
break;
|
|
802
|
+
if (verdict === "rejected")
|
|
803
|
+
attempt++;
|
|
804
|
+
}
|
|
805
|
+
if (!signal?.aborted) {
|
|
806
|
+
const wire = aggregateProviderAccounting(attempts.turnWireAccounting);
|
|
807
|
+
this.#notices.push(workspaceId, workerId, loopId, {
|
|
808
|
+
source: "engine:turn",
|
|
809
|
+
kind: "turn_generated",
|
|
810
|
+
level: "info",
|
|
811
|
+
message: "parsing model response",
|
|
812
|
+
// {§turn-accounting-notice} (#465) — the exact settled derivation,
|
|
813
|
+
// never a second stored fact: a live watcher accrues loop cost per turn.
|
|
814
|
+
accounting: {
|
|
815
|
+
requests: attempts.turnWireAccounting.length,
|
|
816
|
+
costUsd: wire.costUsd,
|
|
817
|
+
inputTokens: wire.usage?.inputTokens ?? null,
|
|
818
|
+
outputTokens: wire.usage?.outputTokens ?? null,
|
|
819
|
+
reasoningTokens: wire.usage?.outputTokenDetails?.reasoningTokens ?? null,
|
|
820
|
+
cacheReadTokens: wire.usage?.inputTokenDetails?.cacheReadTokens ?? null,
|
|
821
|
+
},
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
if (attempts.response === undefined || attempts.split === undefined || attempts.modelCall === null) {
|
|
825
|
+
throw new Error("provider attempt loop completed without a response");
|
|
826
|
+
}
|
|
827
|
+
const emission = {
|
|
828
|
+
response: attempts.response,
|
|
829
|
+
split: attempts.split,
|
|
830
|
+
modelCallId: attempts.modelCall.id,
|
|
831
|
+
railGrammar: attempts.railGrammar,
|
|
832
|
+
railEvidence: attempts.railEvidence,
|
|
833
|
+
emissionAttempts: attempts.emissionAttempts,
|
|
834
|
+
signal: attempts.signal,
|
|
835
|
+
};
|
|
836
|
+
if (emission.split.packetAssistant.reasoning?.length) {
|
|
837
|
+
await Turn.recordSource(this.#db, request.turnId, "reasoning", emission.split.packetAssistant.reasoning, { modelCallId: emission.modelCallId });
|
|
838
|
+
}
|
|
839
|
+
return emission;
|
|
840
|
+
}
|
|
841
|
+
// One logical provider call: its durable model call and attempt row, the exchange
|
|
842
|
+
// under the reasoning observer, and its classification. Capacity recovery and
|
|
843
|
+
// {§provider-recovery} re-issue the same emission attempt; the parser's admission
|
|
844
|
+
// is the one verdict on a completed exchange.
|
|
845
|
+
async #issueProviderCall(args, request, attempts, attempt, strikeStreak) {
|
|
846
|
+
const { provider, workspaceId, workerId, loopId } = args;
|
|
847
|
+
// Capacity recovery may rebuild and resend the request without
|
|
848
|
+
// consuming a grammar-emission attempt. Every logical provider
|
|
849
|
+
// call still receives its own durable sequence and accounting.
|
|
850
|
+
attempts.currentEmissionAttempt = attempt;
|
|
851
|
+
attempts.modelCallSequence++;
|
|
852
|
+
const attributionContext = Object.freeze({
|
|
853
|
+
workspaceId: String(workspaceId),
|
|
854
|
+
workerId: attempts.providerWorkerId,
|
|
855
|
+
loop: request.loopSeq,
|
|
856
|
+
turn: request.seq,
|
|
857
|
+
attempt: attempts.modelCallSequence,
|
|
858
|
+
});
|
|
859
|
+
attempts.attributions = await this.#attemptAttributions(provider, attributionContext);
|
|
860
|
+
request.packet = { ...request.packet, attributions: attempts.attributions };
|
|
861
|
+
const modelCall = await ModelCall.open(this.#db, {
|
|
862
|
+
turnId: request.turnId,
|
|
863
|
+
kind: "emission",
|
|
864
|
+
attributions: attempts.attributions,
|
|
865
|
+
model: provider.model,
|
|
866
|
+
});
|
|
867
|
+
attempts.modelCall = modelCall;
|
|
868
|
+
const attemptRow = await this.#db.engine_open_turn_attempt.get({
|
|
869
|
+
model_call_id: modelCall.id,
|
|
870
|
+
});
|
|
871
|
+
if (attemptRow === undefined) {
|
|
872
|
+
throw new Error(`Engine.runTurn: provider call ${attempts.modelCallSequence} did not open`);
|
|
873
|
+
}
|
|
874
|
+
attempts.attemptId = attemptRow.id;
|
|
875
|
+
attempts.callInFlight = true;
|
|
876
|
+
const reasoning = this.#reasoningObserver(args, request.turnId, modelCall);
|
|
877
|
+
let completedResponse;
|
|
878
|
+
try {
|
|
879
|
+
completedResponse = await this.#generate(args, request, attempts, modelCall, reasoning, strikeStreak);
|
|
880
|
+
}
|
|
881
|
+
catch (error) {
|
|
882
|
+
if (error instanceof ProviderError
|
|
883
|
+
&& RECOVERABLE_PROVIDER_FAILURES.has(error.kind)
|
|
884
|
+
&& attempts.signal?.aborted !== true) {
|
|
885
|
+
await this.#recoverProviderFailure(args, request, attempts, modelCall, attemptRow.id, error);
|
|
886
|
+
return "reissued";
|
|
669
887
|
}
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
const fileRead = {
|
|
676
|
-
op: "READ", aside: null, lineMarker: null, matcher: null,
|
|
677
|
-
target: {
|
|
678
|
-
kind: "url", raw: `file://${pathname}`, scheme: "file",
|
|
679
|
-
username: null, password: null, hostname: null, port: null,
|
|
680
|
-
pathname, query: null, fragment: null,
|
|
681
|
-
},
|
|
682
|
-
metadata: null,
|
|
683
|
-
body: null, position: UNKNOWN_POSITION,
|
|
684
|
-
};
|
|
685
|
-
await this.#dispatch({
|
|
686
|
-
statement: fileRead, workspaceId, workerId, loopId, turnId,
|
|
687
|
-
sequence: nextActionIndex, origin: "_plurnk", onDispatch, onSettled,
|
|
688
|
-
});
|
|
689
|
-
nextActionIndex++;
|
|
888
|
+
if (!(error instanceof ProviderError)
|
|
889
|
+
|| error.kind !== "capacity_exceeded"
|
|
890
|
+
|| attempts.signal?.aborted === true
|
|
891
|
+
|| !await this.#recoverCapacityPacket(args, request, attempts)) {
|
|
892
|
+
throw error;
|
|
690
893
|
}
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
894
|
+
await this.#resendForCapacity(args, request, attempts, modelCall, error);
|
|
895
|
+
return "reissued";
|
|
896
|
+
}
|
|
897
|
+
finally {
|
|
898
|
+
reasoning.end();
|
|
899
|
+
}
|
|
900
|
+
if (attempts.recoveryFailures > 0) {
|
|
901
|
+
this.#notices.push(workspaceId, workerId, loopId, {
|
|
902
|
+
source: "engine:provider",
|
|
903
|
+
kind: "provider_recovered",
|
|
904
|
+
level: "info",
|
|
905
|
+
message: `Provider recovered after ${attempts.recoveryFailures} failed call${attempts.recoveryFailures === 1 ? "" : "s"}.`,
|
|
906
|
+
});
|
|
907
|
+
attempts.recoveryFailures = 0;
|
|
908
|
+
attempts.recoveryStartedAt = null;
|
|
909
|
+
}
|
|
910
|
+
attempts.response = completedResponse;
|
|
911
|
+
attempts.turnWireAccounting.push(...completedResponse.accounting);
|
|
912
|
+
await modelCall.observeResponse(completedResponse, null, attempts.wire.nativeInputs);
|
|
913
|
+
attempts.railEvidence = attempts.railGrammar === undefined ? undefined : completedResponse.grammarEvidence;
|
|
914
|
+
const split = this.#splitResponse(completedResponse, this.#executors()?.availableRuntimes(workspaceId) ?? []);
|
|
915
|
+
attempts.split = split;
|
|
916
|
+
await this.#classifyProviderAttempt(attempts, attemptRow.id, split, attempt, split.emissionValid);
|
|
917
|
+
return split.emissionValid ? "admitted" : "rejected";
|
|
918
|
+
}
|
|
919
|
+
// {§notifications-reasoning-event} — only the parent emission is conversational;
|
|
920
|
+
// BARE has no observer on its isolated calls. Reasoning deltas cite the physical
|
|
921
|
+
// request they stream in; a new request or the call's end closes the open span.
|
|
922
|
+
#reasoningObserver({ workspaceId, workerId, loopId }, turnId, modelCall) {
|
|
923
|
+
let reasoningStarted = false;
|
|
924
|
+
let reasoningRequestSequence = 0;
|
|
925
|
+
const end = () => {
|
|
926
|
+
if (!reasoningStarted)
|
|
927
|
+
return;
|
|
928
|
+
this.#reasoningEventNotify(workspaceId, {
|
|
715
929
|
workerId,
|
|
716
930
|
loopId,
|
|
717
|
-
currentTurnSeq: seq,
|
|
718
|
-
provider,
|
|
719
|
-
gitStatus,
|
|
720
|
-
notices,
|
|
721
|
-
transientOpenLogEntryId,
|
|
722
|
-
promptProjection,
|
|
723
931
|
turnId,
|
|
932
|
+
modelCallId: modelCall.id,
|
|
933
|
+
requestSequence: reasoningRequestSequence,
|
|
934
|
+
phase: "end",
|
|
724
935
|
});
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
kind: "inference",
|
|
739
|
-
status: curationFailure.status,
|
|
740
|
-
outcomes: [],
|
|
741
|
-
fingerprint: "",
|
|
742
|
-
capacityHardStop: false,
|
|
743
|
-
providerParked: false,
|
|
744
|
-
steerStruck: false,
|
|
745
|
-
emptyTurn: false,
|
|
746
|
-
emissionAttempts: 0,
|
|
747
|
-
emissionExhausted: false,
|
|
748
|
-
curationFailure,
|
|
749
|
-
};
|
|
750
|
-
}
|
|
751
|
-
let materializedRequest = await this.#wireMessages(requestPacket, systemCtx, provider);
|
|
752
|
-
let modelMessages = materializedRequest.messages;
|
|
753
|
-
let nativeInputs = materializedRequest.nativeInputs;
|
|
754
|
-
// Curation pressure and provider generation are independent. The
|
|
755
|
-
// provider owns its configured total output envelope.
|
|
756
|
-
let response;
|
|
757
|
-
let splitResponse;
|
|
758
|
-
let railGrammar;
|
|
759
|
-
let railEvidence;
|
|
760
|
-
let emissionAttempts = 0;
|
|
761
|
-
let providerCallInFlight = false;
|
|
762
|
-
let modelCallSequence = 0;
|
|
763
|
-
let currentEmissionAttempt = 0;
|
|
764
|
-
let providerAttemptId = null;
|
|
765
|
-
let providerModelCall = null;
|
|
766
|
-
// {§provider-recovery} — this turn's recovery clock: the first recoverable provider
|
|
767
|
-
// failure starts it; the budget and backoff are the operator's.
|
|
768
|
-
let recoveryStartedAt = null;
|
|
769
|
-
let recoveryFailures = 0;
|
|
770
|
-
let providerParked = false;
|
|
771
|
-
const recoveryBudget = readProviderRecovery();
|
|
772
|
-
const recoveryBackoff = readProviderRecoveryBackoff();
|
|
773
|
-
let providerAttemptAttributions = [];
|
|
774
|
-
const providerSignal = this.#loopSignal(loopId) ?? signal;
|
|
775
|
-
// {§client-metadata}
|
|
776
|
-
const { client } = await WorkspaceSettings.read(this.#db, workspaceId);
|
|
777
|
-
const providerIdentity = await this.#resolveWorkerProviderIdentity(workerId);
|
|
778
|
-
const { workerId: providerWorkerId, primaryWorkerId } = providerIdentity;
|
|
779
|
-
const classifyProviderAttempt = async (id, attemptSplit, emissionAttempt, accepted) => {
|
|
780
|
-
const result = await this.#db.engine_classify_turn_attempt_response.run({
|
|
781
|
-
id,
|
|
782
|
-
accepted: accepted ? 1 : 0,
|
|
783
|
-
parse_errors: JSON.stringify(attemptSplit.parseErrors),
|
|
784
|
-
});
|
|
785
|
-
if (result.changes !== 1) {
|
|
786
|
-
throw new Error(`emission attempt ${id} was not awaiting classification`);
|
|
787
|
-
}
|
|
788
|
-
emissionAttempts = emissionAttempt;
|
|
789
|
-
};
|
|
790
|
-
const recoverCapacityPacket = async () => {
|
|
791
|
-
let baselineMessages = modelMessages;
|
|
792
|
-
if (promptProjection === "automatic") {
|
|
793
|
-
promptProjection = "withheld";
|
|
794
|
-
const candidate = await buildPacket();
|
|
795
|
-
const candidateRequest = await this.#wireMessages(candidate, systemCtx, provider);
|
|
796
|
-
if (JSON.stringify(candidateRequest.messages) !== JSON.stringify(baselineMessages)) {
|
|
797
|
-
requestPacket = candidate;
|
|
798
|
-
materializedRequest = candidateRequest;
|
|
799
|
-
modelMessages = materializedRequest.messages;
|
|
800
|
-
nativeInputs = materializedRequest.nativeInputs;
|
|
801
|
-
return true;
|
|
802
|
-
}
|
|
803
|
-
baselineMessages = candidateRequest.messages;
|
|
936
|
+
reasoningStarted = false;
|
|
937
|
+
};
|
|
938
|
+
const observeRequest = async (...args) => {
|
|
939
|
+
end();
|
|
940
|
+
const settle = await modelCall.observeRequest(...args);
|
|
941
|
+
reasoningRequestSequence = modelCall.requestSequence;
|
|
942
|
+
return settle;
|
|
943
|
+
};
|
|
944
|
+
const observeReasoning = this.#reasoningEventNotify === undefined
|
|
945
|
+
? undefined
|
|
946
|
+
: (delta) => {
|
|
947
|
+
if (reasoningRequestSequence === 0) {
|
|
948
|
+
throw new Error("provider emitted reasoning before opening its physical request");
|
|
804
949
|
}
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
this.#notices.push(workspaceId, workerId, loopId, { source: "engine:turn", kind: "turn_awaiting_model", level: "info", message: "awaiting model response" });
|
|
811
|
-
railGrammar = await this.#operatorGrammar(provider);
|
|
812
|
-
const attemptLimit = readEmissionAttempts();
|
|
813
|
-
const strikeStreak = await this.#strikes.streak(loopId);
|
|
814
|
-
// {§turn-accounting-notice} (#465) — every physical exchange this turn pays
|
|
815
|
-
// for, successes and failed calls alike, so the completion notice carries
|
|
816
|
-
// the exact settled wire spend.
|
|
817
|
-
const turnWireAccounting = [];
|
|
818
|
-
for (let attempt = 1; attempt <= attemptLimit;) {
|
|
819
|
-
// Capacity recovery may rebuild and resend the request without
|
|
820
|
-
// consuming a grammar-emission attempt. Every logical provider
|
|
821
|
-
// call still receives its own durable sequence and accounting.
|
|
822
|
-
currentEmissionAttempt = attempt;
|
|
823
|
-
modelCallSequence++;
|
|
824
|
-
const attributionContext = Object.freeze({
|
|
825
|
-
workspaceId: String(workspaceId),
|
|
826
|
-
workerId: providerWorkerId,
|
|
827
|
-
primaryWorkerId,
|
|
828
|
-
loop: loopSeq,
|
|
829
|
-
turn: seq,
|
|
830
|
-
attempt: modelCallSequence,
|
|
831
|
-
});
|
|
832
|
-
providerAttemptAttributions = await this.#attemptAttributions(provider, attributionContext);
|
|
833
|
-
requestPacket = { ...requestPacket, attributions: providerAttemptAttributions };
|
|
834
|
-
providerModelCall = await ModelCall.open(this.#db, {
|
|
950
|
+
if (!reasoningStarted) {
|
|
951
|
+
reasoningStarted = true;
|
|
952
|
+
this.#reasoningEventNotify(workspaceId, {
|
|
953
|
+
workerId,
|
|
954
|
+
loopId,
|
|
835
955
|
turnId,
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
});
|
|
840
|
-
const attemptRow = await this.#db.engine_open_turn_attempt.get({
|
|
841
|
-
model_call_id: providerModelCall.id,
|
|
842
|
-
});
|
|
843
|
-
if (attemptRow === undefined) {
|
|
844
|
-
throw new Error(`Engine.runTurn: provider call ${modelCallSequence} did not open`);
|
|
845
|
-
}
|
|
846
|
-
providerAttemptId = attemptRow.id;
|
|
847
|
-
providerCallInFlight = true;
|
|
848
|
-
const currentModelCall = providerModelCall;
|
|
849
|
-
let reasoningStarted = false;
|
|
850
|
-
let reasoningRequestSequence = 0;
|
|
851
|
-
const endReasoning = () => {
|
|
852
|
-
if (!reasoningStarted)
|
|
853
|
-
return;
|
|
854
|
-
this.#reasoningEventNotify(workspaceId, {
|
|
855
|
-
workerId,
|
|
856
|
-
loopId,
|
|
857
|
-
turnId,
|
|
858
|
-
modelCallId: currentModelCall.id,
|
|
859
|
-
requestSequence: reasoningRequestSequence,
|
|
860
|
-
phase: "end",
|
|
861
|
-
});
|
|
862
|
-
reasoningStarted = false;
|
|
863
|
-
};
|
|
864
|
-
const observeRequest = async (...args) => {
|
|
865
|
-
endReasoning();
|
|
866
|
-
const settle = await currentModelCall.observeRequest(...args);
|
|
867
|
-
reasoningRequestSequence = currentModelCall.requestSequence;
|
|
868
|
-
return settle;
|
|
869
|
-
};
|
|
870
|
-
// {§notifications-reasoning-event} Only the parent emission is
|
|
871
|
-
// conversational; BARE has no observer on its isolated calls.
|
|
872
|
-
const observeReasoning = this.#reasoningEventNotify === undefined
|
|
873
|
-
? undefined
|
|
874
|
-
: (delta) => {
|
|
875
|
-
if (reasoningRequestSequence === 0) {
|
|
876
|
-
throw new Error("provider emitted reasoning before opening its physical request");
|
|
877
|
-
}
|
|
878
|
-
if (!reasoningStarted) {
|
|
879
|
-
reasoningStarted = true;
|
|
880
|
-
this.#reasoningEventNotify(workspaceId, {
|
|
881
|
-
workerId,
|
|
882
|
-
loopId,
|
|
883
|
-
turnId,
|
|
884
|
-
modelCallId: currentModelCall.id,
|
|
885
|
-
requestSequence: reasoningRequestSequence,
|
|
886
|
-
phase: "start",
|
|
887
|
-
});
|
|
888
|
-
}
|
|
889
|
-
this.#reasoningEventNotify(workspaceId, {
|
|
890
|
-
workerId,
|
|
891
|
-
loopId,
|
|
892
|
-
turnId,
|
|
893
|
-
modelCallId: currentModelCall.id,
|
|
894
|
-
requestSequence: reasoningRequestSequence,
|
|
895
|
-
phase: "content",
|
|
896
|
-
delta,
|
|
897
|
-
});
|
|
898
|
-
};
|
|
899
|
-
let completedResponse;
|
|
900
|
-
try {
|
|
901
|
-
completedResponse = await observed(// {§observability-boundary}
|
|
902
|
-
GEN_AI_REQUEST_SPAN, { model: provider.model, attempt: modelCallSequence }, async (span) => {
|
|
903
|
-
try {
|
|
904
|
-
await this.#packets.recordObservations(requestPacket);
|
|
905
|
-
const generated = await provider.generate({
|
|
906
|
-
messages: modelMessages,
|
|
907
|
-
workerId: providerWorkerId,
|
|
908
|
-
primaryWorkerId,
|
|
909
|
-
signal: providerSignal,
|
|
910
|
-
grammar: railGrammar,
|
|
911
|
-
strikes: strikeStreak,
|
|
912
|
-
attributions: providerAttemptAttributions.length > 0
|
|
913
|
-
? providerAttemptAttributions
|
|
914
|
-
: undefined,
|
|
915
|
-
client: client ?? undefined,
|
|
916
|
-
workspaceId: String(workspaceId),
|
|
917
|
-
loop: loopSeq,
|
|
918
|
-
turn: seq,
|
|
919
|
-
observeRequest,
|
|
920
|
-
observeReasoning,
|
|
921
|
-
callKind: "emission",
|
|
922
|
-
}); // {§provider-surface-generate} {§provider-guarantees-signal-wired} {§provider-guarantees-serial-attempts} {§attribution} {§client-metadata}
|
|
923
|
-
currentModelCall.assertAccounting(generated.accounting);
|
|
924
|
-
providerCallInFlight = false;
|
|
925
|
-
recordCounter(PROVIDER_CALLS, {
|
|
926
|
-
model: provider.model,
|
|
927
|
-
attempt: modelCallSequence,
|
|
928
|
-
status: "resolved",
|
|
929
|
-
});
|
|
930
|
-
span.setAttribute("status", "resolved");
|
|
931
|
-
settleGenAiResponse(span, generated);
|
|
932
|
-
return generated;
|
|
933
|
-
}
|
|
934
|
-
catch (error) {
|
|
935
|
-
if (error instanceof ProviderError) {
|
|
936
|
-
currentModelCall.assertAccounting(error.accounting);
|
|
937
|
-
turnWireAccounting.push(...error.accounting);
|
|
938
|
-
}
|
|
939
|
-
throw error;
|
|
940
|
-
}
|
|
941
|
-
}, genAiRequestOptions(ProviderInstantiate.aliasOf(provider) ?? "plurnk", provider.model));
|
|
942
|
-
}
|
|
943
|
-
catch (error) {
|
|
944
|
-
if (error instanceof ProviderError
|
|
945
|
-
&& RECOVERABLE_PROVIDER_FAILURES.has(error.kind)
|
|
946
|
-
&& providerSignal?.aborted !== true) {
|
|
947
|
-
// {§provider-recovery} — a transient provider failure never ends the loop:
|
|
948
|
-
// record it, wait, and re-issue the same turn; once the budget is spent the
|
|
949
|
-
// outer handler parks the loop instead of failing it.
|
|
950
|
-
recoveryStartedAt ??= Date.now();
|
|
951
|
-
const elapsed = Date.now() - recoveryStartedAt;
|
|
952
|
-
if (elapsed >= recoveryBudget) {
|
|
953
|
-
providerParked = true;
|
|
954
|
-
throw error;
|
|
955
|
-
}
|
|
956
|
-
recoveryFailures += 1;
|
|
957
|
-
const failure = TurnRunner.#providerFailure(error, providerSignal);
|
|
958
|
-
if (error.attempt !== undefined) {
|
|
959
|
-
// {§provider-interrupted-attempt} — the interrupted response stays durable
|
|
960
|
-
// as an unaccepted attempt; it is never admitted or replayed.
|
|
961
|
-
await currentModelCall.observeResponse(error.attempt, failure, nativeInputs);
|
|
962
|
-
await classifyProviderAttempt(providerAttemptId, this.#splitResponse(error.attempt, this.#executors()?.availableRuntimes(workspaceId) ?? []), currentEmissionAttempt, false);
|
|
963
|
-
}
|
|
964
|
-
else {
|
|
965
|
-
await currentModelCall.fail(failure, error.capacity ?? null);
|
|
966
|
-
}
|
|
967
|
-
providerCallInFlight = false;
|
|
968
|
-
await this.#problems.record({
|
|
969
|
-
workerId,
|
|
970
|
-
loopId,
|
|
971
|
-
turnId,
|
|
972
|
-
sequence: nextActionIndex++,
|
|
973
|
-
origin: "_plurnk",
|
|
974
|
-
source: "provider",
|
|
975
|
-
result: failure,
|
|
976
|
-
});
|
|
977
|
-
const wait = Math.min(recoveryBackoff * 2 ** (recoveryFailures - 1), recoveryBackoff * 12);
|
|
978
|
-
this.#notices.push(workspaceId, workerId, loopId, {
|
|
979
|
-
source: "engine:provider",
|
|
980
|
-
kind: "provider_unavailable",
|
|
981
|
-
level: "warn",
|
|
982
|
-
message: `${failure.problem?.title ?? "Provider failure"}: retrying in ${Math.round(wait / 1000)}s (${Math.round(elapsed / 1000)}s of the ${Math.round(recoveryBudget / 1000)}s recovery budget spent).`,
|
|
983
|
-
});
|
|
984
|
-
// An abort during the wait re-enters generate, which refuses on the aborted signal.
|
|
985
|
-
await delay(wait, undefined, { signal: providerSignal }).catch(() => undefined);
|
|
986
|
-
// The response is still owed for this exact input. Failure rows remain
|
|
987
|
-
// durable, but recursively materializing them would mutate and cache-bust
|
|
988
|
-
// the request being recovered. {§provider-recovery}
|
|
989
|
-
continue;
|
|
990
|
-
}
|
|
991
|
-
if (!(error instanceof ProviderError)
|
|
992
|
-
|| error.kind !== "capacity_exceeded"
|
|
993
|
-
|| providerSignal?.aborted === true
|
|
994
|
-
|| !await recoverCapacityPacket()) {
|
|
995
|
-
throw error;
|
|
996
|
-
}
|
|
997
|
-
const failure = TurnRunner.#providerFailure(error, providerSignal);
|
|
998
|
-
await currentModelCall.fail(failure, error.capacity ?? null);
|
|
999
|
-
providerCallInFlight = false;
|
|
1000
|
-
await this.#problems.record({
|
|
1001
|
-
workerId,
|
|
1002
|
-
loopId,
|
|
1003
|
-
turnId,
|
|
1004
|
-
sequence: nextActionIndex++,
|
|
1005
|
-
origin: "_plurnk",
|
|
1006
|
-
source: "provider",
|
|
1007
|
-
result: failure,
|
|
1008
|
-
});
|
|
1009
|
-
// Include the durable recovery signal in the replacement
|
|
1010
|
-
// request while preserving the selected recovery posture.
|
|
1011
|
-
requestPacket = await buildPacket();
|
|
1012
|
-
materializedRequest = await this.#wireMessages(requestPacket, systemCtx, provider);
|
|
1013
|
-
modelMessages = materializedRequest.messages;
|
|
1014
|
-
nativeInputs = materializedRequest.nativeInputs;
|
|
1015
|
-
continue;
|
|
1016
|
-
}
|
|
1017
|
-
finally {
|
|
1018
|
-
endReasoning();
|
|
1019
|
-
}
|
|
1020
|
-
if (recoveryFailures > 0) {
|
|
1021
|
-
this.#notices.push(workspaceId, workerId, loopId, {
|
|
1022
|
-
source: "engine:provider",
|
|
1023
|
-
kind: "provider_recovered",
|
|
1024
|
-
level: "info",
|
|
1025
|
-
message: `Provider recovered after ${recoveryFailures} failed call${recoveryFailures === 1 ? "" : "s"}.`,
|
|
1026
|
-
});
|
|
1027
|
-
recoveryFailures = 0;
|
|
1028
|
-
recoveryStartedAt = null;
|
|
1029
|
-
}
|
|
1030
|
-
response = completedResponse;
|
|
1031
|
-
turnWireAccounting.push(...completedResponse.accounting);
|
|
1032
|
-
await currentModelCall.observeResponse(completedResponse, null, nativeInputs);
|
|
1033
|
-
railEvidence = railGrammar === undefined ? undefined : completedResponse.grammarEvidence;
|
|
1034
|
-
splitResponse = this.#splitResponse(completedResponse, this.#executors()?.availableRuntimes(workspaceId) ?? []);
|
|
1035
|
-
await classifyProviderAttempt(attemptRow.id, splitResponse, attempt, splitResponse.emissionValid);
|
|
1036
|
-
if (splitResponse.emissionValid)
|
|
1037
|
-
break;
|
|
1038
|
-
attempt++;
|
|
1039
|
-
}
|
|
1040
|
-
if (!signal?.aborted) {
|
|
1041
|
-
const wire = aggregateProviderAccounting(turnWireAccounting);
|
|
1042
|
-
this.#notices.push(workspaceId, workerId, loopId, {
|
|
1043
|
-
source: "engine:turn",
|
|
1044
|
-
kind: "turn_generated",
|
|
1045
|
-
level: "info",
|
|
1046
|
-
message: "parsing model response",
|
|
1047
|
-
// {§turn-accounting-notice} (#465) — the exact settled derivation,
|
|
1048
|
-
// never a second stored fact: a live watcher accrues loop cost per turn.
|
|
1049
|
-
accounting: {
|
|
1050
|
-
requests: turnWireAccounting.length,
|
|
1051
|
-
costUsd: wire.costUsd,
|
|
1052
|
-
inputTokens: wire.usage?.inputTokens ?? null,
|
|
1053
|
-
outputTokens: wire.usage?.outputTokens ?? null,
|
|
1054
|
-
reasoningTokens: wire.usage?.outputTokenDetails?.reasoningTokens ?? null,
|
|
1055
|
-
cacheReadTokens: wire.usage?.inputTokenDetails?.cacheReadTokens ?? null,
|
|
1056
|
-
},
|
|
956
|
+
modelCallId: modelCall.id,
|
|
957
|
+
requestSequence: reasoningRequestSequence,
|
|
958
|
+
phase: "start",
|
|
1057
959
|
});
|
|
1058
960
|
}
|
|
1059
|
-
|
|
1060
|
-
catch (err) {
|
|
1061
|
-
// This handler owns only provider-call failures. Parser, cost, SQL,
|
|
1062
|
-
// and engine-contract failures retain their original source.
|
|
1063
|
-
if (err instanceof ModelCallPersistenceError || err instanceof ProviderAccountingIntegrityError)
|
|
1064
|
-
throw err;
|
|
1065
|
-
if (!providerCallInFlight)
|
|
1066
|
-
throw err;
|
|
1067
|
-
providerCallInFlight = false;
|
|
1068
|
-
if (providerAttemptId === null || providerModelCall === null) {
|
|
1069
|
-
throw new Error("provider call failed without durable model-call and attempt identities", { cause: err });
|
|
1070
|
-
}
|
|
1071
|
-
const failure = TurnRunner.#providerFailure(err, providerSignal);
|
|
1072
|
-
const capacityFailure = err instanceof ProviderError && err.kind === "capacity_exceeded";
|
|
1073
|
-
// {§provider-interrupted-attempt} — a provider-declared interruption
|
|
1074
|
-
// carries response evidence without becoming a completed exchange.
|
|
1075
|
-
// Persist it as an unaccepted attempt before settling the failure.
|
|
1076
|
-
if (err instanceof ProviderError && err.attempt !== undefined) {
|
|
1077
|
-
response = err.attempt;
|
|
1078
|
-
await providerModelCall.observeResponse(response, failure, nativeInputs);
|
|
1079
|
-
splitResponse = this.#splitResponse(response, this.#executors()?.availableRuntimes(workspaceId) ?? []);
|
|
1080
|
-
await classifyProviderAttempt(providerAttemptId, splitResponse, currentEmissionAttempt, false);
|
|
1081
|
-
}
|
|
1082
|
-
else {
|
|
1083
|
-
await providerModelCall.fail(failure, err instanceof ProviderError ? err.capacity ?? null : null);
|
|
1084
|
-
if (!capacityFailure)
|
|
1085
|
-
emissionAttempts = currentEmissionAttempt;
|
|
1086
|
-
}
|
|
1087
|
-
// {§turn-never-blank} — a ProviderError means no completed exchange exists.
|
|
1088
|
-
// Persist its exact RFC 9457 result before propagating it. Grammar transport
|
|
1089
|
-
// evidence exists only on completed responses ({§operator-grammar}).
|
|
1090
|
-
// Cancellation is lifecycle truth, not a provider failure. Close the
|
|
1091
|
-
// attempted turn without inventing an assistant response, then let
|
|
1092
|
-
// runLoop/Daemon settle the exact 504/499 loop result.
|
|
1093
|
-
if (providerSignal?.aborted) {
|
|
1094
|
-
const status = providerSignal.reason === LOOP_TIMEOUT_REASON ? 504 : 499;
|
|
1095
|
-
await this.#recordInference({
|
|
1096
|
-
workspaceId, workerId, loopId, turnId,
|
|
1097
|
-
evidence: {
|
|
1098
|
-
packet: StoredPacket.stringify(requestPacket),
|
|
1099
|
-
sections: StoredPacket.sections(requestPacket),
|
|
1100
|
-
usageCurationBudget: this.#packets.curationBudgetFor(requestPacket),
|
|
1101
|
-
finishReason: splitResponse?.callMetadata.finishReason ?? null,
|
|
1102
|
-
model: splitResponse?.callMetadata.model ?? provider.model,
|
|
1103
|
-
meta: JSON.stringify(response?.meta ?? {}),
|
|
1104
|
-
},
|
|
1105
|
-
});
|
|
1106
|
-
await Turn.complete(this.#db, turnId, status);
|
|
1107
|
-
throw err;
|
|
1108
|
-
}
|
|
1109
|
-
const recorded = await this.#problems.record({
|
|
961
|
+
this.#reasoningEventNotify(workspaceId, {
|
|
1110
962
|
workerId,
|
|
1111
963
|
loopId,
|
|
1112
964
|
turnId,
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
965
|
+
modelCallId: modelCall.id,
|
|
966
|
+
requestSequence: reasoningRequestSequence,
|
|
967
|
+
phase: "content",
|
|
968
|
+
delta,
|
|
1117
969
|
});
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
producer: "model",
|
|
1146
|
-
kind: "inference",
|
|
1147
|
-
status: 202,
|
|
1148
|
-
outcomes: [],
|
|
1149
|
-
fingerprint: "",
|
|
1150
|
-
capacityHardStop: false, providerParked: true,
|
|
1151
|
-
providerFailure: recorded.result,
|
|
1152
|
-
steerStruck: false,
|
|
1153
|
-
emptyTurn: false,
|
|
1154
|
-
emissionAttempts,
|
|
1155
|
-
emissionExhausted: false,
|
|
1156
|
-
};
|
|
1157
|
-
}
|
|
1158
|
-
if (capacityFailure) {
|
|
1159
|
-
return {
|
|
1160
|
-
createdTurnIds,
|
|
1161
|
-
turnId,
|
|
1162
|
-
producer: "model",
|
|
1163
|
-
kind: "inference",
|
|
1164
|
-
status: recorded.result.status,
|
|
1165
|
-
outcomes: [],
|
|
1166
|
-
fingerprint: "",
|
|
1167
|
-
capacityHardStop: true,
|
|
1168
|
-
providerParked: false,
|
|
1169
|
-
capacityFailure: recorded.result,
|
|
1170
|
-
steerStruck: false,
|
|
1171
|
-
emptyTurn: false,
|
|
1172
|
-
emissionAttempts,
|
|
1173
|
-
emissionExhausted: false,
|
|
1174
|
-
};
|
|
1175
|
-
}
|
|
1176
|
-
if (err instanceof ProviderError
|
|
1177
|
-
&& err.kind === "invalid_response"
|
|
1178
|
-
&& providerSignal?.aborted !== true) {
|
|
1179
|
-
// {§engine-rails} Contract Strikes: the provider violated its response
|
|
1180
|
-
// contract. The failure is durable and the turn is complete; the strike
|
|
1181
|
-
// rail — never an instant loop death — rules whether the streak ends it.
|
|
1182
|
-
return {
|
|
1183
|
-
createdTurnIds,
|
|
1184
|
-
turnId,
|
|
1185
|
-
producer: "model",
|
|
1186
|
-
kind: "inference",
|
|
1187
|
-
status: recorded.result.status,
|
|
1188
|
-
outcomes: [{ op: null, status: recorded.result.status, problemType: recorded.result.problem?.type ?? null }],
|
|
1189
|
-
fingerprint: `provider-contract-violation:${turnId}`,
|
|
1190
|
-
capacityHardStop: false,
|
|
1191
|
-
providerParked: false,
|
|
1192
|
-
providerFailure: recorded.result,
|
|
1193
|
-
steerStruck: false,
|
|
1194
|
-
emptyTurn: false,
|
|
1195
|
-
emissionAttempts,
|
|
1196
|
-
emissionExhausted: false,
|
|
1197
|
-
};
|
|
1198
|
-
}
|
|
1199
|
-
throw new OperationFailureError(recorded.result, { cause: err });
|
|
1200
|
-
}
|
|
1201
|
-
if (response === undefined || splitResponse === undefined || providerModelCall === null) {
|
|
1202
|
-
throw new Error("provider attempt loop completed without a response");
|
|
1203
|
-
}
|
|
1204
|
-
const emissionModelCallId = providerModelCall.id;
|
|
1205
|
-
if (splitResponse.packetAssistant.reasoning?.length) {
|
|
1206
|
-
await Turn.recordSource(this.#db, turnId, "reasoning", splitResponse.packetAssistant.reasoning, { modelCallId: emissionModelCallId });
|
|
1207
|
-
}
|
|
1208
|
-
if (!splitResponse.emissionValid) {
|
|
1209
|
-
// {§invalid-emission-attempts} Every exhaustion publishes the raw final
|
|
1210
|
-
// response and its recovery fact; {§engine-rails} Contract Strikes rule
|
|
1211
|
-
// how many consecutive frame-contract violations the loop survives.
|
|
1212
|
-
let rejectedModelEntryId;
|
|
1213
|
-
{
|
|
1214
|
-
rejectedModelEntryId = await this.#dispatcher.writeEmissionAttempt({
|
|
1215
|
-
verbatim: splitResponse.packetAssistant.content,
|
|
1216
|
-
workerId,
|
|
1217
|
-
loopId,
|
|
1218
|
-
turnId,
|
|
1219
|
-
sequence: nextActionIndex,
|
|
1220
|
-
modelCallId: emissionModelCallId,
|
|
1221
|
-
});
|
|
1222
|
-
// {§invalid-emission-attempts} — the informed turn carries the parser's own
|
|
1223
|
-
// diagnostic and position: the model sees WHY, not only that it was refused.
|
|
1224
|
-
const diagnostic = splitResponse.parseErrors[0];
|
|
1225
|
-
const cut = allowanceCutMessage(splitResponse.callMetadata.finishReason, response.capacity.responseMax ?? provider.outputBudget);
|
|
1226
|
-
if (cut !== null) {
|
|
1227
|
-
this.#notices.push(workspaceId, workerId, loopId, {
|
|
1228
|
-
source: "engine:capacity",
|
|
1229
|
-
kind: "output_truncated",
|
|
1230
|
-
level: "error",
|
|
1231
|
-
message: `${cut}; no operations were performed`,
|
|
1232
|
-
});
|
|
1233
|
-
}
|
|
1234
|
-
else {
|
|
1235
|
-
this.#notices.push(workspaceId, workerId, loopId, {
|
|
1236
|
-
source: "engine:grammar",
|
|
1237
|
-
kind: "invalid_emission",
|
|
1238
|
-
level: "error",
|
|
1239
|
-
message: diagnostic === undefined
|
|
1240
|
-
? INVALID_EMISSION_RECOVERY_MESSAGE
|
|
1241
|
-
: `${INVALID_EMISSION_RECOVERY_MESSAGE} Parser: ${diagnostic.message}`,
|
|
1242
|
-
...(diagnostic !== undefined && diagnostic.line > 0
|
|
1243
|
-
? { position: { type: "content-offset", line: diagnostic.line, column: diagnostic.column } }
|
|
1244
|
-
: {}),
|
|
1245
|
-
});
|
|
1246
|
-
}
|
|
1247
|
-
}
|
|
1248
|
-
const status = TURN_STATUS_IMPLICIT_CONTINUE;
|
|
1249
|
-
await this.#recordInference({
|
|
1250
|
-
workspaceId, workerId, loopId, turnId,
|
|
1251
|
-
evidence: {
|
|
1252
|
-
packet: StoredPacket.stringify(requestPacket),
|
|
1253
|
-
sections: StoredPacket.sections(requestPacket),
|
|
1254
|
-
usageCurationBudget: this.#packets.curationBudgetFor(requestPacket),
|
|
1255
|
-
finishReason: splitResponse.callMetadata.finishReason,
|
|
1256
|
-
model: splitResponse.callMetadata.model,
|
|
1257
|
-
meta: JSON.stringify(response.meta ?? {}),
|
|
1258
|
-
},
|
|
970
|
+
};
|
|
971
|
+
return { observeRequest, observeReasoning, end };
|
|
972
|
+
}
|
|
973
|
+
// The exchange under its GenAI span: the packet's observations are recorded, the
|
|
974
|
+
// provider generates against the wire request, and the call's accounting is
|
|
975
|
+
// asserted whether it resolved or failed.
|
|
976
|
+
async #generate({ provider, workspaceId }, request, attempts, modelCall, reasoning, strikeStreak) {
|
|
977
|
+
return await observed(// {§observability-boundary}
|
|
978
|
+
genAiRequestName(provider.model), { model: provider.model, attempt: attempts.modelCallSequence }, async (span) => {
|
|
979
|
+
try {
|
|
980
|
+
await this.#packets.recordObservations(request.packet);
|
|
981
|
+
const generated = await provider.generate({
|
|
982
|
+
messages: attempts.wire.messages,
|
|
983
|
+
workerId: attempts.providerWorkerId,
|
|
984
|
+
workspaceId: String(workspaceId),
|
|
985
|
+
signal: attempts.signal,
|
|
986
|
+
grammar: attempts.railGrammar,
|
|
987
|
+
observeRequest: reasoning.observeRequest,
|
|
988
|
+
observeReasoning: reasoning.observeReasoning,
|
|
989
|
+
callKind: "emission",
|
|
990
|
+
}); // {§provider-surface-generate} {§provider-guarantees-signal-wired} {§provider-guarantees-serial-attempts} {§attribution}
|
|
991
|
+
modelCall.assertAccounting(generated.accounting);
|
|
992
|
+
attempts.callInFlight = false;
|
|
993
|
+
recordCounter(PROVIDER_CALLS, {
|
|
994
|
+
model: provider.model,
|
|
995
|
+
attempt: attempts.modelCallSequence,
|
|
996
|
+
status: "resolved",
|
|
1259
997
|
});
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
turnId,
|
|
1264
|
-
producer: "model",
|
|
1265
|
-
kind: "inference",
|
|
1266
|
-
status: TURN_STATUS_IMPLICIT_CONTINUE,
|
|
1267
|
-
outcomes: [{ op: null, status: 500, problemType: "https://problems.plurnk.xyz/engine/invalid-emission-exhausted" }],
|
|
1268
|
-
fingerprint: `frame-contract-violation:${turnId}`,
|
|
1269
|
-
capacityHardStop: false,
|
|
1270
|
-
providerParked: false,
|
|
1271
|
-
steerStruck: false,
|
|
1272
|
-
emptyTurn: false,
|
|
1273
|
-
emissionAttempts,
|
|
1274
|
-
emissionExhausted: true,
|
|
1275
|
-
...(rejectedModelEntryId === undefined ? {} : { rejectedModelEntryId }),
|
|
1276
|
-
};
|
|
998
|
+
span.setAttribute("status", "resolved");
|
|
999
|
+
settleGenAiResponse(span, generated);
|
|
1000
|
+
return generated;
|
|
1277
1001
|
}
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
this.#notices.push(workspaceId, workerId, loopId, notice);
|
|
1283
|
-
}
|
|
1284
|
-
// {§empty-turn} — a response cut at the output allowance with nothing admitted names the
|
|
1285
|
-
// cut, so the model reads the ceiling, never a parser symptom (#478).
|
|
1286
|
-
if (splitResponse.emptyTurn && callMetadata.finishReason === "length") {
|
|
1287
|
-
const cut = allowanceCutMessage(callMetadata.finishReason, response.capacity.responseMax ?? provider.outputBudget);
|
|
1288
|
-
if (cut !== null) {
|
|
1289
|
-
this.#notices.push(workspaceId, workerId, loopId, {
|
|
1290
|
-
source: "engine:turn",
|
|
1291
|
-
kind: "output_truncated",
|
|
1292
|
-
level: "warn",
|
|
1293
|
-
message: `${cut}; no operations were performed`,
|
|
1294
|
-
});
|
|
1002
|
+
catch (error) {
|
|
1003
|
+
if (error instanceof ProviderError) {
|
|
1004
|
+
modelCall.assertAccounting(error.accounting);
|
|
1005
|
+
attempts.turnWireAccounting.push(...error.accounting);
|
|
1295
1006
|
}
|
|
1007
|
+
throw error;
|
|
1296
1008
|
}
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1009
|
+
}, genAiRequestOptions(ProviderInstantiate.providerIdOf(provider) ?? "other", provider.model));
|
|
1010
|
+
}
|
|
1011
|
+
// {§provider-recovery} — a transient provider failure never ends the loop: record
|
|
1012
|
+
// it, wait, and re-issue the same turn; once the budget is spent the failure
|
|
1013
|
+
// handler parks the loop instead of failing it.
|
|
1014
|
+
async #recoverProviderFailure({ workspaceId, workerId, loopId }, request, attempts, modelCall, attemptId, error) {
|
|
1015
|
+
attempts.recoveryStartedAt ??= Date.now();
|
|
1016
|
+
const elapsed = Date.now() - attempts.recoveryStartedAt;
|
|
1017
|
+
if (elapsed >= attempts.recoveryBudget) {
|
|
1018
|
+
attempts.parked = true;
|
|
1019
|
+
throw error;
|
|
1020
|
+
}
|
|
1021
|
+
attempts.recoveryFailures += 1;
|
|
1022
|
+
const failure = _a.#providerFailure(error, attempts.signal);
|
|
1023
|
+
if (error.attempt !== undefined) {
|
|
1024
|
+
// {§provider-interrupted-attempt} — the interrupted response stays durable
|
|
1025
|
+
// as an unaccepted attempt; it is never admitted or replayed.
|
|
1026
|
+
await modelCall.observeResponse(error.attempt, failure, attempts.wire.nativeInputs);
|
|
1027
|
+
await this.#classifyProviderAttempt(attempts, attemptId, this.#splitResponse(error.attempt, this.#executors()?.availableRuntimes(workspaceId) ?? []), attempts.currentEmissionAttempt, false);
|
|
1028
|
+
}
|
|
1029
|
+
else {
|
|
1030
|
+
await modelCall.fail(failure, error.capacity ?? null);
|
|
1031
|
+
}
|
|
1032
|
+
attempts.callInFlight = false;
|
|
1033
|
+
await this.#problems.record({
|
|
1034
|
+
workerId,
|
|
1035
|
+
loopId,
|
|
1036
|
+
turnId: request.turnId,
|
|
1037
|
+
sequence: request.nextActionIndex++,
|
|
1038
|
+
origin: "_plurnk",
|
|
1039
|
+
source: "provider",
|
|
1040
|
+
result: failure,
|
|
1041
|
+
});
|
|
1042
|
+
const wait = Math.min(attempts.recoveryBackoff * 2 ** (attempts.recoveryFailures - 1), readProviderRecoveryBackoffMax());
|
|
1043
|
+
this.#notices.push(workspaceId, workerId, loopId, {
|
|
1044
|
+
source: "engine:provider",
|
|
1045
|
+
kind: "provider_unavailable",
|
|
1046
|
+
level: "warn",
|
|
1047
|
+
message: `${failure.problem?.title ?? "Provider failure"}: retrying in ${Math.round(wait / 1000)}s (${Math.round(elapsed / 1000)}s of the ${Math.round(attempts.recoveryBudget / 1000)}s recovery budget spent).`,
|
|
1048
|
+
});
|
|
1049
|
+
// An abort during the wait re-enters generate, which refuses on the aborted signal.
|
|
1050
|
+
await delay(wait, undefined, { signal: attempts.signal }).catch(() => undefined);
|
|
1051
|
+
// The response is still owed for this exact input. Failure rows remain
|
|
1052
|
+
// durable, but recursively materializing them would mutate and cache-bust
|
|
1053
|
+
// the request being recovered. {§provider-recovery}
|
|
1054
|
+
}
|
|
1055
|
+
// The first capacity rejection withholds the automatic prompt projection; the
|
|
1056
|
+
// rebuilt request is resent only when it actually changed the wire messages.
|
|
1057
|
+
async #recoverCapacityPacket(args, request, attempts) {
|
|
1058
|
+
if (request.promptProjection !== "automatic")
|
|
1059
|
+
return false;
|
|
1060
|
+
request.promptProjection = "withheld";
|
|
1061
|
+
const candidate = await this.#buildPacket(args, request);
|
|
1062
|
+
const candidateRequest = await this.#wireMessages(candidate, request.systemCtx, args.provider);
|
|
1063
|
+
if (JSON.stringify(candidateRequest.messages) === JSON.stringify(attempts.wire.messages))
|
|
1064
|
+
return false;
|
|
1065
|
+
request.packet = candidate;
|
|
1066
|
+
attempts.wire = candidateRequest;
|
|
1067
|
+
return true;
|
|
1068
|
+
}
|
|
1069
|
+
// The capacity rejection is durable as a failed call and a problem row; the
|
|
1070
|
+
// replacement request is rebuilt to include that recovery signal while keeping
|
|
1071
|
+
// the selected projection.
|
|
1072
|
+
async #resendForCapacity(args, request, attempts, modelCall, error) {
|
|
1073
|
+
const { workerId, loopId, provider } = args;
|
|
1074
|
+
const failure = _a.#providerFailure(error, attempts.signal);
|
|
1075
|
+
await modelCall.fail(failure, error.capacity ?? null);
|
|
1076
|
+
attempts.callInFlight = false;
|
|
1077
|
+
await this.#problems.record({
|
|
1078
|
+
workerId,
|
|
1079
|
+
loopId,
|
|
1080
|
+
turnId: request.turnId,
|
|
1081
|
+
sequence: request.nextActionIndex++,
|
|
1082
|
+
origin: "_plurnk",
|
|
1083
|
+
source: "provider",
|
|
1084
|
+
result: failure,
|
|
1085
|
+
});
|
|
1086
|
+
// Include the durable recovery signal in the replacement
|
|
1087
|
+
// request while preserving the selected recovery posture.
|
|
1088
|
+
request.packet = await this.#buildPacket(args, request);
|
|
1089
|
+
attempts.wire = await this.#wireMessages(request.packet, request.systemCtx, provider);
|
|
1090
|
+
}
|
|
1091
|
+
// The attempt row's verdict: accepted, or rejected with the parser's errors. The
|
|
1092
|
+
// classified emission attempt is the turn's spent count.
|
|
1093
|
+
async #classifyProviderAttempt(attempts, id, split, emissionAttempt, accepted) {
|
|
1094
|
+
const result = await this.#db.engine_classify_turn_attempt_response.run({
|
|
1095
|
+
id,
|
|
1096
|
+
accepted: accepted ? 1 : 0,
|
|
1097
|
+
parse_errors: JSON.stringify(split.parseErrors),
|
|
1098
|
+
});
|
|
1099
|
+
if (result.changes !== 1) {
|
|
1100
|
+
throw new Error(`emission attempt ${id} was not awaiting classification`);
|
|
1101
|
+
}
|
|
1102
|
+
attempts.emissionAttempts = emissionAttempt;
|
|
1103
|
+
}
|
|
1104
|
+
// The provider-call failure handler. It owns only failures raised while a call
|
|
1105
|
+
// was in flight; parser, cost, SQL, and engine-contract failures retain their
|
|
1106
|
+
// original source.
|
|
1107
|
+
async #settleProviderFailure(err, args, request, attempts) {
|
|
1108
|
+
if (err instanceof ModelCallPersistenceError || err instanceof ProviderAccountingIntegrityError)
|
|
1109
|
+
throw err;
|
|
1110
|
+
if (!attempts.callInFlight)
|
|
1111
|
+
throw err;
|
|
1112
|
+
attempts.callInFlight = false;
|
|
1113
|
+
if (attempts.attemptId === null || attempts.modelCall === null) {
|
|
1114
|
+
throw new Error("provider call failed without durable model-call and attempt identities", { cause: err });
|
|
1115
|
+
}
|
|
1116
|
+
const { provider, workspaceId, workerId, loopId } = args;
|
|
1117
|
+
const { turnId } = request;
|
|
1118
|
+
const failure = _a.#providerFailure(err, attempts.signal);
|
|
1119
|
+
const capacityFailure = err instanceof ProviderError && err.kind === "capacity_exceeded";
|
|
1120
|
+
// {§provider-interrupted-attempt} — a provider-declared interruption
|
|
1121
|
+
// carries response evidence without becoming a completed exchange.
|
|
1122
|
+
// Persist it as an unaccepted attempt before settling the failure.
|
|
1123
|
+
if (err instanceof ProviderError && err.attempt !== undefined) {
|
|
1124
|
+
attempts.response = err.attempt;
|
|
1125
|
+
await attempts.modelCall.observeResponse(err.attempt, failure, attempts.wire.nativeInputs);
|
|
1126
|
+
attempts.split = this.#splitResponse(err.attempt, this.#executors()?.availableRuntimes(workspaceId) ?? []);
|
|
1127
|
+
await this.#classifyProviderAttempt(attempts, attempts.attemptId, attempts.split, attempts.currentEmissionAttempt, false);
|
|
1128
|
+
}
|
|
1129
|
+
else {
|
|
1130
|
+
await attempts.modelCall.fail(failure, err instanceof ProviderError ? err.capacity ?? null : null);
|
|
1131
|
+
if (!capacityFailure)
|
|
1132
|
+
attempts.emissionAttempts = attempts.currentEmissionAttempt;
|
|
1133
|
+
}
|
|
1134
|
+
const evidence = this.#requestEvidence(provider, request.packet, attempts.split, attempts.response);
|
|
1135
|
+
// {§turn-never-blank} — a ProviderError means no completed exchange exists.
|
|
1136
|
+
// Persist its exact RFC 9457 result before propagating it. Grammar transport
|
|
1137
|
+
// evidence exists only on completed responses ({§operator-grammar}).
|
|
1138
|
+
// Cancellation is lifecycle truth, not a provider failure. Close the
|
|
1139
|
+
// attempted turn without inventing an assistant response, then let
|
|
1140
|
+
// runLoop/Daemon settle the exact 504/499 loop result.
|
|
1141
|
+
if (attempts.signal?.aborted) {
|
|
1142
|
+
const status = attempts.signal.reason === LOOP_TIMEOUT_REASON ? 504 : 499;
|
|
1143
|
+
await this.#recordInference({ workspaceId, workerId, loopId, turnId, evidence });
|
|
1144
|
+
await Turn.complete(this.#db, turnId, status);
|
|
1145
|
+
throw err;
|
|
1146
|
+
}
|
|
1147
|
+
const recorded = await this.#problems.record({
|
|
1148
|
+
workerId,
|
|
1149
|
+
loopId,
|
|
1150
|
+
turnId,
|
|
1151
|
+
sequence: request.nextActionIndex,
|
|
1152
|
+
origin: "_plurnk",
|
|
1153
|
+
source: "provider",
|
|
1154
|
+
result: failure,
|
|
1155
|
+
});
|
|
1156
|
+
// The provider call was attempted, but no completed exchange exists.
|
|
1157
|
+
// Persist the exact request half and failure status; omitting assistant
|
|
1158
|
+
// is materially different from fabricating an empty model turn.
|
|
1159
|
+
await this.#recordInference({ workspaceId, workerId, loopId, turnId, evidence });
|
|
1160
|
+
await Turn.complete(this.#db, turnId, attempts.parked ? 202 : recorded.result.status);
|
|
1161
|
+
const { emissionAttempts } = attempts;
|
|
1162
|
+
if (attempts.parked) {
|
|
1163
|
+
// {§provider-recovery} — the recovery budget is spent: the loop parks exactly like a
|
|
1164
|
+
// [202] wait and resumes on the next prompt or wake; the failure stays durable.
|
|
1165
|
+
// {§loop-attendance} — unattended, nothing will wake it, so LoopDriver concludes instead
|
|
1166
|
+
// and the notice says that rather than promising a resumption nobody can deliver.
|
|
1167
|
+
const { attended } = await LoopPolicyReader.read(this.#db, loopId);
|
|
1168
|
+
this.#notices.push(workspaceId, workerId, loopId, {
|
|
1169
|
+
source: "engine:provider",
|
|
1170
|
+
kind: "provider_unavailable",
|
|
1171
|
+
level: "error",
|
|
1172
|
+
message: `${recorded.result.problem?.title ?? "Provider failure"}: the ${Math.round(attempts.recoveryBudget / 1000)}s recovery budget is spent; ${attended ? "the loop is parked and resumes on the next prompt or wake" : "this run is unattended, so the loop ends here"}.`,
|
|
1375
1173
|
});
|
|
1376
|
-
return {
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1174
|
+
return turnResult(request, 202, { providerParked: true, providerFailure: recorded.result, emissionAttempts });
|
|
1175
|
+
}
|
|
1176
|
+
if (capacityFailure) {
|
|
1177
|
+
return turnResult(request, recorded.result.status, { capacityHardStop: true, capacityFailure: recorded.result, emissionAttempts });
|
|
1178
|
+
}
|
|
1179
|
+
if (err instanceof ProviderError && err.kind === "invalid_response") {
|
|
1180
|
+
// {§engine-rails} Contract Strikes: the provider violated its response
|
|
1181
|
+
// contract. The failure is durable and the turn is complete; the strike
|
|
1182
|
+
// rail — never an instant loop death — rules whether the streak ends it.
|
|
1183
|
+
return turnResult(request, recorded.result.status, {
|
|
1184
|
+
outcomes: [{ op: null, status: recorded.result.status, problemType: recorded.result.problem?.type ?? null }],
|
|
1185
|
+
fingerprint: `provider-contract-violation:${turnId}`,
|
|
1186
|
+
providerFailure: recorded.result,
|
|
1388
1187
|
emissionAttempts,
|
|
1389
|
-
|
|
1390
|
-
};
|
|
1188
|
+
});
|
|
1391
1189
|
}
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1190
|
+
throw new OperationFailureError(recorded.result, { cause: err });
|
|
1191
|
+
}
|
|
1192
|
+
// Inference evidence for a turn without an admitted exchange: the exact request
|
|
1193
|
+
// half, the last attempt's metadata when one exists, the provider's own model otherwise.
|
|
1194
|
+
#requestEvidence(provider, packet, split, response) {
|
|
1195
|
+
return {
|
|
1196
|
+
packet: StoredPacket.stringify(packet),
|
|
1197
|
+
sections: StoredPacket.sections(packet),
|
|
1198
|
+
usageCurationBudget: this.#packets.curationBudgetFor(packet),
|
|
1199
|
+
finishReason: split?.callMetadata.finishReason ?? null,
|
|
1200
|
+
model: split?.callMetadata.model ?? provider.model,
|
|
1201
|
+
meta: JSON.stringify(response?.meta ?? {}),
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
// Phase 5, refused — {§invalid-emission-attempts}: every exhaustion publishes the
|
|
1205
|
+
// raw final response and its recovery fact; {§engine-rails} Contract Strikes rule
|
|
1206
|
+
// how many consecutive frame-contract violations the loop survives.
|
|
1207
|
+
async #rejectExhaustedEmission({ provider, workspaceId, workerId, loopId }, request, emission) {
|
|
1208
|
+
const { split, response } = emission;
|
|
1209
|
+
const { turnId } = request;
|
|
1210
|
+
const rejectedModelEntryId = await this.#dispatcher.writeEmissionAttempt({
|
|
1211
|
+
verbatim: split.packetAssistant.content,
|
|
1212
|
+
workerId,
|
|
1213
|
+
loopId,
|
|
1214
|
+
turnId,
|
|
1215
|
+
sequence: request.nextActionIndex,
|
|
1216
|
+
modelCallId: emission.modelCallId,
|
|
1217
|
+
});
|
|
1218
|
+
// {§invalid-emission-attempts} — the informed turn carries the parser's own
|
|
1219
|
+
// diagnostic and position: the model sees WHY, not only that it was refused.
|
|
1220
|
+
const diagnostic = split.parseErrors[0];
|
|
1221
|
+
const cut = allowanceCutMessage(split.callMetadata.finishReason, response.capacity.responseMax ?? provider.outputBudget);
|
|
1222
|
+
if (cut !== null) {
|
|
1223
|
+
this.#notices.push(workspaceId, workerId, loopId, {
|
|
1224
|
+
source: "engine:capacity",
|
|
1225
|
+
kind: "output_truncated",
|
|
1226
|
+
level: "error",
|
|
1227
|
+
message: `${cut}; no operations were performed`,
|
|
1228
|
+
});
|
|
1229
|
+
}
|
|
1230
|
+
else {
|
|
1231
|
+
this.#notices.push(workspaceId, workerId, loopId, {
|
|
1232
|
+
source: "engine:grammar",
|
|
1233
|
+
kind: "invalid_emission",
|
|
1234
|
+
level: "error",
|
|
1235
|
+
message: diagnostic === undefined
|
|
1236
|
+
? INVALID_EMISSION_RECOVERY_MESSAGE
|
|
1237
|
+
: `${INVALID_EMISSION_RECOVERY_MESSAGE} Parser: ${diagnostic.message}`,
|
|
1238
|
+
...(diagnostic !== undefined && diagnostic.line > 0
|
|
1239
|
+
? { position: { type: "content-offset", line: diagnostic.line, column: diagnostic.column } }
|
|
1240
|
+
: {}),
|
|
1241
|
+
});
|
|
1406
1242
|
}
|
|
1243
|
+
await this.#recordInference({ workspaceId, workerId, loopId, turnId, evidence: this.#requestEvidence(provider, request.packet, split, response) });
|
|
1244
|
+
await Turn.complete(this.#db, turnId, TURN_STATUS_IMPLICIT_CONTINUE);
|
|
1245
|
+
return turnResult(request, TURN_STATUS_IMPLICIT_CONTINUE, {
|
|
1246
|
+
outcomes: [{ op: null, status: 500, problemType: "https://problems.plurnk.xyz/engine/invalid-emission-exhausted" }],
|
|
1247
|
+
fingerprint: `frame-contract-violation:${turnId}`,
|
|
1248
|
+
emissionAttempts: emission.emissionAttempts,
|
|
1249
|
+
emissionExhausted: true,
|
|
1250
|
+
rejectedModelEntryId,
|
|
1251
|
+
});
|
|
1252
|
+
}
|
|
1253
|
+
// Phase 5, admitted — the emission's notices, then {§packet-stored-shape}: admitted
|
|
1254
|
+
// emission data extends the packet while provider-call metadata remains on the
|
|
1255
|
+
// Turn row.
|
|
1256
|
+
async #recordAdmittedEmission({ provider, workspaceId, workerId, loopId }, request, emission) {
|
|
1257
|
+
const { split, response } = emission;
|
|
1258
|
+
const { packetAssistant, callMetadata, parseNotices } = split; // raw assistant content is opaque — split, never interpreted — {§provider-guarantees-assistantraw-opaque}
|
|
1259
|
+
for (const notice of parseNotices) {
|
|
1260
|
+
this.#notices.push(workspaceId, workerId, loopId, notice);
|
|
1261
|
+
}
|
|
1262
|
+
const allowanceCut = allowanceCutMessage(callMetadata.finishReason, response.capacity.responseMax ?? provider.outputBudget);
|
|
1263
|
+
// {§empty-turn} — a response cut at the output allowance with nothing admitted names the
|
|
1264
|
+
// cut, so the model reads the ceiling, never a parser symptom (#478).
|
|
1265
|
+
if (split.emptyTurn && allowanceCut !== null) {
|
|
1266
|
+
this.#notices.push(workspaceId, workerId, loopId, {
|
|
1267
|
+
source: "engine:turn",
|
|
1268
|
+
kind: "output_truncated",
|
|
1269
|
+
level: "warn",
|
|
1270
|
+
message: `${allowanceCut}; no operations were performed`,
|
|
1271
|
+
});
|
|
1272
|
+
}
|
|
1273
|
+
// Non-fatal provider transport notices on an accepted turn. Forward each
|
|
1274
|
+
// Notice with a content-offset `line:col`;
|
|
1275
|
+
// the model resolves it against its own emission — READ ops://<worker>/<loop>/<turn> at the
|
|
1276
|
+
// cited lines ({§turn-ops-entry}) — not an embedded snippet that would duplicate the emission.
|
|
1277
|
+
for (const notice of response.notices ?? []) {
|
|
1278
|
+
const located = typeof notice.position === "number"
|
|
1279
|
+
? this.#offsetToLineColumn(packetAssistant.content, notice.position)
|
|
1280
|
+
: null;
|
|
1281
|
+
this.#notices.push(workspaceId, workerId, loopId, {
|
|
1282
|
+
source: notice.source,
|
|
1283
|
+
kind: notice.kind,
|
|
1284
|
+
message: notice.message ?? "",
|
|
1285
|
+
level: notice.level,
|
|
1286
|
+
...(located !== null
|
|
1287
|
+
? { position: { type: "content-offset", line: located.line, column: located.column } }
|
|
1288
|
+
: {}),
|
|
1289
|
+
});
|
|
1290
|
+
}
|
|
1291
|
+
if (allowanceCut !== null) {
|
|
1292
|
+
this.#notices.push(workspaceId, workerId, loopId, {
|
|
1293
|
+
source: "engine:capacity",
|
|
1294
|
+
kind: "output_truncated",
|
|
1295
|
+
message: allowanceCut,
|
|
1296
|
+
level: "warn",
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
// {§operator-grammar} — transport evidence only: the turn records whether the operator's
|
|
1300
|
+
// grammar reached the wire. Nothing grades the response against it; the parser's
|
|
1301
|
+
// admission is the one verdict (#588).
|
|
1302
|
+
const railKeys = emission.railGrammar === undefined
|
|
1303
|
+
? undefined
|
|
1304
|
+
: { railsAttached: emission.railEvidence?.transported === true ? "client" : "withheld" };
|
|
1305
|
+
// Attach the admitted inference evidence. The turn remains open until
|
|
1306
|
+
// the producer-neutral admitted-turn executor settles every operation
|
|
1307
|
+
// and its exact source artifact.
|
|
1308
|
+
const packet = StoredPacket.admit(request.packet, packetAssistant, response.assistantRaw);
|
|
1309
|
+
await this.#recordInference({
|
|
1310
|
+
workspaceId, workerId, loopId, turnId: request.turnId,
|
|
1311
|
+
evidence: {
|
|
1312
|
+
packet: StoredPacket.stringify(packet),
|
|
1313
|
+
sections: StoredPacket.sections(packet),
|
|
1314
|
+
usageCurationBudget: this.#packets.curationBudgetFor(request.packet), // {§tokenomics-client-gauge}
|
|
1315
|
+
finishReason: callMetadata.finishReason,
|
|
1316
|
+
model: callMetadata.model,
|
|
1317
|
+
// Opaque provider metadata plus the grammar transport key.
|
|
1318
|
+
// {§meta-passthrough}, {§operator-grammar}
|
|
1319
|
+
meta: JSON.stringify({ ...(response.meta ?? {}), ...(railKeys ?? {}) }),
|
|
1320
|
+
},
|
|
1321
|
+
});
|
|
1322
|
+
}
|
|
1323
|
+
// Phase 6 — the admitted program runs under the workspace's command ceiling and
|
|
1324
|
+
// the turn settles on the executor's verdict.
|
|
1325
|
+
async #settleAdmittedTurn(args, request, emission) {
|
|
1326
|
+
const { childProvider, workspaceId, workerId, loopId, onDispatch, onSettled } = args;
|
|
1327
|
+
const { split } = emission;
|
|
1328
|
+
// {§operator-config-workspace-max-commands} — workspace maxCommands
|
|
1329
|
+
// narrows the operator ceiling before the admitted program reaches the
|
|
1330
|
+
// shared executor.
|
|
1331
|
+
const maxCommands = Math.min(readMaxCommands(), (await WorkspaceSettings.read(this.#db, workspaceId)).maxCommands ?? Number.POSITIVE_INFINITY);
|
|
1332
|
+
const executed = await this.executeAdmittedTurn({
|
|
1333
|
+
statements: split.packetAssistant.ops,
|
|
1334
|
+
source: split.sourceBacked ? split.packetAssistant.content : null,
|
|
1335
|
+
sourceModelCallId: emission.modelCallId,
|
|
1336
|
+
origin: "model",
|
|
1337
|
+
workspaceId,
|
|
1338
|
+
workerId,
|
|
1339
|
+
loopId,
|
|
1340
|
+
turnId: request.turnId,
|
|
1341
|
+
fromSequence: request.nextActionIndex,
|
|
1342
|
+
maxCommands,
|
|
1343
|
+
recoverableParseErrors: split.recoverableParseErrors,
|
|
1344
|
+
emptyTurn: split.emptyTurn,
|
|
1345
|
+
proseAnswer: split.proseAnswer,
|
|
1346
|
+
bare: {
|
|
1347
|
+
provider: childProvider,
|
|
1348
|
+
loopSequence: request.loopSeq,
|
|
1349
|
+
turnSequence: request.seq,
|
|
1350
|
+
signal: emission.signal,
|
|
1351
|
+
},
|
|
1352
|
+
signal: emission.signal,
|
|
1353
|
+
onDispatch,
|
|
1354
|
+
onSettled,
|
|
1355
|
+
});
|
|
1356
|
+
return turnResult(request, executed.status, {
|
|
1357
|
+
outcomes: executed.outcomes,
|
|
1358
|
+
fingerprint: executed.fingerprint,
|
|
1359
|
+
emptyTurn: executed.emptyTurn,
|
|
1360
|
+
emissionAttempts: emission.emissionAttempts,
|
|
1361
|
+
});
|
|
1407
1362
|
}
|
|
1408
1363
|
// Split the wire-level ProviderResponse into the two destinations:
|
|
1409
1364
|
// packet.assistant gets the model's emission (content, ops, reasoning);
|
|
@@ -1462,7 +1417,7 @@ export default class TurnRunner {
|
|
|
1462
1417
|
});
|
|
1463
1418
|
}
|
|
1464
1419
|
else {
|
|
1465
|
-
parseErrors.push({ message: err.message, line: err.line, column: err.column, source: err.source
|
|
1420
|
+
parseErrors.push({ message: err.message, line: err.line, column: err.column, source: err.source });
|
|
1466
1421
|
}
|
|
1467
1422
|
}
|
|
1468
1423
|
else {
|
|
@@ -1471,34 +1426,69 @@ export default class TurnRunner {
|
|
|
1471
1426
|
}
|
|
1472
1427
|
}
|
|
1473
1428
|
}
|
|
1474
|
-
//
|
|
1475
|
-
//
|
|
1429
|
+
// {§unparsed-tail-boundary} — the lexer's one boundary fact: from `unparsedTail.from` on,
|
|
1430
|
+
// nothing was read. The statements that closed before it are ordinary facts and run; the
|
|
1431
|
+
// loss itself is one more hard diagnostic, a failed row the model sees. An emission that
|
|
1432
|
+
// lost its boundary before any statement closed has nothing admissible and is resampled.
|
|
1476
1433
|
const tail = parsed.unparsedTail;
|
|
1477
1434
|
if (tail !== undefined) {
|
|
1478
1435
|
hasUnparsedTail = true;
|
|
1479
1436
|
parseErrors.push({ message: tail.reason, line: tail.from.line, column: tail.from.column, source: "grammar" });
|
|
1480
1437
|
}
|
|
1481
1438
|
}
|
|
1439
|
+
// {§prose-conclusion} — a response whose content is prose (no operation, no attempt at one,
|
|
1440
|
+
// not cut at the output allowance) is the model's answer: a SEND to the open messages.
|
|
1441
|
+
// {§quotation} a reply wrapped whole in one markdown fence is delivered as its content.
|
|
1442
|
+
const wrapped = /^(`{3,}|~{3,})(?:markdown|md)[ \t]*\n([\s\S]*?)\n\1[ \t]*$/u.exec(assistant.content.trim());
|
|
1443
|
+
const prose = (wrapped?.[2] ?? assistant.content).trim();
|
|
1444
|
+
const concludes = preParsedOps === undefined
|
|
1445
|
+
&& ops.length === 0
|
|
1446
|
+
&& !hasUnparsedTail
|
|
1447
|
+
&& prose.length > 0
|
|
1448
|
+
&& assistant.finishReason !== "length"
|
|
1449
|
+
&& parseErrors.every((error) => error.message === PlurnkParser.NO_VALID_OPERATION)
|
|
1450
|
+
&& PlurnkParser.operationAttempt(assistant.content, executors) === null
|
|
1451
|
+
// {§quotation} an answer says something of its own: a reply that is nothing but quoted
|
|
1452
|
+
// material is a misfenced program, not prose (operator, 2026-09-18).
|
|
1453
|
+
&& PlurnkParser.unquoted(prose, executors).trim().length > 0
|
|
1454
|
+
// {§quotation} an operation fence that merely missed column zero is a misplaced program,
|
|
1455
|
+
// never an answer: the loop continues and the next packet carries the parser's word.
|
|
1456
|
+
&& !parseNotices.some(({ message }) => (message ?? "").includes("must start its line to run"));
|
|
1457
|
+
const proseAnswer = concludes
|
|
1458
|
+
? { op: "SEND", aside: null, target: null, metadata: null, lineMarker: null, body: { raw: prose, json: null }, position: { line: 1, column: 0 } }
|
|
1459
|
+
: null;
|
|
1460
|
+
if (proseAnswer !== null) {
|
|
1461
|
+
parseErrors.length = 0;
|
|
1462
|
+
ops.push(proseAnswer);
|
|
1463
|
+
}
|
|
1464
|
+
// The statements the content itself closed: a lost boundary is admissible only behind one
|
|
1465
|
+
// of these, never behind a reasoning NOTE alone ({§reasoning-notes}).
|
|
1466
|
+
const contentStatementCount = ops.filter(({ position }) => position.line > 0).length;
|
|
1467
|
+
const reasoning = assistant.reasoning ?? null;
|
|
1468
|
+
const notes = reasoning === null ? [] : PlurnkParser.parseReasoningNotes(reasoning);
|
|
1469
|
+
ops.unshift(...notes);
|
|
1470
|
+
if (notes.length > 0) {
|
|
1471
|
+
// A reasoned NOTE is an operation even when the content contains no program.
|
|
1472
|
+
for (let index = parseErrors.length - 1; index >= 0; index--) {
|
|
1473
|
+
if (parseErrors[index].message === PlurnkParser.NO_VALID_OPERATION)
|
|
1474
|
+
parseErrors.splice(index, 1);
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1482
1477
|
const sourceStatementCount = ops.filter(({ position }) => position.line > 0).length;
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
//
|
|
1486
|
-
|
|
1487
|
-
const recoverableParseErrors = trustworthyBoundary
|
|
1488
|
-
? parseErrors.filter((error) => error.code !== "invalid-turn-structure").toSorted(comparePosition)
|
|
1489
|
-
: [];
|
|
1478
|
+
// {§turn-shape} — every hard diagnostic is recoverable and rides with the admitted program
|
|
1479
|
+
// as a failed row; a lost boundary refuses only what follows it. The harness admits every
|
|
1480
|
+
// program whose meaning it can determine and reports what it could not read.
|
|
1481
|
+
const recoverableParseErrors = [...parseErrors].toSorted(comparePosition);
|
|
1490
1482
|
// {§empty-turn} — no operation and no other hard error: admitted as an empty turn, never
|
|
1491
|
-
// resampled; its advisories ({§bare-heading-advisory}) ride as notices.
|
|
1483
|
+
// resampled; its advisories ({§bare-heading-advisory}) ride as notices. A boundary lost
|
|
1484
|
+
// before any statement closed is not an empty turn: the model tried, and is resampled.
|
|
1492
1485
|
const emptyTurn = preParsedOps === undefined
|
|
1493
|
-
&&
|
|
1486
|
+
&& !hasUnparsedTail
|
|
1494
1487
|
&& sourceStatementCount === 0
|
|
1495
1488
|
&& parseErrors.every((error) => error.message === PlurnkParser.NO_VALID_OPERATION);
|
|
1496
1489
|
const emissionValid = preParsedOps !== undefined
|
|
1497
1490
|
|| emptyTurn
|
|
1498
|
-
|| (
|
|
1499
|
-
&& sourceStatementCount > 0
|
|
1500
|
-
&& recoverableParseErrors.length === parseErrors.length);
|
|
1501
|
-
const reasoning = assistant.reasoning ?? null;
|
|
1491
|
+
|| (hasUnparsedTail ? contentStatementCount > 0 : sourceStatementCount > 0);
|
|
1502
1492
|
return {
|
|
1503
1493
|
packetAssistant: { content: assistant.content, ops, reasoning },
|
|
1504
1494
|
sourceBacked: preParsedOps === undefined,
|
|
@@ -1506,18 +1496,19 @@ export default class TurnRunner {
|
|
|
1506
1496
|
parseErrors,
|
|
1507
1497
|
recoverableParseErrors: emissionValid && !emptyTurn ? recoverableParseErrors : [],
|
|
1508
1498
|
emptyTurn,
|
|
1499
|
+
proseAnswer,
|
|
1509
1500
|
parseNotices,
|
|
1510
1501
|
// The ANTLR model-turn parser is authoritative. At least one source
|
|
1511
|
-
// operation is required;
|
|
1502
|
+
// operation is required; lifecycle omission continues silently. Bounded
|
|
1512
1503
|
// statement failures become durable operation results.
|
|
1513
|
-
//
|
|
1514
|
-
//
|
|
1504
|
+
// A boundary lost before any statement closed rejects; after one, the
|
|
1505
|
+
// statements run and the loss is a row. Pre-parsed ops are Mock's trusted test seam.
|
|
1515
1506
|
emissionValid,
|
|
1516
1507
|
};
|
|
1517
1508
|
}
|
|
1518
1509
|
// #note12 — plugin reference docs are materialized beneath
|
|
1519
1510
|
// worker:///_plurnk/plurnk/ by LoopDocs.
|
|
1520
|
-
// {§exec-
|
|
1511
|
+
// {§exec-lifetime} — a `"turn"` lifetime: abort the worker's open turn-scoped streams via their
|
|
1521
1512
|
// owning scheme (the same registry-routed abort the total reap uses). Called at each pre-turn
|
|
1522
1513
|
// before the turn's own spawns, so every open turn-scoped sub here is from a prior turn — it
|
|
1523
1514
|
// never survives into the subsequent turn. Fire-and-forget: the spawn finalizes async and its
|
|
@@ -1530,4 +1521,6 @@ export default class TurnRunner {
|
|
|
1530
1521
|
return this.#admitted.executeAdmittedTurn(...args);
|
|
1531
1522
|
}
|
|
1532
1523
|
}
|
|
1524
|
+
_a = TurnRunner;
|
|
1525
|
+
export default TurnRunner;
|
|
1533
1526
|
//# sourceMappingURL=TurnRunner.js.map
|