@zackbart/connecta 0.10.4 → 0.10.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +135 -0
- 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 +40 -0
- package/dist/catalog-service.d.ts.map +1 -1
- package/dist/catalog-service.js +97 -15
- package/dist/catalog-service.js.map +1 -1
- package/dist/errors.d.ts +48 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +67 -0
- package/dist/errors.js.map +1 -1
- package/dist/execute.d.ts +72 -0
- package/dist/execute.d.ts.map +1 -1
- package/dist/execute.js +163 -10
- package/dist/execute.js.map +1 -1
- package/dist/index.d.ts +15 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- 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 +59 -29
- package/dist/invocation.js.map +1 -1
- package/dist/meta-tools.d.ts +12 -3
- package/dist/meta-tools.d.ts.map +1 -1
- package/dist/meta-tools.js +185 -30
- 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 +11 -0
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +5 -2
- package/dist/registry.js.map +1 -1
- package/dist/routes/mcp.d.ts.map +1 -1
- package/dist/routes/mcp.js +9 -0
- package/dist/routes/mcp.js.map +1 -1
- package/dist/routes/shared.d.ts +4 -0
- package/dist/routes/shared.d.ts.map +1 -1
- package/dist/routes/shared.js.map +1 -1
- package/dist/skills.d.ts +1 -1
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/code-mode.md +125 -34
- package/documentation/meta-tools.md +91 -9
- package/documentation/rich-output-design.md +212 -0
- package/ethos.md +17 -19
- package/examples/worker/README.md +11 -3
- package/examples/worker/src/d1-activity-row.ts +40 -0
- package/examples/worker/src/d1-activity.ts +3 -2
- package/package.json +1 -1
- package/src/activity.ts +64 -3
- package/src/catalog-service.ts +166 -26
- package/src/errors.ts +102 -1
- package/src/execute.ts +240 -10
- package/src/index.ts +22 -0
- package/src/invocation.ts +59 -17
- package/src/meta-tools.ts +239 -37
- package/src/operator-ui/browser.ts +10 -2
- package/src/operator-ui/generated.ts +1 -1
- package/src/registry.ts +5 -2
- package/src/routes/mcp.ts +9 -0
- package/src/routes/shared.ts +4 -0
- package/src/skills.ts +1 -1
- package/src/version.ts +1 -1
- package/templates/node/package.json +1 -1
package/src/execute.ts
CHANGED
|
@@ -34,6 +34,15 @@ import type {
|
|
|
34
34
|
const EXECUTE_MAX_HOST_CALLS = 20;
|
|
35
35
|
export const EXECUTE_MAX_BATCH_CALLS = 10;
|
|
36
36
|
const EXECUTE_HOST_CALL_TIMEOUT_MS = 15_000;
|
|
37
|
+
/**
|
|
38
|
+
* Default budgets for `connecta.emit`. The byte budget is a transport bound,
|
|
39
|
+
* not a context bound — emitted image/audio blocks reach the model as media,
|
|
40
|
+
* not base64 text — so it sits far above the 24k return-value guard: room for
|
|
41
|
+
* two or three real screenshots after base64's 4/3 inflation, well short of a
|
|
42
|
+
* file-hosting ambition (design record M5).
|
|
43
|
+
*/
|
|
44
|
+
export const EXECUTE_MAX_EMITTED_BYTES = 4_000_000;
|
|
45
|
+
export const EXECUTE_MAX_EMITTED_BLOCKS = 32;
|
|
37
46
|
const diagnosticsEncoder = new TextEncoder();
|
|
38
47
|
|
|
39
48
|
type ExecuteDiagnosticOperation = "search" | "describe" | "call" | "batch";
|
|
@@ -60,6 +69,13 @@ class ExecuteDiagnostics {
|
|
|
60
69
|
admissionMs = 0;
|
|
61
70
|
setupMs = 0;
|
|
62
71
|
executorWallMs = 0;
|
|
72
|
+
private emitted?: { count: number; bytes: number };
|
|
73
|
+
|
|
74
|
+
/** Numbers only, per R8 — and only once something was emitted, so a
|
|
75
|
+
* non-emitting run's diagnostics stay byte-for-byte what they were. */
|
|
76
|
+
recordEmitted(count: number, bytes: number): void {
|
|
77
|
+
if (count > 0) this.emitted = { count, bytes };
|
|
78
|
+
}
|
|
63
79
|
|
|
64
80
|
private stats(operation: ExecuteDiagnosticOperation) {
|
|
65
81
|
let stats = this.operations.get(operation);
|
|
@@ -142,6 +158,7 @@ class ExecuteDiagnostics {
|
|
|
142
158
|
connectorMs: number;
|
|
143
159
|
};
|
|
144
160
|
operations: ExecuteOperationDiagnostics[];
|
|
161
|
+
emitted?: { count: number; bytes: number };
|
|
145
162
|
} {
|
|
146
163
|
const operations = [...this.operations.values()];
|
|
147
164
|
return {
|
|
@@ -157,10 +174,109 @@ class ExecuteDiagnostics {
|
|
|
157
174
|
),
|
|
158
175
|
},
|
|
159
176
|
operations,
|
|
177
|
+
...(this.emitted ? { emitted: this.emitted } : {}),
|
|
160
178
|
};
|
|
161
179
|
}
|
|
162
180
|
}
|
|
163
181
|
|
|
182
|
+
/**
|
|
183
|
+
* One MCP content block a program may emit. The complete set, by design:
|
|
184
|
+
* `resource` and `resource_link` are refused in ethos.md — pointers get
|
|
185
|
+
* followed, and connecta serves no resources for them to point at.
|
|
186
|
+
*/
|
|
187
|
+
export type EmittedBlock =
|
|
188
|
+
| { type: "text"; text: string }
|
|
189
|
+
| { type: "image"; data: string; mimeType: string }
|
|
190
|
+
| { type: "audio"; data: string; mimeType: string };
|
|
191
|
+
|
|
192
|
+
const EMIT_SHAPE_HINT =
|
|
193
|
+
'{ type: "text", text } or { type: "image" | "audio", data (base64), mimeType }';
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Strict M1 validation: required fields present and string-valued, nothing
|
|
197
|
+
* else — no `annotations`, no `_meta`, no sugar forms. Rejected rather than
|
|
198
|
+
* stripped, because silently deleting fields would deliver something the
|
|
199
|
+
* program did not ask to emit.
|
|
200
|
+
*/
|
|
201
|
+
function requireEmittedBlock(raw: unknown): EmittedBlock {
|
|
202
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
203
|
+
throw new Error(
|
|
204
|
+
`connecta.emit accepts exactly one content block: ${EMIT_SHAPE_HINT}`,
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
const block = raw as Record<string, unknown>;
|
|
208
|
+
const fields =
|
|
209
|
+
block.type === "text"
|
|
210
|
+
? ["type", "text"]
|
|
211
|
+
: block.type === "image" || block.type === "audio"
|
|
212
|
+
? ["type", "data", "mimeType"]
|
|
213
|
+
: undefined;
|
|
214
|
+
if (!fields) {
|
|
215
|
+
throw new Error(
|
|
216
|
+
`connecta.emit supports content types "text", "image", and "audio"; got ${JSON.stringify(block.type)}`,
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
for (const field of fields) {
|
|
220
|
+
if (typeof block[field] !== "string") {
|
|
221
|
+
throw new Error(
|
|
222
|
+
`connecta.emit block field "${field}" must be a string: ${EMIT_SHAPE_HINT}`,
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
const extra = Object.keys(block).filter((key) => !fields.includes(key));
|
|
227
|
+
if (extra.length > 0) {
|
|
228
|
+
throw new Error(
|
|
229
|
+
`connecta.emit block carries unsupported field(s) ${extra.map((key) => JSON.stringify(key)).join(", ")}; a "${String(block.type)}" block is exactly { ${fields.join(", ")} }`,
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
return raw as EmittedBlock;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
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.
|
|
241
|
+
*/
|
|
242
|
+
export class EmitCollector {
|
|
243
|
+
readonly blocks: EmittedBlock[] = [];
|
|
244
|
+
bytes = 0;
|
|
245
|
+
constructor(
|
|
246
|
+
private readonly maxBytes: number,
|
|
247
|
+
private readonly maxBlocks: number,
|
|
248
|
+
private readonly diagnostics?: ExecuteDiagnostics,
|
|
249
|
+
) {}
|
|
250
|
+
|
|
251
|
+
accept(raw: unknown): void {
|
|
252
|
+
const block = requireEmittedBlock(raw);
|
|
253
|
+
if (this.blocks.length >= this.maxBlocks) {
|
|
254
|
+
throw new Error(
|
|
255
|
+
`connecta.emit block-count budget exceeded: ${this.maxBlocks} block(s) maximum, 0 remaining`,
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
const size = diagnosticsEncoder.encode(JSON.stringify(block)).byteLength;
|
|
259
|
+
if (this.bytes + size > this.maxBytes) {
|
|
260
|
+
throw new Error(
|
|
261
|
+
`connecta.emit byte budget exceeded: block is ${size} serialized bytes with ${this.maxBytes - this.bytes} of ${this.maxBytes} remaining`,
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
this.blocks.push(block);
|
|
265
|
+
this.bytes += size;
|
|
266
|
+
this.diagnostics?.recordEmitted(this.blocks.length, this.bytes);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** A configured emit budget must be a finite number >= 1; anything else falls back. */
|
|
271
|
+
function resolveEmitBudget(
|
|
272
|
+
value: number | undefined,
|
|
273
|
+
fallback: number,
|
|
274
|
+
): number {
|
|
275
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 1
|
|
276
|
+
? Math.trunc(value)
|
|
277
|
+
: fallback;
|
|
278
|
+
}
|
|
279
|
+
|
|
164
280
|
function serializedDiagnosticBytes(value: unknown): number {
|
|
165
281
|
try {
|
|
166
282
|
const text = JSON.stringify(value);
|
|
@@ -292,8 +408,16 @@ export async function buildSandboxProviders(
|
|
|
292
408
|
maxHostCalls?: number;
|
|
293
409
|
hostCallTimeoutMs?: number;
|
|
294
410
|
discoveryConcurrency?: number;
|
|
411
|
+
/** Per-connector deadline for in-program catalog probes. Default 30_000. */
|
|
412
|
+
probeTimeoutMs?: number;
|
|
295
413
|
onInvocationFailure?: (failure: InvocationFailure) => void;
|
|
296
414
|
diagnostics?: ExecuteDiagnostics;
|
|
415
|
+
/**
|
|
416
|
+
* Where `connecta.emit` collects. The handler that will deliver the
|
|
417
|
+
* blocks owns it; without one, emit fails loudly rather than accept
|
|
418
|
+
* blocks nobody will ever return.
|
|
419
|
+
*/
|
|
420
|
+
emitCollector?: EmitCollector;
|
|
297
421
|
} = {},
|
|
298
422
|
): Promise<ExecutorProvider[]> {
|
|
299
423
|
// All host calls made by one execute_code invocation share a downstream
|
|
@@ -301,9 +425,19 @@ export async function buildSandboxProviders(
|
|
|
301
425
|
const requestScope = {};
|
|
302
426
|
const catalog = new CatalogService(registry, baseUrl, {
|
|
303
427
|
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.
|
|
434
|
+
searchRoute: "connecta.search",
|
|
304
435
|
...(limits.discoveryConcurrency !== undefined
|
|
305
436
|
? { concurrency: limits.discoveryConcurrency }
|
|
306
437
|
: {}),
|
|
438
|
+
...(limits.probeTimeoutMs !== undefined
|
|
439
|
+
? { probeTimeoutMs: limits.probeTimeoutMs }
|
|
440
|
+
: {}),
|
|
307
441
|
});
|
|
308
442
|
const invocation = new InvocationService(registry, catalog, activity);
|
|
309
443
|
const maxHostCalls = Math.max(
|
|
@@ -418,6 +552,18 @@ export async function buildSandboxProviders(
|
|
|
418
552
|
__callNamespace: callNamespace,
|
|
419
553
|
call: (address: unknown, args: unknown) =>
|
|
420
554
|
callAddress(address, args),
|
|
555
|
+
// Emission is a provider function, never an ExecuteResult field —
|
|
556
|
+
// that is what keeps the Executor contract untouched and parity
|
|
557
|
+
// structural (M8). It spends no host-call budget (M7); its own
|
|
558
|
+
// budgets live in the collector.
|
|
559
|
+
emit: async (block: unknown) => {
|
|
560
|
+
if (!limits.emitCollector) {
|
|
561
|
+
throw new Error(
|
|
562
|
+
"connecta.emit is unavailable: no emission collector was configured for this execution",
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
limits.emitCollector.accept(block);
|
|
566
|
+
},
|
|
421
567
|
batch: async (calls: unknown) => {
|
|
422
568
|
const started = Date.now();
|
|
423
569
|
const callCount = Array.isArray(calls) ? calls.length : 0;
|
|
@@ -526,6 +672,7 @@ export async function buildSandboxProviders(
|
|
|
526
672
|
try {
|
|
527
673
|
const result = await typedDiscovery(async () => {
|
|
528
674
|
const args = (raw ?? {}) as {
|
|
675
|
+
address?: unknown;
|
|
529
676
|
addresses?: unknown;
|
|
530
677
|
format?: "compact" | "json";
|
|
531
678
|
fullDescriptions?: boolean;
|
|
@@ -565,7 +712,12 @@ export function createExecuteTool(
|
|
|
565
712
|
executor: Executor,
|
|
566
713
|
logger: Logger,
|
|
567
714
|
activity?: ActivityRequestContext,
|
|
568
|
-
config: {
|
|
715
|
+
config: {
|
|
716
|
+
discoveryConcurrency?: number;
|
|
717
|
+
probeTimeoutMs?: number;
|
|
718
|
+
maxEmittedBytes?: number;
|
|
719
|
+
maxEmittedBlocks?: number;
|
|
720
|
+
} = {},
|
|
569
721
|
) {
|
|
570
722
|
return async (
|
|
571
723
|
{ code, diagnostics: diagnosticsRequested }: {
|
|
@@ -583,6 +735,11 @@ export function createExecuteTool(
|
|
|
583
735
|
let lease;
|
|
584
736
|
let outcome;
|
|
585
737
|
const diagnostics = diagnosticsRequested ? new ExecuteDiagnostics() : undefined;
|
|
738
|
+
const emitted = new EmitCollector(
|
|
739
|
+
resolveEmitBudget(config.maxEmittedBytes, EXECUTE_MAX_EMITTED_BYTES),
|
|
740
|
+
resolveEmitBudget(config.maxEmittedBlocks, EXECUTE_MAX_EMITTED_BLOCKS),
|
|
741
|
+
diagnostics,
|
|
742
|
+
);
|
|
586
743
|
const invocationFailures: InvocationFailure[] = [];
|
|
587
744
|
try {
|
|
588
745
|
// Admission comes before provider construction: queued calls retain no
|
|
@@ -615,10 +772,14 @@ export function createExecuteTool(
|
|
|
615
772
|
onInvocationFailure: (failure) => {
|
|
616
773
|
invocationFailures.push(failure);
|
|
617
774
|
},
|
|
775
|
+
emitCollector: emitted,
|
|
618
776
|
...(diagnostics ? { diagnostics } : {}),
|
|
619
777
|
...(config.discoveryConcurrency !== undefined
|
|
620
778
|
? { discoveryConcurrency: config.discoveryConcurrency }
|
|
621
779
|
: {}),
|
|
780
|
+
...(config.probeTimeoutMs !== undefined
|
|
781
|
+
? { probeTimeoutMs: config.probeTimeoutMs }
|
|
782
|
+
: {}),
|
|
622
783
|
},
|
|
623
784
|
);
|
|
624
785
|
} finally {
|
|
@@ -669,12 +830,15 @@ export function createExecuteTool(
|
|
|
669
830
|
message: `Executor failed: ${msg(err)}`,
|
|
670
831
|
retryable: false,
|
|
671
832
|
},
|
|
833
|
+
...discardedEmits(emitted),
|
|
672
834
|
diagnostics: diagnostics.finish(),
|
|
673
835
|
});
|
|
674
836
|
result.isError = true;
|
|
675
837
|
return result;
|
|
676
838
|
}
|
|
677
|
-
return errorResult(
|
|
839
|
+
return errorResult(
|
|
840
|
+
`Executor failed: ${msg(err)}${discardedEmitsText(emitted)}`,
|
|
841
|
+
);
|
|
678
842
|
} finally {
|
|
679
843
|
// A sandbox timeout or early return must also release any outstanding
|
|
680
844
|
// host waits and signal cooperative connectors to stop their work.
|
|
@@ -717,9 +881,14 @@ export function createExecuteTool(
|
|
|
717
881
|
if (invocationFailure) break;
|
|
718
882
|
}
|
|
719
883
|
if (invocationFailure) {
|
|
884
|
+
// Handed back whole, with no size guard of its own — the failure was
|
|
885
|
+
// framed with bounded caller text (`boundedEchoText`) precisely so
|
|
886
|
+
// this path never needs one. Adding a cap here instead would leave the
|
|
887
|
+
// top-level surfaces, which have the same amplification, uncovered.
|
|
720
888
|
const result = jsonResult({
|
|
721
889
|
error: invocationFailure.details,
|
|
722
890
|
...(logs ? { logs } : {}),
|
|
891
|
+
...discardedEmits(emitted),
|
|
723
892
|
...(diagnostics ? { diagnostics: diagnostics.finish() } : {}),
|
|
724
893
|
});
|
|
725
894
|
result.isError = true;
|
|
@@ -734,12 +903,15 @@ export function createExecuteTool(
|
|
|
734
903
|
retryable: false,
|
|
735
904
|
},
|
|
736
905
|
...(logs ? { logs } : {}),
|
|
906
|
+
...discardedEmits(emitted),
|
|
737
907
|
diagnostics: diagnostics.finish(),
|
|
738
908
|
});
|
|
739
909
|
result.isError = true;
|
|
740
910
|
return result;
|
|
741
911
|
}
|
|
742
|
-
return errorResult(
|
|
912
|
+
return errorResult(
|
|
913
|
+
`${message}${logs ? `\n\nLogs:\n${logs}` : ""}${discardedEmitsText(emitted)}`,
|
|
914
|
+
);
|
|
743
915
|
}
|
|
744
916
|
// A result crossing back as a host BigInt (or otherwise unserializable
|
|
745
917
|
// value) makes JSON.stringify throw — keep that inside the structured
|
|
@@ -757,21 +929,50 @@ export function createExecuteTool(
|
|
|
757
929
|
retryable: false,
|
|
758
930
|
},
|
|
759
931
|
...(logs ? { logs } : {}),
|
|
932
|
+
...discardedEmits(emitted),
|
|
760
933
|
diagnostics: diagnostics.finish(),
|
|
761
934
|
});
|
|
762
935
|
response.isError = true;
|
|
763
936
|
return response;
|
|
764
937
|
}
|
|
765
|
-
return errorResult(
|
|
938
|
+
return errorResult(
|
|
939
|
+
`${message}${logs ? `\n\nLogs:\n${logs}` : ""}${discardedEmitsText(emitted)}`,
|
|
940
|
+
);
|
|
766
941
|
}
|
|
767
|
-
|
|
942
|
+
const response = jsonResult({
|
|
768
943
|
result,
|
|
944
|
+
...(emitted.blocks.length > 0 ? { emitted: emitted.blocks.length } : {}),
|
|
769
945
|
...(logs ? { logs } : {}),
|
|
770
946
|
...(diagnostics ? { diagnostics: diagnostics.finish() } : {}),
|
|
771
947
|
});
|
|
948
|
+
if (emitted.blocks.length > 0) {
|
|
949
|
+
// Emitted image/audio blocks are valid MCP content that ToolResult's
|
|
950
|
+
// text-only typing does not model — the same acknowledged gap
|
|
951
|
+
// guardContent lives with for downstream block passthrough.
|
|
952
|
+
response.content.push(
|
|
953
|
+
...(emitted.blocks as unknown as typeof response.content),
|
|
954
|
+
);
|
|
955
|
+
}
|
|
956
|
+
return response;
|
|
772
957
|
};
|
|
773
958
|
}
|
|
774
959
|
|
|
960
|
+
/** M4: a failed program delivers no blocks, but the discard is visible. */
|
|
961
|
+
function discardedEmits(emitted: EmitCollector): {
|
|
962
|
+
emittedDiscarded?: number;
|
|
963
|
+
} {
|
|
964
|
+
return emitted.blocks.length > 0
|
|
965
|
+
? { emittedDiscarded: emitted.blocks.length }
|
|
966
|
+
: {};
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
/** The same visibility for the plain-text error paths. */
|
|
970
|
+
function discardedEmitsText(emitted: EmitCollector): string {
|
|
971
|
+
return emitted.blocks.length > 0
|
|
972
|
+
? `\n\nemittedDiscarded: ${emitted.blocks.length}`
|
|
973
|
+
: "";
|
|
974
|
+
}
|
|
975
|
+
|
|
775
976
|
/**
|
|
776
977
|
* How the tool opens, and where a program's argument schemas come from. Both
|
|
777
978
|
* differ by surface: on the classic surface `execute_code` is the tool of last
|
|
@@ -794,12 +995,14 @@ const EXECUTE_SCHEMA_SOURCE = {
|
|
|
794
995
|
|
|
795
996
|
const executeDescription = (
|
|
796
997
|
surface: ConnectaSurface,
|
|
998
|
+
emitBudgets: { maxBytes: number; maxBlocks: number },
|
|
797
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.
|
|
798
1000
|
|
|
799
1001
|
Write an async arrow function. It runs with NO network, filesystem, timers, or imports — the only capabilities are:
|
|
800
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.
|
|
801
1003
|
- connecta.call(address, args) and connecta.batch(calls) — call raw addresses.
|
|
802
|
-
- connecta.search(args)
|
|
1004
|
+
- 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
|
+
- 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.
|
|
803
1006
|
- console.log(...) — captured and returned alongside the result.
|
|
804
1007
|
|
|
805
1008
|
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.
|
|
@@ -818,24 +1021,51 @@ export function registerExecuteTool(
|
|
|
818
1021
|
activity?: ActivityRequestContext;
|
|
819
1022
|
requestSignal?: AbortSignal;
|
|
820
1023
|
discoveryConcurrency?: number;
|
|
1024
|
+
/**
|
|
1025
|
+
* The deployment's configured per-connector probe deadline. Programs probe
|
|
1026
|
+
* the same downstream catalogs the top-level tools do, so an operator who
|
|
1027
|
+
* tightened `discovery.probeTimeoutMs` gets it honored inside the sandbox
|
|
1028
|
+
* too rather than silently falling back to the 30s default.
|
|
1029
|
+
*/
|
|
1030
|
+
probeTimeoutMs?: number;
|
|
1031
|
+
/** Aggregate serialized-byte budget for connecta.emit. Default 4_000_000. */
|
|
1032
|
+
maxEmittedBytes?: number;
|
|
1033
|
+
/** Block-count budget for connecta.emit. Default 32. */
|
|
1034
|
+
maxEmittedBlocks?: number;
|
|
821
1035
|
/** The advertised surface, which decides this tool's routing copy. */
|
|
822
1036
|
surface?: ConnectaSurface;
|
|
823
1037
|
},
|
|
824
1038
|
): void {
|
|
1039
|
+
// Resolved once so the description and the collector cannot disagree about
|
|
1040
|
+
// the budgets this deployment actually enforces.
|
|
1041
|
+
const emitBudgets = {
|
|
1042
|
+
maxBytes: resolveEmitBudget(ctx.maxEmittedBytes, EXECUTE_MAX_EMITTED_BYTES),
|
|
1043
|
+
maxBlocks: resolveEmitBudget(
|
|
1044
|
+
ctx.maxEmittedBlocks,
|
|
1045
|
+
EXECUTE_MAX_EMITTED_BLOCKS,
|
|
1046
|
+
),
|
|
1047
|
+
};
|
|
825
1048
|
const handler = createExecuteTool(
|
|
826
1049
|
registry,
|
|
827
1050
|
ctx.baseUrl,
|
|
828
1051
|
ctx.executor,
|
|
829
1052
|
ctx.logger,
|
|
830
1053
|
ctx.activity,
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
1054
|
+
{
|
|
1055
|
+
...(ctx.discoveryConcurrency !== undefined
|
|
1056
|
+
? { discoveryConcurrency: ctx.discoveryConcurrency }
|
|
1057
|
+
: {}),
|
|
1058
|
+
...(ctx.probeTimeoutMs !== undefined
|
|
1059
|
+
? { probeTimeoutMs: ctx.probeTimeoutMs }
|
|
1060
|
+
: {}),
|
|
1061
|
+
maxEmittedBytes: emitBudgets.maxBytes,
|
|
1062
|
+
maxEmittedBlocks: emitBudgets.maxBlocks,
|
|
1063
|
+
},
|
|
834
1064
|
);
|
|
835
1065
|
server.registerTool(
|
|
836
1066
|
"execute_code",
|
|
837
1067
|
{
|
|
838
|
-
description: executeDescription(ctx.surface ?? "classic"),
|
|
1068
|
+
description: executeDescription(ctx.surface ?? "classic", emitBudgets),
|
|
839
1069
|
inputSchema: z.object({
|
|
840
1070
|
code: z
|
|
841
1071
|
.string()
|
package/src/index.ts
CHANGED
|
@@ -111,6 +111,19 @@ export interface ConnectaCallsConfig {
|
|
|
111
111
|
maxBatchResultBytes?: number;
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
/** Budgets for rich output emitted by execute_code programs (`connecta.emit`). */
|
|
115
|
+
export interface ConnectaExecuteConfig {
|
|
116
|
+
/**
|
|
117
|
+
* Aggregate serialized bytes `connecta.emit` accepts per run. Default
|
|
118
|
+
* 4_000_000 — a transport bound, not a context bound: emitted image/audio
|
|
119
|
+
* blocks reach the model as media, not base64 text. Invalid values fall
|
|
120
|
+
* back to the default.
|
|
121
|
+
*/
|
|
122
|
+
maxEmittedBytes?: number;
|
|
123
|
+
/** Content blocks `connecta.emit` accepts per run. Default 32. */
|
|
124
|
+
maxEmittedBlocks?: number;
|
|
125
|
+
}
|
|
126
|
+
|
|
114
127
|
export interface AdmissionPoolConfig {
|
|
115
128
|
/** Simultaneous work admitted to this pool. */
|
|
116
129
|
concurrency?: number;
|
|
@@ -164,6 +177,8 @@ export interface ConnectaConfig {
|
|
|
164
177
|
discovery?: ConnectaDiscoveryConfig;
|
|
165
178
|
/** Deployment-wide call deadlines and result paging threshold. */
|
|
166
179
|
calls?: ConnectaCallsConfig;
|
|
180
|
+
/** Budgets for the `connecta.emit` rich-output channel in execute_code. */
|
|
181
|
+
execute?: ConnectaExecuteConfig;
|
|
167
182
|
/** Bounded MCP and fallback code-mode admission. */
|
|
168
183
|
admission?: ConnectaAdmissionConfig;
|
|
169
184
|
/** Optional browser UI and OAuth result-page labels. */
|
|
@@ -568,6 +583,12 @@ export function createConnecta(config: ConnectaConfig): Connecta {
|
|
|
568
583
|
...(config.discovery?.concurrency !== undefined
|
|
569
584
|
? { discoveryConcurrency: config.discovery.concurrency }
|
|
570
585
|
: {}),
|
|
586
|
+
...(config.execute?.maxEmittedBytes !== undefined
|
|
587
|
+
? { maxEmittedBytes: config.execute.maxEmittedBytes }
|
|
588
|
+
: {}),
|
|
589
|
+
...(config.execute?.maxEmittedBlocks !== undefined
|
|
590
|
+
? { maxEmittedBlocks: config.execute.maxEmittedBlocks }
|
|
591
|
+
: {}),
|
|
571
592
|
...(credentialVault !== undefined ? { credentialVault } : {}),
|
|
572
593
|
...(accessTokens !== undefined ? { accessTokens } : {}),
|
|
573
594
|
...(config.deploymentInfo !== undefined
|
|
@@ -667,6 +688,7 @@ export type {
|
|
|
667
688
|
ActivityReadPage,
|
|
668
689
|
ActivitySink,
|
|
669
690
|
ActivityStore,
|
|
691
|
+
AgentFriction,
|
|
670
692
|
ToolCallActivityEvent,
|
|
671
693
|
} from "./activity.js";
|
|
672
694
|
export { InvalidActivityCursorError } from "./activity.js";
|
package/src/invocation.ts
CHANGED
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
recordToolActivity,
|
|
3
3
|
type ActivityCallSource,
|
|
4
4
|
type ActivityRequestContext,
|
|
5
|
+
type AgentFriction,
|
|
5
6
|
} from "./activity.js";
|
|
6
7
|
import { isCallAdmissionError } from "./call-admission.js";
|
|
7
8
|
import {
|
|
@@ -12,12 +13,13 @@ import {
|
|
|
12
13
|
import {
|
|
13
14
|
classifyCallError,
|
|
14
15
|
ConnectorCallError,
|
|
16
|
+
echoedCallArgs,
|
|
15
17
|
framingError,
|
|
16
18
|
type AuthRecoveryMode,
|
|
17
19
|
type CallErrorDetails,
|
|
18
20
|
} from "./errors.js";
|
|
19
21
|
import { unwrapMcpResult } from "./mcp-result.js";
|
|
20
|
-
import type
|
|
22
|
+
import { splitAddress, type RegistryView } from "./registry.js";
|
|
21
23
|
import { isExplicitlyReadOnly } from "./tool-safety.js";
|
|
22
24
|
import type { ToolDef } from "./types.js";
|
|
23
25
|
import { validateToolInput } from "./validate.js";
|
|
@@ -150,6 +152,13 @@ export interface InvocationContext<T> {
|
|
|
150
152
|
value: unknown,
|
|
151
153
|
resolved: ResolvedCatalogTool,
|
|
152
154
|
) => T | Promise<T>;
|
|
155
|
+
/**
|
|
156
|
+
* Optional payload-free friction class derived from a *successful* result —
|
|
157
|
+
* today only an oversized one that had to be paged. It is deliberately not an
|
|
158
|
+
* `errorCode`: the call succeeded, and a consumer that keys its dashboards on
|
|
159
|
+
* "has an error code" must not count a truncation as a failure.
|
|
160
|
+
*/
|
|
161
|
+
activityFriction?: (value: T) => AgentFriction | undefined;
|
|
153
162
|
/**
|
|
154
163
|
* Called after address/catalog/safety admission and before the first provider
|
|
155
164
|
* attempt. Code mode uses it for its host-call budget.
|
|
@@ -252,6 +261,10 @@ export class InvocationService {
|
|
|
252
261
|
let activityTarget:
|
|
253
262
|
| Pick<ResolvedCatalogTool, "connector" | "toolName">
|
|
254
263
|
| undefined;
|
|
264
|
+
// The address as written, used for activity when resolution never reached
|
|
265
|
+
// a connector. Only its two halves are recorded — the same fields activity
|
|
266
|
+
// has always carried — so no new class of payload enters the log.
|
|
267
|
+
const attempted = splitAddress(address);
|
|
255
268
|
const timing = (): InvocationTiming => ({
|
|
256
269
|
catalogMs,
|
|
257
270
|
admissionMs,
|
|
@@ -262,25 +275,56 @@ export class InvocationService {
|
|
|
262
275
|
});
|
|
263
276
|
const record = (
|
|
264
277
|
outcome: "success" | "error" | "timeout" | "cancelled",
|
|
265
|
-
errorCode?: string,
|
|
278
|
+
classification: { errorCode?: string; friction?: AgentFriction } = {},
|
|
266
279
|
) => {
|
|
267
|
-
|
|
280
|
+
const identity = activityTarget
|
|
281
|
+
? {
|
|
282
|
+
connectorId: activityTarget.connector.id,
|
|
283
|
+
toolName: activityTarget.toolName,
|
|
284
|
+
}
|
|
285
|
+
: attempted;
|
|
286
|
+
if (!identity) return;
|
|
268
287
|
recordToolActivity(this.activity, {
|
|
269
|
-
connectorId:
|
|
270
|
-
toolName:
|
|
271
|
-
address: `${
|
|
288
|
+
connectorId: identity.connectorId,
|
|
289
|
+
toolName: identity.toolName,
|
|
290
|
+
address: `${identity.connectorId}.${identity.toolName}`,
|
|
272
291
|
source: context.source,
|
|
273
292
|
outcome,
|
|
274
293
|
durationMs: Date.now() - started,
|
|
275
294
|
attempts,
|
|
276
|
-
...(errorCode
|
|
295
|
+
...(classification.errorCode
|
|
296
|
+
? { errorCode: classification.errorCode }
|
|
297
|
+
: {}),
|
|
298
|
+
...(classification.friction
|
|
299
|
+
? { friction: classification.friction }
|
|
300
|
+
: {}),
|
|
277
301
|
});
|
|
278
302
|
};
|
|
279
303
|
const failed = (error: CallErrorDetails): InvocationOutcome<T> => {
|
|
280
304
|
const diagnostics = timing();
|
|
281
305
|
const target = resolved ?? activityTarget;
|
|
306
|
+
const echoed =
|
|
307
|
+
error.code === "destructive_tool_requires_approval"
|
|
308
|
+
? echoedCallArgs(args)
|
|
309
|
+
: {};
|
|
282
310
|
const details =
|
|
283
|
-
error.code === "
|
|
311
|
+
error.code === "destructive_tool_requires_approval" && target
|
|
312
|
+
? {
|
|
313
|
+
...error,
|
|
314
|
+
nextAction: {
|
|
315
|
+
tool: "call_destructive_tool" as const,
|
|
316
|
+
arguments: {
|
|
317
|
+
address: `${target.connector.id}.${target.toolName}`,
|
|
318
|
+
...echoed,
|
|
319
|
+
},
|
|
320
|
+
purpose:
|
|
321
|
+
"Ask the MCP host to approve this consequential call. " +
|
|
322
|
+
("args" in echoed
|
|
323
|
+
? "Re-send these arguments and add a short reason for the human reviewer."
|
|
324
|
+
: "Re-send the arguments you just sent — they are too large to echo back — and add a short reason for the human reviewer."),
|
|
325
|
+
},
|
|
326
|
+
}
|
|
327
|
+
: error.code === "auth_required" && target
|
|
284
328
|
? {
|
|
285
329
|
...error,
|
|
286
330
|
connector: target.connector.id,
|
|
@@ -305,16 +349,13 @@ export class InvocationService {
|
|
|
305
349
|
...error,
|
|
306
350
|
connector: target.connector.id,
|
|
307
351
|
operation: `${target.connector.id}.${target.toolName}`,
|
|
308
|
-
nextAction:
|
|
309
|
-
|
|
310
|
-
arguments: {
|
|
352
|
+
nextAction: this.catalog.searchRecovery(
|
|
353
|
+
{
|
|
311
354
|
query: target.toolName,
|
|
312
355
|
connector: target.connector.id,
|
|
313
|
-
includeSchemas: "compact" as const,
|
|
314
356
|
},
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
},
|
|
357
|
+
"Inspect the current input shape if the validation findings are not sufficient.",
|
|
358
|
+
),
|
|
318
359
|
retry:
|
|
319
360
|
`Correct the listed arguments and retry ` +
|
|
320
361
|
`${target.connector.id}.${target.toolName}.`,
|
|
@@ -326,7 +367,7 @@ export class InvocationService {
|
|
|
326
367
|
: details.code === "cancelled"
|
|
327
368
|
? "cancelled"
|
|
328
369
|
: "error",
|
|
329
|
-
details.code,
|
|
370
|
+
{ errorCode: details.code },
|
|
330
371
|
);
|
|
331
372
|
return {
|
|
332
373
|
ok: false,
|
|
@@ -591,7 +632,8 @@ export class InvocationService {
|
|
|
591
632
|
: (result as T);
|
|
592
633
|
resultProcessingMs += Date.now() - processingStarted;
|
|
593
634
|
const diagnostics = timing();
|
|
594
|
-
|
|
635
|
+
const friction = context.activityFriction?.(value);
|
|
636
|
+
record("success", friction ? { friction } : {});
|
|
595
637
|
return {
|
|
596
638
|
ok: true,
|
|
597
639
|
value,
|