opencode-claude-code-wrapper 0.0.5 → 0.0.7
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/index.mjs +341 -23
- package/lib/acp-client.mjs +23 -0
- package/lib/acp-protocol.mjs +292 -0
- package/lib/acp-transformer.mjs +434 -0
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -2,14 +2,11 @@ import { generatePKCE } from "@openauthjs/openauth/pkce";
|
|
|
2
2
|
import { writeFileSync, appendFileSync, mkdirSync } from "fs";
|
|
3
3
|
import { homedir } from "os";
|
|
4
4
|
import { join } from "path";
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
} from "./lib/
|
|
9
|
-
import {
|
|
10
|
-
JSONLToSSETransformer,
|
|
11
|
-
buildCompleteResponse,
|
|
12
|
-
} from "./lib/transformer.mjs";
|
|
5
|
+
import * as fs from "fs";
|
|
6
|
+
import { homedir } from "os";
|
|
7
|
+
import { join } from "path";
|
|
8
|
+
import { AcpClient } from "./lib/acp-protocol.mjs";
|
|
9
|
+
import { AcpToSSETransformer } from "./lib/acp-transformer.mjs";
|
|
13
10
|
|
|
14
11
|
const CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
|
|
15
12
|
|
|
@@ -280,6 +277,13 @@ async function handleClaudeCodeRequest(input, init) {
|
|
|
280
277
|
|
|
281
278
|
// Log full request body to separate file for detailed inspection
|
|
282
279
|
try {
|
|
280
|
+
// Save requests with tools to a separate file
|
|
281
|
+
if (requestBody.tools?.length > 0) {
|
|
282
|
+
writeFileSync(
|
|
283
|
+
join(METRICS_DIR, "last_request_with_tools.json"),
|
|
284
|
+
JSON.stringify(requestBody, null, 2)
|
|
285
|
+
);
|
|
286
|
+
}
|
|
283
287
|
writeFileSync(
|
|
284
288
|
join(METRICS_DIR, "last_request.json"),
|
|
285
289
|
JSON.stringify(requestBody, null, 2)
|
|
@@ -298,33 +302,347 @@ async function handleClaudeCodeRequest(input, init) {
|
|
|
298
302
|
}
|
|
299
303
|
|
|
300
304
|
/**
|
|
301
|
-
* OpenCode plugin that wraps Claude Code
|
|
305
|
+
* OpenCode plugin that wraps Claude Code via ACP protocol
|
|
302
306
|
* @type {import('@opencode-ai/plugin').Plugin}
|
|
303
307
|
*/
|
|
304
308
|
export async function ClaudeCodeWrapperPlugin({ client }) {
|
|
309
|
+
// Store ACP client instances per model
|
|
310
|
+
const acpClients = new Map();
|
|
311
|
+
const transformers = new Map();
|
|
312
|
+
|
|
305
313
|
return {
|
|
306
314
|
auth: {
|
|
307
315
|
provider: "anthropic",
|
|
308
316
|
async loader(getAuth, provider) {
|
|
309
317
|
const auth = await getAuth();
|
|
310
318
|
|
|
311
|
-
// Claude Code
|
|
312
|
-
//
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
319
|
+
// Claude Code ACP agent handles auth internally
|
|
320
|
+
// We just need to provide zero-cost wrapper
|
|
321
|
+
for (const model of Object.values(provider.models)) {
|
|
322
|
+
model.cost = {
|
|
323
|
+
input: 0,
|
|
324
|
+
output: 0,
|
|
325
|
+
cache: { read: 0, write: 0 },
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return {
|
|
330
|
+
apiKey: auth.key || "",
|
|
331
|
+
async fetch(input, init) {
|
|
332
|
+
const auth = await getAuth();
|
|
333
|
+
|
|
334
|
+
// Claude Code CLI - detected by special key
|
|
335
|
+
const isClaudeCodeCLI = auth.key === "claude-code" || auth.key === "cc";
|
|
336
|
+
|
|
337
|
+
if (isClaudeCodeCLI) {
|
|
338
|
+
try {
|
|
339
|
+
return await handleAcpRequest(input, init, auth);
|
|
340
|
+
} catch (error) {
|
|
341
|
+
console.error("[claude-code-acp] error:", error);
|
|
342
|
+
return createErrorResponse(error);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// OAuth auth (Claude Pro/Max) - pass through
|
|
347
|
+
if (auth.type === "oauth") {
|
|
348
|
+
return fetch(input, init);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Default - no special handling
|
|
352
|
+
return fetch(input, init);
|
|
353
|
+
},
|
|
354
|
+
};
|
|
355
|
+
},
|
|
356
|
+
methods: [
|
|
357
|
+
{
|
|
358
|
+
label: "Claude Code (ACP)",
|
|
359
|
+
type: "api",
|
|
360
|
+
authorize: async () => {
|
|
361
|
+
return {
|
|
362
|
+
url: "https://console.anthropic.com/oauth/authorize",
|
|
363
|
+
instructions: "Use the ACP agent to authenticate via Claude Code",
|
|
364
|
+
method: "external",
|
|
365
|
+
};
|
|
366
|
+
},
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
label: "Claude Pro/Max (OAuth)",
|
|
370
|
+
type: "oauth",
|
|
371
|
+
authorize: async () => {
|
|
372
|
+
const { url, verifier } = await authorize("max");
|
|
373
|
+
return {
|
|
374
|
+
url: url,
|
|
375
|
+
instructions: "Paste authorization code here: ",
|
|
376
|
+
method: "code",
|
|
377
|
+
callback: async (code) => {
|
|
378
|
+
const credentials = await exchange(code, verifier);
|
|
379
|
+
return credentials;
|
|
380
|
+
},
|
|
325
381
|
};
|
|
382
|
+
},
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
label: "Create an API Key",
|
|
386
|
+
type: "oauth",
|
|
387
|
+
authorize: async () => {
|
|
388
|
+
const { url, verifier } = await authorize("console");
|
|
389
|
+
return {
|
|
390
|
+
url: url,
|
|
391
|
+
instructions: "Paste authorization code here: ",
|
|
392
|
+
method: "code",
|
|
393
|
+
callback: async (code) => {
|
|
394
|
+
const credentials = await exchange(code, verifier);
|
|
395
|
+
if (credentials.type === "failed") return credentials;
|
|
396
|
+
const result = await fetch(
|
|
397
|
+
`https://api.anthropic.com/oauth/claude_cli/create_api_key`,
|
|
398
|
+
{
|
|
399
|
+
method: "POST",
|
|
400
|
+
headers: {
|
|
401
|
+
"Content-Type": "application/json",
|
|
402
|
+
authorization: `Bearer ${credentials.access}`,
|
|
403
|
+
},
|
|
404
|
+
}
|
|
405
|
+
).then((r) => r.json());
|
|
406
|
+
return { type: "success", key: result.raw_key };
|
|
407
|
+
},
|
|
408
|
+
};
|
|
409
|
+
},
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
provider: "anthropic",
|
|
413
|
+
label: "Manually enter API Key",
|
|
414
|
+
type: "api",
|
|
415
|
+
},
|
|
416
|
+
],
|
|
417
|
+
},
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Handle request via ACP agent (claude-code-acp)
|
|
423
|
+
*/
|
|
424
|
+
async function handleAcpRequest(input, init, auth) {
|
|
425
|
+
// Get or create ACP client for this request
|
|
426
|
+
let requestBody;
|
|
427
|
+
try {
|
|
428
|
+
if (typeof input === "string" || input instanceof URL) {
|
|
429
|
+
const url = new URL(input.toString());
|
|
430
|
+
// Only handle /v1/messages endpoint via ACP
|
|
431
|
+
if (url.pathname !== "/v1/messages") {
|
|
432
|
+
return fetch(input, init);
|
|
433
|
+
}
|
|
434
|
+
requestBody = init?.body ? JSON.parse(init.body) : {};
|
|
435
|
+
} else if (input instanceof Request) {
|
|
436
|
+
const url = new URL(input.url);
|
|
437
|
+
if (url.pathname !== "/v1/messages") {
|
|
438
|
+
return fetch(input, init);
|
|
439
|
+
}
|
|
440
|
+
requestBody = init?.body ? JSON.parse(init.body) : await input.text();
|
|
441
|
+
} else {
|
|
442
|
+
return fetch(input, init);
|
|
443
|
+
}
|
|
444
|
+
} catch (e) {
|
|
445
|
+
return createErrorResponse(new Error("Invalid request"));
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Log incoming request
|
|
449
|
+
logMetric("request", {
|
|
450
|
+
model: requestBody.model,
|
|
451
|
+
stream: requestBody.stream,
|
|
452
|
+
system: requestBody.system ? (typeof requestBody.system === "string" ? requestBody.system.substring(0, 200) : "[array]") : null,
|
|
453
|
+
messages_count: requestBody.messages?.length,
|
|
454
|
+
tools_count: requestBody.tools?.length,
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
try {
|
|
458
|
+
// Get ACP client and transformer
|
|
459
|
+
const model = requestBody.model || "claude-sonnet-4-5-20250929";
|
|
460
|
+
let acpClient = acpClients.get(model);
|
|
461
|
+
let transformer = transformers.get(model);
|
|
462
|
+
|
|
463
|
+
if (!acpClient) {
|
|
464
|
+
acpClient = new AcpClient();
|
|
465
|
+
acpClients.set(model, acpClient);
|
|
466
|
+
await acpClient.connect();
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (!transformer) {
|
|
470
|
+
transformer = new AcpToSSETransformer();
|
|
471
|
+
transformers.set(model, transformer);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Convert API request to ACP format
|
|
475
|
+
const resources = extractResources(requestBody.messages || []);
|
|
476
|
+
const text = extractUserText(requestBody.messages || []);
|
|
477
|
+
|
|
478
|
+
// Create new session if needed
|
|
479
|
+
if (!acpClient.sessionId) {
|
|
480
|
+
await acpClient.newSession({
|
|
481
|
+
mcpServers: extractMcpServers(auth),
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const isStreaming = requestBody.stream === true;
|
|
486
|
+
|
|
487
|
+
// Send prompt to ACP
|
|
488
|
+
const response = await acpClient.prompt(text, resources);
|
|
489
|
+
|
|
490
|
+
if (isStreaming && response.body) {
|
|
491
|
+
// Transform streaming ACP responses to SSE
|
|
492
|
+
const reader = response.body.getReader();
|
|
493
|
+
const decoder = new TextDecoder();
|
|
494
|
+
const encoder = new TextEncoder();
|
|
495
|
+
|
|
496
|
+
const stream = new ReadableStream({
|
|
497
|
+
async pull(controller) {
|
|
498
|
+
const { done, value } = await reader.read();
|
|
499
|
+
if (done) {
|
|
500
|
+
// Finalize transformer
|
|
501
|
+
const finalEvents = transformer.finalize();
|
|
502
|
+
controller.enqueue(encoder.encode(finalEvents));
|
|
503
|
+
controller.close();
|
|
504
|
+
return;
|
|
326
505
|
}
|
|
327
506
|
|
|
507
|
+
const text = decoder.decode(value, { stream: true });
|
|
508
|
+
// Parse ndjson from ACP
|
|
509
|
+
const lines = text.split("\n").filter((line) => line.trim());
|
|
510
|
+
|
|
511
|
+
for (const line of lines) {
|
|
512
|
+
try {
|
|
513
|
+
const message = JSON.parse(line);
|
|
514
|
+
// Transform ACP message to SSE
|
|
515
|
+
const sseEvents = transformer.transformMessage(message);
|
|
516
|
+
if (sseEvents) {
|
|
517
|
+
controller.enqueue(encoder.encode(sseEvents));
|
|
518
|
+
}
|
|
519
|
+
} catch (e) {
|
|
520
|
+
// Skip malformed lines
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
},
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
return new Response(stream, {
|
|
527
|
+
status: 200,
|
|
528
|
+
headers: {
|
|
529
|
+
"Content-Type": "text/event-stream",
|
|
530
|
+
"Cache-Control": "no-cache",
|
|
531
|
+
"Connection": "keep-alive",
|
|
532
|
+
},
|
|
533
|
+
});
|
|
534
|
+
} else {
|
|
535
|
+
return response;
|
|
536
|
+
}
|
|
537
|
+
} catch (error) {
|
|
538
|
+
console.error("[claude-code-acp] error:", error);
|
|
539
|
+
return createErrorResponse(error);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Extract resources (files, images) from messages
|
|
545
|
+
*/
|
|
546
|
+
function extractResources(messages) {
|
|
547
|
+
const resources = [];
|
|
548
|
+
|
|
549
|
+
for (const message of messages) {
|
|
550
|
+
if (message.role === "user" && Array.isArray(message.content)) {
|
|
551
|
+
for (const block of message.content) {
|
|
552
|
+
if (block.type === "image") {
|
|
553
|
+
resources.push({
|
|
554
|
+
type: "image",
|
|
555
|
+
data: block.source.type === "base64" ? block.source.data : "",
|
|
556
|
+
mimeType: block.source.type === "base64" ? block.source.media_type : "",
|
|
557
|
+
uri: block.source.type === "url" ? block.source.url : undefined,
|
|
558
|
+
});
|
|
559
|
+
} else if (block.type === "resource_link") {
|
|
560
|
+
resources.push({
|
|
561
|
+
type: "resource",
|
|
562
|
+
uri: block.uri,
|
|
563
|
+
});
|
|
564
|
+
} else if (block.type === "resource" && block.resource?.uri && block.resource?.text) {
|
|
565
|
+
resources.push({
|
|
566
|
+
type: "resource",
|
|
567
|
+
uri: block.resource.uri,
|
|
568
|
+
text: block.resource.text,
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
return resources;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Extract user text from messages
|
|
580
|
+
*/
|
|
581
|
+
function extractUserText(messages) {
|
|
582
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
583
|
+
const message = messages[i];
|
|
584
|
+
if (message.role === "user") {
|
|
585
|
+
if (typeof message.content === "string") {
|
|
586
|
+
return message.content;
|
|
587
|
+
}
|
|
588
|
+
if (Array.isArray(message.content)) {
|
|
589
|
+
return message.content
|
|
590
|
+
.filter((block) => block.type === "text")
|
|
591
|
+
.map((block) => block.text)
|
|
592
|
+
.join("\n");
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
return "";
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Extract MCP servers from auth
|
|
601
|
+
*/
|
|
602
|
+
function extractMcpServers(auth) {
|
|
603
|
+
// Extract any MCP servers from settings
|
|
604
|
+
// This could be extended to support custom MCP configs
|
|
605
|
+
return [];
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Create an error response
|
|
610
|
+
*/
|
|
611
|
+
function createErrorResponse(error, statusCode = 500) {
|
|
612
|
+
return new Response(
|
|
613
|
+
JSON.stringify({
|
|
614
|
+
type: "error",
|
|
615
|
+
error: {
|
|
616
|
+
type: "api_error",
|
|
617
|
+
message: error.message || String(error),
|
|
618
|
+
},
|
|
619
|
+
}),
|
|
620
|
+
{
|
|
621
|
+
status: statusCode,
|
|
622
|
+
headers: { "Content-Type": "application/json" },
|
|
623
|
+
}
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Log metrics
|
|
629
|
+
*/
|
|
630
|
+
function logMetric(type, data) {
|
|
631
|
+
try {
|
|
632
|
+
const METRICS_DIR = process.env.HOME ? join(homedir(), ".opencode-claude-code-wrapper") : "/tmp";
|
|
633
|
+
const METRICS_FILE = join(METRICS_DIR, "metrics.jsonl");
|
|
634
|
+
fs.mkdirSync(METRICS_DIR, { recursive: true });
|
|
635
|
+
const entry = {
|
|
636
|
+
timestamp: new Date().toISOString(),
|
|
637
|
+
type,
|
|
638
|
+
...data,
|
|
639
|
+
};
|
|
640
|
+
fs.appendFileSync(METRICS_FILE, JSON.stringify(entry) + "\n");
|
|
641
|
+
} catch (e) {
|
|
642
|
+
console.error("[metrics] Failed to log:", e.message);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
328
646
|
return {
|
|
329
647
|
apiKey: "",
|
|
330
648
|
async fetch(input, init) {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Spawn ACP agent (claude-code-acp) binary
|
|
6
|
+
* @returns {ChildProcess} Spawned ACP process
|
|
7
|
+
*/
|
|
8
|
+
export function spawnAcpAgent() {
|
|
9
|
+
const child = spawn("claude-code-acp", [], {
|
|
10
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
11
|
+
env: { ...process.env },
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
return child;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Create a random message ID
|
|
19
|
+
* @returns {string} Random ID
|
|
20
|
+
*/
|
|
21
|
+
export function generateId() {
|
|
22
|
+
return randomUUID();
|
|
23
|
+
}
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import { spawnAcpAgent, generateId } from "./acp-client.mjs";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ACP protocol client
|
|
5
|
+
* Communicates with claude-code-acp via ndjson (newline-delimited JSON)
|
|
6
|
+
*/
|
|
7
|
+
export class AcpClient {
|
|
8
|
+
constructor() {
|
|
9
|
+
this.child = null;
|
|
10
|
+
this.sessionId = null;
|
|
11
|
+
this.handlers = new Map();
|
|
12
|
+
this.pendingRequests = new Map();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Connect to ACP agent
|
|
17
|
+
*/
|
|
18
|
+
async connect() {
|
|
19
|
+
if (this.child) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
this.child = spawnAcpAgent();
|
|
24
|
+
|
|
25
|
+
// Start reading responses
|
|
26
|
+
this.child.stdout.setEncoding("utf8");
|
|
27
|
+
this.child.stdout.on("data", (data) => {
|
|
28
|
+
this.handleResponse(data.toString());
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
this.child.stderr.on("data", (data) => {
|
|
32
|
+
console.error("[ACP] stderr:", data.toString());
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
this.child.on("error", (err) => {
|
|
36
|
+
console.error("[ACP] Process error:", err);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
this.child.on("close", (code) => {
|
|
40
|
+
console.log("[ACP] Process closed with code:", code);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Send initialize request
|
|
44
|
+
await this.sendRequest({
|
|
45
|
+
type: "initialize",
|
|
46
|
+
clientCapabilities: {
|
|
47
|
+
fs: {
|
|
48
|
+
readTextFile: true,
|
|
49
|
+
writeTextFile: true,
|
|
50
|
+
},
|
|
51
|
+
terminal: true,
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Create a new session
|
|
58
|
+
* @param {object} options - Session options
|
|
59
|
+
*/
|
|
60
|
+
async newSession(options = {}) {
|
|
61
|
+
const requestId = generateId();
|
|
62
|
+
|
|
63
|
+
const request = {
|
|
64
|
+
type: "newSession",
|
|
65
|
+
sessionId: null,
|
|
66
|
+
cwd: process.cwd(),
|
|
67
|
+
mcpServers: options.mcpServers || [],
|
|
68
|
+
_meta: options._meta,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const response = await this.sendRequest(request, requestId);
|
|
72
|
+
this.sessionId = response.sessionId;
|
|
73
|
+
return response;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Send a prompt to the session
|
|
78
|
+
* @param {string} text - User message text
|
|
79
|
+
* @param {Array} resources - Context resources (files, images)
|
|
80
|
+
*/
|
|
81
|
+
async prompt(text, resources = []) {
|
|
82
|
+
if (!this.sessionId) {
|
|
83
|
+
throw new Error("No active session. Call newSession() first.");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const requestId = generateId();
|
|
87
|
+
const prompt = [];
|
|
88
|
+
|
|
89
|
+
// Add text chunks
|
|
90
|
+
if (text) {
|
|
91
|
+
prompt.push({ type: "text", text });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Add resource chunks
|
|
95
|
+
for (const resource of resources) {
|
|
96
|
+
prompt.push(resource);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const request = {
|
|
100
|
+
type: "prompt",
|
|
101
|
+
sessionId: this.sessionId,
|
|
102
|
+
prompt,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
return this.sendRequest(request, requestId);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Cancel current prompt
|
|
110
|
+
*/
|
|
111
|
+
async cancel() {
|
|
112
|
+
if (!this.sessionId) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const requestId = generateId();
|
|
117
|
+
await this.sendRequest({
|
|
118
|
+
type: "cancel",
|
|
119
|
+
sessionId: this.sessionId,
|
|
120
|
+
}, requestId);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Set session mode
|
|
125
|
+
* @param {string} mode - Mode to set
|
|
126
|
+
*/
|
|
127
|
+
async setMode(mode) {
|
|
128
|
+
if (!this.sessionId) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const requestId = generateId();
|
|
133
|
+
await this.sendRequest({
|
|
134
|
+
type: "setSessionMode",
|
|
135
|
+
sessionId: this.sessionId,
|
|
136
|
+
modeId: mode,
|
|
137
|
+
}, requestId);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Kill the ACP process
|
|
142
|
+
*/
|
|
143
|
+
kill() {
|
|
144
|
+
if (this.child) {
|
|
145
|
+
this.child.kill();
|
|
146
|
+
this.child = null;
|
|
147
|
+
}
|
|
148
|
+
this.sessionId = null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Send a request to ACP agent
|
|
153
|
+
* @param {object} request - Request object
|
|
154
|
+
* @param {string} requestId - Request ID for response matching
|
|
155
|
+
*/
|
|
156
|
+
async sendRequest(request, requestId) {
|
|
157
|
+
return new Promise((resolve, reject) => {
|
|
158
|
+
this.pendingRequests.set(requestId, { resolve, reject });
|
|
159
|
+
|
|
160
|
+
this.child.stdin.write(JSON.stringify({
|
|
161
|
+
...request,
|
|
162
|
+
_requestId: requestId,
|
|
163
|
+
}) + "\n");
|
|
164
|
+
|
|
165
|
+
// Set timeout (2 minutes)
|
|
166
|
+
setTimeout(() => {
|
|
167
|
+
const pending = this.pendingRequests.get(requestId);
|
|
168
|
+
if (pending) {
|
|
169
|
+
this.pendingRequests.delete(requestId);
|
|
170
|
+
reject(new Error("Request timeout"));
|
|
171
|
+
}
|
|
172
|
+
}, 120000);
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Handle responses from ACP agent
|
|
178
|
+
* @param {string} data - Response data chunk
|
|
179
|
+
*/
|
|
180
|
+
handleResponse(data) {
|
|
181
|
+
const lines = data.split("\n").filter((line) => line.trim());
|
|
182
|
+
|
|
183
|
+
for (const line of lines) {
|
|
184
|
+
try {
|
|
185
|
+
const message = JSON.parse(line);
|
|
186
|
+
this.handleMessage(message);
|
|
187
|
+
} catch (e) {
|
|
188
|
+
console.error("[ACP] Failed to parse response:", line, e);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Handle individual message from ACP agent
|
|
195
|
+
* @param {object} message - Parsed message
|
|
196
|
+
*/
|
|
197
|
+
handleMessage(message) {
|
|
198
|
+
switch (message.type) {
|
|
199
|
+
case "initialize_response":
|
|
200
|
+
this.handleInitializeResponse(message);
|
|
201
|
+
break;
|
|
202
|
+
|
|
203
|
+
case "newSession_response":
|
|
204
|
+
this.handleNewSessionResponse(message);
|
|
205
|
+
break;
|
|
206
|
+
|
|
207
|
+
case "prompt_response":
|
|
208
|
+
this.handlePromptResponse(message);
|
|
209
|
+
break;
|
|
210
|
+
|
|
211
|
+
case "sessionUpdate":
|
|
212
|
+
this.handleSessionUpdate(message);
|
|
213
|
+
break;
|
|
214
|
+
|
|
215
|
+
default:
|
|
216
|
+
console.warn("[ACP] Unknown message type:", message.type);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Handle initialize response
|
|
222
|
+
*/
|
|
223
|
+
handleInitializeResponse(message) {
|
|
224
|
+
const pending = this.pendingRequests.get(message._requestId);
|
|
225
|
+
if (pending) {
|
|
226
|
+
this.pendingRequests.delete(message._requestId);
|
|
227
|
+
pending.resolve(message);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Handle new session response
|
|
233
|
+
*/
|
|
234
|
+
handleNewSessionResponse(message) {
|
|
235
|
+
const pending = this.pendingRequests.get(message._requestId);
|
|
236
|
+
if (pending) {
|
|
237
|
+
this.pendingRequests.delete(message._requestId);
|
|
238
|
+
pending.resolve(message);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Handle prompt response (streamed updates)
|
|
244
|
+
*/
|
|
245
|
+
handlePromptResponse(message) {
|
|
246
|
+
if (message._requestId) {
|
|
247
|
+
// Final response
|
|
248
|
+
const pending = this.pendingRequests.get(message._requestId);
|
|
249
|
+
if (pending) {
|
|
250
|
+
this.pendingRequests.delete(message._requestId);
|
|
251
|
+
pending.resolve(message);
|
|
252
|
+
}
|
|
253
|
+
} else {
|
|
254
|
+
// Session update during streaming
|
|
255
|
+
this.handleSessionUpdate(message);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Handle session updates (streaming events)
|
|
261
|
+
*/
|
|
262
|
+
handleSessionUpdate(update) {
|
|
263
|
+
// Emit events for streaming
|
|
264
|
+
if (this.handlers.has("update")) {
|
|
265
|
+
for (const handler of this.handlers.get("update")) {
|
|
266
|
+
handler(update);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Register an event handler
|
|
273
|
+
* @param {string} event - Event type
|
|
274
|
+
* @param {Function} handler - Event handler
|
|
275
|
+
*/
|
|
276
|
+
on(event, handler) {
|
|
277
|
+
if (!this.handlers.has(event)) {
|
|
278
|
+
this.handlers.set(event, []);
|
|
279
|
+
}
|
|
280
|
+
this.handlers.get(event).push(handler);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Remove all event handlers for an event type
|
|
285
|
+
* @param {string} event - Event type
|
|
286
|
+
*/
|
|
287
|
+
off(event) {
|
|
288
|
+
this.handlers.delete(event);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export default AcpClient;
|
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
import { generateId } from "./cli-runner.mjs";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Transforms ACP ndjson responses to Anthropic SSE streaming format
|
|
5
|
+
*/
|
|
6
|
+
export class AcpToSSETransformer {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.messageId = `msg_${generateId()}`;
|
|
9
|
+
this.contentBlockIndex = 0;
|
|
10
|
+
this.hasStarted = false;
|
|
11
|
+
this.currentBlockStarted = false;
|
|
12
|
+
this.outputTokens = 0;
|
|
13
|
+
this.inputTokens = 0;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Create an SSE event string
|
|
18
|
+
* @param {string} eventType - The event type
|
|
19
|
+
* @param {object} data - The event data
|
|
20
|
+
* @returns {string} Formatted SSE event
|
|
21
|
+
*/
|
|
22
|
+
createSSEEvent(eventType, data) {
|
|
23
|
+
return `event: ${eventType}\ndata: ${JSON.stringify(data)}\n\n`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Transform ACP message to SSE events
|
|
28
|
+
* @param {object} message - ACP message
|
|
29
|
+
* @returns {string} SSE events string
|
|
30
|
+
*/
|
|
31
|
+
transformMessage(message) {
|
|
32
|
+
const events = [];
|
|
33
|
+
|
|
34
|
+
switch (message.type) {
|
|
35
|
+
case "sessionUpdate":
|
|
36
|
+
// Handle different session update types
|
|
37
|
+
const update = message.update;
|
|
38
|
+
switch (update.sessionUpdate) {
|
|
39
|
+
case "agent_message_chunk":
|
|
40
|
+
return this.transformAssistantMessageChunk(update.content);
|
|
41
|
+
case "tool_call":
|
|
42
|
+
return this.transformToolCall(update);
|
|
43
|
+
case "tool_call_update":
|
|
44
|
+
return this.transformToolCallUpdate(update);
|
|
45
|
+
case "plan":
|
|
46
|
+
return this.transformPlan(update.entries);
|
|
47
|
+
case "current_mode_update":
|
|
48
|
+
return []; // Mode updates don't need SSE events
|
|
49
|
+
case "available_commands_update":
|
|
50
|
+
return []; // Command updates don't need SSE events
|
|
51
|
+
default:
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
default:
|
|
56
|
+
console.warn("[Transformer] Unknown ACP message type:", message.type);
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Transform assistant message chunk
|
|
63
|
+
* @param {object} content - Message content
|
|
64
|
+
* @returns {string} SSE events
|
|
65
|
+
*/
|
|
66
|
+
transformAssistantMessageChunk(content) {
|
|
67
|
+
if (!this.hasStarted) {
|
|
68
|
+
this.hasStarted = true;
|
|
69
|
+
return [
|
|
70
|
+
this.createSSEEvent("message_start", {
|
|
71
|
+
type: "message",
|
|
72
|
+
message: {
|
|
73
|
+
id: this.messageId,
|
|
74
|
+
type: "assistant",
|
|
75
|
+
content: [],
|
|
76
|
+
model: "claude-sonnet-4-5-20250929",
|
|
77
|
+
stop_reason: null,
|
|
78
|
+
stop_sequence: null,
|
|
79
|
+
usage: { input_tokens: 0, output_tokens: 0 },
|
|
80
|
+
},
|
|
81
|
+
})
|
|
82
|
+
];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const events = [];
|
|
86
|
+
|
|
87
|
+
if (typeof content === "string") {
|
|
88
|
+
events.push(
|
|
89
|
+
this.createSSEEvent("content_block_delta", {
|
|
90
|
+
type: "content_block_delta",
|
|
91
|
+
index: this.contentBlockIndex,
|
|
92
|
+
delta: { type: "text_delta", text: content },
|
|
93
|
+
})
|
|
94
|
+
);
|
|
95
|
+
this.outputTokens += Math.ceil(content.length / 4);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return events.join("");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Transform tool call
|
|
103
|
+
* @param {object} toolCall - Tool call data
|
|
104
|
+
* @returns {string} SSE events
|
|
105
|
+
*/
|
|
106
|
+
transformToolCall(toolCall) {
|
|
107
|
+
if (!this.hasStarted) {
|
|
108
|
+
this.hasStarted = true;
|
|
109
|
+
return [
|
|
110
|
+
this.createSSEEvent("message_start", {
|
|
111
|
+
type: "message",
|
|
112
|
+
message: {
|
|
113
|
+
id: this.messageId,
|
|
114
|
+
type: "assistant",
|
|
115
|
+
content: [],
|
|
116
|
+
model: "claude-sonnet-4-5-20250929",
|
|
117
|
+
stop_reason: null,
|
|
118
|
+
stop_sequence: null,
|
|
119
|
+
usage: { input_tokens: 0, output_tokens: 0 },
|
|
120
|
+
},
|
|
121
|
+
})
|
|
122
|
+
];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Close current text block if open
|
|
126
|
+
if (this.currentBlockStarted) {
|
|
127
|
+
this.closeCurrentBlock();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const events = [];
|
|
131
|
+
|
|
132
|
+
// Start tool_use block
|
|
133
|
+
events.push(
|
|
134
|
+
this.createSSEEvent("content_block_start", {
|
|
135
|
+
type: "content_block_start",
|
|
136
|
+
index: this.contentBlockIndex,
|
|
137
|
+
content_block: {
|
|
138
|
+
type: "tool_use",
|
|
139
|
+
id: toolCall.toolCallId || `toolu_${generateId()}`,
|
|
140
|
+
name: toolCall.title || "unknown",
|
|
141
|
+
input: toolCall.rawInput || {},
|
|
142
|
+
},
|
|
143
|
+
})
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
// Stream input as input_json_delta
|
|
147
|
+
const inputStr = JSON.stringify(toolCall.rawInput || {});
|
|
148
|
+
events.push(
|
|
149
|
+
this.createSSEEvent("content_block_delta", {
|
|
150
|
+
type: "content_block_delta",
|
|
151
|
+
index: this.contentBlockIndex,
|
|
152
|
+
delta: {
|
|
153
|
+
type: "input_json_delta",
|
|
154
|
+
partial_json: inputStr,
|
|
155
|
+
},
|
|
156
|
+
})
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
// End tool_use block
|
|
160
|
+
events.push(
|
|
161
|
+
this.createSSEEvent("content_block_stop", {
|
|
162
|
+
type: "content_block_stop",
|
|
163
|
+
index: this.contentBlockIndex,
|
|
164
|
+
})
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
this.contentBlockIndex++;
|
|
168
|
+
|
|
169
|
+
return events.join("");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Transform tool call update (result)
|
|
174
|
+
* @param {object} update - Tool call update
|
|
175
|
+
* @returns {string} SSE events
|
|
176
|
+
*/
|
|
177
|
+
transformToolCallUpdate(update) {
|
|
178
|
+
if (update.status === "completed") {
|
|
179
|
+
// Emit result as text block
|
|
180
|
+
if (!this.hasStarted) {
|
|
181
|
+
this.hasStarted = true;
|
|
182
|
+
return [
|
|
183
|
+
this.createSSEEvent("message_start", {
|
|
184
|
+
type: "message",
|
|
185
|
+
message: {
|
|
186
|
+
id: this.messageId,
|
|
187
|
+
type: "assistant",
|
|
188
|
+
content: [],
|
|
189
|
+
model: "claude-sonnet-4-5-20250929",
|
|
190
|
+
stop_reason: null,
|
|
191
|
+
stop_sequence: null,
|
|
192
|
+
usage: { input_tokens: 0, output_tokens: 0 },
|
|
193
|
+
},
|
|
194
|
+
})
|
|
195
|
+
];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const events = [];
|
|
199
|
+
|
|
200
|
+
// Start content block
|
|
201
|
+
events.push(
|
|
202
|
+
this.createSSEEvent("content_block_start", {
|
|
203
|
+
type: "content_block_start",
|
|
204
|
+
index: this.contentBlockIndex,
|
|
205
|
+
content_block: { type: "text", text: "" },
|
|
206
|
+
})
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
// Add content (tool result)
|
|
210
|
+
const content = this.formatToolResult(update);
|
|
211
|
+
events.push(
|
|
212
|
+
this.createSSEEvent("content_block_delta", {
|
|
213
|
+
type: "content_block_delta",
|
|
214
|
+
index: this.contentBlockIndex,
|
|
215
|
+
delta: { type: "text_delta", text: content },
|
|
216
|
+
})
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
// End content block
|
|
220
|
+
events.push(
|
|
221
|
+
this.createSSEEvent("content_block_stop", {
|
|
222
|
+
type: "content_block_stop",
|
|
223
|
+
index: this.contentBlockIndex,
|
|
224
|
+
})
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
this.contentBlockIndex++;
|
|
228
|
+
return events.join("");
|
|
229
|
+
} else if (update.status === "failed") {
|
|
230
|
+
// Show error
|
|
231
|
+
if (!this.hasStarted) {
|
|
232
|
+
this.hasStarted = true;
|
|
233
|
+
return [
|
|
234
|
+
this.createSSEEvent("message_start", {
|
|
235
|
+
type: "message",
|
|
236
|
+
message: {
|
|
237
|
+
id: this.messageId,
|
|
238
|
+
type: "assistant",
|
|
239
|
+
content: [],
|
|
240
|
+
model: "claude-sonnet-4-5-20250929",
|
|
241
|
+
stop_reason: null,
|
|
242
|
+
stop_sequence: null,
|
|
243
|
+
usage: { input_tokens: 0, output_tokens: 0 },
|
|
244
|
+
},
|
|
245
|
+
})
|
|
246
|
+
];
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const events = [];
|
|
250
|
+
|
|
251
|
+
// Start content block
|
|
252
|
+
events.push(
|
|
253
|
+
this.createSSEEvent("content_block_start", {
|
|
254
|
+
type: "content_block_start",
|
|
255
|
+
index: this.contentBlockIndex,
|
|
256
|
+
content_block: { type: "text", text: "" },
|
|
257
|
+
})
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
// Add error content
|
|
261
|
+
const errorContent = this.formatError(update);
|
|
262
|
+
events.push(
|
|
263
|
+
this.createSSEEvent("content_block_delta", {
|
|
264
|
+
type: "content_block_delta",
|
|
265
|
+
index: this.contentBlockIndex,
|
|
266
|
+
delta: { type: "text_delta", text: errorContent },
|
|
267
|
+
})
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
// End content block
|
|
271
|
+
events.push(
|
|
272
|
+
this.createSSEEvent("content_block_stop", {
|
|
273
|
+
type: "content_block_stop",
|
|
274
|
+
index: this.contentBlockIndex,
|
|
275
|
+
})
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
this.contentBlockIndex++;
|
|
279
|
+
return events.join("");
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return [];
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Transform plan/TODO entries
|
|
287
|
+
* @param {Array} entries - Plan entries
|
|
288
|
+
* @returns {string} SSE events
|
|
289
|
+
*/
|
|
290
|
+
transformPlan(entries) {
|
|
291
|
+
if (!this.hasStarted) {
|
|
292
|
+
this.hasStarted = true;
|
|
293
|
+
return [
|
|
294
|
+
this.createSSEEvent("message_start", {
|
|
295
|
+
type: "message",
|
|
296
|
+
message: {
|
|
297
|
+
id: this.messageId,
|
|
298
|
+
type: "assistant",
|
|
299
|
+
content: [],
|
|
300
|
+
model: "claude-sonnet-4-5-20250929",
|
|
301
|
+
stop_reason: null,
|
|
302
|
+
stop_sequence: null,
|
|
303
|
+
usage: { input_tokens: 0, output_tokens: 0 },
|
|
304
|
+
},
|
|
305
|
+
})
|
|
306
|
+
];
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (!entries || entries.length === 0) {
|
|
310
|
+
return [];
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const events = [];
|
|
314
|
+
const planText = entries.map((e) => `- [${e.status === "completed" ? "x" : " "}] ${e.content}`).join("\n");
|
|
315
|
+
|
|
316
|
+
// Start content block
|
|
317
|
+
events.push(
|
|
318
|
+
this.createSSEEvent("content_block_start", {
|
|
319
|
+
type: "content_block_start",
|
|
320
|
+
index: this.contentBlockIndex,
|
|
321
|
+
content_block: { type: "text", text: "" },
|
|
322
|
+
})
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
// Add plan text
|
|
326
|
+
events.push(
|
|
327
|
+
this.createSSEEvent("content_block_delta", {
|
|
328
|
+
type: "content_block_delta",
|
|
329
|
+
index: this.contentBlockIndex,
|
|
330
|
+
delta: { type: "text_delta", text: planText },
|
|
331
|
+
})
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
// End content block
|
|
335
|
+
events.push(
|
|
336
|
+
this.createSSEEvent("content_block_stop", {
|
|
337
|
+
type: "content_block_stop",
|
|
338
|
+
index: this.contentBlockIndex,
|
|
339
|
+
})
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
this.contentBlockIndex++;
|
|
343
|
+
|
|
344
|
+
return events.join("");
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Format tool result for display
|
|
349
|
+
* @param {object} update - Tool call update
|
|
350
|
+
* @returns {string} Formatted result
|
|
351
|
+
*/
|
|
352
|
+
formatToolResult(update) {
|
|
353
|
+
if (update.content && Array.isArray(update.content)) {
|
|
354
|
+
// Join multiple content blocks
|
|
355
|
+
return update.content
|
|
356
|
+
.map((block) => {
|
|
357
|
+
if (block.type === "text") {
|
|
358
|
+
return block.text;
|
|
359
|
+
}
|
|
360
|
+
return "";
|
|
361
|
+
})
|
|
362
|
+
.filter((text) => text)
|
|
363
|
+
.join("\n");
|
|
364
|
+
} else if (typeof update.content === "string") {
|
|
365
|
+
return update.content;
|
|
366
|
+
}
|
|
367
|
+
return "";
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Format error message
|
|
372
|
+
* @param {object} update - Tool call update
|
|
373
|
+
* @returns {string} Formatted error
|
|
374
|
+
*/
|
|
375
|
+
formatError(update) {
|
|
376
|
+
if (update.status === "failed" && update.content) {
|
|
377
|
+
if (typeof update.content === "string") {
|
|
378
|
+
return `Error: ${update.content}`;
|
|
379
|
+
}
|
|
380
|
+
if (Array.isArray(update.content) && update.content.length > 0) {
|
|
381
|
+
const firstBlock = update.content[0];
|
|
382
|
+
if (firstBlock.type === "text") {
|
|
383
|
+
return `Error: ${firstBlock.text}`;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return "Error: Tool execution failed";
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Close current content block
|
|
392
|
+
*/
|
|
393
|
+
closeCurrentBlock() {
|
|
394
|
+
this.currentBlockStarted = false;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Finalize the stream with closing events
|
|
399
|
+
* @returns {string} Final SSE events
|
|
400
|
+
*/
|
|
401
|
+
finalize() {
|
|
402
|
+
const events = [];
|
|
403
|
+
|
|
404
|
+
if (this.hasStarted) {
|
|
405
|
+
// Close any open content block
|
|
406
|
+
if (this.currentBlockStarted) {
|
|
407
|
+
events.push(
|
|
408
|
+
this.createSSEEvent("content_block_stop", {
|
|
409
|
+
type: "content_block_stop",
|
|
410
|
+
index: this.contentBlockIndex,
|
|
411
|
+
})
|
|
412
|
+
);
|
|
413
|
+
this.currentBlockStarted = false;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
events.push(
|
|
417
|
+
this.createSSEEvent("message_delta", {
|
|
418
|
+
type: "message_delta",
|
|
419
|
+
delta: {
|
|
420
|
+
stop_reason: "end_turn",
|
|
421
|
+
stop_sequence: null,
|
|
422
|
+
},
|
|
423
|
+
usage: { output_tokens: this.outputTokens },
|
|
424
|
+
})
|
|
425
|
+
);
|
|
426
|
+
|
|
427
|
+
events.push(
|
|
428
|
+
this.createSSEEvent("message_stop", { type: "message_stop" })
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return events.join("");
|
|
433
|
+
}
|
|
434
|
+
}
|