agentbox-sdk 0.1.0 → 0.1.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.
@@ -0,0 +1,4658 @@
1
+ import {
2
+ createNormalizedEvent,
3
+ normalizeRawAgentEvent,
4
+ toAISDKStream
5
+ } from "./chunk-ZOWBRUQR.js";
6
+ import {
7
+ AgentBoxError,
8
+ AsyncQueue,
9
+ UnsupportedProviderError,
10
+ asError,
11
+ debugAgent,
12
+ debugClaude,
13
+ debugCodex,
14
+ debugOpencode,
15
+ debugRelay,
16
+ debugRuntime,
17
+ debugSetup,
18
+ linesFromTextChunks,
19
+ sleep,
20
+ time,
21
+ waitFor
22
+ } from "./chunk-INMA52FV.js";
23
+ import {
24
+ shellQuote
25
+ } from "./chunk-NSJM57Z4.js";
26
+ import {
27
+ AgentProvider,
28
+ SandboxProvider
29
+ } from "./chunk-GOFJNFAD.js";
30
+
31
+ // src/agents/Agent.ts
32
+ import { randomUUID as randomUUID2 } from "crypto";
33
+
34
+ // src/agents/providers/claude-code.ts
35
+ import { randomUUID } from "crypto";
36
+ import path8 from "path";
37
+
38
+ // src/agents/approval.ts
39
+ function getApprovalMode(options) {
40
+ return options.approvalMode ?? "auto";
41
+ }
42
+ function isInteractiveApproval(options) {
43
+ return getApprovalMode(options) === "interactive";
44
+ }
45
+ function shouldAutoApproveClaudeTools(options) {
46
+ if (options.provider?.autoApproveTools !== void 0) {
47
+ return options.provider.autoApproveTools;
48
+ }
49
+ return !isInteractiveApproval(options);
50
+ }
51
+
52
+ // src/agents/input.ts
53
+ import { readFile } from "fs/promises";
54
+ import path from "path";
55
+ import { fileURLToPath } from "url";
56
+ var IMAGE_MEDIA_TYPE_BY_EXTENSION = {
57
+ ".gif": "image/gif",
58
+ ".jpg": "image/jpeg",
59
+ ".jpeg": "image/jpeg",
60
+ ".png": "image/png",
61
+ ".webp": "image/webp"
62
+ };
63
+ var FILE_MEDIA_TYPE_BY_EXTENSION = {
64
+ ...IMAGE_MEDIA_TYPE_BY_EXTENSION,
65
+ ".csv": "text/csv",
66
+ ".htm": "text/html",
67
+ ".html": "text/html",
68
+ ".json": "application/json",
69
+ ".md": "text/markdown",
70
+ ".pdf": "application/pdf",
71
+ ".txt": "text/plain",
72
+ ".xml": "application/xml",
73
+ ".yaml": "application/yaml",
74
+ ".yml": "application/yaml"
75
+ };
76
+ var CLAUDE_IMAGE_MEDIA_TYPES = /* @__PURE__ */ new Set([
77
+ "image/gif",
78
+ "image/jpeg",
79
+ "image/png",
80
+ "image/webp"
81
+ ]);
82
+ var CLAUDE_TEXT_LIKE_MEDIA_TYPES = /* @__PURE__ */ new Set([
83
+ "application/json",
84
+ "application/xml",
85
+ "application/yaml",
86
+ "text/csv",
87
+ "text/html",
88
+ "text/markdown",
89
+ "text/plain",
90
+ "text/xml"
91
+ ]);
92
+ function normalizeUserInput(input) {
93
+ return typeof input === "string" ? [{ type: "text", text: input }] : input;
94
+ }
95
+ async function resolveUserInputParts(input) {
96
+ const parts = normalizeUserInput(input);
97
+ return Promise.all(parts.map((part) => resolveUserInputPart(part)));
98
+ }
99
+ async function validateProviderUserInput(provider, input) {
100
+ const parts = await resolveUserInputParts(input);
101
+ if (provider === AgentProvider.Codex) {
102
+ const unsupportedPart = parts.find((part) => part.type === "file");
103
+ if (unsupportedPart) {
104
+ throw new AgentBoxError(
105
+ `The codex provider does not yet support "${unsupportedPart.type}" input parts through codex app-server. Codex currently supports text and image input items.`,
106
+ {
107
+ code: "UNSUPPORTED_INPUT_PART",
108
+ details: {
109
+ provider,
110
+ partType: unsupportedPart.type
111
+ }
112
+ }
113
+ );
114
+ }
115
+ return parts;
116
+ }
117
+ if (provider === AgentProvider.ClaudeCode) {
118
+ for (const part of parts) {
119
+ if (part.type === "image") {
120
+ if (!CLAUDE_IMAGE_MEDIA_TYPES.has(part.mediaType)) {
121
+ throw new AgentBoxError(
122
+ `Claude Code only supports image inputs with one of these media types: ${Array.from(CLAUDE_IMAGE_MEDIA_TYPES).join(", ")}.`,
123
+ {
124
+ code: "UNSUPPORTED_INPUT_MEDIA_TYPE",
125
+ details: {
126
+ provider,
127
+ partType: part.type,
128
+ mediaType: part.mediaType
129
+ }
130
+ }
131
+ );
132
+ }
133
+ }
134
+ if (part.type === "file") {
135
+ if (part.mediaType !== "application/pdf" && !isClaudeTextLikeMediaType(part.mediaType)) {
136
+ throw new AgentBoxError(
137
+ `Claude Code only supports PDF and text-like file inputs. Received "${part.mediaType}".`,
138
+ {
139
+ code: "UNSUPPORTED_INPUT_MEDIA_TYPE",
140
+ details: {
141
+ provider,
142
+ partType: part.type,
143
+ mediaType: part.mediaType
144
+ }
145
+ }
146
+ );
147
+ }
148
+ if (part.source.type === "url" && part.mediaType !== "application/pdf" && isClaudeTextLikeMediaType(part.mediaType)) {
149
+ throw new AgentBoxError(
150
+ "Claude Code text-like file inputs must be provided as inline data, not a remote URL.",
151
+ {
152
+ code: "UNSUPPORTED_INPUT_SOURCE",
153
+ details: {
154
+ provider,
155
+ partType: part.type,
156
+ mediaType: part.mediaType,
157
+ sourceType: part.source.type
158
+ }
159
+ }
160
+ );
161
+ }
162
+ }
163
+ }
164
+ }
165
+ return parts;
166
+ }
167
+ function mapToOpenCodeParts(parts) {
168
+ return parts.map((part) => {
169
+ if (part.type === "text") {
170
+ return {
171
+ type: "text",
172
+ text: part.text
173
+ };
174
+ }
175
+ return {
176
+ type: "file",
177
+ url: binarySourceToUrl(part.source, part.mediaType),
178
+ mime: part.mediaType,
179
+ ...part.type === "file" && part.filename ? { filename: part.filename } : {}
180
+ };
181
+ });
182
+ }
183
+ function mapToClaudeUserContent(parts) {
184
+ if (parts.every((part) => part.type === "text")) {
185
+ return joinTextParts(parts);
186
+ }
187
+ return parts.map((part) => {
188
+ if (part.type === "text") {
189
+ return {
190
+ type: "text",
191
+ text: part.text
192
+ };
193
+ }
194
+ if (part.type === "image") {
195
+ if (part.source.type === "url" && isRemoteUrl(part.source.url)) {
196
+ return {
197
+ type: "image",
198
+ source: {
199
+ type: "url",
200
+ url: part.source.url
201
+ }
202
+ };
203
+ }
204
+ return {
205
+ type: "image",
206
+ source: {
207
+ type: "base64",
208
+ media_type: part.mediaType,
209
+ data: part.source.type === "base64" ? part.source.data : dataUrlToBase64(part.source.url)
210
+ }
211
+ };
212
+ }
213
+ if (part.mediaType === "application/pdf") {
214
+ if (part.source.type === "url" && isRemoteUrl(part.source.url)) {
215
+ return {
216
+ type: "document",
217
+ source: {
218
+ type: "url",
219
+ url: part.source.url
220
+ }
221
+ };
222
+ }
223
+ return {
224
+ type: "document",
225
+ source: {
226
+ type: "base64",
227
+ media_type: part.mediaType,
228
+ data: part.source.type === "base64" ? part.source.data : dataUrlToBase64(part.source.url)
229
+ }
230
+ };
231
+ }
232
+ if (!isClaudeTextLikeMediaType(part.mediaType)) {
233
+ throw new AgentBoxError(
234
+ `Claude Code cannot map file inputs with media type "${part.mediaType}".`,
235
+ {
236
+ code: "UNSUPPORTED_INPUT_MEDIA_TYPE",
237
+ details: {
238
+ partType: part.type,
239
+ mediaType: part.mediaType
240
+ }
241
+ }
242
+ );
243
+ }
244
+ return {
245
+ type: "document",
246
+ source: {
247
+ type: "text",
248
+ media_type: "text/plain",
249
+ data: Buffer.from(
250
+ part.source.type === "base64" ? part.source.data : dataUrlToBase64(part.source.url),
251
+ "base64"
252
+ ).toString("utf8")
253
+ }
254
+ };
255
+ });
256
+ }
257
+ async function mapToCodexPromptParts(parts, materializeLocalImage) {
258
+ const mapped = [];
259
+ for (const [index, part] of parts.entries()) {
260
+ if (part.type !== "image") {
261
+ continue;
262
+ }
263
+ if (part.source.type === "url" && isRemoteUrl(part.source.url)) {
264
+ mapped.push({
265
+ type: "image",
266
+ url: part.source.url
267
+ });
268
+ continue;
269
+ }
270
+ mapped.push({
271
+ type: "localImage",
272
+ path: await materializeLocalImage(part, index)
273
+ });
274
+ }
275
+ return mapped;
276
+ }
277
+ function joinTextParts(parts) {
278
+ return parts.map((part) => {
279
+ if (part.type !== "text") {
280
+ throw new AgentBoxError(
281
+ `Cannot join "${part.type}" input parts into a text-only prompt.`,
282
+ {
283
+ code: "UNSUPPORTED_INPUT_PART",
284
+ details: {
285
+ partType: part.type
286
+ }
287
+ }
288
+ );
289
+ }
290
+ return part.text;
291
+ }).join("");
292
+ }
293
+ async function resolveUserInputPart(part) {
294
+ if (part.type === "text") {
295
+ return part;
296
+ }
297
+ if (part.type === "image") {
298
+ const resolved2 = await resolveBinaryContent(part.image, {
299
+ kind: "image",
300
+ mediaType: part.mediaType
301
+ });
302
+ return {
303
+ type: "image",
304
+ mediaType: resolved2.mediaType,
305
+ source: resolved2.source
306
+ };
307
+ }
308
+ const resolved = await resolveBinaryContent(part.data, {
309
+ kind: "file",
310
+ mediaType: part.mediaType,
311
+ filename: part.filename
312
+ });
313
+ return {
314
+ type: "file",
315
+ mediaType: resolved.mediaType,
316
+ filename: resolved.filename,
317
+ source: resolved.source
318
+ };
319
+ }
320
+ async function resolveBinaryContent(content, options) {
321
+ if (content instanceof URL) {
322
+ return resolveUrlContent(content, options);
323
+ }
324
+ if (typeof content === "string") {
325
+ const url = parseStructuredUrl(content);
326
+ if (url) {
327
+ return resolveUrlContent(url, options);
328
+ }
329
+ const mediaType = resolveMediaType("", options);
330
+ return {
331
+ mediaType,
332
+ filename: options.filename,
333
+ source: {
334
+ type: "base64",
335
+ data: content
336
+ }
337
+ };
338
+ }
339
+ if (Buffer.isBuffer(content)) {
340
+ return {
341
+ mediaType: resolveMediaType("", options),
342
+ filename: options.filename,
343
+ source: {
344
+ type: "base64",
345
+ data: content.toString("base64")
346
+ }
347
+ };
348
+ }
349
+ if (content instanceof Uint8Array) {
350
+ return {
351
+ mediaType: resolveMediaType("", options),
352
+ filename: options.filename,
353
+ source: {
354
+ type: "base64",
355
+ data: Buffer.from(content).toString("base64")
356
+ }
357
+ };
358
+ }
359
+ if (content instanceof ArrayBuffer) {
360
+ return {
361
+ mediaType: resolveMediaType("", options),
362
+ filename: options.filename,
363
+ source: {
364
+ type: "base64",
365
+ data: Buffer.from(content).toString("base64")
366
+ }
367
+ };
368
+ }
369
+ throw new AgentBoxError("Unsupported input content type.", {
370
+ code: "UNSUPPORTED_INPUT_SOURCE",
371
+ details: {
372
+ kind: options.kind,
373
+ valueType: typeof content
374
+ }
375
+ });
376
+ }
377
+ async function resolveUrlContent(url, options) {
378
+ if (url.protocol === "data:") {
379
+ const parsed = parseDataUrl(url.toString());
380
+ return {
381
+ mediaType: resolveMediaType("", options, parsed.mediaType),
382
+ filename: options.filename,
383
+ source: {
384
+ type: "base64",
385
+ data: parsed.base64Data
386
+ }
387
+ };
388
+ }
389
+ if (url.protocol === "file:") {
390
+ const filePath = fileURLToPath(url);
391
+ const buffer = await readFile(filePath);
392
+ return {
393
+ mediaType: resolveMediaType(filePath, options),
394
+ filename: options.filename ?? path.basename(filePath),
395
+ source: {
396
+ type: "base64",
397
+ data: buffer.toString("base64")
398
+ }
399
+ };
400
+ }
401
+ if (url.protocol === "http:" || url.protocol === "https:") {
402
+ return {
403
+ mediaType: resolveMediaType(url.pathname, options),
404
+ filename: options.filename ?? inferFilename(url.pathname),
405
+ source: {
406
+ type: "url",
407
+ url: url.toString()
408
+ }
409
+ };
410
+ }
411
+ throw new AgentBoxError(
412
+ `Unsupported input URL protocol "${url.protocol}" for ${options.kind} parts.`,
413
+ {
414
+ code: "UNSUPPORTED_INPUT_SOURCE",
415
+ details: {
416
+ kind: options.kind,
417
+ protocol: url.protocol
418
+ }
419
+ }
420
+ );
421
+ }
422
+ function resolveMediaType(pathname, options, parsedMediaType) {
423
+ const explicitMediaType = options.mediaType ?? parsedMediaType;
424
+ if (explicitMediaType) {
425
+ return explicitMediaType;
426
+ }
427
+ const inferredMediaType = inferMediaType(pathname);
428
+ if (inferredMediaType) {
429
+ return inferredMediaType;
430
+ }
431
+ throw new AgentBoxError(
432
+ `Could not determine a media type for the ${options.kind} input part.`,
433
+ {
434
+ code: "MISSING_INPUT_MEDIA_TYPE",
435
+ details: {
436
+ kind: options.kind,
437
+ pathname
438
+ }
439
+ }
440
+ );
441
+ }
442
+ function inferMediaType(pathname) {
443
+ const extension = path.extname(pathname).toLowerCase();
444
+ return FILE_MEDIA_TYPE_BY_EXTENSION[extension];
445
+ }
446
+ function inferFilename(pathname) {
447
+ const name = path.basename(pathname);
448
+ return name && name !== "." ? name : void 0;
449
+ }
450
+ function parseStructuredUrl(value) {
451
+ if (!/^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(value)) {
452
+ return null;
453
+ }
454
+ try {
455
+ return new URL(value);
456
+ } catch {
457
+ return null;
458
+ }
459
+ }
460
+ function parseDataUrl(value) {
461
+ const separatorIndex = value.indexOf(",");
462
+ if (separatorIndex === -1) {
463
+ throw new AgentBoxError("Invalid data URL input.", {
464
+ code: "INVALID_INPUT_DATA_URL"
465
+ });
466
+ }
467
+ const header = value.slice(5, separatorIndex);
468
+ const body = value.slice(separatorIndex + 1);
469
+ const mediaType = header.split(";")[0] || void 0;
470
+ const isBase64 = header.includes(";base64");
471
+ return {
472
+ mediaType,
473
+ base64Data: isBase64 ? body : Buffer.from(decodeURIComponent(body), "utf8").toString("base64")
474
+ };
475
+ }
476
+ function binarySourceToUrl(source, mediaType) {
477
+ return source.type === "url" ? source.url : `data:${mediaType};base64,${source.data}`;
478
+ }
479
+ function dataUrlToBase64(value) {
480
+ return parseDataUrl(value).base64Data;
481
+ }
482
+ function isRemoteUrl(value) {
483
+ return value.startsWith("http://") || value.startsWith("https://");
484
+ }
485
+ function isClaudeTextLikeMediaType(mediaType) {
486
+ return mediaType.startsWith("text/") || CLAUDE_TEXT_LIKE_MEDIA_TYPES.has(mediaType);
487
+ }
488
+
489
+ // src/agents/config/commands.ts
490
+ import path2 from "path";
491
+ function buildFrontmatter(values) {
492
+ const lines = Object.entries(values).filter(([, value]) => value !== void 0).map(([key, value]) => `${key}: ${JSON.stringify(value)}`);
493
+ return lines.length > 0 ? `---
494
+ ${lines.join("\n")}
495
+ ---
496
+
497
+ ` : "";
498
+ }
499
+ function buildClaudeCommandArtifacts(commands, layout) {
500
+ return (commands ?? []).map((command) => ({
501
+ path: path2.join(layout.claudeDir, "commands", `${command.name}.md`),
502
+ content: buildFrontmatter({
503
+ description: command.description
504
+ }) + command.template
505
+ }));
506
+ }
507
+ function buildOpenCodeCommandsConfig(commands) {
508
+ if (!commands || commands.length === 0) {
509
+ return void 0;
510
+ }
511
+ return Object.fromEntries(
512
+ commands.map((command) => [
513
+ command.name,
514
+ {
515
+ template: command.template,
516
+ ...command.description ? { description: command.description } : {},
517
+ ...command.agent ? { agent: command.agent } : {},
518
+ ...command.model ? { model: command.model } : {},
519
+ ...command.subtask !== void 0 ? { subtask: command.subtask } : {}
520
+ }
521
+ ])
522
+ );
523
+ }
524
+ function assertCommandsSupported(provider, commands) {
525
+ if (!commands || commands.length === 0) {
526
+ return;
527
+ }
528
+ if (provider === AgentProvider.Codex) {
529
+ throw new Error(
530
+ "Custom commands are not supported for Codex in this package yet."
531
+ );
532
+ }
533
+ }
534
+
535
+ // src/agents/config/hooks.ts
536
+ import path3 from "path";
537
+ function hasHookEntries(hooks) {
538
+ if (!hooks || typeof hooks !== "object") {
539
+ return false;
540
+ }
541
+ return Object.values(hooks).some(
542
+ (groups) => Array.isArray(groups) && groups.length > 0
543
+ );
544
+ }
545
+ function readTopLevelHooks(options) {
546
+ if (!options || typeof options !== "object" || !("hooks" in options)) {
547
+ return void 0;
548
+ }
549
+ return options.hooks;
550
+ }
551
+ function readProviderHooks(options) {
552
+ if (!options || typeof options !== "object" || !("provider" in options)) {
553
+ return void 0;
554
+ }
555
+ return options.provider?.hooks;
556
+ }
557
+ function readProviderPlugins(options) {
558
+ if (!options || typeof options !== "object" || !("provider" in options)) {
559
+ return void 0;
560
+ }
561
+ return options.provider?.plugins;
562
+ }
563
+ function legacySharedHooksError(provider) {
564
+ return provider === AgentProvider.OpenCode ? "OpenCode hook plugins must be configured on options.provider.plugins. The shared options.hooks field was removed because hook semantics differ by provider." : `${provider === AgentProvider.ClaudeCode ? "Claude Code" : "Codex"} hooks must be configured on options.provider.hooks. The shared options.hooks field was removed because hook semantics differ by provider.`;
565
+ }
566
+ function invalidGroupedHooksShapeError(provider) {
567
+ return `${provider === AgentProvider.ClaudeCode ? "Claude Code" : "Codex"} hooks must use the native grouped hooks object shape under options.provider.hooks, with each event mapped to an array of matcher groups.`;
568
+ }
569
+ function hasMalformedGroupedHookEntries(hooks) {
570
+ return Object.values(hooks).some(
571
+ (groups) => groups !== void 0 && !Array.isArray(groups)
572
+ );
573
+ }
574
+ function opencodeHooksFieldError() {
575
+ return "OpenCode uses options.provider.plugins for native hook support. options.provider.hooks is not supported for opencode.";
576
+ }
577
+ function unexpectedPluginsFieldError(provider) {
578
+ return `OpenCode plugins are only supported for the opencode provider in this package. Configure them on options.provider.plugins for opencode; received plugin configuration for ${provider}.`;
579
+ }
580
+ function buildClaudeHookSettings(hooks) {
581
+ if (!hasHookEntries(hooks)) {
582
+ return void 0;
583
+ }
584
+ return { hooks };
585
+ }
586
+ function buildCodexHooksFile(hooks) {
587
+ if (!hasHookEntries(hooks)) {
588
+ return void 0;
589
+ }
590
+ return { hooks };
591
+ }
592
+ function toPluginFileName(name, extension) {
593
+ const base = name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
594
+ return `${base || "agentbox-plugin"}.${extension}`;
595
+ }
596
+ function toPluginExportName(name) {
597
+ const base = name.replace(/[^a-zA-Z0-9]+/g, " ").trim();
598
+ const parts = base.split(/\s+/).filter(Boolean).map((part) => part[0].toUpperCase() + part.slice(1));
599
+ const identifier = parts.join("") || "AgentBoxPlugin";
600
+ return /^[A-Za-z_]/.test(identifier) ? identifier : `AgentBoxPlugin${identifier}`;
601
+ }
602
+ function indentBlock(input, spaces) {
603
+ const indent = " ".repeat(spaces);
604
+ return input.split("\n").map((line) => `${indent}${line}`).join("\n");
605
+ }
606
+ function renderOpenCodePluginHook(hook) {
607
+ const body = hook.body.trim().length > 0 ? hook.body.trim() : "return undefined;";
608
+ return [
609
+ ` ${JSON.stringify(hook.event)}: async (input, output) => {`,
610
+ indentBlock(body, 6),
611
+ " },"
612
+ ].join("\n");
613
+ }
614
+ function buildOpenCodePluginSource(plugin) {
615
+ const exportName = toPluginExportName(plugin.name);
616
+ const setup = plugin.setup?.trim();
617
+ const preamble = plugin.preamble?.trim();
618
+ return [
619
+ ...preamble ? [preamble, ""] : [],
620
+ `export const ${exportName} = async (ctx) => {`,
621
+ ...setup ? [indentBlock(setup, 2)] : [],
622
+ " return {",
623
+ ...plugin.hooks.map((hook) => renderOpenCodePluginHook(hook)),
624
+ " };",
625
+ "};",
626
+ "",
627
+ `export default ${exportName};`,
628
+ ""
629
+ ].join("\n");
630
+ }
631
+ function buildOpenCodePluginArtifacts(plugins, opencodeDir) {
632
+ if (!plugins || plugins.length === 0) {
633
+ return [];
634
+ }
635
+ const seenFileNames = /* @__PURE__ */ new Set();
636
+ return plugins.map((plugin) => {
637
+ const fileExtension = plugin.fileExtension ?? "ts";
638
+ const fileName = toPluginFileName(plugin.name, fileExtension);
639
+ if (seenFileNames.has(fileName)) {
640
+ throw new Error(
641
+ `OpenCode plugin names must be unique after normalization. Duplicate plugin file: ${fileName}`
642
+ );
643
+ }
644
+ seenFileNames.add(fileName);
645
+ return {
646
+ path: path3.join(opencodeDir, "plugins", fileName),
647
+ content: buildOpenCodePluginSource(plugin)
648
+ };
649
+ });
650
+ }
651
+ function assertHooksSupported(provider, options) {
652
+ const topLevelHooks = readTopLevelHooks(options);
653
+ if (topLevelHooks !== void 0) {
654
+ throw new Error(legacySharedHooksError(provider));
655
+ }
656
+ const providerHooks = readProviderHooks(options);
657
+ const providerPlugins = readProviderPlugins(options);
658
+ if (provider === AgentProvider.OpenCode) {
659
+ if (providerHooks !== void 0) {
660
+ throw new Error(opencodeHooksFieldError());
661
+ }
662
+ if (providerPlugins === void 0) {
663
+ return void 0;
664
+ }
665
+ if (!Array.isArray(providerPlugins)) {
666
+ throw new Error(
667
+ "OpenCode plugins must be configured as an array on options.provider.plugins."
668
+ );
669
+ }
670
+ return providerPlugins.length > 0 ? providerPlugins : void 0;
671
+ }
672
+ if (providerPlugins !== void 0) {
673
+ throw new Error(unexpectedPluginsFieldError(provider));
674
+ }
675
+ if (providerHooks === void 0) {
676
+ return void 0;
677
+ }
678
+ if (!providerHooks || typeof providerHooks !== "object" || Array.isArray(providerHooks)) {
679
+ throw new Error(invalidGroupedHooksShapeError(provider));
680
+ }
681
+ if (hasMalformedGroupedHookEntries(providerHooks)) {
682
+ throw new Error(invalidGroupedHooksShapeError(provider));
683
+ }
684
+ if (!hasHookEntries(providerHooks)) {
685
+ return void 0;
686
+ }
687
+ return providerHooks;
688
+ }
689
+
690
+ // src/agents/config/mcp.ts
691
+ var SAFE_TOML_KEY = /^[a-zA-Z0-9_-]+$/;
692
+ function assertSafeTomlKey(name, context) {
693
+ if (!SAFE_TOML_KEY.test(name)) {
694
+ throw new Error(
695
+ `${context} name ${JSON.stringify(name)} contains characters that are not safe for TOML keys. Use only alphanumeric characters, hyphens, and underscores.`
696
+ );
697
+ }
698
+ }
699
+ function tomlString(value) {
700
+ return JSON.stringify(value);
701
+ }
702
+ function tomlStringArray(values) {
703
+ return `[${values.map(tomlString).join(", ")}]`;
704
+ }
705
+ function buildClaudeMcpConfig(mcps) {
706
+ if (!mcps || mcps.length === 0) {
707
+ return void 0;
708
+ }
709
+ const mcpServers = Object.fromEntries(
710
+ mcps.filter((mcp) => mcp.enabled !== false).map((mcp) => {
711
+ if (mcp.type === "remote") {
712
+ const headers = {
713
+ ...mcp.headers ?? {},
714
+ ...mcp.bearerTokenEnvVar ? {
715
+ Authorization: `Bearer \${${mcp.bearerTokenEnvVar}}`
716
+ } : {}
717
+ };
718
+ return [
719
+ mcp.name,
720
+ {
721
+ type: "http",
722
+ url: mcp.url,
723
+ ...Object.keys(headers).length > 0 ? { headers } : {}
724
+ }
725
+ ];
726
+ }
727
+ return [
728
+ mcp.name,
729
+ {
730
+ type: "stdio",
731
+ command: mcp.command,
732
+ ...mcp.args?.length ? { args: mcp.args } : {},
733
+ ...mcp.env ? { env: mcp.env } : {}
734
+ }
735
+ ];
736
+ })
737
+ );
738
+ return JSON.stringify({ mcpServers }, null, 2);
739
+ }
740
+ function buildOpenCodeMcpConfig(mcps) {
741
+ if (!mcps || mcps.length === 0) {
742
+ return void 0;
743
+ }
744
+ return Object.fromEntries(
745
+ mcps.filter((mcp) => mcp.enabled !== false).map((mcp) => {
746
+ if (mcp.type === "remote") {
747
+ const headers = {
748
+ ...mcp.headers ?? {},
749
+ ...mcp.bearerTokenEnvVar ? { Authorization: `Bearer {env:${mcp.bearerTokenEnvVar}}` } : {}
750
+ };
751
+ return [
752
+ mcp.name,
753
+ {
754
+ type: "remote",
755
+ url: mcp.url,
756
+ enabled: true,
757
+ ...Object.keys(headers).length > 0 ? { headers } : {}
758
+ }
759
+ ];
760
+ }
761
+ return [
762
+ mcp.name,
763
+ {
764
+ type: "local",
765
+ command: [mcp.command, ...mcp.args ?? []],
766
+ enabled: true,
767
+ ...mcp.env ? { env: mcp.env } : {}
768
+ }
769
+ ];
770
+ })
771
+ );
772
+ }
773
+ function buildCodexConfigToml(opts = {}) {
774
+ const {
775
+ mcps,
776
+ agentSections = [],
777
+ enableHooks = false,
778
+ enableSkills = false,
779
+ enableMultiAgent = false,
780
+ openAiBaseUrl
781
+ } = opts;
782
+ const blocks = [];
783
+ if (openAiBaseUrl) {
784
+ blocks.push(`openai_base_url = ${tomlString(openAiBaseUrl)}`);
785
+ blocks.push("");
786
+ }
787
+ for (const mcp of mcps ?? []) {
788
+ if (mcp.enabled === false) {
789
+ continue;
790
+ }
791
+ assertSafeTomlKey(mcp.name, "MCP server");
792
+ if (mcp.type === "remote") {
793
+ if (mcp.headers && Object.keys(mcp.headers).length > 0) {
794
+ throw new Error(
795
+ `Codex only supports remote MCPs with bearerTokenEnvVar in this package. MCP "${mcp.name}" includes raw headers.`
796
+ );
797
+ }
798
+ blocks.push(`[mcp_servers.${mcp.name}]`);
799
+ blocks.push(`url = ${tomlString(mcp.url)}`);
800
+ if (mcp.bearerTokenEnvVar) {
801
+ blocks.push(
802
+ `bearer_token_env_var = ${tomlString(mcp.bearerTokenEnvVar)}`
803
+ );
804
+ }
805
+ blocks.push("");
806
+ continue;
807
+ }
808
+ blocks.push(`[mcp_servers.${mcp.name}]`);
809
+ blocks.push(`command = ${tomlString(mcp.command)}`);
810
+ if (mcp.args?.length) {
811
+ blocks.push(`args = ${tomlStringArray(mcp.args)}`);
812
+ }
813
+ if (mcp.env && Object.keys(mcp.env).length > 0) {
814
+ blocks.push(`env_vars = ${tomlStringArray(Object.keys(mcp.env))}`);
815
+ }
816
+ blocks.push("");
817
+ }
818
+ const featureLines = [];
819
+ if (enableHooks) featureLines.push("codex_hooks = true");
820
+ if (enableSkills) featureLines.push("skills = true");
821
+ if (enableMultiAgent) featureLines.push("multi_agent = true");
822
+ if (featureLines.length > 0) {
823
+ blocks.push("[features]");
824
+ blocks.push(...featureLines);
825
+ blocks.push("");
826
+ }
827
+ blocks.push(...agentSections);
828
+ if (blocks.length === 0) {
829
+ return void 0;
830
+ }
831
+ return `${blocks.join("\n").trim()}
832
+ `;
833
+ }
834
+
835
+ // src/agents/config/setup.ts
836
+ import { mkdir, chmod, rm, writeFile } from "fs/promises";
837
+ import os from "os";
838
+ import path4 from "path";
839
+
840
+ // src/agents/transports/spawn.ts
841
+ import { spawn } from "child_process";
842
+ import { createInterface } from "readline";
843
+ function spawnCommand(options) {
844
+ const child = spawn(options.command, options.args ?? [], {
845
+ cwd: options.cwd,
846
+ env: options.env,
847
+ stdio: "pipe",
848
+ shell: process.platform === "win32",
849
+ windowsHide: true
850
+ });
851
+ const exitPromise = new Promise((resolve, reject) => {
852
+ child.once("error", reject);
853
+ child.once("close", (code) => resolve(code ?? 0));
854
+ });
855
+ return {
856
+ child,
857
+ wait: () => exitPromise,
858
+ kill: async (signal = "SIGTERM") => {
859
+ child.kill(signal);
860
+ await exitPromise.catch(() => void 0);
861
+ }
862
+ };
863
+ }
864
+ async function waitForHttpReady(url, options) {
865
+ await waitFor(
866
+ async () => {
867
+ try {
868
+ const response = await fetch(url, options?.init);
869
+ return response.ok;
870
+ } catch {
871
+ return false;
872
+ }
873
+ },
874
+ {
875
+ timeoutMs: options?.timeoutMs,
876
+ intervalMs: options?.intervalMs
877
+ }
878
+ );
879
+ }
880
+ async function* linesFromNodeStream(stream) {
881
+ const lineReader = createInterface({ input: stream });
882
+ try {
883
+ for await (const line of lineReader) {
884
+ yield line;
885
+ }
886
+ } finally {
887
+ lineReader.close();
888
+ }
889
+ }
890
+
891
+ // src/agents/config/setup.ts
892
+ function shortLabel(command) {
893
+ const oneLine = command.replace(/\s+/g, " ").trim();
894
+ return oneLine.length > 60 ? `${oneLine.slice(0, 60)}\u2026` : oneLine;
895
+ }
896
+ function agentboxRoot(provider, hasSandbox) {
897
+ return hasSandbox ? `/tmp/agentbox/${provider}` : path4.join(os.tmpdir(), `agentbox-${provider}`);
898
+ }
899
+ function buildLayout(rootDir) {
900
+ const xdgConfigHome = path4.join(rootDir, ".config");
901
+ return {
902
+ rootDir,
903
+ homeDir: rootDir,
904
+ xdgConfigHome,
905
+ agentsDir: path4.join(rootDir, ".agents"),
906
+ claudeDir: path4.join(rootDir, ".claude"),
907
+ opencodeDir: path4.join(xdgConfigHome, "opencode"),
908
+ codexDir: path4.join(rootDir, ".codex")
909
+ };
910
+ }
911
+ function buildLayoutEnv(provider, layout) {
912
+ switch (provider) {
913
+ case "claude-code":
914
+ return { CLAUDE_CONFIG_DIR: layout.claudeDir };
915
+ case "codex":
916
+ return { CODEX_HOME: layout.codexDir };
917
+ case "open-code":
918
+ return {
919
+ OPENCODE_CONFIG: path4.join(layout.opencodeDir, "agentbox.json"),
920
+ OPENCODE_CONFIG_DIR: layout.opencodeDir
921
+ };
922
+ }
923
+ }
924
+ var HostSetupTarget = class {
925
+ constructor(provider, layout, cwd, baseEnv) {
926
+ this.provider = provider;
927
+ this.layout = layout;
928
+ this.cwd = cwd;
929
+ this.baseEnv = baseEnv;
930
+ this.env = buildLayoutEnv(provider, layout);
931
+ }
932
+ provider;
933
+ layout;
934
+ cwd;
935
+ baseEnv;
936
+ env;
937
+ /**
938
+ * Host implementation: write each artifact directly to the local
939
+ * filesystem (we're already on the box), then run the command via
940
+ * `sh -c`. No tarball needed since there's no RPC to amortize.
941
+ */
942
+ async uploadAndRun(files, command) {
943
+ return time(
944
+ debugRuntime,
945
+ `host uploadAndRun ${shortLabel(command)}`,
946
+ async () => {
947
+ await Promise.all(
948
+ files.map(async (entry) => {
949
+ await mkdir(path4.dirname(entry.path), { recursive: true });
950
+ const content = typeof entry.content === "string" ? entry.content : entry.content;
951
+ await writeFile(entry.path, content);
952
+ if (entry.mode && (entry.mode & 73) !== 0) {
953
+ await chmod(entry.path, entry.mode);
954
+ }
955
+ })
956
+ );
957
+ const handle = spawnCommand({
958
+ command: "sh",
959
+ args: ["-c", command],
960
+ cwd: this.cwd,
961
+ env: {
962
+ ...process.env,
963
+ ...this.baseEnv,
964
+ ...this.env
965
+ }
966
+ });
967
+ const exitCode = await handle.wait();
968
+ return {
969
+ exitCode,
970
+ stdout: "",
971
+ stderr: "",
972
+ combinedOutput: ""
973
+ };
974
+ },
975
+ (result) => ({ exit: result.exitCode, files: files.length })
976
+ );
977
+ }
978
+ async runCommand(command, extraEnv) {
979
+ await time(
980
+ debugRuntime,
981
+ `host runCommand ${shortLabel(command)}`,
982
+ async () => {
983
+ const handle = spawnCommand({
984
+ command: process.env.SHELL || "sh",
985
+ args: ["-c", command],
986
+ cwd: this.cwd,
987
+ env: {
988
+ ...process.env,
989
+ ...this.baseEnv,
990
+ ...this.env,
991
+ ...extraEnv ?? {}
992
+ }
993
+ });
994
+ const exitCode = await handle.wait();
995
+ if (exitCode !== 0) {
996
+ throw new Error(`Setup command failed (${exitCode}): ${command}`);
997
+ }
998
+ }
999
+ );
1000
+ }
1001
+ async cleanup() {
1002
+ await rm(this.layout.rootDir, { recursive: true, force: true });
1003
+ }
1004
+ };
1005
+ var SandboxSetupTarget = class {
1006
+ constructor(provider, layout, options) {
1007
+ this.provider = provider;
1008
+ this.layout = layout;
1009
+ this.options = options;
1010
+ this.env = buildLayoutEnv(provider, layout);
1011
+ }
1012
+ provider;
1013
+ layout;
1014
+ options;
1015
+ env;
1016
+ /**
1017
+ * Sandbox implementation: delegate to `Sandbox.uploadAndRun` so the
1018
+ * tarball + extract + exec all happen in a single Modal RPC. This is
1019
+ * the hot path used by `applyDifferentialSetup`.
1020
+ */
1021
+ async uploadAndRun(files, command) {
1022
+ const sandbox = this.options.sandbox;
1023
+ if (!sandbox) {
1024
+ throw new Error(
1025
+ "SandboxSetupTarget.uploadAndRun called without a sandbox."
1026
+ );
1027
+ }
1028
+ return time(
1029
+ debugRuntime,
1030
+ `sandbox uploadAndRun ${shortLabel(command)}`,
1031
+ () => sandbox.uploadAndRun(files, command, {
1032
+ cwd: this.options.cwd,
1033
+ env: {
1034
+ ...this.options.env ?? {},
1035
+ ...this.env
1036
+ }
1037
+ }),
1038
+ (result) => ({ exit: result.exitCode, files: files.length })
1039
+ );
1040
+ }
1041
+ async runCommand(command, extraEnv) {
1042
+ await time(
1043
+ debugRuntime,
1044
+ `sandbox runCommand ${shortLabel(command)}`,
1045
+ async () => {
1046
+ const result = await this.options.sandbox?.run(command, {
1047
+ cwd: this.options.cwd,
1048
+ env: {
1049
+ ...this.options.env ?? {},
1050
+ ...this.env,
1051
+ ...extraEnv ?? {}
1052
+ }
1053
+ });
1054
+ if (result && result.exitCode !== 0) {
1055
+ throw new Error(
1056
+ `Sandbox setup command failed (${result.exitCode}): ${command}`
1057
+ );
1058
+ }
1059
+ }
1060
+ );
1061
+ }
1062
+ async cleanup() {
1063
+ }
1064
+ };
1065
+ async function createSetupTarget(provider, setupId, options) {
1066
+ return time(debugRuntime, `createSetupTarget ${provider}`, async () => {
1067
+ void setupId;
1068
+ const layout = buildLayout(
1069
+ agentboxRoot(provider, Boolean(options.sandbox))
1070
+ );
1071
+ if (options.sandbox) {
1072
+ return new SandboxSetupTarget(provider, layout, options);
1073
+ }
1074
+ await mkdir(layout.homeDir, { recursive: true });
1075
+ await mkdir(layout.xdgConfigHome, { recursive: true });
1076
+ await mkdir(layout.agentsDir, { recursive: true });
1077
+ await mkdir(layout.claudeDir, { recursive: true });
1078
+ await mkdir(layout.opencodeDir, { recursive: true });
1079
+ await mkdir(layout.codexDir, { recursive: true });
1080
+ return new HostSetupTarget(
1081
+ provider,
1082
+ layout,
1083
+ options.cwd ?? process.cwd(),
1084
+ options.env ?? {}
1085
+ );
1086
+ });
1087
+ }
1088
+
1089
+ // src/agents/config/setup-manifest.ts
1090
+ import { createHash } from "crypto";
1091
+ import path5 from "path";
1092
+ var MANIFEST_FILENAME = "setup-manifest.json";
1093
+ var TARGET_MANIFEST_FILENAME = "setup-target.json";
1094
+ var INSTALL_SCRIPT_FILENAME = "install.sh";
1095
+ var MANIFEST_VERSION = 1;
1096
+ function hashArtifact(artifact) {
1097
+ const hasher = createHash("sha256");
1098
+ hasher.update(artifact.executable ? "1" : "0");
1099
+ hasher.update("\0");
1100
+ hasher.update(artifact.content, "utf8");
1101
+ return hasher.digest("hex");
1102
+ }
1103
+ function hashCommand(command) {
1104
+ return createHash("sha256").update(command, "utf8").digest("hex");
1105
+ }
1106
+ function computeTargetArtifacts(artifacts) {
1107
+ const result = {};
1108
+ for (const artifact of artifacts) {
1109
+ result[artifact.path] = hashArtifact(artifact);
1110
+ }
1111
+ return result;
1112
+ }
1113
+ function buildInstallScript(rootDir, installCommandsByKey) {
1114
+ const commandsB64 = Buffer.from(
1115
+ JSON.stringify(installCommandsByKey),
1116
+ "utf8"
1117
+ ).toString("base64");
1118
+ return `#!/usr/bin/env bash
1119
+ set -e
1120
+ ROOT_DIR=${shellQuote(rootDir)}
1121
+ TARGET_MANIFEST="$ROOT_DIR/${TARGET_MANIFEST_FILENAME}"
1122
+ EXISTING_MANIFEST="$ROOT_DIR/${MANIFEST_FILENAME}"
1123
+ export TARGET_MANIFEST EXISTING_MANIFEST
1124
+
1125
+ # Compute stale commands: any command whose hash in the target manifest
1126
+ # differs from (or is missing in) the existing manifest. The python block
1127
+ # emits the commands themselves, NUL-separated, so bash never has to
1128
+ # deserialize JSON.
1129
+ STALE_CMDS_FILE="$(mktemp)"
1130
+ trap 'rm -f "$STALE_CMDS_FILE"' EXIT
1131
+ COMMANDS_B64=${shellQuote(commandsB64)} \\
1132
+ MANIFEST_VERSION=${MANIFEST_VERSION} \\
1133
+ python3 - <<'PY' > "$STALE_CMDS_FILE"
1134
+ import base64, json, os, sys
1135
+ with open(os.environ["TARGET_MANIFEST"], "r", encoding="utf-8") as fh:
1136
+ target = json.load(fh)
1137
+ existing = {}
1138
+ try:
1139
+ with open(os.environ["EXISTING_MANIFEST"], "r", encoding="utf-8") as fh:
1140
+ existing = json.load(fh)
1141
+ except FileNotFoundError:
1142
+ pass
1143
+ expected_version = int(os.environ["MANIFEST_VERSION"])
1144
+ target_cmds = target.get("installCommands", {})
1145
+ existing_cmds = (
1146
+ existing.get("installCommands", {})
1147
+ if existing.get("version") == expected_version
1148
+ else {}
1149
+ )
1150
+ commands = json.loads(base64.b64decode(os.environ["COMMANDS_B64"]))
1151
+ stale = [
1152
+ commands[key]
1153
+ for key, hashed in target_cmds.items()
1154
+ if commands.get(key) is not None and existing_cmds.get(key) != hashed
1155
+ ]
1156
+ sys.stdout.write("\\0".join(stale))
1157
+ PY
1158
+
1159
+ if [ -s "$STALE_CMDS_FILE" ]; then
1160
+ while IFS= read -r -d '' CMD; do
1161
+ [ -z "$CMD" ] && continue
1162
+ bash -c "$CMD" &
1163
+ done < "$STALE_CMDS_FILE"
1164
+ wait
1165
+ fi
1166
+
1167
+ # Persist target manifest as the new manifest atomically. Doing this last
1168
+ # means an interrupted install run doesn't poison the cache for next time.
1169
+ mv "$TARGET_MANIFEST" "$EXISTING_MANIFEST"
1170
+ `;
1171
+ }
1172
+ async function applyDifferentialSetup(target, artifacts, installCommands) {
1173
+ await time(
1174
+ debugSetup,
1175
+ `applyDifferentialSetup ${target.provider}`,
1176
+ async () => {
1177
+ const installCommandsByKey = {};
1178
+ for (let i = 0; i < installCommands.length; i++) {
1179
+ installCommandsByKey[`cmd${i}`] = installCommands[i];
1180
+ }
1181
+ const targetForSandbox = {
1182
+ version: MANIFEST_VERSION,
1183
+ artifacts: computeTargetArtifacts(artifacts),
1184
+ installCommands: Object.fromEntries(
1185
+ Object.entries(installCommandsByKey).map(([key, command]) => [
1186
+ key,
1187
+ hashCommand(command)
1188
+ ])
1189
+ )
1190
+ };
1191
+ const rootDir = target.layout.rootDir;
1192
+ const tarballEntries = [
1193
+ ...artifacts.map((artifact) => ({
1194
+ path: artifact.path,
1195
+ content: artifact.content,
1196
+ mode: artifact.executable ? 493 : 420
1197
+ })),
1198
+ {
1199
+ path: path5.posix.join(rootDir, TARGET_MANIFEST_FILENAME),
1200
+ content: JSON.stringify(targetForSandbox),
1201
+ mode: 420
1202
+ },
1203
+ {
1204
+ path: path5.posix.join(rootDir, INSTALL_SCRIPT_FILENAME),
1205
+ content: buildInstallScript(rootDir, installCommandsByKey),
1206
+ mode: 493
1207
+ }
1208
+ ];
1209
+ await target.uploadAndRun(
1210
+ tarballEntries,
1211
+ `bash ${shellQuote(path5.posix.join(rootDir, INSTALL_SCRIPT_FILENAME))}`
1212
+ );
1213
+ },
1214
+ () => ({
1215
+ artifacts: artifacts.length,
1216
+ installCommands: installCommands.length
1217
+ })
1218
+ );
1219
+ }
1220
+
1221
+ // src/agents/config/skills.ts
1222
+ import path6 from "path";
1223
+ function getSkillTargetDir(provider, layout, skillName) {
1224
+ switch (provider) {
1225
+ case AgentProvider.ClaudeCode:
1226
+ return path6.join(layout.claudeDir, "skills", skillName);
1227
+ case AgentProvider.OpenCode:
1228
+ return path6.join(layout.opencodeDir, "skills", skillName);
1229
+ case AgentProvider.Codex:
1230
+ return path6.join(layout.codexDir, "skills", skillName);
1231
+ }
1232
+ }
1233
+ function skillsCliAgentName(provider) {
1234
+ return provider === AgentProvider.OpenCode ? "opencode" : provider;
1235
+ }
1236
+ function buildSkillsInstallerCommand(provider, skill) {
1237
+ const repo = skill.repo ?? "https://github.com/anthropics/skills";
1238
+ return `npx skills add ${shellQuote(repo)} -g --skill ${shellQuote(skill.name)} --agent ${shellQuote(skillsCliAgentName(provider))} -y`;
1239
+ }
1240
+ async function prepareSkillArtifacts(provider, skills, layout) {
1241
+ const artifacts = [];
1242
+ const installCommands = [];
1243
+ const preparedSkills = [];
1244
+ for (const skill of skills ?? []) {
1245
+ const targetDir = getSkillTargetDir(provider, layout, skill.name);
1246
+ const skillFilePath = path6.join(targetDir, "SKILL.md");
1247
+ if (!("files" in skill)) {
1248
+ installCommands.push(buildSkillsInstallerCommand(provider, skill));
1249
+ preparedSkills.push({ name: skill.name, skillFilePath });
1250
+ continue;
1251
+ }
1252
+ for (const [relativePath, content] of Object.entries(skill.files)) {
1253
+ artifacts.push({
1254
+ path: path6.join(targetDir, relativePath),
1255
+ content,
1256
+ executable: relativePath.startsWith("scripts/")
1257
+ });
1258
+ }
1259
+ preparedSkills.push({ name: skill.name, skillFilePath });
1260
+ }
1261
+ return {
1262
+ artifacts,
1263
+ installCommands,
1264
+ preparedSkills
1265
+ };
1266
+ }
1267
+
1268
+ // src/agents/config/subagents.ts
1269
+ import path7 from "path";
1270
+ function toToolsArray(tools) {
1271
+ const filtered = tools?.map((tool) => tool.trim()).filter(Boolean);
1272
+ return filtered && filtered.length > 0 ? filtered : void 0;
1273
+ }
1274
+ function tomlString2(value) {
1275
+ return JSON.stringify(value);
1276
+ }
1277
+ function yamlScalar(value) {
1278
+ return `'${value.replace(/'/g, "''")}'`;
1279
+ }
1280
+ function buildClaudeSubagentArtifacts(subAgents, layout) {
1281
+ if (!subAgents || subAgents.length === 0) {
1282
+ return [];
1283
+ }
1284
+ return subAgents.map((subAgent) => {
1285
+ const tools = toToolsArray(subAgent.tools);
1286
+ const frontmatterLines = [
1287
+ `name: ${yamlScalar(subAgent.name)}`,
1288
+ `description: ${yamlScalar(subAgent.description)}`,
1289
+ ...subAgent.model ? [`model: ${yamlScalar(subAgent.model)}`] : [],
1290
+ ...tools ? [`tools: ${tools.join(", ")}`] : []
1291
+ ];
1292
+ const content = `---
1293
+ ${frontmatterLines.join("\n")}
1294
+ ---
1295
+
1296
+ ${subAgent.instructions.trim()}
1297
+ `;
1298
+ return {
1299
+ path: path7.join(layout.claudeDir, "agents", `${subAgent.name}.md`),
1300
+ content
1301
+ };
1302
+ });
1303
+ }
1304
+ function mapOpenCodeTools(tools) {
1305
+ if (!tools || tools.length === 0) {
1306
+ return void 0;
1307
+ }
1308
+ return Object.fromEntries(tools.map((tool) => [tool, true]));
1309
+ }
1310
+ function buildOpenCodeSubagentConfig(subAgents) {
1311
+ return Object.fromEntries(
1312
+ (subAgents ?? []).map((subAgent) => [
1313
+ subAgent.name,
1314
+ {
1315
+ mode: "subagent",
1316
+ description: subAgent.description,
1317
+ prompt: subAgent.instructions,
1318
+ ...mapOpenCodeTools(subAgent.tools) ? { tools: mapOpenCodeTools(subAgent.tools) } : {}
1319
+ }
1320
+ ])
1321
+ );
1322
+ }
1323
+ function buildCodexSubagentArtifacts(subAgents, layout) {
1324
+ const artifacts = [];
1325
+ const agentSections = [];
1326
+ for (const subAgent of subAgents ?? []) {
1327
+ const roleConfigRelativePath = `./agents/${subAgent.name}.toml`;
1328
+ const roleConfigPromptPath = `../prompts/${subAgent.name}.md`;
1329
+ artifacts.push({
1330
+ path: path7.join(layout.codexDir, "prompts", `${subAgent.name}.md`),
1331
+ content: subAgent.instructions
1332
+ });
1333
+ const tomlLines = [
1334
+ `model_instructions_file = ${tomlString2(roleConfigPromptPath)}`
1335
+ ];
1336
+ if (subAgent.model) {
1337
+ tomlLines.push(`model = ${tomlString2(subAgent.model)}`);
1338
+ }
1339
+ tomlLines.push(
1340
+ `model_reasoning_effort = ${tomlString2("medium")}`,
1341
+ "",
1342
+ "[features]",
1343
+ "multi_agent = false",
1344
+ ""
1345
+ );
1346
+ artifacts.push({
1347
+ path: path7.join(layout.codexDir, "agents", `${subAgent.name}.toml`),
1348
+ content: tomlLines.join("\n")
1349
+ });
1350
+ agentSections.push(
1351
+ `[agents.${subAgent.name}]`,
1352
+ `description = ${tomlString2(subAgent.description)}`,
1353
+ `config_file = ${tomlString2(roleConfigRelativePath)}`,
1354
+ ""
1355
+ );
1356
+ }
1357
+ return {
1358
+ artifacts,
1359
+ agentSections,
1360
+ enableMultiAgent: (subAgents?.length ?? 0) > 0
1361
+ };
1362
+ }
1363
+
1364
+ // src/agents/cost.ts
1365
+ function addIfNumber(target, key, value) {
1366
+ if (typeof value !== "number" || !Number.isFinite(value)) {
1367
+ return;
1368
+ }
1369
+ target[key] = (target[key] ?? 0) + value;
1370
+ }
1371
+ function asRecord(value) {
1372
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
1373
+ }
1374
+ function mergeUsage(target, source) {
1375
+ if (!source) {
1376
+ return;
1377
+ }
1378
+ addIfNumber(target, "input_tokens", source.input_tokens);
1379
+ addIfNumber(target, "input_tokens", source.inputTokens);
1380
+ addIfNumber(target, "input_tokens", source.input);
1381
+ addIfNumber(target, "output_tokens", source.output_tokens);
1382
+ addIfNumber(target, "output_tokens", source.outputTokens);
1383
+ addIfNumber(target, "output_tokens", source.output);
1384
+ addIfNumber(
1385
+ target,
1386
+ "cache_read_input_tokens",
1387
+ source.cache_read_input_tokens
1388
+ );
1389
+ addIfNumber(target, "cache_read_input_tokens", source.cached_input_tokens);
1390
+ addIfNumber(target, "cache_read_input_tokens", source.cachedInputTokens);
1391
+ addIfNumber(
1392
+ target,
1393
+ "cache_creation_input_tokens",
1394
+ source.cache_creation_input_tokens
1395
+ );
1396
+ addIfNumber(target, "cache_creation_input_tokens", source.cacheWrite);
1397
+ const cache = asRecord(source.cache);
1398
+ if (cache) {
1399
+ addIfNumber(target, "cache_read_input_tokens", cache.read);
1400
+ addIfNumber(target, "cache_creation_input_tokens", cache.write);
1401
+ }
1402
+ }
1403
+ function compactCostData(costData) {
1404
+ const usage = costData.usage ? Object.fromEntries(
1405
+ Object.entries(costData.usage).filter(([, value]) => value !== 0)
1406
+ ) : void 0;
1407
+ const compacted = {
1408
+ ...costData.total_cost_usd !== void 0 ? { total_cost_usd: costData.total_cost_usd } : {},
1409
+ ...costData.duration_ms !== void 0 ? { duration_ms: costData.duration_ms } : {},
1410
+ ...costData.duration_api_ms !== void 0 ? { duration_api_ms: costData.duration_api_ms } : {},
1411
+ ...costData.num_turns !== void 0 ? { num_turns: costData.num_turns } : {},
1412
+ ...usage && Object.keys(usage).length > 0 ? { usage } : {}
1413
+ };
1414
+ return Object.keys(compacted).length > 0 ? compacted : null;
1415
+ }
1416
+ function extractClaudeCostData(events) {
1417
+ for (let i = events.length - 1; i >= 0; i--) {
1418
+ const event = events[i];
1419
+ if (!event) {
1420
+ continue;
1421
+ }
1422
+ if (event.type !== "result") {
1423
+ continue;
1424
+ }
1425
+ const totalCost = typeof event.total_cost_usd === "number" ? event.total_cost_usd : void 0;
1426
+ const usage = {};
1427
+ const modelUsage = asRecord(event.modelUsage);
1428
+ if (modelUsage) {
1429
+ for (const value of Object.values(modelUsage)) {
1430
+ mergeUsage(usage, asRecord(value));
1431
+ }
1432
+ }
1433
+ return compactCostData({
1434
+ ...totalCost !== void 0 ? { total_cost_usd: totalCost } : {},
1435
+ duration_ms: typeof event.duration_ms === "number" ? event.duration_ms : void 0,
1436
+ duration_api_ms: typeof event.duration_api_ms === "number" ? event.duration_api_ms : void 0,
1437
+ num_turns: typeof event.num_turns === "number" ? event.num_turns : void 0,
1438
+ usage
1439
+ });
1440
+ }
1441
+ return null;
1442
+ }
1443
+ function extractCodexCostData(events) {
1444
+ const usage = {};
1445
+ let sawUsage = false;
1446
+ for (const event of events) {
1447
+ const params = asRecord(event.params);
1448
+ const usageCandidate = asRecord(event.usage) ?? asRecord(params?.usage) ?? asRecord(params?.tokenUsage) ?? asRecord(asRecord(params?.turn)?.usage) ?? asRecord(asRecord(params?.turn)?.tokenUsage);
1449
+ if (usageCandidate) {
1450
+ sawUsage = true;
1451
+ mergeUsage(usage, usageCandidate);
1452
+ }
1453
+ }
1454
+ return sawUsage ? compactCostData({ usage }) : null;
1455
+ }
1456
+ function extractOpenCodeCostData(events) {
1457
+ const usage = {};
1458
+ let totalCost = 0;
1459
+ let sawCostData = false;
1460
+ for (const event of events) {
1461
+ const properties = asRecord(event.properties);
1462
+ const part = asRecord(properties?.part) ?? asRecord(event.part);
1463
+ if (part?.type !== "step-finish") {
1464
+ continue;
1465
+ }
1466
+ if (typeof part.cost === "number") {
1467
+ totalCost += part.cost;
1468
+ sawCostData = true;
1469
+ }
1470
+ const tokens = asRecord(part.tokens);
1471
+ if (tokens) {
1472
+ sawCostData = true;
1473
+ mergeUsage(usage, tokens);
1474
+ }
1475
+ }
1476
+ return sawCostData ? compactCostData({
1477
+ ...totalCost > 0 ? { total_cost_usd: totalCost } : {},
1478
+ usage
1479
+ }) : null;
1480
+ }
1481
+
1482
+ // src/agents/providers/claude-code.ts
1483
+ var DAEMON_PROTOCOL_VERSION = "1";
1484
+ var DAEMON_PORT = 43180;
1485
+ var DAEMON_PATH = "/tmp/agentbox/claude-code/daemon.mjs";
1486
+ var DAEMON_LOG_PATH = "/tmp/agentbox/claude-code/daemon.log";
1487
+ var DAEMON_PID_PATH = "/tmp/agentbox/claude-code/daemon.pid";
1488
+ function claudeConfigDir(options) {
1489
+ return path8.join(
1490
+ agentboxRoot(AgentProvider.ClaudeCode, Boolean(options.sandbox)),
1491
+ ".claude"
1492
+ );
1493
+ }
1494
+ function buildClaudeQueryOptions(params) {
1495
+ const provider = params.request.options.provider;
1496
+ const run = params.request.run;
1497
+ const extraArgs = {
1498
+ "mcp-config": params.mcpConfigPath
1499
+ };
1500
+ for (const arg of provider?.args ?? []) {
1501
+ if (typeof arg !== "string") continue;
1502
+ if (arg.startsWith("--")) {
1503
+ extraArgs[arg.slice(2)] = null;
1504
+ }
1505
+ }
1506
+ if (run.systemPrompt) {
1507
+ extraArgs["append-system-prompt"] = run.systemPrompt;
1508
+ }
1509
+ return {
1510
+ cwd: params.cwd ?? params.request.options.cwd,
1511
+ env: params.env,
1512
+ pathToClaudeCodeExecutable: provider?.binary ?? "claude",
1513
+ settings: params.settingsPath,
1514
+ extraArgs,
1515
+ includePartialMessages: true,
1516
+ ...run.model ? { model: run.model } : {},
1517
+ ...run.reasoning ? { effort: run.reasoning } : {},
1518
+ ...provider?.permissionMode ? { permissionMode: provider.permissionMode } : {},
1519
+ ...provider?.permissionMode === "bypassPermissions" ? { allowDangerouslySkipPermissions: true } : {},
1520
+ ...provider?.allowedTools?.length ? { allowedTools: provider.allowedTools } : {},
1521
+ ...run.resumeSessionId ? { resume: run.resumeSessionId } : {}
1522
+ };
1523
+ }
1524
+ function toRawEvent(runId, payload, type) {
1525
+ return {
1526
+ provider: AgentProvider.ClaudeCode,
1527
+ runId,
1528
+ type,
1529
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1530
+ payload
1531
+ };
1532
+ }
1533
+ function extractAssistantText(message) {
1534
+ const content = message.message?.content;
1535
+ if (!Array.isArray(content)) return "";
1536
+ return content.filter((block) => block.type === "text").map((block) => String(block.text ?? "")).join("");
1537
+ }
1538
+ function extractAssistantThinking(message) {
1539
+ const content = message.message?.content;
1540
+ if (!Array.isArray(content)) return "";
1541
+ return content.filter(
1542
+ (block) => block.type === "thinking" || block.type === "redacted_thinking"
1543
+ ).map((block) => String(block.thinking ?? "")).filter(Boolean).join("");
1544
+ }
1545
+ function extractStreamDeltas(message) {
1546
+ const event = message.event;
1547
+ if (!event || event.type !== "content_block_delta") {
1548
+ return { text: "", thinking: "" };
1549
+ }
1550
+ const delta = event.delta;
1551
+ if (!delta) return { text: "", thinking: "" };
1552
+ if (delta.type === "text_delta" && typeof delta.text === "string") {
1553
+ return { text: delta.text, thinking: "" };
1554
+ }
1555
+ if (delta.type === "thinking_delta" && typeof delta.thinking === "string") {
1556
+ return { text: "", thinking: delta.thinking };
1557
+ }
1558
+ return { text: "", thinking: "" };
1559
+ }
1560
+ function createClaudeCodeDaemonScript() {
1561
+ const version = JSON.stringify(DAEMON_PROTOCOL_VERSION);
1562
+ return `import http from "node:http";
1563
+ import { execSync } from "node:child_process";
1564
+ import { existsSync } from "node:fs";
1565
+ import { query } from "@anthropic-ai/claude-agent-sdk";
1566
+
1567
+ const VERSION = ${version};
1568
+ const port = Number(process.argv[2] ?? "${DAEMON_PORT}");
1569
+ const liveRuns = new Map();
1570
+
1571
+ // The SDK's default spawn does \`existsSync(pathToClaudeCodeExecutable)\`
1572
+ // before invoking child_process.spawn \u2014 that check fails on bare names
1573
+ // like "claude" because existsSync doesn't do PATH lookup. Resolve to an
1574
+ // absolute path once at daemon startup so the SDK is happy regardless of
1575
+ // what the host passes.
1576
+ function resolveClaudeBinary(hint) {
1577
+ if (hint && (hint.includes("/") || hint.includes(String.fromCharCode(92)))) {
1578
+ return hint;
1579
+ }
1580
+ const name = hint || "claude";
1581
+ try {
1582
+ const out = execSync("command -v " + name + " || which " + name, {
1583
+ encoding: "utf8",
1584
+ stdio: ["ignore", "pipe", "ignore"],
1585
+ }).trim();
1586
+ if (out && existsSync(out)) return out;
1587
+ } catch {}
1588
+ return name;
1589
+ }
1590
+
1591
+ function createPromptStream() {
1592
+ const queue = [];
1593
+ let resolver = null;
1594
+ let ended = false;
1595
+ return {
1596
+ [Symbol.asyncIterator]: async function* () {
1597
+ while (true) {
1598
+ if (queue.length > 0) { yield queue.shift(); continue; }
1599
+ if (ended) return;
1600
+ await new Promise((r) => { resolver = r; });
1601
+ }
1602
+ },
1603
+ push(message) {
1604
+ queue.push(message);
1605
+ const r = resolver; resolver = null; r?.();
1606
+ },
1607
+ end() {
1608
+ ended = true;
1609
+ const r = resolver; resolver = null; r?.();
1610
+ },
1611
+ };
1612
+ }
1613
+
1614
+ function readJsonBody(req) {
1615
+ return new Promise((resolve, reject) => {
1616
+ const chunks = [];
1617
+ let total = 0;
1618
+ req.on("data", (c) => {
1619
+ total += c.length;
1620
+ if (total > 8 * 1024 * 1024) {
1621
+ req.destroy(new Error("body too large"));
1622
+ return;
1623
+ }
1624
+ chunks.push(c);
1625
+ });
1626
+ req.on("end", () => {
1627
+ try {
1628
+ const text = Buffer.concat(chunks).toString("utf8");
1629
+ resolve(text ? JSON.parse(text) : {});
1630
+ } catch (e) { reject(e); }
1631
+ });
1632
+ req.on("error", reject);
1633
+ });
1634
+ }
1635
+
1636
+ function autoApproveCanUseTool(_toolName, input) {
1637
+ return { behavior: "allow", updatedInput: input };
1638
+ }
1639
+
1640
+ async function handleStart(req, res, runId) {
1641
+ let body;
1642
+ try { body = await readJsonBody(req); }
1643
+ catch (e) {
1644
+ res.writeHead(400, { "content-type": "application/json" });
1645
+ res.end(JSON.stringify({ error: String(e?.message ?? e) }));
1646
+ return;
1647
+ }
1648
+ const { prompt, options } = body || {};
1649
+ if (!prompt) {
1650
+ res.writeHead(400);
1651
+ res.end("missing prompt");
1652
+ return;
1653
+ }
1654
+
1655
+ const promptStream = createPromptStream();
1656
+ promptStream.push(prompt);
1657
+
1658
+ res.writeHead(200, {
1659
+ "content-type": "application/x-ndjson",
1660
+ "transfer-encoding": "chunked",
1661
+ "x-daemon-version": VERSION,
1662
+ });
1663
+
1664
+ const opts = { ...(options || {}) };
1665
+ const autoApprove = !!opts.autoApproveTools;
1666
+ delete opts.autoApproveTools;
1667
+ opts.pathToClaudeCodeExecutable = resolveClaudeBinary(
1668
+ opts.pathToClaudeCodeExecutable,
1669
+ );
1670
+
1671
+ let queryHandle;
1672
+ try {
1673
+ queryHandle = query({
1674
+ prompt: promptStream,
1675
+ options: {
1676
+ ...opts,
1677
+ ...(autoApprove ? { canUseTool: autoApproveCanUseTool } : {}),
1678
+ },
1679
+ });
1680
+ } catch (e) {
1681
+ res.write(JSON.stringify({ _error: String(e?.message ?? e) }) + "\\n");
1682
+ res.end();
1683
+ return;
1684
+ }
1685
+
1686
+ liveRuns.set(runId, { query: queryHandle, prompt: promptStream });
1687
+
1688
+ // Client disconnected (e.g. host process killed) \u2192 tear down.
1689
+ req.on("close", () => {
1690
+ if (!liveRuns.has(runId)) return;
1691
+ liveRuns.delete(runId);
1692
+ promptStream.end();
1693
+ queryHandle.interrupt().catch(() => {});
1694
+ });
1695
+
1696
+ try {
1697
+ for await (const message of queryHandle) {
1698
+ res.write(JSON.stringify(message) + "\\n");
1699
+ if (message.type === "result") break;
1700
+ }
1701
+ } catch (e) {
1702
+ res.write(JSON.stringify({ _error: String(e?.message ?? e) }) + "\\n");
1703
+ } finally {
1704
+ liveRuns.delete(runId);
1705
+ promptStream.end();
1706
+ res.end();
1707
+ }
1708
+ }
1709
+
1710
+ async function handleSendMessage(req, res, runId) {
1711
+ const run = liveRuns.get(runId);
1712
+ if (!run) {
1713
+ res.writeHead(404, { "content-type": "application/json" });
1714
+ res.end(JSON.stringify({ error: "no such run" }));
1715
+ return;
1716
+ }
1717
+ let body;
1718
+ try { body = await readJsonBody(req); }
1719
+ catch (e) {
1720
+ res.writeHead(400);
1721
+ res.end(String(e?.message ?? e));
1722
+ return;
1723
+ }
1724
+ run.prompt.push({
1725
+ type: "user",
1726
+ message: { role: "user", content: body.content },
1727
+ parent_tool_use_id: null,
1728
+ });
1729
+ res.writeHead(204);
1730
+ res.end();
1731
+ }
1732
+
1733
+ async function handleAbort(_req, res, runId) {
1734
+ const run = liveRuns.get(runId);
1735
+ if (!run) {
1736
+ res.writeHead(404);
1737
+ res.end();
1738
+ return;
1739
+ }
1740
+ await run.query.interrupt().catch(() => {});
1741
+ res.writeHead(204);
1742
+ res.end();
1743
+ }
1744
+
1745
+ async function handleDelete(_req, res, runId) {
1746
+ const run = liveRuns.get(runId);
1747
+ if (run) {
1748
+ liveRuns.delete(runId);
1749
+ run.prompt.end();
1750
+ await run.query.interrupt().catch(() => {});
1751
+ }
1752
+ res.writeHead(204);
1753
+ res.end();
1754
+ }
1755
+
1756
+ const server = http.createServer((req, res) => {
1757
+ if (req.method === "GET" && req.url === "/__version") {
1758
+ res.writeHead(200, { "content-type": "text/plain" });
1759
+ res.end(VERSION);
1760
+ return;
1761
+ }
1762
+ const url = req.url ?? "";
1763
+ let m;
1764
+ if (req.method === "POST" && (m = url.match(/^\\/runs\\/([^/]+)\\/start$/))) {
1765
+ handleStart(req, res, decodeURIComponent(m[1]));
1766
+ return;
1767
+ }
1768
+ if (req.method === "POST" && (m = url.match(/^\\/runs\\/([^/]+)\\/sendMessage$/))) {
1769
+ handleSendMessage(req, res, decodeURIComponent(m[1]));
1770
+ return;
1771
+ }
1772
+ if (req.method === "POST" && (m = url.match(/^\\/runs\\/([^/]+)\\/abort$/))) {
1773
+ handleAbort(req, res, decodeURIComponent(m[1]));
1774
+ return;
1775
+ }
1776
+ if (req.method === "DELETE" && (m = url.match(/^\\/runs\\/([^/]+)$/))) {
1777
+ handleDelete(req, res, decodeURIComponent(m[1]));
1778
+ return;
1779
+ }
1780
+ res.writeHead(404, { "content-type": "text/plain" });
1781
+ res.end("not found");
1782
+ });
1783
+
1784
+ server.listen(port, "0.0.0.0", () => {
1785
+ console.error("[claude-code-daemon] listening on :" + port + " v" + VERSION);
1786
+ });
1787
+
1788
+ const shutdown = () => {
1789
+ for (const r of liveRuns.values()) {
1790
+ r.prompt.end();
1791
+ r.query.interrupt().catch(() => {});
1792
+ }
1793
+ server.close();
1794
+ setTimeout(() => process.exit(0), 100).unref();
1795
+ };
1796
+ process.on("SIGTERM", shutdown);
1797
+ process.on("SIGINT", shutdown);
1798
+ `;
1799
+ }
1800
+ var daemonInflight = /* @__PURE__ */ new WeakMap();
1801
+ async function ensureClaudeCodeDaemon(options, env) {
1802
+ const sandbox = options.sandbox;
1803
+ if (!sandbox) return;
1804
+ const key = sandbox;
1805
+ const existing = daemonInflight.get(key);
1806
+ if (existing) return existing;
1807
+ const promise = ensureClaudeCodeDaemonUncached(options, env);
1808
+ daemonInflight.set(key, promise);
1809
+ promise.finally(() => {
1810
+ if (daemonInflight.get(key) === promise) daemonInflight.delete(key);
1811
+ });
1812
+ return promise;
1813
+ }
1814
+ async function ensureClaudeCodeDaemonUncached(options, env) {
1815
+ return time(debugRelay, "ensureClaudeCodeDaemon", async () => {
1816
+ const sandbox = options.sandbox;
1817
+ const probe = await time(
1818
+ debugRelay,
1819
+ "probe daemon version",
1820
+ () => sandbox.run(
1821
+ `curl -fsS --max-time 1 http://127.0.0.1:${DAEMON_PORT}/__version 2>/dev/null`,
1822
+ { cwd: options.cwd, timeoutMs: 1e4 }
1823
+ )
1824
+ );
1825
+ if (probe.exitCode === 0 && probe.combinedOutput.trim() === DAEMON_PROTOCOL_VERSION) {
1826
+ debugRelay(
1827
+ "daemon v%s already running \u2014 reusing",
1828
+ DAEMON_PROTOCOL_VERSION
1829
+ );
1830
+ return;
1831
+ }
1832
+ const daemonDir = path8.posix.dirname(DAEMON_PATH);
1833
+ const daemonNodeModules = `${daemonDir}/node_modules/@anthropic-ai`;
1834
+ const launchCommand = [
1835
+ `NPM_ROOT="$(npm root -g 2>/dev/null)"`,
1836
+ `if [ -z "$NPM_ROOT" ] || [ ! -d "$NPM_ROOT/@anthropic-ai/claude-agent-sdk" ]; then echo "claude-code daemon launch: @anthropic-ai/claude-agent-sdk not found under $NPM_ROOT" >&2; exit 1; fi`,
1837
+ `mkdir -p ${shellQuote(daemonNodeModules)}`,
1838
+ `ln -sfn "$NPM_ROOT/@anthropic-ai/claude-agent-sdk" ${shellQuote(daemonNodeModules + "/claude-agent-sdk")}`,
1839
+ `if [ -f ${shellQuote(DAEMON_PID_PATH)} ]; then kill -TERM "$(cat ${shellQuote(DAEMON_PID_PATH)})" 2>/dev/null || true; fi`,
1840
+ `(fuser -k -n tcp ${DAEMON_PORT} 2>/dev/null || true)`,
1841
+ // Brief grace so the kernel releases the port before the new
1842
+ // daemon's listen() — only matters on warm-sandbox respawns;
1843
+ // adds 200ms otherwise.
1844
+ `sleep 0.2`,
1845
+ `(nohup node ${shellQuote(DAEMON_PATH)} ${DAEMON_PORT} > ${shellQuote(DAEMON_LOG_PATH)} 2>&1 & echo $! > ${shellQuote(DAEMON_PID_PATH)})`
1846
+ ].join(" && ");
1847
+ const launch = await time(
1848
+ debugRelay,
1849
+ "uploadAndRun daemon (write + spawn)",
1850
+ () => sandbox.uploadAndRun(
1851
+ [
1852
+ {
1853
+ path: DAEMON_PATH,
1854
+ content: createClaudeCodeDaemonScript(),
1855
+ mode: 420
1856
+ }
1857
+ ],
1858
+ launchCommand,
1859
+ { cwd: options.cwd, env: { ...env, IS_SANDBOX: "1" } }
1860
+ )
1861
+ );
1862
+ if (launch.exitCode !== 0) {
1863
+ throw new Error(
1864
+ `Could not start claude-code daemon: ${launch.stderr || launch.combinedOutput || "(no output)"}`
1865
+ );
1866
+ }
1867
+ });
1868
+ }
1869
+ var DAEMON_FIRST_REQUEST_RETRY_BUDGET_MS = 3e4;
1870
+ var DAEMON_FIRST_REQUEST_RETRY_INTERVAL_MS = 250;
1871
+ async function fetchWithDaemonRetry(input, init) {
1872
+ const deadline = Date.now() + DAEMON_FIRST_REQUEST_RETRY_BUDGET_MS;
1873
+ let lastError;
1874
+ while (Date.now() < deadline) {
1875
+ try {
1876
+ return await fetch(input, init);
1877
+ } catch (error) {
1878
+ lastError = error;
1879
+ const aborted = error?.name === "AbortError";
1880
+ if (aborted) {
1881
+ throw error;
1882
+ }
1883
+ await sleep(DAEMON_FIRST_REQUEST_RETRY_INTERVAL_MS);
1884
+ }
1885
+ }
1886
+ throw lastError ?? new Error("claude-code daemon request timed out");
1887
+ }
1888
+ async function* parseNdjsonStream(body) {
1889
+ const reader = body.getReader();
1890
+ const decoder = new TextDecoder();
1891
+ let buffer = "";
1892
+ while (true) {
1893
+ const { done, value } = await reader.read();
1894
+ if (done) break;
1895
+ buffer += decoder.decode(value, { stream: true });
1896
+ let idx;
1897
+ while ((idx = buffer.indexOf("\n")) !== -1) {
1898
+ const line = buffer.slice(0, idx).trim();
1899
+ buffer = buffer.slice(idx + 1);
1900
+ if (line) {
1901
+ try {
1902
+ yield JSON.parse(line);
1903
+ } catch {
1904
+ }
1905
+ }
1906
+ }
1907
+ }
1908
+ buffer += decoder.decode();
1909
+ if (buffer.trim()) {
1910
+ try {
1911
+ yield JSON.parse(buffer);
1912
+ } catch {
1913
+ }
1914
+ }
1915
+ }
1916
+ async function daemonBaseUrl(sandbox) {
1917
+ const url = await sandbox.getPreviewLink(DAEMON_PORT);
1918
+ return url.replace(/\/$/, "");
1919
+ }
1920
+ var ClaudeCodeAgentAdapter = class {
1921
+ /**
1922
+ * Sandbox-side preparation. Uploads `.claude/` artifacts and ensures
1923
+ * the daemon is running. `execute()` then dials the daemon directly.
1924
+ */
1925
+ async setup(request) {
1926
+ await time(debugClaude, "claude-code setup()", async () => {
1927
+ const options = request.options;
1928
+ const provider = request.provider;
1929
+ const sandbox = options.sandbox;
1930
+ if (!sandbox) {
1931
+ throw new Error(
1932
+ "claude-code requires a sandbox (the SDK transport runs as a daemon inside the sandbox)."
1933
+ );
1934
+ }
1935
+ const target = await createSetupTarget(provider, "shared-setup", options);
1936
+ const settingsPath = path8.join(target.layout.claudeDir, "settings.json");
1937
+ const mcpConfigPath = path8.join(
1938
+ target.layout.claudeDir,
1939
+ "agentbox-mcp.json"
1940
+ );
1941
+ const hooks = assertHooksSupported(provider, options);
1942
+ assertCommandsSupported(provider, options.commands);
1943
+ const { artifacts: skillArtifacts, installCommands } = await time(
1944
+ debugClaude,
1945
+ "prepareSkillArtifacts",
1946
+ () => prepareSkillArtifacts(provider, options.skills, target.layout)
1947
+ );
1948
+ const hookSettings = buildClaudeHookSettings(hooks) ?? {};
1949
+ const mcpConfigJson = buildClaudeMcpConfig(options.mcps) ?? JSON.stringify({ mcpServers: {} }, null, 2);
1950
+ const artifacts = [
1951
+ ...skillArtifacts,
1952
+ ...buildClaudeCommandArtifacts(options.commands, target.layout),
1953
+ ...buildClaudeSubagentArtifacts(options.subAgents, target.layout),
1954
+ { path: settingsPath, content: JSON.stringify(hookSettings, null, 2) },
1955
+ { path: mcpConfigPath, content: mcpConfigJson }
1956
+ ];
1957
+ const env = { ...options.env ?? {}, ...target.env };
1958
+ await Promise.all([
1959
+ time(
1960
+ debugClaude,
1961
+ "applyDifferentialSetup",
1962
+ () => applyDifferentialSetup(target, artifacts, installCommands)
1963
+ ),
1964
+ ensureClaudeCodeDaemon(options, env)
1965
+ ]);
1966
+ });
1967
+ }
1968
+ async execute(request, sink) {
1969
+ const executeStartedAt = Date.now();
1970
+ debugClaude("execute() start runId=%s", request.runId);
1971
+ const sandbox = request.options.sandbox;
1972
+ if (!sandbox) {
1973
+ throw new Error(
1974
+ "claude-code requires a sandbox (the SDK transport runs as a daemon inside the sandbox)."
1975
+ );
1976
+ }
1977
+ const claudeDir = claudeConfigDir(request.options);
1978
+ const settingsPath = path8.join(claudeDir, "settings.json");
1979
+ const mcpConfigPath = path8.join(claudeDir, "agentbox-mcp.json");
1980
+ const env = {
1981
+ ...request.options.env ?? {},
1982
+ CLAUDE_CONFIG_DIR: claudeDir,
1983
+ // `IS_SANDBOX=1` lets the in-sandbox claude binary accept
1984
+ // `--dangerously-skip-permissions` (i.e. permissionMode
1985
+ // bypassPermissions) when running as root, which is the default
1986
+ // user inside our images.
1987
+ IS_SANDBOX: "1"
1988
+ };
1989
+ const inputParts = await time(
1990
+ debugClaude,
1991
+ "validateProviderUserInput",
1992
+ () => validateProviderUserInput(request.provider, request.run.input)
1993
+ );
1994
+ const userContent = mapToClaudeUserContent(inputParts);
1995
+ const initialUuid = randomUUID();
1996
+ const presetSessionId = request.run.resumeSessionId ?? randomUUID();
1997
+ sink.setSessionId(presetSessionId);
1998
+ const baseUrl = await time(
1999
+ debugClaude,
2000
+ "getPreviewLink daemon",
2001
+ () => daemonBaseUrl(sandbox)
2002
+ );
2003
+ const startUrl = `${baseUrl}/runs/${encodeURIComponent(request.runId)}/start`;
2004
+ const sdkOptions = buildClaudeQueryOptions({
2005
+ request,
2006
+ settingsPath,
2007
+ mcpConfigPath,
2008
+ env
2009
+ });
2010
+ const autoApproveTools = shouldAutoApproveClaudeTools(request.options);
2011
+ const requestBody = {
2012
+ prompt: {
2013
+ type: "user",
2014
+ message: {
2015
+ role: "user",
2016
+ content: userContent
2017
+ },
2018
+ parent_tool_use_id: null
2019
+ },
2020
+ options: {
2021
+ ...sdkOptions,
2022
+ // `sessionId` and `resume` are mutually exclusive — buildClaudeQueryOptions
2023
+ // already set `resume` for the resume path, so only stamp `sessionId` for
2024
+ // fresh runs.
2025
+ ...request.run.resumeSessionId ? {} : { sessionId: presetSessionId },
2026
+ autoApproveTools
2027
+ }
2028
+ };
2029
+ const fetchAbort = new AbortController();
2030
+ const cleanup = async () => {
2031
+ try {
2032
+ await fetch(
2033
+ `${baseUrl}/runs/${encodeURIComponent(request.runId)}/abort`,
2034
+ { method: "POST", headers: sandbox.previewHeaders }
2035
+ );
2036
+ } catch {
2037
+ }
2038
+ fetchAbort.abort();
2039
+ };
2040
+ sink.setAbort(cleanup);
2041
+ sink.onMessage(async (content) => {
2042
+ const parts = await validateProviderUserInput(request.provider, content);
2043
+ const mapped = mapToClaudeUserContent(parts);
2044
+ const messageUuid = randomUUID();
2045
+ await fetch(
2046
+ `${baseUrl}/runs/${encodeURIComponent(request.runId)}/sendMessage`,
2047
+ {
2048
+ method: "POST",
2049
+ headers: {
2050
+ "content-type": "application/json",
2051
+ ...sandbox.previewHeaders
2052
+ },
2053
+ body: JSON.stringify({ content: mapped })
2054
+ }
2055
+ );
2056
+ return { messageId: messageUuid };
2057
+ });
2058
+ const response = await time(
2059
+ debugClaude,
2060
+ "POST /runs/<id>/start",
2061
+ () => fetchWithDaemonRetry(startUrl, {
2062
+ method: "POST",
2063
+ signal: fetchAbort.signal,
2064
+ headers: {
2065
+ "content-type": "application/json",
2066
+ ...sandbox.previewHeaders
2067
+ },
2068
+ body: JSON.stringify(requestBody)
2069
+ })
2070
+ );
2071
+ if (!response.ok || !response.body) {
2072
+ const text = await response.text().catch(() => "");
2073
+ throw new Error(
2074
+ `claude-code daemon /start failed: ${response.status} ${text}`
2075
+ );
2076
+ }
2077
+ sink.setRaw({ baseUrl, runId: request.runId, claudeDir });
2078
+ sink.emitEvent(
2079
+ createNormalizedEvent("run.started", {
2080
+ provider: request.provider,
2081
+ runId: request.runId
2082
+ })
2083
+ );
2084
+ sink.emitEvent(
2085
+ createNormalizedEvent(
2086
+ "message.started",
2087
+ { provider: request.provider, runId: request.runId },
2088
+ { messageId: initialUuid }
2089
+ )
2090
+ );
2091
+ let accumulatedText = "";
2092
+ let pendingMessages = 1;
2093
+ let firstStreamEventLogged = false;
2094
+ let firstTextDeltaLogged = false;
2095
+ const rawPayloads = [];
2096
+ try {
2097
+ for await (const item of parseNdjsonStream(response.body)) {
2098
+ if (item && typeof item === "object" && "_error" in item) {
2099
+ throw new Error(
2100
+ String(item._error ?? "daemon error")
2101
+ );
2102
+ }
2103
+ const message = item;
2104
+ rawPayloads.push(message);
2105
+ sink.emitRaw(toRawEvent(request.runId, message, message.type));
2106
+ if (message.type === "system") {
2107
+ const sys = message;
2108
+ if (sys.subtype === "init" && sys.session_id) {
2109
+ debugClaude(
2110
+ "\u2605 session.init session_id=%s (%dms)",
2111
+ sys.session_id.slice(0, 8),
2112
+ Date.now() - executeStartedAt
2113
+ );
2114
+ }
2115
+ continue;
2116
+ }
2117
+ if (message.type === "stream_event") {
2118
+ if (!firstStreamEventLogged) {
2119
+ firstStreamEventLogged = true;
2120
+ debugClaude(
2121
+ "\u2605 first stream_event (%dms since execute start)",
2122
+ Date.now() - executeStartedAt
2123
+ );
2124
+ }
2125
+ const partial = message;
2126
+ const { text, thinking } = extractStreamDeltas(partial);
2127
+ if (thinking) {
2128
+ sink.emitEvent(
2129
+ createNormalizedEvent(
2130
+ "reasoning.delta",
2131
+ { provider: request.provider, runId: request.runId },
2132
+ { delta: thinking }
2133
+ )
2134
+ );
2135
+ }
2136
+ if (text) {
2137
+ if (!firstTextDeltaLogged) {
2138
+ firstTextDeltaLogged = true;
2139
+ debugClaude(
2140
+ "\u2605 first text delta (%dms since execute start)",
2141
+ Date.now() - executeStartedAt
2142
+ );
2143
+ }
2144
+ accumulatedText += text;
2145
+ sink.emitEvent(
2146
+ createNormalizedEvent(
2147
+ "text.delta",
2148
+ { provider: request.provider, runId: request.runId },
2149
+ { delta: text }
2150
+ )
2151
+ );
2152
+ }
2153
+ continue;
2154
+ }
2155
+ if (message.type === "assistant") {
2156
+ const asst = message;
2157
+ const thinking = extractAssistantThinking(asst);
2158
+ if (thinking) {
2159
+ sink.emitEvent(
2160
+ createNormalizedEvent(
2161
+ "reasoning.delta",
2162
+ { provider: request.provider, runId: request.runId },
2163
+ { delta: thinking }
2164
+ )
2165
+ );
2166
+ }
2167
+ const text = extractAssistantText(asst);
2168
+ sink.emitEvent(
2169
+ createNormalizedEvent(
2170
+ "message.completed",
2171
+ { provider: request.provider, runId: request.runId },
2172
+ {
2173
+ text,
2174
+ ...asst.uuid ? { messageId: String(asst.uuid) } : {}
2175
+ }
2176
+ )
2177
+ );
2178
+ continue;
2179
+ }
2180
+ if (message.type === "result") {
2181
+ const result = message;
2182
+ const resultText = result.subtype === "success" ? result.result : accumulatedText;
2183
+ if (resultText && resultText !== accumulatedText) {
2184
+ accumulatedText = resultText;
2185
+ }
2186
+ pendingMessages--;
2187
+ if (pendingMessages <= 0) break;
2188
+ continue;
2189
+ }
2190
+ }
2191
+ const finalText = accumulatedText;
2192
+ debugClaude(
2193
+ "\u2605 run.completed (%dms since execute start) chars=%d",
2194
+ Date.now() - executeStartedAt,
2195
+ finalText.length
2196
+ );
2197
+ sink.emitEvent(
2198
+ createNormalizedEvent(
2199
+ "run.completed",
2200
+ { provider: request.provider, runId: request.runId },
2201
+ { text: finalText }
2202
+ )
2203
+ );
2204
+ sink.complete({
2205
+ text: finalText,
2206
+ costData: extractClaudeCostData(rawPayloads)
2207
+ });
2208
+ } finally {
2209
+ fetchAbort.abort();
2210
+ }
2211
+ return async () => void 0;
2212
+ }
2213
+ /**
2214
+ * Stateless abort. POSTs to the in-sandbox daemon's
2215
+ * `/runs/<id>/abort`. The daemon calls `query.interrupt()` on the
2216
+ * matching live run; the originating instance's NDJSON read loop
2217
+ * sees the run unwind via a non-success `result` (or stream close).
2218
+ */
2219
+ async attachAbort(request) {
2220
+ const baseUrl = await daemonBaseUrl(request.sandbox);
2221
+ const controller = new AbortController();
2222
+ const timeout = setTimeout(() => controller.abort(), 3e3);
2223
+ try {
2224
+ await fetch(
2225
+ `${baseUrl}/runs/${encodeURIComponent(request.runId)}/abort`,
2226
+ {
2227
+ method: "POST",
2228
+ signal: controller.signal,
2229
+ headers: request.sandbox.previewHeaders
2230
+ }
2231
+ ).catch((error) => {
2232
+ debugClaude("attachAbort POST failed: %o", error);
2233
+ });
2234
+ } finally {
2235
+ clearTimeout(timeout);
2236
+ }
2237
+ }
2238
+ /**
2239
+ * Stateless message injection. POSTs `{ content }` to the daemon's
2240
+ * `/runs/<id>/sendMessage`. The daemon pushes the message into the
2241
+ * matching run's prompt iterable, which the SDK forwards to claude
2242
+ * as a fresh user turn.
2243
+ */
2244
+ async attachSendMessage(request, content) {
2245
+ const baseUrl = await daemonBaseUrl(request.sandbox);
2246
+ const inputParts = await validateProviderUserInput(
2247
+ AgentProvider.ClaudeCode,
2248
+ content
2249
+ );
2250
+ const mapped = mapToClaudeUserContent(inputParts);
2251
+ const response = await fetch(
2252
+ `${baseUrl}/runs/${encodeURIComponent(request.runId)}/sendMessage`,
2253
+ {
2254
+ method: "POST",
2255
+ headers: {
2256
+ "content-type": "application/json",
2257
+ ...request.sandbox.previewHeaders
2258
+ },
2259
+ body: JSON.stringify({ content: mapped })
2260
+ }
2261
+ );
2262
+ if (!response.ok) {
2263
+ throw new Error(
2264
+ `claude-code attachSendMessage failed: ${response.status} ${await response.text().catch(() => "")}`
2265
+ );
2266
+ }
2267
+ }
2268
+ };
2269
+
2270
+ // src/agents/providers/codex.ts
2271
+ import path9 from "path";
2272
+
2273
+ // src/agents/transports/app-server.ts
2274
+ import { createParser } from "eventsource-parser";
2275
+ import { WebSocket } from "ws";
2276
+ async function fetchJson(url, init) {
2277
+ const response = await fetch(url, init);
2278
+ if (!response.ok) {
2279
+ throw new Error(`Request to ${url} failed with ${response.status}.`);
2280
+ }
2281
+ const text = await response.text();
2282
+ if (text.length === 0) {
2283
+ throw new Error(
2284
+ `Request to ${url} returned status ${response.status} with an empty body.`
2285
+ );
2286
+ }
2287
+ try {
2288
+ return JSON.parse(text);
2289
+ } catch (error) {
2290
+ const preview = text.length > 200 ? `${text.slice(0, 200)}\u2026` : text;
2291
+ const cause = error instanceof Error ? error.message : String(error);
2292
+ throw new Error(
2293
+ `Could not parse JSON response from ${url} (status ${response.status}): ${cause}. Body: ${preview}`
2294
+ );
2295
+ }
2296
+ }
2297
+ async function* streamSse(url, init) {
2298
+ const response = await fetch(url, init);
2299
+ if (!response.ok || !response.body) {
2300
+ throw new Error(`Could not open SSE stream at ${url}.`);
2301
+ }
2302
+ const queue = new AsyncQueue();
2303
+ const parser = createParser({
2304
+ onEvent(event) {
2305
+ queue.push({
2306
+ event: event.event || void 0,
2307
+ id: event.id || void 0,
2308
+ data: event.data
2309
+ });
2310
+ },
2311
+ onError(error) {
2312
+ queue.fail(error);
2313
+ }
2314
+ });
2315
+ const reader = response.body.getReader();
2316
+ const decoder = new TextDecoder();
2317
+ void (async () => {
2318
+ try {
2319
+ while (true) {
2320
+ const { done, value } = await reader.read();
2321
+ if (done) {
2322
+ break;
2323
+ }
2324
+ parser.feed(decoder.decode(value, { stream: true }));
2325
+ }
2326
+ parser.feed(decoder.decode());
2327
+ queue.finish();
2328
+ } catch (error) {
2329
+ queue.fail(error);
2330
+ } finally {
2331
+ reader.releaseLock();
2332
+ }
2333
+ })();
2334
+ yield* queue;
2335
+ }
2336
+ async function connectJsonRpcWebSocket(url, options) {
2337
+ const notifications = new AsyncQueue();
2338
+ const socket = new WebSocket(url, { headers: options?.headers });
2339
+ await new Promise((resolve, reject) => {
2340
+ const cleanup = () => {
2341
+ socket.off("open", handleOpen);
2342
+ socket.off("error", handleError);
2343
+ };
2344
+ const handleOpen = () => {
2345
+ cleanup();
2346
+ resolve();
2347
+ };
2348
+ const handleError = (error) => {
2349
+ cleanup();
2350
+ reject(error);
2351
+ };
2352
+ socket.once("open", handleOpen);
2353
+ socket.once("error", handleError);
2354
+ });
2355
+ socket.on("message", (data) => {
2356
+ notifications.push(data.toString());
2357
+ });
2358
+ socket.on("close", () => {
2359
+ notifications.finish();
2360
+ });
2361
+ socket.on("error", (error) => {
2362
+ notifications.fail(error);
2363
+ });
2364
+ return {
2365
+ source: notifications,
2366
+ send: async (line) => {
2367
+ await new Promise((resolve, reject) => {
2368
+ socket.send(line, (error) => {
2369
+ if (error) {
2370
+ reject(error);
2371
+ return;
2372
+ }
2373
+ resolve();
2374
+ });
2375
+ });
2376
+ },
2377
+ close: async () => {
2378
+ await new Promise((resolve) => {
2379
+ if (socket.readyState === WebSocket.CLOSED || socket.readyState === WebSocket.CLOSING) {
2380
+ resolve();
2381
+ return;
2382
+ }
2383
+ socket.once("close", () => resolve());
2384
+ socket.close();
2385
+ });
2386
+ },
2387
+ raw: socket
2388
+ };
2389
+ }
2390
+ var JsonRpcLineClient = class {
2391
+ constructor(source, writeLine) {
2392
+ this.writeLine = writeLine;
2393
+ void this.consume(source);
2394
+ }
2395
+ writeLine;
2396
+ pending = /* @__PURE__ */ new Map();
2397
+ notifications = new AsyncQueue();
2398
+ nextId = 1;
2399
+ async request(method, params) {
2400
+ const id = this.nextId++;
2401
+ const response = new Promise((resolve, reject) => {
2402
+ this.pending.set(id, {
2403
+ resolve,
2404
+ reject
2405
+ });
2406
+ });
2407
+ await this.writeLine(JSON.stringify({ id, method, params }));
2408
+ return response;
2409
+ }
2410
+ async notify(method, params) {
2411
+ await this.writeLine(JSON.stringify({ method, params: params ?? {} }));
2412
+ }
2413
+ async respond(id, result) {
2414
+ await this.writeLine(JSON.stringify({ id, result }));
2415
+ }
2416
+ async respondError(id, error) {
2417
+ await this.writeLine(JSON.stringify({ id, error }));
2418
+ }
2419
+ async *messages() {
2420
+ yield* this.notifications;
2421
+ }
2422
+ async consume(source) {
2423
+ try {
2424
+ for await (const line of source) {
2425
+ if (!line.trim()) {
2426
+ continue;
2427
+ }
2428
+ const message = JSON.parse(line);
2429
+ if (typeof message.method === "string") {
2430
+ this.notifications.push(message);
2431
+ continue;
2432
+ }
2433
+ if (typeof message.id === "number") {
2434
+ const pending = this.pending.get(message.id);
2435
+ if (!pending) {
2436
+ continue;
2437
+ }
2438
+ this.pending.delete(message.id);
2439
+ if (message.error) {
2440
+ pending.reject(message.error);
2441
+ } else {
2442
+ pending.resolve(message.result);
2443
+ }
2444
+ continue;
2445
+ }
2446
+ }
2447
+ const closeError = new Error("JSON-RPC transport closed.");
2448
+ for (const pending of this.pending.values()) {
2449
+ pending.reject(closeError);
2450
+ }
2451
+ this.pending.clear();
2452
+ this.notifications.finish();
2453
+ } catch (error) {
2454
+ for (const pending of this.pending.values()) {
2455
+ pending.reject(error);
2456
+ }
2457
+ this.pending.clear();
2458
+ this.notifications.fail(error);
2459
+ }
2460
+ }
2461
+ };
2462
+
2463
+ // src/agents/providers/codex.ts
2464
+ function codexConfigDir(options) {
2465
+ return path9.join(
2466
+ agentboxRoot(AgentProvider.Codex, Boolean(options.sandbox)),
2467
+ ".codex"
2468
+ );
2469
+ }
2470
+ var REMOTE_CODEX_APP_SERVER_PORT = 43181;
2471
+ var REMOTE_CODEX_APP_SERVER_ID = "shared-app-server";
2472
+ function compactEnv(values) {
2473
+ return Object.fromEntries(
2474
+ Object.entries(values).filter(([, value]) => value !== void 0)
2475
+ );
2476
+ }
2477
+ function buildCodexSandboxMode(options) {
2478
+ return options.sandbox ? "workspace-write" : "read-only";
2479
+ }
2480
+ function buildThreadParams(cwd, options, request) {
2481
+ return {
2482
+ cwd,
2483
+ model: request.run.model ?? null,
2484
+ approvalPolicy: isInteractiveApproval(options) ? "untrusted" : "never",
2485
+ sandbox: buildCodexSandboxMode(options),
2486
+ serviceName: "agentbox",
2487
+ // Persist the rollout on disk so follow-up runs can call `thread/resume`.
2488
+ // `ephemeral: true` threads have no rollout file and resume fails with
2489
+ // "no rollout found for thread id ...".
2490
+ experimentalRawEvents: true
2491
+ };
2492
+ }
2493
+ function buildResumeParams(cwd, options, request) {
2494
+ return {
2495
+ threadId: request.run.resumeSessionId,
2496
+ cwd,
2497
+ model: request.run.model ?? null,
2498
+ approvalPolicy: isInteractiveApproval(options) ? "untrusted" : "never",
2499
+ sandbox: buildCodexSandboxMode(options)
2500
+ };
2501
+ }
2502
+ function buildTurnSandboxPolicy(options) {
2503
+ if (!options.sandbox) {
2504
+ return void 0;
2505
+ }
2506
+ if (options.sandbox.provider === SandboxProvider.LocalDocker) {
2507
+ return {
2508
+ type: "workspaceWrite",
2509
+ networkAccess: true
2510
+ };
2511
+ }
2512
+ return {
2513
+ type: "externalSandbox",
2514
+ networkAccess: "enabled"
2515
+ };
2516
+ }
2517
+ function buildTurnCollaborationMode(request) {
2518
+ const systemPrompt = request.run.systemPrompt;
2519
+ if (!systemPrompt) {
2520
+ return void 0;
2521
+ }
2522
+ return {
2523
+ mode: "custom",
2524
+ settings: {
2525
+ developer_instructions: systemPrompt
2526
+ }
2527
+ };
2528
+ }
2529
+ function buildCodexTurnStartParams(params) {
2530
+ const { threadId, inputItems, request, turnStartOverrides } = params;
2531
+ const sandboxPolicy = buildTurnSandboxPolicy(request.options);
2532
+ return {
2533
+ threadId,
2534
+ input: inputItems,
2535
+ approvalPolicy: isInteractiveApproval(request.options) ? "untrusted" : "never",
2536
+ ...sandboxPolicy ? { sandboxPolicy } : {},
2537
+ ...turnStartOverrides ?? {},
2538
+ model: request.run.model ?? null,
2539
+ effort: request.run.reasoning ?? null,
2540
+ outputSchema: null
2541
+ };
2542
+ }
2543
+ function toRawEvent2(runId, payload, type) {
2544
+ return {
2545
+ provider: AgentProvider.Codex,
2546
+ runId,
2547
+ type,
2548
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2549
+ payload
2550
+ };
2551
+ }
2552
+ function shouldIgnoreCodexError(notification) {
2553
+ if (notification.method !== "error") {
2554
+ return false;
2555
+ }
2556
+ return notification.params?.willRetry === true;
2557
+ }
2558
+ function buildCodexCommandArgs(binary, args, options) {
2559
+ const overrides = [];
2560
+ if (options?.provider?.supportsWebsockets === false) {
2561
+ overrides.push(["supports_websockets", "false"]);
2562
+ }
2563
+ const overrideArgs = overrides.flatMap(([k, v]) => ["-c", `${k}=${v}`]);
2564
+ return ["-u", "XDG_CONFIG_HOME", binary, ...overrideArgs, ...args];
2565
+ }
2566
+ function toNormalizedCodexEvents(runId, notification) {
2567
+ const base = {
2568
+ provider: AgentProvider.Codex,
2569
+ runId,
2570
+ raw: toRawEvent2(runId, notification, notification.method)
2571
+ };
2572
+ if (notification.method === "turn/started") {
2573
+ const turn = notification.params?.turn;
2574
+ const turnId = typeof turn?.id === "string" ? turn.id : void 0;
2575
+ return [
2576
+ createNormalizedEvent(
2577
+ "message.started",
2578
+ base,
2579
+ turnId ? { messageId: turnId } : void 0
2580
+ )
2581
+ ];
2582
+ }
2583
+ if (notification.method === "item/agentMessage/delta") {
2584
+ const delta = typeof notification.params?.delta === "string" ? notification.params.delta : "";
2585
+ return delta ? [createNormalizedEvent("text.delta", base, { delta })] : [];
2586
+ }
2587
+ if (notification.method === "item/reasoning/summaryTextDelta" || notification.method === "item/reasoning/textDelta") {
2588
+ const delta = typeof notification.params?.delta === "string" ? notification.params.delta : typeof notification.params?.text === "string" ? notification.params.text : "";
2589
+ return delta ? [createNormalizedEvent("reasoning.delta", base, { delta })] : [];
2590
+ }
2591
+ if (notification.method === "item/completed") {
2592
+ const item = notification.params?.item;
2593
+ if (!item) {
2594
+ return [];
2595
+ }
2596
+ if (item.type === "agentMessage" && typeof item.text === "string") {
2597
+ return [
2598
+ createNormalizedEvent("message.completed", base, { text: item.text })
2599
+ ];
2600
+ }
2601
+ if (item.type === "reasoning" && item.summary) {
2602
+ return [
2603
+ createNormalizedEvent("reasoning.delta", base, {
2604
+ delta: typeof item.summary === "string" ? item.summary : JSON.stringify(item.summary)
2605
+ })
2606
+ ];
2607
+ }
2608
+ if (item.type === "commandExecution" || item.type === "dynamicToolCall" || item.type === "mcpToolCall" || item.type === "webSearch") {
2609
+ return [
2610
+ createNormalizedEvent("tool.call.completed", base, {
2611
+ toolName: String(
2612
+ item.tool ?? item.command ?? item.server ?? item.query ?? item.type
2613
+ ),
2614
+ callId: String(item.id ?? ""),
2615
+ output: item
2616
+ })
2617
+ ];
2618
+ }
2619
+ }
2620
+ if (notification.method === "item/started") {
2621
+ const item = notification.params?.item;
2622
+ if (item && (item.type === "commandExecution" || item.type === "dynamicToolCall" || item.type === "mcpToolCall" || item.type === "webSearch")) {
2623
+ return [
2624
+ createNormalizedEvent("tool.call.started", base, {
2625
+ toolName: String(
2626
+ item.tool ?? item.command ?? item.server ?? item.query ?? item.type
2627
+ ),
2628
+ callId: String(item.id ?? ""),
2629
+ input: item
2630
+ })
2631
+ ];
2632
+ }
2633
+ }
2634
+ if (notification.method === "turn/completed") {
2635
+ const turn = notification.params?.turn;
2636
+ const text = typeof turn?.lastAgentMessage === "string" ? turn.lastAgentMessage : void 0;
2637
+ return [createNormalizedEvent("run.completed", base, { text })];
2638
+ }
2639
+ if (notification.method === "error") {
2640
+ const error = notification.params?.error;
2641
+ return [
2642
+ createNormalizedEvent("run.error", base, {
2643
+ error: String(error?.message ?? "Codex app-server error")
2644
+ })
2645
+ ];
2646
+ }
2647
+ return [];
2648
+ }
2649
+ function createCodexPermissionEvent(request, notification) {
2650
+ const raw = toRawEvent2(request.runId, notification, notification.method);
2651
+ const params = notification.params;
2652
+ const requestId = notification.id;
2653
+ if (!params || requestId === void 0) {
2654
+ return null;
2655
+ }
2656
+ if (notification.method === "item/commandExecution/requestApproval") {
2657
+ const networkContext = params.networkApprovalContext;
2658
+ const availableDecisions = Array.isArray(params.availableDecisions) ? params.availableDecisions : [];
2659
+ const title = networkContext ? "Approve network access" : "Approve command execution";
2660
+ const message = typeof params.reason === "string" ? params.reason : typeof params.command === "string" ? params.command : void 0;
2661
+ return createNormalizedEvent(
2662
+ "permission.requested",
2663
+ {
2664
+ provider: request.provider,
2665
+ runId: request.runId,
2666
+ raw
2667
+ },
2668
+ {
2669
+ requestId: String(requestId),
2670
+ kind: networkContext ? "network" : "bash",
2671
+ title,
2672
+ message,
2673
+ input: params,
2674
+ canRemember: availableDecisions.includes("acceptForSession")
2675
+ }
2676
+ );
2677
+ }
2678
+ if (notification.method === "item/fileChange/requestApproval") {
2679
+ const availableDecisions = Array.isArray(params.availableDecisions) ? params.availableDecisions : [];
2680
+ return createNormalizedEvent(
2681
+ "permission.requested",
2682
+ {
2683
+ provider: request.provider,
2684
+ runId: request.runId,
2685
+ raw
2686
+ },
2687
+ {
2688
+ requestId: String(requestId),
2689
+ kind: "file-change",
2690
+ title: "Approve file changes",
2691
+ message: typeof params.reason === "string" ? params.reason : "Codex wants to modify files.",
2692
+ input: params,
2693
+ canRemember: availableDecisions.includes("acceptForSession")
2694
+ }
2695
+ );
2696
+ }
2697
+ return null;
2698
+ }
2699
+ function toCodexApprovalDecision(notification, response) {
2700
+ const params = notification.params ?? {};
2701
+ const availableDecisions = Array.isArray(params.availableDecisions) ? params.availableDecisions : [];
2702
+ const proposedExecpolicyAmendment = Array.isArray(
2703
+ params.proposedExecpolicyAmendment
2704
+ ) ? params.proposedExecpolicyAmendment.filter(
2705
+ (part) => typeof part === "string"
2706
+ ) : [];
2707
+ if (response.decision === "deny") {
2708
+ return availableDecisions.includes("decline") ? "decline" : "cancel";
2709
+ }
2710
+ if (response.remember && availableDecisions.includes("acceptForSession")) {
2711
+ return "acceptForSession";
2712
+ }
2713
+ if (proposedExecpolicyAmendment.length > 0 && availableDecisions.some(
2714
+ (decision) => typeof decision === "object" && decision !== null && "acceptWithExecpolicyAmendment" in decision
2715
+ )) {
2716
+ return {
2717
+ acceptWithExecpolicyAmendment: {
2718
+ execpolicy_amendment: proposedExecpolicyAmendment
2719
+ }
2720
+ };
2721
+ }
2722
+ return "accept";
2723
+ }
2724
+ function codexImageExtension(mediaType) {
2725
+ switch (mediaType) {
2726
+ case "image/gif":
2727
+ return ".gif";
2728
+ case "image/jpeg":
2729
+ return ".jpg";
2730
+ case "image/png":
2731
+ return ".png";
2732
+ case "image/webp":
2733
+ return ".webp";
2734
+ default:
2735
+ return ".img";
2736
+ }
2737
+ }
2738
+ async function materializeCodexImage(options, part, index) {
2739
+ if (part.source.type === "url") {
2740
+ return part.source.url;
2741
+ }
2742
+ const root = agentboxRoot(AgentProvider.Codex, Boolean(options.sandbox));
2743
+ const imagePath = path9.join(
2744
+ root,
2745
+ "inputs",
2746
+ `codex-image-${index}${codexImageExtension(part.mediaType)}`
2747
+ );
2748
+ if (options.sandbox) {
2749
+ const encodedPath = `${imagePath}.b64`;
2750
+ await options.sandbox.uploadAndRun(
2751
+ [{ path: encodedPath, content: part.source.data }],
2752
+ [
2753
+ `mkdir -p ${shellQuote(path9.posix.dirname(imagePath))}`,
2754
+ `(base64 --decode < ${shellQuote(encodedPath)} > ${shellQuote(imagePath)} || base64 -D < ${shellQuote(encodedPath)} > ${shellQuote(imagePath)})`,
2755
+ `rm -f ${shellQuote(encodedPath)}`
2756
+ ].join(" && "),
2757
+ { cwd: options.cwd, env: options.env }
2758
+ );
2759
+ return imagePath;
2760
+ }
2761
+ const fs = await import("fs/promises");
2762
+ await fs.mkdir(path9.dirname(imagePath), { recursive: true });
2763
+ await fs.writeFile(imagePath, Buffer.from(part.source.data, "base64"));
2764
+ return imagePath;
2765
+ }
2766
+ function resolveCodexOpenAiBaseUrlFromOptions(options) {
2767
+ return options.env?.OPENAI_BASE_URL ?? options.provider?.env?.OPENAI_BASE_URL;
2768
+ }
2769
+ async function ensureCodexLoginViaConfig(request, target) {
2770
+ const options = request.options;
2771
+ const openAiApiKey = options.env?.OPENAI_API_KEY ?? options.provider?.env?.OPENAI_API_KEY;
2772
+ const openAiBaseUrl = resolveCodexOpenAiBaseUrlFromOptions(options);
2773
+ const extraEnv = {};
2774
+ if (openAiApiKey) {
2775
+ extraEnv.OPENAI_API_KEY = openAiApiKey;
2776
+ }
2777
+ if (openAiBaseUrl) {
2778
+ extraEnv.OPENAI_BASE_URL = openAiBaseUrl;
2779
+ }
2780
+ await target.runCommand(
2781
+ [
2782
+ 'if [ -z "${OPENAI_API_KEY:-}" ]; then exit 0; fi',
2783
+ 'mkdir -p "${CODEX_HOME:-$HOME/.codex}"',
2784
+ "printenv OPENAI_API_KEY | env -u XDG_CONFIG_HOME codex login --with-api-key"
2785
+ ].join("; "),
2786
+ Object.keys(extraEnv).length > 0 ? extraEnv : void 0
2787
+ );
2788
+ }
2789
+ function toRemoteCodexWebSocketUrl(url) {
2790
+ const parsed = new URL(url);
2791
+ parsed.protocol = parsed.protocol === "https:" ? "wss:" : "ws:";
2792
+ return parsed.toString();
2793
+ }
2794
+ async function withCodexAppServer(request, body) {
2795
+ const sandbox = request.sandbox;
2796
+ if (sandbox.provider === SandboxProvider.LocalDocker) {
2797
+ throw new Error(
2798
+ "Codex stateless attach is not supported for local-docker sandboxes; the app-server is in-process."
2799
+ );
2800
+ }
2801
+ const previewUrl = await sandbox.getPreviewLink(REMOTE_CODEX_APP_SERVER_PORT);
2802
+ const transport = await connectJsonRpcWebSocket(
2803
+ toRemoteCodexWebSocketUrl(previewUrl),
2804
+ { headers: sandbox.previewHeaders }
2805
+ );
2806
+ const client = new JsonRpcLineClient(
2807
+ transport.source,
2808
+ transport.send
2809
+ );
2810
+ try {
2811
+ await client.request("initialize", {
2812
+ clientInfo: { title: "AgentBox", name: "AgentBox", version: "0.1.0" },
2813
+ capabilities: { experimentalApi: true }
2814
+ });
2815
+ await client.notify("initialized", {});
2816
+ return await body(client);
2817
+ } finally {
2818
+ await transport.close().catch(() => void 0);
2819
+ }
2820
+ }
2821
+ async function connectRemoteCodexAppServer(url, headers = {}) {
2822
+ return time(debugCodex, "connectRemoteCodexAppServer", async () => {
2823
+ const startedAt = Date.now();
2824
+ let attempt = 0;
2825
+ let lastError;
2826
+ while (Date.now() - startedAt < 3e4) {
2827
+ attempt++;
2828
+ try {
2829
+ const conn = await connectJsonRpcWebSocket(url, { headers });
2830
+ if (attempt > 1) {
2831
+ debugCodex("connected after %d attempt(s)", attempt);
2832
+ }
2833
+ return conn;
2834
+ } catch (error) {
2835
+ lastError = error;
2836
+ await sleep(250);
2837
+ }
2838
+ }
2839
+ throw lastError ?? new Error(`Could not connect to Codex app-server at ${url}.`);
2840
+ });
2841
+ }
2842
+ async function setupCodex(request) {
2843
+ const options = request.options;
2844
+ const provider = request.provider;
2845
+ const hooks = assertHooksSupported(provider, options);
2846
+ assertCommandsSupported(provider, options.commands);
2847
+ const usesRemoteWebSocket = options.sandbox && options.sandbox.provider !== SandboxProvider.LocalDocker;
2848
+ function buildArtifactsFor(layoutTarget) {
2849
+ const { artifacts: subAgentArtifacts, agentSections } = buildCodexSubagentArtifacts(options.subAgents, layoutTarget.layout);
2850
+ const hooksFile = buildCodexHooksFile(hooks);
2851
+ const enableMultiAgent = (options.subAgents?.length ?? 0) > 0;
2852
+ const enableSkills = (options.skills?.length ?? 0) > 0;
2853
+ const openAiBaseUrl = resolveCodexOpenAiBaseUrlFromOptions(options);
2854
+ const configToml = buildCodexConfigToml({
2855
+ mcps: options.mcps,
2856
+ agentSections,
2857
+ enableHooks: Boolean(hooksFile),
2858
+ enableSkills,
2859
+ enableMultiAgent,
2860
+ openAiBaseUrl
2861
+ });
2862
+ const artifacts = [...subAgentArtifacts];
2863
+ if (configToml) {
2864
+ artifacts.push({
2865
+ path: path9.join(layoutTarget.layout.codexDir, "config.toml"),
2866
+ content: configToml
2867
+ });
2868
+ }
2869
+ if (hooksFile) {
2870
+ artifacts.push({
2871
+ path: path9.join(layoutTarget.layout.codexDir, "hooks.json"),
2872
+ content: JSON.stringify(hooksFile, null, 2)
2873
+ });
2874
+ }
2875
+ return { artifacts, installCommands: [] };
2876
+ }
2877
+ if (usesRemoteWebSocket && options.sandbox) {
2878
+ const sandbox = options.sandbox;
2879
+ const sharedTarget = await createSetupTarget(
2880
+ provider,
2881
+ REMOTE_CODEX_APP_SERVER_ID,
2882
+ options
2883
+ );
2884
+ const target2 = await createSetupTarget(provider, "shared-setup", options);
2885
+ const env = compactEnv({
2886
+ ...options.env ?? {},
2887
+ ...sharedTarget.env,
2888
+ ...options.provider?.env ?? {}
2889
+ });
2890
+ await time(
2891
+ debugCodex,
2892
+ "ensureCodexLogin",
2893
+ () => ensureCodexLoginViaConfig(request, sharedTarget)
2894
+ );
2895
+ const { artifacts: serverArtifacts } = buildArtifactsFor(sharedTarget);
2896
+ await applyDifferentialSetup(sharedTarget, serverArtifacts, []);
2897
+ const binary = options.provider?.binary ?? "codex";
2898
+ const pidFilePath = path9.posix.join(
2899
+ sharedTarget.layout.rootDir,
2900
+ "codex-app-server.pid"
2901
+ );
2902
+ const logFilePath = path9.posix.join(
2903
+ sharedTarget.layout.rootDir,
2904
+ "codex-app-server.log"
2905
+ );
2906
+ const serverCwd = sharedTarget.layout.rootDir;
2907
+ const launchResult = await time(
2908
+ debugCodex,
2909
+ "launch app-server (probe + spawn-if-cold)",
2910
+ () => sandbox.run(
2911
+ [
2912
+ `mkdir -p ${shellQuote(sharedTarget.layout.rootDir)}`,
2913
+ `if curl -fsS http://127.0.0.1:${REMOTE_CODEX_APP_SERVER_PORT}/readyz >/dev/null 2>&1; then exit 0; fi`,
2914
+ `if [ -f ${shellQuote(pidFilePath)} ]; then kill "$(cat ${shellQuote(pidFilePath)})" >/dev/null 2>&1 || true; rm -f ${shellQuote(pidFilePath)}; fi`,
2915
+ `(${[
2916
+ `nohup ${[
2917
+ "env",
2918
+ ...buildCodexCommandArgs(
2919
+ binary,
2920
+ [
2921
+ "app-server",
2922
+ "--listen",
2923
+ `ws://0.0.0.0:${REMOTE_CODEX_APP_SERVER_PORT}`
2924
+ ],
2925
+ options
2926
+ )
2927
+ ].map(shellQuote).join(" ")} > ${shellQuote(logFilePath)} 2>&1 &`,
2928
+ `echo $! > ${shellQuote(pidFilePath)}`
2929
+ ].join(" ")})`
2930
+ ].join(" && "),
2931
+ {
2932
+ cwd: serverCwd,
2933
+ env
2934
+ }
2935
+ )
2936
+ );
2937
+ if (launchResult.exitCode !== 0) {
2938
+ throw new Error(
2939
+ `Could not start Codex app-server: ${launchResult.combinedOutput || launchResult.stderr}`
2940
+ );
2941
+ }
2942
+ try {
2943
+ const { artifacts: skillArtifacts2, installCommands: installCommands2 } = await prepareSkillArtifacts(provider, options.skills, target2.layout);
2944
+ await applyDifferentialSetup(target2, skillArtifacts2, installCommands2);
2945
+ } catch (error) {
2946
+ await target2.cleanup().catch(() => void 0);
2947
+ throw error;
2948
+ }
2949
+ return;
2950
+ }
2951
+ const target = await createSetupTarget(provider, "shared-setup", options);
2952
+ try {
2953
+ await ensureCodexLoginViaConfig(request, target);
2954
+ } catch (error) {
2955
+ await target.cleanup().catch(() => void 0);
2956
+ throw error;
2957
+ }
2958
+ const { artifacts: skillArtifacts, installCommands } = await prepareSkillArtifacts(provider, options.skills, target.layout);
2959
+ const { artifacts: configArtifacts } = buildArtifactsFor(target);
2960
+ await applyDifferentialSetup(
2961
+ target,
2962
+ [...skillArtifacts, ...configArtifacts],
2963
+ installCommands
2964
+ );
2965
+ }
2966
+ async function createRuntime(request, inputParts) {
2967
+ const options = request.options;
2968
+ const codexDir = codexConfigDir(options);
2969
+ const env = compactEnv({
2970
+ ...options.env ?? {},
2971
+ CODEX_HOME: codexDir,
2972
+ ...options.provider?.env ?? {}
2973
+ });
2974
+ const runtimeCwd = path9.dirname(codexDir);
2975
+ const inputItems = await buildCodexInputItems(options, inputParts);
2976
+ const usesRemoteWebSocket = options.sandbox && options.sandbox.provider !== SandboxProvider.LocalDocker;
2977
+ if (usesRemoteWebSocket && options.sandbox) {
2978
+ const sandbox = options.sandbox;
2979
+ const previewUrl = await time(
2980
+ debugCodex,
2981
+ "getPreviewLink app-server",
2982
+ () => sandbox.getPreviewLink(REMOTE_CODEX_APP_SERVER_PORT)
2983
+ );
2984
+ const transport = await connectRemoteCodexAppServer(
2985
+ toRemoteCodexWebSocketUrl(previewUrl),
2986
+ sandbox.previewHeaders
2987
+ );
2988
+ debugCodex("\u2605 codex transport established");
2989
+ return {
2990
+ source: transport.source,
2991
+ writeLine: transport.send,
2992
+ cleanup: async () => {
2993
+ await transport?.close().catch(() => void 0);
2994
+ },
2995
+ raw: {
2996
+ transport: transport.raw,
2997
+ previewUrl,
2998
+ port: REMOTE_CODEX_APP_SERVER_PORT,
2999
+ codexDir
3000
+ },
3001
+ inputItems,
3002
+ turnStartOverrides: buildTurnCollaborationMode(request)
3003
+ };
3004
+ }
3005
+ const codexArgs = buildCodexCommandArgs(
3006
+ options.provider?.binary ?? "codex",
3007
+ ["app-server"],
3008
+ options
3009
+ );
3010
+ if (options.sandbox) {
3011
+ const handle = await options.sandbox.runAsync(["env", ...codexArgs], {
3012
+ cwd: runtimeCwd,
3013
+ env
3014
+ });
3015
+ if (!handle.write) {
3016
+ throw new Error(
3017
+ "The selected sandbox does not expose an interactive stdin channel for Codex."
3018
+ );
3019
+ }
3020
+ async function* stdoutLines() {
3021
+ async function* stdoutChunks() {
3022
+ for await (const event of handle) {
3023
+ if (event.type === "stdout" && event.chunk) {
3024
+ yield event.chunk;
3025
+ }
3026
+ }
3027
+ }
3028
+ yield* linesFromTextChunks(stdoutChunks());
3029
+ }
3030
+ return {
3031
+ source: stdoutLines(),
3032
+ writeLine: async (line) => {
3033
+ await handle.write?.(`${line}
3034
+ `);
3035
+ },
3036
+ cleanup: async () => {
3037
+ await handle.kill();
3038
+ },
3039
+ raw: { handle, codexDir },
3040
+ inputItems,
3041
+ turnStartOverrides: buildTurnCollaborationMode(request)
3042
+ };
3043
+ }
3044
+ const processHandle = spawnCommand({
3045
+ command: "env",
3046
+ args: codexArgs,
3047
+ cwd: runtimeCwd,
3048
+ env: {
3049
+ ...process.env,
3050
+ ...env
3051
+ }
3052
+ });
3053
+ return {
3054
+ source: linesFromNodeStream(processHandle.child.stdout),
3055
+ writeLine: async (line) => {
3056
+ processHandle.child.stdin.write(`${line}
3057
+ `);
3058
+ },
3059
+ cleanup: async () => {
3060
+ await processHandle.kill();
3061
+ },
3062
+ raw: { processHandle, codexDir },
3063
+ inputItems,
3064
+ turnStartOverrides: buildTurnCollaborationMode(request)
3065
+ };
3066
+ }
3067
+ async function buildCodexInputItems(options, inputParts) {
3068
+ const textPrompt = joinTextParts(
3069
+ inputParts.filter(
3070
+ (part) => part.type === "text"
3071
+ )
3072
+ );
3073
+ const inputItems = [];
3074
+ if (textPrompt.trim().length > 0) {
3075
+ inputItems.push({
3076
+ type: "text",
3077
+ text: textPrompt,
3078
+ text_elements: []
3079
+ });
3080
+ }
3081
+ inputItems.push(
3082
+ ...await mapToCodexPromptParts(
3083
+ inputParts,
3084
+ async (part, index) => materializeCodexImage(options, part, index)
3085
+ )
3086
+ );
3087
+ return inputItems;
3088
+ }
3089
+ var CodexAgentAdapter = class {
3090
+ async setup(request) {
3091
+ await setupCodex(request);
3092
+ }
3093
+ async execute(request, sink) {
3094
+ const executeStartedAt = Date.now();
3095
+ debugCodex("execute() start runId=%s", request.runId);
3096
+ const inputParts = await time(
3097
+ debugCodex,
3098
+ "validateProviderUserInput",
3099
+ () => validateProviderUserInput(request.provider, request.run.input)
3100
+ );
3101
+ const runtime = await time(
3102
+ debugCodex,
3103
+ "createRuntime",
3104
+ () => createRuntime(request, inputParts)
3105
+ );
3106
+ sink.setRaw(runtime.raw);
3107
+ sink.emitEvent(
3108
+ createNormalizedEvent("run.started", {
3109
+ provider: request.provider,
3110
+ runId: request.runId
3111
+ })
3112
+ );
3113
+ const client = runtime.client ?? new JsonRpcLineClient(
3114
+ runtime.source,
3115
+ runtime.writeLine
3116
+ );
3117
+ const interactiveApproval = isInteractiveApproval(request.options);
3118
+ let rootThreadId;
3119
+ let turnId;
3120
+ let pendingTurns = 1;
3121
+ sink.setAbort(async () => {
3122
+ const threadIdAtAbort = rootThreadId;
3123
+ const turnIdAtAbort = turnId;
3124
+ if (threadIdAtAbort && turnIdAtAbort) {
3125
+ try {
3126
+ await Promise.race([
3127
+ client.request("turn/interrupt", {
3128
+ threadId: threadIdAtAbort,
3129
+ turnId: turnIdAtAbort
3130
+ }),
3131
+ new Promise(
3132
+ (_, reject) => setTimeout(
3133
+ () => reject(new Error("codex turn/interrupt timed out")),
3134
+ 3e3
3135
+ )
3136
+ )
3137
+ ]);
3138
+ } catch {
3139
+ }
3140
+ }
3141
+ await runtime.cleanup().catch(() => void 0);
3142
+ });
3143
+ const sendTurn = async (content) => {
3144
+ if (!rootThreadId) {
3145
+ throw new Error("Cannot send message before thread is started.");
3146
+ }
3147
+ const parts = normalizeUserInput(content);
3148
+ const text = parts.filter((p) => p.type === "text").map((p) => p.text).join("");
3149
+ const inputItems = [];
3150
+ if (text.trim().length > 0) {
3151
+ inputItems.push({ type: "text", text, text_elements: [] });
3152
+ }
3153
+ const response = await client.request(
3154
+ "turn/start",
3155
+ buildCodexTurnStartParams({
3156
+ threadId: rootThreadId,
3157
+ inputItems,
3158
+ request
3159
+ })
3160
+ );
3161
+ return {
3162
+ ...typeof response?.turn?.id === "string" ? { messageId: response.turn.id } : {}
3163
+ };
3164
+ };
3165
+ sink.onMessage(sendTurn);
3166
+ const rawPayloads = [];
3167
+ const completion = new Promise((resolve, reject) => {
3168
+ let finalText = "";
3169
+ void (async () => {
3170
+ let firstClientMessageLogged = false;
3171
+ for await (const message of client.messages()) {
3172
+ if (!firstClientMessageLogged) {
3173
+ firstClientMessageLogged = true;
3174
+ debugCodex(
3175
+ "\u2605 first transport message (%dms since execute start) method=%s",
3176
+ Date.now() - executeStartedAt,
3177
+ message.method
3178
+ );
3179
+ }
3180
+ const raw = toRawEvent2(request.runId, message, message.method);
3181
+ rawPayloads.push(message);
3182
+ sink.emitRaw(raw);
3183
+ if (message.method === "tool/requestUserInput" && message.id !== void 0) {
3184
+ reject(
3185
+ new Error(
3186
+ "Codex tool/requestUserInput approvals are not yet supported by AgentBox."
3187
+ )
3188
+ );
3189
+ return;
3190
+ }
3191
+ const permissionEvent = createCodexPermissionEvent(request, message);
3192
+ if (permissionEvent && message.id !== void 0) {
3193
+ const response = interactiveApproval ? await sink.requestPermission(permissionEvent) : {
3194
+ requestId: permissionEvent.requestId,
3195
+ decision: "allow"
3196
+ };
3197
+ await client.respond(message.id, {
3198
+ decision: toCodexApprovalDecision(message, response)
3199
+ });
3200
+ continue;
3201
+ }
3202
+ for (const event of toNormalizedCodexEvents(request.runId, message)) {
3203
+ sink.emitEvent(event);
3204
+ if (event.type === "text.delta") {
3205
+ finalText += event.delta;
3206
+ }
3207
+ }
3208
+ if (message.method === "thread/started" && !rootThreadId) {
3209
+ rootThreadId = message.params?.thread?.id ?? rootThreadId;
3210
+ }
3211
+ if (message.method === "turn/started") {
3212
+ turnId = message.params?.turn?.id ?? turnId;
3213
+ }
3214
+ if (message.method === "turn/completed" && (!message.params?.threadId || message.params.threadId === rootThreadId)) {
3215
+ pendingTurns--;
3216
+ if (pendingTurns <= 0) {
3217
+ resolve({ text: finalText, turnId, threadId: rootThreadId });
3218
+ return;
3219
+ }
3220
+ }
3221
+ if (message.method === "error" && !shouldIgnoreCodexError(message)) {
3222
+ reject(message);
3223
+ return;
3224
+ }
3225
+ }
3226
+ reject(new Error("Codex transport closed before run completed."));
3227
+ })().catch(reject);
3228
+ });
3229
+ try {
3230
+ if (!runtime.client) {
3231
+ await client.request("initialize", {
3232
+ clientInfo: {
3233
+ title: "AgentBox",
3234
+ name: "AgentBox",
3235
+ version: "0.1.0"
3236
+ },
3237
+ capabilities: {
3238
+ experimentalApi: true
3239
+ }
3240
+ });
3241
+ await client.notify("initialized", {});
3242
+ }
3243
+ const cwd = request.options.cwd ?? process.cwd();
3244
+ const threadResponse = request.run.resumeSessionId ? await client.request(
3245
+ "thread/resume",
3246
+ buildResumeParams(cwd, request.options, request)
3247
+ ) : await client.request(
3248
+ "thread/start",
3249
+ buildThreadParams(cwd, request.options, request)
3250
+ );
3251
+ rootThreadId = threadResponse.thread.id;
3252
+ if ("bindThread" in client && typeof client.bindThread === "function") {
3253
+ client.bindThread(threadResponse.thread.id);
3254
+ }
3255
+ sink.setSessionId(threadResponse.thread.id);
3256
+ rawPayloads.push(threadResponse);
3257
+ sink.emitRaw(
3258
+ toRawEvent2(
3259
+ request.runId,
3260
+ threadResponse,
3261
+ request.run.resumeSessionId ? "thread/resume:result" : "thread/start:result"
3262
+ )
3263
+ );
3264
+ await client.request(
3265
+ "turn/start",
3266
+ buildCodexTurnStartParams({
3267
+ threadId: threadResponse.thread.id,
3268
+ inputItems: runtime.inputItems,
3269
+ request,
3270
+ turnStartOverrides: runtime.turnStartOverrides
3271
+ })
3272
+ );
3273
+ const { text } = await completion;
3274
+ debugCodex(
3275
+ "\u2605 run.completed (%dms since execute start) chars=%d",
3276
+ Date.now() - executeStartedAt,
3277
+ text?.length ?? 0
3278
+ );
3279
+ sink.complete({ text, costData: extractCodexCostData(rawPayloads) });
3280
+ } finally {
3281
+ await runtime.cleanup().catch(() => void 0);
3282
+ }
3283
+ return async () => void 0;
3284
+ }
3285
+ /**
3286
+ * Stateless abort. Calls `turn/interrupt` against the in-sandbox
3287
+ * app-server using `(sessionId, turnId)` provided by the caller —
3288
+ * the SDK does not persist turn state itself; bookkeeping the
3289
+ * current turnId is the caller's responsibility (e.g. via Redis,
3290
+ * driven by the normalized `message.started` event whose
3291
+ * `messageId` IS the codex turnId).
3292
+ *
3293
+ * If `sessionId` or `turnId` is missing the call is a no-op.
3294
+ */
3295
+ async attachAbort(request) {
3296
+ const threadId = request.sessionId;
3297
+ const turnId = request.turnId;
3298
+ if (!threadId || !turnId) {
3299
+ debugCodex(
3300
+ "attachAbort runId=%s skipped: threadId=%s turnId=%s",
3301
+ request.runId,
3302
+ threadId,
3303
+ turnId
3304
+ );
3305
+ return;
3306
+ }
3307
+ await withCodexAppServer(request, async (client) => {
3308
+ await Promise.race([
3309
+ client.request("turn/interrupt", { threadId, turnId }),
3310
+ new Promise(
3311
+ (_, reject) => setTimeout(
3312
+ () => reject(new Error("codex turn/interrupt timed out")),
3313
+ 3e3
3314
+ )
3315
+ )
3316
+ ]).catch((error) => {
3317
+ debugCodex(
3318
+ "attachAbort runId=%s turn/interrupt failed: %o",
3319
+ request.runId,
3320
+ error
3321
+ );
3322
+ });
3323
+ });
3324
+ }
3325
+ /**
3326
+ * Stateless message injection. Uses `request.sessionId` as the codex
3327
+ * threadId and starts a fresh turn against it via `turn/start`.
3328
+ */
3329
+ async attachSendMessage(request, content) {
3330
+ const threadId = request.sessionId;
3331
+ if (!threadId) {
3332
+ throw new Error(
3333
+ `Cannot attachSendMessage to codex run ${request.runId}: sessionId (threadId) is required.`
3334
+ );
3335
+ }
3336
+ const parts = normalizeUserInput(content);
3337
+ const text = joinTextParts(
3338
+ parts.filter(
3339
+ (part) => part.type === "text"
3340
+ )
3341
+ );
3342
+ const inputItems = [];
3343
+ if (text.trim().length > 0) {
3344
+ inputItems.push({ type: "text", text, text_elements: [] });
3345
+ }
3346
+ await withCodexAppServer(request, async (client) => {
3347
+ await client.request("turn/start", {
3348
+ threadId,
3349
+ input: inputItems,
3350
+ approvalPolicy: "never",
3351
+ model: null,
3352
+ effort: null,
3353
+ outputSchema: null
3354
+ });
3355
+ });
3356
+ }
3357
+ };
3358
+
3359
+ // src/agents/providers/opencode.ts
3360
+ import path10 from "path";
3361
+ var SANDBOX_OPENCODE_PORT = 4096;
3362
+ var LOCAL_OPENCODE_PORT = 4096;
3363
+ var SANDBOX_OPENCODE_READY_TIMEOUT_MS = 9e4;
3364
+ var LOCAL_OPENCODE_READY_TIMEOUT_MS = 2e4;
3365
+ var SHARED_OPENCODE_TARGET_ID = "shared-opencode-server";
3366
+ function toRawEvent3(runId, payload, type) {
3367
+ return {
3368
+ provider: AgentProvider.OpenCode,
3369
+ runId,
3370
+ type,
3371
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
3372
+ payload
3373
+ };
3374
+ }
3375
+ function extractText(value) {
3376
+ if (!value) {
3377
+ return "";
3378
+ }
3379
+ if (typeof value === "string") {
3380
+ return value;
3381
+ }
3382
+ if (Array.isArray(value)) {
3383
+ return value.map(extractText).filter(Boolean).join("");
3384
+ }
3385
+ if (typeof value === "object") {
3386
+ const record = value;
3387
+ if (record.type === "text" && typeof record.text === "string") {
3388
+ return record.text;
3389
+ }
3390
+ if (record.type === "reasoning") {
3391
+ return "";
3392
+ }
3393
+ if (record.message) {
3394
+ return extractText(record.message);
3395
+ }
3396
+ if (record.content) {
3397
+ return extractText(record.content);
3398
+ }
3399
+ if (record.parts) {
3400
+ return extractText(record.parts);
3401
+ }
3402
+ if (record.text) {
3403
+ return extractText(record.text);
3404
+ }
3405
+ }
3406
+ return "";
3407
+ }
3408
+ function extractAssistantMessageId(response) {
3409
+ if (!response || typeof response !== "object") {
3410
+ return void 0;
3411
+ }
3412
+ const record = response;
3413
+ const info = record.info && typeof record.info === "object" ? record.info : record.message && typeof record.message === "object" ? record.message : void 0;
3414
+ const id = info?.id ?? record.id;
3415
+ return typeof id === "string" ? id : void 0;
3416
+ }
3417
+ function extractReasoning(value) {
3418
+ if (!value) {
3419
+ return "";
3420
+ }
3421
+ if (Array.isArray(value)) {
3422
+ return value.map(extractReasoning).filter(Boolean).join("");
3423
+ }
3424
+ if (typeof value === "object") {
3425
+ const record = value;
3426
+ if (record.type === "reasoning") {
3427
+ if (typeof record.text === "string") {
3428
+ return record.text;
3429
+ }
3430
+ if (typeof record.reasoning === "string") {
3431
+ return record.reasoning;
3432
+ }
3433
+ }
3434
+ return [
3435
+ extractReasoning(record.message),
3436
+ extractReasoning(record.content),
3437
+ extractReasoning(record.parts)
3438
+ ].filter(Boolean).join("");
3439
+ }
3440
+ return "";
3441
+ }
3442
+ function toOpenCodeModel(model) {
3443
+ if (!model) {
3444
+ return void 0;
3445
+ }
3446
+ const slashIndex = model.indexOf("/");
3447
+ if (slashIndex === -1) {
3448
+ return { modelID: model };
3449
+ }
3450
+ const providerID = model.slice(0, slashIndex).trim();
3451
+ const modelID = model.slice(slashIndex + 1).trim();
3452
+ if (!providerID || !modelID) {
3453
+ return { modelID: model };
3454
+ }
3455
+ return { providerID, modelID };
3456
+ }
3457
+ function buildOpenCodePermissionConfig(interactive) {
3458
+ if (!interactive) {
3459
+ return {
3460
+ read: { "*": "allow" },
3461
+ edit: "allow",
3462
+ bash: "allow",
3463
+ webfetch: "allow",
3464
+ external_directory: "allow",
3465
+ skill: { "*": "allow" },
3466
+ task: "allow"
3467
+ };
3468
+ }
3469
+ return {
3470
+ read: { "*": "allow" },
3471
+ edit: "ask",
3472
+ bash: "ask",
3473
+ webfetch: "ask",
3474
+ external_directory: "ask",
3475
+ skill: { "*": "allow" },
3476
+ task: "ask"
3477
+ };
3478
+ }
3479
+ function createOpenCodePermissionEvent(request, raw, payload) {
3480
+ const properties = payload.properties ?? {};
3481
+ const permission = String(properties.permission ?? "tool");
3482
+ return createNormalizedEvent(
3483
+ "permission.requested",
3484
+ {
3485
+ provider: request.provider,
3486
+ runId: request.runId,
3487
+ raw
3488
+ },
3489
+ {
3490
+ requestId: String(properties.id ?? ""),
3491
+ kind: permission === "bash" ? "bash" : permission === "edit" ? "edit" : permission === "external_directory" ? "file-change" : permission === "webfetch" ? "network" : permission === "task" ? "tool" : "unknown",
3492
+ title: `Approve ${permission} permission`,
3493
+ message: typeof properties.metadata === "object" && properties.metadata !== null ? JSON.stringify(properties.metadata) : `OpenCode requested ${permission} permission.`,
3494
+ input: properties,
3495
+ canRemember: Array.isArray(properties.always) && properties.always.length > 0
3496
+ }
3497
+ );
3498
+ }
3499
+ var OPEN_CODE_REASONING_LEVELS = ["low", "medium", "high", "xhigh"];
3500
+ function openCodeAgentSlug(reasoning) {
3501
+ return reasoning ? `agentbox-${reasoning}` : "agentbox";
3502
+ }
3503
+ function buildOpenCodeConfig(options, systemPrompt, interactiveApproval) {
3504
+ const mcpConfig = buildOpenCodeMcpConfig(options.mcps);
3505
+ const commandsConfig = buildOpenCodeCommandsConfig(options.commands);
3506
+ const baseAgent = {
3507
+ mode: "primary",
3508
+ prompt: systemPrompt,
3509
+ permission: buildOpenCodePermissionConfig(interactiveApproval),
3510
+ tools: {
3511
+ write: true,
3512
+ edit: true,
3513
+ bash: true,
3514
+ webfetch: true,
3515
+ skill: true
3516
+ }
3517
+ };
3518
+ const reasoningVariants = Object.fromEntries(
3519
+ OPEN_CODE_REASONING_LEVELS.map((level) => [
3520
+ `agentbox-${level}`,
3521
+ { ...baseAgent, reasoningEffort: level }
3522
+ ])
3523
+ );
3524
+ return {
3525
+ $schema: "https://opencode.ai/config.json",
3526
+ ...mcpConfig ? { mcp: mcpConfig } : {},
3527
+ ...commandsConfig ? { command: commandsConfig } : {},
3528
+ agent: {
3529
+ agentbox: baseAgent,
3530
+ ...reasoningVariants,
3531
+ ...buildOpenCodeSubagentConfig(options.subAgents)
3532
+ }
3533
+ };
3534
+ }
3535
+ async function ensureSandboxOpenCodeServer(request) {
3536
+ return time(debugOpencode, "ensureSandboxOpenCodeServer", async () => {
3537
+ const sandbox = request.options.sandbox;
3538
+ const options = request.options;
3539
+ const port = SANDBOX_OPENCODE_PORT;
3540
+ const healthCheck = await time(
3541
+ debugOpencode,
3542
+ "health probe (warm path)",
3543
+ () => sandbox.run(
3544
+ `curl -fsS http://127.0.0.1:${port}/global/health >/dev/null 2>&1`,
3545
+ { cwd: options.cwd, timeoutMs: 5e3 }
3546
+ )
3547
+ );
3548
+ if (healthCheck.exitCode === 0) {
3549
+ debugOpencode("opencode server already running \u2014 reusing");
3550
+ return;
3551
+ }
3552
+ debugOpencode("opencode server not running \u2014 cold-spawning");
3553
+ const plugins = assertHooksSupported(request.provider, options);
3554
+ assertCommandsSupported(request.provider, options.commands);
3555
+ const interactiveApproval = isInteractiveApproval(options);
3556
+ const target = await createSetupTarget(
3557
+ request.provider,
3558
+ SHARED_OPENCODE_TARGET_ID,
3559
+ options
3560
+ );
3561
+ const { artifacts: skillArtifacts, installCommands } = await prepareSkillArtifacts(
3562
+ request.provider,
3563
+ options.skills,
3564
+ target.layout
3565
+ );
3566
+ const pluginArtifacts = buildOpenCodePluginArtifacts(
3567
+ plugins,
3568
+ target.layout.opencodeDir
3569
+ );
3570
+ const configPath = path10.join(target.layout.opencodeDir, "agentbox.json");
3571
+ const openCodeConfig = buildOpenCodeConfig(
3572
+ options,
3573
+ request.config.systemPrompt ?? "",
3574
+ interactiveApproval
3575
+ );
3576
+ const commonEnv = {
3577
+ OPENCODE_CONFIG: configPath,
3578
+ OPENCODE_CONFIG_DIR: target.layout.opencodeDir,
3579
+ OPENCODE_DISABLE_DEFAULT_PLUGINS: "true"
3580
+ };
3581
+ await applyDifferentialSetup(
3582
+ target,
3583
+ [
3584
+ ...skillArtifacts,
3585
+ ...pluginArtifacts,
3586
+ {
3587
+ path: configPath,
3588
+ content: JSON.stringify(openCodeConfig, null, 2)
3589
+ }
3590
+ ],
3591
+ installCommands
3592
+ );
3593
+ const binary = options.provider?.binary ?? "opencode";
3594
+ const pidFilePath = path10.posix.join(
3595
+ target.layout.rootDir,
3596
+ "opencode-serve.pid"
3597
+ );
3598
+ const logFilePath = path10.posix.join(
3599
+ target.layout.rootDir,
3600
+ "opencode-serve.log"
3601
+ );
3602
+ const serveEnv = { ...options.env ?? {}, ...commonEnv };
3603
+ const launchCommand = [
3604
+ `mkdir -p ${shellQuote(target.layout.rootDir)}`,
3605
+ `(${[
3606
+ `nohup ${[
3607
+ binary,
3608
+ "serve",
3609
+ "--hostname",
3610
+ "0.0.0.0",
3611
+ "--port",
3612
+ String(port),
3613
+ ...options.provider?.args ?? []
3614
+ ].map(shellQuote).join(" ")} > ${shellQuote(logFilePath)} 2>&1 &`,
3615
+ `echo $! > ${shellQuote(pidFilePath)}`
3616
+ ].join(" ")})`
3617
+ ].join(" && ");
3618
+ const launchResult = await time(
3619
+ debugOpencode,
3620
+ "spawn opencode serve",
3621
+ async () => {
3622
+ const launchHandle = await sandbox.runAsync(launchCommand, {
3623
+ cwd: options.cwd,
3624
+ env: serveEnv
3625
+ });
3626
+ return launchHandle.wait();
3627
+ }
3628
+ );
3629
+ if (launchResult.exitCode !== 0) {
3630
+ await target.cleanup().catch(() => void 0);
3631
+ throw new Error(
3632
+ `Could not start OpenCode server: ${launchResult.combinedOutput || launchResult.stderr}`
3633
+ );
3634
+ }
3635
+ await time(debugOpencode, "poll opencode until ready", async () => {
3636
+ const readyDeadline = Date.now() + SANDBOX_OPENCODE_READY_TIMEOUT_MS;
3637
+ let attempt = 0;
3638
+ while (Date.now() < readyDeadline) {
3639
+ attempt++;
3640
+ const probe = await sandbox.run(
3641
+ `curl -fsS http://127.0.0.1:${port}/global/health >/dev/null 2>&1`,
3642
+ { cwd: options.cwd, timeoutMs: 5e3 }
3643
+ );
3644
+ if (probe.exitCode === 0) {
3645
+ debugOpencode("ready after %d probe attempt(s)", attempt);
3646
+ return;
3647
+ }
3648
+ await sleep(500);
3649
+ }
3650
+ await target.cleanup().catch(() => void 0);
3651
+ throw new Error(
3652
+ `OpenCode server did not become ready within ${SANDBOX_OPENCODE_READY_TIMEOUT_MS}ms.`
3653
+ );
3654
+ });
3655
+ });
3656
+ }
3657
+ async function ensureLocalOpenCodeServer(request) {
3658
+ const options = request.options;
3659
+ try {
3660
+ await waitForHttpReady(
3661
+ `http://127.0.0.1:${LOCAL_OPENCODE_PORT}/global/health`,
3662
+ { timeoutMs: 1e3 }
3663
+ );
3664
+ debugOpencode("local opencode server already running \u2014 reusing");
3665
+ return;
3666
+ } catch {
3667
+ debugOpencode("local opencode server not running \u2014 cold-spawning");
3668
+ }
3669
+ const plugins = assertHooksSupported(request.provider, options);
3670
+ assertCommandsSupported(request.provider, options.commands);
3671
+ const interactiveApproval = isInteractiveApproval(options);
3672
+ const target = await createSetupTarget(
3673
+ request.provider,
3674
+ "shared-setup",
3675
+ options
3676
+ );
3677
+ const { artifacts: skillArtifacts, installCommands } = await prepareSkillArtifacts(
3678
+ request.provider,
3679
+ options.skills,
3680
+ target.layout
3681
+ );
3682
+ const pluginArtifacts = buildOpenCodePluginArtifacts(
3683
+ plugins,
3684
+ target.layout.opencodeDir
3685
+ );
3686
+ const configPath = path10.join(target.layout.opencodeDir, "agentbox.json");
3687
+ const openCodeConfig = buildOpenCodeConfig(
3688
+ options,
3689
+ request.config.systemPrompt ?? "",
3690
+ interactiveApproval
3691
+ );
3692
+ const commonEnv = {
3693
+ OPENCODE_CONFIG: configPath,
3694
+ OPENCODE_CONFIG_DIR: target.layout.opencodeDir,
3695
+ OPENCODE_DISABLE_DEFAULT_PLUGINS: "true"
3696
+ };
3697
+ await applyDifferentialSetup(
3698
+ target,
3699
+ [
3700
+ ...skillArtifacts,
3701
+ ...pluginArtifacts,
3702
+ {
3703
+ path: configPath,
3704
+ content: JSON.stringify(openCodeConfig, null, 2)
3705
+ }
3706
+ ],
3707
+ installCommands
3708
+ );
3709
+ spawnCommand({
3710
+ command: options.provider?.binary ?? "opencode",
3711
+ args: [
3712
+ "serve",
3713
+ "--hostname",
3714
+ "127.0.0.1",
3715
+ "--port",
3716
+ String(LOCAL_OPENCODE_PORT),
3717
+ ...options.provider?.args ?? []
3718
+ ],
3719
+ cwd: options.cwd,
3720
+ env: {
3721
+ ...process.env,
3722
+ ...options.env ?? {},
3723
+ ...commonEnv
3724
+ }
3725
+ });
3726
+ await waitForHttpReady(
3727
+ `http://127.0.0.1:${LOCAL_OPENCODE_PORT}/global/health`,
3728
+ { timeoutMs: LOCAL_OPENCODE_READY_TIMEOUT_MS }
3729
+ );
3730
+ }
3731
+ async function setupOpenCode(request) {
3732
+ if (request.options.sandbox) {
3733
+ await ensureSandboxOpenCodeServer(request);
3734
+ return;
3735
+ }
3736
+ await ensureLocalOpenCodeServer(request);
3737
+ }
3738
+ async function buildOpenCodeRuntime(options) {
3739
+ if (options.sandbox) {
3740
+ const sandbox = options.sandbox;
3741
+ const baseUrl2 = (await sandbox.getPreviewLink(SANDBOX_OPENCODE_PORT)).replace(/\/$/, "");
3742
+ return {
3743
+ baseUrl: baseUrl2,
3744
+ previewHeaders: sandbox.previewHeaders,
3745
+ raw: { baseUrl: baseUrl2, port: SANDBOX_OPENCODE_PORT }
3746
+ };
3747
+ }
3748
+ const baseUrl = `http://127.0.0.1:${LOCAL_OPENCODE_PORT}`;
3749
+ return {
3750
+ baseUrl,
3751
+ previewHeaders: {},
3752
+ raw: { baseUrl, port: LOCAL_OPENCODE_PORT }
3753
+ };
3754
+ }
3755
+ var OpenCodeAgentAdapter = class {
3756
+ async setup(request) {
3757
+ await setupOpenCode(request);
3758
+ }
3759
+ async execute(request, sink) {
3760
+ const executeStartedAt = Date.now();
3761
+ debugOpencode("execute() start runId=%s", request.runId);
3762
+ const inputParts = await time(
3763
+ debugOpencode,
3764
+ "validateProviderUserInput",
3765
+ () => validateProviderUserInput(request.provider, request.run.input)
3766
+ );
3767
+ let pendingMessages = 0;
3768
+ let finalText = "";
3769
+ let streamedTextFromSse = "";
3770
+ const settledMessageIds = /* @__PURE__ */ new Set();
3771
+ let dispatchError;
3772
+ let firstSseEventLogged = false;
3773
+ let resolveAllDone;
3774
+ const allDone = new Promise((resolve) => {
3775
+ resolveAllDone = resolve;
3776
+ });
3777
+ const checkDone = () => {
3778
+ if (pendingMessages === 0) {
3779
+ resolveAllDone();
3780
+ }
3781
+ };
3782
+ let sendToSession;
3783
+ const queuedParts = [];
3784
+ sink.onMessage(async (content) => {
3785
+ pendingMessages++;
3786
+ try {
3787
+ const parts = await validateProviderUserInput(
3788
+ request.provider,
3789
+ content
3790
+ );
3791
+ const mapped = mapToOpenCodeParts(parts);
3792
+ if (sendToSession) {
3793
+ sendToSession(mapped);
3794
+ } else {
3795
+ queuedParts.push(mapped);
3796
+ }
3797
+ } catch (error) {
3798
+ pendingMessages--;
3799
+ if (!dispatchError) {
3800
+ dispatchError = error;
3801
+ }
3802
+ checkDone();
3803
+ throw error;
3804
+ }
3805
+ });
3806
+ const runtime = await time(
3807
+ debugOpencode,
3808
+ "buildOpenCodeRuntime",
3809
+ () => buildOpenCodeRuntime(request.options)
3810
+ );
3811
+ sink.setRaw(runtime.raw);
3812
+ sink.emitEvent(
3813
+ createNormalizedEvent("run.started", {
3814
+ provider: request.provider,
3815
+ runId: request.runId
3816
+ })
3817
+ );
3818
+ const rawPayloads = [];
3819
+ const sseAbort = new AbortController();
3820
+ let sseTask;
3821
+ const dispatchAbort = new AbortController();
3822
+ let capturedSessionId;
3823
+ sink.setAbort(async () => {
3824
+ const sessionIdAtAbort = capturedSessionId;
3825
+ if (sessionIdAtAbort) {
3826
+ try {
3827
+ await Promise.race([
3828
+ fetchJson(
3829
+ `${runtime.baseUrl}/session/${sessionIdAtAbort}/abort`,
3830
+ {
3831
+ method: "POST",
3832
+ headers: {
3833
+ "content-type": "application/json",
3834
+ ...runtime.previewHeaders
3835
+ }
3836
+ }
3837
+ ),
3838
+ new Promise(
3839
+ (_, reject) => setTimeout(
3840
+ () => reject(new Error("opencode POST /session/abort timed out")),
3841
+ 3e3
3842
+ )
3843
+ )
3844
+ ]);
3845
+ } catch {
3846
+ }
3847
+ }
3848
+ dispatchAbort.abort();
3849
+ });
3850
+ try {
3851
+ const interactiveApproval = isInteractiveApproval(request.options);
3852
+ const createdSession = request.run.resumeSessionId ? null : await fetchJson(
3853
+ `${runtime.baseUrl}/session`,
3854
+ {
3855
+ method: "POST",
3856
+ headers: {
3857
+ "content-type": "application/json",
3858
+ ...runtime.previewHeaders
3859
+ },
3860
+ body: JSON.stringify({
3861
+ title: `AgentBox ${request.runId}`
3862
+ })
3863
+ }
3864
+ );
3865
+ const sessionId = request.run.resumeSessionId ?? createdSession?.id ?? createdSession?.sessionId;
3866
+ if (!sessionId) {
3867
+ throw new Error("OpenCode did not return a session id.");
3868
+ }
3869
+ const announcedUserMessageIds = /* @__PURE__ */ new Set();
3870
+ const foreignMessageIds = /* @__PURE__ */ new Set();
3871
+ sseTask = (async () => {
3872
+ try {
3873
+ for await (const event of streamSse(`${runtime.baseUrl}/event`, {
3874
+ headers: runtime.previewHeaders,
3875
+ signal: sseAbort.signal
3876
+ })) {
3877
+ if (!firstSseEventLogged) {
3878
+ firstSseEventLogged = true;
3879
+ debugOpencode(
3880
+ "\u2605 first SSE event (%dms since execute start) type=%s",
3881
+ Date.now() - executeStartedAt,
3882
+ event.event
3883
+ );
3884
+ }
3885
+ let payload = event.data;
3886
+ try {
3887
+ payload = JSON.parse(event.data);
3888
+ } catch {
3889
+ }
3890
+ const raw = toRawEvent3(
3891
+ request.runId,
3892
+ payload,
3893
+ `sse:${event.event ?? "message"}`
3894
+ );
3895
+ if (payload && typeof payload === "object" && !Array.isArray(payload)) {
3896
+ rawPayloads.push(payload);
3897
+ }
3898
+ sink.emitRaw(raw);
3899
+ const eventType = typeof payload?.type === "string" ? String(payload.type) : event.event;
3900
+ if (eventType === "message.updated") {
3901
+ const properties = payload.properties;
3902
+ const info = properties?.info;
3903
+ if (info && typeof info.id === "string" && typeof info.sessionID === "string") {
3904
+ if (info.sessionID !== sessionId) {
3905
+ foreignMessageIds.add(info.id);
3906
+ } else if (info.role === "user" && !announcedUserMessageIds.has(info.id)) {
3907
+ announcedUserMessageIds.add(info.id);
3908
+ sink.emitEvent(
3909
+ createNormalizedEvent(
3910
+ "message.started",
3911
+ {
3912
+ provider: request.provider,
3913
+ runId: request.runId,
3914
+ raw
3915
+ },
3916
+ { messageId: info.id }
3917
+ )
3918
+ );
3919
+ }
3920
+ }
3921
+ }
3922
+ if (eventType === "permission.asked") {
3923
+ const properties = payload.properties;
3924
+ if (properties && typeof properties.sessionID === "string" && properties.sessionID === sessionId) {
3925
+ const permissionEvent = createOpenCodePermissionEvent(
3926
+ request,
3927
+ raw,
3928
+ payload
3929
+ );
3930
+ const response = interactiveApproval ? await sink.requestPermission(permissionEvent) : {
3931
+ requestId: permissionEvent.requestId,
3932
+ decision: "allow"
3933
+ };
3934
+ await fetchJson(
3935
+ `${runtime.baseUrl}/session/${sessionId}/permissions/${permissionEvent.requestId}`,
3936
+ {
3937
+ method: "POST",
3938
+ headers: {
3939
+ "content-type": "application/json",
3940
+ ...runtime.previewHeaders
3941
+ },
3942
+ body: JSON.stringify({
3943
+ response: response.decision === "allow" ? response.remember ? "always" : "once" : "reject"
3944
+ })
3945
+ }
3946
+ );
3947
+ }
3948
+ continue;
3949
+ }
3950
+ const payloadRecord = payload && typeof payload === "object" && !Array.isArray(payload) ? payload : null;
3951
+ if ((payloadRecord?.type === "session.idle" || payloadRecord?.type === "session.error") && !dispatchAbort.signal.aborted) {
3952
+ const properties = payloadRecord.properties;
3953
+ const eventSessionId = typeof properties?.sessionID === "string" ? properties.sessionID : void 0;
3954
+ if (!eventSessionId || eventSessionId === sessionId) {
3955
+ debugOpencode(
3956
+ "\u2605 %s for session=%s \u2014 aborting in-flight dispatch",
3957
+ payloadRecord.type,
3958
+ sessionId
3959
+ );
3960
+ dispatchAbort.abort();
3961
+ }
3962
+ }
3963
+ if (payloadRecord?.type === "message.part.delta") {
3964
+ const properties = payloadRecord.properties;
3965
+ const eventSessionId = typeof properties?.sessionID === "string" ? properties.sessionID : void 0;
3966
+ const eventMessageId = typeof properties?.messageID === "string" ? properties.messageID : void 0;
3967
+ const isForeignSession = eventSessionId !== void 0 && eventSessionId !== sessionId || eventSessionId === void 0 && eventMessageId !== void 0 && foreignMessageIds.has(eventMessageId);
3968
+ if (isForeignSession) {
3969
+ continue;
3970
+ }
3971
+ if (eventMessageId !== void 0 && settledMessageIds.has(eventMessageId)) {
3972
+ continue;
3973
+ }
3974
+ const delta = typeof properties?.delta === "string" ? properties.delta : "";
3975
+ if (delta && properties?.field === "text") {
3976
+ streamedTextFromSse += delta;
3977
+ sink.emitEvent(
3978
+ createNormalizedEvent(
3979
+ "text.delta",
3980
+ {
3981
+ provider: request.provider,
3982
+ runId: request.runId,
3983
+ raw
3984
+ },
3985
+ { delta }
3986
+ )
3987
+ );
3988
+ } else if (delta && (properties?.field === "reasoning" || properties?.field === "thinking")) {
3989
+ sink.emitEvent(
3990
+ createNormalizedEvent(
3991
+ "reasoning.delta",
3992
+ {
3993
+ provider: request.provider,
3994
+ runId: request.runId,
3995
+ raw
3996
+ },
3997
+ { delta }
3998
+ )
3999
+ );
4000
+ }
4001
+ } else {
4002
+ for (const normalized of normalizeRawAgentEvent(raw)) {
4003
+ sink.emitEvent(normalized);
4004
+ }
4005
+ }
4006
+ }
4007
+ } catch {
4008
+ }
4009
+ })();
4010
+ capturedSessionId = sessionId;
4011
+ sink.setSessionId(sessionId);
4012
+ sink.emitRaw(
4013
+ toRawEvent3(
4014
+ request.runId,
4015
+ createdSession ?? { sessionId },
4016
+ request.run.resumeSessionId ? "session.resumed" : "session.created"
4017
+ )
4018
+ );
4019
+ if (createdSession) {
4020
+ rawPayloads.push(createdSession);
4021
+ }
4022
+ sink.emitEvent(
4023
+ createNormalizedEvent("message.started", {
4024
+ provider: request.provider,
4025
+ runId: request.runId
4026
+ })
4027
+ );
4028
+ const agentSlug = openCodeAgentSlug(request.run.reasoning);
4029
+ const dispatchMessage = async (parts) => {
4030
+ const sseTextLengthBeforeDispatch = streamedTextFromSse.length;
4031
+ try {
4032
+ const response = await fetchJson(
4033
+ `${runtime.baseUrl}/session/${sessionId}/message`,
4034
+ {
4035
+ method: "POST",
4036
+ signal: dispatchAbort.signal,
4037
+ headers: {
4038
+ "content-type": "application/json",
4039
+ ...runtime.previewHeaders
4040
+ },
4041
+ body: JSON.stringify({
4042
+ ...request.run.model ? { model: toOpenCodeModel(request.run.model) } : {},
4043
+ agent: agentSlug,
4044
+ parts
4045
+ })
4046
+ }
4047
+ );
4048
+ const rawResponse = toRawEvent3(
4049
+ request.runId,
4050
+ response,
4051
+ "message.response"
4052
+ );
4053
+ if (response && typeof response === "object" && !Array.isArray(response)) {
4054
+ rawPayloads.push(response);
4055
+ }
4056
+ sink.emitRaw(rawResponse);
4057
+ for (const event of normalizeRawAgentEvent(rawResponse)) {
4058
+ sink.emitEvent(event);
4059
+ }
4060
+ const reasoning = extractReasoning(response);
4061
+ if (reasoning) {
4062
+ sink.emitEvent(
4063
+ createNormalizedEvent(
4064
+ "reasoning.delta",
4065
+ {
4066
+ provider: request.provider,
4067
+ runId: request.runId,
4068
+ raw: rawResponse
4069
+ },
4070
+ { delta: reasoning }
4071
+ )
4072
+ );
4073
+ }
4074
+ const text = extractText(response);
4075
+ if (text) {
4076
+ finalText = text;
4077
+ const sseTextForThisDispatch = streamedTextFromSse.slice(
4078
+ sseTextLengthBeforeDispatch
4079
+ );
4080
+ let missing;
4081
+ if (text.startsWith(sseTextForThisDispatch)) {
4082
+ missing = text.slice(sseTextForThisDispatch.length);
4083
+ } else if (sseTextForThisDispatch.length === 0) {
4084
+ missing = text;
4085
+ } else {
4086
+ missing = "";
4087
+ }
4088
+ if (missing.length > 0) {
4089
+ streamedTextFromSse += missing;
4090
+ sink.emitEvent(
4091
+ createNormalizedEvent(
4092
+ "text.delta",
4093
+ {
4094
+ provider: request.provider,
4095
+ runId: request.runId
4096
+ },
4097
+ { delta: missing }
4098
+ )
4099
+ );
4100
+ }
4101
+ const assistantMessageId = extractAssistantMessageId(response);
4102
+ if (assistantMessageId) {
4103
+ settledMessageIds.add(assistantMessageId);
4104
+ }
4105
+ }
4106
+ } catch (error) {
4107
+ if (!dispatchError) {
4108
+ dispatchError = error;
4109
+ }
4110
+ } finally {
4111
+ pendingMessages--;
4112
+ checkDone();
4113
+ }
4114
+ };
4115
+ sendToSession = (parts) => {
4116
+ void dispatchMessage(parts);
4117
+ };
4118
+ for (const queued of queuedParts.splice(0)) {
4119
+ sendToSession(queued);
4120
+ }
4121
+ pendingMessages++;
4122
+ void dispatchMessage(mapToOpenCodeParts(inputParts));
4123
+ await allDone;
4124
+ if (dispatchError && !dispatchAbort.signal.aborted) {
4125
+ throw dispatchError;
4126
+ }
4127
+ debugOpencode(
4128
+ "\u2605 run.completed (%dms since execute start) chars=%d",
4129
+ Date.now() - executeStartedAt,
4130
+ finalText.length
4131
+ );
4132
+ sink.emitEvent(
4133
+ createNormalizedEvent(
4134
+ "run.completed",
4135
+ {
4136
+ provider: request.provider,
4137
+ runId: request.runId
4138
+ },
4139
+ { text: finalText }
4140
+ )
4141
+ );
4142
+ sseAbort.abort();
4143
+ await sseTask;
4144
+ sink.complete({
4145
+ text: finalText,
4146
+ costData: extractOpenCodeCostData(rawPayloads)
4147
+ });
4148
+ } finally {
4149
+ sseAbort.abort();
4150
+ if (sseTask) {
4151
+ await sseTask.catch(() => void 0);
4152
+ }
4153
+ }
4154
+ return async () => void 0;
4155
+ }
4156
+ /**
4157
+ * Stateless abort. Resolve the in-sandbox base URL via
4158
+ * `sandbox.getPreviewLink` and POST to `/session/:id/abort`. Best-effort:
4159
+ * a 3s timeout protects against an unresponsive server, and any error
4160
+ * is swallowed since the originating run will tear itself down once
4161
+ * the server-side abort takes effect.
4162
+ */
4163
+ async attachAbort(request) {
4164
+ if (!request.sessionId) {
4165
+ throw new Error(
4166
+ `Cannot attachAbort to opencode run ${request.runId}: sessionId is required.`
4167
+ );
4168
+ }
4169
+ const baseUrl = (await request.sandbox.getPreviewLink(SANDBOX_OPENCODE_PORT)).replace(/\/$/, "");
4170
+ const controller = new AbortController();
4171
+ const timeout = setTimeout(() => controller.abort(), 3e3);
4172
+ try {
4173
+ await fetch(`${baseUrl}/session/${request.sessionId}/abort`, {
4174
+ method: "POST",
4175
+ signal: controller.signal,
4176
+ headers: {
4177
+ "content-type": "application/json",
4178
+ ...request.sandbox.previewHeaders
4179
+ }
4180
+ }).catch((error) => {
4181
+ debugOpencode(
4182
+ "attachAbort runId=%s POST /abort failed: %o",
4183
+ request.runId,
4184
+ error
4185
+ );
4186
+ });
4187
+ } finally {
4188
+ clearTimeout(timeout);
4189
+ }
4190
+ }
4191
+ /**
4192
+ * Stateless message injection. POST a fresh user message to
4193
+ * `/session/:id/message` with `agent` defaulting to the build agent
4194
+ * — opencode appends it to the running session and the originating
4195
+ * instance picks up the new turn through its existing SSE stream.
4196
+ */
4197
+ async attachSendMessage(request, content) {
4198
+ if (!request.sessionId) {
4199
+ throw new Error(
4200
+ `Cannot attachSendMessage to opencode run ${request.runId}: sessionId is required.`
4201
+ );
4202
+ }
4203
+ const baseUrl = (await request.sandbox.getPreviewLink(SANDBOX_OPENCODE_PORT)).replace(/\/$/, "");
4204
+ const inputParts = await validateProviderUserInput(
4205
+ AgentProvider.OpenCode,
4206
+ content
4207
+ );
4208
+ const parts = mapToOpenCodeParts(inputParts);
4209
+ await fetchJson(
4210
+ `${baseUrl}/session/${request.sessionId}/message`,
4211
+ {
4212
+ method: "POST",
4213
+ headers: {
4214
+ "content-type": "application/json",
4215
+ ...request.sandbox.previewHeaders
4216
+ },
4217
+ body: JSON.stringify({
4218
+ agent: openCodeAgentSlug(void 0),
4219
+ parts
4220
+ })
4221
+ }
4222
+ );
4223
+ }
4224
+ };
4225
+
4226
+ // src/agents/Agent.ts
4227
+ function buildAgentOptionsSystemAppendix(options) {
4228
+ const sections = [];
4229
+ if (options.mcps?.length) {
4230
+ sections.push(
4231
+ [
4232
+ "Configured MCP servers are available for this run:",
4233
+ ...options.mcps.map((mcp) => `- ${mcp.name}`)
4234
+ ].join("\n")
4235
+ );
4236
+ }
4237
+ if (options.skills?.length) {
4238
+ sections.push(
4239
+ [
4240
+ "Configured skills are available for this run:",
4241
+ ...options.skills.map((skill) => `- ${skill.name}`)
4242
+ ].join("\n")
4243
+ );
4244
+ }
4245
+ if (options.subAgents?.length) {
4246
+ sections.push(
4247
+ [
4248
+ "Configured sub-agents are available for delegation:",
4249
+ ...options.subAgents.map(
4250
+ (subAgent) => `- ${subAgent.name}: ${subAgent.description}`
4251
+ )
4252
+ ].join("\n")
4253
+ );
4254
+ }
4255
+ if (options.commands?.length) {
4256
+ sections.push(
4257
+ [
4258
+ "Custom commands are installed for this environment:",
4259
+ ...options.commands.map(
4260
+ (command) => `- /${command.name}${command.description ? `: ${command.description}` : ""}`
4261
+ )
4262
+ ].join("\n")
4263
+ );
4264
+ }
4265
+ return sections.length > 0 ? sections.join("\n\n") : void 0;
4266
+ }
4267
+ function buildRunConfig(options, runConfig) {
4268
+ const appendix = buildAgentOptionsSystemAppendix(options);
4269
+ const systemPrompt = [runConfig.systemPrompt, appendix].filter(Boolean).join("\n\n");
4270
+ return {
4271
+ ...runConfig,
4272
+ ...systemPrompt ? { systemPrompt } : {}
4273
+ };
4274
+ }
4275
+ function createAdapter(provider) {
4276
+ switch (provider) {
4277
+ case AgentProvider.Codex:
4278
+ return new CodexAgentAdapter();
4279
+ case AgentProvider.OpenCode:
4280
+ return new OpenCodeAgentAdapter();
4281
+ case AgentProvider.ClaudeCode:
4282
+ return new ClaudeCodeAgentAdapter();
4283
+ default:
4284
+ throw new UnsupportedProviderError("agent", provider);
4285
+ }
4286
+ }
4287
+ function prepareAgentOptions(_provider, options) {
4288
+ return options;
4289
+ }
4290
+ var AgentRunController = class {
4291
+ id;
4292
+ provider;
4293
+ sessionId;
4294
+ raw;
4295
+ sessionIdReady;
4296
+ abortHandler = async () => void 0;
4297
+ abortRequested = false;
4298
+ eventQueue = new AsyncQueue();
4299
+ rawQueue = new AsyncQueue();
4300
+ events = [];
4301
+ rawEventsList = [];
4302
+ pendingPermissions = /* @__PURE__ */ new Map();
4303
+ messageHandler;
4304
+ text = "";
4305
+ costData = null;
4306
+ settled = false;
4307
+ finished;
4308
+ resolveSessionIdReady;
4309
+ rejectSessionIdReady;
4310
+ resolveFinished;
4311
+ rejectFinished;
4312
+ constructor(provider, id) {
4313
+ this.provider = provider;
4314
+ this.id = id;
4315
+ let resolveFinished;
4316
+ let rejectFinished;
4317
+ let resolveSessionIdReady;
4318
+ let rejectSessionIdReady;
4319
+ this.finished = new Promise((resolve, reject) => {
4320
+ resolveFinished = resolve;
4321
+ rejectFinished = reject;
4322
+ });
4323
+ this.sessionIdReady = new Promise((resolve, reject) => {
4324
+ resolveSessionIdReady = resolve;
4325
+ rejectSessionIdReady = reject;
4326
+ });
4327
+ void this.sessionIdReady.catch(() => void 0);
4328
+ this.resolveFinished = resolveFinished;
4329
+ this.rejectFinished = rejectFinished;
4330
+ this.resolveSessionIdReady = resolveSessionIdReady;
4331
+ this.rejectSessionIdReady = rejectSessionIdReady;
4332
+ }
4333
+ setRaw(raw) {
4334
+ this.raw = raw;
4335
+ }
4336
+ setAbort(abort) {
4337
+ this.abortHandler = abort;
4338
+ if (this.abortRequested) {
4339
+ void abort();
4340
+ }
4341
+ }
4342
+ setSessionId(sessionId) {
4343
+ if (this.sessionId) {
4344
+ return;
4345
+ }
4346
+ this.sessionId = sessionId;
4347
+ this.resolveSessionIdReady(sessionId);
4348
+ }
4349
+ emitRaw(event) {
4350
+ this.rawEventsList.push(event);
4351
+ this.rawQueue.push(event);
4352
+ }
4353
+ pushEvent(event) {
4354
+ this.events.push(event);
4355
+ if (event.type === "text.delta") {
4356
+ this.text += event.delta;
4357
+ } else if ((event.type === "message.completed" || event.type === "run.completed") && event.text) {
4358
+ this.text = event.text;
4359
+ }
4360
+ this.eventQueue.push(event);
4361
+ }
4362
+ emitEvent(event) {
4363
+ this.pushEvent(event);
4364
+ }
4365
+ requestPermission(event) {
4366
+ if (this.settled) {
4367
+ throw new Error("Cannot request permission on a settled agent run.");
4368
+ }
4369
+ if (this.pendingPermissions.has(event.requestId)) {
4370
+ throw new Error(
4371
+ `Permission request ${event.requestId} is already pending for this run.`
4372
+ );
4373
+ }
4374
+ const response = new Promise((resolve, reject) => {
4375
+ this.pendingPermissions.set(event.requestId, {
4376
+ event,
4377
+ resolve,
4378
+ reject
4379
+ });
4380
+ });
4381
+ this.pushEvent(event);
4382
+ return response;
4383
+ }
4384
+ onMessage(handler) {
4385
+ this.messageHandler = handler;
4386
+ }
4387
+ async sendMessage(content) {
4388
+ if (this.settled) {
4389
+ throw new Error("Cannot send a message on a settled agent run.");
4390
+ }
4391
+ if (!this.messageHandler) {
4392
+ throw new Error(
4393
+ "This provider does not support sending messages during a run."
4394
+ );
4395
+ }
4396
+ const textContent = normalizeUserInput(content).filter((p) => p.type === "text").map((p) => p.text).join("");
4397
+ const handlerResult = await this.messageHandler(content);
4398
+ const messageId = handlerResult?.messageId;
4399
+ this.pushEvent(
4400
+ createNormalizedEvent(
4401
+ "message.injected",
4402
+ { provider: this.provider, runId: this.id },
4403
+ {
4404
+ content: textContent || "(non-text content)",
4405
+ ...messageId ? { messageId } : {}
4406
+ }
4407
+ )
4408
+ );
4409
+ }
4410
+ async respondToPermission(response) {
4411
+ const pending = this.pendingPermissions.get(response.requestId);
4412
+ if (!pending) {
4413
+ throw new Error(
4414
+ `Permission request ${response.requestId} is not pending for this run.`
4415
+ );
4416
+ }
4417
+ this.pendingPermissions.delete(response.requestId);
4418
+ const remember = pending.event.canRemember ? response.remember : void 0;
4419
+ const resolvedResponse = {
4420
+ requestId: response.requestId,
4421
+ decision: response.decision,
4422
+ ...remember !== void 0 ? { remember } : {}
4423
+ };
4424
+ this.pushEvent(
4425
+ createNormalizedEvent(
4426
+ "permission.resolved",
4427
+ {
4428
+ provider: this.provider,
4429
+ runId: this.id
4430
+ },
4431
+ {
4432
+ requestId: response.requestId,
4433
+ decision: response.decision,
4434
+ ...remember !== void 0 ? { remember } : {}
4435
+ }
4436
+ )
4437
+ );
4438
+ pending.resolve(resolvedResponse);
4439
+ }
4440
+ clearPendingPermissions(error) {
4441
+ for (const pending of this.pendingPermissions.values()) {
4442
+ pending.reject(error);
4443
+ }
4444
+ this.pendingPermissions.clear();
4445
+ }
4446
+ complete(result) {
4447
+ if (this.settled) {
4448
+ return;
4449
+ }
4450
+ this.settled = true;
4451
+ this.clearPendingPermissions(
4452
+ new Error(
4453
+ "Agent run completed before pending permission requests resolved."
4454
+ )
4455
+ );
4456
+ if (result?.text) {
4457
+ this.text = result.text;
4458
+ }
4459
+ if (result && "costData" in result) {
4460
+ this.costData = result.costData ?? null;
4461
+ }
4462
+ this.eventQueue.finish();
4463
+ this.rawQueue.finish();
4464
+ if (!this.sessionId) {
4465
+ const error = new Error(
4466
+ "Agent run completed before a provider session id was set."
4467
+ );
4468
+ this.rejectSessionIdReady(error);
4469
+ this.rejectFinished(error);
4470
+ return;
4471
+ }
4472
+ this.resolveFinished({
4473
+ id: this.id,
4474
+ provider: this.provider,
4475
+ sessionId: this.sessionId,
4476
+ text: this.text,
4477
+ rawEvents: [...this.rawEventsList],
4478
+ events: [...this.events],
4479
+ costData: this.costData
4480
+ });
4481
+ }
4482
+ fail(error) {
4483
+ if (this.settled) {
4484
+ return;
4485
+ }
4486
+ const normalizedError = asError(error);
4487
+ this.clearPendingPermissions(normalizedError);
4488
+ this.emitEvent(
4489
+ createNormalizedEvent(
4490
+ "run.error",
4491
+ {
4492
+ provider: this.provider,
4493
+ runId: this.id
4494
+ },
4495
+ {
4496
+ error: normalizedError.message
4497
+ }
4498
+ )
4499
+ );
4500
+ this.settled = true;
4501
+ this.eventQueue.finish();
4502
+ this.rawQueue.finish();
4503
+ if (!this.sessionId) {
4504
+ this.rejectSessionIdReady(normalizedError);
4505
+ }
4506
+ this.rejectFinished(normalizedError);
4507
+ }
4508
+ async abort() {
4509
+ this.abortRequested = true;
4510
+ await this.abortHandler();
4511
+ }
4512
+ rawEvents() {
4513
+ return this.rawQueue;
4514
+ }
4515
+ toAISDKEvents() {
4516
+ return toAISDKStream(this);
4517
+ }
4518
+ [Symbol.asyncIterator]() {
4519
+ return this.eventQueue[Symbol.asyncIterator]();
4520
+ }
4521
+ };
4522
+ var Agent = class {
4523
+ adapter;
4524
+ provider;
4525
+ options;
4526
+ setupPromise;
4527
+ constructor(provider, options) {
4528
+ this.provider = provider;
4529
+ this.options = prepareAgentOptions(provider, options);
4530
+ this.adapter = createAdapter(provider);
4531
+ }
4532
+ /**
4533
+ * The sandbox the agent will run inside, if any was passed via
4534
+ * `options.sandbox`. Returns `undefined` for host-mode runs (no sandbox).
4535
+ */
4536
+ get sandbox() {
4537
+ return this.options.sandbox;
4538
+ }
4539
+ /**
4540
+ * Prepare provider-specific runtime state on the configured sandbox
4541
+ * (skill artifacts, MCP/hook/sub-agent config, app-server / relay boot, …).
4542
+ *
4543
+ * `setup()` is REQUIRED before {@link Agent.stream} or {@link Agent.run}
4544
+ * for any sandbox-backed run. `stream` and the underlying
4545
+ * `adapter.execute` deliberately do not trigger setup themselves so
4546
+ * callers can run sandbox-side preparation in parallel with other
4547
+ * long-running work (e.g. `git clone`).
4548
+ *
4549
+ * `execute` does not consume any setup output and does not re-do
4550
+ * setup work. It assumes the relay/server boot performed here is
4551
+ * already up. Skipping `setup()` against a remote sandbox is a
4552
+ * programmer error and surfaces as a connect-retry timeout inside
4553
+ * `execute`, not a silent fallback.
4554
+ *
4555
+ * Idempotent across repeated invocations: subsequent calls return the
4556
+ * promise from the first call. The differential setup cache and the
4557
+ * relay/server probes also make this cheap on warm sandboxes — the
4558
+ * second `setup()` against the same sandbox does ~one round-trip of
4559
+ * work.
4560
+ */
4561
+ async setup(config = {}) {
4562
+ if (this.setupPromise) {
4563
+ await this.setupPromise;
4564
+ return;
4565
+ }
4566
+ debugAgent("setup() provider=%s", this.provider);
4567
+ const startedAt = Date.now();
4568
+ this.setupPromise = (async () => {
4569
+ await this.adapter.setup({
4570
+ provider: this.provider,
4571
+ options: this.options,
4572
+ config
4573
+ });
4574
+ debugAgent(
4575
+ "setup() returned provider=%s after %dms",
4576
+ this.provider,
4577
+ Date.now() - startedAt
4578
+ );
4579
+ })();
4580
+ try {
4581
+ await this.setupPromise;
4582
+ } catch (error) {
4583
+ this.setupPromise = void 0;
4584
+ throw error;
4585
+ }
4586
+ }
4587
+ stream(runConfig) {
4588
+ const runId = runConfig.runId ?? randomUUID2();
4589
+ const streamCalledAt = Date.now();
4590
+ debugAgent("stream() provider=%s runId=%s", this.provider, runId);
4591
+ const run = new AgentRunController(this.provider, runId);
4592
+ const setupPromise = this.setupPromise;
4593
+ const provider = this.provider;
4594
+ const options = this.options;
4595
+ const adapter = this.adapter;
4596
+ void (async () => {
4597
+ try {
4598
+ if (setupPromise) {
4599
+ await setupPromise;
4600
+ }
4601
+ const request = {
4602
+ runId,
4603
+ provider,
4604
+ options,
4605
+ run: buildRunConfig(options, runConfig)
4606
+ };
4607
+ const cleanup = await adapter.execute(request, run);
4608
+ debugAgent(
4609
+ "adapter.execute() returned for runId=%s after %dms",
4610
+ runId,
4611
+ Date.now() - streamCalledAt
4612
+ );
4613
+ run.setAbort(async () => {
4614
+ await cleanup();
4615
+ });
4616
+ } catch (error) {
4617
+ run.fail(error);
4618
+ }
4619
+ })();
4620
+ return run;
4621
+ }
4622
+ async run(runConfig) {
4623
+ return this.stream(runConfig).finished;
4624
+ }
4625
+ rawEvents(runConfig) {
4626
+ return this.stream(runConfig).rawEvents();
4627
+ }
4628
+ /**
4629
+ * Stateless control plane for an in-flight run.
4630
+ *
4631
+ * Returns an {@link AttachedRun} whose `abort()` / `sendMessage()` methods
4632
+ * dial the in-sandbox provider server directly (codex app-server, opencode
4633
+ * HTTP server, claude-code relay control endpoint) — there is no shared
4634
+ * in-memory registry or Redis broker. Any process with the right `sandbox`
4635
+ * + `runId` (+ optional provider-native `sessionId`) can issue commands
4636
+ * against a run started on a different process.
4637
+ *
4638
+ * The originating process keeps owning the event stream that
4639
+ * `agent.stream()` returned; commands attached here cause the in-sandbox
4640
+ * server to emit the natural follow-up events (`turn/aborted`, message
4641
+ * events, etc.), which the originating process ingests through its
4642
+ * existing transport.
4643
+ *
4644
+ * The handle is short-lived: each method call opens a fresh connection,
4645
+ * performs the operation with a timeout, and tears the connection down.
4646
+ */
4647
+ static async attach(request) {
4648
+ const adapter = createAdapter(request.provider);
4649
+ return {
4650
+ abort: () => adapter.attachAbort(request),
4651
+ sendMessage: (content) => adapter.attachSendMessage(request, content)
4652
+ };
4653
+ }
4654
+ };
4655
+
4656
+ export {
4657
+ Agent
4658
+ };