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