@plurnk/plurnk-service 1.4.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 +37 -31
- package/INSTALL.md +16 -11
- package/README.md +1 -0
- package/SPEC.md +631 -397
- package/dist/Paths.d.ts.map +1 -1
- package/dist/Paths.js +2 -3
- package/dist/Paths.js.map +1 -1
- 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 +5 -1
- package/dist/content/index.d.ts.map +1 -1
- package/dist/content/index.js +4 -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/line-marker.d.ts +7 -3
- package/dist/content/line-marker.d.ts.map +1 -1
- package/dist/content/line-marker.js +2 -1
- package/dist/content/line-marker.js.map +1 -1
- package/dist/content/matcher.d.ts +1 -1
- package/dist/content/matcher.d.ts.map +1 -1
- package/dist/content/matcher.js +2 -2
- package/dist/content/matcher.js.map +1 -1
- package/dist/content/read-projector.d.ts +20 -0
- package/dist/content/read-projector.d.ts.map +1 -0
- package/dist/content/read-projector.js +122 -0
- package/dist/content/read-projector.js.map +1 -0
- package/dist/content/read-resolve.d.ts +2 -7
- package/dist/content/read-resolve.d.ts.map +1 -1
- package/dist/content/read-resolve.js +32 -107
- package/dist/content/read-resolve.js.map +1 -1
- package/dist/core/BranchReceipt.d.ts +0 -1
- package/dist/core/BranchReceipt.d.ts.map +1 -1
- package/dist/core/BranchReceipt.js +0 -3
- package/dist/core/BranchReceipt.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.d.ts +3 -0
- package/dist/core/ChannelWrite.d.ts.map +1 -1
- package/dist/core/ChannelWrite.js +3 -3
- package/dist/core/ChannelWrite.js.map +1 -1
- package/dist/core/ChannelWrite.sql +4 -3
- package/dist/core/CoreSchemeServices.d.ts +10 -2
- package/dist/core/CoreSchemeServices.d.ts.map +1 -1
- package/dist/core/CoreSchemeServices.js.map +1 -1
- package/dist/core/Dispatcher.d.ts +14 -8
- package/dist/core/Dispatcher.d.ts.map +1 -1
- package/dist/core/Dispatcher.js +656 -312
- 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 +24 -18
- package/dist/core/Engine.d.ts.map +1 -1
- package/dist/core/Engine.js +620 -392
- package/dist/core/Engine.js.map +1 -1
- package/dist/core/Engine.sql +188 -119
- 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 +4 -1
- package/dist/core/LogBody.d.ts.map +1 -1
- package/dist/core/LogBody.js +46 -2
- 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/LoopLifecycle.d.ts +1 -2
- package/dist/core/LoopLifecycle.d.ts.map +1 -1
- package/dist/core/LoopLifecycle.js +1 -4
- package/dist/core/LoopLifecycle.js.map +1 -1
- package/dist/core/LoopLifecycle.sql +1 -4
- 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 +13 -12
- package/dist/core/PacketBuilder.d.ts.map +1 -1
- package/dist/core/PacketBuilder.js +146 -118
- 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 +5 -2
- package/dist/core/StrikeRail.d.ts.map +1 -1
- package/dist/core/StrikeRail.js +10 -7
- package/dist/core/StrikeRail.js.map +1 -1
- package/dist/core/TerminalResult.d.ts +16 -0
- package/dist/core/TerminalResult.d.ts.map +1 -0
- package/dist/core/TerminalResult.js +49 -0
- package/dist/core/TerminalResult.js.map +1 -0
- package/dist/core/caps/CapsResolve.d.ts +5 -1
- package/dist/core/caps/CapsResolve.d.ts.map +1 -1
- package/dist/core/caps/CapsResolve.js +9 -5
- package/dist/core/caps/CapsResolve.js.map +1 -1
- package/dist/core/caps/DbChannelCaps.d.ts +1 -1
- package/dist/core/caps/DbChannelCaps.d.ts.map +1 -1
- package/dist/core/caps/DbChannelCaps.js +6 -4
- package/dist/core/caps/DbChannelCaps.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 +21 -8
- package/dist/core/caps/DbEntryCaps.js.map +1 -1
- package/dist/core/caps/DbNotifyCaps.d.ts +1 -1
- package/dist/core/caps/DbNotifyCaps.d.ts.map +1 -1
- package/dist/core/caps/DbNotifyCaps.js +6 -4
- package/dist/core/caps/DbNotifyCaps.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/DbSubscriptionCaps.d.ts +4 -6
- package/dist/core/caps/DbSubscriptionCaps.d.ts.map +1 -1
- package/dist/core/caps/DbSubscriptionCaps.js +27 -13
- package/dist/core/caps/DbSubscriptionCaps.js.map +1 -1
- package/dist/core/caps/SchemeCtxImpl.d.ts +11 -3
- package/dist/core/caps/SchemeCtxImpl.d.ts.map +1 -1
- package/dist/core/caps/SchemeCtxImpl.js +10 -8
- 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 +35 -14
- package/dist/core/git-iso.d.ts +3 -2
- package/dist/core/git-iso.d.ts.map +1 -1
- package/dist/core/git-iso.js +27 -12
- package/dist/core/git-iso.js.map +1 -1
- package/dist/core/git-membership.d.ts +1 -0
- package/dist/core/git-membership.d.ts.map +1 -1
- package/dist/core/git-membership.js +87 -26
- package/dist/core/git-membership.js.map +1 -1
- package/dist/core/git-state.d.ts +8 -1
- package/dist/core/git-state.d.ts.map +1 -1
- package/dist/core/git-state.js +36 -17
- package/dist/core/git-state.js.map +1 -1
- package/dist/core/namespace.d.ts +1 -0
- package/dist/core/namespace.d.ts.map +1 -1
- package/dist/core/namespace.js +20 -0
- package/dist/core/namespace.js.map +1 -1
- package/dist/core/optimistic-settlement.d.ts +2 -0
- package/dist/core/optimistic-settlement.d.ts.map +1 -0
- package/dist/core/optimistic-settlement.js +14 -0
- package/dist/core/optimistic-settlement.js.map +1 -0
- package/dist/core/packet-wire.d.ts +5 -2
- package/dist/core/packet-wire.d.ts.map +1 -1
- package/dist/core/packet-wire.js +318 -114
- 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/results.d.ts +4 -2
- package/dist/core/results.d.ts.map +1 -1
- package/dist/core/results.js +6 -0
- package/dist/core/results.js.map +1 -1
- package/dist/core/scheme-types.d.ts +0 -12
- package/dist/core/scheme-types.d.ts.map +1 -1
- package/dist/core/scheme-types.js.map +1 -1
- 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/core/worker-ops.sql +4 -4
- package/dist/core/workspace-settings.js +1 -1
- package/dist/core/workspace-settings.js.map +1 -1
- package/dist/digest/Digest.d.ts.map +1 -1
- package/dist/digest/Digest.js +349 -76
- package/dist/digest/Digest.js.map +1 -1
- package/dist/digest/digest.sql +42 -16
- package/dist/schemes/Exec.d.ts +4 -4
- package/dist/schemes/Exec.d.ts.map +1 -1
- package/dist/schemes/Exec.js +167 -134
- package/dist/schemes/Exec.js.map +1 -1
- package/dist/schemes/ExecOutputScheme.d.ts +4 -5
- package/dist/schemes/ExecOutputScheme.d.ts.map +1 -1
- package/dist/schemes/ExecOutputScheme.js +12 -20
- package/dist/schemes/ExecOutputScheme.js.map +1 -1
- package/dist/schemes/File.d.ts +5 -6
- package/dist/schemes/File.d.ts.map +1 -1
- package/dist/schemes/File.js +35 -41
- package/dist/schemes/File.js.map +1 -1
- package/dist/schemes/Log.d.ts +16 -5
- package/dist/schemes/Log.d.ts.map +1 -1
- package/dist/schemes/Log.js +229 -262
- package/dist/schemes/Log.js.map +1 -1
- package/dist/schemes/Log.sql +50 -27
- package/dist/schemes/Prompt.d.ts +3 -4
- package/dist/schemes/Prompt.d.ts.map +1 -1
- package/dist/schemes/Prompt.js +5 -5
- package/dist/schemes/Prompt.js.map +1 -1
- package/dist/schemes/Skill.d.ts +4 -5
- package/dist/schemes/Skill.d.ts.map +1 -1
- package/dist/schemes/Skill.js +3 -3
- package/dist/schemes/Skill.js.map +1 -1
- package/dist/schemes/Worker.d.ts +7 -7
- package/dist/schemes/Worker.d.ts.map +1 -1
- package/dist/schemes/Worker.js +99 -84
- package/dist/schemes/Worker.js.map +1 -1
- package/dist/schemes/_entry-crud.d.ts +2 -2
- package/dist/schemes/_entry-crud.d.ts.map +1 -1
- package/dist/schemes/_entry-crud.js +29 -10
- package/dist/schemes/_entry-crud.js.map +1 -1
- package/dist/schemes/_entry-crud.sql +11 -17
- package/dist/schemes/_entry-find.d.ts +41 -14
- package/dist/schemes/_entry-find.d.ts.map +1 -1
- package/dist/schemes/_entry-find.js +159 -166
- 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 +13 -8
- package/dist/schemes/_entry-ops.d.ts.map +1 -1
- package/dist/schemes/_entry-ops.js +98 -119
- package/dist/schemes/_entry-ops.js.map +1 -1
- package/dist/schemes/_entry-ops.sql +31 -6
- package/dist/schemes/_entry-semantic.d.ts +5 -7
- package/dist/schemes/_entry-semantic.d.ts.map +1 -1
- package/dist/schemes/_entry-semantic.js +10 -31
- package/dist/schemes/_entry-semantic.js.map +1 -1
- package/dist/schemes/_entry-semantic.sql +2 -2
- 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 +16 -6
- 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/BranchBatches.d.ts +1 -0
- package/dist/server/BranchBatches.d.ts.map +1 -1
- package/dist/server/BranchBatches.js +1 -0
- package/dist/server/BranchBatches.js.map +1 -1
- package/dist/server/Daemon.d.ts +6 -8
- package/dist/server/Daemon.d.ts.map +1 -1
- package/dist/server/Daemon.js +238 -68
- package/dist/server/Daemon.js.map +1 -1
- package/dist/server/DaemonModule.d.ts +3 -3
- package/dist/server/DaemonModule.d.ts.map +1 -1
- package/dist/server/branch-batch.sql +3 -1
- package/dist/server/client-input.d.ts +2 -1
- package/dist/server/client-input.d.ts.map +1 -1
- package/dist/server/client-input.js +7 -1
- package/dist/server/client-input.js.map +1 -1
- package/dist/server/drain.sql +8 -7
- package/dist/server/envelope.d.ts +0 -2
- package/dist/server/envelope.d.ts.map +1 -1
- package/dist/server/envelope.js +0 -1
- package/dist/server/envelope.js.map +1 -1
- package/dist/server/envelope.sql +3 -4
- package/dist/server/lifecycle-recovery.sql +44 -1
- package/dist/server/logEntry.d.ts +2 -1
- package/dist/server/logEntry.d.ts.map +1 -1
- package/dist/server/logEntry.js +15 -2
- package/dist/server/logEntry.js.map +1 -1
- package/dist/server/logEntry.sql +5 -1
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +13 -1
- package/dist/service.js.map +1 -1
- package/migrations/001_schema.sql +730 -221
- package/package.json +53 -46
- package/dist/core/BudgetOverflow.d.ts +0 -23
- package/dist/core/BudgetOverflow.d.ts.map +0 -1
- package/dist/core/BudgetOverflow.js +0 -61
- 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 -44
- package/dist/core/caps/DbTagCaps.js.map +0 -1
- package/dist/matrix/live-specimen.d.ts +0 -9
- package/dist/matrix/live-specimen.d.ts.map +0 -1
- package/dist/matrix/live-specimen.js +0 -37
- package/dist/matrix/live-specimen.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;
|
|
@@ -7,7 +8,6 @@ import { Mimetypes, emptyRegistry } from "@plurnk/plurnk-mimetypes";
|
|
|
7
8
|
import Meta from "@plurnk/plurnk-meta";
|
|
8
9
|
import EntryCrud from "../schemes/_entry-crud.js";
|
|
9
10
|
import SearchIndex from "../schemes/_search-index.js";
|
|
10
|
-
import { markTerminal } from "../schemes/Worker.js";
|
|
11
11
|
import GitMembership from "./git-membership.js";
|
|
12
12
|
import GitState from "./git-state.js";
|
|
13
13
|
import WorkspaceSettings from "./workspace-settings.js";
|
|
@@ -20,13 +20,14 @@ import LoopLifecycle from "./LoopLifecycle.js";
|
|
|
20
20
|
import { readFile } from "node:fs/promises";
|
|
21
21
|
import { fileURLToPath } from "node:url";
|
|
22
22
|
import { setTimeout as delay } from "node:timers/promises";
|
|
23
|
+
import { randomUUID } from "node:crypto";
|
|
23
24
|
// Shared module imported by both Engine and bin/digest.ts, so wire
|
|
24
25
|
// projection and digest projection are structurally one function — no
|
|
25
26
|
// drift between wire and digest possible.
|
|
26
27
|
import PacketWire from "./packet-wire.js";
|
|
27
28
|
import Results, { OperationFailureError } from "./results.js";
|
|
28
29
|
import BranchReceipt from "./BranchReceipt.js";
|
|
29
|
-
import
|
|
30
|
+
import TerminalResult from "./TerminalResult.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
|
|
@@ -41,7 +42,22 @@ import Dispatcher from "./Dispatcher.js";
|
|
|
41
42
|
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";
|
|
46
|
+
import { providerRequestFromStorageRow, } from "./provider-accounting.js";
|
|
47
|
+
import ModelCall, { ModelCallPersistenceError, ProviderAccountingIntegrityError, } from "./ModelCall.js";
|
|
48
|
+
const DEFAULT_MAX_STRIKES = 6;
|
|
49
|
+
const RECORD_STREAM_MIMETYPES = new Set(["application/jsonl", "application/x-ndjson"]);
|
|
50
|
+
const baseMimetype = (mimetype) => mimetype.split(";", 1)[0].trim().toLowerCase();
|
|
51
|
+
// {§exec-stream} Active streams publish only independently meaningful units.
|
|
52
|
+
// Atomic documents wait for close; JSONL stops after its last complete record.
|
|
53
|
+
const streamPublicationEnd = (content, mimetype, cursor, closed) => {
|
|
54
|
+
const type = baseMimetype(mimetype);
|
|
55
|
+
if (closed || type.startsWith("text/"))
|
|
56
|
+
return content.length;
|
|
57
|
+
if (!RECORD_STREAM_MIMETYPES.has(type))
|
|
58
|
+
return cursor;
|
|
59
|
+
return Math.max(cursor, content.lastIndexOf("\n") + 1);
|
|
60
|
+
};
|
|
45
61
|
const ENGINE_PROBLEMS = Object.freeze({
|
|
46
62
|
max_commands_exceeded: {
|
|
47
63
|
status: 429,
|
|
@@ -51,7 +67,7 @@ const ENGINE_PROBLEMS = Object.freeze({
|
|
|
51
67
|
idle_turn: {
|
|
52
68
|
status: 409,
|
|
53
69
|
code: "idle-turn",
|
|
54
|
-
detail: "
|
|
70
|
+
detail: "`## SEND0 [102]` was emitted without an operation to continue from.",
|
|
55
71
|
},
|
|
56
72
|
});
|
|
57
73
|
// The prompt entry target - prompt:///<loop>/<N>, self-only ({§prompt-self-only}):
|
|
@@ -102,7 +118,7 @@ const readMaxCommands = () => {
|
|
|
102
118
|
return n;
|
|
103
119
|
};
|
|
104
120
|
// PLURNK_SERVICE_FILES_ITEMS — the turn-0 catalog preview. null = off;
|
|
105
|
-
// -1 = the
|
|
121
|
+
// -1 = the ordinary markerless page; positive N explicitly caps file rows. 0 / unset = off.
|
|
106
122
|
const normalizeFilesItems = (n) => (!Number.isFinite(n) || n === 0 ? null : n < 0 ? -1 : n);
|
|
107
123
|
const readFilesItems = () => {
|
|
108
124
|
const raw = process.env.PLURNK_SERVICE_FILES_ITEMS;
|
|
@@ -110,13 +126,55 @@ const readFilesItems = () => {
|
|
|
110
126
|
return null;
|
|
111
127
|
return normalizeFilesItems(Number.parseInt(raw, 10));
|
|
112
128
|
};
|
|
113
|
-
import {
|
|
129
|
+
import { aggregateProviderAccounting, ProviderError, resolveActiveAlias, scopeEnvToAlias, } from "@plurnk/plurnk-providers";
|
|
114
130
|
import { validateGbnf } from "@plurnk/gbnf";
|
|
115
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
|
+
});
|
|
116
173
|
// Runtime normalization for a disposition the engine refuses or resolves as a
|
|
117
174
|
// continue after dispatch ({§send}). Every admitted emission itself ends in an
|
|
118
175
|
// explicit disposition SEND ({§emission-admission}).
|
|
119
176
|
const TURN_STATUS_IMPLICIT_CONTINUE = 102;
|
|
177
|
+
const INVALID_EMISSION_RECOVERY_MESSAGE = "Your previous response contained an unrecoverable syntax error. No operations were performed. Try again.";
|
|
120
178
|
const DEFAULT_MIN_CYCLES = 3;
|
|
121
179
|
const DEFAULT_MAX_CYCLE_PERIOD = 4;
|
|
122
180
|
const readPositiveInt = (envVar, fallback) => {
|
|
@@ -141,7 +199,7 @@ const DEFAULT_LOOP_TIMEOUT_MS = 86400000;
|
|
|
141
199
|
const readLoopTimeoutMs = () => readPositiveInt("PLURNK_SERVICE_LOOP_TIMEOUT", DEFAULT_LOOP_TIMEOUT_MS);
|
|
142
200
|
// The wall's abort reason — runLoop branches a mid-turn teardown to the 504 terminal on it.
|
|
143
201
|
const LOOP_TIMEOUT_REASON = "loop_timeout";
|
|
144
|
-
|
|
202
|
+
class Engine {
|
|
145
203
|
static fingerprintTurn(ops) {
|
|
146
204
|
return StrikeRail.fingerprintTurn(ops);
|
|
147
205
|
}
|
|
@@ -158,7 +216,7 @@ export default class Engine {
|
|
|
158
216
|
// Boot-discovered runtime executors. Daemon builds + sets via
|
|
159
217
|
// setExecutors at start(); undefined until then (and in bare tests).
|
|
160
218
|
#executors;
|
|
161
|
-
// {§send-premature-terminate}/SEND
|
|
219
|
+
// {§send-premature-terminate}/scoped SEND signal 202 — park deadlines by loopId, written at dispatch (the
|
|
162
220
|
// marker's seconds; -1 = indefinite), consumed by the daemon's drain park-exit to schedule
|
|
163
221
|
// the deadline wake. In-memory: a daemon restart drops pending deadlines (documented).
|
|
164
222
|
parkDeadlines = new Map();
|
|
@@ -170,9 +228,6 @@ export default class Engine {
|
|
|
170
228
|
#notices;
|
|
171
229
|
#problems;
|
|
172
230
|
#strikes;
|
|
173
|
-
// {§grinder-hard-413-recovery} - loops granted their one over-ceiling recovery turn. Cleared on a
|
|
174
|
-
// fitting turn so a later independent overflow can earn a fresh recovery, and at loop cleanup.
|
|
175
|
-
#hardOverflowRecovery = new Set();
|
|
176
231
|
#packets;
|
|
177
232
|
searchGate = new SearchGate();
|
|
178
233
|
#proposals;
|
|
@@ -208,7 +263,13 @@ export default class Engine {
|
|
|
208
263
|
if (!invalidate && this.#workspaceWarmStatus.get(workspaceId)?.phase === "complete") {
|
|
209
264
|
return Promise.resolve();
|
|
210
265
|
}
|
|
211
|
-
const state = {
|
|
266
|
+
const state = {
|
|
267
|
+
dirty: false,
|
|
268
|
+
materialize,
|
|
269
|
+
ctx,
|
|
270
|
+
abort: new AbortController(),
|
|
271
|
+
promise: Promise.resolve(),
|
|
272
|
+
};
|
|
212
273
|
// Register before publishing the first synchronous Notice. A
|
|
213
274
|
// listener may request another warm from that callback; it must join
|
|
214
275
|
// this state rather than opening a second pump in the re-entrant gap.
|
|
@@ -231,10 +292,14 @@ export default class Engine {
|
|
|
231
292
|
completed: 0, total: 1, percent: 0, level: "info",
|
|
232
293
|
});
|
|
233
294
|
try {
|
|
295
|
+
const signal = current.signal === undefined
|
|
296
|
+
? state.abort.signal
|
|
297
|
+
: AbortSignal.any([current.signal, state.abort.signal]);
|
|
298
|
+
const cancellable = { ...current, signal };
|
|
234
299
|
if (shouldMaterialize)
|
|
235
|
-
await GitMembership.indexGitMembership(
|
|
300
|
+
await GitMembership.indexGitMembership(cancellable);
|
|
236
301
|
await SearchIndex.maintain({
|
|
237
|
-
...
|
|
302
|
+
...cancellable,
|
|
238
303
|
pushNotice: (notice) => {
|
|
239
304
|
if (notice.kind === "embed_progress"
|
|
240
305
|
&& typeof notice.completed === "number"
|
|
@@ -277,9 +342,26 @@ export default class Engine {
|
|
|
277
342
|
workspaceDerivationStatus(workspaceId) {
|
|
278
343
|
return this.#workspaceWarmStatus.get(workspaceId) ?? null;
|
|
279
344
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
345
|
+
cancelDerivations(reason = new DOMException("derivations cancelled", "AbortError")) {
|
|
346
|
+
for (const state of this.#workspaceWarms.values()) {
|
|
347
|
+
if (!state.abort.signal.aborted)
|
|
348
|
+
state.abort.abort(reason);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
// Awaited by Daemon.stop before the db closes. Shutdown supplies the exact
|
|
352
|
+
// cancellation reason it owns; every unrelated failure remains visible.
|
|
353
|
+
async drainDerivations(ignoredReason) {
|
|
354
|
+
const results = await Promise.allSettled([...this.#workspaceWarms.values()].map((state) => state.promise));
|
|
355
|
+
const errors = results
|
|
356
|
+
.filter((result) => result.status === "rejected")
|
|
357
|
+
.flatMap((result) => result.reason instanceof AggregateError
|
|
358
|
+
? [...result.reason.errors]
|
|
359
|
+
: [result.reason])
|
|
360
|
+
.filter((error) => error !== ignoredReason);
|
|
361
|
+
if (errors.length === 1)
|
|
362
|
+
throw errors[0];
|
|
363
|
+
if (errors.length > 1)
|
|
364
|
+
throw new AggregateError(errors, "derivation drain failed");
|
|
283
365
|
}
|
|
284
366
|
async drainWorkspaceDerivations(workspaceId) {
|
|
285
367
|
await this.#workspaceWarms.get(workspaceId)?.promise;
|
|
@@ -294,9 +376,9 @@ export default class Engine {
|
|
|
294
376
|
// parse): warn once per message, never per turn; the turn records railsVerdict "unverifiable".
|
|
295
377
|
static #railGapWarned = new Set();
|
|
296
378
|
static #warnRailVerdictGapOnce(message) {
|
|
297
|
-
if (
|
|
379
|
+
if (_a.#railGapWarned.has(message))
|
|
298
380
|
return;
|
|
299
|
-
|
|
381
|
+
_a.#railGapWarned.add(message);
|
|
300
382
|
process.stderr.write(`plurnk-engine: rail verdict unavailable — the configured grammar did not parse in @plurnk/gbnf (${message})\n`);
|
|
301
383
|
}
|
|
302
384
|
static #requireGrammarEvidence(response) {
|
|
@@ -330,7 +412,8 @@ export default class Engine {
|
|
|
330
412
|
discovery: { registry: emptyRegistry(), handlers: new Map(), skipped: [] },
|
|
331
413
|
});
|
|
332
414
|
// {§tokenomics-agnostic-ruler} — standalone construction and the daemon
|
|
333
|
-
// use the same default; provider counting is confined to
|
|
415
|
+
// use the same default; provider counting is confined to hard
|
|
416
|
+
// context-envelope admission.
|
|
334
417
|
this.#tokenize = tokenize ?? rulerCount;
|
|
335
418
|
const executors = () => this.#executors;
|
|
336
419
|
const loopSignal = (loopId) => this.#loopAborts.get(loopId)?.signal;
|
|
@@ -340,7 +423,6 @@ export default class Engine {
|
|
|
340
423
|
this.#packets = new PacketBuilder({
|
|
341
424
|
db,
|
|
342
425
|
schemes,
|
|
343
|
-
problems: this.#problems,
|
|
344
426
|
executors,
|
|
345
427
|
});
|
|
346
428
|
this.#proposals = new ProposalLifecycle({
|
|
@@ -385,12 +467,16 @@ export default class Engine {
|
|
|
385
467
|
if (this.#executors === undefined)
|
|
386
468
|
throw new Error("registerRuntime: executor registry not wired yet");
|
|
387
469
|
RuntimeTag.assert(tag, "module runtime");
|
|
470
|
+
const normalized = {
|
|
471
|
+
...entry,
|
|
472
|
+
invocation: RuntimeInvocation.assert(entry.invocation, entry.namespaceOwner.name, tag),
|
|
473
|
+
};
|
|
388
474
|
// Preflight both owners before either write; synchronous registration
|
|
389
475
|
// then cannot leave a half-claimed namespace. {§plugin-namespace-arbitration}
|
|
390
|
-
this.#executors.assertCanRegister(tag,
|
|
391
|
-
this.#schemes.assertRuntimeClaim(tag,
|
|
392
|
-
this.#schemes.registerRuntimeScheme(tag,
|
|
393
|
-
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);
|
|
394
480
|
}
|
|
395
481
|
// Supply an explicitly configured local constraint; ANTLR remains the
|
|
396
482
|
// language authority. {§grammar-enforcement-verified-at-boot}
|
|
@@ -414,9 +500,10 @@ export default class Engine {
|
|
|
414
500
|
? variant
|
|
415
501
|
: fileURLToPath(import.meta.resolve(`@plurnk/plurnk-contracts/${variant}`));
|
|
416
502
|
const text = await readFile(path, "utf8"); // unresolvable/unreadable throws — a configured rail never silently degrades
|
|
417
|
-
|
|
503
|
+
const constraint = grammarConstraint(text);
|
|
504
|
+
this.#gbnfCache.set(variant, constraint);
|
|
418
505
|
process.stderr.write(`plurnk-engine: GBNF constraint: ${alias || "(bare)"} → ${variant} (${text.length} chars)\n`);
|
|
419
|
-
return
|
|
506
|
+
return constraint;
|
|
420
507
|
}
|
|
421
508
|
// A lineage's no-parent root; a root worker resolves to itself. Fail hard
|
|
422
509
|
// when corruption leaves a worker without one. {§worker-primary}
|
|
@@ -426,6 +513,16 @@ export default class Engine {
|
|
|
426
513
|
throw new Error(`resolveWorkerPrimary: worker ${workerId} has no lineage root — corrupt parent chain`);
|
|
427
514
|
return root.id;
|
|
428
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
|
+
}
|
|
429
526
|
promptBudgetFor(provider) {
|
|
430
527
|
return this.#packets.promptBudgetFor(provider);
|
|
431
528
|
}
|
|
@@ -433,6 +530,109 @@ export default class Engine {
|
|
|
433
530
|
const tags = Meta.composeAttributions(this.#schemes.attributions(context), this.#executors?.attributions(context) ?? [], await this.#mimetypes.attributions(context), provider.attributions?.(context) ?? []);
|
|
434
531
|
return [...tags];
|
|
435
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
|
+
}
|
|
436
636
|
// {§attribution} — reporting derives from exact provider-request evidence;
|
|
437
637
|
// malformed durable tags fail here instead of being silently filtered.
|
|
438
638
|
async loopAttributions(loopId) {
|
|
@@ -445,24 +645,22 @@ export default class Engine {
|
|
|
445
645
|
});
|
|
446
646
|
return [...Meta.composeAttributions(tags)];
|
|
447
647
|
}
|
|
448
|
-
//
|
|
449
|
-
// 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}
|
|
450
651
|
async loopUsage(loopId) {
|
|
451
652
|
const row = await this.#db.engine_loop_usage.get({ loop_id: loopId });
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
653
|
+
if (row === undefined)
|
|
654
|
+
throw new Error(`loopUsage: loop ${loopId} does not exist`);
|
|
655
|
+
const requests = await this.#db.engine_loop_provider_requests.all({ loop_id: loopId });
|
|
455
656
|
return {
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
costs: parsedCosts.map((cost) => validateProviderCost(cost)),
|
|
460
|
-
// Latest provider attempt on the latest turn, not the billed total.
|
|
461
|
-
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,
|
|
462
660
|
// Latest effective packet allowance; null when uncapped or unknown.
|
|
463
|
-
promptBudget: row
|
|
661
|
+
promptBudget: row.context_size,
|
|
464
662
|
// Latest turn's opaque provider metadata. {§meta-passthrough}
|
|
465
|
-
meta: JSON.parse(row
|
|
663
|
+
meta: JSON.parse(row.meta ?? "{}"),
|
|
466
664
|
};
|
|
467
665
|
}
|
|
468
666
|
// A mapped rail divergence is a CODE-POINT offset into the model's content;
|
|
@@ -485,11 +683,12 @@ export default class Engine {
|
|
|
485
683
|
}
|
|
486
684
|
return { line, column };
|
|
487
685
|
}
|
|
488
|
-
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, }) {
|
|
489
687
|
// A 202 park suspends this durable loop and a later wake re-enters runLoop.
|
|
490
688
|
// Its ceiling therefore counts every prior turn, not merely this process-local
|
|
491
689
|
// execution segment.
|
|
492
690
|
const turnIds = await this.#lifecycle.turnIds(loopId);
|
|
691
|
+
let invalidEmissionRecoveryEntryId = null;
|
|
493
692
|
// Per-loop AbortController for scheme-side cancellation propagation.
|
|
494
693
|
// Chained from the caller's `signal` so an external abort cascades.
|
|
495
694
|
const loopAbort = new AbortController();
|
|
@@ -520,11 +719,11 @@ export default class Engine {
|
|
|
520
719
|
return { turnIds, result, hitMaxTurns: false, reason: "loop_timeout" };
|
|
521
720
|
};
|
|
522
721
|
// Cleanup splits by termination kind:
|
|
523
|
-
// - "graceful" (SEND
|
|
722
|
+
// - "graceful" (SEND signal 202 Accepted): in-flight streaming-scheme spawns
|
|
524
723
|
// are ALLOWED to outlive the loop — they complete naturally, write final
|
|
525
724
|
// channel state, and wake-on-completion (E.4) opens a fresh loop. 202 is
|
|
526
725
|
// the only terminal that means "keep my async work."
|
|
527
|
-
// - "forceful" (SEND
|
|
726
|
+
// - "forceful" (SEND signal 200 done, max_turns, strike, cancel, context-envelope rejection, 4xx/5xx):
|
|
528
727
|
// fire the loop-level abort so leftover spawns tear down. "Done" reaps.
|
|
529
728
|
const cleanup = (kind, reason) => {
|
|
530
729
|
clearTimeout(wall);
|
|
@@ -534,7 +733,6 @@ export default class Engine {
|
|
|
534
733
|
this.#loopAborts.delete(loopId);
|
|
535
734
|
this.#strikes.delete(loopId);
|
|
536
735
|
this.searchGate.cleanup(loopId);
|
|
537
|
-
this.#hardOverflowRecovery.delete(loopId);
|
|
538
736
|
this.#notices.delete(loopId);
|
|
539
737
|
};
|
|
540
738
|
while (true) {
|
|
@@ -547,7 +745,7 @@ export default class Engine {
|
|
|
547
745
|
// check, {§worker-lifecycle-wake-requeue-not-terminal}). The wake's intent is KEEP
|
|
548
746
|
// RUNNING: re-claim atomically and continue — the injected prompt is already
|
|
549
747
|
// this loop's next turn. Returning it as "external" broadcast a QUEUED loop
|
|
550
|
-
// as a terminal result with status 100 — the delegation-flags
|
|
748
|
+
// as a terminal result with status 100 — the delegation-flags race.
|
|
551
749
|
await this.#db.engine_reclaim_queued_loop.run({ loop_id: loopId });
|
|
552
750
|
continue; // claimed (or a racer flipped it first — the re-read decides)
|
|
553
751
|
}
|
|
@@ -584,10 +782,6 @@ export default class Engine {
|
|
|
584
782
|
cleanup("forceful", "max_turns");
|
|
585
783
|
return { turnIds, result, hitMaxTurns: true, reason: "max_turns" };
|
|
586
784
|
}
|
|
587
|
-
// PLURNK_SERVICE_EXEC_WAIT_MS — a post-EXEC breath: if a spawn from the prior turn
|
|
588
|
-
// is still in flight, give it a tunable beat to land in THIS turn's packet
|
|
589
|
-
// before we assemble it. A fixed grace beat, never a wait-for-completion;
|
|
590
|
-
// 0/unset = off. Abortable with the loop signal.
|
|
591
785
|
const execHandler = this.#schemes.get("exec");
|
|
592
786
|
// {§exec-hold-until-concluded} — hold matching runtime/effect
|
|
593
787
|
// streams until conclusion or the fail-open cap, then resume the
|
|
@@ -600,19 +794,15 @@ export default class Engine {
|
|
|
600
794
|
await delay(150, undefined, { signal });
|
|
601
795
|
}
|
|
602
796
|
}
|
|
603
|
-
const execWaitMs = Number(process.env.PLURNK_SERVICE_EXEC_WAIT_MS ?? "0");
|
|
604
|
-
if (execWaitMs > 0) {
|
|
605
|
-
if (execHandler?.hasActiveSpawns?.(workerId) === true)
|
|
606
|
-
await delay(execWaitMs, undefined, { signal });
|
|
607
|
-
}
|
|
608
797
|
let turn;
|
|
609
798
|
const releaseWorkspace = await this.#acquireWorkspaceTurn(workspaceId, workerId);
|
|
610
799
|
try {
|
|
611
800
|
turn = await observed(// {§observability-boundary}
|
|
612
801
|
"loop.turn", { workerId, "loop.id": loopId }, async (span) => {
|
|
613
802
|
const t = await this.runTurn({
|
|
614
|
-
provider, messages, requirements, workspaceId, workerId, loopId, origin, signal, onDispatch,
|
|
803
|
+
provider, childProvider, messages, requirements, workspaceId, workerId, loopId, origin, signal, onDispatch,
|
|
615
804
|
turnNumber: turnIds.length + 1, maxTurns,
|
|
805
|
+
invalidEmissionRecoveryEntryId,
|
|
616
806
|
});
|
|
617
807
|
span.setAttribute("turn.id", t.turnId);
|
|
618
808
|
return t;
|
|
@@ -635,10 +825,18 @@ export default class Engine {
|
|
|
635
825
|
releaseWorkspace();
|
|
636
826
|
}
|
|
637
827
|
turnIds.push(turn.turnId);
|
|
638
|
-
// Invalid provider emissions are retried beneath this turn and never
|
|
639
|
-
// reach the strike rail.
|
|
640
|
-
//
|
|
828
|
+
// {§invalid-emission-attempts} Invalid provider emissions are retried beneath this turn and never
|
|
829
|
+
// reach the strike rail. The first consecutive exhaustion has already
|
|
830
|
+
// exposed its bounded lifeline through ordinary next-turn state. A
|
|
831
|
+
// second exhaustion is terminal; an admitted turn clears the sequence.
|
|
641
832
|
if (turn.emissionExhausted) {
|
|
833
|
+
if (invalidEmissionRecoveryEntryId === null) {
|
|
834
|
+
if (turn.rejectedModelEntryId === undefined) {
|
|
835
|
+
throw new Error("an admitted invalid-emission recovery requires its rejected model-entry identity");
|
|
836
|
+
}
|
|
837
|
+
invalidEmissionRecoveryEntryId = turn.rejectedModelEntryId;
|
|
838
|
+
continue;
|
|
839
|
+
}
|
|
642
840
|
const failure = Results.failure("engine:generation", "invalid-emission-exhausted", 500, `No valid PLAN...SEND turn was received after ${turn.emissionAttempts} emission attempts.`, {}, {
|
|
643
841
|
attempts: turn.emissionAttempts,
|
|
644
842
|
stage: "emission-validation",
|
|
@@ -650,25 +848,24 @@ export default class Engine {
|
|
|
650
848
|
cleanup("forceful", "invalid_emission");
|
|
651
849
|
return { turnIds, result, hitMaxTurns: false, reason: "invalid_emission" };
|
|
652
850
|
}
|
|
653
|
-
|
|
851
|
+
invalidEmissionRecoveryEntryId = null;
|
|
852
|
+
// SPEC {§grinder}: the effective context envelope rejected the request.
|
|
654
853
|
if (turn.budgetHardStop) {
|
|
655
|
-
if (turn.
|
|
656
|
-
throw new Error("a
|
|
854
|
+
if (turn.budgetFailure === undefined) {
|
|
855
|
+
throw new Error("a context-envelope hard-stop requires its exact failure");
|
|
657
856
|
}
|
|
658
|
-
const
|
|
659
|
-
const result = await this.#lifecycle.finish(loopId, failure);
|
|
857
|
+
const result = await this.#lifecycle.finish(loopId, turn.budgetFailure);
|
|
660
858
|
if (result === null)
|
|
661
859
|
throw new Error(`loop ${loopId} became terminal before budget settlement`);
|
|
662
|
-
cleanup("forceful", "
|
|
663
|
-
return { turnIds, result, hitMaxTurns: false, reason: "
|
|
860
|
+
cleanup("forceful", "context_envelope");
|
|
861
|
+
return { turnIds, result, hitMaxTurns: false, reason: "context_envelope" };
|
|
664
862
|
}
|
|
665
|
-
// {§engine-rails} — per-turn strike accounting (cycle detection,
|
|
666
|
-
//
|
|
863
|
+
// {§engine-rails} — per-turn strike accounting (cycle detection,
|
|
864
|
+
// steer coupling, hard operation outcomes). StrikeRail owns the
|
|
667
865
|
// bookkeeping; runLoop owns abandonment.
|
|
668
866
|
const verdict = this.#strikes.assess(loopId, {
|
|
669
867
|
fingerprint: turn.fingerprint,
|
|
670
|
-
|
|
671
|
-
budgetStruck: turn.budgetStruck,
|
|
868
|
+
outcomes: turn.outcomes,
|
|
672
869
|
steerStruck: turn.steerStruck,
|
|
673
870
|
minCycles, maxCyclePeriod, maxStrikes,
|
|
674
871
|
});
|
|
@@ -691,7 +888,11 @@ export default class Engine {
|
|
|
691
888
|
}
|
|
692
889
|
}
|
|
693
890
|
}
|
|
694
|
-
async runTurn({ provider, messages, requirements = "", workspaceId, workerId, loopId, origin = "model", signal, onDispatch, turnNumber = 1, maxTurns = 50, }) {
|
|
891
|
+
async runTurn({ provider, childProvider = provider, messages, requirements = "", workspaceId, workerId, loopId, origin = "model", signal, onDispatch, turnNumber = 1, maxTurns = 50, invalidEmissionRecoveryEntryId, }) {
|
|
892
|
+
const allowInvalidEmissionRecovery = invalidEmissionRecoveryEntryId === null;
|
|
893
|
+
const transientOpenLogEntryId = typeof invalidEmissionRecoveryEntryId === "number"
|
|
894
|
+
? invalidEmissionRecoveryEntryId
|
|
895
|
+
: null;
|
|
695
896
|
// === Turn-as-container model ===
|
|
696
897
|
//
|
|
697
898
|
// Turn rows are created at runTurn OPEN (status=102, placeholder
|
|
@@ -748,14 +949,14 @@ export default class Engine {
|
|
|
748
949
|
// Model ops dispatch after these pre-model rows.
|
|
749
950
|
let nextActionIndex = 1;
|
|
750
951
|
const turnOpenPaths = [];
|
|
751
|
-
// {§
|
|
752
|
-
// 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
|
|
753
954
|
// from the real ops below (not a static print — we lean into the genuine echo paradigm) and
|
|
754
|
-
// written at sequence 1, so it reads first
|
|
955
|
+
// written at sequence 1, so it reads first with the foisted results following.
|
|
755
956
|
const turnZeroMoves = [];
|
|
756
957
|
if (seq === 1) {
|
|
757
958
|
if (workerFirstLoop)
|
|
758
|
-
nextActionIndex = 2; // reserve sequence 1 for
|
|
959
|
+
nextActionIndex = 2; // reserve sequence 1 for initialization
|
|
759
960
|
// Operator doc READs (PLURNK_SERVICE_MD_<ALIAS>, {§actor-boundary-doc-injection}). The docs were materialized
|
|
760
961
|
// as worker://plurnk/<entry> entries by the plurnk worker (LoopDocs, via the
|
|
761
962
|
// {§actor-boundary} keystone); foist a READ of each into THIS turn-0 so the model
|
|
@@ -788,7 +989,6 @@ export default class Engine {
|
|
|
788
989
|
const promptPath = promptTarget(promptLoopSeq, seq);
|
|
789
990
|
const entry = {
|
|
790
991
|
channels: { body: { content: promptRow.prompt, mimetype: "text/markdown" } },
|
|
791
|
-
tags: [],
|
|
792
992
|
attributes: { openPaths },
|
|
793
993
|
};
|
|
794
994
|
await EntryCrud.writeEntry(promptPath.pathname, entry, systemCtx, "prompt", workerId);
|
|
@@ -859,102 +1059,81 @@ export default class Engine {
|
|
|
859
1059
|
// The warm materialized membership before deriving. This second pass is the
|
|
860
1060
|
// ordinary cheap change detector and captures any drift that landed meanwhile.
|
|
861
1061
|
const fsDivergences = await GitMembership.indexGitMembership(systemCtx);
|
|
862
|
-
|
|
1062
|
+
// {§packet-git-status} — one post-reconciliation snapshot supplies both
|
|
1063
|
+
// the compact packet summary and each causal file event's exact XY state.
|
|
1064
|
+
const gitStatus = await GitState.status(this.#db, workspaceId, this.#loopAborts.get(loopId)?.signal);
|
|
1065
|
+
await this.#logFsFictions(workspaceId, fsDivergences, gitStatus);
|
|
863
1066
|
// The refresh above may have changed bodies (including model/client edits
|
|
864
1067
|
// since the startup warm). Re-derive to completion before packet/model
|
|
865
1068
|
// construction. Membership is already current, so this pass does not
|
|
866
1069
|
// consume the filesystem divergences a second time.
|
|
867
1070
|
await this.#queueWorkspaceWarm(systemCtx, true, false);
|
|
868
1071
|
// Turn-0 catalog preview (PLURNK_SERVICE_FILES_ITEMS, {§actor-boundary-catalog-preview}):
|
|
869
|
-
// FIND surveys foisted into the worker's first model turn
|
|
870
|
-
//
|
|
871
|
-
//
|
|
872
|
-
// 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.
|
|
873
1075
|
if (seq === 1) {
|
|
874
1076
|
// {§operator-config-workspace-files-items} — workspace filesItems replaces the env default.
|
|
875
1077
|
const { filesItems: workspaceMI } = await WorkspaceSettings.read(this.#db, workspaceId);
|
|
876
1078
|
const filesItems = workspaceMI !== null ? normalizeFilesItems(workspaceMI) : readFilesItems();
|
|
877
1079
|
if (filesItems !== null && workerFirstLoop) { // {§actor-boundary-catalog-preview} — once per worker
|
|
878
|
-
// engine_scheme_catalog_summary is the workspace-bounded scheme source: ordered,
|
|
879
|
-
// one row per stored entry scheme. log:// is absent —
|
|
880
|
-
// it lives in log_entries, not the catalog (present-mode, the # Log section).
|
|
881
1080
|
const catalogSchemes = await this.#db.engine_scheme_catalog_summary.all({ workspace_id: workspaceId });
|
|
882
|
-
|
|
883
|
-
const
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
foistSchemes.push({ scheme: "file", shallow_items: 0 }); // Empty project surface still receives its orienting FIND.
|
|
894
|
-
for (const { scheme, shallow_items: shallowItems } of foistSchemes) {
|
|
895
|
-
const isFile = scheme === "file";
|
|
896
|
-
const pattern = this.#schemes.manifestFor(scheme)?.folderScopes === true ? "*" : "**";
|
|
897
|
-
// Only the file map takes PLURNK_SERVICE_FILES_ITEMS as a first-N cap;
|
|
898
|
-
// other schemes always render their complete one-level map. An empty file
|
|
899
|
-
// survey has no range: `<1,0>` would turn useful orientation into an error.
|
|
900
|
-
const cap = isFile && filesItems > 0 && shallowItems > 0 ? Math.min(filesItems, shallowItems) : null;
|
|
901
|
-
// The file survey foists as the BARE relative glob — the path shape plurnk.md
|
|
902
|
-
// teaches (`*`, `src/**`, `**/notes.md`; bare = project-relative) — so the turn-0
|
|
903
|
-
// exemplar and the log rows the model reads never train a leading-slash or
|
|
904
|
-
// file:/// habit the rest of the teaching contradicts.
|
|
905
|
-
const catalogFind = {
|
|
906
|
-
op: "FIND", suffix: "", signal: null,
|
|
907
|
-
target: isFile ? { kind: "local", raw: pattern } : {
|
|
908
|
-
kind: "url",
|
|
909
|
-
raw: `${scheme}:///${pattern}`,
|
|
910
|
-
scheme,
|
|
911
|
-
username: null, password: null, hostname: null, port: null,
|
|
912
|
-
pathname: `/${pattern}`,
|
|
913
|
-
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,
|
|
914
1092
|
},
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
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 });
|
|
923
1122
|
nextActionIndex++;
|
|
924
|
-
|
|
925
|
-
// own survey, mirrored OPEN). The <L> cap rides as `<1,N>`, exactly as the model would type it.
|
|
926
|
-
turnZeroMoves.push(`<<FIND(${isFile ? pattern : `${scheme}:///${pattern}`})${cap === null ? "" : `<1,${cap}>`}::FIND`);
|
|
1123
|
+
turnZeroMoves.push(exemplar);
|
|
927
1124
|
}
|
|
928
|
-
// The kernel's self-documenting surface — FIND(worker://plurnk/docs/**), uncapped,
|
|
929
|
-
// always ({§schemes-directory}, published under {§entry-owner}).
|
|
930
|
-
await Owner.kernelId(this.#db, workspaceId); // the row exists even before docs materialize — the empty survey is orienting, never 404
|
|
931
|
-
const kernelDocsFind = {
|
|
932
|
-
op: "FIND", suffix: "", signal: null,
|
|
933
|
-
target: { kind: "url", raw: "worker://plurnk/docs/**", scheme: "worker", username: null, password: null, hostname: "plurnk", port: null, pathname: "/docs/**", query: null, fragment: null },
|
|
934
|
-
body: null, lineMarker: null, position: UNKNOWN_POSITION,
|
|
935
|
-
};
|
|
936
|
-
await this.dispatch({ statement: kernelDocsFind, workspaceId, workerId, loopId, turnId, sequence: nextActionIndex, origin: "plurnk", onDispatch });
|
|
937
|
-
nextActionIndex++;
|
|
938
|
-
turnZeroMoves.push("<<FIND(worker://plurnk/docs/**)::FIND");
|
|
939
|
-
// {§worker-scheme} — the building worker's own scratch gets the same complete
|
|
940
|
-
// one-level map in its perspective alone. It always executes: an empty private
|
|
941
|
-
// space is useful orientation, not grounds to hide the surface.
|
|
942
|
-
const ownFind = {
|
|
943
|
-
op: "FIND", suffix: "", signal: null,
|
|
944
|
-
target: { kind: "url", raw: "worker://~/*", scheme: "worker", username: null, password: null, hostname: "~", port: null, pathname: "/*", query: null, fragment: null },
|
|
945
|
-
body: null, lineMarker: null, position: UNKNOWN_POSITION,
|
|
946
|
-
};
|
|
947
|
-
await this.dispatch({ statement: ownFind, workspaceId, workerId, loopId, turnId, sequence: nextActionIndex, origin: "plurnk", onDispatch });
|
|
948
|
-
nextActionIndex++;
|
|
949
|
-
turnZeroMoves.push("<<FIND(worker://~/*)::FIND"); // {§model-entry} — the own-space survey, into the turn-0 echo
|
|
950
1125
|
}
|
|
951
|
-
// {§
|
|
952
|
-
// 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
|
|
953
1128
|
// the true survey, never a frozen print — and OPEN: the worked example the model orients on,
|
|
954
|
-
// so the grammar can stay thin.
|
|
1129
|
+
// so the grammar can stay thin.
|
|
955
1130
|
if (workerFirstLoop) {
|
|
956
|
-
const
|
|
957
|
-
|
|
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 });
|
|
958
1137
|
}
|
|
959
1138
|
}
|
|
960
1139
|
// {§methods-loop-run-open-paths}: selected workspace paths belong to
|
|
@@ -986,10 +1165,8 @@ export default class Engine {
|
|
|
986
1165
|
await this.#reapTurnScopedStreams(workerId);
|
|
987
1166
|
nextActionIndex += await this.#materializeEnvironmentDeltas({ workspaceId, workerId, loopId, turnId, fromSequence: nextActionIndex });
|
|
988
1167
|
nextActionIndex += await this.#materializeStreamDeltas({ workerId, loopId, turnId, fromSequence: nextActionIndex });
|
|
989
|
-
//
|
|
990
|
-
//
|
|
991
|
-
// rebuild too so it isn't re-shelled on overflow.
|
|
992
|
-
const gitStatus = await GitState.status(this.#db, workspaceId, this.#loopAborts.get(loopId)?.signal);
|
|
1168
|
+
// The post-reconciliation Git snapshot above is threaded into the packet
|
|
1169
|
+
// and every budget rebuild; overflow never shells again.
|
|
993
1170
|
// Notices are non-terminal observations, never operation-failure truth.
|
|
994
1171
|
// Drain once and thread the same set through every grinder rebuild.
|
|
995
1172
|
const notices = this.#notices.drain(loopId)
|
|
@@ -999,95 +1176,56 @@ export default class Engine {
|
|
|
999
1176
|
// written (if turn 1) is part of that query result.
|
|
1000
1177
|
let requestPacket = await this.#packets.buildRequestPacket({
|
|
1001
1178
|
initialMessages: messages, requirements, workspaceId, workerId, loopId,
|
|
1002
|
-
currentTurnSeq: seq, provider, gitStatus, notices,
|
|
1179
|
+
currentTurnSeq: seq, provider, gitStatus, notices, transientOpenLogEntryId,
|
|
1003
1180
|
});
|
|
1004
1181
|
// SPEC {§grinder} — budget grinder, pre-LLM: reclaim window on actual overflow.
|
|
1005
1182
|
const enforced = await this.#packets.enforceBudget({
|
|
1006
|
-
packet: requestPacket, provider,
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
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
|
+
},
|
|
1012
1195
|
rebuild: () => this.#packets.buildRequestPacket({
|
|
1013
1196
|
initialMessages: messages, requirements, workspaceId, workerId, loopId,
|
|
1014
|
-
currentTurnSeq: seq, provider, gitStatus, notices,
|
|
1197
|
+
currentTurnSeq: seq, provider, gitStatus, notices, transientOpenLogEntryId,
|
|
1015
1198
|
}),
|
|
1016
1199
|
});
|
|
1017
|
-
if (enforced.recorded)
|
|
1018
|
-
nextActionIndex += 1; // a fold-to-fit Problem consumed the reserved sequence
|
|
1019
1200
|
requestPacket = enforced.packet;
|
|
1020
|
-
let operationConstraint;
|
|
1021
1201
|
if (!enforced.fit) {
|
|
1022
|
-
// {§
|
|
1023
|
-
//
|
|
1024
|
-
//
|
|
1025
|
-
|
|
1026
|
-
if (
|
|
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) {
|
|
1027
1207
|
const ceiling = this.#packets.ceilingFor(provider);
|
|
1028
1208
|
if (ceiling === null) {
|
|
1029
|
-
throw new Error("an unbounded prompt budget cannot enter
|
|
1209
|
+
throw new Error("an unbounded prompt budget cannot enter context-envelope admission");
|
|
1030
1210
|
}
|
|
1211
|
+
const failure = contextEnvelopeFailure(budgetPressure(requestPacket.tokens, ceiling), contextAdmission);
|
|
1031
1212
|
await this.#problems.record({
|
|
1032
1213
|
workerId,
|
|
1033
1214
|
loopId,
|
|
1034
1215
|
turnId,
|
|
1035
1216
|
sequence: nextActionIndex++,
|
|
1036
|
-
origin: "
|
|
1217
|
+
origin: "plurnk",
|
|
1037
1218
|
source: "engine",
|
|
1038
|
-
result:
|
|
1219
|
+
result: failure,
|
|
1039
1220
|
});
|
|
1040
|
-
//
|
|
1041
|
-
//
|
|
1221
|
+
// Preserve the exact terminal evidence in the stored request. It is
|
|
1222
|
+
// never sent because admission has already failed.
|
|
1042
1223
|
requestPacket = await this.#packets.buildRequestPacket({
|
|
1043
1224
|
initialMessages: messages, requirements, workspaceId, workerId, loopId,
|
|
1044
|
-
currentTurnSeq: seq, provider, gitStatus, notices,
|
|
1225
|
+
currentTurnSeq: seq, provider, gitStatus, notices, transientOpenLogEntryId,
|
|
1045
1226
|
});
|
|
1046
|
-
physicalAdmission = await this.#packets.physicalAdmission(requestPacket, provider, this.#loopAborts.get(loopId)?.signal);
|
|
1047
|
-
if (physicalAdmission.admitted) {
|
|
1048
|
-
this.#hardOverflowRecovery.add(loopId);
|
|
1049
|
-
operationConstraint = {
|
|
1050
|
-
code: "budget-recovery",
|
|
1051
|
-
detail: "budget recovery is active",
|
|
1052
|
-
allowedOperations: BudgetOverflow.recoveryOperations,
|
|
1053
|
-
};
|
|
1054
|
-
}
|
|
1055
|
-
}
|
|
1056
|
-
if (operationConstraint === undefined) {
|
|
1057
|
-
// Hard 413: physically unsendable, or still over after the constrained recovery turn.
|
|
1058
|
-
const ceiling = this.#packets.ceilingFor(provider);
|
|
1059
|
-
if (ceiling === null) {
|
|
1060
|
-
throw new Error("an unbounded prompt budget cannot hard-stop");
|
|
1061
|
-
}
|
|
1062
|
-
if (!enforced.recorded || !physicalAdmission.admitted) {
|
|
1063
|
-
await this.#problems.record({
|
|
1064
|
-
workerId,
|
|
1065
|
-
loopId,
|
|
1066
|
-
turnId,
|
|
1067
|
-
sequence: nextActionIndex++,
|
|
1068
|
-
origin: "plurnk",
|
|
1069
|
-
source: "engine",
|
|
1070
|
-
result: BudgetOverflow.result(requestPacket.tokens, ceiling, false, physicalAdmission.admitted
|
|
1071
|
-
? undefined
|
|
1072
|
-
: {
|
|
1073
|
-
reason: physicalAdmission.reason,
|
|
1074
|
-
detail: physicalAdmission.detail,
|
|
1075
|
-
capacity: physicalAdmission.capacity,
|
|
1076
|
-
tokens: physicalAdmission.measurement?.tokens,
|
|
1077
|
-
tokenKind: physicalAdmission.measurement?.kind,
|
|
1078
|
-
tokenSource: physicalAdmission.measurement?.source,
|
|
1079
|
-
}),
|
|
1080
|
-
});
|
|
1081
|
-
requestPacket = await this.#packets.buildRequestPacket({
|
|
1082
|
-
initialMessages: messages, requirements, workspaceId, workerId, loopId,
|
|
1083
|
-
currentTurnSeq: seq, provider, gitStatus, notices,
|
|
1084
|
-
});
|
|
1085
|
-
}
|
|
1086
|
-
// Skip the LLM, close the turn, and let runLoop abandon.
|
|
1087
1227
|
await this.#db.engine_close_turn.run({
|
|
1088
1228
|
id: turnId, status: 413, packet: StoredPacket.stringify(requestPacket),
|
|
1089
|
-
usage_prompt: 0, usage_completion: 0, usage_reasoning: 0, usage_cached: 0,
|
|
1090
|
-
usage_cost: "[]", usage_cost_usd: 0,
|
|
1091
1229
|
// The attempted turn retains its effective allowance even
|
|
1092
1230
|
// when no provider exchange completed. {§tokenomics-client-gauge}
|
|
1093
1231
|
usage_prompt_budget: this.#packets.promptBudgetFor(provider),
|
|
@@ -1096,22 +1234,16 @@ export default class Engine {
|
|
|
1096
1234
|
return {
|
|
1097
1235
|
turnId,
|
|
1098
1236
|
status: 413,
|
|
1099
|
-
|
|
1237
|
+
outcomes: [],
|
|
1100
1238
|
fingerprint: "",
|
|
1101
|
-
budgetStruck: enforced.struck,
|
|
1102
1239
|
budgetHardStop: true,
|
|
1103
1240
|
steerStruck: false,
|
|
1104
1241
|
emissionAttempts: 0,
|
|
1105
1242
|
emissionExhausted: false,
|
|
1106
|
-
|
|
1243
|
+
budgetFailure: failure,
|
|
1107
1244
|
};
|
|
1108
1245
|
}
|
|
1109
1246
|
}
|
|
1110
|
-
else {
|
|
1111
|
-
// A fitting turn clears the recovery grant; a later independent overflow can earn
|
|
1112
|
-
// a fresh recovery turn (chronic overflow still strikes out via the rail).
|
|
1113
|
-
this.#hardOverflowRecovery.delete(loopId);
|
|
1114
|
-
}
|
|
1115
1247
|
const modelMessages = PacketWire.packetToWireMessages(requestPacket);
|
|
1116
1248
|
// Packet pressure and provider generation are independent. The grinder governs
|
|
1117
1249
|
// only the request packet; maxTokens comes only from the provider envelope and
|
|
@@ -1119,54 +1251,38 @@ export default class Engine {
|
|
|
1119
1251
|
let response;
|
|
1120
1252
|
let splitResponse;
|
|
1121
1253
|
let railGrammar;
|
|
1254
|
+
let railResponseGrammar;
|
|
1122
1255
|
let railEvidence;
|
|
1123
1256
|
let emissionAttempts = 0;
|
|
1124
|
-
const usage = { prompt: 0, completion: 0, reasoning: 0, cached: 0 };
|
|
1125
|
-
let usageCostUsd = 0;
|
|
1126
|
-
const providerCosts = [];
|
|
1127
1257
|
let providerCallInFlight = false;
|
|
1128
1258
|
let providerAttemptSequence = 0;
|
|
1259
|
+
let providerAttemptId = null;
|
|
1260
|
+
let providerModelCall = null;
|
|
1129
1261
|
let providerAttemptAttributions = [];
|
|
1130
1262
|
const providerSignal = this.#loopAborts.get(loopId)?.signal ?? signal;
|
|
1131
1263
|
// {§client-metadata}
|
|
1132
1264
|
const { client } = await WorkspaceSettings.read(this.#db, workspaceId);
|
|
1133
|
-
const
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
await this.#db.
|
|
1138
|
-
|
|
1139
|
-
sequence,
|
|
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({
|
|
1270
|
+
id,
|
|
1140
1271
|
accepted: accepted ? 1 : 0,
|
|
1141
|
-
response: JSON.stringify(attemptResponse),
|
|
1142
1272
|
parse_errors: JSON.stringify(attemptSplit.parseErrors),
|
|
1143
|
-
attributions: JSON.stringify(attributions),
|
|
1144
|
-
usage_prompt: attemptUsage.prompt,
|
|
1145
|
-
usage_completion: attemptUsage.completion,
|
|
1146
|
-
usage_reasoning: attemptUsage.reasoning,
|
|
1147
|
-
usage_cached: attemptUsage.cached,
|
|
1148
|
-
usage_cost: JSON.stringify(attemptCost),
|
|
1149
|
-
usage_cost_usd: attemptCostUsd,
|
|
1150
|
-
finish_reason: attemptSplit.callMetadata.finishReason,
|
|
1151
|
-
model: attemptSplit.callMetadata.model,
|
|
1152
1273
|
});
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
usage.cached += attemptUsage.cached;
|
|
1157
|
-
providerCosts.push(attemptCost);
|
|
1158
|
-
usageCostUsd = usageCostUsd === null || attemptCostUsd === null
|
|
1159
|
-
? null
|
|
1160
|
-
: usageCostUsd + attemptCostUsd;
|
|
1274
|
+
if (result.changes !== 1) {
|
|
1275
|
+
throw new Error(`emission attempt ${id} was not awaiting classification`);
|
|
1276
|
+
}
|
|
1161
1277
|
emissionAttempts = sequence;
|
|
1162
1278
|
};
|
|
1163
1279
|
try {
|
|
1164
1280
|
// {§turn-lifecycle}: bracket the complete provider-attempt window with liveness notices.
|
|
1165
1281
|
if (!signal?.aborted)
|
|
1166
1282
|
this.#notices.push(workspaceId, loopId, { source: "engine:turn", kind: "turn_awaiting_model", level: "info", message: "awaiting model response" });
|
|
1167
|
-
const
|
|
1168
|
-
railGrammar =
|
|
1169
|
-
|
|
1283
|
+
const railConstraint = await this.#grammarConstraint(provider);
|
|
1284
|
+
railGrammar = railConstraint?.transport;
|
|
1285
|
+
railResponseGrammar = railConstraint?.response;
|
|
1170
1286
|
const attemptLimit = readEmissionAttempts();
|
|
1171
1287
|
const maxTokens = this.#packets.maxTokensFor(provider) ?? undefined;
|
|
1172
1288
|
const strikeStreak = this.#strikes.streak(loopId);
|
|
@@ -1178,7 +1294,7 @@ export default class Engine {
|
|
|
1178
1294
|
providerAttemptSequence = attempt;
|
|
1179
1295
|
const attributionContext = Object.freeze({
|
|
1180
1296
|
workspaceId: String(workspaceId),
|
|
1181
|
-
workerId:
|
|
1297
|
+
workerId: providerWorkerId,
|
|
1182
1298
|
primaryWorkerId,
|
|
1183
1299
|
loop: loopSeq,
|
|
1184
1300
|
turn: seq,
|
|
@@ -1186,40 +1302,67 @@ export default class Engine {
|
|
|
1186
1302
|
});
|
|
1187
1303
|
providerAttemptAttributions = await this.#attemptAttributions(provider, attributionContext);
|
|
1188
1304
|
requestPacket = { ...requestPacket, attributions: providerAttemptAttributions };
|
|
1305
|
+
providerModelCall = await ModelCall.open(this.#db, {
|
|
1306
|
+
turnId,
|
|
1307
|
+
sequence: attempt,
|
|
1308
|
+
kind: "emission",
|
|
1309
|
+
attributions: providerAttemptAttributions,
|
|
1310
|
+
model: provider.model,
|
|
1311
|
+
});
|
|
1312
|
+
const attemptRow = await this.#db.engine_open_turn_attempt.get({
|
|
1313
|
+
model_call_id: providerModelCall.id,
|
|
1314
|
+
});
|
|
1315
|
+
if (attemptRow === undefined) {
|
|
1316
|
+
throw new Error(`Engine.runTurn: provider attempt ${attempt} did not open`);
|
|
1317
|
+
}
|
|
1318
|
+
providerAttemptId = attemptRow.id;
|
|
1189
1319
|
providerCallInFlight = true;
|
|
1320
|
+
const currentModelCall = providerModelCall;
|
|
1190
1321
|
const completedResponse = await observed(// {§observability-boundary}
|
|
1191
1322
|
"provider.generate", { model: provider.model, attempt }, async (span) => {
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
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
|
+
}
|
|
1215
1358
|
});
|
|
1216
1359
|
response = completedResponse;
|
|
1217
|
-
|
|
1360
|
+
await currentModelCall.observeResponse(completedResponse);
|
|
1218
1361
|
railEvidence = railGrammar === undefined
|
|
1219
1362
|
? undefined
|
|
1220
|
-
:
|
|
1363
|
+
: _a.#requireGrammarEvidence(completedResponse);
|
|
1221
1364
|
splitResponse = this.#splitResponse(completedResponse);
|
|
1222
|
-
await
|
|
1365
|
+
await classifyProviderAttempt(attemptRow.id, splitResponse, attempt, splitResponse.emissionValid);
|
|
1223
1366
|
if (splitResponse.emissionValid)
|
|
1224
1367
|
break;
|
|
1225
1368
|
}
|
|
@@ -1229,22 +1372,27 @@ export default class Engine {
|
|
|
1229
1372
|
catch (err) {
|
|
1230
1373
|
// This handler owns only provider-call failures. Parser, cost, SQL,
|
|
1231
1374
|
// and engine-contract failures retain their original source.
|
|
1375
|
+
if (err instanceof ModelCallPersistenceError || err instanceof ProviderAccountingIntegrityError)
|
|
1376
|
+
throw err;
|
|
1232
1377
|
if (!providerCallInFlight)
|
|
1233
1378
|
throw err;
|
|
1379
|
+
providerCallInFlight = false;
|
|
1380
|
+
if (providerAttemptId === null || providerModelCall === null) {
|
|
1381
|
+
throw new Error("provider call failed without durable model-call and attempt identities", { cause: err });
|
|
1382
|
+
}
|
|
1383
|
+
const failure = _a.#providerFailure(err, providerSignal);
|
|
1234
1384
|
// {§provider-interrupted-attempt} — a provider-declared interruption
|
|
1235
1385
|
// carries response evidence without becoming a completed exchange.
|
|
1236
1386
|
// Persist it as an unaccepted attempt before settling the failure.
|
|
1237
1387
|
if (err instanceof ProviderError && err.attempt !== undefined) {
|
|
1238
1388
|
response = err.attempt;
|
|
1389
|
+
await providerModelCall.observeResponse(response, failure);
|
|
1239
1390
|
splitResponse = this.#splitResponse(response);
|
|
1240
|
-
await
|
|
1391
|
+
await classifyProviderAttempt(providerAttemptId, splitResponse, providerAttemptSequence, false);
|
|
1241
1392
|
}
|
|
1242
1393
|
else {
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
reason: "provider call failed without response-bearing charge evidence",
|
|
1246
|
-
});
|
|
1247
|
-
usageCostUsd = null;
|
|
1394
|
+
await providerModelCall.fail(failure);
|
|
1395
|
+
emissionAttempts = providerAttemptSequence;
|
|
1248
1396
|
}
|
|
1249
1397
|
// {§turn-never-blank} — a ProviderError means no completed exchange exists.
|
|
1250
1398
|
// Persist its exact RFC 9457 result before propagating it. Grammar evidence
|
|
@@ -1258,12 +1406,6 @@ export default class Engine {
|
|
|
1258
1406
|
id: turnId,
|
|
1259
1407
|
status: providerSignal.reason === LOOP_TIMEOUT_REASON ? 504 : 499,
|
|
1260
1408
|
packet: StoredPacket.stringify(requestPacket),
|
|
1261
|
-
usage_prompt: usage.prompt,
|
|
1262
|
-
usage_completion: usage.completion,
|
|
1263
|
-
usage_reasoning: usage.reasoning,
|
|
1264
|
-
usage_cached: usage.cached,
|
|
1265
|
-
usage_cost: JSON.stringify(providerCosts),
|
|
1266
|
-
usage_cost_usd: usageCostUsd,
|
|
1267
1409
|
usage_prompt_budget: this.#packets.promptBudgetFor(provider),
|
|
1268
1410
|
finish_reason: splitResponse?.callMetadata.finishReason ?? null,
|
|
1269
1411
|
model: splitResponse?.callMetadata.model ?? provider.model,
|
|
@@ -1271,15 +1413,6 @@ export default class Engine {
|
|
|
1271
1413
|
});
|
|
1272
1414
|
throw err;
|
|
1273
1415
|
}
|
|
1274
|
-
const failure = err instanceof ProviderError
|
|
1275
|
-
? { status: err.problem.status, problem: err.problem }
|
|
1276
|
-
: (() => {
|
|
1277
|
-
console.error("Provider failed outside its Problem Details contract:", err);
|
|
1278
|
-
return Results.failure("engine:provider", "provider-contract-violation", 502, "The provider failed without returning its required Problem Details.", {}, {
|
|
1279
|
-
stage: "provider-request",
|
|
1280
|
-
retryable: false,
|
|
1281
|
-
});
|
|
1282
|
-
})();
|
|
1283
1416
|
const recorded = await this.#problems.record({
|
|
1284
1417
|
workerId,
|
|
1285
1418
|
loopId,
|
|
@@ -1296,12 +1429,6 @@ export default class Engine {
|
|
|
1296
1429
|
id: turnId,
|
|
1297
1430
|
status: recorded.result.status,
|
|
1298
1431
|
packet: StoredPacket.stringify(requestPacket),
|
|
1299
|
-
usage_prompt: usage.prompt,
|
|
1300
|
-
usage_completion: usage.completion,
|
|
1301
|
-
usage_reasoning: usage.reasoning,
|
|
1302
|
-
usage_cached: usage.cached,
|
|
1303
|
-
usage_cost: JSON.stringify(providerCosts),
|
|
1304
|
-
usage_cost_usd: usageCostUsd,
|
|
1305
1432
|
usage_prompt_budget: this.#packets.promptBudgetFor(provider),
|
|
1306
1433
|
finish_reason: splitResponse?.callMetadata.finishReason ?? null,
|
|
1307
1434
|
model: splitResponse?.callMetadata.model ?? provider.model,
|
|
@@ -1309,20 +1436,36 @@ export default class Engine {
|
|
|
1309
1436
|
});
|
|
1310
1437
|
throw new OperationFailureError(recorded.result, { cause: err });
|
|
1311
1438
|
}
|
|
1312
|
-
if (response === undefined || splitResponse === undefined) {
|
|
1439
|
+
if (response === undefined || splitResponse === undefined || providerModelCall === null) {
|
|
1313
1440
|
throw new Error("provider attempt loop completed without a response");
|
|
1314
1441
|
}
|
|
1442
|
+
const emissionModelCallId = providerModelCall.id;
|
|
1315
1443
|
if (!splitResponse.emissionValid) {
|
|
1444
|
+
// {§invalid-emission-attempts} The first consecutive exhaustion
|
|
1445
|
+
// publishes only the raw final response and generic recovery fact.
|
|
1446
|
+
let rejectedModelEntryId;
|
|
1447
|
+
if (allowInvalidEmissionRecovery) {
|
|
1448
|
+
rejectedModelEntryId = await this.#dispatcher.writeModelEntry({
|
|
1449
|
+
verbatim: splitResponse.packetAssistant.content,
|
|
1450
|
+
workerId,
|
|
1451
|
+
loopId,
|
|
1452
|
+
turnId,
|
|
1453
|
+
sequence: nextActionIndex,
|
|
1454
|
+
folded: true,
|
|
1455
|
+
modelCallId: emissionModelCallId,
|
|
1456
|
+
admission: "rejected",
|
|
1457
|
+
});
|
|
1458
|
+
this.#notices.push(workspaceId, loopId, {
|
|
1459
|
+
source: "engine:grammar",
|
|
1460
|
+
kind: "invalid_emission",
|
|
1461
|
+
level: "error",
|
|
1462
|
+
message: INVALID_EMISSION_RECOVERY_MESSAGE,
|
|
1463
|
+
});
|
|
1464
|
+
}
|
|
1316
1465
|
await this.#db.engine_close_turn.run({
|
|
1317
1466
|
id: turnId,
|
|
1318
|
-
status: 500,
|
|
1467
|
+
status: allowInvalidEmissionRecovery ? TURN_STATUS_IMPLICIT_CONTINUE : 500,
|
|
1319
1468
|
packet: StoredPacket.stringify(requestPacket),
|
|
1320
|
-
usage_prompt: usage.prompt,
|
|
1321
|
-
usage_completion: usage.completion,
|
|
1322
|
-
usage_reasoning: usage.reasoning,
|
|
1323
|
-
usage_cached: usage.cached,
|
|
1324
|
-
usage_cost: JSON.stringify(providerCosts),
|
|
1325
|
-
usage_cost_usd: usageCostUsd,
|
|
1326
1469
|
usage_prompt_budget: this.#packets.promptBudgetFor(provider),
|
|
1327
1470
|
finish_reason: splitResponse.callMetadata.finishReason,
|
|
1328
1471
|
model: splitResponse.callMetadata.model,
|
|
@@ -1330,14 +1473,14 @@ export default class Engine {
|
|
|
1330
1473
|
});
|
|
1331
1474
|
return {
|
|
1332
1475
|
turnId,
|
|
1333
|
-
status: 500,
|
|
1334
|
-
|
|
1476
|
+
status: allowInvalidEmissionRecovery ? TURN_STATUS_IMPLICIT_CONTINUE : 500,
|
|
1477
|
+
outcomes: [],
|
|
1335
1478
|
fingerprint: "",
|
|
1336
|
-
budgetStruck: enforced.struck,
|
|
1337
1479
|
budgetHardStop: false,
|
|
1338
1480
|
steerStruck: false,
|
|
1339
1481
|
emissionAttempts,
|
|
1340
1482
|
emissionExhausted: true,
|
|
1483
|
+
...(rejectedModelEntryId === undefined ? {} : { rejectedModelEntryId }),
|
|
1341
1484
|
};
|
|
1342
1485
|
}
|
|
1343
1486
|
// {§packet-stored-shape} — admitted emission data extends the packet;
|
|
@@ -1348,7 +1491,7 @@ export default class Engine {
|
|
|
1348
1491
|
}
|
|
1349
1492
|
// Non-fatal provider transport notices on an accepted turn. Forward each
|
|
1350
1493
|
// Notice with a content-offset `line:col`;
|
|
1351
|
-
// the model resolves it against its own emission — READ the folded
|
|
1494
|
+
// the model resolves it against its own emission — READ the folded model-emission row at the
|
|
1352
1495
|
// cited lines ({§model-entry}) — not an embedded snippet that would duplicate the emission.
|
|
1353
1496
|
for (const notice of response.notices ?? []) {
|
|
1354
1497
|
const located = typeof notice.position === "number"
|
|
@@ -1370,12 +1513,14 @@ export default class Engine {
|
|
|
1370
1513
|
if (railGrammar !== undefined) {
|
|
1371
1514
|
if (railEvidence === undefined)
|
|
1372
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");
|
|
1373
1518
|
let verdict = null;
|
|
1374
1519
|
try {
|
|
1375
|
-
verdict = validateGbnf(
|
|
1520
|
+
verdict = validateGbnf(railResponseGrammar, railEvidence.input);
|
|
1376
1521
|
}
|
|
1377
1522
|
catch (cause) {
|
|
1378
|
-
|
|
1523
|
+
_a.#warnRailVerdictGapOnce(cause.message);
|
|
1379
1524
|
}
|
|
1380
1525
|
railKeys = {
|
|
1381
1526
|
railsAttached: railEvidence.transported ? "client" : "withheld",
|
|
@@ -1442,12 +1587,6 @@ export default class Engine {
|
|
|
1442
1587
|
id: turnId,
|
|
1443
1588
|
status: turnStatus,
|
|
1444
1589
|
packet: StoredPacket.stringify(packet),
|
|
1445
|
-
usage_prompt: usage.prompt,
|
|
1446
|
-
usage_completion: usage.completion,
|
|
1447
|
-
usage_reasoning: usage.reasoning,
|
|
1448
|
-
usage_cached: usage.cached,
|
|
1449
|
-
usage_cost: JSON.stringify(providerCosts),
|
|
1450
|
-
usage_cost_usd: usageCostUsd,
|
|
1451
1590
|
usage_prompt_budget: this.#packets.promptBudgetFor(provider), // {§tokenomics-client-gauge}
|
|
1452
1591
|
finish_reason: callMetadata.finishReason,
|
|
1453
1592
|
model: callMetadata.model,
|
|
@@ -1471,22 +1610,19 @@ export default class Engine {
|
|
|
1471
1610
|
// (the PLAN/SEND ops, zero actions), which is its only coherent meaning.
|
|
1472
1611
|
let realCommands = 0;
|
|
1473
1612
|
const admittedOps = packetAssistant.ops.filter((op) => {
|
|
1474
|
-
const constrained = operationConstraint !== undefined
|
|
1475
|
-
&& !operationConstraint.allowedOperations.includes(op.op);
|
|
1476
1613
|
return op.op === "PLAN"
|
|
1477
1614
|
|| op === sendOp
|
|
1478
|
-
|| constrained
|
|
1479
1615
|
|| realCommands++ < maxCommands;
|
|
1480
1616
|
});
|
|
1481
1617
|
const opsToDispatch = scheduleTurnOps(admittedOps);
|
|
1482
|
-
await this.#dispatcher.prepareEditBatches(opsToDispatch.filter((statement) => statement.op === "EDIT"
|
|
1483
|
-
&& (operationConstraint === undefined
|
|
1484
|
-
|| operationConstraint.allowedOperations.includes(statement.op))), {
|
|
1618
|
+
await this.#dispatcher.prepareEditBatches(opsToDispatch.filter((statement) => statement.op === "EDIT"), {
|
|
1485
1619
|
workspaceId, workerId, loopId, turnId,
|
|
1486
1620
|
origin, onDispatch,
|
|
1487
1621
|
});
|
|
1488
1622
|
const droppedCount = opsCount - opsToDispatch.length;
|
|
1489
|
-
const
|
|
1623
|
+
const bareStatements = opsToDispatch.filter((statement) => statement.op === "BARE");
|
|
1624
|
+
let bareResults = null;
|
|
1625
|
+
const outcomes = [];
|
|
1490
1626
|
// Running counter — a multi-file READ writes N rows from one statement (rowsWritten),
|
|
1491
1627
|
// so the next op's sequence picks up after them. Collapses to nextActionIndex+i when
|
|
1492
1628
|
// every op writes one row (the common case).
|
|
@@ -1513,7 +1649,7 @@ export default class Engine {
|
|
|
1513
1649
|
retryable: false,
|
|
1514
1650
|
}),
|
|
1515
1651
|
});
|
|
1516
|
-
|
|
1652
|
+
outcomes.push({ op: null, status: recorded.result.status });
|
|
1517
1653
|
onDispatch?.(recorded.id);
|
|
1518
1654
|
}
|
|
1519
1655
|
};
|
|
@@ -1522,20 +1658,59 @@ export default class Engine {
|
|
|
1522
1658
|
&& typeof statement.signal === "number"
|
|
1523
1659
|
&& TERMINAL_SEND_SIGNALS.has(statement.signal)) {
|
|
1524
1660
|
await recordRecoverableParseErrors();
|
|
1661
|
+
// {§worker-optimistic-settlement} — SEND judges the refreshed
|
|
1662
|
+
// lifecycle after this turn's own fast streams receive one
|
|
1663
|
+
// bounded opportunity to conclude. This is not a sleep and
|
|
1664
|
+
// does not delay a later turn for older monitored streams.
|
|
1665
|
+
const execHandler = this.#schemes.get("exec");
|
|
1666
|
+
await execHandler?.settleTurnSpawns?.(workerId, turnId, readOptimisticSettlementMs(), providerSignal);
|
|
1525
1667
|
}
|
|
1526
1668
|
const result = await observed(// {§observability-boundary}
|
|
1527
1669
|
"op.dispatch", { op: statement.op }, async (span) => {
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
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
|
+
}
|
|
1534
1709
|
span.setAttribute("status", dispatchResult.status);
|
|
1535
1710
|
recordCounter(OPS_DISPATCHED, { op: statement.op, status: dispatchResult.status });
|
|
1536
1711
|
return dispatchResult;
|
|
1537
1712
|
});
|
|
1538
|
-
|
|
1713
|
+
outcomes.push({ op: statement.op, status: result.status });
|
|
1539
1714
|
for (const normalization of result.scopeNormalizations ?? []) {
|
|
1540
1715
|
this.#notices.push(workspaceId, loopId, {
|
|
1541
1716
|
source: "engine:slicer",
|
|
@@ -1591,7 +1766,7 @@ export default class Engine {
|
|
|
1591
1766
|
}
|
|
1592
1767
|
: {
|
|
1593
1768
|
stage: "turn",
|
|
1594
|
-
recovery: "Perform an operation before continuing with
|
|
1769
|
+
recovery: "Perform an operation before continuing with `## SEND0 [102]`.",
|
|
1595
1770
|
retryable: false,
|
|
1596
1771
|
};
|
|
1597
1772
|
await this.#problems.record({
|
|
@@ -1617,14 +1792,22 @@ export default class Engine {
|
|
|
1617
1792
|
? response.assistant.reasoningEncrypted
|
|
1618
1793
|
: undefined;
|
|
1619
1794
|
if (packetAssistant.content.trim().length > 0 || reasoningItems !== undefined) {
|
|
1620
|
-
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
|
+
});
|
|
1621
1805
|
}
|
|
1622
1806
|
return {
|
|
1623
1807
|
turnId,
|
|
1624
1808
|
status: turnStatus,
|
|
1625
|
-
|
|
1809
|
+
outcomes,
|
|
1626
1810
|
fingerprint: StrikeRail.fingerprintTurn(packetAssistant.ops),
|
|
1627
|
-
budgetStruck: enforced.struck,
|
|
1628
1811
|
budgetHardStop: false,
|
|
1629
1812
|
steerStruck,
|
|
1630
1813
|
emissionAttempts,
|
|
@@ -1633,7 +1816,8 @@ export default class Engine {
|
|
|
1633
1816
|
}
|
|
1634
1817
|
// Split the wire-level ProviderResponse into the two destinations:
|
|
1635
1818
|
// packet.assistant gets the model's emission (content, ops, reasoning);
|
|
1636
|
-
// Turn columns get
|
|
1819
|
+
// Turn columns get accepted-call metadata (finishReason, model). Physical
|
|
1820
|
+
// request usage and cost have their own cardinal persistence path.
|
|
1637
1821
|
// {§provider-surface} Text-fragment scraping policy lives
|
|
1638
1822
|
// here — engine owns the parse and the scraping rule, providers stay
|
|
1639
1823
|
// grammar-unaware.
|
|
@@ -1647,10 +1831,8 @@ export default class Engine {
|
|
|
1647
1831
|
const preParsedOps = assistant.ops;
|
|
1648
1832
|
const ops = [];
|
|
1649
1833
|
// PLAN is an ordinary op — emitted by the model, dispatched, and passed to the
|
|
1650
|
-
// client as a log entry. No special hoisting into the reasoning field
|
|
1651
|
-
//
|
|
1652
|
-
// #free-text-capture synthesis of SEND[103] log ops was retired as tech debt
|
|
1653
|
-
// (grammar 0.70 forbids free text between ops, so a prose-only turn strikes 422).
|
|
1834
|
+
// client as a log entry. No special hoisting into the reasoning field. Only
|
|
1835
|
+
// structured operations are executable; interstitial text is not an operation.
|
|
1654
1836
|
// Full PlurnkParseError context is preserved on rejected attempt evidence;
|
|
1655
1837
|
// warnings remain admissible Notices. {§parse-diagnostics}
|
|
1656
1838
|
const parseErrors = [];
|
|
@@ -1671,7 +1853,6 @@ export default class Engine {
|
|
|
1671
1853
|
if (item.kind === "statement") {
|
|
1672
1854
|
ops.push(item.statement);
|
|
1673
1855
|
}
|
|
1674
|
-
// Free text (kind "text") is dropped — #free-text-capture retired (above).
|
|
1675
1856
|
else if (item.kind === "error") {
|
|
1676
1857
|
const err = item.error;
|
|
1677
1858
|
if (err instanceof PlurnkParseError) {
|
|
@@ -1726,7 +1907,7 @@ export default class Engine {
|
|
|
1726
1907
|
const reasoning = assistant.reasoning ?? null;
|
|
1727
1908
|
return {
|
|
1728
1909
|
packetAssistant: { content: assistant.content, ops, reasoning },
|
|
1729
|
-
callMetadata: {
|
|
1910
|
+
callMetadata: { finishReason: assistant.finishReason, model: assistant.model },
|
|
1730
1911
|
parseErrors,
|
|
1731
1912
|
recoverableParseErrors: emissionValid ? recoverableParseErrors : [],
|
|
1732
1913
|
parseNotices,
|
|
@@ -1758,11 +1939,36 @@ export default class Engine {
|
|
|
1758
1939
|
|| r.op === null || r.status_rx === null)
|
|
1759
1940
|
continue;
|
|
1760
1941
|
const termination = r.kind === "loop_termination";
|
|
1761
|
-
|
|
1762
|
-
? BranchReceipt.append(markTerminal(r.terminated_by, r.rx) ?? `loop "${r.prompt ?? ""}" ended (${r.status_rx})`, await BranchReceipt.render(this.#db, r.producer_worker_id))
|
|
1763
|
-
: r.rx;
|
|
1764
|
-
if (rx === null)
|
|
1942
|
+
if (r.rx === null)
|
|
1765
1943
|
throw new Error(`ambient event ${r.event_id} has no materializable result`);
|
|
1944
|
+
const terminal = termination
|
|
1945
|
+
? TerminalResult.parse(r.rx, `ambient loop-termination event ${r.event_id}`)
|
|
1946
|
+
: null;
|
|
1947
|
+
if (terminal !== null && terminal.status !== r.status_rx) {
|
|
1948
|
+
throw new Error(`ambient loop-termination event ${r.event_id} status ${r.status_rx} does not match its terminal result status ${terminal.status}`);
|
|
1949
|
+
}
|
|
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
|
+
}
|
|
1959
|
+
if (terminal !== null) {
|
|
1960
|
+
const inherited = JSON.parse(attrs);
|
|
1961
|
+
if (inherited === null || typeof inherited !== "object" || Array.isArray(inherited)) {
|
|
1962
|
+
throw new TypeError(`ambient loop-termination event ${r.event_id} attrs must be an object`);
|
|
1963
|
+
}
|
|
1964
|
+
const receipt = await BranchReceipt.render(this.#db, r.producer_worker_id);
|
|
1965
|
+
attrs = JSON.stringify({
|
|
1966
|
+
...inherited,
|
|
1967
|
+
kind: "loop_termination",
|
|
1968
|
+
...(r.terminated_by === null ? {} : { terminatedBy: r.terminated_by }),
|
|
1969
|
+
...(receipt === null ? {} : { receipt }),
|
|
1970
|
+
});
|
|
1971
|
+
}
|
|
1766
1972
|
const inserted = await this.#db.engine_insert_ambient_delta.get({
|
|
1767
1973
|
worker_id: workerId, loop_id: loopId, turn_id: turnId, sequence: fromSequence + written,
|
|
1768
1974
|
event_id: r.event_id,
|
|
@@ -1771,12 +1977,21 @@ export default class Engine {
|
|
|
1771
1977
|
scheme: r.scheme,
|
|
1772
1978
|
hostname: r.hostname,
|
|
1773
1979
|
pathname: r.pathname,
|
|
1774
|
-
rx,
|
|
1775
|
-
mimetype_rx:
|
|
1980
|
+
rx: r.rx,
|
|
1981
|
+
mimetype_rx: "application/json",
|
|
1776
1982
|
status: r.status_rx,
|
|
1777
|
-
expanded:
|
|
1778
|
-
attrs
|
|
1983
|
+
expanded: terminal !== null && terminal.status >= 200 && terminal.status < 300 ? 1 : 0,
|
|
1984
|
+
attrs,
|
|
1779
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
|
+
}
|
|
1780
1995
|
if (inserted !== undefined)
|
|
1781
1996
|
written++;
|
|
1782
1997
|
}
|
|
@@ -1801,11 +2016,10 @@ export default class Engine {
|
|
|
1801
2016
|
return this.#liveSubscriptions.cancel(subscriptionId);
|
|
1802
2017
|
}
|
|
1803
2018
|
// {§env-delta} — exec streams as an instance of the ambient-observe machine:
|
|
1804
|
-
// each turn, emit each owned channel's
|
|
1805
|
-
// while the channel streams and preserves the exact terminal result when closed. Ongoing
|
|
1806
|
-
//
|
|
1807
|
-
//
|
|
1808
|
-
// env-observe loop with a byte cursor where env-delta uses a timestamp. {§exec-stream}
|
|
2019
|
+
// each turn, emit each owned channel's next publishable content as a foisted READ row. It is
|
|
2020
|
+
// 200 while the channel streams and preserves the exact terminal result when closed. Ongoing
|
|
2021
|
+
// observations fold; the terminal observation auto-OPENs. The cursor is the streamEnd recorded
|
|
2022
|
+
// on the channel's prior observation. {§exec-stream}
|
|
1809
2023
|
async #materializeStreamDeltas(args) {
|
|
1810
2024
|
const { workerId, loopId, turnId, fromSequence } = args;
|
|
1811
2025
|
const channels = await this.#db.engine_worker_stream_channels.all({ worker_id: workerId });
|
|
@@ -1847,14 +2061,15 @@ export default class Engine {
|
|
|
1847
2061
|
}
|
|
1848
2062
|
return result;
|
|
1849
2063
|
};
|
|
1850
|
-
|
|
1851
|
-
|
|
2064
|
+
const publishEnd = streamPublicationEnd(ch.content, ch.mimetype, cursor, closed);
|
|
2065
|
+
if (publishEnd <= cursor) {
|
|
2066
|
+
// The cursor-terminal race: a channel written in one final
|
|
1852
2067
|
// burst gets fully shown FOLDED while still active; the close then has zero new
|
|
1853
|
-
//
|
|
2068
|
+
// content and the auto-OPEN terminal observation never fired — the model was never shown
|
|
1854
2069
|
// the conclusion of a stream whose result it already holds folded. The same
|
|
1855
|
-
// observation is required when the
|
|
1856
|
-
// information independently of payload.
|
|
1857
|
-
// terse
|
|
2070
|
+
// observation is required when the channel produced no publishable content:
|
|
2071
|
+
// completion is information independently of payload. Text streams retain their
|
|
2072
|
+
// terse marker; structured channels emit a bodyless typed conclusion.
|
|
1858
2073
|
if (closed && priorAttrs.terminal !== true) {
|
|
1859
2074
|
const streamTarget = renderTarget({
|
|
1860
2075
|
scheme: ch.runtime,
|
|
@@ -1867,12 +2082,15 @@ export default class Engine {
|
|
|
1867
2082
|
? `full output already delivered above; READ ${streamTarget} to revisit`
|
|
1868
2083
|
: "stream produced no output";
|
|
1869
2084
|
const sequence = fromSequence + written;
|
|
2085
|
+
const content = baseMimetype(ch.mimetype).startsWith("text/")
|
|
2086
|
+
? `[ stream closed (${ch.close_status ?? 200}) - ${pointer} ]`
|
|
2087
|
+
: "";
|
|
1870
2088
|
await this.#db.engine_insert_stream_delta.run({
|
|
1871
2089
|
worker_id: workerId, loop_id: loopId, turn_id: turnId, sequence,
|
|
1872
2090
|
scheme: ch.runtime, pathname: ch.coord, fragment: visibleFragment,
|
|
1873
2091
|
rx: JSON.stringify(await terminalResult({
|
|
1874
|
-
content
|
|
1875
|
-
mimetype:
|
|
2092
|
+
content,
|
|
2093
|
+
mimetype: ch.mimetype,
|
|
1876
2094
|
}, sequence)),
|
|
1877
2095
|
status: terminal?.status ?? 200,
|
|
1878
2096
|
attrs: JSON.stringify({ streamEnd: ch.content.length, terminal: true }),
|
|
@@ -1886,16 +2104,19 @@ export default class Engine {
|
|
|
1886
2104
|
// into one sequence (lines N..M, then M+1..), not N independent "1:" restarts. {§exec-stream}
|
|
1887
2105
|
const startLine = (ch.content.slice(0, cursor).match(/\n/g)?.length ?? 0) + 1;
|
|
1888
2106
|
const sequence = fromSequence + written;
|
|
1889
|
-
const
|
|
1890
|
-
|
|
1891
|
-
|
|
2107
|
+
const content = ch.content.slice(cursor, publishEnd);
|
|
2108
|
+
const terminalDelivery = closed && publishEnd === ch.content.length;
|
|
2109
|
+
const fields = { content, mimetype: ch.mimetype, startLine };
|
|
2110
|
+
const result = terminalDelivery
|
|
2111
|
+
? await terminalResult(fields, sequence)
|
|
2112
|
+
: { status: 200, ...fields };
|
|
1892
2113
|
await this.#db.engine_insert_stream_delta.run({
|
|
1893
2114
|
worker_id: workerId, loop_id: loopId, turn_id: turnId, sequence,
|
|
1894
2115
|
scheme: ch.runtime, pathname: ch.coord, fragment: visibleFragment,
|
|
1895
2116
|
rx: JSON.stringify(result),
|
|
1896
2117
|
status: result.status,
|
|
1897
|
-
attrs: JSON.stringify({ streamEnd:
|
|
1898
|
-
expanded:
|
|
2118
|
+
attrs: JSON.stringify({ streamEnd: publishEnd, terminal: terminalDelivery }),
|
|
2119
|
+
expanded: terminalDelivery ? 1 : 0, // {§exec-stream} — terminal observation auto-OPENs; ongoing folds
|
|
1899
2120
|
});
|
|
1900
2121
|
written++;
|
|
1901
2122
|
}
|
|
@@ -1903,9 +2124,10 @@ export default class Engine {
|
|
|
1903
2124
|
}
|
|
1904
2125
|
// {§env-delta-filesystem-narration} {§membership-emi-divergence-signal}
|
|
1905
2126
|
// — journal project-file divergence once through the reserved actor.
|
|
1906
|
-
async #logFsFictions(workspaceId, divergences) {
|
|
2127
|
+
async #logFsFictions(workspaceId, divergences, gitStatus) {
|
|
1907
2128
|
if (divergences.length === 0)
|
|
1908
2129
|
return;
|
|
2130
|
+
const gitByPath = new Map(gitStatus?.files.map(({ path, status }) => [path, status]) ?? []);
|
|
1909
2131
|
const worker = await this.#db.envelope_get_worker_by_name.get({ workspace_id: workspaceId, name: "plurnk" })
|
|
1910
2132
|
?? await this.#db.envelope_insert_worker.get({ workspace_id: workspaceId, name: "plurnk", origin: "plurnk" });
|
|
1911
2133
|
if (worker === undefined)
|
|
@@ -1919,14 +2141,18 @@ export default class Engine {
|
|
|
1919
2141
|
const span = editedSpan(d.before, d.after);
|
|
1920
2142
|
await this.#db.engine_insert_log_entry.get({
|
|
1921
2143
|
worker_id: worker.id, loop_id: loop.id, turn_id: turn.id, sequence: sequence++,
|
|
1922
|
-
origin: "plurnk", source: "file",
|
|
2144
|
+
origin: "plurnk", source: "file", model_call_id: null,
|
|
2145
|
+
op: "EDIT", suffix: "", signal: null,
|
|
1923
2146
|
// Match Dispatcher.#extractTarget: a bare file address has NULL scheme
|
|
1924
2147
|
// only in log target metadata; its entry identity remains `file`.
|
|
1925
2148
|
scheme: null, username: null, password: null, hostname: null, port: null,
|
|
1926
2149
|
pathname: d.pathname, query: null, fragment: null, lineMarker: null,
|
|
1927
2150
|
tx: "", mimetype_tx: "text/plain",
|
|
1928
2151
|
rx: JSON.stringify({ status: 200, entryId: d.entryId, channel: d.channel, span }), mimetype_rx: "application/json",
|
|
1929
|
-
status_rx: 200, tokens: 0, state: "resolved", outcome: null,
|
|
2152
|
+
status_rx: 200, tokens: 0, state: "resolved", outcome: null,
|
|
2153
|
+
attrs: gitByPath.has(d.pathname)
|
|
2154
|
+
? JSON.stringify({ git: gitByPath.get(d.pathname) })
|
|
2155
|
+
: "{}",
|
|
1930
2156
|
});
|
|
1931
2157
|
}
|
|
1932
2158
|
}
|
|
@@ -1957,6 +2183,7 @@ export default class Engine {
|
|
|
1957
2183
|
sequence,
|
|
1958
2184
|
origin: "plurnk",
|
|
1959
2185
|
source: null,
|
|
2186
|
+
model_call_id: null,
|
|
1960
2187
|
op: "prompt",
|
|
1961
2188
|
suffix: "",
|
|
1962
2189
|
signal: null,
|
|
@@ -2083,11 +2310,12 @@ export default class Engine {
|
|
|
2083
2310
|
};
|
|
2084
2311
|
const entry = {
|
|
2085
2312
|
channels: { body: { content: prompt, mimetype: "text/markdown" } },
|
|
2086
|
-
tags: [],
|
|
2087
2313
|
attributes: { openPaths },
|
|
2088
2314
|
};
|
|
2089
2315
|
await EntryCrud.writeEntry(pathname, entry, ctx, "prompt", workerId);
|
|
2090
2316
|
return { loopId, turnSeq };
|
|
2091
2317
|
}
|
|
2092
2318
|
}
|
|
2319
|
+
_a = Engine;
|
|
2320
|
+
export default Engine;
|
|
2093
2321
|
//# sourceMappingURL=Engine.js.map
|