@plurnk/plurnk-service 1.5.0 → 1.6.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 +21 -12
- package/INSTALL.md +11 -7
- package/SPEC.md +437 -304
- package/dist/build-info.json +1 -1
- package/dist/content/edit-collision.d.ts +5 -0
- package/dist/content/edit-collision.d.ts.map +1 -0
- package/dist/content/edit-collision.js +14 -0
- package/dist/content/edit-collision.js.map +1 -0
- package/dist/content/edit-receipt.d.ts +3 -2
- package/dist/content/edit-receipt.d.ts.map +1 -1
- package/dist/content/edit-receipt.js +58 -13
- package/dist/content/edit-receipt.js.map +1 -1
- package/dist/content/index.d.ts +4 -1
- package/dist/content/index.d.ts.map +1 -1
- package/dist/content/index.js +3 -1
- package/dist/content/index.js.map +1 -1
- package/dist/content/line-anchors.d.ts +41 -0
- package/dist/content/line-anchors.d.ts.map +1 -0
- package/dist/content/line-anchors.js +191 -0
- package/dist/content/line-anchors.js.map +1 -0
- package/dist/content/read-projector.d.ts +7 -1
- package/dist/content/read-projector.d.ts.map +1 -1
- package/dist/content/read-projector.js +46 -10
- package/dist/content/read-projector.js.map +1 -1
- package/dist/content/read-resolve.js +3 -3
- package/dist/content/read-resolve.js.map +1 -1
- package/dist/core/BudgetReadout.d.ts.map +1 -1
- package/dist/core/BudgetReadout.js +9 -2
- package/dist/core/BudgetReadout.js.map +1 -1
- package/dist/core/ChannelWrite.js +1 -1
- package/dist/core/ChannelWrite.js.map +1 -1
- package/dist/core/Dispatcher.d.ts +13 -3
- package/dist/core/Dispatcher.d.ts.map +1 -1
- package/dist/core/Dispatcher.js +391 -113
- package/dist/core/Dispatcher.js.map +1 -1
- package/dist/core/DurableStatement.d.ts.map +1 -1
- package/dist/core/DurableStatement.js +2 -0
- package/dist/core/DurableStatement.js.map +1 -1
- package/dist/core/Engine.d.ts +13 -14
- package/dist/core/Engine.d.ts.map +1 -1
- package/dist/core/Engine.js +434 -300
- package/dist/core/Engine.js.map +1 -1
- package/dist/core/Engine.sql +156 -90
- package/dist/core/ExecutableTools.d.ts +10 -0
- package/dist/core/ExecutableTools.d.ts.map +1 -0
- package/dist/core/ExecutableTools.js +41 -0
- package/dist/core/ExecutableTools.js.map +1 -0
- package/dist/core/ExecutorRegistry.d.ts +3 -3
- package/dist/core/ExecutorRegistry.d.ts.map +1 -1
- package/dist/core/ExecutorRegistry.js +2 -2
- package/dist/core/ExecutorRegistry.js.map +1 -1
- package/dist/core/LogBody.d.ts +2 -0
- package/dist/core/LogBody.d.ts.map +1 -1
- package/dist/core/LogBody.js +17 -5
- package/dist/core/LogBody.js.map +1 -1
- package/dist/core/LogEntryProjection.d.ts +14 -0
- package/dist/core/LogEntryProjection.d.ts.map +1 -0
- package/dist/core/LogEntryProjection.js +38 -0
- package/dist/core/LogEntryProjection.js.map +1 -0
- package/dist/core/ModelCall.d.ts +27 -0
- package/dist/core/ModelCall.d.ts.map +1 -0
- package/dist/core/ModelCall.js +125 -0
- package/dist/core/ModelCall.js.map +1 -0
- package/dist/core/PacketBuilder.d.ts +10 -10
- package/dist/core/PacketBuilder.d.ts.map +1 -1
- package/dist/core/PacketBuilder.js +131 -103
- package/dist/core/PacketBuilder.js.map +1 -1
- package/dist/core/ProblemLog.d.ts.map +1 -1
- package/dist/core/ProblemLog.js +1 -0
- package/dist/core/ProblemLog.js.map +1 -1
- package/dist/core/ProposalLifecycle.d.ts.map +1 -1
- package/dist/core/ProposalLifecycle.js +3 -4
- package/dist/core/ProposalLifecycle.js.map +1 -1
- package/dist/core/ProviderInstantiate.d.ts.map +1 -1
- package/dist/core/ProviderInstantiate.js +23 -6
- package/dist/core/ProviderInstantiate.js.map +1 -1
- package/dist/core/SchemeRegistry.js +7 -7
- package/dist/core/SchemeRegistry.js.map +1 -1
- package/dist/core/StrikeRail.d.ts +0 -1
- package/dist/core/StrikeRail.d.ts.map +1 -1
- package/dist/core/StrikeRail.js +6 -4
- package/dist/core/StrikeRail.js.map +1 -1
- package/dist/core/caps/CapsResolve.js +1 -1
- package/dist/core/caps/CapsResolve.js.map +1 -1
- package/dist/core/caps/DbEntryCaps.d.ts +2 -1
- package/dist/core/caps/DbEntryCaps.d.ts.map +1 -1
- package/dist/core/caps/DbEntryCaps.js +8 -5
- package/dist/core/caps/DbEntryCaps.js.map +1 -1
- package/dist/core/caps/DbProjectionCaps.d.ts +1 -0
- package/dist/core/caps/DbProjectionCaps.d.ts.map +1 -1
- package/dist/core/caps/DbProjectionCaps.js +29 -0
- package/dist/core/caps/DbProjectionCaps.js.map +1 -1
- package/dist/core/caps/SchemeCtxImpl.d.ts +5 -2
- package/dist/core/caps/SchemeCtxImpl.d.ts.map +1 -1
- package/dist/core/caps/SchemeCtxImpl.js +6 -4
- package/dist/core/caps/SchemeCtxImpl.js.map +1 -1
- package/dist/core/fork.d.ts.map +1 -1
- package/dist/core/fork.js +23 -7
- package/dist/core/fork.js.map +1 -1
- package/dist/core/fork.sql +33 -12
- package/dist/core/git-membership.d.ts.map +1 -1
- package/dist/core/git-membership.js +1 -2
- package/dist/core/git-membership.js.map +1 -1
- package/dist/core/git-state.js +1 -1
- package/dist/core/git-state.js.map +1 -1
- package/dist/core/packet-wire.d.ts +4 -1
- package/dist/core/packet-wire.d.ts.map +1 -1
- package/dist/core/packet-wire.js +248 -90
- package/dist/core/packet-wire.js.map +1 -1
- package/dist/core/provider-accounting.d.ts +24 -0
- package/dist/core/provider-accounting.d.ts.map +1 -0
- package/dist/core/provider-accounting.js +76 -0
- package/dist/core/provider-accounting.js.map +1 -0
- package/dist/core/search-gate.d.ts +2 -2
- package/dist/core/search-gate.d.ts.map +1 -1
- package/dist/core/search-gate.js +8 -8
- package/dist/core/search-gate.js.map +1 -1
- package/dist/core/token-ruler.js +1 -1
- package/dist/core/token-ruler.js.map +1 -1
- package/dist/core/turn-scheduler.js +1 -1
- package/dist/core/turn-scheduler.js.map +1 -1
- package/dist/digest/Digest.d.ts.map +1 -1
- package/dist/digest/Digest.js +220 -109
- package/dist/digest/Digest.js.map +1 -1
- package/dist/digest/digest.sql +40 -18
- package/dist/schemes/Exec.d.ts.map +1 -1
- package/dist/schemes/Exec.js +111 -101
- package/dist/schemes/Exec.js.map +1 -1
- package/dist/schemes/File.d.ts +5 -4
- package/dist/schemes/File.d.ts.map +1 -1
- package/dist/schemes/File.js +24 -22
- package/dist/schemes/File.js.map +1 -1
- package/dist/schemes/Log.d.ts +11 -0
- package/dist/schemes/Log.d.ts.map +1 -1
- package/dist/schemes/Log.js +171 -106
- package/dist/schemes/Log.js.map +1 -1
- package/dist/schemes/Log.sql +48 -32
- package/dist/schemes/Prompt.d.ts +3 -3
- package/dist/schemes/Prompt.d.ts.map +1 -1
- package/dist/schemes/Prompt.js +4 -1
- package/dist/schemes/Prompt.js.map +1 -1
- package/dist/schemes/Skill.d.ts +4 -4
- package/dist/schemes/Skill.d.ts.map +1 -1
- package/dist/schemes/Skill.js +3 -0
- package/dist/schemes/Skill.js.map +1 -1
- package/dist/schemes/Worker.d.ts +4 -4
- package/dist/schemes/Worker.d.ts.map +1 -1
- package/dist/schemes/Worker.js +9 -5
- package/dist/schemes/Worker.js.map +1 -1
- package/dist/schemes/_entry-crud.d.ts +0 -1
- package/dist/schemes/_entry-crud.d.ts.map +1 -1
- package/dist/schemes/_entry-crud.js +15 -9
- package/dist/schemes/_entry-crud.js.map +1 -1
- package/dist/schemes/_entry-crud.sql +2 -14
- package/dist/schemes/_entry-find.d.ts +13 -12
- package/dist/schemes/_entry-find.d.ts.map +1 -1
- package/dist/schemes/_entry-find.js +33 -28
- package/dist/schemes/_entry-find.js.map +1 -1
- package/dist/schemes/_entry-find.sql +2 -21
- package/dist/schemes/_entry-graph.sql +1 -1
- package/dist/schemes/_entry-manifest.d.ts +5 -17
- package/dist/schemes/_entry-manifest.d.ts.map +1 -1
- package/dist/schemes/_entry-manifest.js +28 -24
- package/dist/schemes/_entry-manifest.js.map +1 -1
- package/dist/schemes/_entry-ops.d.ts +5 -4
- package/dist/schemes/_entry-ops.d.ts.map +1 -1
- package/dist/schemes/_entry-ops.js +87 -47
- package/dist/schemes/_entry-ops.js.map +1 -1
- package/dist/schemes/_entry-ops.sql +30 -4
- package/dist/schemes/_entry-send.js +4 -4
- package/dist/schemes/_entry-send.js.map +1 -1
- package/dist/schemes/_search-index.d.ts.map +1 -1
- package/dist/schemes/_search-index.js +10 -2
- package/dist/schemes/_search-index.js.map +1 -1
- package/dist/schemes/exec-abort.js +2 -2
- package/dist/schemes/exec-abort.js.map +1 -1
- package/dist/server/Daemon.d.ts +1 -1
- package/dist/server/Daemon.d.ts.map +1 -1
- package/dist/server/Daemon.js +25 -23
- package/dist/server/Daemon.js.map +1 -1
- package/dist/server/drain.sql +2 -2
- package/dist/server/envelope.d.ts +0 -2
- package/dist/server/envelope.d.ts.map +1 -1
- package/dist/server/envelope.js.map +1 -1
- package/dist/server/envelope.sql +2 -2
- package/dist/server/lifecycle-recovery.sql +25 -15
- package/dist/server/logEntry.d.ts +1 -0
- package/dist/server/logEntry.d.ts.map +1 -1
- package/dist/server/logEntry.js +13 -1
- package/dist/server/logEntry.js.map +1 -1
- package/dist/server/logEntry.sql +5 -1
- package/migrations/001_schema.sql +603 -207
- package/package.json +39 -34
- package/dist/core/BudgetOverflow.d.ts +0 -20
- package/dist/core/BudgetOverflow.d.ts.map +0 -1
- package/dist/core/BudgetOverflow.js +0 -49
- package/dist/core/BudgetOverflow.js.map +0 -1
- package/dist/core/caps/DbTagCaps.d.ts +0 -10
- package/dist/core/caps/DbTagCaps.d.ts.map +0 -1
- package/dist/core/caps/DbTagCaps.js +0 -46
- package/dist/core/caps/DbTagCaps.js.map +0 -1
package/dist/core/Engine.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
var _a;
|
|
1
2
|
import { PlurnkParser, PlurnkParseError, UNKNOWN_POSITION } from "@plurnk/plurnk-contracts";
|
|
2
|
-
import { RuntimeTag } from "@plurnk/plurnk-execs";
|
|
3
|
+
import { RuntimeInvocation, RuntimeTag } from "@plurnk/plurnk-execs";
|
|
3
4
|
import Owner from "./Owner.js";
|
|
4
5
|
const TERMINAL_SEND_SIGNALS = new Set([102, 200, 202, 300, 499]);
|
|
5
6
|
const comparePosition = (a, b) => a.line - b.line || a.column - b.column;
|
|
@@ -19,6 +20,7 @@ import LoopLifecycle from "./LoopLifecycle.js";
|
|
|
19
20
|
import { readFile } from "node:fs/promises";
|
|
20
21
|
import { fileURLToPath } from "node:url";
|
|
21
22
|
import { setTimeout as delay } from "node:timers/promises";
|
|
23
|
+
import { randomUUID } from "node:crypto";
|
|
22
24
|
// Shared module imported by both Engine and bin/digest.ts, so wire
|
|
23
25
|
// projection and digest projection are structurally one function — no
|
|
24
26
|
// drift between wire and digest possible.
|
|
@@ -26,7 +28,6 @@ import PacketWire from "./packet-wire.js";
|
|
|
26
28
|
import Results, { OperationFailureError } from "./results.js";
|
|
27
29
|
import BranchReceipt from "./BranchReceipt.js";
|
|
28
30
|
import TerminalResult from "./TerminalResult.js";
|
|
29
|
-
import BudgetOverflow from "./BudgetOverflow.js";
|
|
30
31
|
import WorkerControlAddress from "./WorkerControlAddress.js";
|
|
31
32
|
import JournalTurn from "./JournalTurn.js";
|
|
32
33
|
// The engine's collaborators — each owns one machine; Engine owns the loop/turn
|
|
@@ -42,7 +43,9 @@ import { observed, observedSync } from "../observe/spans.js";
|
|
|
42
43
|
import { OPS_DISPATCHED, PROVIDER_CALLS, recordCounter } from "../observe/metrics.js";
|
|
43
44
|
import { scheduleTurnOps } from "./turn-scheduler.js";
|
|
44
45
|
import { readOptimisticSettlementMs } from "./optimistic-settlement.js";
|
|
45
|
-
|
|
46
|
+
import { providerRequestFromStorageRow, } from "./provider-accounting.js";
|
|
47
|
+
import ModelCall, { ModelCallPersistenceError, ProviderAccountingIntegrityError, } from "./ModelCall.js";
|
|
48
|
+
const DEFAULT_MAX_STRIKES = 6;
|
|
46
49
|
const RECORD_STREAM_MIMETYPES = new Set(["application/jsonl", "application/x-ndjson"]);
|
|
47
50
|
const baseMimetype = (mimetype) => mimetype.split(";", 1)[0].trim().toLowerCase();
|
|
48
51
|
// {§exec-stream} Active streams publish only independently meaningful units.
|
|
@@ -64,7 +67,7 @@ const ENGINE_PROBLEMS = Object.freeze({
|
|
|
64
67
|
idle_turn: {
|
|
65
68
|
status: 409,
|
|
66
69
|
code: "idle-turn",
|
|
67
|
-
detail: "
|
|
70
|
+
detail: "`## SEND0 [102]` was emitted without an operation to continue from.",
|
|
68
71
|
},
|
|
69
72
|
});
|
|
70
73
|
// The prompt entry target - prompt:///<loop>/<N>, self-only ({§prompt-self-only}):
|
|
@@ -123,9 +126,50 @@ const readFilesItems = () => {
|
|
|
123
126
|
return null;
|
|
124
127
|
return normalizeFilesItems(Number.parseInt(raw, 10));
|
|
125
128
|
};
|
|
126
|
-
import {
|
|
129
|
+
import { aggregateProviderAccounting, ProviderError, resolveActiveAlias, scopeEnvToAlias, } from "@plurnk/plurnk-providers";
|
|
127
130
|
import { validateGbnf } from "@plurnk/gbnf";
|
|
128
131
|
import ProviderInstantiate from "./ProviderInstantiate.js";
|
|
132
|
+
// A generated rail may constrain only the sampled continuation while the chat
|
|
133
|
+
// template contributes a prefix that llama-server preserves in its response.
|
|
134
|
+
// The artifact names the alternate root that composes those bytes for evidence
|
|
135
|
+
// grading; arbitrary grammars without the declaration retain their transport root.
|
|
136
|
+
const grammarConstraint = (grammar) => {
|
|
137
|
+
const declarations = [...grammar.matchAll(/^# @plurnk-response-root ([A-Za-z][A-Za-z0-9-]*)$/gm)];
|
|
138
|
+
if (declarations.length === 0)
|
|
139
|
+
return { transport: grammar, response: grammar };
|
|
140
|
+
if (declarations.length !== 1)
|
|
141
|
+
throw new Error("GBNF constraint declares multiple @plurnk-response-root values");
|
|
142
|
+
const responseRoot = declarations[0][1];
|
|
143
|
+
if (!grammar.split("\n").some((line) => line.startsWith(`${responseRoot} ::=`))) {
|
|
144
|
+
throw new Error(`GBNF constraint declares missing response root ${responseRoot}`);
|
|
145
|
+
}
|
|
146
|
+
const roots = [...grammar.matchAll(/^root ::= ([A-Za-z][A-Za-z0-9-]*)$/gm)];
|
|
147
|
+
if (roots.length !== 1)
|
|
148
|
+
throw new Error("GBNF constraint must declare exactly one simple root");
|
|
149
|
+
return {
|
|
150
|
+
transport: grammar,
|
|
151
|
+
response: grammar.replace(roots[0][0], `root ::= ${responseRoot}`),
|
|
152
|
+
};
|
|
153
|
+
};
|
|
154
|
+
const TOKEN_BUDGET_OVERFLOW_DETAIL = "Token Budget Overflow: Token Usage exceeded Token Ceiling. Newest log items were automatically FOLDed to fit within token budget. Curate the log and/or perform more conservatively scoped or chunked retrieval operations to recover.";
|
|
155
|
+
const tokenBudgetOverflowFailure = (pressure) => Results.failure("engine:context", "token-budget-overflow", 413, TOKEN_BUDGET_OVERFLOW_DETAIL, {}, { ...pressure });
|
|
156
|
+
const budgetPressure = (usage, ceiling) => {
|
|
157
|
+
if (usage <= ceiling)
|
|
158
|
+
throw new Error("context-envelope admission requires positive ruler debt");
|
|
159
|
+
return { usage, ceiling, deficit: usage - ceiling };
|
|
160
|
+
};
|
|
161
|
+
const contextEnvelopeFailure = (pressure, admission) => Results.failure("engine:context", "context-envelope-admission-failed", 413, `The configured context envelope cannot admit this request: ${admission.detail}.`, {}, {
|
|
162
|
+
...pressure,
|
|
163
|
+
stage: "context-envelope-admission",
|
|
164
|
+
contextAdmission: admission.reason,
|
|
165
|
+
contextCapacity: admission.capacity,
|
|
166
|
+
...(admission.measurement === undefined ? {} : {
|
|
167
|
+
promptTokens: admission.measurement.tokens,
|
|
168
|
+
tokenKind: admission.measurement.kind,
|
|
169
|
+
tokenSource: admission.measurement.source,
|
|
170
|
+
}),
|
|
171
|
+
retryable: false,
|
|
172
|
+
});
|
|
129
173
|
// Runtime normalization for a disposition the engine refuses or resolves as a
|
|
130
174
|
// continue after dispatch ({§send}). Every admitted emission itself ends in an
|
|
131
175
|
// explicit disposition SEND ({§emission-admission}).
|
|
@@ -155,7 +199,7 @@ const DEFAULT_LOOP_TIMEOUT_MS = 86400000;
|
|
|
155
199
|
const readLoopTimeoutMs = () => readPositiveInt("PLURNK_SERVICE_LOOP_TIMEOUT", DEFAULT_LOOP_TIMEOUT_MS);
|
|
156
200
|
// The wall's abort reason — runLoop branches a mid-turn teardown to the 504 terminal on it.
|
|
157
201
|
const LOOP_TIMEOUT_REASON = "loop_timeout";
|
|
158
|
-
|
|
202
|
+
class Engine {
|
|
159
203
|
static fingerprintTurn(ops) {
|
|
160
204
|
return StrikeRail.fingerprintTurn(ops);
|
|
161
205
|
}
|
|
@@ -172,7 +216,7 @@ export default class Engine {
|
|
|
172
216
|
// Boot-discovered runtime executors. Daemon builds + sets via
|
|
173
217
|
// setExecutors at start(); undefined until then (and in bare tests).
|
|
174
218
|
#executors;
|
|
175
|
-
// {§send-premature-terminate}/SEND
|
|
219
|
+
// {§send-premature-terminate}/scoped SEND signal 202 — park deadlines by loopId, written at dispatch (the
|
|
176
220
|
// marker's seconds; -1 = indefinite), consumed by the daemon's drain park-exit to schedule
|
|
177
221
|
// the deadline wake. In-memory: a daemon restart drops pending deadlines (documented).
|
|
178
222
|
parkDeadlines = new Map();
|
|
@@ -184,9 +228,6 @@ export default class Engine {
|
|
|
184
228
|
#notices;
|
|
185
229
|
#problems;
|
|
186
230
|
#strikes;
|
|
187
|
-
// {§grinder-hard-413-recovery} - loops granted their one over-ceiling recovery turn. Cleared on a
|
|
188
|
-
// fitting turn so a later independent overflow can earn a fresh recovery, and at loop cleanup.
|
|
189
|
-
#hardOverflowRecovery = new Set();
|
|
190
231
|
#packets;
|
|
191
232
|
searchGate = new SearchGate();
|
|
192
233
|
#proposals;
|
|
@@ -335,9 +376,9 @@ export default class Engine {
|
|
|
335
376
|
// parse): warn once per message, never per turn; the turn records railsVerdict "unverifiable".
|
|
336
377
|
static #railGapWarned = new Set();
|
|
337
378
|
static #warnRailVerdictGapOnce(message) {
|
|
338
|
-
if (
|
|
379
|
+
if (_a.#railGapWarned.has(message))
|
|
339
380
|
return;
|
|
340
|
-
|
|
381
|
+
_a.#railGapWarned.add(message);
|
|
341
382
|
process.stderr.write(`plurnk-engine: rail verdict unavailable — the configured grammar did not parse in @plurnk/gbnf (${message})\n`);
|
|
342
383
|
}
|
|
343
384
|
static #requireGrammarEvidence(response) {
|
|
@@ -371,7 +412,8 @@ export default class Engine {
|
|
|
371
412
|
discovery: { registry: emptyRegistry(), handlers: new Map(), skipped: [] },
|
|
372
413
|
});
|
|
373
414
|
// {§tokenomics-agnostic-ruler} — standalone construction and the daemon
|
|
374
|
-
// use the same default; provider counting is confined to
|
|
415
|
+
// use the same default; provider counting is confined to hard
|
|
416
|
+
// context-envelope admission.
|
|
375
417
|
this.#tokenize = tokenize ?? rulerCount;
|
|
376
418
|
const executors = () => this.#executors;
|
|
377
419
|
const loopSignal = (loopId) => this.#loopAborts.get(loopId)?.signal;
|
|
@@ -381,7 +423,6 @@ export default class Engine {
|
|
|
381
423
|
this.#packets = new PacketBuilder({
|
|
382
424
|
db,
|
|
383
425
|
schemes,
|
|
384
|
-
problems: this.#problems,
|
|
385
426
|
executors,
|
|
386
427
|
});
|
|
387
428
|
this.#proposals = new ProposalLifecycle({
|
|
@@ -426,12 +467,16 @@ export default class Engine {
|
|
|
426
467
|
if (this.#executors === undefined)
|
|
427
468
|
throw new Error("registerRuntime: executor registry not wired yet");
|
|
428
469
|
RuntimeTag.assert(tag, "module runtime");
|
|
470
|
+
const normalized = {
|
|
471
|
+
...entry,
|
|
472
|
+
invocation: RuntimeInvocation.assert(entry.invocation, entry.namespaceOwner.name, tag),
|
|
473
|
+
};
|
|
429
474
|
// Preflight both owners before either write; synchronous registration
|
|
430
475
|
// then cannot leave a half-claimed namespace. {§plugin-namespace-arbitration}
|
|
431
|
-
this.#executors.assertCanRegister(tag,
|
|
432
|
-
this.#schemes.assertRuntimeClaim(tag,
|
|
433
|
-
this.#schemes.registerRuntimeScheme(tag,
|
|
434
|
-
this.#executors.register(tag,
|
|
476
|
+
this.#executors.assertCanRegister(tag, normalized.namespaceOwner);
|
|
477
|
+
this.#schemes.assertRuntimeClaim(tag, normalized.namespaceOwner);
|
|
478
|
+
this.#schemes.registerRuntimeScheme(tag, normalized.executor, normalized.namespaceOwner, scheme);
|
|
479
|
+
this.#executors.register(tag, normalized);
|
|
435
480
|
}
|
|
436
481
|
// Supply an explicitly configured local constraint; ANTLR remains the
|
|
437
482
|
// language authority. {§grammar-enforcement-verified-at-boot}
|
|
@@ -455,9 +500,10 @@ export default class Engine {
|
|
|
455
500
|
? variant
|
|
456
501
|
: fileURLToPath(import.meta.resolve(`@plurnk/plurnk-contracts/${variant}`));
|
|
457
502
|
const text = await readFile(path, "utf8"); // unresolvable/unreadable throws — a configured rail never silently degrades
|
|
458
|
-
|
|
503
|
+
const constraint = grammarConstraint(text);
|
|
504
|
+
this.#gbnfCache.set(variant, constraint);
|
|
459
505
|
process.stderr.write(`plurnk-engine: GBNF constraint: ${alias || "(bare)"} → ${variant} (${text.length} chars)\n`);
|
|
460
|
-
return
|
|
506
|
+
return constraint;
|
|
461
507
|
}
|
|
462
508
|
// A lineage's no-parent root; a root worker resolves to itself. Fail hard
|
|
463
509
|
// when corruption leaves a worker without one. {§worker-primary}
|
|
@@ -467,6 +513,16 @@ export default class Engine {
|
|
|
467
513
|
throw new Error(`resolveWorkerPrimary: worker ${workerId} has no lineage root — corrupt parent chain`);
|
|
468
514
|
return root.id;
|
|
469
515
|
}
|
|
516
|
+
async resolveWorkerProviderIdentity(workerId) {
|
|
517
|
+
const identity = await this.#db.engine_worker_provider_identity.get({ worker_id: workerId });
|
|
518
|
+
if (identity === undefined) {
|
|
519
|
+
throw new Error(`resolveWorkerProviderIdentity: worker ${workerId} has no lineage root — corrupt parent chain`);
|
|
520
|
+
}
|
|
521
|
+
return {
|
|
522
|
+
workerId: identity.worker_id,
|
|
523
|
+
primaryWorkerId: identity.primary_worker_id,
|
|
524
|
+
};
|
|
525
|
+
}
|
|
470
526
|
promptBudgetFor(provider) {
|
|
471
527
|
return this.#packets.promptBudgetFor(provider);
|
|
472
528
|
}
|
|
@@ -474,6 +530,109 @@ export default class Engine {
|
|
|
474
530
|
const tags = Meta.composeAttributions(this.#schemes.attributions(context), this.#executors?.attributions(context) ?? [], await this.#mimetypes.attributions(context), provider.attributions?.(context) ?? []);
|
|
475
531
|
return [...tags];
|
|
476
532
|
}
|
|
533
|
+
#providerAttributions(provider, context) {
|
|
534
|
+
return [...Meta.composeAttributions(provider.attributions?.(context) ?? [])];
|
|
535
|
+
}
|
|
536
|
+
static #providerFailure(error, signal) {
|
|
537
|
+
if (error instanceof ProviderError) {
|
|
538
|
+
return { status: error.problem.status, problem: error.problem };
|
|
539
|
+
}
|
|
540
|
+
if (signal?.aborted === true) {
|
|
541
|
+
const timedOut = signal.reason === LOOP_TIMEOUT_REASON;
|
|
542
|
+
return Results.failure("engine:provider", timedOut ? "provider-call-timeout" : "provider-call-cancelled", timedOut ? 504 : 499, timedOut
|
|
543
|
+
? "The provider call was interrupted by the loop deadline."
|
|
544
|
+
: "The provider call was interrupted by loop cancellation.", {}, { stage: "provider-request", retryable: false });
|
|
545
|
+
}
|
|
546
|
+
console.error("Provider failed outside its Problem Details contract:", error);
|
|
547
|
+
return Results.failure("engine:provider", "provider-contract-violation", 502, "The provider failed without returning its required Problem Details.", {}, { stage: "provider-request", retryable: false });
|
|
548
|
+
}
|
|
549
|
+
async #runBareBatch({ statements, provider, turnId, modelCallSequenceStart, workspaceId, workerId, primaryWorkerId, loopSequence, turnSequence, signal, }) {
|
|
550
|
+
const prepared = [];
|
|
551
|
+
for (const [index, statement] of statements.entries()) {
|
|
552
|
+
const providerWorkerId = randomUUID();
|
|
553
|
+
const attributionContext = Object.freeze({
|
|
554
|
+
workspaceId: String(workspaceId),
|
|
555
|
+
workerId: providerWorkerId,
|
|
556
|
+
primaryWorkerId,
|
|
557
|
+
loop: loopSequence,
|
|
558
|
+
turn: turnSequence,
|
|
559
|
+
attempt: 1,
|
|
560
|
+
});
|
|
561
|
+
const attributions = this.#providerAttributions(provider, attributionContext);
|
|
562
|
+
const modelCall = await ModelCall.open(this.#db, {
|
|
563
|
+
turnId,
|
|
564
|
+
sequence: modelCallSequenceStart + index,
|
|
565
|
+
kind: "bare",
|
|
566
|
+
attributions,
|
|
567
|
+
model: provider.model,
|
|
568
|
+
});
|
|
569
|
+
prepared.push({ statement, modelCall, attributions, providerWorkerId });
|
|
570
|
+
}
|
|
571
|
+
const maxTokens = this.#packets.maxTokensFor(provider) ?? undefined;
|
|
572
|
+
const settlements = await Promise.allSettled(prepared.map(async ({ statement, modelCall, attributions, providerWorkerId }) => {
|
|
573
|
+
try {
|
|
574
|
+
const response = await observed("provider.generate", { model: provider.model, attempt: 1, kind: "bare" }, async (span) => {
|
|
575
|
+
try {
|
|
576
|
+
const generated = await provider.generate({
|
|
577
|
+
messages: [{ role: "user", content: statement.body }],
|
|
578
|
+
workerId: providerWorkerId,
|
|
579
|
+
primaryWorkerId,
|
|
580
|
+
signal,
|
|
581
|
+
maxTokens,
|
|
582
|
+
attributions: attributions.length > 0 ? attributions : undefined,
|
|
583
|
+
workspaceId: String(workspaceId),
|
|
584
|
+
loop: loopSequence,
|
|
585
|
+
turn: turnSequence,
|
|
586
|
+
observeRequest: modelCall.observeRequest,
|
|
587
|
+
callKind: "bare",
|
|
588
|
+
});
|
|
589
|
+
modelCall.assertAccounting(generated.accounting);
|
|
590
|
+
recordCounter(PROVIDER_CALLS, {
|
|
591
|
+
model: provider.model,
|
|
592
|
+
attempt: 1,
|
|
593
|
+
status: "resolved",
|
|
594
|
+
});
|
|
595
|
+
span.setAttribute("status", "resolved");
|
|
596
|
+
return generated;
|
|
597
|
+
}
|
|
598
|
+
catch (error) {
|
|
599
|
+
if (error instanceof ProviderError) {
|
|
600
|
+
modelCall.assertAccounting(error.accounting);
|
|
601
|
+
}
|
|
602
|
+
throw error;
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
await modelCall.observeResponse(response);
|
|
606
|
+
return {
|
|
607
|
+
statement,
|
|
608
|
+
modelCallId: modelCall.id,
|
|
609
|
+
result: Results.assert({
|
|
610
|
+
status: 200,
|
|
611
|
+
content: response.assistant.content,
|
|
612
|
+
mimetype: "text/plain",
|
|
613
|
+
}),
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
catch (error) {
|
|
617
|
+
if (error instanceof ModelCallPersistenceError || error instanceof ProviderAccountingIntegrityError) {
|
|
618
|
+
throw error;
|
|
619
|
+
}
|
|
620
|
+
const failure = _a.#providerFailure(error, signal);
|
|
621
|
+
if (error instanceof ProviderError && error.attempt !== undefined) {
|
|
622
|
+
await modelCall.observeResponse(error.attempt, failure);
|
|
623
|
+
}
|
|
624
|
+
else {
|
|
625
|
+
await modelCall.fail(failure);
|
|
626
|
+
}
|
|
627
|
+
return { statement, modelCallId: modelCall.id, result: failure };
|
|
628
|
+
}
|
|
629
|
+
}));
|
|
630
|
+
const internalFailure = settlements.find((settlement) => settlement.status === "rejected");
|
|
631
|
+
if (internalFailure !== undefined)
|
|
632
|
+
throw internalFailure.reason;
|
|
633
|
+
signal?.throwIfAborted();
|
|
634
|
+
return settlements.map((settlement) => settlement.value);
|
|
635
|
+
}
|
|
477
636
|
// {§attribution} — reporting derives from exact provider-request evidence;
|
|
478
637
|
// malformed durable tags fail here instead of being silently filtered.
|
|
479
638
|
async loopAttributions(loopId) {
|
|
@@ -486,29 +645,22 @@ export default class Engine {
|
|
|
486
645
|
});
|
|
487
646
|
return [...Meta.composeAttributions(tags)];
|
|
488
647
|
}
|
|
489
|
-
//
|
|
490
|
-
// occupancy gauge. {§tokenomics-client-gauge},
|
|
648
|
+
// The request ledger is cardinal evidence; its aggregate and the latest-turn
|
|
649
|
+
// occupancy gauge are derived projections. {§tokenomics-client-gauge},
|
|
650
|
+
// {§notifications-loop-terminated}, {§provider-accounting}
|
|
491
651
|
async loopUsage(loopId) {
|
|
492
652
|
const row = await this.#db.engine_loop_usage.get({ loop_id: loopId });
|
|
493
653
|
if (row === undefined)
|
|
494
654
|
throw new Error(`loopUsage: loop ${loopId} does not exist`);
|
|
495
|
-
const
|
|
496
|
-
if (!Array.isArray(parsedCosts))
|
|
497
|
-
throw new TypeError(`loop ${loopId} monetary evidence is not an array`);
|
|
498
|
-
const costs = parsedCosts.map((cost) => validateProviderCost(cost));
|
|
655
|
+
const requests = await this.#db.engine_loop_provider_requests.all({ loop_id: loopId });
|
|
499
656
|
return {
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
cachedTokens: row?.cached ?? 0,
|
|
504
|
-
costUsd: row?.cost_usd ?? null,
|
|
505
|
-
costs,
|
|
506
|
-
// Latest provider attempt on the latest turn, not the billed total.
|
|
507
|
-
contextTokens: row?.context ?? 0,
|
|
657
|
+
accounting: aggregateProviderAccounting(requests.map(providerRequestFromStorageRow)),
|
|
658
|
+
// Latest physical request on the latest turn, not the billed total.
|
|
659
|
+
contextTokens: row.context,
|
|
508
660
|
// Latest effective packet allowance; null when uncapped or unknown.
|
|
509
|
-
promptBudget: row
|
|
661
|
+
promptBudget: row.context_size,
|
|
510
662
|
// Latest turn's opaque provider metadata. {§meta-passthrough}
|
|
511
|
-
meta: JSON.parse(row
|
|
663
|
+
meta: JSON.parse(row.meta ?? "{}"),
|
|
512
664
|
};
|
|
513
665
|
}
|
|
514
666
|
// A mapped rail divergence is a CODE-POINT offset into the model's content;
|
|
@@ -531,7 +683,7 @@ export default class Engine {
|
|
|
531
683
|
}
|
|
532
684
|
return { line, column };
|
|
533
685
|
}
|
|
534
|
-
async runLoop({ provider, messages, requirements = "", workspaceId, workerId, loopId, maxTurns = 50, maxStrikes = readMaxStrikes(), minCycles = readPositiveInt("PLURNK_SERVICE_MIN_CYCLES", DEFAULT_MIN_CYCLES), maxCyclePeriod = readPositiveInt("PLURNK_SERVICE_MAX_CYCLE_PERIOD", DEFAULT_MAX_CYCLE_PERIOD), origin = "model", signal, onDispatch, }) {
|
|
686
|
+
async runLoop({ provider, childProvider = provider, messages, requirements = "", workspaceId, workerId, loopId, maxTurns = 50, maxStrikes = readMaxStrikes(), minCycles = readPositiveInt("PLURNK_SERVICE_MIN_CYCLES", DEFAULT_MIN_CYCLES), maxCyclePeriod = readPositiveInt("PLURNK_SERVICE_MAX_CYCLE_PERIOD", DEFAULT_MAX_CYCLE_PERIOD), origin = "model", signal, onDispatch, }) {
|
|
535
687
|
// A 202 park suspends this durable loop and a later wake re-enters runLoop.
|
|
536
688
|
// Its ceiling therefore counts every prior turn, not merely this process-local
|
|
537
689
|
// execution segment.
|
|
@@ -567,11 +719,11 @@ export default class Engine {
|
|
|
567
719
|
return { turnIds, result, hitMaxTurns: false, reason: "loop_timeout" };
|
|
568
720
|
};
|
|
569
721
|
// Cleanup splits by termination kind:
|
|
570
|
-
// - "graceful" (SEND
|
|
722
|
+
// - "graceful" (SEND signal 202 Accepted): in-flight streaming-scheme spawns
|
|
571
723
|
// are ALLOWED to outlive the loop — they complete naturally, write final
|
|
572
724
|
// channel state, and wake-on-completion (E.4) opens a fresh loop. 202 is
|
|
573
725
|
// the only terminal that means "keep my async work."
|
|
574
|
-
// - "forceful" (SEND
|
|
726
|
+
// - "forceful" (SEND signal 200 done, max_turns, strike, cancel, context-envelope rejection, 4xx/5xx):
|
|
575
727
|
// fire the loop-level abort so leftover spawns tear down. "Done" reaps.
|
|
576
728
|
const cleanup = (kind, reason) => {
|
|
577
729
|
clearTimeout(wall);
|
|
@@ -581,7 +733,6 @@ export default class Engine {
|
|
|
581
733
|
this.#loopAborts.delete(loopId);
|
|
582
734
|
this.#strikes.delete(loopId);
|
|
583
735
|
this.searchGate.cleanup(loopId);
|
|
584
|
-
this.#hardOverflowRecovery.delete(loopId);
|
|
585
736
|
this.#notices.delete(loopId);
|
|
586
737
|
};
|
|
587
738
|
while (true) {
|
|
@@ -649,7 +800,7 @@ export default class Engine {
|
|
|
649
800
|
turn = await observed(// {§observability-boundary}
|
|
650
801
|
"loop.turn", { workerId, "loop.id": loopId }, async (span) => {
|
|
651
802
|
const t = await this.runTurn({
|
|
652
|
-
provider, messages, requirements, workspaceId, workerId, loopId, origin, signal, onDispatch,
|
|
803
|
+
provider, childProvider, messages, requirements, workspaceId, workerId, loopId, origin, signal, onDispatch,
|
|
653
804
|
turnNumber: turnIds.length + 1, maxTurns,
|
|
654
805
|
invalidEmissionRecoveryEntryId,
|
|
655
806
|
});
|
|
@@ -698,25 +849,23 @@ export default class Engine {
|
|
|
698
849
|
return { turnIds, result, hitMaxTurns: false, reason: "invalid_emission" };
|
|
699
850
|
}
|
|
700
851
|
invalidEmissionRecoveryEntryId = null;
|
|
701
|
-
// SPEC {§grinder}:
|
|
852
|
+
// SPEC {§grinder}: the effective context envelope rejected the request.
|
|
702
853
|
if (turn.budgetHardStop) {
|
|
703
|
-
if (turn.
|
|
704
|
-
throw new Error("a
|
|
854
|
+
if (turn.budgetFailure === undefined) {
|
|
855
|
+
throw new Error("a context-envelope hard-stop requires its exact failure");
|
|
705
856
|
}
|
|
706
|
-
const
|
|
707
|
-
const result = await this.#lifecycle.finish(loopId, failure);
|
|
857
|
+
const result = await this.#lifecycle.finish(loopId, turn.budgetFailure);
|
|
708
858
|
if (result === null)
|
|
709
859
|
throw new Error(`loop ${loopId} became terminal before budget settlement`);
|
|
710
|
-
cleanup("forceful", "
|
|
711
|
-
return { turnIds, result, hitMaxTurns: false, reason: "
|
|
860
|
+
cleanup("forceful", "context_envelope");
|
|
861
|
+
return { turnIds, result, hitMaxTurns: false, reason: "context_envelope" };
|
|
712
862
|
}
|
|
713
|
-
// {§engine-rails} — per-turn strike accounting (cycle detection,
|
|
714
|
-
//
|
|
863
|
+
// {§engine-rails} — per-turn strike accounting (cycle detection,
|
|
864
|
+
// steer coupling, hard operation outcomes). StrikeRail owns the
|
|
715
865
|
// bookkeeping; runLoop owns abandonment.
|
|
716
866
|
const verdict = this.#strikes.assess(loopId, {
|
|
717
867
|
fingerprint: turn.fingerprint,
|
|
718
868
|
outcomes: turn.outcomes,
|
|
719
|
-
budgetStruck: turn.budgetStruck,
|
|
720
869
|
steerStruck: turn.steerStruck,
|
|
721
870
|
minCycles, maxCyclePeriod, maxStrikes,
|
|
722
871
|
});
|
|
@@ -739,7 +888,7 @@ export default class Engine {
|
|
|
739
888
|
}
|
|
740
889
|
}
|
|
741
890
|
}
|
|
742
|
-
async runTurn({ provider, messages, requirements = "", workspaceId, workerId, loopId, origin = "model", signal, onDispatch, turnNumber = 1, maxTurns = 50, invalidEmissionRecoveryEntryId, }) {
|
|
891
|
+
async runTurn({ provider, childProvider = provider, messages, requirements = "", workspaceId, workerId, loopId, origin = "model", signal, onDispatch, turnNumber = 1, maxTurns = 50, invalidEmissionRecoveryEntryId, }) {
|
|
743
892
|
const allowInvalidEmissionRecovery = invalidEmissionRecoveryEntryId === null;
|
|
744
893
|
const transientOpenLogEntryId = typeof invalidEmissionRecoveryEntryId === "number"
|
|
745
894
|
? invalidEmissionRecoveryEntryId
|
|
@@ -800,14 +949,14 @@ export default class Engine {
|
|
|
800
949
|
// Model ops dispatch after these pre-model rows.
|
|
801
950
|
let nextActionIndex = 1;
|
|
802
951
|
const turnOpenPaths = [];
|
|
803
|
-
// {§
|
|
804
|
-
// worked turn PLAN → the environment FINDs the foist ACTUALLY dispatches → SEND
|
|
952
|
+
// {§worker-initialization-entry} — the worker's first turn opens with a kernel-authored initialization: a
|
|
953
|
+
// worked turn PLAN → the environment FINDs the foist ACTUALLY dispatches → SEND signal 102. Built
|
|
805
954
|
// from the real ops below (not a static print — we lean into the genuine echo paradigm) and
|
|
806
|
-
// written at sequence 1, so it reads first
|
|
955
|
+
// written at sequence 1, so it reads first with the foisted results following.
|
|
807
956
|
const turnZeroMoves = [];
|
|
808
957
|
if (seq === 1) {
|
|
809
958
|
if (workerFirstLoop)
|
|
810
|
-
nextActionIndex = 2; // reserve sequence 1 for
|
|
959
|
+
nextActionIndex = 2; // reserve sequence 1 for initialization
|
|
811
960
|
// Operator doc READs (PLURNK_SERVICE_MD_<ALIAS>, {§actor-boundary-doc-injection}). The docs were materialized
|
|
812
961
|
// as worker://plurnk/<entry> entries by the plurnk worker (LoopDocs, via the
|
|
813
962
|
// {§actor-boundary} keystone); foist a READ of each into THIS turn-0 so the model
|
|
@@ -840,7 +989,6 @@ export default class Engine {
|
|
|
840
989
|
const promptPath = promptTarget(promptLoopSeq, seq);
|
|
841
990
|
const entry = {
|
|
842
991
|
channels: { body: { content: promptRow.prompt, mimetype: "text/markdown" } },
|
|
843
|
-
tags: [],
|
|
844
992
|
attributes: { openPaths },
|
|
845
993
|
};
|
|
846
994
|
await EntryCrud.writeEntry(promptPath.pathname, entry, systemCtx, "prompt", workerId);
|
|
@@ -921,97 +1069,71 @@ export default class Engine {
|
|
|
921
1069
|
// consume the filesystem divergences a second time.
|
|
922
1070
|
await this.#queueWorkspaceWarm(systemCtx, true, false);
|
|
923
1071
|
// Turn-0 catalog preview (PLURNK_SERVICE_FILES_ITEMS, {§actor-boundary-catalog-preview}):
|
|
924
|
-
// FIND surveys foisted into the worker's first model turn
|
|
925
|
-
//
|
|
926
|
-
//
|
|
927
|
-
// opening packet demonstrates both navigation forms. Empty results are orientation.
|
|
1072
|
+
// Four FIND surveys foisted into the worker's first model turn establish the project,
|
|
1073
|
+
// commons, private, and documentation surfaces in that order. Their `init`
|
|
1074
|
+
// classification lets the model curate this opening survey as one log set.
|
|
928
1075
|
if (seq === 1) {
|
|
929
1076
|
// {§operator-config-workspace-files-items} — workspace filesItems replaces the env default.
|
|
930
1077
|
const { filesItems: workspaceMI } = await WorkspaceSettings.read(this.#db, workspaceId);
|
|
931
1078
|
const filesItems = workspaceMI !== null ? normalizeFilesItems(workspaceMI) : readFilesItems();
|
|
932
1079
|
if (filesItems !== null && workerFirstLoop) { // {§actor-boundary-catalog-preview} — once per worker
|
|
933
|
-
// engine_scheme_catalog_summary is the workspace-bounded scheme source: ordered,
|
|
934
|
-
// one row per stored entry scheme. log:// is absent —
|
|
935
|
-
// it lives in log_entries, not the catalog (present-mode, the # Log section).
|
|
936
1080
|
const catalogSchemes = await this.#db.engine_scheme_catalog_summary.all({ workspace_id: workspaceId });
|
|
937
|
-
|
|
938
|
-
const
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
foistSchemes.push({ scheme: "file", shallow_items: 0 }); // Empty project surface still receives its orienting FIND.
|
|
949
|
-
for (const { scheme, shallow_items: shallowItems } of foistSchemes) {
|
|
950
|
-
const isFile = scheme === "file";
|
|
951
|
-
const pattern = this.#schemes.manifestFor(scheme)?.folderScopes === true ? "*" : "**";
|
|
952
|
-
// Only the file map takes PLURNK_SERVICE_FILES_ITEMS as an explicit first-N
|
|
953
|
-
// cap. Every other ordinary survey uses FIND's markerless first page.
|
|
954
|
-
const cap = isFile && filesItems > 0 && shallowItems > 0 ? Math.min(filesItems, shallowItems) : null;
|
|
955
|
-
const catalogMarker = cap === null ? null : { marks: [1, cap] };
|
|
956
|
-
// The file survey foists as the BARE relative glob — the path shape plurnk.md
|
|
957
|
-
// teaches (`*`, `src/**`, `**/notes.md`; bare = project-relative) — so the turn-0
|
|
958
|
-
// exemplar and the log rows the model reads never train a leading-slash or
|
|
959
|
-
// file:/// habit the rest of the teaching contradicts.
|
|
960
|
-
const catalogFind = {
|
|
961
|
-
op: "FIND", suffix: "", signal: null,
|
|
962
|
-
target: isFile ? { kind: "local", raw: pattern } : {
|
|
963
|
-
kind: "url",
|
|
964
|
-
raw: `${scheme}:///${pattern}`,
|
|
965
|
-
scheme,
|
|
966
|
-
username: null, password: null, hostname: null, port: null,
|
|
967
|
-
pathname: `/${pattern}`,
|
|
968
|
-
query: null, fragment: null,
|
|
1081
|
+
const fileItems = catalogSchemes.find(({ scheme }) => scheme === "file")?.shallow_items ?? 0;
|
|
1082
|
+
const fileCap = filesItems > 0 && fileItems > 0 ? Math.min(filesItems, fileItems) : null;
|
|
1083
|
+
await Owner.kernelId(this.#db, workspaceId);
|
|
1084
|
+
const surveys = [
|
|
1085
|
+
{
|
|
1086
|
+
statement: {
|
|
1087
|
+
op: "FIND", suffix: "", signal: ["+init"],
|
|
1088
|
+
target: { kind: "local", raw: "*" },
|
|
1089
|
+
body: null,
|
|
1090
|
+
lineMarker: fileCap === null ? null : { marks: [1, fileCap] },
|
|
1091
|
+
position: UNKNOWN_POSITION,
|
|
969
1092
|
},
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
1093
|
+
exemplar: `## FIND0 [+init] (*)${fileCap === null ? "" : ` <1,${fileCap}>`}`,
|
|
1094
|
+
},
|
|
1095
|
+
{
|
|
1096
|
+
statement: {
|
|
1097
|
+
op: "FIND", suffix: "", signal: ["+init"],
|
|
1098
|
+
target: { kind: "url", raw: "worker:///*", scheme: "worker", username: null, password: null, hostname: null, port: null, pathname: "/*", query: null, fragment: null },
|
|
1099
|
+
body: null, lineMarker: null, position: UNKNOWN_POSITION,
|
|
1100
|
+
},
|
|
1101
|
+
exemplar: "## FIND0 [+init] (worker:///*)",
|
|
1102
|
+
},
|
|
1103
|
+
{
|
|
1104
|
+
statement: {
|
|
1105
|
+
op: "FIND", suffix: "", signal: ["+init"],
|
|
1106
|
+
target: { kind: "url", raw: "worker://~/*", scheme: "worker", username: null, password: null, hostname: "~", port: null, pathname: "/*", query: null, fragment: null },
|
|
1107
|
+
body: null, lineMarker: null, position: UNKNOWN_POSITION,
|
|
1108
|
+
},
|
|
1109
|
+
exemplar: "## FIND0 [+init] (worker://~/*)",
|
|
1110
|
+
},
|
|
1111
|
+
{
|
|
1112
|
+
statement: {
|
|
1113
|
+
op: "FIND", suffix: "", signal: ["+init", "+docs"],
|
|
1114
|
+
target: { kind: "url", raw: "worker://plurnk/docs/**", scheme: "worker", username: null, password: null, hostname: "plurnk", port: null, pathname: "/docs/**", query: null, fragment: null },
|
|
1115
|
+
body: null, lineMarker: { marks: [1, -1] }, position: UNKNOWN_POSITION,
|
|
1116
|
+
},
|
|
1117
|
+
exemplar: "## FIND0 [+init,+docs] (worker://plurnk/docs/**) <1,-1>",
|
|
1118
|
+
},
|
|
1119
|
+
];
|
|
1120
|
+
for (const { statement, exemplar } of surveys) {
|
|
1121
|
+
await this.dispatch({ statement, workspaceId, workerId, loopId, turnId, sequence: nextActionIndex, origin: "plurnk", onDispatch });
|
|
978
1122
|
nextActionIndex++;
|
|
979
|
-
|
|
980
|
-
// (the model's own survey, mirrored OPEN). An explicit file cap rides as
|
|
981
|
-
// `<1,N>`; ordinary surveys teach the safe markerless default by example.
|
|
982
|
-
const target = isFile ? pattern : `${scheme}:///${pattern}`;
|
|
983
|
-
turnZeroMoves.push(`<<FIND(${target})${cap === null ? "" : `<1,${cap}>`}::FIND`);
|
|
1123
|
+
turnZeroMoves.push(exemplar);
|
|
984
1124
|
}
|
|
985
|
-
// The kernel's self-documenting surface — FIND(worker://plurnk/docs/**), uncapped,
|
|
986
|
-
// always ({§schemes-directory}, published under {§entry-owner}).
|
|
987
|
-
await Owner.kernelId(this.#db, workspaceId); // the row exists even before docs materialize — the empty survey is orienting, never 404
|
|
988
|
-
const kernelDocsFind = {
|
|
989
|
-
op: "FIND", suffix: "", signal: null,
|
|
990
|
-
target: { kind: "url", raw: "worker://plurnk/docs/**", scheme: "worker", username: null, password: null, hostname: "plurnk", port: null, pathname: "/docs/**", query: null, fragment: null },
|
|
991
|
-
body: null, lineMarker: { marks: [1, -1] }, position: UNKNOWN_POSITION,
|
|
992
|
-
};
|
|
993
|
-
await this.dispatch({ statement: kernelDocsFind, workspaceId, workerId, loopId, turnId, sequence: nextActionIndex, origin: "plurnk", onDispatch });
|
|
994
|
-
nextActionIndex++;
|
|
995
|
-
turnZeroMoves.push("<<FIND(worker://plurnk/docs/**)<1,-1>::FIND");
|
|
996
|
-
// {§worker-scheme} — the building worker's own scratch gets the same markerless
|
|
997
|
-
// one-level survey in its perspective alone. It always executes: an empty private
|
|
998
|
-
// space is useful orientation, not grounds to hide the surface.
|
|
999
|
-
const ownFind = {
|
|
1000
|
-
op: "FIND", suffix: "", signal: null,
|
|
1001
|
-
target: { kind: "url", raw: "worker://~/*", scheme: "worker", username: null, password: null, hostname: "~", port: null, pathname: "/*", query: null, fragment: null },
|
|
1002
|
-
body: null, lineMarker: null, position: UNKNOWN_POSITION,
|
|
1003
|
-
};
|
|
1004
|
-
await this.dispatch({ statement: ownFind, workspaceId, workerId, loopId, turnId, sequence: nextActionIndex, origin: "plurnk", onDispatch });
|
|
1005
|
-
nextActionIndex++;
|
|
1006
|
-
turnZeroMoves.push("<<FIND(worker://~/*)::FIND"); // {§model-entry} — the own-space survey, into the turn-0 echo
|
|
1007
1125
|
}
|
|
1008
|
-
// {§
|
|
1009
|
-
// foisted above (real, their results already in the log) → SEND
|
|
1126
|
+
// {§worker-initialization-entry} — write the kernel's turn-0 initialization OPEN at sequence 1: PLAN → the FINDs actually
|
|
1127
|
+
// foisted above (real, their results already in the log) → SEND signal 102. Dynamic — it reflects
|
|
1010
1128
|
// the true survey, never a frozen print — and OPEN: the worked example the model orients on,
|
|
1011
|
-
// so the grammar can stay thin.
|
|
1129
|
+
// so the grammar can stay thin.
|
|
1012
1130
|
if (workerFirstLoop) {
|
|
1013
|
-
const
|
|
1014
|
-
|
|
1131
|
+
const initialization = [
|
|
1132
|
+
"# PLAN0\n* Initialization complete.\n* Next: address the prompt.",
|
|
1133
|
+
...turnZeroMoves,
|
|
1134
|
+
"## SEND0 [102]\nNext, address the prompt.",
|
|
1135
|
+
].join("\n\n");
|
|
1136
|
+
await this.#dispatcher.writeInitializationEntry({ verbatim: initialization, workerId, loopId, turnId, sequence: 1 });
|
|
1015
1137
|
}
|
|
1016
1138
|
}
|
|
1017
1139
|
// {§methods-loop-run-open-paths}: selected workspace paths belong to
|
|
@@ -1058,83 +1180,50 @@ export default class Engine {
|
|
|
1058
1180
|
});
|
|
1059
1181
|
// SPEC {§grinder} — budget grinder, pre-LLM: reclaim window on actual overflow.
|
|
1060
1182
|
const enforced = await this.#packets.enforceBudget({
|
|
1061
|
-
packet: requestPacket, provider,
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1183
|
+
packet: requestPacket, provider, loopId, turnId,
|
|
1184
|
+
recordOverflow: async (pressure) => {
|
|
1185
|
+
await this.#problems.record({
|
|
1186
|
+
workerId,
|
|
1187
|
+
loopId,
|
|
1188
|
+
turnId,
|
|
1189
|
+
sequence: nextActionIndex++,
|
|
1190
|
+
origin: "plurnk",
|
|
1191
|
+
source: "engine",
|
|
1192
|
+
result: tokenBudgetOverflowFailure(pressure),
|
|
1193
|
+
});
|
|
1194
|
+
},
|
|
1067
1195
|
rebuild: () => this.#packets.buildRequestPacket({
|
|
1068
1196
|
initialMessages: messages, requirements, workspaceId, workerId, loopId,
|
|
1069
1197
|
currentTurnSeq: seq, provider, gitStatus, notices, transientOpenLogEntryId,
|
|
1070
1198
|
}),
|
|
1071
1199
|
});
|
|
1072
|
-
if (enforced.recorded)
|
|
1073
|
-
nextActionIndex += 1; // a fold-to-fit Problem consumed the reserved sequence
|
|
1074
1200
|
requestPacket = enforced.packet;
|
|
1075
1201
|
if (!enforced.fit) {
|
|
1076
|
-
// {§
|
|
1077
|
-
//
|
|
1078
|
-
//
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
if (physicalAdmission.admitted && !this.#hardOverflowRecovery.has(loopId)) {
|
|
1202
|
+
// {§tokenomics-context-envelope-admission}: the overflow Problem is
|
|
1203
|
+
// already durable. Remaining ruler debt may still be admitted; only
|
|
1204
|
+
// the effective total context envelope can terminally reject the turn.
|
|
1205
|
+
const contextAdmission = await this.#packets.contextEnvelopeAdmission(requestPacket, provider, this.#loopAborts.get(loopId)?.signal);
|
|
1206
|
+
if (!contextAdmission.admitted) {
|
|
1082
1207
|
const ceiling = this.#packets.ceilingFor(provider);
|
|
1083
1208
|
if (ceiling === null) {
|
|
1084
|
-
throw new Error("an unbounded prompt budget cannot enter
|
|
1209
|
+
throw new Error("an unbounded prompt budget cannot enter context-envelope admission");
|
|
1085
1210
|
}
|
|
1211
|
+
const failure = contextEnvelopeFailure(budgetPressure(requestPacket.tokens, ceiling), contextAdmission);
|
|
1086
1212
|
await this.#problems.record({
|
|
1087
1213
|
workerId,
|
|
1088
1214
|
loopId,
|
|
1089
1215
|
turnId,
|
|
1090
1216
|
sequence: nextActionIndex++,
|
|
1091
|
-
origin: "
|
|
1217
|
+
origin: "plurnk",
|
|
1092
1218
|
source: "engine",
|
|
1093
|
-
result:
|
|
1219
|
+
result: failure,
|
|
1094
1220
|
});
|
|
1095
|
-
//
|
|
1096
|
-
//
|
|
1221
|
+
// Preserve the exact terminal evidence in the stored request. It is
|
|
1222
|
+
// never sent because admission has already failed.
|
|
1097
1223
|
requestPacket = await this.#packets.buildRequestPacket({
|
|
1098
1224
|
initialMessages: messages, requirements, workspaceId, workerId, loopId,
|
|
1099
1225
|
currentTurnSeq: seq, provider, gitStatus, notices, transientOpenLogEntryId,
|
|
1100
1226
|
});
|
|
1101
|
-
physicalAdmission = await this.#packets.physicalAdmission(requestPacket, provider, this.#loopAborts.get(loopId)?.signal);
|
|
1102
|
-
if (physicalAdmission.admitted) {
|
|
1103
|
-
this.#hardOverflowRecovery.add(loopId);
|
|
1104
|
-
recoveryAdmitted = true;
|
|
1105
|
-
}
|
|
1106
|
-
}
|
|
1107
|
-
if (!recoveryAdmitted) {
|
|
1108
|
-
// Hard 413: physically unsendable, or still over after the informed recovery turn.
|
|
1109
|
-
const ceiling = this.#packets.ceilingFor(provider);
|
|
1110
|
-
if (ceiling === null) {
|
|
1111
|
-
throw new Error("an unbounded prompt budget cannot hard-stop");
|
|
1112
|
-
}
|
|
1113
|
-
if (!enforced.recorded || !physicalAdmission.admitted) {
|
|
1114
|
-
await this.#problems.record({
|
|
1115
|
-
workerId,
|
|
1116
|
-
loopId,
|
|
1117
|
-
turnId,
|
|
1118
|
-
sequence: nextActionIndex++,
|
|
1119
|
-
origin: "plurnk",
|
|
1120
|
-
source: "engine",
|
|
1121
|
-
result: BudgetOverflow.result(requestPacket.tokens, ceiling, false, physicalAdmission.admitted
|
|
1122
|
-
? undefined
|
|
1123
|
-
: {
|
|
1124
|
-
reason: physicalAdmission.reason,
|
|
1125
|
-
detail: physicalAdmission.detail,
|
|
1126
|
-
capacity: physicalAdmission.capacity,
|
|
1127
|
-
tokens: physicalAdmission.measurement?.tokens,
|
|
1128
|
-
tokenKind: physicalAdmission.measurement?.kind,
|
|
1129
|
-
tokenSource: physicalAdmission.measurement?.source,
|
|
1130
|
-
}),
|
|
1131
|
-
});
|
|
1132
|
-
requestPacket = await this.#packets.buildRequestPacket({
|
|
1133
|
-
initialMessages: messages, requirements, workspaceId, workerId, loopId,
|
|
1134
|
-
currentTurnSeq: seq, provider, gitStatus, notices, transientOpenLogEntryId,
|
|
1135
|
-
});
|
|
1136
|
-
}
|
|
1137
|
-
// Skip the LLM, close the turn, and let runLoop abandon.
|
|
1138
1227
|
await this.#db.engine_close_turn.run({
|
|
1139
1228
|
id: turnId, status: 413, packet: StoredPacket.stringify(requestPacket),
|
|
1140
1229
|
// The attempted turn retains its effective allowance even
|
|
@@ -1147,20 +1236,14 @@ export default class Engine {
|
|
|
1147
1236
|
status: 413,
|
|
1148
1237
|
outcomes: [],
|
|
1149
1238
|
fingerprint: "",
|
|
1150
|
-
budgetStruck: enforced.struck,
|
|
1151
1239
|
budgetHardStop: true,
|
|
1152
1240
|
steerStruck: false,
|
|
1153
1241
|
emissionAttempts: 0,
|
|
1154
1242
|
emissionExhausted: false,
|
|
1155
|
-
|
|
1243
|
+
budgetFailure: failure,
|
|
1156
1244
|
};
|
|
1157
1245
|
}
|
|
1158
1246
|
}
|
|
1159
|
-
else {
|
|
1160
|
-
// A fitting turn clears the recovery grant; a later independent overflow can earn
|
|
1161
|
-
// a fresh recovery turn (chronic overflow still strikes out via the rail).
|
|
1162
|
-
this.#hardOverflowRecovery.delete(loopId);
|
|
1163
|
-
}
|
|
1164
1247
|
const modelMessages = PacketWire.packetToWireMessages(requestPacket);
|
|
1165
1248
|
// Packet pressure and provider generation are independent. The grinder governs
|
|
1166
1249
|
// only the request packet; maxTokens comes only from the provider envelope and
|
|
@@ -1168,53 +1251,38 @@ export default class Engine {
|
|
|
1168
1251
|
let response;
|
|
1169
1252
|
let splitResponse;
|
|
1170
1253
|
let railGrammar;
|
|
1254
|
+
let railResponseGrammar;
|
|
1171
1255
|
let railEvidence;
|
|
1172
1256
|
let emissionAttempts = 0;
|
|
1173
1257
|
let providerCallInFlight = false;
|
|
1174
1258
|
let providerAttemptSequence = 0;
|
|
1175
1259
|
let providerAttemptId = null;
|
|
1260
|
+
let providerModelCall = null;
|
|
1176
1261
|
let providerAttemptAttributions = [];
|
|
1177
1262
|
const providerSignal = this.#loopAborts.get(loopId)?.signal ?? signal;
|
|
1178
1263
|
// {§client-metadata}
|
|
1179
1264
|
const { client } = await WorkspaceSettings.read(this.#db, workspaceId);
|
|
1180
|
-
const
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
usage_prompt: attemptUsage.prompt,
|
|
1186
|
-
usage_completion: attemptUsage.completion,
|
|
1187
|
-
usage_reasoning: attemptUsage.reasoning,
|
|
1188
|
-
usage_cached: attemptUsage.cached,
|
|
1189
|
-
usage_cost: JSON.stringify({
|
|
1190
|
-
kind: "unknown",
|
|
1191
|
-
reason: "provider response retained before monetary classification",
|
|
1192
|
-
}),
|
|
1193
|
-
finish_reason: attemptResponse.assistant.finishReason,
|
|
1194
|
-
model: attemptResponse.assistant.model,
|
|
1195
|
-
});
|
|
1196
|
-
};
|
|
1197
|
-
const classifyProviderAttempt = async (id, attemptResponse, attemptSplit, sequence, accepted, failure = null) => {
|
|
1198
|
-
const attemptUsage = attemptSplit.callMetadata.usage;
|
|
1199
|
-
const attemptCost = providerCostFor(provider, attemptUsage, attemptResponse.charge);
|
|
1200
|
-
const attemptCostUsd = providerCostUsd(attemptCost);
|
|
1201
|
-
await this.#db.engine_classify_turn_attempt_response.run({
|
|
1265
|
+
const loopSeq = (await this.#db.engine_loop_sequence.get({ loop_id: loopId }))?.sequence ?? loopId;
|
|
1266
|
+
const providerIdentity = await this.resolveWorkerProviderIdentity(workerId);
|
|
1267
|
+
const { workerId: providerWorkerId, primaryWorkerId } = providerIdentity;
|
|
1268
|
+
const classifyProviderAttempt = async (id, attemptSplit, sequence, accepted) => {
|
|
1269
|
+
const result = await this.#db.engine_classify_turn_attempt_response.run({
|
|
1202
1270
|
id,
|
|
1203
1271
|
accepted: accepted ? 1 : 0,
|
|
1204
1272
|
parse_errors: JSON.stringify(attemptSplit.parseErrors),
|
|
1205
|
-
failure: failure === null ? null : JSON.stringify(failure),
|
|
1206
|
-
usage_cost: JSON.stringify(attemptCost),
|
|
1207
|
-
usage_cost_usd: attemptCostUsd,
|
|
1208
1273
|
});
|
|
1274
|
+
if (result.changes !== 1) {
|
|
1275
|
+
throw new Error(`emission attempt ${id} was not awaiting classification`);
|
|
1276
|
+
}
|
|
1209
1277
|
emissionAttempts = sequence;
|
|
1210
1278
|
};
|
|
1211
1279
|
try {
|
|
1212
1280
|
// {§turn-lifecycle}: bracket the complete provider-attempt window with liveness notices.
|
|
1213
1281
|
if (!signal?.aborted)
|
|
1214
1282
|
this.#notices.push(workspaceId, loopId, { source: "engine:turn", kind: "turn_awaiting_model", level: "info", message: "awaiting model response" });
|
|
1215
|
-
const
|
|
1216
|
-
railGrammar =
|
|
1217
|
-
|
|
1283
|
+
const railConstraint = await this.#grammarConstraint(provider);
|
|
1284
|
+
railGrammar = railConstraint?.transport;
|
|
1285
|
+
railResponseGrammar = railConstraint?.response;
|
|
1218
1286
|
const attemptLimit = readEmissionAttempts();
|
|
1219
1287
|
const maxTokens = this.#packets.maxTokensFor(provider) ?? undefined;
|
|
1220
1288
|
const strikeStreak = this.#strikes.streak(loopId);
|
|
@@ -1226,7 +1294,7 @@ export default class Engine {
|
|
|
1226
1294
|
providerAttemptSequence = attempt;
|
|
1227
1295
|
const attributionContext = Object.freeze({
|
|
1228
1296
|
workspaceId: String(workspaceId),
|
|
1229
|
-
workerId:
|
|
1297
|
+
workerId: providerWorkerId,
|
|
1230
1298
|
primaryWorkerId,
|
|
1231
1299
|
loop: loopSeq,
|
|
1232
1300
|
turn: seq,
|
|
@@ -1234,51 +1302,67 @@ export default class Engine {
|
|
|
1234
1302
|
});
|
|
1235
1303
|
providerAttemptAttributions = await this.#attemptAttributions(provider, attributionContext);
|
|
1236
1304
|
requestPacket = { ...requestPacket, attributions: providerAttemptAttributions };
|
|
1237
|
-
|
|
1238
|
-
|
|
1305
|
+
providerModelCall = await ModelCall.open(this.#db, {
|
|
1306
|
+
turnId,
|
|
1239
1307
|
sequence: attempt,
|
|
1240
|
-
|
|
1308
|
+
kind: "emission",
|
|
1309
|
+
attributions: providerAttemptAttributions,
|
|
1241
1310
|
model: provider.model,
|
|
1242
1311
|
});
|
|
1312
|
+
const attemptRow = await this.#db.engine_open_turn_attempt.get({
|
|
1313
|
+
model_call_id: providerModelCall.id,
|
|
1314
|
+
});
|
|
1243
1315
|
if (attemptRow === undefined) {
|
|
1244
1316
|
throw new Error(`Engine.runTurn: provider attempt ${attempt} did not open`);
|
|
1245
1317
|
}
|
|
1246
1318
|
providerAttemptId = attemptRow.id;
|
|
1247
1319
|
providerCallInFlight = true;
|
|
1320
|
+
const currentModelCall = providerModelCall;
|
|
1248
1321
|
const completedResponse = await observed(// {§observability-boundary}
|
|
1249
1322
|
"provider.generate", { model: provider.model, attempt }, async (span) => {
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1323
|
+
try {
|
|
1324
|
+
const generated = await provider.generate({
|
|
1325
|
+
messages: modelMessages,
|
|
1326
|
+
workerId: providerWorkerId,
|
|
1327
|
+
primaryWorkerId,
|
|
1328
|
+
signal: providerSignal,
|
|
1329
|
+
grammar: railGrammar,
|
|
1330
|
+
maxTokens,
|
|
1331
|
+
strikes: strikeStreak,
|
|
1332
|
+
attributions: providerAttemptAttributions.length > 0
|
|
1333
|
+
? providerAttemptAttributions
|
|
1334
|
+
: undefined,
|
|
1335
|
+
client: client ?? undefined,
|
|
1336
|
+
workspaceId: String(workspaceId),
|
|
1337
|
+
loop: loopSeq,
|
|
1338
|
+
turn: seq,
|
|
1339
|
+
observeRequest: currentModelCall.observeRequest,
|
|
1340
|
+
callKind: "emission",
|
|
1341
|
+
}); // {§provider-surface-generate} {§provider-guarantees-signal-wired} {§provider-guarantees-serial-attempts} {§attribution} {§client-metadata}
|
|
1342
|
+
currentModelCall.assertAccounting(generated.accounting);
|
|
1343
|
+
providerCallInFlight = false;
|
|
1344
|
+
recordCounter(PROVIDER_CALLS, {
|
|
1345
|
+
model: provider.model,
|
|
1346
|
+
attempt,
|
|
1347
|
+
status: "resolved",
|
|
1348
|
+
});
|
|
1349
|
+
span.setAttribute("status", "resolved");
|
|
1350
|
+
return generated;
|
|
1351
|
+
}
|
|
1352
|
+
catch (error) {
|
|
1353
|
+
if (error instanceof ProviderError) {
|
|
1354
|
+
currentModelCall.assertAccounting(error.accounting);
|
|
1355
|
+
}
|
|
1356
|
+
throw error;
|
|
1357
|
+
}
|
|
1274
1358
|
});
|
|
1275
1359
|
response = completedResponse;
|
|
1276
|
-
await
|
|
1360
|
+
await currentModelCall.observeResponse(completedResponse);
|
|
1277
1361
|
railEvidence = railGrammar === undefined
|
|
1278
1362
|
? undefined
|
|
1279
|
-
:
|
|
1363
|
+
: _a.#requireGrammarEvidence(completedResponse);
|
|
1280
1364
|
splitResponse = this.#splitResponse(completedResponse);
|
|
1281
|
-
await classifyProviderAttempt(attemptRow.id,
|
|
1365
|
+
await classifyProviderAttempt(attemptRow.id, splitResponse, attempt, splitResponse.emissionValid);
|
|
1282
1366
|
if (splitResponse.emissionValid)
|
|
1283
1367
|
break;
|
|
1284
1368
|
}
|
|
@@ -1288,44 +1372,26 @@ export default class Engine {
|
|
|
1288
1372
|
catch (err) {
|
|
1289
1373
|
// This handler owns only provider-call failures. Parser, cost, SQL,
|
|
1290
1374
|
// and engine-contract failures retain their original source.
|
|
1375
|
+
if (err instanceof ModelCallPersistenceError || err instanceof ProviderAccountingIntegrityError)
|
|
1376
|
+
throw err;
|
|
1291
1377
|
if (!providerCallInFlight)
|
|
1292
1378
|
throw err;
|
|
1293
1379
|
providerCallInFlight = false;
|
|
1294
|
-
if (providerAttemptId === null) {
|
|
1295
|
-
throw new Error("provider call failed without
|
|
1380
|
+
if (providerAttemptId === null || providerModelCall === null) {
|
|
1381
|
+
throw new Error("provider call failed without durable model-call and attempt identities", { cause: err });
|
|
1296
1382
|
}
|
|
1297
|
-
const failure = err
|
|
1298
|
-
? { status: err.problem.status, problem: err.problem }
|
|
1299
|
-
: providerSignal?.aborted === true
|
|
1300
|
-
? Results.failure("engine:provider", providerSignal.reason === LOOP_TIMEOUT_REASON ? "provider-call-timeout" : "provider-call-cancelled", providerSignal.reason === LOOP_TIMEOUT_REASON ? 504 : 499, providerSignal.reason === LOOP_TIMEOUT_REASON
|
|
1301
|
-
? "The provider call was interrupted by the loop deadline."
|
|
1302
|
-
: "The provider call was interrupted by loop cancellation.", {}, { stage: "provider-request", retryable: false })
|
|
1303
|
-
: (() => {
|
|
1304
|
-
console.error("Provider failed outside its Problem Details contract:", err);
|
|
1305
|
-
return Results.failure("engine:provider", "provider-contract-violation", 502, "The provider failed without returning its required Problem Details.", {}, {
|
|
1306
|
-
stage: "provider-request",
|
|
1307
|
-
retryable: false,
|
|
1308
|
-
});
|
|
1309
|
-
})();
|
|
1383
|
+
const failure = _a.#providerFailure(err, providerSignal);
|
|
1310
1384
|
// {§provider-interrupted-attempt} — a provider-declared interruption
|
|
1311
1385
|
// carries response evidence without becoming a completed exchange.
|
|
1312
1386
|
// Persist it as an unaccepted attempt before settling the failure.
|
|
1313
1387
|
if (err instanceof ProviderError && err.attempt !== undefined) {
|
|
1314
1388
|
response = err.attempt;
|
|
1315
|
-
await
|
|
1389
|
+
await providerModelCall.observeResponse(response, failure);
|
|
1316
1390
|
splitResponse = this.#splitResponse(response);
|
|
1317
|
-
await classifyProviderAttempt(providerAttemptId,
|
|
1391
|
+
await classifyProviderAttempt(providerAttemptId, splitResponse, providerAttemptSequence, false);
|
|
1318
1392
|
}
|
|
1319
1393
|
else {
|
|
1320
|
-
|
|
1321
|
-
kind: "unknown",
|
|
1322
|
-
reason: "provider call failed without response-bearing charge evidence",
|
|
1323
|
-
};
|
|
1324
|
-
await this.#db.engine_fail_turn_attempt.run({
|
|
1325
|
-
id: providerAttemptId,
|
|
1326
|
-
failure: JSON.stringify(failure),
|
|
1327
|
-
usage_cost: JSON.stringify(unknownCost),
|
|
1328
|
-
});
|
|
1394
|
+
await providerModelCall.fail(failure);
|
|
1329
1395
|
emissionAttempts = providerAttemptSequence;
|
|
1330
1396
|
}
|
|
1331
1397
|
// {§turn-never-blank} — a ProviderError means no completed exchange exists.
|
|
@@ -1370,9 +1436,10 @@ export default class Engine {
|
|
|
1370
1436
|
});
|
|
1371
1437
|
throw new OperationFailureError(recorded.result, { cause: err });
|
|
1372
1438
|
}
|
|
1373
|
-
if (response === undefined || splitResponse === undefined) {
|
|
1439
|
+
if (response === undefined || splitResponse === undefined || providerModelCall === null) {
|
|
1374
1440
|
throw new Error("provider attempt loop completed without a response");
|
|
1375
1441
|
}
|
|
1442
|
+
const emissionModelCallId = providerModelCall.id;
|
|
1376
1443
|
if (!splitResponse.emissionValid) {
|
|
1377
1444
|
// {§invalid-emission-attempts} The first consecutive exhaustion
|
|
1378
1445
|
// publishes only the raw final response and generic recovery fact.
|
|
@@ -1385,6 +1452,7 @@ export default class Engine {
|
|
|
1385
1452
|
turnId,
|
|
1386
1453
|
sequence: nextActionIndex,
|
|
1387
1454
|
folded: true,
|
|
1455
|
+
modelCallId: emissionModelCallId,
|
|
1388
1456
|
admission: "rejected",
|
|
1389
1457
|
});
|
|
1390
1458
|
this.#notices.push(workspaceId, loopId, {
|
|
@@ -1408,7 +1476,6 @@ export default class Engine {
|
|
|
1408
1476
|
status: allowInvalidEmissionRecovery ? TURN_STATUS_IMPLICIT_CONTINUE : 500,
|
|
1409
1477
|
outcomes: [],
|
|
1410
1478
|
fingerprint: "",
|
|
1411
|
-
budgetStruck: enforced.struck,
|
|
1412
1479
|
budgetHardStop: false,
|
|
1413
1480
|
steerStruck: false,
|
|
1414
1481
|
emissionAttempts,
|
|
@@ -1446,12 +1513,14 @@ export default class Engine {
|
|
|
1446
1513
|
if (railGrammar !== undefined) {
|
|
1447
1514
|
if (railEvidence === undefined)
|
|
1448
1515
|
throw new Error("configured GBNF response has no final grammar evidence");
|
|
1516
|
+
if (railResponseGrammar === undefined)
|
|
1517
|
+
throw new Error("configured GBNF has no response grammar");
|
|
1449
1518
|
let verdict = null;
|
|
1450
1519
|
try {
|
|
1451
|
-
verdict = validateGbnf(
|
|
1520
|
+
verdict = validateGbnf(railResponseGrammar, railEvidence.input);
|
|
1452
1521
|
}
|
|
1453
1522
|
catch (cause) {
|
|
1454
|
-
|
|
1523
|
+
_a.#warnRailVerdictGapOnce(cause.message);
|
|
1455
1524
|
}
|
|
1456
1525
|
railKeys = {
|
|
1457
1526
|
railsAttached: railEvidence.transported ? "client" : "withheld",
|
|
@@ -1551,6 +1620,8 @@ export default class Engine {
|
|
|
1551
1620
|
origin, onDispatch,
|
|
1552
1621
|
});
|
|
1553
1622
|
const droppedCount = opsCount - opsToDispatch.length;
|
|
1623
|
+
const bareStatements = opsToDispatch.filter((statement) => statement.op === "BARE");
|
|
1624
|
+
let bareResults = null;
|
|
1554
1625
|
const outcomes = [];
|
|
1555
1626
|
// Running counter — a multi-file READ writes N rows from one statement (rowsWritten),
|
|
1556
1627
|
// so the next op's sequence picks up after them. Collapses to nextActionIndex+i when
|
|
@@ -1596,11 +1667,45 @@ export default class Engine {
|
|
|
1596
1667
|
}
|
|
1597
1668
|
const result = await observed(// {§observability-boundary}
|
|
1598
1669
|
"op.dispatch", { op: statement.op }, async (span) => {
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1670
|
+
let dispatchResult;
|
|
1671
|
+
if (statement.op === "BARE") {
|
|
1672
|
+
if (bareResults === null) {
|
|
1673
|
+
const batch = await this.#runBareBatch({
|
|
1674
|
+
statements: bareStatements,
|
|
1675
|
+
provider: childProvider,
|
|
1676
|
+
turnId,
|
|
1677
|
+
modelCallSequenceStart: providerAttemptSequence + 1,
|
|
1678
|
+
workspaceId,
|
|
1679
|
+
workerId,
|
|
1680
|
+
primaryWorkerId,
|
|
1681
|
+
loopSequence: loopSeq,
|
|
1682
|
+
turnSequence: seq,
|
|
1683
|
+
signal: providerSignal,
|
|
1684
|
+
});
|
|
1685
|
+
bareResults = new Map(batch.map((item) => [item.statement, item]));
|
|
1686
|
+
}
|
|
1687
|
+
const bare = bareResults.get(statement);
|
|
1688
|
+
if (bare === undefined) {
|
|
1689
|
+
throw new Error("BARE statement reached dispatch without its batch result");
|
|
1690
|
+
}
|
|
1691
|
+
dispatchResult = await this.#dispatcher.recordBareResult({
|
|
1692
|
+
statement,
|
|
1693
|
+
workspaceId,
|
|
1694
|
+
workerId,
|
|
1695
|
+
loopId,
|
|
1696
|
+
turnId,
|
|
1697
|
+
sequence: rowSeq,
|
|
1698
|
+
origin,
|
|
1699
|
+
onDispatch,
|
|
1700
|
+
}, bare.result, bare.modelCallId);
|
|
1701
|
+
}
|
|
1702
|
+
else {
|
|
1703
|
+
dispatchResult = await this.#dispatcher.dispatch({
|
|
1704
|
+
statement, workspaceId, workerId, loopId, turnId,
|
|
1705
|
+
sequence: rowSeq,
|
|
1706
|
+
origin, onDispatch,
|
|
1707
|
+
});
|
|
1708
|
+
}
|
|
1604
1709
|
span.setAttribute("status", dispatchResult.status);
|
|
1605
1710
|
recordCounter(OPS_DISPATCHED, { op: statement.op, status: dispatchResult.status });
|
|
1606
1711
|
return dispatchResult;
|
|
@@ -1661,7 +1766,7 @@ export default class Engine {
|
|
|
1661
1766
|
}
|
|
1662
1767
|
: {
|
|
1663
1768
|
stage: "turn",
|
|
1664
|
-
recovery: "Perform an operation before continuing with
|
|
1769
|
+
recovery: "Perform an operation before continuing with `## SEND0 [102]`.",
|
|
1665
1770
|
retryable: false,
|
|
1666
1771
|
};
|
|
1667
1772
|
await this.#problems.record({
|
|
@@ -1687,14 +1792,22 @@ export default class Engine {
|
|
|
1687
1792
|
? response.assistant.reasoningEncrypted
|
|
1688
1793
|
: undefined;
|
|
1689
1794
|
if (packetAssistant.content.trim().length > 0 || reasoningItems !== undefined) {
|
|
1690
|
-
await this.#dispatcher.writeModelEntry({
|
|
1795
|
+
await this.#dispatcher.writeModelEntry({
|
|
1796
|
+
verbatim: packetAssistant.content,
|
|
1797
|
+
workerId,
|
|
1798
|
+
loopId,
|
|
1799
|
+
turnId,
|
|
1800
|
+
sequence: errSeq++,
|
|
1801
|
+
folded: true,
|
|
1802
|
+
modelCallId: emissionModelCallId,
|
|
1803
|
+
...(reasoningItems !== undefined ? { reasoningItems } : {}),
|
|
1804
|
+
});
|
|
1691
1805
|
}
|
|
1692
1806
|
return {
|
|
1693
1807
|
turnId,
|
|
1694
1808
|
status: turnStatus,
|
|
1695
1809
|
outcomes,
|
|
1696
1810
|
fingerprint: StrikeRail.fingerprintTurn(packetAssistant.ops),
|
|
1697
|
-
budgetStruck: enforced.struck,
|
|
1698
1811
|
budgetHardStop: false,
|
|
1699
1812
|
steerStruck,
|
|
1700
1813
|
emissionAttempts,
|
|
@@ -1703,7 +1816,8 @@ export default class Engine {
|
|
|
1703
1816
|
}
|
|
1704
1817
|
// Split the wire-level ProviderResponse into the two destinations:
|
|
1705
1818
|
// packet.assistant gets the model's emission (content, ops, reasoning);
|
|
1706
|
-
// Turn columns get
|
|
1819
|
+
// Turn columns get accepted-call metadata (finishReason, model). Physical
|
|
1820
|
+
// request usage and cost have their own cardinal persistence path.
|
|
1707
1821
|
// {§provider-surface} Text-fragment scraping policy lives
|
|
1708
1822
|
// here — engine owns the parse and the scraping rule, providers stay
|
|
1709
1823
|
// grammar-unaware.
|
|
@@ -1793,7 +1907,7 @@ export default class Engine {
|
|
|
1793
1907
|
const reasoning = assistant.reasoning ?? null;
|
|
1794
1908
|
return {
|
|
1795
1909
|
packetAssistant: { content: assistant.content, ops, reasoning },
|
|
1796
|
-
callMetadata: {
|
|
1910
|
+
callMetadata: { finishReason: assistant.finishReason, model: assistant.model },
|
|
1797
1911
|
parseErrors,
|
|
1798
1912
|
recoverableParseErrors: emissionValid ? recoverableParseErrors : [],
|
|
1799
1913
|
parseNotices,
|
|
@@ -1834,6 +1948,14 @@ export default class Engine {
|
|
|
1834
1948
|
throw new Error(`ambient loop-termination event ${r.event_id} status ${r.status_rx} does not match its terminal result status ${terminal.status}`);
|
|
1835
1949
|
}
|
|
1836
1950
|
let attrs = r.attrs ?? "{}";
|
|
1951
|
+
const parsedTags = JSON.parse(r.tags ?? "[]");
|
|
1952
|
+
if (!Array.isArray(parsedTags) || !parsedTags.every((tag) => typeof tag === "string" && tag.length > 0)) {
|
|
1953
|
+
throw new TypeError(`ambient event ${r.event_id} tags must be an array of nonempty strings`);
|
|
1954
|
+
}
|
|
1955
|
+
const tags = [...new Set(parsedTags)].toSorted();
|
|
1956
|
+
if (tags.length !== parsedTags.length || tags.some((tag, index) => tag !== parsedTags[index])) {
|
|
1957
|
+
throw new TypeError(`ambient event ${r.event_id} tags must be unique and sorted`);
|
|
1958
|
+
}
|
|
1837
1959
|
if (terminal !== null) {
|
|
1838
1960
|
const inherited = JSON.parse(attrs);
|
|
1839
1961
|
if (inherited === null || typeof inherited !== "object" || Array.isArray(inherited)) {
|
|
@@ -1861,6 +1983,15 @@ export default class Engine {
|
|
|
1861
1983
|
expanded: terminal !== null && terminal.status >= 200 && terminal.status < 300 ? 1 : 0,
|
|
1862
1984
|
attrs,
|
|
1863
1985
|
});
|
|
1986
|
+
const materialized = inserted ?? await this.#db.engine_ambient_delta_id.get({
|
|
1987
|
+
worker_id: workerId,
|
|
1988
|
+
event_id: r.event_id,
|
|
1989
|
+
});
|
|
1990
|
+
if (materialized === undefined)
|
|
1991
|
+
throw new Error(`ambient event ${r.event_id} has no observer log row after materialization`);
|
|
1992
|
+
for (const tag of tags) {
|
|
1993
|
+
await this.#db.log_write_tag.run({ log_entry_id: materialized.id, tag });
|
|
1994
|
+
}
|
|
1864
1995
|
if (inserted !== undefined)
|
|
1865
1996
|
written++;
|
|
1866
1997
|
}
|
|
@@ -2010,7 +2141,8 @@ export default class Engine {
|
|
|
2010
2141
|
const span = editedSpan(d.before, d.after);
|
|
2011
2142
|
await this.#db.engine_insert_log_entry.get({
|
|
2012
2143
|
worker_id: worker.id, loop_id: loop.id, turn_id: turn.id, sequence: sequence++,
|
|
2013
|
-
origin: "plurnk", source: "file",
|
|
2144
|
+
origin: "plurnk", source: "file", model_call_id: null,
|
|
2145
|
+
op: "EDIT", suffix: "", signal: null,
|
|
2014
2146
|
// Match Dispatcher.#extractTarget: a bare file address has NULL scheme
|
|
2015
2147
|
// only in log target metadata; its entry identity remains `file`.
|
|
2016
2148
|
scheme: null, username: null, password: null, hostname: null, port: null,
|
|
@@ -2051,6 +2183,7 @@ export default class Engine {
|
|
|
2051
2183
|
sequence,
|
|
2052
2184
|
origin: "plurnk",
|
|
2053
2185
|
source: null,
|
|
2186
|
+
model_call_id: null,
|
|
2054
2187
|
op: "prompt",
|
|
2055
2188
|
suffix: "",
|
|
2056
2189
|
signal: null,
|
|
@@ -2177,11 +2310,12 @@ export default class Engine {
|
|
|
2177
2310
|
};
|
|
2178
2311
|
const entry = {
|
|
2179
2312
|
channels: { body: { content: prompt, mimetype: "text/markdown" } },
|
|
2180
|
-
tags: [],
|
|
2181
2313
|
attributes: { openPaths },
|
|
2182
2314
|
};
|
|
2183
2315
|
await EntryCrud.writeEntry(pathname, entry, ctx, "prompt", workerId);
|
|
2184
2316
|
return { loopId, turnSeq };
|
|
2185
2317
|
}
|
|
2186
2318
|
}
|
|
2319
|
+
_a = Engine;
|
|
2320
|
+
export default Engine;
|
|
2187
2321
|
//# sourceMappingURL=Engine.js.map
|