@agent-native/dispatch 0.14.7 → 0.14.9

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 (42) hide show
  1. package/dist/actions/ask_app.d.ts +16 -1
  2. package/dist/actions/ask_app.js +12 -1
  3. package/dist/actions/ask_app.js.map +1 -1
  4. package/dist/actions/ask_app_status.d.ts +23 -0
  5. package/dist/actions/ask_app_status.d.ts.map +1 -0
  6. package/dist/actions/ask_app_status.js +14 -0
  7. package/dist/actions/ask_app_status.js.map +1 -0
  8. package/dist/actions/create-workspace-resource.d.ts +13 -13
  9. package/dist/actions/delete-destination.d.ts +12 -12
  10. package/dist/actions/delete-workspace-resource.d.ts +13 -13
  11. package/dist/actions/index.d.ts.map +1 -1
  12. package/dist/actions/index.js +2 -0
  13. package/dist/actions/index.js.map +1 -1
  14. package/dist/actions/update-workspace-resource.d.ts +13 -13
  15. package/dist/actions/upsert-destination.d.ts +12 -12
  16. package/dist/components/layout/Layout.d.ts +2 -1
  17. package/dist/components/layout/Layout.d.ts.map +1 -1
  18. package/dist/components/layout/Layout.js +5 -5
  19. package/dist/components/layout/Layout.js.map +1 -1
  20. package/dist/routes/pages/agents.d.ts.map +1 -1
  21. package/dist/routes/pages/agents.js +2 -2
  22. package/dist/routes/pages/agents.js.map +1 -1
  23. package/dist/server/lib/mcp-access-store.d.ts.map +1 -1
  24. package/dist/server/lib/mcp-access-store.js +2 -2
  25. package/dist/server/lib/mcp-access-store.js.map +1 -1
  26. package/dist/server/lib/mcp-gateway.d.ts +25 -5
  27. package/dist/server/lib/mcp-gateway.d.ts.map +1 -1
  28. package/dist/server/lib/mcp-gateway.js +196 -6
  29. package/dist/server/lib/mcp-gateway.js.map +1 -1
  30. package/package.json +2 -2
  31. package/src/actions/ask_app.ts +13 -1
  32. package/src/actions/ask_app_status.ts +16 -0
  33. package/src/actions/index.spec.ts +1 -0
  34. package/src/actions/index.ts +2 -0
  35. package/src/components/layout/Layout.spec.tsx +21 -0
  36. package/src/components/layout/Layout.tsx +14 -9
  37. package/src/components/workspace-app-card.spec.tsx +34 -3
  38. package/src/routes/pages/agents.tsx +1 -2
  39. package/src/server/lib/mcp-access-store.spec.ts +3 -3
  40. package/src/server/lib/mcp-access-store.ts +2 -3
  41. package/src/server/lib/mcp-gateway.spec.ts +299 -14
  42. package/src/server/lib/mcp-gateway.ts +264 -6
@@ -1,4 +1,4 @@
1
- import { callAgent, signA2AToken } from "@agent-native/core/a2a";
1
+ import { A2AClient, signA2AToken, type Task } from "@agent-native/core/a2a";
2
2
  import {
3
3
  buildMcpToolName,
4
4
  McpClientManager,
@@ -30,6 +30,23 @@ const DISPATCH_DESCRIPTION =
30
30
  const DISPATCH_COLOR = "#14B8A6";
31
31
  const TARGET_EMBED_SESSION_ATTEMPTS = 3;
32
32
  const TARGET_EMBED_SESSION_RETRY_BASE_MS = 250;
33
+ const DISPATCH_ASK_APP_DEFAULT_INLINE_WAIT_MS = 20_000;
34
+ const DISPATCH_ASK_APP_MAX_INLINE_WAIT_MS = 25_000;
35
+ const DISPATCH_ASK_APP_POLL_INTERVAL_MS = 1_500;
36
+ const DISPATCH_A2A_REQUEST_TIMEOUT_MS = 10_000;
37
+ const DISPATCH_ASK_APP_TERMINAL_STATES = new Set([
38
+ "completed",
39
+ "failed",
40
+ "canceled",
41
+ "input-required",
42
+ ]);
43
+
44
+ class DispatchAskAppInlineDeadlineError extends Error {
45
+ constructor() {
46
+ super("ask_app inline wait deadline reached");
47
+ this.name = "DispatchAskAppInlineDeadlineError";
48
+ }
49
+ }
33
50
 
34
51
  export interface DispatchMcpAccessibleApp {
35
52
  id: string;
@@ -44,6 +61,204 @@ function normalizeAppId(value: string): string {
44
61
  return value.trim().toLowerCase();
45
62
  }
46
63
 
64
+ function boundedDispatchAskAppWaitMs(raw: unknown): number {
65
+ if (raw == null || raw === "") {
66
+ return DISPATCH_ASK_APP_DEFAULT_INLINE_WAIT_MS;
67
+ }
68
+ const parsed = Number(raw);
69
+ if (!Number.isFinite(parsed)) return DISPATCH_ASK_APP_DEFAULT_INLINE_WAIT_MS;
70
+ return Math.max(
71
+ 0,
72
+ Math.min(DISPATCH_ASK_APP_MAX_INLINE_WAIT_MS, Math.trunc(parsed)),
73
+ );
74
+ }
75
+
76
+ function isTerminalDispatchTask(task: Task): boolean {
77
+ return DISPATCH_ASK_APP_TERMINAL_STATES.has(String(task.status.state));
78
+ }
79
+
80
+ function dispatchTaskText(task: Task): string {
81
+ return (
82
+ task.status.message?.parts
83
+ ?.filter(
84
+ (part): part is { type: "text"; text: string } => part.type === "text",
85
+ )
86
+ .map((part) => part.text)
87
+ .join("\n")
88
+ .trim() ?? ""
89
+ );
90
+ }
91
+
92
+ function dispatchAskAppTaskResult(
93
+ app: string,
94
+ task: Task,
95
+ ): {
96
+ app: string;
97
+ routedVia: "a2a";
98
+ taskId: string;
99
+ status: string;
100
+ response?: string;
101
+ error?: string;
102
+ inputRequired?: string;
103
+ pollAfterMs?: number;
104
+ poll?: { tool: "ask_app_status"; arguments: { app: string; taskId: string } };
105
+ message?: string;
106
+ } {
107
+ const status = String(task.status.state);
108
+ const response = dispatchTaskText(task);
109
+ const base = {
110
+ app,
111
+ routedVia: "a2a" as const,
112
+ taskId: task.id,
113
+ status,
114
+ };
115
+
116
+ if (status === "completed") {
117
+ return { ...base, response: response || "(no response)" };
118
+ }
119
+ if (status === "failed" || status === "canceled") {
120
+ return {
121
+ ...base,
122
+ ...(response ? { response } : {}),
123
+ error: response || `ask_app task ${status}.`,
124
+ };
125
+ }
126
+ if (status === "input-required") {
127
+ const inputRequired =
128
+ response || "The agent needs additional input before it can continue.";
129
+ return {
130
+ ...base,
131
+ response: inputRequired,
132
+ inputRequired,
133
+ message: inputRequired,
134
+ };
135
+ }
136
+ return {
137
+ ...base,
138
+ pollAfterMs: DISPATCH_ASK_APP_POLL_INTERVAL_MS,
139
+ poll: {
140
+ tool: "ask_app_status",
141
+ arguments: { app, taskId: task.id },
142
+ },
143
+ message:
144
+ `ask_app is still ${status}. Call ask_app_status with ` +
145
+ `taskId "${task.id}" to retrieve the final response.`,
146
+ };
147
+ }
148
+
149
+ async function createDispatchA2AClient(input: {
150
+ targetUrl: string;
151
+ userEmail: string;
152
+ orgDomain?: string;
153
+ orgSecret?: string;
154
+ deadline?: number;
155
+ }): Promise<{
156
+ client: A2AClient;
157
+ metadata: Record<string, unknown>;
158
+ }> {
159
+ const apiKeys: string[] = [];
160
+ const addSignedToken = async (preferGlobalSecret: boolean) => {
161
+ try {
162
+ const token = await signA2AToken(
163
+ input.userEmail,
164
+ input.orgDomain,
165
+ input.orgSecret,
166
+ { preferGlobalSecret },
167
+ );
168
+ if (token && !apiKeys.includes(token)) apiKeys.push(token);
169
+ } catch {
170
+ // A2A can still be configured for local/dev unauthenticated calls. If
171
+ // signing is unavailable, let the target return its own auth error.
172
+ }
173
+ };
174
+
175
+ if (process.env.A2A_SECRET?.trim()) await addSignedToken(true);
176
+ if (input.orgSecret) await addSignedToken(false);
177
+
178
+ const metadata: Record<string, unknown> = {
179
+ userEmail: input.userEmail,
180
+ ...(input.orgDomain ? { orgDomain: input.orgDomain } : {}),
181
+ ...(getRequestContext()?.requestOrigin
182
+ ? { requestOrigin: getRequestContext()?.requestOrigin }
183
+ : {}),
184
+ };
185
+ const remainingMs =
186
+ input.deadline == null ? null : input.deadline - Date.now();
187
+ return {
188
+ client: new A2AClient(input.targetUrl, apiKeys[0], {
189
+ requestTimeoutMs:
190
+ remainingMs == null
191
+ ? DISPATCH_A2A_REQUEST_TIMEOUT_MS
192
+ : Math.max(1, Math.min(DISPATCH_A2A_REQUEST_TIMEOUT_MS, remainingMs)),
193
+ ...(apiKeys.length > 1 ? { fallbackApiKeys: apiKeys.slice(1) } : {}),
194
+ }),
195
+ metadata,
196
+ };
197
+ }
198
+
199
+ function isTransientDispatchAskAppStatusError(err: unknown): boolean {
200
+ 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
+ );
206
+ }
207
+
208
+ async function runBeforeDispatchAskAppDeadline<T>(
209
+ operation: () => Promise<T>,
210
+ deadline: number,
211
+ ): Promise<T> {
212
+ const remainingMs = deadline - Date.now();
213
+ if (remainingMs <= 0) throw new DispatchAskAppInlineDeadlineError();
214
+
215
+ let timeout: ReturnType<typeof setTimeout> | undefined;
216
+ try {
217
+ return await Promise.race([
218
+ operation(),
219
+ new Promise<T>((_resolve, reject) => {
220
+ timeout = setTimeout(
221
+ () => reject(new DispatchAskAppInlineDeadlineError()),
222
+ remainingMs,
223
+ );
224
+ }),
225
+ ]);
226
+ } finally {
227
+ if (timeout) clearTimeout(timeout);
228
+ }
229
+ }
230
+
231
+ async function waitForDispatchA2ATask(
232
+ client: A2AClient,
233
+ task: Task,
234
+ deadline: number | undefined,
235
+ ): Promise<Task> {
236
+ if (deadline == null || isTerminalDispatchTask(task)) return task;
237
+ let current = task;
238
+ while (!isTerminalDispatchTask(current)) {
239
+ const remaining = deadline - Date.now();
240
+ if (remaining <= 0) return current;
241
+ await new Promise((resolve) =>
242
+ setTimeout(
243
+ resolve,
244
+ Math.min(DISPATCH_ASK_APP_POLL_INTERVAL_MS, remaining),
245
+ ),
246
+ );
247
+ if (Date.now() >= deadline) return current;
248
+ try {
249
+ current = await runBeforeDispatchAskAppDeadline(
250
+ () => client.getTask(task.id),
251
+ deadline,
252
+ );
253
+ } catch (err) {
254
+ if (err instanceof DispatchAskAppInlineDeadlineError) return current;
255
+ if (!isTransientDispatchAskAppStatusError(err)) throw err;
256
+ if (Date.now() >= deadline) return current;
257
+ }
258
+ }
259
+ return current;
260
+ }
261
+
47
262
  function normalizeBaseUrl(raw: string | undefined | null): string | null {
48
263
  const value = raw?.trim();
49
264
  if (!value) return null;
@@ -281,7 +496,8 @@ export async function resolveGrantedDispatchMcpApp(
281
496
  export async function askGrantedDispatchMcpApp(
282
497
  app: string,
283
498
  message: string,
284
- ): Promise<{ app: string; routedVia: "a2a"; response: string }> {
499
+ options?: { async?: boolean; maxWaitMs?: number },
500
+ ): Promise<ReturnType<typeof dispatchAskAppTaskResult>> {
285
501
  const trimmedMessage = message.trim();
286
502
  if (!trimmedMessage) throw new Error("message is required");
287
503
  const target = await resolveGrantedDispatchMcpApp(app);
@@ -296,13 +512,55 @@ export async function askGrantedDispatchMcpApp(
296
512
  ])
297
513
  : [null, null];
298
514
 
299
- const response = await callAgent(target.url, trimmedMessage, {
515
+ const inlineWaitMs =
516
+ options?.async === true
517
+ ? 0
518
+ : boundedDispatchAskAppWaitMs(options?.maxWaitMs);
519
+ const deadline = inlineWaitMs > 0 ? Date.now() + inlineWaitMs : undefined;
520
+
521
+ const { client, metadata } = await createDispatchA2AClient({
522
+ targetUrl: target.url,
523
+ userEmail,
524
+ orgDomain: orgDomain ?? undefined,
525
+ orgSecret: orgSecret ?? undefined,
526
+ deadline,
527
+ });
528
+ const task = await client.send(
529
+ {
530
+ role: "user",
531
+ parts: [{ type: "text", text: trimmedMessage }],
532
+ },
533
+ { async: true, metadata },
534
+ );
535
+ const finalOrRunning = await waitForDispatchA2ATask(client, task, deadline);
536
+ return dispatchAskAppTaskResult(target.id, finalOrRunning);
537
+ }
538
+
539
+ export async function getGrantedDispatchMcpAppTask(
540
+ app: string,
541
+ taskId: string,
542
+ ): Promise<ReturnType<typeof dispatchAskAppTaskResult>> {
543
+ const trimmedTaskId = taskId.trim();
544
+ if (!trimmedTaskId) throw new Error("taskId is required");
545
+ const target = await resolveGrantedDispatchMcpApp(app);
546
+ const userEmail = getRequestUserEmail();
547
+ if (!userEmail) throw new Error("no authenticated user");
548
+
549
+ const orgId = getRequestOrgId();
550
+ const [orgDomain, orgSecret] = orgId
551
+ ? await Promise.all([
552
+ getOrgDomain(orgId).catch(() => null),
553
+ getOrgA2ASecret(orgId).catch(() => null),
554
+ ])
555
+ : [null, null];
556
+ const { client } = await createDispatchA2AClient({
557
+ targetUrl: target.url,
300
558
  userEmail,
301
559
  orgDomain: orgDomain ?? undefined,
302
560
  orgSecret: orgSecret ?? undefined,
303
- timeoutMs: 5 * 60_000,
304
561
  });
305
- return { app: target.id, routedVia: "a2a", response };
562
+ const task = await client.getTask(trimmedTaskId);
563
+ return dispatchAskAppTaskResult(target.id, task);
306
564
  }
307
565
 
308
566
  export async function openGrantedDispatchMcpApp(input: {
@@ -434,7 +692,7 @@ async function callTargetCreateEmbedSession(input: {
434
692
  servers: {
435
693
  [serverId]: {
436
694
  type: "http",
437
- url: `${appBaseUrl(input.app)}/_agent-native/mcp`,
695
+ url: `${appBaseUrl(input.app)}/mcp`,
438
696
  headers: {
439
697
  Authorization: `Bearer ${input.token}`,
440
698
  },