@zackbart/connecta 0.10.5 → 0.11.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 (93) hide show
  1. package/AGENTS.md +8 -6
  2. package/CHANGELOG.md +150 -0
  3. package/README.md +5 -4
  4. package/bin/connecta.mjs +0 -7
  5. package/dist/activity.d.ts +11 -1
  6. package/dist/activity.d.ts.map +1 -1
  7. package/dist/activity.js +44 -3
  8. package/dist/activity.js.map +1 -1
  9. package/dist/catalog-service.d.ts +24 -0
  10. package/dist/catalog-service.d.ts.map +1 -1
  11. package/dist/catalog-service.js +68 -9
  12. package/dist/catalog-service.js.map +1 -1
  13. package/dist/connectors/api.d.ts +2 -2
  14. package/dist/connectors/remote-mcp.d.ts +1 -1
  15. package/dist/errors.d.ts +49 -4
  16. package/dist/errors.d.ts.map +1 -1
  17. package/dist/errors.js +68 -1
  18. package/dist/errors.js.map +1 -1
  19. package/dist/execute.d.ts +73 -3
  20. package/dist/execute.d.ts.map +1 -1
  21. package/dist/execute.js +161 -29
  22. package/dist/execute.js.map +1 -1
  23. package/dist/index.d.ts +28 -30
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +29 -37
  26. package/dist/index.js.map +1 -1
  27. package/dist/invocation.d.ts +9 -2
  28. package/dist/invocation.d.ts.map +1 -1
  29. package/dist/invocation.js +61 -31
  30. package/dist/invocation.js.map +1 -1
  31. package/dist/meta-tools.d.ts +24 -59
  32. package/dist/meta-tools.d.ts.map +1 -1
  33. package/dist/meta-tools.js +107 -359
  34. package/dist/meta-tools.js.map +1 -1
  35. package/dist/operator-ui/generated.d.ts +1 -1
  36. package/dist/operator-ui/generated.d.ts.map +1 -1
  37. package/dist/operator-ui/generated.js +1 -1
  38. package/dist/operator-ui/generated.js.map +1 -1
  39. package/dist/registry.d.ts +12 -10
  40. package/dist/registry.d.ts.map +1 -1
  41. package/dist/registry.js +8 -17
  42. package/dist/registry.js.map +1 -1
  43. package/dist/routes/mcp.d.ts.map +1 -1
  44. package/dist/routes/mcp.js +19 -21
  45. package/dist/routes/mcp.js.map +1 -1
  46. package/dist/routes/shared.d.ts +9 -11
  47. package/dist/routes/shared.d.ts.map +1 -1
  48. package/dist/routes/shared.js.map +1 -1
  49. package/dist/server.js +5 -4
  50. package/dist/server.js.map +1 -1
  51. package/dist/skills.d.ts +8 -18
  52. package/dist/skills.d.ts.map +1 -1
  53. package/dist/skills.js +13 -60
  54. package/dist/skills.js.map +1 -1
  55. package/dist/types.d.ts +6 -20
  56. package/dist/types.d.ts.map +1 -1
  57. package/dist/version.d.ts +1 -1
  58. package/dist/version.js +1 -1
  59. package/documentation/code-first-exploration.md +16 -16
  60. package/documentation/code-mode.md +137 -63
  61. package/documentation/connectors.md +1 -1
  62. package/documentation/meta-tools.md +96 -33
  63. package/documentation/rich-output-design.md +212 -0
  64. package/ethos.md +17 -19
  65. package/examples/node/README.md +1 -2
  66. package/examples/node/src/index.ts +1 -3
  67. package/examples/worker/README.md +19 -16
  68. package/examples/worker/src/d1-activity-row.ts +40 -0
  69. package/examples/worker/src/d1-activity.ts +3 -2
  70. package/examples/worker/src/index.ts +6 -14
  71. package/examples/worker/wrangler.jsonc +3 -6
  72. package/package.json +1 -1
  73. package/src/activity.ts +69 -3
  74. package/src/catalog-service.ts +113 -20
  75. package/src/connectors/api.ts +2 -2
  76. package/src/connectors/remote-mcp.ts +1 -1
  77. package/src/errors.ts +104 -3
  78. package/src/execute.ts +237 -37
  79. package/src/index.ts +60 -67
  80. package/src/invocation.ts +61 -19
  81. package/src/meta-tools.ts +136 -482
  82. package/src/operator-ui/browser.ts +10 -2
  83. package/src/operator-ui/generated.ts +1 -1
  84. package/src/registry.ts +7 -35
  85. package/src/routes/mcp.ts +19 -21
  86. package/src/routes/shared.ts +8 -11
  87. package/src/server.ts +7 -7
  88. package/src/skills.ts +11 -74
  89. package/src/types.ts +6 -21
  90. package/src/version.ts +1 -1
  91. package/templates/node/README.md +2 -1
  92. package/templates/node/package.json +1 -1
  93. package/templates/node/src/index.ts +1 -1
package/src/execute.ts CHANGED
@@ -24,7 +24,6 @@ import {
24
24
  } from "./invocation.js";
25
25
  import type { RegistryView } from "./registry.js";
26
26
  import type {
27
- ConnectaSurface,
28
27
  Executor,
29
28
  ExecutorProvider,
30
29
  Logger,
@@ -34,6 +33,15 @@ import type {
34
33
  const EXECUTE_MAX_HOST_CALLS = 20;
35
34
  export const EXECUTE_MAX_BATCH_CALLS = 10;
36
35
  const EXECUTE_HOST_CALL_TIMEOUT_MS = 15_000;
36
+ /**
37
+ * Default budgets for `connecta.emit`. The byte budget is a transport bound,
38
+ * not a context bound — emitted image/audio blocks reach the model as media,
39
+ * not base64 text — so it sits far above the 24k return-value guard: room for
40
+ * two or three real screenshots after base64's 4/3 inflation, well short of a
41
+ * file-hosting ambition (design record M5).
42
+ */
43
+ export const EXECUTE_MAX_EMITTED_BYTES = 4_000_000;
44
+ export const EXECUTE_MAX_EMITTED_BLOCKS = 32;
37
45
  const diagnosticsEncoder = new TextEncoder();
38
46
 
39
47
  type ExecuteDiagnosticOperation = "search" | "describe" | "call" | "batch";
@@ -60,6 +68,13 @@ class ExecuteDiagnostics {
60
68
  admissionMs = 0;
61
69
  setupMs = 0;
62
70
  executorWallMs = 0;
71
+ private emitted?: { count: number; bytes: number };
72
+
73
+ /** Numbers only, per R8 — and only once something was emitted, so a
74
+ * non-emitting run's diagnostics stay byte-for-byte what they were. */
75
+ recordEmitted(count: number, bytes: number): void {
76
+ if (count > 0) this.emitted = { count, bytes };
77
+ }
63
78
 
64
79
  private stats(operation: ExecuteDiagnosticOperation) {
65
80
  let stats = this.operations.get(operation);
@@ -142,6 +157,7 @@ class ExecuteDiagnostics {
142
157
  connectorMs: number;
143
158
  };
144
159
  operations: ExecuteOperationDiagnostics[];
160
+ emitted?: { count: number; bytes: number };
145
161
  } {
146
162
  const operations = [...this.operations.values()];
147
163
  return {
@@ -157,10 +173,109 @@ class ExecuteDiagnostics {
157
173
  ),
158
174
  },
159
175
  operations,
176
+ ...(this.emitted ? { emitted: this.emitted } : {}),
160
177
  };
161
178
  }
162
179
  }
163
180
 
181
+ /**
182
+ * One MCP content block a program may emit. The complete set, by design:
183
+ * `resource` and `resource_link` are refused in ethos.md — pointers get
184
+ * followed, and connecta serves no resources for them to point at.
185
+ */
186
+ export type EmittedBlock =
187
+ | { type: "text"; text: string }
188
+ | { type: "image"; data: string; mimeType: string }
189
+ | { type: "audio"; data: string; mimeType: string };
190
+
191
+ const EMIT_SHAPE_HINT =
192
+ '{ type: "text", text } or { type: "image" | "audio", data (base64), mimeType }';
193
+
194
+ /**
195
+ * Strict M1 validation: required fields present and string-valued, nothing
196
+ * else — no `annotations`, no `_meta`, no sugar forms. Rejected rather than
197
+ * stripped, because silently deleting fields would deliver something the
198
+ * program did not ask to emit.
199
+ */
200
+ function requireEmittedBlock(raw: unknown): EmittedBlock {
201
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
202
+ throw new Error(
203
+ `connecta.emit accepts exactly one content block: ${EMIT_SHAPE_HINT}`,
204
+ );
205
+ }
206
+ const block = raw as Record<string, unknown>;
207
+ const fields =
208
+ block.type === "text"
209
+ ? ["type", "text"]
210
+ : block.type === "image" || block.type === "audio"
211
+ ? ["type", "data", "mimeType"]
212
+ : undefined;
213
+ if (!fields) {
214
+ throw new Error(
215
+ `connecta.emit supports content types "text", "image", and "audio"; got ${JSON.stringify(block.type)}`,
216
+ );
217
+ }
218
+ for (const field of fields) {
219
+ if (typeof block[field] !== "string") {
220
+ throw new Error(
221
+ `connecta.emit block field "${field}" must be a string: ${EMIT_SHAPE_HINT}`,
222
+ );
223
+ }
224
+ }
225
+ const extra = Object.keys(block).filter((key) => !fields.includes(key));
226
+ if (extra.length > 0) {
227
+ throw new Error(
228
+ `connecta.emit block carries unsupported field(s) ${extra.map((key) => JSON.stringify(key)).join(", ")}; a "${String(block.type)}" block is exactly { ${fields.join(", ")} }`,
229
+ );
230
+ }
231
+ return raw as EmittedBlock;
232
+ }
233
+
234
+ /**
235
+ * Request-local collection for `connecta.emit`. Budgets fail loudly at the
236
+ * crossing call — the block is not partially accepted and prior blocks are
237
+ * unaffected — so a program learns it is over budget while it can still
238
+ * choose differently (M5). Accepted blocks never ride `ExecuteResult`; the
239
+ * handler that owns this collector appends them to the final tool result.
240
+ */
241
+ export class EmitCollector {
242
+ readonly blocks: EmittedBlock[] = [];
243
+ bytes = 0;
244
+ constructor(
245
+ private readonly maxBytes: number,
246
+ private readonly maxBlocks: number,
247
+ private readonly diagnostics?: ExecuteDiagnostics,
248
+ ) {}
249
+
250
+ accept(raw: unknown): void {
251
+ const block = requireEmittedBlock(raw);
252
+ if (this.blocks.length >= this.maxBlocks) {
253
+ throw new Error(
254
+ `connecta.emit block-count budget exceeded: ${this.maxBlocks} block(s) maximum, 0 remaining`,
255
+ );
256
+ }
257
+ const size = diagnosticsEncoder.encode(JSON.stringify(block)).byteLength;
258
+ if (this.bytes + size > this.maxBytes) {
259
+ throw new Error(
260
+ `connecta.emit byte budget exceeded: block is ${size} serialized bytes with ${this.maxBytes - this.bytes} of ${this.maxBytes} remaining`,
261
+ );
262
+ }
263
+ this.blocks.push(block);
264
+ this.bytes += size;
265
+ this.diagnostics?.recordEmitted(this.blocks.length, this.bytes);
266
+ }
267
+ }
268
+
269
+ /** A configured emit budget must be a finite number >= 1; anything else falls back. */
270
+ function resolveEmitBudget(
271
+ value: number | undefined,
272
+ fallback: number,
273
+ ): number {
274
+ return typeof value === "number" && Number.isFinite(value) && value >= 1
275
+ ? Math.trunc(value)
276
+ : fallback;
277
+ }
278
+
164
279
  function serializedDiagnosticBytes(value: unknown): number {
165
280
  try {
166
281
  const text = JSON.stringify(value);
@@ -292,8 +407,16 @@ export async function buildSandboxProviders(
292
407
  maxHostCalls?: number;
293
408
  hostCallTimeoutMs?: number;
294
409
  discoveryConcurrency?: number;
410
+ /** Per-connector deadline for in-program catalog probes. Default 30_000. */
411
+ probeTimeoutMs?: number;
295
412
  onInvocationFailure?: (failure: InvocationFailure) => void;
296
413
  diagnostics?: ExecuteDiagnostics;
414
+ /**
415
+ * Where `connecta.emit` collects. The handler that will deliver the
416
+ * blocks owns it; without one, emit fails loudly rather than accept
417
+ * blocks nobody will ever return.
418
+ */
419
+ emitCollector?: EmitCollector;
297
420
  } = {},
298
421
  ): Promise<ExecutorProvider[]> {
299
422
  // All host calls made by one execute_code invocation share a downstream
@@ -301,9 +424,14 @@ export async function buildSandboxProviders(
301
424
  const requestScope = {};
302
425
  const catalog = new CatalogService(registry, baseUrl, {
303
426
  requestScope,
427
+ // A program that just missed an address cannot call search_tools.
428
+ searchRoute: "connecta.search",
304
429
  ...(limits.discoveryConcurrency !== undefined
305
430
  ? { concurrency: limits.discoveryConcurrency }
306
431
  : {}),
432
+ ...(limits.probeTimeoutMs !== undefined
433
+ ? { probeTimeoutMs: limits.probeTimeoutMs }
434
+ : {}),
307
435
  });
308
436
  const invocation = new InvocationService(registry, catalog, activity);
309
437
  const maxHostCalls = Math.max(
@@ -418,6 +546,18 @@ export async function buildSandboxProviders(
418
546
  __callNamespace: callNamespace,
419
547
  call: (address: unknown, args: unknown) =>
420
548
  callAddress(address, args),
549
+ // Emission is a provider function, never an ExecuteResult field —
550
+ // that is what keeps the Executor contract untouched and parity
551
+ // structural (M8). It spends no host-call budget (M7); its own
552
+ // budgets live in the collector.
553
+ emit: async (block: unknown) => {
554
+ if (!limits.emitCollector) {
555
+ throw new Error(
556
+ "connecta.emit is unavailable: no emission collector was configured for this execution",
557
+ );
558
+ }
559
+ limits.emitCollector.accept(block);
560
+ },
421
561
  batch: async (calls: unknown) => {
422
562
  const started = Date.now();
423
563
  const callCount = Array.isArray(calls) ? calls.length : 0;
@@ -442,8 +582,8 @@ export async function buildSandboxProviders(
442
582
  ),
443
583
  };
444
584
  } catch (err) {
445
- // Same failure shape batch_call reports: the message a program
446
- // can log, plus the typed details it must classify by. A
585
+ // The failure shape connecta.batch reports: the message a
586
+ // program can log, plus the typed details it must classify by. A
447
587
  // thrown host error crosses the sandbox bridge as a bare
448
588
  // message string in every executor, so this is the one place a
449
589
  // program can tell a policy refusal from a transient failure.
@@ -566,7 +706,12 @@ export function createExecuteTool(
566
706
  executor: Executor,
567
707
  logger: Logger,
568
708
  activity?: ActivityRequestContext,
569
- config: { discoveryConcurrency?: number } = {},
709
+ config: {
710
+ discoveryConcurrency?: number;
711
+ probeTimeoutMs?: number;
712
+ maxEmittedBytes?: number;
713
+ maxEmittedBlocks?: number;
714
+ } = {},
570
715
  ) {
571
716
  return async (
572
717
  { code, diagnostics: diagnosticsRequested }: {
@@ -584,6 +729,11 @@ export function createExecuteTool(
584
729
  let lease;
585
730
  let outcome;
586
731
  const diagnostics = diagnosticsRequested ? new ExecuteDiagnostics() : undefined;
732
+ const emitted = new EmitCollector(
733
+ resolveEmitBudget(config.maxEmittedBytes, EXECUTE_MAX_EMITTED_BYTES),
734
+ resolveEmitBudget(config.maxEmittedBlocks, EXECUTE_MAX_EMITTED_BLOCKS),
735
+ diagnostics,
736
+ );
587
737
  const invocationFailures: InvocationFailure[] = [];
588
738
  try {
589
739
  // Admission comes before provider construction: queued calls retain no
@@ -616,10 +766,14 @@ export function createExecuteTool(
616
766
  onInvocationFailure: (failure) => {
617
767
  invocationFailures.push(failure);
618
768
  },
769
+ emitCollector: emitted,
619
770
  ...(diagnostics ? { diagnostics } : {}),
620
771
  ...(config.discoveryConcurrency !== undefined
621
772
  ? { discoveryConcurrency: config.discoveryConcurrency }
622
773
  : {}),
774
+ ...(config.probeTimeoutMs !== undefined
775
+ ? { probeTimeoutMs: config.probeTimeoutMs }
776
+ : {}),
623
777
  },
624
778
  );
625
779
  } finally {
@@ -670,12 +824,15 @@ export function createExecuteTool(
670
824
  message: `Executor failed: ${msg(err)}`,
671
825
  retryable: false,
672
826
  },
827
+ ...discardedEmits(emitted),
673
828
  diagnostics: diagnostics.finish(),
674
829
  });
675
830
  result.isError = true;
676
831
  return result;
677
832
  }
678
- return errorResult(`Executor failed: ${msg(err)}`);
833
+ return errorResult(
834
+ `Executor failed: ${msg(err)}${discardedEmitsText(emitted)}`,
835
+ );
679
836
  } finally {
680
837
  // A sandbox timeout or early return must also release any outstanding
681
838
  // host waits and signal cooperative connectors to stop their work.
@@ -694,7 +851,7 @@ export function createExecuteTool(
694
851
  // Executor bridges necessarily reduce thrown host errors to strings.
695
852
  // Match that terminal string back to the request-local typed failure so
696
853
  // an unhandled tool failure keeps the same structured contract as
697
- // call_tool and batch_call. Failures caught by model code never reach
854
+ // call_tool. Failures caught by model code never reach
698
855
  // outcome.error and therefore remain under that code's control.
699
856
  //
700
857
  // An error the program let through unchanged matches exactly, and an
@@ -718,9 +875,14 @@ export function createExecuteTool(
718
875
  if (invocationFailure) break;
719
876
  }
720
877
  if (invocationFailure) {
878
+ // Handed back whole, with no size guard of its own — the failure was
879
+ // framed with bounded caller text (`boundedEchoText`) precisely so
880
+ // this path never needs one. Adding a cap here instead would leave the
881
+ // top-level surfaces, which have the same amplification, uncovered.
721
882
  const result = jsonResult({
722
883
  error: invocationFailure.details,
723
884
  ...(logs ? { logs } : {}),
885
+ ...discardedEmits(emitted),
724
886
  ...(diagnostics ? { diagnostics: diagnostics.finish() } : {}),
725
887
  });
726
888
  result.isError = true;
@@ -735,12 +897,15 @@ export function createExecuteTool(
735
897
  retryable: false,
736
898
  },
737
899
  ...(logs ? { logs } : {}),
900
+ ...discardedEmits(emitted),
738
901
  diagnostics: diagnostics.finish(),
739
902
  });
740
903
  result.isError = true;
741
904
  return result;
742
905
  }
743
- return errorResult(`${message}${logs ? `\n\nLogs:\n${logs}` : ""}`);
906
+ return errorResult(
907
+ `${message}${logs ? `\n\nLogs:\n${logs}` : ""}${discardedEmitsText(emitted)}`,
908
+ );
744
909
  }
745
910
  // A result crossing back as a host BigInt (or otherwise unserializable
746
911
  // value) makes JSON.stringify throw — keep that inside the structured
@@ -758,49 +923,59 @@ export function createExecuteTool(
758
923
  retryable: false,
759
924
  },
760
925
  ...(logs ? { logs } : {}),
926
+ ...discardedEmits(emitted),
761
927
  diagnostics: diagnostics.finish(),
762
928
  });
763
929
  response.isError = true;
764
930
  return response;
765
931
  }
766
- return errorResult(`${message}${logs ? `\n\nLogs:\n${logs}` : ""}`);
932
+ return errorResult(
933
+ `${message}${logs ? `\n\nLogs:\n${logs}` : ""}${discardedEmitsText(emitted)}`,
934
+ );
767
935
  }
768
- return jsonResult({
936
+ const response = jsonResult({
769
937
  result,
938
+ ...(emitted.blocks.length > 0 ? { emitted: emitted.blocks.length } : {}),
770
939
  ...(logs ? { logs } : {}),
771
940
  ...(diagnostics ? { diagnostics: diagnostics.finish() } : {}),
772
941
  });
942
+ if (emitted.blocks.length > 0) {
943
+ // Emitted image/audio blocks are valid MCP content that ToolResult's
944
+ // text-only typing does not model — the same acknowledged gap
945
+ // guardContent lives with for downstream block passthrough.
946
+ response.content.push(
947
+ ...(emitted.blocks as unknown as typeof response.content),
948
+ );
949
+ }
950
+ return response;
773
951
  };
774
952
  }
775
953
 
776
- /**
777
- * How the tool opens, and where a program's argument schemas come from. Both
778
- * differ by surface: on the classic surface `execute_code` is the tool of last
779
- * resort and its neighbours (`batch_call`, `describe_tools`) own the simpler
780
- * jobs, while on the code-first surface those tools are gone and the program is
781
- * where all of that work happens. Everything after these two phrases is
782
- * identical, so the shared body below has one source of truth.
783
- */
784
- const EXECUTE_ROUTING = {
785
- classic:
786
- "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.",
787
- "code-first":
788
- "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.",
789
- } as const;
790
-
791
- const EXECUTE_SCHEMA_SOURCE = {
792
- classic: "describe_tools",
793
- "code-first": "connecta.describe",
794
- } as const;
954
+ /** M4: a failed program delivers no blocks, but the discard is visible. */
955
+ function discardedEmits(emitted: EmitCollector): {
956
+ emittedDiscarded?: number;
957
+ } {
958
+ return emitted.blocks.length > 0
959
+ ? { emittedDiscarded: emitted.blocks.length }
960
+ : {};
961
+ }
962
+
963
+ /** The same visibility for the plain-text error paths. */
964
+ function discardedEmitsText(emitted: EmitCollector): string {
965
+ return emitted.blocks.length > 0
966
+ ? `\n\nemittedDiscarded: ${emitted.blocks.length}`
967
+ : "";
968
+ }
795
969
 
796
970
  const executeDescription = (
797
- surface: ConnectaSurface,
798
- ) => `${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.
971
+ emitBudgets: { maxBytes: number; maxBlocks: number },
972
+ ) => `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.
799
973
 
800
974
  Write an async arrow function. It runs with NO network, filesystem, timers, or imports — the only capabilities are:
801
- - 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.
975
+ - 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.
802
976
  - connecta.call(address, args) and connecta.batch(calls) — call raw addresses.
803
977
  - 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.
978
+ - 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.
804
979
  - console.log(...) — captured and returned alongside the result.
805
980
 
806
981
  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.
@@ -819,24 +994,49 @@ export function registerExecuteTool(
819
994
  activity?: ActivityRequestContext;
820
995
  requestSignal?: AbortSignal;
821
996
  discoveryConcurrency?: number;
822
- /** The advertised surface, which decides this tool's routing copy. */
823
- surface?: ConnectaSurface;
997
+ /**
998
+ * The deployment's configured per-connector probe deadline. Programs probe
999
+ * the same downstream catalogs the top-level tools do, so an operator who
1000
+ * tightened `discovery.probeTimeoutMs` gets it honored inside the sandbox
1001
+ * too rather than silently falling back to the 30s default.
1002
+ */
1003
+ probeTimeoutMs?: number;
1004
+ /** Aggregate serialized-byte budget for connecta.emit. Default 4_000_000. */
1005
+ maxEmittedBytes?: number;
1006
+ /** Block-count budget for connecta.emit. Default 32. */
1007
+ maxEmittedBlocks?: number;
824
1008
  },
825
1009
  ): void {
1010
+ // Resolved once so the description and the collector cannot disagree about
1011
+ // the budgets this deployment actually enforces.
1012
+ const emitBudgets = {
1013
+ maxBytes: resolveEmitBudget(ctx.maxEmittedBytes, EXECUTE_MAX_EMITTED_BYTES),
1014
+ maxBlocks: resolveEmitBudget(
1015
+ ctx.maxEmittedBlocks,
1016
+ EXECUTE_MAX_EMITTED_BLOCKS,
1017
+ ),
1018
+ };
826
1019
  const handler = createExecuteTool(
827
1020
  registry,
828
1021
  ctx.baseUrl,
829
1022
  ctx.executor,
830
1023
  ctx.logger,
831
1024
  ctx.activity,
832
- ctx.discoveryConcurrency !== undefined
833
- ? { discoveryConcurrency: ctx.discoveryConcurrency }
834
- : {},
1025
+ {
1026
+ ...(ctx.discoveryConcurrency !== undefined
1027
+ ? { discoveryConcurrency: ctx.discoveryConcurrency }
1028
+ : {}),
1029
+ ...(ctx.probeTimeoutMs !== undefined
1030
+ ? { probeTimeoutMs: ctx.probeTimeoutMs }
1031
+ : {}),
1032
+ maxEmittedBytes: emitBudgets.maxBytes,
1033
+ maxEmittedBlocks: emitBudgets.maxBlocks,
1034
+ },
835
1035
  );
836
1036
  server.registerTool(
837
1037
  "execute_code",
838
1038
  {
839
- description: executeDescription(ctx.surface ?? "classic"),
1039
+ description: executeDescription(emitBudgets),
840
1040
  inputSchema: z.object({
841
1041
  code: z
842
1042
  .string()