@adecore/agents 0.21.0 → 0.21.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chat/chat-core.d.ts +5 -1
- package/dist/chat/chat-core.js +16 -6
- package/dist/chat/chat-session.d.ts +13 -0
- package/dist/chat/chat-session.js +75 -5
- package/dist/chat/chat-store.d.ts +3 -2
- package/dist/chat/chat-store.js +42 -11
- package/dist/chat/readable-text.d.ts +2 -0
- package/dist/chat/readable-text.js +16 -0
- package/dist/chat/ui-queries.d.ts +12 -1
- package/dist/chat/ui-queries.js +107 -60
- package/dist/host/handlers.js +1 -1
- package/dist/tasks/task-coordinator.js +6 -2
- package/package.json +3 -3
- package/src/chat/chat-core.ts +26 -6
- package/src/chat/chat-session.ts +85 -5
- package/src/chat/chat-store.ts +53 -11
- package/src/chat/readable-text.ts +18 -0
- package/src/chat/ui-queries.ts +120 -64
- package/src/host/handlers.ts +1 -1
- package/src/tasks/task-coordinator.ts +6 -2
package/src/chat/chat-session.ts
CHANGED
|
@@ -18,7 +18,7 @@ import type {
|
|
|
18
18
|
RuntimeMode
|
|
19
19
|
} from '@adecore/agent-contracts';
|
|
20
20
|
import { GeneratedImageInputSchema, notResumedNote } from '@adecore/agent-contracts';
|
|
21
|
-
import { resolveUiChoice, UiFailure } from '@adecore/intelligent-ui';
|
|
21
|
+
import { resolveUiChoice, UI_FENCE_LANGUAGE, UiFailure, uiHasFence, uiMayReferenceHost } from '@adecore/intelligent-ui';
|
|
22
22
|
import type { UiCompileOptions } from '@adecore/intelligent-ui/compiler';
|
|
23
23
|
import { UiStream, type UiStreamPreview } from '@adecore/intelligent-ui/stream';
|
|
24
24
|
import type { ChatProvider } from '../providers/provider.ts';
|
|
@@ -30,6 +30,7 @@ import type { ChatTitleInput } from './chat-title.ts';
|
|
|
30
30
|
import { ChatError } from './errors.ts';
|
|
31
31
|
import { limitedTurn, limitResumeAt, limitResumeWake } from './limit-resume.ts';
|
|
32
32
|
import { isMainAgentOutput, ThreadProjector } from './projector.ts';
|
|
33
|
+
import { readableAssistantText } from './readable-text.ts';
|
|
33
34
|
import type { SubagentSettlement } from './subagent-settlement.ts';
|
|
34
35
|
import { ChatThread } from './thread.ts';
|
|
35
36
|
import { errorText } from '../error-text.ts';
|
|
@@ -60,6 +61,8 @@ export interface ChatSessionOptions {
|
|
|
60
61
|
items?: ChatItem[];
|
|
61
62
|
// Said once, in front of the next real prompt, and kept in the record until then (a fork's note for its agent).
|
|
62
63
|
preambles?: string[];
|
|
64
|
+
// The access a UI host captured per reply, from the record; never on the wire.
|
|
65
|
+
uiAccess?: Readonly<Record<string, unknown>>;
|
|
63
66
|
provider: ChatProvider;
|
|
64
67
|
// The executable and leading arguments; a test points this at a fake CLI.
|
|
65
68
|
command: string[];
|
|
@@ -76,6 +79,8 @@ export interface ChatSessionOptions {
|
|
|
76
79
|
references?: ChatReferences;
|
|
77
80
|
// Trees per turn, so a settled turn can show what the working tree holds against its start.
|
|
78
81
|
checkpoints?: TurnCheckpoints;
|
|
82
|
+
// The info string of the host's UI fences; `UI_FENCE_LANGUAGE` without one.
|
|
83
|
+
uiFenceLanguage?: string;
|
|
79
84
|
ui?: {
|
|
80
85
|
schemas: UiCompileOptions['querySchemas'];
|
|
81
86
|
observe(item: ChatAssistantItem, final: boolean): void;
|
|
@@ -109,6 +114,8 @@ export interface LimitResumeHooks {
|
|
|
109
114
|
|
|
110
115
|
// Claude Code names a session about six seconds after its first prompt, and a first turn can run for minutes.
|
|
111
116
|
const TITLE_RECHECK_MS = 10_000;
|
|
117
|
+
// Longer than any fence line or the name of a tag a scan looks for, so a match never falls between two deltas.
|
|
118
|
+
const UI_SCAN_OVERLAP = 64;
|
|
112
119
|
|
|
113
120
|
export interface ChatSendExtras {
|
|
114
121
|
uiChoice?: ChatUiChoiceOrigin;
|
|
@@ -193,10 +200,18 @@ export class ChatSession {
|
|
|
193
200
|
private readonly steering = new Set<string>();
|
|
194
201
|
private readonly uiStreams = new Map<string, UiStream>();
|
|
195
202
|
private readonly uiCompiled = new Map<string, UiStreamPreview>();
|
|
203
|
+
private readonly capturedUiAccess: Map<string, unknown>;
|
|
204
|
+
// How much of a streaming reply's text the fence and host scans already covered, so a delta scans only its tail.
|
|
205
|
+
private readonly uiScanned = new Map<string, number>();
|
|
206
|
+
private readonly uiReferencing = new Set<string>();
|
|
207
|
+
private readonly uiAttachments = new Map<string | null, string | undefined>();
|
|
208
|
+
private readonly uiFenceLanguage: string;
|
|
196
209
|
|
|
197
210
|
constructor(options: ChatSessionOptions) {
|
|
198
211
|
this.options = options;
|
|
212
|
+
this.uiFenceLanguage = options.uiFenceLanguage ?? UI_FENCE_LANGUAGE;
|
|
199
213
|
this.pendingPreambles = options.preambles ?? [];
|
|
214
|
+
this.capturedUiAccess = new Map(Object.entries(options.uiAccess ?? {}));
|
|
200
215
|
this.thread = new ChatThread(options.info, options.items);
|
|
201
216
|
this.projector = new ThreadProjector(this.thread, { providerName: options.provider.name });
|
|
202
217
|
}
|
|
@@ -314,6 +329,24 @@ export class ChatSession {
|
|
|
314
329
|
return { queued: false, turnId };
|
|
315
330
|
}
|
|
316
331
|
|
|
332
|
+
/* What a UI host captured for a reply; null where the capture was denied or is still out, undefined where none started. */
|
|
333
|
+
uiAccess(itemId: string): unknown {
|
|
334
|
+
return this.capturedUiAccess.get(itemId);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
setUiAccess(itemId: string, access: unknown): void {
|
|
338
|
+
if (this.frozen) {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
this.capturedUiAccess.set(itemId, access);
|
|
342
|
+
this.options.persistSoon();
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/* The captured access of the replies still in the thread, for the record. */
|
|
346
|
+
get uiAccessRecord(): Record<string, unknown> {
|
|
347
|
+
return Object.fromEntries([...this.capturedUiAccess].filter(([itemId]) => this.thread.get(itemId) !== undefined));
|
|
348
|
+
}
|
|
349
|
+
|
|
317
350
|
updateUiQueries(itemId: string, uiQueries: ChatUiQueryState): void {
|
|
318
351
|
const item = this.thread.get(itemId);
|
|
319
352
|
if (this.frozen || item?.kind !== 'assistant' || item.parentToolUseId) {
|
|
@@ -1828,7 +1861,7 @@ export class ChatSession {
|
|
|
1828
1861
|
this.naming = true;
|
|
1829
1862
|
const turnId = done[0]!.id;
|
|
1830
1863
|
const answer = items
|
|
1831
|
-
.map((item) => (item.kind === 'assistant' && item.turnId === turnId ? item
|
|
1864
|
+
.map((item) => (item.kind === 'assistant' && item.turnId === turnId ? readableAssistantText(item) : ''))
|
|
1832
1865
|
.filter((text) => text !== '')
|
|
1833
1866
|
.join('\n\n');
|
|
1834
1867
|
const agentSessionId = this.thread.info.agentSessionId;
|
|
@@ -1886,19 +1919,28 @@ export class ChatSession {
|
|
|
1886
1919
|
if (event.type === 'reset') {
|
|
1887
1920
|
this.clearUi();
|
|
1888
1921
|
}
|
|
1922
|
+
if (event.type === 'item' && event.item.kind === 'tool') {
|
|
1923
|
+
this.uiAttachments.delete(event.item.turnId);
|
|
1924
|
+
}
|
|
1889
1925
|
const item = event.type === 'item' ? event.item : event.type === 'delta' ? this.thread.get(event.itemId) : null;
|
|
1890
1926
|
if (item?.kind !== 'assistant' || item.parentToolUseId) {
|
|
1891
1927
|
this.options.emit(event);
|
|
1892
1928
|
continue;
|
|
1893
1929
|
}
|
|
1930
|
+
const tail = this.unscannedUiText(item, event.type === 'item');
|
|
1931
|
+
if (uiMayReferenceHost(tail)) {
|
|
1932
|
+
this.uiReferencing.add(item.id);
|
|
1933
|
+
}
|
|
1894
1934
|
let stream = this.uiStreams.get(item.id);
|
|
1895
|
-
if (!stream && !
|
|
1935
|
+
if (!stream && !uiHasFence(tail, this.uiFenceLanguage)) {
|
|
1936
|
+
this.forgetUiScan(item);
|
|
1896
1937
|
this.options.emit(event);
|
|
1897
1938
|
continue;
|
|
1898
1939
|
}
|
|
1899
1940
|
if (!stream) {
|
|
1900
1941
|
stream = new UiStream({
|
|
1901
1942
|
querySchemas: this.options.ui?.schemas,
|
|
1943
|
+
fenceLanguage: this.uiFenceLanguage,
|
|
1902
1944
|
id: item.id,
|
|
1903
1945
|
clock: this.options.clock ?? systemClock,
|
|
1904
1946
|
emit: (preview) => {
|
|
@@ -1908,7 +1950,10 @@ export class ChatSession {
|
|
|
1908
1950
|
});
|
|
1909
1951
|
this.uiStreams.set(item.id, stream);
|
|
1910
1952
|
}
|
|
1911
|
-
this.
|
|
1953
|
+
if (this.uiReferencing.has(item.id)) {
|
|
1954
|
+
this.options.ui?.observe(item, false);
|
|
1955
|
+
}
|
|
1956
|
+
this.forgetUiScan(item);
|
|
1912
1957
|
const latestAttachment = this.latestUiAttachment(item.turnId);
|
|
1913
1958
|
if (event.type === 'item' && !item.streaming) {
|
|
1914
1959
|
const ui = stream
|
|
@@ -1919,7 +1964,7 @@ export class ChatSession {
|
|
|
1919
1964
|
const diagnostics = [...new Set(ui.flatMap((block) => block.diagnostics.map((diagnostic) => diagnostic.message)))].slice(0, 6);
|
|
1920
1965
|
if (diagnostics.length > 0) {
|
|
1921
1966
|
const repair =
|
|
1922
|
-
`Your last
|
|
1967
|
+
`Your last ${this.uiFenceLanguage} block had these diagnostics: ${diagnostics.join(' ')} Use the catalog's supported props and components.`.slice(
|
|
1923
1968
|
0,
|
|
1924
1969
|
1200
|
|
1925
1970
|
);
|
|
@@ -1939,7 +1984,39 @@ export class ChatSession {
|
|
|
1939
1984
|
}
|
|
1940
1985
|
}
|
|
1941
1986
|
|
|
1987
|
+
/*
|
|
1988
|
+
* The text a scan has not covered yet, from the start of the line the last scan ended in, since a fence
|
|
1989
|
+
* is one line; a long line is cut short, behind a character no fence or tag can follow.
|
|
1990
|
+
*/
|
|
1991
|
+
private unscannedUiText(item: ChatAssistantItem, whole: boolean): string {
|
|
1992
|
+
const scanned = whole ? 0 : (this.uiScanned.get(item.id) ?? 0);
|
|
1993
|
+
this.uiScanned.set(item.id, item.text.length);
|
|
1994
|
+
if (scanned === 0 || scanned > item.text.length) {
|
|
1995
|
+
return item.text;
|
|
1996
|
+
}
|
|
1997
|
+
const line = item.text.lastIndexOf('\n', scanned - 1) + 1;
|
|
1998
|
+
const from = Math.max(line, scanned - UI_SCAN_OVERLAP);
|
|
1999
|
+
return from === line ? item.text.slice(from) : `\0${item.text.slice(from)}`;
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
private forgetUiScan(item: ChatAssistantItem): void {
|
|
2003
|
+
if (!item.streaming) {
|
|
2004
|
+
this.uiScanned.delete(item.id);
|
|
2005
|
+
this.uiReferencing.delete(item.id);
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
/* Looked up once per turn, and again only after one of its tool calls changed. */
|
|
1942
2010
|
private latestUiAttachment(turnId: string | null): string | undefined {
|
|
2011
|
+
if (this.uiAttachments.has(turnId)) {
|
|
2012
|
+
return this.uiAttachments.get(turnId);
|
|
2013
|
+
}
|
|
2014
|
+
const attachment = this.findUiAttachment(turnId);
|
|
2015
|
+
this.uiAttachments.set(turnId, attachment);
|
|
2016
|
+
return attachment;
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
private findUiAttachment(turnId: string | null): string | undefined {
|
|
1943
2020
|
for (const item of this.thread.list().toReversed()) {
|
|
1944
2021
|
if (item.kind !== 'tool' || item.turnId !== turnId || item.name !== 'ImageGeneration' || item.state !== 'done') {
|
|
1945
2022
|
continue;
|
|
@@ -1958,6 +2035,9 @@ export class ChatSession {
|
|
|
1958
2035
|
}
|
|
1959
2036
|
this.uiStreams.clear();
|
|
1960
2037
|
this.uiCompiled.clear();
|
|
2038
|
+
this.uiScanned.clear();
|
|
2039
|
+
this.uiReferencing.clear();
|
|
2040
|
+
this.uiAttachments.clear();
|
|
1961
2041
|
}
|
|
1962
2042
|
|
|
1963
2043
|
/* Said on the info as well as by the rows, since a client with the thread closed sees only the info. */
|
package/src/chat/chat-store.ts
CHANGED
|
@@ -20,10 +20,12 @@ const RecordSchema = z.looseObject({
|
|
|
20
20
|
seq: z.number().int().nonnegative().optional(),
|
|
21
21
|
resetSeq: z.number().int().nonnegative().optional(),
|
|
22
22
|
// Never on the wire: what goes in front of the next real prompt, once (a fork's note for its agent).
|
|
23
|
-
preambles: z.array(z.string()).optional()
|
|
23
|
+
preambles: z.array(z.string()).optional(),
|
|
24
|
+
// Never on the wire: the access a UI host captured for each reply that reads it, by item id.
|
|
25
|
+
uiAccess: z.record(z.string(), z.unknown()).optional()
|
|
24
26
|
});
|
|
25
27
|
|
|
26
|
-
const OWN_KEYS = new Set(['info', 'items', 'seq', 'resetSeq', 'preambles']);
|
|
28
|
+
const OWN_KEYS = new Set(['info', 'items', 'seq', 'resetSeq', 'preambles', 'uiAccess']);
|
|
27
29
|
|
|
28
30
|
/* What a host keeps in a chat's record beside the thread; it never reaches the wire. */
|
|
29
31
|
export type ChatRecordExtras = Record<string, unknown>;
|
|
@@ -39,6 +41,7 @@ export interface ChatRecord {
|
|
|
39
41
|
seq: number;
|
|
40
42
|
resetSeq: number;
|
|
41
43
|
preambles: string[];
|
|
44
|
+
uiAccess: Record<string, unknown>;
|
|
42
45
|
extras: ChatRecordExtras;
|
|
43
46
|
// The log as it is on disk, for the lines an attach with `since` may still be answered from.
|
|
44
47
|
lines: ChatLogLine[];
|
|
@@ -54,8 +57,37 @@ function logName(chatId: string): string {
|
|
|
54
57
|
return `${encodeURIComponent(chatId)}.log`;
|
|
55
58
|
}
|
|
56
59
|
|
|
57
|
-
function recordBody(
|
|
58
|
-
|
|
60
|
+
function recordBody(
|
|
61
|
+
info: ChatInfo,
|
|
62
|
+
items: ChatItem[],
|
|
63
|
+
at: ChatSeq,
|
|
64
|
+
preambles: readonly string[],
|
|
65
|
+
extras: ChatRecordExtras,
|
|
66
|
+
uiAccess: Readonly<Record<string, unknown>>
|
|
67
|
+
): string {
|
|
68
|
+
return JSON.stringify({
|
|
69
|
+
info,
|
|
70
|
+
items,
|
|
71
|
+
...at,
|
|
72
|
+
...(preambles.length === 0 ? {} : { preambles }),
|
|
73
|
+
...(Object.keys(uiAccess).length === 0 ? {} : { uiAccess }),
|
|
74
|
+
...withoutOwnKeys(extras)
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/* A record from before `uiAccess` kept the access inside each reply's `uiQueries`, which the item schema now drops. */
|
|
79
|
+
function liftUiAccess(record: unknown): unknown {
|
|
80
|
+
if (typeof record !== 'object' || record === null || 'uiAccess' in record || !Array.isArray((record as { items?: unknown }).items)) {
|
|
81
|
+
return record;
|
|
82
|
+
}
|
|
83
|
+
const uiAccess: Record<string, unknown> = {};
|
|
84
|
+
for (const item of (record as { items: unknown[] }).items) {
|
|
85
|
+
const queries = (item as { uiQueries?: unknown } | null)?.uiQueries;
|
|
86
|
+
if (typeof queries === 'object' && queries !== null && 'access' in queries) {
|
|
87
|
+
uiAccess[String((item as { id: unknown }).id)] = queries.access;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return Object.keys(uiAccess).length === 0 ? record : { ...record, uiAccess };
|
|
59
91
|
}
|
|
60
92
|
|
|
61
93
|
function withoutOwnKeys(extras: ChatRecordExtras): ChatRecordExtras {
|
|
@@ -127,9 +159,10 @@ export class ChatStore {
|
|
|
127
159
|
items: ChatItem[],
|
|
128
160
|
at: ChatSeq = { seq: 0, resetSeq: 0 },
|
|
129
161
|
preambles: readonly string[] = [],
|
|
130
|
-
extras: ChatRecordExtras = {}
|
|
162
|
+
extras: ChatRecordExtras = {},
|
|
163
|
+
uiAccess: Readonly<Record<string, unknown>> = {}
|
|
131
164
|
): Promise<number> {
|
|
132
|
-
const body = recordBody(info, items, at, preambles, extras);
|
|
165
|
+
const body = recordBody(info, items, at, preambles, extras, uiAccess);
|
|
133
166
|
await mkdir(this.dir, { recursive: true, mode: 0o700 });
|
|
134
167
|
await writeAtomic(join(this.dir, recordFileName(chatId)), body);
|
|
135
168
|
return body.length;
|
|
@@ -145,10 +178,11 @@ export class ChatStore {
|
|
|
145
178
|
items: ChatItem[],
|
|
146
179
|
at: ChatSeq = { seq: 0, resetSeq: 0 },
|
|
147
180
|
preambles: readonly string[] = [],
|
|
148
|
-
extras: ChatRecordExtras = {}
|
|
181
|
+
extras: ChatRecordExtras = {},
|
|
182
|
+
uiAccess: Readonly<Record<string, unknown>> = {}
|
|
149
183
|
): void {
|
|
150
184
|
mkdirSync(this.dir, { recursive: true, mode: 0o700 });
|
|
151
|
-
writeAtomicSync(join(this.dir, recordFileName(chatId)), recordBody(info, items, at, preambles, extras));
|
|
185
|
+
writeAtomicSync(join(this.dir, recordFileName(chatId)), recordBody(info, items, at, preambles, extras, uiAccess));
|
|
152
186
|
}
|
|
153
187
|
|
|
154
188
|
/* Null for a chat with nothing on disk to make it of, and for one whose record does not parse (see `unreadable`). */
|
|
@@ -174,7 +208,15 @@ export class ChatStore {
|
|
|
174
208
|
resetSeq = line.seq;
|
|
175
209
|
}
|
|
176
210
|
}
|
|
177
|
-
return {
|
|
211
|
+
return {
|
|
212
|
+
...thread.snapshot(),
|
|
213
|
+
seq,
|
|
214
|
+
resetSeq,
|
|
215
|
+
preambles: snapshot.preambles ?? [],
|
|
216
|
+
uiAccess: snapshot.uiAccess ?? {},
|
|
217
|
+
extras: extrasOf(snapshot),
|
|
218
|
+
lines
|
|
219
|
+
};
|
|
178
220
|
}
|
|
179
221
|
|
|
180
222
|
/*
|
|
@@ -204,7 +246,7 @@ export class ChatStore {
|
|
|
204
246
|
await writeAtomic(join(this.dir, recordFileName(chatId)), JSON.stringify(migrated));
|
|
205
247
|
}
|
|
206
248
|
}
|
|
207
|
-
const parsed = RecordSchema.safeParse(record);
|
|
249
|
+
const parsed = RecordSchema.safeParse(liftUiAccess(record));
|
|
208
250
|
return parsed.success ? parsed.data : UNREADABLE;
|
|
209
251
|
}
|
|
210
252
|
|
|
@@ -260,5 +302,5 @@ function fromLogAlone(lines: ChatLogLine[]): ChatRecord | null {
|
|
|
260
302
|
resetSeq = line.seq;
|
|
261
303
|
}
|
|
262
304
|
}
|
|
263
|
-
return { ...thread.snapshot(), seq: 0, resetSeq, preambles: [], extras: {}, lines };
|
|
305
|
+
return { ...thread.snapshot(), seq: 0, resetSeq, preambles: [], uiAccess: {}, extras: {}, lines };
|
|
264
306
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ChatAssistantItem } from '@adecore/agent-contracts';
|
|
2
|
+
import { uiFallbackText } from '@adecore/intelligent-ui/text';
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* An assistant's words as another agent or a person outside the thread reads them: each UI block becomes
|
|
6
|
+
* its text fallback, with the first query values when they were frozen for the block as stored. It works
|
|
7
|
+
* from the stored block ranges, so it holds whatever fence language the host compiled.
|
|
8
|
+
*/
|
|
9
|
+
export function readableAssistantText(item: Pick<ChatAssistantItem, 'text' | 'ui' | 'uiQueries'>): string {
|
|
10
|
+
if (!item.ui?.length) {
|
|
11
|
+
return item.text;
|
|
12
|
+
}
|
|
13
|
+
const blocks = item.ui.map((block) => {
|
|
14
|
+
const frozen = item.uiQueries?.blocks[block.id];
|
|
15
|
+
return frozen && frozen.revision === block.revision ? { ...block, fallback: frozen.fallback } : block;
|
|
16
|
+
});
|
|
17
|
+
return uiFallbackText(item.text, blocks);
|
|
18
|
+
}
|