@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/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",
|
|
@@ -71,23 +95,52 @@ function discoverySearchLimit(value: unknown): number {
|
|
|
71
95
|
return value;
|
|
72
96
|
}
|
|
73
97
|
|
|
74
|
-
/**
|
|
75
|
-
function discoveryAddresses(
|
|
98
|
+
/** Normalize the single-address convenience form, then validate the bounded list. */
|
|
99
|
+
function discoveryAddresses(
|
|
100
|
+
args: CatalogDescribeArgs,
|
|
101
|
+
describeRoute: DescribeRoute,
|
|
102
|
+
): unknown[] {
|
|
103
|
+
if (args.address !== undefined && args.addresses !== undefined) {
|
|
104
|
+
throw new DiscoveryPolicyError(
|
|
105
|
+
"invalid_args",
|
|
106
|
+
"describe takes either address or addresses, not both.",
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
const value =
|
|
110
|
+
args.address !== undefined
|
|
111
|
+
? typeof args.address === "string"
|
|
112
|
+
? [args.address]
|
|
113
|
+
: undefined
|
|
114
|
+
: args.addresses;
|
|
76
115
|
if (!Array.isArray(value)) {
|
|
77
116
|
throw new DiscoveryPolicyError(
|
|
78
117
|
"invalid_args",
|
|
79
|
-
'describe takes { addresses: ["<connectorId>.<toolName>", ...] }
|
|
118
|
+
'describe takes { address: "<connectorId>.<toolName>" } or { addresses: ["<connectorId>.<toolName>", ...] }.',
|
|
80
119
|
);
|
|
81
120
|
}
|
|
82
121
|
if (value.length > MAX_DESCRIBE_ADDRESSES) {
|
|
83
122
|
throw new DiscoveryPolicyError(
|
|
84
123
|
"invalid_args",
|
|
85
|
-
`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.`,
|
|
86
125
|
);
|
|
87
126
|
}
|
|
88
127
|
return value;
|
|
89
128
|
}
|
|
90
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
|
+
|
|
91
144
|
/** Serialize once and count the exact bytes the MCP adapter would emit. */
|
|
92
145
|
export function boundedDiscoveryText(value: unknown, hint: string): string {
|
|
93
146
|
const text = JSON.stringify(value);
|
|
@@ -150,6 +203,7 @@ function toolsForSafety(
|
|
|
150
203
|
}
|
|
151
204
|
|
|
152
205
|
export interface CatalogDescribeArgs {
|
|
206
|
+
address?: unknown;
|
|
153
207
|
addresses?: unknown;
|
|
154
208
|
format?: "compact" | "json";
|
|
155
209
|
fullDescriptions?: boolean;
|
|
@@ -273,6 +327,8 @@ export class CatalogService {
|
|
|
273
327
|
readonly requestScope: object;
|
|
274
328
|
private readonly probeTimeoutMs: number;
|
|
275
329
|
private readonly concurrency: number;
|
|
330
|
+
private readonly describeRoute: DescribeRoute;
|
|
331
|
+
private readonly searchRoute: SearchRoute;
|
|
276
332
|
private readonly loaded = new Map<string, ToolDef[]>();
|
|
277
333
|
private readonly loading = new Map<string, Promise<ToolDef[]>>();
|
|
278
334
|
|
|
@@ -283,12 +339,43 @@ export class CatalogService {
|
|
|
283
339
|
requestScope?: object;
|
|
284
340
|
probeTimeoutMs?: number;
|
|
285
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;
|
|
286
346
|
} = {},
|
|
287
347
|
) {
|
|
288
348
|
this.requestScope = options.requestScope ?? {};
|
|
289
349
|
this.probeTimeoutMs =
|
|
290
350
|
normalizeTimeoutMs(options.probeTimeoutMs) ?? DEFAULT_PROBE_TIMEOUT_MS;
|
|
291
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 };
|
|
292
379
|
}
|
|
293
380
|
|
|
294
381
|
async loadConnector(
|
|
@@ -332,10 +419,16 @@ export class CatalogService {
|
|
|
332
419
|
if (!resolved) {
|
|
333
420
|
return {
|
|
334
421
|
ok: false,
|
|
335
|
-
error:
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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
|
+
},
|
|
339
432
|
catalogMs: 0,
|
|
340
433
|
};
|
|
341
434
|
}
|
|
@@ -357,10 +450,19 @@ export class CatalogService {
|
|
|
357
450
|
if (!definition) {
|
|
358
451
|
return {
|
|
359
452
|
ok: false,
|
|
360
|
-
error:
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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
|
+
},
|
|
364
466
|
catalogMs: Date.now() - started,
|
|
365
467
|
connector: resolved.connector,
|
|
366
468
|
toolName: resolved.toolName,
|
|
@@ -392,10 +494,16 @@ export class CatalogService {
|
|
|
392
494
|
if (!connector) {
|
|
393
495
|
return {
|
|
394
496
|
ok: false,
|
|
395
|
-
error:
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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
|
+
},
|
|
399
507
|
catalogMs: 0,
|
|
400
508
|
};
|
|
401
509
|
}
|
|
@@ -419,10 +527,16 @@ export class CatalogService {
|
|
|
419
527
|
if (!definition) {
|
|
420
528
|
return {
|
|
421
529
|
ok: false,
|
|
422
|
-
error:
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
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
|
+
},
|
|
426
540
|
catalogMs: Date.now() - started,
|
|
427
541
|
connector,
|
|
428
542
|
toolName: alias,
|
|
@@ -436,8 +550,16 @@ export class CatalogService {
|
|
|
436
550
|
ok: false,
|
|
437
551
|
error: {
|
|
438
552
|
code: "ambiguous_tool_alias",
|
|
439
|
-
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.`,
|
|
440
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
|
+
},
|
|
441
563
|
},
|
|
442
564
|
catalogMs: Date.now() - started,
|
|
443
565
|
connector,
|
|
@@ -473,6 +595,10 @@ export class CatalogService {
|
|
|
473
595
|
connectors,
|
|
474
596
|
this.concurrency,
|
|
475
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.
|
|
476
602
|
this.loadForDiscovery(
|
|
477
603
|
connector.id,
|
|
478
604
|
`search_tools probe of "${connector.id}"`,
|
|
@@ -542,6 +668,10 @@ export class CatalogService {
|
|
|
542
668
|
args.includeSchemas && match.tool.outputSchema
|
|
543
669
|
? renderSearchSchema(match.tool.outputSchema, args.includeSchemas)
|
|
544
670
|
: undefined;
|
|
671
|
+
const schemaKeys =
|
|
672
|
+
args.includeSchemas && args.includeSchemaKeys
|
|
673
|
+
? schemaKeyMetadata(input, match.tool.outputSchema)
|
|
674
|
+
: undefined;
|
|
545
675
|
const description = summarizeDiscoveryDescription(
|
|
546
676
|
match.tool.description,
|
|
547
677
|
args.fullDescriptions === true,
|
|
@@ -571,8 +701,18 @@ export class CatalogService {
|
|
|
571
701
|
...(renderedOutput?.truncated
|
|
572
702
|
? { outputSchemaTruncated: true as const }
|
|
573
703
|
: {}),
|
|
574
|
-
...(
|
|
575
|
-
?
|
|
704
|
+
...(schemaKeys && !renderedInput?.truncated
|
|
705
|
+
? {
|
|
706
|
+
...(schemaKeys.inputKeys
|
|
707
|
+
? { inputKeys: schemaKeys.inputKeys }
|
|
708
|
+
: {}),
|
|
709
|
+
...(schemaKeys.requiredInputKeys
|
|
710
|
+
? { requiredInputKeys: schemaKeys.requiredInputKeys }
|
|
711
|
+
: {}),
|
|
712
|
+
}
|
|
713
|
+
: {}),
|
|
714
|
+
...(schemaKeys?.outputKeys && !renderedOutput?.truncated
|
|
715
|
+
? { outputKeys: schemaKeys.outputKeys }
|
|
576
716
|
: {}),
|
|
577
717
|
...(match.tool.annotations
|
|
578
718
|
? { annotations: match.tool.annotations }
|
|
@@ -677,7 +817,7 @@ export class CatalogService {
|
|
|
677
817
|
}
|
|
678
818
|
|
|
679
819
|
async describe(args: CatalogDescribeArgs): Promise<CatalogDescription[]> {
|
|
680
|
-
const addresses = discoveryAddresses(args.
|
|
820
|
+
const addresses = discoveryAddresses(args, this.describeRoute);
|
|
681
821
|
const format = args.format ?? "compact";
|
|
682
822
|
const resolved = addresses.map((rawAddress) => {
|
|
683
823
|
const address = String(rawAddress);
|
|
@@ -694,7 +834,7 @@ export class CatalogService {
|
|
|
694
834
|
connectorIds,
|
|
695
835
|
this.concurrency,
|
|
696
836
|
(id) =>
|
|
697
|
-
this.loadForDiscovery(id,
|
|
837
|
+
this.loadForDiscovery(id, `${this.describeRoute} probe of "${id}"`),
|
|
698
838
|
);
|
|
699
839
|
const catalogs = new Map<string, ToolDef[] | Error>();
|
|
700
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;
|