@adecore/agents 0.21.0 → 0.21.2
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/async.js +3 -8
- package/dist/chat/chat-core.d.ts +7 -3
- package/dist/chat/chat-core.js +19 -9
- package/dist/chat/chat-log.js +2 -2
- 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 +44 -16
- package/dist/chat/chat-title.js +4 -8
- package/dist/chat/claude-backend.d.ts +1 -0
- package/dist/chat/claude-backend.js +8 -6
- package/dist/chat/claude-title.js +1 -1
- package/dist/chat/codex-backend.d.ts +2 -2
- package/dist/chat/codex-backend.js +5 -8
- package/dist/chat/codex-protocol.js +1 -1
- package/dist/chat/codex-thread.js +4 -5
- package/dist/chat/composer-preferences.js +2 -3
- package/dist/chat/limit-resume.js +2 -7
- 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/chat/visual-store.js +8 -6
- package/dist/context/refusal.js +3 -7
- package/dist/context/verb.js +1 -5
- package/dist/host/agent-host.js +2 -2
- package/dist/host/handlers.js +1 -2
- package/dist/host/wiring.js +1 -1
- package/dist/lineage.js +2 -3
- package/dist/messages/deliver-message.js +6 -11
- package/dist/messages/deliver-notice.js +8 -12
- package/dist/messages/notice-store.js +3 -5
- package/dist/outbox/outbox-worker.js +5 -2
- package/dist/providers/accounts/service.d.ts +2 -1
- package/dist/providers/accounts/service.js +11 -13
- package/dist/providers/claude.js +0 -1
- package/dist/providers/detect.js +2 -10
- package/dist/record-directory.js +0 -1
- package/dist/tasks/task-coordinator.d.ts +1 -0
- package/dist/tasks/task-coordinator.js +40 -36
- package/dist/tasks/task-store.js +1 -1
- package/dist/tasks/task-work.js +2 -2
- package/dist/tasks/waiting-child.js +5 -3
- package/dist/tasks/wake-parent.js +15 -10
- package/dist/tasks/wiring.js +32 -37
- package/dist/usage/aggregate.js +62 -46
- package/dist/usage/exchange.js +1 -1
- package/dist/usage/limits/monitor.d.ts +2 -2
- package/dist/usage/limits/monitor.js +2 -2
- package/dist/usage/limits/probe.js +1 -1
- package/dist/usage/pricing.js +1 -1
- package/dist/usage/roots.js +1 -2
- package/dist/usage/scanner.js +7 -9
- package/dist/usage/usage-service.d.ts +1 -2
- package/dist/usage/usage-service.js +3 -6
- package/package.json +3 -3
- package/src/async.ts +3 -9
- package/src/chat/chat-core.ts +29 -9
- package/src/chat/chat-log.ts +2 -2
- package/src/chat/chat-session.ts +85 -5
- package/src/chat/chat-store.ts +54 -16
- package/src/chat/chat-title.ts +4 -8
- package/src/chat/claude-backend.ts +9 -6
- package/src/chat/claude-title.ts +1 -1
- package/src/chat/claude-transcript.ts +1 -1
- package/src/chat/codex-backend.ts +5 -8
- package/src/chat/codex-protocol.ts +1 -1
- package/src/chat/codex-thread.ts +6 -5
- package/src/chat/composer-preferences.ts +2 -3
- package/src/chat/limit-resume.ts +2 -7
- package/src/chat/readable-text.ts +18 -0
- package/src/chat/thread.ts +1 -1
- package/src/chat/ui-queries.ts +120 -64
- package/src/chat/visual-store.ts +9 -6
- package/src/chat/wake-chat.ts +4 -1
- package/src/context/refusal.ts +3 -7
- package/src/context/verb.ts +1 -5
- package/src/host/agent-host.ts +2 -2
- package/src/host/handlers.ts +1 -2
- package/src/host/wiring.ts +1 -1
- package/src/lineage.ts +2 -3
- package/src/messages/deliver-message.ts +6 -11
- package/src/messages/deliver-notice.ts +8 -12
- package/src/messages/notice-store.ts +3 -5
- package/src/outbox/outbox-worker.ts +5 -2
- package/src/providers/accounts/service.ts +12 -13
- package/src/providers/accounts/variables.ts +1 -0
- package/src/providers/claude.ts +0 -1
- package/src/providers/detect.ts +2 -11
- package/src/record-directory.ts +0 -1
- package/src/skills.ts +0 -1
- package/src/tasks/task-coordinator.ts +41 -35
- package/src/tasks/task-store.ts +1 -1
- package/src/tasks/task-work.ts +2 -2
- package/src/tasks/waiting-child.ts +6 -3
- package/src/tasks/wake-parent.ts +21 -16
- package/src/tasks/wiring.ts +33 -41
- package/src/usage/aggregate.ts +80 -50
- package/src/usage/exchange.ts +1 -1
- package/src/usage/limits/monitor.ts +4 -4
- package/src/usage/limits/probe.ts +2 -2
- package/src/usage/pricing.ts +1 -1
- package/src/usage/roots.ts +1 -2
- package/src/usage/scanner.ts +8 -10
- package/src/usage/usage-service.ts +5 -9
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
|
@@ -4,12 +4,12 @@ import { join } from 'node:path';
|
|
|
4
4
|
import { ChatInfoSchema, ChatItemSchema, type ChatInfo, type ChatItem } from '@adecore/agent-contracts';
|
|
5
5
|
import { z } from 'zod';
|
|
6
6
|
import { isNotFound, writeAtomic, writeAtomicSync } from '../fs.ts';
|
|
7
|
-
import {
|
|
7
|
+
import { recordFileName } from '../record-directory.ts';
|
|
8
8
|
import { migrateInlineAttachments, type AttachmentStore } from './attachment-store.ts';
|
|
9
|
+
import { isBookmarkFileName } from './bookmark-store.ts';
|
|
9
10
|
import { parseLog, type ChatLogLine } from './chat-log.ts';
|
|
10
11
|
import { ChatThread } from './thread.ts';
|
|
11
12
|
import { isVisualFileName } from './visual-store.ts';
|
|
12
|
-
import { recordFileName } from '../record-directory.ts';
|
|
13
13
|
|
|
14
14
|
// zod strips what it does not know inside the info and the items, so a file written by an older build
|
|
15
15
|
// (with `interactionMode`, say) still parses and loses only the dropped field. A record without `seq`
|
|
@@ -20,15 +20,17 @@ 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>;
|
|
30
32
|
|
|
31
|
-
function
|
|
33
|
+
function withoutOwnKeys(record: Record<string, unknown>): ChatRecordExtras {
|
|
32
34
|
return Object.fromEntries(Object.entries(record).filter(([key]) => !OWN_KEYS.has(key)));
|
|
33
35
|
}
|
|
34
36
|
|
|
@@ -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,12 +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
|
+
});
|
|
59
76
|
}
|
|
60
77
|
|
|
61
|
-
|
|
62
|
-
|
|
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 };
|
|
63
91
|
}
|
|
64
92
|
|
|
65
93
|
export interface ChatStoreOptions {
|
|
@@ -127,9 +155,10 @@ export class ChatStore {
|
|
|
127
155
|
items: ChatItem[],
|
|
128
156
|
at: ChatSeq = { seq: 0, resetSeq: 0 },
|
|
129
157
|
preambles: readonly string[] = [],
|
|
130
|
-
extras: ChatRecordExtras = {}
|
|
158
|
+
extras: ChatRecordExtras = {},
|
|
159
|
+
uiAccess: Readonly<Record<string, unknown>> = {}
|
|
131
160
|
): Promise<number> {
|
|
132
|
-
const body = recordBody(info, items, at, preambles, extras);
|
|
161
|
+
const body = recordBody(info, items, at, preambles, extras, uiAccess);
|
|
133
162
|
await mkdir(this.dir, { recursive: true, mode: 0o700 });
|
|
134
163
|
await writeAtomic(join(this.dir, recordFileName(chatId)), body);
|
|
135
164
|
return body.length;
|
|
@@ -145,10 +174,11 @@ export class ChatStore {
|
|
|
145
174
|
items: ChatItem[],
|
|
146
175
|
at: ChatSeq = { seq: 0, resetSeq: 0 },
|
|
147
176
|
preambles: readonly string[] = [],
|
|
148
|
-
extras: ChatRecordExtras = {}
|
|
177
|
+
extras: ChatRecordExtras = {},
|
|
178
|
+
uiAccess: Readonly<Record<string, unknown>> = {}
|
|
149
179
|
): void {
|
|
150
180
|
mkdirSync(this.dir, { recursive: true, mode: 0o700 });
|
|
151
|
-
writeAtomicSync(join(this.dir, recordFileName(chatId)), recordBody(info, items, at, preambles, extras));
|
|
181
|
+
writeAtomicSync(join(this.dir, recordFileName(chatId)), recordBody(info, items, at, preambles, extras, uiAccess));
|
|
152
182
|
}
|
|
153
183
|
|
|
154
184
|
/* Null for a chat with nothing on disk to make it of, and for one whose record does not parse (see `unreadable`). */
|
|
@@ -174,7 +204,15 @@ export class ChatStore {
|
|
|
174
204
|
resetSeq = line.seq;
|
|
175
205
|
}
|
|
176
206
|
}
|
|
177
|
-
return {
|
|
207
|
+
return {
|
|
208
|
+
...thread.snapshot(),
|
|
209
|
+
seq,
|
|
210
|
+
resetSeq,
|
|
211
|
+
preambles: snapshot.preambles ?? [],
|
|
212
|
+
uiAccess: snapshot.uiAccess ?? {},
|
|
213
|
+
extras: withoutOwnKeys(snapshot),
|
|
214
|
+
lines
|
|
215
|
+
};
|
|
178
216
|
}
|
|
179
217
|
|
|
180
218
|
/*
|
|
@@ -204,7 +242,7 @@ export class ChatStore {
|
|
|
204
242
|
await writeAtomic(join(this.dir, recordFileName(chatId)), JSON.stringify(migrated));
|
|
205
243
|
}
|
|
206
244
|
}
|
|
207
|
-
const parsed = RecordSchema.safeParse(record);
|
|
245
|
+
const parsed = RecordSchema.safeParse(liftUiAccess(record));
|
|
208
246
|
return parsed.success ? parsed.data : UNREADABLE;
|
|
209
247
|
}
|
|
210
248
|
|
|
@@ -260,5 +298,5 @@ function fromLogAlone(lines: ChatLogLine[]): ChatRecord | null {
|
|
|
260
298
|
resetSeq = line.seq;
|
|
261
299
|
}
|
|
262
300
|
}
|
|
263
|
-
return { ...thread.snapshot(), seq: 0, resetSeq, preambles: [], extras: {}, lines };
|
|
301
|
+
return { ...thread.snapshot(), seq: 0, resetSeq, preambles: [], uiAccess: {}, extras: {}, lines };
|
|
264
302
|
}
|
package/src/chat/chat-title.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AgentKind } from '@adecore/agent-contracts';
|
|
2
|
-
import { cleanTitle } from '../title-file.ts';
|
|
3
2
|
import type { ProviderRegistry } from '../providers/registry.ts';
|
|
4
3
|
import { runProcess } from '../run-process.ts';
|
|
4
|
+
import { cleanTitle } from '../title-file.ts';
|
|
5
5
|
|
|
6
6
|
// Enough of a conversation to name it; the rest of a long prompt or answer says nothing a title needs.
|
|
7
7
|
const MAX_PROMPT_CHARS = 2000;
|
|
@@ -19,10 +19,7 @@ function cap(text: string, limit: number): string {
|
|
|
19
19
|
return text.length <= limit ? text : `${text.slice(0, limit)}...`;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
/*
|
|
23
|
-
* What the CLI is asked. JSON again, as for a commit message: a model left to write freely opens
|
|
24
|
-
* with a sentence, and that sentence would become the name of the node.
|
|
25
|
-
*/
|
|
22
|
+
/* JSON, since a model left to write freely opens with a sentence, and that sentence would become the name. */
|
|
26
23
|
export function buildTitlePrompt(prompt: string, answer: string): string {
|
|
27
24
|
return [
|
|
28
25
|
'Write a short title for the conversation below, as a person would name it in a list of chats.',
|
|
@@ -42,9 +39,8 @@ export function buildTitlePrompt(prompt: string, answer: string): string {
|
|
|
42
39
|
}
|
|
43
40
|
|
|
44
41
|
/*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* is better than a stray line of it.
|
|
42
|
+
* Strict on purpose: a run that holds no `{"title": "..."}` object printed something other than a
|
|
43
|
+
* title, and a derived name is better than a stray line of it.
|
|
48
44
|
*/
|
|
49
45
|
export function parseTitle(output: string): string | null {
|
|
50
46
|
const start = [...output.matchAll(/\{\s*"title"/g)].at(-1)?.index;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { errorText } from '../error-text.ts';
|
|
2
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { errorText } from '../error-text.ts';
|
|
3
3
|
import { claudeArgs, claudeEnv, promptPrefix } from '../providers/claude.ts';
|
|
4
4
|
import type { ApprovalDecision, BackendHost, BackendLaunch, ChatBackend, TurnInput } from './backend.ts';
|
|
5
5
|
import { ChatChild } from './chat-process.ts';
|
|
@@ -71,12 +71,10 @@ export class ClaudeBackend implements ChatBackend {
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
sendTurn(input: TurnInput): void {
|
|
74
|
-
// The prefix is what the CLI must see first (ultrathink), then the note about the links, then what was typed.
|
|
75
|
-
const prefix = `${promptPrefix(this.launch.selection)}${input.preamble === null ? '' : `${input.preamble}\n\n`}`;
|
|
76
74
|
const promptId = input.promptId ?? randomUUID();
|
|
77
75
|
this.active = true;
|
|
78
76
|
this.protocol.beginPrompt(promptId);
|
|
79
|
-
this.write(
|
|
77
|
+
this.write(this.userMessage(input, promptId));
|
|
80
78
|
}
|
|
81
79
|
|
|
82
80
|
steerTurn(input: TurnInput): Promise<boolean> {
|
|
@@ -88,8 +86,7 @@ export class ClaudeBackend implements ChatBackend {
|
|
|
88
86
|
this.steers.set(promptId, { resolve, reject });
|
|
89
87
|
this.protocol.beginSteer(promptId);
|
|
90
88
|
try {
|
|
91
|
-
|
|
92
|
-
this.write(buildUserMessage({ text: input.text, attachments: input.attachments, prefix, skills: input.skills, promptId }));
|
|
89
|
+
this.write(this.userMessage(input, promptId));
|
|
93
90
|
} catch (error) {
|
|
94
91
|
this.steers.delete(promptId);
|
|
95
92
|
reject(error instanceof Error ? error : new Error(errorText(error)));
|
|
@@ -147,6 +144,12 @@ export class ClaudeBackend implements ChatBackend {
|
|
|
147
144
|
return child?.end() ?? Promise.resolve();
|
|
148
145
|
}
|
|
149
146
|
|
|
147
|
+
private userMessage(input: TurnInput, promptId: string): Record<string, unknown> {
|
|
148
|
+
// What the CLI must see first (ultrathink), then the preamble, then what was typed.
|
|
149
|
+
const prefix = `${promptPrefix(this.launch.selection)}${input.preamble === null ? '' : `${input.preamble}\n\n`}`;
|
|
150
|
+
return buildUserMessage({ text: input.text, attachments: input.attachments, prefix, skills: input.skills, promptId });
|
|
151
|
+
}
|
|
152
|
+
|
|
150
153
|
private async readLines(child: ChatChild): Promise<void> {
|
|
151
154
|
const reader = child.process.stdout.getReader();
|
|
152
155
|
const decoder = new TextDecoder();
|
package/src/chat/claude-title.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { readdir, stat } from 'node:fs/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
-
import { usageRoots } from '../usage/roots.ts';
|
|
5
4
|
import { cleanTitle, readLines } from '../title-file.ts';
|
|
5
|
+
import { usageRoots } from '../usage/roots.ts';
|
|
6
6
|
|
|
7
7
|
interface Progress {
|
|
8
8
|
offset: number;
|
|
@@ -5,8 +5,8 @@ import type { ChatHistoryResult, ChatItem } from '@adecore/agent-contracts';
|
|
|
5
5
|
import { readLines } from '../title-file.ts';
|
|
6
6
|
import { ClaudeProtocol } from './claude-protocol.ts';
|
|
7
7
|
import { readingThread, settledReading } from './subagent-projection.ts';
|
|
8
|
-
import type { ChatThread } from './thread.ts';
|
|
9
8
|
import type { ThreadProjector } from './projector.ts';
|
|
9
|
+
import type { ChatThread } from './thread.ts';
|
|
10
10
|
|
|
11
11
|
const CHUNK_BYTES = 4 * 1024 * 1024;
|
|
12
12
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { accessSync, constants } from 'node:fs';
|
|
2
2
|
import { attachmentImageMime, type ChatSkill } from '@adecore/agent-contracts';
|
|
3
|
+
import { errorText } from '../error-text.ts';
|
|
3
4
|
import { codexServiceTier, codexThreadOptions } from '../providers/codex.ts';
|
|
4
5
|
import type { ApprovalDecision, BackendEvent, BackendHost, BackendLaunch, ChatBackend, TurnInput } from './backend.ts';
|
|
5
6
|
import { CodexProtocol } from './codex-protocol.ts';
|
|
6
|
-
import { attachmentNote } from './input.ts';
|
|
7
7
|
import { CodexTransport, DEFAULT_CODEX_CLIENT, type CodexClientInfo, type CodexFrame } from './codex-transport.ts';
|
|
8
|
-
import {
|
|
8
|
+
import { attachmentNote } from './input.ts';
|
|
9
9
|
|
|
10
10
|
function textInput(text: string) {
|
|
11
11
|
return [{ type: 'text', text, text_elements: [] }];
|
|
@@ -46,18 +46,15 @@ export interface CodexBackendOptions {
|
|
|
46
46
|
*/
|
|
47
47
|
export class CodexBackend implements ChatBackend {
|
|
48
48
|
readonly acknowledgesTurns = true;
|
|
49
|
-
private promptId: string | null = null;
|
|
50
49
|
private readonly launch: BackendLaunch;
|
|
51
|
-
private imageWrites: Promise<void> | null = null;
|
|
52
50
|
private readonly host: BackendHost;
|
|
53
51
|
private readonly protocol: CodexProtocol;
|
|
54
52
|
private readonly client: CodexClientInfo;
|
|
53
|
+
private promptId: string | null = null;
|
|
54
|
+
private imageWrites: Promise<void> | null = null;
|
|
55
55
|
private transport: CodexTransport | null = null;
|
|
56
56
|
private threadId = '';
|
|
57
|
-
|
|
58
|
-
* Codex 0.154 ignores developer instructions on `thread/resume` (measured), so a thread started
|
|
59
|
-
* before anything was linked would never hear of its links; the first prompt of a resumed process says so instead.
|
|
60
|
-
*/
|
|
57
|
+
// Codex 0.154 ignores developer instructions on `thread/resume` (measured), so the first prompt of a resumed process carries them.
|
|
61
58
|
private contextPending: string | null = null;
|
|
62
59
|
private imageInputSupported: boolean | null = null;
|
|
63
60
|
/* A turn asked for that Codex has not named yet; `turn/interrupt` needs that name. */
|
|
@@ -172,7 +172,7 @@ function elicitationInput(meta: Frame): Frame {
|
|
|
172
172
|
return input;
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
-
//
|
|
175
|
+
// The latest reset of every spent window; one without a known reset leaves resuming to a person.
|
|
176
176
|
function spentUntil(windows: readonly UsageWindow[]): number | null {
|
|
177
177
|
const spent = windows.filter((window) => window.used >= 1);
|
|
178
178
|
if (spent.length === 0 || spent.some((window) => window.resetsAt === null)) {
|
package/src/chat/codex-thread.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { ChatItem } from '@adecore/agent-contracts';
|
|
2
|
+
import { errorText } from '../error-text.ts';
|
|
2
3
|
import type { SpawnChatProcess } from './chat-process.ts';
|
|
3
4
|
import { CodexProtocol } from './codex-protocol.ts';
|
|
4
5
|
import { CodexTransport, DEFAULT_CODEX_CLIENT, type CodexClientInfo } from './codex-transport.ts';
|
|
5
6
|
import { ChatError } from './errors.ts';
|
|
6
7
|
import { readingThread, settledReading } from './subagent-projection.ts';
|
|
7
|
-
import { errorText } from '../error-text.ts';
|
|
8
8
|
|
|
9
9
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
10
10
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
@@ -116,14 +116,15 @@ async function nthTurnId(transport: CodexTransport, threadId: string, turns: num
|
|
|
116
116
|
let seen = 0;
|
|
117
117
|
let cursor: string | null = null;
|
|
118
118
|
do {
|
|
119
|
-
const page =
|
|
120
|
-
|
|
121
|
-
|
|
119
|
+
const page = parseThreadItemsPage(
|
|
120
|
+
await transport.request('thread/turns/list', { threadId, limit: 100, sortDirection: 'asc', ...(cursor === null ? {} : { cursor }) })
|
|
121
|
+
);
|
|
122
|
+
for (const turn of page.entries) {
|
|
122
123
|
if (++seen === turns) {
|
|
123
124
|
return typeof turn.id === 'string' ? turn.id : null;
|
|
124
125
|
}
|
|
125
126
|
}
|
|
126
|
-
cursor =
|
|
127
|
+
cursor = page.nextCursor;
|
|
127
128
|
} while (cursor !== null);
|
|
128
129
|
return null;
|
|
129
130
|
}
|
|
@@ -15,9 +15,8 @@ interface Held {
|
|
|
15
15
|
|
|
16
16
|
/*
|
|
17
17
|
* What each connected client says a new chat starts with, for the chats the host starts with no
|
|
18
|
-
* client mounting them. Held per
|
|
19
|
-
*
|
|
20
|
-
* host's own defaults.
|
|
18
|
+
* client mounting them. Held per client only, so with nobody connected a chat gets the host's own
|
|
19
|
+
* defaults rather than the pick of a person who left.
|
|
21
20
|
*/
|
|
22
21
|
export class ComposerPreferences {
|
|
23
22
|
private readonly byClient = new Map<string, Held>();
|
package/src/chat/limit-resume.ts
CHANGED
|
@@ -5,13 +5,8 @@ export const LIMIT_RETRY_DELAYS_MS: readonly number[] = [60_000, 5 * 60_000, 15
|
|
|
5
5
|
|
|
6
6
|
/* The last turn of a thread, when it stopped on a limit. */
|
|
7
7
|
export function limitedTurn(items: readonly ChatItem[]): (ChatTurnItem & { limit: ChatTurnLimit }) | null {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
if (item.kind === 'turn') {
|
|
11
|
-
return item.state === 'error' && item.limit !== undefined ? (item as ChatTurnItem & { limit: ChatTurnLimit }) : null;
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
return null;
|
|
8
|
+
const last = items.findLast((item) => item.kind === 'turn');
|
|
9
|
+
return last?.kind === 'turn' && last.state === 'error' && last.limit !== undefined ? (last as ChatTurnItem & { limit: ChatTurnLimit }) : null;
|
|
15
10
|
}
|
|
16
11
|
|
|
17
12
|
/* How many turns in a row, up to and including this one, stopped on a limit: the tries a person did not step in between. */
|
|
@@ -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
|
+
}
|
package/src/chat/thread.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import type { AgentStatus, ChatApprovalItem, ChatEvent, ChatHistoryResult, ChatInfo, ChatItem, ChatQuestionItem } from '@adecore/agent-contracts';
|
|
2
3
|
import { ChatError } from './errors.ts';
|
|
3
4
|
import { requestSummaries } from './request-summary.ts';
|
|
4
|
-
import type { AgentStatus, ChatApprovalItem, ChatEvent, ChatHistoryResult, ChatInfo, ChatItem, ChatQuestionItem } from '@adecore/agent-contracts';
|
|
5
5
|
|
|
6
6
|
/*
|
|
7
7
|
* The state of one chat as the client sees it. Every mutation answers the event that describes
|