@causyne/agent 1.10.21 → 1.11.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 (55) hide show
  1. package/native-skills/media/professional-document-publishing/SKILL.md +44 -0
  2. package/native-skills/media/screenplay-and-shotlist-forge/SKILL.md +42 -0
  3. package/package.json +1 -1
  4. package/src/acp/client.js +235 -0
  5. package/src/acp/client.ts +282 -0
  6. package/src/acp/peerProfiles.js +122 -0
  7. package/src/acp/peerProfiles.ts +128 -0
  8. package/src/acp/protocol.js +54 -0
  9. package/src/acp/protocol.ts +434 -0
  10. package/src/acp/server.js +609 -0
  11. package/src/acp/server.ts +676 -0
  12. package/src/agentLoop.d.ts +43 -0
  13. package/src/agentLoop.js +142 -8
  14. package/src/commands.js +203 -0
  15. package/src/compiledSkills.js +10 -0
  16. package/src/documentEngine/antiSlopAuditor.js +156 -0
  17. package/src/documentEngine/antiSlopAuditor.ts +221 -0
  18. package/src/documentEngine/documentForge.js +443 -0
  19. package/src/documentEngine/documentForge.ts +519 -0
  20. package/src/documentEngine/screenplayFormatter.js +336 -0
  21. package/src/documentEngine/screenplayFormatter.ts +407 -0
  22. package/src/entrypoint.js +13 -0
  23. package/src/execSafe.js +10 -13
  24. package/src/execSafe.ts +25 -0
  25. package/src/modelRouter.js +56 -123
  26. package/src/modelRouter.ts +134 -0
  27. package/src/observer/advisory.js +164 -0
  28. package/src/observer/advisory.ts +171 -0
  29. package/src/observer/coldObserver.js +462 -0
  30. package/src/observer/coldObserver.ts +540 -0
  31. package/src/observer/evidence.js +435 -0
  32. package/src/observer/evidence.ts +499 -0
  33. package/src/perception.js +303 -118
  34. package/src/perception.ts +387 -0
  35. package/src/procSignal.js +15 -16
  36. package/src/procSignal.ts +31 -0
  37. package/src/promptMentions.js +170 -0
  38. package/src/promptMentions.ts +212 -0
  39. package/src/sandbox.js +1 -1
  40. package/src/textDiff.js +117 -109
  41. package/src/textDiff.ts +169 -0
  42. package/src/toolPacks.js +5 -2
  43. package/src/tools/documentForge.js +215 -0
  44. package/src/tools/documentForge.ts +265 -0
  45. package/src/tools/peerAgent.js +102 -0
  46. package/src/tools/peerAgent.ts +116 -0
  47. package/src/tools/registry.js +3 -1
  48. package/src/tui/completion.js +97 -41
  49. package/src/tui/prompt.js +17 -17
  50. package/src/uncertainty.js +93 -205
  51. package/src/uncertainty.ts +255 -0
  52. package/src/utils/paths.js +15 -18
  53. package/src/utils/paths.ts +36 -0
  54. package/src/vfeTracker.js +122 -181
  55. package/src/vfeTracker.ts +242 -0
@@ -0,0 +1,609 @@
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { AgentLoop } from '../agentLoop.js';
5
+ import { ACP_ERROR_CODES, ACP_METHODS, ACP_PROTOCOL_VERSION, isJsonRpcNotification, isJsonRpcRequest, isJsonRpcResponse } from './protocol.js';
6
+ const MODE_TO_AUTONOMY = {
7
+ autonomous: 'A',
8
+ standard: 'B',
9
+ supervised: 'C'
10
+ };
11
+ const MODES = {
12
+ currentModeId: 'standard',
13
+ availableModes: [
14
+ { id: 'autonomous', name: 'Autonomous', description: 'Runs without pausing for routine approvals' },
15
+ { id: 'standard', name: 'Standard', description: 'Asks before dangerous actions' },
16
+ { id: 'supervised', name: 'Supervised', description: 'Asks before most mutating actions' }
17
+ ]
18
+ };
19
+ const MAX_PROMPT_CHARS = 200_000;
20
+ const MAX_RAW_CHARS = 4000;
21
+ function textFromPrompt(prompt) {
22
+ const parts = [];
23
+ for (const block of prompt) {
24
+ if (block.type !== 'text')
25
+ return null;
26
+ parts.push(block.text);
27
+ }
28
+ return parts.join('\n').slice(0, MAX_PROMPT_CHARS);
29
+ }
30
+ function toolKind(name) {
31
+ if (/^(fs_read|read_file|read|fs_list|list_dir|view_file)$/.test(name))
32
+ return 'read';
33
+ if (/^(fs_write|fs_edit|MultiEdit|write_file|edit_file)$/.test(name))
34
+ return 'edit';
35
+ if (/^(fs_delete|delete_file)$/.test(name))
36
+ return 'delete';
37
+ if (/^(fs_move|move_file)$/.test(name))
38
+ return 'move';
39
+ if (/^(grep|glob|search|find|code_search)$/.test(name))
40
+ return 'search';
41
+ if (/^(sysRun|sysRunBg|shell|terminal|terminal_persistent_shell|gitOp|proc_term)$/.test(name))
42
+ return 'execute';
43
+ if (/^(webFetch|webSearch|web_fetch|web_search)$/.test(name))
44
+ return 'fetch';
45
+ if (/^(request_decision|update_plan|think)$/.test(name))
46
+ return 'think';
47
+ return 'other';
48
+ }
49
+ function bounded(value, limit = MAX_RAW_CHARS) {
50
+ let text;
51
+ try {
52
+ text = typeof value === 'string' ? value : JSON.stringify(value ?? null);
53
+ }
54
+ catch {
55
+ text = '[unserializable]';
56
+ }
57
+ return text.length > limit ? `${text.slice(0, limit)}…[truncated]` : text;
58
+ }
59
+ function toolTitle(name, args) {
60
+ const detail = args.path || args.file_path || args.command || args.cmd || args.query || args.url || args.action;
61
+ const suffix = typeof detail === 'string' && detail.trim() ? ` ${detail.trim()}` : '';
62
+ const title = `${name}${suffix}`;
63
+ return title.length > 200 ? `${title.slice(0, 200)}…` : title;
64
+ }
65
+ export class AcpServer {
66
+ input;
67
+ output;
68
+ loopFactory;
69
+ agentName;
70
+ agentVersion;
71
+ permissionTimeoutMs;
72
+ sessions = new Map();
73
+ pending = new Map();
74
+ nextClientRequestId = 1;
75
+ buffer = '';
76
+ closed = false;
77
+ workspaceRoot = null;
78
+ dataListener = null;
79
+ endListener = null;
80
+ errorListener = null;
81
+ constructor(options = {}) {
82
+ this.input = options.input ?? null;
83
+ this.output = options.output ?? null;
84
+ this.loopFactory = options.loopFactory ?? null;
85
+ this.agentName = options.agentName ?? 'Causyne Agent';
86
+ this.agentVersion = options.agentVersion ?? '0.0.0';
87
+ this.permissionTimeoutMs = options.permissionTimeoutMs ?? 10 * 60 * 1000;
88
+ }
89
+ start() {
90
+ if (!this.input || this.dataListener)
91
+ return;
92
+ this.dataListener = (chunk) => {
93
+ this.buffer += typeof chunk === 'string' ? chunk : chunk.toString('utf8');
94
+ void this.drainBuffer();
95
+ };
96
+ this.endListener = () => { void this.stop(); };
97
+ this.errorListener = () => { void this.stop(); };
98
+ this.input.on('data', this.dataListener);
99
+ this.input.on('end', this.endListener);
100
+ this.input.on('error', this.errorListener);
101
+ }
102
+ async stop() {
103
+ if (this.closed)
104
+ return;
105
+ this.closed = true;
106
+ if (this.input && this.dataListener)
107
+ this.input.off('data', this.dataListener);
108
+ if (this.input && this.endListener)
109
+ this.input.off('end', this.endListener);
110
+ if (this.input && this.errorListener)
111
+ this.input.off('error', this.errorListener);
112
+ for (const session of this.sessions.values()) {
113
+ try {
114
+ session.loop?.abort();
115
+ }
116
+ catch {
117
+ // Shutdown never blocks on a misbehaving tool.
118
+ }
119
+ }
120
+ for (const [id, pending] of this.pending) {
121
+ clearTimeout(pending.timer);
122
+ pending.reject(new Error('server stopped'));
123
+ this.pending.delete(id);
124
+ }
125
+ }
126
+ async drainBuffer() {
127
+ let index = this.buffer.indexOf('\n');
128
+ while (index >= 0) {
129
+ const line = this.buffer.slice(0, index).trim();
130
+ this.buffer = this.buffer.slice(index + 1);
131
+ if (line)
132
+ await this.handleLine(line);
133
+ index = this.buffer.indexOf('\n');
134
+ }
135
+ }
136
+ async handleLine(line) {
137
+ let parsed;
138
+ try {
139
+ parsed = JSON.parse(line);
140
+ }
141
+ catch {
142
+ this.write({ jsonrpc: '2.0', id: null, error: { code: ACP_ERROR_CODES.PARSE_ERROR, message: 'Parse error' } });
143
+ return;
144
+ }
145
+ if (isJsonRpcResponse(parsed)) {
146
+ this.resolveClientRequest(parsed);
147
+ return;
148
+ }
149
+ if (isJsonRpcRequest(parsed)) {
150
+ await this.handleRequest(parsed);
151
+ return;
152
+ }
153
+ if (isJsonRpcNotification(parsed)) {
154
+ await this.handleNotification(parsed);
155
+ return;
156
+ }
157
+ this.write({ jsonrpc: '2.0', id: null, error: { code: ACP_ERROR_CODES.INVALID_REQUEST, message: 'Invalid request' } });
158
+ }
159
+ async handleNotification(notification) {
160
+ if (notification.method === ACP_METHODS.CANCEL) {
161
+ const params = notification.params;
162
+ const session = params && typeof params.sessionId === 'string' ? this.sessions.get(params.sessionId) : undefined;
163
+ if (session) {
164
+ session.cancelled = true;
165
+ try {
166
+ session.loop?.abort();
167
+ }
168
+ catch {
169
+ // Cancellation is best effort.
170
+ }
171
+ }
172
+ }
173
+ }
174
+ async handleRequest(request) {
175
+ try {
176
+ switch (request.method) {
177
+ case ACP_METHODS.INITIALIZE:
178
+ this.writeResult(request.id, this.initialize(request.params));
179
+ return;
180
+ case ACP_METHODS.NEW_SESSION:
181
+ this.writeResult(request.id, this.newSession(request.params));
182
+ return;
183
+ case ACP_METHODS.PROMPT:
184
+ this.writeResult(request.id, await this.prompt(request.params));
185
+ return;
186
+ case ACP_METHODS.SET_MODE:
187
+ this.writeResult(request.id, this.setMode(request.params));
188
+ return;
189
+ case ACP_METHODS.LIST:
190
+ this.writeResult(request.id, this.listSessions(request.params));
191
+ return;
192
+ case ACP_METHODS.CLOSE:
193
+ this.writeResult(request.id, this.closeSession(request.params));
194
+ return;
195
+ case ACP_METHODS.LOAD:
196
+ this.writeError(request.id, ACP_ERROR_CODES.METHOD_NOT_FOUND, 'session/load is not supported');
197
+ return;
198
+ default:
199
+ this.writeError(request.id, ACP_ERROR_CODES.METHOD_NOT_FOUND, `Unknown method: ${request.method}`);
200
+ }
201
+ }
202
+ catch (error) {
203
+ const code = error.acpCode ?? ACP_ERROR_CODES.INTERNAL_ERROR;
204
+ this.writeError(request.id, code, error instanceof Error ? error.message : String(error));
205
+ }
206
+ }
207
+ initialize(params) {
208
+ if (!params || typeof params.protocolVersion !== 'number') {
209
+ throw this.paramsError('initialize requires protocolVersion');
210
+ }
211
+ const capabilities = {
212
+ loadSession: false,
213
+ promptCapabilities: { image: false, audio: false, embeddedContext: false },
214
+ mcpCapabilities: { http: false, sse: false },
215
+ sessionCapabilities: { list: {}, close: {} }
216
+ };
217
+ return {
218
+ protocolVersion: ACP_PROTOCOL_VERSION,
219
+ agentCapabilities: capabilities,
220
+ agentInfo: { name: this.agentName, version: this.agentVersion }
221
+ };
222
+ }
223
+ paramsError(message) {
224
+ const error = new Error(message);
225
+ error.acpCode = ACP_ERROR_CODES.INVALID_PARAMS;
226
+ return error;
227
+ }
228
+ newSession(params) {
229
+ if (!params || typeof params.cwd !== 'string' || !params.cwd.trim()) {
230
+ throw this.paramsError('session/new requires cwd');
231
+ }
232
+ if (Array.isArray(params.mcpServers) && params.mcpServers.length > 0) {
233
+ throw this.paramsError('MCP servers are not supported by this agent');
234
+ }
235
+ if (Array.isArray(params.additionalDirectories) && params.additionalDirectories.length > 0) {
236
+ throw this.paramsError('additionalDirectories is not supported by this agent');
237
+ }
238
+ let cwd;
239
+ try {
240
+ cwd = fs.realpathSync(path.resolve(params.cwd));
241
+ if (!fs.statSync(cwd).isDirectory())
242
+ throw new Error('not a directory');
243
+ }
244
+ catch {
245
+ throw this.paramsError(`cwd is not an accessible directory: ${params.cwd}`);
246
+ }
247
+ if (this.workspaceRoot === null) {
248
+ this.workspaceRoot = cwd;
249
+ process.chdir(cwd);
250
+ }
251
+ else if (this.workspaceRoot !== cwd) {
252
+ throw this.paramsError(`server is bound to ${this.workspaceRoot}; one workspace per server`);
253
+ }
254
+ const sessionId = crypto.randomUUID();
255
+ this.sessions.set(sessionId, {
256
+ id: sessionId,
257
+ cwd,
258
+ modeId: MODES.currentModeId,
259
+ loop: null,
260
+ busy: false,
261
+ cancelled: false,
262
+ updatedAt: Date.now(),
263
+ queue: Promise.resolve()
264
+ });
265
+ return {
266
+ sessionId,
267
+ modes: {
268
+ currentModeId: MODES.currentModeId,
269
+ availableModes: MODES.availableModes.map((mode) => ({ ...mode }))
270
+ }
271
+ };
272
+ }
273
+ requireSession(sessionId) {
274
+ if (typeof sessionId !== 'string' || !sessionId)
275
+ throw this.paramsError('sessionId is required');
276
+ const session = this.sessions.get(sessionId);
277
+ if (!session) {
278
+ const error = new Error(`Unknown session: ${sessionId}`);
279
+ error.acpCode = ACP_ERROR_CODES.RESOURCE_NOT_FOUND;
280
+ throw error;
281
+ }
282
+ return session;
283
+ }
284
+ async prompt(params) {
285
+ if (!params || !Array.isArray(params.prompt)) {
286
+ throw this.paramsError('session/prompt requires a prompt array');
287
+ }
288
+ const session = this.requireSession(params.sessionId);
289
+ const text = textFromPrompt(params.prompt);
290
+ if (text === null || !text.trim()) {
291
+ return { stopReason: 'refusal' };
292
+ }
293
+ session.queue = session.queue.then(() => this.runTurn(session, text), () => this.runTurn(session, text));
294
+ const stopReason = await session.queue;
295
+ return { stopReason };
296
+ }
297
+ async runTurn(session, text) {
298
+ session.updatedAt = Date.now();
299
+ session.cancelled = false;
300
+ session.busy = true;
301
+ const taskId = `acp-${crypto.randomUUID()}`;
302
+ const sink = {
303
+ readyState: 1,
304
+ send: (payload) => this.onLoopPayload(session, payload)
305
+ };
306
+ const loop = this.loopFactory
307
+ ? this.loopFactory({ sessionId: session.id, taskId, userPrompt: text, cwd: session.cwd, sink })
308
+ : new AgentLoop(sink, taskId, text);
309
+ const autonomy = MODE_TO_AUTONOMY[session.modeId];
310
+ if (autonomy) {
311
+ try {
312
+ loop.setAutonomyMode(autonomy);
313
+ }
314
+ catch {
315
+ // A loop without autonomy control simply keeps its configured mode.
316
+ }
317
+ }
318
+ loop.waitForDecision = async (request) => this.requestDecision(session, loop, request);
319
+ session.loop = loop;
320
+ let stopReason = 'end_turn';
321
+ try {
322
+ await loop.run();
323
+ }
324
+ catch {
325
+ stopReason = 'refusal';
326
+ }
327
+ finally {
328
+ session.loop = null;
329
+ session.busy = false;
330
+ if (session.cancelled)
331
+ stopReason = 'cancelled';
332
+ }
333
+ return stopReason;
334
+ }
335
+ requestDecision(session, loop, request) {
336
+ if (request === null || typeof request !== 'object' || Array.isArray(request))
337
+ return Promise.resolve(null);
338
+ const record = request;
339
+ const options = Array.isArray(record.options) ? record.options : [];
340
+ const permissionOptions = [];
341
+ const labels = new Map();
342
+ for (const option of options) {
343
+ if (option === null || typeof option !== 'object' || Array.isArray(option))
344
+ continue;
345
+ const item = option;
346
+ const id = typeof item.id === 'string' ? item.id : '';
347
+ const label = typeof item.label === 'string' ? item.label : id;
348
+ if (!id)
349
+ continue;
350
+ labels.set(id, label);
351
+ permissionOptions.push({ optionId: id, name: label, kind: 'allow_once' });
352
+ }
353
+ if (permissionOptions.length === 0)
354
+ return Promise.resolve(null);
355
+ const toolCall = {
356
+ toolCallId: typeof record.id === 'string' ? record.id : crypto.randomUUID(),
357
+ title: typeof record.question === 'string' ? record.question.slice(0, 300) : 'Decision required',
358
+ kind: 'other',
359
+ status: 'pending'
360
+ };
361
+ return this.sendClientRequest(session, ACP_METHODS.REQUEST_PERMISSION, {
362
+ sessionId: session.id,
363
+ toolCall,
364
+ options: permissionOptions
365
+ }).then((response) => {
366
+ if (!response || response.outcome.outcome !== 'selected')
367
+ return null;
368
+ const optionId = response.outcome.optionId;
369
+ const label = labels.get(optionId);
370
+ if (!label)
371
+ return null;
372
+ return { optionId, label };
373
+ }).catch(() => null);
374
+ }
375
+ onLoopPayload(session, payload) {
376
+ let event;
377
+ try {
378
+ event = JSON.parse(payload);
379
+ }
380
+ catch {
381
+ return;
382
+ }
383
+ this.onLoopEvent(session, event);
384
+ }
385
+ onLoopEvent(session, event) {
386
+ switch (event.type) {
387
+ case 'agent_message': {
388
+ this.notify(session.id, { sessionUpdate: 'agent_message_chunk', content: { type: 'text', text: String(event.content ?? '') } });
389
+ return;
390
+ }
391
+ case 'agent_reasoning': {
392
+ const text = String(event.content ?? '');
393
+ if (text)
394
+ this.notify(session.id, { sessionUpdate: 'agent_thought_chunk', content: { type: 'text', text } });
395
+ return;
396
+ }
397
+ case 'status':
398
+ case 'notice': {
399
+ const text = String(event.message ?? '');
400
+ if (text)
401
+ this.notify(session.id, { sessionUpdate: 'agent_thought_chunk', content: { type: 'text', text } });
402
+ return;
403
+ }
404
+ case 'warning':
405
+ case 'budget_warning':
406
+ case 'error': {
407
+ const text = String(event.message ?? 'Task error');
408
+ this.notify(session.id, { sessionUpdate: 'agent_message_chunk', content: { type: 'text', text } });
409
+ return;
410
+ }
411
+ case 'step_start': {
412
+ const tool = String(event.tool ?? 'tool');
413
+ const args = event.args !== null && typeof event.args === 'object' && !Array.isArray(event.args)
414
+ ? event.args
415
+ : {};
416
+ const toolCallId = typeof event.toolCallId === 'string' ? event.toolCallId : crypto.randomUUID();
417
+ this.notify(session.id, {
418
+ sessionUpdate: 'tool_call',
419
+ toolCallId,
420
+ title: toolTitle(tool, args),
421
+ kind: toolKind(tool),
422
+ status: 'in_progress',
423
+ rawInput: args
424
+ });
425
+ return;
426
+ }
427
+ case 'step_complete': {
428
+ const toolCallId = typeof event.toolCallId === 'string' ? event.toolCallId : null;
429
+ if (!toolCallId)
430
+ return;
431
+ const result = event.result;
432
+ const status = result && result.success === false ? 'failed' : 'completed';
433
+ this.notify(session.id, {
434
+ sessionUpdate: 'tool_call_update',
435
+ toolCallId,
436
+ status,
437
+ content: [{ type: 'content', content: { type: 'text', text: bounded(event.result) } }]
438
+ });
439
+ return;
440
+ }
441
+ case 'step_error': {
442
+ const toolCallId = typeof event.toolCallId === 'string' ? event.toolCallId : null;
443
+ if (!toolCallId)
444
+ return;
445
+ this.notify(session.id, {
446
+ sessionUpdate: 'tool_call_update',
447
+ toolCallId,
448
+ status: 'failed',
449
+ content: [{ type: 'content', content: { type: 'text', text: bounded(event.error ?? 'tool failed') } }]
450
+ });
451
+ return;
452
+ }
453
+ case 'agent_plan': {
454
+ const plan = Array.isArray(event.plan) ? event.plan : [];
455
+ this.notify(session.id, {
456
+ sessionUpdate: 'plan',
457
+ entries: plan.map((entry) => {
458
+ const item = entry !== null && typeof entry === 'object' && !Array.isArray(entry)
459
+ ? entry
460
+ : {};
461
+ const status = item.status === 'completed' || item.status === 'in_progress' ? item.status : 'pending';
462
+ return {
463
+ content: String(item.text ?? item.content ?? 'step'),
464
+ priority: 'medium',
465
+ status
466
+ };
467
+ })
468
+ });
469
+ return;
470
+ }
471
+ case 'approval_request': {
472
+ const toolCallId = String(event.toolCallId ?? '');
473
+ if (!toolCallId)
474
+ return;
475
+ const args = event.args !== null && typeof event.args === 'object' && !Array.isArray(event.args)
476
+ ? event.args
477
+ : {};
478
+ const tool = String(event.tool ?? 'tool');
479
+ const toolCall = {
480
+ toolCallId,
481
+ title: toolTitle(tool, args),
482
+ kind: toolKind(tool),
483
+ status: 'pending',
484
+ rawInput: args
485
+ };
486
+ const options = [
487
+ { optionId: 'allow_once', name: 'Allow this action', kind: 'allow_once' },
488
+ { optionId: 'reject_once', name: 'Reject this action', kind: 'reject_once' }
489
+ ];
490
+ void this.sendClientRequest(session, ACP_METHODS.REQUEST_PERMISSION, {
491
+ sessionId: session.id,
492
+ toolCall,
493
+ options
494
+ }).then((response) => {
495
+ const approved = response?.outcome.outcome === 'selected' && response.outcome.optionId === 'allow_once';
496
+ session.loop?.handleApprovalResponse(toolCallId, approved);
497
+ }).catch(() => {
498
+ session.loop?.handleApprovalResponse(toolCallId, false);
499
+ });
500
+ return;
501
+ }
502
+ default:
503
+ return;
504
+ }
505
+ }
506
+ notify(sessionId, update) {
507
+ const notification = {
508
+ jsonrpc: '2.0',
509
+ method: ACP_METHODS.UPDATE,
510
+ params: { sessionId, update }
511
+ };
512
+ this.write(notification);
513
+ }
514
+ setMode(params) {
515
+ if (!params || typeof params.modeId !== 'string')
516
+ throw this.paramsError('session/set_mode requires modeId');
517
+ const session = this.requireSession(params.sessionId);
518
+ if (!MODES.availableModes.some((mode) => mode.id === params.modeId)) {
519
+ throw this.paramsError(`Unknown mode: ${params.modeId}`);
520
+ }
521
+ session.modeId = params.modeId;
522
+ session.updatedAt = Date.now();
523
+ const autonomy = MODE_TO_AUTONOMY[session.modeId];
524
+ if (autonomy && session.loop) {
525
+ try {
526
+ session.loop.setAutonomyMode(autonomy);
527
+ }
528
+ catch {
529
+ // The running loop keeps its mode; the next turn picks the new one.
530
+ }
531
+ }
532
+ this.notify(session.id, { sessionUpdate: 'current_mode_update', currentModeId: session.modeId });
533
+ return {};
534
+ }
535
+ listSessions(params) {
536
+ const filter = params && typeof params.cwd === 'string' ? path.resolve(params.cwd) : null;
537
+ const sessions = [];
538
+ for (const session of this.sessions.values()) {
539
+ if (filter && session.cwd !== filter)
540
+ continue;
541
+ sessions.push({
542
+ sessionId: session.id,
543
+ cwd: session.cwd,
544
+ updatedAt: new Date(session.updatedAt).toISOString()
545
+ });
546
+ }
547
+ sessions.sort((a, b) => (b.updatedAt ?? '').localeCompare(a.updatedAt ?? ''));
548
+ return { sessions };
549
+ }
550
+ closeSession(params) {
551
+ const session = this.requireSession(params?.sessionId);
552
+ session.cancelled = true;
553
+ try {
554
+ session.loop?.abort();
555
+ }
556
+ catch {
557
+ // Closing never blocks on cancellation.
558
+ }
559
+ this.sessions.delete(session.id);
560
+ return {};
561
+ }
562
+ sendClientRequest(session, method, params) {
563
+ const id = `server-${this.nextClientRequestId++}`;
564
+ return new Promise((resolve, reject) => {
565
+ const timer = setTimeout(() => {
566
+ this.pending.delete(id);
567
+ reject(new Error(`${method} timed out`));
568
+ }, this.permissionTimeoutMs);
569
+ this.pending.set(id, {
570
+ resolve: (value) => resolve(value),
571
+ reject,
572
+ timer
573
+ });
574
+ this.write({ jsonrpc: '2.0', id, method, params });
575
+ });
576
+ }
577
+ resolveClientRequest(response) {
578
+ if (response.id === null || response.id === undefined)
579
+ return;
580
+ const pending = this.pending.get(response.id);
581
+ if (!pending)
582
+ return;
583
+ clearTimeout(pending.timer);
584
+ this.pending.delete(response.id);
585
+ if (response.error) {
586
+ pending.reject(new Error(response.error.message));
587
+ return;
588
+ }
589
+ pending.resolve(response.result);
590
+ }
591
+ writeResult(id, result) {
592
+ this.write({ jsonrpc: '2.0', id, result });
593
+ }
594
+ writeError(id, code, message) {
595
+ const error = { code, message };
596
+ this.write({ jsonrpc: '2.0', id, error });
597
+ }
598
+ write(message) {
599
+ if (!this.output || this.closed)
600
+ return;
601
+ try {
602
+ this.output.write(`${JSON.stringify(message)}\n`);
603
+ }
604
+ catch {
605
+ // A closed pipe must not crash the agent process.
606
+ }
607
+ }
608
+ }
609
+ export { MODES as ACP_SESSION_MODES };