@adecore/agents 0.31.3 → 0.31.4
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.js +26 -16
- package/dist/chat/acp-protocol.d.ts +12 -5
- package/dist/chat/acp-protocol.js +20 -4
- package/dist/chat/index.d.ts +1 -1
- package/dist/providers/acp-provider.js +21 -1
- package/dist/providers/catalog.d.ts +2 -2
- package/dist/providers/catalog.js +6 -3
- package/dist/testing/fake-acp.d.ts +1 -0
- package/dist/testing/fake-acp.js +22 -1
- package/package.json +3 -3
- package/src/chat/acp-backend.ts +27 -15
- package/src/chat/acp-protocol.ts +39 -6
- package/src/chat/index.ts +2 -0
- package/src/providers/acp-provider.ts +23 -2
- package/src/providers/catalog.ts +7 -4
- package/src/testing/fake-acp.ts +26 -1
package/dist/chat/acp-backend.js
CHANGED
|
@@ -2,7 +2,7 @@ import { readFileSync } from 'node:fs';
|
|
|
2
2
|
import { attachmentImageMime } from '@adecore/agent-contracts';
|
|
3
3
|
import { errorText } from '../error-text.js';
|
|
4
4
|
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';
|
|
5
|
+
import { ACP_AUTH_REQUIRED, ACP_PROTOCOL_VERSION, AcpProtocol, acpStopWords, autoAllows, parseInitialize, parseSessionInfo, promptUsageOf, resolveAcpMode, runtimeModeOf, sessionIdOf, thoughtLevelOf } from './acp-protocol.js';
|
|
6
6
|
import { attachmentNote } from './input.js';
|
|
7
7
|
import { JsonRpcError, JsonRpcTransport } from './json-rpc-transport.js';
|
|
8
8
|
export const DEFAULT_ACP_CLIENT = { name: 'agents', title: 'Agents', version: '0.1.0' };
|
|
@@ -86,9 +86,9 @@ export class AcpBackend {
|
|
|
86
86
|
this.capabilities = capabilities;
|
|
87
87
|
const info = await this.openSession(transport, capabilities);
|
|
88
88
|
const modeId = await this.applyMode(transport, info);
|
|
89
|
-
const model = await this.applyModel(transport, info);
|
|
89
|
+
const { model, thoughtLevel } = await this.applyModel(transport, info);
|
|
90
90
|
this.model = model;
|
|
91
|
-
await this.applyThoughtLevel(transport,
|
|
91
|
+
await this.applyThoughtLevel(transport, thoughtLevel);
|
|
92
92
|
this.spec.onSession?.(info);
|
|
93
93
|
this.emit({
|
|
94
94
|
type: 'session',
|
|
@@ -191,25 +191,29 @@ export class AcpBackend {
|
|
|
191
191
|
}
|
|
192
192
|
return resolved.modeId;
|
|
193
193
|
}
|
|
194
|
-
/*
|
|
194
|
+
/*
|
|
195
|
+
* Switches to the selected model where the agent offers it; answers the model the session runs and the
|
|
196
|
+
* reasoning effort it offers there, which may change with the model.
|
|
197
|
+
*/
|
|
195
198
|
async applyModel(transport, info) {
|
|
196
199
|
const models = info.models;
|
|
200
|
+
const thoughtLevel = info.thoughtLevel;
|
|
197
201
|
const wanted = this.spec.model ? this.spec.model(this.launch.selection) : this.launch.selection.model;
|
|
198
202
|
if (models === null || wanted === null || wanted === models.current) {
|
|
199
|
-
return models?.current ?? wanted;
|
|
203
|
+
return { model: models?.current ?? wanted, thoughtLevel };
|
|
200
204
|
}
|
|
201
205
|
if (!models.models.some((model) => model.id === wanted)) {
|
|
202
206
|
this.emit({ type: 'note', level: 'warning', text: `${this.spec.name} does not offer ${this.launch.modelName}. It runs ${models.current}.` });
|
|
203
|
-
return models.current;
|
|
207
|
+
return { model: models.current, thoughtLevel };
|
|
204
208
|
}
|
|
205
209
|
try {
|
|
206
210
|
if (models.configId === null) {
|
|
207
211
|
await transport.request('session/set_model', { sessionId: this.sessionId, modelId: wanted });
|
|
212
|
+
return { model: wanted, thoughtLevel };
|
|
208
213
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
}
|
|
212
|
-
return wanted;
|
|
214
|
+
const result = await transport.request('session/set_config_option', { sessionId: this.sessionId, configId: models.configId, value: wanted });
|
|
215
|
+
const after = thoughtLevelOf(result);
|
|
216
|
+
return { model: wanted, thoughtLevel: after === undefined ? thoughtLevel : after };
|
|
213
217
|
}
|
|
214
218
|
catch (error) {
|
|
215
219
|
if (!(error instanceof JsonRpcError)) {
|
|
@@ -220,20 +224,26 @@ export class AcpBackend {
|
|
|
220
224
|
level: 'warning',
|
|
221
225
|
text: `${this.spec.name} did not switch to ${this.launch.modelName} (${errorText(error)}). It runs ${models.current}.`
|
|
222
226
|
});
|
|
223
|
-
return models.current;
|
|
227
|
+
return { model: models.current, thoughtLevel };
|
|
224
228
|
}
|
|
225
229
|
}
|
|
226
|
-
|
|
227
|
-
|
|
230
|
+
/* Sets the selected effort where the session offers it; a model without one runs as it is. */
|
|
231
|
+
async applyThoughtLevel(transport, level) {
|
|
228
232
|
const effort = this.launch.selection.options.effort;
|
|
229
|
-
|
|
233
|
+
const choice = level?.choices.find((entry) => entry.id === effort);
|
|
234
|
+
if (level === null || choice === undefined || effort === level.current) {
|
|
230
235
|
return;
|
|
231
236
|
}
|
|
232
|
-
|
|
237
|
+
try {
|
|
238
|
+
await transport.request('session/set_config_option', { sessionId: this.sessionId, configId: level.configId, value: choice.id });
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
233
241
|
if (!(error instanceof JsonRpcError)) {
|
|
234
242
|
throw error;
|
|
235
243
|
}
|
|
236
|
-
|
|
244
|
+
const current = level.choices.find((entry) => entry.id === level.current)?.name ?? level.current;
|
|
245
|
+
this.emit({ type: 'note', level: 'warning', text: `${this.spec.name} kept its reasoning at ${current} instead of ${choice.name}.` });
|
|
246
|
+
}
|
|
237
247
|
}
|
|
238
248
|
promptBlocks(input) {
|
|
239
249
|
const sendsImages = this.capabilities?.images === true;
|
|
@@ -52,17 +52,24 @@ export interface AcpModeState {
|
|
|
52
52
|
modes: AcpMode[];
|
|
53
53
|
configId: string | null;
|
|
54
54
|
}
|
|
55
|
+
export interface AcpChoice {
|
|
56
|
+
id: string;
|
|
57
|
+
name: string;
|
|
58
|
+
description: string | null;
|
|
59
|
+
}
|
|
60
|
+
export interface AcpThoughtLevel {
|
|
61
|
+
configId: string;
|
|
62
|
+
current: string;
|
|
63
|
+
choices: AcpChoice[];
|
|
64
|
+
}
|
|
55
65
|
export interface AcpSessionInfo {
|
|
56
66
|
sessionId: string;
|
|
57
67
|
agent: AcpAgentInfo | null;
|
|
58
68
|
models: AcpModelState | null;
|
|
59
69
|
modes: AcpModeState | null;
|
|
60
|
-
thoughtLevel:
|
|
61
|
-
configId: string;
|
|
62
|
-
current: string;
|
|
63
|
-
values: string[];
|
|
64
|
-
} | null;
|
|
70
|
+
thoughtLevel: AcpThoughtLevel | null;
|
|
65
71
|
}
|
|
72
|
+
export declare function thoughtLevelOf(result: unknown): AcpThoughtLevel | null | undefined;
|
|
66
73
|
export declare function parseSessionInfo(result: unknown, sessionId: string, agent: AcpAgentInfo | null): AcpSessionInfo;
|
|
67
74
|
export declare function sessionIdOf(result: unknown): string | null;
|
|
68
75
|
export type AcpModeMap = Partial<Record<RuntimeMode, 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() });
|
|
79
79
|
const ConfigOptionSchema = z.looseObject({
|
|
80
80
|
id: z.string(),
|
|
81
81
|
name: z.string(),
|
|
@@ -144,6 +144,18 @@ function selectValues(option) {
|
|
|
144
144
|
function selectOption(options, category) {
|
|
145
145
|
return options.find((option) => option.type === 'select' && option.category === category && typeof option.currentValue === 'string') ?? null;
|
|
146
146
|
}
|
|
147
|
+
function thoughtOption(options) {
|
|
148
|
+
return (selectOption(options, 'thought_level') ??
|
|
149
|
+
options.find((option) => option.type === 'select' && option.id === 'reasoning_effort' && typeof option.currentValue === 'string') ??
|
|
150
|
+
null);
|
|
151
|
+
}
|
|
152
|
+
/* The reasoning effort in a response that carries the session's config options; undefined when it carries none. */
|
|
153
|
+
export function thoughtLevelOf(result) {
|
|
154
|
+
if (!isRecord(result) || !Array.isArray(result.configOptions)) {
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
return sessionState(validItems(ConfigOptionSchema, result.configOptions), null, null).thoughtLevel;
|
|
158
|
+
}
|
|
147
159
|
export function parseSessionInfo(result, sessionId, agent) {
|
|
148
160
|
const record = isRecord(result) ? result : {};
|
|
149
161
|
const options = validItems(ConfigOptionSchema, Array.isArray(record.configOptions) ? record.configOptions : []);
|
|
@@ -152,7 +164,7 @@ export function parseSessionInfo(result, sessionId, agent) {
|
|
|
152
164
|
function sessionState(options, modes, models) {
|
|
153
165
|
const modelOption = selectOption(options, 'model');
|
|
154
166
|
const modeOption = selectOption(options, 'mode');
|
|
155
|
-
const
|
|
167
|
+
const thought = thoughtOption(options);
|
|
156
168
|
const modeState = ModeStateSchema.safeParse(modes);
|
|
157
169
|
const modelState = ModelStateSchema.safeParse(models);
|
|
158
170
|
return {
|
|
@@ -186,8 +198,12 @@ function sessionState(options, modes, models) {
|
|
|
186
198
|
configId: null
|
|
187
199
|
}
|
|
188
200
|
: null,
|
|
189
|
-
thoughtLevel:
|
|
190
|
-
? {
|
|
201
|
+
thoughtLevel: thought
|
|
202
|
+
? {
|
|
203
|
+
configId: thought.id,
|
|
204
|
+
current: thought.currentValue,
|
|
205
|
+
choices: selectValues(thought).map((value) => ({ id: value.value, name: value.name, description: value.description ?? null }))
|
|
206
|
+
}
|
|
191
207
|
: null
|
|
192
208
|
};
|
|
193
209
|
}
|
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';
|
|
@@ -29,11 +29,31 @@ const OWN_USAGE_FILES = new Set(['grok']);
|
|
|
29
29
|
function adoptableModels(info) {
|
|
30
30
|
return (info.models?.models ?? []).filter((model) => model.id !== '' && !model.id.startsWith('-')).map((model) => ({ slug: model.id, name: model.name }));
|
|
31
31
|
}
|
|
32
|
+
/* The reasoning effort a session offers, as the `effort` option the composer draws for every provider. */
|
|
33
|
+
function effortOption(info) {
|
|
34
|
+
const level = info.thoughtLevel;
|
|
35
|
+
if (level === null || !level.choices.some((choice) => choice.id === level.current)) {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
return [
|
|
39
|
+
{
|
|
40
|
+
id: 'effort',
|
|
41
|
+
label: 'Reasoning',
|
|
42
|
+
type: 'select',
|
|
43
|
+
defaultChoice: level.current,
|
|
44
|
+
choices: level.choices.map((choice) => ({
|
|
45
|
+
id: choice.id,
|
|
46
|
+
label: choice.name,
|
|
47
|
+
...(choice.description === null ? {} : { description: choice.description })
|
|
48
|
+
}))
|
|
49
|
+
}
|
|
50
|
+
];
|
|
51
|
+
}
|
|
32
52
|
export function createAcpProvider(spec) {
|
|
33
53
|
const capabilities = { ...ACP_CAPABILITIES, ...spec.capabilities };
|
|
34
54
|
const onSession = (info) => {
|
|
35
55
|
if (spec.adoptModels !== false && info.models !== null) {
|
|
36
|
-
spec.catalog.adopt(adoptableModels(info), info.models.current);
|
|
56
|
+
spec.catalog.adopt(adoptableModels(info), info.models.current, effortOption(info));
|
|
37
57
|
}
|
|
38
58
|
spec.onSession?.(info);
|
|
39
59
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ModelCatalogData, ModelInfo, ModelSelection } from '@adecore/agent-contracts';
|
|
1
|
+
import type { ModelCatalogData, ModelInfo, ModelOptionDescriptor, ModelSelection } from '@adecore/agent-contracts';
|
|
2
2
|
export type ModelCatalogInput = Omit<ModelCatalogData, 'updatedAt'> & {
|
|
3
3
|
updatedAt?: string;
|
|
4
4
|
};
|
|
@@ -11,7 +11,7 @@ export declare class ModelCatalog {
|
|
|
11
11
|
adopt(models: ReadonlyArray<{
|
|
12
12
|
slug: string;
|
|
13
13
|
name: string;
|
|
14
|
-
}>, defaultModel: string): boolean;
|
|
14
|
+
}>, defaultModel: string, options?: readonly ModelOptionDescriptor[]): boolean;
|
|
15
15
|
list(): ModelInfo[];
|
|
16
16
|
nameOf(slug: string): string;
|
|
17
17
|
resolveModel(name: string): string | null;
|
|
@@ -30,13 +30,16 @@ export class ModelCatalog {
|
|
|
30
30
|
/*
|
|
31
31
|
* Takes the models an agent reported for itself in place of the list, the agent's current model as the
|
|
32
32
|
* default and its order kept; answers whether the models changed. Every model gets the profile the
|
|
33
|
-
* default has now, as an agent names its models but not what each can be asked for
|
|
33
|
+
* default has now, as an agent names its models but not what each can be asked for, with `options`
|
|
34
|
+
* in place of the profile's options of the same id.
|
|
34
35
|
*/
|
|
35
|
-
adopt(models, defaultModel) {
|
|
36
|
+
adopt(models, defaultModel, options = []) {
|
|
36
37
|
if (models.length === 0 || !models.some((model) => model.slug === defaultModel)) {
|
|
37
38
|
return false;
|
|
38
39
|
}
|
|
39
|
-
const
|
|
40
|
+
const base = this.profileOf(this.data.defaultModel) ?? { options: [], contextWindowTokens: {} };
|
|
41
|
+
const replaced = new Set(options.map((option) => option.id));
|
|
42
|
+
const profile = { ...base, options: [...base.options.filter((option) => !replaced.has(option.id)), ...options] };
|
|
40
43
|
const data = {
|
|
41
44
|
...(this.data.updatedAt === undefined ? {} : { updatedAt: this.data.updatedAt }),
|
|
42
45
|
defaultModel,
|
|
@@ -7,6 +7,7 @@ export interface FakeAcpOptions {
|
|
|
7
7
|
legacyModels?: boolean;
|
|
8
8
|
closeSession?: boolean;
|
|
9
9
|
turnUsage?: Record<string, number>;
|
|
10
|
+
reasoningEffort?: 'accept' | 'reject';
|
|
10
11
|
}
|
|
11
12
|
export declare function fakeAcpWith(options: FakeAcpOptions): FakeCli;
|
|
12
13
|
export declare const fakeAcp: FakeCli;
|
package/dist/testing/fake-acp.js
CHANGED
|
@@ -7,6 +7,7 @@ export function fakeAcpWith(options) {
|
|
|
7
7
|
let sessionId = '';
|
|
8
8
|
let mode = modes[0] ?? '';
|
|
9
9
|
let model = 'fake-small';
|
|
10
|
+
let effort = 'low';
|
|
10
11
|
let serverRequestId = 0;
|
|
11
12
|
let callCounter = 0;
|
|
12
13
|
let prompt = null;
|
|
@@ -41,11 +42,24 @@ export function fakeAcpWith(options) {
|
|
|
41
42
|
{ value: 'fake-small', name: 'Fake small' },
|
|
42
43
|
{ value: 'fake-large', name: 'Fake large' }
|
|
43
44
|
];
|
|
45
|
+
const effortChoices = [
|
|
46
|
+
{ value: 'low', name: 'Low', description: 'Answers quickly' },
|
|
47
|
+
{ value: 'high', name: 'High' },
|
|
48
|
+
...(model === 'fake-large' ? [{ value: 'xhigh', name: 'Extra high' }] : [])
|
|
49
|
+
];
|
|
50
|
+
const effortOption = options.reasoningEffort === undefined
|
|
51
|
+
? []
|
|
52
|
+
: [{ id: 'reasoning_effort', name: 'Reasoning effort', type: 'select', currentValue: effort, options: effortChoices }];
|
|
44
53
|
return {
|
|
45
54
|
...(modes.length === 0 ? {} : { modes: { currentModeId: mode, availableModes: modes.map((id) => ({ id, name: id })) } }),
|
|
46
55
|
...(options.legacyModels
|
|
47
56
|
? { models: { currentModelId: model, availableModels: modelChoices.map((choice) => ({ modelId: choice.value, name: choice.name })) } }
|
|
48
|
-
: {
|
|
57
|
+
: {
|
|
58
|
+
configOptions: [
|
|
59
|
+
{ id: 'model', name: 'Model', type: 'select', category: 'model', currentValue: model, options: modelChoices },
|
|
60
|
+
...effortOption
|
|
61
|
+
]
|
|
62
|
+
})
|
|
49
63
|
};
|
|
50
64
|
};
|
|
51
65
|
const askPermission = (toolCall, onAnswer) => {
|
|
@@ -241,6 +255,13 @@ export function fakeAcpWith(options) {
|
|
|
241
255
|
if (params.configId === 'model') {
|
|
242
256
|
model = String(params.value);
|
|
243
257
|
}
|
|
258
|
+
if (params.configId === 'reasoning_effort') {
|
|
259
|
+
if (options.reasoningEffort === 'reject') {
|
|
260
|
+
refuse(id, -32602, 'Reasoning effort is locked');
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
effort = String(params.value);
|
|
264
|
+
}
|
|
244
265
|
respond(id, { configOptions: sessionState().configOptions ?? [] });
|
|
245
266
|
return;
|
|
246
267
|
case 'session/prompt':
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adecore/agents",
|
|
3
|
-
"version": "0.31.
|
|
3
|
+
"version": "0.31.4",
|
|
4
4
|
"description": "Node-compatible agent chat hosts, providers, accounts, usage and durable task coordination.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -81,8 +81,8 @@
|
|
|
81
81
|
"test": "bun --conditions=source test"
|
|
82
82
|
},
|
|
83
83
|
"dependencies": {
|
|
84
|
-
"@adecore/agent-contracts": "0.31.
|
|
85
|
-
"@adecore/intelligent-ui": "0.31.
|
|
84
|
+
"@adecore/agent-contracts": "0.31.4",
|
|
85
|
+
"@adecore/intelligent-ui": "0.31.4",
|
|
86
86
|
"zod": "^4.6.5"
|
|
87
87
|
},
|
|
88
88
|
"devDependencies": {
|
package/src/chat/acp-backend.ts
CHANGED
|
@@ -14,10 +14,12 @@ import {
|
|
|
14
14
|
resolveAcpMode,
|
|
15
15
|
runtimeModeOf,
|
|
16
16
|
sessionIdOf,
|
|
17
|
+
thoughtLevelOf,
|
|
17
18
|
type AcpAgentCapabilities,
|
|
18
19
|
type AcpModeMap,
|
|
19
20
|
type AcpPermissionRequest,
|
|
20
21
|
type AcpSessionInfo,
|
|
22
|
+
type AcpThoughtLevel,
|
|
21
23
|
type AcpTurnUsage
|
|
22
24
|
} from './acp-protocol.ts';
|
|
23
25
|
import type { ApprovalDecision, BackendEvent, BackendHost, BackendLaunch, ChatBackend, TurnInput } from './backend.ts';
|
|
@@ -154,9 +156,9 @@ export class AcpBackend implements ChatBackend {
|
|
|
154
156
|
this.capabilities = capabilities;
|
|
155
157
|
const info = await this.openSession(transport, capabilities);
|
|
156
158
|
const modeId = await this.applyMode(transport, info);
|
|
157
|
-
const model = await this.applyModel(transport, info);
|
|
159
|
+
const { model, thoughtLevel } = await this.applyModel(transport, info);
|
|
158
160
|
this.model = model;
|
|
159
|
-
await this.applyThoughtLevel(transport,
|
|
161
|
+
await this.applyThoughtLevel(transport, thoughtLevel);
|
|
160
162
|
this.spec.onSession?.(info);
|
|
161
163
|
this.emit({
|
|
162
164
|
type: 'session',
|
|
@@ -263,24 +265,29 @@ export class AcpBackend implements ChatBackend {
|
|
|
263
265
|
return resolved.modeId;
|
|
264
266
|
}
|
|
265
267
|
|
|
266
|
-
/*
|
|
267
|
-
|
|
268
|
+
/*
|
|
269
|
+
* Switches to the selected model where the agent offers it; answers the model the session runs and the
|
|
270
|
+
* reasoning effort it offers there, which may change with the model.
|
|
271
|
+
*/
|
|
272
|
+
private async applyModel(transport: JsonRpcTransport, info: AcpSessionInfo): Promise<{ model: string | null; thoughtLevel: AcpThoughtLevel | null }> {
|
|
268
273
|
const models = info.models;
|
|
274
|
+
const thoughtLevel = info.thoughtLevel;
|
|
269
275
|
const wanted = this.spec.model ? this.spec.model(this.launch.selection) : this.launch.selection.model;
|
|
270
276
|
if (models === null || wanted === null || wanted === models.current) {
|
|
271
|
-
return models?.current ?? wanted;
|
|
277
|
+
return { model: models?.current ?? wanted, thoughtLevel };
|
|
272
278
|
}
|
|
273
279
|
if (!models.models.some((model) => model.id === wanted)) {
|
|
274
280
|
this.emit({ type: 'note', level: 'warning', text: `${this.spec.name} does not offer ${this.launch.modelName}. It runs ${models.current}.` });
|
|
275
|
-
return models.current;
|
|
281
|
+
return { model: models.current, thoughtLevel };
|
|
276
282
|
}
|
|
277
283
|
try {
|
|
278
284
|
if (models.configId === null) {
|
|
279
285
|
await transport.request('session/set_model', { sessionId: this.sessionId, modelId: wanted });
|
|
280
|
-
|
|
281
|
-
await transport.request('session/set_config_option', { sessionId: this.sessionId, configId: models.configId, value: wanted });
|
|
286
|
+
return { model: wanted, thoughtLevel };
|
|
282
287
|
}
|
|
283
|
-
|
|
288
|
+
const result = await transport.request('session/set_config_option', { sessionId: this.sessionId, configId: models.configId, value: wanted });
|
|
289
|
+
const after = thoughtLevelOf(result);
|
|
290
|
+
return { model: wanted, thoughtLevel: after === undefined ? thoughtLevel : after };
|
|
284
291
|
} catch (error) {
|
|
285
292
|
if (!(error instanceof JsonRpcError)) {
|
|
286
293
|
throw error;
|
|
@@ -290,21 +297,26 @@ export class AcpBackend implements ChatBackend {
|
|
|
290
297
|
level: 'warning',
|
|
291
298
|
text: `${this.spec.name} did not switch to ${this.launch.modelName} (${errorText(error)}). It runs ${models.current}.`
|
|
292
299
|
});
|
|
293
|
-
return models.current;
|
|
300
|
+
return { model: models.current, thoughtLevel };
|
|
294
301
|
}
|
|
295
302
|
}
|
|
296
303
|
|
|
297
|
-
|
|
298
|
-
|
|
304
|
+
/* Sets the selected effort where the session offers it; a model without one runs as it is. */
|
|
305
|
+
private async applyThoughtLevel(transport: JsonRpcTransport, level: AcpThoughtLevel | null): Promise<void> {
|
|
299
306
|
const effort = this.launch.selection.options.effort;
|
|
300
|
-
|
|
307
|
+
const choice = level?.choices.find((entry) => entry.id === effort);
|
|
308
|
+
if (level === null || choice === undefined || effort === level.current) {
|
|
301
309
|
return;
|
|
302
310
|
}
|
|
303
|
-
|
|
311
|
+
try {
|
|
312
|
+
await transport.request('session/set_config_option', { sessionId: this.sessionId, configId: level.configId, value: choice.id });
|
|
313
|
+
} catch (error) {
|
|
304
314
|
if (!(error instanceof JsonRpcError)) {
|
|
305
315
|
throw error;
|
|
306
316
|
}
|
|
307
|
-
|
|
317
|
+
const current = level.choices.find((entry) => entry.id === level.current)?.name ?? level.current;
|
|
318
|
+
this.emit({ type: 'note', level: 'warning', text: `${this.spec.name} kept its reasoning at ${current} instead of ${choice.name}.` });
|
|
319
|
+
}
|
|
308
320
|
}
|
|
309
321
|
|
|
310
322
|
private promptBlocks(input: TurnInput): unknown[] {
|
package/src/chat/acp-protocol.ts
CHANGED
|
@@ -107,7 +107,7 @@ const ModeStateSchema = z.looseObject({
|
|
|
107
107
|
|
|
108
108
|
const ModeSchema = z.looseObject({ id: z.string(), name: z.string() });
|
|
109
109
|
|
|
110
|
-
const SelectValueSchema = z.looseObject({ value: z.string(), name: z.string() });
|
|
110
|
+
const SelectValueSchema = z.looseObject({ value: z.string(), name: z.string(), description: z.string().nullish() });
|
|
111
111
|
|
|
112
112
|
const ConfigOptionSchema = z.looseObject({
|
|
113
113
|
id: z.string(),
|
|
@@ -221,14 +221,27 @@ export interface AcpModeState {
|
|
|
221
221
|
configId: string | null;
|
|
222
222
|
}
|
|
223
223
|
|
|
224
|
+
export interface AcpChoice {
|
|
225
|
+
id: string;
|
|
226
|
+
name: string;
|
|
227
|
+
description: string | null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/* The reasoning effort a session offers: the config option that sets it, the value it runs at and the values it takes. */
|
|
231
|
+
export interface AcpThoughtLevel {
|
|
232
|
+
configId: string;
|
|
233
|
+
current: string;
|
|
234
|
+
choices: AcpChoice[];
|
|
235
|
+
}
|
|
236
|
+
|
|
224
237
|
/* What `session/new`, `session/load` or `session/resume` said about the session, for a host that fills its catalog from it. */
|
|
225
238
|
export interface AcpSessionInfo {
|
|
226
239
|
sessionId: string;
|
|
227
240
|
agent: AcpAgentInfo | null;
|
|
228
241
|
models: AcpModelState | null;
|
|
229
242
|
modes: AcpModeState | null;
|
|
230
|
-
//
|
|
231
|
-
thoughtLevel:
|
|
243
|
+
// A config option of the `thought_level` category, or one with the id `reasoning_effort` where an agent sets no category.
|
|
244
|
+
thoughtLevel: AcpThoughtLevel | null;
|
|
232
245
|
}
|
|
233
246
|
|
|
234
247
|
type ConfigOption = z.infer<typeof ConfigOptionSchema>;
|
|
@@ -243,6 +256,22 @@ function selectOption(options: ConfigOption[], category: string): ConfigOption |
|
|
|
243
256
|
return options.find((option) => option.type === 'select' && option.category === category && typeof option.currentValue === 'string') ?? null;
|
|
244
257
|
}
|
|
245
258
|
|
|
259
|
+
function thoughtOption(options: ConfigOption[]): ConfigOption | null {
|
|
260
|
+
return (
|
|
261
|
+
selectOption(options, 'thought_level') ??
|
|
262
|
+
options.find((option) => option.type === 'select' && option.id === 'reasoning_effort' && typeof option.currentValue === 'string') ??
|
|
263
|
+
null
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/* The reasoning effort in a response that carries the session's config options; undefined when it carries none. */
|
|
268
|
+
export function thoughtLevelOf(result: unknown): AcpThoughtLevel | null | undefined {
|
|
269
|
+
if (!isRecord(result) || !Array.isArray(result.configOptions)) {
|
|
270
|
+
return undefined;
|
|
271
|
+
}
|
|
272
|
+
return sessionState(validItems(ConfigOptionSchema, result.configOptions), null, null).thoughtLevel;
|
|
273
|
+
}
|
|
274
|
+
|
|
246
275
|
export function parseSessionInfo(result: unknown, sessionId: string, agent: AcpAgentInfo | null): AcpSessionInfo {
|
|
247
276
|
const record = isRecord(result) ? result : {};
|
|
248
277
|
const options = validItems(ConfigOptionSchema, Array.isArray(record.configOptions) ? record.configOptions : []);
|
|
@@ -252,7 +281,7 @@ export function parseSessionInfo(result: unknown, sessionId: string, agent: AcpA
|
|
|
252
281
|
function sessionState(options: ConfigOption[], modes: unknown, models: unknown): Omit<AcpSessionInfo, 'sessionId' | 'agent'> {
|
|
253
282
|
const modelOption = selectOption(options, 'model');
|
|
254
283
|
const modeOption = selectOption(options, 'mode');
|
|
255
|
-
const
|
|
284
|
+
const thought = thoughtOption(options);
|
|
256
285
|
const modeState = ModeStateSchema.safeParse(modes);
|
|
257
286
|
const modelState = ModelStateSchema.safeParse(models);
|
|
258
287
|
return {
|
|
@@ -286,8 +315,12 @@ function sessionState(options: ConfigOption[], modes: unknown, models: unknown):
|
|
|
286
315
|
configId: null
|
|
287
316
|
}
|
|
288
317
|
: null,
|
|
289
|
-
thoughtLevel:
|
|
290
|
-
? {
|
|
318
|
+
thoughtLevel: thought
|
|
319
|
+
? {
|
|
320
|
+
configId: thought.id,
|
|
321
|
+
current: thought.currentValue as string,
|
|
322
|
+
choices: selectValues(thought).map((value) => ({ id: value.value, name: value.name, description: value.description ?? null }))
|
|
323
|
+
}
|
|
291
324
|
: null
|
|
292
325
|
};
|
|
293
326
|
}
|
package/src/chat/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentKind, ProviderCapabilities, ProviderInstall } from '@adecore/agent-contracts';
|
|
1
|
+
import type { AgentKind, ModelOptionDescriptor, ProviderCapabilities, ProviderInstall } from '@adecore/agent-contracts';
|
|
2
2
|
import { createAcpBackend, type AcpAgentSpec, type AcpUsageReport } from '../chat/acp-backend.ts';
|
|
3
3
|
import type { AcpSessionInfo } from '../chat/acp-protocol.ts';
|
|
4
4
|
import { askAcpAccount } from './accounts/acp-account.ts';
|
|
@@ -66,11 +66,32 @@ function adoptableModels(info: AcpSessionInfo): Array<{ slug: string; name: stri
|
|
|
66
66
|
return (info.models?.models ?? []).filter((model) => model.id !== '' && !model.id.startsWith('-')).map((model) => ({ slug: model.id, name: model.name }));
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
/* The reasoning effort a session offers, as the `effort` option the composer draws for every provider. */
|
|
70
|
+
function effortOption(info: AcpSessionInfo): ModelOptionDescriptor[] {
|
|
71
|
+
const level = info.thoughtLevel;
|
|
72
|
+
if (level === null || !level.choices.some((choice) => choice.id === level.current)) {
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
return [
|
|
76
|
+
{
|
|
77
|
+
id: 'effort',
|
|
78
|
+
label: 'Reasoning',
|
|
79
|
+
type: 'select',
|
|
80
|
+
defaultChoice: level.current,
|
|
81
|
+
choices: level.choices.map((choice) => ({
|
|
82
|
+
id: choice.id,
|
|
83
|
+
label: choice.name,
|
|
84
|
+
...(choice.description === null ? {} : { description: choice.description })
|
|
85
|
+
}))
|
|
86
|
+
}
|
|
87
|
+
];
|
|
88
|
+
}
|
|
89
|
+
|
|
69
90
|
export function createAcpProvider(spec: AcpProviderSpec): ChatProvider {
|
|
70
91
|
const capabilities = { ...ACP_CAPABILITIES, ...spec.capabilities };
|
|
71
92
|
const onSession = (info: AcpSessionInfo): void => {
|
|
72
93
|
if (spec.adoptModels !== false && info.models !== null) {
|
|
73
|
-
spec.catalog.adopt(adoptableModels(info), info.models.current);
|
|
94
|
+
spec.catalog.adopt(adoptableModels(info), info.models.current, effortOption(info));
|
|
74
95
|
}
|
|
75
96
|
spec.onSession?.(info);
|
|
76
97
|
};
|
package/src/providers/catalog.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ModelCatalogData, ModelCatalogProfile, ModelInfo, ModelSelection } from '@adecore/agent-contracts';
|
|
1
|
+
import type { ModelCatalogData, ModelCatalogProfile, ModelInfo, ModelOptionDescriptor, ModelSelection } from '@adecore/agent-contracts';
|
|
2
2
|
import manifest from './claude-models.json' with { type: 'json' };
|
|
3
3
|
|
|
4
4
|
// What a host builds a catalog from; one that is never replaced needs no date.
|
|
@@ -41,13 +41,16 @@ export class ModelCatalog {
|
|
|
41
41
|
/*
|
|
42
42
|
* Takes the models an agent reported for itself in place of the list, the agent's current model as the
|
|
43
43
|
* default and its order kept; answers whether the models changed. Every model gets the profile the
|
|
44
|
-
* default has now, as an agent names its models but not what each can be asked for
|
|
44
|
+
* default has now, as an agent names its models but not what each can be asked for, with `options`
|
|
45
|
+
* in place of the profile's options of the same id.
|
|
45
46
|
*/
|
|
46
|
-
adopt(models: ReadonlyArray<{ slug: string; name: string }>, defaultModel: string): boolean {
|
|
47
|
+
adopt(models: ReadonlyArray<{ slug: string; name: string }>, defaultModel: string, options: readonly ModelOptionDescriptor[] = []): boolean {
|
|
47
48
|
if (models.length === 0 || !models.some((model) => model.slug === defaultModel)) {
|
|
48
49
|
return false;
|
|
49
50
|
}
|
|
50
|
-
const
|
|
51
|
+
const base = this.profileOf(this.data.defaultModel) ?? { options: [], contextWindowTokens: {} };
|
|
52
|
+
const replaced = new Set(options.map((option) => option.id));
|
|
53
|
+
const profile = { ...base, options: [...base.options.filter((option) => !replaced.has(option.id)), ...options] };
|
|
51
54
|
const data: ModelCatalogInput = {
|
|
52
55
|
...(this.data.updatedAt === undefined ? {} : { updatedAt: this.data.updatedAt }),
|
|
53
56
|
defaultModel,
|
package/src/testing/fake-acp.ts
CHANGED
|
@@ -24,6 +24,9 @@ export interface FakeAcpOptions {
|
|
|
24
24
|
closeSession?: boolean;
|
|
25
25
|
// The `usage` every `session/prompt` response carries; absent reports none.
|
|
26
26
|
turnUsage?: Record<string, number>;
|
|
27
|
+
// Offers a reasoning effort as the config option `reasoning_effort`, without a category: Low and High, and
|
|
28
|
+
// Extra high on `fake-large`. `reject` refuses every change to it.
|
|
29
|
+
reasoningEffort?: 'accept' | 'reject';
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
export function fakeAcpWith(options: FakeAcpOptions): FakeCli {
|
|
@@ -33,6 +36,7 @@ export function fakeAcpWith(options: FakeAcpOptions): FakeCli {
|
|
|
33
36
|
let sessionId = '';
|
|
34
37
|
let mode = modes[0] ?? '';
|
|
35
38
|
let model = 'fake-small';
|
|
39
|
+
let effort = 'low';
|
|
36
40
|
let serverRequestId = 0;
|
|
37
41
|
let callCounter = 0;
|
|
38
42
|
let prompt: { rpcId: number | string; cancelled: boolean } | null = null;
|
|
@@ -74,11 +78,25 @@ export function fakeAcpWith(options: FakeAcpOptions): FakeCli {
|
|
|
74
78
|
{ value: 'fake-small', name: 'Fake small' },
|
|
75
79
|
{ value: 'fake-large', name: 'Fake large' }
|
|
76
80
|
];
|
|
81
|
+
const effortChoices = [
|
|
82
|
+
{ value: 'low', name: 'Low', description: 'Answers quickly' },
|
|
83
|
+
{ value: 'high', name: 'High' },
|
|
84
|
+
...(model === 'fake-large' ? [{ value: 'xhigh', name: 'Extra high' }] : [])
|
|
85
|
+
];
|
|
86
|
+
const effortOption =
|
|
87
|
+
options.reasoningEffort === undefined
|
|
88
|
+
? []
|
|
89
|
+
: [{ id: 'reasoning_effort', name: 'Reasoning effort', type: 'select', currentValue: effort, options: effortChoices }];
|
|
77
90
|
return {
|
|
78
91
|
...(modes.length === 0 ? {} : { modes: { currentModeId: mode, availableModes: modes.map((id) => ({ id, name: id })) } }),
|
|
79
92
|
...(options.legacyModels
|
|
80
93
|
? { models: { currentModelId: model, availableModels: modelChoices.map((choice) => ({ modelId: choice.value, name: choice.name })) } }
|
|
81
|
-
: {
|
|
94
|
+
: {
|
|
95
|
+
configOptions: [
|
|
96
|
+
{ id: 'model', name: 'Model', type: 'select', category: 'model', currentValue: model, options: modelChoices },
|
|
97
|
+
...effortOption
|
|
98
|
+
]
|
|
99
|
+
})
|
|
82
100
|
};
|
|
83
101
|
};
|
|
84
102
|
|
|
@@ -277,6 +295,13 @@ export function fakeAcpWith(options: FakeAcpOptions): FakeCli {
|
|
|
277
295
|
if (params.configId === 'model') {
|
|
278
296
|
model = String(params.value);
|
|
279
297
|
}
|
|
298
|
+
if (params.configId === 'reasoning_effort') {
|
|
299
|
+
if (options.reasoningEffort === 'reject') {
|
|
300
|
+
refuse(id, -32602, 'Reasoning effort is locked');
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
effort = String(params.value);
|
|
304
|
+
}
|
|
280
305
|
respond(id, { configOptions: sessionState().configOptions ?? [] });
|
|
281
306
|
return;
|
|
282
307
|
case 'session/prompt':
|