@adecore/agents 0.31.6 → 0.31.7

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.
@@ -277,7 +277,9 @@ export class AcpBackend {
277
277
  const named = input.attachments.filter((attachment) => !images.includes(attachment));
278
278
  const parts = [this.contextPending, input.preamble, input.text, attachmentNote(named)].filter((part) => part !== null && part !== '');
279
279
  this.contextPending = null;
280
- return [{ type: 'text', text: parts.join('\n\n') }, ...blocks, ...this.mentionBlocks(input.mentions)];
280
+ const text = parts.join('\n\n');
281
+ this.protocol.prompted(text);
282
+ return [{ type: 'text', text }, ...blocks, ...this.mentionBlocks(input.mentions)];
281
283
  }
282
284
  /*
283
285
  * The text of each mentioned file as a `resource` block, for an agent that takes embedded context. Only a text file
@@ -486,13 +488,18 @@ export class AcpBackend {
486
488
  this.protocol.cancelPermissions();
487
489
  this.emit({ type: 'exit', exitCode, ...(stderr === null ? {} : { stderr }) });
488
490
  }
489
- /* A cancelled turn still spent what it reports. */
491
+ /* A cancelled turn still spent what it reports. A host that fails to keep it costs the record, never the end of the turn. */
490
492
  report(result) {
491
493
  const usage = this.spec.onUsage === undefined ? null : promptUsageOf(result);
492
494
  if (usage === null) {
493
495
  return;
494
496
  }
495
- this.spec.onUsage?.({ ...usage, sessionId: this.sessionId, cwd: this.launch.cwd, model: this.model ?? this.launch.selection.model });
497
+ try {
498
+ this.spec.onUsage?.({ ...usage, sessionId: this.sessionId, cwd: this.launch.cwd, model: this.model ?? this.launch.selection.model });
499
+ }
500
+ catch (error) {
501
+ console.warn(`Could not keep the usage of a ${this.spec.name} turn:`, errorText(error));
502
+ }
496
503
  }
497
504
  emitAll(events) {
498
505
  for (const event of events) {
@@ -133,8 +133,11 @@ export declare class AcpProtocol {
133
133
  private thought;
134
134
  private counter;
135
135
  private costUsd;
136
+ private readonly prompts;
136
137
  constructor(generation: number, modes: AcpModeMap, words: AcpStopWords);
137
138
  update(sessionId: string, params: unknown): BackendEvent[];
139
+ prompted(text: string): void;
140
+ private restatesPrompt;
138
141
  modeEvent(modeId: string): BackendEvent;
139
142
  permission(rpcId: number | string, sessionId: string, params: unknown): AcpPermission | null;
140
143
  answer(requestId: string, decision: ApprovalDecision): {
@@ -1,6 +1,7 @@
1
1
  import { isRecord } from '@adecore/agent-contracts';
2
2
  import { z } from 'zod';
3
3
  import { MODE_ORDER } from '../modes.js';
4
+ import { cleanTitle } from '../title-file.js';
4
5
  /*
5
6
  * The Agent Client Protocol (https://agentclientprotocol.com), version 1, as far as a chat needs it.
6
7
  * Every schema is loose: an agent may add fields, and a field or an update this version does not
@@ -309,6 +310,11 @@ const KIND_NAMES = {
309
310
  switch_mode: 'Switch mode',
310
311
  other: 'Tool'
311
312
  };
313
+ // A session's title comes from its opening prompts; later ones are not kept.
314
+ const KEPT_PROMPTS = 3;
315
+ function foldSpace(text) {
316
+ return text.replace(/\s+/g, ' ').trim();
317
+ }
312
318
  function str(value) {
313
319
  return typeof value === 'string' && value !== '' ? value : null;
314
320
  }
@@ -409,6 +415,8 @@ export class AcpProtocol {
409
415
  thought = null;
410
416
  counter = 0;
411
417
  costUsd = 0;
418
+ // The opening prompts of the process, whitespace folded, which an agent may hand back as the session's title.
419
+ prompts = [];
412
420
  constructor(generation, modes, words) {
413
421
  this.generation = generation;
414
422
  this.modes = modes;
@@ -456,12 +464,28 @@ export class AcpProtocol {
456
464
  }
457
465
  return [{ type: 'usage', contextTokens: usage.data.used, contextWindow: usage.data.size, ...(cost === undefined ? {} : { costUsd: cost }) }];
458
466
  }
459
- case 'session_info_update':
460
- return str(update.title) === null ? [] : [{ type: 'title', title: update.title }];
467
+ case 'session_info_update': {
468
+ const title = cleanTitle(update.title);
469
+ return title === null || this.restatesPrompt(title) ? [] : [{ type: 'title', title }];
470
+ }
461
471
  default:
462
472
  return [];
463
473
  }
464
474
  }
475
+ /* Notes a prompt sent to the agent, so a title that only repeats its start is not taken for a name. */
476
+ prompted(text) {
477
+ if (this.prompts.length < KEPT_PROMPTS) {
478
+ this.prompts.push(foldSpace(text));
479
+ }
480
+ }
481
+ /*
482
+ * Whether a title is only the start of a prompt, as Copilot CLI 1.0.101 names a session: with a host's instructions in
483
+ * front of the message that names every chat after them, and it keeps the host from naming the chat itself.
484
+ */
485
+ restatesPrompt(title) {
486
+ const start = foldSpace(title.endsWith('…') ? title.slice(0, -1) : title);
487
+ return start !== '' && this.prompts.some((prompt) => prompt.startsWith(start));
488
+ }
465
489
  modeEvent(modeId) {
466
490
  const runtimeMode = runtimeModeOf(modeId, this.modes);
467
491
  return { type: 'permissions', permissionMode: modeId, ...(runtimeMode === null ? {} : { effectiveRuntimeMode: runtimeMode }) };
@@ -10,6 +10,7 @@ import { ChatThread } from './thread.js';
10
10
  import { UiReplies } from './ui-replies.js';
11
11
  import { errorText } from '../error-text.js';
12
12
  import { systemClock } from '../outbox/outbox-worker.js';
13
+ import { cleanTitle } from '../title-file.js';
13
14
  // Claude Code names a session about six seconds after its first prompt, and a first turn can run for minutes.
14
15
  const TITLE_RECHECK_MS = 10_000;
15
16
  // Well inside the 60 seconds a client gives a request; the steer itself goes on after it.
@@ -1906,8 +1907,10 @@ export class ChatSession {
1906
1907
  })
1907
1908
  .catch(() => undefined);
1908
1909
  }
1909
- applyTitle(title) {
1910
- if (title === this.thread.info.suggestedTitle) {
1910
+ applyTitle(raw) {
1911
+ // Every info a client reads carries the title, so one it would refuse would leave the chat stuck there.
1912
+ const title = cleanTitle(raw);
1913
+ if (title === null || title === this.thread.info.suggestedTitle) {
1911
1914
  return;
1912
1915
  }
1913
1916
  this.emit([this.thread.patchInfo({ suggestedTitle: title })]);