@zackbart/connecta 0.10.5 → 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 +110 -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 +39 -0
- package/dist/catalog-service.d.ts.map +1 -1
- package/dist/catalog-service.js +72 -11
- 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 +162 -9
- 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 +5 -1
- package/dist/meta-tools.d.ts.map +1 -1
- package/dist/meta-tools.js +171 -26
- 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/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/code-mode.md +119 -31
- package/documentation/meta-tools.md +79 -3
- 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 +135 -22
- package/src/errors.ts +102 -1
- package/src/execute.ts +238 -9
- package/src/index.ts +22 -0
- package/src/invocation.ts +59 -17
- package/src/meta-tools.ts +211 -30
- 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/version.ts +1 -1
- package/templates/node/package.json +1 -1
package/src/catalog-service.ts
CHANGED
|
@@ -13,7 +13,11 @@ import {
|
|
|
13
13
|
mapSettledWithConcurrency,
|
|
14
14
|
resolveDiscoveryConcurrency,
|
|
15
15
|
} from "./concurrency.js";
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
boundedEchoText,
|
|
18
|
+
classifyCallError,
|
|
19
|
+
framingError,
|
|
20
|
+
} from "./errors.js";
|
|
17
21
|
import type { CallErrorDetails } from "./errors.js";
|
|
18
22
|
import type {
|
|
19
23
|
ConnectorOperationOptions,
|
|
@@ -44,6 +48,26 @@ const MAX_QUERY_ANALYSIS_TERM_LENGTH = 64;
|
|
|
44
48
|
|
|
45
49
|
const encoder = new TextEncoder();
|
|
46
50
|
|
|
51
|
+
/**
|
|
52
|
+
* The tool a describe-path error should name when it tells a caller to retry.
|
|
53
|
+
* This is the route the *caller* took, not the deployment's advertised surface:
|
|
54
|
+
* a classic deployment with an executor serves `describe_tools` at top level
|
|
55
|
+
* while every in-program describe still arrives through `connecta.describe`, so
|
|
56
|
+
* one CatalogService cannot infer the answer from `surface` alone. Callers pass
|
|
57
|
+
* the route they own.
|
|
58
|
+
*/
|
|
59
|
+
export type DescribeRoute = "describe_tools" | "connecta.describe";
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The discovery route a routing failure should send a caller back through. Same
|
|
63
|
+
* rule as {@link DescribeRoute} — the route the *caller* took, not the
|
|
64
|
+
* deployment's advertised surface — with one difference worth keeping the two
|
|
65
|
+
* options separate for: `search_tools` exists on both advertised surfaces, so a
|
|
66
|
+
* top-level handler never has to derive this one, while an in-program caller
|
|
67
|
+
* still has to be told about `connecta.search` because it cannot call a tool.
|
|
68
|
+
*/
|
|
69
|
+
export type SearchRoute = "search_tools" | "connecta.search";
|
|
70
|
+
|
|
47
71
|
export class DiscoveryPolicyError extends Error {
|
|
48
72
|
constructor(
|
|
49
73
|
readonly code: "invalid_args" | "result_too_large",
|
|
@@ -72,7 +96,10 @@ function discoverySearchLimit(value: unknown): number {
|
|
|
72
96
|
}
|
|
73
97
|
|
|
74
98
|
/** Normalize the single-address convenience form, then validate the bounded list. */
|
|
75
|
-
function discoveryAddresses(
|
|
99
|
+
function discoveryAddresses(
|
|
100
|
+
args: CatalogDescribeArgs,
|
|
101
|
+
describeRoute: DescribeRoute,
|
|
102
|
+
): unknown[] {
|
|
76
103
|
if (args.address !== undefined && args.addresses !== undefined) {
|
|
77
104
|
throw new DiscoveryPolicyError(
|
|
78
105
|
"invalid_args",
|
|
@@ -94,12 +121,26 @@ function discoveryAddresses(args: CatalogDescribeArgs): unknown[] {
|
|
|
94
121
|
if (value.length > MAX_DESCRIBE_ADDRESSES) {
|
|
95
122
|
throw new DiscoveryPolicyError(
|
|
96
123
|
"invalid_args",
|
|
97
|
-
`addresses must contain at most ${MAX_DESCRIBE_ADDRESSES} entries. Split a larger list across
|
|
124
|
+
`addresses must contain at most ${MAX_DESCRIBE_ADDRESSES} entries. Split a larger list across ${describeRoute} calls.`,
|
|
98
125
|
);
|
|
99
126
|
}
|
|
100
127
|
return value;
|
|
101
128
|
}
|
|
102
129
|
|
|
130
|
+
/**
|
|
131
|
+
* Search terms derived from an address the catalog could not resolve. Bounded
|
|
132
|
+
* because the address is entirely caller-authored: an invented one can be any
|
|
133
|
+
* length, and this string is copied into a recovery record that is itself
|
|
134
|
+
* copied into both halves of the result envelope.
|
|
135
|
+
*/
|
|
136
|
+
function recoveryQuery(address: string): string {
|
|
137
|
+
const separator = address.indexOf(".");
|
|
138
|
+
const candidate = separator >= 0 ? address.slice(separator + 1) : address;
|
|
139
|
+
return boundedEchoText(
|
|
140
|
+
candidate.replaceAll(/[._-]+/g, " ").trim() || address,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
103
144
|
/** Serialize once and count the exact bytes the MCP adapter would emit. */
|
|
104
145
|
export function boundedDiscoveryText(value: unknown, hint: string): string {
|
|
105
146
|
const text = JSON.stringify(value);
|
|
@@ -286,6 +327,8 @@ export class CatalogService {
|
|
|
286
327
|
readonly requestScope: object;
|
|
287
328
|
private readonly probeTimeoutMs: number;
|
|
288
329
|
private readonly concurrency: number;
|
|
330
|
+
private readonly describeRoute: DescribeRoute;
|
|
331
|
+
private readonly searchRoute: SearchRoute;
|
|
289
332
|
private readonly loaded = new Map<string, ToolDef[]>();
|
|
290
333
|
private readonly loading = new Map<string, Promise<ToolDef[]>>();
|
|
291
334
|
|
|
@@ -296,12 +339,43 @@ export class CatalogService {
|
|
|
296
339
|
requestScope?: object;
|
|
297
340
|
probeTimeoutMs?: number;
|
|
298
341
|
concurrency?: number;
|
|
342
|
+
/** The tool describe-path errors name. Default `describe_tools`. */
|
|
343
|
+
describeRoute?: DescribeRoute;
|
|
344
|
+
/** The discovery route recovery records name. Default `search_tools`. */
|
|
345
|
+
searchRoute?: SearchRoute;
|
|
299
346
|
} = {},
|
|
300
347
|
) {
|
|
301
348
|
this.requestScope = options.requestScope ?? {};
|
|
302
349
|
this.probeTimeoutMs =
|
|
303
350
|
normalizeTimeoutMs(options.probeTimeoutMs) ?? DEFAULT_PROBE_TIMEOUT_MS;
|
|
304
351
|
this.concurrency = resolveDiscoveryConcurrency(options.concurrency);
|
|
352
|
+
this.describeRoute = options.describeRoute ?? "describe_tools";
|
|
353
|
+
this.searchRoute = options.searchRoute ?? "search_tools";
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Send a caller back to discovery through the surface it can actually reach.
|
|
358
|
+
* Both variants carry the same scoping arguments because `connecta.search`
|
|
359
|
+
* takes the same ones `search_tools` does; only the key naming the callable
|
|
360
|
+
* differs, the way the ambiguous-alias record already names a function.
|
|
361
|
+
*
|
|
362
|
+
* Not private: `InvocationService` builds the same class of record when a
|
|
363
|
+
* call fails schema validation, and it is this catalog's route that decides
|
|
364
|
+
* which key that record carries. Duplicating the branch there would let the
|
|
365
|
+
* two drift.
|
|
366
|
+
*/
|
|
367
|
+
searchRecovery(
|
|
368
|
+
args: { query: string; connector?: string },
|
|
369
|
+
purpose: string,
|
|
370
|
+
): NonNullable<CallErrorDetails["nextAction"]> {
|
|
371
|
+
const searchArgs = {
|
|
372
|
+
query: args.query,
|
|
373
|
+
...(args.connector !== undefined ? { connector: args.connector } : {}),
|
|
374
|
+
includeSchemas: "compact" as const,
|
|
375
|
+
};
|
|
376
|
+
return this.searchRoute === "connecta.search"
|
|
377
|
+
? { function: "connecta.search", arguments: searchArgs, purpose }
|
|
378
|
+
: { tool: "search_tools", arguments: searchArgs, purpose };
|
|
305
379
|
}
|
|
306
380
|
|
|
307
381
|
async loadConnector(
|
|
@@ -345,10 +419,16 @@ export class CatalogService {
|
|
|
345
419
|
if (!resolved) {
|
|
346
420
|
return {
|
|
347
421
|
ok: false,
|
|
348
|
-
error:
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
422
|
+
error: {
|
|
423
|
+
...framingError(
|
|
424
|
+
"unknown_address",
|
|
425
|
+
`Unknown address "${boundedEchoText(address)}"`,
|
|
426
|
+
),
|
|
427
|
+
nextAction: this.searchRecovery(
|
|
428
|
+
{ query: recoveryQuery(address) },
|
|
429
|
+
"Find the configured canonical address before retrying.",
|
|
430
|
+
),
|
|
431
|
+
},
|
|
352
432
|
catalogMs: 0,
|
|
353
433
|
};
|
|
354
434
|
}
|
|
@@ -370,10 +450,19 @@ export class CatalogService {
|
|
|
370
450
|
if (!definition) {
|
|
371
451
|
return {
|
|
372
452
|
ok: false,
|
|
373
|
-
error:
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
453
|
+
error: {
|
|
454
|
+
...framingError(
|
|
455
|
+
"unknown_tool",
|
|
456
|
+
`Unknown tool "${boundedEchoText(resolved.toolName)}" on connector "${resolved.connector.id}"`,
|
|
457
|
+
),
|
|
458
|
+
nextAction: this.searchRecovery(
|
|
459
|
+
{
|
|
460
|
+
query: recoveryQuery(resolved.toolName),
|
|
461
|
+
connector: resolved.connector.id,
|
|
462
|
+
},
|
|
463
|
+
"Find the connector's current canonical tool address.",
|
|
464
|
+
),
|
|
465
|
+
},
|
|
377
466
|
catalogMs: Date.now() - started,
|
|
378
467
|
connector: resolved.connector,
|
|
379
468
|
toolName: resolved.toolName,
|
|
@@ -405,10 +494,16 @@ export class CatalogService {
|
|
|
405
494
|
if (!connector) {
|
|
406
495
|
return {
|
|
407
496
|
ok: false,
|
|
408
|
-
error:
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
497
|
+
error: {
|
|
498
|
+
...framingError(
|
|
499
|
+
"unknown_address",
|
|
500
|
+
`Unknown address "${boundedEchoText(`${connectorId}.${alias}`)}"`,
|
|
501
|
+
),
|
|
502
|
+
nextAction: this.searchRecovery(
|
|
503
|
+
{ query: recoveryQuery(alias) },
|
|
504
|
+
"Find the configured canonical address before retrying.",
|
|
505
|
+
),
|
|
506
|
+
},
|
|
412
507
|
catalogMs: 0,
|
|
413
508
|
};
|
|
414
509
|
}
|
|
@@ -432,10 +527,16 @@ export class CatalogService {
|
|
|
432
527
|
if (!definition) {
|
|
433
528
|
return {
|
|
434
529
|
ok: false,
|
|
435
|
-
error:
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
530
|
+
error: {
|
|
531
|
+
...framingError(
|
|
532
|
+
"unknown_tool",
|
|
533
|
+
`Unknown tool "${boundedEchoText(alias)}" on connector "${connector.id}"`,
|
|
534
|
+
),
|
|
535
|
+
nextAction: this.searchRecovery(
|
|
536
|
+
{ query: recoveryQuery(alias), connector: connector.id },
|
|
537
|
+
"Find the connector's current canonical tool address.",
|
|
538
|
+
),
|
|
539
|
+
},
|
|
439
540
|
catalogMs: Date.now() - started,
|
|
440
541
|
connector,
|
|
441
542
|
toolName: alias,
|
|
@@ -449,8 +550,16 @@ export class CatalogService {
|
|
|
449
550
|
ok: false,
|
|
450
551
|
error: {
|
|
451
552
|
code: "ambiguous_tool_alias",
|
|
452
|
-
message: `Tool alias "${alias}" is ambiguous on connector "${connector.id}" because ${names} sanitize to the same name. Use connecta.call with an exact address.`,
|
|
553
|
+
message: `Tool alias "${boundedEchoText(alias)}" is ambiguous on connector "${connector.id}" because ${names} sanitize to the same name. Use connecta.call with an exact address.`,
|
|
453
554
|
retryable: false,
|
|
555
|
+
nextAction: {
|
|
556
|
+
function: "connecta.call",
|
|
557
|
+
addresses: [definition, ...collisions].map(
|
|
558
|
+
(tool) => `${connector.id}.${tool.name}`,
|
|
559
|
+
),
|
|
560
|
+
purpose:
|
|
561
|
+
"Choose the intended canonical address and call it with the original arguments.",
|
|
562
|
+
},
|
|
454
563
|
},
|
|
455
564
|
catalogMs: Date.now() - started,
|
|
456
565
|
connector,
|
|
@@ -486,6 +595,10 @@ export class CatalogService {
|
|
|
486
595
|
connectors,
|
|
487
596
|
this.concurrency,
|
|
488
597
|
(connector) =>
|
|
598
|
+
// Unlike the describe path, this label never reaches a caller: search
|
|
599
|
+
// only counts rejected catalogs (`unavailableCatalogs` below) and
|
|
600
|
+
// renders its own guidance, so the folded name here stays internal and
|
|
601
|
+
// needs no surface awareness.
|
|
489
602
|
this.loadForDiscovery(
|
|
490
603
|
connector.id,
|
|
491
604
|
`search_tools probe of "${connector.id}"`,
|
|
@@ -704,7 +817,7 @@ export class CatalogService {
|
|
|
704
817
|
}
|
|
705
818
|
|
|
706
819
|
async describe(args: CatalogDescribeArgs): Promise<CatalogDescription[]> {
|
|
707
|
-
const addresses = discoveryAddresses(args);
|
|
820
|
+
const addresses = discoveryAddresses(args, this.describeRoute);
|
|
708
821
|
const format = args.format ?? "compact";
|
|
709
822
|
const resolved = addresses.map((rawAddress) => {
|
|
710
823
|
const address = String(rawAddress);
|
|
@@ -721,7 +834,7 @@ export class CatalogService {
|
|
|
721
834
|
connectorIds,
|
|
722
835
|
this.concurrency,
|
|
723
836
|
(id) =>
|
|
724
|
-
this.loadForDiscovery(id,
|
|
837
|
+
this.loadForDiscovery(id, `${this.describeRoute} probe of "${id}"`),
|
|
725
838
|
);
|
|
726
839
|
const catalogs = new Map<string, ToolDef[] | Error>();
|
|
727
840
|
loaded.forEach((result, index) => {
|
package/src/errors.ts
CHANGED
|
@@ -43,6 +43,76 @@ function boundedIssueText(
|
|
|
43
43
|
};
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* How many bytes of the caller's own arguments an error envelope will echo
|
|
48
|
+
* back to it. Small on purpose: an error result is not size-guarded the way a
|
|
49
|
+
* *result* is, so an unbounded echo turns a 50 KB argument object into a 100 KB
|
|
50
|
+
* refusal against a deployment that capped results at 1 KB — twice over, since
|
|
51
|
+
* the payload lands in both the text content and `structuredContent`. The agent
|
|
52
|
+
* already holds what it sent; the echo is a convenience, never the record.
|
|
53
|
+
*/
|
|
54
|
+
const MAX_ECHOED_ARGS_BYTES = 512;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The same budget spent on caller-authored *text* — the address it mistyped,
|
|
58
|
+
* the discovery query derived from it — rather than on its arguments.
|
|
59
|
+
*
|
|
60
|
+
* Text is clamped where {@link echoedCallArgs} drops: an address is the thing
|
|
61
|
+
* the refusal exists to correct, so a refusal that named nothing would be
|
|
62
|
+
* useless, while a clipped one still carries the prefix that identifies the
|
|
63
|
+
* mistake. The marker says it was clipped, so nothing reads a clamped address
|
|
64
|
+
* as the address that was sent. Arguments get the opposite rule because they
|
|
65
|
+
* end at a human approving one specific call.
|
|
66
|
+
*
|
|
67
|
+
* The number is not cosmetic. An error result is not size-guarded the way a
|
|
68
|
+
* result is, and every echoed byte lands twice (text content *and*
|
|
69
|
+
* `structuredContent`), so an unbounded address turned a 50 KB typo into a
|
|
70
|
+
* 200 KB refusal against a deployment that capped results at 1 KB.
|
|
71
|
+
*/
|
|
72
|
+
const MAX_ECHOED_TEXT_BYTES = 512;
|
|
73
|
+
|
|
74
|
+
const echoEncoder = new TextEncoder();
|
|
75
|
+
const echoDecoder = new TextDecoder();
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Clamp a string to a UTF-8 byte budget, appending `…` when it clipped.
|
|
79
|
+
* Short strings — the common case, and the one that has to stay exact — are
|
|
80
|
+
* returned unchanged and untagged.
|
|
81
|
+
*/
|
|
82
|
+
export function boundedEchoText(
|
|
83
|
+
value: string,
|
|
84
|
+
maxBytes: number = MAX_ECHOED_TEXT_BYTES,
|
|
85
|
+
): string {
|
|
86
|
+
const bytes = echoEncoder.encode(value);
|
|
87
|
+
if (bytes.length <= maxBytes) return value;
|
|
88
|
+
// Never split a codepoint: walk back off UTF-8 continuation bytes (10xxxxxx)
|
|
89
|
+
// so the clamp cannot manufacture a replacement character.
|
|
90
|
+
let end = Math.max(0, maxBytes);
|
|
91
|
+
while (end > 0 && ((bytes[end] ?? 0) & 0xc0) === 0x80) end--;
|
|
92
|
+
return `${echoDecoder.decode(bytes.slice(0, end))}…`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* `{ args }` when the caller's arguments fit {@link MAX_ECHOED_ARGS_BYTES},
|
|
97
|
+
* `{}` when they do not. All or nothing: a clipped echo would be a *different*
|
|
98
|
+
* call than the one that was refused, and the routes this feeds end at a human
|
|
99
|
+
* approving one. Unserializable arguments are treated the same way as oversized
|
|
100
|
+
* ones — there is nothing honest to put in the field.
|
|
101
|
+
*/
|
|
102
|
+
export function echoedCallArgs(args: unknown): { args?: unknown } {
|
|
103
|
+
if (args === undefined) return {};
|
|
104
|
+
let text: string | undefined;
|
|
105
|
+
try {
|
|
106
|
+
text = JSON.stringify(args);
|
|
107
|
+
} catch {
|
|
108
|
+
return {};
|
|
109
|
+
}
|
|
110
|
+
if (text === undefined) return {};
|
|
111
|
+
return echoEncoder.encode(text).length <= MAX_ECHOED_ARGS_BYTES
|
|
112
|
+
? { args }
|
|
113
|
+
: {};
|
|
114
|
+
}
|
|
115
|
+
|
|
46
116
|
function boundedValidation(
|
|
47
117
|
details: ArgumentValidationDetails | undefined,
|
|
48
118
|
): ArgumentValidationDetails | undefined {
|
|
@@ -179,10 +249,41 @@ export interface CallErrorDetails {
|
|
|
179
249
|
tool: "search_tools";
|
|
180
250
|
arguments: {
|
|
181
251
|
query: string;
|
|
182
|
-
connector
|
|
252
|
+
connector?: string;
|
|
183
253
|
includeSchemas: "compact";
|
|
184
254
|
};
|
|
185
255
|
purpose: string;
|
|
256
|
+
} | {
|
|
257
|
+
tool: "call_destructive_tool";
|
|
258
|
+
arguments: {
|
|
259
|
+
address: string;
|
|
260
|
+
/**
|
|
261
|
+
* The caller's own arguments, echoed only when they fit
|
|
262
|
+
* {@link MAX_ECHOED_ARGS_BYTES} — and then whole, never clipped. Absent
|
|
263
|
+
* means "re-send exactly what you sent": a half-copied argument object
|
|
264
|
+
* routed into a human approval prompt would describe a call nobody made.
|
|
265
|
+
*/
|
|
266
|
+
args?: unknown;
|
|
267
|
+
};
|
|
268
|
+
purpose: string;
|
|
269
|
+
} | {
|
|
270
|
+
/**
|
|
271
|
+
* The same scoped discovery as the `search_tools` route above, addressed to
|
|
272
|
+
* a caller inside `execute_code`, which cannot call a tool. Which of the two
|
|
273
|
+
* a routing failure emits follows the route the caller took, not the
|
|
274
|
+
* deployment's advertised surface.
|
|
275
|
+
*/
|
|
276
|
+
function: "connecta.search";
|
|
277
|
+
arguments: {
|
|
278
|
+
query: string;
|
|
279
|
+
connector?: string;
|
|
280
|
+
includeSchemas: "compact";
|
|
281
|
+
};
|
|
282
|
+
purpose: string;
|
|
283
|
+
} | {
|
|
284
|
+
function: "connecta.call";
|
|
285
|
+
addresses: string[];
|
|
286
|
+
purpose: string;
|
|
186
287
|
};
|
|
187
288
|
/** Explicit retry guidance; recovery never retries or mutates by itself. */
|
|
188
289
|
retry?: string;
|