@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/activity.ts
CHANGED
|
@@ -1,8 +1,33 @@
|
|
|
1
|
+
import { boundedEchoText } from "./errors.js";
|
|
1
2
|
import type { Logger } from "./types.js";
|
|
2
3
|
|
|
4
|
+
/**
|
|
5
|
+
* How long an identity field may be before the store stops believing it.
|
|
6
|
+
*
|
|
7
|
+
* `connectorId` and `toolName` are ordinarily operator- and connector-authored,
|
|
8
|
+
* and 128 bytes is far past any real one. But an address that resolved to
|
|
9
|
+
* nothing is recorded *as written*, which puts a caller-authored string in both
|
|
10
|
+
* fields — and "payload-free by construction" has to mean the event type has
|
|
11
|
+
* nowhere to put a payload, not merely that connecta declines to. A 40 KB
|
|
12
|
+
* invented connector id is a payload wearing an id's clothing.
|
|
13
|
+
*
|
|
14
|
+
* Clamped rather than dropped: the invented id is precisely what an operator
|
|
15
|
+
* needs to see, and its first 128 bytes identify the mistake as well as all
|
|
16
|
+
* 40,000 would. The `…` marker keeps a clamped value from reading as a real one.
|
|
17
|
+
*/
|
|
18
|
+
const MAX_ACTIVITY_NAME_BYTES = 128;
|
|
19
|
+
|
|
20
|
+
/** Two names and the dot between them. */
|
|
21
|
+
const MAX_ACTIVITY_ADDRESS_BYTES = MAX_ACTIVITY_NAME_BYTES * 2 + 1;
|
|
22
|
+
|
|
3
23
|
export type ActivityCallSource =
|
|
4
24
|
| "call_tool"
|
|
5
25
|
| "call_destructive_tool"
|
|
26
|
+
// Read-only history. Nothing emits `batch_call` since issue #273 removed the
|
|
27
|
+
// tool, but activity storage is append-only: rows written by older
|
|
28
|
+
// deployments are still read back — the D1 example maps a stored row straight
|
|
29
|
+
// into this type — and an operator's timeline should not have to lie about
|
|
30
|
+
// where a call came from. Never widen this member back into a live source.
|
|
6
31
|
| "batch_call"
|
|
7
32
|
| "execute_code";
|
|
8
33
|
|
|
@@ -12,6 +37,35 @@ export type ActivityOutcome =
|
|
|
12
37
|
| "timeout"
|
|
13
38
|
| "cancelled";
|
|
14
39
|
|
|
40
|
+
export type AgentFriction =
|
|
41
|
+
| "tool_not_found"
|
|
42
|
+
| "schema_retry"
|
|
43
|
+
| "destructive_reroute"
|
|
44
|
+
| "auth_required"
|
|
45
|
+
| "result_too_large";
|
|
46
|
+
|
|
47
|
+
/** Coarse recovery class derived without inspecting payloads or error prose. */
|
|
48
|
+
export function agentFrictionForCode(
|
|
49
|
+
code: string | undefined,
|
|
50
|
+
): AgentFriction | undefined {
|
|
51
|
+
switch (code) {
|
|
52
|
+
case "unknown_address":
|
|
53
|
+
case "unknown_tool":
|
|
54
|
+
case "ambiguous_tool_alias":
|
|
55
|
+
return "tool_not_found";
|
|
56
|
+
case "invalid_args":
|
|
57
|
+
return "schema_retry";
|
|
58
|
+
case "destructive_tool_requires_approval":
|
|
59
|
+
return "destructive_reroute";
|
|
60
|
+
case "auth_required":
|
|
61
|
+
return "auth_required";
|
|
62
|
+
case "result_too_large":
|
|
63
|
+
return "result_too_large";
|
|
64
|
+
default:
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
15
69
|
/**
|
|
16
70
|
* Authenticated identity attached to an activity event. `id` is intentionally
|
|
17
71
|
* optional: open deployments and shared bearer tokens cannot honestly identify
|
|
@@ -48,7 +102,14 @@ export interface ToolCallActivityEvent {
|
|
|
48
102
|
outcome: ActivityOutcome;
|
|
49
103
|
durationMs: number;
|
|
50
104
|
attempts: number;
|
|
105
|
+
/** Set only when the call actually failed; a truncated success has none. */
|
|
51
106
|
errorCode?: string;
|
|
107
|
+
/**
|
|
108
|
+
* Payload-free recovery class. Usually derived from `errorCode`, but it can
|
|
109
|
+
* also stand alone: a result too large to return inline is friction for the
|
|
110
|
+
* agent while remaining an `outcome: "success"` call with no error code.
|
|
111
|
+
*/
|
|
112
|
+
friction?: AgentFriction;
|
|
52
113
|
serverName: string;
|
|
53
114
|
serverVersion: string;
|
|
54
115
|
deploymentId?: string;
|
|
@@ -121,6 +182,7 @@ export type ActivityEventInput = Pick<
|
|
|
121
182
|
| "durationMs"
|
|
122
183
|
| "attempts"
|
|
123
184
|
| "errorCode"
|
|
185
|
+
| "friction"
|
|
124
186
|
>;
|
|
125
187
|
|
|
126
188
|
/**
|
|
@@ -133,20 +195,24 @@ export function recordToolActivity(
|
|
|
133
195
|
input: ActivityEventInput,
|
|
134
196
|
): void {
|
|
135
197
|
if (!context) return;
|
|
198
|
+
// A caller-supplied class wins because it knows something the code table
|
|
199
|
+
// cannot: friction that belongs to a call which did not fail.
|
|
200
|
+
const friction = input.friction ?? agentFrictionForCode(input.errorCode);
|
|
136
201
|
const event: ToolCallActivityEvent = {
|
|
137
202
|
schemaVersion: 1,
|
|
138
203
|
id: crypto.randomUUID(),
|
|
139
204
|
occurredAt: new Date().toISOString(),
|
|
140
205
|
requestId: context.requestId,
|
|
141
206
|
actor: context.actor,
|
|
142
|
-
connectorId: input.connectorId,
|
|
143
|
-
toolName: input.toolName,
|
|
144
|
-
address: input.address,
|
|
207
|
+
connectorId: boundedEchoText(input.connectorId, MAX_ACTIVITY_NAME_BYTES),
|
|
208
|
+
toolName: boundedEchoText(input.toolName, MAX_ACTIVITY_NAME_BYTES),
|
|
209
|
+
address: boundedEchoText(input.address, MAX_ACTIVITY_ADDRESS_BYTES),
|
|
145
210
|
source: input.source,
|
|
146
211
|
outcome: input.outcome,
|
|
147
212
|
durationMs: Math.max(0, Math.trunc(input.durationMs)),
|
|
148
213
|
attempts: Math.max(1, Math.trunc(input.attempts)),
|
|
149
214
|
...(input.errorCode ? { errorCode: input.errorCode } : {}),
|
|
215
|
+
...(friction ? { friction } : {}),
|
|
150
216
|
serverName: context.serverInfo.name,
|
|
151
217
|
serverVersion: context.serverInfo.version,
|
|
152
218
|
...(context.deploymentId
|
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,13 @@ const MAX_QUERY_ANALYSIS_TERM_LENGTH = 64;
|
|
|
44
48
|
|
|
45
49
|
const encoder = new TextEncoder();
|
|
46
50
|
|
|
51
|
+
/**
|
|
52
|
+
* The discovery route a routing failure should send a caller back through. Same
|
|
53
|
+
* catalog logic serves both the top-level `search_tools` path and the
|
|
54
|
+
* in-program `connecta.search` path, so callers pass the route they own.
|
|
55
|
+
*/
|
|
56
|
+
export type SearchRoute = "search_tools" | "connecta.search";
|
|
57
|
+
|
|
47
58
|
export class DiscoveryPolicyError extends Error {
|
|
48
59
|
constructor(
|
|
49
60
|
readonly code: "invalid_args" | "result_too_large",
|
|
@@ -94,12 +105,26 @@ function discoveryAddresses(args: CatalogDescribeArgs): unknown[] {
|
|
|
94
105
|
if (value.length > MAX_DESCRIBE_ADDRESSES) {
|
|
95
106
|
throw new DiscoveryPolicyError(
|
|
96
107
|
"invalid_args",
|
|
97
|
-
`addresses must contain at most ${MAX_DESCRIBE_ADDRESSES} entries. Split a larger list across
|
|
108
|
+
`addresses must contain at most ${MAX_DESCRIBE_ADDRESSES} entries. Split a larger list across connecta.describe calls.`,
|
|
98
109
|
);
|
|
99
110
|
}
|
|
100
111
|
return value;
|
|
101
112
|
}
|
|
102
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Search terms derived from an address the catalog could not resolve. Bounded
|
|
116
|
+
* because the address is entirely caller-authored: an invented one can be any
|
|
117
|
+
* length, and this string is copied into a recovery record that is itself
|
|
118
|
+
* copied into both halves of the result envelope.
|
|
119
|
+
*/
|
|
120
|
+
function recoveryQuery(address: string): string {
|
|
121
|
+
const separator = address.indexOf(".");
|
|
122
|
+
const candidate = separator >= 0 ? address.slice(separator + 1) : address;
|
|
123
|
+
return boundedEchoText(
|
|
124
|
+
candidate.replaceAll(/[._-]+/g, " ").trim() || address,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
103
128
|
/** Serialize once and count the exact bytes the MCP adapter would emit. */
|
|
104
129
|
export function boundedDiscoveryText(value: unknown, hint: string): string {
|
|
105
130
|
const text = JSON.stringify(value);
|
|
@@ -286,6 +311,7 @@ export class CatalogService {
|
|
|
286
311
|
readonly requestScope: object;
|
|
287
312
|
private readonly probeTimeoutMs: number;
|
|
288
313
|
private readonly concurrency: number;
|
|
314
|
+
private readonly searchRoute: SearchRoute;
|
|
289
315
|
private readonly loaded = new Map<string, ToolDef[]>();
|
|
290
316
|
private readonly loading = new Map<string, Promise<ToolDef[]>>();
|
|
291
317
|
|
|
@@ -296,12 +322,40 @@ export class CatalogService {
|
|
|
296
322
|
requestScope?: object;
|
|
297
323
|
probeTimeoutMs?: number;
|
|
298
324
|
concurrency?: number;
|
|
325
|
+
/** The discovery route recovery records name. Default `search_tools`. */
|
|
326
|
+
searchRoute?: SearchRoute;
|
|
299
327
|
} = {},
|
|
300
328
|
) {
|
|
301
329
|
this.requestScope = options.requestScope ?? {};
|
|
302
330
|
this.probeTimeoutMs =
|
|
303
331
|
normalizeTimeoutMs(options.probeTimeoutMs) ?? DEFAULT_PROBE_TIMEOUT_MS;
|
|
304
332
|
this.concurrency = resolveDiscoveryConcurrency(options.concurrency);
|
|
333
|
+
this.searchRoute = options.searchRoute ?? "search_tools";
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Send a caller back to discovery through the surface it can actually reach.
|
|
338
|
+
* Both variants carry the same scoping arguments because `connecta.search`
|
|
339
|
+
* takes the same ones `search_tools` does; only the key naming the callable
|
|
340
|
+
* differs, the way the ambiguous-alias record already names a function.
|
|
341
|
+
*
|
|
342
|
+
* Not private: `InvocationService` builds the same class of record when a
|
|
343
|
+
* call fails schema validation, and it is this catalog's route that decides
|
|
344
|
+
* which key that record carries. Duplicating the branch there would let the
|
|
345
|
+
* two drift.
|
|
346
|
+
*/
|
|
347
|
+
searchRecovery(
|
|
348
|
+
args: { query: string; connector?: string },
|
|
349
|
+
purpose: string,
|
|
350
|
+
): NonNullable<CallErrorDetails["nextAction"]> {
|
|
351
|
+
const searchArgs = {
|
|
352
|
+
query: args.query,
|
|
353
|
+
...(args.connector !== undefined ? { connector: args.connector } : {}),
|
|
354
|
+
includeSchemas: "compact" as const,
|
|
355
|
+
};
|
|
356
|
+
return this.searchRoute === "connecta.search"
|
|
357
|
+
? { function: "connecta.search", arguments: searchArgs, purpose }
|
|
358
|
+
: { tool: "search_tools", arguments: searchArgs, purpose };
|
|
305
359
|
}
|
|
306
360
|
|
|
307
361
|
async loadConnector(
|
|
@@ -345,10 +399,16 @@ export class CatalogService {
|
|
|
345
399
|
if (!resolved) {
|
|
346
400
|
return {
|
|
347
401
|
ok: false,
|
|
348
|
-
error:
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
402
|
+
error: {
|
|
403
|
+
...framingError(
|
|
404
|
+
"unknown_address",
|
|
405
|
+
`Unknown address "${boundedEchoText(address)}"`,
|
|
406
|
+
),
|
|
407
|
+
nextAction: this.searchRecovery(
|
|
408
|
+
{ query: recoveryQuery(address) },
|
|
409
|
+
"Find the configured canonical address before retrying.",
|
|
410
|
+
),
|
|
411
|
+
},
|
|
352
412
|
catalogMs: 0,
|
|
353
413
|
};
|
|
354
414
|
}
|
|
@@ -370,10 +430,19 @@ export class CatalogService {
|
|
|
370
430
|
if (!definition) {
|
|
371
431
|
return {
|
|
372
432
|
ok: false,
|
|
373
|
-
error:
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
433
|
+
error: {
|
|
434
|
+
...framingError(
|
|
435
|
+
"unknown_tool",
|
|
436
|
+
`Unknown tool "${boundedEchoText(resolved.toolName)}" on connector "${resolved.connector.id}"`,
|
|
437
|
+
),
|
|
438
|
+
nextAction: this.searchRecovery(
|
|
439
|
+
{
|
|
440
|
+
query: recoveryQuery(resolved.toolName),
|
|
441
|
+
connector: resolved.connector.id,
|
|
442
|
+
},
|
|
443
|
+
"Find the connector's current canonical tool address.",
|
|
444
|
+
),
|
|
445
|
+
},
|
|
377
446
|
catalogMs: Date.now() - started,
|
|
378
447
|
connector: resolved.connector,
|
|
379
448
|
toolName: resolved.toolName,
|
|
@@ -405,10 +474,16 @@ export class CatalogService {
|
|
|
405
474
|
if (!connector) {
|
|
406
475
|
return {
|
|
407
476
|
ok: false,
|
|
408
|
-
error:
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
477
|
+
error: {
|
|
478
|
+
...framingError(
|
|
479
|
+
"unknown_address",
|
|
480
|
+
`Unknown address "${boundedEchoText(`${connectorId}.${alias}`)}"`,
|
|
481
|
+
),
|
|
482
|
+
nextAction: this.searchRecovery(
|
|
483
|
+
{ query: recoveryQuery(alias) },
|
|
484
|
+
"Find the configured canonical address before retrying.",
|
|
485
|
+
),
|
|
486
|
+
},
|
|
412
487
|
catalogMs: 0,
|
|
413
488
|
};
|
|
414
489
|
}
|
|
@@ -432,10 +507,16 @@ export class CatalogService {
|
|
|
432
507
|
if (!definition) {
|
|
433
508
|
return {
|
|
434
509
|
ok: false,
|
|
435
|
-
error:
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
510
|
+
error: {
|
|
511
|
+
...framingError(
|
|
512
|
+
"unknown_tool",
|
|
513
|
+
`Unknown tool "${boundedEchoText(alias)}" on connector "${connector.id}"`,
|
|
514
|
+
),
|
|
515
|
+
nextAction: this.searchRecovery(
|
|
516
|
+
{ query: recoveryQuery(alias), connector: connector.id },
|
|
517
|
+
"Find the connector's current canonical tool address.",
|
|
518
|
+
),
|
|
519
|
+
},
|
|
439
520
|
catalogMs: Date.now() - started,
|
|
440
521
|
connector,
|
|
441
522
|
toolName: alias,
|
|
@@ -449,8 +530,16 @@ export class CatalogService {
|
|
|
449
530
|
ok: false,
|
|
450
531
|
error: {
|
|
451
532
|
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.`,
|
|
533
|
+
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
534
|
retryable: false,
|
|
535
|
+
nextAction: {
|
|
536
|
+
function: "connecta.call",
|
|
537
|
+
addresses: [definition, ...collisions].map(
|
|
538
|
+
(tool) => `${connector.id}.${tool.name}`,
|
|
539
|
+
),
|
|
540
|
+
purpose:
|
|
541
|
+
"Choose the intended canonical address and call it with the original arguments.",
|
|
542
|
+
},
|
|
454
543
|
},
|
|
455
544
|
catalogMs: Date.now() - started,
|
|
456
545
|
connector,
|
|
@@ -486,6 +575,10 @@ export class CatalogService {
|
|
|
486
575
|
connectors,
|
|
487
576
|
this.concurrency,
|
|
488
577
|
(connector) =>
|
|
578
|
+
// Unlike the describe path, this label never reaches a caller: search
|
|
579
|
+
// only counts rejected catalogs (`unavailableCatalogs` below) and
|
|
580
|
+
// renders its own guidance, so the folded name here stays internal and
|
|
581
|
+
// needs no surface awareness.
|
|
489
582
|
this.loadForDiscovery(
|
|
490
583
|
connector.id,
|
|
491
584
|
`search_tools probe of "${connector.id}"`,
|
|
@@ -721,7 +814,7 @@ export class CatalogService {
|
|
|
721
814
|
connectorIds,
|
|
722
815
|
this.concurrency,
|
|
723
816
|
(id) =>
|
|
724
|
-
this.loadForDiscovery(id, `
|
|
817
|
+
this.loadForDiscovery(id, `connecta.describe probe of "${id}"`),
|
|
725
818
|
);
|
|
726
819
|
const catalogs = new Map<string, ToolDef[] | Error>();
|
|
727
820
|
loaded.forEach((result, index) => {
|
package/src/connectors/api.ts
CHANGED
|
@@ -20,7 +20,7 @@ export interface ApiTool {
|
|
|
20
20
|
outputSchema?: JsonSchema;
|
|
21
21
|
/**
|
|
22
22
|
* Standard MCP-style behavior hints. Only an explicit readOnlyHint: true
|
|
23
|
-
* admits the tool to call_tool
|
|
23
|
+
* admits the tool to call_tool and execute_code.
|
|
24
24
|
*/
|
|
25
25
|
annotations?: ToolAnnotations;
|
|
26
26
|
handler: (args: any, ctx: ConnectorContext) => Promise<unknown> | unknown;
|
|
@@ -32,7 +32,7 @@ export interface ApiOptions {
|
|
|
32
32
|
description?: string;
|
|
33
33
|
/**
|
|
34
34
|
* Max inline result size (bytes) for this connector's tools before
|
|
35
|
-
* call_tool
|
|
35
|
+
* call_tool truncates and stashes the full text for get_result
|
|
36
36
|
* paging. Overrides the deployment's `calls.maxResultBytes`; omit to inherit
|
|
37
37
|
* it. Must be a whole number of bytes >= 1; anything else warns at startup
|
|
38
38
|
* and is ignored.
|
|
@@ -38,7 +38,7 @@ export interface RemoteMcpOptions {
|
|
|
38
38
|
description?: string;
|
|
39
39
|
/**
|
|
40
40
|
* Max inline result size (bytes) for this connector's tools before
|
|
41
|
-
* call_tool
|
|
41
|
+
* call_tool truncates and stashes the full text for get_result
|
|
42
42
|
* paging. Overrides the deployment's `calls.maxResultBytes`; omit to inherit
|
|
43
43
|
* it. Must be a whole number of bytes >= 1; anything else warns at startup
|
|
44
44
|
* and is ignored.
|
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 {
|
|
@@ -151,7 +221,7 @@ export class ConnectorCallError extends Error {
|
|
|
151
221
|
}
|
|
152
222
|
}
|
|
153
223
|
|
|
154
|
-
/** The `error` object surfaced in
|
|
224
|
+
/** The `error` object surfaced in value-mode call results and connecta.batch entries. */
|
|
155
225
|
export interface CallErrorDetails {
|
|
156
226
|
code: string;
|
|
157
227
|
message: string;
|
|
@@ -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;
|
|
@@ -221,7 +322,7 @@ const RETRYABLE_MESSAGE_RE =
|
|
|
221
322
|
const TIMEOUT_MESSAGE_RE = /timed out|timeout/i;
|
|
222
323
|
|
|
223
324
|
/** Message-text fallback used when an error carries no typed classification. */
|
|
224
|
-
|
|
325
|
+
function messageLooksRetryable(message: string): boolean {
|
|
225
326
|
return RETRYABLE_MESSAGE_RE.test(message);
|
|
226
327
|
}
|
|
227
328
|
|