@adecore/agents 0.31.11 → 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.
- package/dist/chat/acp-backend.d.ts +4 -2
- package/dist/chat/acp-backend.js +50 -11
- package/dist/chat/acp-protocol.d.ts +29 -4
- package/dist/chat/acp-protocol.js +262 -23
- package/dist/chat/backend.d.ts +3 -0
- package/dist/chat/chat-core.js +6 -3
- package/dist/chat/chat-session.js +5 -2
- package/dist/chat/claude-backend.js +4 -4
- package/dist/chat/claude-protocol.d.ts +4 -1
- package/dist/chat/claude-protocol.js +46 -2
- package/dist/chat/codex-backend.js +4 -4
- package/dist/chat/fixtures/antigravity-1.4.1-question.json +69 -0
- package/dist/chat/fixtures/antigravity-1.4.1-quota.json +35 -0
- package/dist/chat/fixtures/grok-1.0.62-limits.json +66 -0
- package/dist/chat/fixtures/grok-1.0.62-question.json +99 -0
- package/dist/chat/json-rpc-transport.js +2 -2
- package/dist/chat/projector.js +22 -10
- package/dist/providers/accounts/service.js +4 -0
- package/dist/providers/claude-provider.js +2 -1
- package/dist/providers/detect.d.ts +1 -1
- package/dist/providers/detect.js +6 -3
- package/dist/providers/registry.d.ts +7 -0
- package/dist/providers/registry.js +64 -5
- package/dist/skills.d.ts +9 -1
- package/dist/skills.js +178 -20
- package/dist/testing/fake-acp-capture.d.ts +1 -0
- package/dist/testing/fake-acp-capture.js +4 -3
- package/dist/usage/accounts.js +3 -2
- package/package.json +3 -3
- package/src/chat/acp-backend.ts +56 -11
- package/src/chat/acp-protocol.ts +308 -26
- package/src/chat/backend.ts +5 -1
- package/src/chat/chat-core.ts +6 -3
- package/src/chat/chat-session.ts +5 -2
- package/src/chat/claude-backend.ts +4 -4
- package/src/chat/claude-protocol.ts +48 -2
- package/src/chat/codex-backend.ts +4 -4
- package/src/chat/fixtures/antigravity-1.4.1-question.json +69 -0
- package/src/chat/fixtures/antigravity-1.4.1-quota.json +35 -0
- package/src/chat/fixtures/grok-1.0.62-limits.json +66 -0
- package/src/chat/fixtures/grok-1.0.62-question.json +99 -0
- package/src/chat/json-rpc-transport.ts +2 -2
- package/src/chat/projector.ts +24 -12
- package/src/providers/accounts/service.ts +4 -0
- package/src/providers/claude-provider.ts +2 -1
- package/src/providers/detect.ts +7 -3
- package/src/providers/registry.ts +71 -5
- package/src/skills.ts +198 -19
- package/src/testing/fake-acp-capture.ts +8 -4
- package/src/usage/accounts.ts +3 -2
|
@@ -46,6 +46,22 @@ function contextTokens(usage: unknown): number {
|
|
|
46
46
|
return isRecord(usage) ? num(usage.input_tokens) + num(usage.cache_creation_input_tokens) + num(usage.cache_read_input_tokens) : 0;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
// How long a prompt cache entry lives, by the field of `usage.cache_creation` that counts what a request wrote with it.
|
|
50
|
+
const CACHE_TTL_MS: Record<string, number> = { ephemeral_5m_input_tokens: 5 * 60_000, ephemeral_1h_input_tokens: 60 * 60_000 };
|
|
51
|
+
|
|
52
|
+
/*
|
|
53
|
+
* The lifetime of the cache a response wrote, or null when it wrote none or names no lifetime. A response that wrote
|
|
54
|
+
* with both answers the shorter, which is when the part of its context that goes cold first does.
|
|
55
|
+
*/
|
|
56
|
+
function cacheTtlWritten(usage: unknown): number | null {
|
|
57
|
+
const creation = isRecord(usage) && isRecord(usage.cache_creation) ? usage.cache_creation : null;
|
|
58
|
+
if (creation === null) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
const written = Object.entries(CACHE_TTL_MS).filter(([field]) => num(creation[field]) > 0);
|
|
62
|
+
return written.length === 0 ? null : Math.min(...written.map(([, ttl]) => ttl));
|
|
63
|
+
}
|
|
64
|
+
|
|
49
65
|
// 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.
|
|
50
66
|
const BACKGROUND_TASK_TYPES = new Set(['local_bash', 'monitor_mcp', 'monitor_ws']);
|
|
51
67
|
const ENDED_TASK_STATUSES = new Set(['completed', 'failed', 'killed', 'stopped']);
|
|
@@ -202,6 +218,9 @@ export class ClaudeProtocol {
|
|
|
202
218
|
private readonly backgroundTasks = new Set<string>();
|
|
203
219
|
// Workflows still running, by task id, to the Workflow call that launched each.
|
|
204
220
|
private readonly workflows = new Map<string, string>();
|
|
221
|
+
// The main-chain response whose cache lifetime the thread has, and the lifetime of the cache last written.
|
|
222
|
+
private cacheMessageId: string | null = null;
|
|
223
|
+
private cacheTtlMs: number | null;
|
|
205
224
|
// The uuid of the last main-chain assistant frame of the turn, which is the line of the transcript the turn ends on.
|
|
206
225
|
private lastUuid: string | null = null;
|
|
207
226
|
// The model each subagent last answered with, so a frame only reports one it had not named yet.
|
|
@@ -224,9 +243,11 @@ export class ClaudeProtocol {
|
|
|
224
243
|
// Background subagents still at work; Claude Code holds a goal's check until they report.
|
|
225
244
|
private readonly backgroundAgents = new Set<string>();
|
|
226
245
|
|
|
227
|
-
|
|
246
|
+
// `cacheTtlMs` is the lifetime the chat knew before this process, so a resumed CLI that only reads the cache still moves its expiry.
|
|
247
|
+
constructor(goal: ChatGoal | null = null, now: () => number = () => Date.now(), cacheTtlMs: number | null = null) {
|
|
228
248
|
this.goal = goal;
|
|
229
249
|
this.now = now;
|
|
250
|
+
this.cacheTtlMs = cacheTtlMs;
|
|
230
251
|
}
|
|
231
252
|
|
|
232
253
|
/* A stop is on its way; the turn it ends leaves the goal set but nobody working on it. */
|
|
@@ -572,6 +593,7 @@ export class ClaudeProtocol {
|
|
|
572
593
|
}
|
|
573
594
|
if (event.type === 'message_start') {
|
|
574
595
|
const message = isRecord(event.message) ? event.message : {};
|
|
596
|
+
this.noteCache(str(message.id), message.usage, events);
|
|
575
597
|
this.streamMessageId = str(message.id);
|
|
576
598
|
this.streamTextCount = 0;
|
|
577
599
|
this.streamThinkingCount = 0;
|
|
@@ -607,6 +629,27 @@ export class ClaudeProtocol {
|
|
|
607
629
|
}
|
|
608
630
|
}
|
|
609
631
|
|
|
632
|
+
/*
|
|
633
|
+
* When the prompt cache goes cold, from the first frame of each main-chain response: its `message_start`, or its
|
|
634
|
+
* first `assistant` frame when the CLI streams no partial messages. The lifetime runs from the start of the request,
|
|
635
|
+
* and a response that only read the cache starts the lifetime it was written with again, as a cache hit does.
|
|
636
|
+
*/
|
|
637
|
+
private noteCache(messageId: string | null, usage: unknown, events: BackendEvent[]): void {
|
|
638
|
+
if (messageId === null || messageId === this.cacheMessageId || !isRecord(usage)) {
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
this.cacheMessageId = messageId;
|
|
642
|
+
const written = cacheTtlWritten(usage);
|
|
643
|
+
if (written !== null) {
|
|
644
|
+
this.cacheTtlMs = written;
|
|
645
|
+
} else if (num(usage.cache_read_input_tokens) === 0) {
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
if (this.cacheTtlMs !== null) {
|
|
649
|
+
events.push({ type: 'usage', cacheTtlMs: this.cacheTtlMs, cacheExpiresAt: this.now() + this.cacheTtlMs });
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
610
653
|
private handleAssistant(frame: Frame, events: BackendEvent[]): void {
|
|
611
654
|
const message = isRecord(frame.message) ? frame.message : {};
|
|
612
655
|
const messageId = str(message.id) ?? 'message';
|
|
@@ -653,6 +696,9 @@ export class ClaudeProtocol {
|
|
|
653
696
|
if (tokens > 0) {
|
|
654
697
|
events.push({ type: 'usage', contextTokens: tokens });
|
|
655
698
|
}
|
|
699
|
+
if (!parentRef) {
|
|
700
|
+
this.noteCache(str(message.id), message.usage, events);
|
|
701
|
+
}
|
|
656
702
|
const error = str(frame.error);
|
|
657
703
|
if (error) {
|
|
658
704
|
events.push({ type: 'note', level: 'error', text: `The request failed: ${error.replaceAll('_', ' ')}` });
|
|
@@ -756,7 +802,7 @@ export class ClaudeProtocol {
|
|
|
756
802
|
events.push({ type: 'usage', costUsd: num(frame.total_cost_usd) });
|
|
757
803
|
}
|
|
758
804
|
if (frame.is_error === true) {
|
|
759
|
-
events.push({ type: 'note', level: 'warning', text: '
|
|
805
|
+
events.push({ type: 'note', level: 'warning', text: 'Claude could not finish a turn in the background. Your message is still waiting.' });
|
|
760
806
|
}
|
|
761
807
|
return;
|
|
762
808
|
}
|
|
@@ -262,7 +262,7 @@ export class CodexBackend implements ChatBackend {
|
|
|
262
262
|
private async runGoalCommand(command: GoalCommand, input: TurnInput): Promise<void> {
|
|
263
263
|
const transport = this.transport;
|
|
264
264
|
if (!transport) {
|
|
265
|
-
this.emit({ type: 'failed', message: '
|
|
265
|
+
this.emit({ type: 'failed', message: 'Codex is not running' });
|
|
266
266
|
return;
|
|
267
267
|
}
|
|
268
268
|
const threadId = this.threadId;
|
|
@@ -328,7 +328,7 @@ export class CodexBackend implements ChatBackend {
|
|
|
328
328
|
async goal(action: ChatGoalAction): Promise<void> {
|
|
329
329
|
const transport = this.transport;
|
|
330
330
|
if (!transport) {
|
|
331
|
-
throw new Error('
|
|
331
|
+
throw new Error('Codex is not running');
|
|
332
332
|
}
|
|
333
333
|
if (action === 'clear') {
|
|
334
334
|
await transport.request('thread/goal/clear', { threadId: this.threadId, origin: 'user' });
|
|
@@ -443,7 +443,7 @@ export class CodexBackend implements ChatBackend {
|
|
|
443
443
|
listThreadItems(params: { threadId: string; cursor?: string; limit: number; sortDirection: 'asc' | 'desc' }): Promise<unknown> {
|
|
444
444
|
const transport = this.transport;
|
|
445
445
|
if (!transport) {
|
|
446
|
-
return Promise.reject(new Error('
|
|
446
|
+
return Promise.reject(new Error('Codex is not running'));
|
|
447
447
|
}
|
|
448
448
|
return transport.request('thread/items/list', params);
|
|
449
449
|
}
|
|
@@ -537,7 +537,7 @@ export class CodexBackend implements ChatBackend {
|
|
|
537
537
|
private request(method: string, params: unknown, accepted?: () => void): void {
|
|
538
538
|
const transport = this.transport;
|
|
539
539
|
if (!transport) {
|
|
540
|
-
this.emit({ type: 'failed', message:
|
|
540
|
+
this.emit({ type: 'failed', message: 'Codex is not running' });
|
|
541
541
|
return;
|
|
542
542
|
}
|
|
543
543
|
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
|
+
}
|
|
@@ -84,7 +84,7 @@ export class JsonRpcTransport {
|
|
|
84
84
|
const timeoutMs = options.timeoutMs === undefined ? this.options.requestTimeoutMs : options.timeoutMs;
|
|
85
85
|
return new Promise((resolve, reject) => {
|
|
86
86
|
if (this.closed) {
|
|
87
|
-
reject(new Error(`${
|
|
87
|
+
reject(new Error(`${label} is not running`));
|
|
88
88
|
return;
|
|
89
89
|
}
|
|
90
90
|
const id = this.nextId++;
|
|
@@ -165,7 +165,7 @@ export class JsonRpcTransport {
|
|
|
165
165
|
}
|
|
166
166
|
|
|
167
167
|
private failed(error: unknown): void {
|
|
168
|
-
const failure = new Error(
|
|
168
|
+
const failure = new Error(`Lost the connection to ${this.options.label}: ${errorText(error)}`);
|
|
169
169
|
this.rejectPending(() => failure);
|
|
170
170
|
try {
|
|
171
171
|
this.options.onError?.(failure);
|
package/src/chat/projector.ts
CHANGED
|
@@ -351,18 +351,30 @@ export class ThreadProjector {
|
|
|
351
351
|
break;
|
|
352
352
|
}
|
|
353
353
|
case 'usage': {
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
|
|
354
|
+
const usage = info.usage;
|
|
355
|
+
const contextTokens = event.contextTokens ?? usage.contextTokens;
|
|
356
|
+
const next = {
|
|
357
|
+
...usage,
|
|
358
|
+
contextTokens,
|
|
359
|
+
contextWindow: event.contextWindow ?? usage.contextWindow,
|
|
360
|
+
costUsd: Math.max(usage.costUsd, event.costUsd ?? 0),
|
|
361
|
+
// The estimate reads the whole thread, so only a new count of tokens is worth it.
|
|
362
|
+
...(contextTokens === usage.contextTokens
|
|
363
|
+
? {}
|
|
364
|
+
: { breakdown: contextTokens > 0 ? estimateContextBreakdown(this.thread.list(), contextTokens) : undefined }),
|
|
365
|
+
...(event.cacheTtlMs === undefined || event.cacheExpiresAt === undefined
|
|
366
|
+
? {}
|
|
367
|
+
: { cacheTtlMs: event.cacheTtlMs, cacheExpiresAt: event.cacheExpiresAt })
|
|
368
|
+
};
|
|
369
|
+
if (
|
|
370
|
+
next.contextTokens !== usage.contextTokens ||
|
|
371
|
+
next.contextWindow !== usage.contextWindow ||
|
|
372
|
+
next.costUsd !== usage.costUsd ||
|
|
373
|
+
next.cacheTtlMs !== usage.cacheTtlMs ||
|
|
374
|
+
next.cacheExpiresAt !== usage.cacheExpiresAt
|
|
375
|
+
) {
|
|
376
|
+
events.push(this.thread.patchInfo({ usage: next }));
|
|
377
|
+
}
|
|
366
378
|
break;
|
|
367
379
|
}
|
|
368
380
|
case 'compaction':
|
|
@@ -225,6 +225,10 @@ export class ProviderAccountsService implements AccountLaunches {
|
|
|
225
225
|
};
|
|
226
226
|
if (isDefaultAccountOf(kind, id)) {
|
|
227
227
|
const account = this.accounts[kind] ?? { kind };
|
|
228
|
+
// Only a start that names it: every turn of a chat on the default account asks with none.
|
|
229
|
+
if (id !== undefined && account.enabled === false) {
|
|
230
|
+
throw refuse('it is turned off');
|
|
231
|
+
}
|
|
228
232
|
return accountEnv(kind, account, provider, baseEnv, variables(account));
|
|
229
233
|
}
|
|
230
234
|
const account = this.accounts[key];
|
|
@@ -33,7 +33,8 @@ export function createClaudeProvider(options: ClaudeBackendOptions = {}): ChatPr
|
|
|
33
33
|
{ kind: 'env', variable: 'ANTHROPIC_API_KEY' }
|
|
34
34
|
]
|
|
35
35
|
},
|
|
36
|
-
|
|
36
|
+
// The version probe must never be the start that updates the CLI.
|
|
37
|
+
detect: (command, env) => detectCli(command, { ...env, DISABLE_AUTOUPDATER: '1' }),
|
|
37
38
|
oneShotArgs: (prompt) => ['-p', prompt, '--output-format', 'text'],
|
|
38
39
|
// `-p` without a prompt argument reads the prompt from stdin.
|
|
39
40
|
oneShotStdinArgs: ['-p', '--output-format', 'text'],
|
package/src/providers/detect.ts
CHANGED
|
@@ -6,18 +6,22 @@ export interface CliDetection {
|
|
|
6
6
|
version: string | null;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
/*
|
|
9
|
+
/*
|
|
10
|
+
* Asks a CLI for its version; a missing binary is a plain "not installed", never an error. `flags` go before
|
|
11
|
+
* `--version`, for a CLI that would otherwise look for an update of itself on any start.
|
|
12
|
+
*/
|
|
10
13
|
export async function detectCli(
|
|
11
14
|
command: string,
|
|
12
15
|
env: Record<string, string | undefined> = process.env,
|
|
13
|
-
spawn: SpawnChatProcess = spawnChatProcess
|
|
16
|
+
spawn: SpawnChatProcess = spawnChatProcess,
|
|
17
|
+
flags: readonly string[] = []
|
|
14
18
|
): Promise<CliDetection> {
|
|
15
19
|
try {
|
|
16
20
|
let exited: (code: number | null) => void = () => undefined;
|
|
17
21
|
const code = new Promise<number | null>((resolve) => {
|
|
18
22
|
exited = resolve;
|
|
19
23
|
});
|
|
20
|
-
const proc = spawn({ command: [command, '--version'], cwd: process.cwd(), env: definedEnv(env), onExit: (exitCode) => exited(exitCode) });
|
|
24
|
+
const proc = spawn({ command: [command, ...flags, '--version'], cwd: process.cwd(), env: definedEnv(env), onExit: (exitCode) => exited(exitCode) });
|
|
21
25
|
proc.stdin.end();
|
|
22
26
|
const output = await new Response(proc.stdout).text();
|
|
23
27
|
if ((await code) !== 0) {
|
|
@@ -72,14 +72,48 @@ export class ProviderRegistry {
|
|
|
72
72
|
private readonly detect: ((command: string) => Promise<CliDetection>) | null;
|
|
73
73
|
private readonly env: Record<string, string | undefined>;
|
|
74
74
|
private readonly cache = new Map<AgentKind, { at: number; detection: CliDetection }>();
|
|
75
|
+
private readonly listeners = new Set<(kind: AgentKind) => void>();
|
|
76
|
+
// What the registry listens to on each provider, so one taken away is let go.
|
|
77
|
+
private readonly stops = new Map<ChatProvider, () => void>();
|
|
75
78
|
|
|
76
79
|
constructor(options: ProviderRegistryOptions = {}) {
|
|
77
|
-
this.providers = options.providers ??
|
|
80
|
+
this.providers = [...(options.providers ?? BUILT_IN_PROVIDERS)];
|
|
78
81
|
this.commands = options.commands ?? {};
|
|
79
82
|
this.detect = options.detect ?? null;
|
|
80
83
|
this.env = options.env ?? process.env;
|
|
81
84
|
}
|
|
82
85
|
|
|
86
|
+
/*
|
|
87
|
+
* Offers one more provider after the host started, such as a CLI a person installed since, or puts it in the
|
|
88
|
+
* place of the one of its kind. The `onChange` listeners hear its kind, so a host hands its clients the new list.
|
|
89
|
+
*/
|
|
90
|
+
add(provider: ChatProvider): void {
|
|
91
|
+
const index = this.providers.findIndex((entry) => entry.kind === provider.kind);
|
|
92
|
+
if (index === -1) {
|
|
93
|
+
this.providers.push(provider);
|
|
94
|
+
} else {
|
|
95
|
+
this.unfollow(this.providers[index]!);
|
|
96
|
+
this.providers[index] = provider;
|
|
97
|
+
}
|
|
98
|
+
this.cache.delete(provider.kind);
|
|
99
|
+
if (this.listeners.size > 0) {
|
|
100
|
+
this.follow(provider);
|
|
101
|
+
}
|
|
102
|
+
this.changed(provider.kind);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/* Stops offering a kind; a record that names it gets the stand-in `get` answers for a kind the host does not offer. */
|
|
106
|
+
remove(kind: AgentKind): void {
|
|
107
|
+
const index = this.providers.findIndex((entry) => entry.kind === kind);
|
|
108
|
+
if (index === -1) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
this.unfollow(this.providers[index]!);
|
|
112
|
+
this.providers.splice(index, 1);
|
|
113
|
+
this.cache.delete(kind);
|
|
114
|
+
this.changed(kind);
|
|
115
|
+
}
|
|
116
|
+
|
|
83
117
|
async list(): Promise<ProviderInfo[]> {
|
|
84
118
|
return await Promise.all(
|
|
85
119
|
this.providers.map(async (provider) => ({
|
|
@@ -128,12 +162,24 @@ export class ProviderRegistry {
|
|
|
128
162
|
return this.get(kind).enabled?.() !== false;
|
|
129
163
|
}
|
|
130
164
|
|
|
131
|
-
/*
|
|
165
|
+
/*
|
|
166
|
+
* Calls the listener with the kind of every provider whose models or capabilities changed, and of every one
|
|
167
|
+
* added or removed, so a host can list them again; answers how to stop.
|
|
168
|
+
*/
|
|
132
169
|
onChange(listener: (kind: AgentKind) => void): () => void {
|
|
133
|
-
|
|
170
|
+
if (this.listeners.size === 0) {
|
|
171
|
+
for (const provider of this.providers) {
|
|
172
|
+
this.follow(provider);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
this.listeners.add(listener);
|
|
134
176
|
return () => {
|
|
135
|
-
|
|
136
|
-
|
|
177
|
+
this.listeners.delete(listener);
|
|
178
|
+
if (this.listeners.size === 0) {
|
|
179
|
+
for (const stop of this.stops.values()) {
|
|
180
|
+
stop();
|
|
181
|
+
}
|
|
182
|
+
this.stops.clear();
|
|
137
183
|
}
|
|
138
184
|
};
|
|
139
185
|
}
|
|
@@ -146,6 +192,26 @@ export class ProviderRegistry {
|
|
|
146
192
|
return unofferedProvider(kind);
|
|
147
193
|
}
|
|
148
194
|
|
|
195
|
+
private follow(provider: ChatProvider): void {
|
|
196
|
+
if (provider.onChange && !this.stops.has(provider)) {
|
|
197
|
+
this.stops.set(
|
|
198
|
+
provider,
|
|
199
|
+
provider.onChange(() => this.changed(provider.kind))
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
private unfollow(provider: ChatProvider): void {
|
|
205
|
+
this.stops.get(provider)?.();
|
|
206
|
+
this.stops.delete(provider);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private changed(kind: AgentKind): void {
|
|
210
|
+
for (const listener of [...this.listeners]) {
|
|
211
|
+
listener(kind);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
149
215
|
private async detection(provider: ChatProvider): Promise<CliDetection> {
|
|
150
216
|
if (provider.enabled?.() === false) {
|
|
151
217
|
return DISABLED;
|