@adecore/agents 0.31.9 → 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/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/chat/acp-protocol.ts
CHANGED
|
@@ -133,6 +133,15 @@ function contextWindowOf(meta: unknown): number | null {
|
|
|
133
133
|
return typeof tokens === 'number' && Number.isSafeInteger(tokens) && tokens > 0 ? tokens : null;
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
+
/*
|
|
137
|
+
* The context tokens an agent counts outside the protocol, in `_meta.totalTokens`: Grok CLI 1.0.62 sends no `usage_update`
|
|
138
|
+
* and puts the session's count there, on every `session/update` notification and on the `session/prompt` response.
|
|
139
|
+
*/
|
|
140
|
+
function vendorContextTokens(meta: unknown): number | null {
|
|
141
|
+
const tokens = isRecord(meta) ? meta.totalTokens : undefined;
|
|
142
|
+
return typeof tokens === 'number' && Number.isSafeInteger(tokens) && tokens > 0 ? tokens : null;
|
|
143
|
+
}
|
|
144
|
+
|
|
136
145
|
const AuthMethodSchema = z.looseObject({ id: z.string(), name: z.string() });
|
|
137
146
|
|
|
138
147
|
const InitializeResponseSchema = z.looseObject({
|
|
@@ -296,6 +305,9 @@ function sessionState(options: ConfigOption[], modes: unknown, models: unknown):
|
|
|
296
305
|
const thought = thoughtOption(options);
|
|
297
306
|
const modeState = ModeStateSchema.safeParse(modes);
|
|
298
307
|
const modelState = ModelStateSchema.safeParse(models);
|
|
308
|
+
const entries = modelState.success ? validItems(ModelEntrySchema, modelState.data.availableModels) : [];
|
|
309
|
+
// Grok CLI 1.0.62 offers its models as a config option too, but names their windows only in the older field.
|
|
310
|
+
const windows = new Map(entries.map((model) => [model.modelId, contextWindowOf(model._meta)]));
|
|
299
311
|
return {
|
|
300
312
|
models: modelOption
|
|
301
313
|
? {
|
|
@@ -304,14 +316,14 @@ function sessionState(options: ConfigOption[], modes: unknown, models: unknown):
|
|
|
304
316
|
id: value.value,
|
|
305
317
|
name: value.name,
|
|
306
318
|
description: null,
|
|
307
|
-
contextWindow: contextWindowOf(value._meta)
|
|
319
|
+
contextWindow: contextWindowOf(value._meta) ?? windows.get(value.value) ?? null
|
|
308
320
|
})),
|
|
309
321
|
configId: modelOption.id
|
|
310
322
|
}
|
|
311
323
|
: modelState.success
|
|
312
324
|
? {
|
|
313
325
|
current: modelState.data.currentModelId,
|
|
314
|
-
models:
|
|
326
|
+
models: entries.map((model) => ({
|
|
315
327
|
id: model.modelId,
|
|
316
328
|
name: model.name,
|
|
317
329
|
description: model.description ?? null,
|
|
@@ -452,6 +464,8 @@ interface ToolState {
|
|
|
452
464
|
// What the thread last heard, so an update that changes nothing about the row sends nothing.
|
|
453
465
|
shown: string | null;
|
|
454
466
|
settled: boolean;
|
|
467
|
+
// A person turned the call down, so whatever diff it carries was never written.
|
|
468
|
+
refused: boolean;
|
|
455
469
|
}
|
|
456
470
|
|
|
457
471
|
// `Edit` would have the thread draw a diff of nothing until the agent sends one.
|
|
@@ -467,6 +481,9 @@ const KIND_NAMES: Record<string, string> = {
|
|
|
467
481
|
// A session's title comes from its opening prompts; later ones are not kept.
|
|
468
482
|
const KEPT_PROMPTS = 3;
|
|
469
483
|
|
|
484
|
+
// 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.
|
|
485
|
+
const REFUSAL_LINE = 'Info: Operation cancelled by user';
|
|
486
|
+
|
|
470
487
|
function foldSpace(text: string): string {
|
|
471
488
|
return text.replace(/\s+/g, ' ').trim();
|
|
472
489
|
}
|
|
@@ -551,7 +568,7 @@ function toolOutput(tool: ToolState): string | null {
|
|
|
551
568
|
return tool.rawOutput;
|
|
552
569
|
}
|
|
553
570
|
if (isRecord(tool.rawOutput)) {
|
|
554
|
-
const text = rawField(tool.rawOutput, 'output', 'stdout', 'content', 'result', 'error');
|
|
571
|
+
const text = rawField(tool.rawOutput, 'output', 'stdout', 'content', 'result', 'error', 'message');
|
|
555
572
|
return text ?? JSON.stringify(tool.rawOutput);
|
|
556
573
|
}
|
|
557
574
|
return null;
|
|
@@ -601,18 +618,28 @@ export class AcpProtocol {
|
|
|
601
618
|
private readonly modes: AcpModeMap;
|
|
602
619
|
private readonly words: AcpStopWords;
|
|
603
620
|
private readonly tools = new Map<string, ToolState>();
|
|
604
|
-
private readonly permissions = new Map<string, { rpcId: number | string; options: Array<z.infer<typeof PermissionOptionSchema>> }>();
|
|
605
|
-
|
|
621
|
+
private readonly permissions = new Map<string, { rpcId: number | string; toolCallId: string; options: Array<z.infer<typeof PermissionOptionSchema>> }>();
|
|
622
|
+
// `held` while the message may still be the refusal line, whose chunks the thread has not heard yet.
|
|
623
|
+
private text: { ref: string; messageId: string | null; text: string; held: boolean } | null = null;
|
|
624
|
+
// Set by a refusal until the next message shows whether it is the refusal line.
|
|
625
|
+
private refusalLine = false;
|
|
606
626
|
private thought: { ref: string; text: string } | null = null;
|
|
607
627
|
private counter = 0;
|
|
608
628
|
private costUsd = 0;
|
|
629
|
+
// Set by the first `usage_update`; from then on a count outside the protocol is never read.
|
|
630
|
+
private standardUsage = false;
|
|
631
|
+
private vendorTokens: number | null = null;
|
|
609
632
|
// The opening prompts of the process, whitespace folded, which an agent may hand back as the session's title.
|
|
610
633
|
private readonly prompts: string[] = [];
|
|
611
634
|
|
|
612
|
-
|
|
635
|
+
private readonly titles: boolean;
|
|
636
|
+
|
|
637
|
+
// `titles` takes the session titles the agent sends as the chat's suggested title.
|
|
638
|
+
constructor(generation: number, modes: AcpModeMap, words: AcpStopWords, titles = false) {
|
|
613
639
|
this.generation = generation;
|
|
614
640
|
this.modes = modes;
|
|
615
641
|
this.words = words;
|
|
642
|
+
this.titles = titles;
|
|
616
643
|
}
|
|
617
644
|
|
|
618
645
|
/* One `session/update` notification; empty for another session, an unknown update or one that does not parse. */
|
|
@@ -621,7 +648,10 @@ export class AcpProtocol {
|
|
|
621
648
|
if (!notification.success || notification.data.sessionId !== sessionId) {
|
|
622
649
|
return [];
|
|
623
650
|
}
|
|
624
|
-
|
|
651
|
+
return [...this.mapUpdate(notification.data.update), ...this.vendorUsage(notification.data._meta)];
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
private mapUpdate(update: z.infer<typeof SessionNotificationSchema>['update']): BackendEvent[] {
|
|
625
655
|
switch (update.sessionUpdate) {
|
|
626
656
|
case 'agent_message_chunk':
|
|
627
657
|
return this.messageChunk(update);
|
|
@@ -651,6 +681,7 @@ export class AcpProtocol {
|
|
|
651
681
|
if (!usage.success) {
|
|
652
682
|
return [];
|
|
653
683
|
}
|
|
684
|
+
this.standardUsage = true;
|
|
654
685
|
const cost = usage.data.cost?.currency === 'USD' ? usage.data.cost.amount : undefined;
|
|
655
686
|
if (cost !== undefined) {
|
|
656
687
|
this.costUsd = cost;
|
|
@@ -658,6 +689,9 @@ export class AcpProtocol {
|
|
|
658
689
|
return [{ type: 'usage', contextTokens: usage.data.used, contextWindow: usage.data.size, ...(cost === undefined ? {} : { costUsd: cost }) }];
|
|
659
690
|
}
|
|
660
691
|
case 'session_info_update': {
|
|
692
|
+
if (!this.titles) {
|
|
693
|
+
return [];
|
|
694
|
+
}
|
|
661
695
|
const title = cleanTitle(update.title);
|
|
662
696
|
return title === null || this.restatesPrompt(title) ? [] : [{ type: 'title', title }];
|
|
663
697
|
}
|
|
@@ -666,6 +700,16 @@ export class AcpProtocol {
|
|
|
666
700
|
}
|
|
667
701
|
}
|
|
668
702
|
|
|
703
|
+
/* The fallback for an agent that sends no `usage_update`: its own count of the context, once each time it changes. */
|
|
704
|
+
private vendorUsage(meta: unknown): BackendEvent[] {
|
|
705
|
+
const tokens = this.standardUsage ? null : vendorContextTokens(meta);
|
|
706
|
+
if (tokens === null || tokens === this.vendorTokens) {
|
|
707
|
+
return [];
|
|
708
|
+
}
|
|
709
|
+
this.vendorTokens = tokens;
|
|
710
|
+
return [{ type: 'usage', contextTokens: tokens }];
|
|
711
|
+
}
|
|
712
|
+
|
|
669
713
|
/* Notes a prompt sent to the agent, so a title that only repeats its start is not taken for a name. */
|
|
670
714
|
prompted(text: string): void {
|
|
671
715
|
if (this.prompts.length < KEPT_PROMPTS) {
|
|
@@ -698,7 +742,7 @@ export class AcpProtocol {
|
|
|
698
742
|
const tool = this.tools.get(request.data.toolCall.toolCallId)!;
|
|
699
743
|
const row = toolRow(tool);
|
|
700
744
|
const options = request.data.options;
|
|
701
|
-
this.permissions.set(requestId, { rpcId, options });
|
|
745
|
+
this.permissions.set(requestId, { rpcId, toolCallId: request.data.toolCall.toolCallId, options });
|
|
702
746
|
events.push({
|
|
703
747
|
type: 'approval.requested',
|
|
704
748
|
requestId,
|
|
@@ -716,9 +760,9 @@ export class AcpProtocol {
|
|
|
716
760
|
/*
|
|
717
761
|
* The answer to a permission request: allow once, reject, or allow always only when the agent offered it.
|
|
718
762
|
* A decision the agent offered no option for falls to the narrower one, and a refusal with no reject option
|
|
719
|
-
* at all is a cancelled outcome.
|
|
763
|
+
* at all is a cancelled outcome. A refusal draws the call's row again without its diff.
|
|
720
764
|
*/
|
|
721
|
-
answer(requestId: string, decision: ApprovalDecision): { rpcId: number | string; result: unknown } | null {
|
|
765
|
+
answer(requestId: string, decision: ApprovalDecision): { rpcId: number | string; result: unknown; events: BackendEvent[] } | null {
|
|
722
766
|
const pending = this.permissions.get(requestId);
|
|
723
767
|
if (!pending) {
|
|
724
768
|
return null;
|
|
@@ -739,7 +783,14 @@ export class AcpProtocol {
|
|
|
739
783
|
: decision === 'allow'
|
|
740
784
|
? byKind('allow_once', 'allow_always')
|
|
741
785
|
: byKind('reject_once', 'reject_always');
|
|
742
|
-
|
|
786
|
+
const result = { outcome: optionId === null ? { outcome: 'cancelled' } : { outcome: 'selected', optionId } };
|
|
787
|
+
const tool = this.tools.get(pending.toolCallId);
|
|
788
|
+
if (decision !== 'deny' || tool === undefined) {
|
|
789
|
+
return { rpcId: pending.rpcId, result, events: [] };
|
|
790
|
+
}
|
|
791
|
+
tool.refused = true;
|
|
792
|
+
this.refusalLine = true;
|
|
793
|
+
return { rpcId: pending.rpcId, result, events: this.toolCall({ toolCallId: pending.toolCallId }, false) };
|
|
743
794
|
}
|
|
744
795
|
|
|
745
796
|
/* Every permission request still open, answered as cancelled, which the protocol asks of a client that cancels a turn. */
|
|
@@ -757,11 +808,17 @@ export class AcpProtocol {
|
|
|
757
808
|
return this.permissions.has(requestId);
|
|
758
809
|
}
|
|
759
810
|
|
|
760
|
-
/*
|
|
761
|
-
|
|
811
|
+
/*
|
|
812
|
+
* The end of a turn, from the `stopReason` of its `session/prompt` response. Only a turn the client cancelled
|
|
813
|
+
* (`cancelled`) ends aborted: Grok 1.0.62 answers `cancelled` by itself when a person turns a call down, and
|
|
814
|
+
* that ends its turn the way any answer does.
|
|
815
|
+
*/
|
|
816
|
+
finish(result: unknown, promptId: string | undefined, cancelled = false): BackendEvent[] {
|
|
762
817
|
const parsed = PromptResponseSchema.safeParse(result);
|
|
763
|
-
const
|
|
764
|
-
const
|
|
818
|
+
const stopReason = parsed.success ? parsed.data.stopReason : 'end_turn';
|
|
819
|
+
const reason = cancelled ? 'cancelled' : stopReason === 'cancelled' ? 'end_turn' : stopReason;
|
|
820
|
+
this.refusalLine = false;
|
|
821
|
+
const events = [...this.closeText(), ...this.closeThought(), ...this.vendorUsage(isRecord(result) ? result._meta : undefined)];
|
|
765
822
|
const base = { ...(promptId === undefined ? {} : { promptId }), costUsd: this.costUsd };
|
|
766
823
|
switch (reason) {
|
|
767
824
|
case 'cancelled':
|
|
@@ -784,6 +841,7 @@ export class AcpProtocol {
|
|
|
784
841
|
|
|
785
842
|
/* A turn that ended without a `stopReason`: the request failed, or was cancelled before the agent said so. */
|
|
786
843
|
abort(promptId: string | undefined, error: string | null): BackendEvent[] {
|
|
844
|
+
this.refusalLine = false;
|
|
787
845
|
const base = { ...(promptId === undefined ? {} : { promptId }), costUsd: this.costUsd };
|
|
788
846
|
return [
|
|
789
847
|
...this.closeText(),
|
|
@@ -803,10 +861,17 @@ export class AcpProtocol {
|
|
|
803
861
|
events.push(...this.closeText());
|
|
804
862
|
}
|
|
805
863
|
if (this.text === null) {
|
|
806
|
-
this.text = { ref: messageId === null ? `message-${++this.counter}` : `message-${messageId}`, messageId, text: '' };
|
|
864
|
+
this.text = { ref: messageId === null ? `message-${++this.counter}` : `message-${messageId}`, messageId, text: '', held: this.refusalLine };
|
|
865
|
+
this.refusalLine = false;
|
|
866
|
+
}
|
|
867
|
+
const text = this.text;
|
|
868
|
+
text.text += chunk.data.content.text;
|
|
869
|
+
if (text.held && !REFUSAL_LINE.startsWith(text.text)) {
|
|
870
|
+
text.held = false;
|
|
871
|
+
events.push({ type: 'text.delta', ref: text.ref, text: text.text });
|
|
872
|
+
} else if (!text.held) {
|
|
873
|
+
events.push({ type: 'text.delta', ref: text.ref, text: chunk.data.content.text });
|
|
807
874
|
}
|
|
808
|
-
this.text.text += chunk.data.content.text;
|
|
809
|
-
events.push({ type: 'text.delta', ref: this.text.ref, text: chunk.data.content.text });
|
|
810
875
|
return events;
|
|
811
876
|
}
|
|
812
877
|
|
|
@@ -827,7 +892,10 @@ export class AcpProtocol {
|
|
|
827
892
|
private closeText(): BackendEvent[] {
|
|
828
893
|
const text = this.text;
|
|
829
894
|
this.text = null;
|
|
830
|
-
|
|
895
|
+
if (text === null || (text.held && text.text === REFUSAL_LINE)) {
|
|
896
|
+
return [];
|
|
897
|
+
}
|
|
898
|
+
return [{ type: 'text.done', ref: text.ref, text: text.text }];
|
|
831
899
|
}
|
|
832
900
|
|
|
833
901
|
private closeThought(): BackendEvent[] {
|
|
@@ -850,7 +918,8 @@ export class AcpProtocol {
|
|
|
850
918
|
diffs: [],
|
|
851
919
|
text: [],
|
|
852
920
|
shown: null,
|
|
853
|
-
settled: false
|
|
921
|
+
settled: false,
|
|
922
|
+
refused: false
|
|
854
923
|
};
|
|
855
924
|
tool.title = call.title ?? tool.title;
|
|
856
925
|
tool.name = call.name ?? tool.name;
|
|
@@ -869,7 +938,7 @@ export class AcpProtocol {
|
|
|
869
938
|
}
|
|
870
939
|
this.tools.set(call.toolCallId, tool);
|
|
871
940
|
const events: BackendEvent[] = [];
|
|
872
|
-
const row = toolRow(tool);
|
|
941
|
+
const row = toolRow(tool.refused ? { ...tool, diffs: [] } : tool);
|
|
873
942
|
const shown = JSON.stringify(row);
|
|
874
943
|
if (shown !== tool.shown) {
|
|
875
944
|
tool.shown = shown;
|
package/src/chat/chat-session.ts
CHANGED
|
@@ -1963,6 +1963,17 @@ export class ChatSession {
|
|
|
1963
1963
|
this.refuseStop(event.taskId, event.message);
|
|
1964
1964
|
return;
|
|
1965
1965
|
}
|
|
1966
|
+
/*
|
|
1967
|
+
* A CLI that counts its tokens but names no window, in a chat that has none yet: the catalog may have learned the
|
|
1968
|
+
* model's window since the chat began, as an ACP agent's catalog does from the first session that reports its models.
|
|
1969
|
+
*/
|
|
1970
|
+
if (event.type === 'usage' && event.contextWindow === undefined && event.contextTokens !== undefined && this.thread.info.usage.contextWindow === null) {
|
|
1971
|
+
const contextWindow = this.options.provider.catalog.contextWindowFor(this.launchedSelection ?? this.thread.info.selection);
|
|
1972
|
+
if (contextWindow !== null) {
|
|
1973
|
+
this.emit(this.projector.project(generation, { ...event, contextWindow }));
|
|
1974
|
+
return;
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1966
1977
|
/*
|
|
1967
1978
|
* A CLI reports the window it was started on. A pick made since waits for the restart the next
|
|
1968
1979
|
* send does, so its report may not put the old size back under a meter that already reads the new one.
|