akanjs 3.0.0-beta.7 → 3.0.0-beta.8
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/common/index.ts +1 -0
- package/common/pathGet.ts +12 -2
- package/common/pathSet.ts +2 -3
- package/common/toPathSegments.ts +10 -0
- package/dictionary/base.dictionary.ts +5 -0
- package/index.ts +5 -0
- package/local/apps/serverLifecycle/serverLifecycle-local.db-shm +0 -0
- package/local/apps/serverLifecycle/serverLifecycle-local_solid.db-shm +0 -0
- package/package.json +1 -1
- package/service/agent.service.ts +92 -1
- package/service/predefinedAdaptor/llm.adaptor.ts +17 -0
- package/store/agentic/index.ts +1 -0
- package/store/agentic/useAgentReference.ts +49 -0
- package/store/hooks.ts +1 -1
- package/types/common/index.d.ts +1 -0
- package/types/common/toPathSegments.d.ts +9 -0
- package/types/dictionary/base.dictionary.d.ts +1 -1
- package/types/dictionary/dictionary.d.ts +8 -8
- package/types/index.d.ts +5 -0
- package/types/service/agent.service.d.ts +54 -0
- package/types/service/predefinedAdaptor/llm.adaptor.d.ts +16 -0
- package/types/store/agentic/index.d.ts +1 -0
- package/types/store/agentic/useAgentReference.d.ts +32 -0
- package/types/store/hooks.d.ts +1 -1
- package/types/ui/Agent/Chat.d.ts +11 -1
- package/types/ui/Agent/Composer.d.ts +9 -2
- package/types/ui/Agent/Menu.d.ts +4 -2
- package/types/ui/Agent/Refer.d.ts +13 -0
- package/types/ui/Agent/useChatQueue.d.ts +3 -1
- package/types/ui/Agent/useChatReferences.d.ts +26 -0
- package/types/ui/Agent/useReferenceMenu.d.ts +43 -0
- package/types/ui/Field/lightRefCache.d.ts +19 -0
- package/types/ui/Field/useRelationOptions.d.ts +39 -0
- package/types/ui/Select.d.ts +2 -0
- package/types/ui/index.d.ts +4 -1
- package/types/vendor/use-agentic/AgentSession.d.ts +54 -1
- package/types/vendor/use-agentic/Reference.d.ts +56 -0
- package/types/vendor/use-agentic/index.d.ts +1 -0
- package/types/vendor/use-agentic/types.d.ts +36 -6
- package/ui/Agent/Bubble.tsx +2 -0
- package/ui/Agent/Chat.tsx +38 -8
- package/ui/Agent/Composer.tsx +18 -1
- package/ui/Agent/Menu.tsx +8 -3
- package/ui/Agent/Queued.tsx +2 -0
- package/ui/Agent/Refer.tsx +44 -0
- package/ui/Agent/sessionHistory.ts +35 -12
- package/ui/Agent/useChatQueue.ts +11 -1
- package/ui/Agent/useChatReferences.ts +67 -0
- package/ui/Agent/useReferenceMenu.ts +108 -0
- package/ui/Field/Relation.tsx +70 -150
- package/ui/Field/lightRefCache.ts +73 -0
- package/ui/Field/useRelationOptions.ts +106 -0
- package/ui/Select.tsx +24 -14
- package/ui/index.ts +8 -0
- package/vendor/use-agentic/AgentSession.ts +127 -1
- package/vendor/use-agentic/Compaction.ts +6 -0
- package/vendor/use-agentic/Reference.ts +99 -0
- package/vendor/use-agentic/Transcript.ts +7 -1
- package/vendor/use-agentic/index.ts +1 -0
- package/vendor/use-agentic/types.ts +38 -7
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AgentProgressReport } from "./AgentProgress";
|
|
2
2
|
import { Compaction, type CompactOptions } from "./Compaction";
|
|
3
|
+
import { Reference } from "./Reference";
|
|
3
4
|
import { ToolOutput } from "./ToolOutput";
|
|
4
5
|
import { type ToolApprovalRequest, ToolRunner } from "./ToolRunner";
|
|
5
6
|
import { Transcript } from "./Transcript";
|
|
@@ -7,6 +8,7 @@ import type {
|
|
|
7
8
|
AgentRunner,
|
|
8
9
|
ChatMessage,
|
|
9
10
|
ContextBlock,
|
|
11
|
+
MessageReference,
|
|
10
12
|
PublishedTool,
|
|
11
13
|
RunnerRequest,
|
|
12
14
|
SurfaceView,
|
|
@@ -132,6 +134,9 @@ export class AgentSession {
|
|
|
132
134
|
#running = false;
|
|
133
135
|
#pending: PendingApproval | null = null;
|
|
134
136
|
#question: PendingQuestion | null = null;
|
|
137
|
+
#staged: MessageReference[] = [];
|
|
138
|
+
#inserts: MessageReference[] = [];
|
|
139
|
+
#chats = 0;
|
|
135
140
|
#progress: (AgentProgressReport & { callId: string }) | null = null;
|
|
136
141
|
#controller: AbortController | null = null;
|
|
137
142
|
#active: Promise<void> | null = null;
|
|
@@ -212,6 +217,117 @@ export class AgentSession {
|
|
|
212
217
|
return this.#question;
|
|
213
218
|
}
|
|
214
219
|
|
|
220
|
+
/**
|
|
221
|
+
* What the user has pointed at and not yet sent.
|
|
222
|
+
*
|
|
223
|
+
* Held by the session rather than by the composer, which is where staged *files* live — and the difference is
|
|
224
|
+
* not an inconsistency. A file only ever arrives from the composer's own picker or drop zone, so composer-local
|
|
225
|
+
* state can reach every producer of one. A reference arrives from whichever component drew the data: a card
|
|
226
|
+
* partway down the page, reaching the session it is already inside. Composer state is unreachable from there,
|
|
227
|
+
* and a chat that replaces its composer has no state to reach anyway.
|
|
228
|
+
*
|
|
229
|
+
* A staging slot, not a tray. `send` empties it, so what somebody pointed at belongs to the message they were
|
|
230
|
+
* writing and never leaks into the next one.
|
|
231
|
+
*/
|
|
232
|
+
get staged(): readonly MessageReference[] {
|
|
233
|
+
return this.#staged;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Pointing at the same field twice replaces it: the newer value is the one they meant, and one chip is honest.
|
|
238
|
+
*
|
|
239
|
+
* Stages the value only. The caller is the composer's own `@` menu, which is already writing the token as the
|
|
240
|
+
* user picks — `refer` is the entry point for everything that is not the composer.
|
|
241
|
+
*/
|
|
242
|
+
stage = (reference: MessageReference) => {
|
|
243
|
+
this.#stage(Reference.clipped(reference));
|
|
244
|
+
this.#announce();
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Points at something from a component that is not the composer — a card the user clicked beside the data.
|
|
249
|
+
*
|
|
250
|
+
* Two halves, because the composer owns one of them: the value is staged here, and the token the message needs
|
|
251
|
+
* is left for whichever chat is rendering this session's draft to write. That is why nothing is queued when no
|
|
252
|
+
* chat is: the value would sit staged with no token anywhere naming it, and the message text is what decides
|
|
253
|
+
* which references a turn carries — so the user would press a button and watch nothing happen, which is the
|
|
254
|
+
* failure this warns about instead.
|
|
255
|
+
*/
|
|
256
|
+
refer = (reference: MessageReference) => {
|
|
257
|
+
const clipped = Reference.clipped(reference);
|
|
258
|
+
this.#stage(clipped);
|
|
259
|
+
if (this.#chats) this.#inserts = [...this.#inserts.filter((one) => !Reference.same(one, clipped)), clipped];
|
|
260
|
+
else
|
|
261
|
+
console.warn(
|
|
262
|
+
`No chat is rendering this agent session, so "${clipped.label}" was staged with no token written for it. ` +
|
|
263
|
+
"The component calling this and an <Agent.Chat /> have to share one session — check which <Agent.Zone> " +
|
|
264
|
+
"each of them is inside.",
|
|
265
|
+
);
|
|
266
|
+
this.#announce();
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Tokens a chat has not written into its draft yet. State rather than a queue somebody drains, for the reason
|
|
271
|
+
* `pendingApproval` is state: two chats may be mounted on one session — a responsive app renders a desktop and
|
|
272
|
+
* a mobile composer and hides one in CSS — and each holds its own draft, so each has to write the token. A
|
|
273
|
+
* destructive read would hand it to whichever rendered first and leave the other silently without it.
|
|
274
|
+
*/
|
|
275
|
+
get pendingInserts(): readonly MessageReference[] {
|
|
276
|
+
return this.#inserts;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** Idempotent by key: the second chat to apply the same insert is acknowledging one that is already gone. */
|
|
280
|
+
insertApplied = (key: string) => {
|
|
281
|
+
const next = this.#inserts.filter((one) => Reference.keyOf(one) !== key);
|
|
282
|
+
if (next.length === this.#inserts.length) return;
|
|
283
|
+
this.#inserts = next;
|
|
284
|
+
this.#announce();
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Stages references back from a message that was parked behind a running turn. What was pointed at since wins:
|
|
289
|
+
* the parked value is the older read of the same field, and letting it land would undo an edit made in between.
|
|
290
|
+
* No token is written — the parked text carries them already, and the composer is putting that text back.
|
|
291
|
+
*/
|
|
292
|
+
restoreStaged = (references: readonly MessageReference[]) => {
|
|
293
|
+
const fresh = references.filter((one) => !this.#staged.some((held) => Reference.same(held, one)));
|
|
294
|
+
if (!fresh.length) return;
|
|
295
|
+
this.#staged = [...fresh.map((one) => Reference.clipped(one)), ...this.#staged];
|
|
296
|
+
this.#announce();
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Registered by a chat for as long as it is rendering this session's draft, so `refer` can tell the difference
|
|
301
|
+
* between a token nobody has written yet and one nobody ever will.
|
|
302
|
+
*/
|
|
303
|
+
attachChat = () => {
|
|
304
|
+
this.#chats += 1;
|
|
305
|
+
return () => {
|
|
306
|
+
this.#chats = Math.max(0, this.#chats - 1);
|
|
307
|
+
};
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
#stage(clipped: MessageReference) {
|
|
311
|
+
const at = this.#staged.findIndex((one) => Reference.same(one, clipped));
|
|
312
|
+
this.#staged =
|
|
313
|
+
at === -1 ? [...this.#staged, clipped] : this.#staged.map((one, idx) => (idx === at ? clipped : one));
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** By key, never by index: what orders the references of a message is its text, and that is not this list. */
|
|
317
|
+
unstage = (key: string) => {
|
|
318
|
+
const next = this.#staged.filter((one) => Reference.keyOf(one) !== key);
|
|
319
|
+
if (next.length === this.#staged.length) return;
|
|
320
|
+
this.#staged = next;
|
|
321
|
+
this.#announce();
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
clearStaged = () => {
|
|
325
|
+
if (!this.#staged.length && !this.#inserts.length) return;
|
|
326
|
+
this.#staged = [];
|
|
327
|
+
this.#inserts = [];
|
|
328
|
+
this.#announce();
|
|
329
|
+
};
|
|
330
|
+
|
|
215
331
|
/** What the tool running now last said about its own progress, for the row that is still spinning. */
|
|
216
332
|
get progress(): (AgentProgressReport & { callId: string }) | null {
|
|
217
333
|
return this.#progress;
|
|
@@ -315,6 +431,8 @@ export class AgentSession {
|
|
|
315
431
|
await this.#active;
|
|
316
432
|
}
|
|
317
433
|
this.#messages = [];
|
|
434
|
+
this.#staged = [];
|
|
435
|
+
this.#inserts = [];
|
|
318
436
|
this.#compactFloor = 0;
|
|
319
437
|
|
|
320
438
|
if (this.#saveTimer) {
|
|
@@ -631,9 +749,17 @@ export class AgentSession {
|
|
|
631
749
|
}
|
|
632
750
|
|
|
633
751
|
#notify() {
|
|
752
|
+
this.#announce();
|
|
753
|
+
this.#schedulePersist();
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* A change that is not the transcript's. Staged references live beside the messages rather than in them, so
|
|
758
|
+
* announcing one has to redraw the composer without scheduling a save of messages nothing touched.
|
|
759
|
+
*/
|
|
760
|
+
#announce() {
|
|
634
761
|
this.#version += 1;
|
|
635
762
|
for (const listener of this.#listeners) listener();
|
|
636
|
-
this.#schedulePersist();
|
|
637
763
|
}
|
|
638
764
|
|
|
639
765
|
/** Debounced: streaming patches the last message on every delta, and a save per delta would thrash storage. */
|
|
@@ -103,6 +103,12 @@ export class Compaction {
|
|
|
103
103
|
|
|
104
104
|
for (const attachment of message.attachments ?? [])
|
|
105
105
|
parts.push(`[attached ${attachment.name}, content not carried into this summary]`);
|
|
106
|
+
|
|
107
|
+
for (const reference of message.references ?? [])
|
|
108
|
+
parts.push(
|
|
109
|
+
`[referenced ${reference.refName}/${reference.refId}${reference.path ? `#${reference.path}` : ""}` +
|
|
110
|
+
` (${reference.label}), value not carried into this summary]`,
|
|
111
|
+
);
|
|
106
112
|
for (const call of message.toolCalls ?? [])
|
|
107
113
|
parts.push(`[called ${call.name} ${Compaction.#clip(JSON.stringify(call.args), 200)}]`);
|
|
108
114
|
for (const result of message.toolResults ?? [])
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { MessageReference } from "./types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The ceiling on what one reference may add to the transcript, and the identity two of them are the same by.
|
|
5
|
+
*
|
|
6
|
+
* A reference is bulkier than it looks and lasts longer than a tool result. The host hands over whatever the
|
|
7
|
+
* screen was already holding — a document with an array of rows in it is tens of kilobytes — and unlike a tool
|
|
8
|
+
* result, which answers one turn's question, a reference is part of a *user* message: it rides the wire on this
|
|
9
|
+
* turn and on every turn after it, and it is the last thing compaction folds, because folding the thing the user
|
|
10
|
+
* pointed at is folding the question.
|
|
11
|
+
*
|
|
12
|
+
* So the value is bounded where it is staged, before it ever enters a message. Clipped rather than dropped, and
|
|
13
|
+
* the note says which: a model shown a value it cannot see the end of asks a narrower question, where one shown
|
|
14
|
+
* nothing answers from the field names.
|
|
15
|
+
*/
|
|
16
|
+
export class Reference {
|
|
17
|
+
/**
|
|
18
|
+
* Characters. The same number as `ToolOutput.limit` and for the same reason — far below any provider's window,
|
|
19
|
+
* comfortably above what one record needs — but its own constant, because the two bound different things and a
|
|
20
|
+
* host that finds one too generous has no reason to have found the other so.
|
|
21
|
+
*/
|
|
22
|
+
static readonly limit = 20_000;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* What the token in the message text spells, so the text is the index into the values and neither can drift
|
|
26
|
+
* from the other. A path is part of the identity: two fields of one document are two references.
|
|
27
|
+
*/
|
|
28
|
+
static keyOf({ refName, refId, path }: MessageReference): string {
|
|
29
|
+
return `${refName}/${refId}${path ? `#${path}` : ""}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
static same(one: MessageReference, other: MessageReference): boolean {
|
|
33
|
+
return Reference.keyOf(one) === Reference.keyOf(other);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The token a reference reads as in the message the user is writing: `@[label](mention:refName/id#path)`.
|
|
38
|
+
*
|
|
39
|
+
* The text is the source of truth for which references a message carries — deleting the token is how somebody
|
|
40
|
+
* takes one back — so the token has to say everything the pointer does. The value is looked up beside it by key.
|
|
41
|
+
*/
|
|
42
|
+
static token(reference: MessageReference): string {
|
|
43
|
+
return `@[${reference.label}](mention:${Reference.keyOf(reference)})`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Greedy on the label alone: a label may hold anything but `]`, and every other part is a name or an id. */
|
|
47
|
+
static readonly pattern = /@\[([^\]]*)\]\(mention:([^/)]+)\/([^#)]+)(?:#([^)]*))?\)/g;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The pointers a draft names, in the order it names them. Pointers only — the value lives in `staged` and is
|
|
51
|
+
* joined on by key, so reading the text can never resurrect a value somebody deleted the token for.
|
|
52
|
+
*/
|
|
53
|
+
static parse(text: string): MessageReference[] {
|
|
54
|
+
return [...text.matchAll(Reference.pattern)].map(([, label, refName, refId, path]) => ({
|
|
55
|
+
refName,
|
|
56
|
+
refId,
|
|
57
|
+
label,
|
|
58
|
+
...(path ? { path } : {}),
|
|
59
|
+
}));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* A value past the ceiling becomes the JSON text up to it. That leaves `value` a string holding a fragment of a
|
|
64
|
+
* structure, which is exactly what it is — and a reader renders a string value as itself, so the model sees the
|
|
65
|
+
* fragment rather than an escaped quotation of one.
|
|
66
|
+
*/
|
|
67
|
+
static clipped(reference: MessageReference): MessageReference {
|
|
68
|
+
if (reference.value === undefined) return reference;
|
|
69
|
+
const json = JSON.stringify(reference.value) ?? "";
|
|
70
|
+
if (json.length <= Reference.limit) return reference;
|
|
71
|
+
return { ...reference, value: `${json.slice(0, Reference.limit)}…`, note: Reference.#note(json.length) };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The text with every token naming `key` taken out, and the gap it leaves closed up. Deleting the chip and
|
|
76
|
+
* deleting the token have to be the same act, because the text is what decides which references a turn carries.
|
|
77
|
+
*/
|
|
78
|
+
static without(text: string, key: string): string {
|
|
79
|
+
return text
|
|
80
|
+
.replace(new RegExp(Reference.pattern.source, "g"), (match, label, refName, refId, path) =>
|
|
81
|
+
Reference.keyOf({ refName, refId, label, path }) === key ? "" : match,
|
|
82
|
+
)
|
|
83
|
+
.replace(/[^\S\n]{2,}/g, " ")
|
|
84
|
+
.replace(/[^\S\n]+$/gm, "");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** What a pasted token resolves to: the pointer is in the text, and no value was ever staged beside it. */
|
|
88
|
+
static readonly unstagedNote =
|
|
89
|
+
"the value was not captured with this reference, so read it again with a tool before answering about it";
|
|
90
|
+
|
|
91
|
+
static #note(chars: number): string {
|
|
92
|
+
return (
|
|
93
|
+
`The value is ${chars} characters (~${Math.ceil(chars / 4)} tokens), past the ${Reference.limit}-character ` +
|
|
94
|
+
`ceiling one reference may add to this conversation, so only the first ${Reference.limit} are shown and the ` +
|
|
95
|
+
`JSON is cut mid-structure. Read a narrower part of it with a tool — one row, one field — rather than asking ` +
|
|
96
|
+
`for the whole value, and tell the user if you cannot answer from what you can see.`
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -45,6 +45,12 @@ export class Transcript {
|
|
|
45
45
|
|
|
46
46
|
/** An empty assistant message is a draft a reload or an abort caught before it said anything. */
|
|
47
47
|
static carries(message: ChatMessage) {
|
|
48
|
-
return
|
|
48
|
+
return (
|
|
49
|
+
!!message.text ||
|
|
50
|
+
!!message.error ||
|
|
51
|
+
!!message.toolCalls?.length ||
|
|
52
|
+
!!message.attachments?.length ||
|
|
53
|
+
!!message.references?.length
|
|
54
|
+
);
|
|
49
55
|
}
|
|
50
56
|
}
|
|
@@ -8,6 +8,7 @@ export * from "./AgentScope";
|
|
|
8
8
|
export * from "./AgentSession";
|
|
9
9
|
export * from "./Compaction";
|
|
10
10
|
export * from "./httpRunner";
|
|
11
|
+
export * from "./Reference";
|
|
11
12
|
export * from "./sharedContext";
|
|
12
13
|
export * from "./surfaceContext";
|
|
13
14
|
export * from "./ToolOutput";
|
|
@@ -118,6 +118,13 @@ export interface ToolCallResult {
|
|
|
118
118
|
error?: string;
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Why an assistant turn ended. `length` is the provider's own ceiling rather than the model's choice, so the turn
|
|
123
|
+
* is incomplete — a truncated answer and a turn cut off before its tool call finished both arrive this way, and
|
|
124
|
+
* neither is distinguishable from `end` without it.
|
|
125
|
+
*/
|
|
126
|
+
export type TurnStop = "end" | "toolUse" | "length";
|
|
127
|
+
|
|
121
128
|
/**
|
|
122
129
|
* A file the user handed the conversation rather than the screen — which is why it rides a message instead of a
|
|
123
130
|
* tool, the same reason `askUser` belongs to the session and not to the surface.
|
|
@@ -131,13 +138,6 @@ export interface ToolCallResult {
|
|
|
131
138
|
* model cannot read and says so in the transcript, because a silently dropped file is one the model then
|
|
132
139
|
* hallucinates about.
|
|
133
140
|
*/
|
|
134
|
-
/**
|
|
135
|
-
* Why an assistant turn ended. `length` is the provider's own ceiling rather than the model's choice, so the turn
|
|
136
|
-
* is incomplete — a truncated answer and a turn cut off before its tool call finished both arrive this way, and
|
|
137
|
-
* neither is distinguishable from `end` without it.
|
|
138
|
-
*/
|
|
139
|
-
export type TurnStop = "end" | "toolUse" | "length";
|
|
140
|
-
|
|
141
141
|
export interface MessageAttachment {
|
|
142
142
|
name: string;
|
|
143
143
|
mimeType: string;
|
|
@@ -154,11 +154,42 @@ export interface MessageAttachment {
|
|
|
154
154
|
ref?: string;
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
/**
|
|
158
|
+
* Data the user pointed at while they were talking, rather than a file they handed over — a record, or one field
|
|
159
|
+
* of one, named in the message the way they named it. It rides a message for the same reason an attachment does:
|
|
160
|
+
* what somebody referred to while asking is part of the asking, and the turn context is rebuilt from the screen
|
|
161
|
+
* every turn, so a screen-shaped carrier forgets what was pointed at three turns ago.
|
|
162
|
+
*
|
|
163
|
+
* **`value` is a snapshot, deliberately.** It is what the data was when the user sent the message, and it is never
|
|
164
|
+
* re-read on a later turn. Re-reading would be wrong twice over: it rewrites what the person was looking at when
|
|
165
|
+
* they spoke, and the common case is an agent that then *edits* the very field it was pointed at, which would
|
|
166
|
+
* leave the reference showing the result and no record of what was being changed from. `refName`, `refId` and
|
|
167
|
+
* `path` are the way back to the current value — a tool re-reads it when the answer needs it.
|
|
168
|
+
*
|
|
169
|
+
* **`value` arrives masked, and nothing downstream can mask it again.** Masking needs the model class
|
|
170
|
+
* (`mask(model, value)`), which no wire carries, so whichever model the host names when it stages the reference is
|
|
171
|
+
* the whole of the decision about what leaves the browser.
|
|
172
|
+
*/
|
|
173
|
+
export interface MessageReference {
|
|
174
|
+
/** The host's own `refName`, unchanged — the vocabulary its published tools already speak. */
|
|
175
|
+
refName: string;
|
|
176
|
+
refId: string;
|
|
177
|
+
/** What the chip draws and what the token in the text spells, so the two can never disagree. */
|
|
178
|
+
label: string;
|
|
179
|
+
/** A dotted path into the document, in `pathSet`'s vocabulary. Absent means the whole of it. */
|
|
180
|
+
path?: string;
|
|
181
|
+
value?: unknown;
|
|
182
|
+
/** Read by the model in place of a value there is none of — clipped, unreadable, or gone from a restored chat. */
|
|
183
|
+
note?: string;
|
|
184
|
+
}
|
|
185
|
+
|
|
157
186
|
export interface ChatMessage {
|
|
158
187
|
role: ChatRole;
|
|
159
188
|
text?: string;
|
|
160
189
|
/** Files the message carries. Content, not instructions — a backend frames them the way it frames context. */
|
|
161
190
|
attachments?: MessageAttachment[];
|
|
191
|
+
/** Data the message points at. Content, framed like attachments and for the same reason. */
|
|
192
|
+
references?: MessageReference[];
|
|
162
193
|
toolCalls?: ToolCallRequest[];
|
|
163
194
|
toolResults?: ToolCallResult[];
|
|
164
195
|
/** A failed or capped turn, recorded in the transcript rather than thrown past it. */
|