@zackbart/connecta 0.38.0 → 0.40.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/CHANGELOG.md +91 -0
- package/README.md +3 -2
- package/bin/connecta.mjs +4 -8
- package/bin/version.mjs +1 -1
- package/dist/access-tokens.js +6 -4
- package/dist/agent-routes.js +231 -0
- package/dist/auth/downstream-oauth/consent.js +519 -0
- package/dist/auth/downstream-oauth/grant-store.js +439 -0
- package/dist/auth/downstream-oauth/refresh-coordinator.js +562 -0
- package/dist/auth/downstream-oauth/refresh-records.js +447 -0
- package/dist/auth/downstream-oauth/sdk-hooks.js +242 -0
- package/dist/auth/downstream-oauth.js +66 -2382
- package/dist/auth/native.js +37 -0
- package/dist/auth/oauth-grant.js +13 -0
- package/dist/auth/static-oauth.js +153 -131
- package/dist/auth-elicitation.js +7 -1
- package/dist/authorization-handoff.js +11 -5
- package/dist/catalog-cache.js +219 -59
- package/dist/catalog-policy.js +18 -0
- package/dist/catalog-service.js +284 -47
- package/dist/catalog.js +18 -2
- package/dist/config.d.ts +16 -11
- package/dist/config.js +9 -6
- package/dist/connector-caller.d.ts +1 -1
- package/dist/connector-health.js +59 -0
- package/dist/connector-status.js +117 -0
- package/dist/connectors/api-connector.d.ts +7 -2
- package/dist/connectors/api-connector.js +7 -4
- package/dist/connectors/negotiation-cache.js +22 -4
- package/dist/connectors/remote-mcp/call.js +164 -0
- package/dist/connectors/remote-mcp/catalog.js +278 -0
- package/dist/connectors/remote-mcp/config.js +263 -0
- package/dist/connectors/remote-mcp/connection.js +682 -0
- package/dist/connectors/remote-mcp/errors.js +488 -0
- package/dist/connectors/remote-mcp/oauth.js +473 -0
- package/dist/connectors/remote-mcp/resources.js +473 -0
- package/dist/connectors/remote-mcp/runtime.js +1 -0
- package/dist/connectors/remote-mcp/transport.js +555 -0
- package/dist/connectors/remote-mcp.js +62 -2863
- package/dist/credential-contract.d.ts +23 -0
- package/dist/credential-reads.js +17 -0
- package/dist/credential-verification.js +53 -0
- package/dist/credentials.d.ts +7 -0
- package/dist/credentials.js +60 -25
- package/dist/describe-config.d.ts +12 -2
- package/dist/describe-config.js +2 -0
- package/dist/discovery-health.js +199 -0
- package/dist/downstream-input.js +2 -0
- package/dist/errors.d.ts +55 -17
- package/dist/errors.js +95 -41
- package/dist/execute.js +152 -114
- package/dist/executor-result.js +3 -1
- package/dist/guest-functions.js +83 -0
- package/dist/guest-runtime.js +60 -19
- package/dist/guest-types.d.ts +19 -5
- package/dist/http/argument-problems.js +35 -0
- package/dist/http/errors.js +5 -4
- package/dist/http/fields.js +192 -13
- package/dist/http/hatch-response.js +73 -0
- package/dist/http/hatch.js +88 -23
- package/dist/http/oauth.js +3 -2
- package/dist/http/provider.js +32 -4
- package/dist/http/safety.js +8 -0
- package/dist/http/tools.js +44 -23
- package/dist/index.d.ts +1 -1
- package/dist/index.js +9 -2
- package/dist/invocation.js +26 -8
- package/dist/membership.d.ts +13 -5
- package/dist/membership.js +34 -2
- package/dist/meta-tools.js +54 -31
- package/dist/native-recovery.js +4 -16
- package/dist/native-tools.js +5 -2
- package/dist/oauth-sealing.js +10 -0
- package/dist/operation-vocabulary.js +135 -13
- package/dist/operator-record.d.ts +2 -2
- package/dist/operator-record.js +1 -0
- package/dist/operator-ui/contract.d.ts +10 -0
- package/dist/operator-ui/generated.js +2 -2
- package/dist/operator-ui/model.d.ts +22 -1
- package/dist/provider-call-facts.js +6 -2
- package/dist/providers/_shared/rest/operation-index.js +48 -50
- package/dist/providers/_shared/rest/value-safety.js +80 -0
- package/dist/providers/breeze/api.js +7 -1
- package/dist/providers/breeze/skill.generated.js +1 -1
- package/dist/providers/breeze/tools.js +4 -3
- package/dist/providers/ccb/api.js +2 -1
- package/dist/providers/ccb/index.js +5 -0
- package/dist/providers/ccb/skill.generated.js +1 -1
- package/dist/providers/cloudflare/api.js +8 -1
- package/dist/providers/cloudflare/skill.generated.js +1 -1
- package/dist/providers/docs/lib/schemas.js +24 -22
- package/dist/providers/docs/safety.js +2 -0
- package/dist/providers/docs/skill.generated.js +1 -1
- package/dist/providers/docs/tools.js +2 -4
- package/dist/providers/drive/api.js +1 -0
- package/dist/providers/drive/lib/contracts.js +8 -5
- package/dist/providers/drive/skill.generated.js +1 -1
- package/dist/providers/drive/tools.js +11 -16
- package/dist/providers/forms/api.js +1 -0
- package/dist/providers/forms/skill.generated.js +1 -1
- package/dist/providers/forms/tools.js +10 -7
- package/dist/providers/github/api.js +1 -0
- package/dist/providers/github/lib/discovery.js +1 -1
- package/dist/providers/github/skill.generated.js +1 -1
- package/dist/providers/github/tools.js +61 -17
- package/dist/providers/gmail/lib/schemas.js +60 -50
- package/dist/providers/gmail/skill.generated.js +1 -1
- package/dist/providers/gmail/tools.js +7 -5
- package/dist/providers/granola/index.js +2 -0
- package/dist/providers/granola/skill.generated.js +1 -1
- package/dist/providers/overflow/api.js +7 -1
- package/dist/providers/overflow/skill.generated.js +1 -1
- package/dist/providers/planning-center/api.js +3 -1
- package/dist/providers/planning-center/lib/constants.d.ts +1 -1
- package/dist/providers/planning-center/skill.generated.js +1 -1
- package/dist/providers/quickbooks/safety.js +80 -39
- package/dist/providers/quickbooks/skill.generated.js +1 -1
- package/dist/providers/sheets/api.js +1 -0
- package/dist/providers/sheets/skill.generated.js +1 -1
- package/dist/providers/sheets/tools.js +13 -14
- package/dist/providers/slides/skill.generated.js +1 -1
- package/dist/providers/slides/tools.js +15 -13
- package/dist/providers/stripe/skill.generated.js +1 -1
- package/dist/providers/tithely/api.js +3 -1
- package/dist/providers/tithely/skill.generated.js +1 -1
- package/dist/providers/vercel/api.js +1 -1
- package/dist/providers/vercel/skill.generated.js +1 -1
- package/dist/providers/vercel/tools.js +4 -3
- package/dist/registry.d.ts +10 -4
- package/dist/registry.js +41 -13
- package/dist/routes/credentials.js +129 -60
- package/dist/routes/dispatch.js +5 -1
- package/dist/routes/mcp.js +30 -14
- package/dist/routes/operator.js +2 -2
- package/dist/routes/shared.d.ts +6 -1
- package/dist/routes/shared.js +7 -1
- package/dist/routes/ui-config.js +2 -2
- package/dist/routes/ui-data.js +126 -20
- package/dist/routes/ui.js +16 -0
- package/dist/runtime/call.js +7 -2
- package/dist/runtime/native-storage.js +12 -0
- package/dist/runtime/request-reads.js +207 -0
- package/dist/runtime/request-scope.js +16 -0
- package/dist/runtime/storage.js +12 -0
- package/dist/skills.js +37 -20
- package/dist/storage/keys.js +53 -3
- package/dist/storage/memory.js +8 -2
- package/dist/storage/sql-native.js +53 -1
- package/dist/storage/sql.js +12 -2
- package/dist/storage-digest.js +32 -0
- package/dist/timeout.js +8 -0
- package/dist/types.d.ts +33 -0
- package/dist/usage-guide.js +16 -35
- package/dist/validate.d.ts +3 -0
- package/dist/validate.js +220 -158
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/write-outcome.js +0 -3
- package/documentation/README.md +13 -13
- package/documentation/architecture.md +385 -20
- package/documentation/auth.md +233 -64
- package/documentation/code-mode.md +122 -39
- package/documentation/deploying.md +6 -3
- package/documentation/integrating.md +16 -2
- package/documentation/meta-tools.md +240 -49
- package/documentation/migration-0.33.md +5 -1
- package/documentation/migration-0.34.md +3 -1
- package/documentation/operator-tests.md +1 -1
- package/documentation/operator-ui.md +105 -12
- package/documentation/pool-membership-upgrade.md +44 -0
- package/documentation/providers.md +105 -0
- package/examples/worker/AGENTS.md +10 -10
- package/examples/worker/README.md +4 -3
- package/examples/worker/src/connecta.config.ts +1 -0
- package/guides/connecting-agents.md +2 -1
- package/guides/how-it-works.md +4 -2
- package/package.json +1 -1
- package/templates/node/AGENTS.md +13 -12
- package/templates/node/package.json +1 -1
- package/templates/node/src/connecta.config.ts +1 -0
package/dist/execute.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { META_ROUTES, routeNames } from "./agent-routes.js";
|
|
1
2
|
import { authCall, authFlow, authInvoke, authMethod } from "./auth/native.js";
|
|
2
3
|
import { nativeRecovery } from "./native-recovery.js";
|
|
3
|
-
import { GUEST_API_DECLARATION } from "./
|
|
4
|
+
import { GUEST_API_DECLARATION, GUEST_FUNCTIONS, UNCOUNTED_GUEST_FUNCTIONS, } from "./guest-functions.js";
|
|
4
5
|
import { bindMcpClient } from "./mcp-client-context.js";
|
|
5
6
|
import { CONFIG_DEFAULTS } from "./config-defaults.js";
|
|
6
7
|
import { Cause, Deferred, Duration, Effect, Exit } from "effect";
|
|
@@ -37,8 +38,11 @@ const EXECUTE_MAX_CODE_BYTES = 64 * 1024;
|
|
|
37
38
|
* never settles at all.
|
|
38
39
|
*/
|
|
39
40
|
const EXECUTE_WATCHDOG_MS = CONFIG_DEFAULTS.execute.watchdogMs;
|
|
40
|
-
/**
|
|
41
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Room to name every connector of a typical deployment. Past it, complete
|
|
43
|
+
* entries plus an exact omission count stay inside this byte cap.
|
|
44
|
+
*/
|
|
45
|
+
export const CONNECTOR_INVENTORY_MAX_BYTES = 1_024;
|
|
42
46
|
/**
|
|
43
47
|
* Default budgets for `connecta.emit`. The byte budget is a transport bound,
|
|
44
48
|
* not a context bound — emitted image/audio blocks reach the model as media,
|
|
@@ -425,6 +429,7 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
|
|
|
425
429
|
concurrency: limits.discoveryConcurrency,
|
|
426
430
|
probeTimeoutMs: limits.probeTimeoutMs,
|
|
427
431
|
defer: limits.defer,
|
|
432
|
+
routes: limits.routes,
|
|
428
433
|
});
|
|
429
434
|
const invocation = new InvocationService(registry, catalog, activity);
|
|
430
435
|
const maxHostCalls = Math.max(1, Math.trunc(limits.maxHostCalls ?? EXECUTE_MAX_HOST_CALLS));
|
|
@@ -468,6 +473,7 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
|
|
|
468
473
|
source: "execute_code",
|
|
469
474
|
sentSecrets,
|
|
470
475
|
timeoutMs,
|
|
476
|
+
...(limits.routes ? { routes: limits.routes } : {}),
|
|
471
477
|
...(signal !== undefined ? { requestSignal: signal } : {}),
|
|
472
478
|
...(limits.dispatchController ? { dispatchSignal: limits.dispatchController.signal } : {}),
|
|
473
479
|
unwrapResult: true,
|
|
@@ -698,6 +704,11 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
|
|
|
698
704
|
catalogDependency(resolved.connector.id, resolved.toolName);
|
|
699
705
|
};
|
|
700
706
|
const operations = {
|
|
707
|
+
// Uncounted: a failed program read a result field off an unawaited host call.
|
|
708
|
+
unawaited: () => {
|
|
709
|
+
if (limits.unawaited)
|
|
710
|
+
limits.unawaited.read = true;
|
|
711
|
+
},
|
|
701
712
|
call,
|
|
702
713
|
result: (id, options, signal, onStage) => Effect.tryPromise({
|
|
703
714
|
try: async () => {
|
|
@@ -822,7 +833,7 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
|
|
|
822
833
|
const args = (raw ?? {});
|
|
823
834
|
const result = flatSearchResult(await catalog.search({
|
|
824
835
|
...args,
|
|
825
|
-
includeSchemas: args.includeSchemas ?? "
|
|
836
|
+
includeSchemas: args.includeSchemas ?? "typescript",
|
|
826
837
|
includeSchemaKeys: args.includeSchemaKeys !== false,
|
|
827
838
|
}, catalogDependency));
|
|
828
839
|
boundedDiscoveryText(result, "Request a smaller limit, omit fullDescriptions, use compact schemas, or pass includeSchemaKeys: false.");
|
|
@@ -856,90 +867,102 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
|
|
|
856
867
|
const failure = boundedGuestFailure(redactAgentOutput(sentSecrets, err));
|
|
857
868
|
return Effect.fail(failure);
|
|
858
869
|
});
|
|
870
|
+
/** End the whole run: later bridge calls, including emit, lose host access. */
|
|
871
|
+
const endRun = (failure) => {
|
|
872
|
+
budgetFailure = failure;
|
|
873
|
+
programWrites?.close();
|
|
874
|
+
limits.dispatchController?.abort();
|
|
875
|
+
limits.onHostCallBudgetExceeded?.(failure);
|
|
876
|
+
return stopped;
|
|
877
|
+
};
|
|
859
878
|
return {
|
|
860
879
|
name: "connecta",
|
|
861
880
|
// Trusted host code the program runs after and cannot undo.
|
|
862
881
|
prelude: guestPrelude(),
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
programWrites?.close();
|
|
873
|
-
limits.dispatchController?.abort();
|
|
874
|
-
limits.onHostCallBudgetExceeded?.(budgetFailure);
|
|
875
|
-
return stopped;
|
|
876
|
-
}
|
|
877
|
-
// L4/M7: spend on entry, before even invalid arguments are checked;
|
|
878
|
-
// emit has a separate budget and never spends this one.
|
|
879
|
-
// Transparent transfer pages consume the already-admitted call, not guest host calls.
|
|
880
|
-
const counted = name !== "emit" && !(name === "result" && typeof args[0] === "string" && transfers.has(args[0]));
|
|
881
|
-
if (counted && ++hostCalls.attempted > maxHostCalls) {
|
|
882
|
-
hostCalls.failed++;
|
|
883
|
-
budgetFailure = new HostCallBudgetExceeded(hostCalls, maxHostCalls);
|
|
884
|
-
programWrites?.close();
|
|
885
|
-
limits.dispatchController?.abort();
|
|
886
|
-
limits.onHostCallBudgetExceeded?.(budgetFailure);
|
|
887
|
-
return stopped;
|
|
888
|
-
}
|
|
889
|
-
if (counted)
|
|
890
|
-
hostCalls.admitted++;
|
|
891
|
-
const started = Date.now();
|
|
892
|
-
const utility = name === "result" || name === "skill";
|
|
893
|
-
let stage = name === "result" ? "result storage" : "skill lookup";
|
|
894
|
-
const invoke = Effect.suspend(() => operation(...args));
|
|
895
|
-
const guarded = utility
|
|
896
|
-
? withDeadlineEffect((utilitySignal) => Effect.suspend(() => operation(args[0], args[1], utilitySignal, (current) => {
|
|
897
|
-
stage = current;
|
|
898
|
-
})), {
|
|
899
|
-
timeoutMs: hostCallTimeoutMs,
|
|
900
|
-
...(hostAccessSignal ? { signal: hostAccessSignal } : {}),
|
|
901
|
-
timeoutError: guestFailure("timeout", `connecta.${name} timed out.`),
|
|
902
|
-
})
|
|
903
|
-
: invoke;
|
|
904
|
-
const settled = runEdge(guarded.pipe(Effect.catch((error) => {
|
|
905
|
-
if (utility && hostAccessSignal?.aborted)
|
|
906
|
-
return typedFailure(guestFailure("cancelled", `connecta.${name} was cancelled because the run ended.`));
|
|
907
|
-
if (utility && error instanceof InvocationFailure && error.code === "timeout") {
|
|
908
|
-
const elapsedMs = Date.now() - started;
|
|
909
|
-
const message = `Operation "connecta.${name}" timed out during ${stage} after ${elapsedMs}ms (effective deadline ${hostCallTimeoutMs}ms).`;
|
|
910
|
-
return typedFailure(new InvocationFailure({
|
|
911
|
-
code: "timeout",
|
|
912
|
-
message,
|
|
913
|
-
retryable: true,
|
|
914
|
-
details: {
|
|
915
|
-
operation: `connecta.${name}`,
|
|
916
|
-
stage,
|
|
917
|
-
elapsedMs,
|
|
918
|
-
deadlineMs: hostCallTimeoutMs,
|
|
919
|
-
},
|
|
920
|
-
}));
|
|
921
|
-
}
|
|
922
|
-
return typedFailure(error);
|
|
923
|
-
}))).then((value) => {
|
|
924
|
-
if (counted)
|
|
925
|
-
hostCalls.succeeded++;
|
|
926
|
-
return budgetFailure ? stopped : redactAgentOutput(sentSecrets, value);
|
|
927
|
-
}, (err) => {
|
|
928
|
-
if (counted)
|
|
929
|
-
hostCalls.failed++;
|
|
882
|
+
// One bridge function per registry entry, admitted by its budget class.
|
|
883
|
+
fns: Object.fromEntries(GUEST_FUNCTIONS.map((fn) => {
|
|
884
|
+
const { name, budget } = fn;
|
|
885
|
+
const operation = operations[name];
|
|
886
|
+
const deadline = "deadline" in fn ? fn.deadline : undefined;
|
|
887
|
+
let notices = 0;
|
|
888
|
+
return [
|
|
889
|
+
name,
|
|
890
|
+
(...args) => {
|
|
930
891
|
if (budgetFailure)
|
|
931
892
|
return stopped;
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
893
|
+
// Uncounted: one per run; any further call through the bridge ends the run.
|
|
894
|
+
if (budget === "once") {
|
|
895
|
+
if (++notices > 1)
|
|
896
|
+
return endRun(guestFailure("budget_exceeded", `execute_code ${fn.label} is limited to one per run.`));
|
|
897
|
+
operation();
|
|
898
|
+
return Promise.resolve();
|
|
899
|
+
}
|
|
900
|
+
// Invalid emits still allocate guest promises and typed rejections.
|
|
901
|
+
// Bound those attempts without consuming the invocation budget or discarding identities.
|
|
902
|
+
if (budget === "emission" && ++emitAttempts > maxEmitAttempts)
|
|
903
|
+
return endRun(guestFailure("budget_exceeded", `execute_code emission-attempt budget exceeded (${maxEmitAttempts} attempts maximum).`));
|
|
904
|
+
// L4/M7: spend on entry, before even invalid arguments are checked;
|
|
905
|
+
// emission has a separate budget and never spends this one.
|
|
906
|
+
// Transparent transfer pages consume the already-admitted call, not guest host calls.
|
|
907
|
+
const counted = budget === "hostCall" && !(name === "result" && typeof args[0] === "string" && transfers.has(args[0]));
|
|
908
|
+
if (counted && ++hostCalls.attempted > maxHostCalls) {
|
|
909
|
+
hostCalls.failed++;
|
|
910
|
+
return endRun(new HostCallBudgetExceeded(hostCalls, maxHostCalls));
|
|
911
|
+
}
|
|
912
|
+
if (counted)
|
|
913
|
+
hostCalls.admitted++;
|
|
914
|
+
const started = Date.now();
|
|
915
|
+
let stage = deadline ?? "";
|
|
916
|
+
const invoke = Effect.suspend(() => operation(...args));
|
|
917
|
+
const guarded = deadline
|
|
918
|
+
? withDeadlineEffect((utilitySignal) => Effect.suspend(() => operation(args[0], args[1], utilitySignal, (current) => {
|
|
919
|
+
stage = current;
|
|
920
|
+
})), {
|
|
921
|
+
timeoutMs: hostCallTimeoutMs,
|
|
922
|
+
...(hostAccessSignal ? { signal: hostAccessSignal } : {}),
|
|
923
|
+
timeoutError: guestFailure("timeout", `connecta.${name} timed out.`),
|
|
924
|
+
})
|
|
925
|
+
: invoke;
|
|
926
|
+
const settled = runEdge(guarded.pipe(Effect.catch((error) => {
|
|
927
|
+
if (deadline && hostAccessSignal?.aborted)
|
|
928
|
+
return typedFailure(guestFailure("cancelled", `connecta.${name} was cancelled because the run ended.`));
|
|
929
|
+
if (deadline && error instanceof InvocationFailure && error.code === "timeout") {
|
|
930
|
+
const elapsedMs = Date.now() - started;
|
|
931
|
+
const message = `Operation "connecta.${name}" timed out during ${stage} after ${elapsedMs}ms (effective deadline ${hostCallTimeoutMs}ms).`;
|
|
932
|
+
return typedFailure(new InvocationFailure({
|
|
933
|
+
code: "timeout",
|
|
934
|
+
message,
|
|
935
|
+
retryable: true,
|
|
936
|
+
details: {
|
|
937
|
+
operation: `connecta.${name}`,
|
|
938
|
+
stage,
|
|
939
|
+
elapsedMs,
|
|
940
|
+
deadlineMs: hostCallTimeoutMs,
|
|
941
|
+
},
|
|
942
|
+
}));
|
|
943
|
+
}
|
|
944
|
+
return typedFailure(error);
|
|
945
|
+
}))).then((value) => {
|
|
946
|
+
if (counted)
|
|
947
|
+
hostCalls.succeeded++;
|
|
948
|
+
return budgetFailure ? stopped : redactAgentOutput(sentSecrets, value);
|
|
949
|
+
}, (err) => {
|
|
950
|
+
if (counted)
|
|
951
|
+
hostCalls.failed++;
|
|
952
|
+
if (budgetFailure)
|
|
953
|
+
return stopped;
|
|
954
|
+
throw redactAgentOutput(sentSecrets, err);
|
|
955
|
+
});
|
|
956
|
+
// The executor owns this promise, and a program may abandon a call
|
|
957
|
+
// that the run's end then cancels before anyone has awaited it.
|
|
958
|
+
// The rejection is still there for whoever does; it is just not an
|
|
959
|
+
// unhandled rejection of the host's, which workerd reports as soon
|
|
960
|
+
// as a microtask passes without a handler.
|
|
961
|
+
settled.catch(() => { });
|
|
962
|
+
return settled;
|
|
963
|
+
},
|
|
964
|
+
];
|
|
965
|
+
})),
|
|
943
966
|
};
|
|
944
967
|
}
|
|
945
968
|
/**
|
|
@@ -1054,6 +1077,7 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
|
|
|
1054
1077
|
const diagnostics = diagnosticsRequested ? new ExecuteDiagnostics() : undefined;
|
|
1055
1078
|
const emitted = new EmitCollector(resolveBudget(config.maxEmittedBytes, EXECUTE_MAX_EMITTED_BYTES), resolveBudget(config.maxEmittedBlocks, EXECUTE_MAX_EMITTED_BLOCKS), diagnostics, maxResultChars);
|
|
1056
1079
|
const resultDependencies = [];
|
|
1080
|
+
const unawaited = { read: false };
|
|
1057
1081
|
const dispatchController = new AbortController();
|
|
1058
1082
|
const terminal = Deferred.makeUnsafe();
|
|
1059
1083
|
let budgetFailure;
|
|
@@ -1104,9 +1128,11 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
|
|
|
1104
1128
|
defer: config.defer,
|
|
1105
1129
|
readOnly: config.readOnly,
|
|
1106
1130
|
maxWrites: config.maxWrites,
|
|
1131
|
+
routes: config.routes,
|
|
1107
1132
|
programWrites,
|
|
1108
1133
|
resultDependencies,
|
|
1109
1134
|
dispatchController,
|
|
1135
|
+
unawaited,
|
|
1110
1136
|
}, requestScope)));
|
|
1111
1137
|
if (signal.aborted) {
|
|
1112
1138
|
return yield* Effect.fail(new ExecutorAdmissionError("executor_cancelled", "Execution was cancelled during sandbox setup."));
|
|
@@ -1142,6 +1168,7 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
|
|
|
1142
1168
|
emitted,
|
|
1143
1169
|
diagnostics,
|
|
1144
1170
|
program,
|
|
1171
|
+
unawaited,
|
|
1145
1172
|
// Emitted text shares the return cap; a follow-up page run gets all of it.
|
|
1146
1173
|
guardResult: (value) => authCall(guardProgramReturn, [
|
|
1147
1174
|
value,
|
|
@@ -1256,7 +1283,7 @@ function failedRun(err, logger, { emitted, diagnostics, program }) {
|
|
|
1256
1283
|
});
|
|
1257
1284
|
}
|
|
1258
1285
|
/** The response for an ExecuteResult: the program's error or its value. */
|
|
1259
|
-
function finishedRun(outcome, { emitted, diagnostics, program, guardResult }) {
|
|
1286
|
+
function finishedRun(outcome, { emitted, diagnostics, program, unawaited, guardResult }) {
|
|
1260
1287
|
return authFlow(function* () {
|
|
1261
1288
|
if (outcome === null || typeof outcome !== "object" || Array.isArray(outcome)) {
|
|
1262
1289
|
return failureResponse("Executor failed: expected an ExecuteResult object.", {
|
|
@@ -1300,6 +1327,7 @@ function finishedRun(outcome, { emitted, diagnostics, program, guardResult }) {
|
|
|
1300
1327
|
name: outcome.failure?.name ?? /^(SyntaxError|ReferenceError|TypeError|RangeError):/.exec(outcome.error)?.[1],
|
|
1301
1328
|
message: outcome.error || "Execution failed without an error message.",
|
|
1302
1329
|
...(outcome.failure?.line !== undefined ? { line: outcome.failure.line - 1 } : {}),
|
|
1330
|
+
unawaited: unawaited.read,
|
|
1303
1331
|
}, program),
|
|
1304
1332
|
});
|
|
1305
1333
|
}
|
|
@@ -1356,50 +1384,59 @@ function failureResponse(message, options) {
|
|
|
1356
1384
|
function discardedEmits(emitted) {
|
|
1357
1385
|
return emitted.blocks.length > 0 ? { emittedDiscarded: emitted.blocks.length } : {};
|
|
1358
1386
|
}
|
|
1387
|
+
const inventoryWords = (text) => text
|
|
1388
|
+
.toLowerCase()
|
|
1389
|
+
.split(/[^\p{L}\p{N}]+/u)
|
|
1390
|
+
.filter(Boolean);
|
|
1391
|
+
/** A title that only respells the id ("Google Calendar" for google_calendar) adds nothing. */
|
|
1392
|
+
function titleAddsInformation(id, title) {
|
|
1393
|
+
const words = inventoryWords(title);
|
|
1394
|
+
const idWords = inventoryWords(id);
|
|
1395
|
+
return words.join("") !== idWords.join("") && words.some((word) => !idWords.includes(word));
|
|
1396
|
+
}
|
|
1397
|
+
/**
|
|
1398
|
+
* Every configured connector, from configuration alone. Detail degrades before
|
|
1399
|
+
* names do: titles go first, then optional guide pointers, and only past the
|
|
1400
|
+
* byte cap do whole entries give way to an exact omission count.
|
|
1401
|
+
*/
|
|
1359
1402
|
function connectorInventory(connectors) {
|
|
1360
1403
|
const prefix = "Connectors: ";
|
|
1361
1404
|
if (connectors.length === 0)
|
|
1362
1405
|
return `${prefix}none.`;
|
|
1363
|
-
const
|
|
1364
|
-
|
|
1406
|
+
const fits = (line) => boundedEchoText(line, CONNECTOR_INVENTORY_MAX_BYTES) === line;
|
|
1407
|
+
const entry = (connector, detail) => {
|
|
1408
|
+
const notes = [];
|
|
1365
1409
|
const title = connector.title?.replace(/\s+/g, " ").trim();
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
const shown = [];
|
|
1373
|
-
for (let index = 0; index < entries.length; index++) {
|
|
1374
|
-
const entry = entries[index];
|
|
1375
|
-
if (entry === undefined)
|
|
1376
|
-
break;
|
|
1377
|
-
const candidate = [...shown, entry].join(", ");
|
|
1378
|
-
const omitted = entries.length - index - 1;
|
|
1379
|
-
const suffix = omitted > 0 ? `; +${omitted} more.` : ".";
|
|
1380
|
-
const serialized = prefix + candidate + suffix;
|
|
1381
|
-
if (boundedEchoText(serialized, CONNECTOR_INVENTORY_MAX_BYTES) !== serialized) {
|
|
1382
|
-
break;
|
|
1410
|
+
if (detail >= 2 && title && titleAddsInformation(connector.id, title))
|
|
1411
|
+
notes.push(boundedEchoText(title, 45));
|
|
1412
|
+
const required = connectorGuideRequired(connector);
|
|
1413
|
+
if (connectorGuide(connector) && (required || detail >= 1)) {
|
|
1414
|
+
const fetch = `skills({name:"${connectorSkillName(connector.id)}"})`;
|
|
1415
|
+
notes.push(required ? `read first: ${fetch}` : `guide: ${fetch}`);
|
|
1383
1416
|
}
|
|
1384
|
-
|
|
1417
|
+
return notes.length > 0 ? `${connector.id} (${notes.join("; ")})` : connector.id;
|
|
1418
|
+
};
|
|
1419
|
+
for (const detail of [2, 1, 0]) {
|
|
1420
|
+
const line = `${prefix}${connectors.map((connector) => entry(connector, detail)).join(", ")}.`;
|
|
1421
|
+
if (fits(line))
|
|
1422
|
+
return line;
|
|
1385
1423
|
}
|
|
1386
|
-
const
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1424
|
+
const entries = connectors.map((connector) => entry(connector, 0));
|
|
1425
|
+
let shown = 0;
|
|
1426
|
+
while (fits(`${prefix}${entries.slice(0, shown + 1).join(", ")}; +${entries.length - shown - 1} more.`))
|
|
1427
|
+
shown++;
|
|
1428
|
+
return `${prefix}${entries.slice(0, shown).join(", ")}${shown > 0 ? "; " : ""}+${entries.length - shown} more.`;
|
|
1390
1429
|
}
|
|
1391
|
-
const executeDescription = (emitBudgets, hostLimits, connectorGuides, connectors, maxWrites, readOnly, native = false) => `${native ? `Call tools directly; combine work here using their program addresses. ${readOnly ? "Read-only endpoint: writes are omitted and refused." : "Writable endpoint: programs may read and write; the client approves."}` : readOnly ?
|
|
1430
|
+
const executeDescription = (emitBudgets, hostLimits, connectorGuides, connectors, maxWrites, readOnly, native = false, routes = META_ROUTES) => `${native ? `Call tools directly; combine work here using their program addresses. ${readOnly ? "Read-only endpoint: writes are omitted and refused." : "Writable endpoint: programs may read and write; the client approves."}` : `${routes === routeNames("code") ? `Every operation uses ${routes.program}.` : readOnly ? `One known read: ${routes.read}. Everything else: ${routes.program}.` : `One known read: ${routes.read}. One known write: ${routes.write}. Everything else: ${routes.program}.`} ${readOnly ? "Read-only endpoint: search and describe omit write tools, and every write call is refused." : "Writable endpoint: programs may read and write; the client approves."}`} Limits: ${hostLimits.maxHostCalls} host calls, ${readOnly ? "" : `${maxWrites} writes, `}${hostLimits.callTimeoutMs === hostLimits.hostCallTimeoutMs ? `${hostLimits.callTimeoutMs / 1_000}s/host call` : `${hostLimits.callTimeoutMs / 1_000}s/call (${hostLimits.hostCallTimeoutMs / 1_000}s result/skill/read)`}.
|
|
1392
1431
|
${connectorInventory(connectors)}
|
|
1393
1432
|
|
|
1394
1433
|
Use async () => { ... }, no arguments. No portable ambient capabilities. API:
|
|
1395
1434
|
\`\`\`ts
|
|
1396
|
-
declare const connecta: {
|
|
1397
1435
|
${GUEST_API_DECLARATION}
|
|
1398
|
-
};
|
|
1399
1436
|
\`\`\`
|
|
1400
|
-
|
|
1437
|
+
search and describe return objects, not arrays. Search shows a TypeScript signature by default; describe, JSON Schema. A call result over 1 MiB returns a paged handle; read it with connecta.result(handle, { offset }), which pages by UTF-8 byte offset. Every function except ${UNCOUNTED_GUEST_FUNCTIONS} spends a host call. Promise.allSettled keeps per-call errors, but exceeding the host-call limit ends the program. When paging is available, an oversized program return carries resultId and nextAction.
|
|
1401
1438
|
|
|
1402
|
-
Never repeat writes for output. emit(result) or emit(block) from result.content forwards MCP blocks
|
|
1439
|
+
Never repeat writes for output. emit(result) or emit(block) from result.content forwards MCP blocks unchanged; an authored Block's image/audio data is base64. emit needs no await. Caps: ${emitBudgets.maxBlocks} blocks/${emitBudgets.maxBytes} bytes. Text shares result cap. connecta.skill("usage"): repair${connectorGuides ? ", guide handling" : ""}.`;
|
|
1403
1440
|
// Module scope, like the other five meta-tool inputs: its JSON Schema is
|
|
1404
1441
|
// derived once per process. Budgets and connectors vary by deployment and
|
|
1405
1442
|
// view, so they live in the description, never here.
|
|
@@ -1439,13 +1476,14 @@ export function registerExecuteTool(server, registry, ctx) {
|
|
|
1439
1476
|
maxWrites,
|
|
1440
1477
|
maxResultChars: ctx.maxResultChars,
|
|
1441
1478
|
requestScope: ctx.requestScope,
|
|
1479
|
+
routes: ctx.routes,
|
|
1442
1480
|
});
|
|
1443
1481
|
server.registerTool("execute_code", {
|
|
1444
1482
|
description: executeDescription(emitBudgets, {
|
|
1445
1483
|
...hostLimits,
|
|
1446
1484
|
// connecta.call falls back to calls.defaultTimeoutMs; result, skill and read always use hostCallTimeoutMs.
|
|
1447
1485
|
callTimeoutMs: normalizeTimeoutMs(ctx.defaultToolTimeoutMs) ?? hostLimits.hostCallTimeoutMs,
|
|
1448
|
-
}, hasConnectorGuides(connectors), connectors, maxWrites, ctx.readOnly === true, ctx.native === true),
|
|
1486
|
+
}, hasConnectorGuides(connectors), connectors, maxWrites, ctx.readOnly === true, ctx.native === true, ctx.routes),
|
|
1449
1487
|
inputSchema: EXECUTE_INPUT,
|
|
1450
1488
|
outputSchema: EXECUTE_OUTPUT,
|
|
1451
1489
|
// The client sees execute_code as a write exactly when the endpoint writes.
|
|
@@ -1480,6 +1518,6 @@ export function registerExecuteTool(server, registry, ctx) {
|
|
|
1480
1518
|
const result = yield* ctx.authElicitation
|
|
1481
1519
|
? authInvoke(ctx.authElicitation, "run", ["execute_code", args, extra, run])
|
|
1482
1520
|
: authCall(run, []);
|
|
1483
|
-
return ctx.native ? nativeRecovery(result
|
|
1521
|
+
return ctx.native ? nativeRecovery(result) : result;
|
|
1484
1522
|
})));
|
|
1485
1523
|
}
|
package/dist/executor-result.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { META_ROUTES } from "./agent-routes.js";
|
|
1
2
|
import { CONFIG_DEFAULTS } from "./config-defaults.js";
|
|
2
3
|
import { msg } from "./errors.js";
|
|
3
4
|
/** ~6k tokens. Sandbox code should filter data down before returning. */
|
|
@@ -7,7 +8,8 @@ export function serializeResultText(value) {
|
|
|
7
8
|
const serialized = JSON.stringify(value);
|
|
8
9
|
return serialized === undefined ? String(value) : serialized;
|
|
9
10
|
}
|
|
10
|
-
|
|
11
|
+
// Only program returns carry it, and every surface running programs lists execute_code.
|
|
12
|
+
const TRUNCATION_HINT = `filter/map/slice data inside ${META_ROUTES.program} and return only what you need`;
|
|
11
13
|
/**
|
|
12
14
|
* Shape the over-cap notice so the **serialized envelope** fits the same cap
|
|
13
15
|
* the raw value missed. Escaping matters: a preview sliced to the cap is JSON
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every `connecta.<name>` function a guest program can reach, in one list.
|
|
3
|
+
*
|
|
4
|
+
* Guest-safe data only: no host implementation, no imports. The guest runtime
|
|
5
|
+
* builds its namespace from it, execute.ts admits each host call by its budget
|
|
6
|
+
* class and binds it by name to an implementation typed against this list, and
|
|
7
|
+
* the execute_code description's compact declaration is joined from it.
|
|
8
|
+
* `GuestApi` in guest-types.ts stays the published type; the package-surface
|
|
9
|
+
* test compiles the joined declaration against it.
|
|
10
|
+
*/
|
|
11
|
+
export const GUEST_FUNCTIONS = [
|
|
12
|
+
/** Searches the catalog and returns one flat page of `{ tools }`. */
|
|
13
|
+
{
|
|
14
|
+
name: "search",
|
|
15
|
+
budget: "hostCall",
|
|
16
|
+
declaration: "search(args?: { query?: string; connector?: string; limit?: number; offset?: number; includeSchemas?: Schemas }): Promise<{ tools: Tool[]; catalogErrors: { connector: string; code: string; message: string }[]; hasMore: boolean; nextOffset?: number }>;",
|
|
17
|
+
},
|
|
18
|
+
/** Describes exact addresses, one entry per address in order. */
|
|
19
|
+
{
|
|
20
|
+
name: "describe",
|
|
21
|
+
budget: "hostCall",
|
|
22
|
+
declaration: "describe(args?: { address?: string; addresses?: string[]; format?: Schemas }): Promise<{ tools: (Tool & { error?: string })[] }>;",
|
|
23
|
+
},
|
|
24
|
+
/** Calls one tool and inlines transfer pages and native content. */
|
|
25
|
+
{
|
|
26
|
+
name: "call",
|
|
27
|
+
budget: "hostCall",
|
|
28
|
+
wrapped: true,
|
|
29
|
+
declaration: "call(address: string, args?: unknown, options?: { timeoutMs?: number }): Promise<Result>;",
|
|
30
|
+
},
|
|
31
|
+
/** Reads one advertised resource URI. */
|
|
32
|
+
{
|
|
33
|
+
name: "read",
|
|
34
|
+
budget: "hostCall",
|
|
35
|
+
declaration: "read(uri: string): Promise<{ contents: { uri: string; text?: string; blob?: string }[] }>;",
|
|
36
|
+
},
|
|
37
|
+
/** Pages a retained result by page or UTF-8 byte offset. */
|
|
38
|
+
{
|
|
39
|
+
name: "result",
|
|
40
|
+
budget: "hostCall",
|
|
41
|
+
deadline: "result storage",
|
|
42
|
+
declaration: "result(id: string | Paged, options?: { page?: number; offset?: number; maxBytes?: number }): Promise<{ text: string; hasMore: boolean; nextOffset?: number }>;",
|
|
43
|
+
},
|
|
44
|
+
/** Fetches a skill's text by exact name. */
|
|
45
|
+
{
|
|
46
|
+
name: "skill",
|
|
47
|
+
budget: "hostCall",
|
|
48
|
+
deadline: "skill lookup",
|
|
49
|
+
alias: { name: "skills", note: "models write connecta.skills, the top-level tool's name." },
|
|
50
|
+
declaration: "skill(name: string): Promise<{ text: string }>;",
|
|
51
|
+
},
|
|
52
|
+
/** Forwards a content block to the client; needs no await. */
|
|
53
|
+
{
|
|
54
|
+
name: "emit",
|
|
55
|
+
budget: "emission",
|
|
56
|
+
declaration: "emit(block: Block | Native | Exclude<Result, Paged>): PromiseLike<void>;",
|
|
57
|
+
},
|
|
58
|
+
/** Tells the host a failed program read a result field off an unawaited call. */
|
|
59
|
+
{
|
|
60
|
+
name: "unawaited",
|
|
61
|
+
budget: "once",
|
|
62
|
+
label: "missing-await notice",
|
|
63
|
+
},
|
|
64
|
+
];
|
|
65
|
+
/** Public guest functions that spend no host call, for the agent-facing text. */
|
|
66
|
+
export const UNCOUNTED_GUEST_FUNCTIONS = GUEST_FUNCTIONS.filter((fn) => fn.budget === "emission")
|
|
67
|
+
.map((fn) => fn.name)
|
|
68
|
+
.join(" and ");
|
|
69
|
+
/**
|
|
70
|
+
* The guest API as the execute_code description shows it: compact shapes of
|
|
71
|
+
* GuestApi in src/guest-types.ts, checked against it by the package-surface
|
|
72
|
+
* declaration test. Arguments match what GuestApi accepts in both directions,
|
|
73
|
+
* results are supertypes of its results, and no field is invented. Type notation, not runnable JS.
|
|
74
|
+
*/
|
|
75
|
+
export const GUEST_API_DECLARATION = `declare const connecta: {
|
|
76
|
+
${GUEST_FUNCTIONS.flatMap((fn) => ("declaration" in fn ? [fn.declaration] : [])).join("\n")}
|
|
77
|
+
};
|
|
78
|
+
type Schemas = "json" | "compact" | "typescript";
|
|
79
|
+
type Tool = { address: string; description?: string; inputSchema?: unknown; outputSchema?: unknown; signature?: string };
|
|
80
|
+
type Paged = { format: "paged"; valueFormat: "json" | "text"; resultId?: string; totalBytes: number; truncated: true; hint: string };
|
|
81
|
+
type Result = ({ format: "json"; data: unknown } | { format: "text"; data: string } | Paged) & { content?: Native[] };
|
|
82
|
+
type Block = { type: "text"; text: string } | { type: "image" | "audio"; data: string; mimeType: string };
|
|
83
|
+
type Native = Block | { type: "resource"; resource: { uri: string } & ({ text: string } | { blob: string }) } | { type: "resource_link"; uri: string; name: string };`;
|
package/dist/guest-runtime.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { GUEST_FUNCTIONS } from "./guest-functions.js";
|
|
1
2
|
/** Capture the single-use runner before evaluating the guest expression. */
|
|
2
3
|
export function wrapGuestProgram(code) {
|
|
3
4
|
return `async () => globalThis.__connecta_run()(async (connecta) => await (\n${code}\n)())`;
|
|
@@ -62,6 +63,34 @@ export function guestPromiseInitializer() {
|
|
|
62
63
|
Object.freeze(NativePromise.prototype);
|
|
63
64
|
})()`;
|
|
64
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* The frozen namespace, joined from the registry. A `wrapped` or `emission`
|
|
68
|
+
* function uses the initializer's local of the same name; an alias shares one
|
|
69
|
+
* function object. Only guest source enters this string.
|
|
70
|
+
*/
|
|
71
|
+
const GUEST_NAMESPACE = (() => {
|
|
72
|
+
const before = [];
|
|
73
|
+
const entries = ["__proto__: null"];
|
|
74
|
+
for (const fn of GUEST_FUNCTIONS) {
|
|
75
|
+
if (fn.budget === "once")
|
|
76
|
+
continue;
|
|
77
|
+
if (fn.budget === "emission") {
|
|
78
|
+
entries.push(`${fn.name}(block) {
|
|
79
|
+
const ref = block && typeof block === "object" ? weakGet(nativeRefs, block) : undefined;
|
|
80
|
+
return trackEmission(${fn.name}(ref || block));
|
|
81
|
+
}`);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const value = `hosted(${"wrapped" in fn ? fn.name : `provider.${fn.name}`})`;
|
|
85
|
+
if (!("alias" in fn)) {
|
|
86
|
+
entries.push(`${fn.name}: ${value}`);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
before.push(` const ${fn.name} = ${value};\n`);
|
|
90
|
+
entries.push(fn.name, `// Undeclared alias: ${fn.alias.note}\n ${fn.alias.name}: ${fn.name}`);
|
|
91
|
+
}
|
|
92
|
+
return `${before.join("")} const namespace = freeze({\n ${entries.join(",\n ")}\n });`;
|
|
93
|
+
})();
|
|
65
94
|
/** Capture a private initializer before a Worker evaluates its guest module. */
|
|
66
95
|
export function guestInitializer() {
|
|
67
96
|
return `(() => {
|
|
@@ -152,21 +181,17 @@ export function guestInitializer() {
|
|
|
152
181
|
}
|
|
153
182
|
});
|
|
154
183
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
const ref = block && typeof block === "object" ? weakGet(nativeRefs, block) : undefined;
|
|
167
|
-
return trackEmission(emit(ref || block));
|
|
168
|
-
}
|
|
169
|
-
});
|
|
184
|
+
// Result fields read off an unawaited host call flag the run, still undefined.
|
|
185
|
+
let unawaited = false;
|
|
186
|
+
const fields = {};
|
|
187
|
+
for (const key of ["data", "format", "content", "tools", "catalogErrors", "total", "text", "resultId", "hasMore", "nextOffset"])
|
|
188
|
+
fields[key] = { get() { unawaited = true; } };
|
|
189
|
+
const hosted = (fn) => function () {
|
|
190
|
+
const promise = apply(fn, undefined, arguments);
|
|
191
|
+
if (promise && typeof promise === "object") defineProperties(promise, fields);
|
|
192
|
+
return promise;
|
|
193
|
+
};
|
|
194
|
+
${GUEST_NAMESPACE}
|
|
170
195
|
globalThis.connecta = namespace;
|
|
171
196
|
defineProperties(globalThis, {
|
|
172
197
|
Error: { value: NativeError, writable: false, configurable: false },
|
|
@@ -176,7 +201,13 @@ export function guestInitializer() {
|
|
|
176
201
|
return async (program) => {
|
|
177
202
|
if (used) throw new NativeError("Guest runner initialization is single-use.");
|
|
178
203
|
used = true;
|
|
179
|
-
|
|
204
|
+
let result;
|
|
205
|
+
try {
|
|
206
|
+
result = await program(namespace);
|
|
207
|
+
} catch (error) {
|
|
208
|
+
if (unawaited) try { await provider.unawaited(); } catch {}
|
|
209
|
+
throw error;
|
|
210
|
+
}
|
|
180
211
|
for (let i = 0; i < pending.length; i++) {
|
|
181
212
|
const report = await pending[i].settled;
|
|
182
213
|
if (!report.ok && !pending[i].handled) throw report.error;
|
|
@@ -200,8 +231,15 @@ const ERROR_NAMES = new Set([
|
|
|
200
231
|
]);
|
|
201
232
|
/** Fixed repair guidance, used only in agent-facing results. */
|
|
202
233
|
export function programError(raw, code) {
|
|
203
|
-
const
|
|
204
|
-
|
|
234
|
+
const text = typeof raw.message === "string" ? raw.message : "Program failed.";
|
|
235
|
+
// Executors may report "TypeError: …" as the message itself; name it once.
|
|
236
|
+
const prefix = /^([A-Za-z]*Error): /.exec(text)?.[1];
|
|
237
|
+
const name = typeof raw.name === "string" && ERROR_NAMES.has(raw.name)
|
|
238
|
+
? raw.name
|
|
239
|
+
: prefix && ERROR_NAMES.has(prefix)
|
|
240
|
+
? prefix
|
|
241
|
+
: "Error";
|
|
242
|
+
const message = prefix === name ? text.slice(name.length + 2) : text;
|
|
205
243
|
const stack = typeof raw.stack === "string" ? raw.stack : "";
|
|
206
244
|
// Compare QuickJS/V8 locations to the wrapper baseline after the source.
|
|
207
245
|
const location = /:(\d+)(?::\d+)?\)?(?:\n|$)/.exec(stack);
|
|
@@ -217,7 +255,10 @@ export function programError(raw, code) {
|
|
|
217
255
|
? Math.max(1, Number(location[1]) - Number(baseline[1]) + code.split("\n").length + 1)
|
|
218
256
|
: null;
|
|
219
257
|
let hint = "Use plain JavaScript in one async () => { ... } expression and the connecta global.";
|
|
220
|
-
if (
|
|
258
|
+
if (raw.unawaited === true) {
|
|
259
|
+
hint = "connecta.* returns a Promise; await it: (await connecta.call(address, args)).data.";
|
|
260
|
+
}
|
|
261
|
+
else if (/\brequire\b|\b[Ii]mports?\b|\bfs\b|filesystem|node:/.test(message) ||
|
|
221
262
|
(name === "SyntaxError" && /\bimport\b/.test(code))) {
|
|
222
263
|
hint =
|
|
223
264
|
"Imports, require, and filesystem access are outside the guest API. Use connecta.call to access configured services.";
|