@plurnk/plurnk-service 0.44.0 → 0.46.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.example +41 -24
- package/README.md +37 -18
- package/SPEC.md +34 -10
- package/dist/core/ChannelWrite.d.ts +4 -0
- package/dist/core/ChannelWrite.d.ts.map +1 -1
- package/dist/core/ChannelWrite.js +9 -0
- package/dist/core/ChannelWrite.js.map +1 -1
- package/dist/core/ChannelWrite.sql +69 -0
- package/dist/core/Engine.d.ts.map +1 -1
- package/dist/core/Engine.js +146 -103
- package/dist/core/Engine.js.map +1 -1
- package/dist/core/Engine.sql +307 -0
- package/dist/core/ExecutorRegistry.d.ts +4 -2
- package/dist/core/ExecutorRegistry.d.ts.map +1 -1
- package/dist/core/ExecutorRegistry.js +20 -5
- package/dist/core/ExecutorRegistry.js.map +1 -1
- package/dist/core/SchemeRegistry.d.ts +3 -0
- package/dist/core/SchemeRegistry.d.ts.map +1 -1
- package/dist/core/SchemeRegistry.js +37 -0
- package/dist/core/SchemeRegistry.js.map +1 -1
- package/dist/core/fork.d.ts.map +1 -1
- package/dist/core/fork.js +8 -1
- package/dist/core/fork.js.map +1 -1
- package/dist/core/fork.sql +50 -0
- package/dist/core/packet-wire.d.ts +1 -0
- package/dist/core/packet-wire.d.ts.map +1 -1
- package/dist/core/packet-wire.js +11 -0
- package/dist/core/packet-wire.js.map +1 -1
- package/dist/core/plugin-attribution.d.ts +5 -0
- package/dist/core/plugin-attribution.d.ts.map +1 -0
- package/dist/core/plugin-attribution.js +39 -0
- package/dist/core/plugin-attribution.js.map +1 -0
- package/dist/core/run-ops.sql +16 -0
- package/dist/core/scheme-types.d.ts +1 -0
- package/dist/core/scheme-types.d.ts.map +1 -1
- package/dist/core/session-settings.d.ts +1 -0
- package/dist/core/session-settings.d.ts.map +1 -1
- package/dist/core/session-settings.js +2 -1
- package/dist/core/session-settings.js.map +1 -1
- package/dist/schemes/Exec.d.ts +2 -1
- package/dist/schemes/Exec.d.ts.map +1 -1
- package/dist/schemes/Exec.js +39 -22
- package/dist/schemes/Exec.js.map +1 -1
- package/dist/schemes/File.d.ts.map +1 -1
- package/dist/schemes/File.js +27 -3
- package/dist/schemes/File.js.map +1 -1
- package/dist/schemes/Log.sql +37 -0
- package/dist/schemes/_entry-crud.sql +88 -0
- package/dist/schemes/_entry-find.sql +31 -0
- package/dist/schemes/_entry-graph.sql +60 -0
- package/dist/schemes/_entry-manifest.d.ts.map +1 -1
- package/dist/schemes/_entry-manifest.js +11 -3
- package/dist/schemes/_entry-manifest.js.map +1 -1
- package/dist/schemes/_entry-ops.sql +20 -0
- package/dist/schemes/_entry-semantic.d.ts.map +1 -1
- package/dist/schemes/_entry-semantic.js +14 -7
- package/dist/schemes/_entry-semantic.js.map +1 -1
- package/dist/schemes/_entry-semantic.sql +80 -0
- package/dist/schemes/exec-abort.d.ts +11 -0
- package/dist/schemes/exec-abort.d.ts.map +1 -0
- package/dist/schemes/exec-abort.js +23 -0
- package/dist/schemes/exec-abort.js.map +1 -0
- package/dist/schemes/exec-env.js +1 -1
- package/dist/schemes/exec-env.js.map +1 -1
- package/dist/server/Daemon.d.ts.map +1 -1
- package/dist/server/Daemon.js +162 -76
- package/dist/server/Daemon.js.map +1 -1
- package/dist/server/clientTurn.sql +10 -0
- package/dist/server/drain.sql +92 -0
- package/dist/server/dsl.d.ts.map +1 -1
- package/dist/server/dsl.js +11 -4
- package/dist/server/dsl.js.map +1 -1
- package/dist/server/envelope.sql +75 -0
- package/dist/server/logEntry.sql +10 -0
- package/dist/server/methods/_dispatchAsClient.d.ts.map +1 -1
- package/dist/server/methods/_dispatchAsClient.js +11 -6
- package/dist/server/methods/_dispatchAsClient.js.map +1 -1
- package/dist/server/methods/entry_read.sql +21 -0
- package/dist/server/methods/log_read.sql +11 -0
- package/dist/server/methods/loop_run.d.ts.map +1 -1
- package/dist/server/methods/loop_run.js +31 -11
- package/dist/server/methods/loop_run.js.map +1 -1
- package/dist/server/methods/loop_run.sql +9 -0
- package/dist/server/methods/session_create.d.ts.map +1 -1
- package/dist/server/methods/session_create.js +10 -3
- package/dist/server/methods/session_create.js.map +1 -1
- package/dist/service.d.ts +6 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +221 -0
- package/dist/service.js.map +1 -0
- package/migrations/0000-00-00.01_schema.sql +2 -2
- package/package.json +27 -13
- package/bin/plurnk-service.ts +0 -176
- package/dist/core/ProviderRegistry.d.ts +0 -42
- package/dist/core/ProviderRegistry.d.ts.map +0 -1
- package/dist/core/ProviderRegistry.js +0 -72
- package/dist/core/ProviderRegistry.js.map +0 -1
- package/dist/core/line-marker.d.ts +0 -23
- package/dist/core/line-marker.d.ts.map +0 -1
- package/dist/core/line-marker.js +0 -321
- package/dist/core/line-marker.js.map +0 -1
- package/dist/core/matcher.d.ts +0 -12
- package/dist/core/matcher.d.ts.map +0 -1
- package/dist/core/matcher.js +0 -72
- package/dist/core/matcher.js.map +0 -1
- package/dist/core/mimetype-binary.d.ts +0 -6
- package/dist/core/mimetype-binary.d.ts.map +0 -1
- package/dist/core/mimetype-binary.js +0 -82
- package/dist/core/mimetype-binary.js.map +0 -1
- package/dist/core/path-mimetype.d.ts +0 -3
- package/dist/core/path-mimetype.d.ts.map +0 -1
- package/dist/core/path-mimetype.js +0 -47
- package/dist/core/path-mimetype.js.map +0 -1
- package/dist/core/plugin-trust.d.ts +0 -4
- package/dist/core/plugin-trust.d.ts.map +0 -1
- package/dist/core/plugin-trust.js +0 -23
- package/dist/core/plugin-trust.js.map +0 -1
- package/dist/providers/Mock.d.ts +0 -43
- package/dist/providers/Mock.d.ts.map +0 -1
- package/dist/providers/Mock.js +0 -36
- package/dist/providers/Mock.js.map +0 -1
- package/dist/server/methods/op_hide.d.ts +0 -5
- package/dist/server/methods/op_hide.d.ts.map +0 -1
- package/dist/server/methods/op_hide.js +0 -24
- package/dist/server/methods/op_hide.js.map +0 -1
- package/dist/server/methods/op_show.d.ts +0 -5
- package/dist/server/methods/op_show.d.ts.map +0 -1
- package/dist/server/methods/op_show.js +0 -24
- package/dist/server/methods/op_show.js.map +0 -1
- package/dist/server/methods/session_set_persona.d.ts +0 -5
- package/dist/server/methods/session_set_persona.d.ts.map +0 -1
- package/dist/server/methods/session_set_persona.js +0 -29
- package/dist/server/methods/session_set_persona.js.map +0 -1
package/dist/core/Engine.js
CHANGED
|
@@ -71,6 +71,7 @@ const readManifestItems = () => {
|
|
|
71
71
|
return null;
|
|
72
72
|
return normalizeManifestItems(Number.parseInt(raw, 10));
|
|
73
73
|
};
|
|
74
|
+
import { ProviderError } from "@plurnk/plurnk-providers";
|
|
74
75
|
// Resolution timeout — proposed entries auto-cancel if nothing arrives
|
|
75
76
|
// within this window. SPEC.md §engine-rails (proposal lifecycle) + §methods (loop.resolve).
|
|
76
77
|
const PROPOSAL_TIMEOUT_DEFAULT_MS = 300000;
|
|
@@ -365,13 +366,12 @@ class Engine {
|
|
|
365
366
|
}
|
|
366
367
|
this.#loopAborts.set(loopId, loopAbort);
|
|
367
368
|
// Cleanup splits by termination kind:
|
|
368
|
-
// - "graceful" (
|
|
369
|
-
//
|
|
370
|
-
//
|
|
371
|
-
//
|
|
372
|
-
// - "forceful" (max_turns,
|
|
373
|
-
//
|
|
374
|
-
// tear down immediately.
|
|
369
|
+
// - "graceful" (SEND[202] Accepted): in-flight streaming-scheme spawns
|
|
370
|
+
// are ALLOWED to outlive the loop — they complete naturally, write final
|
|
371
|
+
// channel state, and wake-on-completion (E.4) opens a fresh loop. 202 is
|
|
372
|
+
// the only terminal that means "keep my async work."
|
|
373
|
+
// - "forceful" (SEND[200] done, max_turns, strike, cancel, budget, 4xx/5xx):
|
|
374
|
+
// fire the loop-level abort so leftover spawns tear down. "Done" reaps.
|
|
375
375
|
const cleanup = (kind, reason) => {
|
|
376
376
|
if (kind === "forceful" && !loopAbort.signal.aborted) {
|
|
377
377
|
loopAbort.abort(reason ?? "loop_forceful_termination");
|
|
@@ -386,10 +386,10 @@ class Engine {
|
|
|
386
386
|
if (row === undefined)
|
|
387
387
|
throw new Error(`Engine.runLoop: loop ${loopId} not found`);
|
|
388
388
|
if (row.status !== 102) {
|
|
389
|
-
//
|
|
390
|
-
// (
|
|
391
|
-
//
|
|
392
|
-
cleanup(row.status
|
|
389
|
+
// Only 202 (Accepted) lets spawns outlive — it IS the async wake
|
|
390
|
+
// contract (E.4). Every other terminal, 200 included, reaps: "done"
|
|
391
|
+
// must not leak running execs. Trust the code's declared intent.
|
|
392
|
+
cleanup(row.status === 202 ? "graceful" : "forceful", `loop_terminal_${row.status}`);
|
|
393
393
|
return { turnIds, finalStatus: row.status, hitMaxTurns: false, reason: "external" };
|
|
394
394
|
}
|
|
395
395
|
if (maxTurns >= 0 && turnIds.length >= maxTurns) {
|
|
@@ -577,6 +577,7 @@ class Engine {
|
|
|
577
577
|
wakeRunNotify: this.#wakeRunNotify,
|
|
578
578
|
tokenize: this.#tokenize,
|
|
579
579
|
mimetypes: this.#mimetypes,
|
|
580
|
+
defaultChannelFor: (s) => this.#schemes.defaultChannelFor(s),
|
|
580
581
|
pushTelemetry: (event) => this.#pushTelemetry(sessionId, loopId, event),
|
|
581
582
|
};
|
|
582
583
|
// SPEC §membership D4/D5 — git-ls-files workspace membership, resolved at
|
|
@@ -599,7 +600,7 @@ class Engine {
|
|
|
599
600
|
// it, not a 404; same plurnk-origin foist as the operator docs.
|
|
600
601
|
if (seq === 1) {
|
|
601
602
|
// #231 — a session's client-chosen manifestItems REPLACES the env default outright.
|
|
602
|
-
const { manifestItems: sessionMI } = await SessionSettings.read(this.#db, sessionId);
|
|
603
|
+
const { manifestItems: sessionMI, autoReadAgents } = await SessionSettings.read(this.#db, sessionId);
|
|
603
604
|
const manifestItems = sessionMI !== null ? normalizeManifestItems(sessionMI) : readManifestItems();
|
|
604
605
|
if (manifestItems !== null) {
|
|
605
606
|
const manifestRead = {
|
|
@@ -618,6 +619,30 @@ class Engine {
|
|
|
618
619
|
});
|
|
619
620
|
nextActionIndex++;
|
|
620
621
|
}
|
|
622
|
+
// #250 — auto-READ the project's AGENTS.md scratchpad into THIS first model turn
|
|
623
|
+
// when the session opted in AND it's a member. The client picks it (gitignored by
|
|
624
|
+
// convention → not a git member); the engine READs it here so its body is part of
|
|
625
|
+
// turn-1's log — a normal file:/// member READ (the model sees only the READ; it
|
|
626
|
+
// stays read-write, so the model edits the scratchpad back as it evolves).
|
|
627
|
+
if (autoReadAgents === true) {
|
|
628
|
+
const agentsMember = await this.#db.crud_get_member_sig.get({ session_id: sessionId, scheme: null, pathname: "/AGENTS.md" });
|
|
629
|
+
if (agentsMember !== undefined) {
|
|
630
|
+
const agentsRead = {
|
|
631
|
+
op: "READ", suffix: "", signal: null, lineMarker: null,
|
|
632
|
+
target: {
|
|
633
|
+
kind: "url", raw: "file:///AGENTS.md", scheme: "file",
|
|
634
|
+
username: null, password: null, hostname: null, port: null,
|
|
635
|
+
pathname: "/AGENTS.md", params: {}, fragment: null,
|
|
636
|
+
},
|
|
637
|
+
body: null, position: { line: 1, column: 1 },
|
|
638
|
+
};
|
|
639
|
+
await this.dispatch({
|
|
640
|
+
statement: agentsRead, sessionId, runId, loopId, turnId,
|
|
641
|
+
sequence: nextActionIndex, origin: "plurnk", onDispatch,
|
|
642
|
+
});
|
|
643
|
+
nextActionIndex++;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
621
646
|
}
|
|
622
647
|
// §env-delta — pre-seed environment deltas (changes since this run last
|
|
623
648
|
// reconciled) as system EDIT rows, before the packet composes; advance
|
|
@@ -631,14 +656,14 @@ class Engine {
|
|
|
631
656
|
// queries log_entries scoped to the run — the prompt entry just
|
|
632
657
|
// written (if turn 1) is part of that query result.
|
|
633
658
|
let requestPacket = await this.#buildRequestPacket({
|
|
634
|
-
initialMessages: messages, requirements, runId, loopId,
|
|
659
|
+
initialMessages: messages, requirements, sessionId, runId, loopId,
|
|
635
660
|
currentTurnSeq: seq, provider, gitStatus,
|
|
636
661
|
});
|
|
637
662
|
// SPEC §grinder — budget grinder, pre-LLM: reclaim window on actual overflow.
|
|
638
663
|
const enforced = await this.#enforceBudget({
|
|
639
664
|
packet: requestPacket, provider, runId, loopId, turnId, sessionId, turnNumber,
|
|
640
665
|
rebuild: (telemetryErrors) => this.#buildRequestPacket({
|
|
641
|
-
initialMessages: messages, requirements, runId, loopId,
|
|
666
|
+
initialMessages: messages, requirements, sessionId, runId, loopId,
|
|
642
667
|
currentTurnSeq: seq, provider, telemetryErrors, gitStatus,
|
|
643
668
|
}),
|
|
644
669
|
});
|
|
@@ -661,7 +686,33 @@ class Engine {
|
|
|
661
686
|
// decode at the free window so a runaway can't reach the context wall.
|
|
662
687
|
const genCeiling = _a.computeCeiling(provider.contextSize, this.#budgetCeiling); // provider.contextSize, the immutable identity, read by the budget — §provider-surface-identity
|
|
663
688
|
const maxTokens = genCeiling === null ? undefined : Math.max(1, genCeiling - requestPacket.tokens);
|
|
664
|
-
|
|
689
|
+
let response;
|
|
690
|
+
// #249 — plugin attribution tags onto the per-turn generate() wire. Value is the
|
|
691
|
+
// active-plugin set (placeholder); real per-turn grounding is deferred.
|
|
692
|
+
const attributions = [...new Set([...this.#schemes.attributions(), ...(this.#executors?.attributions() ?? [])])].toSorted();
|
|
693
|
+
try {
|
|
694
|
+
response = await provider.generate({ messages: modelMessages, runId: String(runId), signal, grammar: await this.#grammarConstraint(), maxTokens, attributions: attributions.length > 0 ? attributions : undefined }); // §provider-surface-generate §provider-guarantees-single-call §provider-guarantees-signal-wired §attribution-plurnk-namespace-reserved
|
|
695
|
+
}
|
|
696
|
+
catch (err) {
|
|
697
|
+
// Every provider error surfaces as telemetry (the client/model sees the cause). #256:
|
|
698
|
+
// grammar_unenforced is the one the MODEL can recover from — the backend didn't
|
|
699
|
+
// constrain the GBNF, so this turn was rejected but a conforming emission next turn is
|
|
700
|
+
// accepted: fall through as an empty no-op turn so the strike rail retries. Every other
|
|
701
|
+
// kind (rate_limit, network_failure, unauthorized, …) is terminal — telemetry'd, then
|
|
702
|
+
// propagated to end the loop (rather than only the opaque loop.run rejection).
|
|
703
|
+
if (err instanceof ProviderError) {
|
|
704
|
+
this.#pushTelemetry(sessionId, loopId, { source: "provider", kind: err.kind, message: err.message });
|
|
705
|
+
if (err.kind !== "grammar_unenforced")
|
|
706
|
+
throw err;
|
|
707
|
+
response = {
|
|
708
|
+
assistant: { content: "", reasoning: null, usage: { prompt: requestPacket.tokens, completion: 0, reasoning: 0, cached: 0, total: requestPacket.tokens }, finishReason: null, model: provider.model },
|
|
709
|
+
assistantRaw: null,
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
else {
|
|
713
|
+
throw err;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
665
716
|
// Engine splits wire-level response: emission (content, reasoning,
|
|
666
717
|
// parsed ops) → packet.assistant per Packet.json §assistant;
|
|
667
718
|
// call-metadata (usage, finishReason, model) → Turn columns per
|
|
@@ -691,10 +742,10 @@ class Engine {
|
|
|
691
742
|
});
|
|
692
743
|
}
|
|
693
744
|
const opsCount = packetAssistant.ops.length;
|
|
694
|
-
//
|
|
695
|
-
//
|
|
745
|
+
// PLAN (reasoning) and informational SEND[103] are no-ops, not actions: both are
|
|
746
|
+
// excluded from the real-op count so a PLAN-only or prose-only turn still strikes
|
|
696
747
|
// as no-ops, and the terminal scan ignores 1xx so they never set turnStatus.
|
|
697
|
-
const realOpsCount = packetAssistant.ops.filter((op) => !(op.op === "SEND" && op.signal === 103 && op.target === null)).length;
|
|
748
|
+
const realOpsCount = packetAssistant.ops.filter((op) => op.op !== "PLAN" && !(op.op === "SEND" && op.signal === 103 && op.target === null)).length;
|
|
698
749
|
const sendOp = packetAssistant.ops.findLast((op) => op.op === "SEND" && typeof op.signal === "number" && op.signal >= 200);
|
|
699
750
|
// Rail #41 (revised): the per-turn requirement is "emit at least one
|
|
700
751
|
// op," not "emit a terminal SEND." SEND is purely a signal verb; many
|
|
@@ -728,7 +779,14 @@ class Engine {
|
|
|
728
779
|
// its emission was truncated.
|
|
729
780
|
// #232 — a session's maxCommands is a tighten-only ceiling: min() the env ceiling.
|
|
730
781
|
const maxCommands = Math.min(readMaxCommands(), (await SessionSettings.read(this.#db, sessionId)).maxCommands ?? Number.POSITIVE_INFINITY);
|
|
731
|
-
|
|
782
|
+
// PLAN (reasoning) and a terminal SEND (signal ≥ 200, the conclusion) are not
|
|
783
|
+
// actions — they always dispatch and never count against the cap. maxCommands
|
|
784
|
+
// bounds real actions only; maxCommands:0 still admits a plan and a conclusion
|
|
785
|
+
// (the PLAN/SEND ops, zero actions), which is its only coherent meaning.
|
|
786
|
+
let realCommands = 0;
|
|
787
|
+
const opsToDispatch = packetAssistant.ops.filter((op) => op.op === "PLAN"
|
|
788
|
+
|| (op.op === "SEND" && typeof op.signal === "number" && op.signal >= 200)
|
|
789
|
+
|| realCommands++ < maxCommands);
|
|
732
790
|
const droppedCount = opsCount - opsToDispatch.length;
|
|
733
791
|
const statuses = [];
|
|
734
792
|
for (const [i, statement] of opsToDispatch.entries()) {
|
|
@@ -772,46 +830,26 @@ class Engine {
|
|
|
772
830
|
const { assistant } = response;
|
|
773
831
|
const preParsedOps = assistant.ops;
|
|
774
832
|
const ops = [];
|
|
775
|
-
//
|
|
776
|
-
//
|
|
777
|
-
//
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
if (op.op !== "PLAN")
|
|
781
|
-
return false;
|
|
782
|
-
const raw = (op.body ?? "").trim();
|
|
783
|
-
if (raw.length > 0)
|
|
784
|
-
planFragments.push(raw);
|
|
785
|
-
return true;
|
|
786
|
-
};
|
|
833
|
+
// PLAN is an ordinary op — emitted by the model, dispatched, and passed to the
|
|
834
|
+
// client as a log entry. No special hoisting into the reasoning field (that
|
|
835
|
+
// legacy paradigm is abandoned). Interstitial free text is DROPPED — the prior
|
|
836
|
+
// #free-text-capture synthesis of SEND[103] log ops was retired as tech debt
|
|
837
|
+
// (grammar 0.70 forbids free text between ops, so a prose-only turn strikes 422).
|
|
787
838
|
// Full PlurnkParseError context (line/column/source) is preserved
|
|
788
839
|
// here so runTurn can build TelemetryEvent envelopes per the
|
|
789
840
|
// grammar 0.17.0 protocol — model needs position info to locate
|
|
790
841
|
// its own offending content on the next turn.
|
|
791
842
|
const parseErrors = [];
|
|
792
843
|
if (preParsedOps !== undefined) {
|
|
793
|
-
|
|
794
|
-
if (!hoistPlan(op))
|
|
795
|
-
ops.push(op);
|
|
844
|
+
ops.push(...preParsedOps);
|
|
796
845
|
}
|
|
797
846
|
else {
|
|
798
847
|
const parsed = PlurnkParser.parse(assistant.content);
|
|
799
848
|
for (const item of parsed.items) {
|
|
800
849
|
if (item.kind === "statement") {
|
|
801
|
-
|
|
802
|
-
ops.push(item.statement);
|
|
803
|
-
}
|
|
804
|
-
else if (item.kind === "text") {
|
|
805
|
-
// #free-text-capture: interstitial prose → an informational
|
|
806
|
-
// SEND[103] broadcast log op, interleaved in emission order.
|
|
807
|
-
const trimmed = item.text.trim();
|
|
808
|
-
if (trimmed.length > 0)
|
|
809
|
-
ops.push({
|
|
810
|
-
op: "SEND", suffix: "", signal: 103, target: null,
|
|
811
|
-
lineMarker: null, body: { raw: trimmed, json: null },
|
|
812
|
-
position: item.position ?? { line: 1, column: 1 },
|
|
813
|
-
});
|
|
850
|
+
ops.push(item.statement);
|
|
814
851
|
}
|
|
852
|
+
// Free text (kind "text") is dropped — #free-text-capture retired (above).
|
|
815
853
|
else if (item.kind === "error") {
|
|
816
854
|
const err = item.error;
|
|
817
855
|
if (err instanceof PlurnkParseError) {
|
|
@@ -834,10 +872,7 @@ class Engine {
|
|
|
834
872
|
parseErrors.push({ message: tail.reason, line: tail.from.line, column: tail.from.column, source: "grammar" });
|
|
835
873
|
}
|
|
836
874
|
}
|
|
837
|
-
const
|
|
838
|
-
const planReasoning = planFragments.join("\n\n");
|
|
839
|
-
const reasoningParts = [wireReasoning, planReasoning].filter((s) => s.length > 0);
|
|
840
|
-
const reasoning = reasoningParts.length > 0 ? reasoningParts.join("\n\n") : null;
|
|
875
|
+
const reasoning = assistant.reasoning ?? null;
|
|
841
876
|
return {
|
|
842
877
|
packetAssistant: { content: assistant.content, ops, reasoning },
|
|
843
878
|
callMetadata: { usage: assistant.usage, finishReason: assistant.finishReason, model: assistant.model },
|
|
@@ -848,11 +883,12 @@ class Engine {
|
|
|
848
883
|
// and §user) BEFORE the provider call. The same packet object is then
|
|
849
884
|
// completed with assistant + assistantRaw after the model responds, so
|
|
850
885
|
// the stored packet and the wire payload share one source of truth.
|
|
851
|
-
async #buildRequestPacket({ initialMessages, requirements, runId, loopId, currentTurnSeq, provider, gitStatus, telemetryErrors: presetTelemetry, }) {
|
|
886
|
+
async #buildRequestPacket({ initialMessages, requirements, sessionId, runId, loopId, currentTurnSeq, provider, gitStatus, telemetryErrors: presetTelemetry, }) {
|
|
852
887
|
const byRole = (role) => initialMessages.filter((m) => m.role === role).map((m) => m.content).join("\n\n");
|
|
853
|
-
// plurnk.md (grammar/dialects
|
|
854
|
-
// scheme catalogue is
|
|
855
|
-
//
|
|
888
|
+
// plurnk.md (grammar/dialects) ONLY — the definition is the hot-path grammar.
|
|
889
|
+
// The scheme catalogue is its own `schemes` section below tools (§schemes-directory),
|
|
890
|
+
// NOT appended here: grammar 0.49+ is scheme-agnostic, so the service advertises
|
|
891
|
+
// the scheme set at packet-time (grammar#239 item 7) via SchemeRegistry.teach().
|
|
856
892
|
const system_definition = byRole("system");
|
|
857
893
|
// the prompt section sources from the loop's most recent prompt entry first
|
|
858
894
|
// (plurnk:///prompt/<loop_id>/<N> for the highest N written to date).
|
|
@@ -869,11 +905,11 @@ class Engine {
|
|
|
869
905
|
// requirements). Read Paths.defaultRequirements (PLURNK_REQUIREMENTS env →
|
|
870
906
|
// requirements.md) fresh each build so edits take effect; a non-empty param wins.
|
|
871
907
|
const baseRequirements = requirements.length > 0 ? requirements : await readFile(Paths.defaultRequirements, "utf8");
|
|
872
|
-
// The op syntax leads the requirements
|
|
873
|
-
//
|
|
874
|
-
//
|
|
875
|
-
|
|
876
|
-
const requirementsText = `Syntax: <<OPsuffix[signal]?(target)?<Line/Result>?:body?:OPsuffix\n\n${
|
|
908
|
+
// The op syntax leads the requirements. PLAN is mandated unconditionally by
|
|
909
|
+
// plurnk.md §Imperatives (grammar 0.70 requires every turn to lead with PLAN),
|
|
910
|
+
// so the service injects no separate plan directive here — the former PLURNK_PLAN
|
|
911
|
+
// gating is retired (PLURNK_PLAN is no longer a flag).
|
|
912
|
+
const requirementsText = `Syntax: <<OPsuffix[signal]?(target)?<Line/Result>?:body?:OPsuffix\n\n${baseRequirements}`;
|
|
877
913
|
const log = await this.#buildLog(runId);
|
|
878
914
|
const telemetryErrors = presetTelemetry ?? await this.#buildTelemetryErrors(loopId, currentTurnSeq);
|
|
879
915
|
const countTokens = (t) => provider.countTokens(t); // §provider-surface-counttokens
|
|
@@ -887,10 +923,13 @@ class Engine {
|
|
|
887
923
|
// omitted, section lines still shown). §tokenomics-render-weight-budget
|
|
888
924
|
const ceiling = _a.computeCeiling(provider.contextSize, this.#budgetCeiling);
|
|
889
925
|
const budgetReadout = this.#renderBudget(PacketWire.measureLogBudget(log, countTokens), ceiling);
|
|
926
|
+
// Per-scheme tally (§packet) so the model sees which schemes hold content without
|
|
927
|
+
// probing e.g. FIND(known://**) every turn. "" when empty → the section is omitted.
|
|
928
|
+
const catalogSummary = await this.#db.engine_scheme_catalog_summary.all({ session_id: sessionId });
|
|
890
929
|
// The default packet: an ordered list of sections, each addressable state
|
|
891
930
|
// (§packet-construction). `slot` is the prompt-cache boundary; the STATIC
|
|
892
|
-
// sections (definition, tools
|
|
893
|
-
//
|
|
931
|
+
// sections (definition, tools) lead the system slot so they form the cached
|
|
932
|
+
// prefix, with the dynamic log after. In the user slot, requirements renders
|
|
894
933
|
// last (the contract closest to the assistant turn); budget/errors/git are
|
|
895
934
|
// peer sections (unbundled). The budget section carries its {{tokensFree}}
|
|
896
935
|
// placeholders here; they resolve below once the assembled total is known.
|
|
@@ -903,6 +942,7 @@ class Engine {
|
|
|
903
942
|
{ name: "budget", slot: "user", header: "Plurnk System Budget", content: budgetReadout, tokens: 0 },
|
|
904
943
|
{ name: "errors", slot: "user", header: "Plurnk System Errors", content: PacketWire.renderErrors(telemetryErrors), tokens: 0 },
|
|
905
944
|
{ name: "git", slot: "user", header: "Plurnk System Git Status", content: PacketWire.renderGit(gitStatus), tokens: 0 },
|
|
945
|
+
{ name: "catalog", slot: "user", header: "Plurnk System Catalog", content: PacketWire.renderCatalog(catalogSummary), tokens: 0 },
|
|
906
946
|
{ name: "requirements", slot: "user", header: "Plurnk System Requirements", content: requirementsText, tokens: 0 },
|
|
907
947
|
];
|
|
908
948
|
// Plugin packet control (§packet-construction): trusted schemes rewrite the
|
|
@@ -961,9 +1001,7 @@ class Engine {
|
|
|
961
1001
|
// The # Plurnk System Tools capability sheet (SPEC §tools). A hook: each enabled
|
|
962
1002
|
// capability contributes one line, rendered above Requirements so the model sees what
|
|
963
1003
|
// it can do before the rules. Each available executor tag contributes its self-documenting
|
|
964
|
-
// example (plurnk-execs#7), retiring the blind EXEC.
|
|
965
|
-
// is a hard requirement gated by PLURNK_PLAN (§requirements-plan-gated), so it joins the
|
|
966
|
-
// rules list, not this optional sheet.
|
|
1004
|
+
// example (plurnk-execs#7), retiring the blind EXEC.
|
|
967
1005
|
// The capability sheet — the live tool surface (wired executor tags). §tools-capability-sheet
|
|
968
1006
|
#collectTools() {
|
|
969
1007
|
const tools = [];
|
|
@@ -1269,13 +1307,13 @@ class Engine {
|
|
|
1269
1307
|
const logEntryId = await this.#writeLog({ statement, result, runId, loopId, turnId, sequence, origin });
|
|
1270
1308
|
onDispatch?.(logEntryId);
|
|
1271
1309
|
// Proposal lifecycle (SPEC.md §engine-rails + §methods loop.resolve; §proposal-202-pauses). When a
|
|
1272
|
-
//
|
|
1273
|
-
//
|
|
1274
|
-
//
|
|
1275
|
-
//
|
|
1276
|
-
//
|
|
1277
|
-
// a pending state.
|
|
1278
|
-
if (result
|
|
1310
|
+
// side-effecting op returns status 202 (a broadcast SEND[202] park is model
|
|
1311
|
+
// speech, not a proposal — #isProposal, #255), the entry is written
|
|
1312
|
+
// state='proposed'; dispatch then PAUSES on a per-entry waiter until
|
|
1313
|
+
// resolution arrives via Engine.resolveProposal (from the loop/resolve RPC,
|
|
1314
|
+
// YOLO listener, or timeout). The post-resolution status replaces 202 in the
|
|
1315
|
+
// result the caller sees, so runTurn never branches on a pending state.
|
|
1316
|
+
if (_a.#isProposal(statement, result)) {
|
|
1279
1317
|
// Effect-gated auto-run (read/pure runtimes, plurnk-service#182):
|
|
1280
1318
|
// no human gate, no loop/proposal notification. Accept + apply
|
|
1281
1319
|
// in-process; the model sees the outcome directly, never a review.
|
|
@@ -1720,11 +1758,12 @@ class Engine {
|
|
|
1720
1758
|
return { status: 400, error: "KILL target must be a URL path with a scheme" };
|
|
1721
1759
|
if (schemeName === "log")
|
|
1722
1760
|
return { status: 405, error: "log:/// is append-only; KILL must bounce" };
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1761
|
+
// Process-KILL: any scheme whose handler exposes kill() aborts a live stream — the
|
|
1762
|
+
// exec handler, registered as "exec" + under every runtime tag (sh/node), so a tag-
|
|
1763
|
+
// addressed stream (sh:///l/t/s) routes here, not to deleteEntry. §exec
|
|
1764
|
+
const killable = this.#schemes.get(schemeName);
|
|
1765
|
+
if (killable !== undefined && typeof killable.kill === "function") {
|
|
1766
|
+
return await killable.kill(pathnameFromPath(path), statement.signal, ctx);
|
|
1728
1767
|
}
|
|
1729
1768
|
if (schemeName === "run") {
|
|
1730
1769
|
// terminate — abort any run by address; whoever holds it may end it.
|
|
@@ -1749,12 +1788,9 @@ class Engine {
|
|
|
1749
1788
|
const delResult = await handler.deleteEntry(pathnameFromPath(path), ctx);
|
|
1750
1789
|
return { status: delResult.status };
|
|
1751
1790
|
}
|
|
1752
|
-
// PLAN — the model's
|
|
1753
|
-
//
|
|
1754
|
-
// (
|
|
1755
|
-
// handler is the op's direct-dispatch semantics (exercised by the dispatch unit
|
|
1756
|
-
// test): a pure no-op (PLAN ∉ MUTATING_OPS) whose body would serialize into the log
|
|
1757
|
-
// row's tx, no runtime effect.
|
|
1791
|
+
// PLAN — the model's reasoning op (the 11th op). An ordinary op: dispatched like any
|
|
1792
|
+
// other, logged, and broadcast to the client as a log entry — but a pure no-op for
|
|
1793
|
+
// state (PLAN ∉ MUTATING_OPS); its body serializes into the log row's tx, no effect.
|
|
1758
1794
|
#handlePlan(statement) {
|
|
1759
1795
|
if (statement.op !== "PLAN")
|
|
1760
1796
|
throw new Error("unreachable");
|
|
@@ -1844,8 +1880,9 @@ class Engine {
|
|
|
1844
1880
|
const status = statement.signal;
|
|
1845
1881
|
if (status === null)
|
|
1846
1882
|
return { status: 400 };
|
|
1847
|
-
if (status === 200 || status === 499) {
|
|
1848
|
-
//
|
|
1883
|
+
if (status === 200 || status === 202 || status === 499) {
|
|
1884
|
+
// The broadcast terminals (200 done, 202 parked-async, 499 cancelled) advance
|
|
1885
|
+
// the loop; each carries its body as the loop's terminal message — the deliverable.
|
|
1849
1886
|
const body = statement.body;
|
|
1850
1887
|
const message = body === null ? null : typeof body === "string" ? body : body.raw;
|
|
1851
1888
|
await this.#db.engine_loop_set_status.run({ status, loop_id: loopId, message });
|
|
@@ -1882,30 +1919,36 @@ class Engine {
|
|
|
1882
1919
|
return path.scheme === "https" ? "http" : path.scheme;
|
|
1883
1920
|
return "file"; // local (bare) → file
|
|
1884
1921
|
}
|
|
1922
|
+
// A status-202 result is a reviewable PROPOSAL (a side-effecting op — EDIT/EXEC/
|
|
1923
|
+
// directed write — paused for client resolution) UNLESS it is a broadcast SEND.
|
|
1924
|
+
// A broadcast SEND[202] is the model PARKING the loop (a terminal disposition,
|
|
1925
|
+
// plurnk.md), never a side-effect — #255: gating the propose/await path on the
|
|
1926
|
+
// bare 202 surfaced model speech as a loop/proposal and froze clients. The 202
|
|
1927
|
+
// is overloaded (proposal-pause vs parked-terminal); the op disambiguates it.
|
|
1928
|
+
static #isProposal(statement, result) {
|
|
1929
|
+
return result.status === 202 && !(statement.op === "SEND" && statement.target === null);
|
|
1930
|
+
}
|
|
1885
1931
|
async #writeLog({ statement, result, runId, loopId, turnId, sequence, origin, }) {
|
|
1886
1932
|
const target = this.#extractTarget(statement.target);
|
|
1887
1933
|
const lineMarkerJson = "lineMarker" in statement && statement.lineMarker !== null
|
|
1888
1934
|
? JSON.stringify(statement.lineMarker)
|
|
1889
1935
|
: null;
|
|
1890
|
-
//
|
|
1891
|
-
//
|
|
1892
|
-
//
|
|
1893
|
-
//
|
|
1894
|
-
//
|
|
1895
|
-
|
|
1896
|
-
const isProposed = result.status === 202;
|
|
1936
|
+
// A proposal (status 202 from a side-effecting op) is written to the log in
|
|
1937
|
+
// state='proposed' until the proposal lifecycle resolves it; attrs holds the
|
|
1938
|
+
// scheme-supplied payload (file diff, exec command, etc.) the client renders
|
|
1939
|
+
// for review and the scheme consumes on accept. A broadcast SEND[202] is a
|
|
1940
|
+
// parked-terminal, NOT a proposal (#isProposal / #255) → state='resolved'.
|
|
1941
|
+
const isProposed = _a.#isProposal(statement, result);
|
|
1897
1942
|
let attrsObj = (result.attrs !== undefined && result.attrs !== null)
|
|
1898
1943
|
? { ...result.attrs }
|
|
1899
1944
|
: {};
|
|
1900
|
-
// EXEC
|
|
1901
|
-
//
|
|
1902
|
-
//
|
|
1903
|
-
//
|
|
1904
|
-
//
|
|
1905
|
-
//
|
|
1906
|
-
//
|
|
1907
|
-
// Runtime comes from statement.signal (EXEC's runtime slot) so it's
|
|
1908
|
-
// resolvable for failed execs too; empty/absent = the default shell.
|
|
1945
|
+
// EXEC stream entry addresses by RUNTIME TAG as authority (§exec): it lives at
|
|
1946
|
+
// <runtime>:///<loop_seq>/<turn_seq>/<sequence> (e.g. sh:///1/1/2) — the runtime tag
|
|
1947
|
+
// is the scheme, the coordinate already unique per statement. The log row's target
|
|
1948
|
+
// points at this same address; its log:/// coordinate shares the trailing
|
|
1949
|
+
// <loop>/<turn>/<seq>, so the model correlates op to stream output. Runtime comes
|
|
1950
|
+
// from statement.signal (EXEC's runtime slot) so it's resolvable for failed execs
|
|
1951
|
+
// too; empty/absent = the default shell.
|
|
1909
1952
|
if (statement.op === "EXEC") {
|
|
1910
1953
|
const seqs = await this.#db.engine_loop_turn_seqs.get({
|
|
1911
1954
|
loop_id: loopId, turn_id: turnId,
|
|
@@ -1913,8 +1956,8 @@ class Engine {
|
|
|
1913
1956
|
if (seqs === undefined)
|
|
1914
1957
|
throw new Error(`Engine.#writeLog: loop_turn_seqs returned no row for loop=${loopId} turn=${turnId}`);
|
|
1915
1958
|
const runtime = (typeof statement.signal === "string" && statement.signal.length > 0) ? statement.signal : "sh";
|
|
1916
|
-
const coordPathname = `/${
|
|
1917
|
-
target.scheme =
|
|
1959
|
+
const coordPathname = `/${seqs.loop_seq}/${seqs.turn_seq}/${sequence}`;
|
|
1960
|
+
target.scheme = runtime;
|
|
1918
1961
|
target.pathname = coordPathname;
|
|
1919
1962
|
attrsObj.pathname = coordPathname;
|
|
1920
1963
|
// Mutate the in-memory result.attrs too: the dispatch path
|