@zackbart/connecta 0.10.6 → 0.12.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/AGENTS.md +8 -6
- package/CHANGELOG.md +80 -0
- package/README.md +5 -4
- package/bin/connecta.mjs +0 -7
- package/dist/activity.d.ts.map +1 -1
- package/dist/activity.js.map +1 -1
- package/dist/apps-shell.d.ts +38 -0
- package/dist/apps-shell.d.ts.map +1 -0
- package/dist/apps-shell.js +175 -0
- package/dist/apps-shell.js.map +1 -0
- package/dist/catalog-service.d.ts +2 -17
- package/dist/catalog-service.d.ts.map +1 -1
- package/dist/catalog-service.js +4 -6
- package/dist/catalog-service.js.map +1 -1
- package/dist/connectors/api.d.ts +2 -2
- package/dist/connectors/remote-mcp.d.ts +1 -1
- package/dist/errors.d.ts +1 -3
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/execute.d.ts +37 -8
- package/dist/execute.d.ts.map +1 -1
- package/dist/execute.js +137 -42
- package/dist/execute.js.map +1 -1
- package/dist/executor-admission.d.ts +8 -0
- package/dist/executor-admission.d.ts.map +1 -1
- package/dist/executor-admission.js +11 -0
- package/dist/executor-admission.js.map +1 -1
- package/dist/executors/quickjs.d.ts.map +1 -1
- package/dist/executors/quickjs.js +2 -2
- package/dist/executors/quickjs.js.map +1 -1
- package/dist/index.d.ts +15 -31
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -37
- package/dist/index.js.map +1 -1
- package/dist/invocation.js +2 -2
- package/dist/invocation.js.map +1 -1
- package/dist/meta-tools.d.ts +19 -58
- package/dist/meta-tools.d.ts.map +1 -1
- package/dist/meta-tools.js +34 -431
- package/dist/meta-tools.js.map +1 -1
- package/dist/registry.d.ts +1 -10
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +3 -15
- package/dist/registry.js.map +1 -1
- package/dist/routes/mcp.d.ts.map +1 -1
- package/dist/routes/mcp.js +67 -30
- package/dist/routes/mcp.js.map +1 -1
- package/dist/routes/shared.d.ts +5 -11
- package/dist/routes/shared.d.ts.map +1 -1
- package/dist/routes/shared.js.map +1 -1
- package/dist/server.js +5 -4
- package/dist/server.js.map +1 -1
- package/dist/skills.d.ts +8 -18
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +13 -60
- package/dist/skills.js.map +1 -1
- package/dist/types.d.ts +6 -20
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/code-first-exploration.md +16 -16
- package/documentation/code-mode.md +130 -41
- package/documentation/connectors.md +1 -1
- package/documentation/mcp-2026-07-28.md +1 -1
- package/documentation/mcp-ui-design.md +382 -0
- package/documentation/meta-tools.md +30 -43
- package/documentation/rich-output-design.md +4 -4
- package/ethos.md +7 -2
- package/examples/node/README.md +1 -2
- package/examples/node/src/index.ts +1 -3
- package/examples/worker/README.md +8 -13
- package/examples/worker/src/index.ts +6 -14
- package/examples/worker/wrangler.jsonc +3 -6
- package/package.json +1 -1
- package/src/activity.ts +5 -0
- package/src/apps-shell.ts +179 -0
- package/src/catalog-service.ts +6 -26
- package/src/connectors/api.ts +2 -2
- package/src/connectors/remote-mcp.ts +1 -1
- package/src/errors.ts +2 -2
- package/src/execute.ts +150 -49
- package/src/executor-admission.ts +12 -0
- package/src/executors/quickjs.ts +2 -1
- package/src/index.ts +40 -69
- package/src/invocation.ts +2 -2
- package/src/meta-tools.ts +38 -565
- package/src/registry.ts +2 -33
- package/src/routes/mcp.ts +79 -30
- package/src/routes/shared.ts +4 -11
- package/src/server.ts +7 -7
- package/src/skills.ts +11 -74
- package/src/types.ts +6 -21
- package/src/version.ts +1 -1
- package/templates/node/README.md +2 -1
- package/templates/node/package.json +1 -1
- package/templates/node/src/index.ts +1 -1
package/src/execute.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { McpServer } from "@modelcontextprotocol/server";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import type { ActivityRequestContext } from "./activity.js";
|
|
4
|
+
import {
|
|
5
|
+
PROGRAM_UI_META_KEY,
|
|
6
|
+
PROGRAM_UI_RESOURCE_URI,
|
|
7
|
+
} from "./apps-shell.js";
|
|
4
8
|
import {
|
|
5
9
|
boundedDiscoveryText,
|
|
6
10
|
CatalogService,
|
|
@@ -15,6 +19,7 @@ import {
|
|
|
15
19
|
} from "./executor-result.js";
|
|
16
20
|
import {
|
|
17
21
|
ExecutorAdmissionError,
|
|
22
|
+
ExecutorExecutionError,
|
|
18
23
|
isAdmittingExecutor,
|
|
19
24
|
} from "./executor-admission.js";
|
|
20
25
|
import { classifyCallError } from "./errors.js";
|
|
@@ -24,7 +29,6 @@ import {
|
|
|
24
29
|
} from "./invocation.js";
|
|
25
30
|
import type { RegistryView } from "./registry.js";
|
|
26
31
|
import type {
|
|
27
|
-
ConnectaSurface,
|
|
28
32
|
Executor,
|
|
29
33
|
ExecutorProvider,
|
|
30
34
|
Logger,
|
|
@@ -70,6 +74,7 @@ class ExecuteDiagnostics {
|
|
|
70
74
|
setupMs = 0;
|
|
71
75
|
executorWallMs = 0;
|
|
72
76
|
private emitted?: { count: number; bytes: number };
|
|
77
|
+
private ui?: number;
|
|
73
78
|
|
|
74
79
|
/** Numbers only, per R8 — and only once something was emitted, so a
|
|
75
80
|
* non-emitting run's diagnostics stay byte-for-byte what they were. */
|
|
@@ -77,6 +82,15 @@ class ExecuteDiagnostics {
|
|
|
77
82
|
if (count > 0) this.emitted = { count, bytes };
|
|
78
83
|
}
|
|
79
84
|
|
|
85
|
+
/**
|
|
86
|
+
* U9: the UI payload gets its own aggregate — one number, the payload's
|
|
87
|
+
* serialized size. Folding it into `emitted` would desync that aggregate's
|
|
88
|
+
* pair, which reports the bytes a specific block count cost.
|
|
89
|
+
*/
|
|
90
|
+
recordUi(bytes: number): void {
|
|
91
|
+
this.ui = bytes;
|
|
92
|
+
}
|
|
93
|
+
|
|
80
94
|
private stats(operation: ExecuteDiagnosticOperation) {
|
|
81
95
|
let stats = this.operations.get(operation);
|
|
82
96
|
if (!stats) {
|
|
@@ -159,6 +173,7 @@ class ExecuteDiagnostics {
|
|
|
159
173
|
};
|
|
160
174
|
operations: ExecuteOperationDiagnostics[];
|
|
161
175
|
emitted?: { count: number; bytes: number };
|
|
176
|
+
ui?: number;
|
|
162
177
|
} {
|
|
163
178
|
const operations = [...this.operations.values()];
|
|
164
179
|
return {
|
|
@@ -175,6 +190,7 @@ class ExecuteDiagnostics {
|
|
|
175
190
|
},
|
|
176
191
|
operations,
|
|
177
192
|
...(this.emitted ? { emitted: this.emitted } : {}),
|
|
193
|
+
...(this.ui !== undefined ? { ui: this.ui } : {}),
|
|
178
194
|
};
|
|
179
195
|
}
|
|
180
196
|
}
|
|
@@ -232,16 +248,49 @@ function requireEmittedBlock(raw: unknown): EmittedBlock {
|
|
|
232
248
|
return raw as EmittedBlock;
|
|
233
249
|
}
|
|
234
250
|
|
|
251
|
+
const UI_SHAPE_HINT =
|
|
252
|
+
"connecta.ui accepts exactly one argument: a non-empty string of HTML";
|
|
253
|
+
|
|
254
|
+
/** What the argument was, named the way the emit validator names a bad field. */
|
|
255
|
+
function describeUiArgument(raw: unknown): string {
|
|
256
|
+
if (raw === null) return "null";
|
|
257
|
+
if (raw === undefined) return "undefined";
|
|
258
|
+
if (Array.isArray(raw)) return "an array";
|
|
259
|
+
if (typeof raw === "string") return "an empty string";
|
|
260
|
+
const kind = typeof raw;
|
|
261
|
+
return `${/^[aeiou]/.test(kind) ? "an" : "a"} ${kind}`;
|
|
262
|
+
}
|
|
263
|
+
|
|
235
264
|
/**
|
|
236
|
-
*
|
|
237
|
-
*
|
|
238
|
-
*
|
|
239
|
-
|
|
240
|
-
|
|
265
|
+
* Strict U1 validation. There is no options parameter and no sugar form, for
|
|
266
|
+
* M1's reason: sugar is how a one-shape contract grows hair. An options bag or
|
|
267
|
+
* an MCP block object is just a non-string, and fails as one.
|
|
268
|
+
*/
|
|
269
|
+
function requireUiHtml(raw: unknown): string {
|
|
270
|
+
if (typeof raw !== "string" || raw.length === 0) {
|
|
271
|
+
throw new Error(`${UI_SHAPE_HINT}; got ${describeUiArgument(raw)}`);
|
|
272
|
+
}
|
|
273
|
+
return raw;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Request-local collection for `connecta.emit` and `connecta.ui`. Budgets fail
|
|
278
|
+
* loudly at the crossing call — nothing is partially accepted and prior blocks
|
|
279
|
+
* are unaffected — so a program learns it is over budget while it can still
|
|
280
|
+
* choose differently (M5, U4). Accepted output never rides `ExecuteResult`; the
|
|
281
|
+
* handler that owns this collector delivers it on the final tool result.
|
|
282
|
+
*
|
|
283
|
+
* The two channels share the byte aggregate and nothing else: a UI payload is
|
|
284
|
+
* not a block, so it spends no block count, and at most one is ever accepted.
|
|
241
285
|
*/
|
|
242
286
|
export class EmitCollector {
|
|
243
287
|
readonly blocks: EmittedBlock[] = [];
|
|
288
|
+
/** The shared transport aggregate: emitted blocks plus the UI payload. */
|
|
244
289
|
bytes = 0;
|
|
290
|
+
/** The one accepted UI payload (U2), delivered in result `_meta` on success. */
|
|
291
|
+
ui?: { html: string };
|
|
292
|
+
/** What the blocks alone cost, so the `emitted` aggregate stays a true pair. */
|
|
293
|
+
private blockBytes = 0;
|
|
245
294
|
constructor(
|
|
246
295
|
private readonly maxBytes: number,
|
|
247
296
|
private readonly maxBlocks: number,
|
|
@@ -263,7 +312,38 @@ export class EmitCollector {
|
|
|
263
312
|
}
|
|
264
313
|
this.blocks.push(block);
|
|
265
314
|
this.bytes += size;
|
|
266
|
-
this.
|
|
315
|
+
this.blockBytes += size;
|
|
316
|
+
this.diagnostics?.recordEmitted(this.blocks.length, this.blockBytes);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* U2 and U4: one payload per run, measured as the serialized bytes of
|
|
321
|
+
* `{ html }` against the same aggregate emit spends. A second call throws
|
|
322
|
+
* naming the constraint rather than replacing the first — one tool result
|
|
323
|
+
* renders one view, and last-wins would silently discard a payload the
|
|
324
|
+
* program deliberately supplied.
|
|
325
|
+
*
|
|
326
|
+
* Multiplicity is checked before shape, so the second call is told what it
|
|
327
|
+
* actually broke. A program whose second payload is also malformed has one
|
|
328
|
+
* problem worth naming — that there is a second payload at all — and a
|
|
329
|
+
* complaint about its type would send the author to fix the wrong thing.
|
|
330
|
+
*/
|
|
331
|
+
acceptUi(raw: unknown): void {
|
|
332
|
+
if (this.ui) {
|
|
333
|
+
throw new Error(
|
|
334
|
+
"connecta.ui accepts at most one payload per run: a view was already accepted and stands",
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
const payload = { html: requireUiHtml(raw) };
|
|
338
|
+
const size = diagnosticsEncoder.encode(JSON.stringify(payload)).byteLength;
|
|
339
|
+
if (this.bytes + size > this.maxBytes) {
|
|
340
|
+
throw new Error(
|
|
341
|
+
`connecta.ui byte budget exceeded: payload is ${size} serialized bytes with ${this.maxBytes - this.bytes} of ${this.maxBytes} remaining`,
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
this.ui = payload;
|
|
345
|
+
this.bytes += size;
|
|
346
|
+
this.diagnostics?.recordUi(size);
|
|
267
347
|
}
|
|
268
348
|
}
|
|
269
349
|
|
|
@@ -425,12 +505,7 @@ export async function buildSandboxProviders(
|
|
|
425
505
|
const requestScope = {};
|
|
426
506
|
const catalog = new CatalogService(registry, baseUrl, {
|
|
427
507
|
requestScope,
|
|
428
|
-
//
|
|
429
|
-
// code-first and a classic-with-executor deployment alike, so the retry
|
|
430
|
-
// advice names connecta.describe regardless of what this server advertises.
|
|
431
|
-
describeRoute: "connecta.describe",
|
|
432
|
-
// Same reasoning for the discovery route a routing failure hands back: the
|
|
433
|
-
// program that just missed an address cannot call search_tools.
|
|
508
|
+
// A program that just missed an address cannot call search_tools.
|
|
434
509
|
searchRoute: "connecta.search",
|
|
435
510
|
...(limits.discoveryConcurrency !== undefined
|
|
436
511
|
? { concurrency: limits.discoveryConcurrency }
|
|
@@ -564,6 +639,18 @@ export async function buildSandboxProviders(
|
|
|
564
639
|
}
|
|
565
640
|
limits.emitCollector.accept(block);
|
|
566
641
|
},
|
|
642
|
+
// The rendered-output channel rides the same bridge for the same
|
|
643
|
+
// reason (U7): one more provider fn, no change to ExecuteResult or
|
|
644
|
+
// the Executor contract. Delivery is the handler's job, not the
|
|
645
|
+
// guest's — nothing here becomes addressable.
|
|
646
|
+
ui: async (html: unknown) => {
|
|
647
|
+
if (!limits.emitCollector) {
|
|
648
|
+
throw new Error(
|
|
649
|
+
"connecta.ui is unavailable: no emission collector was configured for this execution",
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
limits.emitCollector.acceptUi(html);
|
|
653
|
+
},
|
|
567
654
|
batch: async (calls: unknown) => {
|
|
568
655
|
const started = Date.now();
|
|
569
656
|
const callCount = Array.isArray(calls) ? calls.length : 0;
|
|
@@ -588,8 +675,8 @@ export async function buildSandboxProviders(
|
|
|
588
675
|
),
|
|
589
676
|
};
|
|
590
677
|
} catch (err) {
|
|
591
|
-
//
|
|
592
|
-
// can log, plus the typed details it must classify by. A
|
|
678
|
+
// The failure shape connecta.batch reports: the message a
|
|
679
|
+
// program can log, plus the typed details it must classify by. A
|
|
593
680
|
// thrown host error crosses the sandbox bridge as a bare
|
|
594
681
|
// message string in every executor, so this is the one place a
|
|
595
682
|
// program can tell a policy refusal from a transient failure.
|
|
@@ -818,6 +905,9 @@ export function createExecuteTool(
|
|
|
818
905
|
? { retryAfterMs: err.retryAfterMs }
|
|
819
906
|
: {}),
|
|
820
907
|
},
|
|
908
|
+
...(err instanceof ExecutorExecutionError
|
|
909
|
+
? discardedEmits(emitted)
|
|
910
|
+
: {}),
|
|
821
911
|
...(diagnostics ? { diagnostics: diagnostics.finish() } : {}),
|
|
822
912
|
});
|
|
823
913
|
result.isError = true;
|
|
@@ -857,7 +947,7 @@ export function createExecuteTool(
|
|
|
857
947
|
// Executor bridges necessarily reduce thrown host errors to strings.
|
|
858
948
|
// Match that terminal string back to the request-local typed failure so
|
|
859
949
|
// an unhandled tool failure keeps the same structured contract as
|
|
860
|
-
// call_tool
|
|
950
|
+
// call_tool. Failures caught by model code never reach
|
|
861
951
|
// outcome.error and therefore remain under that code's control.
|
|
862
952
|
//
|
|
863
953
|
// An error the program let through unchanged matches exactly, and an
|
|
@@ -942,9 +1032,19 @@ export function createExecuteTool(
|
|
|
942
1032
|
const response = jsonResult({
|
|
943
1033
|
result,
|
|
944
1034
|
...(emitted.blocks.length > 0 ? { emitted: emitted.blocks.length } : {}),
|
|
1035
|
+
// U3: the model learns a view rendered without seeing its bytes. Spread
|
|
1036
|
+
// conditionally so a program that never called connecta.ui produces
|
|
1037
|
+
// today's byte-for-byte response.
|
|
1038
|
+
...(emitted.ui ? { ui: true } : {}),
|
|
945
1039
|
...(logs ? { logs } : {}),
|
|
946
1040
|
...(diagnostics ? { diagnostics: diagnostics.finish() } : {}),
|
|
947
1041
|
});
|
|
1042
|
+
if (emitted.ui) {
|
|
1043
|
+
// _meta is where the Apps spec's best practices put data "not intended
|
|
1044
|
+
// for model context", and how shipped hosts behave. The shell reads
|
|
1045
|
+
// exactly this key out of the tool result the host delivers to it.
|
|
1046
|
+
response._meta = { [PROGRAM_UI_META_KEY]: { html: emitted.ui.html } };
|
|
1047
|
+
}
|
|
948
1048
|
if (emitted.blocks.length > 0) {
|
|
949
1049
|
// Emitted image/audio blocks are valid MCP content that ToolResult's
|
|
950
1050
|
// text-only typing does not model — the same acknowledged gap
|
|
@@ -957,52 +1057,43 @@ export function createExecuteTool(
|
|
|
957
1057
|
};
|
|
958
1058
|
}
|
|
959
1059
|
|
|
960
|
-
/**
|
|
1060
|
+
/**
|
|
1061
|
+
* M4 and U3: a failed program delivers no blocks and no view, but each
|
|
1062
|
+
* discard is visible — and one failure can discard both.
|
|
1063
|
+
*/
|
|
961
1064
|
function discardedEmits(emitted: EmitCollector): {
|
|
962
1065
|
emittedDiscarded?: number;
|
|
1066
|
+
uiDiscarded?: true;
|
|
963
1067
|
} {
|
|
964
|
-
return
|
|
965
|
-
|
|
966
|
-
|
|
1068
|
+
return {
|
|
1069
|
+
...(emitted.blocks.length > 0
|
|
1070
|
+
? { emittedDiscarded: emitted.blocks.length }
|
|
1071
|
+
: {}),
|
|
1072
|
+
...(emitted.ui ? { uiDiscarded: true as const } : {}),
|
|
1073
|
+
};
|
|
967
1074
|
}
|
|
968
1075
|
|
|
969
1076
|
/** The same visibility for the plain-text error paths. */
|
|
970
1077
|
function discardedEmitsText(emitted: EmitCollector): string {
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
1078
|
+
const lines = [
|
|
1079
|
+
...(emitted.blocks.length > 0
|
|
1080
|
+
? [`emittedDiscarded: ${emitted.blocks.length}`]
|
|
1081
|
+
: []),
|
|
1082
|
+
...(emitted.ui ? ["uiDiscarded: true"] : []),
|
|
1083
|
+
];
|
|
1084
|
+
return lines.length > 0 ? `\n\n${lines.join("\n")}` : "";
|
|
974
1085
|
}
|
|
975
1086
|
|
|
976
|
-
/**
|
|
977
|
-
* How the tool opens, and where a program's argument schemas come from. Both
|
|
978
|
-
* differ by surface: on the classic surface `execute_code` is the tool of last
|
|
979
|
-
* resort and its neighbours (`batch_call`, `describe_tools`) own the simpler
|
|
980
|
-
* jobs, while on the code-first surface those tools are gone and the program is
|
|
981
|
-
* where all of that work happens. Everything after these two phrases is
|
|
982
|
-
* identical, so the shared body below has one source of truth.
|
|
983
|
-
*/
|
|
984
|
-
const EXECUTE_ROUTING = {
|
|
985
|
-
classic:
|
|
986
|
-
"Use for dependent multi-step calls, loops, joins, branching, or reducing large results in a sandbox. Never use execute_code for search-only discovery or one downstream call: use search_tools, then call_tool when needed. For 2–10 independent calls use batch_call.",
|
|
987
|
-
"code-first":
|
|
988
|
-
"The primary surface. Use for discovery beyond one lookup, two or more calls, dependent steps, loops, joins, branching, or reducing large results before they reach the model — connecta.search and connecta.describe browse and expand catalogs in the run, and connecta.batch replaces a separate batch tool. The exception is a single call at an address already in hand: search_tools then one call_tool is cheaper than a program.",
|
|
989
|
-
} as const;
|
|
990
|
-
|
|
991
|
-
const EXECUTE_SCHEMA_SOURCE = {
|
|
992
|
-
classic: "describe_tools",
|
|
993
|
-
"code-first": "connecta.describe",
|
|
994
|
-
} as const;
|
|
995
|
-
|
|
996
1087
|
const executeDescription = (
|
|
997
|
-
surface: ConnectaSurface,
|
|
998
1088
|
emitBudgets: { maxBytes: number; maxBlocks: number },
|
|
999
|
-
) =>
|
|
1089
|
+
) => `The primary surface. Use for discovery beyond one lookup, two or more calls, dependent steps, loops, joins, branching, or reducing large results before they reach the model — connecta.search and connecta.describe browse and expand catalogs in the run, and connecta.batch handles independent calls. The exception is a single call at an address already in hand: search_tools then one call_tool is cheaper than a program. Only tools explicitly annotated readOnlyHint: true are available. Each run is limited to ${EXECUTE_MAX_HOST_CALLS} host calls; connecta.batch accepts at most ${EXECUTE_MAX_BATCH_CALLS}; each host call has a ${EXECUTE_HOST_CALL_TIMEOUT_MS / 1_000}-second deadline.
|
|
1000
1090
|
|
|
1001
1091
|
Write an async arrow function. It runs with NO network, filesystem, timers, or imports — the only capabilities are:
|
|
1002
|
-
- One global per connector: every address <connectorId>.<toolName> from search_tools is callable as <connectorId>.<toolName>(args) with a single args object matching the schema from
|
|
1092
|
+
- One global per connector: every address <connectorId>.<toolName> from search_tools is callable as <connectorId>.<toolName>(args) with a single args object matching the schema from connecta.describe. Names are sanitized to JS identifiers: characters outside [A-Za-z0-9_$] become "_" (e.g. my-service.get.thing → my_service.get_thing), leading digits get "_" prefixed, reserved words get "_" appended.
|
|
1003
1093
|
- connecta.call(address, args) and connecta.batch(calls) — call raw addresses.
|
|
1004
1094
|
- connecta.search(args), connecta.describe({ address: "<connectorId>.<toolName>" }), and connecta.describe({ addresses: [...] }) — load and inspect request-local catalogs on demand. Use safety: "readOnly" to avoid advertising calls this sandbox cannot execute; the filter changes results, not authority. Matches carrying schemas also list inputKeys, requiredInputKeys, and outputKeys — the same names the schema shows, ready to check against before building args. They are absent when a schema is not a plain object shape, so read the schema itself rather than assuming a missing list means no fields.
|
|
1005
1095
|
- connecta.emit(block) — deliver rich MCP content alongside the JSON return: exactly { type: "text", text } or { type: "image" | "audio", data (base64), mimeType }, no other fields. Blocks are appended to the result on success only, spend no host calls, and are budgeted per run (${emitBudgets.maxBlocks} blocks, ${emitBudgets.maxBytes} serialized bytes); an over-budget or invalid emit throws catchably and accepts nothing.
|
|
1096
|
+
- connecta.ui(html) — hand the client one rendered view of this run: exactly one argument, a non-empty HTML string, no options and no block object. At most one per run, delivered on success only, out of model context (the envelope just reports ui: true), and spending no host calls. It draws on the same ${emitBudgets.maxBytes}-byte budget as connecta.emit; a second, over-budget, or invalid call throws catchably and accepts nothing. The view is display-only — no network, no tool calls, no links.
|
|
1006
1097
|
- console.log(...) — captured and returned alongside the result.
|
|
1007
1098
|
|
|
1008
1099
|
Tool calls return plain values (MCP text content is JSON-parsed when possible) and throw on downstream errors — use try/catch to handle them. A thrown error carries only a message; connecta.batch reports each call as { address, ok: true, data } or { address, ok: false, error, errorDetails: { code, retryable } }, so use it when the program must tell a policy refusal from a transient failure. Never retry a failure whose retryable is false, and never retry a rate_limited one immediately — the sandbox has no timers. Return a JSON-serializable value; large results are truncated, so reduce data in code instead of returning raw payloads.
|
|
@@ -1032,8 +1123,6 @@ export function registerExecuteTool(
|
|
|
1032
1123
|
maxEmittedBytes?: number;
|
|
1033
1124
|
/** Block-count budget for connecta.emit. Default 32. */
|
|
1034
1125
|
maxEmittedBlocks?: number;
|
|
1035
|
-
/** The advertised surface, which decides this tool's routing copy. */
|
|
1036
|
-
surface?: ConnectaSurface;
|
|
1037
1126
|
},
|
|
1038
1127
|
): void {
|
|
1039
1128
|
// Resolved once so the description and the collector cannot disagree about
|
|
@@ -1065,7 +1154,7 @@ export function registerExecuteTool(
|
|
|
1065
1154
|
server.registerTool(
|
|
1066
1155
|
"execute_code",
|
|
1067
1156
|
{
|
|
1068
|
-
description: executeDescription(
|
|
1157
|
+
description: executeDescription(emitBudgets),
|
|
1069
1158
|
inputSchema: z.object({
|
|
1070
1159
|
code: z
|
|
1071
1160
|
.string()
|
|
@@ -1084,6 +1173,18 @@ export function registerExecuteTool(
|
|
|
1084
1173
|
destructiveHint: false,
|
|
1085
1174
|
openWorldHint: true,
|
|
1086
1175
|
},
|
|
1176
|
+
// U5 and U10: declared unconditionally. A host without the Apps
|
|
1177
|
+
// extension ignores unknown _meta and sees the ordinary envelope, which
|
|
1178
|
+
// is the text fallback the spec mandates — and a stateless aggregator
|
|
1179
|
+
// has nowhere dependable to hold a negotiation check anyway. The
|
|
1180
|
+
// explicit visibility keeps hosts from being told the view may call
|
|
1181
|
+
// execute_code; the default ["model","app"] would say exactly that.
|
|
1182
|
+
_meta: {
|
|
1183
|
+
ui: {
|
|
1184
|
+
resourceUri: PROGRAM_UI_RESOURCE_URI,
|
|
1185
|
+
visibility: ["model"],
|
|
1186
|
+
},
|
|
1187
|
+
},
|
|
1087
1188
|
},
|
|
1088
1189
|
async (args, extra) => {
|
|
1089
1190
|
const controller = new AbortController();
|
|
@@ -36,6 +36,18 @@ export class ExecutorAdmissionError extends Error {
|
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* A lifecycle failure after the sandbox started running. It keeps the stable
|
|
41
|
+
* admission-error envelope while letting response assembly distinguish work
|
|
42
|
+
* torn down in flight from work that never entered the executor.
|
|
43
|
+
*/
|
|
44
|
+
export class ExecutorExecutionError extends ExecutorAdmissionError {
|
|
45
|
+
constructor(code: ExecutorAdmissionErrorCode, message: string) {
|
|
46
|
+
super(code, message);
|
|
47
|
+
this.name = "ExecutorExecutionError";
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
39
51
|
export interface AdmissionLease {
|
|
40
52
|
/** Time spent waiting behind active work. Zero for immediate admission. */
|
|
41
53
|
readonly waitMs: number;
|
package/src/executors/quickjs.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { fileURLToPath } from "node:url";
|
|
|
9
9
|
import {
|
|
10
10
|
AdmissionController,
|
|
11
11
|
ExecutorAdmissionError,
|
|
12
|
+
ExecutorExecutionError,
|
|
12
13
|
} from "../executor-admission.js";
|
|
13
14
|
import type {
|
|
14
15
|
AdmittingExecutor,
|
|
@@ -737,7 +738,7 @@ class QuickJsChildPool implements AdmittingExecutor {
|
|
|
737
738
|
if (!child) return Promise.resolve();
|
|
738
739
|
this.rejectActive(
|
|
739
740
|
slot,
|
|
740
|
-
new
|
|
741
|
+
new ExecutorExecutionError(
|
|
741
742
|
"executor_closed",
|
|
742
743
|
"Executor is shutting down.",
|
|
743
744
|
),
|
package/src/index.ts
CHANGED
|
@@ -18,7 +18,6 @@ import type { ActivityReadGate, ActivityStore } from "./activity.js";
|
|
|
18
18
|
import type {
|
|
19
19
|
Connector,
|
|
20
20
|
ConnectaBranding,
|
|
21
|
-
ConnectaSurface,
|
|
22
21
|
Executor,
|
|
23
22
|
InboundAuth,
|
|
24
23
|
KVStorage,
|
|
@@ -77,10 +76,11 @@ export interface ConnectaDiscoveryConfig {
|
|
|
77
76
|
staleCatalogSeconds?: number;
|
|
78
77
|
/**
|
|
79
78
|
* Deadline (ms) for each downstream probe/catalog call fanned out by
|
|
80
|
-
* `
|
|
81
|
-
* 30_000. A timed-out connector degrades
|
|
82
|
-
* to tool calls. Catalog walks receive the
|
|
83
|
-
* aborts an in-flight page where supported and
|
|
79
|
+
* `search_tools` and by `connecta.search`/`connecta.describe` inside
|
|
80
|
+
* `execute_code`. Defaults to 30_000. A timed-out connector degrades
|
|
81
|
+
* independently; this does not apply to tool calls. Catalog walks receive the
|
|
82
|
+
* same cancellation signal, which aborts an in-flight page where supported and
|
|
83
|
+
* prevents another from starting.
|
|
84
84
|
*/
|
|
85
85
|
probeTimeoutMs?: number;
|
|
86
86
|
}
|
|
@@ -88,9 +88,9 @@ export interface ConnectaDiscoveryConfig {
|
|
|
88
88
|
/** Deployment-wide call deadlines and inline-result paging thresholds. */
|
|
89
89
|
export interface ConnectaCallsConfig {
|
|
90
90
|
/**
|
|
91
|
-
* Deadline (ms) for `call_tool`/`
|
|
92
|
-
* An explicit per-call value wins. Opt-in: unset by default, so
|
|
93
|
-
* long-running calls gain no surprise deadline.
|
|
91
|
+
* Deadline (ms) for `call_tool`/`call_destructive_tool` calls that pass no
|
|
92
|
+
* `timeoutMs`. An explicit per-call value wins. Opt-in: unset by default, so
|
|
93
|
+
* existing long-running calls gain no surprise deadline.
|
|
94
94
|
*
|
|
95
95
|
* This bounds one attempt, not all retries. `execute_code` host calls are
|
|
96
96
|
* unaffected because they already carry their own bound.
|
|
@@ -102,13 +102,6 @@ export interface ConnectaCallsConfig {
|
|
|
102
102
|
* 50_000. Connectors may override it individually.
|
|
103
103
|
*/
|
|
104
104
|
maxResultBytes?: number;
|
|
105
|
-
/**
|
|
106
|
-
* Max serialized `batch_call` envelope size (bytes) before the full batch is
|
|
107
|
-
* stashed for `get_result` and only an ordered outcome summary is returned
|
|
108
|
-
* inline. Must be a finite whole number >= 1; invalid values warn and fall
|
|
109
|
-
* back to 100_000. This cap is independent of per-connector child caps.
|
|
110
|
-
*/
|
|
111
|
-
maxBatchResultBytes?: number;
|
|
112
105
|
}
|
|
113
106
|
|
|
114
107
|
/** Budgets for rich output emitted by execute_code programs (`connecta.emit`). */
|
|
@@ -197,22 +190,12 @@ export interface ConnectaConfig {
|
|
|
197
190
|
/** Deployment metadata exposed by /health (for example a Worker version). */
|
|
198
191
|
deploymentInfo?: Record<string, unknown>;
|
|
199
192
|
/**
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
* "@zackbart/connecta/quickjs".
|
|
193
|
+
* Required sandbox for `execute_code`. Workers use
|
|
194
|
+
* `new DynamicWorkerExecutor({ loader: env.LOADER })` from
|
|
195
|
+
* `@cloudflare/codemode`; Node uses `quickJsExecutor()` from
|
|
196
|
+
* `@zackbart/connecta/quickjs`.
|
|
205
197
|
*/
|
|
206
|
-
executor
|
|
207
|
-
/**
|
|
208
|
-
* Override the surface the `executor` implies. The only reason to set it is
|
|
209
|
-
* `"classic"` alongside an executor — ten tools, the shape the eval gate's
|
|
210
|
-
* *incremental* arm measures ("does adding `execute_code` to classic help on
|
|
211
|
-
* its own?"). The gate's control arm is executor-free classic, which needs no
|
|
212
|
-
* override. `"code-first"` is the default wherever an executor exists and
|
|
213
|
-
* throws without one.
|
|
214
|
-
*/
|
|
215
|
-
surface?: ConnectaSurface;
|
|
198
|
+
executor: Executor;
|
|
216
199
|
}
|
|
217
200
|
|
|
218
201
|
export interface Connecta {
|
|
@@ -306,6 +289,20 @@ function assertNoLegacyConfig(config: ConnectaConfig): void {
|
|
|
306
289
|
"issue #179. Credentials now fail at use; see ethos.md.",
|
|
307
290
|
);
|
|
308
291
|
}
|
|
292
|
+
const calls = candidate.calls;
|
|
293
|
+
if (
|
|
294
|
+
typeof calls === "object" &&
|
|
295
|
+
calls !== null &&
|
|
296
|
+
hasOwn(calls, "maxBatchResultBytes")
|
|
297
|
+
) {
|
|
298
|
+
throw new Error(
|
|
299
|
+
"`calls.maxBatchResultBytes` was removed in issue #273 along with " +
|
|
300
|
+
"batch_call. Remove it; a program's batching is bounded by " +
|
|
301
|
+
"execute_code's own limits — connecta.batch's per-run call ceiling " +
|
|
302
|
+
"and the executor result cap — while each call inside it still honours " +
|
|
303
|
+
"calls.maxResultBytes and any per-connector override.",
|
|
304
|
+
);
|
|
305
|
+
}
|
|
309
306
|
const found: Array<readonly [string, string]> = [];
|
|
310
307
|
if (hasOwn(candidate, "activity")) {
|
|
311
308
|
const activity = candidate.activity;
|
|
@@ -444,43 +441,22 @@ function warnInsecureConfig(
|
|
|
444
441
|
}
|
|
445
442
|
}
|
|
446
443
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
* program to fold discovery and batching into, so it serves classic.
|
|
451
|
-
*
|
|
452
|
-
* Two mistakes are structural rather than recoverable, so neither is warned
|
|
453
|
-
* past: a surface name connecta does not implement, which would otherwise
|
|
454
|
-
* resolve to something the operator did not ask for; and `code-first` without
|
|
455
|
-
* an executor, which would advertise six tools and no program surface.
|
|
456
|
-
*/
|
|
457
|
-
function resolveSurface(config: ConnectaConfig): ConnectaSurface {
|
|
458
|
-
const surface = config.surface;
|
|
459
|
-
if (surface === undefined) {
|
|
460
|
-
return config.executor ? "code-first" : "classic";
|
|
461
|
-
}
|
|
462
|
-
if (surface !== "classic" && surface !== "code-first") {
|
|
444
|
+
export function createConnecta(config: ConnectaConfig): Connecta {
|
|
445
|
+
assertNoLegacyConfig(config);
|
|
446
|
+
if (Object.prototype.hasOwnProperty.call(config, "surface")) {
|
|
463
447
|
throw new Error(
|
|
464
|
-
|
|
465
|
-
|
|
448
|
+
"ConnectaConfig.surface was removed in issue #273. Remove it; connecta " +
|
|
449
|
+
"now serves one seven-tool surface.",
|
|
466
450
|
);
|
|
467
451
|
}
|
|
468
|
-
if (
|
|
452
|
+
if (!config.executor) {
|
|
469
453
|
throw new Error(
|
|
470
|
-
|
|
471
|
-
"
|
|
472
|
-
"
|
|
473
|
-
"
|
|
474
|
-
"one (quickJsExecutor() from \"@zackbart/connecta/quickjs\" on Node, " +
|
|
475
|
-
"new DynamicWorkerExecutor({ loader: env.LOADER }) on Workers).",
|
|
454
|
+
"ConnectaConfig.executor is required. Configure quickJsExecutor() from " +
|
|
455
|
+
'"@zackbart/connecta/quickjs" on Node, or ' +
|
|
456
|
+
"new DynamicWorkerExecutor({ loader: env.LOADER }) from " +
|
|
457
|
+
'"@cloudflare/codemode" on Workers.',
|
|
476
458
|
);
|
|
477
459
|
}
|
|
478
|
-
return surface;
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
export function createConnecta(config: ConnectaConfig): Connecta {
|
|
482
|
-
assertNoLegacyConfig(config);
|
|
483
|
-
const surface = resolveSurface(config);
|
|
484
460
|
const storage = config.storage ?? memoryStorage();
|
|
485
461
|
const logger = config.logger ?? defaultLogger();
|
|
486
462
|
const credentialConnectors = config.connectors.filter((c) => c.credential);
|
|
@@ -524,9 +500,6 @@ export function createConnecta(config: ConnectaConfig): Connecta {
|
|
|
524
500
|
...(config.calls?.maxResultBytes !== undefined
|
|
525
501
|
? { maxResultBytes: config.calls.maxResultBytes }
|
|
526
502
|
: {}),
|
|
527
|
-
...(config.calls?.maxBatchResultBytes !== undefined
|
|
528
|
-
? { maxBatchResultBytes: config.calls.maxBatchResultBytes }
|
|
529
|
-
: {}),
|
|
530
503
|
});
|
|
531
504
|
const inboundAuth = normalizeAuth(
|
|
532
505
|
accessTokens ? [accessTokens.auth, ...configuredAuth] : configuredAuth,
|
|
@@ -542,10 +515,10 @@ export function createConnecta(config: ConnectaConfig): Connecta {
|
|
|
542
515
|
);
|
|
543
516
|
let codeAdmission: AdmissionController | undefined;
|
|
544
517
|
let executor = config.executor;
|
|
545
|
-
if (
|
|
518
|
+
if (!isAdmittingExecutor(executor)) {
|
|
546
519
|
codeAdmission = configuredCodeAdmission;
|
|
547
520
|
executor = withExecutorAdmission(executor, codeAdmission);
|
|
548
|
-
} else if (
|
|
521
|
+
} else if (config.admission?.code) {
|
|
549
522
|
logger.warn(
|
|
550
523
|
"[connecta] admission.code is ignored because the configured executor " +
|
|
551
524
|
"implements acquire() and owns its admission pool; configure that " +
|
|
@@ -571,8 +544,7 @@ export function createConnecta(config: ConnectaConfig): Connecta {
|
|
|
571
544
|
...(config.activity?.deploymentId !== undefined
|
|
572
545
|
? { activityDeploymentId: config.activity.deploymentId }
|
|
573
546
|
: {}),
|
|
574
|
-
|
|
575
|
-
surface,
|
|
547
|
+
executor,
|
|
576
548
|
requestAdmission,
|
|
577
549
|
...(config.calls?.defaultTimeoutMs !== undefined
|
|
578
550
|
? { defaultToolTimeoutMs: config.calls.defaultTimeoutMs }
|
|
@@ -653,7 +625,6 @@ export type {
|
|
|
653
625
|
ConnectorCallAdmissionRule,
|
|
654
626
|
ConnectorRollingWindowBudget,
|
|
655
627
|
ConnectaBranding,
|
|
656
|
-
ConnectaSurface,
|
|
657
628
|
ConnectorCredentialAccess,
|
|
658
629
|
ConnectorCredentialConfig,
|
|
659
630
|
ConnectorCredentialFieldConfig,
|
package/src/invocation.ts
CHANGED
|
@@ -395,8 +395,8 @@ export class InvocationService {
|
|
|
395
395
|
// A connector whose catalog cannot be fetched is as unusable as one
|
|
396
396
|
// whose execution fails, so it feeds health accounting the same way the
|
|
397
397
|
// attempt catch below does — otherwise a connector every call fails
|
|
398
|
-
// against (a revoked downstream grant, say) still reads clean
|
|
399
|
-
//
|
|
398
|
+
// against (a revoked downstream grant, say) still reads clean in the
|
|
399
|
+
// deployment's health log.
|
|
400
400
|
//
|
|
401
401
|
// Recorded HERE rather than inside the registry's catalog fetch because
|
|
402
402
|
// a cache hit that avoids a live listTools call records nothing — it is
|