agents 0.26.0 → 0.27.0
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 +8 -9
- package/dist/{agent-routing-0DR0Tk2v.d.ts → agent-routing-Bhdbvf7s.d.ts} +13 -8
- package/dist/agent-routing.d.ts +1 -1
- package/dist/agent-tool-types.d.ts +1 -1
- package/dist/{agent-tools-YrATZJu2.d.ts → agent-tools-bJZwBMfq.d.ts} +2 -2
- package/dist/agent-tools.d.ts +1 -1
- package/dist/ai-sdk-OoGkFy3s.js +2444 -0
- package/dist/ai-sdk-OoGkFy3s.js.map +1 -0
- package/dist/apply-chunks-1FheRb9W.js +99 -0
- package/dist/apply-chunks-1FheRb9W.js.map +1 -0
- package/dist/broadcast-state-CEyI4uAK.js +69 -0
- package/dist/broadcast-state-CEyI4uAK.js.map +1 -0
- package/dist/browser/ai-sdk.d.ts +1 -1
- package/dist/browser/ai-sdk.js +2 -2
- package/dist/browser/ai-sdk.js.map +1 -1
- package/dist/browser/index.d.ts +1 -1
- package/dist/browser/tanstack-ai.d.ts +1 -1
- package/dist/browser/tanstack-ai.js +1 -1
- package/dist/{browser-DE3d-RLk.d.ts → browser-tmkcAAI3.d.ts} +2 -2
- package/dist/{browser-tool-DuNdlwZ9.d.ts → browser-tool-Bn-nzJQI.d.ts} +3 -3
- package/dist/{browser-tool-CQ10e6Kj.js → browser-tool-CBqs6hqa.js} +58 -18
- package/dist/browser-tool-CBqs6hqa.js.map +1 -0
- package/dist/capability-B4WbF81e.js.map +1 -1
- package/dist/{capability-runner-DBAnGr6L.d.ts → capability-runner-O5VbQzqg.d.ts} +9 -1
- package/dist/channel-Bak_6zDP.d.ts +333 -0
- package/dist/chat/index.d.ts +11 -9
- package/dist/chat/index.js +5 -194
- package/dist/chat/index.js.map +1 -1
- package/dist/chat/react.d.ts +2 -4
- package/dist/chat/react.js +3 -1
- package/dist/chat/react.js.map +1 -1
- package/dist/chat-sdk/index.d.ts +1 -1
- package/dist/chat-sdk/index.js +1 -1
- package/dist/cli.js +144 -0
- package/dist/{client-fyQrLGkD.js → client-1tEQ1Oqz.js} +27 -2
- package/dist/{client-fyQrLGkD.js.map → client-1tEQ1Oqz.js.map} +1 -1
- package/dist/{client-je3koD9Z.d.ts → client-DS3Dz9l0.d.ts} +2 -2
- package/dist/client-tools-aIBO0Fk7.d.ts +53 -0
- package/dist/client.d.ts +1 -1
- package/dist/conversations-DH0ut_8n.d.ts +192 -0
- package/dist/{current-agent-vAk_5JMt.d.ts → current-agent-C07C5o5Y.d.ts} +3 -3
- package/dist/daemon-core-Buubj-y1.js +481 -0
- package/dist/daemon-core-Buubj-y1.js.map +1 -0
- package/dist/daemon-core-CbohVPAu.d.ts +132 -0
- package/dist/{do-oauth-client-provider-Tmf1vgKz.d.ts → do-oauth-client-provider-DfsUcpc3.d.ts} +7 -1
- package/dist/{durable-object-lifecycle-HhgOMWq9.d.ts → durable-object-lifecycle-DRqp475_.d.ts} +2 -2
- package/dist/{channels/email.d.ts → experimental/channels/email/index.d.ts} +25 -12
- package/dist/{channels/email.js → experimental/channels/email/index.js} +13 -35
- package/dist/experimental/channels/email/index.js.map +1 -0
- package/dist/experimental/channels/index.d.ts +270 -0
- package/dist/experimental/channels/index.js +1186 -0
- package/dist/experimental/channels/index.js.map +1 -0
- package/dist/{channels/slack.d.ts → experimental/channels/slack/index.d.ts} +19 -9
- package/dist/experimental/channels/slack/index.js +267 -0
- package/dist/experimental/channels/slack/index.js.map +1 -0
- package/dist/{channels/telegram.d.ts → experimental/channels/telegram/index.d.ts} +19 -9
- package/dist/experimental/channels/telegram/index.js +198 -0
- package/dist/experimental/channels/telegram/index.js.map +1 -0
- package/dist/experimental/channels/web/ai-sdk.d.ts +55 -0
- package/dist/experimental/channels/web/ai-sdk.js +266 -0
- package/dist/experimental/channels/web/ai-sdk.js.map +1 -0
- package/dist/experimental/channels/web/client.d.ts +97 -0
- package/dist/experimental/channels/web/client.js +349 -0
- package/dist/experimental/channels/web/client.js.map +1 -0
- package/dist/experimental/channels/web/index.d.ts +189 -0
- package/dist/experimental/channels/web/index.js +332 -0
- package/dist/experimental/channels/web/index.js.map +1 -0
- package/dist/gateway-BDC07gYO.d.ts +100 -0
- package/dist/handle-zAzh0UW2.d.ts +460 -0
- package/dist/harness/ai-sdk/index.d.ts +175 -0
- package/dist/harness/ai-sdk/index.js +605 -0
- package/dist/harness/ai-sdk/index.js.map +1 -0
- package/dist/harness/container/daemon/index.d.ts +76 -0
- package/dist/harness/container/daemon/index.js +3 -0
- package/dist/harness/container/index.d.ts +740 -0
- package/dist/harness/container/index.js +2152 -0
- package/dist/harness/container/index.js.map +1 -0
- package/dist/harness/container/runtime/index.d.ts +144 -0
- package/dist/harness/container/runtime/index.js +1082 -0
- package/dist/harness/container/runtime/index.js.map +1 -0
- package/dist/harness/opencode/index.d.ts +369 -0
- package/dist/harness/opencode/index.js +1072 -0
- package/dist/harness/opencode/index.js.map +1 -0
- package/dist/harness/pi/index.d.ts +2 -1
- package/dist/harness/store/index.d.ts +24 -0
- package/dist/harness/store/index.js +2 -0
- package/dist/harness/think/index.d.ts +821 -0
- package/dist/harness/think/index.js +2529 -0
- package/dist/harness/think/index.js.map +1 -0
- package/dist/harness-B6B9S3rb.d.ts +203 -0
- package/dist/{images-iiTDSyRu.js → images-Ce9SCu3X.js} +2 -248
- package/dist/images-Ce9SCu3X.js.map +1 -0
- package/dist/{index-BreL0YrQ.d.ts → index-D0A5tbWu.d.ts} +2 -2
- package/dist/index-Ib_4dwOo.d.ts +200 -0
- package/dist/index.d.ts +5 -5
- package/dist/index.js +2 -2
- package/dist/ingress-EFbrS547.js +9 -0
- package/dist/ingress-EFbrS547.js.map +1 -0
- package/dist/input-schema-DNrpTu49.js +23 -0
- package/dist/input-schema-DNrpTu49.js.map +1 -0
- package/dist/internal-kmw70xwR.js +46 -0
- package/dist/internal-kmw70xwR.js.map +1 -0
- package/dist/internal_context.d.ts +1 -1
- package/dist/lifecycle/index.d.ts +3 -3
- package/dist/mcp/client/do-oauth-client-provider.d.ts +1 -1
- package/dist/mcp/client/do-oauth-client-provider.js +9 -0
- package/dist/mcp/client/do-oauth-client-provider.js.map +1 -1
- package/dist/mcp/client/index.d.ts +1 -1
- package/dist/mcp/client/index.js +1 -1
- package/dist/mcp/client/x402.d.ts +35 -4
- package/dist/mcp/client/x402.js +24 -3
- package/dist/mcp/client/x402.js.map +1 -1
- package/dist/mcp/index.d.ts +1 -1
- package/dist/mcp/index.js +2 -2
- package/dist/models/ai-sdk/index.d.ts +16 -14
- package/dist/models/ai-sdk/index.js +1 -2442
- package/dist/models/opencode/index.d.ts +65 -0
- package/dist/models/opencode/index.js +285 -0
- package/dist/models/opencode/index.js.map +1 -0
- package/dist/models/pi-ai/index.d.ts +9 -7
- package/dist/models/pi-ai/index.js +2 -1
- package/dist/models/pi-ai/index.js.map +1 -1
- package/dist/options-pwTNq9z6.d.ts +134 -0
- package/dist/protocol-BEgrJYC5.d.ts +331 -0
- package/dist/protocol-CukuI73M.d.ts +386 -0
- package/dist/protocol-cOklVBBD.js +227 -0
- package/dist/protocol-cOklVBBD.js.map +1 -0
- package/dist/{protocol-kHsr-f0X.js → protocol-eXeJxhgu.js} +2 -67
- package/dist/protocol-eXeJxhgu.js.map +1 -0
- package/dist/protocol-y2sSY584.js +148 -0
- package/dist/protocol-y2sSY584.js.map +1 -0
- package/dist/queue/index.d.ts +1 -1
- package/dist/react.d.ts +2 -2
- package/dist/routing/index.d.ts +2 -2
- package/dist/{scheduler-RdACNXDP.d.ts → scheduler-CrFCnv_J.d.ts} +2 -2
- package/dist/schedules/index.d.ts +1 -1
- package/dist/serializable.d.ts +1 -1
- package/dist/sessions/index.d.ts +11 -434
- package/dist/sessions/index.js +1 -2271
- package/dist/sessions-B9pNuxdT.js +2272 -0
- package/dist/sessions-B9pNuxdT.js.map +1 -0
- package/dist/settings-C45hDliX.js +253 -0
- package/dist/settings-C45hDliX.js.map +1 -0
- package/dist/{transport-PfFnt-Ld.d.ts → settings-NgGwF7Qt.d.ts} +14 -208
- package/dist/{src-BZeu-SCD.js → src-DgCbZtIR.js} +4 -4
- package/dist/{src-BZeu-SCD.js.map → src-DgCbZtIR.js.map} +1 -1
- package/dist/stall-watchdog-BpRF2uwG.js +195 -0
- package/dist/stall-watchdog-BpRF2uwG.js.map +1 -0
- package/dist/state/index.d.ts +1 -1
- package/dist/store-DLeqKuj3.d.ts +310 -0
- package/dist/store-vgcPYo8K.js +432 -0
- package/dist/store-vgcPYo8K.js.map +1 -0
- package/dist/streams/index.d.ts +1 -1
- package/dist/streams/index.js +1 -1
- package/dist/{streams-DW-UCOUc.js → streams-CITClVer.js} +16 -3
- package/dist/streams-CITClVer.js.map +1 -0
- package/dist/{streams-Cmkj9QfW.d.ts → streams-fbG61qM8.d.ts} +2 -2
- package/dist/sub-routing.d.ts +1 -1
- package/dist/tasks/index.d.ts +1 -1
- package/dist/tasks/index.js +1 -1
- package/dist/{tasks-68DESxf0.js → tasks-CkTZNaV1.js} +2 -1
- package/dist/tasks-CkTZNaV1.js.map +1 -0
- package/dist/{tasks-9YxYtByh.d.ts → tasks-PtlB4Uyn.d.ts} +2 -2
- package/dist/tool-3PA8xjB_.js +421 -0
- package/dist/tool-3PA8xjB_.js.map +1 -0
- package/dist/tool-BPJEbwaX.d.ts +278 -0
- package/dist/transport-k2mFrtL8.d.ts +200 -0
- package/dist/turns-DCV3ycDl.js +317 -0
- package/dist/turns-DCV3ycDl.js.map +1 -0
- package/dist/voice/client.js +1 -1
- package/dist/voice/index.d.ts +2 -2
- package/dist/voice/index.js +1 -1
- package/dist/websearch/index.d.ts +58 -0
- package/dist/websearch/index.js +2 -0
- package/dist/websearch/tools/ai-sdk.d.ts +77 -0
- package/dist/websearch/tools/ai-sdk.js +46 -0
- package/dist/websearch/tools/ai-sdk.js.map +1 -0
- package/dist/websearch/tools/pi.d.ts +82 -0
- package/dist/websearch/tools/pi.js +89 -0
- package/dist/websearch/tools/pi.js.map +1 -0
- package/dist/websearch/tools/tanstack-ai.d.ts +71 -0
- package/dist/websearch/tools/tanstack-ai.js +46 -0
- package/dist/websearch/tools/tanstack-ai.js.map +1 -0
- package/dist/websockets/index.d.ts +12 -296
- package/dist/websockets/index.js +1 -1
- package/dist/{websockets-Ci-LZFHH.js → websockets-CmENGb-Z.js} +18 -4
- package/dist/websockets-CmENGb-Z.js.map +1 -0
- package/dist/{wire-types-CrjKvDyi.d.ts → wire-types-Cv0iwjk5.d.ts} +3 -51
- package/dist/workflow-types.d.ts +1 -1
- package/dist/workflows.d.ts +1 -1
- package/dist/workflows.js +1 -1
- package/docs/browse-the-web.md +3 -0
- package/docs/harnesses/think.md +215 -0
- package/docs/index.md +2 -1
- package/docs/lifecycle.md +40 -0
- package/docs/search-the-web.md +133 -0
- package/package.json +117 -38
- package/dist/browser-tool-CQ10e6Kj.js.map +0 -1
- package/dist/channel-Bnm4S7T2.d.ts +0 -491
- package/dist/channels/ai-sdk.d.ts +0 -46
- package/dist/channels/ai-sdk.js +0 -120
- package/dist/channels/ai-sdk.js.map +0 -1
- package/dist/channels/email.js.map +0 -1
- package/dist/channels/index.d.ts +0 -233
- package/dist/channels/index.js +0 -608
- package/dist/channels/index.js.map +0 -1
- package/dist/channels/slack.js +0 -614
- package/dist/channels/slack.js.map +0 -1
- package/dist/channels/tanstack-ai.d.ts +0 -39
- package/dist/channels/tanstack-ai.js +0 -17
- package/dist/channels/tanstack-ai.js.map +0 -1
- package/dist/channels/telegram.js +0 -427
- package/dist/channels/telegram.js.map +0 -1
- package/dist/channels/voice.d.ts +0 -45
- package/dist/channels/voice.js +0 -122
- package/dist/channels/voice.js.map +0 -1
- package/dist/images-iiTDSyRu.js.map +0 -1
- package/dist/index-BB0kqhIz.d.ts +0 -101
- package/dist/ingress-BfetZbMO.js +0 -83
- package/dist/ingress-BfetZbMO.js.map +0 -1
- package/dist/internal-CYlgHl1l.js +0 -59
- package/dist/internal-CYlgHl1l.js.map +0 -1
- package/dist/models/ai-sdk/index.js.map +0 -1
- package/dist/protocol-kHsr-f0X.js.map +0 -1
- package/dist/sessions/index.js.map +0 -1
- package/dist/streams-DW-UCOUc.js.map +0 -1
- package/dist/surface-bZZJqBka.js +0 -17
- package/dist/surface-bZZJqBka.js.map +0 -1
- package/dist/tasks-68DESxf0.js.map +0 -1
- package/dist/tool-schema-CBjGPrsQ.js +0 -31
- package/dist/tool-schema-CBjGPrsQ.js.map +0 -1
- package/dist/websockets-Ci-LZFHH.js.map +0 -1
- package/docs/channels.md +0 -323
|
@@ -0,0 +1,2152 @@
|
|
|
1
|
+
import { i as _classPrivateFieldInitSpec, n as _classPrivateFieldSet2, r as _assertClassBrand, t as _classPrivateFieldGet2 } from "../../classPrivateFieldGet2-DZBYAB34.js";
|
|
2
|
+
import { t as _classPrivateMethodInitSpec } from "../../classPrivateMethodInitSpec-qMjJ6sHQ.js";
|
|
3
|
+
import { t as LifecycleCapability } from "../../capability-B4WbF81e.js";
|
|
4
|
+
import "../../lifecycle-CFu6OV03.js";
|
|
5
|
+
import { n as openHarnessStore } from "../../store-vgcPYo8K.js";
|
|
6
|
+
import { d as parseDaemonMessage, i as CONTAINER_HEALTH_PATH, l as chunkEntries, o as CONTAINER_SESSION_PATH, r as CONTAINER_ENV, s as CONTAINER_TOKEN_HEADER } from "../../protocol-cOklVBBD.js";
|
|
7
|
+
import { WorkerEntrypoint } from "cloudflare:workers";
|
|
8
|
+
//#region src/harness/container/link.ts
|
|
9
|
+
/**
|
|
10
|
+
* The object's side of one session socket: dial the daemon through the
|
|
11
|
+
* container's TCP port, read its `hello`, then hand every later frame to
|
|
12
|
+
* the harness. Failures to reach the daemon are values, so the harness can
|
|
13
|
+
* decide between retrying and giving up.
|
|
14
|
+
*/
|
|
15
|
+
const HELLO_TIMEOUT_MS = 1e4;
|
|
16
|
+
const CONNECT_TIMEOUT_MS = 1e4;
|
|
17
|
+
const HEALTH_TIMEOUT_MS = 2e3;
|
|
18
|
+
function unreachable(message) {
|
|
19
|
+
return {
|
|
20
|
+
_tag: "err",
|
|
21
|
+
error: {
|
|
22
|
+
_tag: "unreachable",
|
|
23
|
+
message
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function errorText$1(error) {
|
|
28
|
+
return error instanceof Error ? error.message : String(error);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Wait until the daemon answers its health check.
|
|
32
|
+
*
|
|
33
|
+
* @param port - The container's TCP port.
|
|
34
|
+
* @param timeoutMs - How long to keep trying.
|
|
35
|
+
* @param running - Whether the container still runs; waiting stops early
|
|
36
|
+
* once it does not.
|
|
37
|
+
* @returns `ok`, or `unreachable` after the timeout or once it stops.
|
|
38
|
+
*/
|
|
39
|
+
async function waitHealthy(port, timeoutMs, running) {
|
|
40
|
+
const deadline = Date.now() + timeoutMs;
|
|
41
|
+
let delay = 250;
|
|
42
|
+
let last = "no answer";
|
|
43
|
+
for (;;) {
|
|
44
|
+
try {
|
|
45
|
+
const response = await port.fetch(`http://container${CONTAINER_HEALTH_PATH}`, { signal: AbortSignal.timeout(HEALTH_TIMEOUT_MS) });
|
|
46
|
+
await response.body?.cancel();
|
|
47
|
+
if (response.ok) return {
|
|
48
|
+
_tag: "ok",
|
|
49
|
+
value: void 0
|
|
50
|
+
};
|
|
51
|
+
last = `health check answered ${response.status}`;
|
|
52
|
+
} catch (error) {
|
|
53
|
+
last = errorText$1(error);
|
|
54
|
+
}
|
|
55
|
+
if (!running()) return unreachable("the container stopped");
|
|
56
|
+
if (Date.now() + delay > deadline) return unreachable(last);
|
|
57
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
58
|
+
delay = Math.min(delay * 2, 2e3);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
var _socket = /* @__PURE__ */ new WeakMap();
|
|
62
|
+
var _open = /* @__PURE__ */ new WeakMap();
|
|
63
|
+
/** One open session socket. */
|
|
64
|
+
var DaemonLink = class DaemonLink {
|
|
65
|
+
constructor(socket) {
|
|
66
|
+
_classPrivateFieldInitSpec(this, _socket, void 0);
|
|
67
|
+
_classPrivateFieldInitSpec(this, _open, true);
|
|
68
|
+
_classPrivateFieldSet2(_socket, this, socket);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Dial the daemon for one session and wait for its `hello`.
|
|
72
|
+
*
|
|
73
|
+
* @param port - The container's TCP port.
|
|
74
|
+
* @param session - The session id.
|
|
75
|
+
* @param token - The shared secret the container was started with.
|
|
76
|
+
* @param handlers - Called for frames after `hello`, and on close.
|
|
77
|
+
* @returns The link and the greeting, or why it failed.
|
|
78
|
+
*/
|
|
79
|
+
static async open(port, session, token, handlers) {
|
|
80
|
+
let response;
|
|
81
|
+
try {
|
|
82
|
+
response = await port.fetch(`http://container${CONTAINER_SESSION_PATH}${encodeURIComponent(session)}`, {
|
|
83
|
+
headers: {
|
|
84
|
+
Upgrade: "websocket",
|
|
85
|
+
[CONTAINER_TOKEN_HEADER]: token
|
|
86
|
+
},
|
|
87
|
+
signal: AbortSignal.timeout(CONNECT_TIMEOUT_MS)
|
|
88
|
+
});
|
|
89
|
+
} catch (error) {
|
|
90
|
+
return unreachable(errorText$1(error));
|
|
91
|
+
}
|
|
92
|
+
const socket = response.webSocket;
|
|
93
|
+
if (!socket) {
|
|
94
|
+
await response.body?.cancel();
|
|
95
|
+
return unreachable(`upgrade refused with ${response.status}`);
|
|
96
|
+
}
|
|
97
|
+
socket.accept();
|
|
98
|
+
const link = new DaemonLink(socket);
|
|
99
|
+
return new Promise((resolve) => {
|
|
100
|
+
let greeted = false;
|
|
101
|
+
const timer = setTimeout(() => {
|
|
102
|
+
if (greeted) return;
|
|
103
|
+
link.close();
|
|
104
|
+
resolve(unreachable("no hello from the daemon"));
|
|
105
|
+
}, HELLO_TIMEOUT_MS);
|
|
106
|
+
socket.addEventListener("message", (event) => {
|
|
107
|
+
const message = parseDaemonMessage(typeof event.data === "string" ? event.data : new TextDecoder().decode(event.data));
|
|
108
|
+
if (!message) return;
|
|
109
|
+
if (!greeted) {
|
|
110
|
+
if (message.type !== "hello") return;
|
|
111
|
+
greeted = true;
|
|
112
|
+
clearTimeout(timer);
|
|
113
|
+
if (message.protocol !== 1) {
|
|
114
|
+
link.close();
|
|
115
|
+
resolve({
|
|
116
|
+
_tag: "err",
|
|
117
|
+
error: {
|
|
118
|
+
_tag: "protocol",
|
|
119
|
+
message: `daemon speaks protocol ${message.protocol}, the harness 1`
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
resolve({
|
|
125
|
+
_tag: "ok",
|
|
126
|
+
value: {
|
|
127
|
+
link,
|
|
128
|
+
hello: message
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (message.type === "frame") handlers.frame(message.seq, message.frame);
|
|
134
|
+
else if (message.type === "caught-up") handlers.caughtUp();
|
|
135
|
+
});
|
|
136
|
+
const onClose = () => {
|
|
137
|
+
if (!_classPrivateFieldGet2(_open, link)) return;
|
|
138
|
+
_classPrivateFieldSet2(_open, link, false);
|
|
139
|
+
clearTimeout(timer);
|
|
140
|
+
if (!greeted) resolve(unreachable("socket closed before hello"));
|
|
141
|
+
else handlers.closed();
|
|
142
|
+
};
|
|
143
|
+
socket.addEventListener("close", onClose);
|
|
144
|
+
socket.addEventListener("error", onClose);
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
/** Whether the socket is still open. */
|
|
148
|
+
get open() {
|
|
149
|
+
return _classPrivateFieldGet2(_open, this);
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Send one message. Returns false when the socket has closed.
|
|
153
|
+
*
|
|
154
|
+
* @param message - The message.
|
|
155
|
+
* @returns Whether it was handed to the socket.
|
|
156
|
+
*/
|
|
157
|
+
send(message) {
|
|
158
|
+
if (!_classPrivateFieldGet2(_open, this)) return false;
|
|
159
|
+
try {
|
|
160
|
+
_classPrivateFieldGet2(_socket, this).send(JSON.stringify(message));
|
|
161
|
+
return true;
|
|
162
|
+
} catch {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/** Close the socket. Does not call `closed`. */
|
|
167
|
+
close() {
|
|
168
|
+
if (!_classPrivateFieldGet2(_open, this)) return;
|
|
169
|
+
_classPrivateFieldSet2(_open, this, false);
|
|
170
|
+
try {
|
|
171
|
+
_classPrivateFieldGet2(_socket, this).close(1e3, "closed by the harness");
|
|
172
|
+
} catch {}
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
//#endregion
|
|
176
|
+
//#region src/harness/container/daemon-source.generated.ts
|
|
177
|
+
/** The harness daemon, bundled: one Node ES module. */
|
|
178
|
+
const DAEMON_SOURCE = "var $=\"/healthz\",M=\"/sessions/\",z=\"x-cf-harness-token\",x={token:\"CF_HARNESS_TOKEN\",runtimeId:\"CF_HARNESS_RUNTIME_ID\",port:\"CF_HARNESS_PORT\"},K=4001,W=4003,xe=256*1024;function B(n){return typeof n==\"object\"&&n!==null&&!Array.isArray(n)}function k(n){return typeof n==\"string\"}function E(n){return typeof n==\"number\"&&Number.isSafeInteger(n)&&n>=0}function A(n){if(n===null)return!0;switch(typeof n){case\"string\":case\"boolean\":return!0;case\"number\":return Number.isFinite(n);case\"object\":return Array.isArray(n)?n.every(A):Object.values(n).every(A);default:return!1}}function be(n){return k(n)?!0:Array.isArray(n)&&n.every(e=>B(e)&&(e.type===\"text\"&&k(e.text)||e.type===\"image\"&&k(e.mediaType)&&k(e.data)))}function U(n){return B(n)&&(n.model===void 0||k(n.model))&&(n.options===void 0||A(n.options))}function Ie(n){try{return JSON.parse(n)}catch{return}}function G(n){let e=Ie(n);if(B(e))switch(e.type){case\"restore\":return E(e.generation)&&Array.isArray(e.entries)&&e.entries.every(A)?{type:\"restore\",generation:e.generation,entries:e.entries}:void 0;case\"open\":return E(e.generation)&&U(e.settings)&&Array.isArray(e.restore)&&e.restore.every(A)?{type:\"open\",generation:e.generation,settings:e.settings,restore:e.restore}:void 0;case\"replay\":return E(e.after)?{type:\"replay\",after:e.after}:void 0;case\"prompt\":return k(e.operationId)&&be(e.input)&&(e.whenBusy===\"followUp\"||e.whenBusy===\"steer\")?{type:\"prompt\",operationId:e.operationId,input:e.input,whenBusy:e.whenBusy}:void 0;case\"abort\":return e.operationId===void 0?{type:\"abort\"}:k(e.operationId)?{type:\"abort\",operationId:e.operationId}:void 0;case\"configure\":return U(e.settings)?{type:\"configure\",settings:e.settings}:void 0;case\"ack\":return E(e.seq)?{type:\"ack\",seq:e.seq}:void 0;default:return}}function _(n){return typeof n==\"string\"?n:n.map(e=>e.type===\"text\"?e.text:\"\").join(`\n`)}function Y(n){let e=[],t=[],r=0;for(let o of n){let s=JSON.stringify(o).length;t.length>0&&r+s>xe&&(e.push(t),t=[],r=0),t.push(o),r+=s}return t.length>0&&e.push(t),e}var X=1e3,T=class{#e;#n;#s;#i=new Map;constructor(e){this.#e=e.adapter,this.#n=e.runtimeId,this.#s=e.maxKeptFrames??1e4}connect(e,t){let r=this.#i.get(e);return r||(r=new J(e,this.#e,this.#n,this.#s),this.#i.set(e,r)),r.attach(t)}async close(){await Promise.all([...this.#i.values()].map(e=>e.shutdown()))}},J=class{#e;#n;#s;#i;#a=0;#r=[];#t=new Map;#l=[];#o;#d;#y;#u;#f=!1;#h=Promise.resolve();constructor(e,t,r,o){this.#e=e,this.#n=t,this.#s=r,this.#i=o}attach(e){let t=this.#u;return this.#u=e,this.#f=!1,t?.close(K,\"replaced by a newer connection\"),this.#c(e,this.#w()),{receive:r=>{if(this.#u!==e)return;let o=G(r);if(!o){this.#c(e,{type:\"error\",message:\"unparseable message\"});return}this.#h=this.#h.then(()=>this.#C(o)).catch(s=>{this.#c(e,{type:\"error\",message:Z(s)})})},closed:()=>{this.#u===e&&(this.#u=void 0,this.#f=!1)}}}async shutdown(){this.#o?.controller.abort();let e=this.#d;this.#d=void 0,await e?.session.close().catch(()=>{})}#w(){let e=[];for(let t of this.#t.values()){let r=t.state;e.push(r.tag===\"settled\"?{operationId:t.id,status:\"settled\",outcome:r.outcome}:{operationId:t.id,status:r.tag})}return{type:\"hello\",protocol:1,runtimeId:this.#s,session:this.#e,adapter:{id:this.#n.id,version:this.#n.version,capabilities:[...this.#n.capabilities??[]]},open:this.#d?.generation??null,lastSeq:this.#a,operations:e}}async#C(e){switch(e.type){case\"restore\":this.#y?.generation!==e.generation&&(this.#y={generation:e.generation,entries:[]}),this.#y.entries.push(...e.entries);return;case\"open\":await this.#x(e);return;case\"replay\":this.#b(e.after);return;case\"prompt\":await this.#I(e.operationId,e.input,e.whenBusy);return;case\"abort\":this.#S(e.operationId);return;case\"configure\":await this.#d?.session.configure?.(e.settings);return;case\"ack\":this.#r=this.#r.filter(t=>t.seq>e.seq);return}}async#x(e){let t=this.#y;if(this.#y=void 0,this.#d?.generation===e.generation)return;let r=t?.generation===e.generation?[...t.entries,...e.restore]:e.restore;this.#d&&(this.#o?.controller.abort(),await this.#d.session.close().catch(()=>{}),this.#d=void 0);let o=await this.#n.open({session:this.#e,settings:e.settings,restore:r,emit:s=>this.#p({kind:\"event\",operationId:this.#o?.id??null,event:s}),persist:s=>{for(let i of Y(s))this.#p({kind:\"persist\",entries:i})},log:(s,i)=>this.#p({kind:\"event\",operationId:this.#o?.id??null,event:{type:\"log\",level:s,message:i}})});this.#d={generation:e.generation,session:o},this.#g()}#b(e){let t=this.#u;if(t){for(let r of this.#r)r.seq>e&&this.#c(t,{type:\"frame\",seq:r.seq,frame:r.frame});this.#c(t,{type:\"caught-up\",lastSeq:this.#a}),this.#f=!0}}async#I(e,t,r){if(this.#t.has(e))return;let o={id:e,input:t,whenBusy:r,state:{tag:\"queued\"}};this.#t.set(e,o),this.#A();let s=this.#o,i=this.#d?.session.steer;if(r===\"steer\"&&s&&i&&await i.call(this.#d?.session,t)&&this.#o===s){o.state={tag:\"running\"},s.folded.push(e),this.#p({kind:\"start\",operationId:e});return}this.#l.push(e),this.#g()}#S(e){let t=e===void 0?[...this.#t.keys()]:this.#t.has(e)?[e]:[];for(let r of t){let o=this.#t.get(r);if(o)if(o.state.tag===\"queued\"){let s=this.#l.indexOf(r);s>=0&&this.#l.splice(s,1),this.#m(o,{status:\"unanswered\",reason:\"aborted\"})}else o.state.tag===\"running\"&&this.#o?.controller.abort()}}#g(){let e=this.#d;if(this.#o||!e)return;let t=this.#l.shift();if(t===void 0)return;let r=this.#t.get(t);if(!r||r.state.tag!==\"queued\"){this.#g();return}let o={id:t,folded:[],controller:new AbortController};this.#o=o,r.state={tag:\"running\"},this.#p({kind:\"start\",operationId:t}),this.#k(e,r,o)}async#k(e,t,r){let o;try{o=await e.session.run({operationId:t.id,input:t.input,signal:r.controller.signal}),r.controller.signal.aborted&&o.status===\"done\"&&(o={status:\"unanswered\",reason:\"aborted\"})}catch(s){o={status:\"unanswered\",reason:r.controller.signal.aborted?\"aborted\":Z(s)}}this.#o===r&&(this.#o=void 0),this.#m(t,o);for(let s of r.folded){let i=this.#t.get(s);i&&this.#m(i,o)}this.#g()}#m(e,t){e.state.tag!==\"settled\"&&(e.state={tag:\"settled\",outcome:t},this.#p({kind:\"settle\",operationId:e.id,outcome:t}))}#p(e){this.#a+=1;let t=this.#a;this.#r.push({seq:t,frame:e}),this.#r.length>this.#i&&this.#r.splice(0,this.#r.length-this.#i);let r=this.#u;r&&this.#f&&this.#c(r,{type:\"frame\",seq:t,frame:e})}#c(e,t){try{e.send(JSON.stringify(t))}catch{}}#A(){if(!(this.#t.size<=X))for(let[e,t]of this.#t){if(this.#t.size<=X)return;t.state.tag===\"settled\"&&this.#t.delete(e)}}};function Z(n){return n instanceof Error?n.message:String(n)}var Q={id:\"echo\",version:\"1\",capabilities:[\"steer\"],open(n){let e=n.restore.length,t=[];return{async run(r){let o=_(r.input),s=/^slow (\\d+) ?(.*)$/s.exec(o);s&&await ke(Number(s[1]),r.signal);let i=/^fail ?(.*)$/s.exec(o);if(i)return{status:\"unanswered\",reason:i[1]||\"failed\"};e+=1;let d=s?s[2]??\"\":o,a=t.splice(0).map(u=>` + ${u}`).join(\"\"),l=`echo: ${d}${a} (turn ${e})`,c=`${r.operationId}:assistant`;return n.emit({type:\"text-delta\",messageId:c,delta:l}),n.emit({type:\"message\",message:{id:c,role:\"assistant\",parts:[{type:\"text\",text:l}],operationId:r.operationId,createdAt:Date.now()}}),n.persist([{turn:e,text:d}]),{status:\"done\",text:l}},steer(r){return t.push(_(r)),!0},async close(){}}}};function ke(n,e){return new Promise((t,r)=>{if(e.aborted){r(new Error(\"aborted\"));return}let o=setTimeout(()=>{e.removeEventListener(\"abort\",s),t()},n),s=()=>{clearTimeout(o),r(new Error(\"aborted\"))};e.addEventListener(\"abort\",s,{once:!0})})}import{spawn as Ne}from\"node:child_process\";import{createHash as De}from\"node:crypto\";import{constants as Me}from\"node:fs\";import{chown as re,copyFile as Be,mkdir as oe,readdir as se,readlink as Je,symlink as Re}from\"node:fs/promises\";import{homedir as Le}from\"node:os\";import{join as O,resolve as L,sep as Ve}from\"node:path\";import{createInterface as Fe}from\"node:readline\";import{mkdir as Ae,open as P,readdir as _e,rm as ve,stat as ee}from\"node:fs/promises\";import{dirname as Oe,join as Ee,relative as Te,resolve as te,sep as ne}from\"node:path\";function w(n){return typeof n==\"object\"&&n!==null&&!Array.isArray(n)}function v(n){if(Array.isArray(n))return n.filter(e=>typeof e==\"string\")}var R=128*1024;function Pe(n){return w(n)&&n.kind===\"file\"&&typeof n.path==\"string\"&&typeof n.offset==\"number\"&&typeof n.data==\"string\"}var N=class{#e;#n;#s=new Map;constructor(e,t){this.#e=te(e),this.#n=t}async restore(e){for(let t of this.#n){let r=this.#t(t);r!==void 0&&r!==this.#e&&await ve(r,{recursive:!0,force:!0})}for(let t of e){if(!Pe(t))continue;let r=this.#t(t.path);if(r===void 0)continue;await Ae(Oe(r),{recursive:!0});let o=t.offset===0?await P(r,\"w\"):await P(r,\"r+\").catch(()=>P(r,\"w\"));try{t.offset>0&&await o.truncate(t.offset),await o.write(Buffer.from(t.data,\"base64\"),0,void 0,t.offset)}finally{await o.close()}}for(let t of await this.#r()){let r=await ee(t);this.#s.set(t,{size:r.size,mtimeMs:r.mtimeMs})}}async collect(){let e=[];for(let t of await this.#r()){let r=await ee(t).catch(()=>{});if(!r)continue;let o=this.#s.get(t);if(o&&o.size===r.size&&o.mtimeMs===r.mtimeMs)continue;let s=o&&r.size>o.size?o.size:0,i=await this.#a(t,s,r.size),d=[];for(let a=0;a<i.length;a+=R)d.push(i.subarray(a,a+R));d.length===0&&d.push(i),d.forEach((a,l)=>{e.push(this.#i(t,s+l*R,a))}),this.#s.set(t,{size:s+i.length,mtimeMs:r.mtimeMs})}return e}#i(e,t,r){return{kind:\"file\",path:Te(this.#e,e).split(ne).join(\"/\"),offset:t,data:r.toString(\"base64\")}}async#a(e,t,r){let o=await P(e,\"r\");try{let s=Buffer.alloc(Math.max(0,r-t)),{bytesRead:i}=await o.read(s,0,s.length,t);return s.subarray(0,i)}finally{await o.close()}}async#r(){let e=[],t=async r=>{let o=await _e(r,{withFileTypes:!0}).catch(()=>[]);for(let s of o){let i=Ee(r,s.name);s.isDirectory()?await t(i):s.isFile()&&e.push(i)}};for(let r of this.#n){let o=this.#t(r);o!==void 0&&await t(o)}return e.sort()}#t(e){let t=te(this.#e,e);return t===this.#e||t.startsWith(this.#e+ne)?t:void 0}};var He=5e3,je=4096;function qe(n){let e={};for(let t of n){if(!w(t)||t.kind!==\"state\")continue;let r=t.values;if(w(r))for(let[o,s]of Object.entries(r))typeof s==\"string\"&&(e[o]=s)}return e}async function ie(n,e){await re(n,e.uid,e.gid);let t=await se(n,{withFileTypes:!0}).catch(()=>[]);for(let r of t){let o=O(n,r.name);r.isDirectory()?await ie(o,e):await re(o,e.uid,e.gid).catch(()=>{})}}async function Ue(n,e,t){let r=L(t),o=async(s,i)=>{let d=await se(s,{withFileTypes:!0}).catch(()=>[]);for(let a of d){let l=O(s,a.name),c=L(l);if(c===r||c.startsWith(r+Ve))continue;let u=O(i,a.name);a.isDirectory()?(await oe(u,{recursive:!0}),await o(l,u)):a.isSymbolicLink()?await Re(await Je(l),u).catch(()=>{}):a.isFile()&&await Be(l,u,Me.COPYFILE_EXCL).catch(()=>{})}};await o(L(n),e)}function $e(n,e){return O(n,De(\"sha256\").update(e).digest(\"hex\").slice(0,32))}var V=class{#e;#n;#s;#i;#a;#r;#t=Promise.resolve();constructor(e,t,r,o){this.#e=e,this.#n=t,this.#s=r,this.#i=o,this.#a=qe(t.restore),this.#r=t.settings}async run(e){let t={operationId:e.operationId,prompt:_(e.input),settings:this.#r,state:this.#a},r=this.#e.command(t),o=this.#e.parser(t),[s,...i]=r.argv;if(s===void 0)throw new Error(\"The CLI command is empty\");let d=this.#e.user,a=Ne(s,i,{cwd:this.#e.cwd??\"/workspace\",env:{...process.env,...r.env,HOME:this.#s},stdio:[\"pipe\",\"pipe\",\"pipe\"],...d?{uid:d.uid,gid:d.gid}:{}});a.stdin.on(\"error\",()=>{}),a.stdin.end(r.stdin??\"\");let l=\"\";a.stderr.setEncoding(\"utf8\"),a.stderr.on(\"data\",p=>{l=(l+p).slice(-je)});let c;Fe({input:a.stdout}).on(\"line\",p=>{if(p.trim()===\"\")return;let y;try{y=o.line(p)}catch(h){this.#n.log(\"warn\",`unreadable CLI output: ${String(h)}`);return}for(let h of y.events??[])this.#n.emit(h);y.state&&this.#l(y.state),y.outcome&&(c=y.outcome)});let b=()=>{a.kill(\"SIGINT\"),setTimeout(()=>a.kill(\"SIGKILL\"),He).unref()};e.signal.addEventListener(\"abort\",b,{once:!0});let m=setInterval(()=>{this.#o()},this.#e.syncIntervalMs??5e3),f=await new Promise(p=>{a.on(\"error\",y=>{l+=`\n${y.message}`,p(null)}),a.on(\"close\",y=>p(y))});return clearInterval(m),e.signal.removeEventListener(\"abort\",b),await this.#o(),e.signal.aborted?{status:\"unanswered\",reason:\"aborted\"}:c??o.end({code:f,stderr:l})}configure(e){this.#r=e}async close(){await this.#t}#l(e){Object.entries(e).some(([r,o])=>this.#a[r]!==o)&&(this.#a={...this.#a,...e},this.#n.persist([{kind:\"state\",values:{...e}}]))}#o(){return this.#t=this.#t.then(async()=>{try{let e=await this.#i.collect();e.length>0&&this.#n.persist(e)}catch(e){this.#n.log(\"warn\",`state sync failed: ${String(e)}`)}}),this.#t}};function ae(n){let e=n.homes??O(Le(),\".harness\",\"sessions\");return{id:n.id,version:n.version??\"1\",capabilities:[\"resume\",\"fork\",\"model\"],async open(t){let r=$e(e,t.session);await oe(r,{recursive:!0}),n.skeleton&&await Ue(n.skeleton,r,e);let o=new N(r,n.stateDirs??[]);return await o.restore(t.restore),n.user&&await ie(r,n.user),new V(n,t,r,o)}}}function de(n){if(n===void 0)return null;try{return JSON.parse(JSON.stringify(n))}catch{return String(n)}}function ze(n){if(typeof n==\"string\")return n;if(Array.isArray(n)){let e=n.map(t=>typeof t==\"object\"&&t!==null&&\"type\"in t&&t.type===\"text\"&&\"text\"in t&&typeof t.text==\"string\"?t.text:void 0).filter(t=>t!==void 0);if(e.length===n.length)return e.join(`\n`)}return de(n)}var D=class{#e=new Map;#n=new Map;#s=new Set;#i;#a;constructor(e=Date.now){this.#a=e}project(e,t){if(e.parent_tool_use_id)return[];switch(e.type){case\"stream_event\":return this.#r(e);case\"assistant\":return this.#t(e,t);case\"user\":return this.#l(e);case\"result\":return this.#o(e);default:return[]}}outcome(e,t){if(t)return{status:\"unanswered\",reason:\"aborted\"};if(e.subtype===\"success\"&&e.is_error!==!0)return{status:\"done\",text:e.result??\"\"};let r=e.errors?.join(\"; \")||e.result;return{status:\"unanswered\",reason:[e.subtype&&e.subtype!==\"success\"?e.subtype:\"\",r].filter(s=>s).join(\": \")||\"error\"}}#r(e){let t=e.event;if(!t)return[];if(t.type===\"message_start\")return this.#i=t.message?.id,[];let r=this.#i;return t.type!==\"content_block_delta\"||!r||!t.delta?[]:t.delta.type===\"text_delta\"&&t.delta.text?[{type:\"text-delta\",messageId:r,delta:t.delta.text}]:t.delta.type===\"thinking_delta\"&&t.delta.thinking?[{type:\"reasoning-delta\",messageId:r,delta:t.delta.thinking}]:[]}#t(e,t){let r=e.message?.id,o=e.message?.content;if(!r||!Array.isArray(o))return[];if(e.uuid!==void 0){if(this.#s.has(e.uuid))return[];this.#s.add(e.uuid)}let s=this.#e.get(r);s||(s={id:r,operationId:t,createdAt:this.#a(),parts:[]},this.#e.set(r,s));for(let i of o)i.type===\"text\"&&i.text?s.parts.push({type:\"text\",text:i.text}):i.type===\"thinking\"&&i.thinking?s.parts.push({type:\"reasoning\",text:i.thinking}):i.type===\"tool_use\"&&i.id&&(s.parts.push({type:\"tool\",toolCallId:i.id,toolName:i.name??\"tool\",input:de(i.input),state:\"running\"}),this.#n.set(i.id,r));return[{type:\"message\",message:this.#d(s)}]}#l(e){let t=e.message?.content;if(!Array.isArray(t))return[];let r=new Set;for(let o of t){if(o.type!==\"tool_result\"||!o.tool_use_id)continue;let s=this.#n.get(o.tool_use_id),i=s===void 0?void 0:this.#e.get(s);i&&(i.parts=i.parts.map(d=>d.type===\"tool\"&&d.toolCallId===o.tool_use_id?{...d,state:o.is_error?\"error\":\"done\",output:ze(o.content)}:d),r.add(i))}return[...r].map(o=>({type:\"message\",message:this.#d(o)}))}#o(e){return[{type:\"usage\",...e.usage?.input_tokens===void 0?{}:{inputTokens:e.usage.input_tokens},...e.usage?.output_tokens===void 0?{}:{outputTokens:e.usage.output_tokens},...e.total_cost_usd===void 0?{}:{costUsd:e.total_cost_usd}}]}#d(e){return{id:e.id,role:\"assistant\",parts:[...e.parts],...e.operationId===void 0?{}:{operationId:e.operationId},createdAt:e.createdAt}}};function Ke(n){if(!w(n))return{};let e=v(n.allowedTools),t=v(n.disallowedTools);return{...typeof n.appendSystemPrompt==\"string\"?{appendSystemPrompt:n.appendSystemPrompt}:{},...e?{allowedTools:e}:{},...t?{disallowedTools:t}:{},...typeof n.maxTurns==\"number\"?{maxTurns:n.maxTurns}:{}}}function We(n){let e=JSON.parse(n);if(!(typeof e!=\"object\"||e===null||!(\"type\"in e)))return e}var le={id:\"claude-code\",stateDirs:[\".claude/projects\"],command(n){let e=Ke(n.settings.options),t=[\"claude\",\"-p\",\"--output-format\",\"stream-json\",\"--verbose\",\"--include-partial-messages\",\"--dangerously-skip-permissions\"];return n.settings.model&&t.push(\"--model\",n.settings.model),n.state.sessionId&&t.push(\"--resume\",n.state.sessionId),e.appendSystemPrompt&&t.push(\"--append-system-prompt\",e.appendSystemPrompt),e.allowedTools?.length&&t.push(\"--allowedTools\",e.allowedTools.join(\",\")),e.disallowedTools?.length&&t.push(\"--disallowedTools\",e.disallowedTools.join(\",\")),e.maxTurns!==void 0&&t.push(\"--max-turns\",String(e.maxTurns)),{argv:t,stdin:n.prompt,env:{DISABLE_AUTOUPDATER:\"1\"}}},parser(n){let e=new D;return{line(t){let r=We(t);return r?{events:e.project(r,n.operationId),...r.session_id?{state:{sessionId:r.session_id}}:{},...r.type===\"result\"?{outcome:e.outcome(r,!1)}:{}}:{}},end({code:t,stderr:r}){return{status:\"unanswered\",reason:`claude exited with ${t??\"a signal\"}: ${r.trim()||\"no output\"}`}}}}};function Ge(n){let e=JSON.parse(n);if(!(typeof e!=\"object\"||e===null))return e}function Ye(n){return n===\"completed\"?\"done\":n===\"failed\"||n===\"declined\"?\"error\":\"running\"}function Xe(n){let e=n.id??\"item\",t=Ye(n.status);switch(n.type){case\"agent_message\":return{type:\"text\",text:n.text??\"\"};case\"reasoning\":return{type:\"reasoning\",text:n.text??\"\"};case\"command_execution\":return{type:\"tool\",toolCallId:e,toolName:\"shell\",input:{command:n.command??\"\"},state:t===\"done\"&&n.exit_code!==0&&n.exit_code!=null?\"error\":t,...n.aggregated_output?{output:n.aggregated_output}:{}};case\"file_change\":return{type:\"tool\",toolCallId:e,toolName:\"file_change\",input:n.changes??null,state:t};case\"mcp_tool_call\":return{type:\"tool\",toolCallId:e,toolName:`${n.server??\"mcp\"}.${n.tool??\"tool\"}`,input:n.arguments??null,state:t,...n.result!==void 0||n.error!==void 0?{output:n.result??n.error??null}:{}};case\"web_search\":return{type:\"tool\",toolCallId:e,toolName:\"web_search\",input:{query:n.query??\"\"},state:t};case\"todo_list\":return{type:\"tool\",toolCallId:e,toolName:\"todo_list\",input:n.items??null,state:t};default:return}}function Ze(n){if(!w(n))return{};let e=v(n.config);return e?{config:e}:{}}var ue={id:\"codex\",stateDirs:[\".codex/sessions\"],command(n){let e=[\"codex\",\"exec\"];n.state.threadId&&e.push(\"resume\",n.state.threadId),e.push(\"--json\",\"--skip-git-repo-check\",\"--dangerously-bypass-approvals-and-sandbox\");let t=process.env.OPENAI_BASE_URL;t&&e.push(\"-c\",\"model_provider=gateway\",\"-c\",`model_providers.gateway={name=\"AI Gateway\",base_url=${JSON.stringify(t)},env_key=\"OPENAI_API_KEY\",wire_api=\"responses\"}`);for(let r of Ze(n.settings.options).config??[])e.push(\"-c\",r);return n.settings.model&&e.push(\"-m\",n.settings.model),e.push(\"-\"),{argv:e,stdin:n.prompt}},parser(n){let e=`${n.operationId}:assistant`,t=Date.now(),r=new Map,o=\"\",s,i=()=>({type:\"message\",message:{id:e,role:\"assistant\",parts:[...r.values()],operationId:n.operationId,createdAt:t}});return{line(d){let a=Ge(d);if(!a)return{};switch(a.type){case\"thread.started\":return a.thread_id?{state:{threadId:a.thread_id}}:{};case\"item.started\":case\"item.updated\":case\"item.completed\":{let l=a.item;if(!l)return{};if(l.type===\"error\")return{events:[{type:\"log\",level:\"warn\",message:l.message??\"error\"}]};let c=Xe(l);return c?(r.set(l.id??String(r.size),c),c.type===\"text\"&&a.type===\"item.completed\"&&(o=c.text),{events:[i()]}):{}}case\"turn.completed\":return{events:[{type:\"usage\",...a.usage?.input_tokens===void 0?{}:{inputTokens:a.usage.input_tokens},...a.usage?.output_tokens===void 0?{}:{outputTokens:a.usage.output_tokens}}],outcome:{status:\"done\",text:o}};case\"turn.failed\":return{outcome:{status:\"unanswered\",reason:a.error?.message??\"turn failed\"}};case\"error\":return s=a.message,{events:[{type:\"log\",level:\"warn\",message:a.message??\"error\"}]};default:return{}}},end({code:d,stderr:a}){return{status:\"unanswered\",reason:`codex exited with ${d??\"a signal\"}: ${s??(a.trim()||\"no output\")}`}}}}};import{timingSafeEqual as st}from\"node:crypto\";import{createServer as it}from\"node:http\";import{createHash as Qe}from\"node:crypto\";var et=\"258EAFA5-E914-47DA-95CA-C5AB0DC85B11\",pe=64*1024*1024,tt=0,ce=1,nt=2,F=8,rt=9,ye=10;function ot(n,e){let t=e.length,r=t<126?Buffer.from([128|n,t]):t<65536?Buffer.from([128|n,126,t>>8,t&255]):Buffer.concat([Buffer.from([128|n,127]),(()=>{let o=Buffer.alloc(8);return o.writeBigUInt64BE(BigInt(t)),o})()]);return Buffer.concat([r,e])}function H(n,e,t,r){let o=n.headers[\"sec-websocket-key\"];if(typeof o!=\"string\"||n.headers.upgrade?.toLowerCase()!==\"websocket\"){e.destroy();return}let s=Qe(\"sha1\").update(o+et).digest(\"base64\");e.write(`HTTP/1.1 101 Switching Protocols\\r\nUpgrade: websocket\\r\nConnection: Upgrade\\r\nSec-WebSocket-Accept: ${s}\\r\n\\r\n`);let i=!0,d=!1,a=()=>{d||(d=!0,i=!1,r.closed())},l=(p,y)=>{i&&e.write(ot(p,y))},c={send:p=>l(ce,Buffer.from(p,\"utf8\")),close:(p,y)=>{if(!i)return;let h=Buffer.alloc(2+Buffer.byteLength(y));h.writeUInt16BE(p),h.write(y,2),l(F,h),i=!1,e.end()}},u=t.length>0?Buffer.from(t):Buffer.alloc(0),b=[],m=0,f=()=>{for(;;){if(u.length<2)return;let p=u[0]??0,y=u[1]??0,h=(p&128)!==0,we=p&15,q=(y&128)!==0,I=y&127,C=2;if(I===126){if(u.length<4)return;I=u.readUInt16BE(2),C=4}else if(I===127){if(u.length<10)return;let g=u.readBigUInt64BE(2);if(g>BigInt(pe)){c.close(1009,\"message too big\");return}I=Number(g),C=10}let Ce=C;if(q&&(C+=4),u.length<C+I)return;let S=Buffer.from(u.subarray(C,C+I));if(q)for(let g=0;g<S.length;g++)S[g]=(S[g]??0)^(u[Ce+g%4]??0);switch(u=u.subarray(C+I),we){case ce:case nt:case tt:{if(b.push(S),m+=S.length,m>pe){c.close(1009,\"message too big\");return}if(h){let g=Buffer.concat(b).toString(\"utf8\");b=[],m=0,r.message(g)}break}case rt:l(ye,S);break;case ye:break;case F:i&&(l(F,S.subarray(0,2)),i=!1),e.end(),a();return;default:c.close(1002,\"unsupported opcode\");return}}};return e.on(\"data\",p=>{u=u.length===0?p:Buffer.concat([u,p]),f()}),e.on(\"close\",a),e.on(\"error\",a),u.length>0&&queueMicrotask(f),c}function at(n,e){if(e===void 0)return!1;let t=Buffer.from(n),r=Buffer.from(e);return t.length===r.length&&st(t,r)}async function dt(n){let e=new T({adapter:n.adapter,runtimeId:n.runtimeId}),t=new Set,r=it((i,d)=>{if(i.url===$){d.writeHead(200,{\"content-type\":\"text/plain\"}),d.end(\"ok\");return}d.writeHead(404),d.end()});r.on(\"upgrade\",(i,d,a)=>{let l=new URL(i.url??\"/\",\"http://container\");if(!l.pathname.startsWith(M)){d.destroy();return}let c=decodeURIComponent(l.pathname.slice(M.length)),u=i.headers[z];if(!at(n.token,Array.isArray(u)?u[0]:u)||c.length===0){H(i,d,a,{message:()=>{},closed:()=>{}})?.close(W,\"unauthorized\");return}let m,f=H(i,d,a,{message:p=>m?.receive(p),closed:()=>{f&&t.delete(f),m?.closed()}});f&&(t.add(f),m=e.connect(c,f))}),await new Promise(i=>{r.listen(n.port,n.host??\"0.0.0.0\",i)});let o=r.address();return{port:typeof o==\"object\"&&o?o.port:0,async close(){for(let i of t)i.close(1001,\"shutting down\");await new Promise(i=>r.close(()=>i())),await e.close()}}}async function fe(n,e=process.env){let t=e[x.token],r=e[x.runtimeId],o=Number(e[x.port]??\"8080\");if(!t||!r||!Number.isInteger(o)||o<=0)throw new Error(`${x.token}, ${x.runtimeId} and ${x.port} must be set; ContainerHarness sets them`);delete e[x.token];let s=await dt({adapter:n,runtimeId:r,token:t,port:o});console.log(`harness-daemon: ${n.id} on ${s.port} (${r})`);let i=()=>{s.close().finally(()=>process.exit(0))};return process.once(\"SIGTERM\",i),process.once(\"SIGINT\",i),s}var lt={\"claude-code\":le,codex:ue};function ut(n){let e=n?/^(\\d+):(\\d+)$/.exec(n):null;return e?{uid:Number(e[1]),gid:Number(e[2])}:void 0}var j=process.env.CF_HARNESS_ADAPTER??\"claude-code\",ge=ut(process.env.CF_HARNESS_USER),me=lt[j],he=j===\"echo\"?Q:me?ae({...me,...ge?{user:ge,homes:\"/home/agent/.harness/sessions\",skeleton:\"/home/agent\"}:{}}):void 0;he||(console.error(`harness-daemon: unknown CF_HARNESS_ADAPTER ${j}`),process.exit(1));await fe(he).catch(n=>{console.error(`harness-daemon: ${String(n)}`),process.exit(1)});\n";
|
|
179
|
+
/** A hash of `DAEMON_SOURCE`, to tell daemon builds apart. */
|
|
180
|
+
const DAEMON_VERSION = "d45c6a8182ef9070";
|
|
181
|
+
//#endregion
|
|
182
|
+
//#region src/harness/container/managed-image.ts
|
|
183
|
+
/**
|
|
184
|
+
* Containers built at runtime from `cloudflare/debian-trixie`, with no
|
|
185
|
+
* image of your own.
|
|
186
|
+
*
|
|
187
|
+
* The first start of a setup runs its steps with `exec()` (create the
|
|
188
|
+
* unprivileged user, install the CLI), writes the bundled daemon, and
|
|
189
|
+
* snapshots the filesystem. Later starts restore the snapshot and are
|
|
190
|
+
* ready as soon as the daemon boots. A snapshot is keyed by the image, the
|
|
191
|
+
* steps, and the daemon build, so changing any of them sets up afresh.
|
|
192
|
+
*
|
|
193
|
+
* The container's entrypoint waits for the daemon to be written and then
|
|
194
|
+
* runs it, so the same entrypoint serves a fresh container (where setup
|
|
195
|
+
* writes the daemon) and a restored one (where it is already there).
|
|
196
|
+
*/
|
|
197
|
+
/** The managed system image: Debian Trixie with Node.js. */
|
|
198
|
+
const MANAGED_IMAGE = "cloudflare/debian-trixie";
|
|
199
|
+
/** The unprivileged user CLIs run as, `uid:gid`. */
|
|
200
|
+
const AGENT_USER = "10001:10001";
|
|
201
|
+
const DAEMON_DIR = "/opt/harness";
|
|
202
|
+
/** Waits for setup to write the daemon, then becomes it. */
|
|
203
|
+
const MANAGED_ENTRYPOINT = [
|
|
204
|
+
"sh",
|
|
205
|
+
"-c",
|
|
206
|
+
`while [ ! -f ${DAEMON_DIR}/ready ]; do sleep 0.1; done; exec node ${DAEMON_DIR}/daemon.mjs`
|
|
207
|
+
];
|
|
208
|
+
/** The agent user's home, which every session home starts as a copy of. */
|
|
209
|
+
const AGENT_HOME = "/home/agent";
|
|
210
|
+
/** Steps every managed agent needs before its own. */
|
|
211
|
+
const BASE_STEPS = [{
|
|
212
|
+
name: "create the agent user and workspace",
|
|
213
|
+
command: [
|
|
214
|
+
"sh",
|
|
215
|
+
"-c",
|
|
216
|
+
`id -u agent >/dev/null 2>&1 || useradd --create-home --uid 10001 --shell /bin/sh agent; mkdir -p /workspace ${DAEMON_DIR} && chown 10001:10001 /workspace`
|
|
217
|
+
]
|
|
218
|
+
}, {
|
|
219
|
+
name: "install git",
|
|
220
|
+
command: [
|
|
221
|
+
"sh",
|
|
222
|
+
"-c",
|
|
223
|
+
"command -v git >/dev/null || (apt-get update -qq && apt-get install -y -qq --no-install-recommends git ca-certificates)"
|
|
224
|
+
]
|
|
225
|
+
}];
|
|
226
|
+
/** How long one setup step may run. */
|
|
227
|
+
const STEP_TIMEOUT_MS = 10 * 6e4;
|
|
228
|
+
/**
|
|
229
|
+
* The key a snapshot of this setup is stored under.
|
|
230
|
+
*
|
|
231
|
+
* @param steps - The setup steps.
|
|
232
|
+
* @returns A stable key for the image, the steps, and the daemon build.
|
|
233
|
+
*/
|
|
234
|
+
async function setupKey(steps) {
|
|
235
|
+
const text = JSON.stringify({
|
|
236
|
+
image: MANAGED_IMAGE,
|
|
237
|
+
steps: steps.map((step) => [step.user ?? "root", ...step.command]),
|
|
238
|
+
daemon: DAEMON_VERSION
|
|
239
|
+
});
|
|
240
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(text));
|
|
241
|
+
return [...new Uint8Array(digest)].slice(0, 16).map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
242
|
+
}
|
|
243
|
+
async function run(container, step, stdin) {
|
|
244
|
+
const asAgent = step.user === "agent";
|
|
245
|
+
const process = await container.exec([...step.command], {
|
|
246
|
+
env: {
|
|
247
|
+
HOME: asAgent ? AGENT_HOME : "/root",
|
|
248
|
+
DEBIAN_FRONTEND: "noninteractive"
|
|
249
|
+
},
|
|
250
|
+
...asAgent ? {
|
|
251
|
+
user: AGENT_USER,
|
|
252
|
+
cwd: AGENT_HOME
|
|
253
|
+
} : {},
|
|
254
|
+
stderr: "combined",
|
|
255
|
+
...stdin === void 0 ? {} : { stdin: new Blob([stdin]).stream() }
|
|
256
|
+
});
|
|
257
|
+
let exited = false;
|
|
258
|
+
process.exitCode.then(() => exited = true, () => exited = true);
|
|
259
|
+
const timer = setTimeout(() => {
|
|
260
|
+
if (!exited) process.kill(9);
|
|
261
|
+
}, STEP_TIMEOUT_MS);
|
|
262
|
+
try {
|
|
263
|
+
const output = await process.output();
|
|
264
|
+
if (output.exitCode === 0) return void 0;
|
|
265
|
+
const text = new TextDecoder().decode(output.stdout).slice(-2e3);
|
|
266
|
+
return {
|
|
267
|
+
_tag: "setup",
|
|
268
|
+
message: `${step.name} exited with ${output.exitCode}: ${text.trim()}`
|
|
269
|
+
};
|
|
270
|
+
} finally {
|
|
271
|
+
clearTimeout(timer);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Set a fresh managed container up: run the steps, install the daemon, and
|
|
276
|
+
* snapshot the result. A failed snapshot is not an error: the container
|
|
277
|
+
* still works, and the next start sets up again.
|
|
278
|
+
*
|
|
279
|
+
* @param container - The running container, started from `MANAGED_IMAGE`
|
|
280
|
+
* with `MANAGED_ENTRYPOINT`.
|
|
281
|
+
* @param steps - The agent's setup, after `BASE_STEPS`.
|
|
282
|
+
* @param key - The setup's key, from `setupKey`.
|
|
283
|
+
* @returns The snapshot to store, `undefined` when none was taken, or why
|
|
284
|
+
* setup failed.
|
|
285
|
+
*/
|
|
286
|
+
async function setUp(container, steps, key) {
|
|
287
|
+
for (const step of [...BASE_STEPS, ...steps]) {
|
|
288
|
+
const failed = await run(container, step).catch((error) => ({
|
|
289
|
+
_tag: "setup",
|
|
290
|
+
message: `${step.name} failed: ${error instanceof Error ? error.message : String(error)}`
|
|
291
|
+
}));
|
|
292
|
+
if (failed) return {
|
|
293
|
+
_tag: "err",
|
|
294
|
+
error: failed
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
const installed = await run(container, {
|
|
298
|
+
name: "install the daemon",
|
|
299
|
+
command: [
|
|
300
|
+
"sh",
|
|
301
|
+
"-c",
|
|
302
|
+
`cat > ${DAEMON_DIR}/daemon.mjs && touch ${DAEMON_DIR}/ready`
|
|
303
|
+
]
|
|
304
|
+
}, DAEMON_SOURCE);
|
|
305
|
+
if (installed) return {
|
|
306
|
+
_tag: "err",
|
|
307
|
+
error: installed
|
|
308
|
+
};
|
|
309
|
+
try {
|
|
310
|
+
return {
|
|
311
|
+
_tag: "ok",
|
|
312
|
+
snapshot: {
|
|
313
|
+
key,
|
|
314
|
+
snapshot: { id: (await container.snapshotContainer({ name: `harness-${key}` })).id }
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
} catch {
|
|
318
|
+
return {
|
|
319
|
+
_tag: "ok",
|
|
320
|
+
snapshot: void 0
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
//#endregion
|
|
325
|
+
//#region src/harness/container/harness.ts
|
|
326
|
+
/** The root session's id. */
|
|
327
|
+
const ROOT_SESSION = "root";
|
|
328
|
+
/** Default `idleTimeoutMs`: the container stops five minutes after its last work. */
|
|
329
|
+
const IDLE_TIMEOUT_MS = 5 * 6e4;
|
|
330
|
+
/** Default `startTimeoutMs`: how long a new container may take to answer. */
|
|
331
|
+
const START_TIMEOUT_MS = 6e4;
|
|
332
|
+
/** How long a container that already runs may take to answer before it is replaced. */
|
|
333
|
+
const ADOPT_TIMEOUT_MS = 1e4;
|
|
334
|
+
/** Default `port`: where the daemon listens in the container. */
|
|
335
|
+
const DAEMON_PORT = 8080;
|
|
336
|
+
/**
|
|
337
|
+
* Longest one wake waits on the container. It waits inside an alarm
|
|
338
|
+
* invocation, which has a 15 minute wall-time limit, so a longer run is
|
|
339
|
+
* waited on across several alarms.
|
|
340
|
+
*/
|
|
341
|
+
const WAIT_BUDGET_MS = 10 * 6e4;
|
|
342
|
+
/**
|
|
343
|
+
* The wake job's heartbeat while it waits. If the object is evicted, the
|
|
344
|
+
* job is still due and its alarm restarts the object, which reattaches.
|
|
345
|
+
*/
|
|
346
|
+
const HEARTBEAT_MS = 3e4;
|
|
347
|
+
/** Consecutive failed starts before a session's open work is given up. */
|
|
348
|
+
const MAX_START_FAILURES = 5;
|
|
349
|
+
/** How often a wait checks that the container still runs. */
|
|
350
|
+
const CONTAINER_CHECK_MS = 2e3;
|
|
351
|
+
/** How often `wait()` re-reads the store while it waits. */
|
|
352
|
+
const SETTLE_POLL_MS = 1e3;
|
|
353
|
+
/** Frames between acknowledgements, so the daemon can forget them. */
|
|
354
|
+
const ACK_EVERY = 32;
|
|
355
|
+
const WAKE_FN = "wake";
|
|
356
|
+
const IDLE_FN = "idle";
|
|
357
|
+
const IDLE_JOB = "container-idle";
|
|
358
|
+
const RUNTIME_KEY = "container-harness:runtime";
|
|
359
|
+
const SNAPSHOT_KEY = "container-harness:snapshot";
|
|
360
|
+
const WORKSPACE_KEY = "container-harness:workspace";
|
|
361
|
+
/**
|
|
362
|
+
* How much longer than `idleTimeoutMs` the platform keeps an inactive
|
|
363
|
+
* object's container. The harness's own idle stop runs first, so it can
|
|
364
|
+
* snapshot the workspace before the container goes.
|
|
365
|
+
*/
|
|
366
|
+
const IDLE_GRACE_MS = 2 * 6e4;
|
|
367
|
+
/** The longest inactivity timeout the platform accepts. */
|
|
368
|
+
const MAX_INACTIVITY_MS = 360 * 6e4;
|
|
369
|
+
/**
|
|
370
|
+
* Snapshots live 30 days from their last restore. One unused for nearly
|
|
371
|
+
* that long is treated as gone rather than tried.
|
|
372
|
+
*/
|
|
373
|
+
const SNAPSHOT_STALE_MS = 696 * 60 * 6e4;
|
|
374
|
+
/**
|
|
375
|
+
* The platform does not say why a start failed. A snapshot that fails this
|
|
376
|
+
* many starts in a row is skipped (not deleted) in favour of the next
|
|
377
|
+
* source: whether that one works decides whether the snapshot was at fault.
|
|
378
|
+
*/
|
|
379
|
+
const SKIP_AFTER_FAILURES = 2;
|
|
380
|
+
/** Snapshot sources, most preferred first. */
|
|
381
|
+
const SNAPSHOT_SOURCES = [{
|
|
382
|
+
from: "workspace",
|
|
383
|
+
storageKey: WORKSPACE_KEY
|
|
384
|
+
}, {
|
|
385
|
+
from: "snapshot",
|
|
386
|
+
storageKey: SNAPSHOT_KEY
|
|
387
|
+
}];
|
|
388
|
+
const MESSAGES = "messages";
|
|
389
|
+
const ENGINE = "engine";
|
|
390
|
+
const OPEN = ["queued", "running"];
|
|
391
|
+
function wakeJobId(session) {
|
|
392
|
+
return `container-wake:${session}`;
|
|
393
|
+
}
|
|
394
|
+
function parseWakePayload(payload) {
|
|
395
|
+
if (typeof payload !== "object" || payload === null || !("session" in payload) || typeof payload.session !== "string") return;
|
|
396
|
+
const failures = "failures" in payload && typeof payload.failures === "number" ? payload.failures : 0;
|
|
397
|
+
return {
|
|
398
|
+
session: payload.session,
|
|
399
|
+
failures
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
function isRecord(value) {
|
|
403
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
404
|
+
}
|
|
405
|
+
function parseSessionState(value) {
|
|
406
|
+
const fields = isRecord(value) ? value : {};
|
|
407
|
+
const cursor = fields.cursor;
|
|
408
|
+
return {
|
|
409
|
+
model: typeof fields.model === "string" ? fields.model : void 0,
|
|
410
|
+
cursor: isRecord(cursor) && typeof cursor.runtimeId === "string" && typeof cursor.seq === "number" ? {
|
|
411
|
+
runtimeId: cursor.runtimeId,
|
|
412
|
+
seq: cursor.seq
|
|
413
|
+
} : null,
|
|
414
|
+
generation: typeof fields.generation === "number" ? fields.generation : 1,
|
|
415
|
+
messagesFrom: typeof fields.messagesFrom === "number" ? fields.messagesFrom : 0,
|
|
416
|
+
handoff: typeof fields.handoff === "string" ? fields.handoff : void 0
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
function sessionStateJson(state) {
|
|
420
|
+
return {
|
|
421
|
+
...state.model === void 0 ? {} : { model: state.model },
|
|
422
|
+
cursor: state.cursor,
|
|
423
|
+
generation: state.generation,
|
|
424
|
+
messagesFrom: state.messagesFrom,
|
|
425
|
+
...state.handoff === void 0 ? {} : { handoff: state.handoff }
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
function parseMeta(value) {
|
|
429
|
+
const fields = isRecord(value) ? value : {};
|
|
430
|
+
return {
|
|
431
|
+
whenBusy: fields.whenBusy === "steer" ? "steer" : "followUp",
|
|
432
|
+
delivered: typeof fields.delivered === "string" ? fields.delivered : null
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
function metaJson(meta) {
|
|
436
|
+
return {
|
|
437
|
+
whenBusy: meta.whenBusy,
|
|
438
|
+
delivered: meta.delivered
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
function parseInput(value) {
|
|
442
|
+
if (typeof value === "string") return value;
|
|
443
|
+
return value;
|
|
444
|
+
}
|
|
445
|
+
function inputJson(input) {
|
|
446
|
+
return input;
|
|
447
|
+
}
|
|
448
|
+
function parseMessage(value) {
|
|
449
|
+
return value;
|
|
450
|
+
}
|
|
451
|
+
function messageJson(message) {
|
|
452
|
+
return message;
|
|
453
|
+
}
|
|
454
|
+
function userParts(input) {
|
|
455
|
+
if (typeof input === "string") return [{
|
|
456
|
+
type: "text",
|
|
457
|
+
text: input
|
|
458
|
+
}];
|
|
459
|
+
return input.map((part) => part.type === "text" ? {
|
|
460
|
+
type: "text",
|
|
461
|
+
text: part.text
|
|
462
|
+
} : {
|
|
463
|
+
type: "image",
|
|
464
|
+
mediaType: part.mediaType,
|
|
465
|
+
data: part.data
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
function withHandoff(input, handoff) {
|
|
469
|
+
const note = {
|
|
470
|
+
type: "text",
|
|
471
|
+
text: handoff
|
|
472
|
+
};
|
|
473
|
+
if (typeof input === "string") return [note, {
|
|
474
|
+
type: "text",
|
|
475
|
+
text: input
|
|
476
|
+
}];
|
|
477
|
+
return [note, ...input];
|
|
478
|
+
}
|
|
479
|
+
function errorText(error) {
|
|
480
|
+
return error instanceof Error ? error.message : String(error);
|
|
481
|
+
}
|
|
482
|
+
var _options = /* @__PURE__ */ new WeakMap();
|
|
483
|
+
var _idleTimeoutMs = /* @__PURE__ */ new WeakMap();
|
|
484
|
+
var _startTimeoutMs = /* @__PURE__ */ new WeakMap();
|
|
485
|
+
var _waitBudgetMs = /* @__PURE__ */ new WeakMap();
|
|
486
|
+
var _heartbeatMs = /* @__PURE__ */ new WeakMap();
|
|
487
|
+
var _retryBaseMs = /* @__PURE__ */ new WeakMap();
|
|
488
|
+
var _idleStopMs = /* @__PURE__ */ new WeakMap();
|
|
489
|
+
var _store = /* @__PURE__ */ new WeakMap();
|
|
490
|
+
var _runtime = /* @__PURE__ */ new WeakMap();
|
|
491
|
+
var _starting = /* @__PURE__ */ new WeakMap();
|
|
492
|
+
var _timeoutSet = /* @__PURE__ */ new WeakMap();
|
|
493
|
+
var _attached = /* @__PURE__ */ new WeakMap();
|
|
494
|
+
var _attaching = /* @__PURE__ */ new WeakMap();
|
|
495
|
+
var _waits = /* @__PURE__ */ new WeakMap();
|
|
496
|
+
var _settleWaiters = /* @__PURE__ */ new WeakMap();
|
|
497
|
+
var _changeWaiters = /* @__PURE__ */ new WeakMap();
|
|
498
|
+
var _listeners = /* @__PURE__ */ new WeakMap();
|
|
499
|
+
var _ContainerHarness_brand = /* @__PURE__ */ new WeakSet();
|
|
500
|
+
/**
|
|
501
|
+
* An agent CLI running in a Cloudflare Container, driven from a Durable
|
|
502
|
+
* Object, behind the same interface as `PiHarness`: `harness.prompt()`,
|
|
503
|
+
* `harness.sessions`, `harness.session(id)`.
|
|
504
|
+
*
|
|
505
|
+
* The harness does not know which agent runs in the container. A preset
|
|
506
|
+
* (`claudeCode()`, `codex()`) says what to install and which adapter the
|
|
507
|
+
* daemon runs; the harness builds the container from
|
|
508
|
+
* `cloudflare/debian-trixie` on first use, installs the bundled daemon, and
|
|
509
|
+
* snapshots the result, so there is no image to build. Credentials stay
|
|
510
|
+
* outside: the CLI calls a placeholder host, and `ContainerEgress` adds the
|
|
511
|
+
* real key. Any adapter that produces `ContainerEvent`s works, including
|
|
512
|
+
* one in an image of your own (`containerAgent()`).
|
|
513
|
+
*
|
|
514
|
+
* The object owns the durable record, in a `HarnessStore`: sessions,
|
|
515
|
+
* operations, the transcript, and the adapter's resume state. The container
|
|
516
|
+
* is disposable. It is started on demand with `ctx.container.start()`, kept
|
|
517
|
+
* for `idleTimeoutMs` after its last work, and then stopped. The next
|
|
518
|
+
* prompt starts a new container and hands the adapter everything it
|
|
519
|
+
* persisted, so the agent resumes its own session there.
|
|
520
|
+
*
|
|
521
|
+
* Each session has one Lifecycle wake job. It runs while the session has
|
|
522
|
+
* open operations: it starts or reattaches to the container, reconciles
|
|
523
|
+
* what the container knows with what the object stored, delivers queued
|
|
524
|
+
* operations, and waits for them, rescheduling itself as a heartbeat. An
|
|
525
|
+
* eviction mid-run leaves the job due, so its alarm restarts the object,
|
|
526
|
+
* which reattaches and replays the frames it missed from its cursor.
|
|
527
|
+
*
|
|
528
|
+
* Recovery, by what was lost:
|
|
529
|
+
*
|
|
530
|
+
* - **The object** (eviction, deploy): the container keeps running for
|
|
531
|
+
* `idleTimeoutMs`. The wake reattaches and replays from the cursor.
|
|
532
|
+
* - **The socket**: the same, on the next heartbeat.
|
|
533
|
+
* - **The container** (idle stop, crash, platform restart): a new one
|
|
534
|
+
* answers with a different runtime id. The session is reopened in it
|
|
535
|
+
* from the persisted resume state; queued operations run there; an
|
|
536
|
+
* operation that was running is settled `container_lost` or run again,
|
|
537
|
+
* per `onContainerLost`.
|
|
538
|
+
*
|
|
539
|
+
* @experimental The API may change before it stabilizes.
|
|
540
|
+
*/
|
|
541
|
+
var ContainerHarness = class extends LifecycleCapability {
|
|
542
|
+
/**
|
|
543
|
+
* @param options - The container, the agent, and session defaults.
|
|
544
|
+
*/
|
|
545
|
+
constructor(options) {
|
|
546
|
+
super("container-harness");
|
|
547
|
+
_classPrivateMethodInitSpec(this, _ContainerHarness_brand);
|
|
548
|
+
_classPrivateFieldInitSpec(this, _options, void 0);
|
|
549
|
+
_classPrivateFieldInitSpec(this, _idleTimeoutMs, void 0);
|
|
550
|
+
_classPrivateFieldInitSpec(this, _startTimeoutMs, void 0);
|
|
551
|
+
_classPrivateFieldInitSpec(this, _waitBudgetMs, WAIT_BUDGET_MS);
|
|
552
|
+
_classPrivateFieldInitSpec(this, _heartbeatMs, HEARTBEAT_MS);
|
|
553
|
+
_classPrivateFieldInitSpec(this, _retryBaseMs, 1e3);
|
|
554
|
+
_classPrivateFieldInitSpec(this, _idleStopMs, void 0);
|
|
555
|
+
_classPrivateFieldInitSpec(this, _store, void 0);
|
|
556
|
+
_classPrivateFieldInitSpec(this, _runtime, void 0);
|
|
557
|
+
_classPrivateFieldInitSpec(this, _starting, void 0);
|
|
558
|
+
_classPrivateFieldInitSpec(this, _timeoutSet, false);
|
|
559
|
+
_classPrivateFieldInitSpec(this, _attached, /* @__PURE__ */ new Map());
|
|
560
|
+
_classPrivateFieldInitSpec(this, _attaching, /* @__PURE__ */ new Map());
|
|
561
|
+
_classPrivateFieldInitSpec(this, _waits, /* @__PURE__ */ new Map());
|
|
562
|
+
_classPrivateFieldInitSpec(this, _settleWaiters, /* @__PURE__ */ new Map());
|
|
563
|
+
_classPrivateFieldInitSpec(this, _changeWaiters, /* @__PURE__ */ new Map());
|
|
564
|
+
_classPrivateFieldInitSpec(this, _listeners, /* @__PURE__ */ new Map());
|
|
565
|
+
const idle = options.idleTimeoutMs ?? IDLE_TIMEOUT_MS;
|
|
566
|
+
if (!Number.isFinite(idle) || idle <= 0 || idle > MAX_INACTIVITY_MS - IDLE_GRACE_MS) throw new Error("ContainerHarness idleTimeoutMs must be positive and at most six hours less two minutes");
|
|
567
|
+
for (const route of options.agent.egress) if (!URL.canParse(route.upstream) || new URL(route.upstream).protocol !== "https:") throw new Error(`ContainerHarness: egress upstream for ${route.host} must be an https URL, got ${JSON.stringify(route.upstream)}`);
|
|
568
|
+
if (options.agent.egress.length > 0 && options.egress === void 0) throw new Error("ContainerHarness: the agent's credentials need `egress: ctx.exports.ContainerEgress`, and `export { ContainerEgress } from \"agents/harness/container\"` in your main module");
|
|
569
|
+
_classPrivateFieldSet2(_options, this, options);
|
|
570
|
+
_classPrivateFieldSet2(_idleTimeoutMs, this, idle);
|
|
571
|
+
_classPrivateFieldSet2(_startTimeoutMs, this, options.startTimeoutMs ?? START_TIMEOUT_MS);
|
|
572
|
+
_classPrivateFieldSet2(_idleStopMs, this, idle);
|
|
573
|
+
this.sessions = new ContainerSessions(this);
|
|
574
|
+
}
|
|
575
|
+
/** Reopen the store, re-arm the container's idle timeout, and wake open sessions. */
|
|
576
|
+
async onStart(_context) {
|
|
577
|
+
const store = await _assertClassBrand(_ContainerHarness_brand, this, _openStore).call(this);
|
|
578
|
+
const container = _classPrivateFieldGet2(_options, this).container;
|
|
579
|
+
if (container.running) {
|
|
580
|
+
await container.setInactivityTimeout(_assertClassBrand(_ContainerHarness_brand, this, _inactivityTimeoutMs).call(this));
|
|
581
|
+
_classPrivateFieldSet2(_timeoutSet, this, true);
|
|
582
|
+
}
|
|
583
|
+
const open = store.operations({ status: OPEN });
|
|
584
|
+
const sessions = new Set(open.map((operation) => operation.session));
|
|
585
|
+
for (const session of sessions) if (!this.lifecycle.jobs.get(wakeJobId(session))) await _assertClassBrand(_ContainerHarness_brand, this, _wake).call(this, session);
|
|
586
|
+
if (sessions.size === 0 && container.running) await _assertClassBrand(_ContainerHarness_brand, this, _scheduleIdle).call(this);
|
|
587
|
+
}
|
|
588
|
+
/** Dispatch the harness's jobs: one wake per session, and the idle stop. */
|
|
589
|
+
async onJob(context) {
|
|
590
|
+
if (context.job.fn === IDLE_FN) return _assertClassBrand(_ContainerHarness_brand, this, _idleStep).call(this);
|
|
591
|
+
if (context.job.fn !== WAKE_FN) return;
|
|
592
|
+
const wake = parseWakePayload(context.job.payload);
|
|
593
|
+
if (wake !== void 0) return _assertClassBrand(_ContainerHarness_brand, this, _wakeStep).call(this, wake);
|
|
594
|
+
}
|
|
595
|
+
/** Close the session sockets. Durable state and the container are untouched. */
|
|
596
|
+
async dispose() {
|
|
597
|
+
for (const session of [..._classPrivateFieldGet2(_attached, this).keys()]) _assertClassBrand(_ContainerHarness_brand, this, _detach).call(this, session);
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* A handle on one session. No I/O until you call it.
|
|
601
|
+
*
|
|
602
|
+
* @param id - The session. Default: the root session.
|
|
603
|
+
* @returns The handle.
|
|
604
|
+
*/
|
|
605
|
+
session(id = ROOT_SESSION) {
|
|
606
|
+
return new ContainerSession(this, id);
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* Submit a prompt and wait for its answer.
|
|
610
|
+
*
|
|
611
|
+
* @param input - The prompt.
|
|
612
|
+
* @param options - Session, idempotency key, and busy behaviour.
|
|
613
|
+
* @returns The result, with the session's transcript after it.
|
|
614
|
+
*/
|
|
615
|
+
prompt(input, options = {}) {
|
|
616
|
+
return this.session(options.session).prompt(input, options);
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Durably submit a prompt. Resolves before the agent runs.
|
|
620
|
+
*
|
|
621
|
+
* @param input - The prompt.
|
|
622
|
+
* @param options - Session, idempotency key, and busy behaviour.
|
|
623
|
+
* @returns The receipt.
|
|
624
|
+
*/
|
|
625
|
+
submit(input, options = {}) {
|
|
626
|
+
return this.session(options.session).submit(input, options);
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Stop one operation, or everything running in a session.
|
|
630
|
+
*
|
|
631
|
+
* @param options - The session, and optionally one operation.
|
|
632
|
+
* @returns Whether anything was stopped.
|
|
633
|
+
*/
|
|
634
|
+
abort(options = {}) {
|
|
635
|
+
return this.session(options.session).abort(options.operationId);
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Wait for an operation to settle.
|
|
639
|
+
*
|
|
640
|
+
* @param operationId - The operation.
|
|
641
|
+
* @param options - The session, and a signal that stops only the wait.
|
|
642
|
+
* @returns How it ended.
|
|
643
|
+
*/
|
|
644
|
+
wait(operationId, options = {}) {
|
|
645
|
+
return this.session(options.session).wait(operationId, options.signal);
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* The session's active transcript.
|
|
649
|
+
*
|
|
650
|
+
* @param options - The session.
|
|
651
|
+
* @returns Its messages since the newest reset.
|
|
652
|
+
*/
|
|
653
|
+
messages(options = {}) {
|
|
654
|
+
return this.session(options.session).messages();
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* Submissions that have not settled yet, oldest first.
|
|
658
|
+
*
|
|
659
|
+
* @param options - Narrow to one session. Default: every session.
|
|
660
|
+
* @returns The open operations.
|
|
661
|
+
*/
|
|
662
|
+
async pending(options = {}) {
|
|
663
|
+
return (await _assertClassBrand(_ContainerHarness_brand, this, _openStore).call(this)).operations({
|
|
664
|
+
...options.session === void 0 ? {} : { session: options.session },
|
|
665
|
+
status: OPEN
|
|
666
|
+
}).map((operation) => ({
|
|
667
|
+
operationId: operation.id,
|
|
668
|
+
session: operation.session,
|
|
669
|
+
status: operation.status === "running" ? "running" : "queued"
|
|
670
|
+
}));
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* The container, as the harness sees it.
|
|
674
|
+
*
|
|
675
|
+
* @returns Whether it runs, and the id the harness started it with.
|
|
676
|
+
*/
|
|
677
|
+
async container() {
|
|
678
|
+
const runtime = await _assertClassBrand(_ContainerHarness_brand, this, _runtimeRecord).call(this);
|
|
679
|
+
return {
|
|
680
|
+
running: _classPrivateFieldGet2(_options, this).container.running,
|
|
681
|
+
runtimeId: runtime?.runtimeId
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Stop the container now. Sessions survive: the next prompt starts a new
|
|
686
|
+
* container and resumes them in it. Running operations are reconciled as
|
|
687
|
+
* if the container had died.
|
|
688
|
+
*/
|
|
689
|
+
async stop() {
|
|
690
|
+
await this.dispose();
|
|
691
|
+
const container = _classPrivateFieldGet2(_options, this).container;
|
|
692
|
+
if (container.running) {
|
|
693
|
+
await _assertClassBrand(_ContainerHarness_brand, this, _saveWorkspace).call(this, container);
|
|
694
|
+
await container.destroy();
|
|
695
|
+
}
|
|
696
|
+
_assertClassBrand(_ContainerHarness_brand, this, _broadcast).call(this, {
|
|
697
|
+
type: "container",
|
|
698
|
+
status: "stopped"
|
|
699
|
+
});
|
|
700
|
+
const store = await _assertClassBrand(_ContainerHarness_brand, this, _openStore).call(this);
|
|
701
|
+
const sessions = new Set(store.operations({ status: OPEN }).map((operation) => operation.session));
|
|
702
|
+
for (const session of sessions) await _assertClassBrand(_ContainerHarness_brand, this, _wake).call(this, session);
|
|
703
|
+
}
|
|
704
|
+
/** @internal */
|
|
705
|
+
async store() {
|
|
706
|
+
return _assertClassBrand(_ContainerHarness_brand, this, _openStore).call(this);
|
|
707
|
+
}
|
|
708
|
+
/** @internal Create a session with the defaults, or fork one. */
|
|
709
|
+
async createSession(id, from) {
|
|
710
|
+
const store = await _assertClassBrand(_ContainerHarness_brand, this, _openStore).call(this);
|
|
711
|
+
store.transaction(() => {
|
|
712
|
+
if (from === void 0) {
|
|
713
|
+
store.createSession({
|
|
714
|
+
id,
|
|
715
|
+
state: sessionStateJson(_assertClassBrand(_ContainerHarness_brand, this, _initial).call(this))
|
|
716
|
+
});
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
const parent = store.session(from);
|
|
720
|
+
if (!parent) throw new Error(`Unknown container session ${from}`);
|
|
721
|
+
const parentState = parseSessionState(parent.state);
|
|
722
|
+
store.createSession({
|
|
723
|
+
id,
|
|
724
|
+
parent: from,
|
|
725
|
+
state: sessionStateJson({
|
|
726
|
+
..._assertClassBrand(_ContainerHarness_brand, this, _initial).call(this),
|
|
727
|
+
model: parentState.model
|
|
728
|
+
})
|
|
729
|
+
});
|
|
730
|
+
store.append(id, MESSAGES, store.read(from, MESSAGES, { after: parentState.messagesFrom }).map((entry) => entry.id === void 0 ? { data: entry.data } : {
|
|
731
|
+
id: entry.id,
|
|
732
|
+
data: entry.data
|
|
733
|
+
}));
|
|
734
|
+
store.copy(from, id, ENGINE);
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
/** @internal */
|
|
738
|
+
async enqueue(session, input, options) {
|
|
739
|
+
const store = await _assertClassBrand(_ContainerHarness_brand, this, _openStore).call(this);
|
|
740
|
+
_assertClassBrand(_ContainerHarness_brand, this, _requireSession).call(this, store, session);
|
|
741
|
+
const operationId = options.operationId ?? crypto.randomUUID();
|
|
742
|
+
const meta = {
|
|
743
|
+
whenBusy: options.whenBusy ?? "followUp",
|
|
744
|
+
delivered: null
|
|
745
|
+
};
|
|
746
|
+
const { accepted } = store.enqueue({
|
|
747
|
+
session,
|
|
748
|
+
id: operationId,
|
|
749
|
+
input: inputJson(input),
|
|
750
|
+
meta: metaJson(meta)
|
|
751
|
+
});
|
|
752
|
+
if (accepted) _assertClassBrand(_ContainerHarness_brand, this, _deliver).call(this, store, session);
|
|
753
|
+
await _assertClassBrand(_ContainerHarness_brand, this, _wake).call(this, session);
|
|
754
|
+
return {
|
|
755
|
+
operationId,
|
|
756
|
+
session,
|
|
757
|
+
accepted
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
/** @internal Withdraw or abort one operation, or all of a session's. */
|
|
761
|
+
async withdraw(session, operationId) {
|
|
762
|
+
const store = await _assertClassBrand(_ContainerHarness_brand, this, _openStore).call(this);
|
|
763
|
+
const targets = operationId === void 0 ? store.operations({
|
|
764
|
+
session,
|
|
765
|
+
status: OPEN
|
|
766
|
+
}) : [store.operation(session, operationId)].filter((operation) => operation !== void 0 && (operation.status === "queued" || operation.status === "running"));
|
|
767
|
+
if (targets.length === 0) return operationId === void 0;
|
|
768
|
+
const attached = _classPrivateFieldGet2(_attached, this).get(session);
|
|
769
|
+
if (operationId === void 0) attached?.link.send({ type: "abort" });
|
|
770
|
+
else if (targets.some((each) => parseMeta(each.meta).delivered !== null)) attached?.link.send({
|
|
771
|
+
type: "abort",
|
|
772
|
+
operationId
|
|
773
|
+
});
|
|
774
|
+
for (const operation of targets) _assertClassBrand(_ContainerHarness_brand, this, _settle).call(this, store, session, operation.id, {
|
|
775
|
+
status: "unanswered",
|
|
776
|
+
reason: "aborted"
|
|
777
|
+
});
|
|
778
|
+
return true;
|
|
779
|
+
}
|
|
780
|
+
/** @internal Wait for an operation to settle. */
|
|
781
|
+
async settled(session, operationId, signal) {
|
|
782
|
+
const store = await _assertClassBrand(_ContainerHarness_brand, this, _openStore).call(this);
|
|
783
|
+
for (;;) {
|
|
784
|
+
const operation = store.operation(session, operationId);
|
|
785
|
+
if (!operation) return {
|
|
786
|
+
operationId,
|
|
787
|
+
session,
|
|
788
|
+
status: "unanswered",
|
|
789
|
+
reason: "not_found"
|
|
790
|
+
};
|
|
791
|
+
const result = resultOf(operation);
|
|
792
|
+
if (result) return result;
|
|
793
|
+
if (signal?.aborted) throw signal.reason;
|
|
794
|
+
await _assertClassBrand(_ContainerHarness_brand, this, _until).call(this, _classPrivateFieldGet2(_settleWaiters, this), `${session}\0${operationId}`, signal, SETTLE_POLL_MS);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
/** @internal The active transcript. */
|
|
798
|
+
async transcript(session) {
|
|
799
|
+
const store = await _assertClassBrand(_ContainerHarness_brand, this, _openStore).call(this);
|
|
800
|
+
const state = parseSessionState(_assertClassBrand(_ContainerHarness_brand, this, _requireSession).call(this, store, session).state);
|
|
801
|
+
return store.read(session, MESSAGES, { after: state.messagesFrom }).map((entry) => parseMessage(entry.data));
|
|
802
|
+
}
|
|
803
|
+
/** @internal */
|
|
804
|
+
async busy(session) {
|
|
805
|
+
return (await _assertClassBrand(_ContainerHarness_brand, this, _openStore).call(this)).operations({
|
|
806
|
+
session,
|
|
807
|
+
status: OPEN
|
|
808
|
+
}).length > 0;
|
|
809
|
+
}
|
|
810
|
+
/** @internal Start a new context: a new agent session, without history. */
|
|
811
|
+
async resetSession(session, handoff) {
|
|
812
|
+
const store = await _assertClassBrand(_ContainerHarness_brand, this, _openStore).call(this);
|
|
813
|
+
if (store.operations({
|
|
814
|
+
session,
|
|
815
|
+
status: OPEN
|
|
816
|
+
}).length > 0) throw new Error(`Container session ${session} is busy; abort it first`);
|
|
817
|
+
store.transaction(() => {
|
|
818
|
+
const state = parseSessionState(_assertClassBrand(_ContainerHarness_brand, this, _requireSession).call(this, store, session).state);
|
|
819
|
+
store.clear(session, ENGINE);
|
|
820
|
+
store.setSessionState(session, sessionStateJson({
|
|
821
|
+
...state,
|
|
822
|
+
generation: state.generation + 1,
|
|
823
|
+
messagesFrom: store.end(session, MESSAGES),
|
|
824
|
+
handoff
|
|
825
|
+
}));
|
|
826
|
+
});
|
|
827
|
+
_assertClassBrand(_ContainerHarness_brand, this, _detach).call(this, session);
|
|
828
|
+
}
|
|
829
|
+
/** @internal */
|
|
830
|
+
async setModel(session, model) {
|
|
831
|
+
const store = await _assertClassBrand(_ContainerHarness_brand, this, _openStore).call(this);
|
|
832
|
+
const state = parseSessionState(_assertClassBrand(_ContainerHarness_brand, this, _requireSession).call(this, store, session).state);
|
|
833
|
+
store.setSessionState(session, sessionStateJson({
|
|
834
|
+
...state,
|
|
835
|
+
model
|
|
836
|
+
}));
|
|
837
|
+
_classPrivateFieldGet2(_attached, this).get(session)?.link.send({
|
|
838
|
+
type: "configure",
|
|
839
|
+
settings: _assertClassBrand(_ContainerHarness_brand, this, _settings).call(this, {
|
|
840
|
+
...state,
|
|
841
|
+
model
|
|
842
|
+
})
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
/** @internal */
|
|
846
|
+
async events(session) {
|
|
847
|
+
let buffered = [];
|
|
848
|
+
let listener;
|
|
849
|
+
let stopped = false;
|
|
850
|
+
const relay = (events) => {
|
|
851
|
+
if (buffered) buffered.push(...events);
|
|
852
|
+
else listener?.(events);
|
|
853
|
+
};
|
|
854
|
+
let set = _classPrivateFieldGet2(_listeners, this).get(session);
|
|
855
|
+
if (!set) {
|
|
856
|
+
set = /* @__PURE__ */ new Set();
|
|
857
|
+
_classPrivateFieldGet2(_listeners, this).set(session, set);
|
|
858
|
+
}
|
|
859
|
+
set.add(relay);
|
|
860
|
+
let snapshot;
|
|
861
|
+
try {
|
|
862
|
+
snapshot = {
|
|
863
|
+
session,
|
|
864
|
+
messages: await this.transcript(session),
|
|
865
|
+
pending: await this.pending({ session }),
|
|
866
|
+
busy: await this.busy(session)
|
|
867
|
+
};
|
|
868
|
+
} catch (error) {
|
|
869
|
+
set.delete(relay);
|
|
870
|
+
throw error;
|
|
871
|
+
}
|
|
872
|
+
return {
|
|
873
|
+
snapshot,
|
|
874
|
+
start: (next) => {
|
|
875
|
+
if (listener || stopped) throw new Error("The event stream has already started");
|
|
876
|
+
listener = next;
|
|
877
|
+
const held = buffered ?? [];
|
|
878
|
+
buffered = void 0;
|
|
879
|
+
if (held.length > 0) next(held);
|
|
880
|
+
},
|
|
881
|
+
stop: async () => {
|
|
882
|
+
stopped = true;
|
|
883
|
+
buffered = void 0;
|
|
884
|
+
listener = void 0;
|
|
885
|
+
_classPrivateFieldGet2(_listeners, this).get(session)?.delete(relay);
|
|
886
|
+
}
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
};
|
|
890
|
+
/** Schedule the session's wake job now, or pull it forward. */
|
|
891
|
+
function _wake(session, time = Date.now(), failures = 0) {
|
|
892
|
+
const payload = {
|
|
893
|
+
session,
|
|
894
|
+
failures
|
|
895
|
+
};
|
|
896
|
+
return this.lifecycle.jobs.push({
|
|
897
|
+
id: wakeJobId(session),
|
|
898
|
+
fn: WAKE_FN,
|
|
899
|
+
time,
|
|
900
|
+
payload,
|
|
901
|
+
singleflight: true,
|
|
902
|
+
recoveryLoop: true
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
/**
|
|
906
|
+
* One run of a session's wake job. It never blocks on the container: it
|
|
907
|
+
* hands the drive (start or attach, reconcile, deliver, wait) to the
|
|
908
|
+
* alarm's tracked work and returns a heartbeat. It completes when the
|
|
909
|
+
* session has no open operations.
|
|
910
|
+
*/
|
|
911
|
+
async function _wakeStep(wake) {
|
|
912
|
+
const { session } = wake;
|
|
913
|
+
const heartbeat = { rescheduleAt: Date.now() + _classPrivateFieldGet2(_heartbeatMs, this) };
|
|
914
|
+
if (_classPrivateFieldGet2(_waits, this).has(session)) return heartbeat;
|
|
915
|
+
const store = await _assertClassBrand(_ContainerHarness_brand, this, _openStore).call(this);
|
|
916
|
+
if (!store.session(session)) return void 0;
|
|
917
|
+
if (store.operations({
|
|
918
|
+
session,
|
|
919
|
+
status: OPEN
|
|
920
|
+
}).length === 0) {
|
|
921
|
+
_assertClassBrand(_ContainerHarness_brand, this, _detach).call(this, session);
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
const drive = _assertClassBrand(_ContainerHarness_brand, this, _drive).call(this, store, wake).catch((error) => {
|
|
925
|
+
const detail = {
|
|
926
|
+
session,
|
|
927
|
+
error: errorText(error)
|
|
928
|
+
};
|
|
929
|
+
console.error("[container-harness] drive failed", detail);
|
|
930
|
+
this.lifecycle.events.emit("container:drive_error", detail);
|
|
931
|
+
return {
|
|
932
|
+
at: Date.now() + _classPrivateFieldGet2(_retryBaseMs, this),
|
|
933
|
+
failures: wake.failures
|
|
934
|
+
};
|
|
935
|
+
}).then((next) => {
|
|
936
|
+
_classPrivateFieldGet2(_waits, this).delete(session);
|
|
937
|
+
_assertClassBrand(_ContainerHarness_brand, this, _wake).call(this, session, next.at, next.failures);
|
|
938
|
+
});
|
|
939
|
+
_classPrivateFieldGet2(_waits, this).set(session, drive);
|
|
940
|
+
this.lifecycle.trackAlarmWork(drive);
|
|
941
|
+
return heartbeat;
|
|
942
|
+
}
|
|
943
|
+
/**
|
|
944
|
+
* Attach, deliver, and wait while the session has open work and a live
|
|
945
|
+
* socket. Resolves with when the wake should look again.
|
|
946
|
+
*/
|
|
947
|
+
async function _drive(store, wake) {
|
|
948
|
+
const { session } = wake;
|
|
949
|
+
const failures = wake.failures + 1;
|
|
950
|
+
await _assertClassBrand(_ContainerHarness_brand, this, _wake).call(this, session, Date.now() + _classPrivateFieldGet2(_heartbeatMs, this), failures);
|
|
951
|
+
const attached = await _assertClassBrand(_ContainerHarness_brand, this, _attach).call(this, session);
|
|
952
|
+
if (attached._tag === "err" && attached.error._tag === "retry") {
|
|
953
|
+
await _assertClassBrand(_ContainerHarness_brand, this, _wake).call(this, session, Date.now() + _classPrivateFieldGet2(_heartbeatMs, this), wake.failures);
|
|
954
|
+
return {
|
|
955
|
+
at: Date.now(),
|
|
956
|
+
failures: wake.failures
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
if (attached._tag === "err") {
|
|
960
|
+
const detail = {
|
|
961
|
+
session,
|
|
962
|
+
failures,
|
|
963
|
+
error: attached.error.message
|
|
964
|
+
};
|
|
965
|
+
console.warn("[container-harness] attach failed", detail);
|
|
966
|
+
this.lifecycle.events.emit("container:attach_error", detail);
|
|
967
|
+
if (failures < MAX_START_FAILURES) {
|
|
968
|
+
const backoff = Math.min(_classPrivateFieldGet2(_retryBaseMs, this) * 2 ** (failures - 1), 3e4);
|
|
969
|
+
return {
|
|
970
|
+
at: Date.now() + backoff,
|
|
971
|
+
failures
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
await _assertClassBrand(_ContainerHarness_brand, this, _clearAllFailures).call(this);
|
|
975
|
+
for (const operation of store.operations({
|
|
976
|
+
session,
|
|
977
|
+
status: OPEN
|
|
978
|
+
})) _assertClassBrand(_ContainerHarness_brand, this, _settle).call(this, store, session, operation.id, {
|
|
979
|
+
status: "unanswered",
|
|
980
|
+
reason: "container_unavailable"
|
|
981
|
+
});
|
|
982
|
+
return {
|
|
983
|
+
at: Date.now(),
|
|
984
|
+
failures: 0
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
await _assertClassBrand(_ContainerHarness_brand, this, _wake).call(this, session, Date.now() + _classPrivateFieldGet2(_heartbeatMs, this), 0);
|
|
988
|
+
_assertClassBrand(_ContainerHarness_brand, this, _deliver).call(this, store, session);
|
|
989
|
+
await _assertClassBrand(_ContainerHarness_brand, this, _waitForWork).call(this, store, session);
|
|
990
|
+
return {
|
|
991
|
+
at: Date.now(),
|
|
992
|
+
failures: 0
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
/**
|
|
996
|
+
* Resolves when the session has no open work, its socket closed, the
|
|
997
|
+
* container stopped, or the budget ran out.
|
|
998
|
+
*/
|
|
999
|
+
async function _waitForWork(store, session) {
|
|
1000
|
+
const budget = new AbortController();
|
|
1001
|
+
const timer = setTimeout(() => budget.abort(), _classPrivateFieldGet2(_waitBudgetMs, this));
|
|
1002
|
+
const watch = setInterval(() => {
|
|
1003
|
+
if (!_classPrivateFieldGet2(_options, this).container.running) _assertClassBrand(_ContainerHarness_brand, this, _detach).call(this, session);
|
|
1004
|
+
}, CONTAINER_CHECK_MS);
|
|
1005
|
+
try {
|
|
1006
|
+
while (!budget.signal.aborted && _classPrivateFieldGet2(_attached, this).get(session)?.link.open === true && store.operations({
|
|
1007
|
+
session,
|
|
1008
|
+
status: OPEN
|
|
1009
|
+
}).length > 0) await _assertClassBrand(_ContainerHarness_brand, this, _until).call(this, _classPrivateFieldGet2(_changeWaiters, this), session, budget.signal);
|
|
1010
|
+
} catch {} finally {
|
|
1011
|
+
clearTimeout(timer);
|
|
1012
|
+
clearInterval(watch);
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
/** The platform's inactivity timeout: past the harness's own idle stop. */
|
|
1016
|
+
function _inactivityTimeoutMs() {
|
|
1017
|
+
return _classPrivateFieldGet2(_idleTimeoutMs, this) + IDLE_GRACE_MS;
|
|
1018
|
+
}
|
|
1019
|
+
/** The idle stop: destroy the container once nothing has used it for `idleTimeoutMs`. */
|
|
1020
|
+
async function _idleStep() {
|
|
1021
|
+
if ((await _assertClassBrand(_ContainerHarness_brand, this, _openStore).call(this)).operations({ status: OPEN }).length > 0) return void 0;
|
|
1022
|
+
const container = _classPrivateFieldGet2(_options, this).container;
|
|
1023
|
+
if (!container.running) return void 0;
|
|
1024
|
+
await this.dispose();
|
|
1025
|
+
await _assertClassBrand(_ContainerHarness_brand, this, _saveWorkspace).call(this, container);
|
|
1026
|
+
await container.destroy();
|
|
1027
|
+
_assertClassBrand(_ContainerHarness_brand, this, _broadcast).call(this, {
|
|
1028
|
+
type: "container",
|
|
1029
|
+
status: "stopped"
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
async function _scheduleIdle() {
|
|
1033
|
+
await this.lifecycle.jobs.push({
|
|
1034
|
+
id: IDLE_JOB,
|
|
1035
|
+
fn: IDLE_FN,
|
|
1036
|
+
time: Date.now() + _classPrivateFieldGet2(_idleStopMs, this)
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
async function _runtimeRecord() {
|
|
1040
|
+
if (_classPrivateFieldGet2(_runtime, this)) return _classPrivateFieldGet2(_runtime, this);
|
|
1041
|
+
await this.lifecycle.ready();
|
|
1042
|
+
const stored = await this.lifecycle.storage.get(RUNTIME_KEY);
|
|
1043
|
+
_classPrivateFieldSet2(_runtime, this, stored);
|
|
1044
|
+
return stored;
|
|
1045
|
+
}
|
|
1046
|
+
/** The running container, started if it is not. Single-flight. */
|
|
1047
|
+
function _ensureRuntime() {
|
|
1048
|
+
_classPrivateFieldGet2(_starting, this) ?? _classPrivateFieldSet2(_starting, this, _assertClassBrand(_ContainerHarness_brand, this, _doEnsureRuntime).call(this).finally(() => {
|
|
1049
|
+
_classPrivateFieldSet2(_starting, this, void 0);
|
|
1050
|
+
}));
|
|
1051
|
+
return _classPrivateFieldGet2(_starting, this);
|
|
1052
|
+
}
|
|
1053
|
+
async function _doEnsureRuntime() {
|
|
1054
|
+
const container = _classPrivateFieldGet2(_options, this).container;
|
|
1055
|
+
const known = await _assertClassBrand(_ContainerHarness_brand, this, _runtimeRecord).call(this);
|
|
1056
|
+
if (container.running && known) {
|
|
1057
|
+
if (!_classPrivateFieldGet2(_timeoutSet, this)) {
|
|
1058
|
+
await container.setInactivityTimeout(_assertClassBrand(_ContainerHarness_brand, this, _inactivityTimeoutMs).call(this));
|
|
1059
|
+
_classPrivateFieldSet2(_timeoutSet, this, true);
|
|
1060
|
+
}
|
|
1061
|
+
if ((await waitHealthy(_assertClassBrand(_ContainerHarness_brand, this, _port).call(this), Math.min(_classPrivateFieldGet2(_startTimeoutMs, this), ADOPT_TIMEOUT_MS), () => container.running))._tag === "ok") return {
|
|
1062
|
+
_tag: "ok",
|
|
1063
|
+
value: known
|
|
1064
|
+
};
|
|
1065
|
+
await container.destroy().catch(() => void 0);
|
|
1066
|
+
} else if (container.running) await container.destroy().catch(() => void 0);
|
|
1067
|
+
return _assertClassBrand(_ContainerHarness_brand, this, _launch).call(this, container);
|
|
1068
|
+
}
|
|
1069
|
+
/**
|
|
1070
|
+
* The daemon's port on the container that runs now. Taken fresh each
|
|
1071
|
+
* time: a port taken before `start()` stays bound to no instance.
|
|
1072
|
+
*/
|
|
1073
|
+
function _port() {
|
|
1074
|
+
return _classPrivateFieldGet2(_options, this).container.getTcpPort(_classPrivateFieldGet2(_options, this).port ?? DAEMON_PORT);
|
|
1075
|
+
}
|
|
1076
|
+
/**
|
|
1077
|
+
* Start the agent's container and route its egress. A managed image is
|
|
1078
|
+
* restored from this setup's snapshot when there is one, and set up (and
|
|
1079
|
+
* snapshotted) when there is not.
|
|
1080
|
+
*/
|
|
1081
|
+
async function _start(container, runtime) {
|
|
1082
|
+
const agent = _classPrivateFieldGet2(_options, this).agent;
|
|
1083
|
+
const image = agent.image;
|
|
1084
|
+
const env = {
|
|
1085
|
+
...agent.env,
|
|
1086
|
+
CF_HARNESS_ADAPTER: agent.adapter,
|
|
1087
|
+
[CONTAINER_ENV.token]: runtime.token,
|
|
1088
|
+
[CONTAINER_ENV.runtimeId]: runtime.runtimeId,
|
|
1089
|
+
[CONTAINER_ENV.port]: String(_classPrivateFieldGet2(_options, this).port ?? DAEMON_PORT)
|
|
1090
|
+
};
|
|
1091
|
+
const base = {
|
|
1092
|
+
enableInternet: _classPrivateFieldGet2(_options, this).enableInternet ?? true,
|
|
1093
|
+
instance: _classPrivateFieldGet2(_options, this).instance ?? "standard-1"
|
|
1094
|
+
};
|
|
1095
|
+
let from = "image";
|
|
1096
|
+
let key;
|
|
1097
|
+
let options;
|
|
1098
|
+
const baseKey = await _assertClassBrand(_ContainerHarness_brand, this, _baseKey).call(this);
|
|
1099
|
+
const workspace = await _assertClassBrand(_ContainerHarness_brand, this, _usableSnapshot).call(this, WORKSPACE_KEY, baseKey);
|
|
1100
|
+
if (image.kind === "custom") if (workspace) {
|
|
1101
|
+
from = "workspace";
|
|
1102
|
+
options = {
|
|
1103
|
+
...base,
|
|
1104
|
+
env,
|
|
1105
|
+
containerSnapshot: workspace.snapshot
|
|
1106
|
+
};
|
|
1107
|
+
} else options = {
|
|
1108
|
+
...base,
|
|
1109
|
+
env,
|
|
1110
|
+
image: image.image
|
|
1111
|
+
};
|
|
1112
|
+
else {
|
|
1113
|
+
key = baseKey;
|
|
1114
|
+
const stored = await _assertClassBrand(_ContainerHarness_brand, this, _usableSnapshot).call(this, SNAPSHOT_KEY, key);
|
|
1115
|
+
const managed = {
|
|
1116
|
+
...base,
|
|
1117
|
+
env: {
|
|
1118
|
+
...env,
|
|
1119
|
+
CF_HARNESS_USER: AGENT_USER
|
|
1120
|
+
},
|
|
1121
|
+
entrypoint: [...MANAGED_ENTRYPOINT]
|
|
1122
|
+
};
|
|
1123
|
+
if (workspace) {
|
|
1124
|
+
from = "workspace";
|
|
1125
|
+
options = {
|
|
1126
|
+
...managed,
|
|
1127
|
+
containerSnapshot: workspace.snapshot
|
|
1128
|
+
};
|
|
1129
|
+
} else if (stored) {
|
|
1130
|
+
from = "snapshot";
|
|
1131
|
+
options = {
|
|
1132
|
+
...managed,
|
|
1133
|
+
containerSnapshot: stored.snapshot
|
|
1134
|
+
};
|
|
1135
|
+
} else {
|
|
1136
|
+
from = "setup";
|
|
1137
|
+
options = {
|
|
1138
|
+
...managed,
|
|
1139
|
+
image: MANAGED_IMAGE
|
|
1140
|
+
};
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
try {
|
|
1144
|
+
container.start(options);
|
|
1145
|
+
} catch (error) {
|
|
1146
|
+
await _assertClassBrand(_ContainerHarness_brand, this, _snapshotFailed).call(this, from);
|
|
1147
|
+
return {
|
|
1148
|
+
_tag: "err",
|
|
1149
|
+
error: {
|
|
1150
|
+
_tag: "unreachable",
|
|
1151
|
+
message: errorText(error)
|
|
1152
|
+
}
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
try {
|
|
1156
|
+
await container.setInactivityTimeout(_assertClassBrand(_ContainerHarness_brand, this, _inactivityTimeoutMs).call(this));
|
|
1157
|
+
_classPrivateFieldSet2(_timeoutSet, this, true);
|
|
1158
|
+
const egress = _classPrivateFieldGet2(_options, this).egress;
|
|
1159
|
+
if (egress && agent.egress.length > 0) {
|
|
1160
|
+
const binding = egress({ props: { routes: agent.egress } });
|
|
1161
|
+
for (const route of agent.egress) await container.interceptOutboundHttp(route.host, binding);
|
|
1162
|
+
}
|
|
1163
|
+
} catch (error) {
|
|
1164
|
+
return {
|
|
1165
|
+
_tag: "err",
|
|
1166
|
+
error: {
|
|
1167
|
+
_tag: "unreachable",
|
|
1168
|
+
message: errorText(error)
|
|
1169
|
+
}
|
|
1170
|
+
};
|
|
1171
|
+
}
|
|
1172
|
+
if (from === "setup" && image.kind === "managed" && key !== void 0) {
|
|
1173
|
+
const setup = await setUp(container, image.setup, key);
|
|
1174
|
+
if (setup._tag === "err") return {
|
|
1175
|
+
_tag: "err",
|
|
1176
|
+
error: {
|
|
1177
|
+
_tag: "unreachable",
|
|
1178
|
+
message: setup.error.message
|
|
1179
|
+
}
|
|
1180
|
+
};
|
|
1181
|
+
if (setup.snapshot) {
|
|
1182
|
+
const record = {
|
|
1183
|
+
...setup.snapshot,
|
|
1184
|
+
usedAt: Date.now()
|
|
1185
|
+
};
|
|
1186
|
+
await this.lifecycle.storage.put(SNAPSHOT_KEY, record);
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
return {
|
|
1190
|
+
_tag: "ok",
|
|
1191
|
+
value: from
|
|
1192
|
+
};
|
|
1193
|
+
}
|
|
1194
|
+
/**
|
|
1195
|
+
* The stored snapshot for `key`, unless it is for another setup, too old
|
|
1196
|
+
* to restore (deleted), or failing (skipped for now, kept).
|
|
1197
|
+
*/
|
|
1198
|
+
async function _usableSnapshot(storageKey, key) {
|
|
1199
|
+
const record = await this.lifecycle.storage.get(storageKey);
|
|
1200
|
+
if (!record || record.key !== key) return void 0;
|
|
1201
|
+
if (Date.now() - (record.usedAt ?? Date.now()) > SNAPSHOT_STALE_MS) {
|
|
1202
|
+
await this.lifecycle.storage.delete(storageKey);
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
1205
|
+
if (record.probe) return record;
|
|
1206
|
+
if ((record.failures?.count ?? 0) >= SKIP_AFTER_FAILURES) return void 0;
|
|
1207
|
+
return record;
|
|
1208
|
+
}
|
|
1209
|
+
/** Index of a start source in `SNAPSHOT_SOURCES`; the image or setup is last. */
|
|
1210
|
+
function _rank(from) {
|
|
1211
|
+
const index = SNAPSHOT_SOURCES.findIndex((source) => source.from === from);
|
|
1212
|
+
return index === -1 ? SNAPSHOT_SOURCES.length : index;
|
|
1213
|
+
}
|
|
1214
|
+
/**
|
|
1215
|
+
* A start failed. Count it against the snapshot it came from. A
|
|
1216
|
+
* preferred snapshot being skipped is not to blame when its fallback
|
|
1217
|
+
* fails too (no capacity, say): its count is cleared, so it is tried
|
|
1218
|
+
* again.
|
|
1219
|
+
*/
|
|
1220
|
+
async function _snapshotFailed(from) {
|
|
1221
|
+
const rank = _assertClassBrand(_ContainerHarness_brand, this, _rank).call(this, from);
|
|
1222
|
+
for (const [index, source] of SNAPSHOT_SOURCES.entries()) {
|
|
1223
|
+
const record = await this.lifecycle.storage.get(source.storageKey);
|
|
1224
|
+
if (!record) continue;
|
|
1225
|
+
if (index === rank && record.probe) {
|
|
1226
|
+
console.warn(`[container-harness] dropping the ${source.from} snapshot: it no longer starts`);
|
|
1227
|
+
await this.lifecycle.storage.delete(source.storageKey);
|
|
1228
|
+
} else if (index === rank) {
|
|
1229
|
+
const now = Date.now();
|
|
1230
|
+
const failed = {
|
|
1231
|
+
...record,
|
|
1232
|
+
failures: {
|
|
1233
|
+
count: (record.failures?.count ?? 0) + 1,
|
|
1234
|
+
since: record.failures?.since ?? now
|
|
1235
|
+
}
|
|
1236
|
+
};
|
|
1237
|
+
await this.lifecycle.storage.put(source.storageKey, failed);
|
|
1238
|
+
} else if (index < rank && record.failures) await _assertClassBrand(_ContainerHarness_brand, this, _clearFailures).call(this, source.storageKey, record);
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
/**
|
|
1242
|
+
* A start came up. Its snapshot's lifetime restarts and its failures
|
|
1243
|
+
* clear. A preferred snapshot that was being skipped is not proven
|
|
1244
|
+
* broken by this (the failures may have been an outage that has just
|
|
1245
|
+
* ended): it is marked to be tried once more, and the caller stops this
|
|
1246
|
+
* container to do so. Returns whether it should.
|
|
1247
|
+
*/
|
|
1248
|
+
async function _snapshotWorked(from) {
|
|
1249
|
+
const rank = _assertClassBrand(_ContainerHarness_brand, this, _rank).call(this, from);
|
|
1250
|
+
let retry = false;
|
|
1251
|
+
for (const [index, source] of SNAPSHOT_SOURCES.entries()) {
|
|
1252
|
+
const record = await this.lifecycle.storage.get(source.storageKey);
|
|
1253
|
+
if (!record) continue;
|
|
1254
|
+
if (index === rank) {
|
|
1255
|
+
const worked = {
|
|
1256
|
+
key: record.key,
|
|
1257
|
+
snapshot: record.snapshot,
|
|
1258
|
+
usedAt: Date.now()
|
|
1259
|
+
};
|
|
1260
|
+
await this.lifecycle.storage.put(source.storageKey, worked);
|
|
1261
|
+
} else if (index < rank && !record.probe && (record.failures?.count ?? 0) >= SKIP_AFTER_FAILURES) {
|
|
1262
|
+
const probe = {
|
|
1263
|
+
key: record.key,
|
|
1264
|
+
snapshot: record.snapshot,
|
|
1265
|
+
...record.usedAt === void 0 ? {} : { usedAt: record.usedAt },
|
|
1266
|
+
probe: true
|
|
1267
|
+
};
|
|
1268
|
+
await this.lifecycle.storage.put(source.storageKey, probe);
|
|
1269
|
+
retry = true;
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
return retry;
|
|
1273
|
+
}
|
|
1274
|
+
/** Forget every snapshot's failures: nothing could be concluded from them. */
|
|
1275
|
+
async function _clearAllFailures() {
|
|
1276
|
+
for (const source of SNAPSHOT_SOURCES) {
|
|
1277
|
+
const record = await this.lifecycle.storage.get(source.storageKey);
|
|
1278
|
+
if (record?.failures || record?.probe) await _assertClassBrand(_ContainerHarness_brand, this, _clearFailures).call(this, source.storageKey, record);
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
async function _clearFailures(storageKey, record) {
|
|
1282
|
+
const cleared = {
|
|
1283
|
+
key: record.key,
|
|
1284
|
+
snapshot: record.snapshot,
|
|
1285
|
+
...record.usedAt === void 0 ? {} : { usedAt: record.usedAt }
|
|
1286
|
+
};
|
|
1287
|
+
await this.lifecycle.storage.put(storageKey, cleared);
|
|
1288
|
+
}
|
|
1289
|
+
/** What a container's filesystem is built from: the setup, or the image. */
|
|
1290
|
+
async function _baseKey() {
|
|
1291
|
+
const image = _classPrivateFieldGet2(_options, this).agent.image;
|
|
1292
|
+
return image.kind === "managed" ? setupKey(image.setup) : `image:${image.image}`;
|
|
1293
|
+
}
|
|
1294
|
+
/**
|
|
1295
|
+
* Snapshot the container's filesystem before the harness stops it, so the
|
|
1296
|
+
* next container starts with the workspace as this one left it. A failed
|
|
1297
|
+
* snapshot costs the workspace edits, never the stop.
|
|
1298
|
+
*/
|
|
1299
|
+
async function _saveWorkspace(container) {
|
|
1300
|
+
try {
|
|
1301
|
+
const snapshot = await container.snapshotContainer({ name: `workspace-${Date.now()}` });
|
|
1302
|
+
const saved = {
|
|
1303
|
+
key: await _assertClassBrand(_ContainerHarness_brand, this, _baseKey).call(this),
|
|
1304
|
+
snapshot: { id: snapshot.id },
|
|
1305
|
+
usedAt: Date.now()
|
|
1306
|
+
};
|
|
1307
|
+
await this.lifecycle.storage.put(WORKSPACE_KEY, saved);
|
|
1308
|
+
} catch (error) {
|
|
1309
|
+
await this.lifecycle.storage.delete(WORKSPACE_KEY);
|
|
1310
|
+
console.warn("[container-harness] workspace snapshot failed; the next container starts without this workspace", { error: errorText(error) });
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
async function _launch(container) {
|
|
1314
|
+
const runtime = {
|
|
1315
|
+
runtimeId: crypto.randomUUID(),
|
|
1316
|
+
token: `${crypto.randomUUID()}${crypto.randomUUID()}`.replaceAll("-", "")
|
|
1317
|
+
};
|
|
1318
|
+
await this.lifecycle.storage.put(RUNTIME_KEY, runtime);
|
|
1319
|
+
_classPrivateFieldSet2(_runtime, this, runtime);
|
|
1320
|
+
_assertClassBrand(_ContainerHarness_brand, this, _broadcast).call(this, {
|
|
1321
|
+
type: "container",
|
|
1322
|
+
status: "starting"
|
|
1323
|
+
});
|
|
1324
|
+
const started = await _assertClassBrand(_ContainerHarness_brand, this, _start).call(this, container, runtime);
|
|
1325
|
+
if (started._tag === "err") {
|
|
1326
|
+
await container.destroy().catch(() => void 0);
|
|
1327
|
+
return started;
|
|
1328
|
+
}
|
|
1329
|
+
const healthy = await waitHealthy(_assertClassBrand(_ContainerHarness_brand, this, _port).call(this), _classPrivateFieldGet2(_startTimeoutMs, this), () => container.running);
|
|
1330
|
+
if (healthy._tag === "err") {
|
|
1331
|
+
await _assertClassBrand(_ContainerHarness_brand, this, _snapshotFailed).call(this, started.value);
|
|
1332
|
+
await container.destroy().catch(() => void 0);
|
|
1333
|
+
return healthy;
|
|
1334
|
+
}
|
|
1335
|
+
if (await _assertClassBrand(_ContainerHarness_brand, this, _snapshotWorked).call(this, started.value)) {
|
|
1336
|
+
await container.destroy().catch(() => void 0);
|
|
1337
|
+
return {
|
|
1338
|
+
_tag: "err",
|
|
1339
|
+
error: {
|
|
1340
|
+
_tag: "retry",
|
|
1341
|
+
message: "retrying a snapshot skipped after failed starts"
|
|
1342
|
+
}
|
|
1343
|
+
};
|
|
1344
|
+
}
|
|
1345
|
+
_assertClassBrand(_ContainerHarness_brand, this, _broadcast).call(this, {
|
|
1346
|
+
type: "container",
|
|
1347
|
+
status: "ready"
|
|
1348
|
+
});
|
|
1349
|
+
return {
|
|
1350
|
+
_tag: "ok",
|
|
1351
|
+
value: runtime
|
|
1352
|
+
};
|
|
1353
|
+
}
|
|
1354
|
+
/** The session's socket, opened and reconciled if it is not. Single-flight. */
|
|
1355
|
+
function _attach(session) {
|
|
1356
|
+
const attached = _classPrivateFieldGet2(_attached, this).get(session);
|
|
1357
|
+
if (attached?.link.open) return Promise.resolve({
|
|
1358
|
+
_tag: "ok",
|
|
1359
|
+
value: attached
|
|
1360
|
+
});
|
|
1361
|
+
let attaching = _classPrivateFieldGet2(_attaching, this).get(session);
|
|
1362
|
+
if (!attaching) {
|
|
1363
|
+
attaching = _assertClassBrand(_ContainerHarness_brand, this, _doAttach).call(this, session).finally(() => {
|
|
1364
|
+
_classPrivateFieldGet2(_attaching, this).delete(session);
|
|
1365
|
+
});
|
|
1366
|
+
_classPrivateFieldGet2(_attaching, this).set(session, attaching);
|
|
1367
|
+
}
|
|
1368
|
+
return attaching;
|
|
1369
|
+
}
|
|
1370
|
+
async function _doAttach(session) {
|
|
1371
|
+
const runtime = await _assertClassBrand(_ContainerHarness_brand, this, _ensureRuntime).call(this);
|
|
1372
|
+
if (runtime._tag === "err") return runtime;
|
|
1373
|
+
const port = _assertClassBrand(_ContainerHarness_brand, this, _port).call(this);
|
|
1374
|
+
let ready;
|
|
1375
|
+
const early = [];
|
|
1376
|
+
let caughtUpEarly = false;
|
|
1377
|
+
const opened = await DaemonLink.open(port, session, runtime.value.token, {
|
|
1378
|
+
frame: (seq, frame) => {
|
|
1379
|
+
if (ready) _assertClassBrand(_ContainerHarness_brand, this, _onFrame).call(this, session, ready, seq, frame);
|
|
1380
|
+
else early.push({
|
|
1381
|
+
seq,
|
|
1382
|
+
frame
|
|
1383
|
+
});
|
|
1384
|
+
},
|
|
1385
|
+
caughtUp: () => {
|
|
1386
|
+
if (ready) _assertClassBrand(_ContainerHarness_brand, this, _onCaughtUp).call(this, session, ready);
|
|
1387
|
+
else caughtUpEarly = true;
|
|
1388
|
+
},
|
|
1389
|
+
closed: () => _assertClassBrand(_ContainerHarness_brand, this, _onClosed).call(this, session, ready)
|
|
1390
|
+
});
|
|
1391
|
+
if (opened._tag === "err") return opened;
|
|
1392
|
+
const { link, hello } = opened.value;
|
|
1393
|
+
const attached = {
|
|
1394
|
+
link,
|
|
1395
|
+
runtimeId: hello.runtimeId,
|
|
1396
|
+
sinceAck: 0,
|
|
1397
|
+
settledThere: []
|
|
1398
|
+
};
|
|
1399
|
+
if (hello.runtimeId !== runtime.value.runtimeId) {
|
|
1400
|
+
const adopted = {
|
|
1401
|
+
...runtime.value,
|
|
1402
|
+
runtimeId: hello.runtimeId
|
|
1403
|
+
};
|
|
1404
|
+
await this.lifecycle.storage.put(RUNTIME_KEY, adopted);
|
|
1405
|
+
_classPrivateFieldSet2(_runtime, this, adopted);
|
|
1406
|
+
}
|
|
1407
|
+
const store = await _assertClassBrand(_ContainerHarness_brand, this, _openStore).call(this);
|
|
1408
|
+
_assertClassBrand(_ContainerHarness_brand, this, _reconcile).call(this, store, session, attached, hello);
|
|
1409
|
+
_classPrivateFieldGet2(_attached, this).get(session)?.link.close();
|
|
1410
|
+
_classPrivateFieldGet2(_attached, this).set(session, attached);
|
|
1411
|
+
ready = attached;
|
|
1412
|
+
for (const each of early) _assertClassBrand(_ContainerHarness_brand, this, _onFrame).call(this, session, attached, each.seq, each.frame);
|
|
1413
|
+
if (caughtUpEarly) _assertClassBrand(_ContainerHarness_brand, this, _onCaughtUp).call(this, session, attached);
|
|
1414
|
+
return {
|
|
1415
|
+
_tag: "ok",
|
|
1416
|
+
value: attached
|
|
1417
|
+
};
|
|
1418
|
+
}
|
|
1419
|
+
/**
|
|
1420
|
+
* The replay is complete. Operations the container reported settled in
|
|
1421
|
+
* `hello` whose frames it no longer kept are settled from the report now,
|
|
1422
|
+
* after every frame it did keep, so their messages land first.
|
|
1423
|
+
*/
|
|
1424
|
+
function _onCaughtUp(session, attached) {
|
|
1425
|
+
const store = _classPrivateFieldGet2(_store, this);
|
|
1426
|
+
if (!store) return;
|
|
1427
|
+
for (const { operationId, outcome } of attached.settledThere.splice(0)) _assertClassBrand(_ContainerHarness_brand, this, _settle).call(this, store, session, operationId, outcome);
|
|
1428
|
+
}
|
|
1429
|
+
/**
|
|
1430
|
+
* Make the container's view of a session and the object's agree, before
|
|
1431
|
+
* anything is delivered: open the adapter session if the container does
|
|
1432
|
+
* not hold it, settle what the container already settled, decide what to
|
|
1433
|
+
* do with work a lost container took with it, abort what the container
|
|
1434
|
+
* still runs but the object has given up on, and replay missed frames.
|
|
1435
|
+
*/
|
|
1436
|
+
function _reconcile(store, session, attached, hello) {
|
|
1437
|
+
const { link } = attached;
|
|
1438
|
+
let state = parseSessionState(_assertClassBrand(_ContainerHarness_brand, this, _requireSession).call(this, store, session).state);
|
|
1439
|
+
const sameRuntime = state.cursor?.runtimeId === hello.runtimeId;
|
|
1440
|
+
let lostAny = false;
|
|
1441
|
+
if (hello.open !== state.generation) {
|
|
1442
|
+
const groups = chunkEntries(store.read(session, ENGINE).map((entry) => entry.data));
|
|
1443
|
+
const last = groups.pop() ?? [];
|
|
1444
|
+
for (const entries of groups) link.send({
|
|
1445
|
+
type: "restore",
|
|
1446
|
+
generation: state.generation,
|
|
1447
|
+
entries
|
|
1448
|
+
});
|
|
1449
|
+
link.send({
|
|
1450
|
+
type: "open",
|
|
1451
|
+
generation: state.generation,
|
|
1452
|
+
settings: _assertClassBrand(_ContainerHarness_brand, this, _settings).call(this, state),
|
|
1453
|
+
restore: last
|
|
1454
|
+
});
|
|
1455
|
+
}
|
|
1456
|
+
const known = new Map(hello.operations.map((operation) => [operation.operationId, operation]));
|
|
1457
|
+
for (const operation of store.operations({
|
|
1458
|
+
session,
|
|
1459
|
+
status: OPEN
|
|
1460
|
+
})) {
|
|
1461
|
+
const there = known.get(operation.id);
|
|
1462
|
+
if (there?.status === "settled") {
|
|
1463
|
+
attached.settledThere.push({
|
|
1464
|
+
operationId: operation.id,
|
|
1465
|
+
outcome: there.outcome
|
|
1466
|
+
});
|
|
1467
|
+
continue;
|
|
1468
|
+
}
|
|
1469
|
+
if (there) continue;
|
|
1470
|
+
const meta = parseMeta(operation.meta);
|
|
1471
|
+
if (!(operation.status === "running" && meta.delivered !== hello.runtimeId)) {
|
|
1472
|
+
if (meta.delivered !== null) store.setOperationMeta(session, operation.id, metaJson({
|
|
1473
|
+
...meta,
|
|
1474
|
+
delivered: null
|
|
1475
|
+
}));
|
|
1476
|
+
continue;
|
|
1477
|
+
}
|
|
1478
|
+
lostAny = true;
|
|
1479
|
+
if (_classPrivateFieldGet2(_options, this).onContainerLost === "retry") store.transaction(() => {
|
|
1480
|
+
store.requeue(session, operation.id);
|
|
1481
|
+
store.setOperationMeta(session, operation.id, metaJson({
|
|
1482
|
+
...meta,
|
|
1483
|
+
delivered: null
|
|
1484
|
+
}));
|
|
1485
|
+
});
|
|
1486
|
+
else _assertClassBrand(_ContainerHarness_brand, this, _settle).call(this, store, session, operation.id, {
|
|
1487
|
+
status: "unanswered",
|
|
1488
|
+
reason: "container_lost"
|
|
1489
|
+
});
|
|
1490
|
+
}
|
|
1491
|
+
for (const there of hello.operations) {
|
|
1492
|
+
if (there.status === "settled") continue;
|
|
1493
|
+
const operation = store.operation(session, there.operationId);
|
|
1494
|
+
if (!operation || operation.status === "done" || operation.status === "unanswered") link.send({
|
|
1495
|
+
type: "abort",
|
|
1496
|
+
operationId: there.operationId
|
|
1497
|
+
});
|
|
1498
|
+
}
|
|
1499
|
+
if (lostAny) _assertClassBrand(_ContainerHarness_brand, this, _emit).call(this, session, [{
|
|
1500
|
+
type: "container",
|
|
1501
|
+
status: "lost"
|
|
1502
|
+
}]);
|
|
1503
|
+
if (!sameRuntime) {
|
|
1504
|
+
state = {
|
|
1505
|
+
...state,
|
|
1506
|
+
cursor: {
|
|
1507
|
+
runtimeId: hello.runtimeId,
|
|
1508
|
+
seq: 0
|
|
1509
|
+
}
|
|
1510
|
+
};
|
|
1511
|
+
store.setSessionState(session, sessionStateJson(state));
|
|
1512
|
+
}
|
|
1513
|
+
link.send({
|
|
1514
|
+
type: "replay",
|
|
1515
|
+
after: state.cursor?.seq ?? 0
|
|
1516
|
+
});
|
|
1517
|
+
}
|
|
1518
|
+
/** Hand every undelivered open operation of the session to the container. */
|
|
1519
|
+
function _deliver(store, session) {
|
|
1520
|
+
const attached = _classPrivateFieldGet2(_attached, this).get(session);
|
|
1521
|
+
if (!attached?.link.open) return;
|
|
1522
|
+
for (const operation of store.operations({
|
|
1523
|
+
session,
|
|
1524
|
+
status: OPEN
|
|
1525
|
+
})) {
|
|
1526
|
+
const meta = parseMeta(operation.meta);
|
|
1527
|
+
if (meta.delivered === attached.runtimeId) continue;
|
|
1528
|
+
let input = parseInput(operation.input);
|
|
1529
|
+
const state = parseSessionState(_assertClassBrand(_ContainerHarness_brand, this, _requireSession).call(this, store, session).state);
|
|
1530
|
+
if (state.handoff !== void 0) input = withHandoff(input, state.handoff);
|
|
1531
|
+
if (!attached.link.send({
|
|
1532
|
+
type: "prompt",
|
|
1533
|
+
operationId: operation.id,
|
|
1534
|
+
input,
|
|
1535
|
+
whenBusy: meta.whenBusy
|
|
1536
|
+
})) return;
|
|
1537
|
+
store.transaction(() => {
|
|
1538
|
+
store.setOperationMeta(session, operation.id, metaJson({
|
|
1539
|
+
...meta,
|
|
1540
|
+
delivered: attached.runtimeId
|
|
1541
|
+
}));
|
|
1542
|
+
if (state.handoff !== void 0) store.setSessionState(session, sessionStateJson({
|
|
1543
|
+
...state,
|
|
1544
|
+
handoff: void 0
|
|
1545
|
+
}));
|
|
1546
|
+
});
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
/** Apply one frame from the container. */
|
|
1550
|
+
function _onFrame(session, attached, seq, frame) {
|
|
1551
|
+
const store = _classPrivateFieldGet2(_store, this);
|
|
1552
|
+
if (!store) return;
|
|
1553
|
+
const events = [];
|
|
1554
|
+
let settled;
|
|
1555
|
+
store.transaction(() => {
|
|
1556
|
+
const record = store.session(session);
|
|
1557
|
+
if (!record) return;
|
|
1558
|
+
let state = parseSessionState(record.state);
|
|
1559
|
+
if (state.cursor?.runtimeId === attached.runtimeId && seq <= state.cursor.seq) return;
|
|
1560
|
+
switch (frame.kind) {
|
|
1561
|
+
case "start":
|
|
1562
|
+
if (store.start(session, frame.operationId)) {
|
|
1563
|
+
const operation = store.operation(session, frame.operationId);
|
|
1564
|
+
if (operation) {
|
|
1565
|
+
const message = {
|
|
1566
|
+
id: `${operation.id}:user`,
|
|
1567
|
+
role: "user",
|
|
1568
|
+
parts: userParts(parseInput(operation.input)),
|
|
1569
|
+
operationId: operation.id,
|
|
1570
|
+
createdAt: operation.createdAt
|
|
1571
|
+
};
|
|
1572
|
+
store.append(session, MESSAGES, [{
|
|
1573
|
+
id: message.id,
|
|
1574
|
+
data: messageJson(message)
|
|
1575
|
+
}]);
|
|
1576
|
+
events.push({
|
|
1577
|
+
type: "message",
|
|
1578
|
+
message,
|
|
1579
|
+
operationId: operation.id
|
|
1580
|
+
});
|
|
1581
|
+
}
|
|
1582
|
+
events.push({
|
|
1583
|
+
type: "operation-start",
|
|
1584
|
+
operationId: frame.operationId
|
|
1585
|
+
});
|
|
1586
|
+
}
|
|
1587
|
+
break;
|
|
1588
|
+
case "event":
|
|
1589
|
+
events.push({
|
|
1590
|
+
...frame.event,
|
|
1591
|
+
operationId: frame.operationId
|
|
1592
|
+
});
|
|
1593
|
+
if (frame.event.type === "message") store.append(session, MESSAGES, [{
|
|
1594
|
+
id: frame.event.message.id,
|
|
1595
|
+
data: messageJson(frame.event.message)
|
|
1596
|
+
}]);
|
|
1597
|
+
break;
|
|
1598
|
+
case "persist":
|
|
1599
|
+
store.append(session, ENGINE, frame.entries.map((data) => ({ data })));
|
|
1600
|
+
break;
|
|
1601
|
+
case "settle":
|
|
1602
|
+
settled = _assertClassBrand(_ContainerHarness_brand, this, _settleInStore).call(this, store, session, frame.operationId, frame.outcome);
|
|
1603
|
+
break;
|
|
1604
|
+
}
|
|
1605
|
+
state = {
|
|
1606
|
+
...state,
|
|
1607
|
+
cursor: {
|
|
1608
|
+
runtimeId: attached.runtimeId,
|
|
1609
|
+
seq
|
|
1610
|
+
}
|
|
1611
|
+
};
|
|
1612
|
+
store.setSessionState(session, sessionStateJson(state));
|
|
1613
|
+
});
|
|
1614
|
+
if (settled) events.push({
|
|
1615
|
+
type: "operation-end",
|
|
1616
|
+
result: settled
|
|
1617
|
+
});
|
|
1618
|
+
_assertClassBrand(_ContainerHarness_brand, this, _emit).call(this, session, events);
|
|
1619
|
+
attached.sinceAck += 1;
|
|
1620
|
+
if (settled || attached.sinceAck >= ACK_EVERY) {
|
|
1621
|
+
attached.sinceAck = 0;
|
|
1622
|
+
attached.link.send({
|
|
1623
|
+
type: "ack",
|
|
1624
|
+
seq
|
|
1625
|
+
});
|
|
1626
|
+
}
|
|
1627
|
+
if (settled) _assertClassBrand(_ContainerHarness_brand, this, _afterSettle).call(this, store, session, settled);
|
|
1628
|
+
}
|
|
1629
|
+
function _onClosed(session, attached) {
|
|
1630
|
+
if (attached && _classPrivateFieldGet2(_attached, this).get(session) === attached) _classPrivateFieldGet2(_attached, this).delete(session);
|
|
1631
|
+
_assertClassBrand(_ContainerHarness_brand, this, _notify).call(this, _classPrivateFieldGet2(_changeWaiters, this), session);
|
|
1632
|
+
}
|
|
1633
|
+
function _detach(session) {
|
|
1634
|
+
const attached = _classPrivateFieldGet2(_attached, this).get(session);
|
|
1635
|
+
_classPrivateFieldGet2(_attached, this).delete(session);
|
|
1636
|
+
attached?.link.close();
|
|
1637
|
+
_assertClassBrand(_ContainerHarness_brand, this, _notify).call(this, _classPrivateFieldGet2(_changeWaiters, this), session);
|
|
1638
|
+
}
|
|
1639
|
+
function _settleInStore(store, session, operationId, outcome) {
|
|
1640
|
+
if (!store.settle(session, operationId, outcome.status === "done" ? {
|
|
1641
|
+
status: "done",
|
|
1642
|
+
result: { text: outcome.text }
|
|
1643
|
+
} : outcome)) return void 0;
|
|
1644
|
+
const operation = store.operation(session, operationId);
|
|
1645
|
+
return operation ? resultOf(operation) : void 0;
|
|
1646
|
+
}
|
|
1647
|
+
/** Settle outside a frame, and tell everyone. */
|
|
1648
|
+
function _settle(store, session, operationId, outcome) {
|
|
1649
|
+
const result = _assertClassBrand(_ContainerHarness_brand, this, _settleInStore).call(this, store, session, operationId, outcome);
|
|
1650
|
+
if (!result) return;
|
|
1651
|
+
_assertClassBrand(_ContainerHarness_brand, this, _emit).call(this, session, [{
|
|
1652
|
+
type: "operation-end",
|
|
1653
|
+
result
|
|
1654
|
+
}]);
|
|
1655
|
+
_assertClassBrand(_ContainerHarness_brand, this, _afterSettle).call(this, store, session, result);
|
|
1656
|
+
}
|
|
1657
|
+
function _afterSettle(store, session, result) {
|
|
1658
|
+
_assertClassBrand(_ContainerHarness_brand, this, _notify).call(this, _classPrivateFieldGet2(_settleWaiters, this), `${session}\0${result.operationId}`);
|
|
1659
|
+
_assertClassBrand(_ContainerHarness_brand, this, _notify).call(this, _classPrivateFieldGet2(_changeWaiters, this), session);
|
|
1660
|
+
if (store.operations({ status: OPEN }).length === 0) _assertClassBrand(_ContainerHarness_brand, this, _scheduleIdle).call(this).catch(() => void 0);
|
|
1661
|
+
}
|
|
1662
|
+
async function _openStore() {
|
|
1663
|
+
if (_classPrivateFieldGet2(_store, this)) return _classPrivateFieldGet2(_store, this);
|
|
1664
|
+
await this.lifecycle.ready();
|
|
1665
|
+
if (_classPrivateFieldGet2(_store, this)) return _classPrivateFieldGet2(_store, this);
|
|
1666
|
+
const store = openHarnessStore(this.lifecycle.storage, { prefix: _classPrivateFieldGet2(_options, this).storePrefix ?? "container_" });
|
|
1667
|
+
store.createSession({
|
|
1668
|
+
id: ROOT_SESSION,
|
|
1669
|
+
state: sessionStateJson(_assertClassBrand(_ContainerHarness_brand, this, _initial).call(this))
|
|
1670
|
+
});
|
|
1671
|
+
_classPrivateFieldSet2(_store, this, store);
|
|
1672
|
+
return store;
|
|
1673
|
+
}
|
|
1674
|
+
function _initial() {
|
|
1675
|
+
return {
|
|
1676
|
+
model: _classPrivateFieldGet2(_options, this).defaults?.model ?? _classPrivateFieldGet2(_options, this).agent.model,
|
|
1677
|
+
cursor: null,
|
|
1678
|
+
generation: 1,
|
|
1679
|
+
messagesFrom: 0,
|
|
1680
|
+
handoff: void 0
|
|
1681
|
+
};
|
|
1682
|
+
}
|
|
1683
|
+
function _settings(state) {
|
|
1684
|
+
const options = _classPrivateFieldGet2(_options, this).defaults?.options ?? _classPrivateFieldGet2(_options, this).agent.options;
|
|
1685
|
+
return {
|
|
1686
|
+
...state.model === void 0 ? {} : { model: state.model },
|
|
1687
|
+
...options === void 0 ? {} : { options }
|
|
1688
|
+
};
|
|
1689
|
+
}
|
|
1690
|
+
function _requireSession(store, session) {
|
|
1691
|
+
const record = store.session(session);
|
|
1692
|
+
if (!record) throw new Error(`Unknown container session ${session}`);
|
|
1693
|
+
return record;
|
|
1694
|
+
}
|
|
1695
|
+
function _emit(session, events) {
|
|
1696
|
+
if (events.length === 0) return;
|
|
1697
|
+
const listeners = _classPrivateFieldGet2(_listeners, this).get(session);
|
|
1698
|
+
if (!listeners) return;
|
|
1699
|
+
for (const listener of listeners) try {
|
|
1700
|
+
listener(events);
|
|
1701
|
+
} catch {}
|
|
1702
|
+
}
|
|
1703
|
+
function _broadcast(event) {
|
|
1704
|
+
for (const session of _classPrivateFieldGet2(_listeners, this).keys()) _assertClassBrand(_ContainerHarness_brand, this, _emit).call(this, session, [event]);
|
|
1705
|
+
}
|
|
1706
|
+
function _notify(waiters, key) {
|
|
1707
|
+
const set = waiters.get(key);
|
|
1708
|
+
waiters.delete(key);
|
|
1709
|
+
if (set) for (const wake of set) wake();
|
|
1710
|
+
}
|
|
1711
|
+
/**
|
|
1712
|
+
* Resolve on the next notify for `key`, after `pollMs` if given, or
|
|
1713
|
+
* reject when `signal` aborts.
|
|
1714
|
+
*
|
|
1715
|
+
* The poll matters for waits in a request: the notify comes from the
|
|
1716
|
+
* alarm that drives the container, a different request context, and a
|
|
1717
|
+
* request whose only pending work is a promise another context resolves
|
|
1718
|
+
* is cancelled by the runtime as hung. A timer of its own keeps it alive,
|
|
1719
|
+
* and the caller re-reads the store when it fires.
|
|
1720
|
+
*/
|
|
1721
|
+
function _until(waiters, key, signal, pollMs) {
|
|
1722
|
+
return new Promise((resolve, reject) => {
|
|
1723
|
+
let set = waiters.get(key);
|
|
1724
|
+
if (!set) {
|
|
1725
|
+
set = /* @__PURE__ */ new Set();
|
|
1726
|
+
waiters.set(key, set);
|
|
1727
|
+
}
|
|
1728
|
+
let timer;
|
|
1729
|
+
const done = () => {
|
|
1730
|
+
clearTimeout(timer);
|
|
1731
|
+
set?.delete(wake);
|
|
1732
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1733
|
+
};
|
|
1734
|
+
const onAbort = () => {
|
|
1735
|
+
done();
|
|
1736
|
+
reject(signal?.reason);
|
|
1737
|
+
};
|
|
1738
|
+
const wake = () => {
|
|
1739
|
+
done();
|
|
1740
|
+
resolve();
|
|
1741
|
+
};
|
|
1742
|
+
if (signal?.aborted) {
|
|
1743
|
+
onAbort();
|
|
1744
|
+
return;
|
|
1745
|
+
}
|
|
1746
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1747
|
+
set.add(wake);
|
|
1748
|
+
if (pollMs !== void 0) timer = setTimeout(wake, pollMs);
|
|
1749
|
+
});
|
|
1750
|
+
}
|
|
1751
|
+
function resultOf(operation) {
|
|
1752
|
+
const base = {
|
|
1753
|
+
operationId: operation.id,
|
|
1754
|
+
session: operation.session
|
|
1755
|
+
};
|
|
1756
|
+
if (operation.status === "done") {
|
|
1757
|
+
const result = operation.result;
|
|
1758
|
+
const text = isRecord(result) && typeof result.text === "string" ? result.text : "";
|
|
1759
|
+
return {
|
|
1760
|
+
...base,
|
|
1761
|
+
status: "done",
|
|
1762
|
+
text
|
|
1763
|
+
};
|
|
1764
|
+
}
|
|
1765
|
+
if (operation.status === "unanswered") return {
|
|
1766
|
+
...base,
|
|
1767
|
+
status: "unanswered",
|
|
1768
|
+
reason: operation.reason ?? "unanswered"
|
|
1769
|
+
};
|
|
1770
|
+
}
|
|
1771
|
+
var _harness = /* @__PURE__ */ new WeakMap();
|
|
1772
|
+
/**
|
|
1773
|
+
* One session, addressed through the harness.
|
|
1774
|
+
*
|
|
1775
|
+
* @experimental The API may change before it stabilizes.
|
|
1776
|
+
*/
|
|
1777
|
+
var ContainerSession = class {
|
|
1778
|
+
/**
|
|
1779
|
+
* @param harness - The harness.
|
|
1780
|
+
* @param id - The session id.
|
|
1781
|
+
*/
|
|
1782
|
+
constructor(harness, id) {
|
|
1783
|
+
_classPrivateFieldInitSpec(this, _harness, void 0);
|
|
1784
|
+
_classPrivateFieldSet2(_harness, this, harness);
|
|
1785
|
+
this.id = id;
|
|
1786
|
+
}
|
|
1787
|
+
/**
|
|
1788
|
+
* Durably submit a prompt. Resolves before the agent runs.
|
|
1789
|
+
*
|
|
1790
|
+
* @param input - The prompt.
|
|
1791
|
+
* @param options - Idempotency key and busy behaviour.
|
|
1792
|
+
* @returns The receipt.
|
|
1793
|
+
*/
|
|
1794
|
+
submit(input, options = {}) {
|
|
1795
|
+
return _classPrivateFieldGet2(_harness, this).enqueue(this.id, input, options);
|
|
1796
|
+
}
|
|
1797
|
+
/**
|
|
1798
|
+
* Submit and wait for the answer and the updated transcript.
|
|
1799
|
+
*
|
|
1800
|
+
* @param input - The prompt.
|
|
1801
|
+
* @param options - Idempotency key and busy behaviour.
|
|
1802
|
+
* @returns The result, with the transcript after it.
|
|
1803
|
+
*/
|
|
1804
|
+
async prompt(input, options = {}) {
|
|
1805
|
+
const receipt = await this.submit(input, options);
|
|
1806
|
+
return {
|
|
1807
|
+
...await this.wait(receipt.operationId),
|
|
1808
|
+
messages: await this.messages()
|
|
1809
|
+
};
|
|
1810
|
+
}
|
|
1811
|
+
/**
|
|
1812
|
+
* Join the running turn, when the adapter can steer; otherwise queue.
|
|
1813
|
+
*
|
|
1814
|
+
* @param input - The prompt.
|
|
1815
|
+
* @param options - Idempotency key.
|
|
1816
|
+
* @returns The receipt.
|
|
1817
|
+
*/
|
|
1818
|
+
steer(input, options = {}) {
|
|
1819
|
+
return this.submit(input, {
|
|
1820
|
+
...options,
|
|
1821
|
+
whenBusy: "steer"
|
|
1822
|
+
});
|
|
1823
|
+
}
|
|
1824
|
+
/**
|
|
1825
|
+
* Wait for an operation to settle. Aborting `signal` stops only the wait.
|
|
1826
|
+
*
|
|
1827
|
+
* @param operationId - The operation.
|
|
1828
|
+
* @param signal - Stops the wait.
|
|
1829
|
+
* @returns How it ended.
|
|
1830
|
+
*/
|
|
1831
|
+
wait(operationId, signal) {
|
|
1832
|
+
return _classPrivateFieldGet2(_harness, this).settled(this.id, operationId, signal);
|
|
1833
|
+
}
|
|
1834
|
+
/**
|
|
1835
|
+
* Withdraw or abort one operation, or, with no id, every open operation
|
|
1836
|
+
* of the session. Aborted operations settle `unanswered` with `aborted`.
|
|
1837
|
+
*
|
|
1838
|
+
* @param operationId - The operation. Default: all of them.
|
|
1839
|
+
* @returns Whether anything was open to abort (always true with no id).
|
|
1840
|
+
*/
|
|
1841
|
+
abort(operationId) {
|
|
1842
|
+
return _classPrivateFieldGet2(_harness, this).withdraw(this.id, operationId);
|
|
1843
|
+
}
|
|
1844
|
+
/**
|
|
1845
|
+
* Start a new context: a new agent session without the old history,
|
|
1846
|
+
* optionally seeded with a handoff note prepended to the next prompt.
|
|
1847
|
+
* Throws while the session is busy.
|
|
1848
|
+
*
|
|
1849
|
+
* @param handoff - The note.
|
|
1850
|
+
*/
|
|
1851
|
+
reset(handoff) {
|
|
1852
|
+
return _classPrivateFieldGet2(_harness, this).resetSession(this.id, handoff);
|
|
1853
|
+
}
|
|
1854
|
+
/**
|
|
1855
|
+
* Change this session's model, by the adapter's model name.
|
|
1856
|
+
*
|
|
1857
|
+
* @param model - The model.
|
|
1858
|
+
*/
|
|
1859
|
+
setModel(model) {
|
|
1860
|
+
return _classPrivateFieldGet2(_harness, this).setModel(this.id, model);
|
|
1861
|
+
}
|
|
1862
|
+
/**
|
|
1863
|
+
* The active transcript, since the newest reset.
|
|
1864
|
+
*
|
|
1865
|
+
* @returns The messages, oldest first.
|
|
1866
|
+
*/
|
|
1867
|
+
messages() {
|
|
1868
|
+
return _classPrivateFieldGet2(_harness, this).transcript(this.id);
|
|
1869
|
+
}
|
|
1870
|
+
/**
|
|
1871
|
+
* This session's events: a snapshot, then live batches.
|
|
1872
|
+
*
|
|
1873
|
+
* @returns The stream.
|
|
1874
|
+
*/
|
|
1875
|
+
events() {
|
|
1876
|
+
return _classPrivateFieldGet2(_harness, this).events(this.id);
|
|
1877
|
+
}
|
|
1878
|
+
/**
|
|
1879
|
+
* Whether the session has open operations.
|
|
1880
|
+
*
|
|
1881
|
+
* @returns True while anything is queued or running.
|
|
1882
|
+
*/
|
|
1883
|
+
busy() {
|
|
1884
|
+
return _classPrivateFieldGet2(_harness, this).busy(this.id);
|
|
1885
|
+
}
|
|
1886
|
+
};
|
|
1887
|
+
var _harness2 = /* @__PURE__ */ new WeakMap();
|
|
1888
|
+
/**
|
|
1889
|
+
* Every session in this object.
|
|
1890
|
+
*
|
|
1891
|
+
* @experimental The API may change before it stabilizes.
|
|
1892
|
+
*/
|
|
1893
|
+
var ContainerSessions = class {
|
|
1894
|
+
/**
|
|
1895
|
+
* @param harness - The harness.
|
|
1896
|
+
*/
|
|
1897
|
+
constructor(harness) {
|
|
1898
|
+
_classPrivateFieldInitSpec(this, _harness2, void 0);
|
|
1899
|
+
_classPrivateFieldSet2(_harness2, this, harness);
|
|
1900
|
+
}
|
|
1901
|
+
/**
|
|
1902
|
+
* A handle on one session.
|
|
1903
|
+
*
|
|
1904
|
+
* @param id - The session id.
|
|
1905
|
+
* @returns The handle.
|
|
1906
|
+
*/
|
|
1907
|
+
get(id) {
|
|
1908
|
+
return _classPrivateFieldGet2(_harness2, this).session(id);
|
|
1909
|
+
}
|
|
1910
|
+
/**
|
|
1911
|
+
* A new top-level session, with the harness's defaults.
|
|
1912
|
+
*
|
|
1913
|
+
* @returns The new session.
|
|
1914
|
+
*/
|
|
1915
|
+
async create() {
|
|
1916
|
+
const id = crypto.randomUUID();
|
|
1917
|
+
await _classPrivateFieldGet2(_harness2, this).createSession(id);
|
|
1918
|
+
return _classPrivateFieldGet2(_harness2, this).session(id);
|
|
1919
|
+
}
|
|
1920
|
+
/**
|
|
1921
|
+
* A new session that continues `from`'s history as a branch.
|
|
1922
|
+
*
|
|
1923
|
+
* @param from - The session to fork.
|
|
1924
|
+
* @returns The new session.
|
|
1925
|
+
*/
|
|
1926
|
+
async fork(from) {
|
|
1927
|
+
const id = crypto.randomUUID();
|
|
1928
|
+
await _classPrivateFieldGet2(_harness2, this).createSession(id, from);
|
|
1929
|
+
return _classPrivateFieldGet2(_harness2, this).session(id);
|
|
1930
|
+
}
|
|
1931
|
+
/**
|
|
1932
|
+
* Every session, oldest first.
|
|
1933
|
+
*
|
|
1934
|
+
* @returns Each session's id, parent, and whether it is busy.
|
|
1935
|
+
*/
|
|
1936
|
+
async list() {
|
|
1937
|
+
const store = await _classPrivateFieldGet2(_harness2, this).store();
|
|
1938
|
+
const busy = new Set(store.operations({ status: OPEN }).map((operation) => operation.session));
|
|
1939
|
+
return store.sessions().map((record) => ({
|
|
1940
|
+
id: record.id,
|
|
1941
|
+
...record.parent === void 0 ? {} : { parent: record.parent },
|
|
1942
|
+
busy: busy.has(record.id)
|
|
1943
|
+
}));
|
|
1944
|
+
}
|
|
1945
|
+
};
|
|
1946
|
+
//#endregion
|
|
1947
|
+
//#region src/harness/container/agents.ts
|
|
1948
|
+
/** What the CLI sends instead of a key; egress replaces it. */
|
|
1949
|
+
const PLACEHOLDER_KEY = "harness-egress";
|
|
1950
|
+
const CREDENTIAL_HEADERS = [
|
|
1951
|
+
"authorization",
|
|
1952
|
+
"x-api-key",
|
|
1953
|
+
"cf-aig-authorization"
|
|
1954
|
+
];
|
|
1955
|
+
function npmInstall(name, pkg) {
|
|
1956
|
+
return {
|
|
1957
|
+
name: `install ${name}`,
|
|
1958
|
+
command: [
|
|
1959
|
+
"npm",
|
|
1960
|
+
"install",
|
|
1961
|
+
"--global",
|
|
1962
|
+
"--no-fund",
|
|
1963
|
+
"--no-audit",
|
|
1964
|
+
pkg
|
|
1965
|
+
]
|
|
1966
|
+
};
|
|
1967
|
+
}
|
|
1968
|
+
/** The egress route for one provider. */
|
|
1969
|
+
function route(host, connection, provider) {
|
|
1970
|
+
return {
|
|
1971
|
+
host,
|
|
1972
|
+
upstream: connection.baseUrl ?? provider.baseUrl,
|
|
1973
|
+
headers: {
|
|
1974
|
+
...connection.apiKey === void 0 ? {} : provider.keyHeader(connection.apiKey),
|
|
1975
|
+
...connection.headers
|
|
1976
|
+
},
|
|
1977
|
+
strip: CREDENTIAL_HEADERS
|
|
1978
|
+
};
|
|
1979
|
+
}
|
|
1980
|
+
/**
|
|
1981
|
+
* Claude Code, installed at runtime on `cloudflare/debian-trixie`.
|
|
1982
|
+
*
|
|
1983
|
+
* @param options - Its provider connection (`apiKey`, `baseUrl`,
|
|
1984
|
+
* `headers`) and settings.
|
|
1985
|
+
* @returns The agent, for `ContainerHarness`'s `agent`.
|
|
1986
|
+
*
|
|
1987
|
+
* @experimental The API may change before it stabilizes.
|
|
1988
|
+
*/
|
|
1989
|
+
function claudeCode(options) {
|
|
1990
|
+
const host = "anthropic.harness.internal";
|
|
1991
|
+
return {
|
|
1992
|
+
adapter: "claude-code",
|
|
1993
|
+
image: {
|
|
1994
|
+
kind: "managed",
|
|
1995
|
+
setup: [npmInstall("Claude Code", `@anthropic-ai/claude-code@${options.version ?? "2.1.289"}`), ...options.setup ?? []]
|
|
1996
|
+
},
|
|
1997
|
+
env: {
|
|
1998
|
+
ANTHROPIC_BASE_URL: `http://${host}`,
|
|
1999
|
+
ANTHROPIC_API_KEY: PLACEHOLDER_KEY,
|
|
2000
|
+
DISABLE_AUTOUPDATER: "1"
|
|
2001
|
+
},
|
|
2002
|
+
egress: [route(host, options, {
|
|
2003
|
+
baseUrl: "https://api.anthropic.com",
|
|
2004
|
+
keyHeader: (key) => ({ "x-api-key": key })
|
|
2005
|
+
})],
|
|
2006
|
+
model: options.model ?? "claude-sonnet-4-5",
|
|
2007
|
+
options: {
|
|
2008
|
+
...options.appendSystemPrompt === void 0 ? {} : { appendSystemPrompt: options.appendSystemPrompt },
|
|
2009
|
+
...options.allowedTools === void 0 ? {} : { allowedTools: [...options.allowedTools] },
|
|
2010
|
+
...options.disallowedTools === void 0 ? {} : { disallowedTools: [...options.disallowedTools] },
|
|
2011
|
+
...options.maxTurns === void 0 ? {} : { maxTurns: options.maxTurns }
|
|
2012
|
+
}
|
|
2013
|
+
};
|
|
2014
|
+
}
|
|
2015
|
+
/**
|
|
2016
|
+
* Codex, installed at runtime on `cloudflare/debian-trixie`.
|
|
2017
|
+
*
|
|
2018
|
+
* @param options - Its provider connection (`apiKey`, `baseUrl`,
|
|
2019
|
+
* `headers`) and settings.
|
|
2020
|
+
* @returns The agent, for `ContainerHarness`'s `agent`.
|
|
2021
|
+
*
|
|
2022
|
+
* @experimental The API may change before it stabilizes.
|
|
2023
|
+
*/
|
|
2024
|
+
function codex(options) {
|
|
2025
|
+
const host = "openai.harness.internal";
|
|
2026
|
+
return {
|
|
2027
|
+
adapter: "codex",
|
|
2028
|
+
image: {
|
|
2029
|
+
kind: "managed",
|
|
2030
|
+
setup: [npmInstall("Codex", `@openai/codex@${options.version ?? "0.157.0"}`), ...options.setup ?? []]
|
|
2031
|
+
},
|
|
2032
|
+
env: {
|
|
2033
|
+
OPENAI_BASE_URL: `http://${host}`,
|
|
2034
|
+
OPENAI_API_KEY: PLACEHOLDER_KEY
|
|
2035
|
+
},
|
|
2036
|
+
egress: [route(host, options, {
|
|
2037
|
+
baseUrl: "https://api.openai.com/v1",
|
|
2038
|
+
keyHeader: (key) => ({ authorization: `Bearer ${key}` })
|
|
2039
|
+
})],
|
|
2040
|
+
model: options.model ?? "gpt-5.1",
|
|
2041
|
+
options: options.config === void 0 ? {} : { config: [...options.config] }
|
|
2042
|
+
};
|
|
2043
|
+
}
|
|
2044
|
+
/**
|
|
2045
|
+
* An agent in an image you built, whose entrypoint serves the daemon
|
|
2046
|
+
* (`serveFromEnv` from `agents/harness/container/runtime`).
|
|
2047
|
+
*
|
|
2048
|
+
* @param options - The image, the adapter, and its environment.
|
|
2049
|
+
* @returns The agent, for `ContainerHarness`'s `agent`.
|
|
2050
|
+
*
|
|
2051
|
+
* @experimental The API may change before it stabilizes.
|
|
2052
|
+
*/
|
|
2053
|
+
function containerAgent(options) {
|
|
2054
|
+
return {
|
|
2055
|
+
adapter: options.adapter,
|
|
2056
|
+
image: {
|
|
2057
|
+
kind: "custom",
|
|
2058
|
+
image: options.image
|
|
2059
|
+
},
|
|
2060
|
+
env: options.env ?? {},
|
|
2061
|
+
egress: options.egress ?? [],
|
|
2062
|
+
...options.model === void 0 ? {} : { model: options.model },
|
|
2063
|
+
...options.options === void 0 ? {} : { options: options.options }
|
|
2064
|
+
};
|
|
2065
|
+
}
|
|
2066
|
+
/**
|
|
2067
|
+
* The echo adapter on `cloudflare/debian-trixie`: no CLI, no model, no
|
|
2068
|
+
* credentials. For smoke tests of the whole path.
|
|
2069
|
+
*
|
|
2070
|
+
* @returns The agent, for `ContainerHarness`'s `agent`.
|
|
2071
|
+
*
|
|
2072
|
+
* @experimental The API may change before it stabilizes.
|
|
2073
|
+
*/
|
|
2074
|
+
function echoAgent() {
|
|
2075
|
+
return {
|
|
2076
|
+
adapter: "echo",
|
|
2077
|
+
image: {
|
|
2078
|
+
kind: "managed",
|
|
2079
|
+
setup: []
|
|
2080
|
+
},
|
|
2081
|
+
env: {},
|
|
2082
|
+
egress: []
|
|
2083
|
+
};
|
|
2084
|
+
}
|
|
2085
|
+
//#endregion
|
|
2086
|
+
//#region src/harness/container/egress.ts
|
|
2087
|
+
/**
|
|
2088
|
+
* Credentials that never enter the container.
|
|
2089
|
+
*
|
|
2090
|
+
* The CLI in the container is pointed at a placeholder host over plain
|
|
2091
|
+
* HTTP (`http://anthropic.harness.internal`) with a placeholder key. The
|
|
2092
|
+
* harness intercepts that host with `ctx.container.interceptOutboundHttp()`
|
|
2093
|
+
* and hands each request to `ContainerEgress`, a `WorkerEntrypoint` running
|
|
2094
|
+
* in your Worker, which drops the placeholder credentials, adds the real
|
|
2095
|
+
* ones, and forwards the request to the real upstream (an AI Gateway, or
|
|
2096
|
+
* the provider). The container can run any command it likes and still has
|
|
2097
|
+
* nothing to steal.
|
|
2098
|
+
*
|
|
2099
|
+
* Plain HTTP inside the container means no certificate to trust: the
|
|
2100
|
+
* connection to the intercept never leaves Cloudflare, and the request to
|
|
2101
|
+
* the upstream is HTTPS.
|
|
2102
|
+
*/
|
|
2103
|
+
/**
|
|
2104
|
+
* Forward one intercepted request to its route's upstream.
|
|
2105
|
+
*
|
|
2106
|
+
* @param request - The request the container made.
|
|
2107
|
+
* @param routes - The routes, by placeholder host.
|
|
2108
|
+
* @param fetcher - How to make the upstream request. Default `fetch`.
|
|
2109
|
+
* @returns The upstream's response, or 403 for a host without a route.
|
|
2110
|
+
*/
|
|
2111
|
+
async function forwardEgress(request, routes, fetcher = fetch) {
|
|
2112
|
+
const url = new URL(request.url);
|
|
2113
|
+
const route = routes.find((each) => each.host === url.hostname);
|
|
2114
|
+
if (!route) return new Response(`No egress route for ${url.hostname}`, { status: 403 });
|
|
2115
|
+
const target = `${route.upstream.replace(/\/+$/, "")}${url.pathname}${url.search}`;
|
|
2116
|
+
const headers = new Headers(request.headers);
|
|
2117
|
+
headers.delete("host");
|
|
2118
|
+
for (const name of route.strip) headers.delete(name);
|
|
2119
|
+
for (const [name, value] of Object.entries(route.headers)) headers.set(name, value);
|
|
2120
|
+
const hasBody = request.method !== "GET" && request.method !== "HEAD";
|
|
2121
|
+
return fetcher(target, {
|
|
2122
|
+
method: request.method,
|
|
2123
|
+
headers,
|
|
2124
|
+
...hasBody ? { body: request.body } : {},
|
|
2125
|
+
redirect: "manual"
|
|
2126
|
+
});
|
|
2127
|
+
}
|
|
2128
|
+
/**
|
|
2129
|
+
* The egress for a `ContainerHarness`'s credentials. Export it from your
|
|
2130
|
+
* Worker's main module so `ctx.exports.ContainerEgress` exists:
|
|
2131
|
+
*
|
|
2132
|
+
* ```ts
|
|
2133
|
+
* export { ContainerEgress } from "agents/harness/container";
|
|
2134
|
+
* ```
|
|
2135
|
+
*
|
|
2136
|
+
* @experimental The API may change before it stabilizes.
|
|
2137
|
+
*/
|
|
2138
|
+
var ContainerEgress = class extends WorkerEntrypoint {
|
|
2139
|
+
/**
|
|
2140
|
+
* Forward one request the container made.
|
|
2141
|
+
*
|
|
2142
|
+
* @param request - The intercepted request.
|
|
2143
|
+
* @returns The upstream's response.
|
|
2144
|
+
*/
|
|
2145
|
+
fetch(request) {
|
|
2146
|
+
return forwardEgress(request, this.ctx.props.routes);
|
|
2147
|
+
}
|
|
2148
|
+
};
|
|
2149
|
+
//#endregion
|
|
2150
|
+
export { ContainerEgress, ContainerHarness, ContainerSession, ContainerSessions, ROOT_SESSION, claudeCode, codex, containerAgent, echoAgent };
|
|
2151
|
+
|
|
2152
|
+
//# sourceMappingURL=index.js.map
|