@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.
- package/AGENTS.md +8 -6
- package/CHANGELOG.md +150 -0
- package/README.md +5 -4
- package/bin/connecta.mjs +0 -7
- package/dist/activity.d.ts +11 -1
- package/dist/activity.d.ts.map +1 -1
- package/dist/activity.js +44 -3
- package/dist/activity.js.map +1 -1
- package/dist/catalog-service.d.ts +24 -0
- package/dist/catalog-service.d.ts.map +1 -1
- package/dist/catalog-service.js +68 -9
- 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 +49 -4
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +68 -1
- package/dist/errors.js.map +1 -1
- package/dist/execute.d.ts +73 -3
- package/dist/execute.d.ts.map +1 -1
- package/dist/execute.js +161 -29
- package/dist/execute.js.map +1 -1
- package/dist/index.d.ts +28 -30
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +29 -37
- package/dist/index.js.map +1 -1
- package/dist/invocation.d.ts +9 -2
- package/dist/invocation.d.ts.map +1 -1
- package/dist/invocation.js +61 -31
- package/dist/invocation.js.map +1 -1
- package/dist/meta-tools.d.ts +24 -59
- package/dist/meta-tools.d.ts.map +1 -1
- package/dist/meta-tools.js +107 -359
- package/dist/meta-tools.js.map +1 -1
- package/dist/operator-ui/generated.d.ts +1 -1
- package/dist/operator-ui/generated.d.ts.map +1 -1
- package/dist/operator-ui/generated.js +1 -1
- package/dist/operator-ui/generated.js.map +1 -1
- package/dist/registry.d.ts +12 -10
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +8 -17
- package/dist/registry.js.map +1 -1
- package/dist/routes/mcp.d.ts.map +1 -1
- package/dist/routes/mcp.js +19 -21
- package/dist/routes/mcp.js.map +1 -1
- package/dist/routes/shared.d.ts +9 -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 +137 -63
- package/documentation/connectors.md +1 -1
- package/documentation/meta-tools.md +96 -33
- package/documentation/rich-output-design.md +212 -0
- package/ethos.md +17 -19
- package/examples/node/README.md +1 -2
- package/examples/node/src/index.ts +1 -3
- package/examples/worker/README.md +19 -16
- package/examples/worker/src/d1-activity-row.ts +40 -0
- package/examples/worker/src/d1-activity.ts +3 -2
- package/examples/worker/src/index.ts +6 -14
- package/examples/worker/wrangler.jsonc +3 -6
- package/package.json +1 -1
- package/src/activity.ts +69 -3
- package/src/catalog-service.ts +113 -20
- package/src/connectors/api.ts +2 -2
- package/src/connectors/remote-mcp.ts +1 -1
- package/src/errors.ts +104 -3
- package/src/execute.ts +237 -37
- package/src/index.ts +60 -67
- package/src/invocation.ts +61 -19
- package/src/meta-tools.ts +136 -482
- package/src/operator-ui/browser.ts +10 -2
- package/src/operator-ui/generated.ts +1 -1
- package/src/registry.ts +7 -35
- package/src/routes/mcp.ts +19 -21
- package/src/routes/shared.ts +8 -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
|
@@ -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
|
-
//
|
|
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: {
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
932
|
+
return errorResult(
|
|
933
|
+
`${message}${logs ? `\n\nLogs:\n${logs}` : ""}${discardedEmitsText(emitted)}`,
|
|
934
|
+
);
|
|
767
935
|
}
|
|
768
|
-
|
|
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
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
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
|
-
|
|
798
|
-
) =>
|
|
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
|
|
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
|
-
/**
|
|
823
|
-
|
|
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
|
-
|
|
833
|
-
|
|
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(
|
|
1039
|
+
description: executeDescription(emitBudgets),
|
|
840
1040
|
inputSchema: z.object({
|
|
841
1041
|
code: z
|
|
842
1042
|
.string()
|