@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.
Files changed (97) hide show
  1. package/AGENTS.md +8 -6
  2. package/CHANGELOG.md +80 -0
  3. package/README.md +5 -4
  4. package/bin/connecta.mjs +0 -7
  5. package/dist/activity.d.ts.map +1 -1
  6. package/dist/activity.js.map +1 -1
  7. package/dist/apps-shell.d.ts +38 -0
  8. package/dist/apps-shell.d.ts.map +1 -0
  9. package/dist/apps-shell.js +175 -0
  10. package/dist/apps-shell.js.map +1 -0
  11. package/dist/catalog-service.d.ts +2 -17
  12. package/dist/catalog-service.d.ts.map +1 -1
  13. package/dist/catalog-service.js +4 -6
  14. package/dist/catalog-service.js.map +1 -1
  15. package/dist/connectors/api.d.ts +2 -2
  16. package/dist/connectors/remote-mcp.d.ts +1 -1
  17. package/dist/errors.d.ts +1 -3
  18. package/dist/errors.d.ts.map +1 -1
  19. package/dist/errors.js +1 -1
  20. package/dist/errors.js.map +1 -1
  21. package/dist/execute.d.ts +37 -8
  22. package/dist/execute.d.ts.map +1 -1
  23. package/dist/execute.js +137 -42
  24. package/dist/execute.js.map +1 -1
  25. package/dist/executor-admission.d.ts +8 -0
  26. package/dist/executor-admission.d.ts.map +1 -1
  27. package/dist/executor-admission.js +11 -0
  28. package/dist/executor-admission.js.map +1 -1
  29. package/dist/executors/quickjs.d.ts.map +1 -1
  30. package/dist/executors/quickjs.js +2 -2
  31. package/dist/executors/quickjs.js.map +1 -1
  32. package/dist/index.d.ts +15 -31
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +23 -37
  35. package/dist/index.js.map +1 -1
  36. package/dist/invocation.js +2 -2
  37. package/dist/invocation.js.map +1 -1
  38. package/dist/meta-tools.d.ts +19 -58
  39. package/dist/meta-tools.d.ts.map +1 -1
  40. package/dist/meta-tools.js +34 -431
  41. package/dist/meta-tools.js.map +1 -1
  42. package/dist/registry.d.ts +1 -10
  43. package/dist/registry.d.ts.map +1 -1
  44. package/dist/registry.js +3 -15
  45. package/dist/registry.js.map +1 -1
  46. package/dist/routes/mcp.d.ts.map +1 -1
  47. package/dist/routes/mcp.js +67 -30
  48. package/dist/routes/mcp.js.map +1 -1
  49. package/dist/routes/shared.d.ts +5 -11
  50. package/dist/routes/shared.d.ts.map +1 -1
  51. package/dist/routes/shared.js.map +1 -1
  52. package/dist/server.js +5 -4
  53. package/dist/server.js.map +1 -1
  54. package/dist/skills.d.ts +8 -18
  55. package/dist/skills.d.ts.map +1 -1
  56. package/dist/skills.js +13 -60
  57. package/dist/skills.js.map +1 -1
  58. package/dist/types.d.ts +6 -20
  59. package/dist/types.d.ts.map +1 -1
  60. package/dist/version.d.ts +1 -1
  61. package/dist/version.js +1 -1
  62. package/documentation/code-first-exploration.md +16 -16
  63. package/documentation/code-mode.md +130 -41
  64. package/documentation/connectors.md +1 -1
  65. package/documentation/mcp-2026-07-28.md +1 -1
  66. package/documentation/mcp-ui-design.md +382 -0
  67. package/documentation/meta-tools.md +30 -43
  68. package/documentation/rich-output-design.md +4 -4
  69. package/ethos.md +7 -2
  70. package/examples/node/README.md +1 -2
  71. package/examples/node/src/index.ts +1 -3
  72. package/examples/worker/README.md +8 -13
  73. package/examples/worker/src/index.ts +6 -14
  74. package/examples/worker/wrangler.jsonc +3 -6
  75. package/package.json +1 -1
  76. package/src/activity.ts +5 -0
  77. package/src/apps-shell.ts +179 -0
  78. package/src/catalog-service.ts +6 -26
  79. package/src/connectors/api.ts +2 -2
  80. package/src/connectors/remote-mcp.ts +1 -1
  81. package/src/errors.ts +2 -2
  82. package/src/execute.ts +150 -49
  83. package/src/executor-admission.ts +12 -0
  84. package/src/executors/quickjs.ts +2 -1
  85. package/src/index.ts +40 -69
  86. package/src/invocation.ts +2 -2
  87. package/src/meta-tools.ts +38 -565
  88. package/src/registry.ts +2 -33
  89. package/src/routes/mcp.ts +79 -30
  90. package/src/routes/shared.ts +4 -11
  91. package/src/server.ts +7 -7
  92. package/src/skills.ts +11 -74
  93. package/src/types.ts +6 -21
  94. package/src/version.ts +1 -1
  95. package/templates/node/README.md +2 -1
  96. package/templates/node/package.json +1 -1
  97. 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
- * Request-local collection for `connecta.emit`. Budgets fail loudly at the
237
- * crossing call the block is not partially accepted and prior blocks are
238
- * unaffected so a program learns it is over budget while it can still
239
- * choose differently (M5). Accepted blocks never ride `ExecuteResult`; the
240
- * handler that owns this collector appends them to the final tool result.
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.diagnostics?.recordEmitted(this.blocks.length, this.bytes);
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
- // Every describe reaching this catalog came from inside a program, on a
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
- // Same failure shape batch_call reports: the message a program
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 and batch_call. Failures caught by model code never reach
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
- /** M4: a failed program delivers no blocks, but the discard is visible. */
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 emitted.blocks.length > 0
965
- ? { emittedDiscarded: emitted.blocks.length }
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
- return emitted.blocks.length > 0
972
- ? `\n\nemittedDiscarded: ${emitted.blocks.length}`
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
- ) => `${EXECUTE_ROUTING[surface]} 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.
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 ${EXECUTE_SCHEMA_SOURCE[surface]}. 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.
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(ctx.surface ?? "classic", emitBudgets),
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;
@@ -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 ExecutorAdmissionError(
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
- * `list_connectors`, `search_tools`, and `describe_tools`. Defaults to
81
- * 30_000. A timed-out connector degrades independently; this does not apply
82
- * to tool calls. Catalog walks receive the same cancellation signal, which
83
- * aborts an in-flight page where supported and prevents another from starting.
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`/`batch_call` calls that pass no `timeoutMs`.
92
- * An explicit per-call value wins. Opt-in: unset by default, so existing
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
- * Sandbox for `execute_code`, and the switch that decides the surface: with
201
- * an executor a model sees the seven code-first tools, without one the nine
202
- * classic ones. Workers: `new DynamicWorkerExecutor({ loader: env.LOADER })`
203
- * from `@cloudflare/codemode`. Node: `quickJsExecutor()` from
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?: 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
- * The advertised surface: the executor is the switch. Configure one and the
449
- * deployment serves the seven-tool code-first surface; omit it and there is no
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
- `ConnectaConfig.surface must be "classic" or "code-first", not ` +
465
- `${JSON.stringify(surface)}.`,
448
+ "ConnectaConfig.surface was removed in issue #273. Remove it; connecta " +
449
+ "now serves one seven-tool surface.",
466
450
  );
467
451
  }
468
- if (surface === "code-first" && !config.executor) {
452
+ if (!config.executor) {
469
453
  throw new Error(
470
- 'ConnectaConfig.surface "code-first" requires an executor: it folds ' +
471
- "list_connectors, describe_tools, and batch_call into connecta.search, " +
472
- "connecta.describe, and connecta.batch inside execute_code, so without " +
473
- "an executor there is nothing left to reach them through. Configure " +
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 (executor && !isAdmittingExecutor(executor)) {
518
+ if (!isAdmittingExecutor(executor)) {
546
519
  codeAdmission = configuredCodeAdmission;
547
520
  executor = withExecutorAdmission(executor, codeAdmission);
548
- } else if (executor && config.admission?.code) {
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
- ...(executor !== undefined ? { executor } : {}),
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 from the
399
- // cheap `list_connectors({ probe: false })` signal.
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