@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.
- 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 +258 -21
- 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 +303 -24
- 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
|
@@ -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;
|