@tangleai/context 0.21.1 → 0.25.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 +40 -0
- package/README.md +24 -23
- package/package.json +4 -4
- package/src/archive.d.ts +3 -3
- package/src/archive.js +49 -44
- package/src/environment.d.ts +52 -22
- package/src/environment.js +491 -548
- package/src/evidence.d.ts +5 -10
- package/src/evidence.js +49 -51
- package/src/index.d.ts +10 -9
- package/src/index.js +9 -10
- package/src/ledger.d.ts +123 -83
- package/src/ledger.js +1039 -1185
- package/src/recall.d.ts +44 -23
- package/src/recall.js +48 -68
- package/src/retention.d.ts +7 -7
- package/src/retention.js +88 -71
- package/src/schemas/evidence.d.ts +11 -10
- package/src/schemas/evidence.js +14 -21
- package/src/schemas/ledger.d.ts +737 -430
- package/src/schemas/ledger.js +130 -243
- package/src/schemas/patch.d.ts +122 -108
- package/src/schemas/patch.js +59 -64
- package/src/storage/memory.d.ts +3 -8
- package/src/storage/memory.js +41 -43
- package/src/storage/slot.d.ts +3 -4
- package/src/storage/slot.js +104 -96
- package/src/storage/transaction.d.ts +3 -18
- package/src/storage/transaction.js +22 -34
package/src/recall.d.ts
CHANGED
|
@@ -1,44 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `recall` — the address scheme compaction writes, and the tool that
|
|
3
|
+
* reads it back.
|
|
4
|
+
*
|
|
5
|
+
* Compaction used to be a one-way door: a dropped tool round left the
|
|
6
|
+
* request as a 60-character excerpt and the rest of it was gone. The
|
|
7
|
+
* excerpt is now a *preview* of something that still exists, and this
|
|
8
|
+
* module owns both halves of that promise — the address a synopsis line
|
|
9
|
+
* carries, and the tool a model calls to follow it. They live together
|
|
10
|
+
* because they are one contract: a name written by `agent.ts` and read
|
|
11
|
+
* by a model has to be produced and parsed in exactly one place, or the
|
|
12
|
+
* two drift and a request ends up naming an address nothing answers.
|
|
13
|
+
*
|
|
14
|
+
* Three decisions:
|
|
15
|
+
*
|
|
16
|
+
* - **The name is content-addressed.** A round's slot name is derived
|
|
17
|
+
* from the round's own bytes, so compacting the same history twice
|
|
18
|
+
* writes the same slot instead of a second copy — idempotence falls
|
|
19
|
+
* out of the naming rather than being bolted on with a counter. The
|
|
20
|
+
* character length rides beside the 32-bit fingerprint. These can
|
|
21
|
+
* collide, so compaction compares exact bytes and refuses a conflicting
|
|
22
|
+
* address before dropping anything from the transcript.
|
|
23
|
+
* - **Recall is a tool, not a mechanism.** Nothing re-expands a dropped
|
|
24
|
+
* round automatically. Automatic re-expansion is a guess about which
|
|
25
|
+
* round matters, and a wrong guess spends the budget it was trying to
|
|
26
|
+
* save; the model already knows what it is looking for. It also keeps
|
|
27
|
+
* the machinery inspectable — a recall appears in `steps` like any
|
|
28
|
+
* other call.
|
|
29
|
+
* - **It never throws.** An unknown address answers `{ error }` with the
|
|
30
|
+
* index address to try instead, exactly as every other tool in this
|
|
31
|
+
* package answers a content-level problem.
|
|
32
|
+
*/
|
|
33
|
+
/** The tool's name. A host that registers its own `recall` keeps it. */
|
|
34
|
+
export declare const RECALL_TOOL_NAME = "recall";
|
|
1
35
|
/**
|
|
2
36
|
* The slot name for one archived round. Content-addressed: same bytes,
|
|
3
37
|
* same name, so a re-compaction overwrites rather than duplicates.
|
|
4
|
-
* @param
|
|
5
|
-
* @returns {string}
|
|
38
|
+
* @param text - the round's serialized wire messages
|
|
6
39
|
*/
|
|
7
|
-
export function roundSlotName(text: string): string;
|
|
40
|
+
export declare function roundSlotName(text: string): string;
|
|
8
41
|
/**
|
|
9
42
|
* The slot name for the index of one archived set. Content-addressed the
|
|
10
43
|
* same way, over the listing itself — so a request that dropped the same
|
|
11
44
|
* rounds names the same index.
|
|
12
|
-
* @param
|
|
13
|
-
* @returns {string}
|
|
45
|
+
* @param text - the index listing
|
|
14
46
|
*/
|
|
15
|
-
export function indexSlotName(text: string): string;
|
|
47
|
+
export declare function indexSlotName(text: string): string;
|
|
16
48
|
/**
|
|
17
49
|
* One address, written the one way it is ever written — the call a model
|
|
18
50
|
* would make to follow it. Everything that names a slot in prose goes
|
|
19
51
|
* through here, so `slotAddressesIn` has a single syntax to read and a
|
|
20
52
|
* reader following an address is never following a second convention.
|
|
21
|
-
* @param {string} name
|
|
22
|
-
* @returns {string}
|
|
23
53
|
*/
|
|
24
|
-
export function slotRef(name: string): string;
|
|
54
|
+
export declare function slotRef(name: string): string;
|
|
25
55
|
/**
|
|
26
56
|
* The address as it appears in a synopsis line. Short on purpose: it is
|
|
27
57
|
* paid for out of the same character budget the rounds were cut to fit.
|
|
28
|
-
* @param
|
|
29
|
-
* @param {number} size - the archived round's size in characters
|
|
30
|
-
* @returns {string}
|
|
58
|
+
* @param size - the archived round's size in characters
|
|
31
59
|
*/
|
|
32
|
-
export function slotAddress(name: string, size: number): string;
|
|
60
|
+
export declare function slotAddress(name: string, size: number): string;
|
|
33
61
|
/**
|
|
34
62
|
* Every slot address named in a piece of text, in order, without
|
|
35
63
|
* duplicates. The one reader of the address syntax — a probe, a test or
|
|
36
64
|
* a host auditing what a request can still reach uses this rather than
|
|
37
65
|
* writing the pattern a second time.
|
|
38
|
-
* @param {string} text
|
|
39
|
-
* @returns {string[]}
|
|
40
66
|
*/
|
|
41
|
-
export function slotAddressesIn(text: string): string[];
|
|
67
|
+
export declare function slotAddressesIn(text: string): string[];
|
|
42
68
|
/**
|
|
43
69
|
* The `recall` tool definition, bound to a ledger.
|
|
44
70
|
*
|
|
@@ -52,15 +78,12 @@ export function slotAddressesIn(text: string): string[];
|
|
|
52
78
|
* into the transcript, so a slot bigger than that is cut there, once, by
|
|
53
79
|
* the same rule as any other oversized result.
|
|
54
80
|
*
|
|
55
|
-
* @param
|
|
56
|
-
* @param {{ index?: string }} [options] - `index` is the address of the
|
|
81
|
+
* @param [options] - `index` is the address of the
|
|
57
82
|
* listing to point at when a name is unknown; it travels in the
|
|
58
83
|
* rejection because a model that mistyped an address needs the place
|
|
59
84
|
* the real ones are written down, not just a refusal.
|
|
60
|
-
* @returns {{ name: string, description: string, inputSchema: any,
|
|
61
|
-
* execute: (input: any) => Promise<any> }}
|
|
62
85
|
*/
|
|
63
|
-
export function createRecallTool(ledger: {
|
|
86
|
+
export declare function createRecallTool(ledger: {
|
|
64
87
|
readSlot: (name: string) => Promise<any>;
|
|
65
88
|
}, options?: {
|
|
66
89
|
index?: string;
|
|
@@ -70,5 +93,3 @@ export function createRecallTool(ledger: {
|
|
|
70
93
|
inputSchema: any;
|
|
71
94
|
execute: (input: any) => Promise<any>;
|
|
72
95
|
};
|
|
73
|
-
/** The tool's name. A host that registers its own `recall` keeps it. */
|
|
74
|
-
export const RECALL_TOOL_NAME: "recall";
|
package/src/recall.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
//@ts-check
|
|
2
1
|
/**
|
|
3
2
|
* `recall` — the address scheme compaction writes, and the tool that
|
|
4
3
|
* reads it back.
|
|
@@ -8,7 +7,7 @@
|
|
|
8
7
|
* excerpt is now a *preview* of something that still exists, and this
|
|
9
8
|
* module owns both halves of that promise — the address a synopsis line
|
|
10
9
|
* carries, and the tool a model calls to follow it. They live together
|
|
11
|
-
* because they are one contract: a name written by `agent.
|
|
10
|
+
* because they are one contract: a name written by `agent.ts` and read
|
|
12
11
|
* by a model has to be produced and parsed in exactly one place, or the
|
|
13
12
|
* two drift and a request ends up naming an address nothing answers.
|
|
14
13
|
*
|
|
@@ -31,77 +30,60 @@
|
|
|
31
30
|
* index address to try instead, exactly as every other tool in this
|
|
32
31
|
* package answers a content-level problem.
|
|
33
32
|
*/
|
|
34
|
-
|
|
35
33
|
import { hashContent } from '@jarenjs/core/string';
|
|
36
|
-
|
|
37
34
|
/** The tool's name. A host that registers its own `recall` keeps it. */
|
|
38
35
|
export const RECALL_TOOL_NAME = 'recall';
|
|
39
|
-
|
|
40
36
|
/** Address prefixes: one archived round, and the index that lists them. */
|
|
41
37
|
const ROUND_PREFIX = 'r-';
|
|
42
38
|
const INDEX_PREFIX = 'rx-';
|
|
43
|
-
|
|
44
39
|
/**
|
|
45
40
|
* The slot name for one archived round. Content-addressed: same bytes,
|
|
46
41
|
* same name, so a re-compaction overwrites rather than duplicates.
|
|
47
|
-
* @param
|
|
48
|
-
* @returns {string}
|
|
42
|
+
* @param text - the round's serialized wire messages
|
|
49
43
|
*/
|
|
50
44
|
export function roundSlotName(text) {
|
|
51
|
-
|
|
45
|
+
return `${ROUND_PREFIX}${hashContent(text)}-${text.length}`;
|
|
52
46
|
}
|
|
53
|
-
|
|
54
47
|
/**
|
|
55
48
|
* The slot name for the index of one archived set. Content-addressed the
|
|
56
49
|
* same way, over the listing itself — so a request that dropped the same
|
|
57
50
|
* rounds names the same index.
|
|
58
|
-
* @param
|
|
59
|
-
* @returns {string}
|
|
51
|
+
* @param text - the index listing
|
|
60
52
|
*/
|
|
61
53
|
export function indexSlotName(text) {
|
|
62
|
-
|
|
54
|
+
return `${INDEX_PREFIX}${hashContent(text)}-${text.length}`;
|
|
63
55
|
}
|
|
64
|
-
|
|
65
56
|
/**
|
|
66
57
|
* One address, written the one way it is ever written — the call a model
|
|
67
58
|
* would make to follow it. Everything that names a slot in prose goes
|
|
68
59
|
* through here, so `slotAddressesIn` has a single syntax to read and a
|
|
69
60
|
* reader following an address is never following a second convention.
|
|
70
|
-
* @param {string} name
|
|
71
|
-
* @returns {string}
|
|
72
61
|
*/
|
|
73
62
|
export function slotRef(name) {
|
|
74
|
-
|
|
63
|
+
return `recall("${name}")`;
|
|
75
64
|
}
|
|
76
|
-
|
|
77
65
|
/**
|
|
78
66
|
* The address as it appears in a synopsis line. Short on purpose: it is
|
|
79
67
|
* paid for out of the same character budget the rounds were cut to fit.
|
|
80
|
-
* @param
|
|
81
|
-
* @param {number} size - the archived round's size in characters
|
|
82
|
-
* @returns {string}
|
|
68
|
+
* @param size - the archived round's size in characters
|
|
83
69
|
*/
|
|
84
70
|
export function slotAddress(name, size) {
|
|
85
|
-
|
|
71
|
+
return `[${slotRef(name)} · ${size}B]`;
|
|
86
72
|
}
|
|
87
|
-
|
|
88
73
|
/**
|
|
89
74
|
* Every slot address named in a piece of text, in order, without
|
|
90
75
|
* duplicates. The one reader of the address syntax — a probe, a test or
|
|
91
76
|
* a host auditing what a request can still reach uses this rather than
|
|
92
77
|
* writing the pattern a second time.
|
|
93
|
-
* @param {string} text
|
|
94
|
-
* @returns {string[]}
|
|
95
78
|
*/
|
|
96
79
|
export function slotAddressesIn(text) {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
80
|
+
const names = [];
|
|
81
|
+
for (const match of String(text ?? '').matchAll(/recall\("([^"\s]+)"\)/g)) {
|
|
82
|
+
if (!names.includes(match[1]))
|
|
83
|
+
names.push(match[1]);
|
|
84
|
+
}
|
|
85
|
+
return names;
|
|
103
86
|
}
|
|
104
|
-
|
|
105
87
|
/**
|
|
106
88
|
* The `recall` tool definition, bound to a ledger.
|
|
107
89
|
*
|
|
@@ -115,46 +97,44 @@ export function slotAddressesIn(text) {
|
|
|
115
97
|
* into the transcript, so a slot bigger than that is cut there, once, by
|
|
116
98
|
* the same rule as any other oversized result.
|
|
117
99
|
*
|
|
118
|
-
* @param
|
|
119
|
-
* @param {{ index?: string }} [options] - `index` is the address of the
|
|
100
|
+
* @param [options] - `index` is the address of the
|
|
120
101
|
* listing to point at when a name is unknown; it travels in the
|
|
121
102
|
* rejection because a model that mistyped an address needs the place
|
|
122
103
|
* the real ones are written down, not just a refusal.
|
|
123
|
-
* @returns {{ name: string, description: string, inputSchema: any,
|
|
124
|
-
* execute: (input: any) => Promise<any> }}
|
|
125
104
|
*/
|
|
126
105
|
export function createRecallTool(ledger, options = {}) {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
106
|
+
return {
|
|
107
|
+
name: RECALL_TOOL_NAME,
|
|
108
|
+
description: 'Fetch an archived conversation round in full by its address. Earlier rounds'
|
|
109
|
+
+ ' that did not fit the history budget were archived, not deleted: each synopsis line'
|
|
110
|
+
+ ' carries its address as recall("name"), and the index address listed in the synopsis'
|
|
111
|
+
+ ' header returns every address there is.',
|
|
112
|
+
inputSchema: {
|
|
113
|
+
type: 'object',
|
|
114
|
+
properties: {
|
|
115
|
+
slot: {
|
|
116
|
+
type: 'string',
|
|
117
|
+
minLength: 1,
|
|
118
|
+
description: 'The address from a synopsis line, e.g. the name inside recall("…").',
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
required: ['slot'],
|
|
122
|
+
additionalProperties: false,
|
|
140
123
|
},
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
return { slot, size: String(content).length, content };
|
|
158
|
-
},
|
|
159
|
-
};
|
|
124
|
+
execute: async ({ slot }) => {
|
|
125
|
+
const content = await ledger.readSlot(slot);
|
|
126
|
+
if (content?.status === 'evicted')
|
|
127
|
+
return { error: `slot '${slot}' was evicted`, ...content };
|
|
128
|
+
if (content === undefined || content === null) {
|
|
129
|
+
return {
|
|
130
|
+
error: `unknown slot '${slot}'`,
|
|
131
|
+
...(options.index === undefined ? {} : { index: options.index }),
|
|
132
|
+
hint: options.index === undefined
|
|
133
|
+
? 'use an address exactly as it appears inside recall("…") in the synopsis'
|
|
134
|
+
: `${slotRef(options.index)} lists every address that exists`,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
return { slot, size: String(content).length, content };
|
|
138
|
+
},
|
|
139
|
+
};
|
|
160
140
|
}
|
package/src/retention.d.ts
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
|
+
/** Serialized UTF-8 bytes, including JSON escaping and punctuation. */
|
|
2
|
+
export declare const jsonBytes: (value: any) => number;
|
|
1
3
|
/**
|
|
2
4
|
* Exact whole-map accounting. Braces belong to overhead; each member owns its
|
|
3
5
|
* leading comma. Archive metadata, contents, reports and tombstones stay visible.
|
|
4
|
-
* @param {Record<string, any>} records
|
|
5
6
|
*/
|
|
6
|
-
export function ledgerFootprint(records: Record<string, any>): {
|
|
7
|
+
export declare function ledgerFootprint(records: Record<string, any>): {
|
|
7
8
|
bytes: any;
|
|
8
9
|
overhead: number;
|
|
9
|
-
classes:
|
|
10
|
+
classes: Record<string, any>;
|
|
10
11
|
};
|
|
11
12
|
/** Lossless checkpoint: repeated note/evidence pairs share a dictionary entry. */
|
|
12
|
-
export function checkpointProgress(goal: any): {
|
|
13
|
+
export declare function checkpointProgress(goal: any): {
|
|
13
14
|
version: number;
|
|
14
15
|
sources: any[];
|
|
15
16
|
records: any[];
|
|
16
17
|
};
|
|
17
18
|
/** Validate exact source coverage and evidence, including prior checkpoints. */
|
|
18
|
-
export function validateCheckpoint(checkpoint: any, goal: any): {
|
|
19
|
+
export declare function validateCheckpoint(checkpoint: any, goal: any): {
|
|
19
20
|
valid: boolean;
|
|
20
21
|
errors: any[];
|
|
21
22
|
};
|
|
22
23
|
/** Complete goal context. No excerpting or silent clipping of evidence. */
|
|
23
|
-
export function goalPrompt(goal: any): string;
|
|
24
|
-
export function jsonBytes(value: any): number;
|
|
24
|
+
export declare function goalPrompt(goal: any): string;
|
package/src/retention.js
CHANGED
|
@@ -1,91 +1,108 @@
|
|
|
1
|
-
//@ts-check
|
|
2
1
|
import { JarenValidator } from '@jarenjs/validate';
|
|
3
|
-
import { GOAL_SCHEMA } from
|
|
2
|
+
import { GOAL_SCHEMA } from "./schemas/ledger.js";
|
|
4
3
|
import { checkOutcome } from '@jarenjs/core/check';
|
|
5
4
|
const checkpointCheck = new JarenValidator({ collectErrors: true, skipErrors: false, unknownFormats: 'ignore' }).compile(GOAL_SCHEMA.properties.checkpoint);
|
|
6
|
-
|
|
7
5
|
/** Serialized UTF-8 bytes, including JSON escaping and punctuation. */
|
|
8
6
|
export const jsonBytes = (value) => new TextEncoder().encode(JSON.stringify(value)).length;
|
|
9
|
-
|
|
10
7
|
/**
|
|
11
8
|
* Exact whole-map accounting. Braces belong to overhead; each member owns its
|
|
12
9
|
* leading comma. Archive metadata, contents, reports and tombstones stay visible.
|
|
13
|
-
* @param {Record<string, any>} records
|
|
14
10
|
*/
|
|
15
11
|
export function ledgerFootprint(records) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
12
|
+
const classes = {};
|
|
13
|
+
Object.keys(records).sort().forEach((key, index) => {
|
|
14
|
+
let kind = 'other';
|
|
15
|
+
if (key.startsWith('ai/snap/'))
|
|
16
|
+
kind = 'snapshots';
|
|
17
|
+
else if (key.startsWith('ai/counters/'))
|
|
18
|
+
kind = 'counters';
|
|
19
|
+
else if (key.startsWith('ai/state/memory/'))
|
|
20
|
+
kind = 'memories';
|
|
21
|
+
else if (key.startsWith('ai/state/skill/'))
|
|
22
|
+
kind = 'skills';
|
|
23
|
+
else if (key.startsWith('ai/state/goal/'))
|
|
24
|
+
kind = 'goals';
|
|
25
|
+
else if (key.startsWith('ai/state/evicted/') || key.startsWith('ai/state/retention/'))
|
|
26
|
+
kind = 'retention';
|
|
27
|
+
else if (key.startsWith('ai/state/slot/'))
|
|
28
|
+
kind = records[key]?.kind?.startsWith('agent-round') ? 'archives' : 'slots';
|
|
29
|
+
else if (key.startsWith('ai/state/slot-content/')) {
|
|
30
|
+
const name = key.slice('ai/state/slot-content/'.length);
|
|
31
|
+
kind = records[`ai/state/slot/${name}`]?.kind?.startsWith('agent-round') ? 'archives' : 'slots';
|
|
32
|
+
}
|
|
33
|
+
const entry = classes[kind] ??= { bytes: 0, items: 0 };
|
|
34
|
+
entry.bytes += jsonBytes(key) + 1 + jsonBytes(records[key]) + (index ? 1 : 0);
|
|
35
|
+
entry.items++;
|
|
36
|
+
});
|
|
37
|
+
return { bytes: 2 + Object.values(classes).reduce((n, entry) => n + entry.bytes, 0), overhead: 2, classes };
|
|
35
38
|
}
|
|
36
|
-
|
|
37
39
|
/** Lossless checkpoint: repeated note/evidence pairs share a dictionary entry. */
|
|
38
40
|
export function checkpointProgress(goal) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
41
|
+
const records = [], sources = [];
|
|
42
|
+
const add = (source, record) => {
|
|
43
|
+
if (typeof source?.id !== 'string' || source.id === '' || !record)
|
|
44
|
+
throw new TypeError('checkpoint needs identified source entries');
|
|
45
|
+
let index = records.findIndex((held) => held.note === record.note && held.evidence === record.evidence);
|
|
46
|
+
if (index < 0) {
|
|
47
|
+
index = records.length;
|
|
48
|
+
records.push({ note: record.note, evidence: record.evidence });
|
|
49
|
+
}
|
|
50
|
+
sources.push({ id: source.id, at: source.at, record: index });
|
|
51
|
+
};
|
|
52
|
+
for (const source of goal.checkpoint?.sources ?? [])
|
|
53
|
+
add(source, goal.checkpoint.records[source.record]);
|
|
54
|
+
for (const entry of goal.progress)
|
|
55
|
+
add(entry, entry);
|
|
56
|
+
return { version: 1, sources, records };
|
|
49
57
|
}
|
|
50
|
-
|
|
51
58
|
/** Validate exact source coverage and evidence, including prior checkpoints. */
|
|
52
59
|
export function validateCheckpoint(checkpoint, goal) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
60
|
+
const shape = checkOutcome(checkpointCheck(checkpoint));
|
|
61
|
+
if (!shape.valid)
|
|
62
|
+
return {
|
|
63
|
+
valid: false, errors: shape.errors.map((error) => ({
|
|
64
|
+
...error,
|
|
65
|
+
code: 'GOAL_CHECKPOINT', docPath: `/checkpoint${error.instancePath ?? ''}`
|
|
66
|
+
}))
|
|
67
|
+
};
|
|
68
|
+
let expected;
|
|
69
|
+
try {
|
|
70
|
+
expected = checkpointProgress(goal);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return { valid: false, errors: [{ code: 'GOAL_CHECKPOINT', docPath: '/goal', message: 'invalid checkpoint source goal' }] };
|
|
74
|
+
}
|
|
75
|
+
const errors = [];
|
|
76
|
+
const add = (path, message) => errors.push({ code: 'GOAL_CHECKPOINT', docPath: path, instancePath: path, message });
|
|
77
|
+
if (checkpoint?.version !== 1 || !Array.isArray(checkpoint?.sources) || !Array.isArray(checkpoint?.records))
|
|
78
|
+
add('/checkpoint', 'expected a versioned checkpoint with sources and records');
|
|
79
|
+
else {
|
|
80
|
+
const sources = new Map(expected.sources.map((source) => [source.id, source]));
|
|
81
|
+
const seen = new Set();
|
|
82
|
+
checkpoint.sources.forEach((source, index) => {
|
|
83
|
+
const held = sources.get(source?.id), record = checkpoint.records[source?.record];
|
|
84
|
+
if (!held || seen.has(source.id) || source.at !== held.at || !Number.isSafeInteger(source.record)
|
|
85
|
+
|| (record?.note !== expected.records[held.record].note || record?.evidence !== expected.records[held.record].evidence))
|
|
86
|
+
add(`/checkpoint/sources/${index}`, 'source is invented, duplicated or changes its note/evidence');
|
|
87
|
+
seen.add(source?.id);
|
|
88
|
+
});
|
|
89
|
+
if (seen.size !== sources.size || [...sources.keys()].some((id) => !seen.has(id)))
|
|
90
|
+
add('/checkpoint/sources', 'every retired source id must remain covered');
|
|
91
|
+
if (checkpoint.records.some((_, index) => !checkpoint.sources.some((source) => source.record === index)))
|
|
92
|
+
add('/checkpoint/records', 'unreferenced checkpoint record');
|
|
93
|
+
}
|
|
94
|
+
return { valid: errors.length === 0, errors };
|
|
79
95
|
}
|
|
80
|
-
|
|
81
96
|
/** Complete goal context. No excerpting or silent clipping of evidence. */
|
|
82
97
|
export function goalPrompt(goal) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
98
|
+
const progress = Array.isArray(goal.progress) ? goal.progress : [];
|
|
99
|
+
const lines = ['## Your objective (persistent, across sessions)', goal.objective];
|
|
100
|
+
if (goal.checkpoint)
|
|
101
|
+
lines.push('', 'Validated checkpoint — this work is DONE, do not repeat it:', JSON.stringify(goal.checkpoint));
|
|
102
|
+
if (progress.length) {
|
|
103
|
+
lines.push('', `Progress recorded so far (${progress.length} entr${progress.length === 1 ? 'y' : 'ies'}) — this work is DONE, do not repeat it:`);
|
|
104
|
+
for (const entry of progress)
|
|
105
|
+
lines.push(`- [${entry.at}] ${entry.note} (evidence: ${entry.evidence})`);
|
|
106
|
+
}
|
|
107
|
+
return lines.join('\n');
|
|
91
108
|
}
|
|
@@ -1,30 +1,31 @@
|
|
|
1
1
|
/** An admitted artifact describes identity and location; validation never fetches it. */
|
|
2
|
-
export const ARTIFACT_SCHEMA: {
|
|
2
|
+
export declare const ARTIFACT_SCHEMA: {
|
|
3
3
|
type: "object";
|
|
4
4
|
properties: Record<string, object>;
|
|
5
5
|
required: string[];
|
|
6
6
|
additionalProperties: false;
|
|
7
7
|
};
|
|
8
8
|
/** Evidence selects content from one admitted artifact. */
|
|
9
|
-
export const EVIDENCE_SCHEMA: {
|
|
9
|
+
export declare const EVIDENCE_SCHEMA: {
|
|
10
10
|
type: "object";
|
|
11
11
|
properties: Record<string, object>;
|
|
12
12
|
required: string[];
|
|
13
13
|
additionalProperties: false;
|
|
14
14
|
};
|
|
15
15
|
/** Status is an explicit author assertion, not an entailment or authority score. */
|
|
16
|
-
export const CLAIM_SCHEMA: {
|
|
16
|
+
export declare const CLAIM_SCHEMA: {
|
|
17
|
+
type: "object";
|
|
18
|
+
properties: Record<string, object>;
|
|
19
|
+
required: string[];
|
|
20
|
+
additionalProperties: false;
|
|
21
|
+
};
|
|
22
|
+
/** Versioned referential envelope; visible ids are the evidence admitted to this view. */
|
|
23
|
+
export declare const CLAIM_EVIDENCE_SCHEMA: {
|
|
17
24
|
type: "object";
|
|
18
25
|
properties: Record<string, object>;
|
|
19
26
|
required: string[];
|
|
20
27
|
additionalProperties: false;
|
|
21
28
|
};
|
|
22
|
-
export namespace CLAIM_EVIDENCE_SCHEMA {
|
|
23
|
-
let type: "object";
|
|
24
|
-
let properties: Record<string, object>;
|
|
25
|
-
let required: string[];
|
|
26
|
-
let additionalProperties: false;
|
|
27
|
-
}
|
|
28
29
|
export type ArtifactRecord = {
|
|
29
30
|
id: string;
|
|
30
31
|
kind: string;
|
|
@@ -42,7 +43,7 @@ export type ClaimRecord = {
|
|
|
42
43
|
id: string;
|
|
43
44
|
text: string;
|
|
44
45
|
critical: boolean;
|
|
45
|
-
status:
|
|
46
|
+
status: 'supported' | 'unresolved';
|
|
46
47
|
evidence: string[];
|
|
47
48
|
};
|
|
48
49
|
export type ClaimEvidenceEnvelope = {
|
package/src/schemas/evidence.js
CHANGED
|
@@ -1,32 +1,25 @@
|
|
|
1
|
-
//@ts-check
|
|
2
1
|
const ID = { type: 'string', minLength: 1 };
|
|
3
2
|
/**
|
|
4
|
-
* @param {Record<string, object>} properties
|
|
5
|
-
* @param {string[]} required
|
|
6
|
-
* @returns {{ type: 'object', properties: Record<string, object>, required: string[], additionalProperties: false }}
|
|
7
3
|
*/
|
|
8
4
|
const record = (properties, required) => ({ type: 'object', properties, required, additionalProperties: false });
|
|
9
|
-
|
|
10
5
|
/** An admitted artifact describes identity and location; validation never fetches it. */
|
|
11
|
-
export const ARTIFACT_SCHEMA = record({
|
|
12
|
-
|
|
6
|
+
export const ARTIFACT_SCHEMA = record({
|
|
7
|
+
id: ID, kind: ID, locator: ID, digest: ID,
|
|
8
|
+
metadata: { type: 'object' }
|
|
9
|
+
}, ['id', 'kind']);
|
|
13
10
|
/** Evidence selects content from one admitted artifact. */
|
|
14
11
|
export const EVIDENCE_SCHEMA = record({ id: ID, artifact: ID, selector: ID, quote: ID }, ['id', 'artifact']);
|
|
15
12
|
/** Status is an explicit author assertion, not an entailment or authority score. */
|
|
16
|
-
export const CLAIM_SCHEMA = record({
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
export const CLAIM_SCHEMA = record({
|
|
14
|
+
id: ID, text: ID, critical: { type: 'boolean' },
|
|
15
|
+
status: { enum: ['supported', 'unresolved'] },
|
|
16
|
+
evidence: { type: 'array', items: ID, uniqueItems: true }
|
|
17
|
+
}, ['id', 'text', 'critical', 'status', 'evidence']);
|
|
19
18
|
/** Versioned referential envelope; visible ids are the evidence admitted to this view. */
|
|
20
19
|
export const CLAIM_EVIDENCE_SCHEMA = {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
...record({
|
|
21
|
+
version: { const: 1 }, artifacts: { type: 'array', items: ARTIFACT_SCHEMA },
|
|
22
|
+
evidence: { type: 'array', items: EVIDENCE_SCHEMA }, claims: { type: 'array', items: CLAIM_SCHEMA },
|
|
23
|
+
visibleEvidence: { type: 'array', items: ID, uniqueItems: true }
|
|
24
|
+
}, ['version', 'artifacts', 'evidence', 'claims', 'visibleEvidence']),
|
|
25
25
|
};
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* @typedef {{ id: string, kind: string, locator?: string, digest?: string, metadata?: Record<string, any> }} ArtifactRecord
|
|
29
|
-
* @typedef {{ id: string, artifact: string, selector?: string, quote?: string }} EvidenceRecord
|
|
30
|
-
* @typedef {{ id: string, text: string, critical: boolean, status: 'supported'|'unresolved', evidence: string[] }} ClaimRecord
|
|
31
|
-
* @typedef {{ version: 1, artifacts: ArtifactRecord[], evidence: EvidenceRecord[], claims: ClaimRecord[], visibleEvidence: string[] }} ClaimEvidenceEnvelope
|
|
32
|
-
*/
|