@adecore/agents 0.31.2 → 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.d.ts +11 -1
- package/dist/chat/acp-backend.js +49 -17
- package/dist/chat/acp-protocol.d.ts +26 -5
- package/dist/chat/acp-protocol.js +53 -5
- package/dist/chat/index.d.ts +2 -2
- package/dist/providers/acp-provider.d.ts +1 -0
- package/dist/providers/acp-provider.js +32 -2
- package/dist/providers/acp-usage-log.d.ts +2 -0
- package/dist/providers/acp-usage-log.js +21 -0
- package/dist/providers/catalog.d.ts +2 -2
- package/dist/providers/catalog.js +6 -3
- package/dist/providers/index.d.ts +1 -0
- package/dist/providers/index.js +1 -0
- package/dist/providers/shell-commands.d.ts +1 -0
- package/dist/providers/shell-commands.js +119 -0
- package/dist/testing/fake-acp.d.ts +2 -0
- package/dist/testing/fake-acp.js +23 -2
- package/dist/usage/aggregate.js +6 -5
- package/dist/usage/index-file.js +25 -17
- package/dist/usage/limits/monitor.js +5 -0
- package/dist/usage/readers/grok.d.ts +3 -0
- package/dist/usage/readers/grok.js +85 -0
- package/dist/usage/record.d.ts +1 -0
- package/dist/usage/record.js +5 -1
- package/dist/usage/roots.js +7 -2
- package/dist/usage/scanner.d.ts +1 -0
- package/dist/usage/scanner.js +39 -5
- package/package.json +3 -3
- package/src/chat/acp-backend.ts +67 -17
- package/src/chat/acp-protocol.ts +95 -7
- package/src/chat/index.ts +14 -2
- package/src/providers/acp-provider.ts +39 -4
- package/src/providers/acp-usage-log.ts +26 -0
- package/src/providers/catalog.ts +7 -4
- package/src/providers/index.ts +1 -0
- package/src/providers/shell-commands.ts +122 -0
- package/src/testing/fake-acp.ts +29 -2
- package/src/usage/aggregate.ts +6 -5
- package/src/usage/index-file.ts +27 -18
- package/src/usage/limits/monitor.ts +5 -0
- package/src/usage/readers/grok.ts +89 -0
- package/src/usage/record.ts +7 -1
- package/src/usage/roots.ts +7 -2
- package/src/usage/scanner.ts +42 -5
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ModelSelection } from '@adecore/agent-contracts';
|
|
2
|
-
import { type AcpModeMap, type AcpSessionInfo } from './acp-protocol.ts';
|
|
2
|
+
import { type AcpModeMap, type AcpPermissionRequest, type AcpSessionInfo, type AcpTurnUsage } from './acp-protocol.ts';
|
|
3
3
|
import type { ApprovalDecision, BackendHost, BackendLaunch, ChatBackend, TurnInput } from './backend.ts';
|
|
4
4
|
export interface AcpClientInfo {
|
|
5
5
|
name: string;
|
|
@@ -16,6 +16,13 @@ export interface AcpAgentSpec {
|
|
|
16
16
|
client?: AcpClientInfo;
|
|
17
17
|
model?(selection: ModelSelection): string | null;
|
|
18
18
|
onSession?(info: AcpSessionInfo): void;
|
|
19
|
+
onUsage?(usage: AcpUsageReport): void;
|
|
20
|
+
autoAllow?(request: AcpPermissionRequest): boolean;
|
|
21
|
+
}
|
|
22
|
+
export interface AcpUsageReport extends AcpTurnUsage {
|
|
23
|
+
sessionId: string;
|
|
24
|
+
cwd: string;
|
|
25
|
+
model: string;
|
|
19
26
|
}
|
|
20
27
|
export declare class AcpBackend implements ChatBackend {
|
|
21
28
|
private readonly launch;
|
|
@@ -29,6 +36,7 @@ export declare class AcpBackend implements ChatBackend {
|
|
|
29
36
|
private replaying;
|
|
30
37
|
private contextPending;
|
|
31
38
|
private prompt;
|
|
39
|
+
private model;
|
|
32
40
|
constructor(launch: BackendLaunch, host: BackendHost, spec: AcpAgentSpec);
|
|
33
41
|
get running(): boolean;
|
|
34
42
|
get pid(): number | null;
|
|
@@ -52,7 +60,9 @@ export declare class AcpBackend implements ChatBackend {
|
|
|
52
60
|
private respond;
|
|
53
61
|
private fail;
|
|
54
62
|
private handleFrame;
|
|
63
|
+
private hostAllows;
|
|
55
64
|
private handleExit;
|
|
65
|
+
private report;
|
|
56
66
|
private emitAll;
|
|
57
67
|
private emit;
|
|
58
68
|
}
|
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, 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' };
|
|
@@ -30,6 +30,7 @@ export class AcpBackend {
|
|
|
30
30
|
replaying = false;
|
|
31
31
|
contextPending = null;
|
|
32
32
|
prompt = null;
|
|
33
|
+
model = null;
|
|
33
34
|
constructor(launch, host, spec) {
|
|
34
35
|
this.launch = launch;
|
|
35
36
|
this.host = host;
|
|
@@ -85,8 +86,9 @@ export class AcpBackend {
|
|
|
85
86
|
this.capabilities = capabilities;
|
|
86
87
|
const info = await this.openSession(transport, capabilities);
|
|
87
88
|
const modeId = await this.applyMode(transport, info);
|
|
88
|
-
const model = await this.applyModel(transport, info);
|
|
89
|
-
|
|
89
|
+
const { model, thoughtLevel } = await this.applyModel(transport, info);
|
|
90
|
+
this.model = model;
|
|
91
|
+
await this.applyThoughtLevel(transport, thoughtLevel);
|
|
90
92
|
this.spec.onSession?.(info);
|
|
91
93
|
this.emit({
|
|
92
94
|
type: 'session',
|
|
@@ -189,25 +191,29 @@ export class AcpBackend {
|
|
|
189
191
|
}
|
|
190
192
|
return resolved.modeId;
|
|
191
193
|
}
|
|
192
|
-
/*
|
|
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
|
+
*/
|
|
193
198
|
async applyModel(transport, info) {
|
|
194
199
|
const models = info.models;
|
|
200
|
+
const thoughtLevel = info.thoughtLevel;
|
|
195
201
|
const wanted = this.spec.model ? this.spec.model(this.launch.selection) : this.launch.selection.model;
|
|
196
202
|
if (models === null || wanted === null || wanted === models.current) {
|
|
197
|
-
return models?.current ?? wanted;
|
|
203
|
+
return { model: models?.current ?? wanted, thoughtLevel };
|
|
198
204
|
}
|
|
199
205
|
if (!models.models.some((model) => model.id === wanted)) {
|
|
200
206
|
this.emit({ type: 'note', level: 'warning', text: `${this.spec.name} does not offer ${this.launch.modelName}. It runs ${models.current}.` });
|
|
201
|
-
return models.current;
|
|
207
|
+
return { model: models.current, thoughtLevel };
|
|
202
208
|
}
|
|
203
209
|
try {
|
|
204
210
|
if (models.configId === null) {
|
|
205
211
|
await transport.request('session/set_model', { sessionId: this.sessionId, modelId: wanted });
|
|
212
|
+
return { model: wanted, thoughtLevel };
|
|
206
213
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
}
|
|
210
|
-
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 };
|
|
211
217
|
}
|
|
212
218
|
catch (error) {
|
|
213
219
|
if (!(error instanceof JsonRpcError)) {
|
|
@@ -218,20 +224,26 @@ export class AcpBackend {
|
|
|
218
224
|
level: 'warning',
|
|
219
225
|
text: `${this.spec.name} did not switch to ${this.launch.modelName} (${errorText(error)}). It runs ${models.current}.`
|
|
220
226
|
});
|
|
221
|
-
return models.current;
|
|
227
|
+
return { model: models.current, thoughtLevel };
|
|
222
228
|
}
|
|
223
229
|
}
|
|
224
|
-
|
|
225
|
-
|
|
230
|
+
/* Sets the selected effort where the session offers it; a model without one runs as it is. */
|
|
231
|
+
async applyThoughtLevel(transport, level) {
|
|
226
232
|
const effort = this.launch.selection.options.effort;
|
|
227
|
-
|
|
233
|
+
const choice = level?.choices.find((entry) => entry.id === effort);
|
|
234
|
+
if (level === null || choice === undefined || effort === level.current) {
|
|
228
235
|
return;
|
|
229
236
|
}
|
|
230
|
-
|
|
237
|
+
try {
|
|
238
|
+
await transport.request('session/set_config_option', { sessionId: this.sessionId, configId: level.configId, value: choice.id });
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
231
241
|
if (!(error instanceof JsonRpcError)) {
|
|
232
242
|
throw error;
|
|
233
243
|
}
|
|
234
|
-
|
|
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
|
+
}
|
|
235
247
|
}
|
|
236
248
|
promptBlocks(input) {
|
|
237
249
|
const sendsImages = this.capabilities?.images === true;
|
|
@@ -265,6 +277,7 @@ export class AcpBackend {
|
|
|
265
277
|
.then((result) => {
|
|
266
278
|
if (this.transport === transport && this.prompt === turn) {
|
|
267
279
|
this.prompt = null;
|
|
280
|
+
this.report(result);
|
|
268
281
|
this.emitAll(this.protocol.finish(turn.cancelled ? { stopReason: 'cancelled' } : result, turn.promptId));
|
|
269
282
|
}
|
|
270
283
|
})
|
|
@@ -373,7 +386,7 @@ export class AcpBackend {
|
|
|
373
386
|
}
|
|
374
387
|
return;
|
|
375
388
|
}
|
|
376
|
-
if (autoAllows(this.launch.runtimeMode, permission.kind)) {
|
|
389
|
+
if (autoAllows(this.launch.runtimeMode, permission.kind) || this.hostAllows(permission.request)) {
|
|
377
390
|
this.emitAll(permission.events.filter((event) => event.type !== 'approval.requested'));
|
|
378
391
|
const answer = this.protocol.answer(permission.requestId, 'allow');
|
|
379
392
|
if (answer) {
|
|
@@ -383,6 +396,14 @@ export class AcpBackend {
|
|
|
383
396
|
}
|
|
384
397
|
this.emitAll(permission.events);
|
|
385
398
|
}
|
|
399
|
+
hostAllows(request) {
|
|
400
|
+
try {
|
|
401
|
+
return this.spec.autoAllow?.(request) === true;
|
|
402
|
+
}
|
|
403
|
+
catch {
|
|
404
|
+
return false;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
386
407
|
handleExit(transport, exitCode, stderr) {
|
|
387
408
|
if (this.transport !== transport) {
|
|
388
409
|
return;
|
|
@@ -392,12 +413,23 @@ export class AcpBackend {
|
|
|
392
413
|
this.protocol.cancelPermissions();
|
|
393
414
|
this.emit({ type: 'exit', exitCode, ...(stderr === null ? {} : { stderr }) });
|
|
394
415
|
}
|
|
416
|
+
/* A cancelled turn still spent what it reports. */
|
|
417
|
+
report(result) {
|
|
418
|
+
const usage = this.spec.onUsage === undefined ? null : promptUsageOf(result);
|
|
419
|
+
if (usage === null) {
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
this.spec.onUsage?.({ ...usage, sessionId: this.sessionId, cwd: this.launch.cwd, model: this.model ?? this.launch.selection.model });
|
|
423
|
+
}
|
|
395
424
|
emitAll(events) {
|
|
396
425
|
for (const event of events) {
|
|
397
426
|
this.emit(event);
|
|
398
427
|
}
|
|
399
428
|
}
|
|
400
429
|
emit(event) {
|
|
430
|
+
if (event.type === 'model') {
|
|
431
|
+
this.model = event.model;
|
|
432
|
+
}
|
|
401
433
|
this.host.onEvent(event);
|
|
402
434
|
}
|
|
403
435
|
}
|
|
@@ -9,6 +9,14 @@ declare const DiffSchema: z.ZodObject<{
|
|
|
9
9
|
oldText: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
10
10
|
newText: z.ZodString;
|
|
11
11
|
}, z.core.$loose>;
|
|
12
|
+
export interface AcpTurnUsage {
|
|
13
|
+
input: number;
|
|
14
|
+
cacheRead: number;
|
|
15
|
+
cacheWrite: number;
|
|
16
|
+
output: number;
|
|
17
|
+
reasoning: number;
|
|
18
|
+
}
|
|
19
|
+
export declare function promptUsageOf(result: unknown): AcpTurnUsage | null;
|
|
12
20
|
export interface AcpAgentInfo {
|
|
13
21
|
name: string;
|
|
14
22
|
title: string | null;
|
|
@@ -44,17 +52,24 @@ export interface AcpModeState {
|
|
|
44
52
|
modes: AcpMode[];
|
|
45
53
|
configId: string | null;
|
|
46
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
|
+
}
|
|
47
65
|
export interface AcpSessionInfo {
|
|
48
66
|
sessionId: string;
|
|
49
67
|
agent: AcpAgentInfo | null;
|
|
50
68
|
models: AcpModelState | null;
|
|
51
69
|
modes: AcpModeState | null;
|
|
52
|
-
thoughtLevel:
|
|
53
|
-
configId: string;
|
|
54
|
-
current: string;
|
|
55
|
-
values: string[];
|
|
56
|
-
} | null;
|
|
70
|
+
thoughtLevel: AcpThoughtLevel | null;
|
|
57
71
|
}
|
|
72
|
+
export declare function thoughtLevelOf(result: unknown): AcpThoughtLevel | null | undefined;
|
|
58
73
|
export declare function parseSessionInfo(result: unknown, sessionId: string, agent: AcpAgentInfo | null): AcpSessionInfo;
|
|
59
74
|
export declare function sessionIdOf(result: unknown): string | null;
|
|
60
75
|
export type AcpModeMap = Partial<Record<RuntimeMode, readonly string[]>>;
|
|
@@ -84,10 +99,16 @@ export declare function toolRow(tool: Pick<ToolState, 'title' | 'name' | 'kind'
|
|
|
84
99
|
input: Record<string, unknown>;
|
|
85
100
|
changes?: ChatFileChange[];
|
|
86
101
|
};
|
|
102
|
+
export interface AcpPermissionRequest {
|
|
103
|
+
kind: string | null;
|
|
104
|
+
title: string | null;
|
|
105
|
+
commands: string[];
|
|
106
|
+
}
|
|
87
107
|
export interface AcpPermission {
|
|
88
108
|
requestId: string;
|
|
89
109
|
rpcId: number | string;
|
|
90
110
|
kind: string | null;
|
|
111
|
+
request: AcpPermissionRequest;
|
|
91
112
|
events: BackendEvent[];
|
|
92
113
|
}
|
|
93
114
|
export interface AcpStopWords {
|
|
@@ -40,12 +40,42 @@ const PermissionRequestSchema = z.looseObject({
|
|
|
40
40
|
});
|
|
41
41
|
const SessionNotificationSchema = z.looseObject({ sessionId: z.string(), update: z.looseObject({ sessionUpdate: z.string() }) });
|
|
42
42
|
const PromptResponseSchema = z.looseObject({ stopReason: z.string() });
|
|
43
|
+
const tokenCount = z.number().int().nonnegative();
|
|
44
|
+
// Unstable in the protocol (`PromptResponse.usage`); only the three totals are required.
|
|
45
|
+
const PromptUsageSchema = z.looseObject({
|
|
46
|
+
totalTokens: tokenCount,
|
|
47
|
+
inputTokens: tokenCount,
|
|
48
|
+
outputTokens: tokenCount,
|
|
49
|
+
thoughtTokens: tokenCount.nullish(),
|
|
50
|
+
cachedReadTokens: tokenCount.nullish(),
|
|
51
|
+
cachedWriteTokens: tokenCount.nullish()
|
|
52
|
+
});
|
|
53
|
+
/*
|
|
54
|
+
* The `usage` a `session/prompt` response carries, or null where the agent reports none. The protocol
|
|
55
|
+
* leaves open whether the input holds the cached part, so the total decides: when the kinds add up
|
|
56
|
+
* to more than it, the input counted the cache too and loses it here.
|
|
57
|
+
*/
|
|
58
|
+
export function promptUsageOf(result) {
|
|
59
|
+
const response = z.looseObject({ usage: PromptUsageSchema }).safeParse(result);
|
|
60
|
+
if (!response.success) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
const usage = response.data.usage;
|
|
64
|
+
const cacheRead = usage.cachedReadTokens ?? 0;
|
|
65
|
+
const cacheWrite = usage.cachedWriteTokens ?? 0;
|
|
66
|
+
const apart = usage.inputTokens + usage.outputTokens + cacheRead + cacheWrite <= usage.totalTokens;
|
|
67
|
+
const input = apart ? usage.inputTokens : Math.max(0, usage.inputTokens - cacheRead - cacheWrite);
|
|
68
|
+
if (input + cacheRead + cacheWrite + usage.outputTokens === 0) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
return { input, cacheRead, cacheWrite, output: usage.outputTokens, reasoning: usage.thoughtTokens ?? 0 };
|
|
72
|
+
}
|
|
43
73
|
const ModeStateSchema = z.looseObject({
|
|
44
74
|
currentModeId: z.string(),
|
|
45
75
|
availableModes: z.array(z.unknown())
|
|
46
76
|
});
|
|
47
77
|
const ModeSchema = z.looseObject({ id: z.string(), name: z.string() });
|
|
48
|
-
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() });
|
|
49
79
|
const ConfigOptionSchema = z.looseObject({
|
|
50
80
|
id: z.string(),
|
|
51
81
|
name: z.string(),
|
|
@@ -114,6 +144,18 @@ function selectValues(option) {
|
|
|
114
144
|
function selectOption(options, category) {
|
|
115
145
|
return options.find((option) => option.type === 'select' && option.category === category && typeof option.currentValue === 'string') ?? null;
|
|
116
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
|
+
}
|
|
117
159
|
export function parseSessionInfo(result, sessionId, agent) {
|
|
118
160
|
const record = isRecord(result) ? result : {};
|
|
119
161
|
const options = validItems(ConfigOptionSchema, Array.isArray(record.configOptions) ? record.configOptions : []);
|
|
@@ -122,7 +164,7 @@ export function parseSessionInfo(result, sessionId, agent) {
|
|
|
122
164
|
function sessionState(options, modes, models) {
|
|
123
165
|
const modelOption = selectOption(options, 'model');
|
|
124
166
|
const modeOption = selectOption(options, 'mode');
|
|
125
|
-
const
|
|
167
|
+
const thought = thoughtOption(options);
|
|
126
168
|
const modeState = ModeStateSchema.safeParse(modes);
|
|
127
169
|
const modelState = ModelStateSchema.safeParse(models);
|
|
128
170
|
return {
|
|
@@ -156,8 +198,12 @@ function sessionState(options, modes, models) {
|
|
|
156
198
|
configId: null
|
|
157
199
|
}
|
|
158
200
|
: null,
|
|
159
|
-
thoughtLevel:
|
|
160
|
-
? {
|
|
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
|
+
}
|
|
161
207
|
: null
|
|
162
208
|
};
|
|
163
209
|
}
|
|
@@ -416,7 +462,9 @@ export class AcpProtocol {
|
|
|
416
462
|
description: tool.title === '' ? null : tool.title,
|
|
417
463
|
canAllowAlways: options.some((option) => option.kind === 'allow_always')
|
|
418
464
|
});
|
|
419
|
-
|
|
465
|
+
const command = rawField(tool.rawInput, 'command', 'cmd');
|
|
466
|
+
const asked = { kind: tool.kind, title: tool.title === '' ? null : tool.title, commands: [...(command === null ? [] : [command]), ...tool.text] };
|
|
467
|
+
return { requestId, rpcId, kind: tool.kind, request: asked, events };
|
|
420
468
|
}
|
|
421
469
|
/*
|
|
422
470
|
* The answer to a permission request: allow once, reject, or allow always only when the agent offered it.
|
package/dist/chat/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { type AcpAgentSpec, type AcpClientInfo, createAcpBackend } from './acp-backend.ts';
|
|
2
|
-
export type { AcpAgentInfo, AcpModel, AcpModelState, AcpMode, AcpModeState, AcpSessionInfo, AcpModeMap } from './acp-protocol.ts';
|
|
1
|
+
export { type AcpAgentSpec, type AcpClientInfo, type AcpUsageReport, createAcpBackend } from './acp-backend.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';
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createAcpBackend } from '../chat/acp-backend.js';
|
|
2
2
|
import { askAcpAccount } from './accounts/acp-account.js';
|
|
3
|
+
import { writeAcpUsage } from './acp-usage-log.js';
|
|
3
4
|
import { detectCli } from './detect.js';
|
|
4
5
|
/*
|
|
5
6
|
* What every ACP agent can do through this backend. A file change comes as the text before and after;
|
|
@@ -22,18 +23,47 @@ export const ACP_CAPABILITIES = {
|
|
|
22
23
|
reportsThinking: true,
|
|
23
24
|
slashCommands: true
|
|
24
25
|
};
|
|
26
|
+
/* Agents whose own usage files the scanner reads; a log of their turns would count every one twice. */
|
|
27
|
+
const OWN_USAGE_FILES = new Set(['grok']);
|
|
25
28
|
/* An agent's model id may reach a command line, so one that reads as a flag is left out. */
|
|
26
29
|
function adoptableModels(info) {
|
|
27
30
|
return (info.models?.models ?? []).filter((model) => model.id !== '' && !model.id.startsWith('-')).map((model) => ({ slug: model.id, name: model.name }));
|
|
28
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
|
+
}
|
|
29
52
|
export function createAcpProvider(spec) {
|
|
30
53
|
const capabilities = { ...ACP_CAPABILITIES, ...spec.capabilities };
|
|
31
54
|
const onSession = (info) => {
|
|
32
55
|
if (spec.adoptModels !== false && info.models !== null) {
|
|
33
|
-
spec.catalog.adopt(adoptableModels(info), info.models.current);
|
|
56
|
+
spec.catalog.adopt(adoptableModels(info), info.models.current, effortOption(info));
|
|
34
57
|
}
|
|
35
58
|
spec.onSession?.(info);
|
|
36
59
|
};
|
|
60
|
+
const usageLog = spec.usageLog === undefined || OWN_USAGE_FILES.has(spec.kind) ? null : spec.usageLog;
|
|
61
|
+
const onUsage = usageLog === null
|
|
62
|
+
? spec.onUsage
|
|
63
|
+
: (report) => {
|
|
64
|
+
writeAcpUsage(usageLog, report, Date.now());
|
|
65
|
+
spec.onUsage?.(report);
|
|
66
|
+
};
|
|
37
67
|
return {
|
|
38
68
|
kind: spec.kind,
|
|
39
69
|
name: spec.name,
|
|
@@ -63,6 +93,6 @@ export function createAcpProvider(spec) {
|
|
|
63
93
|
onSession
|
|
64
94
|
})
|
|
65
95
|
}),
|
|
66
|
-
createBackend: createAcpBackend({ ...spec, onSession })
|
|
96
|
+
createBackend: createAcpBackend({ ...spec, onSession, ...(onUsage === undefined ? {} : { onUsage }) })
|
|
67
97
|
};
|
|
68
98
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { appendFileSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { errorText } from '../error-text.js';
|
|
4
|
+
import { hostUsageLine } from '../usage/readers/host-log.js';
|
|
5
|
+
/*
|
|
6
|
+
* Appends one turn to the session's own file in the folder, so a scan that hashes what it already read
|
|
7
|
+
* of a file never rereads the log of every other session. Written at once, since it is one line per turn.
|
|
8
|
+
*/
|
|
9
|
+
export function writeAcpUsage(folder, report, at) {
|
|
10
|
+
const { sessionId, cwd, model, ...tokens } = report;
|
|
11
|
+
try {
|
|
12
|
+
mkdirSync(folder, { recursive: true, mode: 0o700 });
|
|
13
|
+
appendFileSync(join(folder, `${encodeURIComponent(sessionId === '' ? 'session' : sessionId)}.jsonl`), hostUsageLine({ at, model, session: sessionId, cwd, ...tokens }), {
|
|
14
|
+
mode: 0o600
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
catch (e) {
|
|
18
|
+
// A turn whose line could not be written is missing from the usage page; the chat itself goes on.
|
|
19
|
+
console.warn('Could not write the usage of an ACP turn:', errorText(e));
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -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,
|
|
@@ -12,5 +12,6 @@ export { type CodexRules, codexRulesText, codexRulesPathIn, defaultCodexHome, in
|
|
|
12
12
|
export { codexThreadOptions, codexServiceTier } from './codex.ts';
|
|
13
13
|
export { type CliDetection, detectCli } from './detect.ts';
|
|
14
14
|
export type { ChatProvider, ProviderHome } from './provider.ts';
|
|
15
|
+
export { isOnlyCommands } from './shell-commands.ts';
|
|
15
16
|
export { ProviderRegistry, type ProviderRegistryOptions } from './registry.ts';
|
|
16
17
|
export { MODE_ORDER, narrowerMode, ceilingForOpening, modeFlag } from '../modes.ts';
|
package/dist/providers/index.js
CHANGED
|
@@ -10,5 +10,6 @@ export { createCodexProvider } from './codex-provider.js';
|
|
|
10
10
|
export { codexRulesText, codexRulesPathIn, defaultCodexHome, installCodexRules } from './codex-rules.js';
|
|
11
11
|
export { codexThreadOptions, codexServiceTier } from './codex.js';
|
|
12
12
|
export { detectCli } from './detect.js';
|
|
13
|
+
export { isOnlyCommands } from './shell-commands.js';
|
|
13
14
|
export { ProviderRegistry } from './registry.js';
|
|
14
15
|
export { MODE_ORDER, narrowerMode, ceilingForOpening, modeFlag } from '../modes.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function isOnlyCommands(command: string, allowed: readonly string[]): boolean;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// Unquoted, each of these makes the shell do more than run a word list: a pipe, a redirect, an expansion,
|
|
2
|
+
// a subshell or group, a glob, a comment or a history or negation mark.
|
|
3
|
+
const REFUSED = new Set(['|', '>', '<', '$', '`', '(', ')', '{', '}', '*', '?', '[', '~', '#', '!']);
|
|
4
|
+
/*
|
|
5
|
+
* Whether a shell command line is only simple commands, joined by `&&`, `||`, `;` or newlines, each of
|
|
6
|
+
* which starts with one of `allowed`. Anything the tokenizer cannot prove simple answers false, so a
|
|
7
|
+
* true is safe to act on without a person: a pipe, a redirect, an expansion, a subshell, a background
|
|
8
|
+
* `&`, an unquoted glob or an unbalanced quote all refuse the whole line.
|
|
9
|
+
*/
|
|
10
|
+
export function isOnlyCommands(command, allowed) {
|
|
11
|
+
let word = '';
|
|
12
|
+
let inWord = false;
|
|
13
|
+
let words = [];
|
|
14
|
+
let commands = 0;
|
|
15
|
+
// A separator that still needs a command after it; a trailing `;` or newline needs none.
|
|
16
|
+
let dangling = false;
|
|
17
|
+
const endWord = () => {
|
|
18
|
+
if (inWord) {
|
|
19
|
+
words.push(word);
|
|
20
|
+
}
|
|
21
|
+
word = '';
|
|
22
|
+
inWord = false;
|
|
23
|
+
};
|
|
24
|
+
const endCommand = (separator) => {
|
|
25
|
+
endWord();
|
|
26
|
+
if (words.length === 0) {
|
|
27
|
+
// Blank lines are fine, and so is a newline after `&&` or `||`; an empty command before any other separator is not.
|
|
28
|
+
return separator === '\n';
|
|
29
|
+
}
|
|
30
|
+
if (!allowed.includes(words[0])) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
commands++;
|
|
34
|
+
words = [];
|
|
35
|
+
dangling = separator === '&&' || separator === '||';
|
|
36
|
+
return true;
|
|
37
|
+
};
|
|
38
|
+
for (let i = 0; i < command.length; i++) {
|
|
39
|
+
const char = command[i];
|
|
40
|
+
const next = command[i + 1];
|
|
41
|
+
if (char === ' ' || char === '\t') {
|
|
42
|
+
endWord();
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (char === '\n' || char === ';') {
|
|
46
|
+
if (!endCommand(char)) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if ((char === '&' || char === '|') && next === char) {
|
|
52
|
+
if (!endCommand(char + next)) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
i++;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (char === '&' || REFUSED.has(char)) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
if (char === '\\') {
|
|
62
|
+
if (next === undefined) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
i++;
|
|
66
|
+
if (next !== '\n') {
|
|
67
|
+
word += next;
|
|
68
|
+
inWord = true;
|
|
69
|
+
}
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (char === "'") {
|
|
73
|
+
const close = command.indexOf("'", i + 1);
|
|
74
|
+
if (close === -1) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
word += command.slice(i + 1, close);
|
|
78
|
+
inWord = true;
|
|
79
|
+
i = close;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (char === '"') {
|
|
83
|
+
inWord = true;
|
|
84
|
+
let closed = false;
|
|
85
|
+
for (i++; i < command.length; i++) {
|
|
86
|
+
const quoted = command[i];
|
|
87
|
+
if (quoted === '"') {
|
|
88
|
+
closed = true;
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
if (quoted === '$' || quoted === '`') {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
if (quoted === '\\') {
|
|
95
|
+
const escaped = command[i + 1];
|
|
96
|
+
if (escaped === undefined) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
i++;
|
|
100
|
+
if (escaped === '"' || escaped === '\\' || escaped === '$' || escaped === '`') {
|
|
101
|
+
word += escaped;
|
|
102
|
+
}
|
|
103
|
+
else if (escaped !== '\n') {
|
|
104
|
+
word += quoted + escaped;
|
|
105
|
+
}
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
word += quoted;
|
|
109
|
+
}
|
|
110
|
+
if (!closed) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
word += char;
|
|
116
|
+
inWord = true;
|
|
117
|
+
}
|
|
118
|
+
return endCommand('\n') && commands > 0 && !dangling;
|
|
119
|
+
}
|
|
@@ -6,6 +6,8 @@ export interface FakeAcpOptions {
|
|
|
6
6
|
modes?: readonly string[];
|
|
7
7
|
legacyModels?: boolean;
|
|
8
8
|
closeSession?: boolean;
|
|
9
|
+
turnUsage?: Record<string, number>;
|
|
10
|
+
reasoningEffort?: 'accept' | 'reject';
|
|
9
11
|
}
|
|
10
12
|
export declare function fakeAcpWith(options: FakeAcpOptions): FakeCli;
|
|
11
13
|
export declare const fakeAcp: FakeCli;
|