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/dist/components/LoginCommand.d.ts.map +1 -1
- package/dist/components/LoginCommand.js +1 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -10
- package/dist/stdio/agentBridge.d.ts +74 -0
- package/dist/stdio/agentBridge.d.ts.map +1 -0
- package/dist/stdio/agentBridge.js +486 -0
- package/dist/stdio/index.d.ts +4 -0
- package/dist/stdio/index.d.ts.map +1 -0
- package/dist/stdio/index.js +3 -0
- package/dist/stdio/protocol.d.ts +38 -0
- package/dist/stdio/protocol.d.ts.map +1 -0
- package/dist/stdio/protocol.js +26 -0
- package/dist/stdio/stdioServer.d.ts +34 -0
- package/dist/stdio/stdioServer.d.ts.map +1 -0
- package/dist/stdio/stdioServer.js +125 -0
- package/dist/stdio-cli.d.ts +9 -0
- package/dist/stdio-cli.d.ts.map +1 -0
- package/dist/stdio-cli.js +15 -0
- package/package.json +3 -4
- package/src/components/LoginCommand.tsx +1 -6
- package/src/index.ts +12 -11
- package/src/stdio/agentBridge.ts +756 -0
- package/src/stdio/index.ts +7 -0
- package/src/stdio/protocol.ts +130 -0
- package/src/stdio/stdioServer.ts +163 -0
- package/src/stdio-cli.ts +18 -0
- package/dist/acp/agent.d.ts +0 -32
- package/dist/acp/agent.d.ts.map +0 -1
- package/dist/acp/agent.js +0 -1163
- package/dist/acp/index.d.ts +0 -2
- package/dist/acp/index.d.ts.map +0 -1
- package/dist/acp/index.js +0 -22
- package/dist/acp-cli.d.ts +0 -2
- package/dist/acp-cli.d.ts.map +0 -1
- package/dist/acp-cli.js +0 -4
- package/src/acp/agent.ts +0 -1501
- package/src/acp/index.ts +0 -28
- package/src/acp-cli.ts +0 -5
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
|
-
}
|