@zackbart/connecta 0.7.3 → 0.7.5
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/CHANGELOG.md +68 -0
- package/README.md +6 -3
- package/dist/connector-scope.d.ts +7 -4
- package/dist/connector-scope.d.ts.map +1 -1
- package/dist/connector-scope.js +36 -16
- package/dist/connector-scope.js.map +1 -1
- package/dist/connectors/remote-mcp.d.ts.map +1 -1
- package/dist/connectors/remote-mcp.js +47 -16
- package/dist/connectors/remote-mcp.js.map +1 -1
- package/dist/credential-health.d.ts +3 -2
- package/dist/credential-health.d.ts.map +1 -1
- package/dist/credential-health.js +9 -9
- package/dist/credential-health.js.map +1 -1
- package/dist/execute.d.ts +3 -0
- package/dist/execute.d.ts.map +1 -1
- package/dist/execute.js +70 -29
- package/dist/execute.js.map +1 -1
- package/dist/executor-admission.d.ts +53 -0
- package/dist/executor-admission.d.ts.map +1 -0
- package/dist/executor-admission.js +151 -0
- package/dist/executor-admission.js.map +1 -0
- package/dist/executor-result.d.ts +13 -0
- package/dist/executor-result.d.ts.map +1 -0
- package/dist/executor-result.js +57 -0
- package/dist/executor-result.js.map +1 -0
- package/dist/executors/quickjs-child.d.ts +2 -0
- package/dist/executors/quickjs-child.d.ts.map +1 -0
- package/dist/executors/quickjs-child.js +131 -0
- package/dist/executors/quickjs-child.js.map +1 -0
- package/dist/executors/quickjs-protocol.d.ts +55 -0
- package/dist/executors/quickjs-protocol.d.ts.map +1 -0
- package/dist/executors/quickjs-protocol.js +22 -0
- package/dist/executors/quickjs-protocol.js.map +1 -0
- package/dist/executors/quickjs-runtime.d.ts +26 -0
- package/dist/executors/quickjs-runtime.d.ts.map +1 -0
- package/dist/executors/quickjs-runtime.js +383 -0
- package/dist/executors/quickjs-runtime.js.map +1 -0
- package/dist/executors/quickjs.d.ts +20 -15
- package/dist/executors/quickjs.d.ts.map +1 -1
- package/dist/executors/quickjs.js +459 -311
- package/dist/executors/quickjs.js.map +1 -1
- package/dist/index.d.ts +13 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/meta-tools.d.ts +4 -0
- package/dist/meta-tools.d.ts.map +1 -1
- package/dist/meta-tools.js +3 -2
- package/dist/meta-tools.js.map +1 -1
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +36 -3
- package/dist/node.js.map +1 -1
- package/dist/registry.d.ts +3 -2
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +4 -4
- package/dist/registry.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +11 -4
- package/dist/server.js.map +1 -1
- package/dist/types.d.ts +17 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/ui.d.ts +2 -1
- package/dist/ui.d.ts.map +1 -1
- package/dist/ui.js +3 -3
- package/dist/ui.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/src/connector-scope.ts +43 -18
- package/src/connectors/remote-mcp.ts +56 -19
- package/src/credential-health.ts +20 -6
- package/src/execute.ts +93 -35
- package/src/executor-admission.ts +229 -0
- package/src/executor-result.ts +63 -0
- package/src/executors/quickjs-child.ts +168 -0
- package/src/executors/quickjs-protocol.ts +77 -0
- package/src/executors/quickjs-runtime.ts +440 -0
- package/src/executors/quickjs.ts +660 -353
- package/src/index.ts +28 -4
- package/src/meta-tools.ts +9 -1
- package/src/node.ts +36 -2
- package/src/registry.ts +5 -2
- package/src/server.ts +10 -2
- package/src/types.ts +17 -0
- package/src/ui.ts +6 -1
- package/src/version.ts +1 -1
package/package.json
CHANGED
package/src/connector-scope.ts
CHANGED
|
@@ -3,38 +3,63 @@ import type { Connector, ConnectorContext } from "./types.js";
|
|
|
3
3
|
/** Enough for local transport abort/close without letting cleanup own latency. */
|
|
4
4
|
const CONNECTOR_SCOPE_CLOSE_BUDGET_MS = 100;
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Bound on cleanup continued after the caller-facing window expires.
|
|
8
|
+
*
|
|
9
|
+
* `remoteMcp` spends at most one second asking the downstream to terminate its
|
|
10
|
+
* session, leaving another second for the local close. Custom hooks still get a
|
|
11
|
+
* finite background window: handing a never-settling promise to a Worker's
|
|
12
|
+
* `waitUntil` would otherwise keep the invocation alive until the platform cap.
|
|
13
|
+
*/
|
|
14
|
+
const CONNECTOR_SCOPE_DEFER_BUDGET_MS = 2_000;
|
|
15
|
+
|
|
16
|
+
/** Runtime hook for work that may safely continue after a response is ready. */
|
|
17
|
+
export type DeferredWork = (promise: Promise<unknown>) => void;
|
|
18
|
+
|
|
19
|
+
/** Resolve when `work` settles or `budgetMs` expires; never reject. */
|
|
20
|
+
function waitAtMost(work: Promise<void>, budgetMs: number): Promise<void> {
|
|
21
|
+
return new Promise((resolve) => {
|
|
22
|
+
const timer = setTimeout(resolve, budgetMs);
|
|
23
|
+
work.then(() => {
|
|
24
|
+
clearTimeout(timer);
|
|
25
|
+
resolve();
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
6
30
|
/**
|
|
7
31
|
* Tell a connector that a scope owned by the core has ended.
|
|
8
32
|
*
|
|
9
33
|
* Scope teardown is deliberately best-effort: a missing hook is a no-op and a
|
|
10
34
|
* rejected hook is swallowed so cleanup can never replace the probe result that
|
|
11
35
|
* caused it. The hook gets a small, fixed completion window so edge runtimes do
|
|
12
|
-
* not cut off a real close as the response ends
|
|
13
|
-
*
|
|
14
|
-
* at-most-once guarantee and must not use the scope
|
|
36
|
+
* not cut off a real close as the response ends. When the runtime supplies
|
|
37
|
+
* `defer`, the bounded tail is handed to it without extending the caller-facing
|
|
38
|
+
* window. Callers own the at-most-once guarantee and must not use the scope
|
|
39
|
+
* again after this returns.
|
|
15
40
|
*/
|
|
16
41
|
export async function closeConnectorScope(
|
|
17
42
|
connector: Connector,
|
|
18
43
|
ctx: ConnectorContext,
|
|
44
|
+
defer?: DeferredWork,
|
|
19
45
|
): Promise<void> {
|
|
20
46
|
try {
|
|
21
47
|
const closing = connector.closeScope?.(ctx);
|
|
22
48
|
if (!closing) return;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
});
|
|
49
|
+
// Attach both handlers before either timer can win, so a late rejection is
|
|
50
|
+
// consumed rather than becoming an unhandled rejection.
|
|
51
|
+
const settled = closing.then(
|
|
52
|
+
() => {},
|
|
53
|
+
() => {},
|
|
54
|
+
);
|
|
55
|
+
if (defer) {
|
|
56
|
+
try {
|
|
57
|
+
defer(waitAtMost(settled, CONNECTOR_SCOPE_DEFER_BUDGET_MS));
|
|
58
|
+
} catch {
|
|
59
|
+
// A runtime hook is best-effort too; the caller cap still applies.
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
await waitAtMost(settled, CONNECTOR_SCOPE_CLOSE_BUDGET_MS);
|
|
38
63
|
} catch {
|
|
39
64
|
// The scope is over whether or not the connector managed to clean it up.
|
|
40
65
|
}
|
|
@@ -75,15 +75,14 @@ export interface RemoteMcpOptions {
|
|
|
75
75
|
|
|
76
76
|
/**
|
|
77
77
|
* How long a downstream gets to answer the session-termination DELETE before
|
|
78
|
-
* teardown stops waiting.
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
* does not get to tell us so.
|
|
78
|
+
* teardown stops waiting. This is a network round-trip budget, deliberately
|
|
79
|
+
* independent of the core's 100 ms caller-facing scope-close window: an
|
|
80
|
+
* already-established cross-internet connection avoids setup, but 50 ms is
|
|
81
|
+
* still too short for an ordinary round trip plus modest provider scheduling.
|
|
82
|
+
* The bounded tail is deferred on runtimes that can keep it alive after the
|
|
83
|
+
* response, while callers continue to wait at most 100 ms.
|
|
85
84
|
*/
|
|
86
|
-
const TERMINATE_SESSION_BUDGET_MS =
|
|
85
|
+
const TERMINATE_SESSION_BUDGET_MS = 1_000;
|
|
87
86
|
|
|
88
87
|
/**
|
|
89
88
|
* Ceiling on the tools one catalog refresh will accumulate while walking
|
|
@@ -199,24 +198,62 @@ function msg(err: unknown): string {
|
|
|
199
198
|
* (405 is a legal answer), errors, or never replies all fall through to the
|
|
200
199
|
* close with the session left to age out as it did before.
|
|
201
200
|
*/
|
|
202
|
-
async function terminateSession(
|
|
201
|
+
async function terminateSession(
|
|
202
|
+
transport: Transport,
|
|
203
|
+
logger: Logger,
|
|
204
|
+
connectorId: string,
|
|
205
|
+
): Promise<void> {
|
|
203
206
|
const terminate = (
|
|
204
207
|
transport as Transport & { terminateSession?: () => Promise<void> }
|
|
205
208
|
).terminateSession;
|
|
206
209
|
if (typeof terminate !== "function") return;
|
|
207
210
|
// The SDK issues no request at all when no `mcp-session-id` was captured, so
|
|
208
211
|
// a stateless downstream never sees a spurious DELETE.
|
|
209
|
-
const done = (
|
|
210
|
-
// The session is being abandoned either way; a refusal changes nothing.
|
|
211
|
-
// Caught here rather than at the await below so a late rejection — one
|
|
212
|
-
// arriving after the budget expired — is still consumed.
|
|
213
|
-
});
|
|
212
|
+
const done = Promise.resolve().then(() => terminate.call(transport));
|
|
214
213
|
await new Promise<void>((resolve) => {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
214
|
+
let finished = false;
|
|
215
|
+
const warn = (message: string, error?: unknown) => {
|
|
216
|
+
try {
|
|
217
|
+
if (error === undefined) logger.warn(message);
|
|
218
|
+
else logger.warn(message, error);
|
|
219
|
+
} catch {
|
|
220
|
+
// A diagnostic sink cannot make best-effort teardown observable to the
|
|
221
|
+
// caller in the one way this contract forbids: by replacing its result.
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
const timer = setTimeout(() => {
|
|
225
|
+
if (finished) return;
|
|
226
|
+
finished = true;
|
|
227
|
+
warn(
|
|
228
|
+
`[connecta] connector "${connectorId}" session termination was not ` +
|
|
229
|
+
`acknowledged within ${TERMINATE_SESSION_BUDGET_MS} ms; the ` +
|
|
230
|
+
"downstream may still finish the headers-only DELETE, otherwise " +
|
|
231
|
+
"the session will remain until its provider timeout.",
|
|
232
|
+
);
|
|
218
233
|
resolve();
|
|
219
|
-
});
|
|
234
|
+
}, TERMINATE_SESSION_BUDGET_MS);
|
|
235
|
+
done.then(
|
|
236
|
+
() => {
|
|
237
|
+
if (finished) return;
|
|
238
|
+
finished = true;
|
|
239
|
+
clearTimeout(timer);
|
|
240
|
+
resolve();
|
|
241
|
+
},
|
|
242
|
+
(error) => {
|
|
243
|
+
// The rejection handler stays attached after the timer wins, so an
|
|
244
|
+
// abort or other late failure is consumed without a duplicate warning.
|
|
245
|
+
if (finished) return;
|
|
246
|
+
finished = true;
|
|
247
|
+
clearTimeout(timer);
|
|
248
|
+
warn(
|
|
249
|
+
`[connecta] connector "${connectorId}" session termination was ` +
|
|
250
|
+
"refused or failed; the downstream session may remain until its " +
|
|
251
|
+
"provider timeout.",
|
|
252
|
+
error,
|
|
253
|
+
);
|
|
254
|
+
resolve();
|
|
255
|
+
},
|
|
256
|
+
);
|
|
220
257
|
});
|
|
221
258
|
}
|
|
222
259
|
|
|
@@ -797,7 +834,7 @@ export function remoteMcp(id: string, opts: RemoteMcpOptions): Connector {
|
|
|
797
834
|
// Ask the downstream to drop its session first — closing only aborts our
|
|
798
835
|
// side, and the DELETE that frees the server's rides on the very
|
|
799
836
|
// AbortSignal the close is about to trip.
|
|
800
|
-
if (transport) await terminateSession(transport);
|
|
837
|
+
if (transport) await terminateSession(transport, ctx.logger, id);
|
|
801
838
|
|
|
802
839
|
// Client.close() owns its connected transport. During an unfinished or
|
|
803
840
|
// failed connect there is no cached client yet, so close the transport
|
package/src/credential-health.ts
CHANGED
|
@@ -21,7 +21,10 @@ import {
|
|
|
21
21
|
storedCredentialShape,
|
|
22
22
|
} from "./credentials.js";
|
|
23
23
|
import type { CredentialVault } from "./credentials.js";
|
|
24
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
closeConnectorScope,
|
|
26
|
+
type DeferredWork,
|
|
27
|
+
} from "./connector-scope.js";
|
|
25
28
|
import { DEFAULT_PROBE_TIMEOUT_MS, normalizeTimeoutMs, withTimeout } from "./timeout.js";
|
|
26
29
|
import type {
|
|
27
30
|
Connector,
|
|
@@ -536,6 +539,7 @@ export class CredentialHealthChecker {
|
|
|
536
539
|
async check(
|
|
537
540
|
baseUrl: string,
|
|
538
541
|
opts: CredentialCheckOptions = {},
|
|
542
|
+
defer?: DeferredWork,
|
|
539
543
|
): Promise<CredentialCheckResult[]> {
|
|
540
544
|
// An id naming no connector is reported, not dropped: a typo in a scheduled
|
|
541
545
|
// check would otherwise return an empty list that looks exactly like a
|
|
@@ -546,7 +550,7 @@ export class CredentialHealthChecker {
|
|
|
546
550
|
return mapWithConcurrency(targets, this.concurrency, (target) =>
|
|
547
551
|
typeof target === "string"
|
|
548
552
|
? Promise.resolve({ connectorId: target, skipped: "not_found" as const })
|
|
549
|
-
: this.checkOne(target, baseUrl, opts),
|
|
553
|
+
: this.checkOne(target, baseUrl, opts, defer),
|
|
550
554
|
);
|
|
551
555
|
}
|
|
552
556
|
|
|
@@ -556,13 +560,16 @@ export class CredentialHealthChecker {
|
|
|
556
560
|
* gate is armed BEFORE the sweep starts, so a burst of concurrent requests
|
|
557
561
|
* produces one sweep.
|
|
558
562
|
*/
|
|
559
|
-
sweepIfDue(
|
|
563
|
+
sweepIfDue(
|
|
564
|
+
baseUrl: string,
|
|
565
|
+
defer?: DeferredWork,
|
|
566
|
+
): Promise<CredentialCheckResult[]> | undefined {
|
|
560
567
|
if (!this.onRequest || this.sweeping) return undefined;
|
|
561
568
|
const now = Date.now();
|
|
562
569
|
if (now < this.nextSweepAt) return undefined;
|
|
563
570
|
if (!this.hasCheckableConnectors()) return undefined;
|
|
564
571
|
this.nextSweepAt = now + this.intervalMs;
|
|
565
|
-
const sweep = this.check(baseUrl).finally(() => {
|
|
572
|
+
const sweep = this.check(baseUrl, {}, defer).finally(() => {
|
|
566
573
|
this.sweeping = undefined;
|
|
567
574
|
});
|
|
568
575
|
this.sweeping = sweep;
|
|
@@ -573,6 +580,7 @@ export class CredentialHealthChecker {
|
|
|
573
580
|
connector: Connector,
|
|
574
581
|
baseUrl: string,
|
|
575
582
|
opts: CredentialCheckOptions,
|
|
583
|
+
defer?: DeferredWork,
|
|
576
584
|
): Promise<CredentialCheckResult> {
|
|
577
585
|
const connectorId = connector.id;
|
|
578
586
|
if (!isCheckableConnector(connector)) {
|
|
@@ -587,7 +595,12 @@ export class CredentialHealthChecker {
|
|
|
587
595
|
...(await this.recordOrNothing(connectorId)),
|
|
588
596
|
};
|
|
589
597
|
}
|
|
590
|
-
const run = this.runCheck(
|
|
598
|
+
const run = this.runCheck(
|
|
599
|
+
connector,
|
|
600
|
+
baseUrl,
|
|
601
|
+
opts.force ?? false,
|
|
602
|
+
defer,
|
|
603
|
+
);
|
|
591
604
|
this.inFlight.set(connectorId, run);
|
|
592
605
|
try {
|
|
593
606
|
return await run;
|
|
@@ -607,6 +620,7 @@ export class CredentialHealthChecker {
|
|
|
607
620
|
connector: Connector,
|
|
608
621
|
baseUrl: string,
|
|
609
622
|
force: boolean,
|
|
623
|
+
defer?: DeferredWork,
|
|
610
624
|
): Promise<CredentialCheckResult> {
|
|
611
625
|
const connectorId = connector.id;
|
|
612
626
|
const started = Date.now();
|
|
@@ -711,7 +725,7 @@ export class CredentialHealthChecker {
|
|
|
711
725
|
});
|
|
712
726
|
}
|
|
713
727
|
} finally {
|
|
714
|
-
await closeConnectorScope(connector, ctx);
|
|
728
|
+
await closeConnectorScope(connector, ctx, defer);
|
|
715
729
|
}
|
|
716
730
|
}
|
|
717
731
|
|
package/src/execute.ts
CHANGED
|
@@ -8,10 +8,18 @@ import {
|
|
|
8
8
|
discoverySearchLimit,
|
|
9
9
|
errorResult,
|
|
10
10
|
jsonResult,
|
|
11
|
-
serializeResultText,
|
|
12
11
|
type ToolResult,
|
|
13
12
|
} from "./meta-tools.js";
|
|
13
|
+
import {
|
|
14
|
+
guardExecuteResultValue,
|
|
15
|
+
MAX_EXECUTE_LOG_CHARS,
|
|
16
|
+
truncateExecuteText,
|
|
17
|
+
} from "./executor-result.js";
|
|
14
18
|
import { classifyCallError, ConnectorCallError } from "./errors.js";
|
|
19
|
+
import {
|
|
20
|
+
ExecutorAdmissionError,
|
|
21
|
+
isAdmittingExecutor,
|
|
22
|
+
} from "./executor-admission.js";
|
|
15
23
|
import { unwrapMcpResult } from "./mcp-result.js";
|
|
16
24
|
import type { RegistryView } from "./registry.js";
|
|
17
25
|
import type {
|
|
@@ -22,9 +30,6 @@ import type {
|
|
|
22
30
|
ToolDef,
|
|
23
31
|
} from "./types.js";
|
|
24
32
|
|
|
25
|
-
/** ~6k tokens. Sandbox code should filter data down before returning. */
|
|
26
|
-
const MAX_RESULT_CHARS = 24_000;
|
|
27
|
-
const MAX_LOG_CHARS = 4_000;
|
|
28
33
|
/** Keep one model-written program from amplifying into an unbounded fan-out. */
|
|
29
34
|
export const EXECUTE_MAX_HOST_CALLS = 20;
|
|
30
35
|
export const EXECUTE_MAX_BATCH_CALLS = 10;
|
|
@@ -136,6 +141,7 @@ export async function buildSandboxProviders(
|
|
|
136
141
|
"connecta",
|
|
137
142
|
"console",
|
|
138
143
|
"__invoke",
|
|
144
|
+
"__namespace",
|
|
139
145
|
"__call",
|
|
140
146
|
"__log",
|
|
141
147
|
]);
|
|
@@ -143,9 +149,17 @@ export async function buildSandboxProviders(
|
|
|
143
149
|
const catalogStarted = Date.now();
|
|
144
150
|
const loaded = await Promise.allSettled(
|
|
145
151
|
connectors.map((connector) =>
|
|
146
|
-
registry.getTools(connector.id, baseUrl, requestScope
|
|
152
|
+
registry.getTools(connector.id, baseUrl, requestScope, {
|
|
153
|
+
signal: limits.signal,
|
|
154
|
+
}),
|
|
147
155
|
),
|
|
148
156
|
);
|
|
157
|
+
if (limits.signal?.aborted) {
|
|
158
|
+
throw new ExecutorAdmissionError(
|
|
159
|
+
"executor_cancelled",
|
|
160
|
+
"Execution was cancelled during catalog construction.",
|
|
161
|
+
);
|
|
162
|
+
}
|
|
149
163
|
const catalogs = new Map<string, ToolDef[]>();
|
|
150
164
|
const callAddress = async (address: unknown, args: unknown) => {
|
|
151
165
|
const resolved = registry.resolveAddress(String(address));
|
|
@@ -474,24 +488,6 @@ export async function buildSandboxProviders(
|
|
|
474
488
|
return providers;
|
|
475
489
|
}
|
|
476
490
|
|
|
477
|
-
function truncate(text: string, max: number): string {
|
|
478
|
-
if (text.length <= max) return text;
|
|
479
|
-
return `${text.slice(0, max)}\n--- TRUNCATED (${text.length} chars total) — filter/map/slice data inside your code and return only what you need ---`;
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
function guardResultValue(value: unknown): unknown {
|
|
483
|
-
// Same serialization the call_tool guards measure, so a program returning
|
|
484
|
-
// nothing is rendered one way across every result path (issue #42).
|
|
485
|
-
const text = serializeResultText(value);
|
|
486
|
-
if (text.length <= MAX_RESULT_CHARS) return value;
|
|
487
|
-
return {
|
|
488
|
-
truncated: true,
|
|
489
|
-
preview: text.slice(0, MAX_RESULT_CHARS),
|
|
490
|
-
totalChars: text.length,
|
|
491
|
-
hint: "filter/map/slice data inside execute_code and return only what you need",
|
|
492
|
-
};
|
|
493
|
-
}
|
|
494
|
-
|
|
495
491
|
/** The execute_code handler. Exported for direct testing. */
|
|
496
492
|
export function createExecuteTool(
|
|
497
493
|
registry: RegistryView,
|
|
@@ -500,28 +496,69 @@ export function createExecuteTool(
|
|
|
500
496
|
logger: Logger,
|
|
501
497
|
activity?: ActivityRequestContext,
|
|
502
498
|
) {
|
|
503
|
-
return async (
|
|
499
|
+
return async (
|
|
500
|
+
{ code }: { code: string },
|
|
501
|
+
options: { signal?: AbortSignal } = {},
|
|
502
|
+
): Promise<ToolResult> => {
|
|
504
503
|
const controller = new AbortController();
|
|
505
|
-
const
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
);
|
|
504
|
+
const forwardAbort = () => controller.abort(options.signal?.reason);
|
|
505
|
+
if (options.signal?.aborted) forwardAbort();
|
|
506
|
+
else {
|
|
507
|
+
options.signal?.addEventListener("abort", forwardAbort, { once: true });
|
|
508
|
+
}
|
|
509
|
+
let lease;
|
|
512
510
|
let outcome;
|
|
513
511
|
try {
|
|
514
|
-
|
|
512
|
+
// Admission comes before provider construction: queued calls retain no
|
|
513
|
+
// catalogs, request scopes, or one-closure-per-tool provider arrays.
|
|
514
|
+
if (isAdmittingExecutor(executor)) {
|
|
515
|
+
lease = await executor.acquire({ signal: controller.signal });
|
|
516
|
+
}
|
|
517
|
+
const providers = await buildSandboxProviders(
|
|
518
|
+
registry,
|
|
519
|
+
baseUrl,
|
|
520
|
+
logger,
|
|
521
|
+
activity,
|
|
522
|
+
{ signal: controller.signal },
|
|
523
|
+
);
|
|
524
|
+
if (controller.signal.aborted) {
|
|
525
|
+
throw new ExecutorAdmissionError(
|
|
526
|
+
"executor_cancelled",
|
|
527
|
+
"Execution was cancelled during sandbox setup.",
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
outcome = lease
|
|
531
|
+
? await lease.execute(code, providers)
|
|
532
|
+
: await executor.execute(code, providers);
|
|
515
533
|
} catch (err) {
|
|
534
|
+
if (err instanceof ExecutorAdmissionError) {
|
|
535
|
+
const result = jsonResult({
|
|
536
|
+
error: {
|
|
537
|
+
code: err.code,
|
|
538
|
+
message: err.message,
|
|
539
|
+
retryable: err.retryable,
|
|
540
|
+
...(err.retryAfterMs !== undefined
|
|
541
|
+
? { retryAfterMs: err.retryAfterMs }
|
|
542
|
+
: {}),
|
|
543
|
+
},
|
|
544
|
+
});
|
|
545
|
+
result.isError = true;
|
|
546
|
+
return result;
|
|
547
|
+
}
|
|
516
548
|
return errorResult(`Executor failed: ${msg(err)}`);
|
|
517
549
|
} finally {
|
|
518
550
|
// A sandbox timeout or early return must also release any outstanding
|
|
519
551
|
// host waits and signal cooperative connectors to stop their work.
|
|
520
552
|
controller.abort();
|
|
553
|
+
lease?.release();
|
|
554
|
+
options.signal?.removeEventListener("abort", forwardAbort);
|
|
521
555
|
}
|
|
522
556
|
const logs =
|
|
523
557
|
outcome.logs && outcome.logs.length > 0
|
|
524
|
-
?
|
|
558
|
+
? truncateExecuteText(
|
|
559
|
+
outcome.logs.join("\n"),
|
|
560
|
+
MAX_EXECUTE_LOG_CHARS,
|
|
561
|
+
)
|
|
525
562
|
: undefined;
|
|
526
563
|
if (outcome.error) {
|
|
527
564
|
return errorResult(
|
|
@@ -533,7 +570,7 @@ export function createExecuteTool(
|
|
|
533
570
|
// error path so captured logs survive instead of a raw SDK 500.
|
|
534
571
|
let result: unknown;
|
|
535
572
|
try {
|
|
536
|
-
result =
|
|
573
|
+
result = guardExecuteResultValue(outcome.result);
|
|
537
574
|
} catch (err) {
|
|
538
575
|
return errorResult(
|
|
539
576
|
`Error: result is not JSON-serializable: ${msg(err)}${logs ? `\n\nLogs:\n${logs}` : ""}`,
|
|
@@ -568,6 +605,7 @@ export function registerExecuteTool(
|
|
|
568
605
|
executor: Executor;
|
|
569
606
|
logger: Logger;
|
|
570
607
|
activity?: ActivityRequestContext;
|
|
608
|
+
requestSignal?: AbortSignal;
|
|
571
609
|
},
|
|
572
610
|
): void {
|
|
573
611
|
const handler = createExecuteTool(
|
|
@@ -594,6 +632,26 @@ export function registerExecuteTool(
|
|
|
594
632
|
openWorldHint: true,
|
|
595
633
|
},
|
|
596
634
|
},
|
|
597
|
-
async (args) =>
|
|
635
|
+
async (args, extra) => {
|
|
636
|
+
const controller = new AbortController();
|
|
637
|
+
const signals = [extra.signal, ctx.requestSignal].filter(
|
|
638
|
+
(signal): signal is AbortSignal => signal !== undefined,
|
|
639
|
+
);
|
|
640
|
+
const forwarders = signals.map((signal) => {
|
|
641
|
+
const forward = () => controller.abort(signal.reason);
|
|
642
|
+
if (signal.aborted) forward();
|
|
643
|
+
else signal.addEventListener("abort", forward, { once: true });
|
|
644
|
+
return { signal, forward };
|
|
645
|
+
});
|
|
646
|
+
try {
|
|
647
|
+
return await handler(args as { code: string }, {
|
|
648
|
+
signal: controller.signal,
|
|
649
|
+
});
|
|
650
|
+
} finally {
|
|
651
|
+
for (const { signal, forward } of forwarders) {
|
|
652
|
+
signal.removeEventListener("abort", forward);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
},
|
|
598
656
|
);
|
|
599
657
|
}
|