agentbox-sdk 0.1.1 → 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.
@@ -2,31 +2,37 @@ import {
2
2
  createNormalizedEvent,
3
3
  normalizeRawAgentEvent,
4
4
  toAISDKStream
5
- } from "./chunk-7FLLQJ6J.js";
5
+ } from "./chunk-ZOWBRUQR.js";
6
6
  import {
7
- AGENT_RESERVED_PORTS,
8
7
  AgentBoxError,
9
8
  AsyncQueue,
10
9
  UnsupportedProviderError,
11
10
  asError,
12
- getAvailablePort,
11
+ debugAgent,
12
+ debugClaude,
13
+ debugCodex,
14
+ debugOpencode,
15
+ debugRelay,
16
+ debugRuntime,
17
+ debugSetup,
13
18
  linesFromTextChunks,
14
19
  sleep,
20
+ time,
15
21
  waitFor
16
- } from "./chunk-O7HCJXKW.js";
22
+ } from "./chunk-INMA52FV.js";
17
23
  import {
18
24
  shellQuote
19
25
  } from "./chunk-NSJM57Z4.js";
20
26
  import {
21
27
  AgentProvider,
22
28
  SandboxProvider
23
- } from "./chunk-2NKMDGYH.js";
29
+ } from "./chunk-GOFJNFAD.js";
24
30
 
25
31
  // src/agents/Agent.ts
26
- import { randomUUID as randomUUID3 } from "crypto";
32
+ import { randomUUID as randomUUID2 } from "crypto";
27
33
 
28
34
  // src/agents/providers/claude-code.ts
29
- import { randomUUID as randomUUID2 } from "crypto";
35
+ import { randomUUID } from "crypto";
30
36
  import path8 from "path";
31
37
 
32
38
  // src/agents/approval.ts
@@ -682,7 +688,6 @@ function assertHooksSupported(provider, options) {
682
688
  }
683
689
 
684
690
  // src/agents/config/mcp.ts
685
- import path4 from "path";
686
691
  var SAFE_TOML_KEY = /^[a-zA-Z0-9_-]+$/;
687
692
  function assertSafeTomlKey(name, context) {
688
693
  if (!SAFE_TOML_KEY.test(name)) {
@@ -765,8 +770,20 @@ function buildOpenCodeMcpConfig(mcps) {
765
770
  })
766
771
  );
767
772
  }
768
- function buildCodexConfigToml(mcps, agentSections = [], enableHooks = false) {
773
+ function buildCodexConfigToml(opts = {}) {
774
+ const {
775
+ mcps,
776
+ agentSections = [],
777
+ enableHooks = false,
778
+ enableSkills = false,
779
+ enableMultiAgent = false,
780
+ openAiBaseUrl
781
+ } = opts;
769
782
  const blocks = [];
783
+ if (openAiBaseUrl) {
784
+ blocks.push(`openai_base_url = ${tomlString(openAiBaseUrl)}`);
785
+ blocks.push("");
786
+ }
770
787
  for (const mcp of mcps ?? []) {
771
788
  if (mcp.enabled === false) {
772
789
  continue;
@@ -798,9 +815,13 @@ function buildCodexConfigToml(mcps, agentSections = [], enableHooks = false) {
798
815
  }
799
816
  blocks.push("");
800
817
  }
801
- if (enableHooks) {
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) {
802
823
  blocks.push("[features]");
803
- blocks.push("codex_hooks = true");
824
+ blocks.push(...featureLines);
804
825
  blocks.push("");
805
826
  }
806
827
  blocks.push(...agentSections);
@@ -810,21 +831,11 @@ function buildCodexConfigToml(mcps, agentSections = [], enableHooks = false) {
810
831
  return `${blocks.join("\n").trim()}
811
832
  `;
812
833
  }
813
- function buildClaudeMcpArtifact(mcps, claudeDir) {
814
- const content = buildClaudeMcpConfig(mcps);
815
- if (!content) {
816
- return void 0;
817
- }
818
- return {
819
- path: path4.join(claudeDir, "agentbox-mcp.json"),
820
- content
821
- };
822
- }
823
834
 
824
- // src/agents/config/runtime.ts
825
- import { mkdtemp, mkdir, chmod, rm, writeFile } from "fs/promises";
835
+ // src/agents/config/setup.ts
836
+ import { mkdir, chmod, rm, writeFile } from "fs/promises";
826
837
  import os from "os";
827
- import path5 from "path";
838
+ import path4 from "path";
828
839
 
829
840
  // src/agents/transports/spawn.ts
830
841
  import { spawn } from "child_process";
@@ -877,160 +888,333 @@ async function* linesFromNodeStream(stream) {
877
888
  }
878
889
  }
879
890
 
880
- // src/agents/config/runtime.ts
881
- function createLayout(homeDir) {
882
- const xdgConfigHome = path5.join(homeDir, ".config");
883
- const codexDir = path5.join(homeDir, ".codex");
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");
884
901
  return {
885
- rootDir: homeDir,
886
- homeDir,
902
+ rootDir,
903
+ homeDir: rootDir,
887
904
  xdgConfigHome,
888
- agentsDir: path5.join(homeDir, ".agents"),
889
- claudeDir: path5.join(homeDir, ".claude"),
890
- opencodeDir: path5.join(xdgConfigHome, "opencode"),
891
- codexDir
905
+ agentsDir: path4.join(rootDir, ".agents"),
906
+ claudeDir: path4.join(rootDir, ".claude"),
907
+ opencodeDir: path4.join(xdgConfigHome, "opencode"),
908
+ codexDir: path4.join(rootDir, ".codex")
892
909
  };
893
910
  }
894
- var HostRuntimeTarget = class {
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 {
895
925
  constructor(provider, layout, cwd, baseEnv) {
896
926
  this.provider = provider;
897
927
  this.layout = layout;
898
928
  this.cwd = cwd;
899
929
  this.baseEnv = baseEnv;
900
- this.env = {};
930
+ this.env = buildLayoutEnv(provider, layout);
901
931
  }
902
932
  provider;
903
933
  layout;
904
934
  cwd;
905
935
  baseEnv;
906
936
  env;
907
- async writeArtifact(artifact) {
908
- await mkdir(path5.dirname(artifact.path), { recursive: true });
909
- await writeFile(artifact.path, artifact.content, "utf8");
910
- if (artifact.executable) {
911
- await chmod(artifact.path, 493);
912
- }
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
+ );
913
977
  }
914
978
  async runCommand(command, extraEnv) {
915
- const handle = spawnCommand({
916
- command: process.env.SHELL || "sh",
917
- args: ["-lc", command],
918
- cwd: this.cwd,
919
- env: {
920
- ...process.env,
921
- ...this.baseEnv,
922
- ...this.env,
923
- ...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
+ }
924
998
  }
925
- });
926
- const exitCode = await handle.wait();
927
- if (exitCode !== 0) {
928
- throw new Error(`Setup command failed (${exitCode}): ${command}`);
929
- }
999
+ );
930
1000
  }
931
1001
  async cleanup() {
932
1002
  await rm(this.layout.rootDir, { recursive: true, force: true });
933
1003
  }
934
1004
  };
935
- var SandboxRuntimeTarget = class {
1005
+ var SandboxSetupTarget = class {
936
1006
  constructor(provider, layout, options) {
937
1007
  this.provider = provider;
938
1008
  this.layout = layout;
939
1009
  this.options = options;
940
- this.env = {};
1010
+ this.env = buildLayoutEnv(provider, layout);
941
1011
  }
942
1012
  provider;
943
1013
  layout;
944
1014
  options;
945
1015
  env;
946
- async writeArtifact(artifact) {
947
- const marker = `__AGENTBOX_${Math.random().toString(36).slice(2)}__`;
948
- const command = `mkdir -p ${shellQuote(
949
- path5.posix.dirname(artifact.path)
950
- )} && cat > ${shellQuote(artifact.path)} <<'${marker}'
951
- ${artifact.content}
952
- ${marker}`;
953
- await this.options.sandbox?.run(command, {
954
- cwd: this.options.cwd,
955
- env: {
956
- ...this.options.env ?? {},
957
- ...this.env
958
- }
959
- });
960
- if (artifact.executable) {
961
- await this.options.sandbox?.run(`chmod +x ${shellQuote(artifact.path)}`, {
962
- cwd: this.options.cwd,
963
- env: {
964
- ...this.options.env ?? {},
965
- ...this.env
966
- }
967
- });
968
- }
969
- }
970
- async runCommand(command, extraEnv) {
971
- const result = await this.options.sandbox?.run(command, {
972
- cwd: this.options.cwd,
973
- env: {
974
- ...this.options.env ?? {},
975
- ...this.env,
976
- ...extraEnv ?? {}
977
- }
978
- });
979
- if (result && result.exitCode !== 0) {
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) {
980
1024
  throw new Error(
981
- `Sandbox setup command failed (${result.exitCode}): ${command}`
1025
+ "SandboxSetupTarget.uploadAndRun called without a sandbox."
982
1026
  );
983
1027
  }
984
- }
985
- async cleanup() {
986
- await this.options.sandbox?.run(
987
- `rm -rf ${shellQuote(this.layout.rootDir)}`,
988
- {
1028
+ return time(
1029
+ debugRuntime,
1030
+ `sandbox uploadAndRun ${shortLabel(command)}`,
1031
+ () => sandbox.uploadAndRun(files, command, {
989
1032
  cwd: this.options.cwd,
990
1033
  env: {
991
1034
  ...this.options.env ?? {},
992
1035
  ...this.env
993
1036
  }
994
- }
1037
+ }),
1038
+ (result) => ({ exit: result.exitCode, files: files.length })
995
1039
  );
996
1040
  }
997
- };
998
- async function createRuntimeTarget(provider, runId, options) {
999
- if (options.sandbox) {
1000
- const layout2 = createLayout(`/tmp/agentbox/${provider}/${runId}`);
1001
- await options.sandbox.run(
1002
- [
1003
- `mkdir -p ${shellQuote(layout2.homeDir)}`,
1004
- `mkdir -p ${shellQuote(layout2.xdgConfigHome)}`,
1005
- `mkdir -p ${shellQuote(layout2.agentsDir)}`,
1006
- `mkdir -p ${shellQuote(layout2.claudeDir)}`,
1007
- `mkdir -p ${shellQuote(layout2.opencodeDir)}`,
1008
- `mkdir -p ${shellQuote(layout2.codexDir)}`
1009
- ].join(" && "),
1010
- {
1011
- cwd: options.cwd,
1012
- env: {
1013
- ...options.env ?? {}
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
+ );
1014
1058
  }
1015
1059
  }
1016
1060
  );
1017
- return new SandboxRuntimeTarget(provider, layout2, options);
1018
1061
  }
1019
- const rootDir = await mkdtemp(
1020
- path5.join(os.tmpdir(), `agentbox-${provider}-`)
1021
- );
1022
- const layout = createLayout(rootDir);
1023
- await mkdir(layout.homeDir, { recursive: true });
1024
- await mkdir(layout.xdgConfigHome, { recursive: true });
1025
- await mkdir(layout.agentsDir, { recursive: true });
1026
- await mkdir(layout.claudeDir, { recursive: true });
1027
- await mkdir(layout.opencodeDir, { recursive: true });
1028
- await mkdir(layout.codexDir, { recursive: true });
1029
- return new HostRuntimeTarget(
1030
- provider,
1031
- layout,
1032
- options.cwd ?? process.cwd(),
1033
- options.env ?? {}
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
+ })
1034
1218
  );
1035
1219
  }
1036
1220
 
@@ -1043,12 +1227,15 @@ function getSkillTargetDir(provider, layout, skillName) {
1043
1227
  case AgentProvider.OpenCode:
1044
1228
  return path6.join(layout.opencodeDir, "skills", skillName);
1045
1229
  case AgentProvider.Codex:
1046
- return path6.join(layout.agentsDir, "skills", skillName);
1230
+ return path6.join(layout.codexDir, "skills", skillName);
1047
1231
  }
1048
1232
  }
1233
+ function skillsCliAgentName(provider) {
1234
+ return provider === AgentProvider.OpenCode ? "opencode" : provider;
1235
+ }
1049
1236
  function buildSkillsInstallerCommand(provider, skill) {
1050
1237
  const repo = skill.repo ?? "https://github.com/anthropics/skills";
1051
- return `npx skills add ${shellQuote(repo)} -g --skill ${shellQuote(skill.name)} --agent ${shellQuote(provider)} -y`;
1238
+ return `npx skills add ${shellQuote(repo)} -g --skill ${shellQuote(skill.name)} --agent ${shellQuote(skillsCliAgentName(provider))} -y`;
1052
1239
  }
1053
1240
  async function prepareSkillArtifacts(provider, skills, layout) {
1054
1241
  const artifacts = [];
@@ -1077,11 +1264,6 @@ async function prepareSkillArtifacts(provider, skills, layout) {
1077
1264
  preparedSkills
1078
1265
  };
1079
1266
  }
1080
- async function installSkills(target, installCommands, extraEnv) {
1081
- for (const command of installCommands) {
1082
- await target.runCommand(command, extraEnv);
1083
- }
1084
- }
1085
1267
 
1086
1268
  // src/agents/config/subagents.ts
1087
1269
  import path7 from "path";
@@ -1092,21 +1274,32 @@ function toToolsArray(tools) {
1092
1274
  function tomlString2(value) {
1093
1275
  return JSON.stringify(value);
1094
1276
  }
1095
- function buildClaudeAgentsConfig(subAgents) {
1277
+ function yamlScalar(value) {
1278
+ return `'${value.replace(/'/g, "''")}'`;
1279
+ }
1280
+ function buildClaudeSubagentArtifacts(subAgents, layout) {
1096
1281
  if (!subAgents || subAgents.length === 0) {
1097
- return void 0;
1282
+ return [];
1098
1283
  }
1099
- return Object.fromEntries(
1100
- subAgents.map((subAgent) => [
1101
- subAgent.name,
1102
- {
1103
- description: subAgent.description,
1104
- prompt: subAgent.instructions,
1105
- ...subAgent.model ? { model: subAgent.model } : {},
1106
- ...toToolsArray(subAgent.tools) ? { tools: toToolsArray(subAgent.tools) } : {}
1107
- }
1108
- ])
1109
- );
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
+ });
1110
1303
  }
1111
1304
  function mapOpenCodeTools(tools) {
1112
1305
  if (!tools || tools.length === 0) {
@@ -1131,27 +1324,28 @@ function buildCodexSubagentArtifacts(subAgents, layout) {
1131
1324
  const artifacts = [];
1132
1325
  const agentSections = [];
1133
1326
  for (const subAgent of subAgents ?? []) {
1134
- if (subAgent.model) {
1135
- throw new Error(
1136
- `Codex sub-agent "${subAgent.name}" specifies a model override, which is not supported in this package yet.`
1137
- );
1138
- }
1139
1327
  const roleConfigRelativePath = `./agents/${subAgent.name}.toml`;
1140
1328
  const roleConfigPromptPath = `../prompts/${subAgent.name}.md`;
1141
1329
  artifacts.push({
1142
1330
  path: path7.join(layout.codexDir, "prompts", `${subAgent.name}.md`),
1143
1331
  content: subAgent.instructions
1144
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
+ );
1145
1346
  artifacts.push({
1146
1347
  path: path7.join(layout.codexDir, "agents", `${subAgent.name}.toml`),
1147
- content: [
1148
- `model_instructions_file = ${tomlString2(roleConfigPromptPath)}`,
1149
- `model_reasoning_effort = ${tomlString2("medium")}`,
1150
- "",
1151
- "[features]",
1152
- "multi_agent = false",
1153
- ""
1154
- ].join("\n")
1348
+ content: tomlLines.join("\n")
1155
1349
  });
1156
1350
  agentSections.push(
1157
1351
  `[agents.${subAgent.name}]`,
@@ -1167,355 +1361,166 @@ function buildCodexSubagentArtifacts(subAgents, layout) {
1167
1361
  };
1168
1362
  }
1169
1363
 
1170
- // src/agents/transports/sdk-ws.ts
1171
- import { randomUUID } from "crypto";
1172
- import {
1173
- WebSocket,
1174
- WebSocketServer
1175
- } from "ws";
1176
- function handleIncomingMessage(text, messagesQueue, pendingResponses) {
1177
- const lines = text.split("\n").map((line) => line.trim()).filter(Boolean);
1178
- for (const line of lines) {
1179
- let message;
1180
- try {
1181
- message = JSON.parse(line);
1182
- } catch {
1183
- continue;
1184
- }
1185
- if (message.type === "control_response" && typeof message.response === "object" && message.response !== null) {
1186
- const requestId = String(
1187
- message.response.request_id ?? ""
1188
- );
1189
- const pending = pendingResponses.get(requestId);
1190
- if (pending) {
1191
- pendingResponses.delete(requestId);
1192
- pending.resolve(message);
1193
- }
1194
- }
1195
- messagesQueue.push(message);
1196
- }
1197
- }
1198
- var SdkWsServer = class {
1199
- server;
1200
- socket;
1201
- messagesQueue = new AsyncQueue();
1202
- pendingResponses = /* @__PURE__ */ new Map();
1203
- host;
1204
- port;
1205
- constructor(options) {
1206
- this.host = options?.host ?? "127.0.0.1";
1207
- this.port = options?.port;
1208
- }
1209
- get url() {
1210
- if (!this.port) {
1211
- throw new Error("SDK WebSocket server has not been started yet.");
1212
- }
1213
- return `ws://${this.host}:${this.port}`;
1214
- }
1215
- async start() {
1216
- if (this.server) {
1217
- return;
1218
- }
1219
- this.port ??= await getAvailablePort(this.host);
1220
- this.server = new WebSocketServer({ host: this.host, port: this.port });
1221
- this.server.on("connection", (socket) => {
1222
- this.socket = socket;
1223
- socket.on("message", (data) => this.handleMessage(data));
1224
- socket.on("close", () => {
1225
- if (this.socket === socket) {
1226
- this.socket = void 0;
1227
- }
1228
- });
1229
- });
1230
- this.server.on("error", (error) => {
1231
- this.messagesQueue.fail(error);
1232
- });
1233
- }
1234
- async waitForConnection(timeoutMs = 15e3) {
1235
- await waitFor(async () => Boolean(this.socket), {
1236
- timeoutMs,
1237
- intervalMs: 100
1238
- });
1239
- }
1240
- async send(message) {
1241
- await this.waitForConnection();
1242
- const socket = this.socket;
1243
- if (!socket) {
1244
- throw new Error("SDK WebSocket server has no active connection.");
1245
- }
1246
- await new Promise((resolve, reject) => {
1247
- socket.send(`${JSON.stringify(message)}
1248
- `, (error) => {
1249
- if (error) {
1250
- reject(error);
1251
- return;
1252
- }
1253
- resolve();
1254
- });
1255
- });
1364
+ // src/agents/cost.ts
1365
+ function addIfNumber(target, key, value) {
1366
+ if (typeof value !== "number" || !Number.isFinite(value)) {
1367
+ return;
1256
1368
  }
1257
- async request(request) {
1258
- const requestId = randomUUID();
1259
- const response = new Promise((resolve, reject) => {
1260
- this.pendingResponses.set(requestId, { resolve, reject });
1261
- });
1262
- await this.send({
1263
- type: "control_request",
1264
- request_id: requestId,
1265
- request
1266
- });
1267
- return response;
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;
1268
1377
  }
1269
- async close() {
1270
- this.messagesQueue.finish();
1271
- for (const pending of this.pendingResponses.values()) {
1272
- pending.reject(new Error("SDK WebSocket server closed."));
1273
- }
1274
- this.pendingResponses.clear();
1275
- this.socket?.close();
1276
- if (!this.server) {
1277
- return;
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;
1278
1421
  }
1279
- await new Promise((resolve, reject) => {
1280
- this.server?.close((error) => {
1281
- if (error) {
1282
- reject(error);
1283
- return;
1284
- }
1285
- resolve();
1286
- });
1287
- });
1288
- this.server = void 0;
1289
- this.socket = void 0;
1290
- }
1291
- messages() {
1292
- return this.messagesQueue;
1293
- }
1294
- handleMessage(rawData) {
1295
- handleIncomingMessage(
1296
- rawData.toString(),
1297
- this.messagesQueue,
1298
- this.pendingResponses
1299
- );
1300
- }
1301
- };
1302
- var SharedSdkWsChannel = class {
1303
- constructor(connection, runId, onClose) {
1304
- this.connection = connection;
1305
- this.runId = runId;
1306
- this.onClose = onClose;
1307
- }
1308
- connection;
1309
- runId;
1310
- onClose;
1311
- messagesQueue = new AsyncQueue();
1312
- pendingResponses = /* @__PURE__ */ new Map();
1313
- closed = false;
1314
- handleIncomingMessage(message) {
1315
- if (this.closed) {
1316
- return;
1422
+ if (event.type !== "result") {
1423
+ continue;
1317
1424
  }
1318
- if (message.type === "control_response" && typeof message.response === "object" && message.response !== null) {
1319
- const requestId = String(
1320
- message.response.request_id ?? ""
1321
- );
1322
- const pending = this.pendingResponses.get(requestId);
1323
- if (pending) {
1324
- this.pendingResponses.delete(requestId);
1325
- pending.resolve(message);
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));
1326
1431
  }
1327
1432
  }
1328
- this.messagesQueue.push(message);
1329
- }
1330
- handleConnectionClosed(error) {
1331
- if (this.closed) {
1332
- return;
1333
- }
1334
- for (const pending of this.pendingResponses.values()) {
1335
- pending.reject(error);
1336
- }
1337
- this.pendingResponses.clear();
1338
- this.messagesQueue.fail(error);
1339
- }
1340
- async waitForConnection(timeoutMs = 15e3) {
1341
- await this.connection.waitForConnection(timeoutMs);
1342
- }
1343
- async send(message) {
1344
- await this.connection.sendToRun(this.runId, message);
1345
- }
1346
- async request(request) {
1347
- const requestId = randomUUID();
1348
- const response = new Promise((resolve, reject) => {
1349
- this.pendingResponses.set(requestId, { resolve, reject });
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
1350
1439
  });
1351
- await this.send({
1352
- type: "control_request",
1353
- request_id: requestId,
1354
- request
1355
- });
1356
- return response;
1357
1440
  }
1358
- async close() {
1359
- if (this.closed) {
1360
- return;
1361
- }
1362
- this.closed = true;
1363
- for (const pending of this.pendingResponses.values()) {
1364
- pending.reject(new Error("SDK WebSocket channel closed."));
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);
1365
1452
  }
1366
- this.pendingResponses.clear();
1367
- this.messagesQueue.finish();
1368
- this.onClose();
1369
1453
  }
1370
- messages() {
1371
- return this.messagesQueue;
1372
- }
1373
- };
1374
- var MAX_PENDING_MESSAGES_PER_RUN = 1e3;
1375
- var SharedSdkWsConnection = class {
1376
- constructor(url, headers = {}) {
1377
- this.url = url;
1378
- this.headers = headers;
1379
- }
1380
- url;
1381
- headers;
1382
- socket;
1383
- channels = /* @__PURE__ */ new Map();
1384
- pendingMessages = /* @__PURE__ */ new Map();
1385
- async start() {
1386
- if (this.socket?.readyState === WebSocket.OPEN) {
1387
- return;
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;
1388
1465
  }
1389
- const socket = new WebSocket(this.url, { headers: this.headers });
1390
- this.socket = socket;
1391
- socket.on("message", (data) => {
1392
- const lines = data.toString().split("\n").map((line) => line.trim()).filter(Boolean);
1393
- for (const line of lines) {
1394
- try {
1395
- const envelope = JSON.parse(line);
1396
- if (!envelope.runId || !envelope.message) {
1397
- continue;
1398
- }
1399
- const channel = this.channels.get(envelope.runId);
1400
- if (channel) {
1401
- channel.handleIncomingMessage(envelope.message);
1402
- continue;
1403
- }
1404
- const pending = this.pendingMessages.get(envelope.runId) ?? [];
1405
- if (pending.length < MAX_PENDING_MESSAGES_PER_RUN) {
1406
- pending.push(envelope.message);
1407
- this.pendingMessages.set(envelope.runId, pending);
1408
- }
1409
- } catch (error) {
1410
- const failure = error instanceof Error ? error : new Error("Failed to parse shared SDK message.");
1411
- for (const channel of this.channels.values()) {
1412
- channel.handleConnectionClosed(failure);
1413
- }
1414
- }
1415
- }
1416
- });
1417
- socket.on("close", () => {
1418
- if (this.socket === socket) {
1419
- this.socket = void 0;
1420
- }
1421
- const error = new Error("Shared SDK WebSocket connection closed.");
1422
- for (const channel of this.channels.values()) {
1423
- channel.handleConnectionClosed(error);
1424
- }
1425
- });
1426
- socket.on("error", (error) => {
1427
- for (const channel of this.channels.values()) {
1428
- channel.handleConnectionClosed(error);
1429
- }
1430
- });
1431
- await new Promise((resolve, reject) => {
1432
- const cleanup = () => {
1433
- socket.off("open", handleOpen);
1434
- socket.off("error", handleError);
1435
- };
1436
- const handleOpen = () => {
1437
- cleanup();
1438
- resolve();
1439
- };
1440
- const handleError = (error) => {
1441
- cleanup();
1442
- reject(error);
1443
- };
1444
- socket.once("open", handleOpen);
1445
- socket.once("error", handleError);
1446
- });
1447
- }
1448
- async waitForConnection(timeoutMs = 15e3) {
1449
- await waitFor(async () => this.socket?.readyState === WebSocket.OPEN, {
1450
- timeoutMs,
1451
- intervalMs: 100
1452
- });
1453
- }
1454
- createChannel(runId) {
1455
- const existing = this.channels.get(runId);
1456
- if (existing) {
1457
- return existing;
1466
+ if (typeof part.cost === "number") {
1467
+ totalCost += part.cost;
1468
+ sawCostData = true;
1458
1469
  }
1459
- const channel = new SharedSdkWsChannel(this, runId, () => {
1460
- this.channels.delete(runId);
1461
- });
1462
- this.channels.set(runId, channel);
1463
- const pending = this.pendingMessages.get(runId);
1464
- if (pending?.length) {
1465
- for (const message of pending) {
1466
- channel.handleIncomingMessage(message);
1467
- }
1468
- this.pendingMessages.delete(runId);
1470
+ const tokens = asRecord(part.tokens);
1471
+ if (tokens) {
1472
+ sawCostData = true;
1473
+ mergeUsage(usage, tokens);
1469
1474
  }
1470
- return channel;
1471
1475
  }
1472
- async sendToRun(runId, message) {
1473
- await this.waitForConnection();
1474
- const socket = this.socket;
1475
- if (!socket) {
1476
- throw new Error("Shared SDK WebSocket connection is not open.");
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;
1477
1504
  }
1478
- await new Promise((resolve, reject) => {
1479
- socket.send(`${JSON.stringify({ runId, message })}
1480
- `, (error) => {
1481
- if (error) {
1482
- reject(error);
1483
- return;
1484
- }
1485
- resolve();
1486
- });
1487
- });
1488
1505
  }
1489
- async close() {
1490
- for (const channel of this.channels.values()) {
1491
- await channel.close();
1492
- }
1493
- this.channels.clear();
1494
- this.pendingMessages.clear();
1495
- if (!this.socket) {
1496
- return;
1497
- }
1498
- await new Promise((resolve) => {
1499
- const socket = this.socket;
1500
- this.socket = void 0;
1501
- if (!socket) {
1502
- resolve();
1503
- return;
1504
- }
1505
- if (socket.readyState === WebSocket.CLOSED || socket.readyState === WebSocket.CLOSING) {
1506
- resolve();
1507
- return;
1508
- }
1509
- socket.once("close", () => resolve());
1510
- socket.close();
1511
- });
1506
+ if (run.systemPrompt) {
1507
+ extraArgs["append-system-prompt"] = run.systemPrompt;
1512
1508
  }
1513
- };
1514
-
1515
- // src/agents/providers/claude-code.ts
1516
- var REMOTE_SDK_RELAY_PORT = 43180;
1517
- var REMOTE_SDK_RELAY_PATH = "/tmp/agentbox/claude-code/relay.mjs";
1518
- var sharedRemoteConnectionBySandbox = /* @__PURE__ */ new WeakMap();
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
+ }
1519
1524
  function toRawEvent(runId, payload, type) {
1520
1525
  return {
1521
1526
  provider: AgentProvider.ClaudeCode,
@@ -1526,917 +1531,740 @@ function toRawEvent(runId, payload, type) {
1526
1531
  };
1527
1532
  }
1528
1533
  function extractAssistantText(message) {
1529
- const content = message.message;
1530
- if (Array.isArray(content)) {
1531
- return content.filter((block) => block.type === "text").map((block) => String(block.text ?? "")).join("");
1532
- }
1533
- if (content && Array.isArray(content.content)) {
1534
- return content.content.filter((block) => block.type === "text").map((block) => String(block.text ?? "")).join("");
1535
- }
1536
- return "";
1537
- }
1538
- function extractStreamDelta(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) {
1539
1546
  const event = message.event;
1540
- if (!event) {
1541
- return "";
1547
+ if (!event || event.type !== "content_block_delta") {
1548
+ return { text: "", thinking: "" };
1542
1549
  }
1543
1550
  const delta = event.delta;
1544
- if (typeof delta?.text === "string") {
1545
- return delta.text;
1551
+ if (!delta) return { text: "", thinking: "" };
1552
+ if (delta.type === "text_delta" && typeof delta.text === "string") {
1553
+ return { text: delta.text, thinking: "" };
1546
1554
  }
1547
- if (typeof event.text === "string") {
1548
- return event.text;
1555
+ if (delta.type === "thinking_delta" && typeof delta.thinking === "string") {
1556
+ return { text: "", thinking: delta.thinking };
1549
1557
  }
1550
- return "";
1558
+ return { text: "", thinking: "" };
1551
1559
  }
1552
- function createClaudePermissionEvent(request, message) {
1553
- const requestPayload = message.request ?? {};
1554
- return createNormalizedEvent(
1555
- "permission.requested",
1556
- {
1557
- provider: request.provider,
1558
- runId: request.runId,
1559
- raw: toRawEvent(request.runId, message, message.type)
1560
- },
1561
- {
1562
- requestId: String(message.request_id ?? ""),
1563
- kind: "tool",
1564
- title: `Approve ${String(requestPayload.tool_name ?? "tool")} tool`,
1565
- message: `Claude wants to use ${String(requestPayload.tool_name ?? "tool")}.`,
1566
- input: requestPayload.input,
1567
- canRemember: false
1568
- }
1569
- );
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;
1570
1589
  }
1571
- async function prepareClaudeRuntime(request) {
1572
- const options = request.options;
1573
- const provider = options.provider;
1574
- const target = await createRuntimeTarget(
1575
- request.provider,
1576
- request.runId,
1577
- options
1578
- );
1579
- const hooks = assertHooksSupported(request.provider, options);
1580
- assertCommandsSupported(request.provider, options.commands);
1581
- const { artifacts: skillArtifacts, installCommands } = await prepareSkillArtifacts(
1582
- request.provider,
1583
- options.skills,
1584
- target.layout
1585
- );
1586
- const artifacts = [
1587
- ...skillArtifacts,
1588
- ...buildClaudeCommandArtifacts(options.commands, target.layout)
1589
- ];
1590
- const mcpArtifact = buildClaudeMcpArtifact(
1591
- options.mcps,
1592
- target.layout.claudeDir
1593
- );
1594
- if (mcpArtifact) {
1595
- artifacts.push(mcpArtifact);
1596
- }
1597
- const hookSettings = buildClaudeHookSettings(hooks);
1598
- let settingsPath;
1599
- if (hookSettings) {
1600
- settingsPath = path8.join(target.layout.claudeDir, "settings.json");
1601
- artifacts.push({
1602
- path: settingsPath,
1603
- content: JSON.stringify(hookSettings, null, 2)
1604
- });
1605
- }
1606
- for (const artifact of artifacts) {
1607
- await target.writeArtifact(artifact);
1608
- }
1609
- await installSkills(target, installCommands);
1610
- const agents = buildClaudeAgentsConfig(options.subAgents);
1611
- const initializeRequest = Object.keys({
1612
- ...request.run.systemPrompt ? { systemPrompt: request.run.systemPrompt } : {},
1613
- ...agents ? { agents } : {}
1614
- }).length ? {
1615
- subtype: "initialize",
1616
- ...request.run.systemPrompt ? { systemPrompt: request.run.systemPrompt } : {},
1617
- ...agents ? { agents } : {}
1618
- } : void 0;
1619
- const buildArgs = (sdkUrl) => [
1620
- "--sdk-url",
1621
- sdkUrl,
1622
- "--print",
1623
- "--output-format",
1624
- "stream-json",
1625
- "--input-format",
1626
- "stream-json",
1627
- ...provider?.verbose ? ["--verbose"] : [],
1628
- ...request.run.model ? ["--model", request.run.model] : [],
1629
- ...provider?.permissionMode ? ["--permission-mode", provider.permissionMode] : [],
1630
- ...provider?.allowedTools?.length ? ["--allowedTools", provider.allowedTools.join(",")] : [],
1631
- ...request.run.resumeSessionId ? ["-r", request.run.resumeSessionId] : [],
1632
- ...settingsPath ? ["--settings", settingsPath] : [],
1633
- ...mcpArtifact ? ["--mcp-config", mcpArtifact.path] : [],
1634
- ...provider?.args ?? [],
1635
- "-p",
1636
- ""
1637
- ];
1638
- const env = {
1639
- ...options.env ?? {},
1640
- ...target.env
1641
- };
1590
+
1591
+ function createPromptStream() {
1592
+ const queue = [];
1593
+ let resolver = null;
1594
+ let ended = false;
1642
1595
  return {
1643
- target,
1644
- buildArgs,
1645
- env,
1646
- initializeRequest
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
+ },
1647
1611
  };
1648
1612
  }
1649
- function createRemoteSdkRelayScript() {
1650
- return `
1651
- import crypto from "node:crypto";
1652
- import http from "node:http";
1653
-
1654
- const port = Number(process.argv[2] ?? "43180");
1655
- const magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
1656
- const channels = new Map();
1657
- let hostSocket = null;
1658
1613
 
1659
- function getChannel(runId) {
1660
- let channel = channels.get(runId);
1661
- if (!channel) {
1662
- channel = {
1663
- claude: null,
1664
- pending: {
1665
- toHost: [],
1666
- claude: [],
1667
- },
1668
- };
1669
- channels.set(runId, channel);
1670
- }
1671
- return channel;
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
+ });
1672
1634
  }
1673
1635
 
1674
- function sendFrame(socket, payload, opcode = 0x1) {
1675
- const length = payload.length;
1676
- let header;
1677
- if (length < 126) {
1678
- header = Buffer.alloc(2);
1679
- header[1] = length;
1680
- } else if (length < 65536) {
1681
- header = Buffer.alloc(4);
1682
- header[1] = 126;
1683
- header.writeUInt16BE(length, 2);
1684
- } else {
1685
- header = Buffer.alloc(10);
1686
- header[1] = 127;
1687
- header.writeBigUInt64BE(BigInt(length), 2);
1688
- }
1689
- header[0] = 0x80 | opcode;
1690
- socket.write(Buffer.concat([header, payload]));
1636
+ function autoApproveCanUseTool(_toolName, input) {
1637
+ return { behavior: "allow", updatedInput: input };
1691
1638
  }
1692
1639
 
1693
- function parseFrame(buffer) {
1694
- if (buffer.length < 2) {
1695
- return null;
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;
1696
1653
  }
1697
1654
 
1698
- const first = buffer[0];
1699
- const second = buffer[1];
1700
- const fin = (first & 0x80) !== 0;
1701
- const opcode = first & 0x0f;
1702
- const masked = (second & 0x80) !== 0;
1703
- let length = second & 0x7f;
1704
- let offset = 2;
1655
+ const promptStream = createPromptStream();
1656
+ promptStream.push(prompt);
1705
1657
 
1706
- if (length === 126) {
1707
- if (buffer.length < offset + 2) {
1708
- return null;
1709
- }
1710
- length = buffer.readUInt16BE(offset);
1711
- offset += 2;
1712
- } else if (length === 127) {
1713
- if (buffer.length < offset + 8) {
1714
- return null;
1715
- }
1716
- length = Number(buffer.readBigUInt64BE(offset));
1717
- offset += 8;
1718
- }
1658
+ res.writeHead(200, {
1659
+ "content-type": "application/x-ndjson",
1660
+ "transfer-encoding": "chunked",
1661
+ "x-daemon-version": VERSION,
1662
+ });
1719
1663
 
1720
- let mask;
1721
- if (masked) {
1722
- if (buffer.length < offset + 4) {
1723
- return null;
1724
- }
1725
- mask = buffer.subarray(offset, offset + 4);
1726
- offset += 4;
1727
- }
1664
+ const opts = { ...(options || {}) };
1665
+ const autoApprove = !!opts.autoApproveTools;
1666
+ delete opts.autoApproveTools;
1667
+ opts.pathToClaudeCodeExecutable = resolveClaudeBinary(
1668
+ opts.pathToClaudeCodeExecutable,
1669
+ );
1728
1670
 
1729
- if (buffer.length < offset + length) {
1730
- return null;
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;
1731
1684
  }
1732
1685
 
1733
- let payload = buffer.subarray(offset, offset + length);
1734
- if (mask) {
1735
- payload = Buffer.from(payload);
1736
- for (let index = 0; index < payload.length; index += 1) {
1737
- payload[index] ^= mask[index % 4];
1738
- }
1739
- }
1686
+ liveRuns.set(runId, { query: queryHandle, prompt: promptStream });
1740
1687
 
1741
- return {
1742
- fin,
1743
- opcode,
1744
- payload,
1745
- bytesUsed: offset + length,
1746
- };
1747
- }
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
+ });
1748
1695
 
1749
- function sendHostEnvelope(runId, message) {
1750
- if (!hostSocket) {
1751
- return false;
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();
1752
1707
  }
1753
- sendFrame(
1754
- hostSocket,
1755
- Buffer.from(JSON.stringify({ runId, message }), "utf8"),
1756
- );
1757
- return true;
1758
1708
  }
1759
1709
 
1760
- function flushClaude(channel) {
1761
- const socket = channel.claude;
1762
- if (!socket) {
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" }));
1763
1715
  return;
1764
1716
  }
1765
- while (channel.pending.claude.length > 0) {
1766
- sendFrame(socket, Buffer.from(channel.pending.claude.shift(), "utf8"));
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;
1767
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();
1768
1731
  }
1769
1732
 
1770
- function flushHostBacklog() {
1771
- if (!hostSocket) {
1733
+ async function handleAbort(_req, res, runId) {
1734
+ const run = liveRuns.get(runId);
1735
+ if (!run) {
1736
+ res.writeHead(404);
1737
+ res.end();
1772
1738
  return;
1773
1739
  }
1774
- for (const [runId, channel] of channels.entries()) {
1775
- while (channel.pending.toHost.length > 0) {
1776
- sendHostEnvelope(runId, channel.pending.toHost.shift());
1777
- }
1778
- }
1740
+ await run.query.interrupt().catch(() => {});
1741
+ res.writeHead(204);
1742
+ res.end();
1779
1743
  }
1780
1744
 
1781
- function relayFromClaude(runId, message) {
1782
- const channel = getChannel(runId);
1783
- if (!sendHostEnvelope(runId, message)) {
1784
- channel.pending.toHost.push(message);
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(() => {});
1785
1751
  }
1752
+ res.writeHead(204);
1753
+ res.end();
1786
1754
  }
1787
1755
 
1788
- function registerClaudePeer(socket, runId) {
1789
- const channel = getChannel(runId);
1790
- channel.claude = socket;
1791
- flushClaude(channel);
1792
- let buffer = Buffer.alloc(0);
1793
- let fragments = [];
1794
-
1795
- socket.on("data", (chunk) => {
1796
- buffer = Buffer.concat([buffer, chunk]);
1797
-
1798
- while (true) {
1799
- const frame = parseFrame(buffer);
1800
- if (!frame) {
1801
- return;
1802
- }
1803
- buffer = buffer.subarray(frame.bytesUsed);
1804
-
1805
- if (frame.opcode === 0x8) {
1806
- socket.end();
1807
- return;
1808
- }
1809
- if (frame.opcode === 0x9) {
1810
- sendFrame(socket, frame.payload, 0xA);
1811
- continue;
1812
- }
1813
- if (frame.opcode === 0xA) {
1814
- continue;
1815
- }
1816
- if (frame.opcode === 0x1) {
1817
- fragments = [frame.payload];
1818
- } else if (frame.opcode === 0x0) {
1819
- fragments.push(frame.payload);
1820
- } else {
1821
- continue;
1822
- }
1823
-
1824
- if (frame.fin) {
1825
- const text = Buffer.concat(fragments).toString("utf8");
1826
- for (const line of text
1827
- .split("\\n")
1828
- .map((value) => value.trim())
1829
- .filter(Boolean)) {
1830
- relayFromClaude(runId, JSON.parse(line));
1831
- }
1832
- fragments = [];
1833
- }
1834
- }
1835
- });
1836
-
1837
- const clearPeer = () => {
1838
- const latestChannel = channels.get(runId);
1839
- if (!latestChannel) {
1840
- return;
1841
- }
1842
- if (latestChannel.claude === socket) {
1843
- latestChannel.claude = null;
1844
- }
1845
- if (
1846
- latestChannel.claude === null &&
1847
- latestChannel.pending.toHost.length === 0 &&
1848
- latestChannel.pending.claude.length === 0
1849
- ) {
1850
- channels.delete(runId);
1851
- }
1852
- };
1853
- socket.on("close", clearPeer);
1854
- socket.on("error", clearPeer);
1855
- }
1856
-
1857
- function registerHostPeer(socket) {
1858
- hostSocket = socket;
1859
- flushHostBacklog();
1860
-
1861
- let buffer = Buffer.alloc(0);
1862
- let fragments = [];
1863
-
1864
- socket.on("data", (chunk) => {
1865
- buffer = Buffer.concat([buffer, chunk]);
1866
-
1867
- while (true) {
1868
- const frame = parseFrame(buffer);
1869
- if (!frame) {
1870
- return;
1871
- }
1872
- buffer = buffer.subarray(frame.bytesUsed);
1873
-
1874
- if (frame.opcode === 0x8) {
1875
- socket.end();
1876
- return;
1877
- }
1878
- if (frame.opcode === 0x9) {
1879
- sendFrame(socket, frame.payload, 0xA);
1880
- continue;
1881
- }
1882
- if (frame.opcode === 0xA) {
1883
- continue;
1884
- }
1885
- if (frame.opcode === 0x1) {
1886
- fragments = [frame.payload];
1887
- } else if (frame.opcode === 0x0) {
1888
- fragments.push(frame.payload);
1889
- } else {
1890
- continue;
1891
- }
1892
-
1893
- if (frame.fin) {
1894
- const envelope = JSON.parse(Buffer.concat(fragments).toString("utf8"));
1895
- const runId = String(envelope.runId ?? "");
1896
- const message = envelope.message;
1897
- if (!runId || !message) {
1898
- fragments = [];
1899
- continue;
1900
- }
1901
- const channel = getChannel(runId);
1902
- if (channel.claude) {
1903
- sendFrame(
1904
- channel.claude,
1905
- Buffer.from(JSON.stringify(message) + "\\n", "utf8"),
1906
- );
1907
- } else {
1908
- channel.pending.claude.push(JSON.stringify(message) + "\\n");
1909
- }
1910
- fragments = [];
1911
- }
1912
- }
1913
- });
1914
-
1915
- const clearHost = () => {
1916
- if (hostSocket === socket) {
1917
- hostSocket = null;
1918
- }
1919
- };
1920
- socket.on("close", clearHost);
1921
- socket.on("error", clearHost);
1922
- }
1923
-
1924
- const server = http.createServer((_request, response) => {
1925
- response.writeHead(426, { "content-type": "text/plain" });
1926
- response.end("Upgrade Required");
1927
- });
1928
-
1929
- server.on("upgrade", (request, socket) => {
1930
- if (request.headers.upgrade?.toLowerCase() !== "websocket") {
1931
- socket.destroy();
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);
1932
1760
  return;
1933
1761
  }
1934
-
1935
- const key = request.headers["sec-websocket-key"];
1936
- if (typeof key !== "string") {
1937
- socket.destroy();
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]));
1938
1766
  return;
1939
1767
  }
1940
-
1941
- const accept = crypto
1942
- .createHash("sha1")
1943
- .update(key + magic)
1944
- .digest("base64");
1945
- socket.write(
1946
- [
1947
- "HTTP/1.1 101 Switching Protocols",
1948
- "Upgrade: websocket",
1949
- "Connection: Upgrade",
1950
- "Sec-WebSocket-Accept: " + accept,
1951
- "",
1952
- "",
1953
- ].join("\\r\\n"),
1954
- );
1955
-
1956
- const url = new URL(request.url ?? "/", "http://127.0.0.1");
1957
- const role = url.searchParams.get("role") === "host" ? "host" : "claude";
1958
- if (role === "host") {
1959
- registerHostPeer(socket);
1768
+ if (req.method === "POST" && (m = url.match(/^\\/runs\\/([^/]+)\\/sendMessage$/))) {
1769
+ handleSendMessage(req, res, decodeURIComponent(m[1]));
1960
1770
  return;
1961
1771
  }
1962
-
1963
- const runId = url.searchParams.get("runId") ?? "default";
1964
- registerClaudePeer(socket, runId);
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");
1965
1782
  });
1966
1783
 
1967
- server.listen(port, "0.0.0.0");
1784
+ server.listen(port, "0.0.0.0", () => {
1785
+ console.error("[claude-code-daemon] listening on :" + port + " v" + VERSION);
1786
+ });
1968
1787
 
1969
1788
  const shutdown = () => {
1970
- hostSocket?.destroy();
1971
- for (const channel of channels.values()) {
1972
- channel.claude?.destroy();
1789
+ for (const r of liveRuns.values()) {
1790
+ r.prompt.end();
1791
+ r.query.interrupt().catch(() => {});
1973
1792
  }
1974
- server.close(() => process.exit(0));
1793
+ server.close();
1794
+ setTimeout(() => process.exit(0), 100).unref();
1975
1795
  };
1976
-
1977
- process.on("SIGINT", shutdown);
1978
1796
  process.on("SIGTERM", shutdown);
1979
- `.trimStart();
1980
- }
1981
- function toWebSocketUrl(url) {
1982
- const parsed = new URL(url);
1983
- parsed.protocol = parsed.protocol === "https:" ? "wss:" : "ws:";
1984
- return parsed.toString();
1985
- }
1986
- function toSharedHostWebSocketUrl(url) {
1987
- const parsed = new URL(toWebSocketUrl(url));
1988
- parsed.searchParams.set("role", "host");
1989
- parsed.searchParams.delete("runId");
1990
- return parsed.toString();
1797
+ process.on("SIGINT", shutdown);
1798
+ `;
1991
1799
  }
1992
- function toClaudeRelayUrl(port, runId) {
1993
- const parsed = new URL(`ws://127.0.0.1:${port}/`);
1994
- parsed.searchParams.set("role", "claude");
1995
- parsed.searchParams.set("runId", runId);
1996
- return parsed.toString();
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;
1997
1813
  }
1998
- function buildLocalSdkUrl(server, sandboxProvider) {
1999
- if (sandboxProvider === SandboxProvider.LocalDocker) {
2000
- return server.url.replace("127.0.0.1", "host.docker.internal").replace("0.0.0.0", "host.docker.internal");
2001
- }
2002
- return server.url;
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
+ });
2003
1868
  }
2004
- async function connectRemoteTransport(url, headers = {}) {
2005
- const startedAt = Date.now();
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;
2006
1873
  let lastError;
2007
- while (Date.now() - startedAt < 3e4) {
2008
- const client = new SharedSdkWsConnection(url, headers);
1874
+ while (Date.now() < deadline) {
2009
1875
  try {
2010
- await Promise.race([
2011
- client.start(),
2012
- sleep(2e3).then(() => {
2013
- throw new Error(
2014
- `Timed out connecting to remote SDK bridge at ${url}.`
2015
- );
2016
- })
2017
- ]);
2018
- return client;
1876
+ return await fetch(input, init);
2019
1877
  } catch (error) {
2020
1878
  lastError = error;
2021
- await client.close().catch(() => void 0);
2022
- await sleep(250);
1879
+ const aborted = error?.name === "AbortError";
1880
+ if (aborted) {
1881
+ throw error;
1882
+ }
1883
+ await sleep(DAEMON_FIRST_REQUEST_RETRY_INTERVAL_MS);
2023
1884
  }
2024
1885
  }
2025
- throw lastError ?? new Error(`Could not connect to remote SDK bridge at ${url}.`);
1886
+ throw lastError ?? new Error("claude-code daemon request timed out");
2026
1887
  }
2027
- async function canConnectToRemoteRelay(previewUrl, headers = {}) {
2028
- const parsed = new URL(toWebSocketUrl(previewUrl));
2029
- parsed.searchParams.set("role", "claude");
2030
- parsed.searchParams.set("runId", "__probe__");
2031
- const client = new SharedSdkWsConnection(parsed.toString(), headers);
2032
- try {
2033
- await Promise.race([
2034
- client.start(),
2035
- sleep(2e3).then(() => {
2036
- throw new Error("Timed out connecting to remote relay.");
2037
- })
2038
- ]);
2039
- return true;
2040
- } catch {
2041
- return false;
2042
- } finally {
2043
- await client.close().catch(() => void 0);
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
+ }
2044
1907
  }
2045
- }
2046
- async function ensureSharedRemoteConnection(sandbox, previewUrl) {
2047
- const key = sandbox;
2048
- const existing = sharedRemoteConnectionBySandbox.get(key);
2049
- if (existing) {
1908
+ buffer += decoder.decode();
1909
+ if (buffer.trim()) {
2050
1910
  try {
2051
- const connection = await existing;
2052
- await connection.connection.waitForConnection(1e3);
2053
- return connection;
1911
+ yield JSON.parse(buffer);
2054
1912
  } catch {
2055
- try {
2056
- const stale = await existing;
2057
- await stale.connection.close().catch(() => void 0);
2058
- } catch {
2059
- }
2060
- sharedRemoteConnectionBySandbox.delete(key);
2061
1913
  }
2062
1914
  }
2063
- const created = (async () => {
2064
- const url = toSharedHostWebSocketUrl(previewUrl);
2065
- const connection = await connectRemoteTransport(url, sandbox.previewHeaders);
2066
- return { previewUrl, connection };
2067
- })();
2068
- sharedRemoteConnectionBySandbox.set(key, created);
2069
- try {
2070
- return await created;
2071
- } catch (error) {
2072
- sharedRemoteConnectionBySandbox.delete(key);
2073
- throw error;
2074
- }
2075
1915
  }
2076
- async function ensureRemoteRelay(request, prepared) {
2077
- const sandbox = request.options.sandbox;
2078
- await sandbox.openPort(REMOTE_SDK_RELAY_PORT);
2079
- const previewUrl = await sandbox.getPreviewLink(REMOTE_SDK_RELAY_PORT);
2080
- const previewHeaders = sandbox.previewHeaders;
2081
- if (await canConnectToRemoteRelay(previewUrl, previewHeaders)) {
2082
- return {
2083
- relayPort: REMOTE_SDK_RELAY_PORT,
2084
- relayPath: REMOTE_SDK_RELAY_PATH,
2085
- previewUrl
2086
- };
2087
- }
2088
- await prepared.target.writeArtifact({
2089
- path: REMOTE_SDK_RELAY_PATH,
2090
- content: createRemoteSdkRelayScript()
2091
- });
2092
- const relayLogPath = "/tmp/agentbox/claude-code/relay.log";
2093
- const relayHandle = await sandbox.runAsync(
2094
- [
2095
- `mkdir -p ${shellQuote(path8.posix.dirname(REMOTE_SDK_RELAY_PATH))}`,
2096
- `mkdir -p ${shellQuote(path8.posix.dirname(relayLogPath))}`,
2097
- `node ${shellQuote(REMOTE_SDK_RELAY_PATH)} ${shellQuote(String(REMOTE_SDK_RELAY_PORT))} > ${shellQuote(relayLogPath)} 2>&1`
2098
- ].join(" && "),
2099
- {
2100
- cwd: request.options.cwd,
2101
- env: { ...prepared.env, IS_SANDBOX: "1" }
2102
- }
2103
- );
2104
- let relayExit;
2105
- void relayHandle.wait().then((result) => {
2106
- relayExit = result;
2107
- }).catch((error) => {
2108
- relayExit = error;
2109
- });
2110
- const startedAt = Date.now();
2111
- while (Date.now() - startedAt < 3e4) {
2112
- if (await canConnectToRemoteRelay(previewUrl, previewHeaders)) {
2113
- return {
2114
- relayPort: REMOTE_SDK_RELAY_PORT,
2115
- relayPath: REMOTE_SDK_RELAY_PATH,
2116
- previewUrl,
2117
- handle: relayHandle
2118
- };
2119
- }
2120
- if (relayExit !== void 0) {
2121
- break;
2122
- }
2123
- await sleep(250);
2124
- }
2125
- await relayHandle.kill().catch(() => void 0);
2126
- throw new Error(`Timed out waiting for Claude relay on ${previewUrl}.`);
1916
+ async function daemonBaseUrl(sandbox) {
1917
+ const url = await sandbox.getPreviewLink(DAEMON_PORT);
1918
+ return url.replace(/\/$/, "");
2127
1919
  }
2128
- async function createLocalRuntime(request, prepared) {
2129
- const sandboxProvider = request.options.sandbox?.provider;
2130
- const transport = new SdkWsServer({
2131
- host: sandboxProvider === SandboxProvider.LocalDocker ? "0.0.0.0" : "127.0.0.1"
2132
- });
2133
- await transport.start();
2134
- const args = prepared.buildArgs(buildLocalSdkUrl(transport, sandboxProvider));
2135
- if (request.options.sandbox) {
2136
- const handle = await request.options.sandbox.runAsync(
2137
- [request.options.provider?.binary ?? "claude", ...args],
2138
- {
2139
- cwd: request.options.cwd,
2140
- env: {
2141
- ...prepared.env
2142
- },
2143
- pty: true
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
+ );
2144
1934
  }
2145
- );
2146
- return {
2147
- transport,
2148
- cleanup: async () => {
2149
- await handle.kill();
2150
- await transport.close();
2151
- await prepared.target.cleanup();
2152
- },
2153
- raw: { transport, handle, layout: prepared.target.layout },
2154
- initializeRequest: prepared.initializeRequest
2155
- };
2156
- }
2157
- const processHandle = spawnCommand({
2158
- command: request.options.provider?.binary ?? "claude",
2159
- args,
2160
- cwd: request.options.cwd,
2161
- env: {
2162
- ...process.env,
2163
- ...prepared.env
2164
- }
2165
- });
2166
- return {
2167
- transport,
2168
- cleanup: async () => {
2169
- await processHandle.kill();
2170
- await transport.close();
2171
- await prepared.target.cleanup();
2172
- },
2173
- raw: { transport, processHandle, layout: prepared.target.layout },
2174
- initializeRequest: prepared.initializeRequest
2175
- };
2176
- }
2177
- async function createRemoteSandboxRuntime(request, prepared) {
2178
- const sandbox = request.options.sandbox;
2179
- const relay = await ensureRemoteRelay(request, prepared);
2180
- const args = prepared.buildArgs(
2181
- toClaudeRelayUrl(relay.relayPort, request.runId)
2182
- );
2183
- const handle = await sandbox.runAsync(
2184
- [request.options.provider?.binary ?? "claude", ...args],
2185
- {
2186
- cwd: request.options.cwd,
2187
- env: { ...prepared.env, IS_SANDBOX: "1" },
2188
- pty: true
2189
- }
2190
- );
2191
- const sharedConnection = await ensureSharedRemoteConnection(
2192
- sandbox,
2193
- relay.previewUrl
2194
- );
2195
- const transport = sharedConnection.connection.createChannel(request.runId);
2196
- return {
2197
- transport,
2198
- cleanup: async () => {
2199
- await handle.kill().catch(() => void 0);
2200
- await transport.close().catch(() => void 0);
2201
- await prepared.target.cleanup();
2202
- },
2203
- raw: { transport, handle, relay, layout: prepared.target.layout },
2204
- initializeRequest: prepared.initializeRequest
2205
- };
2206
- }
2207
- async function createRuntime(request) {
2208
- if (request.options.sandbox && request.options.sandbox.provider !== SandboxProvider.LocalDocker) {
2209
- await request.options.sandbox.openPort(REMOTE_SDK_RELAY_PORT);
2210
- const prepared2 = await prepareClaudeRuntime(request);
2211
- return createRemoteSandboxRuntime(request, prepared2);
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
+ });
2212
1967
  }
2213
- const prepared = await prepareClaudeRuntime(request);
2214
- return createLocalRuntime(request, prepared);
2215
- }
2216
- var ClaudeCodeAgentAdapter = class {
2217
1968
  async execute(request, sink) {
2218
- const inputParts = await validateProviderUserInput(
2219
- request.provider,
2220
- request.run.input
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)
2221
1993
  );
2222
1994
  const userContent = mapToClaudeUserContent(inputParts);
2223
- let sessionId = "";
2224
- let accumulatedText = "";
2225
- let usedStreaming = false;
2226
- let pendingMessages = 1;
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
+ });
2227
2010
  const autoApproveTools = shouldAutoApproveClaudeTools(request.options);
2228
- const transportRef = {};
2229
- const queuedSends = [];
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);
2230
2041
  sink.onMessage(async (content) => {
2231
- pendingMessages++;
2232
2042
  const parts = await validateProviderUserInput(request.provider, content);
2233
2043
  const mapped = mapToClaudeUserContent(parts);
2234
- accumulatedText = "";
2235
- usedStreaming = false;
2236
- const payload = {
2237
- type: "user",
2238
- message: { role: "user", content: mapped },
2239
- parent_tool_use_id: null,
2240
- session_id: sessionId || request.run.resumeSessionId || "",
2241
- uuid: randomUUID2()
2242
- };
2243
- if (transportRef.current) {
2244
- await transportRef.current.send(payload);
2245
- } else {
2246
- queuedSends.push(payload);
2247
- }
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 };
2248
2057
  });
2249
- const runtime = await createRuntime(request);
2250
- transportRef.current = runtime.transport;
2251
- sink.setRaw(runtime.raw);
2252
- sink.setAbort(runtime.cleanup);
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 });
2253
2078
  sink.emitEvent(
2254
2079
  createNormalizedEvent("run.started", {
2255
2080
  provider: request.provider,
2256
2081
  runId: request.runId
2257
2082
  })
2258
2083
  );
2259
- const completion = new Promise((resolve, reject) => {
2260
- void (async () => {
2261
- for await (const message of runtime.transport.messages()) {
2262
- sink.emitRaw(toRawEvent(request.runId, message, message.type));
2263
- if (message.type === "system" && message.subtype === "init") {
2264
- sessionId = String(message.session_id ?? "");
2265
- if (sessionId) {
2266
- sink.setSessionId(sessionId);
2267
- }
2268
- continue;
2269
- }
2270
- if (message.type === "control_request" && message.request?.subtype === "can_use_tool") {
2271
- const requestId = String(message.request_id ?? "");
2272
- const requestPayload = message.request;
2273
- const response = autoApproveTools ? {
2274
- requestId,
2275
- decision: "allow"
2276
- } : await sink.requestPermission(
2277
- createClaudePermissionEvent(request, message)
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
2278
2113
  );
2279
- if (response.decision === "allow") {
2280
- sink.emitEvent(
2281
- createNormalizedEvent(
2282
- "tool.call.started",
2283
- {
2284
- provider: request.provider,
2285
- runId: request.runId
2286
- },
2287
- {
2288
- toolName: String(requestPayload.tool_name ?? "tool"),
2289
- callId: requestId,
2290
- input: requestPayload.input
2291
- }
2292
- )
2293
- );
2294
- }
2295
- await runtime.transport.send({
2296
- type: "control_response",
2297
- response: {
2298
- subtype: "success",
2299
- request_id: requestId,
2300
- response: response.decision === "allow" ? {
2301
- behavior: "allow",
2302
- updatedInput: requestPayload.input
2303
- } : {
2304
- behavior: "deny",
2305
- message: "User denied this action."
2306
- }
2307
- }
2308
- });
2309
- continue;
2310
2114
  }
2311
- if (message.type === "stream_event") {
2312
- const delta = extractStreamDelta(message);
2313
- if (delta) {
2314
- usedStreaming = true;
2315
- accumulatedText += delta;
2316
- sink.emitEvent(
2317
- createNormalizedEvent(
2318
- "text.delta",
2319
- {
2320
- provider: request.provider,
2321
- runId: request.runId
2322
- },
2323
- {
2324
- delta
2325
- }
2326
- )
2327
- );
2328
- }
2329
- continue;
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
+ );
2330
2124
  }
2331
- if (message.type === "assistant") {
2332
- const text = extractAssistantText(message);
2333
- if (!usedStreaming && text) {
2334
- accumulatedText = text;
2335
- sink.emitEvent(
2336
- createNormalizedEvent(
2337
- "text.delta",
2338
- {
2339
- provider: request.provider,
2340
- runId: request.runId
2341
- },
2342
- {
2343
- delta: text
2344
- }
2345
- )
2346
- );
2347
- }
2125
+ const partial = message;
2126
+ const { text, thinking } = extractStreamDeltas(partial);
2127
+ if (thinking) {
2348
2128
  sink.emitEvent(
2349
2129
  createNormalizedEvent(
2350
- "message.completed",
2351
- {
2352
- provider: request.provider,
2353
- runId: request.runId
2354
- },
2355
- {
2356
- text
2357
- }
2130
+ "reasoning.delta",
2131
+ { provider: request.provider, runId: request.runId },
2132
+ { delta: thinking }
2358
2133
  )
2359
2134
  );
2360
- continue;
2361
2135
  }
2362
- if (message.type === "result") {
2363
- const subtype = String(message.subtype ?? "success");
2364
- if (subtype === "success") {
2365
- pendingMessages--;
2366
- if (pendingMessages <= 0) {
2367
- resolve({ text: accumulatedText });
2368
- return;
2369
- }
2370
- continue;
2371
- } else {
2372
- reject(
2373
- new Error(
2374
- String(
2375
- message.result ?? message.error ?? "Claude Code run failed."
2376
- )
2377
- )
2136
+ if (text) {
2137
+ if (!firstTextDeltaLogged) {
2138
+ firstTextDeltaLogged = true;
2139
+ debugClaude(
2140
+ "\u2605 first text delta (%dms since execute start)",
2141
+ Date.now() - executeStartedAt
2378
2142
  );
2379
2143
  }
2380
- return;
2144
+ accumulatedText += text;
2145
+ sink.emitEvent(
2146
+ createNormalizedEvent(
2147
+ "text.delta",
2148
+ { provider: request.provider, runId: request.runId },
2149
+ { delta: text }
2150
+ )
2151
+ );
2381
2152
  }
2382
- if (message.type === "auth_status" && message.authenticated === false) {
2383
- reject(
2384
- new Error("Claude Code reported an authentication failure.")
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
+ )
2385
2165
  );
2386
- return;
2387
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;
2388
2189
  }
2389
- reject(new Error("Claude Code transport closed before run completed."));
2390
- })().catch(reject);
2391
- });
2392
- try {
2393
- await runtime.transport.waitForConnection(3e4);
2394
- if (runtime.initializeRequest) {
2395
- const response = await runtime.transport.request(
2396
- runtime.initializeRequest
2397
- );
2398
- sink.emitRaw(
2399
- toRawEvent(request.runId, response, "control_response:initialize")
2400
- );
2401
2190
  }
2402
- await runtime.transport.send({
2403
- type: "user",
2404
- message: {
2405
- role: "user",
2406
- content: userContent
2407
- },
2408
- parent_tool_use_id: null,
2409
- session_id: request.run.resumeSessionId ?? "",
2410
- uuid: randomUUID2()
2411
- });
2412
- sink.emitEvent(
2413
- createNormalizedEvent("message.started", {
2414
- provider: request.provider,
2415
- runId: request.runId
2416
- })
2191
+ const finalText = accumulatedText;
2192
+ debugClaude(
2193
+ "\u2605 run.completed (%dms since execute start) chars=%d",
2194
+ Date.now() - executeStartedAt,
2195
+ finalText.length
2417
2196
  );
2418
- for (const queued of queuedSends.splice(0)) {
2419
- await runtime.transport.send(queued);
2420
- }
2421
- const { text } = await completion;
2422
2197
  sink.emitEvent(
2423
2198
  createNormalizedEvent(
2424
2199
  "run.completed",
2425
- {
2426
- provider: request.provider,
2427
- runId: request.runId
2428
- },
2429
- {
2430
- text
2431
- }
2200
+ { provider: request.provider, runId: request.runId },
2201
+ { text: finalText }
2432
2202
  )
2433
2203
  );
2434
- sink.complete({ text });
2204
+ sink.complete({
2205
+ text: finalText,
2206
+ costData: extractClaudeCostData(rawPayloads)
2207
+ });
2435
2208
  } finally {
2436
- await runtime.cleanup().catch(() => void 0);
2209
+ fetchAbort.abort();
2437
2210
  }
2438
2211
  return async () => void 0;
2439
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
+ }
2440
2268
  };
2441
2269
 
2442
2270
  // src/agents/providers/codex.ts
@@ -2444,7 +2272,7 @@ import path9 from "path";
2444
2272
 
2445
2273
  // src/agents/transports/app-server.ts
2446
2274
  import { createParser } from "eventsource-parser";
2447
- import { WebSocket as WebSocket2 } from "ws";
2275
+ import { WebSocket } from "ws";
2448
2276
  async function fetchJson(url, init) {
2449
2277
  const response = await fetch(url, init);
2450
2278
  if (!response.ok) {
@@ -2507,7 +2335,7 @@ async function* streamSse(url, init) {
2507
2335
  }
2508
2336
  async function connectJsonRpcWebSocket(url, options) {
2509
2337
  const notifications = new AsyncQueue();
2510
- const socket = new WebSocket2(url, { headers: options?.headers });
2338
+ const socket = new WebSocket(url, { headers: options?.headers });
2511
2339
  await new Promise((resolve, reject) => {
2512
2340
  const cleanup = () => {
2513
2341
  socket.off("open", handleOpen);
@@ -2548,7 +2376,7 @@ async function connectJsonRpcWebSocket(url, options) {
2548
2376
  },
2549
2377
  close: async () => {
2550
2378
  await new Promise((resolve) => {
2551
- if (socket.readyState === WebSocket2.CLOSED || socket.readyState === WebSocket2.CLOSING) {
2379
+ if (socket.readyState === WebSocket.CLOSED || socket.readyState === WebSocket.CLOSING) {
2552
2380
  resolve();
2553
2381
  return;
2554
2382
  }
@@ -2633,6 +2461,12 @@ var JsonRpcLineClient = class {
2633
2461
  };
2634
2462
 
2635
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
+ }
2636
2470
  var REMOTE_CODEX_APP_SERVER_PORT = 43181;
2637
2471
  var REMOTE_CODEX_APP_SERVER_ID = "shared-app-server";
2638
2472
  function compactEnv(values) {
@@ -2681,16 +2515,31 @@ function buildTurnSandboxPolicy(options) {
2681
2515
  };
2682
2516
  }
2683
2517
  function buildTurnCollaborationMode(request) {
2684
- if (!request.run.systemPrompt) {
2518
+ const systemPrompt = request.run.systemPrompt;
2519
+ if (!systemPrompt) {
2685
2520
  return void 0;
2686
2521
  }
2687
2522
  return {
2688
2523
  mode: "custom",
2689
2524
  settings: {
2690
- developer_instructions: request.run.systemPrompt
2525
+ developer_instructions: systemPrompt
2691
2526
  }
2692
2527
  };
2693
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
+ }
2694
2543
  function toRawEvent2(runId, payload, type) {
2695
2544
  return {
2696
2545
  provider: AgentProvider.Codex,
@@ -2706,8 +2555,13 @@ function shouldIgnoreCodexError(notification) {
2706
2555
  }
2707
2556
  return notification.params?.willRetry === true;
2708
2557
  }
2709
- function buildCodexCommandArgs(binary, args) {
2710
- return ["-u", "CODEX_HOME", "-u", "XDG_CONFIG_HOME", binary, ...args];
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];
2711
2565
  }
2712
2566
  function toNormalizedCodexEvents(runId, notification) {
2713
2567
  const base = {
@@ -2716,7 +2570,23 @@ function toNormalizedCodexEvents(runId, notification) {
2716
2570
  raw: toRawEvent2(runId, notification, notification.method)
2717
2571
  };
2718
2572
  if (notification.method === "turn/started") {
2719
- return [createNormalizedEvent("message.started", base)];
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 })] : [];
2720
2590
  }
2721
2591
  if (notification.method === "item/completed") {
2722
2592
  const item = notification.params?.item;
@@ -2725,7 +2595,6 @@ function toNormalizedCodexEvents(runId, notification) {
2725
2595
  }
2726
2596
  if (item.type === "agentMessage" && typeof item.text === "string") {
2727
2597
  return [
2728
- createNormalizedEvent("text.delta", base, { delta: item.text }),
2729
2598
  createNormalizedEvent("message.completed", base, { text: item.text })
2730
2599
  ];
2731
2600
  }
@@ -2852,22 +2721,6 @@ function toCodexApprovalDecision(notification, response) {
2852
2721
  }
2853
2722
  return "accept";
2854
2723
  }
2855
- function buildCodexSkillInputItems(skills) {
2856
- return skills.map((skill) => ({
2857
- type: "skill",
2858
- name: skill.name,
2859
- path: skill.skillFilePath
2860
- }));
2861
- }
2862
- function buildCodexPromptText(prompt, skills) {
2863
- if (skills.length === 0) {
2864
- return prompt;
2865
- }
2866
- return [
2867
- `Available skills for this run: ${skills.map((skill) => `$${skill.name}`).join(", ")}.`,
2868
- prompt
2869
- ].join("\n\n");
2870
- }
2871
2724
  function codexImageExtension(mediaType) {
2872
2725
  switch (mediaType) {
2873
2726
  case "image/gif":
@@ -2882,35 +2735,41 @@ function codexImageExtension(mediaType) {
2882
2735
  return ".img";
2883
2736
  }
2884
2737
  }
2885
- async function materializeCodexImage(target, part, index) {
2738
+ async function materializeCodexImage(options, part, index) {
2886
2739
  if (part.source.type === "url") {
2887
2740
  return part.source.url;
2888
2741
  }
2742
+ const root = agentboxRoot(AgentProvider.Codex, Boolean(options.sandbox));
2889
2743
  const imagePath = path9.join(
2890
- target.layout.rootDir,
2744
+ root,
2891
2745
  "inputs",
2892
2746
  `codex-image-${index}${codexImageExtension(part.mediaType)}`
2893
2747
  );
2894
- const encodedPath = `${imagePath}.b64`;
2895
- await target.writeArtifact({
2896
- path: encodedPath,
2897
- content: part.source.data
2898
- });
2899
- await target.runCommand(
2900
- [
2901
- `mkdir -p ${shellQuote(path9.posix.dirname(imagePath))}`,
2902
- `(base64 --decode < ${shellQuote(encodedPath)} > ${shellQuote(imagePath)} || base64 -D < ${shellQuote(encodedPath)} > ${shellQuote(imagePath)})`,
2903
- `rm -f ${shellQuote(encodedPath)}`
2904
- ].join(" && ")
2905
- );
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"));
2906
2764
  return imagePath;
2907
2765
  }
2908
- function resolveCodexOpenAiBaseUrl(request) {
2909
- return request.options.env?.OPENAI_BASE_URL ?? request.options.provider?.env?.OPENAI_BASE_URL;
2766
+ function resolveCodexOpenAiBaseUrlFromOptions(options) {
2767
+ return options.env?.OPENAI_BASE_URL ?? options.provider?.env?.OPENAI_BASE_URL;
2910
2768
  }
2911
- async function ensureCodexLogin(request, target) {
2912
- const openAiApiKey = request.options.env?.OPENAI_API_KEY ?? request.options.provider?.env?.OPENAI_API_KEY;
2913
- const openAiBaseUrl = resolveCodexOpenAiBaseUrl(request);
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);
2914
2773
  const extraEnv = {};
2915
2774
  if (openAiApiKey) {
2916
2775
  extraEnv.OPENAI_API_KEY = openAiApiKey;
@@ -2919,7 +2778,11 @@ async function ensureCodexLogin(request, target) {
2919
2778
  extraEnv.OPENAI_BASE_URL = openAiBaseUrl;
2920
2779
  }
2921
2780
  await target.runCommand(
2922
- 'if [ -z "${OPENAI_API_KEY:-}" ]; then exit 0; fi; printenv OPENAI_API_KEY | env -u CODEX_HOME -u XDG_CONFIG_HOME codex login --with-api-key >/dev/null 2>&1',
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("; "),
2923
2786
  Object.keys(extraEnv).length > 0 ? extraEnv : void 0
2924
2787
  );
2925
2788
  }
@@ -2928,90 +2791,109 @@ function toRemoteCodexWebSocketUrl(url) {
2928
2791
  parsed.protocol = parsed.protocol === "https:" ? "wss:" : "ws:";
2929
2792
  return parsed.toString();
2930
2793
  }
2931
- async function connectRemoteCodexAppServer(url, headers = {}) {
2932
- const startedAt = Date.now();
2933
- let lastError;
2934
- while (Date.now() - startedAt < 3e4) {
2935
- try {
2936
- return await connectJsonRpcWebSocket(url, { headers });
2937
- } catch (error) {
2938
- lastError = error;
2939
- await sleep(250);
2940
- }
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
+ );
2941
2800
  }
2942
- throw lastError ?? new Error(`Could not connect to Codex app-server at ${url}.`);
2943
- }
2944
- async function waitForInternalCodexReady(sandbox, port, cwd, env) {
2945
- const startedAt = Date.now();
2946
- while (Date.now() - startedAt < 6e4) {
2947
- const result = await sandbox.run(`curl -fsS http://127.0.0.1:${port}/readyz >/dev/null`, {
2948
- cwd,
2949
- env,
2950
- timeoutMs: 5e3
2951
- }).catch(() => void 0);
2952
- if (result?.exitCode === 0) {
2953
- return;
2954
- }
2955
- await sleep(250);
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);
2956
2819
  }
2957
- throw new Error(`Codex internal app-server did not become ready on ${port}.`);
2958
2820
  }
2959
- async function createRuntime2(request, inputParts) {
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) {
2960
2843
  const options = request.options;
2844
+ const provider = request.provider;
2845
+ const hooks = assertHooksSupported(provider, options);
2846
+ assertCommandsSupported(provider, options.commands);
2961
2847
  const usesRemoteWebSocket = options.sandbox && options.sandbox.provider !== SandboxProvider.LocalDocker;
2962
- const hooks = assertHooksSupported(request.provider, options);
2963
- assertCommandsSupported(request.provider, options.commands);
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
+ }
2964
2877
  if (usesRemoteWebSocket && options.sandbox) {
2965
2878
  const sandbox = options.sandbox;
2966
- await sandbox.openPort(REMOTE_CODEX_APP_SERVER_PORT);
2967
- const sharedTarget = await createRuntimeTarget(
2968
- request.provider,
2879
+ const sharedTarget = await createSetupTarget(
2880
+ provider,
2969
2881
  REMOTE_CODEX_APP_SERVER_ID,
2970
2882
  options
2971
2883
  );
2972
- await ensureCodexLogin(request, sharedTarget);
2973
- const env2 = compactEnv({
2884
+ const target2 = await createSetupTarget(provider, "shared-setup", options);
2885
+ const env = compactEnv({
2974
2886
  ...options.env ?? {},
2975
2887
  ...sharedTarget.env,
2976
2888
  ...options.provider?.env ?? {}
2977
2889
  });
2978
- const serverCwd = sharedTarget.layout.rootDir;
2979
- const previewUrl = await sandbox.getPreviewLink(
2980
- REMOTE_CODEX_APP_SERVER_PORT
2890
+ await time(
2891
+ debugCodex,
2892
+ "ensureCodexLogin",
2893
+ () => ensureCodexLoginViaConfig(request, sharedTarget)
2981
2894
  );
2982
- const {
2983
- artifacts: subAgentArtifacts2,
2984
- agentSections: agentSections2,
2985
- enableMultiAgent: enableMultiAgent2
2986
- } = buildCodexSubagentArtifacts(options.subAgents, sharedTarget.layout);
2987
- const serverArtifacts = [...subAgentArtifacts2];
2988
- const hooksFile2 = buildCodexHooksFile(hooks);
2989
- const configToml2 = buildCodexConfigToml(
2990
- options.mcps,
2991
- agentSections2,
2992
- Boolean(hooksFile2)
2993
- );
2994
- if (configToml2) {
2995
- serverArtifacts.push({
2996
- path: path9.join(sharedTarget.layout.codexDir, "config.toml"),
2997
- content: configToml2
2998
- });
2999
- }
3000
- if (hooksFile2) {
3001
- serverArtifacts.push({
3002
- path: path9.join(sharedTarget.layout.codexDir, "hooks.json"),
3003
- content: JSON.stringify(hooksFile2, null, 2)
3004
- });
3005
- }
3006
- for (const artifact of serverArtifacts) {
3007
- await sharedTarget.writeArtifact(artifact);
3008
- }
3009
- const configArgs2 = [];
3010
- configArgs2.push("-c", `features.multi_agent=${enableMultiAgent2}`);
3011
- const openAiBaseUrl2 = resolveCodexOpenAiBaseUrl(request);
3012
- if (openAiBaseUrl2) {
3013
- configArgs2.push("-c", `openai_base_url=${JSON.stringify(openAiBaseUrl2)}`);
3014
- }
2895
+ const { artifacts: serverArtifacts } = buildArtifactsFor(sharedTarget);
2896
+ await applyDifferentialSetup(sharedTarget, serverArtifacts, []);
3015
2897
  const binary = options.provider?.binary ?? "codex";
3016
2898
  const pidFilePath = path9.posix.join(
3017
2899
  sharedTarget.layout.rootDir,
@@ -3021,219 +2903,115 @@ async function createRuntime2(request, inputParts) {
3021
2903
  sharedTarget.layout.rootDir,
3022
2904
  "codex-app-server.log"
3023
2905
  );
3024
- const launchResult = await sandbox.run(
3025
- [
3026
- `mkdir -p ${shellQuote(sharedTarget.layout.rootDir)}`,
3027
- `if curl -fsS http://127.0.0.1:${REMOTE_CODEX_APP_SERVER_PORT}/readyz >/dev/null 2>&1; then exit 0; fi`,
3028
- `if [ -f ${shellQuote(pidFilePath)} ]; then kill "$(cat ${shellQuote(pidFilePath)})" >/dev/null 2>&1 || true; rm -f ${shellQuote(pidFilePath)}; fi`,
3029
- `(${[
3030
- `nohup ${[
3031
- "env",
3032
- ...buildCodexCommandArgs(binary, [
3033
- ...configArgs2,
3034
- "app-server",
3035
- "--listen",
3036
- `ws://0.0.0.0:${REMOTE_CODEX_APP_SERVER_PORT}`
3037
- ])
3038
- ].map(shellQuote).join(" ")} > ${shellQuote(logFilePath)} 2>&1 &`,
3039
- `echo $! > ${shellQuote(pidFilePath)}`
3040
- ].join(" ")})`
3041
- ].join(" && "),
3042
- {
3043
- cwd: serverCwd,
3044
- env: env2
3045
- }
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
+ )
3046
2936
  );
3047
2937
  if (launchResult.exitCode !== 0) {
3048
2938
  throw new Error(
3049
2939
  `Could not start Codex app-server: ${launchResult.combinedOutput || launchResult.stderr}`
3050
2940
  );
3051
2941
  }
3052
- await waitForInternalCodexReady(
3053
- sandbox,
3054
- REMOTE_CODEX_APP_SERVER_PORT,
3055
- serverCwd,
3056
- env2
3057
- );
3058
- const target2 = await createRuntimeTarget(
3059
- request.provider,
3060
- request.runId,
3061
- options
3062
- );
3063
2942
  try {
3064
- const {
3065
- artifacts: skillArtifacts2,
3066
- installCommands: installCommands2,
3067
- preparedSkills: preparedSkills2
3068
- } = await prepareSkillArtifacts(
3069
- request.provider,
3070
- options.skills,
3071
- target2.layout
3072
- );
3073
- for (const artifact of skillArtifacts2) {
3074
- await target2.writeArtifact(artifact);
3075
- }
3076
- await installSkills(target2, installCommands2);
3077
- const textPrompt2 = joinTextParts(
3078
- inputParts.filter(
3079
- (part) => part.type === "text"
3080
- )
3081
- );
3082
- const codexPromptText2 = buildCodexPromptText(textPrompt2, preparedSkills2);
3083
- const inputItems2 = [];
3084
- if (codexPromptText2.trim().length > 0) {
3085
- inputItems2.push({
3086
- type: "text",
3087
- text: codexPromptText2,
3088
- text_elements: []
3089
- });
3090
- }
3091
- inputItems2.push(
3092
- ...await mapToCodexPromptParts(
3093
- inputParts,
3094
- async (part, index) => materializeCodexImage(target2, part, index)
3095
- )
3096
- );
3097
- inputItems2.push(...buildCodexSkillInputItems(preparedSkills2));
3098
- const transport = await connectRemoteCodexAppServer(
3099
- toRemoteCodexWebSocketUrl(previewUrl),
3100
- sandbox.previewHeaders
3101
- );
3102
- return {
3103
- source: transport.source,
3104
- writeLine: transport.send,
3105
- cleanup: async () => {
3106
- await transport?.close().catch(() => void 0);
3107
- await target2.cleanup().catch(() => void 0);
3108
- },
3109
- raw: {
3110
- transport: transport.raw,
3111
- previewUrl,
3112
- port: REMOTE_CODEX_APP_SERVER_PORT,
3113
- serverLayout: sharedTarget.layout,
3114
- layout: target2.layout
3115
- },
3116
- inputItems: inputItems2,
3117
- turnStartOverrides: buildTurnCollaborationMode(request)
3118
- };
2943
+ const { artifacts: skillArtifacts2, installCommands: installCommands2 } = await prepareSkillArtifacts(provider, options.skills, target2.layout);
2944
+ await applyDifferentialSetup(target2, skillArtifacts2, installCommands2);
3119
2945
  } catch (error) {
3120
2946
  await target2.cleanup().catch(() => void 0);
3121
2947
  throw error;
3122
2948
  }
2949
+ return;
3123
2950
  }
3124
- const target = await createRuntimeTarget(
3125
- request.provider,
3126
- request.runId,
3127
- options
3128
- );
2951
+ const target = await createSetupTarget(provider, "shared-setup", options);
3129
2952
  try {
3130
- await ensureCodexLogin(request, target);
2953
+ await ensureCodexLoginViaConfig(request, target);
3131
2954
  } catch (error) {
3132
2955
  await target.cleanup().catch(() => void 0);
3133
2956
  throw error;
3134
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);
3135
2969
  const env = compactEnv({
3136
2970
  ...options.env ?? {},
3137
- ...target.env,
2971
+ CODEX_HOME: codexDir,
3138
2972
  ...options.provider?.env ?? {}
3139
2973
  });
3140
- const runtimeCwd = target.layout.rootDir;
3141
- const {
3142
- artifacts: skillArtifacts,
3143
- installCommands,
3144
- preparedSkills
3145
- } = await prepareSkillArtifacts(
3146
- request.provider,
3147
- options.skills,
3148
- target.layout
3149
- );
3150
- const {
3151
- artifacts: subAgentArtifacts,
3152
- agentSections,
3153
- enableMultiAgent
3154
- } = buildCodexSubagentArtifacts(options.subAgents, target.layout);
3155
- const artifacts = [...skillArtifacts, ...subAgentArtifacts];
3156
- const hooksFile = buildCodexHooksFile(hooks);
3157
- const configToml = buildCodexConfigToml(
3158
- options.mcps,
3159
- agentSections,
3160
- Boolean(hooksFile)
3161
- );
3162
- if (configToml) {
3163
- artifacts.push({
3164
- path: path9.join(target.layout.codexDir, "config.toml"),
3165
- content: configToml
3166
- });
3167
- }
3168
- if (hooksFile) {
3169
- artifacts.push({
3170
- path: path9.join(target.layout.codexDir, "hooks.json"),
3171
- content: JSON.stringify(hooksFile, null, 2)
3172
- });
3173
- }
3174
- let instructionsFilePath;
3175
- if (request.run.systemPrompt) {
3176
- instructionsFilePath = path9.join(
3177
- target.layout.codexDir,
3178
- "prompts",
3179
- "agentbox-system.md"
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)
3180
2983
  );
3181
- artifacts.push({
3182
- path: instructionsFilePath,
3183
- content: request.run.systemPrompt
3184
- });
3185
- }
3186
- for (const artifact of artifacts) {
3187
- await target.writeArtifact(artifact);
3188
- }
3189
- await installSkills(target, installCommands);
3190
- const configArgs = [];
3191
- if (instructionsFilePath) {
3192
- configArgs.push(
3193
- "-c",
3194
- `model_instructions_file=${JSON.stringify(instructionsFilePath)}`
2984
+ const transport = await connectRemoteCodexAppServer(
2985
+ toRemoteCodexWebSocketUrl(previewUrl),
2986
+ sandbox.previewHeaders
3195
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
+ };
3196
3004
  }
3197
- configArgs.push("-c", `features.multi_agent=${enableMultiAgent}`);
3198
- const openAiBaseUrl = resolveCodexOpenAiBaseUrl(request);
3199
- if (openAiBaseUrl) {
3200
- configArgs.push("-c", `openai_base_url=${JSON.stringify(openAiBaseUrl)}`);
3201
- }
3202
- const textPrompt = joinTextParts(
3203
- inputParts.filter(
3204
- (part) => part.type === "text"
3205
- )
3206
- );
3207
- const codexPromptText = buildCodexPromptText(textPrompt, preparedSkills);
3208
- const inputItems = [];
3209
- if (codexPromptText.trim().length > 0) {
3210
- inputItems.push({
3211
- type: "text",
3212
- text: codexPromptText,
3213
- text_elements: []
3214
- });
3215
- }
3216
- inputItems.push(
3217
- ...await mapToCodexPromptParts(
3218
- inputParts,
3219
- async (part, index) => materializeCodexImage(target, part, index)
3220
- )
3005
+ const codexArgs = buildCodexCommandArgs(
3006
+ options.provider?.binary ?? "codex",
3007
+ ["app-server"],
3008
+ options
3221
3009
  );
3222
- inputItems.push(...buildCodexSkillInputItems(preparedSkills));
3223
3010
  if (options.sandbox) {
3224
- const handle = await options.sandbox.runAsync(
3225
- [
3226
- "env",
3227
- ...buildCodexCommandArgs(options.provider?.binary ?? "codex", [
3228
- ...configArgs,
3229
- "app-server"
3230
- ])
3231
- ],
3232
- {
3233
- cwd: runtimeCwd,
3234
- env
3235
- }
3236
- );
3011
+ const handle = await options.sandbox.runAsync(["env", ...codexArgs], {
3012
+ cwd: runtimeCwd,
3013
+ env
3014
+ });
3237
3015
  if (!handle.write) {
3238
3016
  throw new Error(
3239
3017
  "The selected sandbox does not expose an interactive stdin channel for Codex."
@@ -3257,18 +3035,15 @@ async function createRuntime2(request, inputParts) {
3257
3035
  },
3258
3036
  cleanup: async () => {
3259
3037
  await handle.kill();
3260
- await target.cleanup();
3261
3038
  },
3262
- raw: { handle, layout: target.layout },
3263
- inputItems
3039
+ raw: { handle, codexDir },
3040
+ inputItems,
3041
+ turnStartOverrides: buildTurnCollaborationMode(request)
3264
3042
  };
3265
3043
  }
3266
3044
  const processHandle = spawnCommand({
3267
3045
  command: "env",
3268
- args: buildCodexCommandArgs(options.provider?.binary ?? "codex", [
3269
- ...configArgs,
3270
- "app-server"
3271
- ]),
3046
+ args: codexArgs,
3272
3047
  cwd: runtimeCwd,
3273
3048
  env: {
3274
3049
  ...process.env,
@@ -3283,19 +3058,51 @@ async function createRuntime2(request, inputParts) {
3283
3058
  },
3284
3059
  cleanup: async () => {
3285
3060
  await processHandle.kill();
3286
- await target.cleanup();
3287
3061
  },
3288
- raw: { processHandle, layout: target.layout },
3289
- inputItems
3062
+ raw: { processHandle, codexDir },
3063
+ inputItems,
3064
+ turnStartOverrides: buildTurnCollaborationMode(request)
3290
3065
  };
3291
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
+ }
3292
3089
  var CodexAgentAdapter = class {
3090
+ async setup(request) {
3091
+ await setupCodex(request);
3092
+ }
3293
3093
  async execute(request, sink) {
3294
- const inputParts = await validateProviderUserInput(
3295
- request.provider,
3296
- request.run.input
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)
3297
3105
  );
3298
- const runtime = await createRuntime2(request, inputParts);
3299
3106
  sink.setRaw(runtime.raw);
3300
3107
  sink.emitEvent(
3301
3108
  createNormalizedEvent("run.started", {
@@ -3343,24 +3150,35 @@ var CodexAgentAdapter = class {
3343
3150
  if (text.trim().length > 0) {
3344
3151
  inputItems.push({ type: "text", text, text_elements: [] });
3345
3152
  }
3346
- pendingTurns++;
3347
- const sandboxPolicy = buildTurnSandboxPolicy(request.options);
3348
- await client.request("turn/start", {
3349
- threadId: rootThreadId,
3350
- input: inputItems,
3351
- approvalPolicy: isInteractiveApproval(request.options) ? "untrusted" : "never",
3352
- ...sandboxPolicy ? { sandboxPolicy } : {},
3353
- model: request.run.model ?? null,
3354
- effort: null,
3355
- outputSchema: null
3356
- });
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
+ };
3357
3164
  };
3358
3165
  sink.onMessage(sendTurn);
3166
+ const rawPayloads = [];
3359
3167
  const completion = new Promise((resolve, reject) => {
3360
3168
  let finalText = "";
3361
3169
  void (async () => {
3170
+ let firstClientMessageLogged = false;
3362
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
+ }
3363
3180
  const raw = toRawEvent2(request.runId, message, message.method);
3181
+ rawPayloads.push(message);
3364
3182
  sink.emitRaw(raw);
3365
3183
  if (message.method === "tool/requestUserInput" && message.id !== void 0) {
3366
3184
  reject(
@@ -3435,6 +3253,7 @@ var CodexAgentAdapter = class {
3435
3253
  client.bindThread(threadResponse.thread.id);
3436
3254
  }
3437
3255
  sink.setSessionId(threadResponse.thread.id);
3256
+ rawPayloads.push(threadResponse);
3438
3257
  sink.emitRaw(
3439
3258
  toRawEvent2(
3440
3259
  request.runId,
@@ -3442,30 +3261,107 @@ var CodexAgentAdapter = class {
3442
3261
  request.run.resumeSessionId ? "thread/resume:result" : "thread/start:result"
3443
3262
  )
3444
3263
  );
3445
- const sandboxPolicy = buildTurnSandboxPolicy(request.options);
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) => {
3446
3347
  await client.request("turn/start", {
3447
- threadId: threadResponse.thread.id,
3448
- input: runtime.inputItems,
3449
- approvalPolicy: isInteractiveApproval(request.options) ? "untrusted" : "never",
3450
- ...sandboxPolicy ? { sandboxPolicy } : {},
3451
- ...runtime.turnStartOverrides ?? {},
3452
- model: request.run.model ?? null,
3348
+ threadId,
3349
+ input: inputItems,
3350
+ approvalPolicy: "never",
3351
+ model: null,
3453
3352
  effort: null,
3454
3353
  outputSchema: null
3455
3354
  });
3456
- const { text } = await completion;
3457
- sink.complete({ text });
3458
- } finally {
3459
- await runtime.cleanup().catch(() => void 0);
3460
- }
3461
- return async () => void 0;
3355
+ });
3462
3356
  }
3463
3357
  };
3464
3358
 
3465
3359
  // src/agents/providers/opencode.ts
3466
3360
  import path10 from "path";
3467
3361
  var SANDBOX_OPENCODE_PORT = 4096;
3362
+ var LOCAL_OPENCODE_PORT = 4096;
3468
3363
  var SANDBOX_OPENCODE_READY_TIMEOUT_MS = 9e4;
3364
+ var LOCAL_OPENCODE_READY_TIMEOUT_MS = 2e4;
3469
3365
  var SHARED_OPENCODE_TARGET_ID = "shared-opencode-server";
3470
3366
  function toRawEvent3(runId, payload, type) {
3471
3367
  return {
@@ -3509,6 +3405,40 @@ function extractText(value) {
3509
3405
  }
3510
3406
  return "";
3511
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
+ }
3512
3442
  function toOpenCodeModel(model) {
3513
3443
  if (!model) {
3514
3444
  return void 0;
@@ -3566,156 +3496,182 @@ function createOpenCodePermissionEvent(request, raw, payload) {
3566
3496
  }
3567
3497
  );
3568
3498
  }
3569
- function buildOpenCodeConfig(request, interactiveApproval) {
3570
- const options = request.options;
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) {
3571
3504
  const mcpConfig = buildOpenCodeMcpConfig(options.mcps);
3572
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
+ );
3573
3524
  return {
3574
3525
  $schema: "https://opencode.ai/config.json",
3575
3526
  ...mcpConfig ? { mcp: mcpConfig } : {},
3576
3527
  ...commandsConfig ? { command: commandsConfig } : {},
3577
3528
  agent: {
3578
- agentbox: {
3579
- mode: "primary",
3580
- prompt: request.run.systemPrompt ?? "",
3581
- permission: buildOpenCodePermissionConfig(interactiveApproval),
3582
- tools: {
3583
- write: true,
3584
- edit: true,
3585
- bash: true,
3586
- webfetch: true,
3587
- skill: true
3588
- }
3589
- },
3529
+ agentbox: baseAgent,
3530
+ ...reasoningVariants,
3590
3531
  ...buildOpenCodeSubagentConfig(options.subAgents)
3591
3532
  }
3592
3533
  };
3593
3534
  }
3594
3535
  async function ensureSandboxOpenCodeServer(request) {
3595
- const sandbox = request.options.sandbox;
3596
- const options = request.options;
3597
- const port = SANDBOX_OPENCODE_PORT;
3598
- await sandbox.openPort(port);
3599
- const previewHeaders = sandbox.previewHeaders;
3600
- const healthCheck = await sandbox.run(
3601
- `curl -fsS http://127.0.0.1:${port}/global/health >/dev/null 2>&1`,
3602
- { cwd: options.cwd, timeoutMs: 5e3 }
3603
- );
3604
- if (healthCheck.exitCode === 0) {
3605
- const baseUrl2 = (await sandbox.getPreviewLink(port)).replace(/\/$/, "");
3606
- return {
3607
- baseUrl: baseUrl2,
3608
- previewHeaders,
3609
- cleanup: async () => {
3610
- },
3611
- raw: { baseUrl: baseUrl2, port, reused: true }
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"
3612
3580
  };
3613
- }
3614
- const plugins = assertHooksSupported(request.provider, options);
3615
- assertCommandsSupported(request.provider, options.commands);
3616
- const interactiveApproval = isInteractiveApproval(options);
3617
- const target = await createRuntimeTarget(
3618
- request.provider,
3619
- SHARED_OPENCODE_TARGET_ID,
3620
- options
3621
- );
3622
- const { artifacts: skillArtifacts, installCommands } = await prepareSkillArtifacts(
3623
- request.provider,
3624
- options.skills,
3625
- target.layout
3626
- );
3627
- const pluginArtifacts = buildOpenCodePluginArtifacts(
3628
- plugins,
3629
- target.layout.opencodeDir
3630
- );
3631
- for (const artifact of [...skillArtifacts, ...pluginArtifacts]) {
3632
- await target.writeArtifact(artifact);
3633
- }
3634
- const configPath = path10.join(target.layout.opencodeDir, "agentbox.json");
3635
- const openCodeConfig = buildOpenCodeConfig(request, interactiveApproval);
3636
- await target.writeArtifact({
3637
- path: configPath,
3638
- content: JSON.stringify(openCodeConfig, null, 2)
3639
- });
3640
- const commonEnv = {
3641
- OPENCODE_CONFIG: configPath,
3642
- OPENCODE_CONFIG_DIR: target.layout.opencodeDir,
3643
- OPENCODE_DISABLE_DEFAULT_PLUGINS: "true"
3644
- };
3645
- await installSkills(target, installCommands, commonEnv);
3646
- const binary = options.provider?.binary ?? "opencode";
3647
- const pidFilePath = path10.posix.join(
3648
- target.layout.rootDir,
3649
- "opencode-serve.pid"
3650
- );
3651
- const logFilePath = path10.posix.join(
3652
- target.layout.rootDir,
3653
- "opencode-serve.log"
3654
- );
3655
- const serveEnv = { ...options.env ?? {}, ...commonEnv };
3656
- const launchCommand = [
3657
- `mkdir -p ${shellQuote(target.layout.rootDir)}`,
3658
- `(${[
3659
- `nohup ${[
3660
- binary,
3661
- "serve",
3662
- "--hostname",
3663
- "0.0.0.0",
3664
- "--port",
3665
- String(port),
3666
- ...options.provider?.args ?? []
3667
- ].map(shellQuote).join(" ")} > ${shellQuote(logFilePath)} 2>&1 &`,
3668
- `echo $! > ${shellQuote(pidFilePath)}`
3669
- ].join(" ")})`
3670
- ].join(" && ");
3671
- const launchHandle = await sandbox.runAsync(launchCommand, {
3672
- cwd: options.cwd,
3673
- env: serveEnv
3674
- });
3675
- const launchResult = await launchHandle.wait();
3676
- if (launchResult.exitCode !== 0) {
3677
- await target.cleanup().catch(() => void 0);
3678
- throw new Error(
3679
- `Could not start OpenCode server: ${launchResult.combinedOutput || launchResult.stderr}`
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
3680
3592
  );
3681
- }
3682
- const readyDeadline = Date.now() + SANDBOX_OPENCODE_READY_TIMEOUT_MS;
3683
- let ready = false;
3684
- while (Date.now() < readyDeadline) {
3685
- const probe = await sandbox.run(
3686
- `curl -fsS http://127.0.0.1:${port}/global/health >/dev/null 2>&1`,
3687
- { cwd: options.cwd, timeoutMs: 5e3 }
3593
+ const binary = options.provider?.binary ?? "opencode";
3594
+ const pidFilePath = path10.posix.join(
3595
+ target.layout.rootDir,
3596
+ "opencode-serve.pid"
3688
3597
  );
3689
- if (probe.exitCode === 0) {
3690
- ready = true;
3691
- break;
3692
- }
3693
- await sleep(500);
3694
- }
3695
- if (!ready) {
3696
- await target.cleanup().catch(() => void 0);
3697
- throw new Error(
3698
- `OpenCode server did not become ready within ${SANDBOX_OPENCODE_READY_TIMEOUT_MS}ms.`
3598
+ const logFilePath = path10.posix.join(
3599
+ target.layout.rootDir,
3600
+ "opencode-serve.log"
3699
3601
  );
3700
- }
3701
- const baseUrl = (await sandbox.getPreviewLink(port)).replace(/\/$/, "");
3702
- return {
3703
- baseUrl,
3704
- previewHeaders,
3705
- cleanup: async () => {
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) {
3706
3630
  await target.cleanup().catch(() => void 0);
3707
- },
3708
- raw: { pidFilePath, logFilePath, baseUrl, layout: target.layout, port }
3709
- };
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
+ });
3710
3656
  }
3711
- async function createLocalRuntime2(request) {
3657
+ async function ensureLocalOpenCodeServer(request) {
3712
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
+ }
3713
3669
  const plugins = assertHooksSupported(request.provider, options);
3714
3670
  assertCommandsSupported(request.provider, options.commands);
3715
3671
  const interactiveApproval = isInteractiveApproval(options);
3716
- const target = await createRuntimeTarget(
3672
+ const target = await createSetupTarget(
3717
3673
  request.provider,
3718
- request.runId,
3674
+ "shared-setup",
3719
3675
  options
3720
3676
  );
3721
3677
  const { artifacts: skillArtifacts, installCommands } = await prepareSkillArtifacts(
@@ -3727,30 +3683,37 @@ async function createLocalRuntime2(request) {
3727
3683
  plugins,
3728
3684
  target.layout.opencodeDir
3729
3685
  );
3730
- for (const artifact of [...skillArtifacts, ...pluginArtifacts]) {
3731
- await target.writeArtifact(artifact);
3732
- }
3733
3686
  const configPath = path10.join(target.layout.opencodeDir, "agentbox.json");
3734
- const openCodeConfig = buildOpenCodeConfig(request, interactiveApproval);
3735
- await target.writeArtifact({
3736
- path: configPath,
3737
- content: JSON.stringify(openCodeConfig, null, 2)
3738
- });
3687
+ const openCodeConfig = buildOpenCodeConfig(
3688
+ options,
3689
+ request.config.systemPrompt ?? "",
3690
+ interactiveApproval
3691
+ );
3739
3692
  const commonEnv = {
3740
3693
  OPENCODE_CONFIG: configPath,
3741
3694
  OPENCODE_CONFIG_DIR: target.layout.opencodeDir,
3742
3695
  OPENCODE_DISABLE_DEFAULT_PLUGINS: "true"
3743
3696
  };
3744
- await installSkills(target, installCommands, commonEnv);
3745
- const hostPort = await getAvailablePort();
3746
- const processHandle = spawnCommand({
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({
3747
3710
  command: options.provider?.binary ?? "opencode",
3748
3711
  args: [
3749
3712
  "serve",
3750
3713
  "--hostname",
3751
3714
  "127.0.0.1",
3752
3715
  "--port",
3753
- String(hostPort),
3716
+ String(LOCAL_OPENCODE_PORT),
3754
3717
  ...options.provider?.args ?? []
3755
3718
  ],
3756
3719
  cwd: options.cwd,
@@ -3760,33 +3723,53 @@ async function createLocalRuntime2(request) {
3760
3723
  ...commonEnv
3761
3724
  }
3762
3725
  });
3763
- const baseUrl = `http://127.0.0.1:${hostPort}`;
3764
- await waitForHttpReady(`${baseUrl}/global/health`, { timeoutMs: 2e4 });
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}`;
3765
3749
  return {
3766
3750
  baseUrl,
3767
3751
  previewHeaders: {},
3768
- cleanup: async () => {
3769
- await processHandle.kill();
3770
- await target.cleanup();
3771
- },
3772
- raw: { processHandle, layout: target.layout }
3752
+ raw: { baseUrl, port: LOCAL_OPENCODE_PORT }
3773
3753
  };
3774
3754
  }
3775
- async function createRuntime3(request) {
3776
- if (request.options.sandbox) {
3777
- return ensureSandboxOpenCodeServer(request);
3778
- }
3779
- return createLocalRuntime2(request);
3780
- }
3781
3755
  var OpenCodeAgentAdapter = class {
3756
+ async setup(request) {
3757
+ await setupOpenCode(request);
3758
+ }
3782
3759
  async execute(request, sink) {
3783
- const inputParts = await validateProviderUserInput(
3784
- request.provider,
3785
- request.run.input
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)
3786
3766
  );
3787
3767
  let pendingMessages = 0;
3788
3768
  let finalText = "";
3769
+ let streamedTextFromSse = "";
3770
+ const settledMessageIds = /* @__PURE__ */ new Set();
3789
3771
  let dispatchError;
3772
+ let firstSseEventLogged = false;
3790
3773
  let resolveAllDone;
3791
3774
  const allDone = new Promise((resolve) => {
3792
3775
  resolveAllDone = resolve;
@@ -3801,7 +3784,10 @@ var OpenCodeAgentAdapter = class {
3801
3784
  sink.onMessage(async (content) => {
3802
3785
  pendingMessages++;
3803
3786
  try {
3804
- const parts = await validateProviderUserInput(request.provider, content);
3787
+ const parts = await validateProviderUserInput(
3788
+ request.provider,
3789
+ content
3790
+ );
3805
3791
  const mapped = mapToOpenCodeParts(parts);
3806
3792
  if (sendToSession) {
3807
3793
  sendToSession(mapped);
@@ -3817,7 +3803,11 @@ var OpenCodeAgentAdapter = class {
3817
3803
  throw error;
3818
3804
  }
3819
3805
  });
3820
- const runtime = await createRuntime3(request);
3806
+ const runtime = await time(
3807
+ debugOpencode,
3808
+ "buildOpenCodeRuntime",
3809
+ () => buildOpenCodeRuntime(request.options)
3810
+ );
3821
3811
  sink.setRaw(runtime.raw);
3822
3812
  sink.emitEvent(
3823
3813
  createNormalizedEvent("run.started", {
@@ -3825,6 +3815,7 @@ var OpenCodeAgentAdapter = class {
3825
3815
  runId: request.runId
3826
3816
  })
3827
3817
  );
3818
+ const rawPayloads = [];
3828
3819
  const sseAbort = new AbortController();
3829
3820
  let sseTask;
3830
3821
  const dispatchAbort = new AbortController();
@@ -3846,9 +3837,7 @@ var OpenCodeAgentAdapter = class {
3846
3837
  ),
3847
3838
  new Promise(
3848
3839
  (_, reject) => setTimeout(
3849
- () => reject(
3850
- new Error("opencode POST /session/abort timed out")
3851
- ),
3840
+ () => reject(new Error("opencode POST /session/abort timed out")),
3852
3841
  3e3
3853
3842
  )
3854
3843
  )
@@ -3877,12 +3866,22 @@ var OpenCodeAgentAdapter = class {
3877
3866
  if (!sessionId) {
3878
3867
  throw new Error("OpenCode did not return a session id.");
3879
3868
  }
3869
+ const announcedUserMessageIds = /* @__PURE__ */ new Set();
3870
+ const foreignMessageIds = /* @__PURE__ */ new Set();
3880
3871
  sseTask = (async () => {
3881
3872
  try {
3882
3873
  for await (const event of streamSse(`${runtime.baseUrl}/event`, {
3883
3874
  headers: runtime.previewHeaders,
3884
3875
  signal: sseAbort.signal
3885
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
+ }
3886
3885
  let payload = event.data;
3887
3886
  try {
3888
3887
  payload = JSON.parse(event.data);
@@ -3893,8 +3892,33 @@ var OpenCodeAgentAdapter = class {
3893
3892
  payload,
3894
3893
  `sse:${event.event ?? "message"}`
3895
3894
  );
3895
+ if (payload && typeof payload === "object" && !Array.isArray(payload)) {
3896
+ rawPayloads.push(payload);
3897
+ }
3896
3898
  sink.emitRaw(raw);
3897
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
+ }
3898
3922
  if (eventType === "permission.asked") {
3899
3923
  const properties = payload.properties;
3900
3924
  if (properties && typeof properties.sessionID === "string" && properties.sessionID === sessionId) {
@@ -3923,8 +3947,61 @@ var OpenCodeAgentAdapter = class {
3923
3947
  }
3924
3948
  continue;
3925
3949
  }
3926
- for (const normalized of normalizeRawAgentEvent(raw)) {
3927
- sink.emitEvent(normalized);
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
+ }
3928
4005
  }
3929
4006
  }
3930
4007
  } catch {
@@ -3939,13 +4016,18 @@ var OpenCodeAgentAdapter = class {
3939
4016
  request.run.resumeSessionId ? "session.resumed" : "session.created"
3940
4017
  )
3941
4018
  );
4019
+ if (createdSession) {
4020
+ rawPayloads.push(createdSession);
4021
+ }
3942
4022
  sink.emitEvent(
3943
4023
  createNormalizedEvent("message.started", {
3944
4024
  provider: request.provider,
3945
4025
  runId: request.runId
3946
4026
  })
3947
4027
  );
4028
+ const agentSlug = openCodeAgentSlug(request.run.reasoning);
3948
4029
  const dispatchMessage = async (parts) => {
4030
+ const sseTextLengthBeforeDispatch = streamedTextFromSse.length;
3949
4031
  try {
3950
4032
  const response = await fetchJson(
3951
4033
  `${runtime.baseUrl}/session/${sessionId}/message`,
@@ -3958,7 +4040,7 @@ var OpenCodeAgentAdapter = class {
3958
4040
  },
3959
4041
  body: JSON.stringify({
3960
4042
  ...request.run.model ? { model: toOpenCodeModel(request.run.model) } : {},
3961
- agent: "agentbox",
4043
+ agent: agentSlug,
3962
4044
  parts
3963
4045
  })
3964
4046
  }
@@ -3968,24 +4050,59 @@ var OpenCodeAgentAdapter = class {
3968
4050
  response,
3969
4051
  "message.response"
3970
4052
  );
4053
+ if (response && typeof response === "object" && !Array.isArray(response)) {
4054
+ rawPayloads.push(response);
4055
+ }
3971
4056
  sink.emitRaw(rawResponse);
3972
4057
  for (const event of normalizeRawAgentEvent(rawResponse)) {
3973
4058
  sink.emitEvent(event);
3974
4059
  }
3975
- const text = extractText(response);
3976
- if (text) {
3977
- finalText = text;
4060
+ const reasoning = extractReasoning(response);
4061
+ if (reasoning) {
3978
4062
  sink.emitEvent(
3979
4063
  createNormalizedEvent(
3980
- "text.delta",
4064
+ "reasoning.delta",
3981
4065
  {
3982
4066
  provider: request.provider,
3983
- runId: request.runId
4067
+ runId: request.runId,
4068
+ raw: rawResponse
3984
4069
  },
3985
- { delta: text }
4070
+ { delta: reasoning }
3986
4071
  )
3987
4072
  );
3988
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
+ }
3989
4106
  } catch (error) {
3990
4107
  if (!dispatchError) {
3991
4108
  dispatchError = error;
@@ -4004,9 +4121,14 @@ var OpenCodeAgentAdapter = class {
4004
4121
  pendingMessages++;
4005
4122
  void dispatchMessage(mapToOpenCodeParts(inputParts));
4006
4123
  await allDone;
4007
- if (dispatchError) {
4124
+ if (dispatchError && !dispatchAbort.signal.aborted) {
4008
4125
  throw dispatchError;
4009
4126
  }
4127
+ debugOpencode(
4128
+ "\u2605 run.completed (%dms since execute start) chars=%d",
4129
+ Date.now() - executeStartedAt,
4130
+ finalText.length
4131
+ );
4010
4132
  sink.emitEvent(
4011
4133
  createNormalizedEvent(
4012
4134
  "run.completed",
@@ -4019,16 +4141,86 @@ var OpenCodeAgentAdapter = class {
4019
4141
  );
4020
4142
  sseAbort.abort();
4021
4143
  await sseTask;
4022
- sink.complete({ text: finalText });
4144
+ sink.complete({
4145
+ text: finalText,
4146
+ costData: extractOpenCodeCostData(rawPayloads)
4147
+ });
4023
4148
  } finally {
4024
4149
  sseAbort.abort();
4025
4150
  if (sseTask) {
4026
4151
  await sseTask.catch(() => void 0);
4027
4152
  }
4028
- await runtime.cleanup().catch(() => void 0);
4029
4153
  }
4030
4154
  return async () => void 0;
4031
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
+ }
4032
4224
  };
4033
4225
 
4034
4226
  // src/agents/Agent.ts
@@ -4092,11 +4284,7 @@ function createAdapter(provider) {
4092
4284
  throw new UnsupportedProviderError("agent", provider);
4093
4285
  }
4094
4286
  }
4095
- function prepareAgentOptions(provider, options) {
4096
- const ports = AGENT_RESERVED_PORTS[provider] ?? [];
4097
- for (const port of ports) {
4098
- void options.sandbox?.openPort(port);
4099
- }
4287
+ function prepareAgentOptions(_provider, options) {
4100
4288
  return options;
4101
4289
  }
4102
4290
  var AgentRunController = class {
@@ -4114,6 +4302,7 @@ var AgentRunController = class {
4114
4302
  pendingPermissions = /* @__PURE__ */ new Map();
4115
4303
  messageHandler;
4116
4304
  text = "";
4305
+ costData = null;
4117
4306
  settled = false;
4118
4307
  finished;
4119
4308
  resolveSessionIdReady;
@@ -4205,14 +4394,18 @@ var AgentRunController = class {
4205
4394
  );
4206
4395
  }
4207
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;
4208
4399
  this.pushEvent(
4209
4400
  createNormalizedEvent(
4210
4401
  "message.injected",
4211
4402
  { provider: this.provider, runId: this.id },
4212
- { content: textContent || "(non-text content)" }
4403
+ {
4404
+ content: textContent || "(non-text content)",
4405
+ ...messageId ? { messageId } : {}
4406
+ }
4213
4407
  )
4214
4408
  );
4215
- await this.messageHandler(content);
4216
4409
  }
4217
4410
  async respondToPermission(response) {
4218
4411
  const pending = this.pendingPermissions.get(response.requestId);
@@ -4263,6 +4456,9 @@ var AgentRunController = class {
4263
4456
  if (result?.text) {
4264
4457
  this.text = result.text;
4265
4458
  }
4459
+ if (result && "costData" in result) {
4460
+ this.costData = result.costData ?? null;
4461
+ }
4266
4462
  this.eventQueue.finish();
4267
4463
  this.rawQueue.finish();
4268
4464
  if (!this.sessionId) {
@@ -4279,7 +4475,8 @@ var AgentRunController = class {
4279
4475
  sessionId: this.sessionId,
4280
4476
  text: this.text,
4281
4477
  rawEvents: [...this.rawEventsList],
4282
- events: [...this.events]
4478
+ events: [...this.events],
4479
+ costData: this.costData
4283
4480
  });
4284
4481
  }
4285
4482
  fail(error) {
@@ -4326,23 +4523,93 @@ var Agent = class {
4326
4523
  adapter;
4327
4524
  provider;
4328
4525
  options;
4526
+ setupPromise;
4329
4527
  constructor(provider, options) {
4330
4528
  this.provider = provider;
4331
4529
  this.options = prepareAgentOptions(provider, options);
4332
4530
  this.adapter = createAdapter(provider);
4333
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
+ }
4334
4587
  stream(runConfig) {
4335
- const runId = randomUUID3();
4588
+ const runId = runConfig.runId ?? randomUUID2();
4589
+ const streamCalledAt = Date.now();
4590
+ debugAgent("stream() provider=%s runId=%s", this.provider, runId);
4336
4591
  const run = new AgentRunController(this.provider, runId);
4337
- const request = {
4338
- runId,
4339
- provider: this.provider,
4340
- options: this.options,
4341
- run: buildRunConfig(this.options, runConfig)
4342
- };
4592
+ const setupPromise = this.setupPromise;
4593
+ const provider = this.provider;
4594
+ const options = this.options;
4595
+ const adapter = this.adapter;
4343
4596
  void (async () => {
4344
4597
  try {
4345
- const cleanup = await this.adapter.execute(request, run);
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
+ );
4346
4613
  run.setAbort(async () => {
4347
4614
  await cleanup();
4348
4615
  });
@@ -4358,6 +4625,32 @@ var Agent = class {
4358
4625
  rawEvents(runConfig) {
4359
4626
  return this.stream(runConfig).rawEvents();
4360
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
+ }
4361
4654
  };
4362
4655
 
4363
4656
  export {