@agent-native/dispatch 0.14.11 → 0.14.13

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.
@@ -34,6 +34,7 @@ const DISPATCH_ASK_APP_DEFAULT_INLINE_WAIT_MS = 20_000;
34
34
  const DISPATCH_ASK_APP_MAX_INLINE_WAIT_MS = 25_000;
35
35
  const DISPATCH_ASK_APP_POLL_INTERVAL_MS = 1_500;
36
36
  const DISPATCH_A2A_REQUEST_TIMEOUT_MS = 10_000;
37
+ const DISPATCH_ASK_APP_STATUS_RETRY_DELAYS_MS = [250, 750, 1_500] as const;
37
38
  const DISPATCH_ASK_APP_TERMINAL_STATES = new Set([
38
39
  "completed",
39
40
  "failed",
@@ -89,10 +90,13 @@ function dispatchTaskText(task: Task): string {
89
90
  );
90
91
  }
91
92
 
92
- function dispatchAskAppTaskResult(
93
- app: string,
94
- task: Task,
95
- ): {
93
+ type DispatchAskAppStatusErrorCategory =
94
+ | "transport"
95
+ | "timeout"
96
+ | "upstream_5xx"
97
+ | "rate_limited";
98
+
99
+ type DispatchAskAppTaskResult = {
96
100
  app: string;
97
101
  routedVia: "a2a";
98
102
  taskId: string;
@@ -100,10 +104,19 @@ function dispatchAskAppTaskResult(
100
104
  response?: string;
101
105
  error?: string;
102
106
  inputRequired?: string;
107
+ statusRead?: "unavailable";
108
+ retryable?: true;
109
+ errorCategory?: DispatchAskAppStatusErrorCategory;
110
+ attempts?: number;
103
111
  pollAfterMs?: number;
104
112
  poll?: { tool: "ask_app_status"; arguments: { app: string; taskId: string } };
105
113
  message?: string;
106
- } {
114
+ };
115
+
116
+ function dispatchAskAppTaskResult(
117
+ app: string,
118
+ task: Task,
119
+ ): DispatchAskAppTaskResult {
107
120
  const status = String(task.status.state);
108
121
  const response = dispatchTaskText(task);
109
122
  const base = {
@@ -197,12 +210,76 @@ async function createDispatchA2AClient(input: {
197
210
  }
198
211
 
199
212
  function isTransientDispatchAskAppStatusError(err: unknown): boolean {
213
+ return dispatchAskAppStatusErrorCategory(err) != null;
214
+ }
215
+
216
+ function dispatchAskAppStatusErrorCategory(
217
+ err: unknown,
218
+ ): DispatchAskAppStatusErrorCategory | null {
200
219
  const message = err instanceof Error ? err.message : String(err ?? "");
201
- return (
202
- /\bfetch failed\b|failed to fetch|networkerror|socket hang up|econnreset|etimedout|timeout|aborted/i.test(
203
- message,
204
- ) || /A2A request failed \((?:429|500|502|503|504)\)/i.test(message)
205
- );
220
+ const causeCode = dispatchAskAppStatusErrorCauseCode(err) ?? "";
221
+ const diagnostic = `${message} ${causeCode}`;
222
+ if (/A2A request failed \(429\)/i.test(message)) return "rate_limited";
223
+ if (/A2A request failed \((?:500|502|503|504)\)/i.test(message)) {
224
+ return "upstream_5xx";
225
+ }
226
+ if (/etimedout|timeout|aborted|aborterror/i.test(diagnostic)) {
227
+ return "timeout";
228
+ }
229
+ if (
230
+ /\bfetch failed\b|failed to fetch|networkerror|socket hang up|econnreset/i.test(
231
+ diagnostic,
232
+ )
233
+ ) {
234
+ return "transport";
235
+ }
236
+ return null;
237
+ }
238
+
239
+ function dispatchAskAppStatusErrorCauseCode(err: unknown): string | undefined {
240
+ if (!(err instanceof Error)) return undefined;
241
+ const directCode = (err as Error & { code?: unknown }).code;
242
+ if (typeof directCode === "string" && directCode.trim()) {
243
+ return directCode.trim();
244
+ }
245
+ if (!err.cause || typeof err.cause !== "object") return undefined;
246
+ const code = (err.cause as { code?: unknown }).code;
247
+ return typeof code === "string" && code.trim() ? code.trim() : undefined;
248
+ }
249
+
250
+ function dispatchAskAppStatusOriginHost(origin: string): string {
251
+ try {
252
+ return new URL(origin).host;
253
+ } catch {
254
+ return "unknown";
255
+ }
256
+ }
257
+
258
+ function dispatchAskAppStatusReadUnavailableResult(
259
+ app: string,
260
+ taskId: string,
261
+ errorCategory: DispatchAskAppStatusErrorCategory,
262
+ attempts: number,
263
+ ): DispatchAskAppTaskResult {
264
+ return {
265
+ app,
266
+ routedVia: "a2a",
267
+ taskId,
268
+ status: "unknown",
269
+ statusRead: "unavailable",
270
+ retryable: true,
271
+ errorCategory,
272
+ attempts,
273
+ pollAfterMs: DISPATCH_ASK_APP_POLL_INTERVAL_MS,
274
+ poll: {
275
+ tool: "ask_app_status",
276
+ arguments: { app, taskId },
277
+ },
278
+ message:
279
+ "The durable ask_app task status could not be read after bounded retries. " +
280
+ "The task may still be running or completed. Retry ask_app_status " +
281
+ "with the same app and taskId; do not resubmit ask_app.",
282
+ };
206
283
  }
207
284
 
208
285
  async function runBeforeDispatchAskAppDeadline<T>(
@@ -539,7 +616,7 @@ export async function askGrantedDispatchMcpApp(
539
616
  export async function getGrantedDispatchMcpAppTask(
540
617
  app: string,
541
618
  taskId: string,
542
- ): Promise<ReturnType<typeof dispatchAskAppTaskResult>> {
619
+ ): Promise<DispatchAskAppTaskResult> {
543
620
  const trimmedTaskId = taskId.trim();
544
621
  if (!trimmedTaskId) throw new Error("taskId is required");
545
622
  const target = await resolveGrantedDispatchMcpApp(app);
@@ -559,8 +636,49 @@ export async function getGrantedDispatchMcpAppTask(
559
636
  orgDomain: orgDomain ?? undefined,
560
637
  orgSecret: orgSecret ?? undefined,
561
638
  });
562
- const task = await client.getTask(trimmedTaskId);
563
- return dispatchAskAppTaskResult(target.id, task);
639
+ const maxAttempts = DISPATCH_ASK_APP_STATUS_RETRY_DELAYS_MS.length + 1;
640
+ for (
641
+ let attempt = 0;
642
+ attempt <= DISPATCH_ASK_APP_STATUS_RETRY_DELAYS_MS.length;
643
+ attempt++
644
+ ) {
645
+ const startedAt = Date.now();
646
+ try {
647
+ const task = await client.getTask(trimmedTaskId);
648
+ return dispatchAskAppTaskResult(target.id, task);
649
+ } catch (err) {
650
+ const delayMs = DISPATCH_ASK_APP_STATUS_RETRY_DELAYS_MS[attempt];
651
+ const errorCategory = dispatchAskAppStatusErrorCategory(err);
652
+ const retryable = errorCategory != null;
653
+ const willRetry = retryable && delayMs != null;
654
+ if (retryable) {
655
+ console.warn("[ask_app_status] tasks/get attempt failed", {
656
+ app: target.id,
657
+ routedVia: "a2a",
658
+ taskId: trimmedTaskId,
659
+ originHost: dispatchAskAppStatusOriginHost(target.url),
660
+ attempt: attempt + 1,
661
+ maxAttempts,
662
+ elapsedMs: Date.now() - startedAt,
663
+ errorCategory,
664
+ errorName: err instanceof Error ? err.name : typeof err,
665
+ causeCode: dispatchAskAppStatusErrorCauseCode(err),
666
+ willRetry,
667
+ });
668
+ }
669
+ if (!retryable) throw err;
670
+ if (delayMs == null) {
671
+ return dispatchAskAppStatusReadUnavailableResult(
672
+ target.id,
673
+ trimmedTaskId,
674
+ errorCategory,
675
+ maxAttempts,
676
+ );
677
+ }
678
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
679
+ }
680
+ }
681
+ throw new Error("ask_app_status retry loop exited unexpectedly.");
564
682
  }
565
683
 
566
684
  export async function openGrantedDispatchMcpApp(input: {