wave-code 0.9.7 → 0.10.1
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/acp/agent.d.ts +28 -0
- package/dist/acp/agent.d.ts.map +1 -0
- package/dist/acp/agent.js +603 -0
- package/dist/acp/index.d.ts +2 -0
- package/dist/acp/index.d.ts.map +1 -0
- package/dist/acp/index.js +22 -0
- package/dist/acp-cli.d.ts +2 -0
- package/dist/acp-cli.d.ts.map +1 -0
- package/dist/acp-cli.js +4 -0
- package/dist/components/ChatInterface.js +1 -1
- package/dist/components/DiffDisplay.d.ts.map +1 -1
- package/dist/components/DiffDisplay.js +33 -89
- package/dist/contexts/useChat.d.ts.map +1 -1
- package/dist/contexts/useChat.js +0 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -0
- package/package.json +4 -2
- package/src/acp/agent.ts +740 -0
- package/src/acp/index.ts +28 -0
- package/src/acp-cli.ts +5 -0
- package/src/components/ChatInterface.tsx +1 -1
- package/src/components/DiffDisplay.tsx +62 -134
- package/src/contexts/useChat.tsx +0 -3
- package/src/index.ts +11 -0
package/src/acp/agent.ts
ADDED
|
@@ -0,0 +1,740 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Agent as WaveAgent,
|
|
3
|
+
AgentOptions,
|
|
4
|
+
PermissionDecision,
|
|
5
|
+
ToolPermissionContext,
|
|
6
|
+
AgentToolBlockUpdateParams,
|
|
7
|
+
Task,
|
|
8
|
+
listSessions as listWaveSessions,
|
|
9
|
+
deleteSession as deleteWaveSession,
|
|
10
|
+
} from "wave-agent-sdk";
|
|
11
|
+
import * as fs from "node:fs/promises";
|
|
12
|
+
import * as path from "node:path";
|
|
13
|
+
import { logger } from "../utils/logger.js";
|
|
14
|
+
import {
|
|
15
|
+
type Agent as AcpAgent,
|
|
16
|
+
type AgentSideConnection,
|
|
17
|
+
type InitializeResponse,
|
|
18
|
+
type NewSessionRequest,
|
|
19
|
+
type NewSessionResponse,
|
|
20
|
+
type LoadSessionRequest,
|
|
21
|
+
type LoadSessionResponse,
|
|
22
|
+
type ListSessionsRequest,
|
|
23
|
+
type ListSessionsResponse,
|
|
24
|
+
type PromptRequest,
|
|
25
|
+
type PromptResponse,
|
|
26
|
+
type CancelNotification,
|
|
27
|
+
type AuthenticateResponse,
|
|
28
|
+
type SessionId as AcpSessionId,
|
|
29
|
+
type ToolCallStatus,
|
|
30
|
+
type StopReason,
|
|
31
|
+
type PermissionOption,
|
|
32
|
+
type SessionInfo,
|
|
33
|
+
type ToolCallContent,
|
|
34
|
+
type ToolCallLocation,
|
|
35
|
+
type ToolKind,
|
|
36
|
+
type SessionConfigOption,
|
|
37
|
+
type SetSessionModeRequest,
|
|
38
|
+
type SetSessionConfigOptionRequest,
|
|
39
|
+
type SetSessionConfigOptionResponse,
|
|
40
|
+
AGENT_METHODS,
|
|
41
|
+
} from "@agentclientprotocol/sdk";
|
|
42
|
+
|
|
43
|
+
export class WaveAcpAgent implements AcpAgent {
|
|
44
|
+
private agents: Map<string, WaveAgent> = new Map();
|
|
45
|
+
private connection: AgentSideConnection;
|
|
46
|
+
|
|
47
|
+
constructor(connection: AgentSideConnection) {
|
|
48
|
+
this.connection = connection;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private getSessionModeState(agent: WaveAgent) {
|
|
52
|
+
return {
|
|
53
|
+
currentModeId: agent.getPermissionMode(),
|
|
54
|
+
availableModes: [
|
|
55
|
+
{
|
|
56
|
+
id: "default",
|
|
57
|
+
name: "Default",
|
|
58
|
+
description: "Ask for permission for restricted tools",
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
id: "acceptEdits",
|
|
62
|
+
name: "Accept Edits",
|
|
63
|
+
description: "Automatically accept file edits",
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: "plan",
|
|
67
|
+
name: "Plan",
|
|
68
|
+
description: "Plan mode for complex tasks",
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
id: "bypassPermissions",
|
|
72
|
+
name: "Bypass Permissions",
|
|
73
|
+
description: "Automatically accept all tool calls",
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private getSessionConfigOptions(agent: WaveAgent): SessionConfigOption[] {
|
|
80
|
+
return [
|
|
81
|
+
{
|
|
82
|
+
id: "permission_mode",
|
|
83
|
+
name: "Permission Mode",
|
|
84
|
+
type: "select",
|
|
85
|
+
category: "mode",
|
|
86
|
+
currentValue: agent.getPermissionMode(),
|
|
87
|
+
options: [
|
|
88
|
+
{ value: "default", name: "Default" },
|
|
89
|
+
{ value: "acceptEdits", name: "Accept Edits" },
|
|
90
|
+
{ value: "plan", name: "Plan" },
|
|
91
|
+
{ value: "bypassPermissions", name: "Bypass Permissions" },
|
|
92
|
+
],
|
|
93
|
+
},
|
|
94
|
+
];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private async cleanupAllAgents() {
|
|
98
|
+
logger.info("Cleaning up all active agents due to connection closure");
|
|
99
|
+
const destroyPromises = Array.from(this.agents.values()).map((agent) =>
|
|
100
|
+
agent.destroy(),
|
|
101
|
+
);
|
|
102
|
+
await Promise.all(destroyPromises);
|
|
103
|
+
this.agents.clear();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async initialize(): Promise<InitializeResponse> {
|
|
107
|
+
logger.info("Initializing WaveAcpAgent");
|
|
108
|
+
// Setup cleanup on connection closure
|
|
109
|
+
this.connection.closed.then(() => this.cleanupAllAgents());
|
|
110
|
+
return {
|
|
111
|
+
protocolVersion: 1,
|
|
112
|
+
agentInfo: {
|
|
113
|
+
name: "wave-agent",
|
|
114
|
+
version: "0.1.0",
|
|
115
|
+
},
|
|
116
|
+
agentCapabilities: {
|
|
117
|
+
loadSession: true,
|
|
118
|
+
sessionCapabilities: {
|
|
119
|
+
list: {},
|
|
120
|
+
close: {},
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async authenticate(): Promise<AuthenticateResponse | void> {
|
|
127
|
+
// No authentication required for now
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private async createAgent(
|
|
131
|
+
sessionId: string | undefined,
|
|
132
|
+
cwd: string,
|
|
133
|
+
): Promise<WaveAgent> {
|
|
134
|
+
const callbacks: AgentOptions["callbacks"] = {};
|
|
135
|
+
const agentRef: { instance?: WaveAgent } = {};
|
|
136
|
+
|
|
137
|
+
const agent = await WaveAgent.create({
|
|
138
|
+
workdir: cwd,
|
|
139
|
+
restoreSessionId: sessionId,
|
|
140
|
+
canUseTool: (context) => {
|
|
141
|
+
if (!agentRef.instance) {
|
|
142
|
+
throw new Error("Agent instance not yet initialized");
|
|
143
|
+
}
|
|
144
|
+
return this.handlePermissionRequest(
|
|
145
|
+
agentRef.instance.sessionId,
|
|
146
|
+
context,
|
|
147
|
+
);
|
|
148
|
+
},
|
|
149
|
+
callbacks: {
|
|
150
|
+
onAssistantContentUpdated: (chunk: string) =>
|
|
151
|
+
callbacks.onAssistantContentUpdated?.(chunk, ""),
|
|
152
|
+
onAssistantReasoningUpdated: (chunk: string) =>
|
|
153
|
+
callbacks.onAssistantReasoningUpdated?.(chunk, ""),
|
|
154
|
+
onToolBlockUpdated: (params: unknown) => {
|
|
155
|
+
const cb = callbacks.onToolBlockUpdated as
|
|
156
|
+
| ((params: unknown) => void)
|
|
157
|
+
| undefined;
|
|
158
|
+
cb?.(params);
|
|
159
|
+
},
|
|
160
|
+
onTasksChange: (tasks) => callbacks.onTasksChange?.(tasks as Task[]),
|
|
161
|
+
onPermissionModeChange: (mode) =>
|
|
162
|
+
callbacks.onPermissionModeChange?.(mode),
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
agentRef.instance = agent;
|
|
167
|
+
const actualSessionId = agent.sessionId;
|
|
168
|
+
this.agents.set(actualSessionId, agent);
|
|
169
|
+
|
|
170
|
+
// Update the callbacks object with the correct sessionId
|
|
171
|
+
Object.assign(callbacks, this.createCallbacks(actualSessionId));
|
|
172
|
+
|
|
173
|
+
return agent;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async newSession(params: NewSessionRequest): Promise<NewSessionResponse> {
|
|
177
|
+
const { cwd } = params;
|
|
178
|
+
logger.info(`Creating new session in ${cwd}`);
|
|
179
|
+
const agent = await this.createAgent(undefined, cwd);
|
|
180
|
+
logger.info(`New session created with ID: ${agent.sessionId}`);
|
|
181
|
+
|
|
182
|
+
// Send initial available commands after agent creation
|
|
183
|
+
setImmediate(() => {
|
|
184
|
+
this.connection.sessionUpdate({
|
|
185
|
+
sessionId: agent.sessionId as AcpSessionId,
|
|
186
|
+
update: {
|
|
187
|
+
sessionUpdate: "available_commands_update",
|
|
188
|
+
availableCommands: agent.getSlashCommands().map((cmd) => ({
|
|
189
|
+
name: cmd.name,
|
|
190
|
+
description: cmd.description,
|
|
191
|
+
input: {
|
|
192
|
+
hint: "Enter arguments...",
|
|
193
|
+
},
|
|
194
|
+
})),
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
sessionId: agent.sessionId as AcpSessionId,
|
|
201
|
+
modes: this.getSessionModeState(agent),
|
|
202
|
+
configOptions: this.getSessionConfigOptions(agent),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async loadSession(params: LoadSessionRequest): Promise<LoadSessionResponse> {
|
|
207
|
+
const { sessionId, cwd } = params;
|
|
208
|
+
logger.info(`Loading session: ${sessionId} in ${cwd}`);
|
|
209
|
+
const agent = await this.createAgent(sessionId, cwd);
|
|
210
|
+
|
|
211
|
+
// Send initial available commands after agent creation
|
|
212
|
+
setImmediate(() => {
|
|
213
|
+
this.connection.sessionUpdate({
|
|
214
|
+
sessionId: agent.sessionId as AcpSessionId,
|
|
215
|
+
update: {
|
|
216
|
+
sessionUpdate: "available_commands_update",
|
|
217
|
+
availableCommands: agent.getSlashCommands().map((cmd) => ({
|
|
218
|
+
name: cmd.name,
|
|
219
|
+
description: cmd.description,
|
|
220
|
+
input: {
|
|
221
|
+
hint: "Enter arguments...",
|
|
222
|
+
},
|
|
223
|
+
})),
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
modes: this.getSessionModeState(agent),
|
|
230
|
+
configOptions: this.getSessionConfigOptions(agent),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async listSessions(
|
|
235
|
+
params: ListSessionsRequest,
|
|
236
|
+
): Promise<ListSessionsResponse> {
|
|
237
|
+
const { cwd } = params;
|
|
238
|
+
logger.info(`listSessions called with params: ${JSON.stringify(params)}`);
|
|
239
|
+
if (!cwd) {
|
|
240
|
+
logger.warn("listSessions called without cwd, returning empty list");
|
|
241
|
+
return { sessions: [] };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
logger.info(`Listing sessions for ${cwd}`);
|
|
245
|
+
const waveSessions = await listWaveSessions(cwd);
|
|
246
|
+
logger.info(`Found ${waveSessions.length} sessions for ${cwd}`);
|
|
247
|
+
const sessions: SessionInfo[] = waveSessions.map((meta) => ({
|
|
248
|
+
sessionId: meta.id as AcpSessionId,
|
|
249
|
+
cwd: meta.workdir,
|
|
250
|
+
updatedAt: meta.lastActiveAt.toISOString(),
|
|
251
|
+
}));
|
|
252
|
+
return { sessions };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async unstable_closeSession(
|
|
256
|
+
params: Record<string, unknown>,
|
|
257
|
+
): Promise<Record<string, unknown>> {
|
|
258
|
+
const sessionId = params.sessionId as string;
|
|
259
|
+
logger.info(`Stopping session ${sessionId}`);
|
|
260
|
+
const agent = this.agents.get(sessionId);
|
|
261
|
+
if (agent) {
|
|
262
|
+
const workdir = agent.workingDirectory;
|
|
263
|
+
await agent.destroy();
|
|
264
|
+
this.agents.delete(sessionId);
|
|
265
|
+
// Delete the session file so it doesn't show up in listSessions
|
|
266
|
+
await deleteWaveSession(sessionId, workdir);
|
|
267
|
+
}
|
|
268
|
+
return {};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async extMethod(
|
|
272
|
+
method: string,
|
|
273
|
+
params: Record<string, unknown>,
|
|
274
|
+
): Promise<Record<string, unknown>> {
|
|
275
|
+
if (method === AGENT_METHODS.session_close) {
|
|
276
|
+
return this.unstable_closeSession(params);
|
|
277
|
+
}
|
|
278
|
+
throw new Error(`Method ${method} not implemented`);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async setSessionMode(params: SetSessionModeRequest): Promise<void> {
|
|
282
|
+
const { sessionId, modeId } = params;
|
|
283
|
+
const agent = this.agents.get(sessionId);
|
|
284
|
+
if (!agent) throw new Error(`Session ${sessionId} not found`);
|
|
285
|
+
agent.setPermissionMode(
|
|
286
|
+
modeId as "default" | "acceptEdits" | "plan" | "bypassPermissions",
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async setSessionConfigOption(
|
|
291
|
+
params: SetSessionConfigOptionRequest,
|
|
292
|
+
): Promise<SetSessionConfigOptionResponse> {
|
|
293
|
+
const { sessionId, configId, value } = params;
|
|
294
|
+
const agent = this.agents.get(sessionId);
|
|
295
|
+
if (!agent) throw new Error(`Session ${sessionId} not found`);
|
|
296
|
+
|
|
297
|
+
if (configId === "permission_mode") {
|
|
298
|
+
agent.setPermissionMode(
|
|
299
|
+
value as "default" | "acceptEdits" | "plan" | "bypassPermissions",
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
configOptions: this.getSessionConfigOptions(agent),
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async prompt(params: PromptRequest): Promise<PromptResponse> {
|
|
309
|
+
const { sessionId, prompt } = params;
|
|
310
|
+
logger.info(`Received prompt for session ${sessionId}`);
|
|
311
|
+
const agent = this.agents.get(sessionId);
|
|
312
|
+
if (!agent) {
|
|
313
|
+
logger.error(`Session ${sessionId} not found`);
|
|
314
|
+
throw new Error(`Session ${sessionId} not found`);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Map ACP prompt to Wave Agent sendMessage
|
|
318
|
+
const textContent = prompt
|
|
319
|
+
.filter((block) => block.type === "text")
|
|
320
|
+
.map((block) => (block as { text: string }).text)
|
|
321
|
+
.join("\n");
|
|
322
|
+
|
|
323
|
+
const images = prompt
|
|
324
|
+
.filter((block) => block.type === "image")
|
|
325
|
+
.map((block) => {
|
|
326
|
+
const img = block as { data: string; mimeType: string };
|
|
327
|
+
return {
|
|
328
|
+
path: `data:${img.mimeType};base64,${img.data}`,
|
|
329
|
+
mimeType: img.mimeType,
|
|
330
|
+
};
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
try {
|
|
334
|
+
logger.info(
|
|
335
|
+
`Sending message to agent: ${textContent.substring(0, 50)}...`,
|
|
336
|
+
);
|
|
337
|
+
await agent.sendMessage(
|
|
338
|
+
textContent,
|
|
339
|
+
images.length > 0 ? images : undefined,
|
|
340
|
+
);
|
|
341
|
+
logger.info(`Message sent successfully for session ${sessionId}`);
|
|
342
|
+
return {
|
|
343
|
+
stopReason: "end_turn" as StopReason,
|
|
344
|
+
};
|
|
345
|
+
} catch (error) {
|
|
346
|
+
if (error instanceof Error && error.message.includes("abort")) {
|
|
347
|
+
logger.info(`Message aborted for session ${sessionId}`);
|
|
348
|
+
return {
|
|
349
|
+
stopReason: "cancelled" as StopReason,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
logger.error(`Error sending message for session ${sessionId}:`, error);
|
|
353
|
+
throw error;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
async cancel(params: CancelNotification): Promise<void> {
|
|
358
|
+
const { sessionId } = params;
|
|
359
|
+
logger.info(`Cancelling message for session ${sessionId}`);
|
|
360
|
+
const agent = this.agents.get(sessionId);
|
|
361
|
+
if (agent) {
|
|
362
|
+
agent.abortMessage();
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
private async handlePermissionRequest(
|
|
367
|
+
sessionId: string,
|
|
368
|
+
context: ToolPermissionContext,
|
|
369
|
+
): Promise<PermissionDecision> {
|
|
370
|
+
logger.info(
|
|
371
|
+
`Handling permission request for ${context.toolName} in session ${sessionId}`,
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
const agent = this.agents.get(sessionId);
|
|
375
|
+
const workdir = agent?.workingDirectory || process.cwd();
|
|
376
|
+
|
|
377
|
+
const toolCallId =
|
|
378
|
+
context.toolCallId ||
|
|
379
|
+
"perm-" + Math.random().toString(36).substring(2, 9);
|
|
380
|
+
|
|
381
|
+
const options: PermissionOption[] = [
|
|
382
|
+
{
|
|
383
|
+
optionId: "allow_once",
|
|
384
|
+
name: "Allow Once",
|
|
385
|
+
kind: "allow_once",
|
|
386
|
+
},
|
|
387
|
+
{
|
|
388
|
+
optionId: "allow_always",
|
|
389
|
+
name: "Allow Always",
|
|
390
|
+
kind: "allow_always",
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
optionId: "reject_once",
|
|
394
|
+
name: "Reject Once",
|
|
395
|
+
kind: "reject_once",
|
|
396
|
+
},
|
|
397
|
+
{
|
|
398
|
+
optionId: "reject_always",
|
|
399
|
+
name: "Reject Always",
|
|
400
|
+
kind: "reject_always",
|
|
401
|
+
},
|
|
402
|
+
];
|
|
403
|
+
|
|
404
|
+
const content = context.toolName
|
|
405
|
+
? await this.getToolContentAsync(
|
|
406
|
+
context.toolName,
|
|
407
|
+
context.toolInput,
|
|
408
|
+
workdir,
|
|
409
|
+
)
|
|
410
|
+
: undefined;
|
|
411
|
+
const locations = context.toolName
|
|
412
|
+
? this.getToolLocations(context.toolName, context.toolInput)
|
|
413
|
+
: undefined;
|
|
414
|
+
const kind = context.toolName
|
|
415
|
+
? this.getToolKind(context.toolName)
|
|
416
|
+
: undefined;
|
|
417
|
+
|
|
418
|
+
try {
|
|
419
|
+
const response = await this.connection.requestPermission({
|
|
420
|
+
sessionId: sessionId as AcpSessionId,
|
|
421
|
+
toolCall: {
|
|
422
|
+
toolCallId,
|
|
423
|
+
title: `Permission for ${context.toolName}`,
|
|
424
|
+
status: "pending",
|
|
425
|
+
rawInput: context.toolInput,
|
|
426
|
+
content,
|
|
427
|
+
locations,
|
|
428
|
+
kind,
|
|
429
|
+
},
|
|
430
|
+
options,
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
if (response.outcome.outcome === "cancelled") {
|
|
434
|
+
return { behavior: "deny", message: "Cancelled by user" };
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const selectedOptionId = response.outcome.optionId;
|
|
438
|
+
logger.info(`User selected permission option: ${selectedOptionId}`);
|
|
439
|
+
|
|
440
|
+
switch (selectedOptionId) {
|
|
441
|
+
case "allow_once":
|
|
442
|
+
return { behavior: "allow" };
|
|
443
|
+
case "allow_always":
|
|
444
|
+
return {
|
|
445
|
+
behavior: "allow",
|
|
446
|
+
newPermissionRule: `${context.toolName}(*)`,
|
|
447
|
+
};
|
|
448
|
+
case "reject_once":
|
|
449
|
+
return { behavior: "deny", message: "Rejected by user" };
|
|
450
|
+
case "reject_always":
|
|
451
|
+
return {
|
|
452
|
+
behavior: "deny",
|
|
453
|
+
message: "Rejected by user",
|
|
454
|
+
newPermissionRule: `!${context.toolName}(*)`,
|
|
455
|
+
};
|
|
456
|
+
default:
|
|
457
|
+
return { behavior: "deny", message: "Unknown option selected" };
|
|
458
|
+
}
|
|
459
|
+
} catch (error) {
|
|
460
|
+
logger.error("Error requesting permission via ACP:", error);
|
|
461
|
+
return {
|
|
462
|
+
behavior: "deny",
|
|
463
|
+
message: `Error requesting permission: ${error instanceof Error ? error.message : String(error)}`,
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
private async getToolContentAsync(
|
|
469
|
+
name: string,
|
|
470
|
+
parameters: Record<string, unknown> | undefined,
|
|
471
|
+
workdir: string,
|
|
472
|
+
): Promise<ToolCallContent[] | undefined> {
|
|
473
|
+
if (!parameters) return undefined;
|
|
474
|
+
if (name === "Write") {
|
|
475
|
+
let oldText: string | null = null;
|
|
476
|
+
try {
|
|
477
|
+
const filePath = parameters.file_path as string;
|
|
478
|
+
const fullPath = path.isAbsolute(filePath)
|
|
479
|
+
? filePath
|
|
480
|
+
: path.join(workdir, filePath);
|
|
481
|
+
oldText = await fs.readFile(fullPath, "utf-8");
|
|
482
|
+
} catch {
|
|
483
|
+
// File might not exist, which is fine for Write
|
|
484
|
+
}
|
|
485
|
+
return [
|
|
486
|
+
{
|
|
487
|
+
type: "diff",
|
|
488
|
+
path: parameters.file_path as string,
|
|
489
|
+
oldText,
|
|
490
|
+
newText: parameters.content as string,
|
|
491
|
+
},
|
|
492
|
+
];
|
|
493
|
+
}
|
|
494
|
+
if (name === "Edit") {
|
|
495
|
+
let oldText: string | null = null;
|
|
496
|
+
let newText: string | null = null;
|
|
497
|
+
try {
|
|
498
|
+
const filePath = parameters.file_path as string;
|
|
499
|
+
const fullPath = path.isAbsolute(filePath)
|
|
500
|
+
? filePath
|
|
501
|
+
: path.join(workdir, filePath);
|
|
502
|
+
oldText = await fs.readFile(fullPath, "utf-8");
|
|
503
|
+
if (oldText) {
|
|
504
|
+
if (parameters.replace_all) {
|
|
505
|
+
newText = oldText
|
|
506
|
+
.split(parameters.old_string as string)
|
|
507
|
+
.join(parameters.new_string as string);
|
|
508
|
+
} else {
|
|
509
|
+
newText = oldText.replace(
|
|
510
|
+
parameters.old_string as string,
|
|
511
|
+
parameters.new_string as string,
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
} catch {
|
|
516
|
+
logger.error("Failed to read file for Edit diff");
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
if (oldText && newText) {
|
|
520
|
+
return [
|
|
521
|
+
{
|
|
522
|
+
type: "diff",
|
|
523
|
+
path: parameters.file_path as string,
|
|
524
|
+
oldText,
|
|
525
|
+
newText,
|
|
526
|
+
},
|
|
527
|
+
];
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Fallback to snippets if file reading fails
|
|
531
|
+
return [
|
|
532
|
+
{
|
|
533
|
+
type: "diff",
|
|
534
|
+
path: parameters.file_path as string,
|
|
535
|
+
oldText: parameters.old_string as string,
|
|
536
|
+
newText: parameters.new_string as string,
|
|
537
|
+
},
|
|
538
|
+
];
|
|
539
|
+
}
|
|
540
|
+
return this.getToolContent(name, parameters);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
private getToolContent(
|
|
544
|
+
name: string,
|
|
545
|
+
parameters: Record<string, unknown> | undefined,
|
|
546
|
+
): ToolCallContent[] | undefined {
|
|
547
|
+
if (!parameters) return undefined;
|
|
548
|
+
if (name === "Write") {
|
|
549
|
+
return [
|
|
550
|
+
{
|
|
551
|
+
type: "diff",
|
|
552
|
+
path: parameters.file_path as string,
|
|
553
|
+
oldText: null,
|
|
554
|
+
newText: parameters.content as string,
|
|
555
|
+
},
|
|
556
|
+
];
|
|
557
|
+
}
|
|
558
|
+
if (name === "Edit") {
|
|
559
|
+
return [
|
|
560
|
+
{
|
|
561
|
+
type: "diff",
|
|
562
|
+
path: parameters.file_path as string,
|
|
563
|
+
oldText: parameters.old_string as string,
|
|
564
|
+
newText: parameters.new_string as string,
|
|
565
|
+
},
|
|
566
|
+
];
|
|
567
|
+
}
|
|
568
|
+
return undefined;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
private getToolLocations(
|
|
572
|
+
name: string,
|
|
573
|
+
parameters: Record<string, unknown> | undefined,
|
|
574
|
+
): ToolCallLocation[] | undefined {
|
|
575
|
+
if (!parameters) return undefined;
|
|
576
|
+
if (name === "Write" || name === "Edit" || name === "Read") {
|
|
577
|
+
return [
|
|
578
|
+
{
|
|
579
|
+
path: parameters.file_path as string,
|
|
580
|
+
line: parameters.offset as number,
|
|
581
|
+
},
|
|
582
|
+
];
|
|
583
|
+
}
|
|
584
|
+
return undefined;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
private getToolKind(name: string): ToolKind {
|
|
588
|
+
switch (name) {
|
|
589
|
+
case "Read":
|
|
590
|
+
case "Glob":
|
|
591
|
+
case "Grep":
|
|
592
|
+
case "LSP":
|
|
593
|
+
return "read";
|
|
594
|
+
case "Write":
|
|
595
|
+
case "Edit":
|
|
596
|
+
return "edit";
|
|
597
|
+
case "Bash":
|
|
598
|
+
return "execute";
|
|
599
|
+
case "Agent":
|
|
600
|
+
return "other";
|
|
601
|
+
default:
|
|
602
|
+
return "other";
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
private createCallbacks(sessionId: string): AgentOptions["callbacks"] {
|
|
607
|
+
const getAgent = () => this.agents.get(sessionId);
|
|
608
|
+
return {
|
|
609
|
+
onAssistantContentUpdated: (chunk: string) => {
|
|
610
|
+
this.connection.sessionUpdate({
|
|
611
|
+
sessionId: sessionId as AcpSessionId,
|
|
612
|
+
update: {
|
|
613
|
+
sessionUpdate: "agent_message_chunk",
|
|
614
|
+
content: {
|
|
615
|
+
type: "text",
|
|
616
|
+
text: chunk,
|
|
617
|
+
},
|
|
618
|
+
},
|
|
619
|
+
});
|
|
620
|
+
},
|
|
621
|
+
onAssistantReasoningUpdated: (chunk: string) => {
|
|
622
|
+
this.connection.sessionUpdate({
|
|
623
|
+
sessionId: sessionId as AcpSessionId,
|
|
624
|
+
update: {
|
|
625
|
+
sessionUpdate: "agent_thought_chunk",
|
|
626
|
+
content: {
|
|
627
|
+
type: "text",
|
|
628
|
+
text: chunk,
|
|
629
|
+
},
|
|
630
|
+
},
|
|
631
|
+
});
|
|
632
|
+
},
|
|
633
|
+
onToolBlockUpdated: (params: AgentToolBlockUpdateParams) => {
|
|
634
|
+
const { id, name, stage, success, error, result, parameters } = params;
|
|
635
|
+
|
|
636
|
+
let parsedParameters: Record<string, unknown> | undefined = undefined;
|
|
637
|
+
if (parameters) {
|
|
638
|
+
try {
|
|
639
|
+
parsedParameters = JSON.parse(parameters);
|
|
640
|
+
} catch {
|
|
641
|
+
// Ignore parse errors during streaming
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
const content =
|
|
646
|
+
name && parsedParameters
|
|
647
|
+
? this.getToolContent(name, parsedParameters)
|
|
648
|
+
: undefined;
|
|
649
|
+
const locations =
|
|
650
|
+
name && parsedParameters
|
|
651
|
+
? this.getToolLocations(name, parsedParameters)
|
|
652
|
+
: undefined;
|
|
653
|
+
const kind = name ? this.getToolKind(name) : undefined;
|
|
654
|
+
|
|
655
|
+
if (stage === "start") {
|
|
656
|
+
this.connection.sessionUpdate({
|
|
657
|
+
sessionId: sessionId as AcpSessionId,
|
|
658
|
+
update: {
|
|
659
|
+
sessionUpdate: "tool_call",
|
|
660
|
+
toolCallId: id,
|
|
661
|
+
title: name || "Tool Call",
|
|
662
|
+
status: "pending",
|
|
663
|
+
content,
|
|
664
|
+
locations,
|
|
665
|
+
kind,
|
|
666
|
+
rawInput: parsedParameters,
|
|
667
|
+
},
|
|
668
|
+
});
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
if (stage === "streaming") {
|
|
673
|
+
// We don't support streaming tool arguments in ACP yet
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
const status: ToolCallStatus =
|
|
678
|
+
stage === "end"
|
|
679
|
+
? success
|
|
680
|
+
? "completed"
|
|
681
|
+
: "failed"
|
|
682
|
+
: stage === "running"
|
|
683
|
+
? "in_progress"
|
|
684
|
+
: "pending";
|
|
685
|
+
|
|
686
|
+
this.connection.sessionUpdate({
|
|
687
|
+
sessionId: sessionId as AcpSessionId,
|
|
688
|
+
update: {
|
|
689
|
+
sessionUpdate: "tool_call_update",
|
|
690
|
+
toolCallId: id,
|
|
691
|
+
status,
|
|
692
|
+
title: name || "Tool Call",
|
|
693
|
+
rawOutput: result || error,
|
|
694
|
+
content,
|
|
695
|
+
locations,
|
|
696
|
+
kind,
|
|
697
|
+
rawInput: parsedParameters,
|
|
698
|
+
},
|
|
699
|
+
});
|
|
700
|
+
},
|
|
701
|
+
onTasksChange: (tasks) => {
|
|
702
|
+
this.connection.sessionUpdate({
|
|
703
|
+
sessionId: sessionId as AcpSessionId,
|
|
704
|
+
update: {
|
|
705
|
+
sessionUpdate: "plan",
|
|
706
|
+
entries: tasks.map((task) => ({
|
|
707
|
+
content: task.subject,
|
|
708
|
+
status:
|
|
709
|
+
task.status === "completed"
|
|
710
|
+
? "completed"
|
|
711
|
+
: task.status === "in_progress"
|
|
712
|
+
? "in_progress"
|
|
713
|
+
: "pending",
|
|
714
|
+
priority: "medium",
|
|
715
|
+
})),
|
|
716
|
+
},
|
|
717
|
+
});
|
|
718
|
+
},
|
|
719
|
+
onPermissionModeChange: (mode) => {
|
|
720
|
+
this.connection.sessionUpdate({
|
|
721
|
+
sessionId: sessionId as AcpSessionId,
|
|
722
|
+
update: {
|
|
723
|
+
sessionUpdate: "current_mode_update",
|
|
724
|
+
currentModeId: mode,
|
|
725
|
+
},
|
|
726
|
+
});
|
|
727
|
+
const agent = getAgent();
|
|
728
|
+
if (agent) {
|
|
729
|
+
this.connection.sessionUpdate({
|
|
730
|
+
sessionId: sessionId as AcpSessionId,
|
|
731
|
+
update: {
|
|
732
|
+
sessionUpdate: "config_option_update",
|
|
733
|
+
configOptions: this.getSessionConfigOptions(agent),
|
|
734
|
+
},
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
},
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
}
|