@zackbart/connecta 0.7.9 → 0.8.1
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/README.md +47 -148
- package/dist/activity.d.ts +0 -8
- package/dist/activity.d.ts.map +1 -1
- package/dist/activity.js +0 -1
- package/dist/activity.js.map +1 -1
- package/dist/auth/bearer.d.ts +1 -7
- package/dist/auth/bearer.d.ts.map +1 -1
- package/dist/auth/bearer.js +3 -24
- package/dist/auth/bearer.js.map +1 -1
- package/dist/auth/clerk.d.ts +2 -11
- package/dist/auth/clerk.d.ts.map +1 -1
- package/dist/auth/clerk.js +31 -46
- package/dist/auth/clerk.js.map +1 -1
- package/dist/call-admission.d.ts.map +1 -1
- package/dist/call-admission.js +9 -4
- package/dist/call-admission.js.map +1 -1
- package/dist/catalog-fingerprint.d.ts +20 -0
- package/dist/catalog-fingerprint.d.ts.map +1 -0
- package/dist/catalog-fingerprint.js +34 -0
- package/dist/catalog-fingerprint.js.map +1 -0
- package/dist/catalog-limits.d.ts +10 -0
- package/dist/catalog-limits.d.ts.map +1 -0
- package/dist/catalog-limits.js +10 -0
- package/dist/catalog-limits.js.map +1 -0
- package/dist/catalog-service.d.ts +137 -0
- package/dist/catalog-service.d.ts.map +1 -0
- package/dist/catalog-service.js +401 -0
- package/dist/catalog-service.js.map +1 -0
- package/dist/catalog.d.ts +6 -0
- package/dist/catalog.d.ts.map +1 -1
- package/dist/catalog.js +53 -2
- package/dist/catalog.js.map +1 -1
- package/dist/concurrency.d.ts +4 -0
- package/dist/concurrency.d.ts.map +1 -0
- package/dist/concurrency.js +31 -0
- package/dist/concurrency.js.map +1 -0
- package/dist/connectors/api.d.ts.map +1 -1
- package/dist/connectors/api.js +22 -12
- package/dist/connectors/api.js.map +1 -1
- package/dist/connectors/remote-mcp.d.ts.map +1 -1
- package/dist/connectors/remote-mcp.js +53 -41
- package/dist/connectors/remote-mcp.js.map +1 -1
- package/dist/credentials.d.ts +3 -2
- package/dist/credentials.d.ts.map +1 -1
- package/dist/errors.d.ts +19 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/execute.d.ts +10 -16
- package/dist/execute.d.ts.map +1 -1
- package/dist/execute.js +143 -330
- package/dist/execute.js.map +1 -1
- package/dist/executor-result.d.ts +0 -3
- package/dist/executor-result.d.ts.map +1 -1
- package/dist/executor-result.js +3 -3
- package/dist/executor-result.js.map +1 -1
- package/dist/executors/quickjs-child.js +3 -3
- package/dist/executors/quickjs-child.js.map +1 -1
- package/dist/executors/quickjs-protocol.d.ts +4 -1
- package/dist/executors/quickjs-protocol.d.ts.map +1 -1
- package/dist/executors/quickjs-runtime.d.ts.map +1 -1
- package/dist/executors/quickjs-runtime.js +7 -2
- package/dist/executors/quickjs-runtime.js.map +1 -1
- package/dist/executors/quickjs.d.ts.map +1 -1
- package/dist/executors/quickjs.js +4 -1
- package/dist/executors/quickjs.js.map +1 -1
- package/dist/favicon.js +2 -2
- package/dist/favicon.js.map +1 -1
- package/dist/index.d.ts +8 -82
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +58 -104
- package/dist/index.js.map +1 -1
- package/dist/invocation.d.ts +101 -0
- package/dist/invocation.d.ts.map +1 -0
- package/dist/invocation.js +405 -0
- package/dist/invocation.js.map +1 -0
- package/dist/meta-tools.d.ts +9 -95
- package/dist/meta-tools.d.ts.map +1 -1
- package/dist/meta-tools.js +235 -725
- package/dist/meta-tools.js.map +1 -1
- package/dist/operator-ui/generated.d.ts +3 -0
- package/dist/operator-ui/generated.d.ts.map +1 -0
- package/dist/operator-ui/generated.js +5 -0
- package/dist/operator-ui/generated.js.map +1 -0
- package/dist/operator-ui/model.d.ts +73 -0
- package/dist/operator-ui/model.d.ts.map +1 -0
- package/dist/operator-ui/model.js +28 -0
- package/dist/operator-ui/model.js.map +1 -0
- package/dist/registry.d.ts +33 -184
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +345 -330
- package/dist/registry.js.map +1 -1
- package/dist/retired-toolkits.d.ts +3 -0
- package/dist/retired-toolkits.d.ts.map +1 -0
- package/dist/retired-toolkits.js +9 -0
- package/dist/retired-toolkits.js.map +1 -0
- package/dist/routes/activity.d.ts +3 -0
- package/dist/routes/activity.d.ts.map +1 -0
- package/dist/routes/activity.js +170 -0
- package/dist/routes/activity.js.map +1 -0
- package/dist/routes/credentials.d.ts +3 -0
- package/dist/routes/credentials.d.ts.map +1 -0
- package/dist/routes/credentials.js +170 -0
- package/dist/routes/credentials.js.map +1 -0
- package/dist/routes/mcp.d.ts +8 -0
- package/dist/routes/mcp.d.ts.map +1 -0
- package/dist/routes/mcp.js +270 -0
- package/dist/routes/mcp.js.map +1 -0
- package/dist/routes/oauth.d.ts +4 -0
- package/dist/routes/oauth.d.ts.map +1 -0
- package/dist/routes/oauth.js +263 -0
- package/dist/routes/oauth.js.map +1 -0
- package/dist/routes/shared.d.ts +76 -0
- package/dist/routes/shared.d.ts.map +1 -0
- package/dist/routes/shared.js +140 -0
- package/dist/routes/shared.js.map +1 -0
- package/dist/routes/ui.d.ts +3 -0
- package/dist/routes/ui.d.ts.map +1 -0
- package/dist/routes/ui.js +116 -0
- package/dist/routes/ui.js.map +1 -0
- package/dist/server.d.ts +9 -40
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +62 -1322
- package/dist/server.js.map +1 -1
- package/dist/skills.d.ts +3 -21
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +15 -43
- package/dist/skills.js.map +1 -1
- package/dist/storage/file.d.ts.map +1 -1
- package/dist/storage/file.js +3 -1
- package/dist/storage/file.js.map +1 -1
- package/dist/storage/memory.d.ts.map +1 -1
- package/dist/storage/memory.js +3 -1
- package/dist/storage/memory.js.map +1 -1
- package/dist/timeout.d.ts.map +1 -1
- package/dist/timeout.js +2 -4
- package/dist/timeout.js.map +1 -1
- package/dist/types.d.ts +9 -59
- package/dist/types.d.ts.map +1 -1
- package/dist/ui.d.ts +3 -94
- package/dist/ui.d.ts.map +1 -1
- package/dist/ui.js +30 -1427
- package/dist/ui.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +15 -8
- package/src/activity.ts +0 -9
- package/src/auth/bearer.ts +4 -33
- package/src/auth/clerk.ts +35 -48
- package/src/call-admission.ts +8 -6
- package/src/catalog-fingerprint.ts +52 -0
- package/src/catalog-limits.ts +11 -0
- package/src/catalog-service.ts +601 -0
- package/src/catalog.ts +57 -2
- package/src/concurrency.ts +39 -0
- package/src/connectors/api.ts +22 -12
- package/src/connectors/remote-mcp.ts +63 -45
- package/src/credentials.ts +2 -2
- package/src/errors.ts +21 -1
- package/src/execute.ts +193 -423
- package/src/executor-result.ts +3 -3
- package/src/executors/quickjs-child.ts +8 -4
- package/src/executors/quickjs-protocol.ts +1 -1
- package/src/executors/quickjs-runtime.ts +5 -1
- package/src/executors/quickjs.ts +11 -8
- package/src/favicon.ts +2 -2
- package/src/index.ts +70 -222
- package/src/invocation.ts +580 -0
- package/src/meta-tools.ts +393 -985
- package/src/operator-ui/browser.css +360 -0
- package/src/operator-ui/browser.ts +987 -0
- package/src/operator-ui/generated.ts +4 -0
- package/src/operator-ui/model.ts +105 -0
- package/src/registry.ts +459 -439
- package/src/retired-toolkits.ts +14 -0
- package/src/routes/activity.ts +215 -0
- package/src/routes/credentials.ts +254 -0
- package/src/routes/mcp.ts +339 -0
- package/src/routes/oauth.ts +322 -0
- package/src/routes/shared.ts +240 -0
- package/src/routes/ui.ts +143 -0
- package/src/server.ts +64 -1731
- package/src/skills.ts +15 -43
- package/src/storage/file.ts +3 -1
- package/src/storage/memory.ts +3 -1
- package/src/timeout.ts +2 -4
- package/src/types.ts +8 -60
- package/src/ui.ts +60 -1538
- package/src/version.ts +1 -1
- package/dist/credential-health.d.ts +0 -227
- package/dist/credential-health.d.ts.map +0 -1
- package/dist/credential-health.js +0 -606
- package/dist/credential-health.js.map +0 -1
- package/dist/toolkits.d.ts +0 -146
- package/dist/toolkits.d.ts.map +0 -1
- package/dist/toolkits.js +0 -322
- package/dist/toolkits.js.map +0 -1
- package/src/credential-health.ts +0 -832
- package/src/toolkits.ts +0 -461
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
import {
|
|
2
|
+
recordToolActivity,
|
|
3
|
+
type ActivityCallSource,
|
|
4
|
+
type ActivityRequestContext,
|
|
5
|
+
} from "./activity.js";
|
|
6
|
+
import { isCallAdmissionError } from "./call-admission.js";
|
|
7
|
+
import {
|
|
8
|
+
CatalogService,
|
|
9
|
+
type CatalogResolution,
|
|
10
|
+
type ResolvedCatalogTool,
|
|
11
|
+
} from "./catalog-service.js";
|
|
12
|
+
import {
|
|
13
|
+
classifyCallError,
|
|
14
|
+
ConnectorCallError,
|
|
15
|
+
messageLooksRetryable,
|
|
16
|
+
type AuthRecoveryMode,
|
|
17
|
+
type CallErrorDetails,
|
|
18
|
+
} from "./errors.js";
|
|
19
|
+
import { unwrapMcpResult } from "./mcp-result.js";
|
|
20
|
+
import type { RegistryView } from "./registry.js";
|
|
21
|
+
import type { ToolDef } from "./types.js";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The longest the engine will park a synchronous inbound request in *waiting
|
|
25
|
+
* alone*. The engine already treats ~15 s as the outer bound of one reasonable
|
|
26
|
+
* connector call (EXECUTE_HOST_CALL_TIMEOUT_MS), so sleeping for minutes trades
|
|
27
|
+
* a fast, informative failure for a hung one. A connector-reported window this
|
|
28
|
+
* long isn't truncated — it's declined (see `retryBackoffMs`) and reported
|
|
29
|
+
* verbatim as `error.retryAfterMs`, so the agent, which can afford to wait,
|
|
30
|
+
* decides when to re-issue.
|
|
31
|
+
*/
|
|
32
|
+
export const MAX_RETRY_BACKOFF_MS = 10_000;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* How long to wait before the next attempt, or `undefined` for "don't retry".
|
|
36
|
+
*
|
|
37
|
+
* A connector that read a `Retry-After` header knows the window exactly, so it
|
|
38
|
+
* is honoured **exactly or not at all**: truncating an exponential *guess* is
|
|
39
|
+
* harmless, but truncating a *known* window means deliberately retrying inside
|
|
40
|
+
* a rate limit — the harm this channel exists to prevent. A window longer than
|
|
41
|
+
* `MAX_RETRY_BACKOFF_MS` therefore declines the retry rather than shortening
|
|
42
|
+
* it. (`retryAfterMs` is normalized non-negative, so `0` means "retry now".)
|
|
43
|
+
* Connectors that report no window keep the historical exponential guess.
|
|
44
|
+
*
|
|
45
|
+
* Waits are per attempt, matching the per-attempt `timeoutMs` race in
|
|
46
|
+
* `InvocationService.invoke`. Exported for direct testing.
|
|
47
|
+
*/
|
|
48
|
+
export function retryBackoffMs(
|
|
49
|
+
attempt: number,
|
|
50
|
+
retryAfterMs: number | undefined,
|
|
51
|
+
): number | undefined {
|
|
52
|
+
if (retryAfterMs === undefined) {
|
|
53
|
+
return Math.min(250 * 2 ** (attempt - 1), 1_000);
|
|
54
|
+
}
|
|
55
|
+
return retryAfterMs <= MAX_RETRY_BACKOFF_MS ? retryAfterMs : undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The one fail-closed admission predicate for every invocation adapter. */
|
|
59
|
+
function isExplicitlyReadOnly(definition: ToolDef): boolean {
|
|
60
|
+
return (
|
|
61
|
+
definition.annotations?.readOnlyHint === true &&
|
|
62
|
+
definition.annotations?.destructiveHint !== true
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function retrySafe(definition: ToolDef): boolean {
|
|
67
|
+
return (
|
|
68
|
+
definition.annotations?.readOnlyHint === true ||
|
|
69
|
+
definition.annotations?.idempotentHint === true
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function framingError(code: string, message: string): CallErrorDetails {
|
|
74
|
+
return { code, message, retryable: messageLooksRetryable(message) };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function callerCancelledDetails(): CallErrorDetails {
|
|
78
|
+
return {
|
|
79
|
+
code: "cancelled",
|
|
80
|
+
message: "Tool call was cancelled by the caller.",
|
|
81
|
+
retryable: false,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function recoveryMode(
|
|
86
|
+
registry: RegistryView,
|
|
87
|
+
connector: ResolvedCatalogTool["connector"],
|
|
88
|
+
baseUrl: string,
|
|
89
|
+
): AuthRecoveryMode {
|
|
90
|
+
if (connector.startAuth) return "oauth";
|
|
91
|
+
if (
|
|
92
|
+
connector.credential &&
|
|
93
|
+
registry.contextFor(connector.id, baseUrl).credential
|
|
94
|
+
) {
|
|
95
|
+
return "operator_config";
|
|
96
|
+
}
|
|
97
|
+
return "unavailable";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function isCallerCancellation(
|
|
101
|
+
error: unknown,
|
|
102
|
+
signal: AbortSignal | undefined,
|
|
103
|
+
): boolean {
|
|
104
|
+
return (
|
|
105
|
+
signal?.aborted === true ||
|
|
106
|
+
(isCallAdmissionError(error) && error.admissionKind === "cancelled")
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function assertRawMcpSuccess(
|
|
111
|
+
kind: ResolvedCatalogTool["connector"]["kind"],
|
|
112
|
+
result: unknown,
|
|
113
|
+
): void {
|
|
114
|
+
if (kind !== "mcp" || result == null || typeof result !== "object") return;
|
|
115
|
+
const mcpResult = result as {
|
|
116
|
+
content?: Array<{ type?: string; text?: string }>;
|
|
117
|
+
isError?: boolean;
|
|
118
|
+
};
|
|
119
|
+
if (!mcpResult.isError) return;
|
|
120
|
+
throw new Error(
|
|
121
|
+
mcpResult.content
|
|
122
|
+
?.filter((block) => block.type === "text")
|
|
123
|
+
.map((block) => block.text ?? "")
|
|
124
|
+
.join("") || "Downstream tool call failed",
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface InvocationTiming {
|
|
129
|
+
catalogMs: number;
|
|
130
|
+
admissionMs: number;
|
|
131
|
+
connectorMs: number;
|
|
132
|
+
backoffMs: number;
|
|
133
|
+
resultProcessingMs: number;
|
|
134
|
+
totalMs: number;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
interface InvocationBase {
|
|
138
|
+
durationMs: number;
|
|
139
|
+
attempts: number;
|
|
140
|
+
timing: InvocationTiming;
|
|
141
|
+
resolved?: ResolvedCatalogTool;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export type InvocationOutcome<T> =
|
|
145
|
+
| (InvocationBase & { ok: true; value: T; resolved: ResolvedCatalogTool })
|
|
146
|
+
| (InvocationBase & { ok: false; error: CallErrorDetails });
|
|
147
|
+
|
|
148
|
+
export interface InvocationContext<T> {
|
|
149
|
+
source: ActivityCallSource;
|
|
150
|
+
allowDestructive?: boolean;
|
|
151
|
+
timeoutMs?: number;
|
|
152
|
+
maxRetries?: number;
|
|
153
|
+
requestSignal?: AbortSignal;
|
|
154
|
+
unwrapResult?: boolean;
|
|
155
|
+
/**
|
|
156
|
+
* Caller-owned result policy. MCP passes fields + paging here; code mode
|
|
157
|
+
* normally accepts the already-unwrapped value unchanged.
|
|
158
|
+
*/
|
|
159
|
+
processResult?: (
|
|
160
|
+
value: unknown,
|
|
161
|
+
resolved: ResolvedCatalogTool,
|
|
162
|
+
) => T | Promise<T>;
|
|
163
|
+
/**
|
|
164
|
+
* Called after address/catalog/safety admission and before the first provider
|
|
165
|
+
* attempt. Code mode uses it for its host-call budget.
|
|
166
|
+
*/
|
|
167
|
+
beforeDispatch?: () => void;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export class InvocationFailure extends Error {
|
|
171
|
+
readonly code: string;
|
|
172
|
+
readonly retryable: boolean;
|
|
173
|
+
readonly retryAfterMs: number | undefined;
|
|
174
|
+
readonly connector: string | undefined;
|
|
175
|
+
readonly operation: string | undefined;
|
|
176
|
+
readonly recovery: CallErrorDetails["recovery"];
|
|
177
|
+
readonly nextAction: CallErrorDetails["nextAction"];
|
|
178
|
+
readonly retry: string | undefined;
|
|
179
|
+
|
|
180
|
+
constructor(readonly details: CallErrorDetails) {
|
|
181
|
+
super(details.message);
|
|
182
|
+
this.name = "InvocationFailure";
|
|
183
|
+
this.code = details.code;
|
|
184
|
+
this.retryable = details.retryable;
|
|
185
|
+
this.retryAfterMs = details.retryAfterMs;
|
|
186
|
+
this.connector = details.connector;
|
|
187
|
+
this.operation = details.operation;
|
|
188
|
+
this.recovery = details.recovery;
|
|
189
|
+
this.nextAction = details.nextAction;
|
|
190
|
+
this.retry = details.retry;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Shared downstream invocation engine. Address/catalog resolution is delegated
|
|
196
|
+
* to the request-local CatalogService; every remaining call semantic lives
|
|
197
|
+
* here so MCP and code mode cannot drift independently.
|
|
198
|
+
*/
|
|
199
|
+
export class InvocationService {
|
|
200
|
+
constructor(
|
|
201
|
+
private readonly registry: RegistryView,
|
|
202
|
+
private readonly catalog: CatalogService,
|
|
203
|
+
private readonly activity?: ActivityRequestContext,
|
|
204
|
+
) {}
|
|
205
|
+
|
|
206
|
+
async invoke<T = unknown>(
|
|
207
|
+
address: string,
|
|
208
|
+
args: unknown,
|
|
209
|
+
context: InvocationContext<T>,
|
|
210
|
+
): Promise<InvocationOutcome<T>> {
|
|
211
|
+
return this.invokeWithResolution(address, args, context, () =>
|
|
212
|
+
this.catalog.resolveTool(
|
|
213
|
+
address,
|
|
214
|
+
context.requestSignal !== undefined
|
|
215
|
+
? { signal: context.requestSignal }
|
|
216
|
+
: {},
|
|
217
|
+
),
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Code-mode namespace dispatch preserves JavaScript-safe tool aliases while
|
|
223
|
+
* still feeding the resolved catalog entry through the one invocation path.
|
|
224
|
+
*/
|
|
225
|
+
async invokeToolAlias<T = unknown>(
|
|
226
|
+
connectorId: string,
|
|
227
|
+
toolAlias: string,
|
|
228
|
+
aliasFor: (toolName: string) => string,
|
|
229
|
+
args: unknown,
|
|
230
|
+
context: InvocationContext<T>,
|
|
231
|
+
): Promise<InvocationOutcome<T>> {
|
|
232
|
+
return this.invokeWithResolution(
|
|
233
|
+
`${connectorId}.${toolAlias}`,
|
|
234
|
+
args,
|
|
235
|
+
context,
|
|
236
|
+
() =>
|
|
237
|
+
this.catalog.resolveToolAlias(
|
|
238
|
+
connectorId,
|
|
239
|
+
toolAlias,
|
|
240
|
+
aliasFor,
|
|
241
|
+
context.requestSignal !== undefined
|
|
242
|
+
? { signal: context.requestSignal }
|
|
243
|
+
: {},
|
|
244
|
+
),
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
private async invokeWithResolution<T>(
|
|
249
|
+
address: string,
|
|
250
|
+
args: unknown,
|
|
251
|
+
context: InvocationContext<T>,
|
|
252
|
+
resolve: () => Promise<CatalogResolution>,
|
|
253
|
+
): Promise<InvocationOutcome<T>> {
|
|
254
|
+
const started = Date.now();
|
|
255
|
+
let catalogMs = 0;
|
|
256
|
+
let admissionMs = 0;
|
|
257
|
+
let connectorMs = 0;
|
|
258
|
+
let backoffMs = 0;
|
|
259
|
+
let resultProcessingMs = 0;
|
|
260
|
+
let attempts = 0;
|
|
261
|
+
let resolved: ResolvedCatalogTool | undefined;
|
|
262
|
+
let activityTarget:
|
|
263
|
+
| Pick<ResolvedCatalogTool, "connector" | "toolName">
|
|
264
|
+
| undefined;
|
|
265
|
+
const timing = (): InvocationTiming => ({
|
|
266
|
+
catalogMs,
|
|
267
|
+
admissionMs,
|
|
268
|
+
connectorMs,
|
|
269
|
+
backoffMs,
|
|
270
|
+
resultProcessingMs,
|
|
271
|
+
totalMs: Date.now() - started,
|
|
272
|
+
});
|
|
273
|
+
const record = (
|
|
274
|
+
outcome: "success" | "error" | "timeout" | "cancelled",
|
|
275
|
+
errorCode?: string,
|
|
276
|
+
) => {
|
|
277
|
+
if (!activityTarget) return;
|
|
278
|
+
recordToolActivity(this.activity, {
|
|
279
|
+
connectorId: activityTarget.connector.id,
|
|
280
|
+
toolName: activityTarget.toolName,
|
|
281
|
+
address: `${activityTarget.connector.id}.${activityTarget.toolName}`,
|
|
282
|
+
source: context.source,
|
|
283
|
+
outcome,
|
|
284
|
+
durationMs: Date.now() - started,
|
|
285
|
+
attempts,
|
|
286
|
+
...(errorCode ? { errorCode } : {}),
|
|
287
|
+
});
|
|
288
|
+
};
|
|
289
|
+
const failed = (error: CallErrorDetails): InvocationOutcome<T> => {
|
|
290
|
+
const diagnostics = timing();
|
|
291
|
+
const target = resolved ?? activityTarget;
|
|
292
|
+
const details =
|
|
293
|
+
error.code === "auth_required" && target
|
|
294
|
+
? {
|
|
295
|
+
...error,
|
|
296
|
+
connector: target.connector.id,
|
|
297
|
+
operation: `${target.connector.id}.${target.toolName}`,
|
|
298
|
+
recovery: recoveryMode(
|
|
299
|
+
this.registry,
|
|
300
|
+
target.connector,
|
|
301
|
+
this.catalog.baseUrl,
|
|
302
|
+
),
|
|
303
|
+
nextAction: {
|
|
304
|
+
tool: "authorize_connector" as const,
|
|
305
|
+
arguments: { connector: target.connector.id },
|
|
306
|
+
operatorHandoff:
|
|
307
|
+
"Give the URL and instructions it returns to the operator.",
|
|
308
|
+
},
|
|
309
|
+
retry:
|
|
310
|
+
`Retry ${target.connector.id}.${target.toolName} after ` +
|
|
311
|
+
"the operator completes recovery.",
|
|
312
|
+
}
|
|
313
|
+
: error;
|
|
314
|
+
record(
|
|
315
|
+
details.code === "timeout"
|
|
316
|
+
? "timeout"
|
|
317
|
+
: details.code === "cancelled"
|
|
318
|
+
? "cancelled"
|
|
319
|
+
: "error",
|
|
320
|
+
details.code,
|
|
321
|
+
);
|
|
322
|
+
return {
|
|
323
|
+
ok: false,
|
|
324
|
+
durationMs: Date.now() - started,
|
|
325
|
+
attempts,
|
|
326
|
+
timing: diagnostics,
|
|
327
|
+
...(resolved ? { resolved } : {}),
|
|
328
|
+
error: details,
|
|
329
|
+
};
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
const resolution = await resolve();
|
|
333
|
+
catalogMs += resolution.catalogMs;
|
|
334
|
+
if (!resolution.ok) {
|
|
335
|
+
if (resolution.connector && resolution.toolName) {
|
|
336
|
+
activityTarget = {
|
|
337
|
+
connector: resolution.connector,
|
|
338
|
+
toolName: resolution.toolName,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
if (resolution.cause && resolution.connector) {
|
|
342
|
+
if (context.requestSignal?.aborted) {
|
|
343
|
+
return failed(callerCancelledDetails());
|
|
344
|
+
}
|
|
345
|
+
// A connector whose catalog cannot be fetched is as unusable as one
|
|
346
|
+
// whose execution fails, so it feeds health accounting the same way the
|
|
347
|
+
// attempt catch below does — otherwise a connector every call fails
|
|
348
|
+
// against (a revoked downstream grant, say) still reads clean from the
|
|
349
|
+
// cheap `list_connectors({ probe: false })` signal.
|
|
350
|
+
//
|
|
351
|
+
// Recorded HERE rather than inside the registry's catalog fetch because
|
|
352
|
+
// a cache hit that avoids a live listTools call records nothing — it is
|
|
353
|
+
// not evidence of health. Success stays what it has always been: an
|
|
354
|
+
// actual downstream call that returned.
|
|
355
|
+
this.registry.recordFailure(
|
|
356
|
+
resolution.connector.id,
|
|
357
|
+
Date.now() - started,
|
|
358
|
+
resolution.cause,
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
return failed(resolution.error);
|
|
362
|
+
}
|
|
363
|
+
resolved = resolution.resolved;
|
|
364
|
+
activityTarget = resolved;
|
|
365
|
+
|
|
366
|
+
if (!isExplicitlyReadOnly(resolved.definition) && !context.allowDestructive) {
|
|
367
|
+
const canonicalAddress = `${resolved.connector.id}.${resolved.toolName}`;
|
|
368
|
+
return failed(
|
|
369
|
+
framingError(
|
|
370
|
+
"destructive_tool_requires_approval",
|
|
371
|
+
`Tool "${canonicalAddress}" is not explicitly read-only. Invoke it through call_destructive_tool so the MCP host can request explicit approval.`,
|
|
372
|
+
),
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
try {
|
|
377
|
+
context.beforeDispatch?.();
|
|
378
|
+
} catch (error) {
|
|
379
|
+
return failed(classifyCallError(error));
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const maxRetries = Math.min(
|
|
383
|
+
2,
|
|
384
|
+
Math.max(0, Math.trunc(context.maxRetries ?? 0)),
|
|
385
|
+
);
|
|
386
|
+
let result: unknown;
|
|
387
|
+
while (true) {
|
|
388
|
+
attempts++;
|
|
389
|
+
let permit: Awaited<ReturnType<RegistryView["admitCall"]>> | undefined;
|
|
390
|
+
const controller =
|
|
391
|
+
context.timeoutMs || context.requestSignal
|
|
392
|
+
? new AbortController()
|
|
393
|
+
: undefined;
|
|
394
|
+
const forwardAbort = () =>
|
|
395
|
+
controller?.abort(context.requestSignal?.reason);
|
|
396
|
+
if (context.requestSignal?.aborted) forwardAbort();
|
|
397
|
+
else {
|
|
398
|
+
context.requestSignal?.addEventListener("abort", forwardAbort, {
|
|
399
|
+
once: true,
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
403
|
+
let onAbort: (() => void) | undefined;
|
|
404
|
+
let attemptError: unknown;
|
|
405
|
+
let attemptFailed = false;
|
|
406
|
+
try {
|
|
407
|
+
const admissionStarted = Date.now();
|
|
408
|
+
try {
|
|
409
|
+
permit = await this.registry.admitCall(resolved.connector.id, {
|
|
410
|
+
toolName: resolved.toolName,
|
|
411
|
+
args: args ?? {},
|
|
412
|
+
...(context.requestSignal !== undefined
|
|
413
|
+
? { signal: context.requestSignal }
|
|
414
|
+
: {}),
|
|
415
|
+
});
|
|
416
|
+
} finally {
|
|
417
|
+
admissionMs += Date.now() - admissionStarted;
|
|
418
|
+
}
|
|
419
|
+
const connectorContext = this.registry.contextFor(
|
|
420
|
+
resolved.connector.id,
|
|
421
|
+
this.catalog.baseUrl,
|
|
422
|
+
this.catalog.requestScope,
|
|
423
|
+
{
|
|
424
|
+
...(controller?.signal !== undefined
|
|
425
|
+
? { signal: controller.signal }
|
|
426
|
+
: {}),
|
|
427
|
+
...(context.timeoutMs !== undefined
|
|
428
|
+
? { timeoutMs: context.timeoutMs }
|
|
429
|
+
: {}),
|
|
430
|
+
},
|
|
431
|
+
);
|
|
432
|
+
if (resolved.connector.credential && !connectorContext.credential) {
|
|
433
|
+
throw new ConnectorCallError(
|
|
434
|
+
"auth_required",
|
|
435
|
+
"Operator-managed credential storage is not configured. Call " +
|
|
436
|
+
`authorize_connector({ connector: "${resolved.connector.id}" }).`,
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
let rejectCancelled!: (reason: unknown) => void;
|
|
440
|
+
const cancelled = controller
|
|
441
|
+
? new Promise<never>((_, reject) => {
|
|
442
|
+
rejectCancelled = reject;
|
|
443
|
+
})
|
|
444
|
+
: undefined;
|
|
445
|
+
onAbort = () => {
|
|
446
|
+
rejectCancelled(
|
|
447
|
+
controller?.signal.reason ??
|
|
448
|
+
new ConnectorCallError("timeout", "Tool call was cancelled"),
|
|
449
|
+
);
|
|
450
|
+
};
|
|
451
|
+
controller?.signal.addEventListener("abort", onAbort, { once: true });
|
|
452
|
+
if (controller?.signal.aborted) onAbort();
|
|
453
|
+
if (controller?.signal.aborted) await cancelled;
|
|
454
|
+
if (context.timeoutMs) {
|
|
455
|
+
timer = setTimeout(() => {
|
|
456
|
+
controller?.abort(
|
|
457
|
+
new ConnectorCallError(
|
|
458
|
+
"timeout",
|
|
459
|
+
`Tool call timed out after ${context.timeoutMs}ms`,
|
|
460
|
+
),
|
|
461
|
+
);
|
|
462
|
+
}, context.timeoutMs);
|
|
463
|
+
}
|
|
464
|
+
const connectorStarted = Date.now();
|
|
465
|
+
try {
|
|
466
|
+
const pending = resolved.connector.callTool(
|
|
467
|
+
resolved.toolName,
|
|
468
|
+
args ?? {},
|
|
469
|
+
connectorContext,
|
|
470
|
+
);
|
|
471
|
+
const raw = cancelled
|
|
472
|
+
? await Promise.race([pending, cancelled])
|
|
473
|
+
: await pending;
|
|
474
|
+
// isError is checked here for BOTH result shapes so every adapter
|
|
475
|
+
// reports the same downstream-failure wording, and the throw lands
|
|
476
|
+
// inside the attempt where it stays retry-eligible and feeds health.
|
|
477
|
+
assertRawMcpSuccess(resolved.connector.kind, raw);
|
|
478
|
+
result = context.unwrapResult
|
|
479
|
+
? unwrapMcpResult(resolved.connector.kind, raw)
|
|
480
|
+
: raw;
|
|
481
|
+
} finally {
|
|
482
|
+
connectorMs += Date.now() - connectorStarted;
|
|
483
|
+
}
|
|
484
|
+
} catch (error) {
|
|
485
|
+
attemptFailed = true;
|
|
486
|
+
attemptError = error;
|
|
487
|
+
} finally {
|
|
488
|
+
if (timer) clearTimeout(timer);
|
|
489
|
+
if (onAbort) {
|
|
490
|
+
controller?.signal.removeEventListener("abort", onAbort);
|
|
491
|
+
}
|
|
492
|
+
context.requestSignal?.removeEventListener("abort", forwardAbort);
|
|
493
|
+
permit?.release();
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (attemptFailed) {
|
|
497
|
+
const callerCancelled = isCallerCancellation(
|
|
498
|
+
attemptError,
|
|
499
|
+
context.requestSignal,
|
|
500
|
+
);
|
|
501
|
+
const details = callerCancelled
|
|
502
|
+
? callerCancelledDetails()
|
|
503
|
+
: classifyCallError(attemptError);
|
|
504
|
+
if (
|
|
505
|
+
!callerCancelled &&
|
|
506
|
+
attempts <= maxRetries &&
|
|
507
|
+
retrySafe(resolved.definition) &&
|
|
508
|
+
details.retryable
|
|
509
|
+
) {
|
|
510
|
+
const wait = retryBackoffMs(attempts, details.retryAfterMs);
|
|
511
|
+
if (wait !== undefined) {
|
|
512
|
+
const backoffStarted = Date.now();
|
|
513
|
+
if (wait > 0) {
|
|
514
|
+
const completed = await new Promise<boolean>((resolve) => {
|
|
515
|
+
let settled = false;
|
|
516
|
+
const finish = (value: boolean) => {
|
|
517
|
+
if (settled) return;
|
|
518
|
+
settled = true;
|
|
519
|
+
clearTimeout(timer);
|
|
520
|
+
context.requestSignal?.removeEventListener("abort", cancel);
|
|
521
|
+
resolve(value);
|
|
522
|
+
};
|
|
523
|
+
const timer = setTimeout(() => finish(true), wait);
|
|
524
|
+
const cancel = () => finish(false);
|
|
525
|
+
context.requestSignal?.addEventListener("abort", cancel, {
|
|
526
|
+
once: true,
|
|
527
|
+
});
|
|
528
|
+
if (context.requestSignal?.aborted) cancel();
|
|
529
|
+
});
|
|
530
|
+
backoffMs += Date.now() - backoffStarted;
|
|
531
|
+
if (!completed) return failed(callerCancelledDetails());
|
|
532
|
+
} else {
|
|
533
|
+
backoffMs += Date.now() - backoffStarted;
|
|
534
|
+
}
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
// The reported window is longer than the engine will park a
|
|
538
|
+
// synchronous request for. Fall through to failure with
|
|
539
|
+
// retryAfterMs reported verbatim so the agent can re-issue.
|
|
540
|
+
}
|
|
541
|
+
if (!callerCancelled && !isCallAdmissionError(attemptError)) {
|
|
542
|
+
this.registry.recordFailure(
|
|
543
|
+
resolved.connector.id,
|
|
544
|
+
Date.now() - started,
|
|
545
|
+
attemptError,
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
return failed(details);
|
|
549
|
+
}
|
|
550
|
+
break;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
this.registry.recordSuccess(resolved.connector.id, Date.now() - started);
|
|
554
|
+
const processingStarted = Date.now();
|
|
555
|
+
try {
|
|
556
|
+
const value = context.processResult
|
|
557
|
+
? await context.processResult(result, resolved)
|
|
558
|
+
: (result as T);
|
|
559
|
+
resultProcessingMs += Date.now() - processingStarted;
|
|
560
|
+
const diagnostics = timing();
|
|
561
|
+
record("success");
|
|
562
|
+
return {
|
|
563
|
+
ok: true,
|
|
564
|
+
value,
|
|
565
|
+
resolved,
|
|
566
|
+
durationMs: Date.now() - started,
|
|
567
|
+
attempts,
|
|
568
|
+
timing: diagnostics,
|
|
569
|
+
};
|
|
570
|
+
} catch (error) {
|
|
571
|
+
resultProcessingMs += Date.now() - processingStarted;
|
|
572
|
+
return failed(
|
|
573
|
+
framingError(
|
|
574
|
+
"result_processing_failed",
|
|
575
|
+
error instanceof Error ? error.message : String(error),
|
|
576
|
+
),
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|