@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
|
@@ -7,12 +7,12 @@ export interface CodexBackendOptions {
|
|
|
7
7
|
}
|
|
8
8
|
export declare class CodexBackend implements ChatBackend {
|
|
9
9
|
readonly acknowledgesTurns = true;
|
|
10
|
-
private promptId;
|
|
11
10
|
private readonly launch;
|
|
12
|
-
private imageWrites;
|
|
13
11
|
private readonly host;
|
|
14
12
|
private readonly protocol;
|
|
15
13
|
private readonly client;
|
|
14
|
+
private promptId;
|
|
15
|
+
private imageWrites;
|
|
16
16
|
private transport;
|
|
17
17
|
private threadId;
|
|
18
18
|
private contextPending;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { accessSync, constants } from 'node:fs';
|
|
2
2
|
import { attachmentImageMime } from '@adecore/agent-contracts';
|
|
3
|
+
import { errorText } from '../error-text.js';
|
|
3
4
|
import { codexServiceTier, codexThreadOptions } from '../providers/codex.js';
|
|
4
5
|
import { CodexProtocol } from './codex-protocol.js';
|
|
5
|
-
import { attachmentNote } from './input.js';
|
|
6
6
|
import { CodexTransport, DEFAULT_CODEX_CLIENT } from './codex-transport.js';
|
|
7
|
-
import {
|
|
7
|
+
import { attachmentNote } from './input.js';
|
|
8
8
|
function textInput(text) {
|
|
9
9
|
return [{ type: 'text', text, text_elements: [] }];
|
|
10
10
|
}
|
|
@@ -37,18 +37,15 @@ export function parseSkillsList(result) {
|
|
|
37
37
|
*/
|
|
38
38
|
export class CodexBackend {
|
|
39
39
|
acknowledgesTurns = true;
|
|
40
|
-
promptId = null;
|
|
41
40
|
launch;
|
|
42
|
-
imageWrites = null;
|
|
43
41
|
host;
|
|
44
42
|
protocol;
|
|
45
43
|
client;
|
|
44
|
+
promptId = null;
|
|
45
|
+
imageWrites = null;
|
|
46
46
|
transport = null;
|
|
47
47
|
threadId = '';
|
|
48
|
-
|
|
49
|
-
* Codex 0.154 ignores developer instructions on `thread/resume` (measured), so a thread started
|
|
50
|
-
* before anything was linked would never hear of its links; the first prompt of a resumed process says so instead.
|
|
51
|
-
*/
|
|
48
|
+
// Codex 0.154 ignores developer instructions on `thread/resume` (measured), so the first prompt of a resumed process carries them.
|
|
52
49
|
contextPending = null;
|
|
53
50
|
imageInputSupported = null;
|
|
54
51
|
/* A turn asked for that Codex has not named yet; `turn/interrupt` needs that name. */
|
|
@@ -130,7 +130,7 @@ function elicitationInput(meta) {
|
|
|
130
130
|
}
|
|
131
131
|
return input;
|
|
132
132
|
}
|
|
133
|
-
//
|
|
133
|
+
// The latest reset of every spent window; one without a known reset leaves resuming to a person.
|
|
134
134
|
function spentUntil(windows) {
|
|
135
135
|
const spent = windows.filter((window) => window.used >= 1);
|
|
136
136
|
if (spent.length === 0 || spent.some((window) => window.resetsAt === null)) {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
import { errorText } from '../error-text.js';
|
|
1
2
|
import { CodexProtocol } from './codex-protocol.js';
|
|
2
3
|
import { CodexTransport, DEFAULT_CODEX_CLIENT } from './codex-transport.js';
|
|
3
4
|
import { ChatError } from './errors.js';
|
|
4
5
|
import { readingThread, settledReading } from './subagent-projection.js';
|
|
5
|
-
import { errorText } from '../error-text.js';
|
|
6
6
|
function isRecord(value) {
|
|
7
7
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
8
8
|
}
|
|
@@ -85,14 +85,13 @@ async function nthTurnId(transport, threadId, turns) {
|
|
|
85
85
|
let seen = 0;
|
|
86
86
|
let cursor = null;
|
|
87
87
|
do {
|
|
88
|
-
const page = await transport.request('thread/turns/list', { threadId, limit: 100, sortDirection: 'asc', ...(cursor === null ? {} : { cursor }) });
|
|
89
|
-
|
|
90
|
-
for (const turn of data) {
|
|
88
|
+
const page = parseThreadItemsPage(await transport.request('thread/turns/list', { threadId, limit: 100, sortDirection: 'asc', ...(cursor === null ? {} : { cursor }) }));
|
|
89
|
+
for (const turn of page.entries) {
|
|
91
90
|
if (++seen === turns) {
|
|
92
91
|
return typeof turn.id === 'string' ? turn.id : null;
|
|
93
92
|
}
|
|
94
93
|
}
|
|
95
|
-
cursor =
|
|
94
|
+
cursor = page.nextCursor;
|
|
96
95
|
} while (cursor !== null);
|
|
97
96
|
return null;
|
|
98
97
|
}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* What each connected client says a new chat starts with, for the chats the host starts with no
|
|
3
|
-
* client mounting them. Held per
|
|
4
|
-
*
|
|
5
|
-
* host's own defaults.
|
|
3
|
+
* client mounting them. Held per client only, so with nobody connected a chat gets the host's own
|
|
4
|
+
* defaults rather than the pick of a person who left.
|
|
6
5
|
*/
|
|
7
6
|
export class ComposerPreferences {
|
|
8
7
|
byClient = new Map();
|
|
@@ -2,13 +2,8 @@
|
|
|
2
2
|
export const LIMIT_RETRY_DELAYS_MS = [60_000, 5 * 60_000, 15 * 60_000];
|
|
3
3
|
/* The last turn of a thread, when it stopped on a limit. */
|
|
4
4
|
export function limitedTurn(items) {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
if (item.kind === 'turn') {
|
|
8
|
-
return item.state === 'error' && item.limit !== undefined ? item : null;
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
return null;
|
|
5
|
+
const last = items.findLast((item) => item.kind === 'turn');
|
|
6
|
+
return last?.kind === 'turn' && last.state === 'error' && last.limit !== undefined ? last : null;
|
|
12
7
|
}
|
|
13
8
|
/* 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. */
|
|
14
9
|
function limitedInARow(items, turn) {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { uiFallbackText } from '@adecore/intelligent-ui/text';
|
|
2
|
+
/*
|
|
3
|
+
* An assistant's words as another agent or a person outside the thread reads them: each UI block becomes
|
|
4
|
+
* its text fallback, with the first query values when they were frozen for the block as stored. It works
|
|
5
|
+
* from the stored block ranges, so it holds whatever fence language the host compiled.
|
|
6
|
+
*/
|
|
7
|
+
export function readableAssistantText(item) {
|
|
8
|
+
if (!item.ui?.length) {
|
|
9
|
+
return item.text;
|
|
10
|
+
}
|
|
11
|
+
const blocks = item.ui.map((block) => {
|
|
12
|
+
const frozen = item.uiQueries?.blocks[block.id];
|
|
13
|
+
return frozen && frozen.revision === block.revision ? { ...block, fallback: frozen.fallback } : block;
|
|
14
|
+
});
|
|
15
|
+
return uiFallbackText(item.text, blocks);
|
|
16
|
+
}
|
|
@@ -3,6 +3,7 @@ import type { ChatUiLinkPayload, ChatUiLinkReading, ChatAssistantItem, ChatInfo,
|
|
|
3
3
|
import { type UiValue } from '@adecore/intelligent-ui';
|
|
4
4
|
import { type UiLinkTarget } from '@adecore/intelligent-ui/links';
|
|
5
5
|
import type { ChatSession } from './chat-session.ts';
|
|
6
|
+
import { ChatError } from './errors.ts';
|
|
6
7
|
export interface ChatUiSource {
|
|
7
8
|
args: z.ZodType;
|
|
8
9
|
result: z.ZodType;
|
|
@@ -14,11 +15,19 @@ export interface ChatUiHost {
|
|
|
14
15
|
sources: Readonly<Record<string, ChatUiSource>>;
|
|
15
16
|
link?(info: ChatInfo, access: unknown, target: UiLinkTarget): Promise<ChatUiLinkReading>;
|
|
16
17
|
}
|
|
18
|
+
declare class UiQueryError extends ChatError<string> {
|
|
19
|
+
readonly reason: string;
|
|
20
|
+
constructor(code: string, reason: string, message: string);
|
|
21
|
+
}
|
|
22
|
+
export declare class ChatUiRefusal extends UiQueryError {
|
|
23
|
+
constructor(code: string, reason: string);
|
|
24
|
+
}
|
|
17
25
|
export declare class ChatUiQueries {
|
|
18
26
|
readonly schemas: Record<string, z.ZodType>;
|
|
19
27
|
private readonly captures;
|
|
20
28
|
private readonly freezing;
|
|
21
29
|
private readonly cache;
|
|
30
|
+
private readonly latest;
|
|
22
31
|
private readonly reading;
|
|
23
32
|
private readonly attempts;
|
|
24
33
|
private readonly active;
|
|
@@ -33,6 +42,8 @@ export declare class ChatUiQueries {
|
|
|
33
42
|
choiceValues(session: ChatSession, payload: ChatUiChoicePayload | ChatUiLinkPayload): Promise<Record<string, unknown>>;
|
|
34
43
|
link(session: ChatSession, payload: ChatUiLinkPayload): Promise<ChatUiLinkReading>;
|
|
35
44
|
forget(chatId: string): void;
|
|
45
|
+
private evict;
|
|
36
46
|
private linkReading;
|
|
37
|
-
private
|
|
47
|
+
private failure;
|
|
38
48
|
}
|
|
49
|
+
export {};
|
package/dist/chat/ui-queries.js
CHANGED
|
@@ -1,18 +1,40 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { copyUiValue, UiBudget } from '@adecore/intelligent-ui';
|
|
2
|
+
import { copyUiValue, UI_HOST_LIMITS, UiBudget, uiMayReferenceHost } from '@adecore/intelligent-ui';
|
|
3
3
|
import { UiLinkResolutionSchema, uiLinkTargets } from '@adecore/intelligent-ui/links';
|
|
4
4
|
import { uiQueryArguments, uiQueryFallback } from '@adecore/intelligent-ui/query';
|
|
5
5
|
import { ChatError } from './errors.js';
|
|
6
|
+
/*
|
|
7
|
+
* A failure a client can say in its own words: `reason` is a code that stays the same across versions,
|
|
8
|
+
* the message is the fallback for a client that does not know the code.
|
|
9
|
+
*/
|
|
10
|
+
class UiQueryError extends ChatError {
|
|
11
|
+
reason;
|
|
12
|
+
constructor(code, reason, message) {
|
|
13
|
+
super(code, message);
|
|
14
|
+
this.reason = reason;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/*
|
|
18
|
+
* What a host's `authorize`, `read` or `link` throws to say why with a code: `code` reaches the reading's
|
|
19
|
+
* `code` and should stay the same across versions, `reason` is the plain sentence a client shows without it.
|
|
20
|
+
*/
|
|
21
|
+
export class ChatUiRefusal extends UiQueryError {
|
|
22
|
+
constructor(code, reason) {
|
|
23
|
+
super('refused-query', code, reason);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
6
26
|
const MAX_QUERY_BYTES = 64 * 1024;
|
|
7
27
|
const MAX_SNAPSHOT_BYTES = 128 * 1024;
|
|
8
|
-
const MAX_QUERIES = 8;
|
|
9
|
-
const REFRESH_MS = 10_000;
|
|
10
28
|
const READ_TIMEOUT_MS = 8_000;
|
|
29
|
+
// A changed input reads at once, but a query never reads more often than this across its inputs.
|
|
30
|
+
const INPUT_READ_MS = 250;
|
|
11
31
|
export class ChatUiQueries {
|
|
12
32
|
schemas;
|
|
13
33
|
captures = new Map();
|
|
14
34
|
freezing = new Set();
|
|
15
35
|
cache = new Map();
|
|
36
|
+
// The read id of the newest reading per cache key, which a remount or a second client is answered with.
|
|
37
|
+
latest = new Map();
|
|
16
38
|
reading = new Map();
|
|
17
39
|
attempts = new Map();
|
|
18
40
|
active = new Map();
|
|
@@ -23,14 +45,16 @@ export class ChatUiQueries {
|
|
|
23
45
|
this.schemas = Object.fromEntries(Object.entries(host.sources).map(([name, source]) => [name, source.args]));
|
|
24
46
|
}
|
|
25
47
|
observe(session, item, final) {
|
|
26
|
-
|
|
48
|
+
const key = `${session.id}:${item.id}`;
|
|
49
|
+
// Once a capture started, every delta of a streaming reply would scan its whole text again for nothing.
|
|
50
|
+
const captured = this.captures.has(key) || item.uiQueries?.authorChatId === session.id;
|
|
51
|
+
if (!captured && !uiMayReferenceHost(item.text) && !item.ui?.some((block) => Object.keys(block.queries).length)) {
|
|
27
52
|
return;
|
|
28
53
|
}
|
|
29
54
|
const capture = this.access(session, item);
|
|
30
55
|
if (!final || !item.ui?.length) {
|
|
31
56
|
return;
|
|
32
57
|
}
|
|
33
|
-
const key = `${session.id}:${item.id}`;
|
|
34
58
|
if (this.freezing.has(key)) {
|
|
35
59
|
return;
|
|
36
60
|
}
|
|
@@ -47,7 +71,7 @@ export class ChatUiQueries {
|
|
|
47
71
|
}
|
|
48
72
|
const readings = Object.create(null);
|
|
49
73
|
const values = Object.create(null);
|
|
50
|
-
for (const name of Object.keys(block.queries).slice(0,
|
|
74
|
+
for (const name of Object.keys(block.queries).slice(0, UI_HOST_LIMITS.queries)) {
|
|
51
75
|
const reading = await this.query(session, {
|
|
52
76
|
chatId: session.id,
|
|
53
77
|
itemId: item.id,
|
|
@@ -64,12 +88,12 @@ export class ChatUiQueries {
|
|
|
64
88
|
if (this.host.link) {
|
|
65
89
|
const targets = uiLinkTargets(block, {}, values);
|
|
66
90
|
const access = await this.access(session, current);
|
|
67
|
-
for (const [id, target] of Object.entries(targets).slice(0,
|
|
91
|
+
for (const [id, target] of Object.entries(targets).slice(0, UI_HOST_LIMITS.links)) {
|
|
68
92
|
try {
|
|
69
|
-
links[id] = this.linkReading(await this.host.link(session.info, access
|
|
93
|
+
links[id] = this.linkReading(await this.host.link(session.info, access, target));
|
|
70
94
|
}
|
|
71
95
|
catch (error) {
|
|
72
|
-
links[id] = { state: 'plain',
|
|
96
|
+
links[id] = { state: 'plain', ...this.failure(error) };
|
|
73
97
|
}
|
|
74
98
|
}
|
|
75
99
|
}
|
|
@@ -94,9 +118,9 @@ export class ChatUiQueries {
|
|
|
94
118
|
if (Buffer.byteLength(JSON.stringify(blocks)) > MAX_SNAPSHOT_BYTES) {
|
|
95
119
|
frozen.readings = Object.fromEntries(Object.keys(readings).map((name) => [
|
|
96
120
|
name,
|
|
97
|
-
{ state: 'failed', readAt: Date.now(), reason: 'The frozen query values are too large.' }
|
|
121
|
+
{ state: 'failed', readAt: Date.now(), code: 'snapshot-too-large', reason: 'The frozen query values are too large.' }
|
|
98
122
|
]));
|
|
99
|
-
frozen.links = Object.fromEntries(Object.keys(links).map((id) => [id, { state: 'plain', reason: 'This link must be checked again.' }]));
|
|
123
|
+
frozen.links = Object.fromEntries(Object.keys(links).map((id) => [id, { state: 'plain', code: 'link-unchecked', reason: 'This link must be checked again.' }]));
|
|
100
124
|
frozen.fallback = `${block.fallback.slice(0, 2048)}\nSources: ${names}\nThe frozen query values are too large.`;
|
|
101
125
|
}
|
|
102
126
|
if (Buffer.byteLength(JSON.stringify(blocks)) > MAX_SNAPSHOT_BYTES) {
|
|
@@ -108,6 +132,7 @@ export class ChatUiQueries {
|
|
|
108
132
|
.catch(() => { })
|
|
109
133
|
.finally(() => this.freezing.delete(key));
|
|
110
134
|
}
|
|
135
|
+
/* The writer's access for a reply, captured once; null where it was denied or is unavailable. */
|
|
111
136
|
access(session, item) {
|
|
112
137
|
const key = `${session.id}:${item.id}`;
|
|
113
138
|
const existing = this.captures.get(key);
|
|
@@ -115,10 +140,11 @@ export class ChatUiQueries {
|
|
|
115
140
|
return existing;
|
|
116
141
|
}
|
|
117
142
|
if (item.uiQueries?.authorChatId === session.id) {
|
|
118
|
-
return Promise.resolve(item.
|
|
143
|
+
return Promise.resolve(session.uiAccess(item.id) ?? null);
|
|
119
144
|
}
|
|
120
145
|
// A pending capture is persisted as denied, so a restart cannot widen the writer's rights.
|
|
121
|
-
session.
|
|
146
|
+
session.setUiAccess(item.id, null);
|
|
147
|
+
session.updateUiQueries(item.id, { authorChatId: session.id, blocks: {} });
|
|
122
148
|
const captured = this.host
|
|
123
149
|
.capture(session.info)
|
|
124
150
|
.then((access) => {
|
|
@@ -126,11 +152,10 @@ export class ChatUiQueries {
|
|
|
126
152
|
if (Buffer.byteLength(JSON.stringify(captured)) > 32 * 1024) {
|
|
127
153
|
throw new Error('The UI access snapshot is too large.');
|
|
128
154
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
return state;
|
|
155
|
+
session.setUiAccess(item.id, captured);
|
|
156
|
+
return captured;
|
|
132
157
|
})
|
|
133
|
-
.catch(() =>
|
|
158
|
+
.catch(() => null)
|
|
134
159
|
.finally(() => this.captures.delete(key));
|
|
135
160
|
this.captures.set(key, captured);
|
|
136
161
|
return captured;
|
|
@@ -139,18 +164,18 @@ export class ChatUiQueries {
|
|
|
139
164
|
const item = session.thread.get(payload.itemId);
|
|
140
165
|
const block = item?.kind === 'assistant' && !item.streaming && !item.parentToolUseId ? item.ui?.find((entry) => entry.id === payload.blockId) : undefined;
|
|
141
166
|
if (item?.kind !== 'assistant' || !block?.complete || block.revision !== payload.revision) {
|
|
142
|
-
throw new
|
|
167
|
+
throw new UiQueryError('stale-ui-block', 'block-stale', 'This completed UI block is no longer available.');
|
|
143
168
|
}
|
|
144
169
|
return { item, block };
|
|
145
170
|
}
|
|
146
171
|
stored(session, payload) {
|
|
147
172
|
const { item, block } = this.storedBlock(session, payload);
|
|
148
|
-
if (!Object.hasOwn(block.queries, payload.query) || Object.keys(block.queries).length >
|
|
149
|
-
throw new
|
|
173
|
+
if (!Object.hasOwn(block.queries, payload.query) || Object.keys(block.queries).length > UI_HOST_LIMITS.queries) {
|
|
174
|
+
throw new UiQueryError('refused-query', 'query-undeclared', 'This block does not declare an allowed query.');
|
|
150
175
|
}
|
|
151
176
|
const name = block.queries[payload.query].source;
|
|
152
177
|
if (!Object.hasOwn(this.host.sources, name)) {
|
|
153
|
-
throw new
|
|
178
|
+
throw new UiQueryError('refused-query', 'source-unregistered', 'This query source is not registered.');
|
|
154
179
|
}
|
|
155
180
|
return { item, block, source: this.host.sources[name] };
|
|
156
181
|
}
|
|
@@ -164,19 +189,18 @@ export class ChatUiQueries {
|
|
|
164
189
|
({ source, block, item } = this.stored(session, payload));
|
|
165
190
|
args = uiQueryArguments(block, payload.query, payload.values);
|
|
166
191
|
args = source.args.parse(args);
|
|
167
|
-
|
|
168
|
-
if (
|
|
169
|
-
throw new
|
|
192
|
+
authorAccess = await this.access(session, item);
|
|
193
|
+
if (authorAccess === null) {
|
|
194
|
+
throw new UiQueryError('refused-query', 'access-unavailable', 'The writer’s original access is unavailable.');
|
|
170
195
|
}
|
|
171
|
-
authorAccess = access.access;
|
|
172
196
|
await source.authorize(session.info, authorAccess, args);
|
|
173
197
|
}
|
|
174
198
|
catch (error) {
|
|
175
|
-
return { state: 'refused', readAt: Date.now(),
|
|
199
|
+
return { state: 'refused', readAt: Date.now(), ...this.failure(error) };
|
|
176
200
|
}
|
|
177
201
|
const key = JSON.stringify([session.id, item.id, block.id, block.revision, payload.query, args]);
|
|
178
|
-
const cached =
|
|
179
|
-
if (cached && Date.now() - cached.reading.readAt <
|
|
202
|
+
const cached = this.cache.get(this.latest.get(key) ?? '');
|
|
203
|
+
if (cached && Date.now() - cached.reading.readAt < UI_HOST_LIMITS.refreshMilliseconds) {
|
|
180
204
|
return cached.reading;
|
|
181
205
|
}
|
|
182
206
|
const pending = this.reading.get(key);
|
|
@@ -184,14 +208,17 @@ export class ChatUiQueries {
|
|
|
184
208
|
return pending;
|
|
185
209
|
}
|
|
186
210
|
const identity = JSON.stringify([session.id, item.id, block.id, block.revision, payload.query]);
|
|
187
|
-
|
|
188
|
-
|
|
211
|
+
const now = Date.now();
|
|
212
|
+
if (now - (this.attempts.get(key) ?? -Infinity) < UI_HOST_LIMITS.refreshMilliseconds ||
|
|
213
|
+
now - (this.attempts.get(identity) ?? -Infinity) < INPUT_READ_MS) {
|
|
214
|
+
return { state: 'failed', readAt: now, code: 'refresh-limit', reason: 'This query may refresh once every ten seconds.' };
|
|
189
215
|
}
|
|
190
216
|
if ((this.active.get(session.id) ?? 0) >= 2 || this.totalActive >= 16) {
|
|
191
|
-
return { state: 'failed', readAt:
|
|
217
|
+
return { state: 'failed', readAt: now, code: 'busy', reason: 'Too many UI queries are being read.' };
|
|
192
218
|
}
|
|
193
|
-
this.attempts.set(
|
|
194
|
-
|
|
219
|
+
this.attempts.set(key, now);
|
|
220
|
+
this.attempts.set(identity, now);
|
|
221
|
+
while (this.attempts.size > 256) {
|
|
195
222
|
this.attempts.delete(this.attempts.keys().next().value);
|
|
196
223
|
}
|
|
197
224
|
this.active.set(session.id, (this.active.get(session.id) ?? 0) + 1);
|
|
@@ -202,21 +229,24 @@ export class ChatUiQueries {
|
|
|
202
229
|
timeout.unref?.();
|
|
203
230
|
const read = Promise.race([
|
|
204
231
|
work,
|
|
205
|
-
new Promise((_, reject) => controller.signal.addEventListener('abort', () => reject(new
|
|
232
|
+
new Promise((_, reject) => controller.signal.addEventListener('abort', () => reject(new UiQueryError('refused-query', 'timed-out', 'The UI query timed out.')), {
|
|
233
|
+
once: true
|
|
234
|
+
}))
|
|
206
235
|
])
|
|
207
236
|
.then((result) => {
|
|
208
237
|
const value = copyUiValue(source.result.parse(result), new UiBudget());
|
|
209
238
|
if (Buffer.byteLength(JSON.stringify(value)) > MAX_QUERY_BYTES) {
|
|
210
|
-
throw new
|
|
239
|
+
throw new UiQueryError('refused-query', 'result-too-large', 'The UI query result is too large.');
|
|
211
240
|
}
|
|
212
241
|
const reading = { state: 'fresh', value, readId: randomUUID(), readAt: Date.now() };
|
|
213
242
|
this.cache.set(reading.readId, { key, chatId: session.id, reading });
|
|
243
|
+
this.latest.set(key, reading.readId);
|
|
214
244
|
while (this.cache.size > 128) {
|
|
215
|
-
this.
|
|
245
|
+
this.evict(this.cache.keys().next().value);
|
|
216
246
|
}
|
|
217
247
|
return reading;
|
|
218
248
|
})
|
|
219
|
-
.catch((error) => ({ state: 'failed', readAt: Date.now(),
|
|
249
|
+
.catch((error) => ({ state: 'failed', readAt: Date.now(), ...this.failure(error) }))
|
|
220
250
|
.finally(() => {
|
|
221
251
|
clearTimeout(timeout);
|
|
222
252
|
this.reading.delete(key);
|
|
@@ -237,31 +267,40 @@ export class ChatUiQueries {
|
|
|
237
267
|
this.reading.set(key, read);
|
|
238
268
|
return read;
|
|
239
269
|
}
|
|
270
|
+
/* The issued value of every query the block declares, each checked against the read id the person saw. */
|
|
240
271
|
async choiceValues(session, payload) {
|
|
241
272
|
const item = session.thread.get(payload.itemId);
|
|
242
273
|
const block = item?.kind === 'assistant' ? item.ui?.find((entry) => entry.id === payload.blockId) : undefined;
|
|
243
274
|
if (!block || item?.kind !== 'assistant' || ('choiceId' in payload && item.uiAnswers?.[block.id]?.revision === payload.revision)) {
|
|
244
275
|
return {};
|
|
245
276
|
}
|
|
277
|
+
const names = Object.keys(block.queries);
|
|
246
278
|
const values = Object.create(null);
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
279
|
+
if (names.length === 0) {
|
|
280
|
+
return values;
|
|
281
|
+
}
|
|
282
|
+
const access = await this.access(session, item);
|
|
283
|
+
if (access === null) {
|
|
284
|
+
throw new UiQueryError('refused-query', 'access-unavailable', 'The writer’s original access is unavailable.');
|
|
285
|
+
}
|
|
286
|
+
for (const name of names) {
|
|
287
|
+
const { source } = this.stored(session, { ...payload, query: name });
|
|
250
288
|
const args = source.args.parse(uiQueryArguments(block, name, payload.values));
|
|
251
|
-
|
|
252
|
-
|
|
289
|
+
await source.authorize(session.info, access, args);
|
|
290
|
+
const readId = payload.reads !== undefined && Object.hasOwn(payload.reads, name) ? payload.reads[name] : undefined;
|
|
253
291
|
const key = JSON.stringify([session.id, item.id, block.id, block.revision, name, args]);
|
|
254
|
-
const
|
|
255
|
-
const cached = this.cache.get(readId);
|
|
292
|
+
const cached = readId === undefined ? undefined : this.cache.get(readId);
|
|
256
293
|
const frozen = item.uiQueries?.blocks[block.id];
|
|
257
294
|
const snapshot = frozen?.revision === block.revision ? frozen?.readings[name] : undefined;
|
|
258
|
-
const reading =
|
|
259
|
-
?
|
|
260
|
-
:
|
|
261
|
-
?
|
|
262
|
-
:
|
|
295
|
+
const reading = readId === undefined
|
|
296
|
+
? undefined
|
|
297
|
+
: cached?.key === key
|
|
298
|
+
? cached.reading
|
|
299
|
+
: snapshot?.readId === readId && JSON.stringify(args) === JSON.stringify(source.args.parse(uiQueryArguments(block, name)))
|
|
300
|
+
? snapshot
|
|
301
|
+
: undefined;
|
|
263
302
|
if (!reading || reading.state !== 'fresh') {
|
|
264
|
-
throw new
|
|
303
|
+
throw new UiQueryError('stale-ui-query', 'stale-read', 'Refresh this query before sending its choice.');
|
|
265
304
|
}
|
|
266
305
|
values[name] = reading.value;
|
|
267
306
|
}
|
|
@@ -270,34 +309,41 @@ export class ChatUiQueries {
|
|
|
270
309
|
async link(session, payload) {
|
|
271
310
|
try {
|
|
272
311
|
if (!this.host.link) {
|
|
273
|
-
throw new
|
|
312
|
+
throw new UiQueryError('refused-query', 'links-unsupported', 'This host does not provide UI links.');
|
|
274
313
|
}
|
|
275
314
|
const { item, block } = this.storedBlock(session, payload);
|
|
276
315
|
if (!item.uiQueries) {
|
|
277
|
-
throw new
|
|
316
|
+
throw new UiQueryError('refused-query', 'access-unavailable', 'The writer’s original access is unavailable.');
|
|
278
317
|
}
|
|
279
318
|
const access = await this.access(session, item);
|
|
280
|
-
if (access
|
|
281
|
-
throw new
|
|
319
|
+
if (access === null) {
|
|
320
|
+
throw new UiQueryError('refused-query', 'access-unavailable', 'The writer’s original access is unavailable.');
|
|
282
321
|
}
|
|
283
322
|
const queries = await this.choiceValues(session, payload);
|
|
284
323
|
const target = uiLinkTargets(block, payload.values, queries)[payload.nodeId];
|
|
285
324
|
if (!target) {
|
|
286
|
-
throw new
|
|
325
|
+
throw new UiQueryError('refused-query', 'link-unsupported', 'This visible node is not a supported link.');
|
|
287
326
|
}
|
|
288
|
-
return this.linkReading(await this.host.link(session.info, access
|
|
327
|
+
return this.linkReading(await this.host.link(session.info, access, target));
|
|
289
328
|
}
|
|
290
329
|
catch (error) {
|
|
291
|
-
return { state: 'plain',
|
|
330
|
+
return { state: 'plain', ...this.failure(error) };
|
|
292
331
|
}
|
|
293
332
|
}
|
|
294
333
|
forget(chatId) {
|
|
295
334
|
for (const [id, cached] of this.cache) {
|
|
296
335
|
if (cached.chatId === chatId) {
|
|
297
|
-
this.
|
|
336
|
+
this.evict(id);
|
|
298
337
|
}
|
|
299
338
|
}
|
|
300
339
|
}
|
|
340
|
+
evict(readId) {
|
|
341
|
+
const cached = this.cache.get(readId);
|
|
342
|
+
this.cache.delete(readId);
|
|
343
|
+
if (cached && this.latest.get(cached.key) === readId) {
|
|
344
|
+
this.latest.delete(cached.key);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
301
347
|
linkReading(value) {
|
|
302
348
|
const reading = UiLinkResolutionSchema.parse(copyUiValue(value, new UiBudget()));
|
|
303
349
|
if (reading.state === 'chip' && !reading.target) {
|
|
@@ -305,7 +351,8 @@ export class ChatUiQueries {
|
|
|
305
351
|
}
|
|
306
352
|
return reading;
|
|
307
353
|
}
|
|
308
|
-
|
|
309
|
-
|
|
354
|
+
failure(error) {
|
|
355
|
+
const reason = (error instanceof Error ? error.message : 'This query could not be read.').slice(0, 240);
|
|
356
|
+
return error instanceof UiQueryError ? { reason, code: error.reason } : { reason };
|
|
310
357
|
}
|
|
311
358
|
}
|
|
@@ -59,6 +59,12 @@ function tooLarge(bytes) {
|
|
|
59
59
|
return refuse('visual-too-large', `The page is ${mebibytes(bytes)} and a visual may be at most ${mebibytes(VISUAL_LIMITS.bytes)}; ` +
|
|
60
60
|
'load libraries from a public CDN URL and refer to large images by their http(s) URL instead of inlining them');
|
|
61
61
|
}
|
|
62
|
+
function refuseOversized(html) {
|
|
63
|
+
const bytes = Buffer.byteLength(html, 'utf8');
|
|
64
|
+
if (bytes > VISUAL_LIMITS.bytes) {
|
|
65
|
+
tooLarge(bytes);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
62
68
|
/*
|
|
63
69
|
* The visuals of every chat. The list of a chat is one file beside its record under `<home>/chats`;
|
|
64
70
|
* each page is a file in the chat's attachment folder, `<id>.html`, so whatever serves an attachment
|
|
@@ -104,9 +110,7 @@ export class VisualStore {
|
|
|
104
110
|
if (html.trim() === '') {
|
|
105
111
|
refuse('visual-invalid', 'The page is empty; pass one self-contained HTML document');
|
|
106
112
|
}
|
|
107
|
-
|
|
108
|
-
tooLarge(Buffer.byteLength(html, 'utf8'));
|
|
109
|
-
}
|
|
113
|
+
refuseOversized(html);
|
|
110
114
|
return this.writeWorkspaceFile(chatId, name, html);
|
|
111
115
|
}
|
|
112
116
|
writePreview(chatId, png) {
|
|
@@ -116,9 +120,7 @@ export class VisualStore {
|
|
|
116
120
|
async publish(chatId, input) {
|
|
117
121
|
const { title, html, maxHeight, layout, heights } = checked(input);
|
|
118
122
|
// Before the bootstrap is added, so a page far too large is never copied first.
|
|
119
|
-
|
|
120
|
-
tooLarge(Buffer.byteLength(html, 'utf8'));
|
|
121
|
-
}
|
|
123
|
+
refuseOversized(html);
|
|
122
124
|
const page = Buffer.from(injectVisualBootstrap(html), 'utf8');
|
|
123
125
|
if (page.byteLength > VISUAL_LIMITS.bytes) {
|
|
124
126
|
tooLarge(page.byteLength);
|
package/dist/context/refusal.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* How every no reads: `refused`, the code a script branches on and the sentence a model reads, and
|
|
3
|
-
* under it a line per thing the actor can do instead.
|
|
4
|
-
*
|
|
5
|
-
* two, and everything under the first row is read as advice.
|
|
3
|
+
* under it a line per thing the actor can do instead. A line keeps its tabs but loses its newlines,
|
|
4
|
+
* since everything under the first row is read as advice.
|
|
6
5
|
*/
|
|
7
6
|
/* A field that goes into a tab-separated line; a tab or a newline in a title would split the row. */
|
|
8
7
|
export function field(value) {
|
|
@@ -19,10 +18,7 @@ export function refusalBody(code, message, lines = []) {
|
|
|
19
18
|
const [first, ...rest] = rows(code, message, lines);
|
|
20
19
|
return [`refused\t${first}`, ...rest].join('\n');
|
|
21
20
|
}
|
|
22
|
-
/*
|
|
23
|
-
* The same without the word that names it, for a host that adds the prefix in its CLI:
|
|
24
|
-
* The context CLI puts the prefix on, so a CLI of an older build still prints one refusal.
|
|
25
|
-
*/
|
|
21
|
+
/* Without the `refused` prefix, for a host whose CLI adds it, so a CLI of an older build still prints one refusal. */
|
|
26
22
|
export function refusalRows(code, message, lines = []) {
|
|
27
23
|
return rows(code, message, lines).join('\n');
|
|
28
24
|
}
|
package/dist/context/verb.js
CHANGED
|
@@ -223,11 +223,7 @@ export function lengthOf(input) {
|
|
|
223
223
|
export function requiredField(needs) {
|
|
224
224
|
return z.string({ error: needs }).min(1, needs);
|
|
225
225
|
}
|
|
226
|
-
/*
|
|
227
|
-
* What a refusal lists, or one line saying the set is empty. A refusal that promises the groups of a
|
|
228
|
-
* canvas and then prints nothing reads as a daemon that lost them, where the truth is that there are
|
|
229
|
-
* none, which is a different thing to do something about.
|
|
230
|
-
*/
|
|
226
|
+
/* A refusal that promises a list and prints nothing reads as a host that lost it, where there is none. */
|
|
231
227
|
export function orNote(lines, note) {
|
|
232
228
|
return lines.length === 0 ? [`note\t${note}`] : lines;
|
|
233
229
|
}
|
package/dist/host/agent-host.js
CHANGED
|
@@ -10,8 +10,8 @@ function isAgentRequest(type) {
|
|
|
10
10
|
}
|
|
11
11
|
/*
|
|
12
12
|
* The chats of an app that has no daemon: it answers the agent requests over any `FramePort` and
|
|
13
|
-
* sends the agent events back on it, frames checked on arrival. One port is one client.
|
|
14
|
-
* goes on at the next start through its CLI's own session.
|
|
13
|
+
* sends the agent events back on it, frames checked on arrival. One port is one client. A chat
|
|
14
|
+
* closed with the host goes on at the next start through its CLI's own session.
|
|
15
15
|
*/
|
|
16
16
|
export class AgentHost {
|
|
17
17
|
chats;
|
package/dist/host/handlers.js
CHANGED
|
@@ -32,7 +32,7 @@ export function chatHandlers(chats, providers) {
|
|
|
32
32
|
return {};
|
|
33
33
|
},
|
|
34
34
|
'chat.send': (payload) => chats.send(payload.chatId, payload.text, { delivery: payload.delivery, mentions: payload.mentions, skills: payload.skills, chats: payload.chats }, payload.attachments),
|
|
35
|
-
'chat.uiChoice': (payload) => chats.choose(payload),
|
|
35
|
+
'chat.uiChoice': (payload, clientId) => chats.choose(payload, clientId),
|
|
36
36
|
'ui.link': (payload, clientId) => chats.linkUi(payload, clientId),
|
|
37
37
|
'ui.query': (payload, clientId) => chats.queryUi(payload, clientId),
|
|
38
38
|
'chat.unqueue': (payload) => ({ message: chats.unqueue(payload.chatId, payload.messageId) }),
|
|
@@ -81,7 +81,6 @@ export function chatHandlers(chats, providers) {
|
|
|
81
81
|
'chat.list': () => ({ chats: chats.list() })
|
|
82
82
|
};
|
|
83
83
|
}
|
|
84
|
-
/* The accounts are the person's settings of this host. */
|
|
85
84
|
export function accountHandlers(accounts) {
|
|
86
85
|
return {
|
|
87
86
|
'accounts.list': () => accounts.snapshot(),
|