@adecore/agents 0.29.3 → 0.30.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 +59 -0
- package/dist/chat/acp-backend.js +407 -0
- package/dist/chat/acp-protocol.d.ts +131 -0
- package/dist/chat/acp-protocol.js +605 -0
- package/dist/chat/codex-transport.d.ts +4 -27
- package/dist/chat/codex-transport.js +4 -185
- package/dist/chat/fixtures/acp-v1-turn.json +240 -0
- package/dist/chat/index.d.ts +2 -0
- package/dist/chat/index.js +1 -0
- package/dist/chat/json-rpc-transport.d.ts +50 -0
- package/dist/chat/json-rpc-transport.js +203 -0
- package/dist/providers/accounts/accounts.d.ts +10 -6
- package/dist/providers/accounts/accounts.js +19 -19
- package/dist/providers/accounts/service.d.ts +3 -0
- package/dist/providers/accounts/service.js +20 -13
- package/dist/providers/accounts/store.d.ts +4 -5
- package/dist/providers/accounts/store.js +12 -12
- package/dist/providers/acp-provider.d.ts +22 -0
- package/dist/providers/acp-provider.js +45 -0
- package/dist/providers/catalog.js +1 -0
- package/dist/providers/index.d.ts +1 -0
- package/dist/providers/index.js +1 -0
- package/dist/providers/registry.d.ts +1 -0
- package/dist/providers/registry.js +4 -0
- package/dist/testing/fake-acp.d.ts +10 -0
- package/dist/testing/fake-acp.js +262 -0
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.js +1 -0
- package/package.json +3 -3
- package/src/chat/acp-backend.ts +476 -0
- package/src/chat/acp-protocol.ts +763 -0
- package/src/chat/codex-transport.ts +6 -207
- package/src/chat/fixtures/acp-v1-turn.json +240 -0
- package/src/chat/index.ts +2 -0
- package/src/chat/json-rpc-transport.ts +253 -0
- package/src/providers/accounts/accounts.ts +28 -25
- package/src/providers/accounts/service.ts +37 -14
- package/src/providers/accounts/store.ts +14 -17
- package/src/providers/acp-provider.ts +73 -0
- package/src/providers/catalog.ts +1 -0
- package/src/providers/index.ts +1 -0
- package/src/providers/registry.ts +5 -0
- package/src/testing/fake-acp.ts +296 -0
- package/src/testing/index.ts +1 -0
package/src/providers/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// Public API: exports.test.ts snapshots this list, so a name added or dropped here is an API change.
|
|
2
|
+
export { ACP_CAPABILITIES, type AcpProviderSpec, createAcpProvider } from './acp-provider.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';
|
|
@@ -94,6 +94,11 @@ export class ProviderRegistry {
|
|
|
94
94
|
);
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
/* The kinds this host offers, in the order every menu lists them. */
|
|
98
|
+
kinds(): AgentKind[] {
|
|
99
|
+
return this.providers.map((provider) => provider.kind);
|
|
100
|
+
}
|
|
101
|
+
|
|
97
102
|
/* The provider of a kind, one this host does not offer included. */
|
|
98
103
|
get(kind: AgentKind): ChatProvider {
|
|
99
104
|
return this.providers.find((provider) => provider.kind === kind) ?? this.fallback(kind);
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Network-free ACP agent. Prompt commands exercise permissions, cancels, plans, edits and resumes
|
|
3
|
+
* through the JSON-RPC shapes of the Agent Client Protocol, version 1.
|
|
4
|
+
*/
|
|
5
|
+
import type { FakeCli } from './fake-cli.ts';
|
|
6
|
+
|
|
7
|
+
type Frame = Record<string, unknown>;
|
|
8
|
+
|
|
9
|
+
// The prompts of every session, outside any one process: a real agent keeps them on disk, so a later process loads them too.
|
|
10
|
+
const sessionHistory = new Map<string, string[]>();
|
|
11
|
+
|
|
12
|
+
export interface FakeAcpOptions {
|
|
13
|
+
// Advertises `loadSession`; on by default.
|
|
14
|
+
loadSession?: boolean;
|
|
15
|
+
// Advertises `session/resume`, which a client then prefers over a load.
|
|
16
|
+
resumeSession?: boolean;
|
|
17
|
+
// Answers session requests with "authentication required" until `authenticate` names this method.
|
|
18
|
+
authMethod?: string;
|
|
19
|
+
// The modes the session offers, as `session/set_mode` takes them; empty offers none.
|
|
20
|
+
modes?: readonly string[];
|
|
21
|
+
// Reports models the way agents did before config options: `models` and `session/set_model`.
|
|
22
|
+
legacyModels?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function fakeAcpWith(options: FakeAcpOptions): FakeCli {
|
|
26
|
+
return (io) => {
|
|
27
|
+
const modes = options.modes ?? ['ask', 'edit', 'yolo'];
|
|
28
|
+
let authenticated = options.authMethod === undefined;
|
|
29
|
+
let sessionId = '';
|
|
30
|
+
let mode = modes[0] ?? '';
|
|
31
|
+
let model = 'fake-small';
|
|
32
|
+
let serverRequestId = 0;
|
|
33
|
+
let callCounter = 0;
|
|
34
|
+
let prompt: { rpcId: number | string; cancelled: boolean } | null = null;
|
|
35
|
+
let permission: { rpcId: number; onAnswer(outcome: Frame): void } | null = null;
|
|
36
|
+
|
|
37
|
+
const send = (frame: Frame): void => {
|
|
38
|
+
io.out({ jsonrpc: '2.0', ...frame });
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const respond = (id: unknown, result: unknown): void => {
|
|
42
|
+
send({ id, result });
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const refuse = (id: unknown, code: number, message: string): void => {
|
|
46
|
+
send({ id, error: { code, message } });
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const update = (fields: Frame): void => {
|
|
50
|
+
send({ method: 'session/update', params: { sessionId, update: fields } });
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const say = (text: string): void => {
|
|
54
|
+
const half = Math.ceil(text.length / 2);
|
|
55
|
+
update({ sessionUpdate: 'agent_message_chunk', content: { type: 'text', text: text.slice(0, half) } });
|
|
56
|
+
update({ sessionUpdate: 'agent_message_chunk', content: { type: 'text', text: text.slice(half) } });
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const endTurn = (stopReason: string): void => {
|
|
60
|
+
if (prompt === null) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
update({ sessionUpdate: 'usage_update', used: 1200, size: 200000, cost: { amount: 0.01, currency: 'USD' } });
|
|
64
|
+
respond(prompt.rpcId, { stopReason });
|
|
65
|
+
prompt = null;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const sessionState = (): Frame => {
|
|
69
|
+
const modelChoices = [
|
|
70
|
+
{ value: 'fake-small', name: 'Fake small' },
|
|
71
|
+
{ value: 'fake-large', name: 'Fake large' }
|
|
72
|
+
];
|
|
73
|
+
return {
|
|
74
|
+
...(modes.length === 0 ? {} : { modes: { currentModeId: mode, availableModes: modes.map((id) => ({ id, name: id })) } }),
|
|
75
|
+
...(options.legacyModels
|
|
76
|
+
? { models: { currentModelId: model, availableModels: modelChoices.map((choice) => ({ modelId: choice.value, name: choice.name })) } }
|
|
77
|
+
: { configOptions: [{ id: 'model', name: 'Model', type: 'select', category: 'model', currentValue: model, options: modelChoices }] })
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const askPermission = (toolCall: Frame, onAnswer: (outcome: Frame) => void): void => {
|
|
82
|
+
const rpcId = serverRequestId++;
|
|
83
|
+
permission = { rpcId, onAnswer };
|
|
84
|
+
send({
|
|
85
|
+
id: rpcId,
|
|
86
|
+
method: 'session/request_permission',
|
|
87
|
+
params: {
|
|
88
|
+
sessionId,
|
|
89
|
+
toolCall,
|
|
90
|
+
options: [
|
|
91
|
+
{ optionId: 'once', name: 'Allow once', kind: 'allow_once' },
|
|
92
|
+
{ optionId: 'always', name: 'Always allow', kind: 'allow_always' },
|
|
93
|
+
{ optionId: 'no', name: 'Reject', kind: 'reject_once' }
|
|
94
|
+
]
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const handlePrompt = (id: unknown, params: Frame): void => {
|
|
100
|
+
const blocks = Array.isArray(params.prompt) ? (params.prompt as Frame[]) : [];
|
|
101
|
+
const text = blocks
|
|
102
|
+
.filter((block) => block.type === 'text')
|
|
103
|
+
.map((block) => String(block.text))
|
|
104
|
+
.join('\n');
|
|
105
|
+
const images = blocks.filter((block) => block.type === 'image').length;
|
|
106
|
+
const history = sessionHistory.get(sessionId) ?? [];
|
|
107
|
+
sessionHistory.set(sessionId, [...history, text]);
|
|
108
|
+
prompt = { rpcId: id as number | string, cancelled: false };
|
|
109
|
+
const last = text.split('\n\n').at(-1) ?? text;
|
|
110
|
+
if (last === 'history?') {
|
|
111
|
+
say(`prompts before: ${history.length}`);
|
|
112
|
+
endTurn('end_turn');
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (last === 'mode?') {
|
|
116
|
+
say(`mode: ${mode}, model: ${model}`);
|
|
117
|
+
endTurn('end_turn');
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (last === 'crash') {
|
|
121
|
+
io.exit(1);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (last === 'slow') {
|
|
125
|
+
say('working');
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (last === 'plan') {
|
|
129
|
+
update({
|
|
130
|
+
sessionUpdate: 'plan',
|
|
131
|
+
entries: [
|
|
132
|
+
{ content: 'Read the code', priority: 'high', status: 'completed' },
|
|
133
|
+
{ content: 'Write the fix', priority: 'medium', status: 'in_progress' }
|
|
134
|
+
]
|
|
135
|
+
});
|
|
136
|
+
endTurn('end_turn');
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (last === 'refuse' || last === 'max tokens') {
|
|
140
|
+
say('no');
|
|
141
|
+
endTurn(last === 'refuse' ? 'refusal' : 'max_tokens');
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (last.startsWith('edit:')) {
|
|
145
|
+
const path = last.slice(5).trim();
|
|
146
|
+
const toolCallId = `call-${++callCounter}`;
|
|
147
|
+
const content = [{ type: 'diff', path, oldText: 'one\ntwo\n', newText: 'one\nthree\n' }];
|
|
148
|
+
update({ sessionUpdate: 'tool_call', toolCallId, title: `Edit ${path}`, kind: 'edit', status: 'pending', locations: [{ path }] });
|
|
149
|
+
askPermission({ toolCallId, title: `Edit ${path}`, kind: 'edit', content }, (outcome) => {
|
|
150
|
+
const allowed = outcome.outcome === 'selected' && outcome.optionId !== 'no';
|
|
151
|
+
update({ sessionUpdate: 'tool_call_update', toolCallId, status: allowed ? 'completed' : 'failed', content });
|
|
152
|
+
endTurn(outcome.outcome === 'cancelled' ? 'cancelled' : 'end_turn');
|
|
153
|
+
});
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (last.startsWith('tool:')) {
|
|
157
|
+
const command = last.slice(5).trim();
|
|
158
|
+
const toolCallId = `call-${++callCounter}`;
|
|
159
|
+
update({ sessionUpdate: 'agent_thought_chunk', content: { type: 'text', text: 'I should run it.' } });
|
|
160
|
+
update({ sessionUpdate: 'tool_call', toolCallId, title: `Run ${command}`, kind: 'execute', status: 'pending', rawInput: { command } });
|
|
161
|
+
askPermission({ toolCallId }, (outcome) => {
|
|
162
|
+
if (outcome.outcome === 'cancelled') {
|
|
163
|
+
update({ sessionUpdate: 'tool_call_update', toolCallId, status: 'failed' });
|
|
164
|
+
endTurn('cancelled');
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (outcome.optionId === 'no') {
|
|
168
|
+
update({
|
|
169
|
+
sessionUpdate: 'tool_call_update',
|
|
170
|
+
toolCallId,
|
|
171
|
+
status: 'failed',
|
|
172
|
+
content: [{ type: 'content', content: { type: 'text', text: 'rejected' } }]
|
|
173
|
+
});
|
|
174
|
+
say('not run');
|
|
175
|
+
endTurn('end_turn');
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
update({ sessionUpdate: 'tool_call_update', toolCallId, status: 'in_progress' });
|
|
179
|
+
update({
|
|
180
|
+
sessionUpdate: 'tool_call_update',
|
|
181
|
+
toolCallId,
|
|
182
|
+
status: 'completed',
|
|
183
|
+
content: [{ type: 'content', content: { type: 'text', text: `ran ${command} (${String(outcome.optionId)})` } }]
|
|
184
|
+
});
|
|
185
|
+
say('done');
|
|
186
|
+
endTurn('end_turn');
|
|
187
|
+
});
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
update({ sessionUpdate: 'agent_thought_chunk', content: { type: 'text', text: 'Thinking.' } });
|
|
191
|
+
say(`echo: ${text}${images > 0 ? ` (${images} images)` : ''}`);
|
|
192
|
+
endTurn('end_turn');
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const handle = (frame: Frame): void => {
|
|
196
|
+
const id = frame.id;
|
|
197
|
+
const params = (typeof frame.params === 'object' && frame.params !== null ? frame.params : {}) as Frame;
|
|
198
|
+
if (frame.method === undefined) {
|
|
199
|
+
// The client's answer to a permission request.
|
|
200
|
+
if (permission !== null && id === permission.rpcId) {
|
|
201
|
+
const answered = permission;
|
|
202
|
+
permission = null;
|
|
203
|
+
const result = frame.result as { outcome?: Frame } | undefined;
|
|
204
|
+
answered.onAnswer(result?.outcome ?? { outcome: 'cancelled' });
|
|
205
|
+
}
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
switch (frame.method) {
|
|
209
|
+
case 'initialize':
|
|
210
|
+
respond(id, {
|
|
211
|
+
protocolVersion: 1,
|
|
212
|
+
agentCapabilities: {
|
|
213
|
+
loadSession: options.loadSession ?? true,
|
|
214
|
+
promptCapabilities: { image: true, audio: false, embeddedContext: false },
|
|
215
|
+
sessionCapabilities: options.resumeSession ? { resume: {} } : {}
|
|
216
|
+
},
|
|
217
|
+
authMethods: options.authMethod === undefined ? [] : [{ id: options.authMethod, name: 'Sign in' }],
|
|
218
|
+
agentInfo: { name: 'fake-acp', title: 'Fake ACP', version: '0.0.1' }
|
|
219
|
+
});
|
|
220
|
+
return;
|
|
221
|
+
case 'authenticate':
|
|
222
|
+
authenticated = params.methodId === options.authMethod;
|
|
223
|
+
respond(id, {});
|
|
224
|
+
return;
|
|
225
|
+
case 'session/new':
|
|
226
|
+
if (!authenticated) {
|
|
227
|
+
refuse(id, -32000, 'Authentication required');
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
sessionId = `fake-session-${Math.random().toString(36).slice(2, 8)}`;
|
|
231
|
+
sessionHistory.set(sessionId, []);
|
|
232
|
+
respond(id, { sessionId, ...sessionState() });
|
|
233
|
+
return;
|
|
234
|
+
case 'session/load':
|
|
235
|
+
case 'session/resume': {
|
|
236
|
+
const history = sessionHistory.get(String(params.sessionId));
|
|
237
|
+
if (!authenticated) {
|
|
238
|
+
refuse(id, -32000, 'Authentication required');
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (history === undefined) {
|
|
242
|
+
refuse(id, -32002, 'Session not found');
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
sessionId = String(params.sessionId);
|
|
246
|
+
if (frame.method === 'session/load') {
|
|
247
|
+
for (const earlier of history) {
|
|
248
|
+
update({ sessionUpdate: 'user_message_chunk', content: { type: 'text', text: earlier } });
|
|
249
|
+
update({ sessionUpdate: 'agent_message_chunk', content: { type: 'text', text: 'replayed' } });
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
respond(id, sessionState());
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
case 'session/set_mode':
|
|
256
|
+
mode = String(params.modeId);
|
|
257
|
+
respond(id, {});
|
|
258
|
+
update({ sessionUpdate: 'current_mode_update', currentModeId: mode });
|
|
259
|
+
return;
|
|
260
|
+
case 'session/set_model':
|
|
261
|
+
model = String(params.modelId);
|
|
262
|
+
respond(id, {});
|
|
263
|
+
return;
|
|
264
|
+
case 'session/set_config_option':
|
|
265
|
+
if (params.configId === 'model') {
|
|
266
|
+
model = String(params.value);
|
|
267
|
+
}
|
|
268
|
+
respond(id, { configOptions: sessionState().configOptions ?? [] });
|
|
269
|
+
return;
|
|
270
|
+
case 'session/prompt':
|
|
271
|
+
handlePrompt(id, params);
|
|
272
|
+
return;
|
|
273
|
+
case 'session/cancel':
|
|
274
|
+
if (prompt !== null) {
|
|
275
|
+
prompt.cancelled = true;
|
|
276
|
+
if (permission === null) {
|
|
277
|
+
endTurn('cancelled');
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return;
|
|
281
|
+
default:
|
|
282
|
+
if (id !== undefined) {
|
|
283
|
+
refuse(id, -32601, `Method not found: ${String(frame.method)}`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
return {
|
|
289
|
+
onLine: (line) => {
|
|
290
|
+
handle(JSON.parse(line) as Frame);
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export const fakeAcp: FakeCli = fakeAcpWith({});
|
package/src/testing/index.ts
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 { type FakeIo, type FakeProgram, type FakeCli, type StartedFake, type InProcessCli, inProcess, runOverStdio } from './fake-cli.ts';
|
|
3
|
+
export { type FakeAcpOptions, fakeAcpWith, fakeAcp } from './fake-acp.ts';
|
|
3
4
|
export { fakeClaude, claudeStoppingOnResume } from './fake-claude.ts';
|
|
4
5
|
export { FAKE_CHILD_STEPS, fakeCodexForks, type FakeCodexOptions, fakeCodexWith, fakeCodex } from './fake-codex.ts';
|
|
5
6
|
export { testAccounts } from './accounts.ts';
|