@adecore/agents 0.31.12 → 0.32.0

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.
Files changed (50) hide show
  1. package/dist/chat/acp-backend.d.ts +4 -2
  2. package/dist/chat/acp-backend.js +50 -11
  3. package/dist/chat/acp-protocol.d.ts +29 -4
  4. package/dist/chat/acp-protocol.js +258 -21
  5. package/dist/chat/backend.d.ts +3 -0
  6. package/dist/chat/chat-core.js +6 -3
  7. package/dist/chat/chat-session.js +5 -2
  8. package/dist/chat/claude-backend.js +4 -4
  9. package/dist/chat/claude-protocol.d.ts +4 -1
  10. package/dist/chat/claude-protocol.js +46 -2
  11. package/dist/chat/codex-backend.js +4 -4
  12. package/dist/chat/fixtures/antigravity-1.4.1-question.json +69 -0
  13. package/dist/chat/fixtures/antigravity-1.4.1-quota.json +35 -0
  14. package/dist/chat/fixtures/grok-1.0.62-limits.json +66 -0
  15. package/dist/chat/fixtures/grok-1.0.62-question.json +99 -0
  16. package/dist/chat/json-rpc-transport.js +2 -2
  17. package/dist/chat/projector.js +22 -10
  18. package/dist/providers/accounts/service.js +4 -0
  19. package/dist/providers/claude-provider.js +2 -1
  20. package/dist/providers/detect.d.ts +1 -1
  21. package/dist/providers/detect.js +6 -3
  22. package/dist/providers/registry.d.ts +7 -0
  23. package/dist/providers/registry.js +64 -5
  24. package/dist/skills.d.ts +9 -1
  25. package/dist/skills.js +178 -20
  26. package/dist/testing/fake-acp-capture.d.ts +1 -0
  27. package/dist/testing/fake-acp-capture.js +4 -3
  28. package/dist/usage/accounts.js +3 -2
  29. package/package.json +3 -3
  30. package/src/chat/acp-backend.ts +56 -11
  31. package/src/chat/acp-protocol.ts +303 -24
  32. package/src/chat/backend.ts +5 -1
  33. package/src/chat/chat-core.ts +6 -3
  34. package/src/chat/chat-session.ts +5 -2
  35. package/src/chat/claude-backend.ts +4 -4
  36. package/src/chat/claude-protocol.ts +48 -2
  37. package/src/chat/codex-backend.ts +4 -4
  38. package/src/chat/fixtures/antigravity-1.4.1-question.json +69 -0
  39. package/src/chat/fixtures/antigravity-1.4.1-quota.json +35 -0
  40. package/src/chat/fixtures/grok-1.0.62-limits.json +66 -0
  41. package/src/chat/fixtures/grok-1.0.62-question.json +99 -0
  42. package/src/chat/json-rpc-transport.ts +2 -2
  43. package/src/chat/projector.ts +24 -12
  44. package/src/providers/accounts/service.ts +4 -0
  45. package/src/providers/claude-provider.ts +2 -1
  46. package/src/providers/detect.ts +7 -3
  47. package/src/providers/registry.ts +71 -5
  48. package/src/skills.ts +198 -19
  49. package/src/testing/fake-acp-capture.ts +8 -4
  50. package/src/usage/accounts.ts +3 -2
@@ -557,7 +557,7 @@ export class ChatSession {
557
557
  compact() {
558
558
  const compaction = this.options.provider.capabilities.compaction;
559
559
  if (compaction === 'none') {
560
- throw new ChatError('chat-unsupported', `${this.options.provider.name} cannot fold its context`);
560
+ throw new ChatError('chat-unsupported', `${this.options.provider.name} cannot compact its context`);
561
561
  }
562
562
  if (compaction === 'prompt') {
563
563
  this.send('/compact');
@@ -711,7 +711,9 @@ export class ChatSession {
711
711
  ...usage,
712
712
  contextTokens: 0,
713
713
  contextWindow: this.options.provider.catalog.contextWindowFor(this.thread.info.selection),
714
- breakdown: undefined
714
+ breakdown: undefined,
715
+ cacheTtlMs: undefined,
716
+ cacheExpiresAt: undefined
715
717
  }
716
718
  })
717
719
  ]);
@@ -1639,6 +1641,7 @@ export class ChatSession {
1639
1641
  resumeNote: this.options.resumeNote?.() ?? null,
1640
1642
  resumeInstructions: this.options.resumeInstructions?.() ?? null,
1641
1643
  ...(info.goal === undefined ? {} : { goal: info.goal }),
1644
+ ...(info.usage.cacheTtlMs === undefined ? {} : { cacheTtlMs: info.usage.cacheTtlMs }),
1642
1645
  ...(folders.length > 0 ? { folders } : {}),
1643
1646
  ...(projectConfig ? {} : { projectConfig }),
1644
1647
  ...(this.options.spawn ? { spawn: this.options.spawn } : {})
@@ -32,7 +32,7 @@ export class ClaudeBackend {
32
32
  this.launch = launch;
33
33
  this.host = host;
34
34
  this.allowedTools = options.allowedTools ?? [];
35
- this.protocol = new ClaudeProtocol(launch.goal ?? null, launch.now);
35
+ this.protocol = new ClaudeProtocol(launch.goal ?? null, launch.now, launch.cacheTtlMs ?? null);
36
36
  }
37
37
  get running() {
38
38
  return this.child !== null;
@@ -260,10 +260,10 @@ export class ClaudeBackend {
260
260
  this.failed(error);
261
261
  }
262
262
  failed(error) {
263
- this.rejectSteers(`Claude transport failed: ${errorText(error)}`);
263
+ this.rejectSteers(`Lost the connection to Claude: ${errorText(error)}`);
264
264
  this.protocol.forgetPending();
265
265
  try {
266
- this.host.onEvent({ type: 'failed', message: `Claude transport failed: ${errorText(error)}`, processAlive: this.running });
266
+ this.host.onEvent({ type: 'failed', message: `Lost the connection to Claude: ${errorText(error)}`, processAlive: this.running });
267
267
  }
268
268
  catch (reportError) {
269
269
  console.error('Reporting a Claude transport failure failed:', errorText(reportError));
@@ -311,7 +311,7 @@ export class ClaudeBackend {
311
311
  return;
312
312
  }
313
313
  this.child = null;
314
- this.rejectSteers('Claude exited before acknowledging your message');
314
+ this.rejectSteers('Claude stopped before it got your message');
315
315
  this.protocol.forgetPending();
316
316
  this.host.onEvent({ type: 'exit', exitCode, ...(stderr === null ? {} : { stderr }) });
317
317
  }
@@ -15,6 +15,8 @@ export declare class ClaudeProtocol {
15
15
  private readonly foregroundShells;
16
16
  private readonly backgroundTasks;
17
17
  private readonly workflows;
18
+ private cacheMessageId;
19
+ private cacheTtlMs;
18
20
  private lastUuid;
19
21
  private readonly subagentModels;
20
22
  private apiError;
@@ -26,7 +28,7 @@ export declare class ClaudeProtocol {
26
28
  private goalAnswered;
27
29
  private interrupting;
28
30
  private readonly backgroundAgents;
29
- constructor(goal?: ChatGoal | null, now?: () => number);
31
+ constructor(goal?: ChatGoal | null, now?: () => number, cacheTtlMs?: number | null);
30
32
  noteInterrupt(): void;
31
33
  private setGoal;
32
34
  beginSteer(promptId: string): void;
@@ -45,6 +47,7 @@ export declare class ClaudeProtocol {
45
47
  stopTaskRequest(taskId: string): unknown;
46
48
  private handleControlResponse;
47
49
  private handleStreamEvent;
50
+ private noteCache;
48
51
  private handleAssistant;
49
52
  private trackGoal;
50
53
  private settleGoal;
@@ -27,6 +27,20 @@ function taskUsage(usage) {
27
27
  function contextTokens(usage) {
28
28
  return isRecord(usage) ? num(usage.input_tokens) + num(usage.cache_creation_input_tokens) + num(usage.cache_read_input_tokens) : 0;
29
29
  }
30
+ // How long a prompt cache entry lives, by the field of `usage.cache_creation` that counts what a request wrote with it.
31
+ const CACHE_TTL_MS = { ephemeral_5m_input_tokens: 5 * 60_000, ephemeral_1h_input_tokens: 60 * 60_000 };
32
+ /*
33
+ * The lifetime of the cache a response wrote, or null when it wrote none or names no lifetime. A response that wrote
34
+ * with both answers the shorter, which is when the part of its context that goes cold first does.
35
+ */
36
+ function cacheTtlWritten(usage) {
37
+ const creation = isRecord(usage) && isRecord(usage.cache_creation) ? usage.cache_creation : null;
38
+ if (creation === null) {
39
+ return null;
40
+ }
41
+ const written = Object.entries(CACHE_TTL_MS).filter(([field]) => num(creation[field]) > 0);
42
+ return written.length === 0 ? null : Math.min(...written.map(([, ttl]) => ttl));
43
+ }
30
44
  // The kinds of task Claude Code keeps beside its turns that are a command or a monitor; a subagent has a row of its own.
31
45
  const BACKGROUND_TASK_TYPES = new Set(['local_bash', 'monitor_mcp', 'monitor_ws']);
32
46
  const ENDED_TASK_STATUSES = new Set(['completed', 'failed', 'killed', 'stopped']);
@@ -168,6 +182,9 @@ export class ClaudeProtocol {
168
182
  backgroundTasks = new Set();
169
183
  // Workflows still running, by task id, to the Workflow call that launched each.
170
184
  workflows = new Map();
185
+ // The main-chain response whose cache lifetime the thread has, and the lifetime of the cache last written.
186
+ cacheMessageId = null;
187
+ cacheTtlMs;
171
188
  // The uuid of the last main-chain assistant frame of the turn, which is the line of the transcript the turn ends on.
172
189
  lastUuid = null;
173
190
  // The model each subagent last answered with, so a frame only reports one it had not named yet.
@@ -187,9 +204,11 @@ export class ClaudeProtocol {
187
204
  interrupting = false;
188
205
  // Background subagents still at work; Claude Code holds a goal's check until they report.
189
206
  backgroundAgents = new Set();
190
- constructor(goal = null, now = () => Date.now()) {
207
+ // `cacheTtlMs` is the lifetime the chat knew before this process, so a resumed CLI that only reads the cache still moves its expiry.
208
+ constructor(goal = null, now = () => Date.now(), cacheTtlMs = null) {
191
209
  this.goal = goal;
192
210
  this.now = now;
211
+ this.cacheTtlMs = cacheTtlMs;
193
212
  }
194
213
  /* A stop is on its way; the turn it ends leaves the goal set but nobody working on it. */
195
214
  noteInterrupt() {
@@ -524,6 +543,7 @@ export class ClaudeProtocol {
524
543
  }
525
544
  if (event.type === 'message_start') {
526
545
  const message = isRecord(event.message) ? event.message : {};
546
+ this.noteCache(str(message.id), message.usage, events);
527
547
  this.streamMessageId = str(message.id);
528
548
  this.streamTextCount = 0;
529
549
  this.streamThinkingCount = 0;
@@ -559,6 +579,27 @@ export class ClaudeProtocol {
559
579
  }
560
580
  }
561
581
  }
582
+ /*
583
+ * When the prompt cache goes cold, from the first frame of each main-chain response: its `message_start`, or its
584
+ * first `assistant` frame when the CLI streams no partial messages. The lifetime runs from the start of the request,
585
+ * and a response that only read the cache starts the lifetime it was written with again, as a cache hit does.
586
+ */
587
+ noteCache(messageId, usage, events) {
588
+ if (messageId === null || messageId === this.cacheMessageId || !isRecord(usage)) {
589
+ return;
590
+ }
591
+ this.cacheMessageId = messageId;
592
+ const written = cacheTtlWritten(usage);
593
+ if (written !== null) {
594
+ this.cacheTtlMs = written;
595
+ }
596
+ else if (num(usage.cache_read_input_tokens) === 0) {
597
+ return;
598
+ }
599
+ if (this.cacheTtlMs !== null) {
600
+ events.push({ type: 'usage', cacheTtlMs: this.cacheTtlMs, cacheExpiresAt: this.now() + this.cacheTtlMs });
601
+ }
602
+ }
562
603
  handleAssistant(frame, events) {
563
604
  const message = isRecord(frame.message) ? frame.message : {};
564
605
  const messageId = str(message.id) ?? 'message';
@@ -607,6 +648,9 @@ export class ClaudeProtocol {
607
648
  if (tokens > 0) {
608
649
  events.push({ type: 'usage', contextTokens: tokens });
609
650
  }
651
+ if (!parentRef) {
652
+ this.noteCache(str(message.id), message.usage, events);
653
+ }
610
654
  const error = str(frame.error);
611
655
  if (error) {
612
656
  events.push({ type: 'note', level: 'error', text: `The request failed: ${error.replaceAll('_', ' ')}` });
@@ -704,7 +748,7 @@ export class ClaudeProtocol {
704
748
  events.push({ type: 'usage', costUsd: num(frame.total_cost_usd) });
705
749
  }
706
750
  if (frame.is_error === true) {
707
- events.push({ type: 'note', level: 'warning', text: 'A background Claude turn failed; the submitted prompt is still pending.' });
751
+ events.push({ type: 'note', level: 'warning', text: 'Claude could not finish a turn in the background. Your message is still waiting.' });
708
752
  }
709
753
  return;
710
754
  }
@@ -243,7 +243,7 @@ export class CodexBackend {
243
243
  async runGoalCommand(command, input) {
244
244
  const transport = this.transport;
245
245
  if (!transport) {
246
- this.emit({ type: 'failed', message: 'thread/goal failed: Codex is not running' });
246
+ this.emit({ type: 'failed', message: 'Codex is not running' });
247
247
  return;
248
248
  }
249
249
  const threadId = this.threadId;
@@ -308,7 +308,7 @@ export class CodexBackend {
308
308
  async goal(action) {
309
309
  const transport = this.transport;
310
310
  if (!transport) {
311
- throw new Error('thread/goal failed: Codex is not running');
311
+ throw new Error('Codex is not running');
312
312
  }
313
313
  if (action === 'clear') {
314
314
  await transport.request('thread/goal/clear', { threadId: this.threadId, origin: 'user' });
@@ -417,7 +417,7 @@ export class CodexBackend {
417
417
  listThreadItems(params) {
418
418
  const transport = this.transport;
419
419
  if (!transport) {
420
- return Promise.reject(new Error('thread/items/list failed: Codex is not running'));
420
+ return Promise.reject(new Error('Codex is not running'));
421
421
  }
422
422
  return transport.request('thread/items/list', params);
423
423
  }
@@ -502,7 +502,7 @@ export class CodexBackend {
502
502
  request(method, params, accepted) {
503
503
  const transport = this.transport;
504
504
  if (!transport) {
505
- this.emit({ type: 'failed', message: `${method} failed: Codex is not running` });
505
+ this.emit({ type: 'failed', message: 'Codex is not running' });
506
506
  return;
507
507
  }
508
508
  void transport
@@ -0,0 +1,69 @@
1
+ {
2
+ "version": "Antigravity ACP server 1.4.1, `agy_acp_server.par`",
3
+ "source": "Not recorded: a question needs the model to choose to ask one. Built from `ExternalOnInteractionHook` in the server's acp_server/hooks.py inside agy_acp_server.par 1.4.1: a `tool_call` with an `interaction_` id and an empty rawInput, then one `session/request_permission` per question with an `allow_once` option per choice, answered with the chosen optionId.",
4
+ "sessionId": "agy-session-1",
5
+ "initialize": {
6
+ "protocolVersion": 1,
7
+ "agentCapabilities": {
8
+ "loadSession": true,
9
+ "promptCapabilities": {
10
+ "image": false,
11
+ "embeddedContext": true
12
+ }
13
+ },
14
+ "authMethods": []
15
+ },
16
+ "sessionNew": {
17
+ "sessionId": "agy-session-1"
18
+ },
19
+ "commands": [],
20
+ "setConfigOption": {},
21
+ "promptUpdates": [
22
+ {
23
+ "sessionUpdate": "tool_call",
24
+ "toolCallId": "interaction_3f9a1c2e",
25
+ "title": "Which test runner should I set up?",
26
+ "rawInput": {},
27
+ "status": "pending"
28
+ },
29
+ {
30
+ "permission": {
31
+ "sessionId": "agy-session-1",
32
+ "toolCall": {
33
+ "toolCallId": "interaction_3f9a1c2e",
34
+ "title": "Which test runner should I set up?",
35
+ "rawInput": {},
36
+ "status": "pending"
37
+ },
38
+ "options": [
39
+ {
40
+ "optionId": "opt_builtin",
41
+ "name": "The built-in runner",
42
+ "kind": "allow_once"
43
+ },
44
+ {
45
+ "optionId": "opt_separate",
46
+ "name": "A separate runner",
47
+ "kind": "allow_once"
48
+ }
49
+ ]
50
+ }
51
+ },
52
+ {
53
+ "sessionUpdate": "tool_call_update",
54
+ "toolCallId": "interaction_3f9a1c2e",
55
+ "status": "completed",
56
+ "rawOutput": "Response received"
57
+ },
58
+ {
59
+ "sessionUpdate": "agent_message_chunk",
60
+ "content": {
61
+ "type": "text",
62
+ "text": "Setting it up."
63
+ }
64
+ }
65
+ ],
66
+ "promptResponse": {
67
+ "stopReason": "end_turn"
68
+ }
69
+ }
@@ -0,0 +1,35 @@
1
+ {
2
+ "version": "Antigravity ACP server 1.4.1, `agy_acp_server.par`",
3
+ "source": "Not recorded: no account here runs out of quota on demand. Built from the server's own Python modules inside agy_acp_server.par 1.4.1 (acp_server/server.py and ccpa_connection/quota_errors.py): on QUOTA_EXHAUSTED it writes this message as an agent_message_chunk and answers the prompt with `end_turn`; the reset is the delay in its two largest units, or the time when only that is known.",
4
+ "sessionId": "agy-session-1",
5
+ "initialize": {
6
+ "protocolVersion": 1,
7
+ "agentCapabilities": {
8
+ "loadSession": true,
9
+ "promptCapabilities": {
10
+ "image": false,
11
+ "embeddedContext": true
12
+ }
13
+ },
14
+ "authMethods": []
15
+ },
16
+ "sessionNew": {
17
+ "sessionId": "agy-session-1"
18
+ },
19
+ "commands": [],
20
+ "setConfigOption": {},
21
+ "promptUpdates": [
22
+ {
23
+ "sessionUpdate": "agent_message_chunk",
24
+ "content": {
25
+ "type": "text",
26
+ "text": "Usage Limit Reached\n\nYou have reached your current quota for this period. Your limit will reset in 4 days, 23 hours."
27
+ }
28
+ }
29
+ ],
30
+ "promptResponse": {
31
+ "stopReason": "end_turn"
32
+ },
33
+ "resetOn": "Usage Limit Reached\n\nYou have reached your current quota for this period. Your limit will reset on Oct 23, 2026 14:05 UTC.",
34
+ "noReset": "Usage Limit Reached\n\nYou have reached your current quota for this period."
35
+ }
@@ -0,0 +1,66 @@
1
+ {
2
+ "version": "Grok 1.0.62, `grok --no-auto-update agent stdio`",
3
+ "source": "Not recorded: no account here runs into these limits on demand. Built from the source of xai-org/grok-build at 2bdd1d6a, the closest to 1.0.62 it publishes: `handle_sampling_failure` in acp_session_impl/sampler_turn.rs fails `session/prompt` with -32003 after a 429 and with -32603 for an overloaded model once its retries ran out, and sampling/error.rs builds the `data` (a string, or `message` with `http_status`). The bodies after the status line are examples.",
4
+ "sessionId": "grok-session-1",
5
+ "initialize": {
6
+ "protocolVersion": 1,
7
+ "agentCapabilities": {
8
+ "loadSession": true,
9
+ "promptCapabilities": {
10
+ "image": false,
11
+ "embeddedContext": true
12
+ }
13
+ },
14
+ "authMethods": []
15
+ },
16
+ "sessionNew": {
17
+ "sessionId": "grok-session-1"
18
+ },
19
+ "commands": [],
20
+ "setConfigOption": {},
21
+ "promptUpdates": [
22
+ {
23
+ "sessionUpdate": "agent_thought_chunk",
24
+ "content": {
25
+ "type": "text",
26
+ "text": "Reading the request."
27
+ }
28
+ }
29
+ ],
30
+ "promptResponse": {
31
+ "stopReason": "end_turn"
32
+ },
33
+ "errors": {
34
+ "freeUsage": {
35
+ "code": -32003,
36
+ "message": "Rate limited",
37
+ "data": "API error (status 429 Too Many Requests): {\"code\":\"subscription:free-usage-exhausted\",\"error\":\"You have used the free usage of this period.\"}"
38
+ },
39
+ "rateLimited": {
40
+ "code": -32003,
41
+ "message": "Rate limited",
42
+ "data": "API error (status 429 Too Many Requests): {\"code\":\"rate_limit_exceeded\",\"error\":\"Too many requests.\"}"
43
+ },
44
+ "overloaded": {
45
+ "code": -32603,
46
+ "message": "Internal error",
47
+ "data": {
48
+ "message": "API error (status 529): Overloaded",
49
+ "http_status": 529
50
+ }
51
+ },
52
+ "overloadedStream": {
53
+ "code": -32603,
54
+ "message": "Internal error",
55
+ "data": "stream error (overloaded_error): Overloaded"
56
+ },
57
+ "badRequest": {
58
+ "code": -32603,
59
+ "message": "Internal error",
60
+ "data": {
61
+ "message": "API error (status 400 Bad Request): The prompt is too long.",
62
+ "http_status": 400
63
+ }
64
+ }
65
+ }
66
+ }
@@ -0,0 +1,99 @@
1
+ {
2
+ "version": "Grok 1.0.62, `grok --no-auto-update agent stdio`",
3
+ "source": "Not recorded: a question needs the model to choose to ask one. Built from the source of xai-org/grok-build at 2bdd1d6a, the closest to 1.0.62 it publishes: the ext request `_x.ai/ask_user_question` (acp_session_impl/spawn.rs) with the params of ask_user_question/types.rs; its answer is `{ outcome: \"accepted\", answers, annotations? }`, keyed by the question's text.",
4
+ "sessionId": "grok-session-1",
5
+ "initialize": {
6
+ "protocolVersion": 1,
7
+ "agentCapabilities": {
8
+ "loadSession": true,
9
+ "promptCapabilities": {
10
+ "image": false,
11
+ "embeddedContext": true
12
+ }
13
+ },
14
+ "authMethods": []
15
+ },
16
+ "sessionNew": {
17
+ "sessionId": "grok-session-1"
18
+ },
19
+ "commands": [],
20
+ "setConfigOption": {},
21
+ "promptUpdates": [
22
+ {
23
+ "sessionUpdate": "tool_call",
24
+ "toolCallId": "call-ask-1",
25
+ "title": "Ask the user",
26
+ "kind": "other",
27
+ "status": "pending"
28
+ },
29
+ {
30
+ "request": {
31
+ "method": "_x.ai/ask_user_question",
32
+ "params": {
33
+ "sessionId": "grok-session-1",
34
+ "toolCallId": "call-ask-1",
35
+ "mode": "default",
36
+ "questions": [
37
+ {
38
+ "question": "Which database should the service use?",
39
+ "options": [
40
+ {
41
+ "label": "Postgres",
42
+ "description": "The one the other services run on"
43
+ },
44
+ {
45
+ "label": "SQLite",
46
+ "description": "A file beside the service"
47
+ }
48
+ ],
49
+ "multiSelect": false
50
+ },
51
+ {
52
+ "question": "Which checks should run before a release?",
53
+ "options": [
54
+ {
55
+ "label": "Tests",
56
+ "description": "The unit tests"
57
+ },
58
+ {
59
+ "label": "Lint, strict",
60
+ "description": "Every warning fails"
61
+ },
62
+ {
63
+ "label": "Types",
64
+ "description": "The type check"
65
+ }
66
+ ],
67
+ "multiSelect": true
68
+ },
69
+ {
70
+ "question": "Anything else to keep in mind?",
71
+ "options": [
72
+ {
73
+ "label": "No",
74
+ "description": "Nothing"
75
+ }
76
+ ],
77
+ "multiSelect": null
78
+ }
79
+ ]
80
+ }
81
+ }
82
+ },
83
+ {
84
+ "sessionUpdate": "tool_call_update",
85
+ "toolCallId": "call-ask-1",
86
+ "status": "completed"
87
+ },
88
+ {
89
+ "sessionUpdate": "agent_message_chunk",
90
+ "content": {
91
+ "type": "text",
92
+ "text": "Noted."
93
+ }
94
+ }
95
+ ],
96
+ "promptResponse": {
97
+ "stopReason": "end_turn"
98
+ }
99
+ }
@@ -48,7 +48,7 @@ export class JsonRpcTransport {
48
48
  const timeoutMs = options.timeoutMs === undefined ? this.options.requestTimeoutMs : options.timeoutMs;
49
49
  return new Promise((resolve, reject) => {
50
50
  if (this.closed) {
51
- reject(new Error(`${method} failed: ${label} is not running`));
51
+ reject(new Error(`${label} is not running`));
52
52
  return;
53
53
  }
54
54
  const id = this.nextId++;
@@ -122,7 +122,7 @@ export class JsonRpcTransport {
122
122
  this.failed(error);
123
123
  }
124
124
  failed(error) {
125
- const failure = new Error(`${this.options.label} transport failed: ${errorText(error)}`);
125
+ const failure = new Error(`Lost the connection to ${this.options.label}: ${errorText(error)}`);
126
126
  this.rejectPending(() => failure);
127
127
  try {
128
128
  this.options.onError?.(failure);
@@ -308,16 +308,28 @@ export class ThreadProjector {
308
308
  break;
309
309
  }
310
310
  case 'usage': {
311
- const contextTokens = event.contextTokens ?? info.usage.contextTokens;
312
- events.push(this.thread.patchInfo({
313
- usage: {
314
- ...info.usage,
315
- contextTokens,
316
- contextWindow: event.contextWindow ?? info.usage.contextWindow,
317
- costUsd: Math.max(info.usage.costUsd, event.costUsd ?? 0),
318
- breakdown: contextTokens > 0 ? estimateContextBreakdown(this.thread.list(), contextTokens) : undefined
319
- }
320
- }));
311
+ const usage = info.usage;
312
+ const contextTokens = event.contextTokens ?? usage.contextTokens;
313
+ const next = {
314
+ ...usage,
315
+ contextTokens,
316
+ contextWindow: event.contextWindow ?? usage.contextWindow,
317
+ costUsd: Math.max(usage.costUsd, event.costUsd ?? 0),
318
+ // The estimate reads the whole thread, so only a new count of tokens is worth it.
319
+ ...(contextTokens === usage.contextTokens
320
+ ? {}
321
+ : { breakdown: contextTokens > 0 ? estimateContextBreakdown(this.thread.list(), contextTokens) : undefined }),
322
+ ...(event.cacheTtlMs === undefined || event.cacheExpiresAt === undefined
323
+ ? {}
324
+ : { cacheTtlMs: event.cacheTtlMs, cacheExpiresAt: event.cacheExpiresAt })
325
+ };
326
+ if (next.contextTokens !== usage.contextTokens ||
327
+ next.contextWindow !== usage.contextWindow ||
328
+ next.costUsd !== usage.costUsd ||
329
+ next.cacheTtlMs !== usage.cacheTtlMs ||
330
+ next.cacheExpiresAt !== usage.cacheExpiresAt) {
331
+ events.push(this.thread.patchInfo({ usage: next }));
332
+ }
321
333
  break;
322
334
  }
323
335
  case 'compaction':
@@ -153,6 +153,10 @@ export class ProviderAccountsService {
153
153
  };
154
154
  if (isDefaultAccountOf(kind, id)) {
155
155
  const account = this.accounts[kind] ?? { kind };
156
+ // Only a start that names it: every turn of a chat on the default account asks with none.
157
+ if (id !== undefined && account.enabled === false) {
158
+ throw refuse('it is turned off');
159
+ }
156
160
  return accountEnv(kind, account, provider, baseEnv, variables(account));
157
161
  }
158
162
  const account = this.accounts[key];
@@ -31,7 +31,8 @@ export function createClaudeProvider(options = {}) {
31
31
  { kind: 'env', variable: 'ANTHROPIC_API_KEY' }
32
32
  ]
33
33
  },
34
- detect: detectCli,
34
+ // The version probe must never be the start that updates the CLI.
35
+ detect: (command, env) => detectCli(command, { ...env, DISABLE_AUTOUPDATER: '1' }),
35
36
  oneShotArgs: (prompt) => ['-p', prompt, '--output-format', 'text'],
36
37
  // `-p` without a prompt argument reads the prompt from stdin.
37
38
  oneShotStdinArgs: ['-p', '--output-format', 'text'],
@@ -3,4 +3,4 @@ export interface CliDetection {
3
3
  installed: boolean;
4
4
  version: string | null;
5
5
  }
6
- export declare function detectCli(command: string, env?: Record<string, string | undefined>, spawn?: SpawnChatProcess): Promise<CliDetection>;
6
+ export declare function detectCli(command: string, env?: Record<string, string | undefined>, spawn?: SpawnChatProcess, flags?: readonly string[]): Promise<CliDetection>;
@@ -1,13 +1,16 @@
1
1
  import { spawnChatProcess } from '../chat/chat-process.js';
2
2
  import { definedEnv } from './accounts/launch.js';
3
- /* Asks a CLI for its version; a missing binary is a plain "not installed", never an error. */
4
- export async function detectCli(command, env = process.env, spawn = spawnChatProcess) {
3
+ /*
4
+ * Asks a CLI for its version; a missing binary is a plain "not installed", never an error. `flags` go before
5
+ * `--version`, for a CLI that would otherwise look for an update of itself on any start.
6
+ */
7
+ export async function detectCli(command, env = process.env, spawn = spawnChatProcess, flags = []) {
5
8
  try {
6
9
  let exited = () => undefined;
7
10
  const code = new Promise((resolve) => {
8
11
  exited = resolve;
9
12
  });
10
- const proc = spawn({ command: [command, '--version'], cwd: process.cwd(), env: definedEnv(env), onExit: (exitCode) => exited(exitCode) });
13
+ const proc = spawn({ command: [command, ...flags, '--version'], cwd: process.cwd(), env: definedEnv(env), onExit: (exitCode) => exited(exitCode) });
11
14
  proc.stdin.end();
12
15
  const output = await new Response(proc.stdout).text();
13
16
  if ((await code) !== 0) {
@@ -16,7 +16,11 @@ export declare class ProviderRegistry {
16
16
  private readonly detect;
17
17
  private readonly env;
18
18
  private readonly cache;
19
+ private readonly listeners;
20
+ private readonly stops;
19
21
  constructor(options?: ProviderRegistryOptions);
22
+ add(provider: ChatProvider): void;
23
+ remove(kind: AgentKind): void;
20
24
  list(): Promise<ProviderInfo[]>;
21
25
  kinds(): AgentKind[];
22
26
  get(kind: AgentKind): ChatProvider;
@@ -26,5 +30,8 @@ export declare class ProviderRegistry {
26
30
  onChange(listener: (kind: AgentKind) => void): () => void;
27
31
  invalidate(kind: AgentKind): void;
28
32
  protected fallback(kind: AgentKind): ChatProvider;
33
+ private follow;
34
+ private unfollow;
35
+ private changed;
29
36
  private detection;
30
37
  }