@tangleai/context 0.21.1

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.
@@ -0,0 +1,47 @@
1
+ /** Exact bytes of the archive sub-map, including durable loss metadata. */
2
+ export function archiveFootprint(records: any): {
3
+ items: number;
4
+ bytes: number;
5
+ };
6
+ /** Plan deterministic oldest-unreferenced eviction, or refuse without mutation. */
7
+ export function retainArchives(current: any, added: any, limits: any, options?: {}): {
8
+ error: string;
9
+ code: string;
10
+ retention?: undefined;
11
+ next?: undefined;
12
+ report?: undefined;
13
+ footprint?: undefined;
14
+ } | {
15
+ error: string;
16
+ code: string;
17
+ retention: {
18
+ refused: boolean;
19
+ limits: any;
20
+ current: {
21
+ items: number;
22
+ bytes: number;
23
+ };
24
+ attempted: {
25
+ items: number;
26
+ bytes: number;
27
+ };
28
+ };
29
+ next?: undefined;
30
+ report?: undefined;
31
+ footprint?: undefined;
32
+ } | {
33
+ next: any;
34
+ report: {
35
+ version: number;
36
+ policy: string;
37
+ evicted: any[];
38
+ written: any;
39
+ };
40
+ footprint: {
41
+ items: number;
42
+ bytes: number;
43
+ };
44
+ error?: undefined;
45
+ code?: undefined;
46
+ retention?: undefined;
47
+ };
package/src/archive.js ADDED
@@ -0,0 +1,53 @@
1
+ //@ts-check
2
+ import { jsonBytes } from './retention.js';
3
+ const SLOT = 'ai/state/slot/', CONTENT = 'ai/state/slot-content/';
4
+ const EVICTED = 'ai/state/evicted/', REPORT = 'ai/state/retention/archive';
5
+ const archived = (value) => value?.kind === 'agent-round' || value?.kind === 'agent-round-index';
6
+
7
+ /** Exact bytes of the archive sub-map, including durable loss metadata. */
8
+ export function archiveFootprint(records) {
9
+ const names = Object.keys(records).filter((key) => key.startsWith(SLOT) && archived(records[key]));
10
+ const keys = new Set(names.flatMap((key) => [key, `${CONTENT}${records[key].name}`]));
11
+ for (const key of Object.keys(records)) if (key.startsWith(EVICTED) || key === REPORT) keys.add(key);
12
+ return { items: names.length, bytes: jsonBytes(Object.fromEntries([...keys].sort().map((key) => [key, records[key]]))) };
13
+ }
14
+
15
+ /** Plan deterministic oldest-unreferenced eviction, or refuse without mutation. */
16
+ export function retainArchives(current, added, limits, options = {}) {
17
+ const next = structuredClone(current);
18
+ const protectedNames = new Set([...(options.protectedNames ?? []), ...added.map((entry) => entry.slot.name)]);
19
+ const references = JSON.stringify(Object.entries(current).filter(([key]) =>
20
+ key.startsWith('ai/state/memory/') || key.startsWith('ai/state/goal/')))
21
+ + (options.referenceText ?? '');
22
+ for (const { slot, text } of added) {
23
+ if (options.immutable === true && Object.hasOwn(next, `${CONTENT}${slot.name}`)
24
+ && next[`${CONTENT}${slot.name}`] !== text)
25
+ return { error: `archive address collision at '${slot.name}'`, code: 'ARCHIVE_COLLISION' };
26
+ next[`${SLOT}${slot.name}`] = slot;
27
+ next[`${CONTENT}${slot.name}`] = text;
28
+ delete next[`${EVICTED}${slot.name}`];
29
+ }
30
+ const evicted = [];
31
+ const report = { version: 1, policy: 'oldest-unreferenced', evicted,
32
+ written: added.map((entry) => entry.slot.name) };
33
+ next[REPORT] = report;
34
+ const candidates = Object.entries(next).filter(([key, slot]) => key.startsWith(SLOT) && archived(slot)
35
+ && !slot.pinned && !protectedNames.has(slot.name) && !references.includes(slot.name))
36
+ .sort(([, a], [, b]) => a.at < b.at ? -1 : a.at > b.at ? 1 : a.name < b.name ? -1 : 1);
37
+ const fits = () => {
38
+ const footprint = archiveFootprint(next);
39
+ return footprint.items <= (limits.maxItems ?? Infinity) && footprint.bytes <= (limits.maxBytes ?? Infinity);
40
+ };
41
+ for (const [, slot] of candidates) {
42
+ if (fits()) break;
43
+ const keys = [`${SLOT}${slot.name}`, `${CONTENT}${slot.name}`];
44
+ const bytes = jsonBytes(Object.fromEntries(keys.map((key) => [key, next[key]])));
45
+ const tombstone = { version: 1, name: slot.name, status: 'evicted', bytes, reason: 'archive-budget' };
46
+ for (const key of keys) delete next[key];
47
+ next[`${EVICTED}${slot.name}`] = tombstone;
48
+ evicted.push(tombstone);
49
+ }
50
+ if (!fits()) return { error: 'archive budget cannot preserve protected addresses and loss metadata',
51
+ code: 'ARCHIVE_BUDGET', retention: { refused: true, limits, current: archiveFootprint(current), attempted: archiveFootprint(next) } };
52
+ return { next, report, footprint: archiveFootprint(next) };
53
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * The address of one chunk: parent, strategy, size, index. Derived, so
3
+ * the same split always names the same slots — that is what makes
4
+ * re-chunking idempotent rather than duplicating, and it is why nothing
5
+ * here keeps a mapping from a parent to its pieces.
6
+ * @param {string} parent
7
+ * @param {string} strategy
8
+ * @param {number} size
9
+ * @param {number} index
10
+ * @returns {string}
11
+ */
12
+ export function chunkSlotName(parent: string, strategy: string, size: number, index: number): string;
13
+ /** The prefix every chunk of one split shares — a family, addressable as one. */
14
+ export function chunkFamily(parent: any, strategy: any, size: any): string;
15
+ /**
16
+ * Create an environment over a slot store.
17
+ *
18
+ * @param {{ ledger?: any,
19
+ * storage?: { get: (key: string) => Promise<any>,
20
+ * set: (key: string, value: any) => Promise<void>,
21
+ * delete: (key: string) => Promise<void>,
22
+ * keys: (prefix?: string) => Promise<string[]> },
23
+ * compileQuery?: (document: any) => (data: any) => any,
24
+ * excerptChars?: number, digestSlots?: number, matchLimit?: number,
25
+ * chunkSize?: number, now?: () => string }} [options]
26
+ * - `ledger` shares an existing ledger — the normal case, because the
27
+ * agent's archived rounds and the corpus then live in one store and
28
+ * one `recall` reaches both. Given `storage` instead, a ledger is
29
+ * built over it; given neither, everything runs in memory.
30
+ * - `compileQuery` is the `select` seam (`compileJsonQuery` from
31
+ * `@jarenjs/json/query`). Absent, `select` declines with a stated
32
+ * reason and every other operation is unaffected.
33
+ * @returns {any}
34
+ */
35
+ export function createEnvironment(options?: {
36
+ ledger?: any;
37
+ storage?: {
38
+ get: (key: string) => Promise<any>;
39
+ set: (key: string, value: any) => Promise<void>;
40
+ delete: (key: string) => Promise<void>;
41
+ keys: (prefix?: string) => Promise<string[]>;
42
+ };
43
+ compileQuery?: (document: any) => (data: any) => any;
44
+ excerptChars?: number;
45
+ digestSlots?: number;
46
+ matchLimit?: number;
47
+ chunkSize?: number;
48
+ now?: () => string;
49
+ }): any;
50
+ /**
51
+ * The environment as a toolbox definition list: the five operations plus
52
+ * `read`, ready for `createToolbox().add(...)`.
53
+ *
54
+ * They are defined here rather than in the agent for the reason `recall`
55
+ * is: a tool a model calls and an operation a harness calls have to be
56
+ * the same thing, or the tested path and the shipped path drift.
57
+ *
58
+ * Every schema is deliberately small — one required string, optional
59
+ * numbers — because the tier this package targets gets a tool call right
60
+ * in proportion to how few decisions it has to make.
61
+ * @param {any} environment - from {@link createEnvironment}
62
+ * @returns {any[]} tool definitions
63
+ */
64
+ export function environmentTools(environment: any): any[];
65
+ /** The kind a chunk slot is written under, so a digest can group them. */
66
+ export const CHUNK_KIND: "chunk";