@plurnk/plurnk-service 0.45.0 → 0.49.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 +33 -3
- package/README.md +1 -1
- package/SPEC.md +12 -8
- package/dist/core/Engine.d.ts.map +1 -1
- package/dist/core/Engine.js +61 -46
- package/dist/core/Engine.js.map +1 -1
- package/dist/core/Engine.sql +16 -0
- package/dist/core/ExecutorRegistry.d.ts +3 -0
- package/dist/core/ExecutorRegistry.d.ts.map +1 -1
- package/dist/core/ExecutorRegistry.js +7 -2
- 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 +76 -6
- package/dist/core/SchemeRegistry.js.map +1 -1
- package/dist/core/packet-inject.d.ts +3 -0
- package/dist/core/packet-inject.d.ts.map +1 -0
- package/dist/core/packet-inject.js +16 -0
- package/dist/core/packet-inject.js.map +1 -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/scheme-types.d.ts +1 -0
- package/dist/core/scheme-types.d.ts.map +1 -1
- package/dist/core/teaching.d.ts +3 -0
- package/dist/core/teaching.d.ts.map +1 -0
- package/dist/core/teaching.js +11 -0
- package/dist/core/teaching.js.map +1 -0
- package/dist/schemes/Exec.d.ts +2 -2
- package/dist/schemes/Exec.d.ts.map +1 -1
- package/dist/schemes/Exec.js +40 -29
- package/dist/schemes/Exec.js.map +1 -1
- package/dist/schemes/ExecOutputScheme.d.ts +20 -0
- package/dist/schemes/ExecOutputScheme.d.ts.map +1 -0
- package/dist/schemes/ExecOutputScheme.js +38 -0
- package/dist/schemes/ExecOutputScheme.js.map +1 -0
- package/dist/schemes/Known.d.ts.map +1 -1
- package/dist/schemes/Known.js +0 -1
- package/dist/schemes/Known.js.map +1 -1
- package/dist/schemes/Log.d.ts.map +1 -1
- package/dist/schemes/Log.js +0 -1
- package/dist/schemes/Log.js.map +1 -1
- package/dist/schemes/Run.d.ts.map +1 -1
- package/dist/schemes/Run.js +0 -1
- package/dist/schemes/Run.js.map +1 -1
- package/dist/schemes/Unknown.d.ts.map +1 -1
- package/dist/schemes/Unknown.js +0 -1
- package/dist/schemes/Unknown.js.map +1 -1
- package/dist/schemes/_entry-manifest.d.ts.map +1 -1
- package/dist/schemes/_entry-manifest.js +7 -2
- package/dist/schemes/_entry-manifest.js.map +1 -1
- 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 +16 -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-receipt.d.ts +4 -0
- package/dist/schemes/exec-receipt.d.ts.map +1 -0
- package/dist/schemes/exec-receipt.js +25 -0
- package/dist/schemes/exec-receipt.js.map +1 -0
- package/dist/server/Daemon.d.ts.map +1 -1
- package/dist/server/Daemon.js +62 -42
- package/dist/server/Daemon.js.map +1 -1
- package/dist/server/drain.sql +10 -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/service.js +1 -1
- package/dist/service.js.map +1 -1
- package/docs/known.md +3 -0
- package/docs/log.md +25 -0
- package/docs/run.md +3 -0
- package/docs/unknown.md +3 -0
- package/migrations/0000-00-00.01_schema.sql +2 -2
- package/package.json +13 -12
package/.env.example
CHANGED
|
@@ -32,6 +32,10 @@ PLURNK_DB_PATH=~/.plurnk/plurnk.db
|
|
|
32
32
|
PLURNK_HOST=127.0.0.1
|
|
33
33
|
PLURNK_PORT=3044
|
|
34
34
|
|
|
35
|
+
# --- Client (read by the `plurnk` CLI from this shared home; the daemon ignores it) ---
|
|
36
|
+
# The daemon WebSocket URL the client connects to (mirrors PLURNK_HOST/PORT above).
|
|
37
|
+
# PLURNK_WS=ws://127.0.0.1:3044
|
|
38
|
+
|
|
35
39
|
# --- Model aliases ---
|
|
36
40
|
# PLURNK_MODEL is the active provider for every loop (a client may override per loop
|
|
37
41
|
# via loop.run({alias})). Out of the box it is `plurnk`. Change this one line to use
|
|
@@ -64,7 +68,12 @@ PLURNK_RPC_TIMEOUT=30000
|
|
|
64
68
|
# so a fast exec's output can land in it instead of a turn later. A fixed grace
|
|
65
69
|
# beat, NOT a wait-for-completion (slow execs proceed + surface via the wake path).
|
|
66
70
|
# 0 = off (the model sees fast-exec output a turn late, as today).
|
|
67
|
-
PLURNK_EXEC_WAIT_MS=
|
|
71
|
+
PLURNK_EXEC_WAIT_MS=1000
|
|
72
|
+
# Teardown reap grace: when a loop/run tears down a background exec, the spawn gets a polite
|
|
73
|
+
# signal first (SIGHUP, or the model's KILL[code]); a stream that IGNORES it is hard-killed
|
|
74
|
+
# (SIGKILL, to the whole process group) this many ms later — so the reap can't wedge on a
|
|
75
|
+
# signal-ignoring child. plurnk-execs refuses to bake this number; the consumer owns it.
|
|
76
|
+
PLURNK_EXEC_KILL_GRACE_MS=2000
|
|
68
77
|
PLURNK_LOOP_TIMEOUT=86400000
|
|
69
78
|
|
|
70
79
|
# --- Engine rails ---
|
|
@@ -88,7 +97,7 @@ PLURNK_GIT_ALLOWED=1
|
|
|
88
97
|
# repos explicitly via the `repo` overlay. SPEC §membership forest.
|
|
89
98
|
PLURNK_GIT_AUTO=1
|
|
90
99
|
|
|
91
|
-
# --- Reference docs (auto-READ at turn
|
|
100
|
+
# --- Reference docs (auto-READ at turn 1) ---
|
|
92
101
|
# PLURNK_MD_<ALIAS>=<path> materializes <path>'s markdown as a plurnk://<ALIAS>.md
|
|
93
102
|
# entry the model READs at turn 0 — an idiomatic way to inject standing context
|
|
94
103
|
# (an ordinary entry + READ op, not a bespoke packet section). ~ expands to home;
|
|
@@ -96,7 +105,14 @@ PLURNK_GIT_AUTO=1
|
|
|
96
105
|
# per session via session.create settings.mdDocs (content, not a path); those UNION
|
|
97
106
|
# with these env docs, keyed by alias — the client wins a collision (#231).
|
|
98
107
|
# Commented out = no docs by default.
|
|
99
|
-
|
|
108
|
+
PLURNK_MD_POLICY=~/.plurnk/AGENTS.md
|
|
109
|
+
|
|
110
|
+
# --- AGENTS.md (auto-READ at turn 1) ---
|
|
111
|
+
# Scan project root and if there's an agent file, pick it into the manifest and
|
|
112
|
+
# read its entire content into turn 1. Accepts multiple.
|
|
113
|
+
# PLURNK_AGENTS_FILES="AGENTS.md,CLAUDE.md"
|
|
114
|
+
PLURNK_AGENTS_FILES="AGENTS.md"
|
|
115
|
+
PLURNK_AGENTS_AUTO=1
|
|
100
116
|
|
|
101
117
|
# Turn-0 manifest preview: foist a READ of plurnk://manifest.json into the model's
|
|
102
118
|
# first turn so a run opens with the session catalog, not blank. -1 = the full
|
|
@@ -141,6 +157,10 @@ PLURNK_VERSION_POLL_TTL=3600000
|
|
|
141
157
|
# @plurnk/plurnk-grammar; an absolute/relative path is your own. =0 (or empty) disables.
|
|
142
158
|
# PLURNK_PROVIDERS_GBNF=plurnk.gbnf
|
|
143
159
|
|
|
160
|
+
# GBNF debug, honored natively by @plurnk/plurnk-providers (>=0.13.0): validate the grammar locally
|
|
161
|
+
# (fail-hard if malformed) and run UNCONSTRAINED, never sending it to the model. Dev aid; off by default.
|
|
162
|
+
PLURNK_GBNF_DEBUG=0
|
|
163
|
+
|
|
144
164
|
# Provider retry attempts (providers 0.7+): how many times generate() retries a
|
|
145
165
|
# TRANSIENT failure (429 rate-limit, 5xx/network) with exponential backoff (base 2s
|
|
146
166
|
# is a provider constant — the COUNT is the operator knob). REQUIRED, fail-hard if
|
|
@@ -163,6 +183,16 @@ PLURNK_PROVIDER_RETRY_ATTEMPTS=3
|
|
|
163
183
|
# for "on, zero third-party". Example: =acme-execs-cobol,@firewolf/firepad
|
|
164
184
|
PLURNK_PLUGINS_TRUSTED_ONLY=0
|
|
165
185
|
|
|
186
|
+
# PLURNK_DOCS_EXCLUDE — comma list of scheme/exec names dropped from BOTH the teaching oneliner
|
|
187
|
+
# and the materialized pull-doc on load. The self-evident (plurnk/file) + retired (exec) names the
|
|
188
|
+
# model needs no doc for. Empty → exclude nothing. Unknown names are inert (a filter, not a contract).
|
|
189
|
+
PLURNK_DOCS_EXCLUDE="plurnk,file,exec"
|
|
190
|
+
|
|
191
|
+
# PLURNK_PACKET_INJECT — an operator markdown file injected as a section right after the teaching
|
|
192
|
+
# (the cached prefix). Read per-turn (live edits); a set-but-unreadable path fails the turn HARD.
|
|
193
|
+
# `~/` expands to home. Unset → no section. The pressure valve for "improve the packet" without a fork.
|
|
194
|
+
# PLURNK_PACKET_INJECT="~/injection.md"
|
|
195
|
+
|
|
166
196
|
# --- Semantic search (~query chunking) ---
|
|
167
197
|
# Project Semantics tiles each entry into <=window chunks so a large body is fully
|
|
168
198
|
# searchable, not truncated. ACTIVE only when the installed embedder reports its
|
package/README.md
CHANGED
|
@@ -41,7 +41,7 @@ Config + state live in `~/.plurnk/` (created on first run): set `PLURNK_MODEL` a
|
|
|
41
41
|
|
|
42
42
|
## Semantic search
|
|
43
43
|
|
|
44
|
-
`FIND` ranks via an optional embedder peer, `@plurnk/plurnk-mimetypes-embeddings` (heavy native deps; not installed by default). Absent → `
|
|
44
|
+
`FIND`'s `~query` ranks semantically via an optional embedder peer, `@plurnk/plurnk-mimetypes-embeddings` (heavy native deps; not installed by default). Absent → `~query` falls back to FTS keyword ranking and `start` prints an `embedder inactive` notice. Enable vector search: `npm i @plurnk/plurnk-mimetypes-embeddings`.
|
|
45
45
|
|
|
46
46
|
## Tests
|
|
47
47
|
|
package/SPEC.md
CHANGED
|
@@ -193,10 +193,10 @@ Beyond the three creation ops:
|
|
|
193
193
|
A run is a **log plus a cancellation scope** — one `AbortController` per run, reused while live and replaced only once aborted, so a cancel ends the run as a unit and a later `loop.run` is never born cancelled. A run's queued loops are advanced by a **drain**: a single per-run worker that claims loops atomically (status 100→102) and runs each under the run's scope. A loop may spawn **streams** (execs) that outlive it; each is a row in the subscription registry (§subscriptions) — the durable record of what the run holds open. Cancellation and conclusion are defined against these structures, never wall-clock timing.
|
|
194
194
|
|
|
195
195
|
- **One drain advances a run.** At most one drain is registered for a run at any instant: a `loop.run` or wake on a run with a live drain folds in (active→next-turn) or enqueues a loop that drain claims, never a second parallel drain. A drain's start and its empty-queue teardown relinquish run under one per-run lock, so the teardown's re-claim cannot race a concurrent start into a double-drain. {§run-lifecycle-single-drain}
|
|
196
|
-
- **A cancel reaps every stream the run holds — by the registry.** `loop.cancel` / `KILL` / shutdown abort the run scope AND iterate the run's open subscriptions, aborting each via its owning scheme; the registry is the source of truth, the in-process abort signal a fast-path optimization. A stream that is running, mid-spawn (its row written before it is killable), or spawned after the cancel is reaped alike. {§run-lifecycle-total-reap}
|
|
196
|
+
- **A cancel reaps every stream the run holds — by the registry.** `loop.cancel` / `KILL` / shutdown abort the run scope AND iterate the run's open subscriptions, aborting each via its owning scheme; the registry is the source of truth, the in-process abort signal a fast-path optimization. A stream that is running, mid-spawn (its row written before it is killable), or spawned after the cancel is reaped alike. The teardown abort is a BOUNDED reap — the executor sends a polite signal then SIGKILL after a consumer-set grace (`PLURNK_EXEC_KILL_GRACE_MS`), so a signal-ignoring stream can't wedge it; a model `KILL[code]` on a live stream instead delivers exactly that signal once (bare `KILL` → the executor's SIGHUP default, `KILL[9]` → SIGKILL), the model owning any escalation. {§run-lifecycle-total-reap}
|
|
197
197
|
- **A stream's kill binds to the scope it captured at spawn.** A stream captures the run's cancellation scope as it registers and wires its kill to it, re-checking `aborted` AFTER wiring — no check-then-listen gap can drop an abort that lands mid-registration. Because the scope is replaced only once aborted, a captured-then-replaced scope is necessarily already aborted, so replacement never strands a live stream. {§run-lifecycle-exec-epoch-bound}
|
|
198
198
|
- **A cancelled run is not resurrected by its own torn-down work.** A stream conclusion delivered to a cancelled, idle run starts no fresh drain: an aborted (499) conclusion is skipped, and a straggler that concluded cleanly surfaces its deliverable as an environment delta (§env-delta), never a revived loop. The cancel was deliberate; only an explicit `loop.run` resumes the run. {§run-lifecycle-no-resurrection}
|
|
199
|
-
- **A stream conclusion always reaches its run.** When a backgrounded stream concludes, the daemon routes it through the same inject seam as any loop source (§actor-boundary-passive-wake): an active run folds the
|
|
199
|
+
- **A stream conclusion always reaches its run.** When a backgrounded stream concludes, the daemon routes it through the same inject seam as any loop source (§actor-boundary-passive-wake): an active run folds the conclusion into its next turn; a run parked at a **slept loop** (`SEND[202]`) **awakens that loop in place** — the slept loop *is* the continuation, so there is no fresh loop and no summary-as-prompt fiction. The result is never lost: a parked loop sleeps rather than ending, and the stream's status-transition is the OPEN event (§actor-boundary-passive-wake) that wakes it; on resume it reads the concluded stream's own state, not a synthetic prompt. {§run-lifecycle-wake-liveness}
|
|
200
200
|
- **A loop is never stranded by a drain's exit.** A drain relinquishes its registry slot only after a lock-held re-claim confirms the queue is empty; a loop enqueued during that teardown is either re-claimed by the exiting drain or claimed by a fresh drain that a later inject starts. The relinquish and the start are serialized, so neither the lost-loop hang nor a transient double-drain can occur. {§run-lifecycle-no-lost-loop}
|
|
201
201
|
|
|
202
202
|
---
|
|
@@ -561,7 +561,7 @@ Engine routes unconditionally to `exec` scheme (path slot is `cwd`, not a URI).
|
|
|
561
561
|
|
|
562
562
|
**Effect-gating.** Each executor declares an `effect` (`pure` | `read` | `host`); the service maps it to policy (`EffectPolicy`). A `host` runtime (subprocess; file-backed sqlite) mutates the host → **propose** (lifecycle §proposal): the run waits for a human gate, then spawns and writes stdout/stderr to channels of an `exec:///<runtime>/<loop>/<turn>/<seq>` entry (the executor is the URI authority; the coordinate that follows matches the op's log-row coordinate, e.g. `exec:///sh/1/1/2`), returning `102 Processing` immediately. Channel state transitions (`active` → `closed`/`errored`) drive what the model sees at subsequent turn boundaries (§channel-state). {§exec-host-proposes}
|
|
563
563
|
|
|
564
|
-
A `read` runtime (observes external state, e.g. search) or `pure` runtime (no observable effect, e.g. `:memory:` sqlite) is side-effect-free → **auto-run** in-process: no proposal, no human gate, no notification. The run is awaited synchronously and its
|
|
564
|
+
A `read` runtime (observes external state, e.g. search) or `pure` runtime (no observable effect, e.g. `:memory:` sqlite) is side-effect-free → **auto-run** in-process: no proposal, no human gate, no notification. The run is awaited synchronously and its output's RECEIPT — the `<tag>:///<coord>` address + a structural `OrientIndex` (counts/shape/keys/headings, never the content) — rides back as the EXEC result body the same turn; the model READs the address to pull content. Receipt-only (read/pure included) is the containment that keeps tool output off the packet — #240. {§exec-readpure-inline}
|
|
565
565
|
|
|
566
566
|
`SEND[499](exec:///<runtime>/<loop>/<turn>/<seq>)` cancels in-flight subprocess via subscription registry's stored AbortController (§stream-control).
|
|
567
567
|
|
|
@@ -896,7 +896,7 @@ registry.register("loop.run", {
|
|
|
896
896
|
|
|
897
897
|
| Method | Params | Result | Notes |
|
|
898
898
|
|-------------------|-------------------------------------|------------------------|-------|
|
|
899
|
-
| `loop.run` | `prompt: string`, `maxTurns?: number`, `alias?: string`, `flags?: LoopFlags` | `{ loopId,
|
|
899
|
+
| `loop.run` | `prompt: string`, `maxTurns?: number`, `alias?: string`, `flags?: LoopFlags` | `{ loopId, action, finalStatus: 100 }` | Model-driven loop. **Accepts and returns immediately** (`finalStatus: 100`; `action` = `enqueued_new_loop` \| `injected_next_turn`) — it never blocks on the loop, which can park indefinitely (`SEND[202]`, §run-lifecycle-wake-liveness). The loop's outcome — `finalStatus`, `turnIds`, `hitMaxTurns`, `usage` — arrives on the **`loop/terminated`** event. Optional `alias` overrides the boot-time `PLURNK_MODEL`. Optional `flags` carries per-loop flags (`{yolo?: boolean}`; more as wired — see §engine-rails). Streams `log/entry` and `loop/proposal` during. `longRunning: false`. {§methods-loop-run} |
|
|
900
900
|
| `loop.resolve` | `logEntryId: number`, `decision: "accept" \| "reject" \| "cancel"`, `body?: string`, `outcome?: string` | `{ status, logEntryId }` | Resolve a pending proposal (status=202 log entry). Engine.dispatch unpauses on resolution. |
|
|
901
901
|
| `loop.cancel` | `reason?: string` | `{ cancelled, runId, reason }` | Abort the attached run's active drain. `{cancelled: true}` if a drain was running, `{false}` if idle. Cancelled loops close at 499; queued-but-unclaimed loops stay enqueued. Default reason `user_cancelled`. {§methods-loop-cancel} |
|
|
902
902
|
| `providers.list` | none | `{ aliases: ProviderAlias[] }` | Lists configured `PLURNK_MODEL_<alias>` entries with `{alias, provider, model, active}`. Clients use to populate model-selection UI. |
|
|
@@ -947,7 +947,7 @@ Server-initiated events on the same WebSocket.
|
|
|
947
947
|
| `loop/proposal` | `{ logEntryId, sessionId, runId, loopId, turnId, op, target, body, attrs, flags }` | Dispatch pauses on status=202. Carries `flags` so server-YOLO clients can suppress review UI. Client responds with `loop.resolve` (or `PLURNK_PROPOSAL_TIMEOUT_MS` fires). |
|
|
948
948
|
| `session/created` | `{ id, name, projectRoot }` | Any client creates a session. |
|
|
949
949
|
| `stream/event` | `{ entryId, channel, state, contentLength }` | Channel content grows or state transitions. {§notifications-stream-event-on-channel-change} |
|
|
950
|
-
| `stream/concluded` | `{ entryId, target, subscriptionId, scheme, closeStatus, summary, wakeAction, wakeLoopId? }` | A streaming subscription closed (subprocess finished / errored / cancelled). `wakeAction` says
|
|
950
|
+
| `stream/concluded` | `{ entryId, target, subscriptionId, scheme, closeStatus, summary, wakeAction, wakeLoopId? }` | A streaming subscription closed (subprocess finished / errored / cancelled). `wakeAction` says how the conclusion reached the run: `resumed-loop` (a slept `202` loop resumed in place, §run-lifecycle-wake-liveness), `no-op-active-loop` (folded into a live loop's next turn), `skipped-aborted`/`skipped-cancelled`/`skipped-no-provider`, or `no-loop` (nothing to resume). `summary` rides the notification for client display; it is no longer fed to the model as a prompt. {§notifications-stream-concluded} |
|
|
951
951
|
| `telemetry/event` | `{ loopId, event: TelemetryEvent }` | A TelemetryEvent (parse error, engine-rail strike/cycle/sudden-death, scheme/provider failure) was buffered — the same envelope the model sees on the next packet, delivered live for client surfacing. {§notifications-telemetry-event} |
|
|
952
952
|
|
|
953
953
|
`stream/event` carries metadata only, never content. Clients fetch via `entry.read({target})`. **Every notification envelope carries its `sessionId`** (and `runId` where the emitter has it) so a multi-session client — one connection, many sessions — can route it ({§notifications-envelope-carries-sessionid}); the broadcast stays session-scoped too.
|
|
@@ -1190,7 +1190,7 @@ The wire projection (`PacketWire.renderSlot`) groups sections by slot into the s
|
|
|
1190
1190
|
|
|
1191
1191
|
**Prompt as a first-class entry.** Each loop's prompt is written on loop start as a plurnk-origin `EDIT` against `plurnk:///prompt/<loop_id>` (indexable, body channel, text/markdown). At render time the current loop's prompt body materializes into the `prompt` section; the entry itself stays READ/FOLD-able like any other. The foisted `EDIT`'s **log row is folded by default** (`expanded=0`): the prompt body already lives in the `prompt` section, so the log keeps the action for forensics while collapsing the duplicate body, re-OPENable like any fold (§open-fold). {§prompt-fold}
|
|
1192
1192
|
|
|
1193
|
-
**The entry catalog.** `plurnk:///manifest.json` is a real session entry the model READs to discover what's available — rewritten every turn as a live view of the full entry set. Built in the schemes layer (`_entry-manifest`) and materialized like any entry (the engine only orchestrates the per-turn write — the same pattern as git membership), so it's READable and queryable. Body is `application/json`: a flat, **complete, unranked** array — one item per entry across all schemes, every entry listed in no relevance order, each `{ path, tags?, channels: { <
|
|
1193
|
+
**The entry catalog.** `plurnk:///manifest.json` is a real session entry the model READs to discover what's available — rewritten every turn as a live view of the full entry set. Built in the schemes layer (`_entry-manifest`) and materialized like any entry (the engine only orchestrates the per-turn write — the same pattern as git membership), so it's READable and queryable. Body is `application/json`: a flat, **complete, unranked** array — one item per entry across all schemes, every entry listed in no relevance order, each `{ path, tags?, channels: { <uri>: { mimetype, tokens, lines } } }` — every channel keyed by the URI the model READs (the default channel by the bare path, a non-default by `path#channel`), so it reaches a channel without guessing. `tags` is present only when the entry carries `entry_tags` — its own categorization, surfaced so the model sees it in the directory and can `FIND` by tag without a separate read. The model ranks and filters the catalog itself by querying it (task-aware); the catalog never ranks for it — the instant it did, it would be an index again. `tokens` is the provider's write-time count (budget depth), `lines` the content extent from `Mimetypes.process().totalLines`. The engine counts neither. It does not list itself. {§packet-manifest-catalog}
|
|
1194
1194
|
|
|
1195
1195
|
### §telemetry user.telemetry — model-facing runtime telemetry
|
|
1196
1196
|
|
|
@@ -1230,11 +1230,15 @@ Strike accounting, cycle detection, sudden-death thresholds, and no-ops bookkeep
|
|
|
1230
1230
|
|
|
1231
1231
|
A `# Plurnk System Tools` section renders **above** `# Plurnk System Requirements` — a hook-populated list of the capabilities enabled this session, so the model sees what it can *do* before the rules it must follow. Each enabled capability contributes one line via `Engine.#collectTools`; the whole section is omitted when nothing is enabled. {§tools-capability-sheet}
|
|
1232
1232
|
|
|
1233
|
-
**Contributors: the wired executor tags.** Each available executor tag
|
|
1233
|
+
**Contributors: the wired executor tags.** Each available executor tag *with an example* contributes ONE line — its canonical usage — plus a `(docs: plurnk://docs/<tag>.md)` pointer when its package ships documentation, via the shared `teachingLine` (identical shape to the scheme directory, §schemes). A tag with no example contributes nothing; `PLURNK_DOCS_EXCLUDE` drops a named tag's line + doc. The boot `ExecutorRegistry` probes availability per tag, retiring the model's blind `<<EXEC[sh]…`.
|
|
1234
1234
|
|
|
1235
1235
|
### §schemes user.schemes — the scheme directory
|
|
1236
1236
|
|
|
1237
|
-
A `# Plurnk System Schemes` section renders in the system slot **after the definition (plurnk.md — grammar + imperatives) and the tools sheet** — a terse directory of the scheme families available this session, so the model knows what URI schemes exist before it acts. Each scheme that ships a `manifest.example` contributes ONE line — its canonical usage — plus a `(docs: plurnk://docs/<scheme>.md)` pointer when
|
|
1237
|
+
A `# Plurnk System Schemes` section renders in the system slot **after the definition (plurnk.md — grammar + imperatives) and the tools sheet** — a terse directory of the scheme families available this session, so the model knows what URI schemes exist before it acts. Each scheme that ships a `manifest.example` contributes ONE line — its canonical usage (no scheme prefix; the example self-documents) — plus a `(docs: plurnk://docs/<scheme>.md)` pointer when a doc exists. The in-tree core schemes author their depth in `docs/<name>.md` (loaded at boot, shipped with the package); daughter schemes ship `manifest.documentation`. The verbose semantics live in that pull doc (materialized like any entry, READ on demand), not the hot path — terse pushes, depth pulls, via the same `teachingLine` as the tools sheet (§tools). A scheme with no example (provisional) is omitted; `PLURNK_DOCS_EXCLUDE` drops a named scheme's line + doc. {§schemes-directory}
|
|
1238
|
+
|
|
1239
|
+
### §inject system.inject — the operator injection
|
|
1240
|
+
|
|
1241
|
+
When `PLURNK_PACKET_INJECT` names a readable markdown file, its content renders as a `# Plurnk Operator Notes` section in the system slot **right after the teaching** (definition → tools → schemes → inject), before the log — part of the cached prefix. Read per-turn so the operator's edits take effect live; a set-but-unreadable path fails the turn hard (a deliberate setting with a broken path is a misconfig, surfaced not hidden). `~/` expands to home. It's the operator-side complement to the plugin section hook — a pressure valve so reshaping the packet edits operator content, never the core. Unset → no section. {§packet-inject}
|
|
1238
1242
|
|
|
1239
1243
|
**The scheme self-doc contract.** `example` is the hot-path one-liner; `documentation` is the deep doc — the exact shape execs already use (`example` + `documentation`). `SchemeRegistry.teach()` renders the directory; `docEntries()` materializes the docs (per loop.run, alongside the operator docs). `documentation` rides a service-side `SchemeManifest` extension until plurnk-schemes#25 lands it in the contract.
|
|
1240
1244
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Engine.d.ts","sourceRoot":"","sources":["../../src/core/Engine.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAwF,MAAM,wBAAwB,CAAC;AAMpJ,OAAO,KAAK,cAAc,MAAM,qBAAqB,CAAC;AACtD,OAAO,EAAE,SAAS,EAAiB,MAAM,0BAA0B,CAAC;AACpE,OAAO,KAAK,EAAE,EAAE,EAAc,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"Engine.d.ts","sourceRoot":"","sources":["../../src/core/Engine.ts"],"names":[],"mappings":"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"}
|
package/dist/core/Engine.js
CHANGED
|
@@ -8,6 +8,8 @@ import { foldAuthorityIntoPath, renderAddress } from "./plurnk-uri.js";
|
|
|
8
8
|
import GitState from "./git-state.js";
|
|
9
9
|
import Fork from "./fork.js";
|
|
10
10
|
import RunCap from "./run-cap.js";
|
|
11
|
+
import { teachingLine, docsExcludeSet } from "./teaching.js";
|
|
12
|
+
import { readPacketInject } from "./packet-inject.js";
|
|
11
13
|
import SessionSettings from "./session-settings.js";
|
|
12
14
|
import { decodePathParens } from "./path-decode.js";
|
|
13
15
|
import { DEFAULT_LOOP_FLAGS } from "./scheme-types.js";
|
|
@@ -366,13 +368,12 @@ class Engine {
|
|
|
366
368
|
}
|
|
367
369
|
this.#loopAborts.set(loopId, loopAbort);
|
|
368
370
|
// Cleanup splits by termination kind:
|
|
369
|
-
// - "graceful" (
|
|
370
|
-
//
|
|
371
|
-
//
|
|
372
|
-
//
|
|
373
|
-
// - "forceful" (max_turns,
|
|
374
|
-
//
|
|
375
|
-
// tear down immediately.
|
|
371
|
+
// - "graceful" (SEND[202] Accepted): in-flight streaming-scheme spawns
|
|
372
|
+
// are ALLOWED to outlive the loop — they complete naturally, write final
|
|
373
|
+
// channel state, and wake-on-completion (E.4) opens a fresh loop. 202 is
|
|
374
|
+
// the only terminal that means "keep my async work."
|
|
375
|
+
// - "forceful" (SEND[200] done, max_turns, strike, cancel, budget, 4xx/5xx):
|
|
376
|
+
// fire the loop-level abort so leftover spawns tear down. "Done" reaps.
|
|
376
377
|
const cleanup = (kind, reason) => {
|
|
377
378
|
if (kind === "forceful" && !loopAbort.signal.aborted) {
|
|
378
379
|
loopAbort.abort(reason ?? "loop_forceful_termination");
|
|
@@ -387,10 +388,10 @@ class Engine {
|
|
|
387
388
|
if (row === undefined)
|
|
388
389
|
throw new Error(`Engine.runLoop: loop ${loopId} not found`);
|
|
389
390
|
if (row.status !== 102) {
|
|
390
|
-
//
|
|
391
|
-
// (
|
|
392
|
-
//
|
|
393
|
-
cleanup(row.status
|
|
391
|
+
// Only 202 (Accepted) lets spawns outlive — it IS the async wake
|
|
392
|
+
// contract (E.4). Every other terminal, 200 included, reaps: "done"
|
|
393
|
+
// must not leak running execs. Trust the code's declared intent.
|
|
394
|
+
cleanup(row.status === 202 ? "graceful" : "forceful", `loop_terminal_${row.status}`);
|
|
394
395
|
return { turnIds, finalStatus: row.status, hitMaxTurns: false, reason: "external" };
|
|
395
396
|
}
|
|
396
397
|
if (maxTurns >= 0 && turnIds.length >= maxTurns) {
|
|
@@ -578,6 +579,7 @@ class Engine {
|
|
|
578
579
|
wakeRunNotify: this.#wakeRunNotify,
|
|
579
580
|
tokenize: this.#tokenize,
|
|
580
581
|
mimetypes: this.#mimetypes,
|
|
582
|
+
defaultChannelFor: (s) => this.#schemes.defaultChannelFor(s),
|
|
581
583
|
pushTelemetry: (event) => this.#pushTelemetry(sessionId, loopId, event),
|
|
582
584
|
};
|
|
583
585
|
// SPEC §membership D4/D5 — git-ls-files workspace membership, resolved at
|
|
@@ -656,14 +658,14 @@ class Engine {
|
|
|
656
658
|
// queries log_entries scoped to the run — the prompt entry just
|
|
657
659
|
// written (if turn 1) is part of that query result.
|
|
658
660
|
let requestPacket = await this.#buildRequestPacket({
|
|
659
|
-
initialMessages: messages, requirements, runId, loopId,
|
|
661
|
+
initialMessages: messages, requirements, sessionId, runId, loopId,
|
|
660
662
|
currentTurnSeq: seq, provider, gitStatus,
|
|
661
663
|
});
|
|
662
664
|
// SPEC §grinder — budget grinder, pre-LLM: reclaim window on actual overflow.
|
|
663
665
|
const enforced = await this.#enforceBudget({
|
|
664
666
|
packet: requestPacket, provider, runId, loopId, turnId, sessionId, turnNumber,
|
|
665
667
|
rebuild: (telemetryErrors) => this.#buildRequestPacket({
|
|
666
|
-
initialMessages: messages, requirements, runId, loopId,
|
|
668
|
+
initialMessages: messages, requirements, sessionId, runId, loopId,
|
|
667
669
|
currentTurnSeq: seq, provider, telemetryErrors, gitStatus,
|
|
668
670
|
}),
|
|
669
671
|
});
|
|
@@ -694,14 +696,16 @@ class Engine {
|
|
|
694
696
|
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
697
|
}
|
|
696
698
|
catch (err) {
|
|
697
|
-
//
|
|
698
|
-
//
|
|
699
|
-
//
|
|
700
|
-
//
|
|
701
|
-
//
|
|
702
|
-
//
|
|
703
|
-
if (err instanceof ProviderError
|
|
704
|
-
this.#pushTelemetry(sessionId, loopId, { source: "provider", kind:
|
|
699
|
+
// Every provider error surfaces as telemetry (the client/model sees the cause). #256:
|
|
700
|
+
// grammar_unenforced is the one the MODEL can recover from — the backend didn't
|
|
701
|
+
// constrain the GBNF, so this turn was rejected but a conforming emission next turn is
|
|
702
|
+
// accepted: fall through as an empty no-op turn so the strike rail retries. Every other
|
|
703
|
+
// kind (rate_limit, network_failure, unauthorized, …) is terminal — telemetry'd, then
|
|
704
|
+
// propagated to end the loop (rather than only the opaque loop.run rejection).
|
|
705
|
+
if (err instanceof ProviderError) {
|
|
706
|
+
this.#pushTelemetry(sessionId, loopId, { source: "provider", kind: err.kind, message: err.message });
|
|
707
|
+
if (err.kind !== "grammar_unenforced")
|
|
708
|
+
throw err;
|
|
705
709
|
response = {
|
|
706
710
|
assistant: { content: "", reasoning: null, usage: { prompt: requestPacket.tokens, completion: 0, reasoning: 0, cached: 0, total: requestPacket.tokens }, finishReason: null, model: provider.model },
|
|
707
711
|
assistantRaw: null,
|
|
@@ -881,7 +885,7 @@ class Engine {
|
|
|
881
885
|
// and §user) BEFORE the provider call. The same packet object is then
|
|
882
886
|
// completed with assistant + assistantRaw after the model responds, so
|
|
883
887
|
// the stored packet and the wire payload share one source of truth.
|
|
884
|
-
async #buildRequestPacket({ initialMessages, requirements, runId, loopId, currentTurnSeq, provider, gitStatus, telemetryErrors: presetTelemetry, }) {
|
|
888
|
+
async #buildRequestPacket({ initialMessages, requirements, sessionId, runId, loopId, currentTurnSeq, provider, gitStatus, telemetryErrors: presetTelemetry, }) {
|
|
885
889
|
const byRole = (role) => initialMessages.filter((m) => m.role === role).map((m) => m.content).join("\n\n");
|
|
886
890
|
// plurnk.md (grammar/dialects) ONLY — the definition is the hot-path grammar.
|
|
887
891
|
// The scheme catalogue is its own `schemes` section below tools (§schemes-directory),
|
|
@@ -921,6 +925,9 @@ class Engine {
|
|
|
921
925
|
// omitted, section lines still shown). §tokenomics-render-weight-budget
|
|
922
926
|
const ceiling = _a.computeCeiling(provider.contextSize, this.#budgetCeiling);
|
|
923
927
|
const budgetReadout = this.#renderBudget(PacketWire.measureLogBudget(log, countTokens), ceiling);
|
|
928
|
+
// Per-scheme tally (§packet) so the model sees which schemes hold content without
|
|
929
|
+
// probing e.g. FIND(known://**) every turn. "" when empty → the section is omitted.
|
|
930
|
+
const catalogSummary = await this.#db.engine_scheme_catalog_summary.all({ session_id: sessionId });
|
|
924
931
|
// The default packet: an ordered list of sections, each addressable state
|
|
925
932
|
// (§packet-construction). `slot` is the prompt-cache boundary; the STATIC
|
|
926
933
|
// sections (definition, tools) lead the system slot so they form the cached
|
|
@@ -928,15 +935,18 @@ class Engine {
|
|
|
928
935
|
// last (the contract closest to the assistant turn); budget/errors/git are
|
|
929
936
|
// peer sections (unbundled). The budget section carries its {{tokensFree}}
|
|
930
937
|
// placeholders here; they resolve below once the assembled total is known.
|
|
938
|
+
const inject = await readPacketInject(); // #240 — operator section, per-turn, fail-hard on a broken path
|
|
931
939
|
const defaults = [
|
|
932
940
|
{ name: "definition", slot: "system", header: null, content: system_definition, tokens: 0 },
|
|
933
941
|
{ name: "tools", slot: "system", header: "Plurnk System Tools", content: tools.join("\n"), tokens: 0 },
|
|
934
942
|
{ name: "schemes", slot: "system", header: "Plurnk System Schemes", content: this.#schemes.teach(), tokens: 0 },
|
|
943
|
+
...(inject !== null ? [{ name: "inject", slot: "system", header: "Plurnk Operator Notes", content: inject, tokens: 0 }] : []),
|
|
935
944
|
{ name: "log", slot: "system", header: "Plurnk System Log", content: PacketWire.renderLog(log), tokens: 0 },
|
|
936
945
|
{ name: "prompt", slot: "user", header: "Plurnk System User Prompt", content: prompt, tokens: 0 },
|
|
937
946
|
{ name: "budget", slot: "user", header: "Plurnk System Budget", content: budgetReadout, tokens: 0 },
|
|
938
947
|
{ name: "errors", slot: "user", header: "Plurnk System Errors", content: PacketWire.renderErrors(telemetryErrors), tokens: 0 },
|
|
939
948
|
{ name: "git", slot: "user", header: "Plurnk System Git Status", content: PacketWire.renderGit(gitStatus), tokens: 0 },
|
|
949
|
+
{ name: "catalog", slot: "user", header: "Plurnk System Catalog", content: PacketWire.renderCatalog(catalogSummary), tokens: 0 },
|
|
940
950
|
{ name: "requirements", slot: "user", header: "Plurnk System Requirements", content: requirementsText, tokens: 0 },
|
|
941
951
|
];
|
|
942
952
|
// Plugin packet control (§packet-construction): trusted schemes rewrite the
|
|
@@ -1006,14 +1016,16 @@ class Engine {
|
|
|
1006
1016
|
// bullets + bare op forms match the packet's list/op rendering (no `- `,
|
|
1007
1017
|
// no backticks — see packet-wire.ts).
|
|
1008
1018
|
if (this.#executors !== undefined) {
|
|
1019
|
+
const excluded = docsExcludeSet();
|
|
1009
1020
|
for (const tag of this.#executors.availableRuntimes()) {
|
|
1021
|
+
if (excluded.has(tag))
|
|
1022
|
+
continue; // #240 — PLURNK_DOCS_EXCLUDE drops the oneliner + the doc
|
|
1010
1023
|
const entry = this.#executors.entry(tag);
|
|
1024
|
+
// #240 — identical treatment with the scheme directory: the example IS the oneliner,
|
|
1025
|
+
// the fuller doc (materialized at plurnk://docs/<tag>.md) rides an inline link whose
|
|
1026
|
+
// token cost lives on that manifest entry. No example → no line (like a provisional scheme).
|
|
1011
1027
|
if (entry?.example)
|
|
1012
|
-
tools.push(
|
|
1013
|
-
// #note12 — link the executor's fuller doc (materialized at plurnk:///docs/<tag>.md);
|
|
1014
|
-
// its token cost rides that manifest entry, so no inline recount here.
|
|
1015
|
-
if (entry?.documentation)
|
|
1016
|
-
tools.push(`* docs for ${tag}: plurnk://docs/${tag}.md`);
|
|
1028
|
+
tools.push(teachingLine(entry.example, tag, Boolean(entry.documentation)));
|
|
1017
1029
|
}
|
|
1018
1030
|
}
|
|
1019
1031
|
return tools;
|
|
@@ -1022,9 +1034,12 @@ class Engine {
|
|
|
1022
1034
|
// materialized at plurnk:///docs/<name>.md by loop_run (like operator docs) so the
|
|
1023
1035
|
// catalogue's doc-links READ and the manifest carries each doc's token cost.
|
|
1024
1036
|
docEntries() {
|
|
1025
|
-
const out = this.#schemes.docs();
|
|
1037
|
+
const out = this.#schemes.docs(); // scheme docs already drop PLURNK_DOCS_EXCLUDE names
|
|
1026
1038
|
if (this.#executors !== undefined) {
|
|
1039
|
+
const excluded = docsExcludeSet();
|
|
1027
1040
|
for (const tag of this.#executors.availableRuntimes()) {
|
|
1041
|
+
if (excluded.has(tag))
|
|
1042
|
+
continue; // #240 — exec docs honor the same exclude
|
|
1028
1043
|
const doc = this.#executors.entry(tag)?.documentation;
|
|
1029
1044
|
if (doc !== undefined && doc.length > 0)
|
|
1030
1045
|
out.push({ name: tag, content: doc });
|
|
@@ -1752,11 +1767,12 @@ class Engine {
|
|
|
1752
1767
|
return { status: 400, error: "KILL target must be a URL path with a scheme" };
|
|
1753
1768
|
if (schemeName === "log")
|
|
1754
1769
|
return { status: 405, error: "log:/// is append-only; KILL must bounce" };
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1770
|
+
// Process-KILL: any scheme whose handler exposes kill() aborts a live stream — the
|
|
1771
|
+
// exec handler, registered as "exec" + under every runtime tag (sh/node), so a tag-
|
|
1772
|
+
// addressed stream (sh:///l/t/s) routes here, not to deleteEntry. §exec
|
|
1773
|
+
const killable = this.#schemes.get(schemeName);
|
|
1774
|
+
if (killable !== undefined && typeof killable.kill === "function") {
|
|
1775
|
+
return await killable.kill(pathnameFromPath(path), statement.signal, ctx);
|
|
1760
1776
|
}
|
|
1761
1777
|
if (schemeName === "run") {
|
|
1762
1778
|
// terminate — abort any run by address; whoever holds it may end it.
|
|
@@ -1873,8 +1889,9 @@ class Engine {
|
|
|
1873
1889
|
const status = statement.signal;
|
|
1874
1890
|
if (status === null)
|
|
1875
1891
|
return { status: 400 };
|
|
1876
|
-
if (status === 200 || status === 499) {
|
|
1877
|
-
//
|
|
1892
|
+
if (status === 200 || status === 202 || status === 499) {
|
|
1893
|
+
// The broadcast terminals (200 done, 202 parked-async, 499 cancelled) advance
|
|
1894
|
+
// the loop; each carries its body as the loop's terminal message — the deliverable.
|
|
1878
1895
|
const body = statement.body;
|
|
1879
1896
|
const message = body === null ? null : typeof body === "string" ? body : body.raw;
|
|
1880
1897
|
await this.#db.engine_loop_set_status.run({ status, loop_id: loopId, message });
|
|
@@ -1934,15 +1951,13 @@ class Engine {
|
|
|
1934
1951
|
let attrsObj = (result.attrs !== undefined && result.attrs !== null)
|
|
1935
1952
|
? { ...result.attrs }
|
|
1936
1953
|
: {};
|
|
1937
|
-
// EXEC
|
|
1938
|
-
//
|
|
1939
|
-
//
|
|
1940
|
-
//
|
|
1941
|
-
//
|
|
1942
|
-
//
|
|
1943
|
-
//
|
|
1944
|
-
// Runtime comes from statement.signal (EXEC's runtime slot) so it's
|
|
1945
|
-
// resolvable for failed execs too; empty/absent = the default shell.
|
|
1954
|
+
// EXEC stream entry addresses by RUNTIME TAG as authority (§exec): it lives at
|
|
1955
|
+
// <runtime>:///<loop_seq>/<turn_seq>/<sequence> (e.g. sh:///1/1/2) — the runtime tag
|
|
1956
|
+
// is the scheme, the coordinate already unique per statement. The log row's target
|
|
1957
|
+
// points at this same address; its log:/// coordinate shares the trailing
|
|
1958
|
+
// <loop>/<turn>/<seq>, so the model correlates op to stream output. Runtime comes
|
|
1959
|
+
// from statement.signal (EXEC's runtime slot) so it's resolvable for failed execs
|
|
1960
|
+
// too; empty/absent = the default shell.
|
|
1946
1961
|
if (statement.op === "EXEC") {
|
|
1947
1962
|
const seqs = await this.#db.engine_loop_turn_seqs.get({
|
|
1948
1963
|
loop_id: loopId, turn_id: turnId,
|
|
@@ -1950,8 +1965,8 @@ class Engine {
|
|
|
1950
1965
|
if (seqs === undefined)
|
|
1951
1966
|
throw new Error(`Engine.#writeLog: loop_turn_seqs returned no row for loop=${loopId} turn=${turnId}`);
|
|
1952
1967
|
const runtime = (typeof statement.signal === "string" && statement.signal.length > 0) ? statement.signal : "sh";
|
|
1953
|
-
const coordPathname = `/${
|
|
1954
|
-
target.scheme =
|
|
1968
|
+
const coordPathname = `/${seqs.loop_seq}/${seqs.turn_seq}/${sequence}`;
|
|
1969
|
+
target.scheme = runtime;
|
|
1955
1970
|
target.pathname = coordPathname;
|
|
1956
1971
|
attrsObj.pathname = coordPathname;
|
|
1957
1972
|
// Mutate the in-memory result.attrs too: the dispatch path
|