@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.
- package/native-skills/media/professional-document-publishing/SKILL.md +44 -0
- package/native-skills/media/screenplay-and-shotlist-forge/SKILL.md +42 -0
- package/package.json +1 -1
- package/src/acp/client.js +235 -0
- package/src/acp/client.ts +282 -0
- package/src/acp/peerProfiles.js +122 -0
- package/src/acp/peerProfiles.ts +128 -0
- package/src/acp/protocol.js +54 -0
- package/src/acp/protocol.ts +434 -0
- package/src/acp/server.js +609 -0
- package/src/acp/server.ts +676 -0
- package/src/agentLoop.d.ts +43 -0
- package/src/agentLoop.js +142 -8
- package/src/commands.js +203 -0
- package/src/compiledSkills.js +10 -0
- package/src/documentEngine/antiSlopAuditor.js +156 -0
- package/src/documentEngine/antiSlopAuditor.ts +221 -0
- package/src/documentEngine/documentForge.js +443 -0
- package/src/documentEngine/documentForge.ts +519 -0
- package/src/documentEngine/screenplayFormatter.js +336 -0
- package/src/documentEngine/screenplayFormatter.ts +407 -0
- package/src/entrypoint.js +13 -0
- package/src/execSafe.js +10 -13
- package/src/execSafe.ts +25 -0
- package/src/modelRouter.js +56 -123
- package/src/modelRouter.ts +134 -0
- package/src/observer/advisory.js +164 -0
- package/src/observer/advisory.ts +171 -0
- package/src/observer/coldObserver.js +462 -0
- package/src/observer/coldObserver.ts +540 -0
- package/src/observer/evidence.js +435 -0
- package/src/observer/evidence.ts +499 -0
- package/src/perception.js +303 -118
- package/src/perception.ts +387 -0
- package/src/procSignal.js +15 -16
- package/src/procSignal.ts +31 -0
- package/src/promptMentions.js +170 -0
- package/src/promptMentions.ts +212 -0
- package/src/sandbox.js +1 -1
- package/src/textDiff.js +117 -109
- package/src/textDiff.ts +169 -0
- package/src/toolPacks.js +5 -2
- package/src/tools/documentForge.js +215 -0
- package/src/tools/documentForge.ts +265 -0
- package/src/tools/peerAgent.js +102 -0
- package/src/tools/peerAgent.ts +116 -0
- package/src/tools/registry.js +3 -1
- package/src/tui/completion.js +97 -41
- package/src/tui/prompt.js +17 -17
- package/src/uncertainty.js +93 -205
- package/src/uncertainty.ts +255 -0
- package/src/utils/paths.js +15 -18
- package/src/utils/paths.ts +36 -0
- package/src/vfeTracker.js +122 -181
- 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 };
|