@plurnk/plurnk-service 1.7.0 → 1.8.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 +29 -42
- package/INSTALL.md +49 -10
- package/README.md +11 -1
- package/SPEC.md +631 -279
- package/digest-sql/channel-dispositions/dispositions.sql +15 -0
- package/digest-sql/channel-state/state.sql +6 -0
- package/digest-sql/curation/curation.sql +5 -0
- package/digest-sql/derivations/derivations.sql +4 -0
- package/digest-sql/embeddings/embeddings.sql +4 -0
- package/digest-sql/entry-dispositions/dispositions.sql +15 -0
- package/digest-sql/entry-state/state.sql +6 -0
- package/digest-sql/token-counts/token-counts.sql +2 -0
- package/dist/Paths.d.ts +2 -5
- package/dist/Paths.d.ts.map +1 -1
- package/dist/Paths.js +9 -21
- package/dist/Paths.js.map +1 -1
- package/dist/build-info.json +1 -1
- package/dist/content/read-projector.d.ts.map +1 -1
- package/dist/content/read-projector.js +4 -3
- package/dist/content/read-projector.js.map +1 -1
- package/dist/core/BudgetReadout.d.ts.map +1 -1
- package/dist/core/BudgetReadout.js +12 -22
- package/dist/core/BudgetReadout.js.map +1 -1
- package/dist/core/ChannelWrite.d.ts.map +1 -1
- package/dist/core/ChannelWrite.js.map +1 -1
- package/dist/core/ChannelWrite.sql +2 -1
- package/dist/core/Dispatcher.d.ts +7 -6
- package/dist/core/Dispatcher.d.ts.map +1 -1
- package/dist/core/Dispatcher.js +34 -88
- package/dist/core/Dispatcher.js.map +1 -1
- package/dist/core/Engine.d.ts +11 -8
- package/dist/core/Engine.d.ts.map +1 -1
- package/dist/core/Engine.js +45 -27
- package/dist/core/Engine.js.map +1 -1
- package/dist/core/Engine.sql +112 -107
- package/dist/core/ExecutorRegistry.d.ts +8 -2
- package/dist/core/ExecutorRegistry.d.ts.map +1 -1
- package/dist/core/ExecutorRegistry.js +6 -3
- package/dist/core/ExecutorRegistry.js.map +1 -1
- package/dist/core/HostPaths.d.ts +29 -0
- package/dist/core/HostPaths.d.ts.map +1 -0
- package/dist/core/HostPaths.js +74 -0
- package/dist/core/HostPaths.js.map +1 -0
- package/dist/core/LegacyHome.d.ts +10 -0
- package/dist/core/LegacyHome.d.ts.map +1 -0
- package/dist/core/LegacyHome.js +170 -0
- package/dist/core/LegacyHome.js.map +1 -0
- package/dist/core/LogBody.d.ts +1 -1
- package/dist/core/LogBody.d.ts.map +1 -1
- package/dist/core/LogBody.js +2 -2
- package/dist/core/LogBody.js.map +1 -1
- package/dist/core/LogEntryProjection.js +1 -1
- package/dist/core/LogEntryProjection.js.map +1 -1
- package/dist/core/LogVisibility.d.ts +28 -0
- package/dist/core/LogVisibility.d.ts.map +1 -0
- package/dist/core/LogVisibility.js +215 -0
- package/dist/core/LogVisibility.js.map +1 -0
- package/dist/core/LoopLifecycle.d.ts +1 -0
- package/dist/core/LoopLifecycle.d.ts.map +1 -1
- package/dist/core/LoopLifecycle.js +8 -0
- package/dist/core/LoopLifecycle.js.map +1 -1
- package/dist/core/LoopLifecycle.sql +10 -0
- package/dist/core/OperatorConfig.d.ts +6 -0
- package/dist/core/OperatorConfig.d.ts.map +1 -0
- package/dist/core/OperatorConfig.js +92 -0
- package/dist/core/OperatorConfig.js.map +1 -0
- package/dist/core/OverflowTurn.d.ts +11 -0
- package/dist/core/OverflowTurn.d.ts.map +1 -0
- package/dist/core/OverflowTurn.js +77 -0
- package/dist/core/OverflowTurn.js.map +1 -0
- package/dist/core/Owner.js +2 -2
- package/dist/core/Owner.js.map +1 -1
- package/dist/core/PacketBuilder.d.ts +3 -15
- package/dist/core/PacketBuilder.d.ts.map +1 -1
- package/dist/core/PacketBuilder.js +48 -121
- package/dist/core/PacketBuilder.js.map +1 -1
- package/dist/core/ProblemLog.js +1 -1
- package/dist/core/ProblemLog.js.map +1 -1
- package/dist/core/ProposalLifecycle.d.ts.map +1 -1
- package/dist/core/ProposalLifecycle.js +3 -23
- package/dist/core/ProposalLifecycle.js.map +1 -1
- package/dist/core/ProviderInstantiate.d.ts +9 -5
- package/dist/core/ProviderInstantiate.d.ts.map +1 -1
- package/dist/core/ProviderInstantiate.js +99 -104
- package/dist/core/ProviderInstantiate.js.map +1 -1
- package/dist/core/ResourceMutations.d.ts.map +1 -1
- package/dist/core/ResourceMutations.js +5 -3
- package/dist/core/ResourceMutations.js.map +1 -1
- package/dist/core/SchemeRegistry.d.ts +0 -1
- package/dist/core/SchemeRegistry.d.ts.map +1 -1
- package/dist/core/SchemeRegistry.js +7 -11
- package/dist/core/SchemeRegistry.js.map +1 -1
- package/dist/core/ToolResources.d.ts +19 -0
- package/dist/core/ToolResources.d.ts.map +1 -0
- package/dist/core/ToolResources.js +122 -0
- package/dist/core/ToolResources.js.map +1 -0
- package/dist/core/Turn.d.ts +27 -0
- package/dist/core/Turn.d.ts.map +1 -0
- package/dist/core/Turn.js +43 -0
- package/dist/core/Turn.js.map +1 -0
- package/dist/core/Turn.sql +72 -0
- package/dist/core/TurnOps.d.ts +7 -0
- package/dist/core/TurnOps.d.ts.map +1 -0
- package/dist/core/TurnOps.js +63 -0
- package/dist/core/TurnOps.js.map +1 -0
- package/dist/core/TurnRunner.d.ts +12 -4
- package/dist/core/TurnRunner.d.ts.map +1 -1
- package/dist/core/TurnRunner.js +1045 -898
- package/dist/core/TurnRunner.js.map +1 -1
- package/dist/core/WorkerName.d.ts +1 -1
- package/dist/core/WorkerName.d.ts.map +1 -1
- package/dist/core/env-defaults.d.ts.map +1 -1
- package/dist/core/env-defaults.js +7 -5
- package/dist/core/env-defaults.js.map +1 -1
- package/dist/core/fork.d.ts.map +1 -1
- package/dist/core/fork.js +15 -4
- package/dist/core/fork.js.map +1 -1
- package/dist/core/fork.sql +57 -27
- package/dist/core/git-env.d.ts +0 -1
- package/dist/core/git-env.d.ts.map +1 -1
- package/dist/core/git-env.js +0 -3
- package/dist/core/git-env.js.map +1 -1
- package/dist/core/git-membership.d.ts.map +1 -1
- package/dist/core/git-membership.js +10 -26
- package/dist/core/git-membership.js.map +1 -1
- package/dist/core/git-state.d.ts.map +1 -1
- package/dist/core/git-state.js +13 -14
- package/dist/core/git-state.js.map +1 -1
- package/dist/core/operation-target-groups.d.ts +3 -0
- package/dist/core/operation-target-groups.d.ts.map +1 -0
- package/dist/core/operation-target-groups.js +53 -0
- package/dist/core/operation-target-groups.js.map +1 -0
- package/dist/core/packet-inject.d.ts +0 -1
- package/dist/core/packet-inject.d.ts.map +1 -1
- package/dist/core/packet-inject.js +6 -13
- package/dist/core/packet-inject.js.map +1 -1
- package/dist/core/packet-wire.d.ts +4 -0
- package/dist/core/packet-wire.d.ts.map +1 -1
- package/dist/core/packet-wire.js +208 -44
- package/dist/core/packet-wire.js.map +1 -1
- package/dist/core/plurnk-uri.d.ts +1 -1
- package/dist/core/plurnk-uri.d.ts.map +1 -1
- package/dist/core/plurnk-uri.js +3 -3
- package/dist/core/plurnk-uri.js.map +1 -1
- package/dist/core/teaching.d.ts.map +1 -1
- package/dist/core/teaching.js +4 -5
- package/dist/core/teaching.js.map +1 -1
- package/dist/core/worker-settings.d.ts +9 -0
- package/dist/core/worker-settings.d.ts.map +1 -0
- package/dist/core/worker-settings.js +25 -0
- package/dist/core/worker-settings.js.map +1 -0
- package/dist/core/workspace-settings.d.ts +0 -11
- package/dist/core/workspace-settings.d.ts.map +1 -1
- package/dist/core/workspace-settings.js +3 -47
- package/dist/core/workspace-settings.js.map +1 -1
- package/dist/digest/Digest.d.ts.map +1 -1
- package/dist/digest/Digest.js +164 -92
- package/dist/digest/Digest.js.map +1 -1
- package/dist/digest/digest.sql +8 -2
- package/dist/observe/genai.d.ts +9 -0
- package/dist/observe/genai.d.ts.map +1 -0
- package/dist/observe/genai.js +31 -0
- package/dist/observe/genai.js.map +1 -0
- package/dist/observe/spans.d.ts +3 -3
- package/dist/observe/spans.d.ts.map +1 -1
- package/dist/observe/spans.js +20 -5
- package/dist/observe/spans.js.map +1 -1
- package/dist/schemes/EffectPolicy.d.ts +3 -0
- package/dist/schemes/EffectPolicy.d.ts.map +1 -1
- package/dist/schemes/EffectPolicy.js +38 -6
- package/dist/schemes/EffectPolicy.js.map +1 -1
- package/dist/schemes/Exec.d.ts.map +1 -1
- package/dist/schemes/Exec.js +55 -13
- package/dist/schemes/Exec.js.map +1 -1
- package/dist/schemes/Log.d.ts +6 -2
- package/dist/schemes/Log.d.ts.map +1 -1
- package/dist/schemes/Log.js +73 -22
- package/dist/schemes/Log.js.map +1 -1
- package/dist/schemes/Log.sql +25 -7
- package/dist/schemes/Prompt.d.ts.map +1 -1
- package/dist/schemes/Prompt.js +1 -2
- package/dist/schemes/Prompt.js.map +1 -1
- package/dist/schemes/QuestionTool.d.ts +10 -0
- package/dist/schemes/QuestionTool.d.ts.map +1 -0
- package/dist/schemes/QuestionTool.js +81 -0
- package/dist/schemes/QuestionTool.js.map +1 -0
- package/dist/schemes/Skill.d.ts.map +1 -1
- package/dist/schemes/Skill.js +4 -1
- package/dist/schemes/Skill.js.map +1 -1
- package/dist/schemes/Worker.d.ts.map +1 -1
- package/dist/schemes/Worker.js +20 -3
- package/dist/schemes/Worker.js.map +1 -1
- package/dist/schemes/_entry-crud.sql +19 -1
- package/dist/schemes/_entry-find.d.ts +5 -3
- package/dist/schemes/_entry-find.d.ts.map +1 -1
- package/dist/schemes/_entry-find.js +102 -22
- package/dist/schemes/_entry-find.js.map +1 -1
- package/dist/schemes/_entry-find.sql +8 -6
- package/dist/schemes/_entry-graph.sql +2 -6
- package/dist/schemes/_entry-manifest.d.ts.map +1 -1
- package/dist/schemes/_entry-manifest.js +12 -1
- package/dist/schemes/_entry-manifest.js.map +1 -1
- package/dist/schemes/_search-index.d.ts.map +1 -1
- package/dist/schemes/_search-index.js +37 -25
- package/dist/schemes/_search-index.js.map +1 -1
- package/dist/server/Daemon.d.ts +58 -7
- package/dist/server/Daemon.d.ts.map +1 -1
- package/dist/server/Daemon.js +551 -113
- package/dist/server/Daemon.js.map +1 -1
- package/dist/server/DrainSupervisor.d.ts +12 -9
- package/dist/server/DrainSupervisor.d.ts.map +1 -1
- package/dist/server/DrainSupervisor.js +59 -10
- package/dist/server/DrainSupervisor.js.map +1 -1
- package/dist/server/client-input.d.ts +5 -2
- package/dist/server/client-input.d.ts.map +1 -1
- package/dist/server/client-input.js +53 -34
- package/dist/server/client-input.js.map +1 -1
- package/dist/server/dispatch-as-plurnk.d.ts.map +1 -1
- package/dist/server/dispatch-as-plurnk.js +28 -6
- package/dist/server/dispatch-as-plurnk.js.map +1 -1
- package/dist/server/drain.sql +26 -11
- package/dist/server/envelope.d.ts +1 -1
- package/dist/server/envelope.d.ts.map +1 -1
- package/dist/server/envelope.js +4 -4
- package/dist/server/envelope.js.map +1 -1
- package/dist/server/envelope.sql +25 -1
- package/dist/server/lifecycle-recovery.sql +11 -1
- package/dist/server/logEntry.d.ts +2 -1
- package/dist/server/logEntry.d.ts.map +1 -1
- package/dist/server/logEntry.js +3 -2
- package/dist/server/logEntry.js.map +1 -1
- package/dist/server/logEntry.sql +7 -0
- package/dist/server/loop-model.d.ts +2 -2
- package/dist/server/loop-model.d.ts.map +1 -1
- package/dist/server/loop-model.js +23 -33
- package/dist/server/loop-model.js.map +1 -1
- package/dist/server/loopDocs.d.ts.map +1 -1
- package/dist/server/loopDocs.js +61 -10
- package/dist/server/loopDocs.js.map +1 -1
- package/dist/server/loopDocs.sql +4 -2
- package/dist/server/model-catalog.d.ts +3 -0
- package/dist/server/model-catalog.d.ts.map +1 -0
- package/dist/server/model-catalog.js +83 -0
- package/dist/server/model-catalog.js.map +1 -0
- package/dist/server/model-route.d.ts +7 -0
- package/dist/server/model-route.d.ts.map +1 -0
- package/dist/server/model-route.js +47 -0
- package/dist/server/model-route.js.map +1 -0
- package/dist/server/module-discovery.d.ts +13 -0
- package/dist/server/module-discovery.d.ts.map +1 -0
- package/dist/server/module-discovery.js +78 -0
- package/dist/server/module-discovery.js.map +1 -0
- package/dist/server/skillDocs.d.ts +9 -0
- package/dist/server/skillDocs.d.ts.map +1 -0
- package/dist/server/skillDocs.js +212 -0
- package/dist/server/skillDocs.js.map +1 -0
- package/dist/server/skillDocs.sql +14 -0
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +182 -123
- package/dist/service.js.map +1 -1
- package/migrations/001_schema.sql +424 -65
- package/package.json +37 -37
- package/dist/core/ExecutableTools.d.ts +0 -11
- package/dist/core/ExecutableTools.d.ts.map +0 -1
- package/dist/core/ExecutableTools.js +0 -49
- package/dist/core/ExecutableTools.js.map +0 -1
- package/dist/core/JournalTurn.d.ts +0 -9
- package/dist/core/JournalTurn.d.ts.map +0 -1
- package/dist/core/JournalTurn.js +0 -14
- package/dist/core/JournalTurn.js.map +0 -1
- package/dist/core/JournalTurn.sql +0 -10
- package/dist/core/git-iso.d.ts +0 -13
- package/dist/core/git-iso.d.ts.map +0 -1
- package/dist/core/git-iso.js +0 -267
- package/dist/core/git-iso.js.map +0 -1
- package/dist/core/search-gate.d.ts +0 -17
- package/dist/core/search-gate.d.ts.map +0 -1
- package/dist/core/search-gate.js +0 -77
- package/dist/core/search-gate.js.map +0 -1
- package/dist/digest/Digest.sql +0 -1
package/dist/core/TurnRunner.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
var _a;
|
|
2
2
|
import { PlurnkParser, PlurnkParseError, UNKNOWN_POSITION } from "@plurnk/plurnk-contracts";
|
|
3
3
|
import Owner from "./Owner.js";
|
|
4
|
-
const TERMINAL_SEND_SIGNALS = new Set([102, 200, 202,
|
|
4
|
+
const TERMINAL_SEND_SIGNALS = new Set([102, 200, 202, 499]);
|
|
5
5
|
const comparePosition = (a, b) => a.line - b.line || a.column - b.column;
|
|
6
6
|
import Meta from "@plurnk/plurnk-meta";
|
|
7
7
|
import EntryCrud from "../schemes/_entry-crud.js";
|
|
@@ -11,6 +11,14 @@ import WorkspaceSettings from "./workspace-settings.js";
|
|
|
11
11
|
import { editedSpan } from "../content/index.js";
|
|
12
12
|
import { promptPathname, promptLoopPrefix, renderTarget } from "./plurnk-uri.js";
|
|
13
13
|
import { readFile } from "node:fs/promises";
|
|
14
|
+
// {§grammar-rail-registration} — bare names are the built-in rail namespace;
|
|
15
|
+
// everything else is an import specifier resolved through the Node chain.
|
|
16
|
+
export const resolveGrammarRailPath = (variant) => variant.startsWith("/") || variant.startsWith(".")
|
|
17
|
+
|| variant.includes("/") || variant.includes(":")
|
|
18
|
+
? (variant.startsWith("/") || variant.startsWith(".")
|
|
19
|
+
? variant
|
|
20
|
+
: fileURLToPath(import.meta.resolve(variant)))
|
|
21
|
+
: fileURLToPath(import.meta.resolve(`@plurnk/plurnk-contracts/${variant}`));
|
|
14
22
|
import { fileURLToPath } from "node:url";
|
|
15
23
|
import { randomUUID } from "node:crypto";
|
|
16
24
|
// Shared module imported by both Engine and bin/digest.ts, so wire
|
|
@@ -20,16 +28,22 @@ import PacketWire from "./packet-wire.js";
|
|
|
20
28
|
import Results, { OperationFailureError } from "./results.js";
|
|
21
29
|
import BranchReceipt from "./BranchReceipt.js";
|
|
22
30
|
import TerminalResult from "./TerminalResult.js";
|
|
31
|
+
import LoopLifecycle from "./LoopLifecycle.js";
|
|
23
32
|
import WorkerControlAddress from "./WorkerControlAddress.js";
|
|
24
|
-
import
|
|
33
|
+
import Turn from "./Turn.js";
|
|
25
34
|
import LogBody from "./LogBody.js";
|
|
35
|
+
import LogVisibility from "./LogVisibility.js";
|
|
26
36
|
import StrikeRail from "./StrikeRail.js";
|
|
27
37
|
import StoredPacket from "./StoredPacket.js";
|
|
28
38
|
import { observed, observedSync } from "../observe/spans.js";
|
|
39
|
+
import { GEN_AI_REQUEST_SPAN, genAiRequestOptions, settleGenAiResponse, } from "../observe/genai.js";
|
|
29
40
|
import { OPS_DISPATCHED, PROVIDER_CALLS, recordCounter } from "../observe/metrics.js";
|
|
30
41
|
import { scheduleTurnOps } from "./turn-scheduler.js";
|
|
42
|
+
import { expandSafeUriTargetGroup } from "./operation-target-groups.js";
|
|
31
43
|
import { readOptimisticSettlementMs } from "./optimistic-settlement.js";
|
|
32
44
|
import ModelCall, { ModelCallPersistenceError, ProviderAccountingIntegrityError, } from "./ModelCall.js";
|
|
45
|
+
import OverflowTurn from "./OverflowTurn.js";
|
|
46
|
+
import TurnOps from "./TurnOps.js";
|
|
33
47
|
const RECORD_STREAM_MIMETYPES = new Set(["application/jsonl", "application/x-ndjson"]);
|
|
34
48
|
const baseMimetype = (mimetype) => mimetype.split(";", 1)[0].trim().toLowerCase();
|
|
35
49
|
// {§exec-stream} Active streams publish only independently meaningful units.
|
|
@@ -56,8 +70,8 @@ const ENGINE_PROBLEMS = Object.freeze({
|
|
|
56
70
|
});
|
|
57
71
|
// The prompt entry target - prompt:///<loop>/<N>, self-only ({§prompt-self-only}):
|
|
58
72
|
// the owner rides the owner_id column, the address carries only the loop coordinate.
|
|
59
|
-
const promptTarget = (loopSeq,
|
|
60
|
-
const storage = promptPathname(loopSeq,
|
|
73
|
+
const promptTarget = (loopSeq, promptOrdinal) => {
|
|
74
|
+
const storage = promptPathname(loopSeq, promptOrdinal);
|
|
61
75
|
return {
|
|
62
76
|
kind: "url", raw: `prompt://${storage}`,
|
|
63
77
|
scheme: "prompt", username: null, password: null,
|
|
@@ -101,7 +115,7 @@ const readFilesItems = () => {
|
|
|
101
115
|
return null;
|
|
102
116
|
return normalizeFilesItems(Number.parseInt(raw, 10));
|
|
103
117
|
};
|
|
104
|
-
import { ProviderError,
|
|
118
|
+
import { ProviderError, scopeEnvToAlias, } from "@plurnk/plurnk-providers";
|
|
105
119
|
import { validateGbnf } from "@plurnk/gbnf";
|
|
106
120
|
import ProviderInstantiate from "./ProviderInstantiate.js";
|
|
107
121
|
// A generated rail may constrain only the sampled continuation while the chat
|
|
@@ -126,8 +140,8 @@ const grammarConstraint = (grammar) => {
|
|
|
126
140
|
response: grammar.replace(roots[0][0], `root ::= ${responseRoot}`),
|
|
127
141
|
};
|
|
128
142
|
};
|
|
129
|
-
const
|
|
130
|
-
const curationOverflowFailure = (pressure) => Results.failure("engine:context", "token-budget-overflow", 413,
|
|
143
|
+
const TOKEN_BUDGET_OVERFLOW_HARD_DETAIL = "Context Token Budget Overflow: tokensActiveTotal exceeded tokensActiveMax, and the causal overflow turn could not make enough room.";
|
|
144
|
+
const curationOverflowFailure = (pressure) => Results.failure("engine:context", "token-budget-overflow", 413, TOKEN_BUDGET_OVERFLOW_HARD_DETAIL, {}, {
|
|
131
145
|
usage: pressure.weight,
|
|
132
146
|
ceiling: pressure.budget,
|
|
133
147
|
deficit: pressure.excess,
|
|
@@ -196,7 +210,7 @@ class TurnRunner {
|
|
|
196
210
|
_a.#railGapWarned.add(message);
|
|
197
211
|
process.stderr.write(`plurnk-engine: rail verdict unavailable — the configured grammar did not parse in @plurnk/gbnf (${message})\n`);
|
|
198
212
|
}
|
|
199
|
-
static #requireGrammarEvidence(response) {
|
|
213
|
+
static #requireGrammarEvidence(response, allowWithheld) {
|
|
200
214
|
const evidence = response.grammarEvidence;
|
|
201
215
|
if (evidence === undefined) {
|
|
202
216
|
throw new Error("provider contract violation: configured GBNF response omitted grammar evidence");
|
|
@@ -209,32 +223,42 @@ class TurnRunner {
|
|
|
209
223
|
|| input.slice(evidence.contentStart).join("") !== response.assistant.content) {
|
|
210
224
|
throw new Error("provider contract violation: grammar evidence does not map exactly to assistant.content");
|
|
211
225
|
}
|
|
226
|
+
if (!allowWithheld && !evidence.transported) {
|
|
227
|
+
throw new Error("provider contract violation: configured GBNF was not transported outside explicit debug mode");
|
|
228
|
+
}
|
|
212
229
|
return evidence;
|
|
213
230
|
}
|
|
214
231
|
async #grammarConstraint(provider) {
|
|
215
|
-
//
|
|
216
|
-
//
|
|
217
|
-
// {§grammar-
|
|
218
|
-
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
232
|
+
// A real alias scopes its rail; an alias-free direct route uses the
|
|
233
|
+
// global provider configuration. Unrelated alias settings never apply.
|
|
234
|
+
// {§grammar-configuration-admission}
|
|
235
|
+
ProviderInstantiate.assertGrammarConfigurationScope(provider);
|
|
236
|
+
const alias = ProviderInstantiate.configurationAliasOf(provider);
|
|
237
|
+
const scoped = alias === undefined
|
|
238
|
+
? process.env
|
|
239
|
+
: scopeEnvToAlias(process.env, alias, [
|
|
240
|
+
"PLURNK_PROVIDERS_GBNF",
|
|
241
|
+
"PLURNK_PROVIDERS_GBNF_DEBUG",
|
|
242
|
+
]);
|
|
243
|
+
const variant = scoped.PLURNK_PROVIDERS_GBNF;
|
|
225
244
|
if (variant === undefined || variant === "" || variant === "0")
|
|
226
245
|
return undefined;
|
|
246
|
+
const allowWithheld = scoped.PLURNK_PROVIDERS_GBNF_DEBUG !== undefined
|
|
247
|
+
&& scoped.PLURNK_PROVIDERS_GBNF_DEBUG !== ""
|
|
248
|
+
&& scoped.PLURNK_PROVIDERS_GBNF_DEBUG !== "0";
|
|
227
249
|
const hit = this.#gbnfCache.get(variant);
|
|
228
250
|
if (hit !== undefined)
|
|
229
|
-
return hit;
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
251
|
+
return { ...hit, allowWithheld };
|
|
252
|
+
// {§grammar-rail-registration} — a bare name is a built-in rail subpath
|
|
253
|
+
// under @plurnk/plurnk-contracts; any other form is an import specifier
|
|
254
|
+
// (an operator file path or a package export subpath), so a third-party
|
|
255
|
+
// rail package plugs in with no built-in registry change.
|
|
256
|
+
const path = resolveGrammarRailPath(variant);
|
|
233
257
|
const text = await readFile(path, "utf8"); // unresolvable/unreadable throws — a configured rail never silently degrades
|
|
234
258
|
const constraint = grammarConstraint(text);
|
|
235
259
|
this.#gbnfCache.set(variant, constraint);
|
|
236
260
|
process.stderr.write(`plurnk-engine: GBNF constraint: ${alias || "(bare)"} → ${variant} (${text.length} chars)\n`);
|
|
237
|
-
return constraint;
|
|
261
|
+
return { ...constraint, allowWithheld };
|
|
238
262
|
}
|
|
239
263
|
// A lineage's no-parent root; a root worker resolves to itself. Fail hard
|
|
240
264
|
// when corruption leaves a worker without one. {§worker-primary}
|
|
@@ -282,7 +306,7 @@ class TurnRunner {
|
|
|
282
306
|
}
|
|
283
307
|
const settlements = await Promise.allSettled(prepared.map(async ({ statement, modelCall, attributions, providerWorkerId }) => {
|
|
284
308
|
try {
|
|
285
|
-
const response = await observed(
|
|
309
|
+
const response = await observed(GEN_AI_REQUEST_SPAN, { model: provider.model, attempt: 1, kind: "bare" }, async (span) => {
|
|
286
310
|
try {
|
|
287
311
|
const generated = await provider.generate({
|
|
288
312
|
messages: [{ role: "user", content: statement.body }],
|
|
@@ -303,6 +327,7 @@ class TurnRunner {
|
|
|
303
327
|
status: "resolved",
|
|
304
328
|
});
|
|
305
329
|
span.setAttribute("status", "resolved");
|
|
330
|
+
settleGenAiResponse(span, generated);
|
|
306
331
|
return generated;
|
|
307
332
|
}
|
|
308
333
|
catch (error) {
|
|
@@ -311,7 +336,7 @@ class TurnRunner {
|
|
|
311
336
|
}
|
|
312
337
|
throw error;
|
|
313
338
|
}
|
|
314
|
-
});
|
|
339
|
+
}, genAiRequestOptions(ProviderInstantiate.aliasOf(provider) ?? "plurnk", provider.model));
|
|
315
340
|
await modelCall.observeResponse(response);
|
|
316
341
|
return {
|
|
317
342
|
statement,
|
|
@@ -361,777 +386,44 @@ class TurnRunner {
|
|
|
361
386
|
}
|
|
362
387
|
return { line, column };
|
|
363
388
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
// Turn rows are created at runTurn OPEN (status=102, placeholder
|
|
372
|
-
// packet) so things can be written into the turn before the model
|
|
373
|
-
// is called: the user prompt on turn 1; later, system signals or
|
|
374
|
-
// injected Notices on any turn. The turn is CLOSED at
|
|
375
|
-
// the end with the final packet + status + usage stats.
|
|
376
|
-
//
|
|
377
|
-
// sequence is "ordinal of stuff in this turn." Pre-model
|
|
378
|
-
// writes consume low indices; model ops continue from there.
|
|
379
|
-
const seqRow = await this.#db.engine_next_turn_sequence.get({ loop_id: loopId });
|
|
380
|
-
const seq = seqRow.next;
|
|
381
|
-
// loops.sequence is the loop's ordinal within the worker. Turn-0 foists that belong to the
|
|
382
|
-
// WORKER (manifest preview, AGENTS, operator docs) gate on the worker's first loop, not every loop's
|
|
383
|
-
// first turn ({§actor-boundary-catalog-preview}); per-loop foists such as
|
|
384
|
-
// {§prompt-entry} still fire each loop. Read once, turn-1 only.
|
|
385
|
-
const loopRow = seq === 1
|
|
386
|
-
? await this.#db.engine_get_loop_prompt.get({ loop_id: loopId })
|
|
387
|
-
: undefined;
|
|
388
|
-
const workerFirstLoop = (loopRow?.sequence ?? 0) === 1;
|
|
389
|
-
const openRow = await this.#db.engine_open_turn.get({
|
|
390
|
-
loop_id: loopId, sequence: seq,
|
|
391
|
-
});
|
|
392
|
-
if (openRow === undefined)
|
|
393
|
-
throw new Error("Engine.runTurn: turn open returned no row");
|
|
394
|
-
const turnId = openRow.id;
|
|
395
|
-
// {§env-delta-log-pull} — establish a fresh worker's observation
|
|
396
|
-
// baseline immediately after its first turn opens. A fork already has
|
|
397
|
-
// its parent's cursor, so the NULL-guarded statement leaves it intact.
|
|
398
|
-
// Events committed after this statement belong to this or a later
|
|
399
|
-
// closed pull window; pre-existing state is read through the ordinary
|
|
400
|
-
// shared-world projections in this first packet.
|
|
401
|
-
await this.#db.engine_initialize_ambient_cursor.get({ workspace_id: workspaceId, worker_id: workerId });
|
|
402
|
-
// Threaded per turn, never engine state, so concurrent loops on
|
|
403
|
-
// different providers each read their own honest tokenizer values.
|
|
404
|
-
const systemCtx = {
|
|
405
|
-
db: this.#db, workspaceId, workerId, loopId, turnId,
|
|
406
|
-
writer: "plurnk",
|
|
407
|
-
signal: this.#loopSignal(loopId),
|
|
408
|
-
streamEventNotify: this.#streamEventNotify,
|
|
409
|
-
wakeWorkerNotify: this.#wakeWorkerNotify,
|
|
410
|
-
weigh: this.#weighContent,
|
|
411
|
-
mimetypes: this.#mimetypes,
|
|
412
|
-
defaultChannelFor: (s) => this.#schemes.defaultChannelFor(s, workspaceId),
|
|
413
|
-
pushNotice: (notice) => this.#notices.push(workspaceId, loopId, notice),
|
|
414
|
-
requestInteraction: (request) => this.#interactions.request(request, { workspaceId, workerId, loopId, turnId }, this.#loopSignal(loopId)),
|
|
415
|
-
};
|
|
416
|
-
// Pre-model writes. Each prompt the model has not seen yet becomes an
|
|
417
|
-
// actionless `prompt` log row whose target is its durable prompt:// entry:
|
|
418
|
-
// - Turn 1: loop.prompt is the initial user prompt.
|
|
419
|
-
// - Turn N>1: only if Engine.inject (or wake-on-completion via
|
|
420
|
-
// daemon.inject) wrote a prompt entry for this turn slot
|
|
421
|
-
// between turn N-1 and N. Inject writes directly to entries;
|
|
422
|
-
// we DON'T re-foist here for N>1.
|
|
423
|
-
// Model ops dispatch after these pre-model rows.
|
|
424
|
-
let nextActionIndex = 1;
|
|
425
|
-
const turnOpenPaths = [];
|
|
426
|
-
// {§worker-initialization-entry} — the worker's first turn opens with a kernel-authored initialization: a
|
|
427
|
-
// worked turn PLAN → the environment FINDs the foist ACTUALLY dispatches → SEND signal 102. Built
|
|
428
|
-
// from the real ops below (not a static print — we lean into the genuine echo paradigm) and
|
|
429
|
-
// written at sequence 1, so it reads first with the foisted results following.
|
|
430
|
-
const turnZeroMoves = [];
|
|
431
|
-
if (seq === 1) {
|
|
432
|
-
if (workerFirstLoop)
|
|
433
|
-
nextActionIndex = 2; // reserve sequence 1 for initialization
|
|
434
|
-
// Operator doc READs (PLURNK_SERVICE_MD_<ALIAS>, {§actor-boundary-doc-injection}). The docs were materialized
|
|
435
|
-
// as worker://plurnk/<entry> entries by the plurnk worker (LoopDocs, via the
|
|
436
|
-
// {§actor-boundary} keystone); foist a READ of each into THIS turn-0 so the model
|
|
437
|
-
// reads them inline. It sees only the READ — the materializing EDIT
|
|
438
|
-
// lives in the plurnk worker's log, never the model's.
|
|
439
|
-
// {§operator-config-workspace-md-docs} — env docs union the workspace's client docs; foist a READ of
|
|
440
|
-
// each materialized worker://plurnk/<alias>.md (LoopDocs materialized the same set).
|
|
441
|
-
const { mdDocs } = await WorkspaceSettings.read(this.#db, workspaceId);
|
|
442
|
-
// {§actor-boundary-doc-injection} — operator docs appear on the worker's first loop.
|
|
443
|
-
for (const doc of workerFirstLoop ? await WorkspaceSettings.resolveDocs(mdDocs) : []) {
|
|
444
|
-
const docTarget = {
|
|
445
|
-
kind: "url", raw: `worker://plurnk/${doc.entryName}`, scheme: "worker",
|
|
446
|
-
username: null, password: null, hostname: "plurnk", port: null,
|
|
447
|
-
pathname: `/${doc.entryName}`, query: null, fragment: null,
|
|
448
|
-
};
|
|
449
|
-
const docRead = {
|
|
450
|
-
op: "READ", suffix: "", signal: null, target: docTarget,
|
|
451
|
-
lineMarker: null, body: null, position: UNKNOWN_POSITION,
|
|
452
|
-
};
|
|
453
|
-
await this.#dispatch({
|
|
454
|
-
statement: docRead, workspaceId, workerId, loopId, turnId,
|
|
455
|
-
sequence: nextActionIndex, origin: "plurnk", onDispatch,
|
|
456
|
-
});
|
|
457
|
-
nextActionIndex++;
|
|
458
|
-
}
|
|
459
|
-
const promptRow = loopRow; // {§prompt-entry} — per-loop; fires every loop's turn 1
|
|
460
|
-
if (promptRow !== undefined && typeof promptRow.prompt === "string" && promptRow.prompt.length > 0) {
|
|
461
|
-
const openPaths = assertOpenPaths(JSON.parse(promptRow.open_paths), `Loop ${loopId} open_paths`);
|
|
462
|
-
const promptLoopSeq = promptRow.sequence; // the loop's per-worker sequence — model-facing, matching log coordinates (owner: the db id read as prompt/2/1)
|
|
463
|
-
const promptPath = promptTarget(promptLoopSeq, seq);
|
|
464
|
-
const entry = {
|
|
465
|
-
channels: { body: { content: promptRow.prompt, mimetype: "text/markdown" } },
|
|
466
|
-
attributes: { openPaths },
|
|
467
|
-
};
|
|
468
|
-
await EntryCrud.writeEntry(promptPath.pathname, entry, systemCtx, "prompt", workerId);
|
|
469
|
-
turnOpenPaths.push(...openPaths);
|
|
470
|
-
const promptLogId = await this.#writePromptLog({
|
|
471
|
-
workerId,
|
|
472
|
-
loopId,
|
|
473
|
-
turnId,
|
|
474
|
-
sequence: nextActionIndex,
|
|
475
|
-
target: promptPath,
|
|
476
|
-
content: promptRow.prompt,
|
|
477
|
-
});
|
|
478
|
-
onDispatch?.(promptLogId);
|
|
479
|
-
nextActionIndex++;
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
// {§prompt-loop-containment}: the loop contains every prompt that arrived
|
|
483
|
-
// while it ran. Publish each undelivered frame as a prompt row, oldest
|
|
484
|
-
// first, so rapid arrivals reach the model together exactly once.
|
|
485
|
-
{
|
|
486
|
-
const loopSeqRow = await this.#db.engine_loop_sequence.get({ loop_id: loopId });
|
|
487
|
-
const loopSeq = loopSeqRow?.sequence ?? loopId;
|
|
488
|
-
const prefix = promptLoopPrefix(loopSeq);
|
|
489
|
-
const undelivered = (await this.#db.drain_undelivered_prompts_for_loop.all({
|
|
490
|
-
owner_id: workerId,
|
|
491
|
-
pattern: `${prefix}%`,
|
|
492
|
-
prefix_len: prefix.length,
|
|
493
|
-
loop_id: loopId,
|
|
494
|
-
}))
|
|
495
|
-
.filter((r) => typeof r.content === "string" && r.content.length > 0);
|
|
496
|
-
for (const injectedRow of undelivered) {
|
|
497
|
-
const attributes = parsePromptAttributes(injectedRow.attributes, `Prompt ${injectedRow.pathname} attributes`);
|
|
498
|
-
const encodedOpenPaths = attributes.openPaths;
|
|
499
|
-
if (encodedOpenPaths !== undefined) {
|
|
500
|
-
turnOpenPaths.push(...assertOpenPaths(encodedOpenPaths, `Prompt ${injectedRow.pathname} openPaths`));
|
|
501
|
-
}
|
|
502
|
-
const ordinal = Number(injectedRow.pathname.split("/").filter(Boolean).at(-1));
|
|
503
|
-
const injTarget = promptTarget(loopSeq, ordinal);
|
|
504
|
-
const promptLogId = await this.#writePromptLog({
|
|
505
|
-
workerId,
|
|
506
|
-
loopId,
|
|
507
|
-
turnId,
|
|
508
|
-
sequence: nextActionIndex,
|
|
509
|
-
target: injTarget,
|
|
510
|
-
content: injectedRow.content,
|
|
511
|
-
});
|
|
512
|
-
onDispatch?.(promptLogId);
|
|
513
|
-
nextActionIndex++;
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
// The persistent search-index pass (_search-index.maintain) attaches
|
|
517
|
-
// every readable entry/log projection to complete graph/FTS/vector derivations.
|
|
518
|
-
// NOT an action: no log entry, no sequence slot,
|
|
519
|
-
// not dispatched. There is no materialized manifest entry — the catalog
|
|
520
|
-
// is served on demand by FIND: recursive when asked, shallow-mapped below.
|
|
521
|
-
// {§semantic-embed-dedup} — one pass-wide semantic plan binds every
|
|
522
|
-
// chunk counter to the derivation identity it produces.
|
|
523
|
-
// SPEC {§membership} D4/D5 — git-ls-files workspace membership, resolved at
|
|
524
|
-
// prompt-composition (EMI is eager + exhaustive — git is the only bound). When the
|
|
525
|
-
// workspace's project_root is a git working tree, tracked files are
|
|
526
|
-
// members without a client `add`; active members are materialized
|
|
527
|
-
// (disk → body channel) so they appear in the catalog. No-ops
|
|
528
|
-
// on headless / non-git workspaces. Runs BEFORE the derivation pump so
|
|
529
|
-
// this turn's packet reflects them.
|
|
530
|
-
// Workspace creation starts this eagerly. Joining here is the correctness
|
|
531
|
-
// boundary: the model never runs against partial graph/vector coverage.
|
|
532
|
-
await this.#warmWorkspace(systemCtx, false);
|
|
533
|
-
// The warm materialized membership before deriving. This second pass is the
|
|
534
|
-
// ordinary cheap change detector and captures any drift that landed meanwhile.
|
|
535
|
-
const fsDivergences = await GitMembership.indexGitMembership(systemCtx);
|
|
536
|
-
// {§packet-git-status} — one post-reconciliation snapshot supplies both
|
|
537
|
-
// the compact packet summary and each causal file event's exact XY state.
|
|
538
|
-
const gitStatus = await GitState.status(this.#db, workspaceId, this.#loopSignal(loopId));
|
|
539
|
-
await this.#logFsFictions(workspaceId, fsDivergences, gitStatus);
|
|
540
|
-
// The refresh above may have changed bodies (including model/client edits
|
|
541
|
-
// since the startup warm). Re-derive to completion before packet/model
|
|
542
|
-
// construction. Membership is already current, so this pass does not
|
|
543
|
-
// consume the filesystem divergences a second time.
|
|
544
|
-
await this.#warmWorkspace(systemCtx, true, false);
|
|
545
|
-
// Turn-0 catalog preview (PLURNK_SERVICE_FILES_ITEMS, {§actor-boundary-catalog-preview}):
|
|
546
|
-
// Four FIND surveys foisted into the worker's first model turn establish the project,
|
|
547
|
-
// commons, private, and documentation surfaces in that order. Their `init`
|
|
548
|
-
// classification lets the model curate this opening survey as one log set.
|
|
549
|
-
if (seq === 1) {
|
|
550
|
-
// {§operator-config-workspace-files-items} — workspace filesItems replaces the env default.
|
|
551
|
-
const { filesItems: workspaceMI } = await WorkspaceSettings.read(this.#db, workspaceId);
|
|
552
|
-
const filesItems = workspaceMI !== null ? normalizeFilesItems(workspaceMI) : readFilesItems();
|
|
553
|
-
if (filesItems !== null && workerFirstLoop) { // {§actor-boundary-catalog-preview} — once per worker
|
|
554
|
-
const catalogSchemes = await this.#db.engine_scheme_catalog_summary.all({ workspace_id: workspaceId });
|
|
555
|
-
const fileItems = catalogSchemes.find(({ scheme }) => scheme === "file")?.shallow_items ?? 0;
|
|
556
|
-
const fileCap = filesItems > 0 && fileItems > 0 ? Math.min(filesItems, fileItems) : null;
|
|
557
|
-
await Owner.kernelId(this.#db, workspaceId);
|
|
558
|
-
const surveys = [
|
|
559
|
-
{
|
|
560
|
-
statement: {
|
|
561
|
-
op: "FIND", suffix: "", signal: ["+init"],
|
|
562
|
-
target: { kind: "local", raw: "*" },
|
|
563
|
-
body: null,
|
|
564
|
-
lineMarker: fileCap === null ? null : { marks: [1, fileCap] },
|
|
565
|
-
position: UNKNOWN_POSITION,
|
|
566
|
-
},
|
|
567
|
-
exemplar: `## FIND0 [+init] (*)${fileCap === null ? "" : ` <1,${fileCap}>`}`,
|
|
568
|
-
},
|
|
569
|
-
{
|
|
570
|
-
statement: {
|
|
571
|
-
op: "FIND", suffix: "", signal: ["+init"],
|
|
572
|
-
target: { kind: "url", raw: "worker:///*", scheme: "worker", username: null, password: null, hostname: null, port: null, pathname: "/*", query: null, fragment: null },
|
|
573
|
-
body: null, lineMarker: null, position: UNKNOWN_POSITION,
|
|
574
|
-
},
|
|
575
|
-
exemplar: "## FIND0 [+init] (worker:///*)",
|
|
576
|
-
},
|
|
577
|
-
{
|
|
578
|
-
statement: {
|
|
579
|
-
op: "FIND", suffix: "", signal: ["+init"],
|
|
580
|
-
target: { kind: "url", raw: "worker://~/*", scheme: "worker", username: null, password: null, hostname: "~", port: null, pathname: "/*", query: null, fragment: null },
|
|
581
|
-
body: null, lineMarker: null, position: UNKNOWN_POSITION,
|
|
582
|
-
},
|
|
583
|
-
exemplar: "## FIND0 [+init] (worker://~/*)",
|
|
584
|
-
},
|
|
585
|
-
{
|
|
586
|
-
statement: {
|
|
587
|
-
op: "FIND", suffix: "", signal: ["+init", "+docs"],
|
|
588
|
-
target: { kind: "url", raw: "worker://plurnk/docs/**", scheme: "worker", username: null, password: null, hostname: "plurnk", port: null, pathname: "/docs/**", query: null, fragment: null },
|
|
589
|
-
body: null, lineMarker: { marks: [1, -1] }, position: UNKNOWN_POSITION,
|
|
590
|
-
},
|
|
591
|
-
exemplar: "## FIND0 [+init,+docs] (worker://plurnk/docs/**) <1,-1>",
|
|
592
|
-
},
|
|
593
|
-
];
|
|
594
|
-
for (const { statement, exemplar } of surveys) {
|
|
595
|
-
await this.#dispatch({ statement, workspaceId, workerId, loopId, turnId, sequence: nextActionIndex, origin: "plurnk", onDispatch });
|
|
596
|
-
nextActionIndex++;
|
|
597
|
-
turnZeroMoves.push(exemplar);
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
// {§worker-initialization-entry} — write the kernel's turn-0 initialization OPEN at sequence 1: PLAN → the FINDs actually
|
|
601
|
-
// foisted above (real, their results already in the log) → SEND signal 102. Dynamic — it reflects
|
|
602
|
-
// the true survey, never a frozen print — and OPEN: the worked example the model orients on,
|
|
603
|
-
// so the grammar can stay thin.
|
|
604
|
-
if (workerFirstLoop) {
|
|
605
|
-
const initialization = [
|
|
606
|
-
"# PLAN0\n* Initialization complete.\n* Next: address the prompt.",
|
|
607
|
-
...turnZeroMoves,
|
|
608
|
-
"## SEND0 [102]\nNext, address the prompt.",
|
|
609
|
-
].join("\n\n");
|
|
610
|
-
await this.#dispatcher.writeInitializationEntry({ verbatim: initialization, workerId, loopId, turnId, sequence: 1 });
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
// {§methods-loop-run-open-paths}: selected workspace paths belong to
|
|
614
|
-
// the prompt frame. Publish the frame, then dispatch ordinary core READs
|
|
615
|
-
// in that same turn; missing/non-member paths retain their normal 4xx.
|
|
616
|
-
for (const raw of turnOpenPaths) {
|
|
617
|
-
const pathname = raw.startsWith("/") ? raw : `/${raw}`;
|
|
618
|
-
const fileRead = {
|
|
619
|
-
op: "READ", suffix: "", signal: null, lineMarker: null,
|
|
620
|
-
target: {
|
|
621
|
-
kind: "url", raw: `file://${pathname}`, scheme: "file",
|
|
622
|
-
username: null, password: null, hostname: null, port: null,
|
|
623
|
-
pathname, query: null, fragment: null,
|
|
624
|
-
},
|
|
625
|
-
body: null, position: UNKNOWN_POSITION,
|
|
626
|
-
};
|
|
627
|
-
await this.#dispatch({
|
|
628
|
-
statement: fileRead, workspaceId, workerId, loopId, turnId,
|
|
629
|
-
sequence: nextActionIndex, origin: "plurnk", onDispatch,
|
|
630
|
-
});
|
|
631
|
-
nextActionIndex++;
|
|
632
|
-
}
|
|
633
|
-
// {§env-delta-log-pull} — materialize ambient observations before packet
|
|
634
|
-
// composition and reserve their action indices. {§exec-stream} owns the
|
|
635
|
-
// distinct byte-cursor path for this worker's streams.
|
|
636
|
-
// {§exec-poll} — EXEC `<0>` is turn-scoped: reap the worker's open turn-scoped streams (necessarily
|
|
637
|
-
// from a prior turn — this runs before the turn's own spawns) so a `<0>` never survives into
|
|
638
|
-
// the subsequent turn. The terminal output then surfaces born-OPEN via the stream-delta path.
|
|
639
|
-
await this.#reapTurnScopedStreams(workerId);
|
|
640
|
-
nextActionIndex += await this.#materializeEnvironmentDeltas({ workspaceId, workerId, loopId, turnId, fromSequence: nextActionIndex });
|
|
641
|
-
nextActionIndex += await this.#materializeStreamDeltas({ workspaceId, workerId, loopId, turnId, fromSequence: nextActionIndex });
|
|
642
|
-
// The post-reconciliation Git snapshot above is threaded into the packet
|
|
643
|
-
// and every budget rebuild; overflow never shells again.
|
|
644
|
-
// Notices are non-terminal observations, never operation-failure truth.
|
|
645
|
-
// Drain once and thread the same set through every grinder rebuild.
|
|
646
|
-
const notices = this.#notices.drain(loopId)
|
|
647
|
-
.filter((event) => event.level !== "info");
|
|
648
|
-
// Build the model request packet ({§packet-stored-shape}). The log build
|
|
649
|
-
// queries log_entries scoped to the worker — the prompt entry just
|
|
650
|
-
// written (if turn 1) is part of that query result.
|
|
651
|
-
let promptProjection = "automatic";
|
|
652
|
-
const buildPacket = () => this.#packets.buildRequestPacket({
|
|
653
|
-
initialMessages: messages,
|
|
654
|
-
requirements,
|
|
655
|
-
workspaceId,
|
|
656
|
-
workerId,
|
|
657
|
-
loopId,
|
|
658
|
-
currentTurnSeq: seq,
|
|
659
|
-
provider,
|
|
660
|
-
gitStatus,
|
|
661
|
-
notices,
|
|
662
|
-
transientOpenLogEntryId,
|
|
663
|
-
promptProjection,
|
|
664
|
-
});
|
|
665
|
-
let requestPacket = await buildPacket();
|
|
666
|
-
// SPEC {§grinder} — budget grinder, pre-LLM: reclaim window on actual overflow.
|
|
667
|
-
const enforced = await this.#packets.enforceBudget({
|
|
668
|
-
packet: requestPacket, provider, loopId, turnId,
|
|
669
|
-
recordOverflow: async (pressure) => {
|
|
670
|
-
await this.#problems.record({
|
|
671
|
-
workerId,
|
|
672
|
-
loopId,
|
|
673
|
-
turnId,
|
|
674
|
-
sequence: nextActionIndex++,
|
|
675
|
-
origin: "plurnk",
|
|
676
|
-
source: "engine",
|
|
677
|
-
result: curationOverflowFailure(pressure),
|
|
678
|
-
});
|
|
679
|
-
},
|
|
680
|
-
rebuild: buildPacket,
|
|
681
|
-
});
|
|
682
|
-
requestPacket = enforced.packet;
|
|
683
|
-
let boundaryRolledBack = enforced.boundaryRolledBack;
|
|
684
|
-
let modelMessages = PacketWire.packetToWireMessages(requestPacket);
|
|
685
|
-
// Curation pressure and provider generation are independent. The
|
|
686
|
-
// provider owns its configured total output envelope.
|
|
687
|
-
let response;
|
|
688
|
-
let splitResponse;
|
|
689
|
-
let railGrammar;
|
|
690
|
-
let railResponseGrammar;
|
|
691
|
-
let railEvidence;
|
|
692
|
-
let emissionAttempts = 0;
|
|
693
|
-
let providerCallInFlight = false;
|
|
694
|
-
let modelCallSequence = 0;
|
|
695
|
-
let currentEmissionAttempt = 0;
|
|
696
|
-
let providerAttemptId = null;
|
|
697
|
-
let providerModelCall = null;
|
|
698
|
-
let providerAttemptAttributions = [];
|
|
699
|
-
const providerSignal = this.#loopSignal(loopId) ?? signal;
|
|
700
|
-
// {§client-metadata}
|
|
701
|
-
const { client } = await WorkspaceSettings.read(this.#db, workspaceId);
|
|
702
|
-
const loopSeq = (await this.#db.engine_loop_sequence.get({ loop_id: loopId }))?.sequence ?? loopId;
|
|
703
|
-
const providerIdentity = await this.#resolveWorkerProviderIdentity(workerId);
|
|
704
|
-
const { workerId: providerWorkerId, primaryWorkerId } = providerIdentity;
|
|
705
|
-
const classifyProviderAttempt = async (id, attemptSplit, emissionAttempt, accepted) => {
|
|
706
|
-
const result = await this.#db.engine_classify_turn_attempt_response.run({
|
|
707
|
-
id,
|
|
708
|
-
accepted: accepted ? 1 : 0,
|
|
709
|
-
parse_errors: JSON.stringify(attemptSplit.parseErrors),
|
|
710
|
-
});
|
|
711
|
-
if (result.changes !== 1) {
|
|
712
|
-
throw new Error(`emission attempt ${id} was not awaiting classification`);
|
|
713
|
-
}
|
|
714
|
-
emissionAttempts = emissionAttempt;
|
|
715
|
-
};
|
|
716
|
-
const recoverCapacityPacket = async () => {
|
|
717
|
-
let baselineMessages = modelMessages;
|
|
718
|
-
if (promptProjection === "automatic") {
|
|
719
|
-
promptProjection = "withheld";
|
|
720
|
-
const candidate = await buildPacket();
|
|
721
|
-
const candidateMessages = PacketWire.packetToWireMessages(candidate);
|
|
722
|
-
if (JSON.stringify(candidateMessages) !== JSON.stringify(baselineMessages)) {
|
|
723
|
-
requestPacket = candidate;
|
|
724
|
-
modelMessages = candidateMessages;
|
|
725
|
-
return true;
|
|
726
|
-
}
|
|
727
|
-
baselineMessages = candidateMessages;
|
|
728
|
-
}
|
|
729
|
-
if (!boundaryRolledBack) {
|
|
730
|
-
await this.#packets.rollbackNewestBoundary(loopId, turnId);
|
|
731
|
-
boundaryRolledBack = true;
|
|
732
|
-
const candidate = await buildPacket();
|
|
733
|
-
const candidateMessages = PacketWire.packetToWireMessages(candidate);
|
|
734
|
-
if (JSON.stringify(candidateMessages) !== JSON.stringify(baselineMessages)) {
|
|
735
|
-
requestPacket = candidate;
|
|
736
|
-
modelMessages = candidateMessages;
|
|
737
|
-
return true;
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
return false;
|
|
741
|
-
};
|
|
742
|
-
try {
|
|
743
|
-
// {§turn-lifecycle}: bracket the complete provider-attempt window with liveness notices.
|
|
744
|
-
if (!signal?.aborted)
|
|
745
|
-
this.#notices.push(workspaceId, loopId, { source: "engine:turn", kind: "turn_awaiting_model", level: "info", message: "awaiting model response" });
|
|
746
|
-
const railConstraint = await this.#grammarConstraint(provider);
|
|
747
|
-
railGrammar = railConstraint?.transport;
|
|
748
|
-
railResponseGrammar = railConstraint?.response;
|
|
749
|
-
const attemptLimit = readEmissionAttempts();
|
|
750
|
-
const strikeStreak = this.#strikes.streak(loopId);
|
|
751
|
-
for (let attempt = 1; attempt <= attemptLimit;) {
|
|
752
|
-
// Capacity recovery may rebuild and resend the request without
|
|
753
|
-
// consuming a grammar-emission attempt. Every logical provider
|
|
754
|
-
// call still receives its own durable sequence and accounting.
|
|
755
|
-
currentEmissionAttempt = attempt;
|
|
756
|
-
modelCallSequence++;
|
|
757
|
-
const attributionContext = Object.freeze({
|
|
758
|
-
workspaceId: String(workspaceId),
|
|
759
|
-
workerId: providerWorkerId,
|
|
760
|
-
primaryWorkerId,
|
|
761
|
-
loop: loopSeq,
|
|
762
|
-
turn: seq,
|
|
763
|
-
attempt: modelCallSequence,
|
|
764
|
-
});
|
|
765
|
-
providerAttemptAttributions = await this.#attemptAttributions(provider, attributionContext);
|
|
766
|
-
requestPacket = { ...requestPacket, attributions: providerAttemptAttributions };
|
|
767
|
-
providerModelCall = await ModelCall.open(this.#db, {
|
|
768
|
-
turnId,
|
|
769
|
-
sequence: modelCallSequence,
|
|
770
|
-
kind: "emission",
|
|
771
|
-
attributions: providerAttemptAttributions,
|
|
772
|
-
model: provider.model,
|
|
773
|
-
});
|
|
774
|
-
const attemptRow = await this.#db.engine_open_turn_attempt.get({
|
|
775
|
-
model_call_id: providerModelCall.id,
|
|
776
|
-
});
|
|
777
|
-
if (attemptRow === undefined) {
|
|
778
|
-
throw new Error(`Engine.runTurn: provider call ${modelCallSequence} did not open`);
|
|
779
|
-
}
|
|
780
|
-
providerAttemptId = attemptRow.id;
|
|
781
|
-
providerCallInFlight = true;
|
|
782
|
-
const currentModelCall = providerModelCall;
|
|
783
|
-
let completedResponse;
|
|
784
|
-
try {
|
|
785
|
-
completedResponse = await observed(// {§observability-boundary}
|
|
786
|
-
"provider.generate", { model: provider.model, attempt: modelCallSequence }, async (span) => {
|
|
787
|
-
try {
|
|
788
|
-
const generated = await provider.generate({
|
|
789
|
-
messages: modelMessages,
|
|
790
|
-
workerId: providerWorkerId,
|
|
791
|
-
primaryWorkerId,
|
|
792
|
-
signal: providerSignal,
|
|
793
|
-
grammar: railGrammar,
|
|
794
|
-
strikes: strikeStreak,
|
|
795
|
-
attributions: providerAttemptAttributions.length > 0
|
|
796
|
-
? providerAttemptAttributions
|
|
797
|
-
: undefined,
|
|
798
|
-
client: client ?? undefined,
|
|
799
|
-
workspaceId: String(workspaceId),
|
|
800
|
-
loop: loopSeq,
|
|
801
|
-
turn: seq,
|
|
802
|
-
observeRequest: currentModelCall.observeRequest,
|
|
803
|
-
callKind: "emission",
|
|
804
|
-
}); // {§provider-surface-generate} {§provider-guarantees-signal-wired} {§provider-guarantees-serial-attempts} {§attribution} {§client-metadata}
|
|
805
|
-
currentModelCall.assertAccounting(generated.accounting);
|
|
806
|
-
providerCallInFlight = false;
|
|
807
|
-
recordCounter(PROVIDER_CALLS, {
|
|
808
|
-
model: provider.model,
|
|
809
|
-
attempt: modelCallSequence,
|
|
810
|
-
status: "resolved",
|
|
811
|
-
});
|
|
812
|
-
span.setAttribute("status", "resolved");
|
|
813
|
-
return generated;
|
|
814
|
-
}
|
|
815
|
-
catch (error) {
|
|
816
|
-
if (error instanceof ProviderError) {
|
|
817
|
-
currentModelCall.assertAccounting(error.accounting);
|
|
818
|
-
}
|
|
819
|
-
throw error;
|
|
820
|
-
}
|
|
821
|
-
});
|
|
822
|
-
}
|
|
823
|
-
catch (error) {
|
|
824
|
-
if (!(error instanceof ProviderError)
|
|
825
|
-
|| error.kind !== "capacity_exceeded"
|
|
826
|
-
|| providerSignal?.aborted === true
|
|
827
|
-
|| !await recoverCapacityPacket()) {
|
|
828
|
-
throw error;
|
|
829
|
-
}
|
|
830
|
-
const failure = _a.#providerFailure(error, providerSignal);
|
|
831
|
-
await currentModelCall.fail(failure, error.capacity ?? null);
|
|
832
|
-
providerCallInFlight = false;
|
|
833
|
-
await this.#problems.record({
|
|
834
|
-
workerId,
|
|
835
|
-
loopId,
|
|
836
|
-
turnId,
|
|
837
|
-
sequence: nextActionIndex++,
|
|
838
|
-
origin: "plurnk",
|
|
839
|
-
source: "provider",
|
|
840
|
-
result: failure,
|
|
841
|
-
});
|
|
842
|
-
// Include the durable recovery signal in the replacement
|
|
843
|
-
// request while preserving the selected recovery posture.
|
|
844
|
-
requestPacket = await buildPacket();
|
|
845
|
-
modelMessages = PacketWire.packetToWireMessages(requestPacket);
|
|
846
|
-
continue;
|
|
847
|
-
}
|
|
848
|
-
response = completedResponse;
|
|
849
|
-
await currentModelCall.observeResponse(completedResponse);
|
|
850
|
-
railEvidence = railGrammar === undefined
|
|
851
|
-
? undefined
|
|
852
|
-
: _a.#requireGrammarEvidence(completedResponse);
|
|
853
|
-
splitResponse = this.#splitResponse(completedResponse);
|
|
854
|
-
await classifyProviderAttempt(attemptRow.id, splitResponse, attempt, splitResponse.emissionValid);
|
|
855
|
-
if (splitResponse.emissionValid)
|
|
856
|
-
break;
|
|
857
|
-
attempt++;
|
|
858
|
-
}
|
|
859
|
-
if (!signal?.aborted)
|
|
860
|
-
this.#notices.push(workspaceId, loopId, { source: "engine:turn", kind: "turn_generated", level: "info", message: "parsing model response" });
|
|
861
|
-
}
|
|
862
|
-
catch (err) {
|
|
863
|
-
// This handler owns only provider-call failures. Parser, cost, SQL,
|
|
864
|
-
// and engine-contract failures retain their original source.
|
|
865
|
-
if (err instanceof ModelCallPersistenceError || err instanceof ProviderAccountingIntegrityError)
|
|
866
|
-
throw err;
|
|
867
|
-
if (!providerCallInFlight)
|
|
868
|
-
throw err;
|
|
869
|
-
providerCallInFlight = false;
|
|
870
|
-
if (providerAttemptId === null || providerModelCall === null) {
|
|
871
|
-
throw new Error("provider call failed without durable model-call and attempt identities", { cause: err });
|
|
872
|
-
}
|
|
873
|
-
const failure = _a.#providerFailure(err, providerSignal);
|
|
874
|
-
const capacityFailure = err instanceof ProviderError && err.kind === "capacity_exceeded";
|
|
875
|
-
// {§provider-interrupted-attempt} — a provider-declared interruption
|
|
876
|
-
// carries response evidence without becoming a completed exchange.
|
|
877
|
-
// Persist it as an unaccepted attempt before settling the failure.
|
|
878
|
-
if (err instanceof ProviderError && err.attempt !== undefined) {
|
|
879
|
-
response = err.attempt;
|
|
880
|
-
await providerModelCall.observeResponse(response, failure);
|
|
881
|
-
splitResponse = this.#splitResponse(response);
|
|
882
|
-
await classifyProviderAttempt(providerAttemptId, splitResponse, currentEmissionAttempt, false);
|
|
883
|
-
}
|
|
884
|
-
else {
|
|
885
|
-
await providerModelCall.fail(failure, err instanceof ProviderError ? err.capacity ?? null : null);
|
|
886
|
-
if (!capacityFailure)
|
|
887
|
-
emissionAttempts = currentEmissionAttempt;
|
|
888
|
-
}
|
|
889
|
-
// {§turn-never-blank} — a ProviderError means no completed exchange exists.
|
|
890
|
-
// Persist its exact RFC 9457 result before propagating it. Grammar evidence
|
|
891
|
-
// and its engine-owned verdict exist only on completed responses
|
|
892
|
-
// ({§rail-truth-engine-verdict}). Cancellation is lifecycle truth, not a
|
|
893
|
-
// provider failure. Close the
|
|
894
|
-
// attempted turn without inventing an assistant response, then let
|
|
895
|
-
// runLoop/Daemon settle the exact 504/499 loop result.
|
|
896
|
-
if (providerSignal?.aborted) {
|
|
897
|
-
await this.#db.engine_close_turn.run({
|
|
898
|
-
id: turnId,
|
|
899
|
-
status: providerSignal.reason === LOOP_TIMEOUT_REASON ? 504 : 499,
|
|
900
|
-
packet: StoredPacket.stringify(requestPacket),
|
|
901
|
-
usage_curation_budget: this.#packets.curationBudgetFor(provider),
|
|
902
|
-
finish_reason: splitResponse?.callMetadata.finishReason ?? null,
|
|
903
|
-
model: splitResponse?.callMetadata.model ?? provider.model,
|
|
904
|
-
meta: JSON.stringify(response?.meta ?? {}),
|
|
905
|
-
});
|
|
906
|
-
throw err;
|
|
907
|
-
}
|
|
908
|
-
const recorded = await this.#problems.record({
|
|
909
|
-
workerId,
|
|
910
|
-
loopId,
|
|
911
|
-
turnId,
|
|
912
|
-
sequence: nextActionIndex,
|
|
913
|
-
origin: "plurnk",
|
|
914
|
-
source: "provider",
|
|
915
|
-
result: failure,
|
|
916
|
-
});
|
|
917
|
-
// The provider call was attempted, but no completed exchange exists.
|
|
918
|
-
// Persist the exact request half and failure status; omitting assistant
|
|
919
|
-
// is materially different from fabricating an empty model turn.
|
|
920
|
-
await this.#db.engine_close_turn.run({
|
|
921
|
-
id: turnId,
|
|
922
|
-
status: recorded.result.status,
|
|
923
|
-
packet: StoredPacket.stringify(requestPacket),
|
|
924
|
-
usage_curation_budget: this.#packets.curationBudgetFor(provider),
|
|
925
|
-
finish_reason: splitResponse?.callMetadata.finishReason ?? null,
|
|
926
|
-
model: splitResponse?.callMetadata.model ?? provider.model,
|
|
927
|
-
meta: JSON.stringify(response?.meta ?? {}),
|
|
928
|
-
});
|
|
929
|
-
if (capacityFailure) {
|
|
930
|
-
return {
|
|
931
|
-
turnId,
|
|
932
|
-
status: recorded.result.status,
|
|
933
|
-
outcomes: [],
|
|
934
|
-
fingerprint: "",
|
|
935
|
-
capacityHardStop: true,
|
|
936
|
-
capacityFailure: recorded.result,
|
|
937
|
-
steerStruck: false,
|
|
938
|
-
emissionAttempts,
|
|
939
|
-
emissionExhausted: false,
|
|
940
|
-
};
|
|
941
|
-
}
|
|
942
|
-
throw new OperationFailureError(recorded.result, { cause: err });
|
|
943
|
-
}
|
|
944
|
-
if (response === undefined || splitResponse === undefined || providerModelCall === null) {
|
|
945
|
-
throw new Error("provider attempt loop completed without a response");
|
|
946
|
-
}
|
|
947
|
-
const emissionModelCallId = providerModelCall.id;
|
|
948
|
-
if (!splitResponse.emissionValid) {
|
|
949
|
-
// {§invalid-emission-attempts} The first consecutive exhaustion
|
|
950
|
-
// publishes only the raw final response and generic recovery fact.
|
|
951
|
-
let rejectedModelEntryId;
|
|
952
|
-
if (allowInvalidEmissionRecovery) {
|
|
953
|
-
rejectedModelEntryId = await this.#dispatcher.writeModelEntry({
|
|
954
|
-
verbatim: splitResponse.packetAssistant.content,
|
|
955
|
-
workerId,
|
|
956
|
-
loopId,
|
|
957
|
-
turnId,
|
|
958
|
-
sequence: nextActionIndex,
|
|
959
|
-
folded: true,
|
|
960
|
-
modelCallId: emissionModelCallId,
|
|
961
|
-
admission: "rejected",
|
|
962
|
-
});
|
|
963
|
-
this.#notices.push(workspaceId, loopId, {
|
|
964
|
-
source: "engine:grammar",
|
|
965
|
-
kind: "invalid_emission",
|
|
966
|
-
level: "error",
|
|
967
|
-
message: INVALID_EMISSION_RECOVERY_MESSAGE,
|
|
968
|
-
});
|
|
969
|
-
}
|
|
970
|
-
await this.#db.engine_close_turn.run({
|
|
971
|
-
id: turnId,
|
|
972
|
-
status: allowInvalidEmissionRecovery ? TURN_STATUS_IMPLICIT_CONTINUE : 500,
|
|
973
|
-
packet: StoredPacket.stringify(requestPacket),
|
|
974
|
-
usage_curation_budget: this.#packets.curationBudgetFor(provider),
|
|
975
|
-
finish_reason: splitResponse.callMetadata.finishReason,
|
|
976
|
-
model: splitResponse.callMetadata.model,
|
|
977
|
-
meta: JSON.stringify(response.meta ?? {}),
|
|
978
|
-
});
|
|
979
|
-
return {
|
|
980
|
-
turnId,
|
|
981
|
-
status: allowInvalidEmissionRecovery ? TURN_STATUS_IMPLICIT_CONTINUE : 500,
|
|
982
|
-
outcomes: [],
|
|
983
|
-
fingerprint: "",
|
|
984
|
-
capacityHardStop: false,
|
|
985
|
-
steerStruck: false,
|
|
986
|
-
emissionAttempts,
|
|
987
|
-
emissionExhausted: true,
|
|
988
|
-
...(rejectedModelEntryId === undefined ? {} : { rejectedModelEntryId }),
|
|
989
|
-
};
|
|
990
|
-
}
|
|
991
|
-
// {§packet-stored-shape} — admitted emission data extends the packet;
|
|
992
|
-
// provider-call metadata remains on the Turn row.
|
|
993
|
-
const { packetAssistant, callMetadata, parseNotices, recoverableParseErrors, } = splitResponse; // raw assistant content is opaque — split, never interpreted — {§provider-guarantees-assistantraw-opaque}
|
|
994
|
-
for (const notice of parseNotices) {
|
|
995
|
-
this.#notices.push(workspaceId, loopId, notice);
|
|
996
|
-
}
|
|
997
|
-
// Non-fatal provider transport notices on an accepted turn. Forward each
|
|
998
|
-
// Notice with a content-offset `line:col`;
|
|
999
|
-
// the model resolves it against its own emission — READ the folded model-emission row at the
|
|
1000
|
-
// cited lines ({§model-entry}) — not an embedded snippet that would duplicate the emission.
|
|
1001
|
-
for (const notice of response.notices ?? []) {
|
|
1002
|
-
const located = typeof notice.position === "number"
|
|
1003
|
-
? this.#offsetToLineColumn(packetAssistant.content, notice.position)
|
|
1004
|
-
: null;
|
|
1005
|
-
this.#notices.push(workspaceId, loopId, {
|
|
1006
|
-
source: notice.source,
|
|
1007
|
-
kind: notice.kind,
|
|
1008
|
-
message: notice.message ?? "",
|
|
1009
|
-
level: notice.level,
|
|
1010
|
-
...(located !== null
|
|
1011
|
-
? { position: { type: "content-offset", line: located.line, column: located.column } }
|
|
1012
|
-
: {}),
|
|
1013
|
-
});
|
|
1014
|
-
}
|
|
1015
|
-
// Grade configured local evidence independently. Endpoint-owned
|
|
1016
|
-
// constraints remain provider observations. {§rail-truth-engine-verdict}
|
|
1017
|
-
let railKeys;
|
|
1018
|
-
if (railGrammar !== undefined) {
|
|
1019
|
-
if (railEvidence === undefined)
|
|
1020
|
-
throw new Error("configured GBNF response has no final grammar evidence");
|
|
1021
|
-
if (railResponseGrammar === undefined)
|
|
1022
|
-
throw new Error("configured GBNF has no response grammar");
|
|
1023
|
-
let verdict = null;
|
|
1024
|
-
try {
|
|
1025
|
-
verdict = validateGbnf(railResponseGrammar, railEvidence.input);
|
|
1026
|
-
}
|
|
1027
|
-
catch (cause) {
|
|
1028
|
-
_a.#warnRailVerdictGapOnce(cause.message);
|
|
1029
|
-
}
|
|
1030
|
-
railKeys = {
|
|
1031
|
-
railsAttached: railEvidence.transported ? "client" : "withheld",
|
|
1032
|
-
railsVerdict: verdict?.status ?? "unverifiable",
|
|
1033
|
-
};
|
|
1034
|
-
if (verdict !== null && verdict.status !== "accept") {
|
|
1035
|
-
const contentPosition = verdict.pos >= railEvidence.contentStart
|
|
1036
|
-
? verdict.pos - railEvidence.contentStart
|
|
1037
|
-
: null;
|
|
1038
|
-
const located = contentPosition === null
|
|
1039
|
-
? null
|
|
1040
|
-
: this.#offsetToLineColumn(packetAssistant.content, contentPosition);
|
|
1041
|
-
this.#notices.push(workspaceId, loopId, {
|
|
1042
|
-
source: "engine:rails",
|
|
1043
|
-
kind: "grammar_unenforced",
|
|
1044
|
-
message: verdict.status === "reject"
|
|
1045
|
-
? `emission rejects the grammar at raw code point ${verdict.pos}`
|
|
1046
|
-
: `emission is an incomplete grammar sentence (ends at raw code point ${verdict.pos})`,
|
|
1047
|
-
level: "warn",
|
|
1048
|
-
...(located === null
|
|
1049
|
-
? {}
|
|
1050
|
-
: { position: { type: "content-offset", line: located.line, column: located.column } }),
|
|
1051
|
-
});
|
|
1052
|
-
}
|
|
1053
|
-
}
|
|
1054
|
-
const opsCount = packetAssistant.ops.length;
|
|
1055
|
-
const finalOp = packetAssistant.ops.at(-1);
|
|
389
|
+
// {§turn-ops-admission-path} — source acquisition ends before this seam.
|
|
390
|
+
// Every admitted producer program is scheduled, dispatched, recorded, and
|
|
391
|
+
// completed here; inference is only the model-specific way one program is
|
|
392
|
+
// acquired and supplied with BARE capability.
|
|
393
|
+
async #executeAdmittedTurn({ statements, source, sourceFolded, sourceModelCallId = null, sourceReasoningItems, origin, workspaceId, workerId, loopId, turnId, fromSequence, maxCommands = Number.POSITIVE_INFINITY, enforceIdle = false, failOnOperationError = false, recoverableParseErrors = [], bare, signal, onDispatch, }) {
|
|
394
|
+
const plan = statements[0];
|
|
395
|
+
const finalOp = statements.at(-1);
|
|
1056
396
|
if (finalOp?.op !== "SEND") {
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
throw new Error("an admitted
|
|
397
|
+
throw new Error("an admitted operation batch must end in a disposition SEND");
|
|
398
|
+
}
|
|
399
|
+
if (source !== null && plan?.op !== "PLAN") {
|
|
400
|
+
throw new Error("an admitted source-backed turnOps program must begin with PLAN");
|
|
1061
401
|
}
|
|
1062
402
|
const dispositionSignal = finalOp.signal;
|
|
1063
403
|
if (typeof dispositionSignal !== "number" || !TERMINAL_SEND_SIGNALS.has(dispositionSignal)) {
|
|
1064
|
-
throw new Error("an admitted
|
|
404
|
+
throw new Error("an admitted turnOps program must end in a terminal disposition SEND");
|
|
1065
405
|
}
|
|
1066
406
|
const sendOp = finalOp;
|
|
1067
|
-
|
|
1068
|
-
// state, never trusting the model's code. They strike via turn.steerStruck
|
|
1069
|
-
// ({§engine-rails}): the loop continues, the model sees the steering hint not the strike
|
|
1070
|
-
// count, and a non-resolver spins out to the engine's 500.
|
|
407
|
+
let turnStatus = dispositionSignal;
|
|
1071
408
|
let steerStruck = false;
|
|
1072
|
-
// Engine errors raised this turn, minted as op='error' log rows after dispatch (they share the
|
|
1073
|
-
// post-dispatch sequence counter). {§operation-result-uniform-error-channel}
|
|
1074
409
|
const pendingEngineErrors = [];
|
|
1075
|
-
|
|
1076
|
-
// pending set): the terminal SEND is judged AT ITS OWN DISPATCH — after the emission's
|
|
1077
|
-
// earlier ops executed — so a same-turn KILL+[200] repairs in one turn and a same-turn
|
|
1078
|
-
// WORK+[200] is caught. A refused final disposition (409) strikes via
|
|
1079
|
-
// the dispatch-loop check below.
|
|
1080
|
-
let turnStatus = dispositionSignal;
|
|
1081
|
-
// Idle turn: an implicit-continue (102) that did no WORK — its ops are only PLAN/SEND, no mid op.
|
|
1082
|
-
// The model continued with nothing to do.
|
|
1083
|
-
const midOpsCount = packetAssistant.ops.filter((op) => op.op !== "PLAN" && op.op !== "SEND").length
|
|
410
|
+
const middleCount = statements.filter((statement) => statement.op !== "PLAN" && statement.op !== "SEND").length
|
|
1084
411
|
+ recoverableParseErrors.length;
|
|
1085
|
-
if (
|
|
412
|
+
if (enforceIdle && turnStatus === TURN_STATUS_IMPLICIT_CONTINUE && middleCount === 0) {
|
|
1086
413
|
steerStruck = true;
|
|
1087
414
|
pendingEngineErrors.push("idle_turn");
|
|
1088
415
|
}
|
|
1089
|
-
// Close the turn with the final packet, status, and usage stats.
|
|
1090
|
-
const packet = StoredPacket.admit(requestPacket, packetAssistant, response.assistantRaw);
|
|
1091
|
-
await this.#db.engine_close_turn.run({
|
|
1092
|
-
id: turnId,
|
|
1093
|
-
status: turnStatus,
|
|
1094
|
-
packet: StoredPacket.stringify(packet),
|
|
1095
|
-
usage_curation_budget: this.#packets.curationBudgetFor(provider), // {§tokenomics-client-gauge}
|
|
1096
|
-
finish_reason: callMetadata.finishReason,
|
|
1097
|
-
model: callMetadata.model,
|
|
1098
|
-
// Opaque provider metadata plus engine-authored rail keys.
|
|
1099
|
-
// {§meta-passthrough}, {§rail-truth-engine-verdict}
|
|
1100
|
-
meta: JSON.stringify({ ...(response.meta ?? {}), ...(railKeys ?? {}) }),
|
|
1101
|
-
});
|
|
1102
|
-
// Dispatch model ops starting at nextActionIndex (continues the
|
|
1103
|
-
// turn's running counter after any pre-model writes).
|
|
1104
|
-
//
|
|
1105
|
-
// Max-commands ceiling: OFF by default (unlimited) — every generated op dispatches.
|
|
1106
|
-
// A degenerate op-loop is a sampler failure guarded at generation, not by dropping
|
|
1107
|
-
// already-generated work post-hoc. The ceiling is an OPT-IN operator/client bound:
|
|
1108
|
-
// when set, overflow ops drop without per-op log entries (no forensics flood) and the
|
|
1109
|
-
// model gets one notices signal next packet.
|
|
1110
|
-
// {§operator-config-workspace-max-commands} — workspace maxCommands min()s the env ceiling.
|
|
1111
|
-
const maxCommands = Math.min(readMaxCommands(), (await WorkspaceSettings.read(this.#db, workspaceId)).maxCommands ?? Number.POSITIVE_INFINITY);
|
|
1112
|
-
// PLAN (intended goals) and the final disposition SEND are not actions —
|
|
1113
|
-
// they always dispatch and never count against the cap. maxCommands
|
|
1114
|
-
// bounds real actions only; maxCommands:0 still admits a plan and a disposition
|
|
1115
|
-
// (the PLAN/SEND ops, zero actions), which is its only coherent meaning.
|
|
1116
416
|
let realCommands = 0;
|
|
1117
|
-
const
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
});
|
|
1122
|
-
const
|
|
1123
|
-
|
|
1124
|
-
workspaceId, workerId, loopId, turnId,
|
|
1125
|
-
origin, onDispatch,
|
|
1126
|
-
});
|
|
1127
|
-
const droppedCount = opsCount - opsToDispatch.length;
|
|
1128
|
-
const bareStatements = opsToDispatch.filter((statement) => statement.op === "BARE");
|
|
417
|
+
const admitted = statements.filter((statement) => statement.op === "PLAN"
|
|
418
|
+
|| statement === sendOp
|
|
419
|
+
|| realCommands++ < maxCommands);
|
|
420
|
+
const scheduled = scheduleTurnOps(admitted.flatMap(expandSafeUriTargetGroup));
|
|
421
|
+
await this.#dispatcher.prepareEditBatches(scheduled.filter((statement) => statement.op === "EDIT"), { workspaceId, workerId, loopId, turnId, origin, onDispatch });
|
|
422
|
+
const droppedCount = statements.length - admitted.length;
|
|
423
|
+
const bareStatements = scheduled.filter((statement) => statement.op === "BARE");
|
|
1129
424
|
let bareResults = null;
|
|
1130
425
|
const outcomes = [];
|
|
1131
|
-
|
|
1132
|
-
// so the next op's sequence picks up after them. Collapses to nextActionIndex+i when
|
|
1133
|
-
// every op writes one row (the common case).
|
|
1134
|
-
let rowSeq = nextActionIndex;
|
|
426
|
+
let rowSequence = fromSequence;
|
|
1135
427
|
let parseErrorsRecorded = false;
|
|
1136
428
|
const recordRecoverableParseErrors = async () => {
|
|
1137
429
|
if (parseErrorsRecorded)
|
|
@@ -1142,8 +434,8 @@ class TurnRunner {
|
|
|
1142
434
|
workerId,
|
|
1143
435
|
loopId,
|
|
1144
436
|
turnId,
|
|
1145
|
-
sequence:
|
|
1146
|
-
origin
|
|
437
|
+
sequence: rowSequence++,
|
|
438
|
+
origin,
|
|
1147
439
|
source: "grammar",
|
|
1148
440
|
result: Results.failure("grammar:parser", "invalid-operation-syntax", 400, error.message, {}, {
|
|
1149
441
|
line: error.line,
|
|
@@ -1158,39 +450,35 @@ class TurnRunner {
|
|
|
1158
450
|
onDispatch?.(recorded.id);
|
|
1159
451
|
}
|
|
1160
452
|
};
|
|
1161
|
-
for (const statement of
|
|
1162
|
-
if (statement
|
|
1163
|
-
&& typeof statement.signal === "number"
|
|
1164
|
-
&& TERMINAL_SEND_SIGNALS.has(statement.signal)) {
|
|
453
|
+
for (const statement of scheduled) {
|
|
454
|
+
if (statement === sendOp) {
|
|
1165
455
|
await recordRecoverableParseErrors();
|
|
1166
|
-
// {§worker-optimistic-settlement} — SEND judges the refreshed
|
|
1167
|
-
// lifecycle after this turn's own fast streams receive one
|
|
1168
|
-
// bounded opportunity to conclude. This is not a sleep and
|
|
1169
|
-
// does not delay a later turn for older monitored streams.
|
|
1170
456
|
const execHandler = this.#schemes.get("exec");
|
|
1171
|
-
await execHandler?.settleTurnSpawns?.(workerId, turnId, readOptimisticSettlementMs(),
|
|
457
|
+
await execHandler?.settleTurnSpawns?.(workerId, turnId, readOptimisticSettlementMs(), signal);
|
|
1172
458
|
}
|
|
1173
|
-
const result = await observed(
|
|
1174
|
-
"op.dispatch", { op: statement.op }, async (span) => {
|
|
459
|
+
const result = await observed("op.dispatch", { op: statement.op }, async (span) => {
|
|
1175
460
|
let dispatchResult;
|
|
1176
461
|
if (statement.op === "BARE") {
|
|
462
|
+
if (bare === undefined) {
|
|
463
|
+
throw new Error(`${origin} turnOps cannot execute BARE without provider acquisition context`);
|
|
464
|
+
}
|
|
1177
465
|
if (bareResults === null) {
|
|
1178
466
|
const batch = await this.#runBareBatch({
|
|
1179
467
|
statements: bareStatements,
|
|
1180
|
-
provider:
|
|
468
|
+
provider: bare.provider,
|
|
1181
469
|
turnId,
|
|
1182
|
-
modelCallSequenceStart:
|
|
470
|
+
modelCallSequenceStart: bare.modelCallSequenceStart,
|
|
1183
471
|
workspaceId,
|
|
1184
472
|
workerId,
|
|
1185
|
-
primaryWorkerId,
|
|
1186
|
-
loopSequence:
|
|
1187
|
-
turnSequence:
|
|
1188
|
-
signal:
|
|
473
|
+
primaryWorkerId: bare.primaryWorkerId,
|
|
474
|
+
loopSequence: bare.loopSequence,
|
|
475
|
+
turnSequence: bare.turnSequence,
|
|
476
|
+
signal: bare.signal,
|
|
1189
477
|
});
|
|
1190
478
|
bareResults = new Map(batch.map((item) => [item.statement, item]));
|
|
1191
479
|
}
|
|
1192
|
-
const
|
|
1193
|
-
if (
|
|
480
|
+
const bareResult = bareResults.get(statement);
|
|
481
|
+
if (bareResult === undefined) {
|
|
1194
482
|
throw new Error("BARE statement reached dispatch without its batch result");
|
|
1195
483
|
}
|
|
1196
484
|
dispatchResult = await this.#dispatcher.recordBareResult({
|
|
@@ -1199,16 +487,21 @@ class TurnRunner {
|
|
|
1199
487
|
workerId,
|
|
1200
488
|
loopId,
|
|
1201
489
|
turnId,
|
|
1202
|
-
sequence:
|
|
490
|
+
sequence: rowSequence,
|
|
1203
491
|
origin,
|
|
1204
492
|
onDispatch,
|
|
1205
|
-
},
|
|
493
|
+
}, bareResult.result, bareResult.modelCallId);
|
|
1206
494
|
}
|
|
1207
495
|
else {
|
|
1208
496
|
dispatchResult = await this.#dispatcher.dispatch({
|
|
1209
|
-
statement,
|
|
1210
|
-
|
|
1211
|
-
|
|
497
|
+
statement,
|
|
498
|
+
workspaceId,
|
|
499
|
+
workerId,
|
|
500
|
+
loopId,
|
|
501
|
+
turnId,
|
|
502
|
+
sequence: rowSequence,
|
|
503
|
+
origin,
|
|
504
|
+
onDispatch,
|
|
1212
505
|
});
|
|
1213
506
|
}
|
|
1214
507
|
span.setAttribute("status", dispatchResult.status);
|
|
@@ -1216,6 +509,8 @@ class TurnRunner {
|
|
|
1216
509
|
return dispatchResult;
|
|
1217
510
|
});
|
|
1218
511
|
outcomes.push({ op: statement.op, status: result.status });
|
|
512
|
+
if (failOnOperationError && result.status >= 400)
|
|
513
|
+
throw new OperationFailureError(result);
|
|
1219
514
|
for (const normalization of result.scopeNormalizations ?? []) {
|
|
1220
515
|
this.#notices.push(workspaceId, loopId, {
|
|
1221
516
|
source: "engine:slicer",
|
|
@@ -1224,39 +519,20 @@ class TurnRunner {
|
|
|
1224
519
|
message: `Scope <${normalization.requested.join(",")}> was normalized to <${normalization.canonical.join(",")}>.`,
|
|
1225
520
|
});
|
|
1226
521
|
}
|
|
1227
|
-
// {§engine-rails} — a refused final disposition leaves both loop
|
|
1228
|
-
// and turn continuing, and its 409 steering ruling strikes once.
|
|
1229
522
|
if (statement === sendOp && result.status === 409) {
|
|
1230
523
|
steerStruck = true;
|
|
1231
524
|
turnStatus = TURN_STATUS_IMPLICIT_CONTINUE;
|
|
1232
|
-
await this.#db.engine_reconcile_turn_status.run({ id: turnId, status: turnStatus });
|
|
1233
|
-
}
|
|
1234
|
-
// {§send-300-choices}: a question resolves through the proposal system; whatever the
|
|
1235
|
-
// resolution (answer/reject/timeout), the LOOP continues to the turn where the model
|
|
1236
|
-
// reads it; the turn record is a continue, never a 300 terminal.
|
|
1237
|
-
if (statement === sendOp && sendOp.signal === 300 && result.status !== 409) {
|
|
1238
|
-
turnStatus = TURN_STATUS_IMPLICIT_CONTINUE;
|
|
1239
|
-
await this.#db.engine_reconcile_turn_status.run({ id: turnId, status: turnStatus });
|
|
1240
525
|
}
|
|
1241
|
-
// A broadcast [202] is a conditional wait, not an unconditional turn status:
|
|
1242
|
-
// live work parks at 202; completed-but-unobserved work continues at 102; an
|
|
1243
|
-
// empty join completes at 200. Persist and return the dispatcher's actual ruling.
|
|
1244
526
|
if (statement === sendOp
|
|
1245
527
|
&& result.status !== 409
|
|
1246
528
|
&& sendOp.target === null
|
|
1247
529
|
&& sendOp.signal === 202
|
|
1248
530
|
&& result.status !== 202) {
|
|
1249
531
|
turnStatus = result.status;
|
|
1250
|
-
await this.#db.engine_reconcile_turn_status.run({ id: turnId, status: turnStatus });
|
|
1251
532
|
}
|
|
1252
|
-
|
|
533
|
+
rowSequence += result.rowsWritten ?? 1;
|
|
1253
534
|
}
|
|
1254
535
|
await recordRecoverableParseErrors();
|
|
1255
|
-
// Engine rail failures mint as op='error' log rows at the turn's next
|
|
1256
|
-
// free sequence. Bounded syntax failures were recorded in their
|
|
1257
|
-
// authored turn before its terminal disposition.
|
|
1258
|
-
let errSeq = rowSeq;
|
|
1259
|
-
// max_commands_exceeded IS model-facing: dropped ops the model emitted that didn't run.
|
|
1260
536
|
if (droppedCount > 0)
|
|
1261
537
|
pendingEngineErrors.push("max_commands_exceeded");
|
|
1262
538
|
for (const kind of pendingEngineErrors) {
|
|
@@ -1278,47 +554,887 @@ class TurnRunner {
|
|
|
1278
554
|
workerId,
|
|
1279
555
|
loopId,
|
|
1280
556
|
turnId,
|
|
1281
|
-
sequence:
|
|
1282
|
-
origin: "
|
|
557
|
+
sequence: rowSequence++,
|
|
558
|
+
origin: "_plurnk",
|
|
1283
559
|
source: "rail",
|
|
1284
560
|
result: Results.failure("engine:rail", problem.code, problem.status, problem.detail, {}, extensions),
|
|
1285
561
|
});
|
|
1286
562
|
}
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
// packet shows the model exactly what it last produced. ALWAYS born FOLDED — the old
|
|
1291
|
-
// born-OPEN-on-error auto-trigger was conditional helpfulness that bred its own hazards
|
|
1292
|
-
// (a 24k-char ramble mirrored open re-injects itself into the next packet: cost,
|
|
1293
|
-
// contamination, pressure feedback).
|
|
1294
|
-
// {§encrypted-reasoning-carrier} — core relays every provider-normalized
|
|
1295
|
-
// item unchanged.
|
|
1296
|
-
const reasoningItems = response.assistant.reasoningEncrypted?.length
|
|
1297
|
-
? response.assistant.reasoningEncrypted
|
|
1298
|
-
: undefined;
|
|
1299
|
-
if (packetAssistant.content.trim().length > 0 || reasoningItems !== undefined) {
|
|
1300
|
-
await this.#dispatcher.writeModelEntry({
|
|
1301
|
-
verbatim: packetAssistant.content,
|
|
563
|
+
if (source !== null) {
|
|
564
|
+
await this.#dispatcher.writeTurnOps({
|
|
565
|
+
verbatim: source,
|
|
1302
566
|
workerId,
|
|
1303
567
|
loopId,
|
|
1304
568
|
turnId,
|
|
1305
|
-
sequence:
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
569
|
+
sequence: rowSequence,
|
|
570
|
+
origin,
|
|
571
|
+
folded: sourceFolded,
|
|
572
|
+
modelCallId: sourceModelCallId,
|
|
573
|
+
...(sourceReasoningItems !== undefined ? { reasoningItems: sourceReasoningItems } : {}),
|
|
1309
574
|
});
|
|
1310
575
|
}
|
|
576
|
+
await Turn.complete(this.#db, turnId, turnStatus);
|
|
1311
577
|
return {
|
|
1312
|
-
turnId,
|
|
1313
578
|
status: turnStatus,
|
|
1314
579
|
outcomes,
|
|
1315
|
-
fingerprint: StrikeRail.fingerprintTurn(
|
|
1316
|
-
capacityHardStop: false,
|
|
580
|
+
fingerprint: StrikeRail.fingerprintTurn(statements),
|
|
1317
581
|
steerStruck,
|
|
1318
|
-
emissionAttempts,
|
|
1319
|
-
emissionExhausted: false,
|
|
1320
582
|
};
|
|
1321
583
|
}
|
|
584
|
+
async runTurn({ provider, childProvider = provider, messages, requirements = "", workspaceId, workerId, loopId, signal, onDispatch, turnNumber = 1, maxTurns = 50, invalidEmissionRecoveryEntryId, }) {
|
|
585
|
+
const allowInvalidEmissionRecovery = invalidEmissionRecoveryEntryId === null;
|
|
586
|
+
const transientOpenLogEntryId = typeof invalidEmissionRecoveryEntryId === "number"
|
|
587
|
+
? invalidEmissionRecoveryEntryId
|
|
588
|
+
: null;
|
|
589
|
+
// === Producer-neutral turn container ===
|
|
590
|
+
//
|
|
591
|
+
// Every producer opens the same durable turn and completes it only
|
|
592
|
+
// after its ordered operations settle. Model packets and provider
|
|
593
|
+
// metadata are optional inference evidence, not turn identity.
|
|
594
|
+
const initialSequence = await this.#db.engine_next_turn_sequence.get({ loop_id: loopId });
|
|
595
|
+
if (initialSequence === undefined)
|
|
596
|
+
throw new Error("Engine.runTurn: next turn sequence is unavailable");
|
|
597
|
+
// loops.sequence is the loop's ordinal within the worker. Turn-0 foists that belong to the
|
|
598
|
+
// WORKER (manifest preview, AGENTS, operator docs) gate on the worker's first loop, not every loop's
|
|
599
|
+
// first turn ({§actor-boundary-catalog-preview}); per-loop foists such as
|
|
600
|
+
// {§prompt-entry} still fires once per loop. Durable publication state,
|
|
601
|
+
// rather than the model-turn ordinal, prevents an overflow diversion
|
|
602
|
+
// from replaying the prompt and its automatic path READs.
|
|
603
|
+
const loopRow = await this.#db.engine_get_loop_prompt.get({ loop_id: loopId });
|
|
604
|
+
const workerFirstLoop = (loopRow?.sequence ?? 0) === 1;
|
|
605
|
+
const createdTurnIds = [];
|
|
606
|
+
try {
|
|
607
|
+
// {§worker-initialization-entry} — turn zero is an ordinary `_plurnk`
|
|
608
|
+
// operation turn. Its model-facing zero is a phase label; durable turn
|
|
609
|
+
// coordinates remain one-based.
|
|
610
|
+
const initializationTurn = initialSequence.next === 1 && workerFirstLoop
|
|
611
|
+
? await Turn.open(this.#db, { loopId, producer: "_plurnk", kind: "initialization" })
|
|
612
|
+
: null;
|
|
613
|
+
if (initializationTurn !== null)
|
|
614
|
+
createdTurnIds.push(initializationTurn.id);
|
|
615
|
+
let modelTurn = initializationTurn === null
|
|
616
|
+
? await Turn.open(this.#db, { loopId, producer: "model", kind: "inference" })
|
|
617
|
+
: null;
|
|
618
|
+
if (modelTurn !== null)
|
|
619
|
+
createdTurnIds.push(modelTurn.id);
|
|
620
|
+
// {§env-delta-log-pull} — establish a fresh worker's observation
|
|
621
|
+
// baseline immediately after its first turn opens. A fork already has
|
|
622
|
+
// its parent's cursor, so the NULL-guarded statement leaves it intact.
|
|
623
|
+
// Events committed after this statement belong to this or a later
|
|
624
|
+
// closed pull window; pre-existing state is read through the ordinary
|
|
625
|
+
// shared-world projections in this first packet.
|
|
626
|
+
await this.#db.engine_initialize_ambient_cursor.get({ workspace_id: workspaceId, worker_id: workerId });
|
|
627
|
+
// Threaded per turn, never engine state, so concurrent loops on
|
|
628
|
+
// different providers each read their own honest tokenizer values.
|
|
629
|
+
const systemContext = (contextTurnId) => ({
|
|
630
|
+
db: this.#db, workspaceId, workerId, loopId, turnId: contextTurnId,
|
|
631
|
+
writer: "_plurnk",
|
|
632
|
+
signal: this.#loopSignal(loopId),
|
|
633
|
+
streamEventNotify: this.#streamEventNotify,
|
|
634
|
+
wakeWorkerNotify: this.#wakeWorkerNotify,
|
|
635
|
+
weigh: this.#weighContent,
|
|
636
|
+
mimetypes: this.#mimetypes,
|
|
637
|
+
defaultChannelFor: (s) => this.#schemes.defaultChannelFor(s, workspaceId),
|
|
638
|
+
pushNotice: (notice) => this.#notices.push(workspaceId, loopId, notice),
|
|
639
|
+
requestInteraction: (request) => this.#interactions.request(request, { workspaceId, workerId, loopId, turnId: contextTurnId }, this.#loopSignal(loopId)),
|
|
640
|
+
});
|
|
641
|
+
let systemCtx = systemContext(initializationTurn?.id ?? modelTurn.id);
|
|
642
|
+
const initializationStatements = [];
|
|
643
|
+
// {§worker-initialization-entry} — the worker's first turn is the worked
|
|
644
|
+
// example itself: an ordinary PLAN, the actual orienting operations,
|
|
645
|
+
// and an ordinary terminal SEND.
|
|
646
|
+
if (initializationTurn !== null) {
|
|
647
|
+
const plan = {
|
|
648
|
+
op: "PLAN", delimiter: "", annotation: null,
|
|
649
|
+
signal: null, target: null, lineMarker: null,
|
|
650
|
+
body: "* Discover the tooling available and survey the workspace file root.",
|
|
651
|
+
position: UNKNOWN_POSITION,
|
|
652
|
+
};
|
|
653
|
+
initializationStatements.push(plan);
|
|
654
|
+
// {§turn0-agents-stunt} — the project AGENTS.md (materialized by LoopDocs as
|
|
655
|
+
// worker://plurnk/agents.md) gets one foisted READ on the worker's first
|
|
656
|
+
// loop, so local repo guidance is visible turn-0 content. Global policy
|
|
657
|
+
// stays in the system prompt; nothing else is force-read.
|
|
658
|
+
if (workerFirstLoop) {
|
|
659
|
+
const kernelId = await Owner.kernelId(this.#db, workspaceId);
|
|
660
|
+
const agentsEntry = await this.#db.crud_find_workspace_entry.get({
|
|
661
|
+
workspace_id: workspaceId,
|
|
662
|
+
owner_id: kernelId,
|
|
663
|
+
scheme: "worker",
|
|
664
|
+
pathname: "/agents.md",
|
|
665
|
+
});
|
|
666
|
+
if (agentsEntry !== undefined) {
|
|
667
|
+
const agentsTarget = {
|
|
668
|
+
kind: "url", raw: "worker://plurnk/agents.md", scheme: "worker",
|
|
669
|
+
username: null, password: null, hostname: "plurnk", port: null,
|
|
670
|
+
pathname: "/agents.md", query: null, fragment: null,
|
|
671
|
+
};
|
|
672
|
+
const agentsRead = {
|
|
673
|
+
op: "READ", delimiter: "", annotation: null, signal: ["+_plurnk", "+init", "+policy"], target: agentsTarget,
|
|
674
|
+
lineMarker: null, body: null, position: UNKNOWN_POSITION,
|
|
675
|
+
};
|
|
676
|
+
initializationStatements.push(agentsRead);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
// The persistent search-index pass (_search-index.maintain) attaches
|
|
681
|
+
// every readable entry/log projection to complete graph/FTS/vector derivations.
|
|
682
|
+
// NOT an action: no log entry, no sequence slot,
|
|
683
|
+
// not dispatched. There is no materialized manifest entry — the catalog
|
|
684
|
+
// is served on demand by FIND: recursive when asked, shallow-mapped below.
|
|
685
|
+
// {§semantic-embed-dedup} — one pass-wide semantic plan binds every
|
|
686
|
+
// chunk counter to the derivation identity it produces.
|
|
687
|
+
// SPEC {§membership} D4/D5 — git-ls-files workspace membership, resolved at
|
|
688
|
+
// prompt-composition (EMI is eager + exhaustive — git is the only bound). When the
|
|
689
|
+
// workspace's project_root is a git working tree, tracked files are
|
|
690
|
+
// members without a client `add`; active members are materialized
|
|
691
|
+
// (disk → body channel) so they appear in the catalog. No-ops
|
|
692
|
+
// on headless / non-git workspaces. Runs BEFORE the derivation pump so
|
|
693
|
+
// this turn's packet reflects them.
|
|
694
|
+
// Workspace creation starts this eagerly. Joining here is the correctness
|
|
695
|
+
// boundary: the model never runs against partial graph/vector coverage.
|
|
696
|
+
await this.#warmWorkspace(systemCtx, false);
|
|
697
|
+
// The warm materialized membership before deriving. This second pass is the
|
|
698
|
+
// ordinary cheap change detector and captures any drift that landed meanwhile.
|
|
699
|
+
const fsDivergences = await GitMembership.indexGitMembership(systemCtx);
|
|
700
|
+
// {§packet-git-status} — one post-reconciliation snapshot supplies both
|
|
701
|
+
// the compact packet summary and each causal file event's exact XY state.
|
|
702
|
+
const gitStatus = await GitState.status(this.#db, workspaceId, this.#loopSignal(loopId));
|
|
703
|
+
await this.#logFsFictions(workspaceId, fsDivergences, gitStatus);
|
|
704
|
+
// The refresh above may have changed bodies (including model/client edits
|
|
705
|
+
// since the startup warm). Re-derive to completion before packet/model
|
|
706
|
+
// construction. Membership is already current, so this pass does not
|
|
707
|
+
// consume the filesystem divergences a second time.
|
|
708
|
+
await this.#warmWorkspace(systemCtx, true, false);
|
|
709
|
+
// Turn-0 catalog preview (PLURNK_SERVICE_FILES_ITEMS, {§actor-boundary-catalog-preview}):
|
|
710
|
+
// Six bodyless FIND surveys in the worker's packetless initialization turn establish the skills tree
|
|
711
|
+
// (authored and Plurnk-generated families), attached tools tree, project, commons, and private
|
|
712
|
+
// surfaces in that order.
|
|
713
|
+
// Their `init` classification lets the model curate this opening survey as one log set.
|
|
714
|
+
if (initializationTurn !== null) {
|
|
715
|
+
// {§operator-config-workspace-files-items} — workspace filesItems replaces the env default.
|
|
716
|
+
const { filesItems: workspaceMI } = await WorkspaceSettings.read(this.#db, workspaceId);
|
|
717
|
+
const filesItems = workspaceMI !== null ? normalizeFilesItems(workspaceMI) : readFilesItems();
|
|
718
|
+
if (filesItems !== null && workerFirstLoop) { // {§actor-boundary-catalog-preview} — once per worker
|
|
719
|
+
const catalogSchemes = await this.#db.engine_scheme_catalog_summary.all({ workspace_id: workspaceId });
|
|
720
|
+
const fileItems = catalogSchemes.find(({ scheme }) => scheme === "file")?.shallow_items ?? 0;
|
|
721
|
+
const fileCap = filesItems > 0 && fileItems > 0 ? Math.min(filesItems, fileItems) : null;
|
|
722
|
+
// {§tools-resource-materialization} — PLURNK_MCP_EXPANDED servers
|
|
723
|
+
// contribute one complete tool-tree survey each, directly after the
|
|
724
|
+
// family survey, so turn 0 names every tool they expose in order.
|
|
725
|
+
const registry = this.#executors();
|
|
726
|
+
const toolExpansions = [];
|
|
727
|
+
for (const tag of registry?.availableRuntimes(workspaceId) ?? []) {
|
|
728
|
+
const entry = registry?.entry(tag, workspaceId);
|
|
729
|
+
if (entry?.resourcesPath !== "/tools" || entry.expandTools !== true)
|
|
730
|
+
continue;
|
|
731
|
+
toolExpansions.push({
|
|
732
|
+
statement: {
|
|
733
|
+
op: "FIND", delimiter: "", annotation: `enabled tools: ${tag}`, signal: ["+_plurnk", "+init", "+tools"],
|
|
734
|
+
target: { kind: "url", raw: `worker://plurnk/tools/${tag}/**`, scheme: "worker", username: null, password: null, hostname: "plurnk", port: null, pathname: `/tools/${tag}/**`, query: null, fragment: null },
|
|
735
|
+
body: null, lineMarker: { marks: [1, -1] }, position: UNKNOWN_POSITION,
|
|
736
|
+
},
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
const surveys = [
|
|
740
|
+
{
|
|
741
|
+
statement: {
|
|
742
|
+
op: "FIND", delimiter: "", annotation: null, signal: ["+_plurnk", "+init", "+skills"],
|
|
743
|
+
target: { kind: "url", raw: "worker://plurnk/skills/*.md", scheme: "worker", username: null, password: null, hostname: "plurnk", port: null, pathname: "/skills/*.md", query: null, fragment: null },
|
|
744
|
+
body: null, lineMarker: { marks: [1, -1] }, position: UNKNOWN_POSITION,
|
|
745
|
+
},
|
|
746
|
+
},
|
|
747
|
+
{
|
|
748
|
+
statement: {
|
|
749
|
+
op: "FIND", delimiter: "", annotation: null, signal: ["+_plurnk", "+init", "+skills"],
|
|
750
|
+
target: { kind: "url", raw: "worker://plurnk/skills/plurnk/*.md", scheme: "worker", username: null, password: null, hostname: "plurnk", port: null, pathname: "/skills/plurnk/*.md", query: null, fragment: null },
|
|
751
|
+
body: null, lineMarker: { marks: [1, -1] }, position: UNKNOWN_POSITION,
|
|
752
|
+
},
|
|
753
|
+
},
|
|
754
|
+
{
|
|
755
|
+
// {§tools-resource-materialization} — enabled tool families
|
|
756
|
+
// (MCP servers) survey at family level; each row's summary is
|
|
757
|
+
// the server one-liner or its flagship invocation form, so the
|
|
758
|
+
// discovery row itself orients. Servers named in
|
|
759
|
+
// PLURNK_MCP_EXPANDED add a second survey of their complete
|
|
760
|
+
// tool tree.
|
|
761
|
+
statement: {
|
|
762
|
+
op: "FIND", delimiter: "", annotation: "enabled tools", signal: ["+_plurnk", "+init", "+tools"],
|
|
763
|
+
target: { kind: "url", raw: "worker://plurnk/tools/*.md", scheme: "worker", username: null, password: null, hostname: "plurnk", port: null, pathname: "/tools/*.md", query: null, fragment: null },
|
|
764
|
+
body: null, lineMarker: { marks: [1, -1] }, position: UNKNOWN_POSITION,
|
|
765
|
+
},
|
|
766
|
+
},
|
|
767
|
+
...toolExpansions,
|
|
768
|
+
{
|
|
769
|
+
statement: {
|
|
770
|
+
op: "FIND", delimiter: "", annotation: "workspace files", signal: ["+_plurnk", "+init"],
|
|
771
|
+
target: { kind: "local", raw: "*" },
|
|
772
|
+
body: null,
|
|
773
|
+
lineMarker: fileCap === null ? null : { marks: [1, fileCap] },
|
|
774
|
+
position: UNKNOWN_POSITION,
|
|
775
|
+
},
|
|
776
|
+
},
|
|
777
|
+
{
|
|
778
|
+
statement: {
|
|
779
|
+
op: "FIND", delimiter: "", annotation: "workspace entries", signal: ["+_plurnk", "+init"],
|
|
780
|
+
target: { kind: "url", raw: "worker:///*", scheme: "worker", username: null, password: null, hostname: null, port: null, pathname: "/*", query: null, fragment: null },
|
|
781
|
+
body: null, lineMarker: null, position: UNKNOWN_POSITION,
|
|
782
|
+
},
|
|
783
|
+
},
|
|
784
|
+
{
|
|
785
|
+
statement: {
|
|
786
|
+
op: "FIND", delimiter: "", annotation: "worker entries", signal: ["+_plurnk", "+init"],
|
|
787
|
+
target: { kind: "url", raw: "worker://~/*", scheme: "worker", username: null, password: null, hostname: "~", port: null, pathname: "/*", query: null, fragment: null },
|
|
788
|
+
body: null, lineMarker: null, position: UNKNOWN_POSITION,
|
|
789
|
+
},
|
|
790
|
+
},
|
|
791
|
+
];
|
|
792
|
+
initializationStatements.push(...surveys.map(({ statement }) => statement));
|
|
793
|
+
}
|
|
794
|
+
const send = {
|
|
795
|
+
op: "SEND", delimiter: "", annotation: null,
|
|
796
|
+
signal: 102, target: null, lineMarker: null,
|
|
797
|
+
body: { raw: "Next: Address the prompt.", json: null },
|
|
798
|
+
position: UNKNOWN_POSITION,
|
|
799
|
+
};
|
|
800
|
+
initializationStatements.push(send);
|
|
801
|
+
const source = TurnOps.renderInternal(initializationStatements);
|
|
802
|
+
const admitted = TurnOps.parseInternal(source);
|
|
803
|
+
const result = await this.#executeAdmittedTurn({
|
|
804
|
+
statements: admitted,
|
|
805
|
+
source,
|
|
806
|
+
sourceFolded: false,
|
|
807
|
+
origin: "_plurnk",
|
|
808
|
+
workspaceId,
|
|
809
|
+
workerId,
|
|
810
|
+
loopId,
|
|
811
|
+
turnId: initializationTurn.id,
|
|
812
|
+
fromSequence: 1,
|
|
813
|
+
failOnOperationError: true,
|
|
814
|
+
signal: this.#loopSignal(loopId),
|
|
815
|
+
onDispatch,
|
|
816
|
+
});
|
|
817
|
+
if (result.status !== TURN_STATUS_IMPLICIT_CONTINUE) {
|
|
818
|
+
throw new Error(`initialization SEND returned ${result.status}; expected ${TURN_STATUS_IMPLICIT_CONTINUE}`);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
// Initialization is a complete preceding turn, not a set of rows
|
|
822
|
+
// interleaved with the model boundary. The engine may now acquire the
|
|
823
|
+
// inference turn inside the same warmed workspace cycle.
|
|
824
|
+
if (modelTurn === null) {
|
|
825
|
+
modelTurn = await Turn.open(this.#db, { loopId, producer: "model", kind: "inference" });
|
|
826
|
+
createdTurnIds.push(modelTurn.id);
|
|
827
|
+
}
|
|
828
|
+
const seq = modelTurn.sequence;
|
|
829
|
+
const turnId = modelTurn.id;
|
|
830
|
+
systemCtx = systemContext(turnId);
|
|
831
|
+
// Pre-model writes. Each prompt the model has not seen yet becomes a
|
|
832
|
+
// `prompt` operation row whose target is its durable prompt:// entry.
|
|
833
|
+
// Model operations continue the same turn sequence after these rows.
|
|
834
|
+
let nextActionIndex = 1;
|
|
835
|
+
const turnOpenPaths = [];
|
|
836
|
+
if (turnNumber === 1 && loopRow?.prompt_published === 0) {
|
|
837
|
+
const promptRow = loopRow; // {§prompt-entry} — one durable publication per loop
|
|
838
|
+
if (promptRow !== undefined && typeof promptRow.prompt === "string" && promptRow.prompt.length > 0) {
|
|
839
|
+
const openPaths = assertOpenPaths(JSON.parse(promptRow.open_paths), `Loop ${loopId} open_paths`);
|
|
840
|
+
const promptLoopSeq = promptRow.sequence;
|
|
841
|
+
const promptPath = promptTarget(promptLoopSeq, 1);
|
|
842
|
+
const entry = {
|
|
843
|
+
channels: { body: { content: promptRow.prompt, mimetype: "text/markdown" } },
|
|
844
|
+
attributes: { openPaths },
|
|
845
|
+
};
|
|
846
|
+
await EntryCrud.writeEntry(promptPath.pathname, entry, systemCtx, "prompt", workerId);
|
|
847
|
+
turnOpenPaths.push(...openPaths);
|
|
848
|
+
const promptLogId = await this.#writePromptLog({
|
|
849
|
+
workerId,
|
|
850
|
+
loopId,
|
|
851
|
+
turnId,
|
|
852
|
+
sequence: nextActionIndex++,
|
|
853
|
+
target: promptPath,
|
|
854
|
+
content: promptRow.prompt,
|
|
855
|
+
});
|
|
856
|
+
onDispatch?.(promptLogId);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
// {§prompt-loop-containment}: the loop contains every prompt that arrived
|
|
860
|
+
// while it ran. Publish each undelivered frame oldest-first exactly once.
|
|
861
|
+
{
|
|
862
|
+
const loopSeqRow = await this.#db.engine_loop_sequence.get({ loop_id: loopId });
|
|
863
|
+
const loopSeq = loopSeqRow?.sequence ?? loopId;
|
|
864
|
+
const prefix = promptLoopPrefix(loopSeq);
|
|
865
|
+
const undelivered = (await this.#db.drain_undelivered_prompts_for_loop.all({
|
|
866
|
+
owner_id: workerId,
|
|
867
|
+
pattern: `${prefix}%`,
|
|
868
|
+
prefix_len: prefix.length,
|
|
869
|
+
loop_id: loopId,
|
|
870
|
+
}))
|
|
871
|
+
.filter((row) => typeof row.content === "string" && row.content.length > 0);
|
|
872
|
+
for (const injectedRow of undelivered) {
|
|
873
|
+
const attributes = parsePromptAttributes(injectedRow.attributes, `Prompt ${injectedRow.pathname} attributes`);
|
|
874
|
+
if (attributes.openPaths !== undefined) {
|
|
875
|
+
turnOpenPaths.push(...assertOpenPaths(attributes.openPaths, `Prompt ${injectedRow.pathname} openPaths`));
|
|
876
|
+
}
|
|
877
|
+
const ordinal = Number(injectedRow.pathname.split("/").filter(Boolean).at(-1));
|
|
878
|
+
const promptLogId = await this.#writePromptLog({
|
|
879
|
+
workerId,
|
|
880
|
+
loopId,
|
|
881
|
+
turnId,
|
|
882
|
+
sequence: nextActionIndex++,
|
|
883
|
+
target: promptTarget(loopSeq, ordinal),
|
|
884
|
+
content: injectedRow.content,
|
|
885
|
+
});
|
|
886
|
+
onDispatch?.(promptLogId);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
// {§methods-loop-run-open-paths}: selected workspace paths belong to
|
|
890
|
+
// the prompt frame. Publish the frame, then dispatch ordinary core READs
|
|
891
|
+
// in that same turn; missing/non-member paths retain their normal 4xx.
|
|
892
|
+
for (const raw of turnOpenPaths) {
|
|
893
|
+
const pathname = raw.startsWith("/") ? raw : `/${raw}`;
|
|
894
|
+
const fileRead = {
|
|
895
|
+
op: "READ", delimiter: "", annotation: null, signal: null, lineMarker: null,
|
|
896
|
+
target: {
|
|
897
|
+
kind: "url", raw: `file://${pathname}`, scheme: "file",
|
|
898
|
+
username: null, password: null, hostname: null, port: null,
|
|
899
|
+
pathname, query: null, fragment: null,
|
|
900
|
+
},
|
|
901
|
+
body: null, position: UNKNOWN_POSITION,
|
|
902
|
+
};
|
|
903
|
+
await this.#dispatch({
|
|
904
|
+
statement: fileRead, workspaceId, workerId, loopId, turnId,
|
|
905
|
+
sequence: nextActionIndex, origin: "_plurnk", onDispatch,
|
|
906
|
+
});
|
|
907
|
+
nextActionIndex++;
|
|
908
|
+
}
|
|
909
|
+
// {§env-delta-log-pull} — materialize ambient observations before packet
|
|
910
|
+
// composition and reserve their action indices. {§exec-stream} owns the
|
|
911
|
+
// distinct byte-cursor path for this worker's streams.
|
|
912
|
+
// {§exec-poll} — EXEC `<0>` is turn-scoped: reap the worker's open turn-scoped streams (necessarily
|
|
913
|
+
// from a prior turn — this runs before the turn's own spawns) so a `<0>` never survives into
|
|
914
|
+
// the subsequent turn. The terminal output then surfaces born-OPEN via the stream-delta path.
|
|
915
|
+
await this.#reapTurnScopedStreams(workerId);
|
|
916
|
+
nextActionIndex += await this.#materializeEnvironmentDeltas({ workspaceId, workerId, loopId, turnId, fromSequence: nextActionIndex });
|
|
917
|
+
nextActionIndex += await this.#materializeStreamDeltas({ workspaceId, workerId, loopId, turnId, fromSequence: nextActionIndex });
|
|
918
|
+
// The post-reconciliation Git snapshot above is threaded into the packet
|
|
919
|
+
// and every budget rebuild; overflow never shells again.
|
|
920
|
+
// Notices are non-terminal observations, never operation-failure truth.
|
|
921
|
+
// Drain once and thread the same set through every overflow rebuild.
|
|
922
|
+
const notices = this.#notices.drain(loopId)
|
|
923
|
+
.filter((event) => event.level !== "info");
|
|
924
|
+
// Build the model request packet ({§packet-stored-shape}). The log build
|
|
925
|
+
// queries log_entries scoped to the worker — the prompt entry just
|
|
926
|
+
// written (if turn 1) is part of that query result.
|
|
927
|
+
let promptProjection = "automatic";
|
|
928
|
+
const buildPacket = () => this.#packets.buildRequestPacket({
|
|
929
|
+
initialMessages: messages,
|
|
930
|
+
requirements,
|
|
931
|
+
workspaceId,
|
|
932
|
+
workerId,
|
|
933
|
+
loopId,
|
|
934
|
+
currentTurnSeq: seq,
|
|
935
|
+
provider,
|
|
936
|
+
gitStatus,
|
|
937
|
+
notices,
|
|
938
|
+
transientOpenLogEntryId,
|
|
939
|
+
promptProjection,
|
|
940
|
+
});
|
|
941
|
+
let requestPacket = await buildPacket();
|
|
942
|
+
// {§overflow-turn} — measured overflow diverts this would-be model
|
|
943
|
+
// turn into an ordinary packetless `_plurnk` operation batch. Every
|
|
944
|
+
// visibility effect goes through Dispatcher FOLD; provider I/O is
|
|
945
|
+
// structurally unreachable on this branch.
|
|
946
|
+
const pressure = this.#packets.curationOverflow(requestPacket, provider);
|
|
947
|
+
if (pressure !== null) {
|
|
948
|
+
const folds = await OverflowTurn.plan(this.#db, loopId, turnId);
|
|
949
|
+
await Turn.becomeOverflow(this.#db, turnId);
|
|
950
|
+
const internalStatements = [
|
|
951
|
+
OverflowTurn.planStatement(),
|
|
952
|
+
...folds.map(({ statement }) => statement),
|
|
953
|
+
OverflowTurn.sendStatement(),
|
|
954
|
+
];
|
|
955
|
+
const source = TurnOps.renderInternal(internalStatements);
|
|
956
|
+
const admitted = TurnOps.parseInternal(source);
|
|
957
|
+
const executed = await this.#executeAdmittedTurn({
|
|
958
|
+
statements: admitted,
|
|
959
|
+
source,
|
|
960
|
+
sourceFolded: true,
|
|
961
|
+
origin: "_plurnk",
|
|
962
|
+
workspaceId,
|
|
963
|
+
workerId,
|
|
964
|
+
loopId,
|
|
965
|
+
turnId,
|
|
966
|
+
fromSequence: nextActionIndex,
|
|
967
|
+
failOnOperationError: true,
|
|
968
|
+
signal: this.#loopSignal(loopId),
|
|
969
|
+
onDispatch,
|
|
970
|
+
});
|
|
971
|
+
if (executed.status !== TURN_STATUS_IMPLICIT_CONTINUE) {
|
|
972
|
+
throw new Error(`overflow SEND returned ${executed.status}; expected ${TURN_STATUS_IMPLICIT_CONTINUE}`);
|
|
973
|
+
}
|
|
974
|
+
const rebuilt = await buildPacket();
|
|
975
|
+
const remaining = this.#packets.curationOverflow(rebuilt, provider);
|
|
976
|
+
const curationFailure = folds.length === 0 || remaining !== null
|
|
977
|
+
? curationOverflowFailure(remaining ?? pressure)
|
|
978
|
+
: undefined;
|
|
979
|
+
return {
|
|
980
|
+
createdTurnIds,
|
|
981
|
+
turnId,
|
|
982
|
+
producer: "_plurnk",
|
|
983
|
+
kind: "overflow",
|
|
984
|
+
status: curationFailure?.status ?? TURN_STATUS_IMPLICIT_CONTINUE,
|
|
985
|
+
outcomes: [],
|
|
986
|
+
fingerprint: "",
|
|
987
|
+
capacityHardStop: false,
|
|
988
|
+
steerStruck: false,
|
|
989
|
+
emissionAttempts: 0,
|
|
990
|
+
emissionExhausted: false,
|
|
991
|
+
...(curationFailure === undefined ? {} : { curationFailure }),
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
let modelMessages = PacketWire.packetToWireMessages(requestPacket);
|
|
995
|
+
// Curation pressure and provider generation are independent. The
|
|
996
|
+
// provider owns its configured total output envelope.
|
|
997
|
+
let response;
|
|
998
|
+
let splitResponse;
|
|
999
|
+
let railGrammar;
|
|
1000
|
+
let railResponseGrammar;
|
|
1001
|
+
let railAllowWithheld = false;
|
|
1002
|
+
let railEvidence;
|
|
1003
|
+
let emissionAttempts = 0;
|
|
1004
|
+
let providerCallInFlight = false;
|
|
1005
|
+
let modelCallSequence = 0;
|
|
1006
|
+
let currentEmissionAttempt = 0;
|
|
1007
|
+
let providerAttemptId = null;
|
|
1008
|
+
let providerModelCall = null;
|
|
1009
|
+
let providerAttemptAttributions = [];
|
|
1010
|
+
const providerSignal = this.#loopSignal(loopId) ?? signal;
|
|
1011
|
+
// {§client-metadata}
|
|
1012
|
+
const { client } = await WorkspaceSettings.read(this.#db, workspaceId);
|
|
1013
|
+
const loopSeq = (await this.#db.engine_loop_sequence.get({ loop_id: loopId }))?.sequence ?? loopId;
|
|
1014
|
+
const providerIdentity = await this.#resolveWorkerProviderIdentity(workerId);
|
|
1015
|
+
const { workerId: providerWorkerId, primaryWorkerId } = providerIdentity;
|
|
1016
|
+
const classifyProviderAttempt = async (id, attemptSplit, emissionAttempt, accepted) => {
|
|
1017
|
+
const result = await this.#db.engine_classify_turn_attempt_response.run({
|
|
1018
|
+
id,
|
|
1019
|
+
accepted: accepted ? 1 : 0,
|
|
1020
|
+
parse_errors: JSON.stringify(attemptSplit.parseErrors),
|
|
1021
|
+
});
|
|
1022
|
+
if (result.changes !== 1) {
|
|
1023
|
+
throw new Error(`emission attempt ${id} was not awaiting classification`);
|
|
1024
|
+
}
|
|
1025
|
+
emissionAttempts = emissionAttempt;
|
|
1026
|
+
};
|
|
1027
|
+
const recoverCapacityPacket = async () => {
|
|
1028
|
+
let baselineMessages = modelMessages;
|
|
1029
|
+
if (promptProjection === "automatic") {
|
|
1030
|
+
promptProjection = "withheld";
|
|
1031
|
+
const candidate = await buildPacket();
|
|
1032
|
+
const candidateMessages = PacketWire.packetToWireMessages(candidate);
|
|
1033
|
+
if (JSON.stringify(candidateMessages) !== JSON.stringify(baselineMessages)) {
|
|
1034
|
+
requestPacket = candidate;
|
|
1035
|
+
modelMessages = candidateMessages;
|
|
1036
|
+
return true;
|
|
1037
|
+
}
|
|
1038
|
+
baselineMessages = candidateMessages;
|
|
1039
|
+
}
|
|
1040
|
+
return false;
|
|
1041
|
+
};
|
|
1042
|
+
try {
|
|
1043
|
+
// {§turn-lifecycle}: bracket the complete provider-attempt window with liveness notices.
|
|
1044
|
+
if (!signal?.aborted)
|
|
1045
|
+
this.#notices.push(workspaceId, loopId, { source: "engine:turn", kind: "turn_awaiting_model", level: "info", message: "awaiting model response" });
|
|
1046
|
+
const railConstraint = await this.#grammarConstraint(provider);
|
|
1047
|
+
railGrammar = railConstraint?.transport;
|
|
1048
|
+
railResponseGrammar = railConstraint?.response;
|
|
1049
|
+
railAllowWithheld = railConstraint?.allowWithheld ?? false;
|
|
1050
|
+
const attemptLimit = readEmissionAttempts();
|
|
1051
|
+
const strikeStreak = this.#strikes.streak(loopId);
|
|
1052
|
+
for (let attempt = 1; attempt <= attemptLimit;) {
|
|
1053
|
+
// Capacity recovery may rebuild and resend the request without
|
|
1054
|
+
// consuming a grammar-emission attempt. Every logical provider
|
|
1055
|
+
// call still receives its own durable sequence and accounting.
|
|
1056
|
+
currentEmissionAttempt = attempt;
|
|
1057
|
+
modelCallSequence++;
|
|
1058
|
+
const attributionContext = Object.freeze({
|
|
1059
|
+
workspaceId: String(workspaceId),
|
|
1060
|
+
workerId: providerWorkerId,
|
|
1061
|
+
primaryWorkerId,
|
|
1062
|
+
loop: loopSeq,
|
|
1063
|
+
turn: seq,
|
|
1064
|
+
attempt: modelCallSequence,
|
|
1065
|
+
});
|
|
1066
|
+
providerAttemptAttributions = await this.#attemptAttributions(provider, attributionContext);
|
|
1067
|
+
requestPacket = { ...requestPacket, attributions: providerAttemptAttributions };
|
|
1068
|
+
providerModelCall = await ModelCall.open(this.#db, {
|
|
1069
|
+
turnId,
|
|
1070
|
+
sequence: modelCallSequence,
|
|
1071
|
+
kind: "emission",
|
|
1072
|
+
attributions: providerAttemptAttributions,
|
|
1073
|
+
model: provider.model,
|
|
1074
|
+
});
|
|
1075
|
+
const attemptRow = await this.#db.engine_open_turn_attempt.get({
|
|
1076
|
+
model_call_id: providerModelCall.id,
|
|
1077
|
+
});
|
|
1078
|
+
if (attemptRow === undefined) {
|
|
1079
|
+
throw new Error(`Engine.runTurn: provider call ${modelCallSequence} did not open`);
|
|
1080
|
+
}
|
|
1081
|
+
providerAttemptId = attemptRow.id;
|
|
1082
|
+
providerCallInFlight = true;
|
|
1083
|
+
const currentModelCall = providerModelCall;
|
|
1084
|
+
let completedResponse;
|
|
1085
|
+
try {
|
|
1086
|
+
completedResponse = await observed(// {§observability-boundary}
|
|
1087
|
+
GEN_AI_REQUEST_SPAN, { model: provider.model, attempt: modelCallSequence }, async (span) => {
|
|
1088
|
+
try {
|
|
1089
|
+
const generated = await provider.generate({
|
|
1090
|
+
messages: modelMessages,
|
|
1091
|
+
workerId: providerWorkerId,
|
|
1092
|
+
primaryWorkerId,
|
|
1093
|
+
signal: providerSignal,
|
|
1094
|
+
grammar: railGrammar,
|
|
1095
|
+
strikes: strikeStreak,
|
|
1096
|
+
attributions: providerAttemptAttributions.length > 0
|
|
1097
|
+
? providerAttemptAttributions
|
|
1098
|
+
: undefined,
|
|
1099
|
+
client: client ?? undefined,
|
|
1100
|
+
workspaceId: String(workspaceId),
|
|
1101
|
+
loop: loopSeq,
|
|
1102
|
+
turn: seq,
|
|
1103
|
+
observeRequest: currentModelCall.observeRequest,
|
|
1104
|
+
callKind: "emission",
|
|
1105
|
+
}); // {§provider-surface-generate} {§provider-guarantees-signal-wired} {§provider-guarantees-serial-attempts} {§attribution} {§client-metadata}
|
|
1106
|
+
currentModelCall.assertAccounting(generated.accounting);
|
|
1107
|
+
providerCallInFlight = false;
|
|
1108
|
+
recordCounter(PROVIDER_CALLS, {
|
|
1109
|
+
model: provider.model,
|
|
1110
|
+
attempt: modelCallSequence,
|
|
1111
|
+
status: "resolved",
|
|
1112
|
+
});
|
|
1113
|
+
span.setAttribute("status", "resolved");
|
|
1114
|
+
settleGenAiResponse(span, generated);
|
|
1115
|
+
return generated;
|
|
1116
|
+
}
|
|
1117
|
+
catch (error) {
|
|
1118
|
+
if (error instanceof ProviderError) {
|
|
1119
|
+
currentModelCall.assertAccounting(error.accounting);
|
|
1120
|
+
}
|
|
1121
|
+
throw error;
|
|
1122
|
+
}
|
|
1123
|
+
}, genAiRequestOptions(ProviderInstantiate.aliasOf(provider) ?? "plurnk", provider.model));
|
|
1124
|
+
}
|
|
1125
|
+
catch (error) {
|
|
1126
|
+
if (!(error instanceof ProviderError)
|
|
1127
|
+
|| error.kind !== "capacity_exceeded"
|
|
1128
|
+
|| providerSignal?.aborted === true
|
|
1129
|
+
|| !await recoverCapacityPacket()) {
|
|
1130
|
+
throw error;
|
|
1131
|
+
}
|
|
1132
|
+
const failure = _a.#providerFailure(error, providerSignal);
|
|
1133
|
+
await currentModelCall.fail(failure, error.capacity ?? null);
|
|
1134
|
+
providerCallInFlight = false;
|
|
1135
|
+
await this.#problems.record({
|
|
1136
|
+
workerId,
|
|
1137
|
+
loopId,
|
|
1138
|
+
turnId,
|
|
1139
|
+
sequence: nextActionIndex++,
|
|
1140
|
+
origin: "_plurnk",
|
|
1141
|
+
source: "provider",
|
|
1142
|
+
result: failure,
|
|
1143
|
+
});
|
|
1144
|
+
// Include the durable recovery signal in the replacement
|
|
1145
|
+
// request while preserving the selected recovery posture.
|
|
1146
|
+
requestPacket = await buildPacket();
|
|
1147
|
+
modelMessages = PacketWire.packetToWireMessages(requestPacket);
|
|
1148
|
+
continue;
|
|
1149
|
+
}
|
|
1150
|
+
response = completedResponse;
|
|
1151
|
+
await currentModelCall.observeResponse(completedResponse);
|
|
1152
|
+
railEvidence = railGrammar === undefined
|
|
1153
|
+
? undefined
|
|
1154
|
+
: _a.#requireGrammarEvidence(completedResponse, railAllowWithheld);
|
|
1155
|
+
splitResponse = this.#splitResponse(completedResponse);
|
|
1156
|
+
await classifyProviderAttempt(attemptRow.id, splitResponse, attempt, splitResponse.emissionValid);
|
|
1157
|
+
if (splitResponse.emissionValid)
|
|
1158
|
+
break;
|
|
1159
|
+
attempt++;
|
|
1160
|
+
}
|
|
1161
|
+
if (!signal?.aborted)
|
|
1162
|
+
this.#notices.push(workspaceId, loopId, { source: "engine:turn", kind: "turn_generated", level: "info", message: "parsing model response" });
|
|
1163
|
+
}
|
|
1164
|
+
catch (err) {
|
|
1165
|
+
// This handler owns only provider-call failures. Parser, cost, SQL,
|
|
1166
|
+
// and engine-contract failures retain their original source.
|
|
1167
|
+
if (err instanceof ModelCallPersistenceError || err instanceof ProviderAccountingIntegrityError)
|
|
1168
|
+
throw err;
|
|
1169
|
+
if (!providerCallInFlight)
|
|
1170
|
+
throw err;
|
|
1171
|
+
providerCallInFlight = false;
|
|
1172
|
+
if (providerAttemptId === null || providerModelCall === null) {
|
|
1173
|
+
throw new Error("provider call failed without durable model-call and attempt identities", { cause: err });
|
|
1174
|
+
}
|
|
1175
|
+
const failure = _a.#providerFailure(err, providerSignal);
|
|
1176
|
+
const capacityFailure = err instanceof ProviderError && err.kind === "capacity_exceeded";
|
|
1177
|
+
// {§provider-interrupted-attempt} — a provider-declared interruption
|
|
1178
|
+
// carries response evidence without becoming a completed exchange.
|
|
1179
|
+
// Persist it as an unaccepted attempt before settling the failure.
|
|
1180
|
+
if (err instanceof ProviderError && err.attempt !== undefined) {
|
|
1181
|
+
response = err.attempt;
|
|
1182
|
+
await providerModelCall.observeResponse(response, failure);
|
|
1183
|
+
splitResponse = this.#splitResponse(response);
|
|
1184
|
+
await classifyProviderAttempt(providerAttemptId, splitResponse, currentEmissionAttempt, false);
|
|
1185
|
+
}
|
|
1186
|
+
else {
|
|
1187
|
+
await providerModelCall.fail(failure, err instanceof ProviderError ? err.capacity ?? null : null);
|
|
1188
|
+
if (!capacityFailure)
|
|
1189
|
+
emissionAttempts = currentEmissionAttempt;
|
|
1190
|
+
}
|
|
1191
|
+
// {§turn-never-blank} — a ProviderError means no completed exchange exists.
|
|
1192
|
+
// Persist its exact RFC 9457 result before propagating it. Grammar evidence
|
|
1193
|
+
// and its engine-owned verdict exist only on completed responses
|
|
1194
|
+
// ({§rail-truth-engine-verdict}). Cancellation is lifecycle truth, not a
|
|
1195
|
+
// provider failure. Close the
|
|
1196
|
+
// attempted turn without inventing an assistant response, then let
|
|
1197
|
+
// runLoop/Daemon settle the exact 504/499 loop result.
|
|
1198
|
+
if (providerSignal?.aborted) {
|
|
1199
|
+
const status = providerSignal.reason === LOOP_TIMEOUT_REASON ? 504 : 499;
|
|
1200
|
+
await Turn.recordInference(this.#db, turnId, {
|
|
1201
|
+
packet: StoredPacket.stringify(requestPacket),
|
|
1202
|
+
usageCurationBudget: this.#packets.curationBudgetFor(provider),
|
|
1203
|
+
finishReason: splitResponse?.callMetadata.finishReason ?? null,
|
|
1204
|
+
model: splitResponse?.callMetadata.model ?? provider.model,
|
|
1205
|
+
meta: JSON.stringify(response?.meta ?? {}),
|
|
1206
|
+
});
|
|
1207
|
+
await Turn.complete(this.#db, turnId, status);
|
|
1208
|
+
throw err;
|
|
1209
|
+
}
|
|
1210
|
+
const recorded = await this.#problems.record({
|
|
1211
|
+
workerId,
|
|
1212
|
+
loopId,
|
|
1213
|
+
turnId,
|
|
1214
|
+
sequence: nextActionIndex,
|
|
1215
|
+
origin: "_plurnk",
|
|
1216
|
+
source: "provider",
|
|
1217
|
+
result: failure,
|
|
1218
|
+
});
|
|
1219
|
+
// The provider call was attempted, but no completed exchange exists.
|
|
1220
|
+
// Persist the exact request half and failure status; omitting assistant
|
|
1221
|
+
// is materially different from fabricating an empty model turn.
|
|
1222
|
+
await Turn.recordInference(this.#db, turnId, {
|
|
1223
|
+
packet: StoredPacket.stringify(requestPacket),
|
|
1224
|
+
usageCurationBudget: this.#packets.curationBudgetFor(provider),
|
|
1225
|
+
finishReason: splitResponse?.callMetadata.finishReason ?? null,
|
|
1226
|
+
model: splitResponse?.callMetadata.model ?? provider.model,
|
|
1227
|
+
meta: JSON.stringify(response?.meta ?? {}),
|
|
1228
|
+
});
|
|
1229
|
+
await Turn.complete(this.#db, turnId, recorded.result.status);
|
|
1230
|
+
if (capacityFailure) {
|
|
1231
|
+
return {
|
|
1232
|
+
createdTurnIds,
|
|
1233
|
+
turnId,
|
|
1234
|
+
producer: "model",
|
|
1235
|
+
kind: "inference",
|
|
1236
|
+
status: recorded.result.status,
|
|
1237
|
+
outcomes: [],
|
|
1238
|
+
fingerprint: "",
|
|
1239
|
+
capacityHardStop: true,
|
|
1240
|
+
capacityFailure: recorded.result,
|
|
1241
|
+
steerStruck: false,
|
|
1242
|
+
emissionAttempts,
|
|
1243
|
+
emissionExhausted: false,
|
|
1244
|
+
};
|
|
1245
|
+
}
|
|
1246
|
+
throw new OperationFailureError(recorded.result, { cause: err });
|
|
1247
|
+
}
|
|
1248
|
+
if (response === undefined || splitResponse === undefined || providerModelCall === null) {
|
|
1249
|
+
throw new Error("provider attempt loop completed without a response");
|
|
1250
|
+
}
|
|
1251
|
+
const emissionModelCallId = providerModelCall.id;
|
|
1252
|
+
if (!splitResponse.emissionValid) {
|
|
1253
|
+
// {§invalid-emission-attempts} The first consecutive exhaustion
|
|
1254
|
+
// publishes only the raw final response and generic recovery fact.
|
|
1255
|
+
let rejectedModelEntryId;
|
|
1256
|
+
if (allowInvalidEmissionRecovery) {
|
|
1257
|
+
rejectedModelEntryId = await this.#dispatcher.writeEmissionAttempt({
|
|
1258
|
+
verbatim: splitResponse.packetAssistant.content,
|
|
1259
|
+
workerId,
|
|
1260
|
+
loopId,
|
|
1261
|
+
turnId,
|
|
1262
|
+
sequence: nextActionIndex,
|
|
1263
|
+
modelCallId: emissionModelCallId,
|
|
1264
|
+
...(response.assistant.reasoningEncrypted?.length
|
|
1265
|
+
? { reasoningItems: response.assistant.reasoningEncrypted }
|
|
1266
|
+
: {}),
|
|
1267
|
+
});
|
|
1268
|
+
this.#notices.push(workspaceId, loopId, {
|
|
1269
|
+
source: "engine:grammar",
|
|
1270
|
+
kind: "invalid_emission",
|
|
1271
|
+
level: "error",
|
|
1272
|
+
message: INVALID_EMISSION_RECOVERY_MESSAGE,
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
1275
|
+
const status = allowInvalidEmissionRecovery ? TURN_STATUS_IMPLICIT_CONTINUE : 500;
|
|
1276
|
+
await Turn.recordInference(this.#db, turnId, {
|
|
1277
|
+
packet: StoredPacket.stringify(requestPacket),
|
|
1278
|
+
usageCurationBudget: this.#packets.curationBudgetFor(provider),
|
|
1279
|
+
finishReason: splitResponse.callMetadata.finishReason,
|
|
1280
|
+
model: splitResponse.callMetadata.model,
|
|
1281
|
+
meta: JSON.stringify(response.meta ?? {}),
|
|
1282
|
+
});
|
|
1283
|
+
await Turn.complete(this.#db, turnId, status);
|
|
1284
|
+
return {
|
|
1285
|
+
createdTurnIds,
|
|
1286
|
+
turnId,
|
|
1287
|
+
producer: "model",
|
|
1288
|
+
kind: "inference",
|
|
1289
|
+
status: allowInvalidEmissionRecovery ? TURN_STATUS_IMPLICIT_CONTINUE : 500,
|
|
1290
|
+
outcomes: [],
|
|
1291
|
+
fingerprint: "",
|
|
1292
|
+
capacityHardStop: false,
|
|
1293
|
+
steerStruck: false,
|
|
1294
|
+
emissionAttempts,
|
|
1295
|
+
emissionExhausted: true,
|
|
1296
|
+
...(rejectedModelEntryId === undefined ? {} : { rejectedModelEntryId }),
|
|
1297
|
+
};
|
|
1298
|
+
}
|
|
1299
|
+
// {§packet-stored-shape} — admitted emission data extends the packet;
|
|
1300
|
+
// provider-call metadata remains on the Turn row.
|
|
1301
|
+
const { packetAssistant, callMetadata, parseNotices, recoverableParseErrors, } = splitResponse; // raw assistant content is opaque — split, never interpreted — {§provider-guarantees-assistantraw-opaque}
|
|
1302
|
+
for (const notice of parseNotices) {
|
|
1303
|
+
this.#notices.push(workspaceId, loopId, notice);
|
|
1304
|
+
}
|
|
1305
|
+
// Non-fatal provider transport notices on an accepted turn. Forward each
|
|
1306
|
+
// Notice with a content-offset `line:col`;
|
|
1307
|
+
// the model resolves it against its own emission — READ the folded turnOps row at the
|
|
1308
|
+
// cited lines ({§turn-ops-entry}) — not an embedded snippet that would duplicate the emission.
|
|
1309
|
+
for (const notice of response.notices ?? []) {
|
|
1310
|
+
const located = typeof notice.position === "number"
|
|
1311
|
+
? this.#offsetToLineColumn(packetAssistant.content, notice.position)
|
|
1312
|
+
: null;
|
|
1313
|
+
this.#notices.push(workspaceId, loopId, {
|
|
1314
|
+
source: notice.source,
|
|
1315
|
+
kind: notice.kind,
|
|
1316
|
+
message: notice.message ?? "",
|
|
1317
|
+
level: notice.level,
|
|
1318
|
+
...(located !== null
|
|
1319
|
+
? { position: { type: "content-offset", line: located.line, column: located.column } }
|
|
1320
|
+
: {}),
|
|
1321
|
+
});
|
|
1322
|
+
}
|
|
1323
|
+
// Grade configured local evidence independently. Endpoint-owned
|
|
1324
|
+
// constraints remain provider observations. {§rail-truth-engine-verdict}
|
|
1325
|
+
let railKeys;
|
|
1326
|
+
if (railGrammar !== undefined) {
|
|
1327
|
+
if (railEvidence === undefined)
|
|
1328
|
+
throw new Error("configured GBNF response has no final grammar evidence");
|
|
1329
|
+
if (railResponseGrammar === undefined)
|
|
1330
|
+
throw new Error("configured GBNF has no response grammar");
|
|
1331
|
+
let verdict = null;
|
|
1332
|
+
try {
|
|
1333
|
+
verdict = validateGbnf(railResponseGrammar, railEvidence.input);
|
|
1334
|
+
}
|
|
1335
|
+
catch (cause) {
|
|
1336
|
+
_a.#warnRailVerdictGapOnce(cause.message);
|
|
1337
|
+
}
|
|
1338
|
+
railKeys = {
|
|
1339
|
+
railsAttached: railEvidence.transported ? "client" : "withheld",
|
|
1340
|
+
railsVerdict: verdict?.status ?? "unverifiable",
|
|
1341
|
+
};
|
|
1342
|
+
if (verdict !== null && verdict.status !== "accept") {
|
|
1343
|
+
const contentPosition = verdict.pos >= railEvidence.contentStart
|
|
1344
|
+
? verdict.pos - railEvidence.contentStart
|
|
1345
|
+
: null;
|
|
1346
|
+
const located = contentPosition === null
|
|
1347
|
+
? null
|
|
1348
|
+
: this.#offsetToLineColumn(packetAssistant.content, contentPosition);
|
|
1349
|
+
this.#notices.push(workspaceId, loopId, {
|
|
1350
|
+
source: "engine:rails",
|
|
1351
|
+
kind: "grammar_unenforced",
|
|
1352
|
+
message: verdict.status === "reject"
|
|
1353
|
+
? `emission rejects the grammar at raw code point ${verdict.pos}`
|
|
1354
|
+
: `emission is an incomplete grammar sentence (ends at raw code point ${verdict.pos})`,
|
|
1355
|
+
level: "warn",
|
|
1356
|
+
...(located === null
|
|
1357
|
+
? {}
|
|
1358
|
+
: { position: { type: "content-offset", line: located.line, column: located.column } }),
|
|
1359
|
+
});
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
// Attach the admitted inference evidence. The turn remains open until
|
|
1363
|
+
// the producer-neutral admitted-turn executor settles every operation
|
|
1364
|
+
// and its exact source artifact.
|
|
1365
|
+
const packet = StoredPacket.admit(requestPacket, packetAssistant, response.assistantRaw);
|
|
1366
|
+
await Turn.recordInference(this.#db, turnId, {
|
|
1367
|
+
packet: StoredPacket.stringify(packet),
|
|
1368
|
+
usageCurationBudget: this.#packets.curationBudgetFor(provider), // {§tokenomics-client-gauge}
|
|
1369
|
+
finishReason: callMetadata.finishReason,
|
|
1370
|
+
model: callMetadata.model,
|
|
1371
|
+
// Opaque provider metadata plus engine-authored rail keys.
|
|
1372
|
+
// {§meta-passthrough}, {§rail-truth-engine-verdict}
|
|
1373
|
+
meta: JSON.stringify({ ...(response.meta ?? {}), ...(railKeys ?? {}) }),
|
|
1374
|
+
});
|
|
1375
|
+
// {§operator-config-workspace-max-commands} — workspace maxCommands
|
|
1376
|
+
// narrows the operator ceiling before the admitted program reaches the
|
|
1377
|
+
// shared executor.
|
|
1378
|
+
const maxCommands = Math.min(readMaxCommands(), (await WorkspaceSettings.read(this.#db, workspaceId)).maxCommands ?? Number.POSITIVE_INFINITY);
|
|
1379
|
+
const reasoningItems = response.assistant.reasoningEncrypted?.length
|
|
1380
|
+
? response.assistant.reasoningEncrypted
|
|
1381
|
+
: undefined;
|
|
1382
|
+
const executed = await this.#executeAdmittedTurn({
|
|
1383
|
+
statements: packetAssistant.ops,
|
|
1384
|
+
source: splitResponse.sourceBacked ? packetAssistant.content : null,
|
|
1385
|
+
sourceFolded: true,
|
|
1386
|
+
sourceModelCallId: emissionModelCallId,
|
|
1387
|
+
...(reasoningItems !== undefined ? { sourceReasoningItems: reasoningItems } : {}),
|
|
1388
|
+
origin: "model",
|
|
1389
|
+
workspaceId,
|
|
1390
|
+
workerId,
|
|
1391
|
+
loopId,
|
|
1392
|
+
turnId,
|
|
1393
|
+
fromSequence: nextActionIndex,
|
|
1394
|
+
maxCommands,
|
|
1395
|
+
enforceIdle: true,
|
|
1396
|
+
recoverableParseErrors,
|
|
1397
|
+
bare: {
|
|
1398
|
+
provider: childProvider,
|
|
1399
|
+
modelCallSequenceStart: modelCallSequence + 1,
|
|
1400
|
+
primaryWorkerId,
|
|
1401
|
+
loopSequence: loopSeq,
|
|
1402
|
+
turnSequence: seq,
|
|
1403
|
+
signal: providerSignal,
|
|
1404
|
+
},
|
|
1405
|
+
signal: providerSignal,
|
|
1406
|
+
onDispatch,
|
|
1407
|
+
});
|
|
1408
|
+
return {
|
|
1409
|
+
createdTurnIds,
|
|
1410
|
+
turnId,
|
|
1411
|
+
producer: "model",
|
|
1412
|
+
kind: "inference",
|
|
1413
|
+
status: executed.status,
|
|
1414
|
+
outcomes: executed.outcomes,
|
|
1415
|
+
fingerprint: executed.fingerprint,
|
|
1416
|
+
capacityHardStop: false,
|
|
1417
|
+
steerStruck: executed.steerStruck,
|
|
1418
|
+
emissionAttempts,
|
|
1419
|
+
emissionExhausted: false,
|
|
1420
|
+
};
|
|
1421
|
+
}
|
|
1422
|
+
catch (cause) {
|
|
1423
|
+
const completionFailures = [];
|
|
1424
|
+
for (const createdTurnId of createdTurnIds) {
|
|
1425
|
+
try {
|
|
1426
|
+
await Turn.failOpen(this.#db, createdTurnId);
|
|
1427
|
+
}
|
|
1428
|
+
catch (completionCause) {
|
|
1429
|
+
completionFailures.push(completionCause);
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
if (completionFailures.length > 0) {
|
|
1433
|
+
throw new AggregateError([cause, ...completionFailures], "turn execution failed and its open turn containers could not all be completed");
|
|
1434
|
+
}
|
|
1435
|
+
throw cause;
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1322
1438
|
// Split the wire-level ProviderResponse into the two destinations:
|
|
1323
1439
|
// packet.assistant gets the model's emission (content, ops, reasoning);
|
|
1324
1440
|
// Turn columns get accepted-call metadata (finishReason, model). Physical
|
|
@@ -1412,6 +1528,7 @@ class TurnRunner {
|
|
|
1412
1528
|
const reasoning = assistant.reasoning ?? null;
|
|
1413
1529
|
return {
|
|
1414
1530
|
packetAssistant: { content: assistant.content, ops, reasoning },
|
|
1531
|
+
sourceBacked: preParsedOps === undefined,
|
|
1415
1532
|
callMetadata: { finishReason: assistant.finishReason, model: assistant.model },
|
|
1416
1533
|
parseErrors,
|
|
1417
1534
|
recoverableParseErrors: emissionValid ? recoverableParseErrors : [],
|
|
@@ -1424,8 +1541,8 @@ class TurnRunner {
|
|
|
1424
1541
|
emissionValid,
|
|
1425
1542
|
};
|
|
1426
1543
|
}
|
|
1427
|
-
// #note12 —
|
|
1428
|
-
//
|
|
1544
|
+
// #note12 — plugin reference docs are materialized beneath
|
|
1545
|
+
// worker://plurnk/skills/plurnk/ by LoopDocs.
|
|
1429
1546
|
async #materializeEnvironmentDeltas(args) {
|
|
1430
1547
|
const { workspaceId, workerId, loopId, turnId, fromSequence } = args;
|
|
1431
1548
|
const rows = await this.#db.engine_pull_ambient_events.all({ workspace_id: workspaceId, worker_id: workerId });
|
|
@@ -1487,7 +1604,9 @@ class TurnRunner {
|
|
|
1487
1604
|
mimetypeTx: "text/plain",
|
|
1488
1605
|
mimetypeRx: "application/json",
|
|
1489
1606
|
}, this.#weighContent),
|
|
1490
|
-
|
|
1607
|
+
folded: LogVisibility.serialize(terminal !== null && terminal.status >= 200 && terminal.status < 300
|
|
1608
|
+
? LogVisibility.OPEN
|
|
1609
|
+
: LogVisibility.FOLDED),
|
|
1491
1610
|
attrs,
|
|
1492
1611
|
});
|
|
1493
1612
|
const materialized = inserted ?? await this.#db.engine_ambient_delta_id.get({
|
|
@@ -1567,8 +1686,9 @@ class TurnRunner {
|
|
|
1567
1686
|
// content and the auto-OPEN terminal observation never fired — the model was never shown
|
|
1568
1687
|
// the conclusion of a stream whose result it already holds folded. The same
|
|
1569
1688
|
// observation is required when the channel produced no publishable content:
|
|
1570
|
-
// completion is information independently of payload.
|
|
1571
|
-
// terse marker;
|
|
1689
|
+
// completion is information independently of payload. A text stream that delivered
|
|
1690
|
+
// content retains its terse revisit marker; an empty text stream and every structured
|
|
1691
|
+
// channel emit a bodyless typed conclusion.
|
|
1572
1692
|
if (closed && priorAttrs.terminal !== true) {
|
|
1573
1693
|
const streamTarget = renderTarget({
|
|
1574
1694
|
scheme: ch.runtime,
|
|
@@ -1577,12 +1697,9 @@ class TurnRunner {
|
|
|
1577
1697
|
});
|
|
1578
1698
|
if (streamTarget === null)
|
|
1579
1699
|
throw new Error(`stream ${ch.subscription_id} has no renderable address`);
|
|
1580
|
-
const pointer = cursor > 0
|
|
1581
|
-
? `full output already delivered above; READ ${streamTarget} to revisit`
|
|
1582
|
-
: "stream produced no output";
|
|
1583
1700
|
const sequence = fromSequence + written;
|
|
1584
|
-
const content = baseMimetype(ch.mimetype).startsWith("text/")
|
|
1585
|
-
? `[ stream closed (${ch.close_status ?? 200}) - ${
|
|
1701
|
+
const content = cursor > 0 && baseMimetype(ch.mimetype).startsWith("text/")
|
|
1702
|
+
? `[ stream closed (${ch.close_status ?? 200}) - full output already delivered above; READ ${streamTarget} to revisit ]`
|
|
1586
1703
|
: "";
|
|
1587
1704
|
const rx = JSON.stringify(await terminalResult({
|
|
1588
1705
|
content,
|
|
@@ -1602,7 +1719,7 @@ class TurnRunner {
|
|
|
1602
1719
|
}, this.#weighContent),
|
|
1603
1720
|
status: terminal?.status ?? 200,
|
|
1604
1721
|
attrs: JSON.stringify({ streamEnd: ch.content.length, terminal: true }),
|
|
1605
|
-
|
|
1722
|
+
folded: LogVisibility.serialize(LogVisibility.OPEN),
|
|
1606
1723
|
});
|
|
1607
1724
|
written++;
|
|
1608
1725
|
}
|
|
@@ -1633,55 +1750,85 @@ class TurnRunner {
|
|
|
1633
1750
|
}, this.#weighContent),
|
|
1634
1751
|
status: result.status,
|
|
1635
1752
|
attrs: JSON.stringify({ streamEnd: publishEnd, terminal: terminalDelivery }),
|
|
1636
|
-
|
|
1753
|
+
folded: LogVisibility.serialize(terminalDelivery ? LogVisibility.OPEN : LogVisibility.FOLDED), // {§exec-stream} — terminal observation auto-OPENs; ongoing folds
|
|
1637
1754
|
});
|
|
1638
1755
|
written++;
|
|
1639
1756
|
}
|
|
1640
1757
|
return written;
|
|
1641
1758
|
}
|
|
1642
1759
|
// {§env-delta-filesystem-narration} {§membership-emi-divergence-signal}
|
|
1643
|
-
// —
|
|
1760
|
+
// — record project-file divergence once through the reserved actor.
|
|
1644
1761
|
async #logFsFictions(workspaceId, divergences, gitStatus) {
|
|
1645
1762
|
if (divergences.length === 0)
|
|
1646
1763
|
return;
|
|
1647
1764
|
const gitByPath = new Map(gitStatus?.files.map(({ path, status }) => [path, status]) ?? []);
|
|
1648
1765
|
const worker = await this.#db.envelope_get_worker_by_name.get({ workspace_id: workspaceId, name: "plurnk" })
|
|
1649
|
-
?? await this.#db.envelope_insert_worker.get({ workspace_id: workspaceId, name: "plurnk", origin: "
|
|
1766
|
+
?? await this.#db.envelope_insert_worker.get({ workspace_id: workspaceId, name: "plurnk", origin: "_plurnk" });
|
|
1650
1767
|
if (worker === undefined)
|
|
1651
1768
|
throw new Error("logFsFictions: plurnk worker resolution returned no row");
|
|
1652
1769
|
const loop = await this.#db.envelope_insert_client_loop.get({ worker_id: worker.id });
|
|
1653
1770
|
if (loop === undefined)
|
|
1654
1771
|
throw new Error("logFsFictions: loop insert returned no row");
|
|
1655
|
-
const turn = await
|
|
1656
|
-
let
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1772
|
+
const turn = await Turn.open(this.#db, { loopId: loop.id, producer: "_plurnk", kind: "operation" });
|
|
1773
|
+
let turnOpen = true;
|
|
1774
|
+
try {
|
|
1775
|
+
let sequence = 1;
|
|
1776
|
+
for (const d of divergences) {
|
|
1777
|
+
const span = editedSpan(d.before, d.after);
|
|
1778
|
+
const rx = JSON.stringify({ status: 200, entryId: d.entryId, channel: d.channel, span });
|
|
1779
|
+
const attrs = gitByPath.has(d.pathname)
|
|
1780
|
+
? JSON.stringify({ git: gitByPath.get(d.pathname) })
|
|
1781
|
+
: "{}";
|
|
1782
|
+
await this.#db.engine_insert_log_entry.get({
|
|
1783
|
+
worker_id: worker.id, loop_id: loop.id, turn_id: turn.id, sequence: sequence++,
|
|
1784
|
+
origin: "_plurnk", source: "file", model_call_id: null,
|
|
1785
|
+
op: "EDIT", delimiter: "", signal: null,
|
|
1786
|
+
// Match Dispatcher.#extractTarget: a bare file address has NULL scheme
|
|
1787
|
+
// only in log target metadata; its entry identity remains `file`.
|
|
1788
|
+
scheme: null, username: null, password: null, hostname: null, port: null,
|
|
1789
|
+
pathname: d.pathname, query: null, fragment: null, lineMarker: null,
|
|
1790
|
+
tx: "", mimetype_tx: "text/plain",
|
|
1791
|
+
rx, mimetype_rx: "application/json",
|
|
1792
|
+
status_rx: 200,
|
|
1793
|
+
weight: LogBody.weight({
|
|
1794
|
+
op: "EDIT",
|
|
1795
|
+
attrs,
|
|
1796
|
+
tx: "",
|
|
1797
|
+
rx,
|
|
1798
|
+
mimetypeTx: "text/plain",
|
|
1799
|
+
mimetypeRx: "application/json",
|
|
1800
|
+
}, this.#weighContent),
|
|
1801
|
+
state: "resolved", outcome: null,
|
|
1676
1802
|
attrs,
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1803
|
+
});
|
|
1804
|
+
}
|
|
1805
|
+
await Turn.complete(this.#db, turn.id, 200);
|
|
1806
|
+
turnOpen = false;
|
|
1807
|
+
const closed = await new LoopLifecycle(this.#db).finish(loop.id, { status: 200 });
|
|
1808
|
+
if (closed === null)
|
|
1809
|
+
throw new Error(`logFsFictions: narration loop ${loop.id} was not open at completion`);
|
|
1810
|
+
}
|
|
1811
|
+
catch (cause) {
|
|
1812
|
+
const settlementFailures = [];
|
|
1813
|
+
const failure = Results.failure("engine:filesystem-narration", "narration-failed", 500, "Filesystem divergence narration failed before its operation turn settled.", {}, { stage: "filesystem-narration", retryable: false });
|
|
1814
|
+
if (turnOpen) {
|
|
1815
|
+
try {
|
|
1816
|
+
await Turn.complete(this.#db, turn.id, failure.status);
|
|
1817
|
+
}
|
|
1818
|
+
catch (turnCause) {
|
|
1819
|
+
settlementFailures.push(turnCause);
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
try {
|
|
1823
|
+
await new LoopLifecycle(this.#db).finish(loop.id, failure);
|
|
1824
|
+
}
|
|
1825
|
+
catch (loopCause) {
|
|
1826
|
+
settlementFailures.push(loopCause);
|
|
1827
|
+
}
|
|
1828
|
+
if (settlementFailures.length > 0) {
|
|
1829
|
+
throw new AggregateError([cause, ...settlementFailures], `filesystem narration ${turn.id} failed to settle`);
|
|
1830
|
+
}
|
|
1831
|
+
throw cause;
|
|
1685
1832
|
}
|
|
1686
1833
|
}
|
|
1687
1834
|
async #writePromptLog({ workerId, loopId, turnId, sequence, target, content, }) {
|
|
@@ -1691,11 +1838,11 @@ class TurnRunner {
|
|
|
1691
1838
|
loop_id: loopId,
|
|
1692
1839
|
turn_id: turnId,
|
|
1693
1840
|
sequence,
|
|
1694
|
-
origin: "
|
|
1841
|
+
origin: "_plurnk",
|
|
1695
1842
|
source: null,
|
|
1696
1843
|
model_call_id: null,
|
|
1697
1844
|
op: "prompt",
|
|
1698
|
-
|
|
1845
|
+
delimiter: "",
|
|
1699
1846
|
signal: null,
|
|
1700
1847
|
scheme: target.scheme,
|
|
1701
1848
|
username: target.username,
|