@guuey/threads 0.28.1 → 0.30.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/dist/fold-rows.d.ts +92 -5
- package/dist/fold-rows.d.ts.map +1 -1
- package/dist/fold-rows.js +191 -8
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/rows.d.ts +14 -1
- package/dist/rows.d.ts.map +1 -1
- package/dist/store.d.ts +14 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +14 -4
- package/dist/testing/contract-suite.d.ts.map +1 -1
- package/dist/testing/contract-suite.js +22 -0
- package/package.json +3 -3
package/dist/fold-rows.d.ts
CHANGED
|
@@ -20,13 +20,37 @@ export interface RowCtx {
|
|
|
20
20
|
untrustedOrigin?: boolean;
|
|
21
21
|
}
|
|
22
22
|
/**
|
|
23
|
-
*
|
|
24
|
-
* paragraphs, so they join with
|
|
25
|
-
* contract as `@guuey/agent-client`'s
|
|
26
|
-
* concatenation jammed "…instead.Here's
|
|
27
|
-
* contribute nothing (no stacked separators).
|
|
23
|
+
* The ANSWER as plain text: the message's text blocks WITHOUT interim
|
|
24
|
+
* narration. Distinct text blocks are distinct paragraphs, so they join with
|
|
25
|
+
* a paragraph break, the same block-boundary contract as `@guuey/agent-client`'s
|
|
26
|
+
* live flat-text fold (guuey#98); raw concatenation jammed "…instead.Here's
|
|
27
|
+
* your packing…". Empty blocks contribute nothing (no stacked separators).
|
|
28
|
+
* This is the row's `text`: what a reloaded thread, a preview and a hook read.
|
|
28
29
|
*/
|
|
29
30
|
export declare function messageText(msg: AgMessage): string;
|
|
31
|
+
/** The message's interim narration, one entry per non-empty narration block, in order. */
|
|
32
|
+
export declare function messageNarration(msg: AgMessage): string[];
|
|
33
|
+
/**
|
|
34
|
+
* EVERY text block, narration included, joined as the answer text was joined
|
|
35
|
+
* before narration split out of it: the MODEL's view of a stored message.
|
|
36
|
+
* `loadHistory` replays this, so the model's conversation history is the same
|
|
37
|
+
* bytes whether a row was written before the split or after it.
|
|
38
|
+
*/
|
|
39
|
+
export declare function messageFullText(msg: AgMessage): string;
|
|
40
|
+
/** A stored message's text, split: the answer, the narration, and everything (the model's view). */
|
|
41
|
+
export interface StoredTextParts {
|
|
42
|
+
answer: string;
|
|
43
|
+
narration: string[];
|
|
44
|
+
all: string;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Split a stored row's `content` into answer, narration and the model's full
|
|
48
|
+
* text, when `content` is a stored `AgMessage` (an object, or the JSON string
|
|
49
|
+
* an AppSync writer stores). Anything else is null, and a reader falls back to
|
|
50
|
+
* the row's `text`. It is read through core's lenient stored-record reader, so
|
|
51
|
+
* an unknown block is omitted rather than failing the row.
|
|
52
|
+
*/
|
|
53
|
+
export declare function storedTextParts(content: unknown): StoredTextParts | null;
|
|
30
54
|
export declare function agMessageToRow(msg: AgMessage, ctx: RowCtx & {
|
|
31
55
|
turnRecord?: AgTurnRecord;
|
|
32
56
|
}): ThreadMessageRow;
|
|
@@ -76,6 +100,65 @@ export declare function cardRowToAgArtifact(row: ThreadMessageRow): AgArtifact |
|
|
|
76
100
|
* artifactIds there.
|
|
77
101
|
*/
|
|
78
102
|
export declare function reassembleFold(rows: ThreadMessageRow[], snapshot: ThreadSnapshotRow | undefined): AgReduceResult;
|
|
103
|
+
/**
|
|
104
|
+
* Split a snapshot's STORED thread-memory array (what a binding reads back)
|
|
105
|
+
* into the view this runtime reads and the elements it carries (AgJSON
|
|
106
|
+
* draft.4 §0.2's stored-record reader). Stored data is a wire across a rolling
|
|
107
|
+
* release, so it is never taken as `AgMemoryRecord[]`:
|
|
108
|
+
* - `threadMemory`: the records the reader materializes with scope `thread`;
|
|
109
|
+
* the view a fold is seeded with and a prompt recalls;
|
|
110
|
+
* - `carriedThreadMemory`: every other stored element, verbatim and in stored
|
|
111
|
+
* order: a record the reader cannot materialize (an undefined `scope`, a
|
|
112
|
+
* missing `value`) and a readable record of another scope.
|
|
113
|
+
* The reader either materializes an element or reports it at `[i]`. If a core
|
|
114
|
+
* version ever did both for one element (a partial report), pairing the view
|
|
115
|
+
* back to the stored elements would desync, so then EVERY element is carried
|
|
116
|
+
* and nothing is seeded: safe at rest, and visible as a full carry.
|
|
117
|
+
*/
|
|
118
|
+
export declare function splitStoredThreadMemory(stored: readonly JsonValue[]): Pick<ThreadSnapshotRow, "threadMemory"> & {
|
|
119
|
+
carriedThreadMemory: JsonValue[];
|
|
120
|
+
};
|
|
121
|
+
/**
|
|
122
|
+
* The one stored thread-memory array a binding writes: the carried elements
|
|
123
|
+
* FIRST, then this runtime's thread records. Stored order is precedence for a
|
|
124
|
+
* reader that seeds every record (the reducer keys memory by scope and key, and
|
|
125
|
+
* the last write wins), and the carried elements were written before this
|
|
126
|
+
* turn, so write order stays time order.
|
|
127
|
+
*/
|
|
128
|
+
export declare function joinStoredThreadMemory(row: Pick<ThreadSnapshotRow, "threadMemory" | "carriedThreadMemory">): JsonValue[];
|
|
129
|
+
/**
|
|
130
|
+
* The thread records a turn persists: each record of the fold, except that a
|
|
131
|
+
* record the turn did NOT change is persisted as the PRIOR stored record
|
|
132
|
+
* instead of the fold's rebuild of it (guuey#1669).
|
|
133
|
+
*
|
|
134
|
+
* The prior view comes from the stored-record reader, whose elements are raw
|
|
135
|
+
* copies that keep members this runtime does not know. The fold cannot
|
|
136
|
+
* carry those: {@link seedEventsForReducer} rebuilds each record as a
|
|
137
|
+
* `memory.write` from its known fields, and the reducer keeps only those. So
|
|
138
|
+
* persisting the fold alone would drop a record's unknown members on the first
|
|
139
|
+
* turn that re-persists the thread, even though the turn never touched it. A
|
|
140
|
+
* record keeps what it was stored with until a turn changes it.
|
|
141
|
+
*
|
|
142
|
+
* - **Identity** is the reducer's: scope plus `key ?? ""`. A fold record meets
|
|
143
|
+
* the LAST prior record of that identity, the one the seed left standing,
|
|
144
|
+
* because the last write wins. Several key-less records of one scope were
|
|
145
|
+
* already collapsed to that one by the reducer before this existed; that
|
|
146
|
+
* limit predates the overlay and is not papered over here.
|
|
147
|
+
* - **Unchanged** means the known fields a turn can write are deep-equal:
|
|
148
|
+
* `scope`, `key`, `value`, `reason`, `durable`, `turnId` and `_meta`
|
|
149
|
+
* (draft.8: a `memory.write` sets it, and a patch can replace it without
|
|
150
|
+
* touching the value or the turnId, so a `_meta`-only change must count as
|
|
151
|
+
* a change or the prior `_meta` would be persisted over it). A live write
|
|
152
|
+
* stamps the current turn's `turnId`, so a turn that rewrites the SAME value
|
|
153
|
+
* still changes the record, and the fold's record wins (the unknown members
|
|
154
|
+
* described the previous write). **`threadId` is deliberately left out:** a
|
|
155
|
+
* `memory.write` has no `threadId` on its event arm, so no turn can set it and
|
|
156
|
+
* the seed cannot carry it. Comparing it would mark every stored record that
|
|
157
|
+
* has one as changed, which is exactly the loss this prevents.
|
|
158
|
+
* - Records the turn removed are absent from the fold and stay absent; a
|
|
159
|
+
* record the turn added has no prior and is the fold's own.
|
|
160
|
+
*/
|
|
161
|
+
export declare function keepUnchangedThreadMemory(priorView: readonly AgMemoryRecord[], folded: readonly AgMemoryRecord[]): AgMemoryRecord[];
|
|
79
162
|
/**
|
|
80
163
|
* Synthetic events that seed a reducer with prior latest-replace state before
|
|
81
164
|
* folding a turn (design §8.1). Seeds state + thread-memory ONLY — never
|
|
@@ -88,6 +171,10 @@ export declare function reassembleFold(rows: ThreadMessageRow[], snapshot: Threa
|
|
|
88
171
|
* Each synthetic `memory.write` carries `turnId` so the reducer's SET handler
|
|
89
172
|
* lands it back onto the AgMemoryRecord — without it, a re-seeded thread-memory
|
|
90
173
|
* record loses its turnId and memory byte-identity breaks from turn 2 on.
|
|
174
|
+
* It carries the record's `_meta` the same way: AgJSON draft.8 declares
|
|
175
|
+
* `_meta` on both the record and `memory.write`, and the reducer folds it
|
|
176
|
+
* onto the record (set on create, replaced on update), so a record's host
|
|
177
|
+
* side-channel (e.g. `_meta.provenance`) survives every re-seeded turn.
|
|
91
178
|
* (`threadId` is NOT carried: `memory.write` has no threadId on its event arm,
|
|
92
179
|
* so it cannot round-trip through a live write — that is expected.)
|
|
93
180
|
*/
|
package/dist/fold-rows.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fold-rows.d.ts","sourceRoot":"","sources":["../src/fold-rows.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EACV,SAAS,EACT,UAAU,EACV,YAAY,EAEZ,cAAc,EACd,OAAO,EACP,cAAc,EACd,SAAS,EACV,MAAM,sBAAsB,CAAC;AAE9B,OAAO,KAAK,EAAE,gBAAgB,EAAwC,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAG3G,MAAM,WAAW,MAAM;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,EAAE,EAAE,MAAM,CAAC;IACX,eAAe,EAAE,MAAM,CAAC;IACxB;;;;OAIG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;
|
|
1
|
+
{"version":3,"file":"fold-rows.d.ts","sourceRoot":"","sources":["../src/fold-rows.ts"],"names":[],"mappings":"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"}
|
package/dist/fold-rows.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readStoredAgMessage } from "@silverprotocol/core";
|
|
1
|
+
import { readStoredAgMemoryRecords, readStoredAgMessage } from "@silverprotocol/core";
|
|
2
2
|
import { asUiResource, scanProviderRawForUiResource, toolResultLocator } from "@guuey/mcp-apps-host/narrowing";
|
|
3
3
|
/** AgRole → the row's coarse authorRole projection (secondary; role of record lives in content). */
|
|
4
4
|
function roleToAuthor(role) {
|
|
@@ -9,20 +9,78 @@ function roleToAuthor(role) {
|
|
|
9
9
|
return "agent"; // assistant | tool
|
|
10
10
|
}
|
|
11
11
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* concatenation jammed "…instead.Here's your packing…". Empty blocks
|
|
16
|
-
* contribute nothing (no stacked separators).
|
|
12
|
+
* A text block the producer marked as INTERIM narration (AgJSON's open-string
|
|
13
|
+
* `phase`; an OpenAI `commentary` message arrives this way), not the answer.
|
|
14
|
+
* Only the one value this package knows splits; any other phase is answer text.
|
|
17
15
|
*/
|
|
18
|
-
|
|
16
|
+
function isNarration(block) {
|
|
17
|
+
return block.type === "text" && block.phase === "interim";
|
|
18
|
+
}
|
|
19
|
+
/** The non-empty text blocks, in order, that `keep` admits, joined as paragraphs. */
|
|
20
|
+
function joinText(msg, keep) {
|
|
19
21
|
const parts = [];
|
|
20
22
|
for (const block of msg.content) {
|
|
21
|
-
if (block.type === "text" && block.text)
|
|
23
|
+
if (block.type === "text" && block.text && keep(block))
|
|
22
24
|
parts.push(block.text);
|
|
23
25
|
}
|
|
24
26
|
return parts.join("\n\n");
|
|
25
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* The ANSWER as plain text: the message's text blocks WITHOUT interim
|
|
30
|
+
* narration. Distinct text blocks are distinct paragraphs, so they join with
|
|
31
|
+
* a paragraph break, the same block-boundary contract as `@guuey/agent-client`'s
|
|
32
|
+
* live flat-text fold (guuey#98); raw concatenation jammed "…instead.Here's
|
|
33
|
+
* your packing…". Empty blocks contribute nothing (no stacked separators).
|
|
34
|
+
* This is the row's `text`: what a reloaded thread, a preview and a hook read.
|
|
35
|
+
*/
|
|
36
|
+
export function messageText(msg) {
|
|
37
|
+
return joinText(msg, (block) => !isNarration(block));
|
|
38
|
+
}
|
|
39
|
+
/** The message's interim narration, one entry per non-empty narration block, in order. */
|
|
40
|
+
export function messageNarration(msg) {
|
|
41
|
+
const out = [];
|
|
42
|
+
for (const block of msg.content) {
|
|
43
|
+
if (isNarration(block) && block.type === "text" && block.text)
|
|
44
|
+
out.push(block.text);
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* EVERY text block, narration included, joined as the answer text was joined
|
|
50
|
+
* before narration split out of it: the MODEL's view of a stored message.
|
|
51
|
+
* `loadHistory` replays this, so the model's conversation history is the same
|
|
52
|
+
* bytes whether a row was written before the split or after it.
|
|
53
|
+
*/
|
|
54
|
+
export function messageFullText(msg) {
|
|
55
|
+
return joinText(msg, () => true);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Split a stored row's `content` into answer, narration and the model's full
|
|
59
|
+
* text, when `content` is a stored `AgMessage` (an object, or the JSON string
|
|
60
|
+
* an AppSync writer stores). Anything else is null, and a reader falls back to
|
|
61
|
+
* the row's `text`. It is read through core's lenient stored-record reader, so
|
|
62
|
+
* an unknown block is omitted rather than failing the row.
|
|
63
|
+
*/
|
|
64
|
+
export function storedTextParts(content) {
|
|
65
|
+
let raw = content;
|
|
66
|
+
if (typeof content === "string") {
|
|
67
|
+
try {
|
|
68
|
+
raw = JSON.parse(content);
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
if (err instanceof SyntaxError)
|
|
72
|
+
return null; // not JSON: not a stored AgMessage
|
|
73
|
+
throw err;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (raw === null || typeof raw !== "object")
|
|
77
|
+
return null;
|
|
78
|
+
const read = readStoredAgMessage(raw);
|
|
79
|
+
if (read.value === undefined)
|
|
80
|
+
return null;
|
|
81
|
+
const msg = read.value;
|
|
82
|
+
return { answer: messageText(msg), narration: messageNarration(msg), all: messageFullText(msg) };
|
|
83
|
+
}
|
|
26
84
|
export function agMessageToRow(msg, ctx) {
|
|
27
85
|
const text = messageText(msg);
|
|
28
86
|
const kind = "text";
|
|
@@ -282,6 +340,126 @@ export function reassembleFold(rows, snapshot) {
|
|
|
282
340
|
...(snapshot?.workingState !== undefined ? { state: snapshot.workingState } : {}),
|
|
283
341
|
};
|
|
284
342
|
}
|
|
343
|
+
/**
|
|
344
|
+
* Split a snapshot's STORED thread-memory array (what a binding reads back)
|
|
345
|
+
* into the view this runtime reads and the elements it carries (AgJSON
|
|
346
|
+
* draft.4 §0.2's stored-record reader). Stored data is a wire across a rolling
|
|
347
|
+
* release, so it is never taken as `AgMemoryRecord[]`:
|
|
348
|
+
* - `threadMemory`: the records the reader materializes with scope `thread`;
|
|
349
|
+
* the view a fold is seeded with and a prompt recalls;
|
|
350
|
+
* - `carriedThreadMemory`: every other stored element, verbatim and in stored
|
|
351
|
+
* order: a record the reader cannot materialize (an undefined `scope`, a
|
|
352
|
+
* missing `value`) and a readable record of another scope.
|
|
353
|
+
* The reader either materializes an element or reports it at `[i]`. If a core
|
|
354
|
+
* version ever did both for one element (a partial report), pairing the view
|
|
355
|
+
* back to the stored elements would desync, so then EVERY element is carried
|
|
356
|
+
* and nothing is seeded: safe at rest, and visible as a full carry.
|
|
357
|
+
*/
|
|
358
|
+
export function splitStoredThreadMemory(stored) {
|
|
359
|
+
const read = readStoredAgMemoryRecords(stored);
|
|
360
|
+
const omitted = new Set(read.reports.flatMap((r) => (r.path.length === 1 && typeof r.path[0] === "number" ? [r.path[0]] : [])));
|
|
361
|
+
if (omitted.size !== read.reports.length || read.value.length + omitted.size !== stored.length) {
|
|
362
|
+
return { threadMemory: [], carriedThreadMemory: [...stored] };
|
|
363
|
+
}
|
|
364
|
+
const threadMemory = [];
|
|
365
|
+
const carriedThreadMemory = [];
|
|
366
|
+
// Readable elements come back in stored order, so the k-th one not omitted is `read.value[k]`.
|
|
367
|
+
let k = 0;
|
|
368
|
+
stored.forEach((element, i) => {
|
|
369
|
+
if (omitted.has(i)) {
|
|
370
|
+
carriedThreadMemory.push(element);
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
const rec = read.value[k++];
|
|
374
|
+
if (rec !== undefined && rec.scope === "thread")
|
|
375
|
+
threadMemory.push(rec);
|
|
376
|
+
else
|
|
377
|
+
carriedThreadMemory.push(element);
|
|
378
|
+
});
|
|
379
|
+
return { threadMemory, carriedThreadMemory };
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* The one stored thread-memory array a binding writes: the carried elements
|
|
383
|
+
* FIRST, then this runtime's thread records. Stored order is precedence for a
|
|
384
|
+
* reader that seeds every record (the reducer keys memory by scope and key, and
|
|
385
|
+
* the last write wins), and the carried elements were written before this
|
|
386
|
+
* turn, so write order stays time order.
|
|
387
|
+
*/
|
|
388
|
+
export function joinStoredThreadMemory(row) {
|
|
389
|
+
return [...(row.carriedThreadMemory ?? []), ...row.threadMemory];
|
|
390
|
+
}
|
|
391
|
+
/** Structural equality over JSON values (object key order does not matter). */
|
|
392
|
+
function jsonEqual(a, b) {
|
|
393
|
+
if (a === b)
|
|
394
|
+
return true;
|
|
395
|
+
if (a === undefined || b === undefined || a === null || b === null)
|
|
396
|
+
return false;
|
|
397
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
398
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length)
|
|
399
|
+
return false;
|
|
400
|
+
return a.every((v, i) => jsonEqual(v, b[i]));
|
|
401
|
+
}
|
|
402
|
+
if (typeof a !== "object" || typeof b !== "object")
|
|
403
|
+
return false;
|
|
404
|
+
const keys = Object.keys(a);
|
|
405
|
+
if (keys.length !== Object.keys(b).length)
|
|
406
|
+
return false;
|
|
407
|
+
return keys.every((k) => Object.prototype.hasOwnProperty.call(b, k) && jsonEqual(a[k], b[k]));
|
|
408
|
+
}
|
|
409
|
+
/** The reducer's own memory identity (`${scope}${key ?? ""}`): a key-less record is its scope's singleton. */
|
|
410
|
+
function memoryIdentity(rec) {
|
|
411
|
+
return `${rec.scope}${rec.key ?? ""}`;
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* The thread records a turn persists: each record of the fold, except that a
|
|
415
|
+
* record the turn did NOT change is persisted as the PRIOR stored record
|
|
416
|
+
* instead of the fold's rebuild of it (guuey#1669).
|
|
417
|
+
*
|
|
418
|
+
* The prior view comes from the stored-record reader, whose elements are raw
|
|
419
|
+
* copies that keep members this runtime does not know. The fold cannot
|
|
420
|
+
* carry those: {@link seedEventsForReducer} rebuilds each record as a
|
|
421
|
+
* `memory.write` from its known fields, and the reducer keeps only those. So
|
|
422
|
+
* persisting the fold alone would drop a record's unknown members on the first
|
|
423
|
+
* turn that re-persists the thread, even though the turn never touched it. A
|
|
424
|
+
* record keeps what it was stored with until a turn changes it.
|
|
425
|
+
*
|
|
426
|
+
* - **Identity** is the reducer's: scope plus `key ?? ""`. A fold record meets
|
|
427
|
+
* the LAST prior record of that identity, the one the seed left standing,
|
|
428
|
+
* because the last write wins. Several key-less records of one scope were
|
|
429
|
+
* already collapsed to that one by the reducer before this existed; that
|
|
430
|
+
* limit predates the overlay and is not papered over here.
|
|
431
|
+
* - **Unchanged** means the known fields a turn can write are deep-equal:
|
|
432
|
+
* `scope`, `key`, `value`, `reason`, `durable`, `turnId` and `_meta`
|
|
433
|
+
* (draft.8: a `memory.write` sets it, and a patch can replace it without
|
|
434
|
+
* touching the value or the turnId, so a `_meta`-only change must count as
|
|
435
|
+
* a change or the prior `_meta` would be persisted over it). A live write
|
|
436
|
+
* stamps the current turn's `turnId`, so a turn that rewrites the SAME value
|
|
437
|
+
* still changes the record, and the fold's record wins (the unknown members
|
|
438
|
+
* described the previous write). **`threadId` is deliberately left out:** a
|
|
439
|
+
* `memory.write` has no `threadId` on its event arm, so no turn can set it and
|
|
440
|
+
* the seed cannot carry it. Comparing it would mark every stored record that
|
|
441
|
+
* has one as changed, which is exactly the loss this prevents.
|
|
442
|
+
* - Records the turn removed are absent from the fold and stay absent; a
|
|
443
|
+
* record the turn added has no prior and is the fold's own.
|
|
444
|
+
*/
|
|
445
|
+
export function keepUnchangedThreadMemory(priorView, folded) {
|
|
446
|
+
const prior = new Map();
|
|
447
|
+
for (const rec of priorView)
|
|
448
|
+
prior.set(memoryIdentity(rec), rec);
|
|
449
|
+
return folded.map((rec) => {
|
|
450
|
+
const before = prior.get(memoryIdentity(rec));
|
|
451
|
+
if (before === undefined)
|
|
452
|
+
return rec;
|
|
453
|
+
const unchanged = before.scope === rec.scope &&
|
|
454
|
+
before.key === rec.key &&
|
|
455
|
+
jsonEqual(before.value, rec.value) &&
|
|
456
|
+
before.reason === rec.reason &&
|
|
457
|
+
before.durable === rec.durable &&
|
|
458
|
+
before.turnId === rec.turnId &&
|
|
459
|
+
jsonEqual(before._meta, rec._meta);
|
|
460
|
+
return unchanged ? before : rec;
|
|
461
|
+
});
|
|
462
|
+
}
|
|
285
463
|
/**
|
|
286
464
|
* Synthetic events that seed a reducer with prior latest-replace state before
|
|
287
465
|
* folding a turn (design §8.1). Seeds state + thread-memory ONLY — never
|
|
@@ -294,6 +472,10 @@ export function reassembleFold(rows, snapshot) {
|
|
|
294
472
|
* Each synthetic `memory.write` carries `turnId` so the reducer's SET handler
|
|
295
473
|
* lands it back onto the AgMemoryRecord — without it, a re-seeded thread-memory
|
|
296
474
|
* record loses its turnId and memory byte-identity breaks from turn 2 on.
|
|
475
|
+
* It carries the record's `_meta` the same way: AgJSON draft.8 declares
|
|
476
|
+
* `_meta` on both the record and `memory.write`, and the reducer folds it
|
|
477
|
+
* onto the record (set on create, replaced on update), so a record's host
|
|
478
|
+
* side-channel (e.g. `_meta.provenance`) survives every re-seeded turn.
|
|
297
479
|
* (`threadId` is NOT carried: `memory.write` has no threadId on its event arm,
|
|
298
480
|
* so it cannot round-trip through a live write — that is expected.)
|
|
299
481
|
*/
|
|
@@ -313,6 +495,7 @@ export function seedEventsForReducer(priorState, priorThreadMemory) {
|
|
|
313
495
|
...(rec.reason !== undefined ? { reason: rec.reason } : {}),
|
|
314
496
|
...(rec.durable !== undefined ? { durable: rec.durable } : {}),
|
|
315
497
|
...(rec.turnId !== undefined ? { turnId: rec.turnId } : {}),
|
|
498
|
+
...(rec._meta !== undefined ? { _meta: rec._meta } : {}),
|
|
316
499
|
});
|
|
317
500
|
}
|
|
318
501
|
return events;
|
package/dist/index.d.ts
CHANGED
|
@@ -21,5 +21,5 @@ export type { HandoffEvent, ReportEvent, StoredHistoryMessage, ThreadMessageEven
|
|
|
21
21
|
export { ThreadStore, type AppendFoldInput, type AppendFoldResult, type AppendMessageInput, type AppendMessageResult, type EnsureThreadInput, } from "./store.js";
|
|
22
22
|
export { InMemoryThreadPersistence } from "./in-memory.js";
|
|
23
23
|
export { HttpThreadPersistence, HttpThreadStoreError, type HttpThreadPersistenceOptions, type ThreadScope, } from "./http.js";
|
|
24
|
-
export { agArtifactToCardRow, agMessageToRow, cardRowToAgArtifact, messageText, producingToolName, reassembleFold, rowToAgMessage, seedEventsForReducer, toolNamesByCallId, uiCardArtifactsFromMessages, type RowCtx, } from "./fold-rows.js";
|
|
24
|
+
export { agArtifactToCardRow, agMessageToRow, cardRowToAgArtifact, messageFullText, messageNarration, messageText, producingToolName, storedTextParts, type StoredTextParts, joinStoredThreadMemory, reassembleFold, rowToAgMessage, seedEventsForReducer, splitStoredThreadMemory, toolNamesByCallId, uiCardArtifactsFromMessages, type RowCtx, } from "./fold-rows.js";
|
|
25
25
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,YAAY,EACV,YAAY,EACZ,WAAW,EACX,oBAAoB,EACpB,kBAAkB,EAClB,iBAAiB,EACjB,iBAAiB,EACjB,gBAAgB,EAChB,qBAAqB,EACrB,SAAS,EACT,iBAAiB,GAClB,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,WAAW,EACX,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,EACvB,KAAK,mBAAmB,EACxB,KAAK,iBAAiB,GACvB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,yBAAyB,EAAE,MAAM,gBAAgB,CAAC;AAC3D,OAAO,EACL,qBAAqB,EACrB,oBAAoB,EACpB,KAAK,4BAA4B,EACjC,KAAK,WAAW,GACjB,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,mBAAmB,EACnB,cAAc,EACd,mBAAmB,EACnB,WAAW,EACX,iBAAiB,EACjB,cAAc,EACd,cAAc,EACd,oBAAoB,EACpB,iBAAiB,EACjB,2BAA2B,EAC3B,KAAK,MAAM,GACZ,MAAM,gBAAgB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,YAAY,EACV,YAAY,EACZ,WAAW,EACX,oBAAoB,EACpB,kBAAkB,EAClB,iBAAiB,EACjB,iBAAiB,EACjB,gBAAgB,EAChB,qBAAqB,EACrB,SAAS,EACT,iBAAiB,GAClB,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,WAAW,EACX,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,EACvB,KAAK,mBAAmB,EACxB,KAAK,iBAAiB,GACvB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,yBAAyB,EAAE,MAAM,gBAAgB,CAAC;AAC3D,OAAO,EACL,qBAAqB,EACrB,oBAAoB,EACpB,KAAK,4BAA4B,EACjC,KAAK,WAAW,GACjB,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,mBAAmB,EACnB,cAAc,EACd,mBAAmB,EACnB,eAAe,EACf,gBAAgB,EAChB,WAAW,EACX,iBAAiB,EACjB,eAAe,EACf,KAAK,eAAe,EACpB,sBAAsB,EACtB,cAAc,EACd,cAAc,EACd,oBAAoB,EACpB,uBAAuB,EACvB,iBAAiB,EACjB,2BAA2B,EAC3B,KAAK,MAAM,GACZ,MAAM,gBAAgB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { ThreadStore, } from "./store.js";
|
|
2
2
|
export { InMemoryThreadPersistence } from "./in-memory.js";
|
|
3
3
|
export { HttpThreadPersistence, HttpThreadStoreError, } from "./http.js";
|
|
4
|
-
export { agArtifactToCardRow, agMessageToRow, cardRowToAgArtifact, messageText, producingToolName, reassembleFold, rowToAgMessage, seedEventsForReducer, toolNamesByCallId, uiCardArtifactsFromMessages, } from "./fold-rows.js";
|
|
4
|
+
export { agArtifactToCardRow, agMessageToRow, cardRowToAgArtifact, messageFullText, messageNarration, messageText, producingToolName, storedTextParts, joinStoredThreadMemory, reassembleFold, rowToAgMessage, seedEventsForReducer, splitStoredThreadMemory, toolNamesByCallId, uiCardArtifactsFromMessages, } from "./fold-rows.js";
|
package/dist/rows.d.ts
CHANGED
|
@@ -123,8 +123,21 @@ export interface ThreadSnapshotRow {
|
|
|
123
123
|
userId: string;
|
|
124
124
|
/** AgReduceResult.state — opaque working blob. */
|
|
125
125
|
workingState?: JsonValue;
|
|
126
|
-
/**
|
|
126
|
+
/**
|
|
127
|
+
* The thread's memory records this runtime reads: `AgReduceResult.memory`
|
|
128
|
+
* filtered to scope='thread'. A binding returns the readable view here, and
|
|
129
|
+
* stored elements it could not read travel in {@link carriedThreadMemory}.
|
|
130
|
+
*/
|
|
127
131
|
threadMemory: AgMemoryRecord[];
|
|
132
|
+
/**
|
|
133
|
+
* Stored thread-memory elements this runtime does not read or own: a record
|
|
134
|
+
* written in a newer vocabulary (an unknown scope, a missing value) or of
|
|
135
|
+
* another scope, verbatim and in stored order. A binding splits them off on
|
|
136
|
+
* read (`splitStoredThreadMemory`) and joins them back into the one stored
|
|
137
|
+
* array on write (`joinStoredThreadMemory`), so they are never dropped at
|
|
138
|
+
* rest by a runtime that cannot read them. Absent = none.
|
|
139
|
+
*/
|
|
140
|
+
carriedThreadMemory?: JsonValue[];
|
|
128
141
|
lastTurnId?: string;
|
|
129
142
|
updatedAt: string;
|
|
130
143
|
}
|
package/dist/rows.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rows.d.ts","sourceRoot":"","sources":["../src/rows.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAEtE,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAC;AAC5D,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;AAE1D,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,OAAO,CAAC;IAChB,gEAAgE;IAChE,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,SAAS,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,QAAQ,CAAC;IACf,+EAA+E;IAC/E,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,kIAAkI;IAClI,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;GAMG;AACH,MAAM,MAAM,kBAAkB,GAAG,YAAY,GAAG,WAAW,CAAC;AAE5D,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,eAAe,EAAE,MAAM,CAAC;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,iBAAiB,CAAC;IACxB,UAAU,EAAE,iBAAiB,CAAC;IAC9B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,wDAAwD;IACxD,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,qEAAqE;IACrE,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;;;;;;OAQG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;;OAIG;IACH,KAAK,CAAC,EAAE,kBAAkB,CAAC;CAC5B;AAED,iDAAiD;AACjD,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,kDAAkD;IAClD,YAAY,CAAC,EAAE,SAAS,CAAC;IACzB
|
|
1
|
+
{"version":3,"file":"rows.d.ts","sourceRoot":"","sources":["../src/rows.ts"],"names":[],"mappings":"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"}
|
package/dist/store.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgReduceResult } from "@silverprotocol/core";
|
|
1
|
+
import type { AgMemoryRecord, AgReduceResult, JsonValue } from "@silverprotocol/core";
|
|
2
2
|
import type { ThreadMessageEvent, StoredHistoryMessage, ThreadMessageKind, ThreadMessageRole, ThreadPersistencePort, ThreadSnapshotRow } from "./rows.js";
|
|
3
3
|
export interface EnsureThreadInput {
|
|
4
4
|
/** Client-supplied thread id (localStorage). Absent on first contact. */
|
|
@@ -58,6 +58,19 @@ export interface AppendFoldInput {
|
|
|
58
58
|
* {absent, true} vocabulary as {@link AppendMessageInput.untrustedOrigin}.
|
|
59
59
|
*/
|
|
60
60
|
untrustedOrigin?: boolean;
|
|
61
|
+
/**
|
|
62
|
+
* The prior snapshot's `carriedThreadMemory`: stored thread-memory elements
|
|
63
|
+
* this runtime did not read or own. Written back unchanged beside the fold's
|
|
64
|
+
* own thread records, so they are never dropped at rest. Absent = none.
|
|
65
|
+
*/
|
|
66
|
+
carriedThreadMemory?: readonly JsonValue[];
|
|
67
|
+
/**
|
|
68
|
+
* The prior snapshot's `threadMemory`: the view the reducer was seeded
|
|
69
|
+
* from. A record this turn did not change is persisted as this prior record,
|
|
70
|
+
* with the members it was stored with (`keepUnchangedThreadMemory`), never as
|
|
71
|
+
* the fold's rebuild of it. Absent = the fold's records as they are.
|
|
72
|
+
*/
|
|
73
|
+
priorThreadMemory?: readonly AgMemoryRecord[];
|
|
61
74
|
}
|
|
62
75
|
export interface AppendFoldResult {
|
|
63
76
|
messageSeqs: number[];
|
package/dist/store.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAUtF,OAAO,KAAK,EACV,kBAAkB,EAClB,oBAAoB,EACpB,iBAAiB,EACjB,iBAAiB,EAEjB,qBAAqB,EAErB,iBAAiB,EAClB,MAAM,WAAW,CAAC;AAOnB,MAAM,WAAW,iBAAiB;IAChC,yEAAyE;IACzE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,yCAAyC;IACzC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,wEAAwE;IACxE,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,iBAAiB,CAAC;IACxB,yEAAyE;IACzE,OAAO,EAAE,OAAO,CAAC;IACjB,iEAAiE;IACjE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,+EAA+E;IAC/E,eAAe,EAAE,MAAM,CAAC;IACxB,IAAI,CAAC,EAAE,iBAAiB,CAAC;IACzB;;;;;OAKG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;;OAIG;IACH,KAAK,CAAC,EAAE,kBAAkB,CAAC;CAC5B;AAED,MAAM,WAAW,mBAAmB;IAClC,GAAG,EAAE,MAAM,CAAC;IACZ,0EAA0E;IAC1E,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,cAAc,CAAC;IACrB,2EAA2E;IAC3E,mBAAmB,EAAE,MAAM,CAAC;IAC5B;;;;;OAKG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;;;OAIG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,SAAS,SAAS,EAAE,CAAC;IAC3C;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,SAAS,cAAc,EAAE,CAAC;CAC/C;AAED,MAAM,WAAW,gBAAgB;IAC/B,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,kEAAkE;IAClE,oBAAoB,EAAE,MAAM,CAAC;IAC7B;;;OAGG;IACH,OAAO,EAAE,OAAO,CAAC;CAClB;AAMD,qBAAa,WAAW;IACV,OAAO,CAAC,QAAQ,CAAC,EAAE;gBAAF,EAAE,EAAE,qBAAqB;IAEtD;;;;;;;;;;;;;OAaG;IACG,YAAY,CAAC,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAAC,MAAM,CAAC;IA4B7D;;;;;;;OAOG;IACG,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAKpE,4EAA4E;IACtE,WAAW,CACf,QAAQ,EAAE,MAAM,EAChB,KAAK,GAAE,MAA8B,GACpC,OAAO,CAAC,oBAAoB,EAAE,CAAC;IA2BlC;;;;OAIG;IACG,aAAa,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,mBAAmB,CAAC;IA8B5E,+EAA+E;IACzE,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,GAAG,SAAS,CAAC;IAI3E;;;;;;;;;;;;;;;;;OAiBG;IACG,UAAU,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,gBAAgB,CAAC;CAqGpE"}
|
package/dist/store.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* DynamoDB).
|
|
9
9
|
*/
|
|
10
10
|
import { randomUUID } from "node:crypto";
|
|
11
|
-
import { agMessageToRow, agArtifactToCardRow, producingToolName, toolNamesByCallId, uiCardArtifactsFromMessages, } from "./fold-rows.js";
|
|
11
|
+
import { agMessageToRow, agArtifactToCardRow, keepUnchangedThreadMemory, producingToolName, toolNamesByCallId, uiCardArtifactsFromMessages, storedTextParts, } from "./fold-rows.js";
|
|
12
12
|
/** Max history messages fed back as context — bounds latency on long threads. */
|
|
13
13
|
const DEFAULT_HISTORY_LIMIT = 40;
|
|
14
14
|
const PREVIEW_MAX_LEN = 240;
|
|
@@ -85,11 +85,17 @@ export class ThreadStore {
|
|
|
85
85
|
// Known trade-off: cards still consume the DynamoDB Limit before this
|
|
86
86
|
// filter, so card-heavy threads under-fill the window (bounded, most-
|
|
87
87
|
// recent-first; revisit with an over-fetch if it bites).
|
|
88
|
+
// The model's text is EVERY text block from the stored message, narration
|
|
89
|
+
// included (`storedTextParts(...).all`): the row's `text` is the answer only
|
|
90
|
+
// since narration split out of it, so reading `text` here would change what
|
|
91
|
+
// the model is replayed. For rows written before the split, `all` is the same
|
|
92
|
+
// bytes their `text` holds. A row whose content is not a stored AgMessage
|
|
93
|
+
// (an event, a managed-adapter row) keeps its `text`.
|
|
88
94
|
return rows.filter((r) => r.kind !== 'card').map((r) => ({
|
|
89
95
|
seq: r.seq,
|
|
90
96
|
authorRole: r.authorRole,
|
|
91
97
|
kind: r.kind,
|
|
92
|
-
text: r.text ?? null,
|
|
98
|
+
text: storedTextParts(r.content)?.all || (r.text ?? null),
|
|
93
99
|
content: r.content ?? null,
|
|
94
100
|
at: r.at,
|
|
95
101
|
}));
|
|
@@ -161,7 +167,8 @@ export class ThreadStore {
|
|
|
161
167
|
const msg = fold.messages[i];
|
|
162
168
|
const clientMessageId = sentinelClaimed ? `${clientMessageIdBase}#agent#${i}` : sentinelKey;
|
|
163
169
|
sentinelClaimed = true;
|
|
164
|
-
|
|
170
|
+
// The preview is the ANSWER: interim narration (`phase: "interim"`) never becomes a thread's preview.
|
|
171
|
+
const text = msg.content.reduce((acc, b) => (b.type === 'text' && b.phase !== 'interim' ? acc + b.text : acc), '');
|
|
165
172
|
const now = new Date().toISOString();
|
|
166
173
|
// Text-less agent messages (tool-call + tool-result only — the canonical
|
|
167
174
|
// card-producing turn) must not blank the preview: '' takes the SET
|
|
@@ -210,7 +217,7 @@ export class ThreadStore {
|
|
|
210
217
|
}, producingToolName(art, toolNames)));
|
|
211
218
|
artifactSeqs.push(seq);
|
|
212
219
|
}
|
|
213
|
-
const threadMemory = fold.memory.filter((m) => m.scope === 'thread');
|
|
220
|
+
const threadMemory = keepUnchangedThreadMemory(input.priorThreadMemory ?? [], fold.memory.filter((m) => m.scope === 'thread'));
|
|
214
221
|
const droppedDurableMemory = fold.memory.length - threadMemory.length;
|
|
215
222
|
if (skipSnapshot !== true) {
|
|
216
223
|
const lastTurnId = fold.turns.length ? fold.turns[fold.turns.length - 1].turnId : undefined;
|
|
@@ -218,6 +225,9 @@ export class ThreadStore {
|
|
|
218
225
|
threadId,
|
|
219
226
|
userId,
|
|
220
227
|
threadMemory,
|
|
228
|
+
...(input.carriedThreadMemory !== undefined && input.carriedThreadMemory.length > 0
|
|
229
|
+
? { carriedThreadMemory: [...input.carriedThreadMemory] }
|
|
230
|
+
: {}),
|
|
221
231
|
updatedAt: new Date().toISOString(),
|
|
222
232
|
...(fold.state !== undefined ? { workingState: fold.state } : {}),
|
|
223
233
|
...(lastTurnId ? { lastTurnId } : {}),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contract-suite.d.ts","sourceRoot":"","sources":["../../src/testing/contract-suite.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"contract-suite.d.ts","sourceRoot":"","sources":["../../src/testing/contract-suite.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,qBAAqB,EAAa,MAAM,YAAY,CAAC;AAEnE,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,qBAAqB,CAAC;IAC5B,OAAO,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACtC;AAiCD;;;GAGG;AACH,wBAAgB,iCAAiC,CAC/C,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,OAAO,CAAC,wBAAwB,CAAC,GAC5C,IAAI,CA0KN"}
|
|
@@ -174,5 +174,27 @@ export function runThreadPersistenceContractSuite(name, make) {
|
|
|
174
174
|
expect(snap?.workingState).toBeUndefined();
|
|
175
175
|
});
|
|
176
176
|
});
|
|
177
|
+
it("carried thread memory round-trips unchanged beside the readable records", async () => {
|
|
178
|
+
await withHarness(make, async (port) => {
|
|
179
|
+
await port.createThread(threadRow("t1"));
|
|
180
|
+
// Elements this runtime cannot read or does not own: an unknown scope,
|
|
181
|
+
// a record with no value, a readable record of another scope.
|
|
182
|
+
const carried = [
|
|
183
|
+
{ scope: "team", key: "plan", value: { tier: "pro" } },
|
|
184
|
+
{ scope: "thread", key: "orphan" },
|
|
185
|
+
{ scope: "user", key: "name", value: "Ada" },
|
|
186
|
+
];
|
|
187
|
+
await port.putSnapshot({
|
|
188
|
+
threadId: "t1",
|
|
189
|
+
userId: "g_contract",
|
|
190
|
+
threadMemory: [{ scope: "thread", key: "k", value: "v1" }],
|
|
191
|
+
carriedThreadMemory: carried,
|
|
192
|
+
updatedAt: "2026-08-07T00:00:03.000Z",
|
|
193
|
+
});
|
|
194
|
+
const snap = await port.getSnapshot("t1");
|
|
195
|
+
expect(snap?.threadMemory).toEqual([{ scope: "thread", key: "k", value: "v1" }]);
|
|
196
|
+
expect(JSON.stringify(snap?.carriedThreadMemory)).toBe(JSON.stringify(carried));
|
|
197
|
+
});
|
|
198
|
+
});
|
|
177
199
|
});
|
|
178
200
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@guuey/threads",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.30.0",
|
|
4
4
|
"description": "Universal session/thread persistence for AgJSON agents — the ThreadStore contract (append-fold, history, snapshots), the fold↔row mapping, and an in-memory binding. Guuey's hosted runtime is one binding; implement the port against your own store and run the exported contract suite for the same guarantees.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
}
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@silverprotocol/core": "0.
|
|
26
|
-
"@guuey/mcp-apps-host": "0.
|
|
25
|
+
"@silverprotocol/core": "0.11.0",
|
|
26
|
+
"@guuey/mcp-apps-host": "0.30.0"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@types/node": "^24.0.0",
|