@adecore/agents 0.31.3 → 0.31.5
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/acp-backend.d.ts +7 -0
- package/dist/chat/acp-backend.js +112 -21
- package/dist/chat/acp-protocol.d.ts +18 -5
- package/dist/chat/acp-protocol.js +53 -12
- package/dist/chat/index.d.ts +1 -1
- package/dist/providers/accounts/acp-account.d.ts +1 -0
- package/dist/providers/accounts/acp-account.js +25 -3
- package/dist/providers/acp-agent-state.d.ts +35 -0
- package/dist/providers/acp-agent-state.js +170 -0
- package/dist/providers/acp-provider.d.ts +2 -0
- package/dist/providers/acp-provider.js +29 -13
- package/dist/providers/catalog.d.ts +4 -2
- package/dist/providers/catalog.js +18 -5
- package/dist/providers/index.d.ts +1 -0
- package/dist/providers/index.js +1 -0
- package/dist/providers/provider.d.ts +1 -0
- package/dist/providers/registry.d.ts +1 -0
- package/dist/providers/registry.js +9 -0
- package/dist/testing/fake-acp.d.ts +4 -0
- package/dist/testing/fake-acp.js +33 -5
- package/package.json +3 -3
- package/src/chat/acp-backend.ts +120 -20
- package/src/chat/acp-protocol.ts +79 -14
- package/src/chat/index.ts +2 -0
- package/src/providers/accounts/acp-account.ts +34 -3
- package/src/providers/acp-agent-state.ts +206 -0
- package/src/providers/acp-provider.ts +32 -15
- package/src/providers/catalog.ts +23 -6
- package/src/providers/index.ts +1 -0
- package/src/providers/provider.ts +2 -0
- package/src/providers/registry.ts +10 -0
- package/src/testing/fake-acp.ts +43 -5
|
@@ -17,6 +17,8 @@ export interface AcpAgentSpec {
|
|
|
17
17
|
model?(selection: ModelSelection): string | null;
|
|
18
18
|
onSession?(info: AcpSessionInfo): void;
|
|
19
19
|
onUsage?(usage: AcpUsageReport): void;
|
|
20
|
+
onContextReport?(): void;
|
|
21
|
+
onCommands?(commands: string[]): void;
|
|
20
22
|
autoAllow?(request: AcpPermissionRequest): boolean;
|
|
21
23
|
}
|
|
22
24
|
export interface AcpUsageReport extends AcpTurnUsage {
|
|
@@ -37,6 +39,9 @@ export declare class AcpBackend implements ChatBackend {
|
|
|
37
39
|
private contextPending;
|
|
38
40
|
private prompt;
|
|
39
41
|
private model;
|
|
42
|
+
private contextReported;
|
|
43
|
+
private commands;
|
|
44
|
+
private early;
|
|
40
45
|
constructor(launch: BackendLaunch, host: BackendHost, spec: AcpAgentSpec);
|
|
41
46
|
get running(): boolean;
|
|
42
47
|
get pid(): number | null;
|
|
@@ -49,6 +54,7 @@ export declare class AcpBackend implements ChatBackend {
|
|
|
49
54
|
private applyModel;
|
|
50
55
|
private applyThoughtLevel;
|
|
51
56
|
private promptBlocks;
|
|
57
|
+
private mentionBlocks;
|
|
52
58
|
sendTurn(input: TurnInput): void;
|
|
53
59
|
compact(): void;
|
|
54
60
|
interrupt(): void;
|
|
@@ -60,6 +66,7 @@ export declare class AcpBackend implements ChatBackend {
|
|
|
60
66
|
private respond;
|
|
61
67
|
private fail;
|
|
62
68
|
private handleFrame;
|
|
69
|
+
private holdEarly;
|
|
63
70
|
private hostAllows;
|
|
64
71
|
private handleExit;
|
|
65
72
|
private report;
|
package/dist/chat/acp-backend.js
CHANGED
|
@@ -1,13 +1,22 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
1
|
+
import { readFileSync, realpathSync, statSync } from 'node:fs';
|
|
2
|
+
import { isAbsolute, relative, resolve } from 'node:path';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
2
4
|
import { attachmentImageMime } from '@adecore/agent-contracts';
|
|
3
5
|
import { errorText } from '../error-text.js';
|
|
4
6
|
import { MODE_ORDER } from '../modes.js';
|
|
5
|
-
import { ACP_AUTH_REQUIRED, ACP_PROTOCOL_VERSION, AcpProtocol, acpStopWords, autoAllows, parseInitialize, parseSessionInfo, promptUsageOf, resolveAcpMode, runtimeModeOf, sessionIdOf } from './acp-protocol.js';
|
|
7
|
+
import { ACP_AUTH_REQUIRED, ACP_PROTOCOL_VERSION, AcpProtocol, acpStopWords, autoAllows, parseInitialize, parseSessionInfo, promptUsageOf, resolveAcpMode, runtimeModeOf, sessionIdOf, sessionUpdateOf, thoughtLevelOf } from './acp-protocol.js';
|
|
6
8
|
import { attachmentNote } from './input.js';
|
|
7
9
|
import { JsonRpcError, JsonRpcTransport } from './json-rpc-transport.js';
|
|
8
10
|
export const DEFAULT_ACP_CLIENT = { name: 'agents', title: 'Agents', version: '0.1.0' };
|
|
9
11
|
// What `initialize` and a session request may take; a prompt and a login wait for as long as the agent runs.
|
|
10
12
|
const REQUEST_TIMEOUT_MS = 60_000;
|
|
13
|
+
// What a prompt carries of mentioned files; a larger file stays a path the agent opens with its own tools.
|
|
14
|
+
const MAX_EMBEDDED_FILE_BYTES = 256 * 1024;
|
|
15
|
+
const MAX_EMBEDDED_BYTES = 1024 * 1024;
|
|
16
|
+
function inside(folder, path) {
|
|
17
|
+
const rest = relative(folder, path);
|
|
18
|
+
return rest === '' || (!rest.startsWith('..') && !isAbsolute(rest));
|
|
19
|
+
}
|
|
11
20
|
function nameOf(client) {
|
|
12
21
|
return client.title ?? client.name;
|
|
13
22
|
}
|
|
@@ -31,6 +40,10 @@ export class AcpBackend {
|
|
|
31
40
|
contextPending = null;
|
|
32
41
|
prompt = null;
|
|
33
42
|
model = null;
|
|
43
|
+
contextReported = false;
|
|
44
|
+
commands = [];
|
|
45
|
+
// An agent may send updates for a new session before it answers with the session's id.
|
|
46
|
+
early = [];
|
|
34
47
|
constructor(launch, host, spec) {
|
|
35
48
|
this.launch = launch;
|
|
36
49
|
this.host = host;
|
|
@@ -86,9 +99,9 @@ export class AcpBackend {
|
|
|
86
99
|
this.capabilities = capabilities;
|
|
87
100
|
const info = await this.openSession(transport, capabilities);
|
|
88
101
|
const modeId = await this.applyMode(transport, info);
|
|
89
|
-
const model = await this.applyModel(transport, info);
|
|
102
|
+
const { model, thoughtLevel } = await this.applyModel(transport, info);
|
|
90
103
|
this.model = model;
|
|
91
|
-
await this.applyThoughtLevel(transport,
|
|
104
|
+
await this.applyThoughtLevel(transport, thoughtLevel);
|
|
92
105
|
this.spec.onSession?.(info);
|
|
93
106
|
this.emit({
|
|
94
107
|
type: 'session',
|
|
@@ -97,6 +110,9 @@ export class AcpBackend {
|
|
|
97
110
|
effectiveRuntimeMode: this.launch.runtimeMode,
|
|
98
111
|
...(modeId === null ? {} : { permissionMode: modeId })
|
|
99
112
|
});
|
|
113
|
+
for (const params of this.early.splice(0)) {
|
|
114
|
+
this.emitAll(this.protocol.update(this.sessionId, params));
|
|
115
|
+
}
|
|
100
116
|
}
|
|
101
117
|
sessionParams() {
|
|
102
118
|
const folders = this.launch.folders ?? [];
|
|
@@ -191,25 +207,29 @@ export class AcpBackend {
|
|
|
191
207
|
}
|
|
192
208
|
return resolved.modeId;
|
|
193
209
|
}
|
|
194
|
-
/*
|
|
210
|
+
/*
|
|
211
|
+
* Switches to the selected model where the agent offers it; answers the model the session runs and the
|
|
212
|
+
* reasoning effort it offers there, which may change with the model.
|
|
213
|
+
*/
|
|
195
214
|
async applyModel(transport, info) {
|
|
196
215
|
const models = info.models;
|
|
216
|
+
const thoughtLevel = info.thoughtLevel;
|
|
197
217
|
const wanted = this.spec.model ? this.spec.model(this.launch.selection) : this.launch.selection.model;
|
|
198
218
|
if (models === null || wanted === null || wanted === models.current) {
|
|
199
|
-
return models?.current ?? wanted;
|
|
219
|
+
return { model: models?.current ?? wanted, thoughtLevel };
|
|
200
220
|
}
|
|
201
221
|
if (!models.models.some((model) => model.id === wanted)) {
|
|
202
222
|
this.emit({ type: 'note', level: 'warning', text: `${this.spec.name} does not offer ${this.launch.modelName}. It runs ${models.current}.` });
|
|
203
|
-
return models.current;
|
|
223
|
+
return { model: models.current, thoughtLevel };
|
|
204
224
|
}
|
|
205
225
|
try {
|
|
206
226
|
if (models.configId === null) {
|
|
207
227
|
await transport.request('session/set_model', { sessionId: this.sessionId, modelId: wanted });
|
|
228
|
+
return { model: wanted, thoughtLevel };
|
|
208
229
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
}
|
|
212
|
-
return wanted;
|
|
230
|
+
const result = await transport.request('session/set_config_option', { sessionId: this.sessionId, configId: models.configId, value: wanted });
|
|
231
|
+
const after = thoughtLevelOf(result);
|
|
232
|
+
return { model: wanted, thoughtLevel: after === undefined ? thoughtLevel : after };
|
|
213
233
|
}
|
|
214
234
|
catch (error) {
|
|
215
235
|
if (!(error instanceof JsonRpcError)) {
|
|
@@ -220,20 +240,26 @@ export class AcpBackend {
|
|
|
220
240
|
level: 'warning',
|
|
221
241
|
text: `${this.spec.name} did not switch to ${this.launch.modelName} (${errorText(error)}). It runs ${models.current}.`
|
|
222
242
|
});
|
|
223
|
-
return models.current;
|
|
243
|
+
return { model: models.current, thoughtLevel };
|
|
224
244
|
}
|
|
225
245
|
}
|
|
226
|
-
|
|
227
|
-
|
|
246
|
+
/* Sets the selected effort where the session offers it; a model without one runs as it is. */
|
|
247
|
+
async applyThoughtLevel(transport, level) {
|
|
228
248
|
const effort = this.launch.selection.options.effort;
|
|
229
|
-
|
|
249
|
+
const choice = level?.choices.find((entry) => entry.id === effort);
|
|
250
|
+
if (level === null || choice === undefined || effort === level.current) {
|
|
230
251
|
return;
|
|
231
252
|
}
|
|
232
|
-
|
|
253
|
+
try {
|
|
254
|
+
await transport.request('session/set_config_option', { sessionId: this.sessionId, configId: level.configId, value: choice.id });
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
233
257
|
if (!(error instanceof JsonRpcError)) {
|
|
234
258
|
throw error;
|
|
235
259
|
}
|
|
236
|
-
|
|
260
|
+
const current = level.choices.find((entry) => entry.id === level.current)?.name ?? level.current;
|
|
261
|
+
this.emit({ type: 'note', level: 'warning', text: `${this.spec.name} kept its reasoning at ${current} instead of ${choice.name}.` });
|
|
262
|
+
}
|
|
237
263
|
}
|
|
238
264
|
promptBlocks(input) {
|
|
239
265
|
const sendsImages = this.capabilities?.images === true;
|
|
@@ -251,7 +277,51 @@ export class AcpBackend {
|
|
|
251
277
|
const named = input.attachments.filter((attachment) => !images.includes(attachment));
|
|
252
278
|
const parts = [this.contextPending, input.preamble, input.text, attachmentNote(named)].filter((part) => part !== null && part !== '');
|
|
253
279
|
this.contextPending = null;
|
|
254
|
-
return [{ type: 'text', text: parts.join('\n\n') }, ...blocks];
|
|
280
|
+
return [{ type: 'text', text: parts.join('\n\n') }, ...blocks, ...this.mentionBlocks(input.mentions)];
|
|
281
|
+
}
|
|
282
|
+
/*
|
|
283
|
+
* The text of each mentioned file as a `resource` block, for an agent that takes embedded context. Only a text file
|
|
284
|
+
* inside the chat's folders goes along, since a mention is a path anyone who may send could name; the rest stays
|
|
285
|
+
* in the prompt as the path it is.
|
|
286
|
+
*/
|
|
287
|
+
mentionBlocks(mentions) {
|
|
288
|
+
if (this.capabilities?.embeddedContext !== true || mentions.length === 0) {
|
|
289
|
+
return [];
|
|
290
|
+
}
|
|
291
|
+
const real = (path) => {
|
|
292
|
+
try {
|
|
293
|
+
return realpathSync(path);
|
|
294
|
+
}
|
|
295
|
+
catch {
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
const folders = [this.launch.cwd, ...(this.launch.folders ?? [])].flatMap((folder) => real(folder) ?? []);
|
|
300
|
+
const blocks = [];
|
|
301
|
+
let budget = MAX_EMBEDDED_BYTES;
|
|
302
|
+
for (const mention of new Set(mentions)) {
|
|
303
|
+
const path = real(resolve(this.launch.cwd, mention));
|
|
304
|
+
if (path === null || !folders.some((folder) => inside(folder, path))) {
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
let text;
|
|
308
|
+
try {
|
|
309
|
+
const stat = statSync(path);
|
|
310
|
+
if (!stat.isFile() || stat.size > MAX_EMBEDDED_FILE_BYTES || stat.size > budget) {
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
text = readFileSync(path, 'utf8');
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
if (text.includes('\u0000')) {
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
budget -= Buffer.byteLength(text);
|
|
322
|
+
blocks.push({ type: 'resource', resource: { uri: pathToFileURL(path).href, text } });
|
|
323
|
+
}
|
|
324
|
+
return blocks;
|
|
255
325
|
}
|
|
256
326
|
sendTurn(input) {
|
|
257
327
|
const transport = this.transport;
|
|
@@ -279,8 +349,12 @@ export class AcpBackend {
|
|
|
279
349
|
}
|
|
280
350
|
});
|
|
281
351
|
}
|
|
282
|
-
/* ACP has no call to fold the context
|
|
352
|
+
/* ACP has no call to fold the context, so an agent that lists a `compact` command gets it as a prompt. */
|
|
283
353
|
compact() {
|
|
354
|
+
if (this.commands.includes('compact')) {
|
|
355
|
+
this.sendTurn({ text: '/compact', preamble: null, attachments: [], mentions: [], skills: [] });
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
284
358
|
this.emit({ type: 'note', level: 'warning', text: `${this.spec.name} cannot fold its context on request.` });
|
|
285
359
|
this.emit({ type: 'turn.done', state: 'done', costUsd: 0 });
|
|
286
360
|
}
|
|
@@ -351,9 +425,11 @@ export class AcpBackend {
|
|
|
351
425
|
}
|
|
352
426
|
const id = typeof frame.id === 'number' || typeof frame.id === 'string' ? frame.id : null;
|
|
353
427
|
if (frame.method === 'session/update' && id === null) {
|
|
354
|
-
if (
|
|
355
|
-
this.
|
|
428
|
+
if (this.sessionId === '' || this.replaying) {
|
|
429
|
+
this.holdEarly(frame.params);
|
|
430
|
+
return;
|
|
356
431
|
}
|
|
432
|
+
this.emitAll(this.protocol.update(this.sessionId, frame.params));
|
|
357
433
|
return;
|
|
358
434
|
}
|
|
359
435
|
if (id === null) {
|
|
@@ -386,6 +462,13 @@ export class AcpBackend {
|
|
|
386
462
|
}
|
|
387
463
|
this.emitAll(permission.events);
|
|
388
464
|
}
|
|
465
|
+
/* Holds an update until the session is known; of a history a load replays, only the commands, since the thread has the rest. */
|
|
466
|
+
holdEarly(params) {
|
|
467
|
+
const update = sessionUpdateOf(params);
|
|
468
|
+
if (update !== null && (!this.replaying || update.commands !== null)) {
|
|
469
|
+
this.early.push(params);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
389
472
|
hostAllows(request) {
|
|
390
473
|
try {
|
|
391
474
|
return this.spec.autoAllow?.(request) === true;
|
|
@@ -420,6 +503,14 @@ export class AcpBackend {
|
|
|
420
503
|
if (event.type === 'model') {
|
|
421
504
|
this.model = event.model;
|
|
422
505
|
}
|
|
506
|
+
if (event.type === 'session' && event.slashCommands !== undefined) {
|
|
507
|
+
this.commands = event.slashCommands;
|
|
508
|
+
this.spec.onCommands?.(event.slashCommands);
|
|
509
|
+
}
|
|
510
|
+
if (event.type === 'usage' && event.contextWindow !== undefined && !this.contextReported) {
|
|
511
|
+
this.contextReported = true;
|
|
512
|
+
this.spec.onContextReport?.();
|
|
513
|
+
}
|
|
423
514
|
this.host.onEvent(event);
|
|
424
515
|
}
|
|
425
516
|
}
|
|
@@ -29,6 +29,7 @@ export interface AcpAgentCapabilities {
|
|
|
29
29
|
additionalDirectories: boolean;
|
|
30
30
|
closeSession: boolean;
|
|
31
31
|
images: boolean;
|
|
32
|
+
embeddedContext: boolean;
|
|
32
33
|
authMethods: string[];
|
|
33
34
|
agent: AcpAgentInfo | null;
|
|
34
35
|
}
|
|
@@ -37,6 +38,7 @@ export interface AcpModel {
|
|
|
37
38
|
id: string;
|
|
38
39
|
name: string;
|
|
39
40
|
description: string | null;
|
|
41
|
+
contextWindow: number | null;
|
|
40
42
|
}
|
|
41
43
|
export interface AcpModelState {
|
|
42
44
|
current: string;
|
|
@@ -52,18 +54,29 @@ export interface AcpModeState {
|
|
|
52
54
|
modes: AcpMode[];
|
|
53
55
|
configId: string | null;
|
|
54
56
|
}
|
|
57
|
+
export interface AcpChoice {
|
|
58
|
+
id: string;
|
|
59
|
+
name: string;
|
|
60
|
+
description: string | null;
|
|
61
|
+
}
|
|
62
|
+
export interface AcpThoughtLevel {
|
|
63
|
+
configId: string;
|
|
64
|
+
current: string;
|
|
65
|
+
choices: AcpChoice[];
|
|
66
|
+
}
|
|
55
67
|
export interface AcpSessionInfo {
|
|
56
68
|
sessionId: string;
|
|
57
69
|
agent: AcpAgentInfo | null;
|
|
58
70
|
models: AcpModelState | null;
|
|
59
71
|
modes: AcpModeState | null;
|
|
60
|
-
thoughtLevel:
|
|
61
|
-
configId: string;
|
|
62
|
-
current: string;
|
|
63
|
-
values: string[];
|
|
64
|
-
} | null;
|
|
72
|
+
thoughtLevel: AcpThoughtLevel | null;
|
|
65
73
|
}
|
|
74
|
+
export declare function thoughtLevelOf(result: unknown): AcpThoughtLevel | null | undefined;
|
|
66
75
|
export declare function parseSessionInfo(result: unknown, sessionId: string, agent: AcpAgentInfo | null): AcpSessionInfo;
|
|
76
|
+
export declare function sessionUpdateOf(params: unknown): {
|
|
77
|
+
sessionId: string;
|
|
78
|
+
commands: string[] | null;
|
|
79
|
+
} | null;
|
|
67
80
|
export declare function sessionIdOf(result: unknown): string | null;
|
|
68
81
|
export type AcpModeMap = Partial<Record<RuntimeMode, readonly string[]>>;
|
|
69
82
|
export declare function resolveAcpMode(requested: RuntimeMode, map: AcpModeMap, offered: readonly string[]): {
|
|
@@ -75,7 +75,7 @@ const ModeStateSchema = z.looseObject({
|
|
|
75
75
|
availableModes: z.array(z.unknown())
|
|
76
76
|
});
|
|
77
77
|
const ModeSchema = z.looseObject({ id: z.string(), name: z.string() });
|
|
78
|
-
const SelectValueSchema = z.looseObject({ value: z.string(), name: z.string() });
|
|
78
|
+
const SelectValueSchema = z.looseObject({ value: z.string(), name: z.string(), description: z.string().nullish(), _meta: z.unknown().optional() });
|
|
79
79
|
const ConfigOptionSchema = z.looseObject({
|
|
80
80
|
id: z.string(),
|
|
81
81
|
name: z.string(),
|
|
@@ -89,14 +89,19 @@ const ModelStateSchema = z.looseObject({
|
|
|
89
89
|
currentModelId: z.string(),
|
|
90
90
|
availableModels: z.array(z.unknown())
|
|
91
91
|
});
|
|
92
|
-
const ModelEntrySchema = z.looseObject({ modelId: z.string(), name: z.string(), description: z.string().nullish() });
|
|
92
|
+
const ModelEntrySchema = z.looseObject({ modelId: z.string(), name: z.string(), description: z.string().nullish(), _meta: z.unknown().optional() });
|
|
93
|
+
/* The context window an agent names for a model in the model's `_meta` (`totalContextTokens`, not part of the protocol); null where it names none. */
|
|
94
|
+
function contextWindowOf(meta) {
|
|
95
|
+
const tokens = isRecord(meta) ? meta.totalContextTokens : undefined;
|
|
96
|
+
return typeof tokens === 'number' && Number.isSafeInteger(tokens) && tokens > 0 ? tokens : null;
|
|
97
|
+
}
|
|
93
98
|
const AuthMethodSchema = z.looseObject({ id: z.string(), name: z.string() });
|
|
94
99
|
const InitializeResponseSchema = z.looseObject({
|
|
95
100
|
protocolVersion: z.number(),
|
|
96
101
|
agentCapabilities: z
|
|
97
102
|
.looseObject({
|
|
98
103
|
loadSession: z.boolean().optional(),
|
|
99
|
-
promptCapabilities: z.looseObject({ image: z.boolean().optional() }).optional(),
|
|
104
|
+
promptCapabilities: z.looseObject({ image: z.boolean().optional(), embeddedContext: z.boolean().optional() }).optional(),
|
|
100
105
|
sessionCapabilities: z
|
|
101
106
|
.looseObject({
|
|
102
107
|
resume: z.unknown().optional(),
|
|
@@ -132,6 +137,7 @@ export function parseInitialize(result) {
|
|
|
132
137
|
additionalDirectories: sessions?.additionalDirectories !== undefined && sessions.additionalDirectories !== null,
|
|
133
138
|
closeSession: sessions?.close !== undefined && sessions.close !== null,
|
|
134
139
|
images: capabilities?.promptCapabilities?.image === true,
|
|
140
|
+
embeddedContext: capabilities?.promptCapabilities?.embeddedContext === true,
|
|
135
141
|
authMethods: validItems(AuthMethodSchema, parsed.data.authMethods).map((method) => method.id),
|
|
136
142
|
agent: agent ? { name: agent.name, title: agent.title ?? null, version: agent.version } : null
|
|
137
143
|
};
|
|
@@ -144,6 +150,18 @@ function selectValues(option) {
|
|
|
144
150
|
function selectOption(options, category) {
|
|
145
151
|
return options.find((option) => option.type === 'select' && option.category === category && typeof option.currentValue === 'string') ?? null;
|
|
146
152
|
}
|
|
153
|
+
function thoughtOption(options) {
|
|
154
|
+
return (selectOption(options, 'thought_level') ??
|
|
155
|
+
options.find((option) => option.type === 'select' && option.id === 'reasoning_effort' && typeof option.currentValue === 'string') ??
|
|
156
|
+
null);
|
|
157
|
+
}
|
|
158
|
+
/* The reasoning effort in a response that carries the session's config options; undefined when it carries none. */
|
|
159
|
+
export function thoughtLevelOf(result) {
|
|
160
|
+
if (!isRecord(result) || !Array.isArray(result.configOptions)) {
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
return sessionState(validItems(ConfigOptionSchema, result.configOptions), null, null).thoughtLevel;
|
|
164
|
+
}
|
|
147
165
|
export function parseSessionInfo(result, sessionId, agent) {
|
|
148
166
|
const record = isRecord(result) ? result : {};
|
|
149
167
|
const options = validItems(ConfigOptionSchema, Array.isArray(record.configOptions) ? record.configOptions : []);
|
|
@@ -152,14 +170,19 @@ export function parseSessionInfo(result, sessionId, agent) {
|
|
|
152
170
|
function sessionState(options, modes, models) {
|
|
153
171
|
const modelOption = selectOption(options, 'model');
|
|
154
172
|
const modeOption = selectOption(options, 'mode');
|
|
155
|
-
const
|
|
173
|
+
const thought = thoughtOption(options);
|
|
156
174
|
const modeState = ModeStateSchema.safeParse(modes);
|
|
157
175
|
const modelState = ModelStateSchema.safeParse(models);
|
|
158
176
|
return {
|
|
159
177
|
models: modelOption
|
|
160
178
|
? {
|
|
161
179
|
current: modelOption.currentValue,
|
|
162
|
-
models: selectValues(modelOption).map((value) => ({
|
|
180
|
+
models: selectValues(modelOption).map((value) => ({
|
|
181
|
+
id: value.value,
|
|
182
|
+
name: value.name,
|
|
183
|
+
description: null,
|
|
184
|
+
contextWindow: contextWindowOf(value._meta)
|
|
185
|
+
})),
|
|
163
186
|
configId: modelOption.id
|
|
164
187
|
}
|
|
165
188
|
: modelState.success
|
|
@@ -168,7 +191,8 @@ function sessionState(options, modes, models) {
|
|
|
168
191
|
models: validItems(ModelEntrySchema, modelState.data.availableModels).map((model) => ({
|
|
169
192
|
id: model.modelId,
|
|
170
193
|
name: model.name,
|
|
171
|
-
description: model.description ?? null
|
|
194
|
+
description: model.description ?? null,
|
|
195
|
+
contextWindow: contextWindowOf(model._meta)
|
|
172
196
|
})),
|
|
173
197
|
configId: null
|
|
174
198
|
}
|
|
@@ -186,11 +210,30 @@ function sessionState(options, modes, models) {
|
|
|
186
210
|
configId: null
|
|
187
211
|
}
|
|
188
212
|
: null,
|
|
189
|
-
thoughtLevel:
|
|
190
|
-
? {
|
|
213
|
+
thoughtLevel: thought
|
|
214
|
+
? {
|
|
215
|
+
configId: thought.id,
|
|
216
|
+
current: thought.currentValue,
|
|
217
|
+
choices: selectValues(thought).map((value) => ({ id: value.value, name: value.name, description: value.description ?? null }))
|
|
218
|
+
}
|
|
191
219
|
: null
|
|
192
220
|
};
|
|
193
221
|
}
|
|
222
|
+
function commandNames(commands) {
|
|
223
|
+
return validItems(CommandSchema, Array.isArray(commands) ? commands : []).map((command) => command.name);
|
|
224
|
+
}
|
|
225
|
+
/* The session a `session/update` notification is for, and the commands it lists when it is an `available_commands_update`; null when it does not parse. */
|
|
226
|
+
export function sessionUpdateOf(params) {
|
|
227
|
+
const notification = SessionNotificationSchema.safeParse(params);
|
|
228
|
+
if (!notification.success) {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
const update = notification.data.update;
|
|
232
|
+
return {
|
|
233
|
+
sessionId: notification.data.sessionId,
|
|
234
|
+
commands: update.sessionUpdate === 'available_commands_update' ? commandNames(update.availableCommands) : null
|
|
235
|
+
};
|
|
236
|
+
}
|
|
194
237
|
export function sessionIdOf(result) {
|
|
195
238
|
const parsed = SessionResponseSchema.safeParse(result);
|
|
196
239
|
return parsed.success ? (parsed.data.sessionId ?? null) : null;
|
|
@@ -390,10 +433,8 @@ export class AcpProtocol {
|
|
|
390
433
|
}
|
|
391
434
|
case 'plan':
|
|
392
435
|
return this.plan(update);
|
|
393
|
-
case 'available_commands_update':
|
|
394
|
-
|
|
395
|
-
return [{ type: 'session', agentSessionId: null, model: null, slashCommands: commands.map((command) => command.name) }];
|
|
396
|
-
}
|
|
436
|
+
case 'available_commands_update':
|
|
437
|
+
return [{ type: 'session', agentSessionId: null, model: null, slashCommands: commandNames(update.availableCommands) }];
|
|
397
438
|
case 'current_mode_update':
|
|
398
439
|
return typeof update.currentModeId === 'string' ? [this.modeEvent(update.currentModeId)] : [];
|
|
399
440
|
case 'config_option_update': {
|
package/dist/chat/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { type AcpAgentSpec, type AcpClientInfo, type AcpUsageReport, createAcpBackend } from './acp-backend.ts';
|
|
2
|
-
export type { AcpAgentInfo, AcpModel, AcpModelState, AcpMode, AcpModeState, AcpPermissionRequest, AcpSessionInfo, AcpModeMap, AcpTurnUsage } from './acp-protocol.ts';
|
|
2
|
+
export type { AcpAgentInfo, AcpModel, AcpModelState, AcpMode, AcpModeState, AcpPermissionRequest, AcpSessionInfo, AcpModeMap, AcpChoice, AcpThoughtLevel, AcpTurnUsage } from './acp-protocol.ts';
|
|
3
3
|
export { AttachmentStore } from './attachment-store.ts';
|
|
4
4
|
export type { BackendLaunch, TurnInput, ApprovalDecision, BackendEvent, BackendHost, ChatBackend } from './backend.ts';
|
|
5
5
|
export { runningInBackground, type BackgroundWork } from './background-work.ts';
|
|
@@ -2,7 +2,7 @@ import { mkdtemp, rm } from 'node:fs/promises';
|
|
|
2
2
|
import { tmpdir } from 'node:os';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { DEFAULT_ACP_CLIENT } from '../../chat/acp-backend.js';
|
|
5
|
-
import { ACP_AUTH_REQUIRED, ACP_PROTOCOL_VERSION, parseInitialize, parseSessionInfo, sessionIdOf } from '../../chat/acp-protocol.js';
|
|
5
|
+
import { ACP_AUTH_REQUIRED, ACP_PROTOCOL_VERSION, parseInitialize, parseSessionInfo, sessionIdOf, sessionUpdateOf } from '../../chat/acp-protocol.js';
|
|
6
6
|
import { JsonRpcError, JsonRpcTransport } from '../../chat/json-rpc-transport.js';
|
|
7
7
|
import { ASK_TIMEOUT_MS } from './status.js';
|
|
8
8
|
const SIGNED_OUT = { signedIn: false, email: null, plan: null, organization: null };
|
|
@@ -35,6 +35,21 @@ export async function askAcpAccount(probe) {
|
|
|
35
35
|
const folder = await mkdtemp(join(tmpdir(), 'acp-account-'));
|
|
36
36
|
const client = probe.client ?? DEFAULT_ACP_CLIENT;
|
|
37
37
|
let transport = null;
|
|
38
|
+
// An agent may list its commands before it answers with the session's id.
|
|
39
|
+
const listed = new Map();
|
|
40
|
+
let sessionId = null;
|
|
41
|
+
const noteCommands = (frame) => {
|
|
42
|
+
const update = frame.method === 'session/update' && (frame.id === undefined || frame.id === null) ? sessionUpdateOf(frame.params) : null;
|
|
43
|
+
if (update === null || update.commands === null) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (sessionId === null) {
|
|
47
|
+
listed.set(update.sessionId, update.commands);
|
|
48
|
+
}
|
|
49
|
+
else if (update.sessionId === sessionId) {
|
|
50
|
+
probe.onCommands?.(update.commands);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
38
53
|
try {
|
|
39
54
|
const started = new JsonRpcTransport({
|
|
40
55
|
command: [...probe.command],
|
|
@@ -46,7 +61,10 @@ export async function askAcpAccount(probe) {
|
|
|
46
61
|
// The probe's own deadline covers every request.
|
|
47
62
|
requestTimeoutMs: null,
|
|
48
63
|
versionField: true,
|
|
49
|
-
onFrame: (frame) =>
|
|
64
|
+
onFrame: (frame) => {
|
|
65
|
+
noteCommands(frame);
|
|
66
|
+
refuseRequest(started, frame);
|
|
67
|
+
},
|
|
50
68
|
onExit: () => undefined
|
|
51
69
|
});
|
|
52
70
|
transport = started;
|
|
@@ -69,11 +87,15 @@ export async function askAcpAccount(probe) {
|
|
|
69
87
|
}
|
|
70
88
|
throw error;
|
|
71
89
|
}
|
|
72
|
-
|
|
90
|
+
sessionId = sessionIdOf(result);
|
|
73
91
|
if (sessionId === null) {
|
|
74
92
|
throw new Error(`${probe.name} opened a session without an id`);
|
|
75
93
|
}
|
|
76
94
|
probe.onSession?.(parseSessionInfo(result, sessionId, capabilities.agent));
|
|
95
|
+
const commands = listed.get(sessionId);
|
|
96
|
+
if (commands !== undefined) {
|
|
97
|
+
probe.onCommands?.(commands);
|
|
98
|
+
}
|
|
77
99
|
if (capabilities.closeSession) {
|
|
78
100
|
// The answer is known by now; an agent that cannot close it keeps an empty session, nothing worse.
|
|
79
101
|
await started.request('session/close', { sessionId }).catch(() => undefined);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type ProviderCapabilities } from '@adecore/agent-contracts';
|
|
2
|
+
import type { AcpSessionInfo } from '../chat/acp-protocol.ts';
|
|
3
|
+
import type { ModelCatalog } from './catalog.ts';
|
|
4
|
+
export interface AcpCatalogStore {
|
|
5
|
+
load(): unknown;
|
|
6
|
+
save(value: unknown): void;
|
|
7
|
+
}
|
|
8
|
+
export interface AcpAgentStateOptions {
|
|
9
|
+
catalog: ModelCatalog;
|
|
10
|
+
adoptModels: boolean;
|
|
11
|
+
store: AcpCatalogStore | null;
|
|
12
|
+
base: ProviderCapabilities;
|
|
13
|
+
overrides: Partial<ProviderCapabilities>;
|
|
14
|
+
}
|
|
15
|
+
export declare class AcpAgentState {
|
|
16
|
+
readonly capabilities: ProviderCapabilities;
|
|
17
|
+
private readonly options;
|
|
18
|
+
private readonly listeners;
|
|
19
|
+
private adopted;
|
|
20
|
+
private reportsContext;
|
|
21
|
+
private commands;
|
|
22
|
+
constructor(options: AcpAgentStateOptions);
|
|
23
|
+
onChange(listener: () => void): () => void;
|
|
24
|
+
session(info: AcpSessionInfo): void;
|
|
25
|
+
commandsListed(commands: readonly string[]): void;
|
|
26
|
+
contextReported(): void;
|
|
27
|
+
private restore;
|
|
28
|
+
private learned;
|
|
29
|
+
private refresh;
|
|
30
|
+
private adopt;
|
|
31
|
+
private settle;
|
|
32
|
+
private save;
|
|
33
|
+
private notify;
|
|
34
|
+
private stored;
|
|
35
|
+
}
|