@andreprado/agentkit 0.1.0-alpha.9 → 0.1.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.
Files changed (122) hide show
  1. package/README.md +18 -1
  2. package/docs/guides/add-channel.md +251 -7
  3. package/docs/guides/add-knowledge.md +10 -0
  4. package/docs/guides/add-managed-composio.md +165 -0
  5. package/docs/guides/add-tool.md +10 -3
  6. package/docs/guides/channel-security.md +162 -32
  7. package/docs/guides/connect-discord.md +178 -0
  8. package/docs/guides/connect-slack.md +126 -0
  9. package/docs/guides/connect-telegram.md +61 -1
  10. package/docs/guides/connect-whatsapp-evolution.md +121 -0
  11. package/docs/guides/connect-whatsapp-uazapi.md +139 -0
  12. package/docs/guides/connect-whatsapp-zapster.md +119 -16
  13. package/docs/guides/create-agent.md +31 -4
  14. package/docs/guides/debug-channel.md +159 -0
  15. package/docs/guides/improve-from-production.md +151 -0
  16. package/docs/guides/prepare-deploy.md +32 -14
  17. package/docs/guides/replay-production-traces.md +72 -0
  18. package/docs/guides/run-evals.md +95 -25
  19. package/docs/guides/security-rules.md +9 -5
  20. package/docs/guides/send-feedback.md +135 -0
  21. package/docs/guides/use-jev.md +67 -0
  22. package/docs/guides/use-provider.md +70 -3
  23. package/docs/llms-full.txt +295 -25
  24. package/docs/llms.txt +54 -7
  25. package/package.json +3 -7
  26. package/src/cli/args.ts +23 -2
  27. package/src/cli/cloud-client.ts +121 -9
  28. package/src/cli/commands/channels.ts +856 -36
  29. package/src/cli/commands/feedback.ts +438 -0
  30. package/src/cli/commands/provider.ts +47 -0
  31. package/src/cli/commands/transcribe.ts +171 -0
  32. package/src/cli/deploy-chat-ui.ts +232 -18
  33. package/src/cli/deploy-readiness.ts +227 -14
  34. package/src/cli/help.ts +67 -9
  35. package/src/cli/index.ts +740 -35
  36. package/src/cli/new-command.ts +41 -0
  37. package/src/cloud/client.ts +4 -3
  38. package/src/cloud/contracts.ts +1 -1
  39. package/src/create-project.ts +18 -35
  40. package/src/index.ts +565 -11
  41. package/src/providers/codex-auth.ts +111 -0
  42. package/src/providers/pi.ts +88 -19
  43. package/src/providers/test.ts +36 -0
  44. package/src/providers/types.ts +8 -0
  45. package/src/runtime/channel-test-harness.ts +21 -1
  46. package/src/runtime/channels/discord.ts +904 -0
  47. package/src/runtime/channels/generic-webhook.ts +682 -0
  48. package/src/runtime/channels/net-guard.ts +480 -0
  49. package/src/runtime/channels/provider-fetch.ts +54 -0
  50. package/src/runtime/channels/slack.ts +652 -0
  51. package/src/runtime/channels/telegram.ts +379 -15
  52. package/src/runtime/channels/whatsapp-evolution.ts +1330 -0
  53. package/src/runtime/channels/whatsapp-meta.ts +9 -0
  54. package/src/runtime/channels/whatsapp-uazapi.ts +1192 -0
  55. package/src/runtime/channels/whatsapp-zapster.ts +702 -40
  56. package/src/runtime/channels.ts +83 -3
  57. package/src/runtime/chat.ts +70 -44
  58. package/src/runtime/config.ts +512 -20
  59. package/src/runtime/core/manifest.ts +75 -5
  60. package/src/runtime/core/targets.ts +5 -5
  61. package/src/runtime/deploy-readiness.ts +34 -4
  62. package/src/runtime/dev-server.ts +639 -39
  63. package/src/runtime/env.ts +8 -3
  64. package/src/runtime/evals.ts +445 -74
  65. package/src/runtime/improve.ts +868 -0
  66. package/src/runtime/inspect.ts +173 -4
  67. package/src/runtime/integrations/composio.ts +425 -0
  68. package/src/runtime/knowledge/embeddings.ts +45 -7
  69. package/src/runtime/knowledge/ingest.ts +69 -6
  70. package/src/runtime/knowledge/retrieve.ts +25 -5
  71. package/src/runtime/knowledge/schema.ts +45 -1
  72. package/src/runtime/knowledge/vector.ts +30 -30
  73. package/src/runtime/prompt-context.ts +141 -0
  74. package/src/runtime/runtime-contract.ts +71 -7
  75. package/src/runtime/skills.ts +95 -0
  76. package/src/runtime/targets/cloudflare/build.ts +1010 -208
  77. package/src/runtime/targets/container/server.ts +1 -1
  78. package/src/runtime/targets/vps/deploy.ts +26 -9
  79. package/src/runtime/tool-runner.ts +9 -1
  80. package/src/runtime/tools.ts +26 -2
  81. package/src/runtime/transcription.ts +483 -0
  82. package/src/storage/sqlite.ts +7 -2
  83. package/src/templates/blank.ts +37 -9
  84. package/src/templates/dentista.ts +40 -14
  85. package/src/templates/skills/agentkit-build-agent/SKILL.md +34 -5
  86. package/src/templates/skills/agentkit-build-agent/templates/appointment-intake.instructions.md +2 -1
  87. package/src/templates/skills/agentkit-capsule/SKILL.md +32 -3
  88. package/src/templates/skills/agentkit-capsule/references/docs-router.md +2 -2
  89. package/src/templates/skills/agentkit-channels/SKILL.md +66 -1
  90. package/src/templates/skills/agentkit-channels/references/channel-buffering.md +8 -1
  91. package/src/templates/skills/agentkit-channels/references/channel-debugging.md +28 -3
  92. package/src/templates/skills/agentkit-channels/references/discord.md +93 -0
  93. package/src/templates/skills/agentkit-channels/references/slack.md +56 -0
  94. package/src/templates/skills/agentkit-channels/references/telegram.md +34 -0
  95. package/src/templates/skills/agentkit-channels/references/whatsapp-evolution.md +57 -0
  96. package/src/templates/skills/agentkit-channels/references/whatsapp-uazapi.md +54 -0
  97. package/src/templates/skills/agentkit-channels/references/whatsapp-zapster.md +42 -8
  98. package/src/templates/skills/agentkit-database/SKILL.md +11 -0
  99. package/src/templates/skills/agentkit-deploy/SKILL.md +9 -1
  100. package/src/templates/skills/agentkit-evals/SKILL.md +77 -13
  101. package/src/templates/skills/agentkit-evals/templates/multi-turn.eval.md +13 -6
  102. package/src/templates/skills/agentkit-evals/templates/no-leak.eval.md +8 -4
  103. package/src/templates/skills/agentkit-evals/templates/smoke.eval.md +8 -4
  104. package/src/templates/skills/agentkit-evals/templates/tool-call.eval.md +16 -7
  105. package/src/templates/skills/agentkit-improve/SKILL.md +96 -0
  106. package/src/templates/skills/agentkit-improve/references/replay-side-effects.md +18 -0
  107. package/src/templates/skills/agentkit-improve/references/trace-packets.md +22 -0
  108. package/src/templates/skills/agentkit-improve/templates/regression.eval.md +18 -0
  109. package/src/templates/skills/agentkit-integrations/SKILL.md +98 -0
  110. package/src/templates/skills/agentkit-knowledge/SKILL.md +4 -1
  111. package/src/templates/skills/agentkit-prompts/SKILL.md +3 -1
  112. package/src/templates/skills/agentkit-provider/SKILL.md +29 -4
  113. package/src/templates/skills/agentkit-security/SKILL.md +5 -2
  114. package/src/templates/skills/agentkit-tools/SKILL.md +8 -1
  115. package/src/templates/skills/agentkit-tools/examples/eval-safe-external-action.tool.md +8 -8
  116. package/src/templates/skills/agentkit-tools/examples/jev-service-fit.tool.md +110 -0
  117. package/src/templates/skills/agentkit-troubleshooting/SKILL.md +25 -1
  118. package/src/templates/support.ts +42 -12
  119. package/docs/guides/agentkit-skills-architecture.md +0 -471
  120. package/docs/guides/channels-implementation-map.md +0 -243
  121. package/docs/guides/channels-production-handoff.md +0 -101
  122. package/docs/portable-deploy-release-checklist.md +0 -41
@@ -0,0 +1,480 @@
1
+ import { lookup as dnsLookup } from "node:dns/promises";
2
+ import type { IncomingMessage } from "node:http";
3
+ import { request as httpsRequest } from "node:https";
4
+ import type { LookupFunction } from "node:net";
5
+ import { pipeline, Readable } from "node:stream";
6
+ import { createBrotliDecompress, createUnzip } from "node:zlib";
7
+
8
+ import { AgentKitError } from "../errors";
9
+
10
+ // Outbound requests to operator-configured URLs (webhook outputs, self-hosted
11
+ // WhatsApp gateways) must not reach private networks. Checking the hostname
12
+ // text is not enough: a public name can resolve to a private address, and a
13
+ // public server can redirect to one. So every guarded request resolves DNS,
14
+ // rejects blocked addresses, pins the connection to the checked address, and
15
+ // never follows redirects.
16
+
17
+ export type OutboundResolver = (hostname: string) => Promise<string[]>;
18
+
19
+ export type ResolvedOutboundUrl = {
20
+ href: string;
21
+ address: string;
22
+ family: 4 | 6;
23
+ };
24
+
25
+ export type PinnedRequestInit = {
26
+ method: string;
27
+ headers: Record<string, string>;
28
+ body?: string;
29
+ signal?: AbortSignal;
30
+ };
31
+
32
+ export type PublicHttpsFetch = (url: string, init?: RequestInit) => Promise<Response>;
33
+
34
+ // Test seams only. Production callers never pass these, so private addresses
35
+ // stay blocked and TLS is verified against the system CA store.
36
+ export type PinnedTransportOptions = {
37
+ isBlockedAddress?: (address: string) => boolean;
38
+ ca?: string;
39
+ };
40
+
41
+ export function createPublicHttpsFetch(
42
+ label: string,
43
+ resolver: OutboundResolver = resolveOutboundAddresses,
44
+ transport: PinnedTransportOptions = {},
45
+ ): PublicHttpsFetch {
46
+ return async (url, init = {}) => {
47
+ const parsed = new URL(url);
48
+
49
+ if (parsed.protocol !== "https:") {
50
+ throw new AgentKitError("channel_payload_invalid", `${label} must use HTTPS.`);
51
+ }
52
+
53
+ if (isBlockedOutboundHost(parsed.hostname)) {
54
+ throw new AgentKitError("channel_payload_invalid", `${label} host is not allowed.`);
55
+ }
56
+
57
+ if (init.body !== undefined && init.body !== null && typeof init.body !== "string") {
58
+ throw new AgentKitError("channel_payload_invalid", `${label} requests only support string bodies.`);
59
+ }
60
+
61
+ const signal = init.signal ?? undefined;
62
+ const resolved = await resolvePublicOutboundUrl(parsed.href, label, resolver, signal, transport.isBlockedAddress);
63
+ const headers: Record<string, string> = {};
64
+
65
+ new Headers(init.headers).forEach((value, key) => {
66
+ headers[key] = value;
67
+ });
68
+
69
+ return pinnedHttpsFetch(
70
+ resolved,
71
+ {
72
+ method: init.method ?? "GET",
73
+ headers,
74
+ body: init.body ?? undefined,
75
+ signal,
76
+ },
77
+ label,
78
+ transport,
79
+ );
80
+ };
81
+ }
82
+
83
+ export async function resolvePublicOutboundUrl(
84
+ href: string,
85
+ label: string,
86
+ resolver: OutboundResolver = resolveOutboundAddresses,
87
+ signal?: AbortSignal,
88
+ isBlockedAddress: (address: string) => boolean = isBlockedOutboundHost,
89
+ ): Promise<ResolvedOutboundUrl> {
90
+ const url = new URL(href);
91
+ let addresses: string[];
92
+
93
+ try {
94
+ addresses = await resolveWithinDeadline(resolver, url.hostname, signal, label);
95
+ } catch (error) {
96
+ if (error instanceof AgentKitError) {
97
+ throw error;
98
+ }
99
+
100
+ throw new AgentKitError(
101
+ "channel_provider_unavailable",
102
+ `${label} host could not be resolved: ${error instanceof Error ? error.message : String(error)}`,
103
+ );
104
+ }
105
+
106
+ if (addresses.length === 0) {
107
+ throw new AgentKitError("channel_provider_unavailable", `${label} host did not resolve.`);
108
+ }
109
+
110
+ for (const address of addresses) {
111
+ if (isBlockedAddress(address)) {
112
+ throw new AgentKitError("channel_payload_invalid", `${label} host resolved to a blocked address.`);
113
+ }
114
+ }
115
+
116
+ const address = normalizeOutboundHost(addresses[0]);
117
+
118
+ return {
119
+ href,
120
+ address,
121
+ family: parseIpv4Octets(address) ? 4 : 6,
122
+ };
123
+ }
124
+
125
+ // Node's https.request does not follow redirects, so a 3xx response is
126
+ // returned to the caller as-is rather than being chased to another host.
127
+ export function pinnedHttpsFetch(
128
+ resolved: ResolvedOutboundUrl,
129
+ init: PinnedRequestInit,
130
+ label: string,
131
+ transport: PinnedTransportOptions = {},
132
+ ): Promise<Response> {
133
+ const url = new URL(resolved.href);
134
+ const isBlockedAddress = transport.isBlockedAddress ?? isBlockedOutboundHost;
135
+ // Node calls lookup with `all: true` (for happy-eyeballs family selection),
136
+ // which expects an array of records instead of a single address.
137
+ const lookup = ((_hostname: string, options: { all?: boolean }, callback: (...args: unknown[]) => void) => {
138
+ if (options?.all) {
139
+ callback(null, [{ address: resolved.address, family: resolved.family }]);
140
+ return;
141
+ }
142
+
143
+ callback(null, resolved.address, resolved.family);
144
+ }) as unknown as LookupFunction;
145
+
146
+ return new Promise((resolve, reject) => {
147
+ const request = httpsRequest(
148
+ url,
149
+ {
150
+ method: init.method,
151
+ headers: init.headers,
152
+ lookup,
153
+ signal: init.signal,
154
+ ...(transport.ca ? { ca: transport.ca } : {}),
155
+ },
156
+ (response) => {
157
+ // Everything here runs outside the promise executor, so any throw must
158
+ // become a rejection or it would crash the process.
159
+ try {
160
+ assertAllowedRemoteAddress(response.socket.remoteAddress, label, isBlockedAddress);
161
+ const status = response.statusCode ?? 502;
162
+ const bodyless = init.method.toUpperCase() === "HEAD" || NULL_BODY_STATUSES.has(status);
163
+
164
+ if (bodyless) {
165
+ response.resume();
166
+ }
167
+
168
+ resolve(
169
+ new Response(bodyless ? null : (Readable.toWeb(decodedResponseBody(response)) as unknown as ReadableStream<Uint8Array>), {
170
+ status,
171
+ headers: headersFromIncomingMessage(response.headers),
172
+ }),
173
+ );
174
+ } catch (error) {
175
+ response.destroy(error instanceof Error ? error : new Error(String(error)));
176
+ reject(error);
177
+ }
178
+ },
179
+ );
180
+
181
+ request.on("socket", (socket) => {
182
+ const validateSocket = () => {
183
+ try {
184
+ assertAllowedRemoteAddress(socket.remoteAddress, label, isBlockedAddress);
185
+ } catch (error) {
186
+ request.destroy(error instanceof Error ? error : new Error(String(error)));
187
+ }
188
+ };
189
+
190
+ socket.once("connect", validateSocket);
191
+ socket.once("secureConnect", validateSocket);
192
+ });
193
+ request.once("error", reject);
194
+ request.end(init.body);
195
+ });
196
+ }
197
+
198
+ function decodedResponseBody(response: IncomingMessage): Readable {
199
+ const encodings = response.headers["content-encoding"]?.toLowerCase().split(",").map((value) => value.trim()) ?? [];
200
+
201
+ if (encodings.length === 0 || !encodings.every((value) => ["gzip", "x-gzip", "deflate", "x-deflate", "br"].includes(value))) {
202
+ return response;
203
+ }
204
+
205
+ const decoders = encodings.reverse().map((encoding) => encoding === "br" ? createBrotliDecompress() : createUnzip());
206
+ // Keep decoding streamed so callers enforce limits on decoded bytes. Pipeline
207
+ // propagates source errors and destroys the socket when the reader cancels.
208
+ pipeline([response, ...decoders], () => {});
209
+ return decoders[decoders.length - 1];
210
+ }
211
+
212
+ export async function resolveOutboundAddresses(hostname: string): Promise<string[]> {
213
+ const host = normalizeOutboundHost(hostname);
214
+
215
+ if (parseIpv4Octets(host) || parseIpv6Segments(host)) {
216
+ return [host];
217
+ }
218
+
219
+ const records = await dnsLookup(host, {
220
+ all: true,
221
+ verbatim: true,
222
+ });
223
+
224
+ return records.map((record) => record.address);
225
+ }
226
+
227
+ export function isBlockedOutboundHost(hostname: string): boolean {
228
+ const host = normalizeOutboundHost(hostname);
229
+
230
+ if (
231
+ host === "localhost" ||
232
+ host.endsWith(".localhost") ||
233
+ host === "metadata.google.internal"
234
+ ) {
235
+ return true;
236
+ }
237
+
238
+ const ipv4 = parseIpv4Octets(host);
239
+
240
+ if (ipv4) {
241
+ return isBlockedIpv4Octets(ipv4);
242
+ }
243
+
244
+ const ipv6 = parseIpv6Segments(host);
245
+
246
+ if (ipv6) {
247
+ return isBlockedIpv6Segments(ipv6);
248
+ }
249
+
250
+ return false;
251
+ }
252
+
253
+ export function normalizeOutboundHost(hostname: string): string {
254
+ return hostname.toLowerCase().replace(/^\[/, "").replace(/\]$/, "").replace(/\.$/, "");
255
+ }
256
+
257
+ // The Fetch Response constructor rejects a body for these statuses.
258
+ const NULL_BODY_STATUSES = new Set([101, 103, 204, 205, 304]);
259
+
260
+ // Resolution happens before the request exists, so the caller's deadline has to
261
+ // cover it here; otherwise a stalled resolver outlives the request timeout.
262
+ // Nothing has been sent yet, so this failure is always safe to retry.
263
+ async function resolveWithinDeadline(
264
+ resolver: OutboundResolver,
265
+ hostname: string,
266
+ signal: AbortSignal | undefined,
267
+ label: string,
268
+ ): Promise<string[]> {
269
+ const deadlineError = () =>
270
+ new AgentKitError("channel_provider_unavailable", `${label} host resolution did not finish before the request deadline.`);
271
+
272
+ if (!signal) {
273
+ return resolver(hostname);
274
+ }
275
+
276
+ if (signal.aborted) {
277
+ throw deadlineError();
278
+ }
279
+
280
+ return new Promise((resolve, reject) => {
281
+ const onAbort = () => reject(deadlineError());
282
+
283
+ signal.addEventListener("abort", onAbort, { once: true });
284
+ resolver(hostname).then(
285
+ (addresses) => {
286
+ signal.removeEventListener("abort", onAbort);
287
+ resolve(addresses);
288
+ },
289
+ (error) => {
290
+ signal.removeEventListener("abort", onAbort);
291
+ reject(error);
292
+ },
293
+ );
294
+ });
295
+ }
296
+
297
+ function assertAllowedRemoteAddress(
298
+ address: string | undefined,
299
+ label: string,
300
+ isBlockedAddress: (address: string) => boolean,
301
+ ): void {
302
+ if (!address || isBlockedAddress(address)) {
303
+ throw new AgentKitError("channel_payload_invalid", `${label} connected to a blocked address.`);
304
+ }
305
+ }
306
+
307
+ function headersFromIncomingMessage(headers: Record<string, string | string[] | number | undefined>): Headers {
308
+ const result = new Headers();
309
+
310
+ for (const [key, value] of Object.entries(headers)) {
311
+ if (Array.isArray(value)) {
312
+ for (const item of value) {
313
+ result.append(key, item);
314
+ }
315
+ continue;
316
+ }
317
+
318
+ if (value !== undefined) {
319
+ result.set(key, String(value));
320
+ }
321
+ }
322
+
323
+ return result;
324
+ }
325
+
326
+ function parseIpv4Octets(host: string): number[] | undefined {
327
+ const parts = host.split(".");
328
+
329
+ if (parts.length !== 4) {
330
+ return undefined;
331
+ }
332
+
333
+ const octets = parts.map((part) => Number(part));
334
+
335
+ if (octets.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
336
+ return undefined;
337
+ }
338
+
339
+ return octets;
340
+ }
341
+
342
+ function isBlockedIpv4Octets(parts: number[]): boolean {
343
+ const [a, b] = parts;
344
+
345
+ return (
346
+ a === 0 ||
347
+ a === 10 ||
348
+ a === 127 ||
349
+ (a === 100 && b >= 64 && b <= 127) ||
350
+ (a === 169 && b === 254) ||
351
+ (a === 172 && b >= 16 && b <= 31) ||
352
+ (a === 192 && b === 168) ||
353
+ (a === 198 && (b === 18 || b === 19)) ||
354
+ a >= 224
355
+ );
356
+ }
357
+
358
+ function parseIpv6Segments(host: string): number[] | undefined {
359
+ if (!host.includes(":")) {
360
+ return undefined;
361
+ }
362
+
363
+ if (host === "::") {
364
+ return new Array(8).fill(0);
365
+ }
366
+
367
+ const [leftRaw, rightRaw, extra] = host.split("::");
368
+
369
+ if (extra !== undefined) {
370
+ return undefined;
371
+ }
372
+
373
+ const left = parseIpv6SegmentSide(leftRaw ?? "");
374
+ const right = parseIpv6SegmentSide(rightRaw ?? "");
375
+
376
+ if (!left || !right) {
377
+ return undefined;
378
+ }
379
+
380
+ if (rightRaw === undefined) {
381
+ return left.length === 8 ? left : undefined;
382
+ }
383
+
384
+ const missing = 8 - left.length - right.length;
385
+
386
+ if (missing < 1) {
387
+ return undefined;
388
+ }
389
+
390
+ return [...left, ...new Array(missing).fill(0), ...right];
391
+ }
392
+
393
+ function parseIpv6SegmentSide(value: string): number[] | undefined {
394
+ if (!value) {
395
+ return [];
396
+ }
397
+
398
+ const segments: number[] = [];
399
+
400
+ const parts = value.split(":");
401
+
402
+ for (const [index, part] of parts.entries()) {
403
+ if (part.includes(".")) {
404
+ if (index !== parts.length - 1) {
405
+ return undefined;
406
+ }
407
+
408
+ const octets = parseIpv4Octets(part);
409
+
410
+ if (!octets) {
411
+ return undefined;
412
+ }
413
+
414
+ segments.push((octets[0] << 8) | octets[1], (octets[2] << 8) | octets[3]);
415
+ continue;
416
+ }
417
+
418
+ if (!/^[0-9a-f]{1,4}$/i.test(part)) {
419
+ return undefined;
420
+ }
421
+
422
+ segments.push(parseInt(part, 16));
423
+ }
424
+
425
+ return segments;
426
+ }
427
+
428
+ function isBlockedIpv6Segments(parts: number[]): boolean {
429
+ if (parts.length !== 8) {
430
+ return true;
431
+ }
432
+
433
+ const embeddedIpv4 = ipv4OctetsFromIpv6Segments(parts);
434
+
435
+ if (embeddedIpv4) {
436
+ return isBlockedIpv4Octets(embeddedIpv4);
437
+ }
438
+
439
+ const isAllZero = parts.every((part) => part === 0);
440
+
441
+ if (isAllZero || (parts.slice(0, 7).every((part) => part === 0) && parts[7] === 1)) {
442
+ return true;
443
+ }
444
+
445
+ const first = parts[0];
446
+
447
+ return (
448
+ (first & 0xfe00) === 0xfc00 ||
449
+ (first & 0xffc0) === 0xfe80 ||
450
+ (first & 0xff00) === 0xff00
451
+ );
452
+ }
453
+
454
+ function ipv4OctetsFromIpv6Segments(parts: number[]): number[] | undefined {
455
+ const trailingIpv4 = [
456
+ (parts[6] >> 8) & 0xff,
457
+ parts[6] & 0xff,
458
+ (parts[7] >> 8) & 0xff,
459
+ parts[7] & 0xff,
460
+ ];
461
+
462
+ if (parts.slice(0, 5).every((part) => part === 0) && (parts[5] === 0xffff || parts[5] === 0)) {
463
+ return trailingIpv4;
464
+ }
465
+
466
+ if (parts[0] === 0x64 && parts[1] === 0xff9b && parts.slice(2, 6).every((part) => part === 0)) {
467
+ return trailingIpv4;
468
+ }
469
+
470
+ if (parts[0] === 0x2002) {
471
+ return [
472
+ (parts[1] >> 8) & 0xff,
473
+ parts[1] & 0xff,
474
+ (parts[2] >> 8) & 0xff,
475
+ parts[2] & 0xff,
476
+ ];
477
+ }
478
+
479
+ return undefined;
480
+ }
@@ -0,0 +1,54 @@
1
+ import { isAgentKitError } from "../errors";
2
+
3
+ // Deadlines for outbound provider calls. Without them a hung provider stalls
4
+ // a delivery job until the runtime's own socket defaults give up.
5
+ export const CHANNEL_SEND_TIMEOUT_MS = 15_000;
6
+ export const CHANNEL_STATUS_TIMEOUT_MS = 15_000;
7
+ export const CHANNEL_DOWNLOAD_TIMEOUT_MS = 60_000;
8
+
9
+ export type SendRequestFailure = {
10
+ retryable: boolean;
11
+ code: "channel_provider_unavailable" | "channel_send_failed";
12
+ reason: string;
13
+ };
14
+
15
+ // Classifies a failure thrown by fetch() for a non-idempotent send. A deadline
16
+ // can fire after the provider has already accepted the message, and none of
17
+ // the chat providers deduplicate on our idempotency key, so retrying a timed
18
+ // out send risks delivering the reply twice.
19
+ export function classifySendRequestError(error: unknown): SendRequestFailure {
20
+ if (isOutboundGuardRejection(error)) {
21
+ return {
22
+ retryable: false,
23
+ code: "channel_send_failed",
24
+ reason: "was blocked by the outbound network guard",
25
+ };
26
+ }
27
+
28
+ if (isDeadlineError(error)) {
29
+ return {
30
+ retryable: false,
31
+ code: "channel_send_failed",
32
+ reason: "timed out before a response; the provider may already have accepted the message, so it will not be retried",
33
+ };
34
+ }
35
+
36
+ return {
37
+ retryable: true,
38
+ code: "channel_provider_unavailable",
39
+ reason: "failed before a response",
40
+ };
41
+ }
42
+
43
+ // Downloads and status checks are safe to repeat; only a guard rejection is final.
44
+ export function isRetryableReadRequestError(error: unknown): boolean {
45
+ return !isOutboundGuardRejection(error);
46
+ }
47
+
48
+ export function isDeadlineError(error: unknown): boolean {
49
+ return error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError");
50
+ }
51
+
52
+ function isOutboundGuardRejection(error: unknown): boolean {
53
+ return isAgentKitError(error) && error.code === "channel_payload_invalid";
54
+ }