arborkit 1.0.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 +21 -0
- package/LICENSE +21 -0
- package/README.md +134 -0
- package/dist/addressing.d.ts +22 -0
- package/dist/addressing.js +56 -0
- package/dist/addressing.js.map +1 -0
- package/dist/ag-ui.d.ts +45 -0
- package/dist/ag-ui.js +50 -0
- package/dist/ag-ui.js.map +1 -0
- package/dist/arbor.d.ts +73 -0
- package/dist/arbor.js +1542 -0
- package/dist/arbor.js.map +1 -0
- package/dist/artifact-tree.d.ts +66 -0
- package/dist/artifact-tree.js +285 -0
- package/dist/artifact-tree.js.map +1 -0
- package/dist/clock.d.ts +15 -0
- package/dist/clock.js +23 -0
- package/dist/clock.js.map +1 -0
- package/dist/decompose.d.ts +18 -0
- package/dist/decompose.js +22 -0
- package/dist/decompose.js.map +1 -0
- package/dist/delta-storage.d.ts +55 -0
- package/dist/delta-storage.js +106 -0
- package/dist/delta-storage.js.map +1 -0
- package/dist/delta.d.ts +55 -0
- package/dist/delta.js +740 -0
- package/dist/delta.js.map +1 -0
- package/dist/embedding-port.d.ts +17 -0
- package/dist/embedding-port.js +21 -0
- package/dist/embedding-port.js.map +1 -0
- package/dist/embedding-text.d.ts +14 -0
- package/dist/embedding-text.js +21 -0
- package/dist/embedding-text.js.map +1 -0
- package/dist/errors.d.ts +37 -0
- package/dist/errors.js +59 -0
- package/dist/errors.js.map +1 -0
- package/dist/event-log.d.ts +75 -0
- package/dist/event-log.js +82 -0
- package/dist/event-log.js.map +1 -0
- package/dist/file-storage.d.ts +22 -0
- package/dist/file-storage.js +42 -0
- package/dist/file-storage.js.map +1 -0
- package/dist/ids.d.ts +17 -0
- package/dist/ids.js +22 -0
- package/dist/ids.js.map +1 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +1826 -0
- package/dist/index.js.map +1 -0
- package/dist/json-edit.d.ts +18 -0
- package/dist/json-edit.js +85 -0
- package/dist/json-edit.js.map +1 -0
- package/dist/jsonpointer.d.ts +14 -0
- package/dist/jsonpointer.js +33 -0
- package/dist/jsonpointer.js.map +1 -0
- package/dist/mutator.d.ts +59 -0
- package/dist/mutator.js +244 -0
- package/dist/mutator.js.map +1 -0
- package/dist/navigator.d.ts +85 -0
- package/dist/navigator.js +192 -0
- package/dist/navigator.js.map +1 -0
- package/dist/path-glob.d.ts +13 -0
- package/dist/path-glob.js +40 -0
- package/dist/path-glob.js.map +1 -0
- package/dist/registry-validator.d.ts +15 -0
- package/dist/registry-validator.js +11 -0
- package/dist/registry-validator.js.map +1 -0
- package/dist/replay.d.ts +38 -0
- package/dist/replay.js +183 -0
- package/dist/replay.js.map +1 -0
- package/dist/semantic-index.d.ts +88 -0
- package/dist/semantic-index.js +226 -0
- package/dist/semantic-index.js.map +1 -0
- package/dist/storage.d.ts +39 -0
- package/dist/storage.js +378 -0
- package/dist/storage.js.map +1 -0
- package/dist/toolset.d.ts +78 -0
- package/dist/toolset.js +306 -0
- package/dist/toolset.js.map +1 -0
- package/dist/type-aware-decision.d.ts +8 -0
- package/dist/type-aware-decision.js +17 -0
- package/dist/type-aware-decision.js.map +1 -0
- package/dist/type-registry.d.ts +20 -0
- package/dist/type-registry.js +17 -0
- package/dist/type-registry.js.map +1 -0
- package/dist/types.d.ts +28 -0
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -0
- package/dist/vector-index-port.d.ts +34 -0
- package/dist/vector-index-port.js +49 -0
- package/dist/vector-index-port.js.map +1 -0
- package/dist/zod-adapter.d.ts +13 -0
- package/dist/zod-adapter.js +34 -0
- package/dist/zod-adapter.js.map +1 -0
- package/package.json +47 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 1.0.0 — 2026-07-03
|
|
4
|
+
|
|
5
|
+
First public release of Arbor as `arborkit`. The milestone arc:
|
|
6
|
+
|
|
7
|
+
- Artifact tree with size/type-aware decomposition — large JSON splits into addressable nodes automatically (M1–M2).
|
|
8
|
+
- Typed nodes via a Zod adapter and a pluggable type registry (M3).
|
|
9
|
+
- Lazy navigator with glob-based `find` over paths, types, and tags (M4).
|
|
10
|
+
- Per-node semantic index with a stale-node lifecycle and batched `reindex` (M5).
|
|
11
|
+
- Snapshot persistence: `serializeArtifact`/`restoreArtifact` over a `StoragePort` (memory + atomic file impls) (M6).
|
|
12
|
+
- Reversible event log with value-level replay: `getAt`, `diff`, time-travel, and `revert` (M7).
|
|
13
|
+
- Scoped agent toolset — `describe`/`get`/`patch`/`find`/`search`/`history` with structured results and read/write scopes (M8).
|
|
14
|
+
- End-to-end content-site scenario plus a runnable example (M9).
|
|
15
|
+
- Hardening round 1: tx rollback restores index state, deep-cloned tool results, atomic file saves, type-aware revert (M10).
|
|
16
|
+
- Packaging: ESM build via tsup, exports map, pack-and-run capstone in plain Node (M11).
|
|
17
|
+
- Opt-in event-log compaction: `compactTo` + `baseSeq` sliding window, floor-aware replay, StoredArtifact v2 (v1 files still load) (M12).
|
|
18
|
+
- Delta persistence: checkpoint + appendable NDJSON journal with forward-replay restore (`DeltaStoragePort`, memory + file impls) (M13).
|
|
19
|
+
- Hardening round 2: move guards, aliasing hygiene, interleave-safe reindex, tags recorded in events, move-aware revert (M14).
|
|
20
|
+
- API polish: async `VectorIndexPort`, `patch` returns `{id, path, version}`, `find` returns `{hits, truncated}`, single `isWithin` scope helper (M15).
|
|
21
|
+
- `createArbor`/`restoreArbor` facade — one-call wiring, guarded restore, auto-checkpoint on first `saveDelta` — and published as `arborkit` (M15–M16).
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 D. A. Pominov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# Arbor (`arborkit`)
|
|
2
|
+
|
|
3
|
+
A general-purpose TypeScript core for multi-agent systems built around one shared
|
|
4
|
+
**artifact tree**: agents navigate and edit a JSON tree through scoped tools, with a
|
|
5
|
+
per-node exact + semantic index, a reversible event log, snapshots + delta persistence,
|
|
6
|
+
and time-travel. **Zero runtime dependencies.**
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm install arborkit
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
ESM-only, Node ≥ 20.6.
|
|
13
|
+
|
|
14
|
+
## The stack
|
|
15
|
+
|
|
16
|
+
- **Tree** — decompose a JSON value into addressable nodes (stable ids + JSON-Pointer paths), reconstruct any subtree.
|
|
17
|
+
- **Mutations** — `set`/`insert`/`remove`/`move` with scope + optimistic-version guards, recorded in a reversible event log; atomic transactions.
|
|
18
|
+
- **Types** — optional per-type validation + decomposition override (`TypeRegistry`); a structural Zod adapter (zod is a dev-only dependency).
|
|
19
|
+
- **Navigate** — `describe`/`get`/`find` (by id, path, tag, or glob) — depth-bounded and paginated; `find` returns `{hits, truncated}` so truncation is never silent.
|
|
20
|
+
- **Semantic index** — per-node embeddings via pluggable `EmbeddingPort`/`VectorIndexPort` (async — DB-backed adapters welcome); `search` by meaning returns `{results, staleCount}`, off the mutation path (mutations only mark stale; an async reindexer embeds).
|
|
21
|
+
- **Storage** — serialize the whole artifact (tree + log + vectors) to memory or a JSON file; or persist incrementally (checkpoint + appendable NDJSON journal); restore intact either way.
|
|
22
|
+
- **Replay** — reconstruct any past version, `diff` two versions, `revert` a node (append-only, path-addressed).
|
|
23
|
+
- **Toolset** — hand an agent a scoped, async, structured-result bundle: `describe`/`get`/`find`/`search`/`patch`/`history`. Writes are confined to `writeScope`, reads to `readScope`; errors are returned, never thrown across the boundary. `patch` returns `{id, path, version}` of the touched node.
|
|
24
|
+
- **Facade** — `createArbor`/`restoreArbor` wire all of the above in one call.
|
|
25
|
+
|
|
26
|
+
## Quickstart
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { createArbor, restoreArbor, MockEmbeddingPort, MemoryDeltaStorage, sizeBasedDecision } from "arborkit";
|
|
30
|
+
|
|
31
|
+
const delta = new MemoryDeltaStorage();
|
|
32
|
+
const arbor = createArbor({
|
|
33
|
+
initial: { pages: {}, plan: "" },
|
|
34
|
+
decompose: sizeBasedDecision(1), // decompose aggressively so this tiny demo gets addressable nodes
|
|
35
|
+
embedding: new MockEmbeddingPort(), // swap in a real EmbeddingPort (e.g. an API-backed one)
|
|
36
|
+
delta,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Hand an agent a scoped toolset — writes confined to /pages:
|
|
40
|
+
const tools = arbor.toolset({ owner: "writer", writeScope: "/pages", readScope: "/pages" });
|
|
41
|
+
const ins = await tools.patch({ path: "/pages" }, { op: "insert", key: "home", value: { title: "Home" } });
|
|
42
|
+
if (!ins.ok) throw new Error(ins.error.message); // ins.value === { id, path: "/pages/home", version }
|
|
43
|
+
const home = await tools.get({ path: "/pages/home" }); // home.value.content === { title: "Home" } (a clone)
|
|
44
|
+
const refused = await tools.patch({ path: "/plan" }, { op: "set", value: "hacked" });
|
|
45
|
+
// refused.ok === false — out of scope; violations are returned, never thrown
|
|
46
|
+
|
|
47
|
+
// Semantic search — mutations only mark nodes stale; reindex() embeds:
|
|
48
|
+
await arbor.index!.reindex();
|
|
49
|
+
const found = await arbor.index!.search("home page"); // { results, staleCount }
|
|
50
|
+
|
|
51
|
+
// Incremental persistence — the first saveDelta() auto-checkpoints:
|
|
52
|
+
await arbor.saveDelta();
|
|
53
|
+
|
|
54
|
+
// Later (e.g. a new process): restore, then time-travel:
|
|
55
|
+
const restored = await restoreArbor({ decompose: sizeBasedDecision(1), embedding: new MockEmbeddingPort(), delta });
|
|
56
|
+
const past = restored!.replay.getAt("/pages/home", 0); // undefined — before the insert
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Lifecycle notes
|
|
60
|
+
|
|
61
|
+
- The first `saveDelta()` auto-checkpoints — a journal without a checkpoint is
|
|
62
|
+
unrestorable, so the facade snapshots instead of appending.
|
|
63
|
+
- `checkpoint({ keepLast: N })` first compacts the event log to a sliding window of
|
|
64
|
+
the last N events, then snapshots — this is the knob that bounds both memory and
|
|
65
|
+
checkpoint size.
|
|
66
|
+
- Time-travel (`getAt`/`reconstructValueAt`/`revert`) below the compaction floor
|
|
67
|
+
throws — that history is gone by design.
|
|
68
|
+
- `restoreArbor` MUST be given the same `decompose`/`registry` as the original run:
|
|
69
|
+
journal-touched nodes are re-decomposed on delta restore, so a different policy
|
|
70
|
+
would silently reshape the tree.
|
|
71
|
+
|
|
72
|
+
## Format compatibility
|
|
73
|
+
|
|
74
|
+
StoredArtifact **v1** files (written before compaction existed) still load; **v2**
|
|
75
|
+
adds `baseSeq` (the persisted compaction floor). Delta storage keeps a checkpoint
|
|
76
|
+
plus an NDJSON journal; a torn journal tail is isolated on restore.
|
|
77
|
+
|
|
78
|
+
## Scope & limits (read this before adopting)
|
|
79
|
+
|
|
80
|
+
- **Single process, single writer.** The tree lives in memory; storage is a snapshot
|
|
81
|
+
target, not a database. There is no locking — two processes sharing one artifact
|
|
82
|
+
file will clobber each other. One artifact = one run = one process.
|
|
83
|
+
- **Scoping is a guardrail, not a security boundary.** `writeScope`/`readScope`
|
|
84
|
+
contain an agent's *tool calls* (including prompt-injected ones — a writer scoped
|
|
85
|
+
to `/pages/home` has no path to `/secret`). They do NOT isolate *code*: every
|
|
86
|
+
toolset shares the same heap, and anything holding the `Mutator` or tree can
|
|
87
|
+
bypass scope. Do not run mutually-untrusted agent code in one process.
|
|
88
|
+
- **Log growth is bounded by opt-in compaction.** `EventLog.compactTo(floorSeq)` drops
|
|
89
|
+
history before a floor — e.g. `log.compactTo(log.length() - N)` keeps a sliding window
|
|
90
|
+
of the last N events, capping both memory and the serialized event payload (the floor
|
|
91
|
+
is persisted as `baseSeq` and survives restore). Time-travel below the floor throws —
|
|
92
|
+
that history is gone. Nothing auto-compacts; choose a policy (per run, sliding window,
|
|
93
|
+
or never; `checkpoint({keepLast})` is the facade shortcut).
|
|
94
|
+
- **Saves can be incremental (opt-in delta persistence).** `DeltaStoragePort` (memory + file)
|
|
95
|
+
splits persistence into a periodic full checkpoint and cheap event appends — a
|
|
96
|
+
routine save costs O(new events) instead of rewriting the whole artifact. Restore loads
|
|
97
|
+
the checkpoint and forward-replays the journal, preserving node types and the vectors of
|
|
98
|
+
unchanged nodes (touched nodes are re-decomposed and left stale for reindex). A checkpoint
|
|
99
|
+
still serializes the whole **tree** — delta-of-tree is future work; restore must use the
|
|
100
|
+
same decompose decision as the original run.
|
|
101
|
+
- **Vector search is brute-force cosine** in the bundled `MemoryVectorIndex` — comfortable
|
|
102
|
+
to ~10⁴ vectors; plug a real ANN store into the (async) `VectorIndexPort` beyond that.
|
|
103
|
+
- **Ops are id-anchored** (a useful property for a future CRDT backend), but there is
|
|
104
|
+
**no CRDT**: no merge, no convergence, no multi-writer conflict resolution.
|
|
105
|
+
- **`ifVersion` on `insert` is parent-scoped:** it is a compare-and-set on the
|
|
106
|
+
*container's* version, and every sibling insert bumps the container. Use it to
|
|
107
|
+
guard "the container hasn't changed", not "my item is new".
|
|
108
|
+
|
|
109
|
+
## Run the example
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
npm run example # narrated end-to-end content-site scenario (examples/content-site.ts)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Develop
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
npm test # vitest
|
|
119
|
+
npm run typecheck # tsc --noEmit
|
|
120
|
+
npm run build # tsup → dist/ (ESM + type declarations)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Subpath imports work too: `import { Replay } from "arborkit/replay";` — every
|
|
124
|
+
module in `src/` maps onto `arborkit/<module>`.
|
|
125
|
+
|
|
126
|
+
## Docs
|
|
127
|
+
|
|
128
|
+
Design spec and milestone plans live in [`docs/superpowers/`](docs/superpowers/).
|
|
129
|
+
|
|
130
|
+
## Status
|
|
131
|
+
|
|
132
|
+
**v1 core complete (M1–M9), hardened (M10), packaged (M11), log compaction (M12), delta persistence (M13), hardening-2 (M14), API polish + facade (M15), published as arborkit (M16):** tree, mutations + reversible log, optional types, exact navigation, semantic index, storage, replay/time-travel, scoped agent toolset, end-to-end scenario, index-lifecycle hardening, and an installable ESM build.
|
|
133
|
+
|
|
134
|
+
Deferred (post-v1): LangChain `tool()` / MCP-server adapters over the toolset; `getAt`/`revert` as toolset methods; DB-backed storage & vector adapters (SQLite/sqlite-vec, Postgres/pgvector); a CRDT backend.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { NodeId, ArbNode } from './types.js';
|
|
2
|
+
import { ArtifactTree } from './artifact-tree.js';
|
|
3
|
+
import './ids.js';
|
|
4
|
+
import './clock.js';
|
|
5
|
+
import './decompose.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Resolves nodes by stable id and by JSON Pointer path.
|
|
9
|
+
* `path` is DERIVED from the current structure (not stored), so it stays
|
|
10
|
+
* consistent automatically — id is identity, path is current location.
|
|
11
|
+
*/
|
|
12
|
+
declare class Addressing {
|
|
13
|
+
private readonly tree;
|
|
14
|
+
constructor(tree: ArtifactTree);
|
|
15
|
+
byId(id: NodeId): ArbNode | undefined;
|
|
16
|
+
/** Compute the JSON Pointer for a node by walking parent links to the root. */
|
|
17
|
+
pathOf(id: NodeId): string;
|
|
18
|
+
/** Resolve a JSON Pointer to a node, or undefined if any segment is missing. */
|
|
19
|
+
byPath(pointer: string): ArbNode | undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export { Addressing };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// src/jsonpointer.ts
|
|
2
|
+
function encodeSegment(s) {
|
|
3
|
+
return s.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
4
|
+
}
|
|
5
|
+
function decodeSegment(s) {
|
|
6
|
+
return s.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
7
|
+
}
|
|
8
|
+
function buildPointer(segments) {
|
|
9
|
+
if (segments.length === 0) return "";
|
|
10
|
+
return "/" + segments.map((s) => encodeSegment(String(s))).join("/");
|
|
11
|
+
}
|
|
12
|
+
function parsePointer(pointer) {
|
|
13
|
+
if (pointer === "") return [];
|
|
14
|
+
if (!pointer.startsWith("/")) {
|
|
15
|
+
throw new Error(`Invalid JSON Pointer (must be "" or start with "/"): ${pointer}`);
|
|
16
|
+
}
|
|
17
|
+
return pointer.slice(1).split("/").map(decodeSegment);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// src/addressing.ts
|
|
21
|
+
var Addressing = class {
|
|
22
|
+
constructor(tree) {
|
|
23
|
+
this.tree = tree;
|
|
24
|
+
}
|
|
25
|
+
tree;
|
|
26
|
+
byId(id) {
|
|
27
|
+
return this.tree.get(id);
|
|
28
|
+
}
|
|
29
|
+
/** Compute the JSON Pointer for a node by walking parent links to the root. */
|
|
30
|
+
pathOf(id) {
|
|
31
|
+
const cur0 = this.tree.get(id);
|
|
32
|
+
if (!cur0) throw new Error(`Unknown node: ${id}`);
|
|
33
|
+
const segments = [];
|
|
34
|
+
let cur = cur0;
|
|
35
|
+
while (cur && cur.parentId !== null) {
|
|
36
|
+
segments.unshift(cur.key);
|
|
37
|
+
cur = this.tree.get(cur.parentId);
|
|
38
|
+
}
|
|
39
|
+
return buildPointer(segments);
|
|
40
|
+
}
|
|
41
|
+
/** Resolve a JSON Pointer to a node, or undefined if any segment is missing. */
|
|
42
|
+
byPath(pointer) {
|
|
43
|
+
const segments = parsePointer(pointer);
|
|
44
|
+
let cur = this.tree.root();
|
|
45
|
+
for (const seg of segments) {
|
|
46
|
+
if (!cur) return void 0;
|
|
47
|
+
cur = this.tree.childByKey(cur.id, seg);
|
|
48
|
+
if (!cur) return void 0;
|
|
49
|
+
}
|
|
50
|
+
return cur;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
export {
|
|
54
|
+
Addressing
|
|
55
|
+
};
|
|
56
|
+
//# sourceMappingURL=addressing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/jsonpointer.ts","../src/addressing.ts"],"sourcesContent":["/** Escape a single reference token per RFC 6901: ~ -> ~0, / -> ~1. */\r\nexport function encodeSegment(s: string): string {\r\n return s.replace(/~/g, \"~0\").replace(/\\//g, \"~1\");\r\n}\r\n\r\n/** Unescape a single reference token per RFC 6901: ~1 -> /, then ~0 -> ~. */\r\nexport function decodeSegment(s: string): string {\r\n return s.replace(/~1/g, \"/\").replace(/~0/g, \"~\");\r\n}\r\n\r\n/** Build a JSON Pointer string. Empty segment list => \"\" (root). */\r\nexport function buildPointer(segments: ReadonlyArray<string | number>): string {\r\n if (segments.length === 0) return \"\";\r\n return \"/\" + segments.map((s) => encodeSegment(String(s))).join(\"/\");\r\n}\r\n\r\n/** Append one child key to a parent pointer, escaping the key exactly as `buildPointer` does. */\r\nexport function appendPointer(parent: string, key: string | number): string {\r\n return parent + \"/\" + encodeSegment(String(key));\r\n}\r\n\r\n/** True when `path` is at or under `scope` (JSON Pointer prefix). Undefined scope = everywhere. */\r\nexport function isWithin(path: string, scope: string | undefined): boolean {\r\n return scope === undefined || path === scope || path.startsWith(scope + \"/\");\r\n}\r\n\r\n/** Parse a JSON Pointer into decoded segments. \"\" => [] (root). */\r\nexport function parsePointer(pointer: string): string[] {\r\n if (pointer === \"\") return [];\r\n if (!pointer.startsWith(\"/\")) {\r\n throw new Error(`Invalid JSON Pointer (must be \"\" or start with \"/\"): ${pointer}`);\r\n }\r\n return pointer.slice(1).split(\"/\").map(decodeSegment);\r\n}\r\n","import type { ArbNode, NodeId } from \"./types\";\r\nimport type { ArtifactTree } from \"./artifact-tree\";\r\nimport { buildPointer, parsePointer } from \"./jsonpointer\";\r\n\r\n/**\r\n * Resolves nodes by stable id and by JSON Pointer path.\r\n * `path` is DERIVED from the current structure (not stored), so it stays\r\n * consistent automatically — id is identity, path is current location.\r\n */\r\nexport class Addressing {\r\n constructor(private readonly tree: ArtifactTree) {}\r\n\r\n byId(id: NodeId): ArbNode | undefined {\r\n return this.tree.get(id);\r\n }\r\n\r\n /** Compute the JSON Pointer for a node by walking parent links to the root. */\r\n pathOf(id: NodeId): string {\r\n const cur0 = this.tree.get(id);\r\n if (!cur0) throw new Error(`Unknown node: ${id}`);\r\n const segments: (string | number)[] = [];\r\n let cur: ArbNode | undefined = cur0;\r\n while (cur && cur.parentId !== null) {\r\n segments.unshift(cur.key as string | number);\r\n cur = this.tree.get(cur.parentId);\r\n }\r\n return buildPointer(segments);\r\n }\r\n\r\n /** Resolve a JSON Pointer to a node, or undefined if any segment is missing. */\r\n byPath(pointer: string): ArbNode | undefined {\r\n const segments = parsePointer(pointer);\r\n let cur: ArbNode | undefined = this.tree.root();\r\n for (const seg of segments) {\r\n if (!cur) return undefined;\r\n // parsePointer yields UNescaped segments and node keys are raw strings —\r\n // they match childByKey's String(key) map keys directly.\r\n cur = this.tree.childByKey(cur.id, seg);\r\n if (!cur) return undefined;\r\n }\r\n return cur;\r\n }\r\n}\r\n"],"mappings":";AACO,SAAS,cAAc,GAAmB;AAC/C,SAAO,EAAE,QAAQ,MAAM,IAAI,EAAE,QAAQ,OAAO,IAAI;AAClD;AAGO,SAAS,cAAc,GAAmB;AAC/C,SAAO,EAAE,QAAQ,OAAO,GAAG,EAAE,QAAQ,OAAO,GAAG;AACjD;AAGO,SAAS,aAAa,UAAkD;AAC7E,MAAI,SAAS,WAAW,EAAG,QAAO;AAClC,SAAO,MAAM,SAAS,IAAI,CAAC,MAAM,cAAc,OAAO,CAAC,CAAC,CAAC,EAAE,KAAK,GAAG;AACrE;AAaO,SAAS,aAAa,SAA2B;AACtD,MAAI,YAAY,GAAI,QAAO,CAAC;AAC5B,MAAI,CAAC,QAAQ,WAAW,GAAG,GAAG;AAC5B,UAAM,IAAI,MAAM,wDAAwD,OAAO,EAAE;AAAA,EACnF;AACA,SAAO,QAAQ,MAAM,CAAC,EAAE,MAAM,GAAG,EAAE,IAAI,aAAa;AACtD;;;ACxBO,IAAM,aAAN,MAAiB;AAAA,EACtB,YAA6B,MAAoB;AAApB;AAAA,EAAqB;AAAA,EAArB;AAAA,EAE7B,KAAK,IAAiC;AACpC,WAAO,KAAK,KAAK,IAAI,EAAE;AAAA,EACzB;AAAA;AAAA,EAGA,OAAO,IAAoB;AACzB,UAAM,OAAO,KAAK,KAAK,IAAI,EAAE;AAC7B,QAAI,CAAC,KAAM,OAAM,IAAI,MAAM,iBAAiB,EAAE,EAAE;AAChD,UAAM,WAAgC,CAAC;AACvC,QAAI,MAA2B;AAC/B,WAAO,OAAO,IAAI,aAAa,MAAM;AACnC,eAAS,QAAQ,IAAI,GAAsB;AAC3C,YAAM,KAAK,KAAK,IAAI,IAAI,QAAQ;AAAA,IAClC;AACA,WAAO,aAAa,QAAQ;AAAA,EAC9B;AAAA;AAAA,EAGA,OAAO,SAAsC;AAC3C,UAAM,WAAW,aAAa,OAAO;AACrC,QAAI,MAA2B,KAAK,KAAK,KAAK;AAC9C,eAAW,OAAO,UAAU;AAC1B,UAAI,CAAC,IAAK,QAAO;AAGjB,YAAM,KAAK,KAAK,WAAW,IAAI,IAAI,GAAG;AACtC,UAAI,CAAC,IAAK,QAAO;AAAA,IACnB;AACA,WAAO;AAAA,EACT;AACF;","names":[]}
|
package/dist/ag-ui.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Json } from './types.js';
|
|
2
|
+
import { ArtifactTree } from './artifact-tree.js';
|
|
3
|
+
import { EventLog, MutationEvent } from './event-log.js';
|
|
4
|
+
import './ids.js';
|
|
5
|
+
import './clock.js';
|
|
6
|
+
import './decompose.js';
|
|
7
|
+
|
|
8
|
+
/** RFC 6902 operation (the subset Arbor emits). */
|
|
9
|
+
type JsonPatchOp = {
|
|
10
|
+
op: "replace" | "add";
|
|
11
|
+
path: string;
|
|
12
|
+
value: Json;
|
|
13
|
+
} | {
|
|
14
|
+
op: "remove";
|
|
15
|
+
path: string;
|
|
16
|
+
} | {
|
|
17
|
+
op: "move";
|
|
18
|
+
from: string;
|
|
19
|
+
path: string;
|
|
20
|
+
};
|
|
21
|
+
interface AgUiStateSnapshot {
|
|
22
|
+
type: "STATE_SNAPSHOT";
|
|
23
|
+
snapshot: Json;
|
|
24
|
+
}
|
|
25
|
+
interface AgUiStateDelta {
|
|
26
|
+
type: "STATE_DELTA";
|
|
27
|
+
delta: JsonPatchOp[];
|
|
28
|
+
}
|
|
29
|
+
/** One mutation event as an RFC 6902 op, or null for pre-M7 events without paths. */
|
|
30
|
+
declare function toJsonPatch(e: MutationEvent): JsonPatchOp | null;
|
|
31
|
+
/** The full current state as an AG-UI STATE_SNAPSHOT event. */
|
|
32
|
+
declare function snapshotEvent(tree: ArtifactTree): AgUiStateSnapshot;
|
|
33
|
+
/** Retained events with seq >= sinceSeq as ONE AG-UI STATE_DELTA event (pathless
|
|
34
|
+
* pre-M7 events are skipped). Returns the delta plus the next since-cursor.
|
|
35
|
+
*
|
|
36
|
+
* Throws InvalidOpError when `sinceSeq` is below the log's compaction floor:
|
|
37
|
+
* the events in [sinceSeq, baseSeq) are gone, and applying a gapped delta would
|
|
38
|
+
* silently diverge the client. On this error the caller must re-send a fresh
|
|
39
|
+
* `snapshotEvent` (and resume deltas from the current version) instead. */
|
|
40
|
+
declare function deltaSince(log: EventLog, sinceSeq: number): {
|
|
41
|
+
event: AgUiStateDelta;
|
|
42
|
+
nextSeq: number;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export { type AgUiStateDelta, type AgUiStateSnapshot, type JsonPatchOp, deltaSince, snapshotEvent, toJsonPatch };
|
package/dist/ag-ui.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
var ArborError = class extends Error {
|
|
3
|
+
constructor(code, message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.code = code;
|
|
6
|
+
this.name = new.target.name;
|
|
7
|
+
}
|
|
8
|
+
code;
|
|
9
|
+
};
|
|
10
|
+
var InvalidOpError = class extends ArborError {
|
|
11
|
+
constructor(message) {
|
|
12
|
+
super("INVALID_OP", message);
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// src/ag-ui.ts
|
|
17
|
+
function toJsonPatch(e) {
|
|
18
|
+
switch (e.kind) {
|
|
19
|
+
case "set":
|
|
20
|
+
return e.path === void 0 ? null : { op: "replace", path: e.path, value: e.after ?? null };
|
|
21
|
+
case "insert":
|
|
22
|
+
return e.path === void 0 ? null : { op: "add", path: e.path, value: e.after ?? null };
|
|
23
|
+
case "remove":
|
|
24
|
+
return e.path === void 0 ? null : { op: "remove", path: e.path };
|
|
25
|
+
case "move":
|
|
26
|
+
return e.fromPath === void 0 || e.toPath === void 0 ? null : { op: "move", from: e.fromPath, path: e.toPath };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function snapshotEvent(tree) {
|
|
30
|
+
return { type: "STATE_SNAPSHOT", snapshot: tree.toJson() };
|
|
31
|
+
}
|
|
32
|
+
function deltaSince(log, sinceSeq) {
|
|
33
|
+
if (sinceSeq < log.baseSeqValue()) {
|
|
34
|
+
throw new InvalidOpError(
|
|
35
|
+
`deltaSince: events [${sinceSeq}, ${log.baseSeqValue()}) were compacted away \u2014 send a fresh STATE_SNAPSHOT instead`
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
const delta = [];
|
|
39
|
+
for (const e of log.since(sinceSeq)) {
|
|
40
|
+
const op = toJsonPatch(e);
|
|
41
|
+
if (op) delta.push(op);
|
|
42
|
+
}
|
|
43
|
+
return { event: { type: "STATE_DELTA", delta }, nextSeq: log.length() };
|
|
44
|
+
}
|
|
45
|
+
export {
|
|
46
|
+
deltaSince,
|
|
47
|
+
snapshotEvent,
|
|
48
|
+
toJsonPatch
|
|
49
|
+
};
|
|
50
|
+
//# sourceMappingURL=ag-ui.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/errors.ts","../src/ag-ui.ts"],"sourcesContent":["import type { NodeId } from \"./types\";\r\n\r\n/** A reference to a node: by stable id or by JSON Pointer path. */\r\nexport type Ref = { id: NodeId } | { path: string };\r\n\r\nexport class ArborError extends Error {\r\n constructor(\r\n public readonly code: string,\r\n message: string,\r\n ) {\r\n super(message);\r\n this.name = new.target.name;\r\n }\r\n}\r\n\r\nexport class NodeNotFoundError extends ArborError {\r\n constructor(public readonly ref: Ref) {\r\n super(\"NODE_NOT_FOUND\", `Node not found: ${JSON.stringify(ref)}`);\r\n }\r\n}\r\n\r\nexport class ScopeViolationError extends ArborError {\r\n constructor(\r\n public readonly targetPath: string,\r\n public readonly scope: string,\r\n ) {\r\n super(\"SCOPE_VIOLATION\", `Access outside scope: ${targetPath} (scope: ${scope})`);\r\n }\r\n}\r\n\r\nexport class StaleVersionError extends ArborError {\r\n constructor(\r\n public readonly id: NodeId,\r\n public readonly expected: number,\r\n public readonly actual: number,\r\n ) {\r\n super(\"STALE_VERSION\", `Stale version for ${id}: expected ${expected}, actual ${actual}`);\r\n }\r\n}\r\n\r\nexport class InvalidOpError extends ArborError {\r\n constructor(message: string) {\r\n super(\"INVALID_OP\", message);\r\n }\r\n}\r\n\r\nexport class ValidationError extends ArborError {\r\n constructor(\r\n public readonly type: string | undefined,\r\n public readonly details: string,\r\n ) {\r\n super(\"VALIDATION_ERROR\", `Validation failed${type ? ` for type ${type}` : \"\"}: ${details}`);\r\n }\r\n}\r\n","// AG-UI adapter — expose the artifact tree + event log as AG-UI shared-state\r\n// events (STATE_SNAPSHOT / STATE_DELTA with RFC 6902 JSON Patch ops). Zero-dep:\r\n// these are plain objects shaped like AG-UI events, no AG-UI SDK required.\r\nimport type { Json } from \"./types\";\r\nimport type { ArtifactTree } from \"./artifact-tree\";\r\nimport type { EventLog, MutationEvent } from \"./event-log\";\r\nimport { InvalidOpError } from \"./errors\";\r\n\r\n/** RFC 6902 operation (the subset Arbor emits). */\r\nexport type JsonPatchOp =\r\n | { op: \"replace\" | \"add\"; path: string; value: Json }\r\n | { op: \"remove\"; path: string }\r\n | { op: \"move\"; from: string; path: string };\r\n\r\nexport interface AgUiStateSnapshot {\r\n type: \"STATE_SNAPSHOT\";\r\n snapshot: Json;\r\n}\r\n\r\nexport interface AgUiStateDelta {\r\n type: \"STATE_DELTA\";\r\n delta: JsonPatchOp[];\r\n}\r\n\r\n/** One mutation event as an RFC 6902 op, or null for pre-M7 events without paths. */\r\nexport function toJsonPatch(e: MutationEvent): JsonPatchOp | null {\r\n switch (e.kind) {\r\n case \"set\":\r\n return e.path === undefined ? null : { op: \"replace\", path: e.path, value: e.after ?? null };\r\n case \"insert\":\r\n return e.path === undefined ? null : { op: \"add\", path: e.path, value: e.after ?? null };\r\n case \"remove\":\r\n return e.path === undefined ? null : { op: \"remove\", path: e.path };\r\n case \"move\":\r\n return e.fromPath === undefined || e.toPath === undefined\r\n ? null\r\n : { op: \"move\", from: e.fromPath, path: e.toPath };\r\n }\r\n}\r\n\r\n/** The full current state as an AG-UI STATE_SNAPSHOT event. */\r\nexport function snapshotEvent(tree: ArtifactTree): AgUiStateSnapshot {\r\n return { type: \"STATE_SNAPSHOT\", snapshot: tree.toJson() };\r\n}\r\n\r\n/** Retained events with seq >= sinceSeq as ONE AG-UI STATE_DELTA event (pathless\r\n * pre-M7 events are skipped). Returns the delta plus the next since-cursor.\r\n *\r\n * Throws InvalidOpError when `sinceSeq` is below the log's compaction floor:\r\n * the events in [sinceSeq, baseSeq) are gone, and applying a gapped delta would\r\n * silently diverge the client. On this error the caller must re-send a fresh\r\n * `snapshotEvent` (and resume deltas from the current version) instead. */\r\nexport function deltaSince(log: EventLog, sinceSeq: number): { event: AgUiStateDelta; nextSeq: number } {\r\n if (sinceSeq < log.baseSeqValue()) {\r\n throw new InvalidOpError(\r\n `deltaSince: events [${sinceSeq}, ${log.baseSeqValue()}) were compacted away — send a fresh STATE_SNAPSHOT instead`,\r\n );\r\n }\r\n const delta: JsonPatchOp[] = [];\r\n for (const e of log.since(sinceSeq)) {\r\n const op = toJsonPatch(e);\r\n if (op) delta.push(op);\r\n }\r\n return { event: { type: \"STATE_DELTA\", delta }, nextSeq: log.length() };\r\n}\r\n"],"mappings":";AAKO,IAAM,aAAN,cAAyB,MAAM;AAAA,EACpC,YACkB,MAChB,SACA;AACA,UAAM,OAAO;AAHG;AAIhB,SAAK,OAAO,WAAW;AAAA,EACzB;AAAA,EALkB;AAMpB;AA2BO,IAAM,iBAAN,cAA6B,WAAW;AAAA,EAC7C,YAAY,SAAiB;AAC3B,UAAM,cAAc,OAAO;AAAA,EAC7B;AACF;;;ACnBO,SAAS,YAAY,GAAsC;AAChE,UAAQ,EAAE,MAAM;AAAA,IACd,KAAK;AACH,aAAO,EAAE,SAAS,SAAY,OAAO,EAAE,IAAI,WAAW,MAAM,EAAE,MAAM,OAAO,EAAE,SAAS,KAAK;AAAA,IAC7F,KAAK;AACH,aAAO,EAAE,SAAS,SAAY,OAAO,EAAE,IAAI,OAAO,MAAM,EAAE,MAAM,OAAO,EAAE,SAAS,KAAK;AAAA,IACzF,KAAK;AACH,aAAO,EAAE,SAAS,SAAY,OAAO,EAAE,IAAI,UAAU,MAAM,EAAE,KAAK;AAAA,IACpE,KAAK;AACH,aAAO,EAAE,aAAa,UAAa,EAAE,WAAW,SAC5C,OACA,EAAE,IAAI,QAAQ,MAAM,EAAE,UAAU,MAAM,EAAE,OAAO;AAAA,EACvD;AACF;AAGO,SAAS,cAAc,MAAuC;AACnE,SAAO,EAAE,MAAM,kBAAkB,UAAU,KAAK,OAAO,EAAE;AAC3D;AASO,SAAS,WAAW,KAAe,UAA8D;AACtG,MAAI,WAAW,IAAI,aAAa,GAAG;AACjC,UAAM,IAAI;AAAA,MACR,uBAAuB,QAAQ,KAAK,IAAI,aAAa,CAAC;AAAA,IACxD;AAAA,EACF;AACA,QAAM,QAAuB,CAAC;AAC9B,aAAW,KAAK,IAAI,MAAM,QAAQ,GAAG;AACnC,UAAM,KAAK,YAAY,CAAC;AACxB,QAAI,GAAI,OAAM,KAAK,EAAE;AAAA,EACvB;AACA,SAAO,EAAE,OAAO,EAAE,MAAM,eAAe,MAAM,GAAG,SAAS,IAAI,OAAO,EAAE;AACxE;","names":[]}
|
package/dist/arbor.d.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Json } from './types.js';
|
|
2
|
+
import { ArtifactTree } from './artifact-tree.js';
|
|
3
|
+
import { Addressing } from './addressing.js';
|
|
4
|
+
import { EventLog } from './event-log.js';
|
|
5
|
+
import { Mutator } from './mutator.js';
|
|
6
|
+
import { Replay } from './replay.js';
|
|
7
|
+
import { SemanticIndex } from './semantic-index.js';
|
|
8
|
+
import { ToolsetBinding, Toolset } from './toolset.js';
|
|
9
|
+
import { TypeRegistry } from './type-registry.js';
|
|
10
|
+
import { DecomposeDecision } from './decompose.js';
|
|
11
|
+
import { IdGen } from './ids.js';
|
|
12
|
+
import { Clock } from './clock.js';
|
|
13
|
+
import { EmbeddingPort } from './embedding-port.js';
|
|
14
|
+
import { VectorIndexPort } from './vector-index-port.js';
|
|
15
|
+
import { StoragePort } from './storage.js';
|
|
16
|
+
import { DeltaStoragePort } from './delta-storage.js';
|
|
17
|
+
import './errors.js';
|
|
18
|
+
import './navigator.js';
|
|
19
|
+
|
|
20
|
+
/** Everything `createArbor`/`restoreArbor` need. All optional — sensible defaults. */
|
|
21
|
+
interface ArborOpts {
|
|
22
|
+
/** Initial JSON for a fresh artifact (default {}). Ignored by restoreArbor. */
|
|
23
|
+
initial?: Json;
|
|
24
|
+
/** Node types: per-type validation, decompose override, embedText. */
|
|
25
|
+
registry?: TypeRegistry;
|
|
26
|
+
/** Base decompose policy (default sizeBasedDecision(200)); made type-aware when a registry is given. */
|
|
27
|
+
decompose?: DecomposeDecision;
|
|
28
|
+
idGen?: IdGen;
|
|
29
|
+
clock?: Clock;
|
|
30
|
+
/** Enables the semantic index (vectors default to MemoryVectorIndex). */
|
|
31
|
+
embedding?: EmbeddingPort;
|
|
32
|
+
vectors?: VectorIndexPort;
|
|
33
|
+
/** Whole-artifact persistence: enables save(); restoreArbor falls back to it. */
|
|
34
|
+
storage?: StoragePort;
|
|
35
|
+
/** Incremental persistence: enables saveDelta()/checkpoint(); restoreArbor prefers it. */
|
|
36
|
+
delta?: DeltaStoragePort;
|
|
37
|
+
}
|
|
38
|
+
/** A fully wired artifact: the live components plus lifecycle helpers. */
|
|
39
|
+
interface Arbor {
|
|
40
|
+
readonly tree: ArtifactTree;
|
|
41
|
+
readonly addressing: Addressing;
|
|
42
|
+
readonly log: EventLog;
|
|
43
|
+
readonly mutator: Mutator;
|
|
44
|
+
readonly replay: Replay;
|
|
45
|
+
/** Present iff `embedding` was configured. */
|
|
46
|
+
readonly index?: SemanticIndex;
|
|
47
|
+
readonly vectors: VectorIndexPort;
|
|
48
|
+
/** A scoped agent-facing toolset over this artifact. */
|
|
49
|
+
toolset(binding?: ToolsetBinding): Toolset;
|
|
50
|
+
/** Whole-artifact snapshot to `storage`. */
|
|
51
|
+
save(): Promise<void>;
|
|
52
|
+
/** Append events since the last saveDelta/checkpoint to the delta journal. */
|
|
53
|
+
saveDelta(): Promise<void>;
|
|
54
|
+
/** Full snapshot to delta storage (clears the journal). `keepLast` first compacts
|
|
55
|
+
* the log to a sliding window of that many events. */
|
|
56
|
+
checkpoint(opts?: {
|
|
57
|
+
keepLast?: number;
|
|
58
|
+
}): Promise<void>;
|
|
59
|
+
}
|
|
60
|
+
/** Build a fresh, fully wired artifact from `opts.initial` (default {}). */
|
|
61
|
+
declare function createArbor(opts?: ArborOpts): Arbor;
|
|
62
|
+
/**
|
|
63
|
+
* Restore a fully wired artifact from persistence: prefers `delta` (checkpoint +
|
|
64
|
+
* forward-replayed journal), falls back to `storage`, returns null when neither has
|
|
65
|
+
* data. Owns the restore invariants: a fresh (or caller-provided) vector index is
|
|
66
|
+
* upserted from the snapshot, the SemanticIndex is re-created (it re-seeds its stale
|
|
67
|
+
* queue from node meta), and the idGen is guarded against collisions with restored
|
|
68
|
+
* node ids. Use the SAME `decompose`/`registry` as the original run — journal-touched
|
|
69
|
+
* nodes are re-decomposed on delta restore.
|
|
70
|
+
*/
|
|
71
|
+
declare function restoreArbor(opts: ArborOpts): Promise<Arbor | null>;
|
|
72
|
+
|
|
73
|
+
export { type Arbor, type ArborOpts, createArbor, restoreArbor };
|