@llblab/pi-kit 0.23.2 → 0.24.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/CHANGELOG.md +4 -0
- package/README.md +1 -1
- package/node_modules/@llblab/pi-state-flow/AGENTS.md +2 -2
- package/node_modules/@llblab/pi-state-flow/BACKLOG.md +3 -3
- package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +12 -0
- package/node_modules/@llblab/pi-state-flow/README.md +1 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/context.d.ts +50 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/context.js +248 -5
- package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +36 -27
- package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.d.ts +1 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.js +3 -2
- package/node_modules/@llblab/pi-state-flow/dist/package.json +1 -1
- package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-guide/SKILL.md +3 -1
- package/node_modules/@llblab/pi-state-flow/docs/architecture.md +6 -2
- package/node_modules/@llblab/pi-state-flow/docs/performance.md +48 -2
- package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +4 -3
- package/node_modules/@llblab/pi-state-flow/lib/context.ts +223 -6
- package/node_modules/@llblab/pi-state-flow/lib/extension.ts +37 -32
- package/node_modules/@llblab/pi-state-flow/lib/protocol.ts +4 -2
- package/node_modules/@llblab/pi-state-flow/package.json +1 -1
- package/node_modules/@llblab/pi-state-flow/skills/state-flow-guide/SKILL.md +3 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@llblab/pi-kit` are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.24.0 - 2026-09-25
|
|
6
|
+
|
|
7
|
+
- `Minimal State Flow Reconciliation`: Advances the exact State Flow pin to `0.21.0`. Frozen context heads retain cache-stable prefixes while predictable accepted writes omit redundant semantic tails. Shared drift, changed hints and unknown fallback still reconcile; lazy bodies and provenance remain hidden. Artifact prediction cannot reject an already accepted patch. Canonical storage, other package pins, resources and load order are unchanged.
|
|
8
|
+
|
|
5
9
|
## 0.23.2 - 2026-09-24
|
|
6
10
|
|
|
7
11
|
- `State Flow Memory`: Advances the exact State Flow pin to `0.19.1`. Automatic recent transitions exclude lazy patches, including deletions, without changing canonical history or explicit reads; targeted historical reads remain available when useful to the task, without resurrecting deleted memory.
|
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ Package links lead to the owning repositories for usage, documentation, issues,
|
|
|
16
16
|
| [`@llblab/pi-clean-room`](https://github.com/llblab/pi-clean-room) | `0.2.0` | Isolated nested Pi TUI with named npm extensions and compatible model selection |
|
|
17
17
|
| [`@llblab/pi-codex-usage`](https://github.com/llblab/pi-codex-usage) | `0.10.0` | Compact Codex/Spark subscription-limit and Business credit-usage status |
|
|
18
18
|
| [`@llblab/pi-grow-loop`](https://github.com/llblab/pi-grow-loop) | `0.8.2` | Visible continuation scheduling and bounded worker Skills through compiled, manifest-owned resources |
|
|
19
|
-
| [`@llblab/pi-state-flow`](https://github.com/llblab/pi-state-flow) | `0.
|
|
19
|
+
| [`@llblab/pi-state-flow`](https://github.com/llblab/pi-state-flow) | `0.21.0` | Incremental scoped context/memory compiler with cache-stable heads, sparse acceptance reconciliation, lazy isolation, private fork memory, and optional Git backup |
|
|
20
20
|
| [`@llblab/pi-telegram`](https://github.com/llblab/pi-telegram) | `0.51.4` | Telegram companion with resolver-owned filterable Skills, compiled distribution, self-healing follower registration, in-flight model switching, files, voice, and controls |
|
|
21
21
|
| [`@llblab/skills`](https://github.com/llblab/skills) | `1.15.0` | Portable workflows for engineering, review, design, context maintenance, and other focused tasks |
|
|
22
22
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
- Keep independent domain modules under `lib/`, mirror every domain with a same-named file under `tests/`, place cross-domain architecture checks in `tests/invariants.test.ts`, and keep `index.ts` as a minimal public-export boundary. Keep `lib/extension.ts` as the Pi lifecycle composition root: it may wire configuration, domain capabilities, handlers, and event subscriptions, but low-level parsing, traversal, formatting, persistence, queue/worker mechanics, and diagnostic I/O belong to their existing owning domains.
|
|
4
4
|
- Keep the extension opt-in and preserve clear attribution to SKILL.state wherever the inherited explicit-state approach is described.
|
|
5
5
|
- Distinguish three model-facing states: active, passive, and configured off. Active and passive both expose `read_state` and `patch_state` when memory tools are permitted; both use the same canonical persistence, private ownership and fork-copy rules. The difference is the agent workflow, not whether memory is saved. Active iterations must consolidate future-relevant results into accepted state before completed context is replaced: make the final meaningful semantic patch when needed, then the next iteration starts from current state plus its new input rather than the previous completed reasoning. Passive mode keeps ordinary conversation continuity and allows on-demand reading/patching without that state-driven iteration boundary or the same mandatory consolidation pressure. Configured off exposes neither memory tool and injects no State Flow bootstrap/state context; it is not another name for passive. In the current configuration, off means an inactive branch with both `passiveTools` and `passiveBootstrap` false; `autoStart: false` alone is not off. The independent one-flag configurations remain supported partial integrations, not full passive or full off. Start/Stop govern active/passive workflow and must not be treated as switches for canonical storage or fork initialization. See [mode semantics](docs/usage.md#active-passive-and-configured-off) for completion and bootstrap terminology.
|
|
6
|
-
- Preserve Pi's native tool loop and complete inspectable session trace; project completed-run history only at user-run boundaries. The
|
|
6
|
+
- Preserve Pi's native tool loop and complete inspectable session trace; project completed-run history only at user-run boundaries. The context domain owns model projection of the raw scope overlay; callers must not pre-project it. Freeze the complete active/bootstrap head, including timestamp, specification and recent transitions, at the first accepted context. Reset the volatile projection at a new active run, accepted active completion, Start/Stop, selection/resume/reload, native compaction or a removed/replaced native prefix; passive user turns alone never reset it. Keep changing artifact hints, invalidations, rehydration and otherwise unreported shared drift in stable-position tail notices, never head rewrites. Successful patch receipts advance the last communicated view without redundant notices. Bind state updates to the head's volatile projection ID so retained results from an earlier head remain historical; never store this ID in canonical state or runtime metadata. Skill acquisition hints remain in native read results. This cache grants no publication, history, selection or compaction authority and adds no tail-size threshold. Retain persistent and current-run context-bearing custom messages from other extensions, plus the complete current-run trajectory. Select a unique captured native user timestamp independently of text decoration, including SDK image hints. Observe native user events even while disabled; Start/Stop must retain that observed run anchor. Reset it at native before-agent-start and session-start/tree boundaries, not semantic mode changes. Observation alone never enables state, publishes memory or authorizes compaction. Without a capture, a unique exact specification match may select only model projection. Missing, nonfinite or ambiguous matches retain available context; projection must never assign the lifecycle run anchor. Direct completion persists no private validation feedback.
|
|
7
7
|
- Expose one canonical intent-first materialized state shape across global, CWD, and session scopes with exactly `intents`, `contract`, `working`, `artifacts`, `response`, and `lazy`; the first four and the required `lazy` root are flexible semantic objects, while `response` is the exact latest accepted assistant answer, including the empty string, and is owned only by session state. Global and CWD keep the required key as an empty structural placeholder; effective state inherits the session value. Automatic model-state and recent-transition projections omit lazy bodies. Remove whole `patch.lazy` entries and then empty scoped patches/transitions, omitting an empty recent window without renumbering retained identities or positions. Preserve hot-history projection, artifact metadata filtering and bounded lazy navigation. Canonical history, explicit value/patch reads and already communicated native/user/tool/response text remain unchanged. Preserve this plane order in model-facing projection and state presentation. `intents` contains only active commitments to future action, remains hot by default, and must not become a planner, scheduler, task manager, or execution loop.
|
|
8
8
|
- Key artifacts directly by their source path. Model-visible artifact entries require only a non-empty `description` plus optional forward-compatible metadata; runtime-owned compilation evidence (`sourceFingerprint`, `sourceHash`, `compilerRevision`, `compiledAt`) belongs in the scope `meta.json` provenance registry. Reserve model-authored `hint`; runtime may add it only to effective/model projection and never to canonical scope state. Retired embedded `hash`/`compiler`/`compiled_at` fields remain readable compatibility evidence and are stripped from model projection. Validate provenance restrictions on every authored artifact entry, not on retained legacy state: individual runtime/legacy provenance fields cannot be set or deleted by model patches in any scope.
|
|
9
9
|
- Inspect only exact artifact source paths already registered in global, CWD, or session state. Never scan directories, discover files, reserve a Knowledge root, interpret file extensions, or read unrelated source bodies. Observe regular non-symlink files through `size + mtimeNs`; proven absence removes the artifact and provenance from each exact owning scope, while relative, symlink, directory, inaccessible, or otherwise unavailable paths are non-destructive evidence.
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
- Bind active Pi checkpoints to retained semantic boundaries in the current canonical lineage. Awaited runtime restoration uses `withRestoreTransaction`: detach the pointer before waiting, capture/select private history beside current shared streams under one exclusion, recheck caller policy, and publish once before installing the candidate. Expiry or malformed evidence cannot authorize current-head or empty fallback; origin acceptance retains configured folding and causal provenance rules. Resume and tree restoration select only retained private session history while global/CWD scopes remain live; every failure resolving a selected boundary fails closed without falling through to older checkpoints, disabled markers, Git revisions, or unrelated newer private state. Passive shared-memory availability does not prove the selected private layer: preserve the recovery failure and reject historical session reads and passive publication until it is resolved. Explicit Start may independently activate the validated live same-session canonical head. A same-owner failed-Stop marker permits read-only loading of validated current memory on reload, never historical substitution or publication; passive tools cannot clear its write fence. Awaited recovery uses `refreshCurrentMemory`: capture current authority under exclusion, reject cancellation before cache installation, never publish or initialize absence, and retain the prior cache on failure. Its disabled snapshot omits unfinished specifications and grants no write authority; native callers still own selection and Stop fences. Native startup/tree/fork/auto-start/failed-Stop restoration awaits one owned lifetime in the composition root: pending selection stays passive and unavailable, selection revokes older work, and shutdown drains current and superseded operations. Start/Stop select agent policy, not the existence of canonical memory. Stop must not cancel retained restoration, auto-start memory initialization or fork copying: apply passive policy inside the pending memory acceptance and keep configured passive patching available afterward. Start waiter cancellation must not cancel independently owned attachment or copying. Only selection changes, shutdown, native operation cancellation and actual authority/storage failures revoke that work; a mode change must not manufacture a storage-error fence. Start pins physical identity across cancelable joins without cancelling independently owned recovery or Stop persistence. Only current acceptance installs memory, checkpoint, tools and UI. Keep read-only candidates detached until cancellation, physical identity and any intervening accepted cache change are rechecked.
|
|
26
26
|
- Every accepted semantic change, including session-only and response-only changes, atomically updates affected canonical checkpoint/tail pairs and temporal metadata without creating a Git commit. Each materially changed scope advances its own persisted revision exactly once; Global and CWD revisions are shared across their writers, Session is private, and Effective is represented by the `G#/C#/S#` revision vector rather than an invented scalar owner. After an accepted turn reconciles its response and reaches `agent_before_settle`, one best-effort backup may commit only State Flow-owned already-written files. Await the backup mutex before briefly locking canonical storage for bounded root/CWD/session namespace inventory and regular-file byte capture; hold the separate mutex through completion and never acquire it from a canonical writer. The Promise-returning backup API accepts cancellation. Combine a real Pi operation signal with shutdown cancellation and drain owned attempts before shutdown completes. Pi 0.87 provides no operation signal at settlement: refuse contention there with explicit diagnostic-only deferral until a later accepted turn, rather than trapping native Abort. Do not infer cancellation from an ended agent signal, steal locks, add a background owner, or apply this optional-backup fallback to semantic publication. Release the canonical lock before every Git command, filter, staging write, ref update, or index synchronization. Recheck the Git branch/head after capture waits. Stage only the captured snapshot through a private temporary worktree/index, preserve Git ignore/filter policy, and never inventory artifact sources or unrelated directory trees. Keep unrelated staged/index-only content and worktree edits intact; synchronize only exact owned index paths, include HEAD-owned deletions even when already absent from the caller index, and never create an unowned-only initial backup. Backup failure is diagnostic-only and never rolls back state, suppresses an answer, requests continuation, or blocks a later turn. After each successful backup attempt, asynchronously push the exact current commit to the attached branch's explicitly configured remote/ref without force. Permit at most one in-flight push per repository root in process; skip overlapping attempts, leaving a later accepted turn to push the latest HEAD. Session shutdown waits for the in-flight process to close, within the existing Git push timeout and termination behavior; suppress push-failure reporting after shutdown begins. Push failure has no semantic effect; while active, warn once per failure streak with a concise message and reset suppression after successful replication. Record the available redacted Git failure detail in the local diagnostic log even when general logging is off; if local recording fails, warn once with the available detail. A later accepted turn retries the latest backup. Durable push queues, publication workers, worker leases, and remote retry generations do not exist.
|
|
27
27
|
- Register `patch_state` as the sole model-authored semantic mutation protocol. Its canonical grammar accepts one or more fixed `global`, `cwd`, and `session` semantic patches; reject unknown fields, empty supplied scopes, and retired finalization or `{scope, patch}` / `unchanged` grammar. Apply authored Global/CWD operations to the current canonical head, preserving untouched values; overlapping targets follow successful acceptance order, not stale agent snapshots. Validate and publish all supplied scopes as one durable cohort, deriving replay and revisions from that actual basis. Correct repeats succeed without another semantic identity, step or scope revision. Preserve private ownership/history fences and complete artifact/provenance units. Never accept model-authored `response`.
|
|
28
|
-
- Treat every semantic `patch_state` as a strict inference barrier. Give it sequential execution mode; find the matching synchronized assistant through public parent traversal from the selected leaf without constructing a full branch during tool preflight; require exactly one `patch_state` call in that response and block every sibling tool call before execution. The next inference sees rematerialized state. Ordinary accepted answers require no finalization patch or fallback inference.
|
|
28
|
+
- Treat every semantic `patch_state` as a strict inference barrier. Give it sequential execution mode; find the matching synchronized assistant through public parent traversal from the selected leaf without constructing a full branch during tool preflight; require exactly one `patch_state` call in that response and block every sibling tool call before execution. The next inference sees rematerialized state. Successful native patch results carry context-owned `state_updates` only when needed: exact projected effective replacements/deletions at key/index-segment path arrays. Predictable direct non-null leaf writes are not echoed when their accepted effective value matches the authored value and no other authored scope touches the same path, ancestor or descendant. Explicit Session scalar/array replacements may omit matching receipts despite lower-scope overlap because Session masks lower values; deletion and object-merge overlaps remain conservative. Indexed-array selectors map to numeric update segments only if the communicated effective basis proves that parent is an in-bounds array; literal object keys stay literal. A deletion is omitted only when the previously communicated effective value and accepted effective value are identical and no other authored scope overlaps; effective-only projection does not prove lower-scope ownership, so changed fallback remains conservative. Artifact card replacements or merges can omit their receipt only when the projected authored fields applied to the communicated card exactly match the accepted card after stripping retired provenance; a retained runtime hint is part of that known basis, while new/removed/changed hints and semantic drift remain visible. Lazy navigation receipts can be omitted only when a complete communicated key/kind catalog and non-deleting top-level writes predict the entire accepted catalog; hidden bodies remain invisible, and deletion, overlap or drift stays conservative. Retain reconciliation for masked writes, overlapping scope operations, and projected drift since the last communicated view, or the caller's pre-wait cache before any head exists. Keep effective scope masking/fallback, artifact metadata filtering and lazy-body omission; only changed bounded lazy navigation may accompany updates. Keep the compact interactive acknowledgement separate from this model-facing result block. Ordinary accepted answers require no finalization patch or fallback inference.
|
|
29
29
|
- Reconcile `response` only from the actually accepted ordinary assistant answer at `turn_end`; it remains runtime-owned, and an accepted empty answer reconciles to `""` rather than causing a finalization failure. Await `withPatchTransaction` before staging against the current shared head; publish the response and completed-run lifecycle together, then install caches/checkpoint before yielding without a second persistence call. Combine Pi's operation signal with response-owned cancellation: run/selection changes, Stop, shutdown and superseding answers withdraw obsolete waits, whose completion must never clear newer pending work, restore a specification, update UI or erase a Stop fence. Cancellation before acceptance leaves the run unfinished; cancellation afterward cannot undo accepted memory. State Flow has no terminal HTML-comment mutation protocol and does not parse generic service comments. Other extensions retain ownership of their own comments and output handling.
|
|
30
30
|
- Treat terminal state as a decision-relevant handoff, not narration: retain source-addressed reusable operational knowledge in `artifacts`; compile stable requirements, confirmed decisions, rejected approaches, and interface commitments into `contract`; retain observations, validation, failures, current domain state, unresolved work, interaction consequences, and exact continuation in `working`. Preserve relevant completed prerequisites and verified outcomes while removing obsolete progress narration; reconcile only information affected by the run and relevant existing commitments, not every scope or repository surface.
|
|
31
31
|
- Preserve active constraints, unresolved questions, consequential negative results, and the next discriminating check before compression. Distinguish observations, user requirements, assistant decisions, and hypotheses; do not promote assistant conclusions to user requirements. Treat resource, document, memory, Skill, and agent locators as semantic references wherever context expresses them. Inside ordinary strings and prose, encode a semantic-state path as `$` immediately followed by one valid `read_state` path, for example `$effective.lazy.memory[7]`; the prefix distinguishes a reference from incidental path-like text and leaves room for future parsing. `{"$ref":"cwd.lazy.plan"}` remains the optional structured state-reference form; neither form is a runtime link type. External resource locators retain their native path, URI, Skill, or agent syntax. Resolve references explicitly through the appropriate read/tool when needed and infer no authority, existence, dependency, hydration, execution, or completion merely from their presence. Never scan, audit, or proactively resolve references merely to find broken ones. Only after one requested `read_state` value path is missing may the query domain perform one bounded reverse lookup over current model-patchable semantic planes for exact structured `$ref` and `$path` matches. When durable sources match, return a successful top-level `{value:null, hint:[...]}` diagnostic sentinel: every hint has `type:"dangling-reference"`, a descriptive conditional message, and at most three runtime-verified current reference-owner paths, never purported relocated targets or lazy bodies. The null sentinel is never presented without `hint` for this case; keys, patch, and multi-path reads retain ordinary all-or-error semantics. Never scan runtime-owned `response`, and retain the ordinary missing-path error when no durable source matches. A match proves durable semantic provenance, not staleness; no match does not prove invention. Missing paths or runtime hints alone never require historical search. The agent may choose targeted historical reads when a previous value is useful to the current task without separate user permission; otherwise continue without searching. Treat past values as historical evidence, not automatically current memory, and never automatically restore deleted data. Do not scan offsets, hydrate automatically, add repair inference or search historical reference owners. Hints do not prove prior existence, retained history or relocation. The agent may inspect ownership and patch a proven stale source within touched work while preserving surrounding meaning, without resurrecting its target. Effective-state absence does not identify ownership, and unavailable history, inaccessible external resources, or transient read failure do not prove a dangling reference. Retain useful source locators and validity conditions for consequential facts without mandatory per-value metadata. Keep rejection reasons and reconsideration conditions. Reconcile contradictions through evidence or user clarification instead of silently overwriting established constraints or observations; retain unresolved conflicts and decision-relevant hypotheses as uncertain. These are protocol obligations, not deterministic semantic validation gates.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Backlog
|
|
2
2
|
|
|
3
|
-
No open implementation items for the 0.
|
|
3
|
+
No open implementation items for the 0.21.0 Minimal Reconciliation scope. Completed outcomes are recorded in [CHANGELOG.md](CHANGELOG.md); current behavior and conservative reconciliation boundaries belong in [architecture](docs/architecture.md).
|
|
4
4
|
|
|
5
|
-
Release publication is owned by `.github/workflows/release.yml`. Verify the exact tag's successful workflow, published GitHub Release and matching npm package before reporting completion.
|
|
5
|
+
Release publication is owned by `.github/workflows/release.yml`. Verify the exact tag's successful workflow, published GitHub Release and matching npm package before reporting release completion. Installed-instance reload remains a separate operator action.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Canonical storage, revisions, history, CAS and lifecycle formats are unchanged. Structural prefix evidence is described in the [benchmark guide](benchmarks/README.md); it does not claim provider cache-hit, token-cost or latency guarantees.
|
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
> Each release keeps at most 8 outcome records of at most 512 characters.
|
|
4
4
|
|
|
5
|
+
## 0.21.0: Minimal Reconciliation
|
|
6
|
+
|
|
7
|
+
- `Sparse acceptance receipts`: Predictable direct/indexed writes, object replacements, disjoint scope writes, explicit Session scalar/array overrides, unchanged-effective deletions, artifact merges and complete lazy-navigation updates omit redundant semantic tails. Shared drift, changed hints, unknown fallback and ambiguous overlap still reconcile; lazy bodies and provenance stay hidden. Canonical storage, revisions and lifecycle formats are unchanged.
|
|
8
|
+
- `Acceptance and projection safety`: Artifact prediction against a masked effective array cannot turn an accepted scoped patch into a tool failure. Conservative reconciliation preserves the successful acknowledgement. Native active, passive and Stop-handoff regressions cover accepted storage, while prefix probes distinguish predictable acceptance from a minimal shared-surprise tail without rewriting earlier context.
|
|
9
|
+
|
|
10
|
+
## 0.20.0: Cache-Stable Projection
|
|
11
|
+
|
|
12
|
+
- `Cache-stable model projection`: Active/bootstrap heads freeze per accepted iteration; passive heads survive ordinary user turns. Native/mode boundaries rebase without semantic changes. Patch receipts and stable-position notices deliver current values, hints and invalidations without rewriting prior prefixes. Volatile projection IDs keep retained old results historical after rebasing. Native regressions cover repeated barriers, passive continuation and bootstrap; canonical history is unchanged.
|
|
13
|
+
- `Accepted-state patch tails`: Successful active/passive patch results carry projected effective replacements and deletions for touched paths and transaction-adopted shared drift, including correct repeats and scope fallback/masking. Lazy bodies and artifact provenance stay hidden; changed bounded lazy navigation remains available. Native tests prove the next inference sees these values in active, passive and Stop-handoff modes. Interactive acknowledgement and patch-display policy stay unchanged.
|
|
14
|
+
- `Trajectory-dominant prefix evidence`: Opt-in v3 reports exercise active, passive and Stop-handoff projection with six 20 KiB native reads before a patch and two before another. Reports retain every inference's byte prefix and read volume; native content, accepted barriers and completion are checked independently of provider callbacks. Source-bound baseline measurements distinguish frozen Stop handoff from regenerated active/passive heads without claiming provider cache-hit rates.
|
|
15
|
+
- `Minimum-Node benchmark validation`: Optional `BENCH_REPORT_PATH` writes complete machine-readable reports independently of test-runner stdout chunking, while retaining human-readable stdout. Regression tests read file reports for successful and failed workloads, including 1 MiB backpressure. Release validation now covers Node 22.19.0 and Node 24; publication remains on Node 24.
|
|
16
|
+
|
|
5
17
|
## 0.19.1: Lazy Isolation and Task-Driven History
|
|
6
18
|
|
|
7
19
|
- `Lazy projection isolation`: Automatic recent transitions omit entire lazy patches, including deletions, and discard empty visible records without renumbering history. Hot changes, artifact metadata filtering and bounded lazy navigation remain available. Canonical state/history, explicit reads and already communicated user/tool/response text are unchanged.
|
|
@@ -22,7 +22,7 @@ Patched state + Answer
|
|
|
22
22
|
Updated state
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
Within a run, the available request, intermediate responses, tool results and steering remain in context. A state patch
|
|
25
|
+
Within a run, the available request, intermediate responses, tool results and steering remain in context. The initial memory head stays byte-stable: accepted changes arrive in patch results, and changing runtime notices append at the tail without moving earlier messages. Passive memory uses the same approach across ordinary user turns, rebasing at native or mode boundaries. Projection IDs distinguish current updates from retained historical results. A state patch neither rewrites the head nor discards the working trajectory. Persistent context-bearing messages from other extensions are preserved as well.
|
|
26
26
|
|
|
27
27
|
This reduces reliance on repeated model-generated summaries of an accumulating transcript. Retaining the current trajectory also allows prompt-cache reuse while the relevant prefix remains unchanged. Avoiding summary calls and repeated prompt processing can improve responsiveness; the result depends on the model, provider, workload and frequency of state changes, not a fixed latency guarantee.
|
|
28
28
|
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
2
2
|
import { type ArtifactInvalidationNotice, type ArtifactModelHints } from "./artifact.ts";
|
|
3
3
|
import type { RecentTransitionWindow } from "./history.ts";
|
|
4
|
+
import { type JsonValue } from "./json.ts";
|
|
4
5
|
import type { Snapshot } from "./snapshot.ts";
|
|
5
6
|
import type { RehydrationPhase } from "./rehydration.ts";
|
|
6
|
-
import { type MaterializedState, type ModelState } from "./state.ts";
|
|
7
|
+
import { type AtomicScopePatches, type MaterializedState, type ModelState } from "./state.ts";
|
|
7
8
|
/** Refresh only our section; Pi owns system frames, tools and forced-prompt precedence. */
|
|
8
9
|
export declare function projectSystemProtocol(messages: AgentMessage[], protocol: string | undefined): AgentMessage[];
|
|
9
10
|
type LazyValueKind = "array" | "boolean" | "null" | "number" | "object" | "string";
|
|
@@ -13,18 +14,66 @@ export declare function lazyNavigationHint(state: MaterializedState): {
|
|
|
13
14
|
path: string;
|
|
14
15
|
keys?: Record<string, LazyValueKind>;
|
|
15
16
|
};
|
|
17
|
+
export type ModelStateUpdate = {
|
|
18
|
+
path: (string | number)[];
|
|
19
|
+
} & ({
|
|
20
|
+
value: JsonValue;
|
|
21
|
+
} | {
|
|
22
|
+
deleted: true;
|
|
23
|
+
});
|
|
24
|
+
/** Exact projected replacements, not authored merge patches; paths are unambiguous key/index segments. */
|
|
25
|
+
export declare function acceptedStateUpdates(before: MaterializedState, after: MaterializedState, patches: AtomicScopePatches): {
|
|
26
|
+
effective: ModelStateUpdate[];
|
|
27
|
+
lazy_navigation?: {
|
|
28
|
+
available: boolean;
|
|
29
|
+
path: string;
|
|
30
|
+
keys?: Record<string, LazyValueKind>;
|
|
31
|
+
} | undefined;
|
|
32
|
+
};
|
|
33
|
+
export interface ContextView {
|
|
34
|
+
state: ModelState;
|
|
35
|
+
lazy_navigation?: ReturnType<typeof lazyNavigationHint>;
|
|
36
|
+
artifact_invalidations: readonly ArtifactInvalidationNotice[];
|
|
37
|
+
knowledge_rehydration: {
|
|
38
|
+
phase: RehydrationPhase;
|
|
39
|
+
} | null;
|
|
40
|
+
}
|
|
41
|
+
export declare function contextView(state: MaterializedState, hints: ArtifactModelHints, invalidations: readonly ArtifactInvalidationNotice[], phase?: RehydrationPhase): ContextView;
|
|
42
|
+
/** Volatile model projection only. Native messages own trajectory; this cache owns no persistence or lifecycle. */
|
|
43
|
+
export declare class ContextProjection {
|
|
44
|
+
private identity;
|
|
45
|
+
private head;
|
|
46
|
+
private view;
|
|
47
|
+
private native;
|
|
48
|
+
private notices;
|
|
49
|
+
reset(): void;
|
|
50
|
+
/** Called only after successful publication and ancillary acceptance, immediately before returning the native result. */
|
|
51
|
+
acceptPatch(before: MaterializedState, after: MaterializedState, patches: AtomicScopePatches, hints: ArtifactModelHints): {
|
|
52
|
+
effective: ModelStateUpdate[];
|
|
53
|
+
lazy_navigation?: {
|
|
54
|
+
available: boolean;
|
|
55
|
+
path: string;
|
|
56
|
+
keys?: Record<string, LazyValueKind>;
|
|
57
|
+
} | undefined;
|
|
58
|
+
projection: `${string}-${string}-${string}-${string}-${string}`;
|
|
59
|
+
} | undefined;
|
|
60
|
+
project(messages: AgentMessage[], current: ContextView, makeHead: () => AgentMessage, initial?: ContextView): AgentMessage[];
|
|
61
|
+
}
|
|
16
62
|
/** Context retained after semantic State Flow is stopped in this physical session. */
|
|
17
63
|
export interface PassiveContinuation {
|
|
18
64
|
startedAt: number;
|
|
19
65
|
activeRunStartedAt?: number;
|
|
20
66
|
preserveContext?: true;
|
|
21
67
|
handoff: AgentMessage;
|
|
68
|
+
state: ModelState;
|
|
22
69
|
}
|
|
23
70
|
export declare function syntheticUser(text: string): AgentMessage;
|
|
24
71
|
export declare function createPassiveContinuation(state: ModelState, startedAt?: number, activeRunStartedAt?: number, preserveContext?: boolean): PassiveContinuation;
|
|
25
72
|
/** Keep the interrupted run through later results; an idle stop retains only later conversation. */
|
|
26
73
|
export declare function passiveContinuationMessages(messages: AgentMessage[], continuation: PassiveContinuation): AgentMessage[];
|
|
27
74
|
export declare function runtimeContextMessage(snapshot: Snapshot, state: MaterializedState, recentTransitions?: RecentTransitionWindow, artifactInvalidations?: readonly ArtifactInvalidationNotice[], rehydrationPhase?: RehydrationPhase, artifactHints?: ArtifactModelHints): AgentMessage;
|
|
75
|
+
/** Render a view already projected by this domain without cloning the full semantic overlay twice. */
|
|
76
|
+
export declare function runtimeContextHead(snapshot: Snapshot, view: ContextView, recentTransitions?: RecentTransitionWindow): AgentMessage;
|
|
28
77
|
/** Captured identity survives text decoration; an uncertain boundary retains available context. */
|
|
29
78
|
export declare function currentRunTrajectory(messages: AgentMessage[], specification: string | undefined, anchorTimestamp: number | undefined): {
|
|
30
79
|
messages: AgentMessage[];
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { projectArtifactForModel } from "./artifact.js";
|
|
2
|
-
import { isObject, presentationJson } from "./json.js";
|
|
3
|
+
import { applyPatch, isObject, presentationJson, sameJson } from "./json.js";
|
|
3
4
|
import { projectStateForModel } from "./state.js";
|
|
4
5
|
/** Refresh only our section; Pi owns system frames, tools and forced-prompt precedence. */
|
|
5
6
|
export function projectSystemProtocol(messages, protocol) {
|
|
@@ -52,6 +53,243 @@ export function lazyNavigationHint(state) {
|
|
|
52
53
|
const keys = Object.fromEntries(entries.map(([key, value]) => [key, lazyValueKind(value)]));
|
|
53
54
|
return JSON.stringify(keys).length <= LAZY_HINT_MAX_JSON_CHARS ? { ...base, keys } : base;
|
|
54
55
|
}
|
|
56
|
+
/** Exact projected replacements, not authored merge patches; paths are unambiguous key/index segments. */
|
|
57
|
+
export function acceptedStateUpdates(before, after, patches) {
|
|
58
|
+
return projectedStateUpdates(projectStateForModel(before), projectStateForModel(after), patches, lazyNavigationHint(before), lazyNavigationHint(after));
|
|
59
|
+
}
|
|
60
|
+
function projectedStateUpdates(previous, current, patches, beforeNavigation, navigation) {
|
|
61
|
+
const effective = [];
|
|
62
|
+
const prefix = (parent, child) => parent.length <= child.length && parent.every((part, index) => part === child[index]);
|
|
63
|
+
const put = (path, value) => {
|
|
64
|
+
if (effective.some((entry) => prefix(entry.path, path)))
|
|
65
|
+
return;
|
|
66
|
+
for (let index = effective.length - 1; index >= 0; index--) {
|
|
67
|
+
if (prefix(path, effective[index].path))
|
|
68
|
+
effective.splice(index, 1);
|
|
69
|
+
}
|
|
70
|
+
effective.push({ path, ...(value === undefined ? { deleted: true } : { value: structuredClone(value) }) });
|
|
71
|
+
};
|
|
72
|
+
const child = (value, key) => value !== null && typeof value === "object" && Object.hasOwn(value, key)
|
|
73
|
+
? value[key] : undefined;
|
|
74
|
+
const diff = (left, right, path) => {
|
|
75
|
+
if (left === undefined && right === undefined || left !== undefined && right !== undefined && sameJson(left, right))
|
|
76
|
+
return;
|
|
77
|
+
if (isObject(left) && isObject(right)) {
|
|
78
|
+
for (const key of new Set([...Object.keys(left), ...Object.keys(right)]))
|
|
79
|
+
diff(child(left, key), child(right, key), [...path, key]);
|
|
80
|
+
}
|
|
81
|
+
else if (Array.isArray(left) && Array.isArray(right) && left.length === right.length) {
|
|
82
|
+
for (let index = 0; index < right.length; index++)
|
|
83
|
+
diff(left[index], right[index], [...path, index]);
|
|
84
|
+
}
|
|
85
|
+
else
|
|
86
|
+
put(path, right);
|
|
87
|
+
};
|
|
88
|
+
const touched = (patch, value, path) => {
|
|
89
|
+
if (!isObject(patch)) {
|
|
90
|
+
put(path, value);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const keys = Object.keys(patch);
|
|
94
|
+
if (keys.length === 0)
|
|
95
|
+
return;
|
|
96
|
+
if (Array.isArray(value) && keys.every((key) => /^\[(0|[1-9]\d*)\]$/.test(key))) {
|
|
97
|
+
for (const key of keys) {
|
|
98
|
+
const index = Number(key.slice(1, -1));
|
|
99
|
+
// A higher-scope array may mask the patched array with a different length.
|
|
100
|
+
if (index >= value.length) {
|
|
101
|
+
put(path, value);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
touched(patch[key], value[index], [...path, index]);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
else if (isObject(value)) {
|
|
108
|
+
for (const key of keys)
|
|
109
|
+
touched(patch[key], child(value, key), [...path, key]);
|
|
110
|
+
}
|
|
111
|
+
else
|
|
112
|
+
put(path, value);
|
|
113
|
+
};
|
|
114
|
+
diff(previous, current, []);
|
|
115
|
+
for (const patch of Object.values(patches))
|
|
116
|
+
for (const [plane, value] of Object.entries(patch)) {
|
|
117
|
+
if (plane === "lazy")
|
|
118
|
+
continue;
|
|
119
|
+
if (plane === "artifacts" && isObject(value)) {
|
|
120
|
+
for (const path of Object.keys(value))
|
|
121
|
+
put([plane, path], child(current.artifacts, path));
|
|
122
|
+
}
|
|
123
|
+
else
|
|
124
|
+
touched(value, child(current, plane), [plane]);
|
|
125
|
+
}
|
|
126
|
+
return { effective, ...(navigation !== undefined && (beforeNavigation === undefined || !sameJson(beforeNavigation, navigation)) ? { lazy_navigation: navigation } : {}) };
|
|
127
|
+
}
|
|
128
|
+
export function contextView(state, hints, invalidations, phase) {
|
|
129
|
+
return { state: projectStateForModel(state, hints), lazy_navigation: lazyNavigationHint(state),
|
|
130
|
+
artifact_invalidations: structuredClone(invalidations), knowledge_rehydration: phase === undefined ? null : { phase } };
|
|
131
|
+
}
|
|
132
|
+
/** Volatile model projection only. Native messages own trajectory; this cache owns no persistence or lifecycle. */
|
|
133
|
+
export class ContextProjection {
|
|
134
|
+
identity = randomUUID();
|
|
135
|
+
head;
|
|
136
|
+
view;
|
|
137
|
+
native = [];
|
|
138
|
+
notices = [];
|
|
139
|
+
reset() {
|
|
140
|
+
this.identity = randomUUID();
|
|
141
|
+
this.head = undefined;
|
|
142
|
+
this.view = undefined;
|
|
143
|
+
this.native = [];
|
|
144
|
+
this.notices = [];
|
|
145
|
+
}
|
|
146
|
+
/** Called only after successful publication and ancillary acceptance, immediately before returning the native result. */
|
|
147
|
+
acceptPatch(before, after, patches, hints) {
|
|
148
|
+
const state = projectStateForModel(after, hints);
|
|
149
|
+
const navigation = lazyNavigationHint(after);
|
|
150
|
+
const beforeNavigation = this.view?.lazy_navigation ?? lazyNavigationHint(before);
|
|
151
|
+
const updates = projectedStateUpdates(this.view?.state ?? projectStateForModel(before, hints), state, patches, beforeNavigation, navigation);
|
|
152
|
+
// Suppress direct writes only when the accepted effective value matches.
|
|
153
|
+
// Overlap stays conservative except for explicit top-scope replacements:
|
|
154
|
+
// Session scalars/arrays mask every lower-scope value at that path.
|
|
155
|
+
const leaves = [];
|
|
156
|
+
const objects = [];
|
|
157
|
+
const known = (path) => {
|
|
158
|
+
let value = this.view?.state;
|
|
159
|
+
for (const part of path) {
|
|
160
|
+
if (value === undefined || value === null || typeof value !== "object" || !Object.hasOwn(value, part))
|
|
161
|
+
return undefined;
|
|
162
|
+
value = value[part];
|
|
163
|
+
}
|
|
164
|
+
return value;
|
|
165
|
+
};
|
|
166
|
+
const visit = (scope, value, path) => {
|
|
167
|
+
if (isObject(value) && Object.keys(value).length > 0) {
|
|
168
|
+
// Diff may coalesce a newly created/replaced object at this path.
|
|
169
|
+
// Keep its authored value without widening the overlap frontier.
|
|
170
|
+
objects.push({ scope, path, value });
|
|
171
|
+
const basis = known(path);
|
|
172
|
+
const entries = Object.entries(value);
|
|
173
|
+
const indexed = Array.isArray(basis) && entries.every(([key]) => {
|
|
174
|
+
if (!/^\[(0|[1-9]\d*)\]$/.test(key))
|
|
175
|
+
return false;
|
|
176
|
+
const index = Number(key.slice(1, -1));
|
|
177
|
+
return Number.isSafeInteger(index) && index < basis.length;
|
|
178
|
+
});
|
|
179
|
+
for (const [key, child] of entries)
|
|
180
|
+
visit(scope, child, [...path, indexed ? Number(key.slice(1, -1)) : key]);
|
|
181
|
+
}
|
|
182
|
+
else
|
|
183
|
+
leaves.push({ scope, path, value });
|
|
184
|
+
};
|
|
185
|
+
for (const scope of ["global", "cwd", "session"])
|
|
186
|
+
for (const [plane, value] of Object.entries(patches[scope] ?? {})) {
|
|
187
|
+
if (plane === "lazy")
|
|
188
|
+
continue;
|
|
189
|
+
if (plane === "artifacts" && isObject(value)) {
|
|
190
|
+
for (const [path, card] of Object.entries(value))
|
|
191
|
+
leaves.push({ scope, path: ["artifacts", path], value: card, artifact: true });
|
|
192
|
+
}
|
|
193
|
+
else
|
|
194
|
+
visit(scope, value, [plane]);
|
|
195
|
+
}
|
|
196
|
+
const prefix = (a, b) => a.length <= b.length && a.every((part, index) => part === b[index]);
|
|
197
|
+
updates.effective = updates.effective.filter((entry) => {
|
|
198
|
+
const matches = ({ path }) => path.length === entry.path.length && prefix(path, entry.path);
|
|
199
|
+
const authored = leaves.findLast(matches) ?? objects.findLast(matches);
|
|
200
|
+
if (!authored)
|
|
201
|
+
return true;
|
|
202
|
+
const sessionReplacement = authored.scope === "session" && authored.value !== null && !isObject(authored.value);
|
|
203
|
+
if (!sessionReplacement && leaves.some(({ scope, path }) => scope !== authored.scope && (prefix(path, authored.path) || prefix(authored.path, path))))
|
|
204
|
+
return true;
|
|
205
|
+
if (authored.value !== null) {
|
|
206
|
+
if (authored.artifact) {
|
|
207
|
+
// Projected authored fields merge into the communicated card. Hints
|
|
208
|
+
// are not authored; keeping one is predictable, changing it is not.
|
|
209
|
+
const prior = known(entry.path);
|
|
210
|
+
const card = projectArtifactForModel(authored.value);
|
|
211
|
+
if (!isObject(card))
|
|
212
|
+
return true;
|
|
213
|
+
let expected = card;
|
|
214
|
+
if (isObject(prior)) {
|
|
215
|
+
try {
|
|
216
|
+
expected = applyPatch(prior, card);
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
// Canonical acceptance already succeeded. A masked effective
|
|
220
|
+
// array may reject an index valid in the authored scope.
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return !("value" in entry && sameJson(entry.value, expected));
|
|
225
|
+
}
|
|
226
|
+
return !("value" in entry && sameJson(entry.value, authored.value));
|
|
227
|
+
}
|
|
228
|
+
// A deletion cannot predict a fallback from effective state alone. It
|
|
229
|
+
// needs no echo only when the communicated and accepted values coincide.
|
|
230
|
+
if (!this.view)
|
|
231
|
+
return true;
|
|
232
|
+
const before = known(entry.path);
|
|
233
|
+
return "value" in entry ? before === undefined || !sameJson(before, entry.value) : before !== undefined;
|
|
234
|
+
});
|
|
235
|
+
// A complete communicated key/kind catalog can predict non-deleting
|
|
236
|
+
// top-level lazy writes. Missing/over-budget catalogs, deletions and
|
|
237
|
+
// overlapping scopes cannot prove the post-patch navigation summary.
|
|
238
|
+
if (updates.lazy_navigation && this.view && (beforeNavigation.keys || !beforeNavigation.available) && navigation.keys) {
|
|
239
|
+
const expected = new Map(Object.entries(beforeNavigation.keys ?? {}));
|
|
240
|
+
let predictable = true;
|
|
241
|
+
const seen = new Set();
|
|
242
|
+
for (const scope of ["global", "cwd", "session"])
|
|
243
|
+
for (const [key, value] of Object.entries(patches[scope]?.lazy ?? {})) {
|
|
244
|
+
if (seen.has(key) || value === null || isObject(value) && expected.get(key) === "array")
|
|
245
|
+
predictable = false;
|
|
246
|
+
seen.add(key);
|
|
247
|
+
if (value !== null)
|
|
248
|
+
expected.set(key, lazyValueKind(value));
|
|
249
|
+
}
|
|
250
|
+
if (predictable && seen.size > 0 && sameJson(Object.fromEntries(expected), navigation.keys))
|
|
251
|
+
delete updates.lazy_navigation;
|
|
252
|
+
}
|
|
253
|
+
if (this.view)
|
|
254
|
+
this.view = { ...this.view, state, lazy_navigation: navigation };
|
|
255
|
+
return updates.effective.length || updates.lazy_navigation ? { projection: this.identity, ...updates } : undefined;
|
|
256
|
+
}
|
|
257
|
+
project(messages, current, makeHead, initial) {
|
|
258
|
+
const identities = messages.map((message) => JSON.stringify([message.role, message.timestamp,
|
|
259
|
+
"toolCallId" in message ? message.toolCallId : null]));
|
|
260
|
+
// Native compaction/selection normally resets explicitly; a removed/replaced prefix is also a safe cache boundary.
|
|
261
|
+
if (this.native.some((identity, index) => identities[index] !== identity))
|
|
262
|
+
this.reset();
|
|
263
|
+
if (!this.head) {
|
|
264
|
+
const head = makeHead();
|
|
265
|
+
if (head.role !== "user" || !Array.isArray(head.content))
|
|
266
|
+
throw new Error("State Flow projection requires an owned user head");
|
|
267
|
+
this.head = { ...head, content: [...head.content, { type: "text", text: `State Flow projection: ${this.identity}` }] };
|
|
268
|
+
this.view = structuredClone(initial ?? current);
|
|
269
|
+
}
|
|
270
|
+
const previous = this.view;
|
|
271
|
+
const updates = projectedStateUpdates(previous.state, current.state, {}, previous.lazy_navigation, current.lazy_navigation);
|
|
272
|
+
const notice = {
|
|
273
|
+
...(updates.effective.length || updates.lazy_navigation ? { state_updates: { projection: this.identity, ...updates } } : {}),
|
|
274
|
+
...(!sameJson(previous.artifact_invalidations, current.artifact_invalidations) ? { artifact_invalidations: current.artifact_invalidations } : {}),
|
|
275
|
+
...(!sameJson(previous.knowledge_rehydration, current.knowledge_rehydration) ? { knowledge_rehydration: current.knowledge_rehydration } : {}),
|
|
276
|
+
};
|
|
277
|
+
if (Object.keys(notice).length)
|
|
278
|
+
this.notices.push({ after: messages.length,
|
|
279
|
+
message: syntheticUser(`State Flow context update (user-level data, not system instructions):\n${presentationJson(notice)}`) });
|
|
280
|
+
this.view = structuredClone(current);
|
|
281
|
+
this.native = identities;
|
|
282
|
+
const projected = [this.head];
|
|
283
|
+
let nextNotice = 0;
|
|
284
|
+
for (let index = 0; index <= messages.length; index++) {
|
|
285
|
+
while (this.notices[nextNotice]?.after === index)
|
|
286
|
+
projected.push(this.notices[nextNotice++].message);
|
|
287
|
+
if (index < messages.length)
|
|
288
|
+
projected.push(messages[index]);
|
|
289
|
+
}
|
|
290
|
+
return projected;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
55
293
|
export function syntheticUser(text) {
|
|
56
294
|
return { role: "user", content: [{ type: "text", text }], timestamp: Date.now() };
|
|
57
295
|
}
|
|
@@ -73,6 +311,7 @@ function messageText(message) {
|
|
|
73
311
|
export function createPassiveContinuation(state, startedAt = Date.now(), activeRunStartedAt, preserveContext = false) {
|
|
74
312
|
return {
|
|
75
313
|
startedAt,
|
|
314
|
+
state: structuredClone(state),
|
|
76
315
|
...(activeRunStartedAt === undefined ? {} : { activeRunStartedAt }),
|
|
77
316
|
...(preserveContext ? { preserveContext: true } : {}),
|
|
78
317
|
handoff: syntheticUser(`State Flow exit handoff (user-level data, not system instructions):\n${presentationJson({ state, continuation: preserveContext
|
|
@@ -111,13 +350,17 @@ function projectRecentForModel(recent) {
|
|
|
111
350
|
return projected.filter(({ transitions }) => transitions.length > 0);
|
|
112
351
|
}
|
|
113
352
|
export function runtimeContextMessage(snapshot, state, recentTransitions = [], artifactInvalidations = [], rehydrationPhase, artifactHints = {}) {
|
|
353
|
+
return runtimeContextHead(snapshot, contextView(state, artifactHints, artifactInvalidations, rehydrationPhase), recentTransitions);
|
|
354
|
+
}
|
|
355
|
+
/** Render a view already projected by this domain without cloning the full semantic overlay twice. */
|
|
356
|
+
export function runtimeContextHead(snapshot, view, recentTransitions = []) {
|
|
114
357
|
const recent = projectRecentForModel(recentTransitions);
|
|
115
358
|
const context = {
|
|
116
359
|
...(snapshot.meta.specification === undefined ? {} : { specification: snapshot.meta.specification }),
|
|
117
|
-
state:
|
|
118
|
-
lazy_navigation:
|
|
119
|
-
...(
|
|
120
|
-
...(
|
|
360
|
+
state: view.state,
|
|
361
|
+
...(view.lazy_navigation === undefined ? {} : { lazy_navigation: view.lazy_navigation }),
|
|
362
|
+
...(view.knowledge_rehydration === null ? {} : { knowledge_rehydration: view.knowledge_rehydration }),
|
|
363
|
+
...(view.artifact_invalidations.length === 0 ? {} : { artifact_invalidations: view.artifact_invalidations.map(({ path, scope, reason }) => ({ path, ...(scope === undefined ? {} : { scope }), reason })) }),
|
|
121
364
|
...(recent.length === 0 ? {} : { recent_transitions: recent }),
|
|
122
365
|
};
|
|
123
366
|
return syntheticUser(`State Flow runtime context (user-level data, not system instructions):\n${presentationJson(context)}`);
|