wave-code 0.19.2 → 0.19.3

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.js DELETED
@@ -1,1163 +0,0 @@
1
- import { Agent as WaveAgent, listSessions as listWaveSessions, listAllSessions as listAllWaveSessions, deleteSession as deleteWaveSession, truncateContent, BASH_TOOL_NAME, EDIT_TOOL_NAME, WRITE_TOOL_NAME, EXIT_PLAN_MODE_TOOL_NAME, ENTER_PLAN_MODE_TOOL_NAME, ASK_USER_QUESTION_TOOL_NAME, } from "wave-agent-sdk";
2
- import { logger } from "../utils/logger.js";
3
- import { AGENT_METHODS, } from "@agentclientprotocol/sdk";
4
- export class WaveAcpAgent {
5
- constructor(connection) {
6
- this.agents = new Map();
7
- this.taskCache = new Map();
8
- this.connection = connection;
9
- }
10
- getSessionModeState(agent) {
11
- return {
12
- currentModeId: agent.getPermissionMode(),
13
- availableModes: [
14
- {
15
- id: "default",
16
- name: "Default",
17
- description: "Ask for permission for restricted tools",
18
- },
19
- {
20
- id: "acceptEdits",
21
- name: "Accept Edits",
22
- description: "Automatically accept file edits",
23
- },
24
- {
25
- id: "plan",
26
- name: "Plan",
27
- description: "Plan mode for complex tasks",
28
- },
29
- {
30
- id: "bypassPermissions",
31
- name: "Bypass Permissions",
32
- description: "Automatically accept all tool calls",
33
- },
34
- {
35
- id: "dontAsk",
36
- name: "Don't Ask",
37
- description: "Automatically deny restricted tools unless pre-approved",
38
- },
39
- ],
40
- };
41
- }
42
- getSessionConfigOptions(agent) {
43
- const configuredModels = agent.getConfiguredModels();
44
- const currentModel = agent.getModelConfig().model || "";
45
- return [
46
- {
47
- id: "permission_mode",
48
- name: "Permission Mode",
49
- description: "Controls how the agent requests permission",
50
- type: "select",
51
- category: "mode",
52
- currentValue: agent.getPermissionMode(),
53
- options: [
54
- { value: "default", name: "Default" },
55
- { value: "acceptEdits", name: "Accept Edits" },
56
- { value: "plan", name: "Plan" },
57
- { value: "bypassPermissions", name: "Bypass Permissions" },
58
- { value: "dontAsk", name: "Don't Ask" },
59
- ],
60
- },
61
- {
62
- id: "model",
63
- name: "Model",
64
- description: "The AI model to use for this session",
65
- type: "select",
66
- category: "model",
67
- currentValue: currentModel,
68
- options: configuredModels.map((m) => ({
69
- value: m,
70
- name: m,
71
- })),
72
- },
73
- ];
74
- }
75
- async cleanupAllAgents() {
76
- logger.info("Cleaning up all active agents due to connection closure");
77
- const destroyPromises = Array.from(this.agents.values()).map((agent) => agent.destroy());
78
- await Promise.all(destroyPromises);
79
- this.agents.clear();
80
- }
81
- async initialize() {
82
- logger.info("Initializing WaveAcpAgent");
83
- // Setup cleanup on connection closure
84
- this.connection.closed.then(() => this.cleanupAllAgents());
85
- return {
86
- protocolVersion: 1,
87
- agentInfo: {
88
- name: "wave-agent",
89
- version: "0.1.0",
90
- },
91
- agentCapabilities: {
92
- loadSession: true,
93
- mcpCapabilities: { http: true, sse: true },
94
- sessionCapabilities: {
95
- list: {},
96
- close: {},
97
- },
98
- promptCapabilities: {
99
- image: true,
100
- embeddedContext: true,
101
- },
102
- },
103
- };
104
- }
105
- async authenticate() {
106
- // No authentication required for now
107
- }
108
- async createAgent(sessionId, cwd, mcpServers) {
109
- const callbacks = {};
110
- const agentRef = {};
111
- const sdkMcpServers = mcpServers
112
- ? convertAcpMcpServers(mcpServers)
113
- : undefined;
114
- const agent = await WaveAgent.create({
115
- workdir: cwd,
116
- restoreSessionId: sessionId,
117
- stream: true,
118
- mcpServers: sdkMcpServers,
119
- canUseTool: (context) => {
120
- if (!agentRef.instance) {
121
- throw new Error("Agent instance not yet initialized");
122
- }
123
- return this.handlePermissionRequest(agentRef.instance.sessionId, context);
124
- },
125
- callbacks: {
126
- onAssistantContentUpdated: (params) => callbacks.onAssistantContentUpdated?.(params),
127
- onAssistantReasoningUpdated: (params) => callbacks.onAssistantReasoningUpdated?.(params),
128
- onToolBlockUpdated: (params) => {
129
- const cb = callbacks.onToolBlockUpdated;
130
- cb?.(params);
131
- },
132
- onTasksChange: (tasks) => callbacks.onTasksChange?.(tasks),
133
- onPermissionModeChange: (mode) => callbacks.onPermissionModeChange?.(mode),
134
- onModelChange: (model) => callbacks.onModelChange?.(model),
135
- onUserMessageAdded: (params) => callbacks.onUserMessageAdded?.(params),
136
- onMcpServersChange: (servers) => callbacks.onMcpServersChange?.(servers),
137
- onSessionIdChange: (newSessionId) => callbacks.onSessionIdChange?.(newSessionId),
138
- onLatestTotalTokensChange: (tokens) => callbacks.onLatestTotalTokensChange?.(tokens),
139
- onLoadingChange: (loading) => callbacks.onLoadingChange?.(loading),
140
- },
141
- });
142
- agentRef.instance = agent;
143
- const actualSessionId = agent.sessionId;
144
- this.agents.set(actualSessionId, agent);
145
- // Update the callbacks object with the correct sessionId
146
- const { callbacks: cb } = this.createCallbacks(actualSessionId);
147
- Object.assign(callbacks, cb);
148
- // Send initial available commands after agent creation
149
- // Use setImmediate to ensure the client receives the session response before the update
150
- setImmediate(() => {
151
- this.connection.sessionUpdate({
152
- sessionId: actualSessionId,
153
- update: {
154
- sessionUpdate: "available_commands_update",
155
- availableCommands: agent.getSlashCommands().map((cmd) => ({
156
- name: cmd.name,
157
- description: cmd.description,
158
- input: {
159
- hint: "Enter arguments...",
160
- },
161
- })),
162
- },
163
- });
164
- });
165
- return agent;
166
- }
167
- async newSession(params) {
168
- const { cwd, mcpServers } = params;
169
- logger.info(`Creating new session in ${cwd}`);
170
- const agent = await this.createAgent(undefined, cwd, mcpServers);
171
- logger.info(`New session created with ID: ${agent.sessionId}`);
172
- return {
173
- sessionId: agent.sessionId,
174
- modes: this.getSessionModeState(agent),
175
- configOptions: this.getSessionConfigOptions(agent),
176
- };
177
- }
178
- async loadSession(params) {
179
- const { sessionId, cwd, mcpServers } = params;
180
- logger.info(`Loading session: ${sessionId} in ${cwd}`);
181
- const agent = await this.createAgent(sessionId, cwd, mcpServers);
182
- // Replay conversation history via session/update notifications per ACP spec
183
- await this.replayConversationHistory(agent);
184
- return {
185
- modes: this.getSessionModeState(agent),
186
- configOptions: this.getSessionConfigOptions(agent),
187
- };
188
- }
189
- async listSessions(params) {
190
- const { cwd } = params;
191
- logger.info(`listSessions called with params: ${JSON.stringify(params)}`);
192
- let waveSessions;
193
- if (!cwd) {
194
- logger.info("listSessions called without cwd, listing all sessions");
195
- waveSessions = await listAllWaveSessions();
196
- }
197
- else {
198
- logger.info(`Listing sessions for ${cwd}`);
199
- waveSessions = await listWaveSessions(cwd);
200
- }
201
- logger.info(`Found ${waveSessions.length} sessions`);
202
- const sessions = waveSessions.map((meta) => ({
203
- sessionId: meta.id,
204
- cwd: meta.workdir,
205
- title: meta.firstMessage ? truncateContent(meta.firstMessage) : undefined,
206
- updatedAt: meta.lastActiveAt.toISOString(),
207
- }));
208
- return { sessions };
209
- }
210
- async unstable_closeSession(params) {
211
- const sessionId = params.sessionId;
212
- logger.info(`Stopping session ${sessionId}`);
213
- const agent = this.agents.get(sessionId);
214
- if (agent) {
215
- const workdir = agent.workingDirectory;
216
- await agent.destroy();
217
- this.agents.delete(sessionId);
218
- // Delete the session file so it doesn't show up in listSessions
219
- await deleteWaveSession(sessionId, workdir);
220
- }
221
- return {};
222
- }
223
- async extMethod(method, params) {
224
- if (method === AGENT_METHODS.session_close) {
225
- return this.unstable_closeSession(params);
226
- }
227
- throw new Error(`Method ${method} not implemented`);
228
- }
229
- async setSessionMode(params) {
230
- const { sessionId, modeId } = params;
231
- const agent = this.agents.get(sessionId);
232
- if (!agent)
233
- throw new Error(`Session ${sessionId} not found`);
234
- agent.setPermissionMode(modeId);
235
- }
236
- async setSessionConfigOption(params) {
237
- const { sessionId, configId, value } = params;
238
- const agent = this.agents.get(sessionId);
239
- if (!agent)
240
- throw new Error(`Session ${sessionId} not found`);
241
- if (configId === "permission_mode") {
242
- agent.setPermissionMode(value);
243
- }
244
- else if (configId === "model" && typeof value === "string") {
245
- agent.setModel(value);
246
- }
247
- return {
248
- configOptions: this.getSessionConfigOptions(agent),
249
- };
250
- }
251
- async prompt(params) {
252
- const { sessionId, prompt, messageId } = params;
253
- logger.info(`Received prompt for session ${sessionId}`);
254
- logger.debug(`Prompt content for session ${sessionId}:`, prompt);
255
- const agent = this.agents.get(sessionId);
256
- if (!agent) {
257
- logger.error(`Session ${sessionId} not found`);
258
- throw new Error(`Session ${sessionId} not found`);
259
- }
260
- // Map ACP prompt to Wave Agent sendMessage
261
- const textBlocks = [];
262
- const images = [];
263
- for (const block of prompt) {
264
- if (block.type === "text") {
265
- textBlocks.push(block.text);
266
- }
267
- else if (block.type === "resource_link") {
268
- const link = block;
269
- textBlocks.push(`[${link.name}](${link.uri})`);
270
- }
271
- else if (block.type === "resource") {
272
- const embedded = block;
273
- textBlocks.push(`[Resource](${embedded.resource.uri})`);
274
- }
275
- else if (block.type === "image") {
276
- const img = block;
277
- images.push({
278
- path: img.data.startsWith("data:")
279
- ? img.data
280
- : `data:${img.mimeType};base64,${img.data}`,
281
- mimeType: img.mimeType,
282
- });
283
- }
284
- }
285
- const textContent = textBlocks.join("\n");
286
- const usagesBefore = agent.usages.length;
287
- try {
288
- await agent.sendMessage(textContent, images.length > 0 ? images : undefined);
289
- logger.info(`Message sent successfully for session ${sessionId}`);
290
- return {
291
- stopReason: "end_turn",
292
- ...mapTurnUsage(agent.usages.slice(usagesBefore)),
293
- ...(messageId ? { userMessageId: messageId } : {}),
294
- };
295
- }
296
- catch (error) {
297
- if (error instanceof Error && error.message.includes("abort")) {
298
- logger.info(`Message aborted for session ${sessionId}`);
299
- return {
300
- stopReason: "cancelled",
301
- ...mapTurnUsage(agent.usages.slice(usagesBefore)),
302
- ...(messageId ? { userMessageId: messageId } : {}),
303
- };
304
- }
305
- logger.error(`Error sending message for session ${sessionId}:`, error);
306
- throw error;
307
- }
308
- }
309
- async cancel(params) {
310
- const { sessionId } = params;
311
- logger.info(`Cancelling message for session ${sessionId}`);
312
- const agent = this.agents.get(sessionId);
313
- if (agent) {
314
- agent.abortMessage();
315
- }
316
- }
317
- getAllowAlwaysName(context) {
318
- if (context.toolName === BASH_TOOL_NAME) {
319
- const command = context.toolInput?.command || "";
320
- if (command.startsWith("mkdir")) {
321
- return "Yes, and auto-accept edits";
322
- }
323
- if (context.suggestedPrefix) {
324
- const prefix = context.suggestedPrefix.length > 12
325
- ? context.suggestedPrefix.substring(0, 9) + "..."
326
- : context.suggestedPrefix;
327
- return `Yes, always allow ${prefix}`;
328
- }
329
- return "Yes, always allow this command";
330
- }
331
- if (context.toolName === EDIT_TOOL_NAME ||
332
- context.toolName === WRITE_TOOL_NAME) {
333
- return "Yes, and auto-accept edits";
334
- }
335
- if (context.toolName === EXIT_PLAN_MODE_TOOL_NAME) {
336
- return "Yes, auto-accept edits";
337
- }
338
- return "Allow Always";
339
- }
340
- async handleAskQuestion(sessionId, toolCallId, context) {
341
- try {
342
- const questions = context.toolInput?.questions || [];
343
- const request = {
344
- toolCallId,
345
- title: questions.length === 1 ? questions[0].header : undefined,
346
- questions: questions.map((q, qi) => ({
347
- id: `q${qi}`,
348
- prompt: q.question,
349
- options: q.options.map((opt, oi) => ({
350
- id: String(oi),
351
- label: opt.label,
352
- description: opt.description,
353
- })),
354
- allowMultiple: q.multiSelect,
355
- })),
356
- };
357
- const response = await this.connection.extMethod("wave/ask_question", request);
358
- const outcome = response.outcome;
359
- if (outcome === "cancelled") {
360
- return { behavior: "deny", message: "Cancelled by user" };
361
- }
362
- // outcome === "answered"
363
- const answers = response.answers || [];
364
- const answerMap = {};
365
- for (const answer of answers) {
366
- const qIndex = parseInt(answer.questionId.replace("q", ""), 10);
367
- const question = questions[qIndex];
368
- if (!question)
369
- continue;
370
- const selectedLabels = answer.selectedOptionIds
371
- .map((id) => question.options[parseInt(id, 10)]?.label)
372
- .filter(Boolean);
373
- answerMap[question.question] = selectedLabels.join(", ");
374
- }
375
- return { behavior: "allow", message: JSON.stringify(answerMap) };
376
- }
377
- catch (error) {
378
- logger.warn("wave/ask_question extMethod failed, falling back to requestPermission", { error });
379
- return null;
380
- }
381
- }
382
- async handleCreatePlan(sessionId, toolCallId, context) {
383
- try {
384
- const cachedTasks = this.taskCache.get(sessionId) || [];
385
- const request = {
386
- toolCallId,
387
- plan: context.planContent || "",
388
- todos: cachedTasks
389
- .filter((t) => t.status !== "deleted")
390
- .map((t) => ({
391
- id: t.id,
392
- content: t.subject,
393
- status: t.status === "completed"
394
- ? "completed"
395
- : t.status === "in_progress"
396
- ? "in_progress"
397
- : "pending",
398
- })),
399
- };
400
- const response = await this.connection.extMethod("wave/create_plan", request);
401
- const outcome = response.outcome;
402
- if (outcome === "accepted") {
403
- const mode = response.mode;
404
- return {
405
- behavior: "allow",
406
- newPermissionMode: (mode || "default"),
407
- };
408
- }
409
- if (outcome === "rejected") {
410
- const reason = response.reason;
411
- return { behavior: "deny", message: reason || "Plan rejected" };
412
- }
413
- // cancelled or unknown outcome
414
- return { behavior: "deny", message: "Cancelled by user" };
415
- }
416
- catch (error) {
417
- logger.warn("wave/create_plan extMethod failed, falling back to requestPermission", { error });
418
- return null;
419
- }
420
- }
421
- async handlePermissionRequest(sessionId, context) {
422
- logger.info(`Handling permission request for ${context.toolName} in session ${sessionId}`);
423
- const agent = this.agents.get(sessionId);
424
- const toolCallId = context.toolCallId ||
425
- "perm-" + Math.random().toString(36).substring(2, 9);
426
- let effectiveName = context.toolName;
427
- let effectiveCompactParams = undefined;
428
- if (agent?.messages && context.toolCallId) {
429
- const toolBlock = agent.messages
430
- .flatMap((m) => m.blocks)
431
- .find((b) => b.type === "tool" && b.id === context.toolCallId);
432
- if (toolBlock) {
433
- effectiveName = toolBlock.name || effectiveName;
434
- effectiveCompactParams =
435
- toolBlock.compactParams || effectiveCompactParams;
436
- }
437
- }
438
- const displayTitle = effectiveName && effectiveCompactParams
439
- ? `${effectiveName}: ${effectiveCompactParams}`
440
- : effectiveName || "Tool Call";
441
- let options = [
442
- {
443
- optionId: "allow_once",
444
- name: "Allow Once",
445
- kind: "allow_once",
446
- },
447
- {
448
- optionId: "allow_always",
449
- name: "Allow Always",
450
- kind: "allow_always",
451
- },
452
- {
453
- optionId: "reject_once",
454
- name: "Reject Once",
455
- kind: "reject_once",
456
- },
457
- ];
458
- if (context.toolName === BASH_TOOL_NAME ||
459
- context.toolName === EDIT_TOOL_NAME ||
460
- context.toolName === WRITE_TOOL_NAME) {
461
- options = [
462
- {
463
- optionId: "allow_once",
464
- name: "Yes, proceed",
465
- kind: "allow_once",
466
- },
467
- {
468
- optionId: "allow_always",
469
- name: this.getAllowAlwaysName(context),
470
- kind: "allow_always",
471
- },
472
- ];
473
- }
474
- else if (context.toolName === EXIT_PLAN_MODE_TOOL_NAME) {
475
- options = [
476
- {
477
- optionId: "allow_once",
478
- name: "Yes, manually approve edits",
479
- kind: "allow_once",
480
- },
481
- {
482
- optionId: "allow_always",
483
- name: "Yes, auto-accept edits",
484
- kind: "allow_always",
485
- },
486
- ];
487
- }
488
- else if (context.toolName === ENTER_PLAN_MODE_TOOL_NAME) {
489
- options = [
490
- {
491
- optionId: "allow_once",
492
- name: "Yes, enter plan mode",
493
- kind: "allow_once",
494
- },
495
- {
496
- optionId: "reject_once",
497
- name: "No, start implementing now",
498
- kind: "reject_once",
499
- },
500
- ];
501
- }
502
- else if (context.toolName === ASK_USER_QUESTION_TOOL_NAME) {
503
- options = [];
504
- }
505
- const content = context.toolName
506
- ? this.getToolContent(context.toolName, context.toolInput, undefined, context.planContent)
507
- : undefined;
508
- const locations = context.toolName
509
- ? this.getToolLocations(context.toolName, context.toolInput)
510
- : undefined;
511
- const kind = context.toolName
512
- ? this.getToolKind(context.toolName)
513
- : undefined;
514
- // Try extension methods first, fall back to requestPermission
515
- if (context.toolName === ASK_USER_QUESTION_TOOL_NAME) {
516
- const extResult = await this.handleAskQuestion(sessionId, toolCallId, context);
517
- if (extResult !== null)
518
- return extResult;
519
- // Fall through to existing requestPermission logic
520
- }
521
- if (context.toolName === EXIT_PLAN_MODE_TOOL_NAME) {
522
- const extResult = await this.handleCreatePlan(sessionId, toolCallId, context);
523
- if (extResult !== null)
524
- return extResult;
525
- // Fall through to existing requestPermission logic
526
- }
527
- try {
528
- const response = await this.connection.requestPermission({
529
- sessionId: sessionId,
530
- toolCall: {
531
- toolCallId,
532
- title: displayTitle,
533
- status: "pending",
534
- rawInput: context.toolInput,
535
- content,
536
- locations,
537
- kind,
538
- },
539
- options,
540
- });
541
- if (response.outcome.outcome === "cancelled") {
542
- return { behavior: "deny", message: "Cancelled by user" };
543
- }
544
- if (context.toolName === ASK_USER_QUESTION_TOOL_NAME) {
545
- return {
546
- behavior: "allow",
547
- message: response.message,
548
- };
549
- }
550
- const selectedOptionId = response.outcome.optionId;
551
- logger.info(`User selected permission option: ${selectedOptionId}`);
552
- switch (selectedOptionId) {
553
- case "allow_always":
554
- if (context.toolName === BASH_TOOL_NAME) {
555
- const command = context.toolInput?.command || "";
556
- const rule = context.suggestedPrefix || command;
557
- return {
558
- behavior: "allow",
559
- newPermissionRule: `${BASH_TOOL_NAME}(${rule})`,
560
- };
561
- }
562
- if (context.toolName === EDIT_TOOL_NAME ||
563
- context.toolName === WRITE_TOOL_NAME ||
564
- context.toolName === EXIT_PLAN_MODE_TOOL_NAME) {
565
- return {
566
- behavior: "allow",
567
- newPermissionMode: "acceptEdits",
568
- };
569
- }
570
- return {
571
- behavior: "allow",
572
- newPermissionRule: context.toolName,
573
- };
574
- case "allow_once":
575
- if (context.toolName === EXIT_PLAN_MODE_TOOL_NAME) {
576
- return { behavior: "allow", newPermissionMode: "default" };
577
- }
578
- if (context.toolName === ENTER_PLAN_MODE_TOOL_NAME) {
579
- return { behavior: "allow", newPermissionMode: "plan" };
580
- }
581
- return { behavior: "allow" };
582
- case "reject_once":
583
- return { behavior: "deny", message: "Rejected by user" };
584
- default:
585
- return { behavior: "deny", message: "Unknown option selected" };
586
- }
587
- }
588
- catch (error) {
589
- logger.error("Error requesting permission via ACP:", error);
590
- return {
591
- behavior: "deny",
592
- message: `Error requesting permission: ${error instanceof Error ? error.message : String(error)}`,
593
- };
594
- }
595
- }
596
- getToolContent(name, parameters, shortResult, planContent) {
597
- const contents = [];
598
- if (parameters) {
599
- if (name === WRITE_TOOL_NAME &&
600
- typeof parameters.file_path === "string" &&
601
- typeof parameters.content === "string") {
602
- contents.push({
603
- type: "diff",
604
- path: parameters.file_path,
605
- oldText: null,
606
- newText: parameters.content,
607
- });
608
- }
609
- else if (name === EDIT_TOOL_NAME &&
610
- typeof parameters.file_path === "string" &&
611
- typeof parameters.old_string === "string" &&
612
- typeof parameters.new_string === "string") {
613
- contents.push({
614
- type: "diff",
615
- path: parameters.file_path,
616
- oldText: parameters.old_string,
617
- newText: parameters.new_string,
618
- });
619
- }
620
- else if (name === EXIT_PLAN_MODE_TOOL_NAME && planContent) {
621
- contents.push({
622
- type: "content",
623
- content: {
624
- type: "text",
625
- text: planContent,
626
- },
627
- });
628
- }
629
- else if (name === ASK_USER_QUESTION_TOOL_NAME &&
630
- Array.isArray(parameters.questions)) {
631
- const markdown = parameters.questions
632
- .map((q, i) => {
633
- let text = `### Question ${i + 1}\n${q.question}\n`;
634
- if (Array.isArray(q.options)) {
635
- text += q.options
636
- .map((opt) => `- ${opt.label}${opt.description ? `: ${opt.description}` : ""}`)
637
- .join("\n");
638
- }
639
- return text;
640
- })
641
- .join("\n\n");
642
- contents.push({
643
- type: "content",
644
- content: {
645
- type: "text",
646
- text: markdown,
647
- },
648
- });
649
- }
650
- if (name.startsWith("mcp__")) {
651
- contents.push({
652
- type: "content",
653
- content: {
654
- type: "text",
655
- text: "```json\n" + JSON.stringify(parameters, null, 2) + "\n```",
656
- },
657
- });
658
- }
659
- }
660
- if (shortResult) {
661
- contents.push({
662
- type: "content",
663
- content: {
664
- type: "text",
665
- text: name === BASH_TOOL_NAME
666
- ? "```\n" + shortResult + "\n```"
667
- : shortResult,
668
- },
669
- });
670
- }
671
- return contents.length > 0 ? contents : undefined;
672
- }
673
- getToolLocations(name, parameters, extraStartLineNumber) {
674
- if (!parameters)
675
- return undefined;
676
- if (name === "Write" ||
677
- name === "Edit" ||
678
- name === "Read" ||
679
- name === "LSP") {
680
- const filePath = (parameters.file_path || parameters.filePath);
681
- let line = extraStartLineNumber ??
682
- parameters.startLineNumber ??
683
- parameters.line ??
684
- parameters.offset;
685
- if (name === "Write" && line === undefined) {
686
- line = 1;
687
- }
688
- if (filePath) {
689
- return [
690
- {
691
- path: filePath,
692
- line: line,
693
- },
694
- ];
695
- }
696
- }
697
- return undefined;
698
- }
699
- getToolKind(name) {
700
- switch (name) {
701
- case "Read":
702
- case "Glob":
703
- case "Grep":
704
- case "LSP":
705
- return "read";
706
- case "Write":
707
- case "Edit":
708
- return "edit";
709
- case "Bash":
710
- return "execute";
711
- case "Agent":
712
- return "other";
713
- default:
714
- return "other";
715
- }
716
- }
717
- async replayConversationHistory(agent) {
718
- const sessionId = agent.sessionId;
719
- const history = agent.messages;
720
- for (const message of history) {
721
- if (message.isMeta)
722
- continue;
723
- const messageId = message.id;
724
- for (const block of message.blocks) {
725
- if (block.type === "text") {
726
- const textBlock = block;
727
- const update = message.role === "user"
728
- ? {
729
- sessionUpdate: "user_message_chunk",
730
- content: { type: "text", text: textBlock.content },
731
- messageId,
732
- }
733
- : {
734
- sessionUpdate: "agent_message_chunk",
735
- content: { type: "text", text: textBlock.content },
736
- messageId,
737
- };
738
- this.connection.sessionUpdate({ sessionId, update });
739
- }
740
- else if (block.type === "reasoning") {
741
- const reasoningBlock = block;
742
- this.connection.sessionUpdate({
743
- sessionId,
744
- update: {
745
- sessionUpdate: "agent_thought_chunk",
746
- content: { type: "text", text: reasoningBlock.content },
747
- messageId,
748
- },
749
- });
750
- }
751
- else if (block.type === "tool") {
752
- const toolBlock = block;
753
- const toolCallId = toolBlock.id ||
754
- "replay-" + Math.random().toString(36).substring(2, 9);
755
- const effectiveName = toolBlock.name || "Tool";
756
- const effectiveCompactParams = toolBlock.compactParams;
757
- const displayTitle = effectiveName && effectiveCompactParams
758
- ? `${effectiveName}: ${effectiveCompactParams}`
759
- : effectiveName;
760
- let parsedParameters;
761
- if (toolBlock.parameters) {
762
- try {
763
- const parsed = JSON.parse(toolBlock.parameters);
764
- parsedParameters = Array.isArray(parsed)
765
- ? { args: parsed }
766
- : parsed;
767
- }
768
- catch {
769
- // Ignore parse errors
770
- }
771
- }
772
- const content = effectiveName && (parsedParameters || toolBlock.shortResult)
773
- ? this.getToolContent(effectiveName, parsedParameters, toolBlock.shortResult)
774
- : undefined;
775
- const locations = effectiveName && parsedParameters
776
- ? this.getToolLocations(effectiveName, parsedParameters)
777
- : undefined;
778
- const kind = effectiveName
779
- ? this.getToolKind(effectiveName)
780
- : undefined;
781
- // Emit tool_call (creation)
782
- this.connection.sessionUpdate({
783
- sessionId,
784
- update: {
785
- sessionUpdate: "tool_call",
786
- toolCallId,
787
- title: displayTitle,
788
- status: "pending",
789
- content,
790
- locations,
791
- kind,
792
- rawInput: parsedParameters,
793
- },
794
- });
795
- // Emit tool_call_update with final status
796
- const status = toolBlock.stage === "end"
797
- ? toolBlock.success
798
- ? "completed"
799
- : "failed"
800
- : toolBlock.stage === "running"
801
- ? "in_progress"
802
- : "pending";
803
- this.connection.sessionUpdate({
804
- sessionId,
805
- update: {
806
- sessionUpdate: "tool_call_update",
807
- toolCallId,
808
- status,
809
- title: displayTitle,
810
- rawOutput: toolBlock.result || toolBlock.error,
811
- content,
812
- locations,
813
- kind,
814
- rawInput: parsedParameters,
815
- },
816
- });
817
- }
818
- else if (block.type === "compact") {
819
- const compactBlock = block;
820
- this.connection.sessionUpdate({
821
- sessionId,
822
- update: {
823
- sessionUpdate: "agent_message_chunk",
824
- content: { type: "text", text: compactBlock.content },
825
- messageId,
826
- },
827
- });
828
- }
829
- // Skip: image, bang, error, file_history, task_notification blocks
830
- }
831
- }
832
- }
833
- createCallbacks(sessionId) {
834
- const sessionRef = { id: sessionId };
835
- const getAgent = () => this.agents.get(sessionRef.id);
836
- const toolStates = new Map();
837
- return {
838
- callbacks: {
839
- onAssistantContentUpdated: (params) => {
840
- this.connection.sessionUpdate({
841
- sessionId: sessionRef.id,
842
- update: {
843
- sessionUpdate: "agent_message_chunk",
844
- content: {
845
- type: "text",
846
- text: params.chunk,
847
- },
848
- },
849
- });
850
- },
851
- onAssistantReasoningUpdated: (params) => {
852
- this.connection.sessionUpdate({
853
- sessionId: sessionRef.id,
854
- update: {
855
- sessionUpdate: "agent_thought_chunk",
856
- content: {
857
- type: "text",
858
- text: params.chunk,
859
- },
860
- },
861
- });
862
- },
863
- onToolBlockUpdated: (params) => {
864
- const { id, name, stage, success, error, result, parameters, compactParams, shortResult, startLineNumber, } = params;
865
- let state = toolStates.get(id);
866
- if (!state) {
867
- state = {};
868
- toolStates.set(id, state);
869
- }
870
- if (name)
871
- state.name = name;
872
- if (compactParams)
873
- state.compactParams = compactParams;
874
- if (shortResult)
875
- state.shortResult = shortResult;
876
- if (startLineNumber !== undefined)
877
- state.startLineNumber = startLineNumber;
878
- const effectiveName = state.name || name;
879
- const effectiveCompactParams = state.compactParams || compactParams;
880
- const effectiveShortResult = state.shortResult || shortResult;
881
- const effectiveStartLineNumber = state.startLineNumber !== undefined
882
- ? state.startLineNumber
883
- : startLineNumber;
884
- const displayTitle = effectiveName && effectiveCompactParams
885
- ? `${effectiveName}: ${effectiveCompactParams}`
886
- : effectiveName || "Tool Call";
887
- let parsedParameters = undefined;
888
- if (parameters) {
889
- try {
890
- const parsed = JSON.parse(parameters);
891
- parsedParameters = Array.isArray(parsed)
892
- ? { args: parsed }
893
- : parsed;
894
- }
895
- catch {
896
- // Ignore parse errors during streaming
897
- }
898
- }
899
- const content = effectiveName && (parsedParameters || effectiveShortResult)
900
- ? this.getToolContent(effectiveName, parsedParameters, effectiveShortResult)
901
- : undefined;
902
- const locations = effectiveName && parsedParameters
903
- ? this.getToolLocations(effectiveName, parsedParameters, effectiveStartLineNumber)
904
- : undefined;
905
- const kind = effectiveName
906
- ? this.getToolKind(effectiveName)
907
- : undefined;
908
- if (stage === "start") {
909
- this.connection.sessionUpdate({
910
- sessionId: sessionRef.id,
911
- update: {
912
- sessionUpdate: "tool_call",
913
- toolCallId: id,
914
- title: displayTitle,
915
- status: "pending",
916
- content,
917
- locations,
918
- kind,
919
- rawInput: parsedParameters,
920
- },
921
- });
922
- return;
923
- }
924
- if (stage === "streaming") {
925
- // We don't support streaming tool arguments in ACP yet
926
- return;
927
- }
928
- const status = stage === "end"
929
- ? success
930
- ? "completed"
931
- : "failed"
932
- : stage === "running"
933
- ? "in_progress"
934
- : "pending";
935
- this.connection.sessionUpdate({
936
- sessionId: sessionRef.id,
937
- update: {
938
- sessionUpdate: "tool_call_update",
939
- toolCallId: id,
940
- status,
941
- title: displayTitle,
942
- rawOutput: result || error,
943
- content,
944
- locations,
945
- kind,
946
- rawInput: parsedParameters,
947
- },
948
- });
949
- if (stage === "end") {
950
- toolStates.delete(id);
951
- }
952
- },
953
- onTasksChange: (tasks) => {
954
- this.taskCache.set(sessionRef.id, tasks);
955
- this.connection.sessionUpdate({
956
- sessionId: sessionRef.id,
957
- update: {
958
- sessionUpdate: "plan",
959
- entries: tasks
960
- .filter((t) => t.status !== "deleted")
961
- .map((task) => ({
962
- content: task.subject,
963
- status: task.status === "completed"
964
- ? "completed"
965
- : task.status === "in_progress"
966
- ? "in_progress"
967
- : "pending",
968
- priority: "medium",
969
- })),
970
- },
971
- });
972
- },
973
- onPermissionModeChange: (mode) => {
974
- this.connection.sessionUpdate({
975
- sessionId: sessionRef.id,
976
- update: {
977
- sessionUpdate: "current_mode_update",
978
- currentModeId: mode,
979
- },
980
- });
981
- const agent = getAgent();
982
- if (agent) {
983
- this.connection.sessionUpdate({
984
- sessionId: sessionRef.id,
985
- update: {
986
- sessionUpdate: "config_option_update",
987
- configOptions: this.getSessionConfigOptions(agent),
988
- },
989
- });
990
- }
991
- },
992
- onModelChange: () => {
993
- const agent = getAgent();
994
- if (agent) {
995
- this.connection.sessionUpdate({
996
- sessionId: sessionRef.id,
997
- update: {
998
- sessionUpdate: "config_option_update",
999
- configOptions: this.getSessionConfigOptions(agent),
1000
- },
1001
- });
1002
- }
1003
- },
1004
- onUserMessageAdded: (params) => {
1005
- this.connection.sessionUpdate({
1006
- sessionId: sessionRef.id,
1007
- update: {
1008
- sessionUpdate: "user_message_chunk",
1009
- content: { type: "text", text: params.content },
1010
- },
1011
- });
1012
- },
1013
- onMcpServersChange: (servers) => {
1014
- for (const server of servers) {
1015
- this.connection.sessionUpdate({
1016
- sessionId: sessionRef.id,
1017
- update: {
1018
- sessionUpdate: "ext_notification",
1019
- method: "mcp_server_status",
1020
- params: {
1021
- name: server.name,
1022
- status: server.status,
1023
- toolCount: server.toolCount,
1024
- error: server.error,
1025
- },
1026
- },
1027
- });
1028
- }
1029
- },
1030
- onSessionIdChange: (newSessionId) => {
1031
- const oldSessionId = sessionRef.id;
1032
- if (oldSessionId === newSessionId)
1033
- return;
1034
- // Update agents Map key
1035
- const agent = this.agents.get(oldSessionId);
1036
- if (agent) {
1037
- this.agents.delete(oldSessionId);
1038
- this.agents.set(newSessionId, agent);
1039
- }
1040
- // Update taskCache key
1041
- const tasks = this.taskCache.get(oldSessionId);
1042
- if (tasks) {
1043
- this.taskCache.delete(oldSessionId);
1044
- this.taskCache.set(newSessionId, tasks);
1045
- }
1046
- // Update ref so all subsequent callbacks use new ID
1047
- sessionRef.id = newSessionId;
1048
- // Notify ACP client
1049
- this.connection.sessionUpdate({
1050
- sessionId: oldSessionId,
1051
- update: {
1052
- sessionUpdate: "ext_notification",
1053
- method: "session_id_change",
1054
- params: { oldSessionId, newSessionId },
1055
- },
1056
- });
1057
- },
1058
- onLatestTotalTokensChange: (tokens) => {
1059
- const agent = getAgent();
1060
- const contextWindowSize = agent?.getMaxInputTokens() ?? 0;
1061
- this.connection.sessionUpdate({
1062
- sessionId: sessionRef.id,
1063
- update: {
1064
- sessionUpdate: "usage_update",
1065
- size: contextWindowSize,
1066
- used: tokens,
1067
- },
1068
- });
1069
- },
1070
- onLoadingChange: (loading) => {
1071
- if (!loading) {
1072
- this.connection.sessionUpdate({
1073
- sessionId: sessionRef.id,
1074
- update: {
1075
- sessionUpdate: "agent_message_chunk",
1076
- content: { type: "text", text: "" },
1077
- _meta: { endOfTurn: true },
1078
- },
1079
- });
1080
- }
1081
- },
1082
- },
1083
- sessionRef,
1084
- };
1085
- }
1086
- }
1087
- /**
1088
- * Convert ACP McpServer[] to SDK Record<string, McpServerConfig>.
1089
- */
1090
- function convertAcpMcpServers(servers) {
1091
- const result = {};
1092
- for (const server of servers) {
1093
- const config = {};
1094
- if ("type" in server && server.type === "http") {
1095
- config.type = "http";
1096
- config.url = server.url;
1097
- config.headers = convertHttpHeaders(server.headers);
1098
- }
1099
- else if ("type" in server && server.type === "sse") {
1100
- config.type = "sse";
1101
- config.url = server.url;
1102
- config.headers = convertHttpHeaders(server.headers);
1103
- }
1104
- else {
1105
- // stdio (no type discriminator)
1106
- config.command = server.command;
1107
- config.args = server.args;
1108
- config.env = convertEnvVariables(server.env);
1109
- }
1110
- result[server.name] = config;
1111
- }
1112
- return result;
1113
- }
1114
- /**
1115
- * Convert ACP EnvVariable[] to SDK Record<string, string>.
1116
- */
1117
- function convertEnvVariables(env) {
1118
- const result = {};
1119
- for (const entry of env) {
1120
- result[entry.name] = entry.value;
1121
- }
1122
- return result;
1123
- }
1124
- /**
1125
- * Convert ACP HttpHeader[] to SDK Record<string, string>.
1126
- */
1127
- function convertHttpHeaders(headers) {
1128
- const result = {};
1129
- for (const entry of headers) {
1130
- result[entry.name] = entry.value;
1131
- }
1132
- return result;
1133
- }
1134
- /**
1135
- * Map per-turn SDK usages to a single ACP Usage object.
1136
- * Returns undefined when there are no usage entries.
1137
- */
1138
- function mapTurnUsage(newUsages) {
1139
- if (newUsages.length === 0)
1140
- return {};
1141
- let inputTokens = 0;
1142
- let outputTokens = 0;
1143
- let totalTokens = 0;
1144
- let cachedReadTokens = 0;
1145
- let cachedWriteTokens = 0;
1146
- for (const u of newUsages) {
1147
- inputTokens += u.prompt_tokens;
1148
- outputTokens += u.completion_tokens;
1149
- totalTokens += u.total_tokens;
1150
- cachedReadTokens += u.cache_read_input_tokens ?? 0;
1151
- cachedWriteTokens += u.cache_creation_input_tokens ?? 0;
1152
- }
1153
- const usage = {
1154
- inputTokens,
1155
- outputTokens,
1156
- totalTokens,
1157
- };
1158
- if (cachedReadTokens > 0)
1159
- usage.cachedReadTokens = cachedReadTokens;
1160
- if (cachedWriteTokens > 0)
1161
- usage.cachedWriteTokens = cachedWriteTokens;
1162
- return { usage };
1163
- }