@guuey/threads 0.31.0 → 0.32.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/README.md +81 -0
- package/dist/closed.d.ts +22 -0
- package/dist/closed.d.ts.map +1 -0
- package/dist/closed.js +28 -0
- package/dist/fold-rows.d.ts +278 -5
- package/dist/fold-rows.d.ts.map +1 -1
- package/dist/fold-rows.js +475 -21
- package/dist/http-read.d.ts +47 -0
- package/dist/http-read.d.ts.map +1 -0
- package/dist/http-read.js +274 -0
- package/dist/http.d.ts +37 -34
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +145 -22
- package/dist/in-memory.d.ts +2 -7
- package/dist/in-memory.d.ts.map +1 -1
- package/dist/in-memory.js +27 -1
- package/dist/index.d.ts +10 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -2
- package/dist/item-size.d.ts +56 -0
- package/dist/item-size.d.ts.map +1 -0
- package/dist/item-size.js +125 -0
- package/dist/rows.d.ts +122 -1
- package/dist/rows.d.ts.map +1 -1
- package/dist/rows.js +13 -1
- package/dist/seal.d.ts +63 -0
- package/dist/seal.d.ts.map +1 -0
- package/dist/seal.js +120 -0
- package/dist/snapshot-plan.d.ts +61 -0
- package/dist/snapshot-plan.d.ts.map +1 -0
- package/dist/snapshot-plan.js +71 -0
- package/dist/store.d.ts +130 -2
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +274 -66
- package/dist/stored-row.d.ts +17 -3
- package/dist/stored-row.d.ts.map +1 -1
- package/dist/stored-row.js +2 -21
- package/dist/testing/contract-suite.d.ts +37 -1
- package/dist/testing/contract-suite.d.ts.map +1 -1
- package/dist/testing/contract-suite.js +122 -1
- package/package.json +3 -3
package/dist/fold-rows.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readStoredAgMemoryRecords, readStoredAgMessage } from "@silverprotocol/core";
|
|
1
|
+
import { AgProviderMeta, 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) {
|
|
@@ -58,10 +58,24 @@ export function messageFullText(msg) {
|
|
|
58
58
|
* Split a stored row's `content` into answer, narration and the model's full
|
|
59
59
|
* text, when `content` is a stored `AgMessage` (an object, or the JSON string
|
|
60
60
|
* an AppSync writer stores). Anything else is null, and a reader falls back to
|
|
61
|
-
* the row's `text`.
|
|
62
|
-
*
|
|
61
|
+
* the row's `text`. The message is read by {@link readStoredRowMessage}, so an
|
|
62
|
+
* unknown block is omitted rather than failing the row, and an elided answer
|
|
63
|
+
* reads from the row's `text`. `text` is required so that every caller passes
|
|
64
|
+
* the row's `text` on purpose (`undefined` or null when the row has none).
|
|
63
65
|
*/
|
|
64
|
-
export function storedTextParts(content) {
|
|
66
|
+
export function storedTextParts(content, text) {
|
|
67
|
+
const msg = readStoredContentMessage(content, text);
|
|
68
|
+
if (msg === undefined)
|
|
69
|
+
return null;
|
|
70
|
+
return { answer: messageText(msg), narration: messageNarration(msg), all: messageFullText(msg) };
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* A stored row's `content` read as its message: a stored `AgMessage` object,
|
|
74
|
+
* or the JSON string an AppSync writer stores, through
|
|
75
|
+
* {@link readStoredRowMessage}. `undefined` for anything else (not JSON, or
|
|
76
|
+
* not a message core can materialize).
|
|
77
|
+
*/
|
|
78
|
+
function readStoredContentMessage(content, text) {
|
|
65
79
|
let raw = content;
|
|
66
80
|
if (typeof content === "string") {
|
|
67
81
|
try {
|
|
@@ -69,17 +83,139 @@ export function storedTextParts(content) {
|
|
|
69
83
|
}
|
|
70
84
|
catch (err) {
|
|
71
85
|
if (err instanceof SyntaxError)
|
|
72
|
-
return
|
|
86
|
+
return undefined; // not JSON: not a stored AgMessage
|
|
73
87
|
throw err;
|
|
74
88
|
}
|
|
75
89
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
90
|
+
return readStoredRowMessage(raw, text);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* The tool calls a stored row's message makes, in block order, as
|
|
94
|
+
* {@link StoredToolCallView}s (guuey#2388). The name rides only the call; a
|
|
95
|
+
* result carries just the id, and a facet may store the result in a message
|
|
96
|
+
* (and so a row) of its own, so a reader that names a result reads the calls
|
|
97
|
+
* of the rows around it (guuey#2487). Empty when `content` is not a stored
|
|
98
|
+
* message.
|
|
99
|
+
*/
|
|
100
|
+
export function storedToolCallViews(content, text) {
|
|
101
|
+
const msg = readStoredContentMessage(content, text);
|
|
102
|
+
if (msg === undefined)
|
|
103
|
+
return [];
|
|
104
|
+
const calls = [];
|
|
105
|
+
for (const block of msg.content) {
|
|
106
|
+
if (block.type !== "tool-call")
|
|
107
|
+
continue;
|
|
108
|
+
const view = { toolCallId: block.toolCallId, name: block.name, input: block.input };
|
|
109
|
+
if (block.title !== undefined)
|
|
110
|
+
view.title = block.title;
|
|
111
|
+
calls.push(view);
|
|
112
|
+
}
|
|
113
|
+
return calls;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* The tool results a stored row's message carries that NO card row stands for
|
|
117
|
+
* (guuey#2487): every `tool-result` block that does not carry a UI resource
|
|
118
|
+
* ({@link toolResultCarriesUi}), without its `_meta` (guuey#122), in block
|
|
119
|
+
* order. A host draws from such a result (an in-process tool's
|
|
120
|
+
* `structuredContent`, or its text) live; this is how a reader finds it again
|
|
121
|
+
* after a reload. A UI-carrying result is left out: its card row already
|
|
122
|
+
* carries it. Empty when `content` is not a stored message.
|
|
123
|
+
*/
|
|
124
|
+
export function storedToolResults(content, text) {
|
|
125
|
+
const msg = readStoredContentMessage(content, text);
|
|
126
|
+
if (msg === undefined)
|
|
127
|
+
return [];
|
|
128
|
+
const results = [];
|
|
129
|
+
for (const block of msg.content) {
|
|
130
|
+
if (block.type === "tool-result" && !toolResultCarriesUi(block))
|
|
131
|
+
results.push(withoutToolResultMeta(block));
|
|
132
|
+
}
|
|
133
|
+
return results;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* The call id of EVERY `tool-result` block a stored row's message carries, in
|
|
137
|
+
* block order, whether it carries a UI resource or not (guuey#2487). A reader
|
|
138
|
+
* that names results by their calls uses it to tell where a call's results
|
|
139
|
+
* sit on a page, card-drawn ones included: a call's first result row is
|
|
140
|
+
* written straight after the call row, and a UI-carrying result's card row
|
|
141
|
+
* straight after the result (guuey#2396). Empty when `content` is not a
|
|
142
|
+
* stored message.
|
|
143
|
+
*/
|
|
144
|
+
export function storedToolResultCallIds(content, text) {
|
|
145
|
+
const msg = readStoredContentMessage(content, text);
|
|
146
|
+
if (msg === undefined)
|
|
147
|
+
return [];
|
|
148
|
+
const ids = [];
|
|
149
|
+
for (const block of msg.content) {
|
|
150
|
+
if (block.type === "tool-result")
|
|
151
|
+
ids.push(block.toolCallId);
|
|
152
|
+
}
|
|
153
|
+
return ids;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* A row's stored message as every reader of a row reads it ({@link storedTextParts},
|
|
157
|
+
* {@link rowToAgMessage}, `classifyStoredRow`): `content` (a stored `AgMessage`
|
|
158
|
+
* object) through core's lenient stored-record reader, with an ELIDED ANSWER
|
|
159
|
+
* restored from the row's `text`. `undefined` when core cannot materialize the
|
|
160
|
+
* message. Like core's reader, it returns a view, never a record to persist.
|
|
161
|
+
*
|
|
162
|
+
* A stored message may leave its answer out of `content` because the row's
|
|
163
|
+
* `text` carries the same words: an answer block whose `text` is the
|
|
164
|
+
* {@link ELIDED_VALUE_KEY} marker ({@link isElidedAnswerBlock}). Such a message
|
|
165
|
+
* is rebuilt once, before it is read: the row's `text` (every answer block,
|
|
166
|
+
* joined) becomes the text of the first answer block, and every later answer
|
|
167
|
+
* block is left out, since that `text` already holds it. An answer block here
|
|
168
|
+
* is a block core can read as a text block, not narration, that is elided or
|
|
169
|
+
* non-empty. Every other block stays as stored. When the answer blocks stand
|
|
170
|
+
* together, as a reply's do, the rebuilt message reads the same text as the
|
|
171
|
+
* unelided one.
|
|
172
|
+
*
|
|
173
|
+
* Only the marker reads the row's `text`: a message with no answer blocks reads
|
|
174
|
+
* as it always has, with an empty answer, whatever the row's `text` says. On a
|
|
175
|
+
* row with no `text` (absent, NULL or empty) there is nothing to restore: only
|
|
176
|
+
* the elided blocks are left out and every other block reads as stored, so a
|
|
177
|
+
* stored word is never erased. An elided answer alone then reads as an empty
|
|
178
|
+
* answer in {@link storedTextParts} and `classifyStoredRow`, as its unelided
|
|
179
|
+
* twin with an empty answer would; the rebuilt message leaves the block out.
|
|
180
|
+
*/
|
|
181
|
+
export function readStoredRowMessage(content, text) {
|
|
182
|
+
if (!isStoredMessageObject(content) || !content.content.some(isElidedAnswerBlock)) {
|
|
183
|
+
return readStoredAgMessage(content).value;
|
|
184
|
+
}
|
|
185
|
+
const stored = content.content;
|
|
186
|
+
if (!text) {
|
|
187
|
+
return readStoredAgMessage({ ...content, content: stored.filter((element) => !isElidedAnswerBlock(element)) }).value;
|
|
188
|
+
}
|
|
189
|
+
// Each elided answer opened to an empty string first, so core judges every other field of it.
|
|
190
|
+
const opened = stored.map((element) => (isElidedAnswerBlock(element) ? { ...element, text: "" } : element));
|
|
191
|
+
const read = readStoredAgMessage({ ...content, content: opened });
|
|
79
192
|
if (read.value === undefined)
|
|
80
|
-
return
|
|
81
|
-
const
|
|
82
|
-
|
|
193
|
+
return undefined;
|
|
194
|
+
const unread = new Set(read.reports.flatMap((r) => (r.path[0] === "content" && typeof r.path[1] === "number" ? [r.path[1]] : [])));
|
|
195
|
+
const isAnswer = (element, at) => !unread.has(at) && isAnswerTextElement(element) && (element.text !== "" || isElidedAnswerBlock(stored[at]));
|
|
196
|
+
const first = opened.findIndex(isAnswer);
|
|
197
|
+
const rebuilt = opened.flatMap((element, at) => {
|
|
198
|
+
if (at === first && isAnswerTextElement(element))
|
|
199
|
+
return [{ ...element, text }];
|
|
200
|
+
return isAnswer(element, at) ? [] : [element];
|
|
201
|
+
});
|
|
202
|
+
return readStoredAgMessage({ ...content, content: rebuilt }).value;
|
|
203
|
+
}
|
|
204
|
+
/** A stored value with a `content` array: the shape a stored `AgMessage` has, before core reads it. */
|
|
205
|
+
function isStoredMessageObject(value) {
|
|
206
|
+
return value !== null && typeof value === "object" && "content" in value && Array.isArray(value.content);
|
|
207
|
+
}
|
|
208
|
+
/** A stored element shaped as a text block that is not interim narration. */
|
|
209
|
+
function isNonNarrationTextElement(element) {
|
|
210
|
+
if (element === null || typeof element !== "object" || Array.isArray(element))
|
|
211
|
+
return false;
|
|
212
|
+
if (!("type" in element) || element.type !== "text" || !("text" in element))
|
|
213
|
+
return false;
|
|
214
|
+
return !("phase" in element && element.phase === "interim");
|
|
215
|
+
}
|
|
216
|
+
/** A stored answer block that carries its words: a non-narration text element whose `text` is a string. */
|
|
217
|
+
function isAnswerTextElement(element) {
|
|
218
|
+
return isNonNarrationTextElement(element) && typeof element.text === "string";
|
|
83
219
|
}
|
|
84
220
|
export function agMessageToRow(msg, ctx) {
|
|
85
221
|
const text = messageText(msg);
|
|
@@ -109,6 +245,241 @@ export function agMessageToRow(msg, ctx) {
|
|
|
109
245
|
...(ctx.untrustedOrigin === true ? { untrustedOrigin: true } : {}),
|
|
110
246
|
};
|
|
111
247
|
}
|
|
248
|
+
/**
|
|
249
|
+
* The key of the marker a stored value is replaced by when the row would not
|
|
250
|
+
* fit its store's {@link RowBudget}: `{ "com.guuey/elided": { bytes } }`, where
|
|
251
|
+
* `bytes` is the UTF-8 length of the elided value's JSON. It stands in for the
|
|
252
|
+
* value whole (a tool-call `input`, its `providerMetadata.wireInput`, or a
|
|
253
|
+
* tool-result `uiData` the turn's card row also carries), so the block keeps
|
|
254
|
+
* an object where an object was.
|
|
255
|
+
*/
|
|
256
|
+
export const ELIDED_VALUE_KEY = "com.guuey/elided";
|
|
257
|
+
/**
|
|
258
|
+
* A row cannot fit its store's {@link RowBudget}, even with every value it
|
|
259
|
+
* carries that may be elided elided. Counts only, never row content.
|
|
260
|
+
*/
|
|
261
|
+
export class StoredRowTooLargeError extends Error {
|
|
262
|
+
bytes;
|
|
263
|
+
maxBytes;
|
|
264
|
+
rowKind;
|
|
265
|
+
index;
|
|
266
|
+
name = "StoredRowTooLargeError";
|
|
267
|
+
constructor(bytes, maxBytes, rowKind,
|
|
268
|
+
/** The row's index among the turn's message or card rows (0 for an append). */
|
|
269
|
+
index) {
|
|
270
|
+
super(`stored ${rowKind} row ${index} is ${bytes} bytes; the store's row budget is ${maxBytes}`);
|
|
271
|
+
this.bytes = bytes;
|
|
272
|
+
this.maxBytes = maxBytes;
|
|
273
|
+
this.rowKind = rowKind;
|
|
274
|
+
this.index = index;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* A row carries a number its store cannot hold as written (see
|
|
279
|
+
* {@link RowBudget.numberFits}) outside any tool-call value that could be
|
|
280
|
+
* elided. Never names the number: it may be part of a tool's input or result.
|
|
281
|
+
*/
|
|
282
|
+
export class StoredRowUnstorableNumberError extends Error {
|
|
283
|
+
rowKind;
|
|
284
|
+
index;
|
|
285
|
+
name = "StoredRowUnstorableNumberError";
|
|
286
|
+
constructor(rowKind,
|
|
287
|
+
/** The row's index among the turn's message or card rows (0 for an append). */
|
|
288
|
+
index) {
|
|
289
|
+
super(`stored ${rowKind} row ${index} carries a number its store cannot hold`);
|
|
290
|
+
this.rowKind = rowKind;
|
|
291
|
+
this.index = index;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
export function isStoredRowRefusal(err) {
|
|
295
|
+
return err instanceof StoredRowTooLargeError || err instanceof StoredRowUnstorableNumberError;
|
|
296
|
+
}
|
|
297
|
+
/** Whether `value` (any JSON-shaped data) holds a number `fits` refuses, at any depth. */
|
|
298
|
+
export function carriesUnfitNumber(value, fits) {
|
|
299
|
+
if (typeof value === "number")
|
|
300
|
+
return !fits(value);
|
|
301
|
+
if (value === null || typeof value !== "object")
|
|
302
|
+
return false;
|
|
303
|
+
const children = Array.isArray(value) ? value : Object.values(value);
|
|
304
|
+
return children.some((child) => carriesUnfitNumber(child, fits));
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Throw when `row` cannot be stored under `budget`: {@link StoredRowUnstorableNumberError}
|
|
308
|
+
* when it carries a number the store cannot hold, {@link StoredRowTooLargeError}
|
|
309
|
+
* when it measures over the budget. No-op without a budget.
|
|
310
|
+
*/
|
|
311
|
+
export function assertRowFits(row, budget, rowKind, index) {
|
|
312
|
+
if (budget === undefined)
|
|
313
|
+
return;
|
|
314
|
+
if (budget.numberFits !== undefined && carriesUnfitNumber(row, budget.numberFits)) {
|
|
315
|
+
throw new StoredRowUnstorableNumberError(rowKind, index);
|
|
316
|
+
}
|
|
317
|
+
const bytes = budget.bytesOf(row);
|
|
318
|
+
if (bytes > budget.maxBytes)
|
|
319
|
+
throw new StoredRowTooLargeError(bytes, budget.maxBytes, rowKind, index);
|
|
320
|
+
}
|
|
321
|
+
function jsonBytes(value) {
|
|
322
|
+
return Buffer.byteLength(JSON.stringify(value), "utf8");
|
|
323
|
+
}
|
|
324
|
+
function elidedMarker(bytes) {
|
|
325
|
+
return { [ELIDED_VALUE_KEY]: { bytes } };
|
|
326
|
+
}
|
|
327
|
+
function withElided(block, field, bytes) {
|
|
328
|
+
if (field === "input")
|
|
329
|
+
return { ...block, input: elidedMarker(bytes) };
|
|
330
|
+
return {
|
|
331
|
+
...block,
|
|
332
|
+
providerMetadata: AgProviderMeta.parse({ ...block.providerMetadata, wireInput: elidedMarker(bytes) }),
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
/** The tool-call value `field` of `block`, when it has one. */
|
|
336
|
+
function toolCallValue(block, field) {
|
|
337
|
+
return field === "input" ? block.input : block.providerMetadata?.["wireInput"];
|
|
338
|
+
}
|
|
339
|
+
function isElidedMarker(value) {
|
|
340
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) && ELIDED_VALUE_KEY in value;
|
|
341
|
+
}
|
|
342
|
+
/** {@link CardUiDataByCallId} over a turn's card rows (rows of another kind are skipped). */
|
|
343
|
+
export function cardUiDataByCallId(cardRows) {
|
|
344
|
+
const byCallId = new Map();
|
|
345
|
+
for (const row of cardRows) {
|
|
346
|
+
if (row.kind !== "card" || row.toolName === undefined)
|
|
347
|
+
continue;
|
|
348
|
+
const art = cardRowToAgArtifact(row);
|
|
349
|
+
if (art === undefined)
|
|
350
|
+
continue;
|
|
351
|
+
for (const part of art.parts) {
|
|
352
|
+
if (part.type !== "tool-result" || part.uiData === undefined)
|
|
353
|
+
continue;
|
|
354
|
+
const entry = { name: row.toolName, uiData: part.uiData };
|
|
355
|
+
const known = byCallId.get(part.toolCallId);
|
|
356
|
+
if (known === undefined)
|
|
357
|
+
byCallId.set(part.toolCallId, [entry]);
|
|
358
|
+
else
|
|
359
|
+
known.push(entry);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return byCallId;
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* The card row's copy of a tool-result block's `uiData`, when the block's
|
|
366
|
+
* `uiData` is an inline UI resource with text (`{ uri, text }`, bare or as
|
|
367
|
+
* `{ resource: {...} }`) and a card row of the turn carries the same value for
|
|
368
|
+
* the same call. Only then is the message row's copy a pure duplicate.
|
|
369
|
+
*/
|
|
370
|
+
function cardCopyOf(block, cards) {
|
|
371
|
+
const uiData = block.uiData;
|
|
372
|
+
if (uiData === undefined || isElidedMarker(uiData) || asUiResource(uiData)?.text === undefined)
|
|
373
|
+
return undefined;
|
|
374
|
+
return cards.get(block.toolCallId)?.find((card) => jsonEqual(card.uiData, uiData));
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* A stored ANSWER block left out because the row's `text` carries the same
|
|
378
|
+
* words: a `text` block whose `text` is the {@link ELIDED_VALUE_KEY} marker
|
|
379
|
+
* (`{ "com.guuey/elided": { bytes } }`, `bytes` the UTF-8 length of the
|
|
380
|
+
* elided string's JSON) in place of the string. A narration block
|
|
381
|
+
* (`phase: "interim"`) is never one: the row's `text` is the answer only, so
|
|
382
|
+
* it holds no narration to read back. Read on the raw stored element, since
|
|
383
|
+
* core's lenient reader leaves such a block out (its `text` is not a string).
|
|
384
|
+
*/
|
|
385
|
+
export function isElidedAnswerBlock(element) {
|
|
386
|
+
return isNonNarrationTextElement(element) && isElidedMarker(element.text);
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* {@link agMessageToRow}, bounded by the store's {@link RowBudget}. Under the
|
|
390
|
+
* budget (or with none) the row is exactly `agMessageToRow(msg, ctx)`.
|
|
391
|
+
*
|
|
392
|
+
* First, when the budget declares `numberFits`, every tool-call value that
|
|
393
|
+
* carries a number the store cannot hold is replaced by the
|
|
394
|
+
* {@link ELIDED_VALUE_KEY} marker (`reason: "number"`; after `JSON.parse` such
|
|
395
|
+
* a number has already lost its digits, so storing it would store a wrong
|
|
396
|
+
* one). A row that still carries one elsewhere throws
|
|
397
|
+
* {@link StoredRowUnstorableNumberError}.
|
|
398
|
+
*
|
|
399
|
+
* Then, over the size budget, values are replaced one at a time
|
|
400
|
+
* (`reason: "size"`) until the row fits. Pure duplicates go first: a
|
|
401
|
+
* tool-result's `uiData` that is an inline UI resource with text, when a card
|
|
402
|
+
* row of the turn (`cards`, see {@link cardUiDataByCallId}) carries the same
|
|
403
|
+
* value for the same call. Then every `providerMetadata.wireInput` (a second
|
|
404
|
+
* copy of the input, carried only when it differs), then every `input`.
|
|
405
|
+
* Within each tier the largest goes first, ties in block order. A row that
|
|
406
|
+
* still does not fit throws {@link StoredRowTooLargeError}.
|
|
407
|
+
*/
|
|
408
|
+
export function agMessageToBoundedRow(msg, ctx, budget, index, cards = new Map()) {
|
|
409
|
+
let row = agMessageToRow(msg, ctx);
|
|
410
|
+
if (budget === undefined)
|
|
411
|
+
return { row, elided: [] };
|
|
412
|
+
const content = [...msg.content];
|
|
413
|
+
const elided = [];
|
|
414
|
+
const elide = (at, field, reason) => {
|
|
415
|
+
const block = content[at];
|
|
416
|
+
if (block === undefined)
|
|
417
|
+
return;
|
|
418
|
+
if (block.type === "tool-result") {
|
|
419
|
+
const card = field === "uiData" ? cardCopyOf(block, cards) : undefined;
|
|
420
|
+
if (card === undefined)
|
|
421
|
+
return;
|
|
422
|
+
const bytes = jsonBytes(card.uiData);
|
|
423
|
+
content[at] = { ...block, uiData: elidedMarker(bytes) };
|
|
424
|
+
elided.push({ toolCallId: block.toolCallId, name: card.name, field, reason, bytes });
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
if (block.type !== "tool-call" || field === "uiData")
|
|
428
|
+
return;
|
|
429
|
+
const value = toolCallValue(block, field);
|
|
430
|
+
if (value === undefined)
|
|
431
|
+
return;
|
|
432
|
+
const bytes = jsonBytes(value);
|
|
433
|
+
content[at] = withElided(block, field, bytes);
|
|
434
|
+
elided.push({ toolCallId: block.toolCallId, name: block.name, field, reason, bytes });
|
|
435
|
+
};
|
|
436
|
+
const fits = budget.numberFits;
|
|
437
|
+
if (fits !== undefined && carriesUnfitNumber(row, fits)) {
|
|
438
|
+
msg.content.forEach((block, at) => {
|
|
439
|
+
if (block.type !== "tool-call")
|
|
440
|
+
return;
|
|
441
|
+
for (const field of ["wireInput", "input"]) {
|
|
442
|
+
const current = content[at];
|
|
443
|
+
if (current === undefined || current.type !== "tool-call")
|
|
444
|
+
continue;
|
|
445
|
+
if (carriesUnfitNumber(toolCallValue(current, field), fits))
|
|
446
|
+
elide(at, field, "number");
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
row = agMessageToRow({ ...msg, content }, ctx);
|
|
450
|
+
if (carriesUnfitNumber(row, fits))
|
|
451
|
+
throw new StoredRowUnstorableNumberError("message", index);
|
|
452
|
+
}
|
|
453
|
+
let bytes = budget.bytesOf(row);
|
|
454
|
+
if (bytes <= budget.maxBytes)
|
|
455
|
+
return { row, elided };
|
|
456
|
+
const candidates = [];
|
|
457
|
+
content.forEach((block, at) => {
|
|
458
|
+
if (block.type === "tool-result") {
|
|
459
|
+
const card = cardCopyOf(block, cards);
|
|
460
|
+
if (card !== undefined)
|
|
461
|
+
candidates.push({ at, field: "uiData", bytes: jsonBytes(card.uiData), tier: 0 });
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
if (block.type !== "tool-call")
|
|
465
|
+
return;
|
|
466
|
+
const wire = toolCallValue(block, "wireInput");
|
|
467
|
+
if (wire !== undefined && !isElidedMarker(wire)) {
|
|
468
|
+
candidates.push({ at, field: "wireInput", bytes: jsonBytes(wire), tier: 1 });
|
|
469
|
+
}
|
|
470
|
+
if (!isElidedMarker(block.input))
|
|
471
|
+
candidates.push({ at, field: "input", bytes: jsonBytes(block.input), tier: 2 });
|
|
472
|
+
});
|
|
473
|
+
candidates.sort((a, b) => a.tier - b.tier || b.bytes - a.bytes || a.at - b.at);
|
|
474
|
+
for (const c of candidates) {
|
|
475
|
+
elide(c.at, c.field, "size");
|
|
476
|
+
row = agMessageToRow({ ...msg, content }, ctx);
|
|
477
|
+
bytes = budget.bytesOf(row);
|
|
478
|
+
if (bytes <= budget.maxBytes)
|
|
479
|
+
return { row, elided };
|
|
480
|
+
}
|
|
481
|
+
throw new StoredRowTooLargeError(bytes, budget.maxBytes, "message", index);
|
|
482
|
+
}
|
|
112
483
|
/**
|
|
113
484
|
* A turn's `displayRequired` is content a model provider requires the host to
|
|
114
485
|
* show to the person who asked, under that provider's terms, and those terms
|
|
@@ -169,6 +540,55 @@ function withoutToolResultMeta(block) {
|
|
|
169
540
|
const { _meta: _dropped, ...rest } = block;
|
|
170
541
|
return rest;
|
|
171
542
|
}
|
|
543
|
+
/**
|
|
544
|
+
* Whether a tool-result block carries a UI resource, and so becomes a card row
|
|
545
|
+
* ({@link uiCardArtifactsFromMessages}) rather than a stored tool result
|
|
546
|
+
* ({@link storedToolResults}). The one predicate both share, so a result is a
|
|
547
|
+
* card or a stored tool result, never both and never neither.
|
|
548
|
+
*
|
|
549
|
+
* It judges the block AS STORED: without its `_meta` (guuey#122), which never
|
|
550
|
+
* persists. The card projection sees the live block and the stored-row reader
|
|
551
|
+
* sees the stored one, so a predicate that read `_meta` would answer the two
|
|
552
|
+
* differently (a `_meta.ui` locator is visible to the first only). Judging the
|
|
553
|
+
* stored view gives one answer on both sides by construction.
|
|
554
|
+
*
|
|
555
|
+
* A block carries UI only on DURABLE evidence, what a card row needs to mount
|
|
556
|
+
* after a reload. As stored:
|
|
557
|
+
* - its `uiData` is an MCP resource payload (`{ uri, text|blob }`, bare or as
|
|
558
|
+
* `{ resource }`), which mounts inline;
|
|
559
|
+
* - its `uiData` or `structuredContent` names a `ui://` locator, which mounts
|
|
560
|
+
* by a fresh read of the uri (`structuredContent` for a producer that
|
|
561
|
+
* withholds `_meta`, guuey#209);
|
|
562
|
+
* - its `uiData` is the {@link ELIDED_VALUE_KEY} marker, which stands in for
|
|
563
|
+
* an inline resource the turn's card row carries whole
|
|
564
|
+
* ({@link agMessageToBoundedRow});
|
|
565
|
+
* - or a `provider-raw` part carries a `ui://` resource.
|
|
566
|
+
*
|
|
567
|
+
* Anything else is a stored tool result, which a host reads by its tool name.
|
|
568
|
+
* That includes plain view data in `uiData`: AgJSON §2.1 routes a result's
|
|
569
|
+
* payload there whenever the producer set any `_meta.ui` sibling, so an
|
|
570
|
+
* object in `uiData` alone is no evidence that a card can mount. When
|
|
571
|
+
* `_meta.ui` named no `ui://` locator, nothing could ever mount it; when it
|
|
572
|
+
* did, the locator rode `_meta` alone and the writer strips it, so the stored
|
|
573
|
+
* row cannot mount it either. A card row for either would draw as an expired
|
|
574
|
+
* chip on every reload. (A writer that persisted that locator would make it a
|
|
575
|
+
* card here, by this same rule.)
|
|
576
|
+
*
|
|
577
|
+
* `_meta` alone never mints a card (the guuey#170 pin). Across a rolling
|
|
578
|
+
* release: a writer before this predicate minted a card row from a `_meta.ui`
|
|
579
|
+
* locator, so a thread written then holds that card row while this predicate
|
|
580
|
+
* reads the same stored result as one with no UI resource. A reader that
|
|
581
|
+
* serves both kinds keeps one item per result by leaving out a stored tool
|
|
582
|
+
* result whose call already has a card row on its read (the public transcript
|
|
583
|
+
* read does).
|
|
584
|
+
*/
|
|
585
|
+
export function toolResultCarriesUi(block) {
|
|
586
|
+
const stored = withoutToolResultMeta(block);
|
|
587
|
+
return (asUiResource(stored.uiData) !== undefined ||
|
|
588
|
+
isElidedMarker(stored.uiData) ||
|
|
589
|
+
toolResultLocator(stored) !== undefined ||
|
|
590
|
+
stored.content.some((part) => part.type === "provider-raw" && scanProviderRawForUiResource(part.raw) !== undefined));
|
|
591
|
+
}
|
|
172
592
|
export function uiCardArtifactsFromMessages(messages) {
|
|
173
593
|
const artifacts = [];
|
|
174
594
|
for (const msg of messages) {
|
|
@@ -187,11 +607,10 @@ export function uiCardArtifactsFromMessages(messages) {
|
|
|
187
607
|
// read the render's own locator would never mint a placeholder row and
|
|
188
608
|
// the read plane 404s it. Mount MATERIAL still never rides `_meta` here
|
|
189
609
|
// (see the metaOnly pin in fold-rows.test.ts): a bare locator persists
|
|
190
|
-
// as a placeholder whose mount is a re-fetch.
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
if (!carriesUi)
|
|
610
|
+
// as a placeholder whose mount is a re-fetch. A locator named ONLY in
|
|
611
|
+
// `_meta.ui` mints nothing: it does not persist, so its card could not
|
|
612
|
+
// mount after a reload (toolResultCarriesUi judges durable evidence).
|
|
613
|
+
if (!toolResultCarriesUi(block))
|
|
195
614
|
continue;
|
|
196
615
|
artifacts.push({
|
|
197
616
|
artifactId: `${msg.id}#ui#${i}`,
|
|
@@ -238,6 +657,40 @@ export function producingToolName(art, toolNames) {
|
|
|
238
657
|
}
|
|
239
658
|
return undefined;
|
|
240
659
|
}
|
|
660
|
+
/**
|
|
661
|
+
* toolCallId → the index of the fold message that carries that call's
|
|
662
|
+
* `tool-result` block (guuey#2396). A card is drawn when its tool-result
|
|
663
|
+
* arrives, so this is where its card row belongs in the turn's stored order.
|
|
664
|
+
* First message wins on a duplicate id, as in {@link toolNamesByCallId}.
|
|
665
|
+
*/
|
|
666
|
+
export function toolResultMessageIndexByCallId(messages) {
|
|
667
|
+
const indices = new Map();
|
|
668
|
+
messages.forEach((msg, index) => {
|
|
669
|
+
for (const block of msg.content) {
|
|
670
|
+
if (block.type === "tool-result" && !indices.has(block.toolCallId)) {
|
|
671
|
+
indices.set(block.toolCallId, index);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
});
|
|
675
|
+
return indices;
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* The index of the fold message a card artifact was drawn with: the message
|
|
679
|
+
* holding the tool-result its first tool-result part names (guuey#2396).
|
|
680
|
+
* `undefined` when unresolvable (a first-class `artifact.*` event with no
|
|
681
|
+
* tool-result part, or a call outside this fold): such a card keeps the
|
|
682
|
+
* turn's tail.
|
|
683
|
+
*/
|
|
684
|
+
export function producingMessageIndex(art, resultIndices) {
|
|
685
|
+
for (const part of art.parts) {
|
|
686
|
+
if (isJsonObjectLike(part) && part.type === "tool-result" && typeof part.toolCallId === "string") {
|
|
687
|
+
const index = resultIndices.get(part.toolCallId);
|
|
688
|
+
if (index !== undefined)
|
|
689
|
+
return index;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
return undefined;
|
|
693
|
+
}
|
|
241
694
|
export function agArtifactToCardRow(art, ctx, toolName) {
|
|
242
695
|
// guuey#122: the projection lane strips tool-result _meta upstream, but
|
|
243
696
|
// REAL artifact.* events land here verbatim — apply the same persistence
|
|
@@ -278,12 +731,13 @@ function isJsonObjectLike(v) {
|
|
|
278
731
|
* undefined value in a closed set, is omitted whole and the rest restores in
|
|
279
732
|
* order; unknown fields pass through untouched. Only a record the reader
|
|
280
733
|
* cannot materialize (no string `id`, an undefined `role`, no `content`
|
|
281
|
-
* array) falls to the text projection.
|
|
734
|
+
* array) falls to the text projection. A stored message whose answer block is
|
|
735
|
+
* elided reads its answer from the row's `text` ({@link readStoredRowMessage}).
|
|
282
736
|
*/
|
|
283
737
|
export function rowToAgMessage(row) {
|
|
284
|
-
const stored =
|
|
285
|
-
if (stored
|
|
286
|
-
return stored
|
|
738
|
+
const stored = readStoredRowMessage(row.content, row.text);
|
|
739
|
+
if (stored !== undefined)
|
|
740
|
+
return stored;
|
|
287
741
|
const role = row.authorRole === "user" ? "user" : row.authorRole === "system" ? "system" : "assistant";
|
|
288
742
|
const text = row.text ?? "";
|
|
289
743
|
return {
|
|
@@ -389,7 +843,7 @@ export function joinStoredThreadMemory(row) {
|
|
|
389
843
|
return [...(row.carriedThreadMemory ?? []), ...row.threadMemory];
|
|
390
844
|
}
|
|
391
845
|
/** Structural equality over JSON values (object key order does not matter). */
|
|
392
|
-
function jsonEqual(a, b) {
|
|
846
|
+
export function jsonEqual(a, b) {
|
|
393
847
|
if (a === b)
|
|
394
848
|
return true;
|
|
395
849
|
if (a === undefined || b === undefined || a === null || b === null)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { type ThreadMessageRow, type ThreadRow, type ThreadSnapshotRow } from "./rows.js";
|
|
2
|
+
import type { ThreadScope } from "./http.js";
|
|
3
|
+
/**
|
|
4
|
+
* A JSON object as it came off the wire: every member is unread until narrowed.
|
|
5
|
+
*
|
|
6
|
+
* `@guuey/agent-client` keeps the same guard (`wire-object.ts`). It is not
|
|
7
|
+
* shared because no package both depend on exports one that reads an
|
|
8
|
+
* `unknown`: `@guuey/mcp-apps-host`'s `isJsonObject` takes a `JsonValue` and
|
|
9
|
+
* narrows its members to `JsonValue`, which a parsed body is not proven to be,
|
|
10
|
+
* and the two packages do not depend on each other. The guard is one line and
|
|
11
|
+
* every member it yields is narrowed at its read.
|
|
12
|
+
*/
|
|
13
|
+
export interface WireObject {
|
|
14
|
+
readonly [key: string]: unknown;
|
|
15
|
+
}
|
|
16
|
+
/** Whether `value` is a JSON object: not `null`, not an array, not a scalar. */
|
|
17
|
+
export declare function isWireObject(value: unknown): value is WireObject;
|
|
18
|
+
/** `scope`'s result: the three strings the server binds the token to. */
|
|
19
|
+
export declare function readThreadScope(value: unknown): ThreadScope | undefined;
|
|
20
|
+
/** `incrementSeq`'s result: the integer seq the server allocated. */
|
|
21
|
+
export declare function readAllocatedSeq(value: unknown): number | undefined;
|
|
22
|
+
/** A `getThread` row. */
|
|
23
|
+
export declare function readThreadRow(value: unknown): ThreadRow | undefined;
|
|
24
|
+
/**
|
|
25
|
+
* A message row as read off the wire: a legacy row may carry no `userId`, which
|
|
26
|
+
* the binding fills in (`HttpThreadPersistence`) before the row leaves it.
|
|
27
|
+
*/
|
|
28
|
+
export type WireThreadMessageRow = Omit<ThreadMessageRow, "userId"> & {
|
|
29
|
+
userId?: string;
|
|
30
|
+
};
|
|
31
|
+
/** A message row (`listRecentMessages`, `findByClientMessageId`). */
|
|
32
|
+
export declare function readThreadMessageRow(value: unknown): WireThreadMessageRow | undefined;
|
|
33
|
+
/**
|
|
34
|
+
* `listRecentMessages`' result: every row must read, or the list is refused. A
|
|
35
|
+
* row reads unless it lacks a member that identifies it or carries a malformed
|
|
36
|
+
* `untrustedOrigin` (see the module doc); a vocabulary value or a `userId` this
|
|
37
|
+
* release does not know never refuses it.
|
|
38
|
+
*/
|
|
39
|
+
export declare function readThreadMessageRows(value: unknown): WireThreadMessageRow[] | undefined;
|
|
40
|
+
/**
|
|
41
|
+
* A `getSnapshot` row. Its memory arrays are JSON (checked, never assumed); the
|
|
42
|
+
* thread records are re-read through the one stored-memory reader
|
|
43
|
+
* (`splitStoredThreadMemory`), and any element this release cannot read as a
|
|
44
|
+
* thread record joins the carried elements, so it is kept, never dropped.
|
|
45
|
+
*/
|
|
46
|
+
export declare function readThreadSnapshotRow(value: unknown): ThreadSnapshotRow | undefined;
|
|
47
|
+
//# sourceMappingURL=http-read.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http-read.d.ts","sourceRoot":"","sources":["../src/http-read.ts"],"names":[],"mappings":"AAwCA,OAAO,EAQL,KAAK,gBAAgB,EACrB,KAAK,SAAS,EACd,KAAK,iBAAiB,EAEvB,MAAM,WAAW,CAAC;AACnB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAE7C;;;;;;;;;GASG;AACH,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACjC;AAED,gFAAgF;AAChF,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,UAAU,CAEhE;AA0BD,yEAAyE;AACzE,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,WAAW,GAAG,SAAS,CAKvE;AAED,qEAAqE;AACrE,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAEnE;AAED,yBAAyB;AACzB,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,SAAS,GAAG,SAAS,CAmDnE;AAsCD;;;GAGG;AACH,MAAM,MAAM,oBAAoB,GAAG,IAAI,CAAC,gBAAgB,EAAE,QAAQ,CAAC,GAAG;IAAE,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAE1F,qEAAqE;AACrE,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,oBAAoB,GAAG,SAAS,CA0CrF;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,GAAG,oBAAoB,EAAE,GAAG,SAAS,CASxF;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,GAAG,iBAAiB,GAAG,SAAS,CA8BnF"}
|