@zackbart/connecta 0.2.1 → 0.3.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/CHANGELOG.md +38 -0
- package/dist/connectors/api.d.ts +14 -0
- package/dist/connectors/api.d.ts.map +1 -1
- package/dist/connectors/api.js +51 -1
- package/dist/connectors/api.js.map +1 -1
- package/dist/connectors/remote-mcp.d.ts.map +1 -1
- package/dist/connectors/remote-mcp.js +22 -8
- package/dist/connectors/remote-mcp.js.map +1 -1
- package/dist/credentials.d.ts.map +1 -1
- package/dist/credentials.js +4 -1
- package/dist/credentials.js.map +1 -1
- package/dist/errors.d.ts +33 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +51 -0
- package/dist/errors.js.map +1 -0
- package/dist/execute.d.ts.map +1 -1
- package/dist/execute.js +10 -6
- package/dist/execute.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/meta-tools.d.ts.map +1 -1
- package/dist/meta-tools.js +18 -22
- package/dist/meta-tools.js.map +1 -1
- package/dist/node.d.ts +1 -0
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js.map +1 -1
- package/dist/storage/file.d.ts +6 -2
- package/dist/storage/file.d.ts.map +1 -1
- package/dist/storage/file.js +3 -2
- package/dist/storage/file.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +4 -1
- package/src/connectors/api.ts +70 -1
- package/src/connectors/remote-mcp.ts +32 -13
- package/src/credentials.ts +4 -1
- package/src/errors.ts +83 -0
- package/src/execute.ts +13 -6
- package/src/index.ts +2 -0
- package/src/meta-tools.ts +37 -35
- package/src/node.ts +1 -0
- package/src/storage/file.ts +12 -3
- package/src/version.ts +1 -1
package/src/errors.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// Typed failure contract for connector tool calls. Web-API only — no node:
|
|
2
|
+
// imports here.
|
|
3
|
+
|
|
4
|
+
/** Machine-readable classification of a failed connector tool call. */
|
|
5
|
+
export type ConnectorCallErrorCode =
|
|
6
|
+
| "timeout"
|
|
7
|
+
| "auth_required"
|
|
8
|
+
| "rate_limited"
|
|
9
|
+
| "unavailable"
|
|
10
|
+
| "invalid_args"
|
|
11
|
+
| "connector_call_failed";
|
|
12
|
+
|
|
13
|
+
const RETRYABLE_BY_CODE: Record<ConnectorCallErrorCode, boolean> = {
|
|
14
|
+
timeout: true,
|
|
15
|
+
rate_limited: true,
|
|
16
|
+
unavailable: true,
|
|
17
|
+
auth_required: false,
|
|
18
|
+
invalid_args: false,
|
|
19
|
+
connector_call_failed: false,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Throw from `Connector.callTool` (or anything beneath it) to classify a
|
|
24
|
+
* failure exactly. Untyped errors fall back to a message-text heuristic, so a
|
|
25
|
+
* connector whose legitimate error text mentions "timeout" is misread as a
|
|
26
|
+
* retryable timeout — this class is the escape hatch. `retryable` defaults per
|
|
27
|
+
* code (timeout, rate_limited, and unavailable retry; the rest do not) and may
|
|
28
|
+
* be overridden.
|
|
29
|
+
*/
|
|
30
|
+
export class ConnectorCallError extends Error {
|
|
31
|
+
readonly code: ConnectorCallErrorCode;
|
|
32
|
+
readonly retryable: boolean;
|
|
33
|
+
|
|
34
|
+
constructor(
|
|
35
|
+
code: ConnectorCallErrorCode,
|
|
36
|
+
message: string,
|
|
37
|
+
opts: { retryable?: boolean; cause?: unknown } = {},
|
|
38
|
+
) {
|
|
39
|
+
super(
|
|
40
|
+
message,
|
|
41
|
+
opts.cause !== undefined ? { cause: opts.cause } : undefined,
|
|
42
|
+
);
|
|
43
|
+
this.name = "ConnectorCallError";
|
|
44
|
+
this.code = code;
|
|
45
|
+
this.retryable = opts.retryable ?? RETRYABLE_BY_CODE[code];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The `error` object surfaced in call_tool/batch_call value-mode results. */
|
|
50
|
+
export interface CallErrorDetails {
|
|
51
|
+
code: string;
|
|
52
|
+
message: string;
|
|
53
|
+
retryable: boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const RETRYABLE_MESSAGE_RE =
|
|
57
|
+
/timeout|timed out|econnreset|econnrefused|temporar|rate.?limit|429|502|503|504|refcountedcanceler|different request/i;
|
|
58
|
+
const TIMEOUT_MESSAGE_RE = /timed out|timeout/i;
|
|
59
|
+
|
|
60
|
+
/** Message-text fallback used when an error carries no typed classification. */
|
|
61
|
+
export function messageLooksRetryable(message: string): boolean {
|
|
62
|
+
return RETRYABLE_MESSAGE_RE.test(message);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Classify a value thrown by a connector call. A `ConnectorCallError` is
|
|
67
|
+
* authoritative; anything else falls back to the historical message-text
|
|
68
|
+
* heuristic.
|
|
69
|
+
*/
|
|
70
|
+
export function classifyCallError(
|
|
71
|
+
err: unknown,
|
|
72
|
+
fallbackCode = "connector_call_failed",
|
|
73
|
+
): CallErrorDetails {
|
|
74
|
+
if (err instanceof ConnectorCallError) {
|
|
75
|
+
return { code: err.code, message: err.message, retryable: err.retryable };
|
|
76
|
+
}
|
|
77
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
78
|
+
return {
|
|
79
|
+
code: TIMEOUT_MESSAGE_RE.test(message) ? "timeout" : fallbackCode,
|
|
80
|
+
message,
|
|
81
|
+
retryable: RETRYABLE_MESSAGE_RE.test(message),
|
|
82
|
+
};
|
|
83
|
+
}
|
package/src/execute.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { z } from "zod";
|
|
|
3
3
|
import { compactSchema, rankTools, summarizeDescription } from "./catalog.js";
|
|
4
4
|
import { recordToolActivity, type ActivityRequestContext } from "./activity.js";
|
|
5
5
|
import { errorResult, jsonResult, type ToolResult } from "./meta-tools.js";
|
|
6
|
+
import { classifyCallError, ConnectorCallError } from "./errors.js";
|
|
6
7
|
import { unwrapMcpResult } from "./mcp-result.js";
|
|
7
8
|
import type { Registry } from "./registry.js";
|
|
8
9
|
import type {
|
|
@@ -191,7 +192,10 @@ export async function buildSandboxProviders(
|
|
|
191
192
|
try {
|
|
192
193
|
timer = setTimeout(() => {
|
|
193
194
|
controller.abort(
|
|
194
|
-
new
|
|
195
|
+
new ConnectorCallError(
|
|
196
|
+
"timeout",
|
|
197
|
+
`Tool call timed out after ${hostCallTimeoutMs}ms`,
|
|
198
|
+
),
|
|
195
199
|
);
|
|
196
200
|
}, hostCallTimeoutMs);
|
|
197
201
|
const pending = resolved.connector.callTool(
|
|
@@ -216,16 +220,16 @@ export async function buildSandboxProviders(
|
|
|
216
220
|
return value;
|
|
217
221
|
} catch (err) {
|
|
218
222
|
registry.recordFailure(resolved.connector.id, Date.now() - started, err);
|
|
219
|
-
const
|
|
223
|
+
const details = classifyCallError(err);
|
|
220
224
|
recordToolActivity(activity, {
|
|
221
225
|
connectorId: resolved.connector.id,
|
|
222
226
|
toolName: resolved.toolName,
|
|
223
227
|
address: `${resolved.connector.id}.${resolved.toolName}`,
|
|
224
228
|
source: "execute_code",
|
|
225
|
-
outcome:
|
|
229
|
+
outcome: details.code === "timeout" ? "timeout" : "error",
|
|
226
230
|
durationMs: Date.now() - started,
|
|
227
231
|
attempts: 1,
|
|
228
|
-
errorCode:
|
|
232
|
+
errorCode: details.code,
|
|
229
233
|
});
|
|
230
234
|
throw err;
|
|
231
235
|
} finally {
|
|
@@ -269,8 +273,11 @@ export async function buildSandboxProviders(
|
|
|
269
273
|
);
|
|
270
274
|
continue;
|
|
271
275
|
}
|
|
276
|
+
// `await` (not a bare promise return) so a synchronous throw inside
|
|
277
|
+
// callAddress never sits handler-less for the thenable-adoption
|
|
278
|
+
// microtask — workerd reports that gap as an unhandled rejection.
|
|
272
279
|
fns[key] = async (args: unknown) =>
|
|
273
|
-
callAddress(`${connector.id}.${t.name}`, args);
|
|
280
|
+
await callAddress(`${connector.id}.${t.name}`, args);
|
|
274
281
|
}
|
|
275
282
|
if (Object.keys(fns).length > 0) {
|
|
276
283
|
providers.push({ name: ns, fns });
|
|
@@ -288,7 +295,7 @@ export async function buildSandboxProviders(
|
|
|
288
295
|
`connecta.batch accepts at most ${EXECUTE_MAX_BATCH_CALLS} calls`,
|
|
289
296
|
);
|
|
290
297
|
}
|
|
291
|
-
return Promise.all(
|
|
298
|
+
return await Promise.all(
|
|
292
299
|
calls.map(async (call) => {
|
|
293
300
|
const item = call as { address?: unknown; args?: unknown };
|
|
294
301
|
try {
|
package/src/index.ts
CHANGED
|
@@ -159,6 +159,8 @@ export function createConnecta(config: ConnectaConfig): Connecta {
|
|
|
159
159
|
|
|
160
160
|
export { remoteMcp } from "./connectors/remote-mcp.js";
|
|
161
161
|
export { api } from "./connectors/api.js";
|
|
162
|
+
export { ConnectorCallError } from "./errors.js";
|
|
163
|
+
export type { ConnectorCallErrorCode, CallErrorDetails } from "./errors.js";
|
|
162
164
|
export { bearerToken } from "./auth/bearer.js";
|
|
163
165
|
export { memoryStorage } from "./storage/memory.js";
|
|
164
166
|
export { CONNECTA_VERSION } from "./version.js";
|
package/src/meta-tools.ts
CHANGED
|
@@ -7,6 +7,12 @@ import {
|
|
|
7
7
|
type ActivityRequestContext,
|
|
8
8
|
} from "./activity.js";
|
|
9
9
|
import { unwrapMcpResult } from "./mcp-result.js";
|
|
10
|
+
import {
|
|
11
|
+
classifyCallError,
|
|
12
|
+
ConnectorCallError,
|
|
13
|
+
messageLooksRetryable,
|
|
14
|
+
type CallErrorDetails,
|
|
15
|
+
} from "./errors.js";
|
|
10
16
|
import type { Registry } from "./registry.js";
|
|
11
17
|
import { AVAILABLE_SKILLS } from "./skills.js";
|
|
12
18
|
import type { KVStorage, ToolDef } from "./types.js";
|
|
@@ -45,21 +51,11 @@ const DEFAULT_SEARCH_LIMIT = 25;
|
|
|
45
51
|
const enc = new TextEncoder();
|
|
46
52
|
const dec = new TextDecoder();
|
|
47
53
|
|
|
48
|
-
|
|
49
|
-
code: string;
|
|
50
|
-
message: string;
|
|
51
|
-
retryable: boolean;
|
|
52
|
-
}
|
|
54
|
+
type ErrorDetails = CallErrorDetails;
|
|
53
55
|
|
|
56
|
+
/** Details for failures that never reached a connector (no thrown value). */
|
|
54
57
|
function errorDetails(code: string, message: string): ErrorDetails {
|
|
55
|
-
return {
|
|
56
|
-
code,
|
|
57
|
-
message,
|
|
58
|
-
retryable:
|
|
59
|
-
/timeout|timed out|econnreset|econnrefused|temporar|rate.?limit|429|502|503|504|refcountedcanceler|different request/i.test(
|
|
60
|
-
message,
|
|
61
|
-
),
|
|
62
|
-
};
|
|
58
|
+
return { code, message, retryable: messageLooksRetryable(message) };
|
|
63
59
|
}
|
|
64
60
|
|
|
65
61
|
/** True if `b` is a UTF-8 continuation byte (0b10xxxxxx). */
|
|
@@ -332,11 +328,10 @@ export function createMetaTools(
|
|
|
332
328
|
...(errorCode ? { errorCode } : {}),
|
|
333
329
|
});
|
|
334
330
|
};
|
|
335
|
-
const failed = (
|
|
331
|
+
const failed = (error: ErrorDetails): RunCallOutcome => {
|
|
336
332
|
const durationMs = Date.now() - started;
|
|
337
|
-
const error = errorDetails(code, message);
|
|
338
333
|
const diagnostics = timing();
|
|
339
|
-
record(code === "timeout" ? "timeout" : "error", code);
|
|
334
|
+
record(error.code === "timeout" ? "timeout" : "error", error.code);
|
|
340
335
|
return {
|
|
341
336
|
toolResult:
|
|
342
337
|
call.resultMode === "value"
|
|
@@ -347,7 +342,7 @@ export function createMetaTools(
|
|
|
347
342
|
attempts,
|
|
348
343
|
...(call.diagnostics ? { timing: diagnostics } : {}),
|
|
349
344
|
})
|
|
350
|
-
: errorResult(message),
|
|
345
|
+
: errorResult(error.message),
|
|
351
346
|
durationMs,
|
|
352
347
|
attempts,
|
|
353
348
|
timing: diagnostics,
|
|
@@ -355,7 +350,9 @@ export function createMetaTools(
|
|
|
355
350
|
};
|
|
356
351
|
};
|
|
357
352
|
if (!resolved) {
|
|
358
|
-
return failed(
|
|
353
|
+
return failed(
|
|
354
|
+
errorDetails("unknown_address", `Unknown address "${call.address}"`),
|
|
355
|
+
);
|
|
359
356
|
}
|
|
360
357
|
const results = registry.resultsStorage();
|
|
361
358
|
const fields = call.fields && call.fields.length > 0 ? call.fields : null;
|
|
@@ -375,13 +372,17 @@ export function createMetaTools(
|
|
|
375
372
|
).find((tool) => tool.name === resolved.toolName);
|
|
376
373
|
} catch (err) {
|
|
377
374
|
catalogMs += Date.now() - catalogStarted;
|
|
378
|
-
|
|
375
|
+
// classifyCallError so a typed auth_required thrown while listing tools
|
|
376
|
+
// (e.g. a revoked downstream OAuth grant) keeps its code.
|
|
377
|
+
return failed(classifyCallError(err, "catalog_lookup_failed"));
|
|
379
378
|
}
|
|
380
379
|
catalogMs += Date.now() - catalogStarted;
|
|
381
380
|
if (!definition) {
|
|
382
381
|
return failed(
|
|
383
|
-
|
|
384
|
-
|
|
382
|
+
errorDetails(
|
|
383
|
+
"unknown_tool",
|
|
384
|
+
`Unknown tool "${resolved.toolName}" on connector "${resolved.connector.id}"`,
|
|
385
|
+
),
|
|
385
386
|
);
|
|
386
387
|
}
|
|
387
388
|
const explicitlyReadOnly =
|
|
@@ -389,8 +390,10 @@ export function createMetaTools(
|
|
|
389
390
|
definition.annotations?.destructiveHint !== true;
|
|
390
391
|
if (!explicitlyReadOnly && !options.allowDestructive) {
|
|
391
392
|
return failed(
|
|
392
|
-
|
|
393
|
-
|
|
393
|
+
errorDetails(
|
|
394
|
+
"destructive_tool_requires_approval",
|
|
395
|
+
`Tool "${call.address}" is not explicitly read-only. Invoke it through call_destructive_tool so the MCP host can request explicit approval.`,
|
|
396
|
+
),
|
|
394
397
|
);
|
|
395
398
|
}
|
|
396
399
|
const retrySafe =
|
|
@@ -420,7 +423,12 @@ export function createMetaTools(
|
|
|
420
423
|
pending,
|
|
421
424
|
new Promise<never>((_, reject) => {
|
|
422
425
|
timer = setTimeout(() => {
|
|
423
|
-
reject(
|
|
426
|
+
reject(
|
|
427
|
+
new ConnectorCallError(
|
|
428
|
+
"timeout",
|
|
429
|
+
`Tool call timed out after ${timeoutMs}ms`,
|
|
430
|
+
),
|
|
431
|
+
);
|
|
424
432
|
controller?.abort();
|
|
425
433
|
}, timeoutMs);
|
|
426
434
|
}),
|
|
@@ -441,7 +449,7 @@ export function createMetaTools(
|
|
|
441
449
|
} catch (err) {
|
|
442
450
|
// Includes connector setup, downstream execution, and timeout wait.
|
|
443
451
|
connectorMs += Date.now() - connectorStarted;
|
|
444
|
-
const details =
|
|
452
|
+
const details = classifyCallError(err);
|
|
445
453
|
if (attempts <= maxRetries && retrySafe && details.retryable) {
|
|
446
454
|
const backoffStarted = Date.now();
|
|
447
455
|
await new Promise((resolve) =>
|
|
@@ -455,12 +463,7 @@ export function createMetaTools(
|
|
|
455
463
|
Date.now() - started,
|
|
456
464
|
err,
|
|
457
465
|
);
|
|
458
|
-
return failed(
|
|
459
|
-
/timed out|timeout/i.test(msg(err))
|
|
460
|
-
? "timeout"
|
|
461
|
-
: "connector_call_failed",
|
|
462
|
-
msg(err),
|
|
463
|
-
);
|
|
466
|
+
return failed(details);
|
|
464
467
|
} finally {
|
|
465
468
|
if (timer) clearTimeout(timer);
|
|
466
469
|
}
|
|
@@ -531,7 +534,7 @@ export function createMetaTools(
|
|
|
531
534
|
};
|
|
532
535
|
} catch (err) {
|
|
533
536
|
resultProcessingMs += Date.now() - processingStarted;
|
|
534
|
-
return failed("result_processing_failed", msg(err));
|
|
537
|
+
return failed(errorDetails("result_processing_failed", msg(err)));
|
|
535
538
|
}
|
|
536
539
|
}
|
|
537
540
|
|
|
@@ -846,12 +849,11 @@ export function createMetaTools(
|
|
|
846
849
|
const results = settled.map((s, i) => {
|
|
847
850
|
const address = args.calls[i].address;
|
|
848
851
|
if (s.status === "rejected") {
|
|
849
|
-
const message = msg(s.reason);
|
|
850
852
|
return {
|
|
851
853
|
address,
|
|
852
854
|
ok: false,
|
|
853
|
-
error:
|
|
854
|
-
errorDetails:
|
|
855
|
+
error: msg(s.reason),
|
|
856
|
+
errorDetails: classifyCallError(s.reason, "batch_call_failed"),
|
|
855
857
|
};
|
|
856
858
|
}
|
|
857
859
|
const r = s.value;
|
package/src/node.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { pipeline } from "node:stream/promises";
|
|
|
5
5
|
import type { Connecta } from "./index.js";
|
|
6
6
|
|
|
7
7
|
export { fileStorage } from "./storage/file.js";
|
|
8
|
+
export type { FileStorageOptions } from "./storage/file.js";
|
|
8
9
|
|
|
9
10
|
/** 10 MiB. Tool arguments are JSON; nothing legitimate approaches this. */
|
|
10
11
|
const DEFAULT_MAX_BODY_BYTES = 10 * 1024 * 1024;
|
package/src/storage/file.ts
CHANGED
|
@@ -6,19 +6,28 @@ import {
|
|
|
6
6
|
writeFileSync,
|
|
7
7
|
} from "node:fs";
|
|
8
8
|
import { dirname } from "node:path";
|
|
9
|
-
import type { KVStorage } from "../types.js";
|
|
9
|
+
import type { KVStorage, Logger } from "../types.js";
|
|
10
10
|
|
|
11
11
|
interface Entry {
|
|
12
12
|
value: string;
|
|
13
13
|
exp?: number; // epoch ms
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
export interface FileStorageOptions {
|
|
17
|
+
/** Destination for the corrupt-state-file recovery report. Default console. */
|
|
18
|
+
logger?: Logger;
|
|
19
|
+
}
|
|
20
|
+
|
|
16
21
|
/**
|
|
17
22
|
* JSON-file-backed KVStorage for Node. Loads once, persists on every write via
|
|
18
23
|
* a temp-file + rename (atomic-ish). Only reachable via the "@zackbart/connecta/node"
|
|
19
24
|
* subpath so the main entry stays Workers-clean.
|
|
20
25
|
*/
|
|
21
|
-
export function fileStorage(
|
|
26
|
+
export function fileStorage(
|
|
27
|
+
path: string,
|
|
28
|
+
opts: FileStorageOptions = {},
|
|
29
|
+
): KVStorage {
|
|
30
|
+
const logger: Logger = opts.logger ?? console;
|
|
22
31
|
let data: Record<string, Entry> = {};
|
|
23
32
|
if (existsSync(path)) {
|
|
24
33
|
try {
|
|
@@ -39,7 +48,7 @@ export function fileStorage(path: string): KVStorage {
|
|
|
39
48
|
`than overwrite it. Move or repair the file, then restart.`,
|
|
40
49
|
);
|
|
41
50
|
}
|
|
42
|
-
|
|
51
|
+
logger.error(
|
|
43
52
|
`[connecta] state file ${path} is not valid JSON ` +
|
|
44
53
|
`(${error instanceof Error ? error.message : String(error)}) — ` +
|
|
45
54
|
`moved to ${quarantine}, starting from empty state. Downstream ` +
|
package/src/version.ts
CHANGED