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