@adecore/agents 0.31.9 → 0.31.11

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.
@@ -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: validItems(ModelEntrySchema, modelState.data.availableModels).map((model) => ({
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
- private text: { ref: string; messageId: string | null; text: string } | null = null;
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
- constructor(generation: number, modes: AcpModeMap, words: AcpStopWords) {
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
- const update = notification.data.update;
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
- return { rpcId: pending.rpcId, result: { outcome: optionId === null ? { outcome: 'cancelled' } : { outcome: 'selected', optionId } } };
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
- /* The end of a turn, from the `stopReason` of its `session/prompt` response. */
761
- finish(result: unknown, promptId: string | undefined): BackendEvent[] {
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 reason = parsed.success ? parsed.data.stopReason : 'end_turn';
764
- const events = [...this.closeText(), ...this.closeThought()];
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
- return text === null ? [] : [{ type: 'text.done', ref: text.ref, text: text.text }];
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;
@@ -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.