@adecore/agents 0.31.8 → 0.31.10
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 +1 -0
- package/dist/chat/acp-backend.js +7 -3
- package/dist/chat/acp-protocol.d.ts +10 -2
- package/dist/chat/acp-protocol.js +81 -18
- package/dist/chat/chat-session.js +11 -0
- package/dist/chat/fixtures/copilot-1.0.101-denied.json +1050 -0
- package/dist/chat/fixtures/copilot-1.0.101-turn.json +1 -0
- package/dist/chat/fixtures/grok-1.0.62-denied.json +630 -0
- package/dist/chat/fixtures/grok-1.0.62-usage.json +475 -0
- package/dist/providers/acp-agent-state.js +14 -1
- package/dist/testing/fake-acp-capture.d.ts +2 -1
- package/dist/testing/fake-acp-capture.js +29 -9
- package/dist/usage/scanner.d.ts +6 -0
- package/dist/usage/scanner.js +101 -74
- package/package.json +3 -3
- package/src/chat/acp-backend.ts +10 -3
- package/src/chat/acp-protocol.ts +90 -21
- package/src/chat/chat-session.ts +11 -0
- package/src/chat/fixtures/copilot-1.0.101-denied.json +1050 -0
- package/src/chat/fixtures/copilot-1.0.101-turn.json +1 -0
- package/src/chat/fixtures/grok-1.0.62-denied.json +630 -0
- package/src/chat/fixtures/grok-1.0.62-usage.json +475 -0
- package/src/providers/acp-agent-state.ts +15 -1
- package/src/testing/fake-acp-capture.ts +34 -11
- package/src/usage/scanner.ts +117 -81
|
@@ -19,6 +19,7 @@ export interface AcpAgentSpec {
|
|
|
19
19
|
onUsage?(usage: AcpUsageReport): void;
|
|
20
20
|
onContextReport?(): void;
|
|
21
21
|
onCommands?(commands: string[]): void;
|
|
22
|
+
adoptTitles?: boolean;
|
|
22
23
|
autoAllow?(request: AcpPermissionRequest): boolean;
|
|
23
24
|
}
|
|
24
25
|
export interface AcpUsageReport extends AcpTurnUsage {
|
package/dist/chat/acp-backend.js
CHANGED
|
@@ -49,7 +49,7 @@ export class AcpBackend {
|
|
|
49
49
|
this.host = host;
|
|
50
50
|
this.spec = spec;
|
|
51
51
|
this.client = spec.client ?? DEFAULT_ACP_CLIENT;
|
|
52
|
-
this.protocol = new AcpProtocol(launch.generation, spec.modes ?? {}, acpStopWords(spec.name));
|
|
52
|
+
this.protocol = new AcpProtocol(launch.generation, spec.modes ?? {}, acpStopWords(spec.name), spec.adoptTitles === true);
|
|
53
53
|
}
|
|
54
54
|
get running() {
|
|
55
55
|
return this.transport !== null;
|
|
@@ -340,7 +340,7 @@ export class AcpBackend {
|
|
|
340
340
|
if (this.transport === transport && this.prompt === turn) {
|
|
341
341
|
this.prompt = null;
|
|
342
342
|
this.report(result);
|
|
343
|
-
this.emitAll(this.protocol.finish(turn.
|
|
343
|
+
this.emitAll(this.protocol.finish(result, turn.promptId, turn.cancelled));
|
|
344
344
|
}
|
|
345
345
|
})
|
|
346
346
|
.catch((error) => {
|
|
@@ -387,7 +387,11 @@ export class AcpBackend {
|
|
|
387
387
|
if (!answer) {
|
|
388
388
|
return false;
|
|
389
389
|
}
|
|
390
|
-
|
|
390
|
+
if (!this.respond(requestId, answer.rpcId, answer.result)) {
|
|
391
|
+
return false;
|
|
392
|
+
}
|
|
393
|
+
this.emitAll(answer.events);
|
|
394
|
+
return true;
|
|
391
395
|
}
|
|
392
396
|
/* ACP asks no questions of its own; an agent asks in its message and the next prompt answers. */
|
|
393
397
|
respondQuestion() {
|
|
@@ -99,6 +99,7 @@ interface ToolState {
|
|
|
99
99
|
text: string[];
|
|
100
100
|
shown: string | null;
|
|
101
101
|
settled: boolean;
|
|
102
|
+
refused: boolean;
|
|
102
103
|
}
|
|
103
104
|
export declare function toolRow(tool: Pick<ToolState, 'title' | 'name' | 'kind' | 'rawInput' | 'locations' | 'diffs'>): {
|
|
104
105
|
name: string;
|
|
@@ -130,12 +131,18 @@ export declare class AcpProtocol {
|
|
|
130
131
|
private readonly tools;
|
|
131
132
|
private readonly permissions;
|
|
132
133
|
private text;
|
|
134
|
+
private refusalLine;
|
|
133
135
|
private thought;
|
|
134
136
|
private counter;
|
|
135
137
|
private costUsd;
|
|
138
|
+
private standardUsage;
|
|
139
|
+
private vendorTokens;
|
|
136
140
|
private readonly prompts;
|
|
137
|
-
|
|
141
|
+
private readonly titles;
|
|
142
|
+
constructor(generation: number, modes: AcpModeMap, words: AcpStopWords, titles?: boolean);
|
|
138
143
|
update(sessionId: string, params: unknown): BackendEvent[];
|
|
144
|
+
private mapUpdate;
|
|
145
|
+
private vendorUsage;
|
|
139
146
|
prompted(text: string): void;
|
|
140
147
|
private restatesPrompt;
|
|
141
148
|
modeEvent(modeId: string): BackendEvent;
|
|
@@ -143,6 +150,7 @@ export declare class AcpProtocol {
|
|
|
143
150
|
answer(requestId: string, decision: ApprovalDecision): {
|
|
144
151
|
rpcId: number | string;
|
|
145
152
|
result: unknown;
|
|
153
|
+
events: BackendEvent[];
|
|
146
154
|
} | null;
|
|
147
155
|
cancelPermissions(): Array<{
|
|
148
156
|
requestId: string;
|
|
@@ -150,7 +158,7 @@ export declare class AcpProtocol {
|
|
|
150
158
|
result: unknown;
|
|
151
159
|
}>;
|
|
152
160
|
hasPermission(requestId: string): boolean;
|
|
153
|
-
finish(result: unknown, promptId: string | undefined): BackendEvent[];
|
|
161
|
+
finish(result: unknown, promptId: string | undefined, cancelled?: boolean): BackendEvent[];
|
|
154
162
|
abort(promptId: string | undefined, error: string | null): BackendEvent[];
|
|
155
163
|
private messageChunk;
|
|
156
164
|
private thoughtChunk;
|
|
@@ -96,6 +96,14 @@ function contextWindowOf(meta) {
|
|
|
96
96
|
const tokens = isRecord(meta) ? meta.totalContextTokens : undefined;
|
|
97
97
|
return typeof tokens === 'number' && Number.isSafeInteger(tokens) && tokens > 0 ? tokens : null;
|
|
98
98
|
}
|
|
99
|
+
/*
|
|
100
|
+
* The context tokens an agent counts outside the protocol, in `_meta.totalTokens`: Grok CLI 1.0.62 sends no `usage_update`
|
|
101
|
+
* and puts the session's count there, on every `session/update` notification and on the `session/prompt` response.
|
|
102
|
+
*/
|
|
103
|
+
function vendorContextTokens(meta) {
|
|
104
|
+
const tokens = isRecord(meta) ? meta.totalTokens : undefined;
|
|
105
|
+
return typeof tokens === 'number' && Number.isSafeInteger(tokens) && tokens > 0 ? tokens : null;
|
|
106
|
+
}
|
|
99
107
|
const AuthMethodSchema = z.looseObject({ id: z.string(), name: z.string() });
|
|
100
108
|
const InitializeResponseSchema = z.looseObject({
|
|
101
109
|
protocolVersion: z.number(),
|
|
@@ -174,6 +182,9 @@ function sessionState(options, modes, models) {
|
|
|
174
182
|
const thought = thoughtOption(options);
|
|
175
183
|
const modeState = ModeStateSchema.safeParse(modes);
|
|
176
184
|
const modelState = ModelStateSchema.safeParse(models);
|
|
185
|
+
const entries = modelState.success ? validItems(ModelEntrySchema, modelState.data.availableModels) : [];
|
|
186
|
+
// Grok CLI 1.0.62 offers its models as a config option too, but names their windows only in the older field.
|
|
187
|
+
const windows = new Map(entries.map((model) => [model.modelId, contextWindowOf(model._meta)]));
|
|
177
188
|
return {
|
|
178
189
|
models: modelOption
|
|
179
190
|
? {
|
|
@@ -182,14 +193,14 @@ function sessionState(options, modes, models) {
|
|
|
182
193
|
id: value.value,
|
|
183
194
|
name: value.name,
|
|
184
195
|
description: null,
|
|
185
|
-
contextWindow: contextWindowOf(value._meta)
|
|
196
|
+
contextWindow: contextWindowOf(value._meta) ?? windows.get(value.value) ?? null
|
|
186
197
|
})),
|
|
187
198
|
configId: modelOption.id
|
|
188
199
|
}
|
|
189
200
|
: modelState.success
|
|
190
201
|
? {
|
|
191
202
|
current: modelState.data.currentModelId,
|
|
192
|
-
models:
|
|
203
|
+
models: entries.map((model) => ({
|
|
193
204
|
id: model.modelId,
|
|
194
205
|
name: model.name,
|
|
195
206
|
description: model.description ?? null,
|
|
@@ -312,6 +323,8 @@ const KIND_NAMES = {
|
|
|
312
323
|
};
|
|
313
324
|
// A session's title comes from its opening prompts; later ones are not kept.
|
|
314
325
|
const KEPT_PROMPTS = 3;
|
|
326
|
+
// What Copilot CLI 1.0.101 writes as a message of its own after a call a person turned down; the thread shows the refusal already.
|
|
327
|
+
const REFUSAL_LINE = 'Info: Operation cancelled by user';
|
|
315
328
|
function foldSpace(text) {
|
|
316
329
|
return text.replace(/\s+/g, ' ').trim();
|
|
317
330
|
}
|
|
@@ -388,7 +401,7 @@ function toolOutput(tool) {
|
|
|
388
401
|
return tool.rawOutput;
|
|
389
402
|
}
|
|
390
403
|
if (isRecord(tool.rawOutput)) {
|
|
391
|
-
const text = rawField(tool.rawOutput, 'output', 'stdout', 'content', 'result', 'error');
|
|
404
|
+
const text = rawField(tool.rawOutput, 'output', 'stdout', 'content', 'result', 'error', 'message');
|
|
392
405
|
return text ?? JSON.stringify(tool.rawOutput);
|
|
393
406
|
}
|
|
394
407
|
return null;
|
|
@@ -411,16 +424,25 @@ export class AcpProtocol {
|
|
|
411
424
|
words;
|
|
412
425
|
tools = new Map();
|
|
413
426
|
permissions = new Map();
|
|
427
|
+
// `held` while the message may still be the refusal line, whose chunks the thread has not heard yet.
|
|
414
428
|
text = null;
|
|
429
|
+
// Set by a refusal until the next message shows whether it is the refusal line.
|
|
430
|
+
refusalLine = false;
|
|
415
431
|
thought = null;
|
|
416
432
|
counter = 0;
|
|
417
433
|
costUsd = 0;
|
|
434
|
+
// Set by the first `usage_update`; from then on a count outside the protocol is never read.
|
|
435
|
+
standardUsage = false;
|
|
436
|
+
vendorTokens = null;
|
|
418
437
|
// The opening prompts of the process, whitespace folded, which an agent may hand back as the session's title.
|
|
419
438
|
prompts = [];
|
|
420
|
-
|
|
439
|
+
titles;
|
|
440
|
+
// `titles` takes the session titles the agent sends as the chat's suggested title.
|
|
441
|
+
constructor(generation, modes, words, titles = false) {
|
|
421
442
|
this.generation = generation;
|
|
422
443
|
this.modes = modes;
|
|
423
444
|
this.words = words;
|
|
445
|
+
this.titles = titles;
|
|
424
446
|
}
|
|
425
447
|
/* One `session/update` notification; empty for another session, an unknown update or one that does not parse. */
|
|
426
448
|
update(sessionId, params) {
|
|
@@ -428,7 +450,9 @@ export class AcpProtocol {
|
|
|
428
450
|
if (!notification.success || notification.data.sessionId !== sessionId) {
|
|
429
451
|
return [];
|
|
430
452
|
}
|
|
431
|
-
|
|
453
|
+
return [...this.mapUpdate(notification.data.update), ...this.vendorUsage(notification.data._meta)];
|
|
454
|
+
}
|
|
455
|
+
mapUpdate(update) {
|
|
432
456
|
switch (update.sessionUpdate) {
|
|
433
457
|
case 'agent_message_chunk':
|
|
434
458
|
return this.messageChunk(update);
|
|
@@ -458,6 +482,7 @@ export class AcpProtocol {
|
|
|
458
482
|
if (!usage.success) {
|
|
459
483
|
return [];
|
|
460
484
|
}
|
|
485
|
+
this.standardUsage = true;
|
|
461
486
|
const cost = usage.data.cost?.currency === 'USD' ? usage.data.cost.amount : undefined;
|
|
462
487
|
if (cost !== undefined) {
|
|
463
488
|
this.costUsd = cost;
|
|
@@ -465,6 +490,9 @@ export class AcpProtocol {
|
|
|
465
490
|
return [{ type: 'usage', contextTokens: usage.data.used, contextWindow: usage.data.size, ...(cost === undefined ? {} : { costUsd: cost }) }];
|
|
466
491
|
}
|
|
467
492
|
case 'session_info_update': {
|
|
493
|
+
if (!this.titles) {
|
|
494
|
+
return [];
|
|
495
|
+
}
|
|
468
496
|
const title = cleanTitle(update.title);
|
|
469
497
|
return title === null || this.restatesPrompt(title) ? [] : [{ type: 'title', title }];
|
|
470
498
|
}
|
|
@@ -472,6 +500,15 @@ export class AcpProtocol {
|
|
|
472
500
|
return [];
|
|
473
501
|
}
|
|
474
502
|
}
|
|
503
|
+
/* The fallback for an agent that sends no `usage_update`: its own count of the context, once each time it changes. */
|
|
504
|
+
vendorUsage(meta) {
|
|
505
|
+
const tokens = this.standardUsage ? null : vendorContextTokens(meta);
|
|
506
|
+
if (tokens === null || tokens === this.vendorTokens) {
|
|
507
|
+
return [];
|
|
508
|
+
}
|
|
509
|
+
this.vendorTokens = tokens;
|
|
510
|
+
return [{ type: 'usage', contextTokens: tokens }];
|
|
511
|
+
}
|
|
475
512
|
/* Notes a prompt sent to the agent, so a title that only repeats its start is not taken for a name. */
|
|
476
513
|
prompted(text) {
|
|
477
514
|
if (this.prompts.length < KEPT_PROMPTS) {
|
|
@@ -501,7 +538,7 @@ export class AcpProtocol {
|
|
|
501
538
|
const tool = this.tools.get(request.data.toolCall.toolCallId);
|
|
502
539
|
const row = toolRow(tool);
|
|
503
540
|
const options = request.data.options;
|
|
504
|
-
this.permissions.set(requestId, { rpcId, options });
|
|
541
|
+
this.permissions.set(requestId, { rpcId, toolCallId: request.data.toolCall.toolCallId, options });
|
|
505
542
|
events.push({
|
|
506
543
|
type: 'approval.requested',
|
|
507
544
|
requestId,
|
|
@@ -518,7 +555,7 @@ export class AcpProtocol {
|
|
|
518
555
|
/*
|
|
519
556
|
* The answer to a permission request: allow once, reject, or allow always only when the agent offered it.
|
|
520
557
|
* A decision the agent offered no option for falls to the narrower one, and a refusal with no reject option
|
|
521
|
-
* at all is a cancelled outcome.
|
|
558
|
+
* at all is a cancelled outcome. A refusal draws the call's row again without its diff.
|
|
522
559
|
*/
|
|
523
560
|
answer(requestId, decision) {
|
|
524
561
|
const pending = this.permissions.get(requestId);
|
|
@@ -540,7 +577,14 @@ export class AcpProtocol {
|
|
|
540
577
|
: decision === 'allow'
|
|
541
578
|
? byKind('allow_once', 'allow_always')
|
|
542
579
|
: byKind('reject_once', 'reject_always');
|
|
543
|
-
|
|
580
|
+
const result = { outcome: optionId === null ? { outcome: 'cancelled' } : { outcome: 'selected', optionId } };
|
|
581
|
+
const tool = this.tools.get(pending.toolCallId);
|
|
582
|
+
if (decision !== 'deny' || tool === undefined) {
|
|
583
|
+
return { rpcId: pending.rpcId, result, events: [] };
|
|
584
|
+
}
|
|
585
|
+
tool.refused = true;
|
|
586
|
+
this.refusalLine = true;
|
|
587
|
+
return { rpcId: pending.rpcId, result, events: this.toolCall({ toolCallId: pending.toolCallId }, false) };
|
|
544
588
|
}
|
|
545
589
|
/* Every permission request still open, answered as cancelled, which the protocol asks of a client that cancels a turn. */
|
|
546
590
|
cancelPermissions() {
|
|
@@ -555,11 +599,17 @@ export class AcpProtocol {
|
|
|
555
599
|
hasPermission(requestId) {
|
|
556
600
|
return this.permissions.has(requestId);
|
|
557
601
|
}
|
|
558
|
-
/*
|
|
559
|
-
|
|
602
|
+
/*
|
|
603
|
+
* The end of a turn, from the `stopReason` of its `session/prompt` response. Only a turn the client cancelled
|
|
604
|
+
* (`cancelled`) ends aborted: Grok 1.0.62 answers `cancelled` by itself when a person turns a call down, and
|
|
605
|
+
* that ends its turn the way any answer does.
|
|
606
|
+
*/
|
|
607
|
+
finish(result, promptId, cancelled = false) {
|
|
560
608
|
const parsed = PromptResponseSchema.safeParse(result);
|
|
561
|
-
const
|
|
562
|
-
const
|
|
609
|
+
const stopReason = parsed.success ? parsed.data.stopReason : 'end_turn';
|
|
610
|
+
const reason = cancelled ? 'cancelled' : stopReason === 'cancelled' ? 'end_turn' : stopReason;
|
|
611
|
+
this.refusalLine = false;
|
|
612
|
+
const events = [...this.closeText(), ...this.closeThought(), ...this.vendorUsage(isRecord(result) ? result._meta : undefined)];
|
|
563
613
|
const base = { ...(promptId === undefined ? {} : { promptId }), costUsd: this.costUsd };
|
|
564
614
|
switch (reason) {
|
|
565
615
|
case 'cancelled':
|
|
@@ -581,6 +631,7 @@ export class AcpProtocol {
|
|
|
581
631
|
}
|
|
582
632
|
/* A turn that ended without a `stopReason`: the request failed, or was cancelled before the agent said so. */
|
|
583
633
|
abort(promptId, error) {
|
|
634
|
+
this.refusalLine = false;
|
|
584
635
|
const base = { ...(promptId === undefined ? {} : { promptId }), costUsd: this.costUsd };
|
|
585
636
|
return [
|
|
586
637
|
...this.closeText(),
|
|
@@ -599,10 +650,18 @@ export class AcpProtocol {
|
|
|
599
650
|
events.push(...this.closeText());
|
|
600
651
|
}
|
|
601
652
|
if (this.text === null) {
|
|
602
|
-
this.text = { ref: messageId === null ? `message-${++this.counter}` : `message-${messageId}`, messageId, text: '' };
|
|
653
|
+
this.text = { ref: messageId === null ? `message-${++this.counter}` : `message-${messageId}`, messageId, text: '', held: this.refusalLine };
|
|
654
|
+
this.refusalLine = false;
|
|
655
|
+
}
|
|
656
|
+
const text = this.text;
|
|
657
|
+
text.text += chunk.data.content.text;
|
|
658
|
+
if (text.held && !REFUSAL_LINE.startsWith(text.text)) {
|
|
659
|
+
text.held = false;
|
|
660
|
+
events.push({ type: 'text.delta', ref: text.ref, text: text.text });
|
|
661
|
+
}
|
|
662
|
+
else if (!text.held) {
|
|
663
|
+
events.push({ type: 'text.delta', ref: text.ref, text: chunk.data.content.text });
|
|
603
664
|
}
|
|
604
|
-
this.text.text += chunk.data.content.text;
|
|
605
|
-
events.push({ type: 'text.delta', ref: this.text.ref, text: chunk.data.content.text });
|
|
606
665
|
return events;
|
|
607
666
|
}
|
|
608
667
|
thoughtChunk(update) {
|
|
@@ -621,7 +680,10 @@ export class AcpProtocol {
|
|
|
621
680
|
closeText() {
|
|
622
681
|
const text = this.text;
|
|
623
682
|
this.text = null;
|
|
624
|
-
|
|
683
|
+
if (text === null || (text.held && text.text === REFUSAL_LINE)) {
|
|
684
|
+
return [];
|
|
685
|
+
}
|
|
686
|
+
return [{ type: 'text.done', ref: text.ref, text: text.text }];
|
|
625
687
|
}
|
|
626
688
|
closeThought() {
|
|
627
689
|
const thought = this.thought;
|
|
@@ -642,7 +704,8 @@ export class AcpProtocol {
|
|
|
642
704
|
diffs: [],
|
|
643
705
|
text: [],
|
|
644
706
|
shown: null,
|
|
645
|
-
settled: false
|
|
707
|
+
settled: false,
|
|
708
|
+
refused: false
|
|
646
709
|
};
|
|
647
710
|
tool.title = call.title ?? tool.title;
|
|
648
711
|
tool.name = call.name ?? tool.name;
|
|
@@ -659,7 +722,7 @@ export class AcpProtocol {
|
|
|
659
722
|
}
|
|
660
723
|
this.tools.set(call.toolCallId, tool);
|
|
661
724
|
const events = [];
|
|
662
|
-
const row = toolRow(tool);
|
|
725
|
+
const row = toolRow(tool.refused ? { ...tool, diffs: [] } : tool);
|
|
663
726
|
const shown = JSON.stringify(row);
|
|
664
727
|
if (shown !== tool.shown) {
|
|
665
728
|
tool.shown = shown;
|
|
@@ -1708,6 +1708,17 @@ export class ChatSession {
|
|
|
1708
1708
|
this.refuseStop(event.taskId, event.message);
|
|
1709
1709
|
return;
|
|
1710
1710
|
}
|
|
1711
|
+
/*
|
|
1712
|
+
* A CLI that counts its tokens but names no window, in a chat that has none yet: the catalog may have learned the
|
|
1713
|
+
* model's window since the chat began, as an ACP agent's catalog does from the first session that reports its models.
|
|
1714
|
+
*/
|
|
1715
|
+
if (event.type === 'usage' && event.contextWindow === undefined && event.contextTokens !== undefined && this.thread.info.usage.contextWindow === null) {
|
|
1716
|
+
const contextWindow = this.options.provider.catalog.contextWindowFor(this.launchedSelection ?? this.thread.info.selection);
|
|
1717
|
+
if (contextWindow !== null) {
|
|
1718
|
+
this.emit(this.projector.project(generation, { ...event, contextWindow }));
|
|
1719
|
+
return;
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1711
1722
|
/*
|
|
1712
1723
|
* A CLI reports the window it was started on. A pick made since waits for the restart the next
|
|
1713
1724
|
* send does, so its report may not put the old size back under a meter that already reads the new one.
|