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.
- package/README.md +43 -4
- package/dist/{Sandbox-BQX-sWzs.d.ts → Sandbox-CByFJI8X.d.ts} +56 -3
- package/dist/agents/index.d.ts +54 -5
- package/dist/agents/index.js +4 -4
- package/dist/{chunk-G27423WX.js → chunk-4MBB6QHD.js} +2118 -1825
- package/dist/{chunk-2NKMDGYH.js → chunk-GOFJNFAD.js} +1 -1
- package/dist/{chunk-O7HCJXKW.js → chunk-INMA52FV.js} +56 -23
- package/dist/{chunk-X7AWPYDK.js → chunk-LPKKT6YT.js} +351 -47
- package/dist/chunk-ZOWBRUQR.js +476 -0
- package/dist/cli.js +1 -1
- package/dist/enums.d.ts +1 -1
- package/dist/enums.js +1 -1
- package/dist/events/index.d.ts +85 -3
- package/dist/events/index.js +4 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +7 -5
- package/dist/sandboxes/index.d.ts +48 -4
- package/dist/sandboxes/index.js +3 -3
- package/dist/{types-Et22oPap.d.ts → types-B3N-Qo2q.d.ts} +145 -5
- package/images/browser-agent.mjs +3 -3
- package/package.json +5 -2
- package/dist/chunk-7FLLQJ6J.js +0 -185
|
@@ -2,31 +2,37 @@ import {
|
|
|
2
2
|
createNormalizedEvent,
|
|
3
3
|
normalizeRawAgentEvent,
|
|
4
4
|
toAISDKStream
|
|
5
|
-
} from "./chunk-
|
|
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
|
-
|
|
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-
|
|
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-
|
|
29
|
+
} from "./chunk-GOFJNFAD.js";
|
|
24
30
|
|
|
25
31
|
// src/agents/Agent.ts
|
|
26
|
-
import { randomUUID as
|
|
32
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
27
33
|
|
|
28
34
|
// src/agents/providers/claude-code.ts
|
|
29
|
-
import { randomUUID
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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/
|
|
825
|
-
import {
|
|
835
|
+
// src/agents/config/setup.ts
|
|
836
|
+
import { mkdir, chmod, rm, writeFile } from "fs/promises";
|
|
826
837
|
import os from "os";
|
|
827
|
-
import
|
|
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/
|
|
881
|
-
function
|
|
882
|
-
const
|
|
883
|
-
|
|
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
|
|
886
|
-
homeDir,
|
|
902
|
+
rootDir,
|
|
903
|
+
homeDir: rootDir,
|
|
887
904
|
xdgConfigHome,
|
|
888
|
-
agentsDir:
|
|
889
|
-
claudeDir:
|
|
890
|
-
opencodeDir:
|
|
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
|
-
|
|
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
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
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
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
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
|
|
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
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
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
|
-
|
|
1025
|
+
"SandboxSetupTarget.uploadAndRun called without a sandbox."
|
|
982
1026
|
);
|
|
983
1027
|
}
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
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
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
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
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
layout,
|
|
1032
|
-
|
|
1033
|
-
|
|
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.
|
|
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
|
|
1277
|
+
function yamlScalar(value) {
|
|
1278
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
1279
|
+
}
|
|
1280
|
+
function buildClaudeSubagentArtifacts(subAgents, layout) {
|
|
1096
1281
|
if (!subAgents || subAgents.length === 0) {
|
|
1097
|
-
return
|
|
1282
|
+
return [];
|
|
1098
1283
|
}
|
|
1099
|
-
return
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
{
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
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/
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
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
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
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
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
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
|
-
|
|
1280
|
-
|
|
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
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
const
|
|
1323
|
-
|
|
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
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
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
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
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
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
};
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
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
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
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
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
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
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
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
|
-
|
|
1490
|
-
|
|
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
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
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
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
return
|
|
1537
|
-
|
|
1538
|
-
|
|
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 (
|
|
1545
|
-
|
|
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
|
|
1548
|
-
return
|
|
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
|
|
1553
|
-
const
|
|
1554
|
-
return
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
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
|
-
|
|
1572
|
-
|
|
1573
|
-
const
|
|
1574
|
-
|
|
1575
|
-
|
|
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
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
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
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
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
|
|
1675
|
-
|
|
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
|
|
1694
|
-
|
|
1695
|
-
|
|
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
|
|
1699
|
-
|
|
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
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
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
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
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
|
-
|
|
1730
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
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
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
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
|
|
1761
|
-
const
|
|
1762
|
-
if (!
|
|
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
|
-
|
|
1766
|
-
|
|
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
|
|
1771
|
-
|
|
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
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
}
|
|
1778
|
-
}
|
|
1740
|
+
await run.query.interrupt().catch(() => {});
|
|
1741
|
+
res.writeHead(204);
|
|
1742
|
+
res.end();
|
|
1779
1743
|
}
|
|
1780
1744
|
|
|
1781
|
-
function
|
|
1782
|
-
const
|
|
1783
|
-
if (
|
|
1784
|
-
|
|
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
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
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
|
-
|
|
1936
|
-
if (
|
|
1937
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1964
|
-
|
|
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
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1789
|
+
for (const r of liveRuns.values()) {
|
|
1790
|
+
r.prompt.end();
|
|
1791
|
+
r.query.interrupt().catch(() => {});
|
|
1973
1792
|
}
|
|
1974
|
-
server.close(
|
|
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
|
-
|
|
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
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
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
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
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
|
-
|
|
2005
|
-
|
|
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()
|
|
2008
|
-
const client = new SharedSdkWsConnection(url, headers);
|
|
1874
|
+
while (Date.now() < deadline) {
|
|
2009
1875
|
try {
|
|
2010
|
-
await
|
|
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
|
-
|
|
2022
|
-
|
|
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(
|
|
1886
|
+
throw lastError ?? new Error("claude-code daemon request timed out");
|
|
2026
1887
|
}
|
|
2027
|
-
async function
|
|
2028
|
-
const
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2077
|
-
const
|
|
2078
|
-
|
|
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
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
{
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
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
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
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
|
|
2219
|
-
|
|
2220
|
-
|
|
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
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
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
|
|
2229
|
-
|
|
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
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
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
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
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
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
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
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
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
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
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
|
-
"
|
|
2351
|
-
{
|
|
2352
|
-
|
|
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 (
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
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
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
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
|
-
|
|
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({
|
|
2204
|
+
sink.complete({
|
|
2205
|
+
text: finalText,
|
|
2206
|
+
costData: extractClaudeCostData(rawPayloads)
|
|
2207
|
+
});
|
|
2435
2208
|
} finally {
|
|
2436
|
-
|
|
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
|
|
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
|
|
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 ===
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
2744
|
+
root,
|
|
2891
2745
|
"inputs",
|
|
2892
2746
|
`codex-image-${index}${codexImageExtension(part.mediaType)}`
|
|
2893
2747
|
);
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
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
|
|
2909
|
-
return
|
|
2766
|
+
function resolveCodexOpenAiBaseUrlFromOptions(options) {
|
|
2767
|
+
return options.env?.OPENAI_BASE_URL ?? options.provider?.env?.OPENAI_BASE_URL;
|
|
2910
2768
|
}
|
|
2911
|
-
async function
|
|
2912
|
-
const
|
|
2913
|
-
const
|
|
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
|
-
|
|
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
|
|
2932
|
-
const
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
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
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
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
|
|
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
|
-
|
|
2963
|
-
|
|
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
|
|
2967
|
-
|
|
2968
|
-
request.provider,
|
|
2879
|
+
const sharedTarget = await createSetupTarget(
|
|
2880
|
+
provider,
|
|
2969
2881
|
REMOTE_CODEX_APP_SERVER_ID,
|
|
2970
2882
|
options
|
|
2971
2883
|
);
|
|
2972
|
-
await
|
|
2973
|
-
const
|
|
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
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2890
|
+
await time(
|
|
2891
|
+
debugCodex,
|
|
2892
|
+
"ensureCodexLogin",
|
|
2893
|
+
() => ensureCodexLoginViaConfig(request, sharedTarget)
|
|
2981
2894
|
);
|
|
2982
|
-
const {
|
|
2983
|
-
|
|
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
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
`
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
"
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
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
|
-
|
|
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
|
|
3125
|
-
request.provider,
|
|
3126
|
-
request.runId,
|
|
3127
|
-
options
|
|
3128
|
-
);
|
|
2951
|
+
const target = await createSetupTarget(provider, "shared-setup", options);
|
|
3129
2952
|
try {
|
|
3130
|
-
await
|
|
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
|
-
|
|
2971
|
+
CODEX_HOME: codexDir,
|
|
3138
2972
|
...options.provider?.env ?? {}
|
|
3139
2973
|
});
|
|
3140
|
-
const runtimeCwd =
|
|
3141
|
-
const
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
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
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
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
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
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
|
-
|
|
3227
|
-
|
|
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,
|
|
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:
|
|
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,
|
|
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
|
|
3295
|
-
|
|
3296
|
-
|
|
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
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
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
|
-
|
|
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
|
|
3448
|
-
input:
|
|
3449
|
-
approvalPolicy:
|
|
3450
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3570
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
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
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
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
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
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
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
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
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
3672
|
+
const target = await createSetupTarget(
|
|
3717
3673
|
request.provider,
|
|
3718
|
-
|
|
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(
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
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
|
|
3745
|
-
|
|
3746
|
-
|
|
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(
|
|
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
|
-
|
|
3764
|
-
|
|
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
|
-
|
|
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
|
|
3784
|
-
|
|
3785
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
3927
|
-
|
|
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:
|
|
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
|
|
3976
|
-
if (
|
|
3977
|
-
finalText = text;
|
|
4060
|
+
const reasoning = extractReasoning(response);
|
|
4061
|
+
if (reasoning) {
|
|
3978
4062
|
sink.emitEvent(
|
|
3979
4063
|
createNormalizedEvent(
|
|
3980
|
-
"
|
|
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:
|
|
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({
|
|
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(
|
|
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
|
-
{
|
|
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 =
|
|
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
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
|
|
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
|
-
|
|
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 {
|