@standardagents/builder 0.27.12 → 0.28.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/dist/built-in-routes.js +273 -21
- package/dist/built-in-routes.js.map +1 -1
- package/dist/{index-DReIS8eP.d.ts → index-BzXyIJPA.d.ts} +163 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +909 -38
- package/dist/index.js.map +1 -1
- package/dist/plugin.js +24 -2
- package/dist/plugin.js.map +1 -1
- package/dist/runtime.d.ts +127 -5
- package/dist/runtime.js +855 -36
- package/dist/runtime.js.map +1 -1
- package/dist/test.d.ts +1 -1
- package/package.json +5 -5
package/dist/runtime.js
CHANGED
|
@@ -28,6 +28,28 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
28
28
|
};
|
|
29
29
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
30
30
|
|
|
31
|
+
// src/agents/client-forwarding.ts
|
|
32
|
+
function encodeStreamChunk(bytes) {
|
|
33
|
+
let binary = "";
|
|
34
|
+
const step = 32768;
|
|
35
|
+
for (let i = 0; i < bytes.length; i += step) {
|
|
36
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + step));
|
|
37
|
+
}
|
|
38
|
+
return btoa(binary);
|
|
39
|
+
}
|
|
40
|
+
function decodeStreamChunk(data) {
|
|
41
|
+
const binary = atob(data);
|
|
42
|
+
const bytes = new Uint8Array(binary.length);
|
|
43
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
44
|
+
return bytes;
|
|
45
|
+
}
|
|
46
|
+
var CLIENT_STREAM_CHUNK_BYTES;
|
|
47
|
+
var init_client_forwarding = __esm({
|
|
48
|
+
"src/agents/client-forwarding.ts"() {
|
|
49
|
+
CLIENT_STREAM_CHUNK_BYTES = 256 * 1024;
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
31
53
|
// src/agents/types.ts
|
|
32
54
|
var SUBAGENT_INITIAL_ARGUMENTS_THREAD_PROPERTY, MAX_STEPS, MAX_RETRIES_PER_MODEL, TIMESTAMP_MULTIPLIER, STREAM_COOLDOWN, BACKOFF_BASE;
|
|
33
55
|
var init_types = __esm({
|
|
@@ -920,6 +942,20 @@ var init_ProviderRegistry = __esm({
|
|
|
920
942
|
const threadEnv = state.thread.metadata?.env ?? state.thread.metadata?.tenvs ?? null;
|
|
921
943
|
const agentBuilderId = state.env.AGENT_BUILDER.idFromName("singleton");
|
|
922
944
|
const agentBuilder = state.env.AGENT_BUILDER.get(agentBuilderId);
|
|
945
|
+
let instanceEnv = null;
|
|
946
|
+
if (typeof agentBuilder.getInstanceEnvEntries === "function") {
|
|
947
|
+
try {
|
|
948
|
+
const entries = await agentBuilder.getInstanceEnvEntries();
|
|
949
|
+
if (Array.isArray(entries) && entries.length > 0) {
|
|
950
|
+
instanceEnv = {};
|
|
951
|
+
for (const entry of entries) {
|
|
952
|
+
if (entry?.name) instanceEnv[entry.name] = entry.value;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
} catch (error) {
|
|
956
|
+
console.warn("buildThreadScopedEnv: instance env lookup failed:", error);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
923
959
|
let userId = state.thread.metadata?.user_id ?? null;
|
|
924
960
|
let parent = state.thread.metadata?.parent ?? null;
|
|
925
961
|
let hops = 0;
|
|
@@ -956,11 +992,12 @@ var init_ProviderRegistry = __esm({
|
|
|
956
992
|
console.warn("buildThreadScopedEnv:", diagnostic);
|
|
957
993
|
}
|
|
958
994
|
}
|
|
959
|
-
if (!userEnv && !threadEnv) {
|
|
995
|
+
if (!instanceEnv && !userEnv && !threadEnv) {
|
|
960
996
|
this.scopeDiagnostics.set(state.env, diagnostic);
|
|
961
997
|
return state.env;
|
|
962
998
|
}
|
|
963
|
-
|
|
999
|
+
if (instanceEnv) diagnostic = `${diagnostic ? diagnostic + "; " : ""}instance env: ${Object.keys(instanceEnv).length} entries`;
|
|
1000
|
+
const merged = { ...state.env, ...instanceEnv ?? {}, ...userEnv ?? {}, ...threadEnv ?? {} };
|
|
964
1001
|
this.scopeDiagnostics.set(merged, diagnostic);
|
|
965
1002
|
return merged;
|
|
966
1003
|
}
|
|
@@ -974,7 +1011,7 @@ var init_ProviderRegistry = __esm({
|
|
|
974
1011
|
* @param thread - Thread instance for loading model from virtual modules
|
|
975
1012
|
* @returns Provider and model definition
|
|
976
1013
|
*/
|
|
977
|
-
async getProvider(modelName, env, thread) {
|
|
1014
|
+
async getProvider(modelName, env, thread, getState) {
|
|
978
1015
|
if (!thread) {
|
|
979
1016
|
throw new Error("Thread instance required for model lookup");
|
|
980
1017
|
}
|
|
@@ -1007,6 +1044,18 @@ var init_ProviderRegistry = __esm({
|
|
|
1007
1044
|
);
|
|
1008
1045
|
}
|
|
1009
1046
|
const config = resolved.providerConfig;
|
|
1047
|
+
if (typeof providerDefinition.createFetch === "function") {
|
|
1048
|
+
const state = getState ? await getState() : void 0;
|
|
1049
|
+
const configWithFetch = {
|
|
1050
|
+
...config,
|
|
1051
|
+
fetch: providerDefinition.createFetch({
|
|
1052
|
+
providerName,
|
|
1053
|
+
config,
|
|
1054
|
+
state
|
|
1055
|
+
})
|
|
1056
|
+
};
|
|
1057
|
+
return this.buildResult(providerFactory(configWithFetch), modelDef, false);
|
|
1058
|
+
}
|
|
1010
1059
|
const fingerprint = this.getProviderFingerprint(providerName, modelDef, config);
|
|
1011
1060
|
const cached = this.providerCache.get(modelName);
|
|
1012
1061
|
if (cached?.fingerprint === fingerprint) {
|
|
@@ -1879,7 +1928,16 @@ var init_LLMRequest = __esm({
|
|
|
1879
1928
|
static async callModel(modelId, context, state, logId) {
|
|
1880
1929
|
const { ProviderRegistry: ProviderRegistry2 } = await Promise.resolve().then(() => (init_providers(), providers_exports));
|
|
1881
1930
|
const providerEnv = await ProviderRegistry2.buildThreadScopedEnv(state);
|
|
1882
|
-
const
|
|
1931
|
+
const getThreadState = async () => {
|
|
1932
|
+
const { ThreadStateImpl: ThreadStateImpl2 } = await Promise.resolve().then(() => (init_ThreadStateImpl(), ThreadStateImpl_exports));
|
|
1933
|
+
return ThreadStateImpl2.fromFlowState(state);
|
|
1934
|
+
};
|
|
1935
|
+
const { provider, modelName, modelDef, viaPlatform, platformProxyOrigin } = await ProviderRegistry2.getProvider(
|
|
1936
|
+
modelId,
|
|
1937
|
+
providerEnv,
|
|
1938
|
+
state.thread.instance,
|
|
1939
|
+
getThreadState
|
|
1940
|
+
);
|
|
1883
1941
|
const streamAbort = new AbortController();
|
|
1884
1942
|
const parentSignal = state.abortController?.signal;
|
|
1885
1943
|
let onParentAbort;
|
|
@@ -4329,8 +4387,17 @@ ${errorLines.join("\n")}`);
|
|
|
4329
4387
|
responseParts.push(`Other tools run: ${successfulToolNames.join(", ")}`);
|
|
4330
4388
|
}
|
|
4331
4389
|
} else {
|
|
4332
|
-
if (toolResponseOptions.include_text_response
|
|
4333
|
-
|
|
4390
|
+
if (toolResponseOptions.include_text_response) {
|
|
4391
|
+
let responseText = textContent;
|
|
4392
|
+
if (!responseText) {
|
|
4393
|
+
const recovered = await state.storage.sql.exec(
|
|
4394
|
+
`SELECT content FROM messages WHERE parent_id = ? AND role = 'assistant' AND content IS NOT NULL AND content != '' AND (tool_calls IS NULL OR tool_calls = '') ORDER BY created_at DESC LIMIT 1`,
|
|
4395
|
+
toolMessageId
|
|
4396
|
+
);
|
|
4397
|
+
const recoveredRow = recovered.toArray()[0];
|
|
4398
|
+
if (recoveredRow?.content) responseText = recoveredRow.content;
|
|
4399
|
+
}
|
|
4400
|
+
if (responseText) responseParts.push(responseText);
|
|
4334
4401
|
}
|
|
4335
4402
|
if (toolResponseOptions.include_tool_calls || toolResponseOptions.include_errors) {
|
|
4336
4403
|
const childAssistantMessagesResult = await state.storage.sql.exec(
|
|
@@ -6424,12 +6491,14 @@ var init_FlowEngine = __esm({
|
|
|
6424
6491
|
} catch (error) {
|
|
6425
6492
|
console.error("Fatal error in executeAsync:", error);
|
|
6426
6493
|
await this.logError(state, error, "fatal_execution_error", "FlowEngine.executeAsync");
|
|
6494
|
+
await this.settleTerminalFailure(state, error);
|
|
6427
6495
|
throw error;
|
|
6428
6496
|
}
|
|
6429
6497
|
} else {
|
|
6430
6498
|
this.executeAsync(state, state.threadId).catch(async (error) => {
|
|
6431
6499
|
console.error("Fatal error in executeAsync:", error);
|
|
6432
6500
|
await this.logError(state, error, "fatal_execution_error", "FlowEngine.executeAsync");
|
|
6501
|
+
await this.settleTerminalFailure(state, error);
|
|
6433
6502
|
});
|
|
6434
6503
|
}
|
|
6435
6504
|
return {
|
|
@@ -6483,10 +6552,27 @@ var init_FlowEngine = __esm({
|
|
|
6483
6552
|
console.warn(
|
|
6484
6553
|
`[FlowEngine] Finalized ${rows.length} orphaned incomplete log(s) for thread ${state.threadId}`
|
|
6485
6554
|
);
|
|
6555
|
+
await state.thread.instance.failPendingMessagesAndBroadcast?.();
|
|
6486
6556
|
} catch (err) {
|
|
6487
6557
|
console.error("[FlowEngine] Failed to finalize orphaned logs (non-fatal):", err);
|
|
6488
6558
|
}
|
|
6489
6559
|
}
|
|
6560
|
+
/**
|
|
6561
|
+
* A terminal execution failure ended the run without a normal assistant
|
|
6562
|
+
* reply. Persist a durable failed assistant message + settle pending
|
|
6563
|
+
* messages so every connected head lands on a visible end state instead of
|
|
6564
|
+
* a stuck "Working" spinner. Best-effort — never masks the original error.
|
|
6565
|
+
*/
|
|
6566
|
+
static async settleTerminalFailure(state, error) {
|
|
6567
|
+
try {
|
|
6568
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
6569
|
+
await state.thread.instance.settleTerminalFailure?.(
|
|
6570
|
+
`The agent run failed unexpectedly: ${detail}`
|
|
6571
|
+
);
|
|
6572
|
+
} catch (settleError) {
|
|
6573
|
+
console.error("[FlowEngine] Failed to settle terminal failure (non-fatal):", settleError);
|
|
6574
|
+
}
|
|
6575
|
+
}
|
|
6490
6576
|
static async executeAsync(state, threadId) {
|
|
6491
6577
|
const effectiveMaxSteps = Math.max(
|
|
6492
6578
|
MAX_STEPS,
|
|
@@ -12761,6 +12847,7 @@ function runCodeViaThreadRpc(threadInstance, threadId, source, options) {
|
|
|
12761
12847
|
var ThreadStateImpl, ExecutionStateImpl;
|
|
12762
12848
|
var init_ThreadStateImpl = __esm({
|
|
12763
12849
|
"src/agents/ThreadStateImpl.ts"() {
|
|
12850
|
+
init_client_forwarding();
|
|
12764
12851
|
init_types();
|
|
12765
12852
|
init_utilities();
|
|
12766
12853
|
init_code_execution();
|
|
@@ -13345,9 +13432,10 @@ var init_ThreadStateImpl = __esm({
|
|
|
13345
13432
|
}
|
|
13346
13433
|
const instance = this._threadInstance;
|
|
13347
13434
|
const toolCallId = this._flowState?.currentToolCall?.id;
|
|
13348
|
-
const
|
|
13349
|
-
|
|
13350
|
-
|
|
13435
|
+
const canPark = !!(toolCallId && this._executionState && typeof instance.dispatchClientTool === "function");
|
|
13436
|
+
const side = this._flowState?.currentSide === "b" ? "b" : "a";
|
|
13437
|
+
const mustParkForApproval = request.requestPermission != null && await this.forwardedCallNeedsApproval(request);
|
|
13438
|
+
if (mustParkForApproval && canPark && toolCallId && typeof instance.hasBridge === "function" && await instance.hasBridge()) {
|
|
13351
13439
|
await instance.dispatchClientTool(request, toolCallId, side);
|
|
13352
13440
|
this._executionState.stop();
|
|
13353
13441
|
return {
|
|
@@ -13359,6 +13447,55 @@ var init_ThreadStateImpl = __esm({
|
|
|
13359
13447
|
const local = await this._threadInstance.callClientTool(request);
|
|
13360
13448
|
if (local.ok || !this.isNoClient(local)) return local;
|
|
13361
13449
|
const env = this._threadInstance.env;
|
|
13450
|
+
if (env?.AGENT_BUILDER_THREAD) {
|
|
13451
|
+
const seen = /* @__PURE__ */ new Set([this._metadata.id]);
|
|
13452
|
+
let parentId = await this.resolveParentThreadId(this._metadata.id);
|
|
13453
|
+
while (parentId && !seen.has(parentId)) {
|
|
13454
|
+
seen.add(parentId);
|
|
13455
|
+
try {
|
|
13456
|
+
const stub = env.AGENT_BUILDER_THREAD.get(env.AGENT_BUILDER_THREAD.idFromName(parentId));
|
|
13457
|
+
const res = await stub.callClientTool(request);
|
|
13458
|
+
if (res.ok || !this.isNoClient(res)) return res;
|
|
13459
|
+
} catch {
|
|
13460
|
+
}
|
|
13461
|
+
parentId = await this.resolveParentThreadId(parentId);
|
|
13462
|
+
}
|
|
13463
|
+
}
|
|
13464
|
+
if (canPark && toolCallId) {
|
|
13465
|
+
await instance.dispatchClientTool(request, toolCallId, side);
|
|
13466
|
+
if (typeof instance.notifyClientWaiting === "function") {
|
|
13467
|
+
try {
|
|
13468
|
+
await instance.notifyClientWaiting(toolCallId, this._metadata.id, this._metadata.user_id ?? null);
|
|
13469
|
+
} catch {
|
|
13470
|
+
}
|
|
13471
|
+
}
|
|
13472
|
+
this._executionState.stop();
|
|
13473
|
+
return {
|
|
13474
|
+
ok: true,
|
|
13475
|
+
result: `\u23F3 Waiting for your machine to reconnect to run ${request.tool}\u2026`,
|
|
13476
|
+
data: { deferred: true, parked: true, waitingForClient: true, toolCallId }
|
|
13477
|
+
};
|
|
13478
|
+
}
|
|
13479
|
+
return local;
|
|
13480
|
+
}
|
|
13481
|
+
/**
|
|
13482
|
+
* Open a bidirectional byte stream to the connected client (e.g. a raw TCP
|
|
13483
|
+
* tunnel for Worker-side TLS termination). Streams require a LIVE bridge on
|
|
13484
|
+
* this thread and never park — a stream is only meaningful while the client
|
|
13485
|
+
* is attached. Subagents must not call this directly; they proxy whole
|
|
13486
|
+
* requests up to the root thread instead (request-up / stream-down).
|
|
13487
|
+
*/
|
|
13488
|
+
async callClientStream(request) {
|
|
13489
|
+
const instance = this._threadInstance;
|
|
13490
|
+
if (typeof instance.openClientStream !== "function") {
|
|
13491
|
+
return {
|
|
13492
|
+
ok: false,
|
|
13493
|
+
error: "This thread runtime cannot open client streams. Client streaming requires the DurableThread bridge."
|
|
13494
|
+
};
|
|
13495
|
+
}
|
|
13496
|
+
const local = await instance.openClientStream(request);
|
|
13497
|
+
if (local.ok || !this.isNoClient(local)) return local;
|
|
13498
|
+
const env = this._threadInstance.env;
|
|
13362
13499
|
if (!env?.AGENT_BUILDER_THREAD) return local;
|
|
13363
13500
|
const seen = /* @__PURE__ */ new Set([this._metadata.id]);
|
|
13364
13501
|
let parentId = await this.resolveParentThreadId(this._metadata.id);
|
|
@@ -13366,14 +13503,86 @@ var init_ThreadStateImpl = __esm({
|
|
|
13366
13503
|
seen.add(parentId);
|
|
13367
13504
|
try {
|
|
13368
13505
|
const stub = env.AGENT_BUILDER_THREAD.get(env.AGENT_BUILDER_THREAD.idFromName(parentId));
|
|
13369
|
-
|
|
13370
|
-
|
|
13506
|
+
if (typeof stub.proxyStreamOpen === "function") {
|
|
13507
|
+
const opened = await stub.proxyStreamOpen(request);
|
|
13508
|
+
if (opened.ok && opened.id) {
|
|
13509
|
+
return { ok: true, duplex: this.buildProxyDuplex(stub, opened.id) };
|
|
13510
|
+
}
|
|
13511
|
+
if (opened.error && !opened.noClient) {
|
|
13512
|
+
return { ok: false, error: opened.error };
|
|
13513
|
+
}
|
|
13514
|
+
}
|
|
13371
13515
|
} catch {
|
|
13372
13516
|
}
|
|
13373
13517
|
parentId = await this.resolveParentThreadId(parentId);
|
|
13374
13518
|
}
|
|
13375
13519
|
return local;
|
|
13376
13520
|
}
|
|
13521
|
+
/**
|
|
13522
|
+
* Build a client duplex that bridges to a stream opened on an ancestor DO
|
|
13523
|
+
* (the bridge holder) via chunk-pull RPCs. `readable` polls `proxyStreamPull`;
|
|
13524
|
+
* `writable` forwards each chunk with `proxyStreamWrite`; teardown calls
|
|
13525
|
+
* `proxyStreamAbort`.
|
|
13526
|
+
*/
|
|
13527
|
+
buildProxyDuplex(stub, id) {
|
|
13528
|
+
let settleDone;
|
|
13529
|
+
const done = new Promise((r) => settleDone = r);
|
|
13530
|
+
let closed = false;
|
|
13531
|
+
const readable = new ReadableStream({
|
|
13532
|
+
async pull(controller) {
|
|
13533
|
+
while (!closed) {
|
|
13534
|
+
let res;
|
|
13535
|
+
try {
|
|
13536
|
+
res = await stub.proxyStreamPull(id);
|
|
13537
|
+
} catch (error) {
|
|
13538
|
+
controller.error(error instanceof Error ? error : new Error(String(error)));
|
|
13539
|
+
settleDone({ ok: false, error: "proxy stream pull failed" });
|
|
13540
|
+
return;
|
|
13541
|
+
}
|
|
13542
|
+
for (const chunk of res.chunks) {
|
|
13543
|
+
controller.enqueue(decodeStreamChunk(chunk));
|
|
13544
|
+
}
|
|
13545
|
+
if (res.done) {
|
|
13546
|
+
controller.close();
|
|
13547
|
+
settleDone(res.error ? { ok: false, error: res.error } : { ok: true });
|
|
13548
|
+
return;
|
|
13549
|
+
}
|
|
13550
|
+
if (res.chunks.length > 0) return;
|
|
13551
|
+
await new Promise((r) => setTimeout(r, 25));
|
|
13552
|
+
}
|
|
13553
|
+
},
|
|
13554
|
+
cancel() {
|
|
13555
|
+
closed = true;
|
|
13556
|
+
void stub.proxyStreamAbort(id).catch(() => {
|
|
13557
|
+
});
|
|
13558
|
+
}
|
|
13559
|
+
});
|
|
13560
|
+
const writable = new WritableStream({
|
|
13561
|
+
async write(chunk) {
|
|
13562
|
+
await stub.proxyStreamWrite(id, encodeStreamChunk(chunk));
|
|
13563
|
+
},
|
|
13564
|
+
close() {
|
|
13565
|
+
void stub.proxyStreamEnd?.(id).catch?.(() => {
|
|
13566
|
+
});
|
|
13567
|
+
},
|
|
13568
|
+
abort() {
|
|
13569
|
+
closed = true;
|
|
13570
|
+
void stub.proxyStreamAbort(id).catch(() => {
|
|
13571
|
+
});
|
|
13572
|
+
}
|
|
13573
|
+
});
|
|
13574
|
+
return {
|
|
13575
|
+
readable,
|
|
13576
|
+
writable,
|
|
13577
|
+
close: (error) => {
|
|
13578
|
+
closed = true;
|
|
13579
|
+
void stub.proxyStreamAbort(id).catch(() => {
|
|
13580
|
+
});
|
|
13581
|
+
settleDone(error ? { ok: false, error } : { ok: true });
|
|
13582
|
+
},
|
|
13583
|
+
done
|
|
13584
|
+
};
|
|
13585
|
+
}
|
|
13377
13586
|
isNoClient(res) {
|
|
13378
13587
|
return res.ok === false && !!res.data && typeof res.data === "object" && res.data.noClient === true;
|
|
13379
13588
|
}
|
|
@@ -15922,6 +16131,7 @@ function isQualifiedName(name) {
|
|
|
15922
16131
|
}
|
|
15923
16132
|
|
|
15924
16133
|
// src/durable-objects/DurableThread.ts
|
|
16134
|
+
init_client_forwarding();
|
|
15925
16135
|
init_sessionTools();
|
|
15926
16136
|
|
|
15927
16137
|
// src/durable-objects/bridge-owner.ts
|
|
@@ -16152,6 +16362,11 @@ var DurableThread = class _DurableThread extends DurableObject {
|
|
|
16152
16362
|
// setValue() so external KV writes (release/reassign) invalidate it too.
|
|
16153
16363
|
executionOwnerCache = void 0;
|
|
16154
16364
|
pendingClientCalls = /* @__PURE__ */ new Map();
|
|
16365
|
+
// Live bidirectional client byte streams (e.g. TCP tunnels for Worker-side
|
|
16366
|
+
// TLS). Keyed by stream id; each stream is bound to the bridge socket it was
|
|
16367
|
+
// opened on and dies with it. Streams are in-memory only — they exist solely
|
|
16368
|
+
// while the client is attached and never park durably.
|
|
16369
|
+
clientStreams = /* @__PURE__ */ new Map();
|
|
16155
16370
|
alarmQueue;
|
|
16156
16371
|
alarmQueueRearmed = false;
|
|
16157
16372
|
currentAbortController = null;
|
|
@@ -16211,6 +16426,9 @@ var DurableThread = class _DurableThread extends DurableObject {
|
|
|
16211
16426
|
case "watchdogCheck":
|
|
16212
16427
|
await this.watchdogCheck(args.threadId);
|
|
16213
16428
|
break;
|
|
16429
|
+
case "clientParkTimeout":
|
|
16430
|
+
await this.failParkedClientCall(args.toolCallId, args.threadId);
|
|
16431
|
+
break;
|
|
16214
16432
|
default:
|
|
16215
16433
|
throw new Error(`Unknown method: ${method}`);
|
|
16216
16434
|
}
|
|
@@ -18194,6 +18412,69 @@ ${resultContent}${attachmentSummary}`;
|
|
|
18194
18412
|
* Stop the currently executing thread (RPC method)
|
|
18195
18413
|
* Simple "off" switch - stops turns, only cleared by user messages
|
|
18196
18414
|
*/
|
|
18415
|
+
/**
|
|
18416
|
+
* Flip every still-`pending` message to `failed` and broadcast the change to
|
|
18417
|
+
* all connected heads. This is THE settle path for any terminal condition
|
|
18418
|
+
* that leaves a turn half-open — user stop, lost execution, terminal
|
|
18419
|
+
* execution failure, and expired parked forwarded calls — so no head is
|
|
18420
|
+
* left rendering a stuck "Working" state. Returns the failure timestamp.
|
|
18421
|
+
*/
|
|
18422
|
+
async failPendingMessagesAndBroadcast() {
|
|
18423
|
+
const now = Date.now() * TIMESTAMP_MULTIPLIER;
|
|
18424
|
+
await this.ctx.storage.sql.exec(
|
|
18425
|
+
`UPDATE messages SET status = 'failed', response_completed_at = ? WHERE status = 'pending'`,
|
|
18426
|
+
now
|
|
18427
|
+
);
|
|
18428
|
+
const cursor = await this.ctx.storage.sql.exec(
|
|
18429
|
+
`SELECT id, role, content, tool_calls, tool_call_id, tool_status, created_at, log_id, status, request_sent_at, response_completed_at FROM messages WHERE response_completed_at = ?`,
|
|
18430
|
+
now
|
|
18431
|
+
);
|
|
18432
|
+
for (const row of cursor) {
|
|
18433
|
+
this.broadcastMessage({
|
|
18434
|
+
id: row.id,
|
|
18435
|
+
role: row.role,
|
|
18436
|
+
content: row.content,
|
|
18437
|
+
tool_calls: row.tool_calls,
|
|
18438
|
+
tool_call_id: row.tool_call_id,
|
|
18439
|
+
tool_status: row.tool_status,
|
|
18440
|
+
created_at: row.created_at,
|
|
18441
|
+
log_id: row.log_id,
|
|
18442
|
+
status: "failed",
|
|
18443
|
+
request_sent_at: row.request_sent_at,
|
|
18444
|
+
response_completed_at: row.response_completed_at
|
|
18445
|
+
});
|
|
18446
|
+
}
|
|
18447
|
+
return now;
|
|
18448
|
+
}
|
|
18449
|
+
/**
|
|
18450
|
+
* A terminal failure ended a turn without a normal assistant reply: settle
|
|
18451
|
+
* every pending message and persist a durable `failed` assistant message so
|
|
18452
|
+
* all connected heads land on a real, visible end state — never a stuck
|
|
18453
|
+
* "Working" spinner. Used by the FlowEngine's terminal catch and the parked
|
|
18454
|
+
* forwarded-call TTL.
|
|
18455
|
+
*/
|
|
18456
|
+
async settleTerminalFailure(errorMessage) {
|
|
18457
|
+
await this.ensureMigrated();
|
|
18458
|
+
await this.failPendingMessagesAndBroadcast();
|
|
18459
|
+
const messageId = crypto.randomUUID();
|
|
18460
|
+
const timestamp = Date.now() * TIMESTAMP_MULTIPLIER;
|
|
18461
|
+
await this.ctx.storage.sql.exec(
|
|
18462
|
+
`INSERT INTO messages (id, role, content, created_at, status, prompt) VALUES (?, ?, ?, ?, ?, ?)`,
|
|
18463
|
+
messageId,
|
|
18464
|
+
"assistant",
|
|
18465
|
+
errorMessage,
|
|
18466
|
+
timestamp,
|
|
18467
|
+
"failed",
|
|
18468
|
+
null
|
|
18469
|
+
);
|
|
18470
|
+
this.broadcastMessage({
|
|
18471
|
+
id: messageId,
|
|
18472
|
+
role: "assistant",
|
|
18473
|
+
content: errorMessage,
|
|
18474
|
+
created_at: timestamp,
|
|
18475
|
+
status: "failed"
|
|
18476
|
+
});
|
|
18477
|
+
}
|
|
18197
18478
|
async stop() {
|
|
18198
18479
|
await this.ensureMigrated();
|
|
18199
18480
|
try {
|
|
@@ -18203,30 +18484,7 @@ ${resultContent}${attachmentSummary}`;
|
|
|
18203
18484
|
if (this.currentAbortController) {
|
|
18204
18485
|
this.currentAbortController.abort();
|
|
18205
18486
|
}
|
|
18206
|
-
const now =
|
|
18207
|
-
const updateCursor = await this.ctx.storage.sql.exec(
|
|
18208
|
-
`UPDATE messages SET status = 'failed', response_completed_at = ? WHERE status = 'pending'`,
|
|
18209
|
-
now
|
|
18210
|
-
);
|
|
18211
|
-
const cursor = await this.ctx.storage.sql.exec(
|
|
18212
|
-
`SELECT id, role, content, tool_calls, tool_call_id, tool_status, created_at, log_id, status, request_sent_at, response_completed_at FROM messages WHERE response_completed_at = ?`,
|
|
18213
|
-
now
|
|
18214
|
-
);
|
|
18215
|
-
for (const row of cursor) {
|
|
18216
|
-
this.broadcastMessage({
|
|
18217
|
-
id: row.id,
|
|
18218
|
-
role: row.role,
|
|
18219
|
-
content: row.content,
|
|
18220
|
-
tool_calls: row.tool_calls,
|
|
18221
|
-
tool_call_id: row.tool_call_id,
|
|
18222
|
-
tool_status: row.tool_status,
|
|
18223
|
-
created_at: row.created_at,
|
|
18224
|
-
log_id: row.log_id,
|
|
18225
|
-
status: "failed",
|
|
18226
|
-
request_sent_at: row.request_sent_at,
|
|
18227
|
-
response_completed_at: row.response_completed_at
|
|
18228
|
-
});
|
|
18229
|
-
}
|
|
18487
|
+
const now = await this.failPendingMessagesAndBroadcast();
|
|
18230
18488
|
const messagesWithToolsCursor = await this.ctx.storage.sql.exec(
|
|
18231
18489
|
`SELECT id, tool_calls, depth FROM messages WHERE tool_calls IS NOT NULL AND tool_calls != 'null' AND (depth IS NULL OR depth = 0)`
|
|
18232
18490
|
);
|
|
@@ -19703,6 +19961,7 @@ ${resultContent}${attachmentSummary}`;
|
|
|
19703
19961
|
socket.close();
|
|
19704
19962
|
} catch {
|
|
19705
19963
|
}
|
|
19964
|
+
this.failClientStreamsForSocket(socket);
|
|
19706
19965
|
this.failPendingClientCallsIfNoBridge();
|
|
19707
19966
|
}
|
|
19708
19967
|
/**
|
|
@@ -19958,6 +20217,352 @@ ${resultContent}${attachmentSummary}`;
|
|
|
19958
20217
|
}
|
|
19959
20218
|
return { side };
|
|
19960
20219
|
}
|
|
20220
|
+
// ─── Client byte streams (Worker-side TLS tunnels) ─────────────────────────
|
|
20221
|
+
// A stream is a bidirectional byte pipe over the bridge WebSocket: the
|
|
20222
|
+
// client opens a raw TCP socket per the stream's `meta` and shuttles bytes
|
|
20223
|
+
// both ways; the Worker terminates TLS over those bytes (node:tls over the
|
|
20224
|
+
// duplex), so the client only ever relays ciphertext. Streams are bound to a
|
|
20225
|
+
// live socket and never park.
|
|
20226
|
+
/** Default time to wait for the client to accept a stream open. */
|
|
20227
|
+
static CLIENT_STREAM_OPEN_TIMEOUT_MS = 3e4;
|
|
20228
|
+
/** Open a bidirectional byte stream to the connected owner client. */
|
|
20229
|
+
async openClientStream(request) {
|
|
20230
|
+
const owner = await this.getExecutionOwner();
|
|
20231
|
+
const socket = this.getOpenBridgeSocket(owner?.client_id ?? null);
|
|
20232
|
+
if (!socket) {
|
|
20233
|
+
return {
|
|
20234
|
+
ok: false,
|
|
20235
|
+
error: "The workspace is not available right now. Try again in a moment.",
|
|
20236
|
+
data: { noClient: true }
|
|
20237
|
+
};
|
|
20238
|
+
}
|
|
20239
|
+
const id = crypto.randomUUID();
|
|
20240
|
+
let controller = null;
|
|
20241
|
+
const readable = new ReadableStream({
|
|
20242
|
+
start: (c) => {
|
|
20243
|
+
controller = c;
|
|
20244
|
+
},
|
|
20245
|
+
cancel: () => {
|
|
20246
|
+
this.abortClientStream(id, "The stream reader was canceled.");
|
|
20247
|
+
}
|
|
20248
|
+
});
|
|
20249
|
+
let settleDone;
|
|
20250
|
+
const done = new Promise((resolve) => {
|
|
20251
|
+
settleDone = resolve;
|
|
20252
|
+
});
|
|
20253
|
+
const state = {
|
|
20254
|
+
socket,
|
|
20255
|
+
controller,
|
|
20256
|
+
opened: null,
|
|
20257
|
+
openTimer: null,
|
|
20258
|
+
settleDone,
|
|
20259
|
+
settled: false
|
|
20260
|
+
};
|
|
20261
|
+
this.clientStreams.set(id, state);
|
|
20262
|
+
this.bridgeLastSeen.set(socket, Date.now());
|
|
20263
|
+
const openResult = await new Promise((resolve) => {
|
|
20264
|
+
state.opened = resolve;
|
|
20265
|
+
const timeoutMs = typeof request.timeoutMs === "number" && request.timeoutMs > 0 ? request.timeoutMs : _DurableThread.CLIENT_STREAM_OPEN_TIMEOUT_MS;
|
|
20266
|
+
state.openTimer = setTimeout(() => {
|
|
20267
|
+
resolve({
|
|
20268
|
+
ok: false,
|
|
20269
|
+
error: `The client did not accept the ${request.tool} stream within ${Math.round(timeoutMs / 1e3)}s.`
|
|
20270
|
+
});
|
|
20271
|
+
}, timeoutMs);
|
|
20272
|
+
try {
|
|
20273
|
+
socket.send(
|
|
20274
|
+
JSON.stringify({
|
|
20275
|
+
type: "stream_open",
|
|
20276
|
+
id,
|
|
20277
|
+
tool: request.tool,
|
|
20278
|
+
meta: request.meta ?? {}
|
|
20279
|
+
})
|
|
20280
|
+
);
|
|
20281
|
+
} catch (error) {
|
|
20282
|
+
resolve({
|
|
20283
|
+
ok: false,
|
|
20284
|
+
error: `Failed to reach the client: ${error instanceof Error ? error.message : String(error)}`
|
|
20285
|
+
});
|
|
20286
|
+
}
|
|
20287
|
+
});
|
|
20288
|
+
state.opened = null;
|
|
20289
|
+
if (state.openTimer) {
|
|
20290
|
+
clearTimeout(state.openTimer);
|
|
20291
|
+
state.openTimer = null;
|
|
20292
|
+
}
|
|
20293
|
+
if (!openResult.ok) {
|
|
20294
|
+
this.clientStreams.delete(id);
|
|
20295
|
+
state.settled = true;
|
|
20296
|
+
settleDone({ ok: false, error: openResult.error });
|
|
20297
|
+
return { ok: false, error: openResult.error ?? "Stream open failed." };
|
|
20298
|
+
}
|
|
20299
|
+
const sendChunked = (bytes) => {
|
|
20300
|
+
for (let i = 0; i < bytes.length; i += CLIENT_STREAM_CHUNK_BYTES) {
|
|
20301
|
+
const chunk = bytes.subarray(i, i + CLIENT_STREAM_CHUNK_BYTES);
|
|
20302
|
+
socket.send(JSON.stringify({ type: "stream_data", id, data: encodeStreamChunk(chunk) }));
|
|
20303
|
+
}
|
|
20304
|
+
};
|
|
20305
|
+
const writable = new WritableStream({
|
|
20306
|
+
write: (chunk) => {
|
|
20307
|
+
if (!this.clientStreams.has(id)) {
|
|
20308
|
+
throw new Error("The client stream is closed.");
|
|
20309
|
+
}
|
|
20310
|
+
sendChunked(chunk);
|
|
20311
|
+
},
|
|
20312
|
+
close: () => {
|
|
20313
|
+
try {
|
|
20314
|
+
if (this.clientStreams.has(id)) {
|
|
20315
|
+
socket.send(JSON.stringify({ type: "stream_end", id }));
|
|
20316
|
+
}
|
|
20317
|
+
} catch {
|
|
20318
|
+
}
|
|
20319
|
+
},
|
|
20320
|
+
abort: (reason) => {
|
|
20321
|
+
this.abortClientStream(id, reason instanceof Error ? reason.message : String(reason));
|
|
20322
|
+
}
|
|
20323
|
+
});
|
|
20324
|
+
const duplex = {
|
|
20325
|
+
readable,
|
|
20326
|
+
writable,
|
|
20327
|
+
close: (error) => this.abortClientStream(id, error),
|
|
20328
|
+
done
|
|
20329
|
+
};
|
|
20330
|
+
return { ok: true, duplex };
|
|
20331
|
+
}
|
|
20332
|
+
// ─── Subagent stream proxy-up (chunk-pull, no cross-DO ReadableStream) ─────
|
|
20333
|
+
// A subagent (child thread, no bridge) proxies its whole stream request UP to
|
|
20334
|
+
// this bridge-holding root. We open the client stream locally, then buffer
|
|
20335
|
+
// incoming bytes so the child can drain them over plain request/response RPCs
|
|
20336
|
+
// — sidestepping the unproven DO-to-DO ReadableStream return.
|
|
20337
|
+
proxyStreams = /* @__PURE__ */ new Map();
|
|
20338
|
+
async proxyStreamOpen(request) {
|
|
20339
|
+
const opened = await this.openClientStream(request);
|
|
20340
|
+
if (!opened.ok) {
|
|
20341
|
+
const noClient = !!(opened.data && opened.data.noClient);
|
|
20342
|
+
return { ok: false, error: opened.error, noClient };
|
|
20343
|
+
}
|
|
20344
|
+
const id = crypto.randomUUID();
|
|
20345
|
+
const duplex = opened.duplex;
|
|
20346
|
+
const reader = duplex.readable.getReader();
|
|
20347
|
+
const writer = duplex.writable.getWriter();
|
|
20348
|
+
const entry = {
|
|
20349
|
+
writer,
|
|
20350
|
+
reader,
|
|
20351
|
+
buffer: [],
|
|
20352
|
+
done: false,
|
|
20353
|
+
error: void 0,
|
|
20354
|
+
pump: Promise.resolve(),
|
|
20355
|
+
close: (error) => duplex.close(error)
|
|
20356
|
+
};
|
|
20357
|
+
entry.pump = (async () => {
|
|
20358
|
+
try {
|
|
20359
|
+
while (true) {
|
|
20360
|
+
const { value, done } = await reader.read();
|
|
20361
|
+
if (done) break;
|
|
20362
|
+
if (value) entry.buffer.push(value);
|
|
20363
|
+
}
|
|
20364
|
+
} catch (error) {
|
|
20365
|
+
entry.error = error instanceof Error ? error.message : String(error);
|
|
20366
|
+
} finally {
|
|
20367
|
+
entry.done = true;
|
|
20368
|
+
}
|
|
20369
|
+
})();
|
|
20370
|
+
void duplex.done.then((d) => {
|
|
20371
|
+
entry.done = true;
|
|
20372
|
+
if (!d.ok && !entry.error) entry.error = d.error;
|
|
20373
|
+
});
|
|
20374
|
+
this.proxyStreams.set(id, entry);
|
|
20375
|
+
return { ok: true, id };
|
|
20376
|
+
}
|
|
20377
|
+
async proxyStreamWrite(id, dataB64) {
|
|
20378
|
+
const entry = this.proxyStreams.get(id);
|
|
20379
|
+
if (!entry) return;
|
|
20380
|
+
try {
|
|
20381
|
+
await entry.writer.write(decodeStreamChunk(dataB64));
|
|
20382
|
+
} catch {
|
|
20383
|
+
}
|
|
20384
|
+
}
|
|
20385
|
+
async proxyStreamPull(id) {
|
|
20386
|
+
const entry = this.proxyStreams.get(id);
|
|
20387
|
+
if (!entry) return { chunks: [], done: true, error: "unknown proxy stream" };
|
|
20388
|
+
const chunks = entry.buffer.splice(0).map((b) => encodeStreamChunk(b));
|
|
20389
|
+
if (entry.done && entry.buffer.length === 0) {
|
|
20390
|
+
this.proxyStreams.delete(id);
|
|
20391
|
+
return { chunks, done: true, error: entry.error };
|
|
20392
|
+
}
|
|
20393
|
+
return { chunks, done: false };
|
|
20394
|
+
}
|
|
20395
|
+
async proxyStreamEnd(id) {
|
|
20396
|
+
const entry = this.proxyStreams.get(id);
|
|
20397
|
+
if (!entry) return;
|
|
20398
|
+
try {
|
|
20399
|
+
await entry.writer.close();
|
|
20400
|
+
} catch {
|
|
20401
|
+
}
|
|
20402
|
+
}
|
|
20403
|
+
async proxyStreamAbort(id) {
|
|
20404
|
+
const entry = this.proxyStreams.get(id);
|
|
20405
|
+
if (!entry) return;
|
|
20406
|
+
this.proxyStreams.delete(id);
|
|
20407
|
+
try {
|
|
20408
|
+
entry.close("aborted");
|
|
20409
|
+
} catch {
|
|
20410
|
+
}
|
|
20411
|
+
}
|
|
20412
|
+
/** Settle and remove a client stream exactly once. */
|
|
20413
|
+
settleClientStream(id, result) {
|
|
20414
|
+
const state = this.clientStreams.get(id);
|
|
20415
|
+
if (!state) return;
|
|
20416
|
+
this.clientStreams.delete(id);
|
|
20417
|
+
if (state.openTimer) clearTimeout(state.openTimer);
|
|
20418
|
+
if (state.opened) state.opened({ ok: false, error: result.error ?? "Stream closed." });
|
|
20419
|
+
if (!state.settled) {
|
|
20420
|
+
state.settled = true;
|
|
20421
|
+
try {
|
|
20422
|
+
if (result.ok) state.controller?.close();
|
|
20423
|
+
else state.controller?.error(new Error(result.error ?? "Client stream aborted."));
|
|
20424
|
+
} catch {
|
|
20425
|
+
}
|
|
20426
|
+
state.settleDone(result);
|
|
20427
|
+
}
|
|
20428
|
+
}
|
|
20429
|
+
/** Abort a stream from the Worker side, telling the client to tear down. */
|
|
20430
|
+
abortClientStream(id, error) {
|
|
20431
|
+
const state = this.clientStreams.get(id);
|
|
20432
|
+
if (!state) return;
|
|
20433
|
+
try {
|
|
20434
|
+
if (state.socket.readyState === WebSocket.OPEN) {
|
|
20435
|
+
state.socket.send(JSON.stringify({ type: "stream_abort", id, error: error ?? null }));
|
|
20436
|
+
}
|
|
20437
|
+
} catch {
|
|
20438
|
+
}
|
|
20439
|
+
this.settleClientStream(id, error ? { ok: false, error } : { ok: true });
|
|
20440
|
+
}
|
|
20441
|
+
/** Handle an incoming stream_* frame from a bridge client. */
|
|
20442
|
+
handleClientStreamFrame(ws, parsed) {
|
|
20443
|
+
const id = typeof parsed.id === "string" ? parsed.id : null;
|
|
20444
|
+
if (!id) return;
|
|
20445
|
+
const state = this.clientStreams.get(id);
|
|
20446
|
+
if (!state || state.socket !== ws) return;
|
|
20447
|
+
switch (parsed.type) {
|
|
20448
|
+
case "stream_opened": {
|
|
20449
|
+
if (state.opened) {
|
|
20450
|
+
const resolve = state.opened;
|
|
20451
|
+
state.opened = null;
|
|
20452
|
+
if (state.openTimer) {
|
|
20453
|
+
clearTimeout(state.openTimer);
|
|
20454
|
+
state.openTimer = null;
|
|
20455
|
+
}
|
|
20456
|
+
resolve({ ok: true });
|
|
20457
|
+
}
|
|
20458
|
+
return;
|
|
20459
|
+
}
|
|
20460
|
+
case "stream_error": {
|
|
20461
|
+
const error = typeof parsed.error === "string" ? parsed.error : "Client stream error.";
|
|
20462
|
+
this.settleClientStream(id, { ok: false, error });
|
|
20463
|
+
return;
|
|
20464
|
+
}
|
|
20465
|
+
case "stream_data": {
|
|
20466
|
+
if (typeof parsed.data !== "string") return;
|
|
20467
|
+
try {
|
|
20468
|
+
state.controller?.enqueue(decodeStreamChunk(parsed.data));
|
|
20469
|
+
} catch {
|
|
20470
|
+
this.abortClientStream(id, "Failed to deliver stream data.");
|
|
20471
|
+
}
|
|
20472
|
+
return;
|
|
20473
|
+
}
|
|
20474
|
+
case "stream_end": {
|
|
20475
|
+
this.settleClientStream(id, { ok: true });
|
|
20476
|
+
return;
|
|
20477
|
+
}
|
|
20478
|
+
case "stream_abort": {
|
|
20479
|
+
const error = typeof parsed.error === "string" ? parsed.error : void 0;
|
|
20480
|
+
this.settleClientStream(id, { ok: false, error: error ?? "Client aborted the stream." });
|
|
20481
|
+
return;
|
|
20482
|
+
}
|
|
20483
|
+
}
|
|
20484
|
+
}
|
|
20485
|
+
/** Fail every stream bound to a socket that closed or died. */
|
|
20486
|
+
failClientStreamsForSocket(ws) {
|
|
20487
|
+
for (const [id, state] of Array.from(this.clientStreams.entries())) {
|
|
20488
|
+
if (state.socket === ws) {
|
|
20489
|
+
this.settleClientStream(id, { ok: false, error: "The client connection was lost." });
|
|
20490
|
+
}
|
|
20491
|
+
}
|
|
20492
|
+
}
|
|
20493
|
+
// ─── Parked-call wake + bounded TTL ────────────────────────────────────────
|
|
20494
|
+
/** How long a parked forwarded call waits for a client before failing visibly. */
|
|
20495
|
+
static CLIENT_PARK_TTL_MS = 30 * 60 * 1e3;
|
|
20496
|
+
/**
|
|
20497
|
+
* A forwarded call parked with NO client attached: ask the user's hub to
|
|
20498
|
+
* summon the owning machine's daemon to this thread, and arm the bounded
|
|
20499
|
+
* park TTL that turns an unreachable host into a visible durable failure
|
|
20500
|
+
* instead of a silent hang.
|
|
20501
|
+
*/
|
|
20502
|
+
async notifyClientWaiting(toolCallId, threadId, userId) {
|
|
20503
|
+
try {
|
|
20504
|
+
await this.alarmQueue.enqueue(
|
|
20505
|
+
"clientParkTimeout",
|
|
20506
|
+
{ toolCallId, threadId },
|
|
20507
|
+
_DurableThread.CLIENT_PARK_TTL_MS
|
|
20508
|
+
);
|
|
20509
|
+
} catch (error) {
|
|
20510
|
+
console.error("[DurableThread] Failed to arm park TTL (non-fatal):", error);
|
|
20511
|
+
}
|
|
20512
|
+
if (!userId) return;
|
|
20513
|
+
try {
|
|
20514
|
+
const namespace = this.env.AGENT_BUILDER_USER_HUB;
|
|
20515
|
+
if (!namespace) return;
|
|
20516
|
+
const hub = namespace.get(namespace.idFromName(userId));
|
|
20517
|
+
await hub.requestWake(threadId);
|
|
20518
|
+
} catch (error) {
|
|
20519
|
+
console.error("[DurableThread] Hub wake failed (non-fatal):", error);
|
|
20520
|
+
}
|
|
20521
|
+
}
|
|
20522
|
+
/**
|
|
20523
|
+
* Park TTL expired: if the call is still pending, settle it as a durable,
|
|
20524
|
+
* VISIBLE failure — rewrite the waiting placeholder, mark the pending row
|
|
20525
|
+
* failed, fail any still-pending assistant messages, and persist a durable
|
|
20526
|
+
* failed assistant message so every connected head settles instead of
|
|
20527
|
+
* showing "Working" forever.
|
|
20528
|
+
*/
|
|
20529
|
+
async failParkedClientCall(toolCallId, threadId) {
|
|
20530
|
+
await this.ensureMigrated();
|
|
20531
|
+
await this.ensurePendingClientTable();
|
|
20532
|
+
const cursor = await this.ctx.storage.sql.exec(
|
|
20533
|
+
`SELECT status, tool FROM pending_client_calls WHERE tool_call_id = ? LIMIT 1`,
|
|
20534
|
+
toolCallId
|
|
20535
|
+
);
|
|
20536
|
+
const row = cursor.toArray()[0];
|
|
20537
|
+
if (!row || row.status !== "pending") return;
|
|
20538
|
+
const errorText = "Could not reach your machine to run this operation. Reconnect it and retry.";
|
|
20539
|
+
await this.ctx.storage.sql.exec(
|
|
20540
|
+
`UPDATE pending_client_calls SET status = 'failed' WHERE tool_call_id = ?`,
|
|
20541
|
+
toolCallId
|
|
20542
|
+
);
|
|
20543
|
+
await this.ctx.storage.sql.exec(
|
|
20544
|
+
`UPDATE messages SET content = ?, tool_status = 'error' WHERE tool_call_id = ? AND role = 'tool'`,
|
|
20545
|
+
`Error: ${errorText}`,
|
|
20546
|
+
toolCallId
|
|
20547
|
+
);
|
|
20548
|
+
try {
|
|
20549
|
+
const msgCursor = await this.ctx.storage.sql.exec(
|
|
20550
|
+
`SELECT id, role, content, tool_call_id, tool_status, created_at, depth
|
|
20551
|
+
FROM messages WHERE tool_call_id = ? AND role = 'tool' LIMIT 1`,
|
|
20552
|
+
toolCallId
|
|
20553
|
+
);
|
|
20554
|
+
const msg = msgCursor.toArray()[0];
|
|
20555
|
+
if (msg) this.broadcastMessage(msg);
|
|
20556
|
+
} catch {
|
|
20557
|
+
}
|
|
20558
|
+
await this.settleTerminalFailure(errorText);
|
|
20559
|
+
if (threadId) {
|
|
20560
|
+
try {
|
|
20561
|
+
await this.disarmExecutionWatchdog(threadId);
|
|
20562
|
+
} catch {
|
|
20563
|
+
}
|
|
20564
|
+
}
|
|
20565
|
+
}
|
|
19961
20566
|
/**
|
|
19962
20567
|
* Broadcast a log record to all connected WebSocket clients
|
|
19963
20568
|
*/
|
|
@@ -20174,6 +20779,10 @@ ${resultContent}${attachmentSummary}`;
|
|
|
20174
20779
|
this.resolveClientCall(ws, parsed);
|
|
20175
20780
|
return;
|
|
20176
20781
|
}
|
|
20782
|
+
if (parsed && typeof parsed.type === "string" && parsed.type.startsWith("stream_") && parsed.type !== "stream_ping" && parsed.type !== "stream_pong") {
|
|
20783
|
+
this.handleClientStreamFrame(ws, parsed);
|
|
20784
|
+
return;
|
|
20785
|
+
}
|
|
20177
20786
|
}
|
|
20178
20787
|
} catch (error) {
|
|
20179
20788
|
console.error("[DurableThread] Error handling WebSocket message:", error);
|
|
@@ -20194,6 +20803,7 @@ ${resultContent}${attachmentSummary}`;
|
|
|
20194
20803
|
this.bridgeSockets.delete(ws);
|
|
20195
20804
|
this.bridgeLastSeen.delete(ws);
|
|
20196
20805
|
this.bridgeClientInfo.delete(ws);
|
|
20806
|
+
this.failClientStreamsForSocket(ws);
|
|
20197
20807
|
this.failPendingClientCallsIfNoBridge();
|
|
20198
20808
|
}
|
|
20199
20809
|
}
|
|
@@ -20215,6 +20825,7 @@ ${resultContent}${attachmentSummary}`;
|
|
|
20215
20825
|
this.bridgeSockets.delete(ws);
|
|
20216
20826
|
this.bridgeLastSeen.delete(ws);
|
|
20217
20827
|
this.bridgeClientInfo.delete(ws);
|
|
20828
|
+
this.failClientStreamsForSocket(ws);
|
|
20218
20829
|
this.failPendingClientCallsIfNoBridge();
|
|
20219
20830
|
}
|
|
20220
20831
|
}
|
|
@@ -20532,6 +21143,11 @@ ${resultContent}${attachmentSummary}`;
|
|
|
20532
21143
|
data: { id, is_complete: true, error, error_type: errorType }
|
|
20533
21144
|
});
|
|
20534
21145
|
}
|
|
21146
|
+
try {
|
|
21147
|
+
await this.failPendingMessagesAndBroadcast();
|
|
21148
|
+
} catch (error2) {
|
|
21149
|
+
console.error("[DurableThread] Failed to settle pending messages (non-fatal):", error2);
|
|
21150
|
+
}
|
|
20535
21151
|
}
|
|
20536
21152
|
/**
|
|
20537
21153
|
* A subagent whose execution the watchdog gave up on leaves its parent
|
|
@@ -23523,11 +24139,24 @@ ${result ?? error ?? "No result content."}${attachmentSummary}`;
|
|
|
23523
24139
|
throw new Error("User value user_id must be a non-empty string");
|
|
23524
24140
|
}
|
|
23525
24141
|
}
|
|
24142
|
+
/**
|
|
24143
|
+
* The per-user hub is the sharded home for account-wide KV. When the binding
|
|
24144
|
+
* exists, the singleton delegates so the global object is no longer a
|
|
24145
|
+
* per-user bottleneck; without it (pre-hub deploys, unit tests) it keeps the
|
|
24146
|
+
* local `user_values` table.
|
|
24147
|
+
*/
|
|
24148
|
+
userHub(userId) {
|
|
24149
|
+
const namespace = this.env.AGENT_BUILDER_USER_HUB;
|
|
24150
|
+
if (!namespace) return null;
|
|
24151
|
+
return namespace.get(namespace.idFromName(userId));
|
|
24152
|
+
}
|
|
23526
24153
|
/** Read one JSON value from a user's account-wide key-value store. */
|
|
23527
24154
|
async getUserValue(userId, key) {
|
|
23528
24155
|
await this.ensureMigrated();
|
|
23529
24156
|
this.assertValidUserValueUserId(userId);
|
|
23530
24157
|
this.assertValidUserValueKey(key);
|
|
24158
|
+
const hub = this.userHub(userId);
|
|
24159
|
+
if (hub) return hub.getUserValue(userId, key);
|
|
23531
24160
|
const cursor = await this.ctx.storage.sql.exec(
|
|
23532
24161
|
`SELECT value FROM user_values WHERE user_id = ? AND key = ? LIMIT 1`,
|
|
23533
24162
|
userId,
|
|
@@ -23548,6 +24177,8 @@ ${result ?? error ?? "No result content."}${attachmentSummary}`;
|
|
|
23548
24177
|
await this.ensureMigrated();
|
|
23549
24178
|
this.assertValidUserValueUserId(userId);
|
|
23550
24179
|
this.assertValidUserValueKey(key);
|
|
24180
|
+
const hub = this.userHub(userId);
|
|
24181
|
+
if (hub) return hub.setUserValue(userId, key, value);
|
|
23551
24182
|
if (value === null || value === void 0) {
|
|
23552
24183
|
await this.ctx.storage.sql.exec(
|
|
23553
24184
|
`DELETE FROM user_values WHERE user_id = ? AND key = ?`,
|
|
@@ -23584,6 +24215,8 @@ ${result ?? error ?? "No result content."}${attachmentSummary}`;
|
|
|
23584
24215
|
async listUserValues(userId, options = {}) {
|
|
23585
24216
|
await this.ensureMigrated();
|
|
23586
24217
|
this.assertValidUserValueUserId(userId);
|
|
24218
|
+
const hub = this.userHub(userId);
|
|
24219
|
+
if (hub) return hub.listUserValues(userId, options);
|
|
23587
24220
|
const limit = Math.min(Math.max(Math.trunc(options.limit ?? 100), 1), 500);
|
|
23588
24221
|
const offset = Math.max(Math.trunc(options.offset ?? 0), 0);
|
|
23589
24222
|
const prefix = typeof options.prefix === "string" && options.prefix.length > 0 ? options.prefix : null;
|
|
@@ -25332,6 +25965,192 @@ function formatMs(ms) {
|
|
|
25332
25965
|
const minutes = Math.floor(seconds / 60);
|
|
25333
25966
|
return `${minutes}m${String(seconds % 60).padStart(2, "0")}s`;
|
|
25334
25967
|
}
|
|
25968
|
+
var DurableUserHub = class extends DurableObject {
|
|
25969
|
+
/** Persistent client (daemon) sockets, restored across hibernation. */
|
|
25970
|
+
clientSockets = /* @__PURE__ */ new Set();
|
|
25971
|
+
clientInfo = /* @__PURE__ */ new Map();
|
|
25972
|
+
migrated = false;
|
|
25973
|
+
constructor(ctx, env) {
|
|
25974
|
+
super(ctx, env);
|
|
25975
|
+
if (typeof WebSocketRequestResponsePair !== "undefined") {
|
|
25976
|
+
ctx.setWebSocketAutoResponse(
|
|
25977
|
+
new WebSocketRequestResponsePair("stream_ping", "stream_pong")
|
|
25978
|
+
);
|
|
25979
|
+
}
|
|
25980
|
+
for (const ws of ctx.getWebSockets()) {
|
|
25981
|
+
const attachment = ws.deserializeAttachment();
|
|
25982
|
+
if (attachment?.type === "hub_client") {
|
|
25983
|
+
this.clientSockets.add(ws);
|
|
25984
|
+
this.clientInfo.set(ws, {
|
|
25985
|
+
clientId: typeof attachment.clientId === "string" ? attachment.clientId : null,
|
|
25986
|
+
clientName: typeof attachment.clientName === "string" ? attachment.clientName : null,
|
|
25987
|
+
clientKind: typeof attachment.clientKind === "string" ? attachment.clientKind : null
|
|
25988
|
+
});
|
|
25989
|
+
}
|
|
25990
|
+
}
|
|
25991
|
+
}
|
|
25992
|
+
ensureMigrated() {
|
|
25993
|
+
if (this.migrated) return;
|
|
25994
|
+
this.ctx.storage.sql.exec(`
|
|
25995
|
+
CREATE TABLE IF NOT EXISTS user_values (
|
|
25996
|
+
user_id TEXT NOT NULL,
|
|
25997
|
+
key TEXT NOT NULL,
|
|
25998
|
+
value TEXT NOT NULL,
|
|
25999
|
+
created_at INTEGER NOT NULL,
|
|
26000
|
+
updated_at INTEGER NOT NULL,
|
|
26001
|
+
PRIMARY KEY (user_id, key)
|
|
26002
|
+
)
|
|
26003
|
+
`);
|
|
26004
|
+
this.ctx.storage.sql.exec(
|
|
26005
|
+
`CREATE INDEX IF NOT EXISTS idx_user_values_user ON user_values(user_id)`
|
|
26006
|
+
);
|
|
26007
|
+
this.migrated = true;
|
|
26008
|
+
}
|
|
26009
|
+
// ─── Persistent client socket ──────────────────────────────────────────────
|
|
26010
|
+
/**
|
|
26011
|
+
* Accept the user's persistent client (daemon) WebSocket. The daemon holds
|
|
26012
|
+
* this ONE socket for its whole lifetime; per-thread bridge sockets are
|
|
26013
|
+
* opened separately and only while a thread is active.
|
|
26014
|
+
*/
|
|
26015
|
+
async fetch(request) {
|
|
26016
|
+
const url = new URL(request.url);
|
|
26017
|
+
if (request.headers.get("Upgrade") === "websocket") {
|
|
26018
|
+
const pair = new WebSocketPair();
|
|
26019
|
+
const [client, server] = [pair[0], pair[1]];
|
|
26020
|
+
const info = {
|
|
26021
|
+
clientId: url.searchParams.get("client_id"),
|
|
26022
|
+
clientName: url.searchParams.get("client_name"),
|
|
26023
|
+
clientKind: url.searchParams.get("client_kind")
|
|
26024
|
+
};
|
|
26025
|
+
this.ctx.acceptWebSocket(server);
|
|
26026
|
+
server.serializeAttachment({ type: "hub_client", ...info });
|
|
26027
|
+
this.clientSockets.add(server);
|
|
26028
|
+
this.clientInfo.set(server, info);
|
|
26029
|
+
try {
|
|
26030
|
+
server.send(JSON.stringify({ type: "hub_ready" }));
|
|
26031
|
+
} catch {
|
|
26032
|
+
}
|
|
26033
|
+
return new Response(null, { status: 101, webSocket: client });
|
|
26034
|
+
}
|
|
26035
|
+
return new Response("Not found", { status: 404 });
|
|
26036
|
+
}
|
|
26037
|
+
async webSocketMessage(ws, _message) {
|
|
26038
|
+
if (!this.clientSockets.has(ws)) this.clientSockets.add(ws);
|
|
26039
|
+
}
|
|
26040
|
+
async webSocketClose(ws) {
|
|
26041
|
+
this.clientSockets.delete(ws);
|
|
26042
|
+
this.clientInfo.delete(ws);
|
|
26043
|
+
}
|
|
26044
|
+
async webSocketError(ws) {
|
|
26045
|
+
this.clientSockets.delete(ws);
|
|
26046
|
+
this.clientInfo.delete(ws);
|
|
26047
|
+
}
|
|
26048
|
+
/** Whether a persistent client (daemon) is connected right now. */
|
|
26049
|
+
async hasClient() {
|
|
26050
|
+
for (const ws of this.clientSockets) {
|
|
26051
|
+
if (ws.readyState === WebSocket.OPEN) return true;
|
|
26052
|
+
}
|
|
26053
|
+
return false;
|
|
26054
|
+
}
|
|
26055
|
+
// ─── Wake orchestration (the load-bearing RPC) ─────────────────────────────
|
|
26056
|
+
/**
|
|
26057
|
+
* Signal the connected daemon to attach a bridge to `threadId`. Called by a
|
|
26058
|
+
* cold thread DO that parked a forwarded call with no client attached. The
|
|
26059
|
+
* daemon reacts by opening a per-thread bridge socket, which triggers the
|
|
26060
|
+
* thread's drain-on-attach. Returns whether a daemon was reachable.
|
|
26061
|
+
*/
|
|
26062
|
+
async requestWake(threadId) {
|
|
26063
|
+
const frame = { type: "wake", threadId };
|
|
26064
|
+
const payload = JSON.stringify(frame);
|
|
26065
|
+
let delivered = false;
|
|
26066
|
+
for (const ws of Array.from(this.clientSockets)) {
|
|
26067
|
+
if (ws.readyState !== WebSocket.OPEN) {
|
|
26068
|
+
this.clientSockets.delete(ws);
|
|
26069
|
+
this.clientInfo.delete(ws);
|
|
26070
|
+
continue;
|
|
26071
|
+
}
|
|
26072
|
+
try {
|
|
26073
|
+
ws.send(payload);
|
|
26074
|
+
delivered = true;
|
|
26075
|
+
} catch {
|
|
26076
|
+
this.clientSockets.delete(ws);
|
|
26077
|
+
this.clientInfo.delete(ws);
|
|
26078
|
+
}
|
|
26079
|
+
}
|
|
26080
|
+
return { delivered };
|
|
26081
|
+
}
|
|
26082
|
+
// ─── Account-wide KV (sharded off the singleton) ───────────────────────────
|
|
26083
|
+
async getUserValue(userId, key) {
|
|
26084
|
+
this.ensureMigrated();
|
|
26085
|
+
const cursor = await this.ctx.storage.sql.exec(
|
|
26086
|
+
`SELECT value FROM user_values WHERE user_id = ? AND key = ? LIMIT 1`,
|
|
26087
|
+
userId,
|
|
26088
|
+
key
|
|
26089
|
+
);
|
|
26090
|
+
const raw = cursor.toArray()[0]?.value;
|
|
26091
|
+
if (raw === void 0) return null;
|
|
26092
|
+
try {
|
|
26093
|
+
return JSON.parse(raw);
|
|
26094
|
+
} catch {
|
|
26095
|
+
return null;
|
|
26096
|
+
}
|
|
26097
|
+
}
|
|
26098
|
+
async setUserValue(userId, key, value) {
|
|
26099
|
+
this.ensureMigrated();
|
|
26100
|
+
if (value === null || value === void 0) {
|
|
26101
|
+
await this.ctx.storage.sql.exec(
|
|
26102
|
+
`DELETE FROM user_values WHERE user_id = ? AND key = ?`,
|
|
26103
|
+
userId,
|
|
26104
|
+
key
|
|
26105
|
+
);
|
|
26106
|
+
return;
|
|
26107
|
+
}
|
|
26108
|
+
const serialized = JSON.stringify(value);
|
|
26109
|
+
const now = Date.now();
|
|
26110
|
+
await this.ctx.storage.sql.exec(
|
|
26111
|
+
`INSERT INTO user_values (user_id, key, value, created_at, updated_at)
|
|
26112
|
+
VALUES (?1, ?2, ?3, ?4, ?4)
|
|
26113
|
+
ON CONFLICT(user_id, key) DO UPDATE SET
|
|
26114
|
+
value = excluded.value,
|
|
26115
|
+
updated_at = excluded.updated_at`,
|
|
26116
|
+
userId,
|
|
26117
|
+
key,
|
|
26118
|
+
serialized,
|
|
26119
|
+
now
|
|
26120
|
+
);
|
|
26121
|
+
}
|
|
26122
|
+
async listUserValues(userId, options = {}) {
|
|
26123
|
+
this.ensureMigrated();
|
|
26124
|
+
const limit = Math.min(Math.max(Math.trunc(options.limit ?? 100), 1), 500);
|
|
26125
|
+
const offset = Math.max(Math.trunc(options.offset ?? 0), 0);
|
|
26126
|
+
const prefix = typeof options.prefix === "string" && options.prefix.length > 0 ? options.prefix : null;
|
|
26127
|
+
const escapedPrefix = prefix ? `${prefix.replace(/[\\%_]/g, (m) => `\\${m}`)}%` : null;
|
|
26128
|
+
const where = escapedPrefix ? `WHERE user_id = ? AND key LIKE ? ESCAPE '\\'` : `WHERE user_id = ?`;
|
|
26129
|
+
const whereArgs = escapedPrefix ? [userId, escapedPrefix] : [userId];
|
|
26130
|
+
const totalCursor = await this.ctx.storage.sql.exec(
|
|
26131
|
+
`SELECT COUNT(*) AS total FROM user_values ${where}`,
|
|
26132
|
+
...whereArgs
|
|
26133
|
+
);
|
|
26134
|
+
const total = Number(totalCursor.toArray()[0]?.total ?? 0);
|
|
26135
|
+
const cursor = await this.ctx.storage.sql.exec(
|
|
26136
|
+
`SELECT key, value, updated_at FROM user_values ${where}
|
|
26137
|
+
ORDER BY key ASC LIMIT ? OFFSET ?`,
|
|
26138
|
+
...whereArgs,
|
|
26139
|
+
limit,
|
|
26140
|
+
offset
|
|
26141
|
+
);
|
|
26142
|
+
const entries = cursor.toArray().map((row) => {
|
|
26143
|
+
let value = null;
|
|
26144
|
+
try {
|
|
26145
|
+
value = JSON.parse(row.value);
|
|
26146
|
+
} catch {
|
|
26147
|
+
value = null;
|
|
26148
|
+
}
|
|
26149
|
+
return { key: row.key, value, updated_at: Number(row.updated_at) };
|
|
26150
|
+
});
|
|
26151
|
+
return { entries, total, limit, offset };
|
|
26152
|
+
}
|
|
26153
|
+
};
|
|
25335
26154
|
|
|
25336
26155
|
// src/runtime.ts
|
|
25337
26156
|
init_utilities();
|
|
@@ -25694,6 +26513,6 @@ init_context();
|
|
|
25694
26513
|
init_ProviderRegistry();
|
|
25695
26514
|
init_types2();
|
|
25696
26515
|
|
|
25697
|
-
export { CodeExecutionBridge, DurableAgentBuilder, DurableThread, FlowStateSdk, NamespaceResolutionError, ProviderRegistry, ThreadWatchdog, authenticate, buildImageDescription, cat, createNamespaceContext, createThreadEndpointHandler, defineController2 as defineController, emitThreadEvent, enhanceFlowState, exists, find, forceTurn, generateImageDescription, getFileStats, getMessages, getMessagesToSummarize, getShortName, getThumbnail, getUnsummarizedImageAttachments, getVisibleAgentNames, getVisibleModelNames, getVisiblePromptNames, getVisibleToolNames, grep, hasImageAttachments, head, injectMessage, isQualifiedName, linkFile, mkdir, move, optimizeImageContext, parseQualifiedName, qualifyName, queueTool, readFile, readdir, reloadHistory, rename, replaceImagesWithDescriptions, requireAdmin, requireAuth, resolveAgent, resolveHook, resolveModel, resolvePrompt, resolveTool, rmdir, stat, tail, unlink, updateThread, writeFile, writeImage };
|
|
26516
|
+
export { CodeExecutionBridge, DurableAgentBuilder, DurableThread, DurableUserHub, FlowStateSdk, NamespaceResolutionError, ProviderRegistry, ThreadWatchdog, authenticate, buildImageDescription, cat, createNamespaceContext, createThreadEndpointHandler, defineController2 as defineController, emitThreadEvent, enhanceFlowState, exists, find, forceTurn, generateImageDescription, getFileStats, getMessages, getMessagesToSummarize, getShortName, getThumbnail, getUnsummarizedImageAttachments, getVisibleAgentNames, getVisibleModelNames, getVisiblePromptNames, getVisibleToolNames, grep, hasImageAttachments, head, injectMessage, isQualifiedName, linkFile, mkdir, move, optimizeImageContext, parseQualifiedName, qualifyName, queueTool, readFile, readdir, reloadHistory, rename, replaceImagesWithDescriptions, requireAdmin, requireAuth, resolveAgent, resolveHook, resolveModel, resolvePrompt, resolveTool, rmdir, stat, tail, unlink, updateThread, writeFile, writeImage };
|
|
25698
26517
|
//# sourceMappingURL=runtime.js.map
|
|
25699
26518
|
//# sourceMappingURL=runtime.js.map
|