@adecore/agents 0.31.1 → 0.31.3
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 +11 -1
- package/dist/chat/acp-backend.js +24 -2
- package/dist/chat/acp-protocol.d.ts +15 -0
- package/dist/chat/acp-protocol.js +36 -2
- package/dist/chat/index.d.ts +2 -2
- package/dist/providers/accounts/acp-account.d.ts +19 -0
- package/dist/providers/accounts/acp-account.js +101 -0
- package/dist/providers/accounts/service.d.ts +1 -0
- package/dist/providers/accounts/service.js +14 -4
- package/dist/providers/accounts/status.d.ts +2 -0
- package/dist/providers/accounts/status.js +3 -1
- package/dist/providers/acp-provider.d.ts +3 -0
- package/dist/providers/acp-provider.js +33 -1
- package/dist/providers/acp-usage-log.d.ts +2 -0
- package/dist/providers/acp-usage-log.js +21 -0
- package/dist/providers/catalog.d.ts +4 -0
- package/dist/providers/catalog.js +21 -0
- package/dist/providers/index.d.ts +2 -0
- package/dist/providers/index.js +2 -0
- package/dist/providers/provider.d.ts +2 -0
- package/dist/providers/shell-commands.d.ts +1 -0
- package/dist/providers/shell-commands.js +119 -0
- package/dist/testing/fake-acp.d.ts +2 -0
- package/dist/testing/fake-acp.js +10 -2
- package/dist/usage/aggregate.js +6 -5
- package/dist/usage/index-file.js +25 -17
- package/dist/usage/limits/monitor.js +5 -0
- package/dist/usage/readers/grok.d.ts +3 -0
- package/dist/usage/readers/grok.js +85 -0
- package/dist/usage/record.d.ts +1 -0
- package/dist/usage/record.js +5 -1
- package/dist/usage/roots.js +7 -2
- package/dist/usage/scanner.d.ts +1 -0
- package/dist/usage/scanner.js +39 -5
- package/package.json +3 -3
- package/src/chat/acp-backend.ts +40 -2
- package/src/chat/acp-protocol.ts +61 -2
- package/src/chat/index.ts +12 -2
- package/src/providers/accounts/acp-account.ts +129 -0
- package/src/providers/accounts/service.ts +15 -5
- package/src/providers/accounts/status.ts +4 -1
- package/src/providers/acp-provider.ts +45 -2
- package/src/providers/acp-usage-log.ts +26 -0
- package/src/providers/catalog.ts +23 -0
- package/src/providers/index.ts +2 -0
- package/src/providers/provider.ts +3 -0
- package/src/providers/shell-commands.ts +122 -0
- package/src/testing/fake-acp.ts +14 -2
- package/src/usage/aggregate.ts +6 -5
- package/src/usage/index-file.ts +27 -18
- package/src/usage/limits/monitor.ts +5 -0
- package/src/usage/readers/grok.ts +89 -0
- package/src/usage/record.ts +7 -1
- package/src/usage/roots.ts +7 -2
- package/src/usage/scanner.ts +42 -5
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ModelSelection } from '@adecore/agent-contracts';
|
|
2
|
-
import { type AcpModeMap, type AcpSessionInfo } from './acp-protocol.ts';
|
|
2
|
+
import { type AcpModeMap, type AcpPermissionRequest, type AcpSessionInfo, type AcpTurnUsage } from './acp-protocol.ts';
|
|
3
3
|
import type { ApprovalDecision, BackendHost, BackendLaunch, ChatBackend, TurnInput } from './backend.ts';
|
|
4
4
|
export interface AcpClientInfo {
|
|
5
5
|
name: string;
|
|
@@ -16,6 +16,13 @@ export interface AcpAgentSpec {
|
|
|
16
16
|
client?: AcpClientInfo;
|
|
17
17
|
model?(selection: ModelSelection): string | null;
|
|
18
18
|
onSession?(info: AcpSessionInfo): void;
|
|
19
|
+
onUsage?(usage: AcpUsageReport): void;
|
|
20
|
+
autoAllow?(request: AcpPermissionRequest): boolean;
|
|
21
|
+
}
|
|
22
|
+
export interface AcpUsageReport extends AcpTurnUsage {
|
|
23
|
+
sessionId: string;
|
|
24
|
+
cwd: string;
|
|
25
|
+
model: string;
|
|
19
26
|
}
|
|
20
27
|
export declare class AcpBackend implements ChatBackend {
|
|
21
28
|
private readonly launch;
|
|
@@ -29,6 +36,7 @@ export declare class AcpBackend implements ChatBackend {
|
|
|
29
36
|
private replaying;
|
|
30
37
|
private contextPending;
|
|
31
38
|
private prompt;
|
|
39
|
+
private model;
|
|
32
40
|
constructor(launch: BackendLaunch, host: BackendHost, spec: AcpAgentSpec);
|
|
33
41
|
get running(): boolean;
|
|
34
42
|
get pid(): number | null;
|
|
@@ -52,7 +60,9 @@ export declare class AcpBackend implements ChatBackend {
|
|
|
52
60
|
private respond;
|
|
53
61
|
private fail;
|
|
54
62
|
private handleFrame;
|
|
63
|
+
private hostAllows;
|
|
55
64
|
private handleExit;
|
|
65
|
+
private report;
|
|
56
66
|
private emitAll;
|
|
57
67
|
private emit;
|
|
58
68
|
}
|
package/dist/chat/acp-backend.js
CHANGED
|
@@ -2,7 +2,7 @@ import { readFileSync } from 'node:fs';
|
|
|
2
2
|
import { attachmentImageMime } from '@adecore/agent-contracts';
|
|
3
3
|
import { errorText } from '../error-text.js';
|
|
4
4
|
import { MODE_ORDER } from '../modes.js';
|
|
5
|
-
import { ACP_AUTH_REQUIRED, ACP_PROTOCOL_VERSION, AcpProtocol, acpStopWords, autoAllows, parseInitialize, parseSessionInfo, resolveAcpMode, runtimeModeOf, sessionIdOf } from './acp-protocol.js';
|
|
5
|
+
import { ACP_AUTH_REQUIRED, ACP_PROTOCOL_VERSION, AcpProtocol, acpStopWords, autoAllows, parseInitialize, parseSessionInfo, promptUsageOf, resolveAcpMode, runtimeModeOf, sessionIdOf } from './acp-protocol.js';
|
|
6
6
|
import { attachmentNote } from './input.js';
|
|
7
7
|
import { JsonRpcError, JsonRpcTransport } from './json-rpc-transport.js';
|
|
8
8
|
export const DEFAULT_ACP_CLIENT = { name: 'agents', title: 'Agents', version: '0.1.0' };
|
|
@@ -30,6 +30,7 @@ export class AcpBackend {
|
|
|
30
30
|
replaying = false;
|
|
31
31
|
contextPending = null;
|
|
32
32
|
prompt = null;
|
|
33
|
+
model = null;
|
|
33
34
|
constructor(launch, host, spec) {
|
|
34
35
|
this.launch = launch;
|
|
35
36
|
this.host = host;
|
|
@@ -86,6 +87,7 @@ export class AcpBackend {
|
|
|
86
87
|
const info = await this.openSession(transport, capabilities);
|
|
87
88
|
const modeId = await this.applyMode(transport, info);
|
|
88
89
|
const model = await this.applyModel(transport, info);
|
|
90
|
+
this.model = model;
|
|
89
91
|
await this.applyThoughtLevel(transport, info);
|
|
90
92
|
this.spec.onSession?.(info);
|
|
91
93
|
this.emit({
|
|
@@ -265,6 +267,7 @@ export class AcpBackend {
|
|
|
265
267
|
.then((result) => {
|
|
266
268
|
if (this.transport === transport && this.prompt === turn) {
|
|
267
269
|
this.prompt = null;
|
|
270
|
+
this.report(result);
|
|
268
271
|
this.emitAll(this.protocol.finish(turn.cancelled ? { stopReason: 'cancelled' } : result, turn.promptId));
|
|
269
272
|
}
|
|
270
273
|
})
|
|
@@ -373,7 +376,7 @@ export class AcpBackend {
|
|
|
373
376
|
}
|
|
374
377
|
return;
|
|
375
378
|
}
|
|
376
|
-
if (autoAllows(this.launch.runtimeMode, permission.kind)) {
|
|
379
|
+
if (autoAllows(this.launch.runtimeMode, permission.kind) || this.hostAllows(permission.request)) {
|
|
377
380
|
this.emitAll(permission.events.filter((event) => event.type !== 'approval.requested'));
|
|
378
381
|
const answer = this.protocol.answer(permission.requestId, 'allow');
|
|
379
382
|
if (answer) {
|
|
@@ -383,6 +386,14 @@ export class AcpBackend {
|
|
|
383
386
|
}
|
|
384
387
|
this.emitAll(permission.events);
|
|
385
388
|
}
|
|
389
|
+
hostAllows(request) {
|
|
390
|
+
try {
|
|
391
|
+
return this.spec.autoAllow?.(request) === true;
|
|
392
|
+
}
|
|
393
|
+
catch {
|
|
394
|
+
return false;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
386
397
|
handleExit(transport, exitCode, stderr) {
|
|
387
398
|
if (this.transport !== transport) {
|
|
388
399
|
return;
|
|
@@ -392,12 +403,23 @@ export class AcpBackend {
|
|
|
392
403
|
this.protocol.cancelPermissions();
|
|
393
404
|
this.emit({ type: 'exit', exitCode, ...(stderr === null ? {} : { stderr }) });
|
|
394
405
|
}
|
|
406
|
+
/* A cancelled turn still spent what it reports. */
|
|
407
|
+
report(result) {
|
|
408
|
+
const usage = this.spec.onUsage === undefined ? null : promptUsageOf(result);
|
|
409
|
+
if (usage === null) {
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
this.spec.onUsage?.({ ...usage, sessionId: this.sessionId, cwd: this.launch.cwd, model: this.model ?? this.launch.selection.model });
|
|
413
|
+
}
|
|
395
414
|
emitAll(events) {
|
|
396
415
|
for (const event of events) {
|
|
397
416
|
this.emit(event);
|
|
398
417
|
}
|
|
399
418
|
}
|
|
400
419
|
emit(event) {
|
|
420
|
+
if (event.type === 'model') {
|
|
421
|
+
this.model = event.model;
|
|
422
|
+
}
|
|
401
423
|
this.host.onEvent(event);
|
|
402
424
|
}
|
|
403
425
|
}
|
|
@@ -9,6 +9,14 @@ declare const DiffSchema: z.ZodObject<{
|
|
|
9
9
|
oldText: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
10
10
|
newText: z.ZodString;
|
|
11
11
|
}, z.core.$loose>;
|
|
12
|
+
export interface AcpTurnUsage {
|
|
13
|
+
input: number;
|
|
14
|
+
cacheRead: number;
|
|
15
|
+
cacheWrite: number;
|
|
16
|
+
output: number;
|
|
17
|
+
reasoning: number;
|
|
18
|
+
}
|
|
19
|
+
export declare function promptUsageOf(result: unknown): AcpTurnUsage | null;
|
|
12
20
|
export interface AcpAgentInfo {
|
|
13
21
|
name: string;
|
|
14
22
|
title: string | null;
|
|
@@ -19,6 +27,7 @@ export interface AcpAgentCapabilities {
|
|
|
19
27
|
loadSession: boolean;
|
|
20
28
|
resumeSession: boolean;
|
|
21
29
|
additionalDirectories: boolean;
|
|
30
|
+
closeSession: boolean;
|
|
22
31
|
images: boolean;
|
|
23
32
|
authMethods: string[];
|
|
24
33
|
agent: AcpAgentInfo | null;
|
|
@@ -83,10 +92,16 @@ export declare function toolRow(tool: Pick<ToolState, 'title' | 'name' | 'kind'
|
|
|
83
92
|
input: Record<string, unknown>;
|
|
84
93
|
changes?: ChatFileChange[];
|
|
85
94
|
};
|
|
95
|
+
export interface AcpPermissionRequest {
|
|
96
|
+
kind: string | null;
|
|
97
|
+
title: string | null;
|
|
98
|
+
commands: string[];
|
|
99
|
+
}
|
|
86
100
|
export interface AcpPermission {
|
|
87
101
|
requestId: string;
|
|
88
102
|
rpcId: number | string;
|
|
89
103
|
kind: string | null;
|
|
104
|
+
request: AcpPermissionRequest;
|
|
90
105
|
events: BackendEvent[];
|
|
91
106
|
}
|
|
92
107
|
export interface AcpStopWords {
|
|
@@ -40,6 +40,36 @@ const PermissionRequestSchema = z.looseObject({
|
|
|
40
40
|
});
|
|
41
41
|
const SessionNotificationSchema = z.looseObject({ sessionId: z.string(), update: z.looseObject({ sessionUpdate: z.string() }) });
|
|
42
42
|
const PromptResponseSchema = z.looseObject({ stopReason: z.string() });
|
|
43
|
+
const tokenCount = z.number().int().nonnegative();
|
|
44
|
+
// Unstable in the protocol (`PromptResponse.usage`); only the three totals are required.
|
|
45
|
+
const PromptUsageSchema = z.looseObject({
|
|
46
|
+
totalTokens: tokenCount,
|
|
47
|
+
inputTokens: tokenCount,
|
|
48
|
+
outputTokens: tokenCount,
|
|
49
|
+
thoughtTokens: tokenCount.nullish(),
|
|
50
|
+
cachedReadTokens: tokenCount.nullish(),
|
|
51
|
+
cachedWriteTokens: tokenCount.nullish()
|
|
52
|
+
});
|
|
53
|
+
/*
|
|
54
|
+
* The `usage` a `session/prompt` response carries, or null where the agent reports none. The protocol
|
|
55
|
+
* leaves open whether the input holds the cached part, so the total decides: when the kinds add up
|
|
56
|
+
* to more than it, the input counted the cache too and loses it here.
|
|
57
|
+
*/
|
|
58
|
+
export function promptUsageOf(result) {
|
|
59
|
+
const response = z.looseObject({ usage: PromptUsageSchema }).safeParse(result);
|
|
60
|
+
if (!response.success) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
const usage = response.data.usage;
|
|
64
|
+
const cacheRead = usage.cachedReadTokens ?? 0;
|
|
65
|
+
const cacheWrite = usage.cachedWriteTokens ?? 0;
|
|
66
|
+
const apart = usage.inputTokens + usage.outputTokens + cacheRead + cacheWrite <= usage.totalTokens;
|
|
67
|
+
const input = apart ? usage.inputTokens : Math.max(0, usage.inputTokens - cacheRead - cacheWrite);
|
|
68
|
+
if (input + cacheRead + cacheWrite + usage.outputTokens === 0) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
return { input, cacheRead, cacheWrite, output: usage.outputTokens, reasoning: usage.thoughtTokens ?? 0 };
|
|
72
|
+
}
|
|
43
73
|
const ModeStateSchema = z.looseObject({
|
|
44
74
|
currentModeId: z.string(),
|
|
45
75
|
availableModes: z.array(z.unknown())
|
|
@@ -70,7 +100,8 @@ const InitializeResponseSchema = z.looseObject({
|
|
|
70
100
|
sessionCapabilities: z
|
|
71
101
|
.looseObject({
|
|
72
102
|
resume: z.unknown().optional(),
|
|
73
|
-
additionalDirectories: z.unknown().optional()
|
|
103
|
+
additionalDirectories: z.unknown().optional(),
|
|
104
|
+
close: z.unknown().optional()
|
|
74
105
|
})
|
|
75
106
|
.optional()
|
|
76
107
|
})
|
|
@@ -99,6 +130,7 @@ export function parseInitialize(result) {
|
|
|
99
130
|
loadSession: capabilities?.loadSession === true,
|
|
100
131
|
resumeSession: sessions?.resume !== undefined && sessions.resume !== null,
|
|
101
132
|
additionalDirectories: sessions?.additionalDirectories !== undefined && sessions.additionalDirectories !== null,
|
|
133
|
+
closeSession: sessions?.close !== undefined && sessions.close !== null,
|
|
102
134
|
images: capabilities?.promptCapabilities?.image === true,
|
|
103
135
|
authMethods: validItems(AuthMethodSchema, parsed.data.authMethods).map((method) => method.id),
|
|
104
136
|
agent: agent ? { name: agent.name, title: agent.title ?? null, version: agent.version } : null
|
|
@@ -414,7 +446,9 @@ export class AcpProtocol {
|
|
|
414
446
|
description: tool.title === '' ? null : tool.title,
|
|
415
447
|
canAllowAlways: options.some((option) => option.kind === 'allow_always')
|
|
416
448
|
});
|
|
417
|
-
|
|
449
|
+
const command = rawField(tool.rawInput, 'command', 'cmd');
|
|
450
|
+
const asked = { kind: tool.kind, title: tool.title === '' ? null : tool.title, commands: [...(command === null ? [] : [command]), ...tool.text] };
|
|
451
|
+
return { requestId, rpcId, kind: tool.kind, request: asked, events };
|
|
418
452
|
}
|
|
419
453
|
/*
|
|
420
454
|
* The answer to a permission request: allow once, reject, or allow always only when the agent offered it.
|
package/dist/chat/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { type AcpAgentSpec, type AcpClientInfo, createAcpBackend } from './acp-backend.ts';
|
|
2
|
-
export type { AcpAgentInfo, AcpModel, AcpModelState, AcpMode, AcpModeState, AcpSessionInfo, AcpModeMap } from './acp-protocol.ts';
|
|
1
|
+
export { type AcpAgentSpec, type AcpClientInfo, type AcpUsageReport, createAcpBackend } from './acp-backend.ts';
|
|
2
|
+
export type { AcpAgentInfo, AcpModel, AcpModelState, AcpMode, AcpModeState, AcpPermissionRequest, AcpSessionInfo, AcpModeMap, AcpTurnUsage } from './acp-protocol.ts';
|
|
3
3
|
export { AttachmentStore } from './attachment-store.ts';
|
|
4
4
|
export type { BackendLaunch, TurnInput, ApprovalDecision, BackendEvent, BackendHost, ChatBackend } from './backend.ts';
|
|
5
5
|
export { runningInBackground, type BackgroundWork } from './background-work.ts';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type AcpClientInfo } from '../../chat/acp-backend.ts';
|
|
2
|
+
import { type AcpSessionInfo } from '../../chat/acp-protocol.ts';
|
|
3
|
+
import type { SpawnChatProcess } from '../../chat/chat-process.ts';
|
|
4
|
+
import { type AccountReading } from './status.ts';
|
|
5
|
+
export interface ProbeTimer {
|
|
6
|
+
setTimeout(run: () => void, ms: number): unknown;
|
|
7
|
+
clearTimeout(handle: unknown): void;
|
|
8
|
+
}
|
|
9
|
+
export interface AcpAccountProbe {
|
|
10
|
+
name: string;
|
|
11
|
+
command: readonly string[];
|
|
12
|
+
env: Record<string, string | undefined>;
|
|
13
|
+
client?: AcpClientInfo;
|
|
14
|
+
spawn?: SpawnChatProcess;
|
|
15
|
+
timeoutMs?: number;
|
|
16
|
+
timer?: ProbeTimer;
|
|
17
|
+
onSession?(info: AcpSessionInfo): void;
|
|
18
|
+
}
|
|
19
|
+
export declare function askAcpAccount(probe: AcpAccountProbe): Promise<AccountReading>;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { DEFAULT_ACP_CLIENT } from '../../chat/acp-backend.js';
|
|
5
|
+
import { ACP_AUTH_REQUIRED, ACP_PROTOCOL_VERSION, parseInitialize, parseSessionInfo, sessionIdOf } from '../../chat/acp-protocol.js';
|
|
6
|
+
import { JsonRpcError, JsonRpcTransport } from '../../chat/json-rpc-transport.js';
|
|
7
|
+
import { ASK_TIMEOUT_MS } from './status.js';
|
|
8
|
+
const SIGNED_OUT = { signedIn: false, email: null, plan: null, organization: null };
|
|
9
|
+
const SIGNED_IN = { signedIn: true, email: null, plan: null, organization: null };
|
|
10
|
+
const REAL_TIMER = {
|
|
11
|
+
setTimeout: (run, ms) => setTimeout(run, ms),
|
|
12
|
+
clearTimeout: (handle) => clearTimeout(handle)
|
|
13
|
+
};
|
|
14
|
+
function definedEnv(env) {
|
|
15
|
+
return Object.fromEntries(Object.entries(env).filter((entry) => entry[1] !== undefined));
|
|
16
|
+
}
|
|
17
|
+
function withDeadline(work, ms, timer, message) {
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
const handle = timer.setTimeout(() => reject(new Error(message)), ms);
|
|
20
|
+
work.then((value) => {
|
|
21
|
+
timer.clearTimeout(handle);
|
|
22
|
+
resolve(value);
|
|
23
|
+
}, (e) => {
|
|
24
|
+
timer.clearTimeout(handle);
|
|
25
|
+
reject(e instanceof Error ? e : new Error(String(e)));
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
/*
|
|
30
|
+
* Asks an ACP agent whether the account's environment is signed in: a session it opens means yes, "authentication
|
|
31
|
+
* required" means no. ACP has no request that says who, so a reading carries no email or plan. The session is opened
|
|
32
|
+
* in an empty folder of its own with no MCP servers, and `authenticate` is never sent: a probe must not start a login.
|
|
33
|
+
*/
|
|
34
|
+
export async function askAcpAccount(probe) {
|
|
35
|
+
const folder = await mkdtemp(join(tmpdir(), 'acp-account-'));
|
|
36
|
+
const client = probe.client ?? DEFAULT_ACP_CLIENT;
|
|
37
|
+
let transport = null;
|
|
38
|
+
try {
|
|
39
|
+
const started = new JsonRpcTransport({
|
|
40
|
+
command: [...probe.command],
|
|
41
|
+
cwd: folder,
|
|
42
|
+
env: definedEnv(probe.env),
|
|
43
|
+
...(probe.spawn ? { spawn: probe.spawn } : {}),
|
|
44
|
+
label: probe.name,
|
|
45
|
+
errorName: 'AcpRpcError',
|
|
46
|
+
// The probe's own deadline covers every request.
|
|
47
|
+
requestTimeoutMs: null,
|
|
48
|
+
versionField: true,
|
|
49
|
+
onFrame: (frame) => refuseRequest(started, frame),
|
|
50
|
+
onExit: () => undefined
|
|
51
|
+
});
|
|
52
|
+
transport = started;
|
|
53
|
+
const reading = async () => {
|
|
54
|
+
const capabilities = parseInitialize(await started.request('initialize', {
|
|
55
|
+
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
56
|
+
clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, terminal: false },
|
|
57
|
+
clientInfo: { name: client.name, title: client.title ?? client.name, version: client.version }
|
|
58
|
+
}));
|
|
59
|
+
if (capabilities.protocolVersion !== ACP_PROTOCOL_VERSION) {
|
|
60
|
+
throw new Error(`${probe.name} speaks ACP version ${capabilities.protocolVersion}; only version ${ACP_PROTOCOL_VERSION} is supported`);
|
|
61
|
+
}
|
|
62
|
+
let result;
|
|
63
|
+
try {
|
|
64
|
+
result = await started.request('session/new', { cwd: folder, mcpServers: [] });
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
if (error instanceof JsonRpcError && error.code === ACP_AUTH_REQUIRED) {
|
|
68
|
+
return SIGNED_OUT;
|
|
69
|
+
}
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
const sessionId = sessionIdOf(result);
|
|
73
|
+
if (sessionId === null) {
|
|
74
|
+
throw new Error(`${probe.name} opened a session without an id`);
|
|
75
|
+
}
|
|
76
|
+
probe.onSession?.(parseSessionInfo(result, sessionId, capabilities.agent));
|
|
77
|
+
if (capabilities.closeSession) {
|
|
78
|
+
// The answer is known by now; an agent that cannot close it keeps an empty session, nothing worse.
|
|
79
|
+
await started.request('session/close', { sessionId }).catch(() => undefined);
|
|
80
|
+
}
|
|
81
|
+
return SIGNED_IN;
|
|
82
|
+
};
|
|
83
|
+
return await withDeadline(reading(), probe.timeoutMs ?? ASK_TIMEOUT_MS, probe.timer ?? REAL_TIMER, `${probe.name} did not answer in time`);
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
await transport?.dispose();
|
|
87
|
+
await rm(folder, { recursive: true, force: true });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/* The probe advertises nothing, so a request from the agent gets a refusal rather than a silence it would wait on. */
|
|
91
|
+
function refuseRequest(transport, frame) {
|
|
92
|
+
if (frame.id === undefined || frame.id === null || !transport.alive) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
transport.respondError(frame.id, -32601, `Method not found: ${String(frame.method)}`);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
// The agent closed its input; the deadline or the exit settles the probe.
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -33,6 +33,7 @@ export declare class ProviderAccountsService implements AccountLaunches {
|
|
|
33
33
|
private readonly providers;
|
|
34
34
|
private readonly env;
|
|
35
35
|
private readonly ask;
|
|
36
|
+
private readonly asksEveryKind;
|
|
36
37
|
private readonly prepareShadow;
|
|
37
38
|
private readonly folderExists;
|
|
38
39
|
private readonly install;
|
|
@@ -8,7 +8,7 @@ import { errorText } from '../../error-text.js';
|
|
|
8
8
|
import { accountEnv, accountFolder, canContinue, defaultFolder, expandHome, isDefaultAccount, isOfferedKind, transcriptFolder } from './accounts.js';
|
|
9
9
|
import { AccountError, isDefaultAccountOf } from './launch.js';
|
|
10
10
|
import { prepareShadowHome } from './shadow-home.js';
|
|
11
|
-
import { askAccountAs } from './status.js';
|
|
11
|
+
import { ASKED_KINDS, askAccountAs } from './status.js';
|
|
12
12
|
import { acceptAccounts, accountsPath, InvalidAccountError, readAccounts, wireAccounts, writeAccounts } from './store.js';
|
|
13
13
|
import { DEFAULT_ACCOUNTS_HOST, platformSecrets, secretKey, SecretsUnavailableError } from './variables.js';
|
|
14
14
|
/* Who is signed in changes by hand and rarely; often enough to notice, rarely enough not to start CLIs all day. */
|
|
@@ -61,6 +61,8 @@ export class ProviderAccountsService {
|
|
|
61
61
|
providers;
|
|
62
62
|
env;
|
|
63
63
|
ask;
|
|
64
|
+
// A host's own `ask` is taken to answer for every kind; the default only for `ASKED_KINDS`.
|
|
65
|
+
asksEveryKind;
|
|
64
66
|
prepareShadow;
|
|
65
67
|
folderExists;
|
|
66
68
|
install;
|
|
@@ -87,6 +89,7 @@ export class ProviderAccountsService {
|
|
|
87
89
|
this.providers = options.providers;
|
|
88
90
|
this.env = options.env ?? process.env;
|
|
89
91
|
this.ask = options.ask ?? askAccountAs(options.client ?? DEFAULT_CODEX_CLIENT);
|
|
92
|
+
this.asksEveryKind = options.ask !== undefined;
|
|
90
93
|
this.prepareShadow = options.prepareShadow ?? prepareShadowHome;
|
|
91
94
|
this.folderExists = options.folderExists ?? isFolder;
|
|
92
95
|
this.install = options.install;
|
|
@@ -106,10 +109,15 @@ export class ProviderAccountsService {
|
|
|
106
109
|
snapshot() {
|
|
107
110
|
const loginCommands = {};
|
|
108
111
|
const chatLogins = [];
|
|
112
|
+
const silentLogins = [];
|
|
109
113
|
for (const kind of this.kinds()) {
|
|
110
|
-
const
|
|
114
|
+
const provider = this.providers.get(kind);
|
|
115
|
+
const home = provider.home;
|
|
111
116
|
if (home?.loginCommand !== undefined) {
|
|
112
117
|
loginCommands[kind] = home.loginCommand;
|
|
118
|
+
if (provider.askAccount === undefined && !this.asksEveryKind && !ASKED_KINDS.includes(kind)) {
|
|
119
|
+
silentLogins.push(kind);
|
|
120
|
+
}
|
|
113
121
|
}
|
|
114
122
|
else if (home !== undefined) {
|
|
115
123
|
chatLogins.push(kind);
|
|
@@ -120,7 +128,8 @@ export class ProviderAccountsService {
|
|
|
120
128
|
statuses: Object.entries(this.accounts).map(([id, account]) => this.statusOf(id, account)),
|
|
121
129
|
secretsAvailable: this.secrets !== null,
|
|
122
130
|
loginCommands,
|
|
123
|
-
...(chatLogins.length > 0 ? { chatLogins } : {})
|
|
131
|
+
...(chatLogins.length > 0 ? { chatLogins } : {}),
|
|
132
|
+
...(silentLogins.length > 0 ? { silentLogins } : {})
|
|
124
133
|
};
|
|
125
134
|
}
|
|
126
135
|
/* The account under an id, for a launch; null for an id this machine does not have. */
|
|
@@ -510,7 +519,8 @@ export class ProviderAccountsService {
|
|
|
510
519
|
if (provider.home.loginCommand === undefined) {
|
|
511
520
|
return this.status(id, account, 'ready', { message });
|
|
512
521
|
}
|
|
513
|
-
const
|
|
522
|
+
const env = accountEnv(id, account, provider, this.env, values);
|
|
523
|
+
const reading = await (provider.askAccount ? provider.askAccount(provider.command, env) : this.ask(kind, provider.command, env));
|
|
514
524
|
return this.status(id, account, reading.signedIn ? 'ready' : 'signed-out', {
|
|
515
525
|
email: reading.email,
|
|
516
526
|
plan: reading.plan,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AgentKind } from '@adecore/agent-contracts';
|
|
2
2
|
import { type CodexClientInfo } from '../../chat/codex-transport.ts';
|
|
3
|
+
export declare const ASK_TIMEOUT_MS = 20000;
|
|
3
4
|
export interface AccountReading {
|
|
4
5
|
signedIn: boolean;
|
|
5
6
|
email: string | null;
|
|
@@ -9,4 +10,5 @@ export interface AccountReading {
|
|
|
9
10
|
export type AskAccount = (kind: AgentKind, command: readonly string[], env: Record<string, string | undefined>) => Promise<AccountReading>;
|
|
10
11
|
export declare function readClaudeAuthStatus(output: string): AccountReading;
|
|
11
12
|
export declare function readCodexAccount(answer: unknown): AccountReading;
|
|
13
|
+
export declare const ASKED_KINDS: readonly AgentKind[];
|
|
12
14
|
export declare function askAccountAs(client: CodexClientInfo): AskAccount;
|
|
@@ -3,7 +3,7 @@ import { withTimeout } from '../../async.js';
|
|
|
3
3
|
import { CodexTransport } from '../../chat/codex-transport.js';
|
|
4
4
|
import { runProcess } from '../../run-process.js';
|
|
5
5
|
/* A CLI that has not said who is signed in by now is not going to; the next pass asks again. */
|
|
6
|
-
const ASK_TIMEOUT_MS = 20_000;
|
|
6
|
+
export const ASK_TIMEOUT_MS = 20_000;
|
|
7
7
|
function text(value) {
|
|
8
8
|
return typeof value === 'string' && value !== '' ? value : null;
|
|
9
9
|
}
|
|
@@ -69,6 +69,8 @@ async function askCodex(command, env, client) {
|
|
|
69
69
|
transport.kill();
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
|
+
/* The kinds `askAccountAs` can ask; a provider with a reader of its own answers for itself. */
|
|
73
|
+
export const ASKED_KINDS = ['claude', 'codex'];
|
|
72
74
|
/* Starts the CLI in the account's environment and asks it; only Claude Code and Codex can say. */
|
|
73
75
|
export function askAccountAs(client) {
|
|
74
76
|
return (kind, command, env) => {
|
|
@@ -19,5 +19,8 @@ export interface AcpProviderSpec extends AcpAgentSpec {
|
|
|
19
19
|
firstPromptArgs?(prompt: string): string[];
|
|
20
20
|
detect?(command: string, env: Record<string, string | undefined>): Promise<CliDetection>;
|
|
21
21
|
enabled?(): boolean;
|
|
22
|
+
accountProbe?: boolean;
|
|
23
|
+
adoptModels?: boolean;
|
|
24
|
+
usageLog?: string;
|
|
22
25
|
}
|
|
23
26
|
export declare function createAcpProvider(spec: AcpProviderSpec): ChatProvider;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { createAcpBackend } from '../chat/acp-backend.js';
|
|
2
|
+
import { askAcpAccount } from './accounts/acp-account.js';
|
|
3
|
+
import { writeAcpUsage } from './acp-usage-log.js';
|
|
2
4
|
import { detectCli } from './detect.js';
|
|
3
5
|
/*
|
|
4
6
|
* What every ACP agent can do through this backend. A file change comes as the text before and after;
|
|
@@ -21,8 +23,27 @@ export const ACP_CAPABILITIES = {
|
|
|
21
23
|
reportsThinking: true,
|
|
22
24
|
slashCommands: true
|
|
23
25
|
};
|
|
26
|
+
/* Agents whose own usage files the scanner reads; a log of their turns would count every one twice. */
|
|
27
|
+
const OWN_USAGE_FILES = new Set(['grok']);
|
|
28
|
+
/* An agent's model id may reach a command line, so one that reads as a flag is left out. */
|
|
29
|
+
function adoptableModels(info) {
|
|
30
|
+
return (info.models?.models ?? []).filter((model) => model.id !== '' && !model.id.startsWith('-')).map((model) => ({ slug: model.id, name: model.name }));
|
|
31
|
+
}
|
|
24
32
|
export function createAcpProvider(spec) {
|
|
25
33
|
const capabilities = { ...ACP_CAPABILITIES, ...spec.capabilities };
|
|
34
|
+
const onSession = (info) => {
|
|
35
|
+
if (spec.adoptModels !== false && info.models !== null) {
|
|
36
|
+
spec.catalog.adopt(adoptableModels(info), info.models.current);
|
|
37
|
+
}
|
|
38
|
+
spec.onSession?.(info);
|
|
39
|
+
};
|
|
40
|
+
const usageLog = spec.usageLog === undefined || OWN_USAGE_FILES.has(spec.kind) ? null : spec.usageLog;
|
|
41
|
+
const onUsage = usageLog === null
|
|
42
|
+
? spec.onUsage
|
|
43
|
+
: (report) => {
|
|
44
|
+
writeAcpUsage(usageLog, report, Date.now());
|
|
45
|
+
spec.onUsage?.(report);
|
|
46
|
+
};
|
|
26
47
|
return {
|
|
27
48
|
kind: spec.kind,
|
|
28
49
|
name: spec.name,
|
|
@@ -41,6 +62,17 @@ export function createAcpProvider(spec) {
|
|
|
41
62
|
throw new Error(`${spec.name} is only available as a chat.`);
|
|
42
63
|
}),
|
|
43
64
|
detect: spec.detect ?? ((command, env) => detectCli(command, env)),
|
|
44
|
-
|
|
65
|
+
...(spec.accountProbe === false
|
|
66
|
+
? {}
|
|
67
|
+
: {
|
|
68
|
+
askAccount: (command, env) => askAcpAccount({
|
|
69
|
+
name: spec.name,
|
|
70
|
+
command,
|
|
71
|
+
env: { ...env, ...spec.env },
|
|
72
|
+
...(spec.client ? { client: spec.client } : {}),
|
|
73
|
+
onSession
|
|
74
|
+
})
|
|
75
|
+
}),
|
|
76
|
+
createBackend: createAcpBackend({ ...spec, onSession, ...(onUsage === undefined ? {} : { onUsage }) })
|
|
45
77
|
};
|
|
46
78
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { appendFileSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { errorText } from '../error-text.js';
|
|
4
|
+
import { hostUsageLine } from '../usage/readers/host-log.js';
|
|
5
|
+
/*
|
|
6
|
+
* Appends one turn to the session's own file in the folder, so a scan that hashes what it already read
|
|
7
|
+
* of a file never rereads the log of every other session. Written at once, since it is one line per turn.
|
|
8
|
+
*/
|
|
9
|
+
export function writeAcpUsage(folder, report, at) {
|
|
10
|
+
const { sessionId, cwd, model, ...tokens } = report;
|
|
11
|
+
try {
|
|
12
|
+
mkdirSync(folder, { recursive: true, mode: 0o700 });
|
|
13
|
+
appendFileSync(join(folder, `${encodeURIComponent(sessionId === '' ? 'session' : sessionId)}.jsonl`), hostUsageLine({ at, model, session: sessionId, cwd, ...tokens }), {
|
|
14
|
+
mode: 0o600
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
catch (e) {
|
|
18
|
+
// A turn whose line could not be written is missing from the usage page; the chat itself goes on.
|
|
19
|
+
console.warn('Could not write the usage of an ACP turn:', errorText(e));
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -8,6 +8,10 @@ export declare class ModelCatalog {
|
|
|
8
8
|
constructor(data?: ModelCatalogInput);
|
|
9
9
|
get defaultModel(): string;
|
|
10
10
|
replace(data: ModelCatalogData): boolean;
|
|
11
|
+
adopt(models: ReadonlyArray<{
|
|
12
|
+
slug: string;
|
|
13
|
+
name: string;
|
|
14
|
+
}>, defaultModel: string): boolean;
|
|
11
15
|
list(): ModelInfo[];
|
|
12
16
|
nameOf(slug: string): string;
|
|
13
17
|
resolveModel(name: string): string | null;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import manifest from './claude-models.json' with { type: 'json' };
|
|
2
|
+
const ADOPTED_PROFILE = 'agent';
|
|
2
3
|
function timeOf(data) {
|
|
3
4
|
return data.updatedAt === undefined ? 0 : Date.parse(data.updatedAt);
|
|
4
5
|
}
|
|
@@ -26,6 +27,26 @@ export class ModelCatalog {
|
|
|
26
27
|
this.data = data;
|
|
27
28
|
return changed;
|
|
28
29
|
}
|
|
30
|
+
/*
|
|
31
|
+
* Takes the models an agent reported for itself in place of the list, the agent's current model as the
|
|
32
|
+
* default and its order kept; answers whether the models changed. Every model gets the profile the
|
|
33
|
+
* default has now, as an agent names its models but not what each can be asked for.
|
|
34
|
+
*/
|
|
35
|
+
adopt(models, defaultModel) {
|
|
36
|
+
if (models.length === 0 || !models.some((model) => model.slug === defaultModel)) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
const profile = this.profileOf(this.data.defaultModel) ?? { options: [], contextWindowTokens: {} };
|
|
40
|
+
const data = {
|
|
41
|
+
...(this.data.updatedAt === undefined ? {} : { updatedAt: this.data.updatedAt }),
|
|
42
|
+
defaultModel,
|
|
43
|
+
profiles: { [ADOPTED_PROFILE]: profile },
|
|
44
|
+
models: models.map((model) => ({ slug: model.slug, name: model.name, profile: ADOPTED_PROFILE }))
|
|
45
|
+
};
|
|
46
|
+
const changed = JSON.stringify(data) !== JSON.stringify(this.data);
|
|
47
|
+
this.data = data;
|
|
48
|
+
return changed;
|
|
49
|
+
}
|
|
29
50
|
list() {
|
|
30
51
|
return this.data.models.map((model) => ({
|
|
31
52
|
slug: model.slug,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { ACP_CAPABILITIES, type AcpProviderSpec, createAcpProvider } from './acp-provider.ts';
|
|
2
|
+
export { type AcpAccountProbe, askAcpAccount, type ProbeTimer } from './accounts/acp-account.ts';
|
|
2
3
|
export { AccountError, type AccountLaunches, storedAccount, launchEnv } from './accounts/launch.ts';
|
|
3
4
|
export { ProviderAccountsService, type ProviderAccountsOptions } from './accounts/service.ts';
|
|
4
5
|
export type { ShadowHomeReport } from './accounts/shadow-home.ts';
|
|
@@ -11,5 +12,6 @@ export { type CodexRules, codexRulesText, codexRulesPathIn, defaultCodexHome, in
|
|
|
11
12
|
export { codexThreadOptions, codexServiceTier } from './codex.ts';
|
|
12
13
|
export { type CliDetection, detectCli } from './detect.ts';
|
|
13
14
|
export type { ChatProvider, ProviderHome } from './provider.ts';
|
|
15
|
+
export { isOnlyCommands } from './shell-commands.ts';
|
|
14
16
|
export { ProviderRegistry, type ProviderRegistryOptions } from './registry.ts';
|
|
15
17
|
export { MODE_ORDER, narrowerMode, ceilingForOpening, modeFlag } from '../modes.ts';
|
package/dist/providers/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Public API: exports.test.ts snapshots this list, so a name added or dropped here is an API change.
|
|
2
2
|
export { ACP_CAPABILITIES, createAcpProvider } from './acp-provider.js';
|
|
3
|
+
export { askAcpAccount } from './accounts/acp-account.js';
|
|
3
4
|
export { AccountError, storedAccount, launchEnv } from './accounts/launch.js';
|
|
4
5
|
export { ProviderAccountsService } from './accounts/service.js';
|
|
5
6
|
export { variablesProblem, keychainService } from './accounts/variables.js';
|
|
@@ -9,5 +10,6 @@ export { createCodexProvider } from './codex-provider.js';
|
|
|
9
10
|
export { codexRulesText, codexRulesPathIn, defaultCodexHome, installCodexRules } from './codex-rules.js';
|
|
10
11
|
export { codexThreadOptions, codexServiceTier } from './codex.js';
|
|
11
12
|
export { detectCli } from './detect.js';
|
|
13
|
+
export { isOnlyCommands } from './shell-commands.js';
|
|
12
14
|
export { ProviderRegistry } from './registry.js';
|
|
13
15
|
export { MODE_ORDER, narrowerMode, ceilingForOpening, modeFlag } from '../modes.js';
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AgentKind, ProviderCapabilities, ProviderInstall } from '@adecore/agent-contracts';
|
|
2
2
|
import type { BackendHost, BackendLaunch, ChatBackend } from '../chat/backend.ts';
|
|
3
|
+
import type { AccountReading } from './accounts/status.ts';
|
|
3
4
|
import type { ModelCatalog } from './catalog.ts';
|
|
4
5
|
import type { CliDetection } from './detect.ts';
|
|
5
6
|
export interface ProviderHome {
|
|
@@ -23,5 +24,6 @@ export interface ChatProvider {
|
|
|
23
24
|
firstPromptArgs(prompt: string): string[];
|
|
24
25
|
detect(command: string, env: Record<string, string | undefined>): Promise<CliDetection>;
|
|
25
26
|
enabled?(): boolean;
|
|
27
|
+
askAccount?(command: readonly string[], env: Record<string, string | undefined>): Promise<AccountReading>;
|
|
26
28
|
createBackend(launch: BackendLaunch, host: BackendHost): ChatBackend;
|
|
27
29
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function isOnlyCommands(command: string, allowed: readonly string[]): boolean;
|