@agent-native/dispatch 0.32.0 → 0.33.0-nightly-20260831223557
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/dist/actions/ask_app.js +0 -1
- package/dist/actions/ask_app.js.map +1 -1
- package/dist/actions/provider-api-register.d.ts +8 -8
- package/dist/components/automation-create-dialog.d.ts +16 -0
- package/dist/components/automation-create-dialog.d.ts.map +1 -0
- package/dist/components/automation-create-dialog.js +213 -0
- package/dist/components/automation-create-dialog.js.map +1 -0
- package/dist/components/automation-details-panel.d.ts.map +1 -1
- package/dist/components/automation-details-panel.js +35 -4
- package/dist/components/automation-details-panel.js.map +1 -1
- package/dist/hooks/use-navigation-state.d.ts +1 -0
- package/dist/hooks/use-navigation-state.d.ts.map +1 -1
- package/dist/hooks/use-navigation-state.js +5 -0
- package/dist/hooks/use-navigation-state.js.map +1 -1
- package/dist/lib/automation-display.d.ts.map +1 -1
- package/dist/lib/automation-display.js +4 -0
- package/dist/lib/automation-display.js.map +1 -1
- package/dist/lib/automations.d.ts +2 -1
- package/dist/lib/automations.d.ts.map +1 -1
- package/dist/lib/automations.js.map +1 -1
- package/dist/routes/pages/automations.d.ts.map +1 -1
- package/dist/routes/pages/automations.js +16 -24
- package/dist/routes/pages/automations.js.map +1 -1
- package/dist/server/lib/mcp-gateway.d.ts.map +1 -1
- package/dist/server/lib/mcp-gateway.js +27 -3
- package/dist/server/lib/mcp-gateway.js.map +1 -1
- package/package.json +3 -3
- package/src/actions/ask_app.ts +0 -1
- package/src/components/automation-create-dialog.tsx +759 -0
- package/src/components/automation-details-panel.tsx +80 -12
- package/src/hooks/use-navigation-state.spec.ts +13 -0
- package/src/hooks/use-navigation-state.ts +6 -0
- package/src/lib/automation-display.ts +2 -0
- package/src/lib/automations.ts +2 -1
- package/src/routes/pages/automations.spec.tsx +11 -0
- package/src/routes/pages/automations.tsx +27 -57
- package/src/server/lib/mcp-gateway.spec.ts +37 -2
- package/src/server/lib/mcp-gateway.ts +40 -3
|
@@ -1,11 +1,17 @@
|
|
|
1
|
+
import { appPath } from "@agent-native/core/client/api-path";
|
|
2
|
+
import { writeClipboardText } from "@agent-native/core/client/clipboard";
|
|
1
3
|
import { useActionQuery } from "@agent-native/core/client/hooks";
|
|
4
|
+
import { useT } from "@agent-native/core/client/i18n";
|
|
2
5
|
import {
|
|
3
6
|
IconAlertTriangle,
|
|
7
|
+
IconCheck,
|
|
8
|
+
IconCopy,
|
|
4
9
|
IconClock,
|
|
5
10
|
IconExternalLink,
|
|
6
11
|
IconPlayerPause,
|
|
7
12
|
IconPlayerPlay,
|
|
8
13
|
} from "@tabler/icons-react";
|
|
14
|
+
import { useState } from "react";
|
|
9
15
|
import { Link } from "react-router";
|
|
10
16
|
|
|
11
17
|
import {
|
|
@@ -107,6 +113,7 @@ export function AutomationDetailsPanel({
|
|
|
107
113
|
isToggling = false,
|
|
108
114
|
onToggle,
|
|
109
115
|
}: AutomationDetailsPanelProps) {
|
|
116
|
+
const t = useT();
|
|
110
117
|
const runsQuery = useActionQuery<AutomationRun[]>(
|
|
111
118
|
"list-automation-runs",
|
|
112
119
|
{
|
|
@@ -118,7 +125,18 @@ export function AutomationDetailsPanel({
|
|
|
118
125
|
);
|
|
119
126
|
const runs = runsQuery.data ?? [];
|
|
120
127
|
const status = automationStatus(automation);
|
|
121
|
-
const isScheduled = automation.triggerType
|
|
128
|
+
const isScheduled = automation.triggerType === "schedule";
|
|
129
|
+
const [copied, setCopied] = useState(false);
|
|
130
|
+
const webhookUrl =
|
|
131
|
+
automation.webhookPath && typeof window !== "undefined"
|
|
132
|
+
? window.location.origin + appPath(automation.webhookPath)
|
|
133
|
+
: null;
|
|
134
|
+
const triggerLabel =
|
|
135
|
+
automation.triggerType === "webhook"
|
|
136
|
+
? t("jobs.webhook", { defaultValue: "Webhook" })
|
|
137
|
+
: isScheduled
|
|
138
|
+
? "Schedule"
|
|
139
|
+
: "Event";
|
|
122
140
|
const prompt = automation.body?.trim();
|
|
123
141
|
const hasExecutionTarget = Boolean(
|
|
124
142
|
automation.executionHostId ||
|
|
@@ -156,8 +174,7 @@ export function AutomationDetailsPanel({
|
|
|
156
174
|
{automation.name}
|
|
157
175
|
</h2>
|
|
158
176
|
<p className="mt-1 text-xs text-muted-foreground">
|
|
159
|
-
{
|
|
160
|
-
{automationScopeLabel(automation)}
|
|
177
|
+
{triggerLabel} · {automationScopeLabel(automation)}
|
|
161
178
|
</p>
|
|
162
179
|
</div>
|
|
163
180
|
</header>
|
|
@@ -183,7 +200,15 @@ export function AutomationDetailsPanel({
|
|
|
183
200
|
<dl className="mt-3 grid grid-cols-2 gap-x-4 gap-y-4">
|
|
184
201
|
<DetailField
|
|
185
202
|
label="Trigger"
|
|
186
|
-
value={
|
|
203
|
+
value={
|
|
204
|
+
automation.triggerType === "webhook"
|
|
205
|
+
? t("jobs.webhookTrigger", {
|
|
206
|
+
defaultValue: "Webhook-triggered",
|
|
207
|
+
})
|
|
208
|
+
: isScheduled
|
|
209
|
+
? "Scheduled"
|
|
210
|
+
: "Event-triggered"
|
|
211
|
+
}
|
|
187
212
|
/>
|
|
188
213
|
<DetailField
|
|
189
214
|
label={isScheduled ? "Schedule" : "Event"}
|
|
@@ -204,6 +229,47 @@ export function AutomationDetailsPanel({
|
|
|
204
229
|
/>
|
|
205
230
|
</>
|
|
206
231
|
) : null}
|
|
232
|
+
{webhookUrl ? (
|
|
233
|
+
<div className="col-span-2 rounded-md border border-primary/20 bg-primary/5 p-3">
|
|
234
|
+
<div className="flex items-center justify-between gap-3">
|
|
235
|
+
<span className="text-xs font-medium text-foreground">
|
|
236
|
+
{t("jobs.webhookUrl", { defaultValue: "Webhook URL" })}
|
|
237
|
+
</span>
|
|
238
|
+
<Button
|
|
239
|
+
type="button"
|
|
240
|
+
variant="outline"
|
|
241
|
+
size="sm"
|
|
242
|
+
className="h-7 shrink-0 gap-1.5 text-xs"
|
|
243
|
+
onClick={() => {
|
|
244
|
+
void writeClipboardText(webhookUrl).then((didCopy) => {
|
|
245
|
+
setCopied(didCopy);
|
|
246
|
+
if (didCopy) {
|
|
247
|
+
window.setTimeout(() => setCopied(false), 1800);
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
}}
|
|
251
|
+
>
|
|
252
|
+
{copied ? (
|
|
253
|
+
<IconCheck className="size-3.5" />
|
|
254
|
+
) : (
|
|
255
|
+
<IconCopy className="size-3.5" />
|
|
256
|
+
)}
|
|
257
|
+
{copied
|
|
258
|
+
? t("common.copied", { defaultValue: "Copied" })
|
|
259
|
+
: t("common.copy", { defaultValue: "Copy URL" })}
|
|
260
|
+
</Button>
|
|
261
|
+
</div>
|
|
262
|
+
<code className="mt-2 block break-all text-[11px] leading-5 text-muted-foreground">
|
|
263
|
+
{webhookUrl}
|
|
264
|
+
</code>
|
|
265
|
+
<p className="mt-2 text-xs text-muted-foreground">
|
|
266
|
+
{t("jobs.webhookUrlHint", {
|
|
267
|
+
defaultValue:
|
|
268
|
+
"Paste this URL into a service that sends HTTP POST webhooks.",
|
|
269
|
+
})}
|
|
270
|
+
</p>
|
|
271
|
+
</div>
|
|
272
|
+
) : null}
|
|
207
273
|
<DetailField
|
|
208
274
|
label="Condition"
|
|
209
275
|
value={automation.condition || "Always"}
|
|
@@ -218,14 +284,16 @@ export function AutomationDetailsPanel({
|
|
|
218
284
|
/>
|
|
219
285
|
<DetailField label="Mode" value={automation.mode || "Agentic"} />
|
|
220
286
|
<DetailField label="Runs as" value={runAsLabel(automation.runAs)} />
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
287
|
+
{isScheduled ? (
|
|
288
|
+
<DetailField
|
|
289
|
+
label="Next run"
|
|
290
|
+
value={
|
|
291
|
+
automation.enabled
|
|
292
|
+
? formatTimestamp(automation.nextRun)
|
|
293
|
+
: "Paused"
|
|
294
|
+
}
|
|
295
|
+
/>
|
|
296
|
+
) : null}
|
|
229
297
|
<DetailField
|
|
230
298
|
label="Last run"
|
|
231
299
|
value={formatTimestamp(automation.lastRun)}
|
|
@@ -97,6 +97,19 @@ describe("buildDispatchNavigationState", () => {
|
|
|
97
97
|
});
|
|
98
98
|
});
|
|
99
99
|
|
|
100
|
+
it("keeps the selected automation in application state", () => {
|
|
101
|
+
expect(
|
|
102
|
+
buildDispatchNavigationState(
|
|
103
|
+
"/automations",
|
|
104
|
+
"?automationId=personal%3Amorning-digest",
|
|
105
|
+
),
|
|
106
|
+
).toEqual({
|
|
107
|
+
view: "automations",
|
|
108
|
+
path: "/automations",
|
|
109
|
+
automationId: "personal:morning-digest",
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
100
113
|
it("recognizes Admin routes without losing the underlying view", () => {
|
|
101
114
|
expect(buildDispatchNavigationState("/admin/metrics")).toEqual({
|
|
102
115
|
view: "metrics",
|
|
@@ -42,6 +42,7 @@ export interface NavigationState {
|
|
|
42
42
|
usageScope?: "me" | "workspace" | "app";
|
|
43
43
|
usageUserEmail?: string;
|
|
44
44
|
usageAppId?: string;
|
|
45
|
+
automationId?: string;
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
export function useNavigationState(extensions?: DispatchExtensionConfig) {
|
|
@@ -214,6 +215,11 @@ export function buildDispatchNavigationState(
|
|
|
214
215
|
if (usageUserEmail) state.usageUserEmail = usageUserEmail;
|
|
215
216
|
}
|
|
216
217
|
|
|
218
|
+
if (state.view === "automations") {
|
|
219
|
+
const automationId = new URLSearchParams(search).get("automationId");
|
|
220
|
+
if (automationId) state.automationId = automationId;
|
|
221
|
+
}
|
|
222
|
+
|
|
217
223
|
return state;
|
|
218
224
|
}
|
|
219
225
|
|
|
@@ -33,6 +33,7 @@ export function automationTroubleshootPath(
|
|
|
33
33
|
|
|
34
34
|
export function automationTarget(item: DispatchAutomationItem): string {
|
|
35
35
|
if (item.triggerType === "event" && item.event) return item.event;
|
|
36
|
+
if (item.triggerType === "webhook") return "On webhook";
|
|
36
37
|
if (item.scheduleDescription) return item.scheduleDescription;
|
|
37
38
|
if (item.schedule) return item.schedule;
|
|
38
39
|
return item.triggerType || "schedule";
|
|
@@ -72,6 +73,7 @@ export function automationLastCheck(item: DispatchAutomationItem): string {
|
|
|
72
73
|
export function automationNextRun(item: DispatchAutomationItem): string {
|
|
73
74
|
if (!item.enabled) return "paused";
|
|
74
75
|
if (item.triggerType === "event") return "on event";
|
|
76
|
+
if (item.triggerType === "webhook") return "on webhook";
|
|
75
77
|
return item.nextRun ? relativeRunTime(item.nextRun) : "not scheduled";
|
|
76
78
|
}
|
|
77
79
|
|
package/src/lib/automations.ts
CHANGED
|
@@ -9,8 +9,9 @@ export interface DispatchAutomationItem {
|
|
|
9
9
|
orgId?: string;
|
|
10
10
|
scope?: "personal" | "organization";
|
|
11
11
|
canUpdate?: boolean;
|
|
12
|
-
triggerType?: "schedule" | "event" | (string & {});
|
|
12
|
+
triggerType?: "schedule" | "event" | "webhook" | (string & {});
|
|
13
13
|
event?: string;
|
|
14
|
+
webhookPath?: string;
|
|
14
15
|
schedule?: string;
|
|
15
16
|
scheduleDescription?: string;
|
|
16
17
|
condition?: string;
|
|
@@ -64,7 +64,18 @@ vi.mock("@tanstack/react-query", () => ({
|
|
|
64
64
|
}));
|
|
65
65
|
|
|
66
66
|
vi.mock("@agent-native/core/client/hooks", () => ({
|
|
67
|
+
useActionMutation: () => ({
|
|
68
|
+
isPending: false,
|
|
69
|
+
mutate: vi.fn(),
|
|
70
|
+
reset: vi.fn(),
|
|
71
|
+
error: null,
|
|
72
|
+
}),
|
|
67
73
|
useChangeVersions: () => 0,
|
|
74
|
+
useActionQuery: () => ({
|
|
75
|
+
data: [],
|
|
76
|
+
isLoading: false,
|
|
77
|
+
error: null,
|
|
78
|
+
}),
|
|
68
79
|
}));
|
|
69
80
|
|
|
70
81
|
vi.mock("@agent-native/core/client/agent-chat", () => ({
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { sendToAgentChat } from "@agent-native/core/client/agent-chat";
|
|
2
|
-
import { PromptComposer } from "@agent-native/core/client/composer";
|
|
3
1
|
import { useChangeVersions } from "@agent-native/core/client/hooks";
|
|
4
2
|
import {
|
|
5
3
|
IconFileSearch,
|
|
@@ -13,16 +11,12 @@ import { useMemo, useState } from "react";
|
|
|
13
11
|
import { Link, useSearchParams } from "react-router";
|
|
14
12
|
import { toast } from "sonner";
|
|
15
13
|
|
|
14
|
+
import { AutomationCreateDialog } from "../../components/automation-create-dialog";
|
|
16
15
|
import { AutomationDetailsPanel } from "../../components/automation-details-panel";
|
|
17
16
|
import { DispatchShell } from "../../components/dispatch-shell";
|
|
18
17
|
import { Badge } from "../../components/ui/badge";
|
|
19
18
|
import { Button } from "../../components/ui/button";
|
|
20
19
|
import { Input } from "../../components/ui/input";
|
|
21
|
-
import {
|
|
22
|
-
Popover,
|
|
23
|
-
PopoverContent,
|
|
24
|
-
PopoverTrigger,
|
|
25
|
-
} from "../../components/ui/popover";
|
|
26
20
|
import {
|
|
27
21
|
Select,
|
|
28
22
|
SelectContent,
|
|
@@ -134,57 +128,24 @@ function useToggleAutomation() {
|
|
|
134
128
|
});
|
|
135
129
|
}
|
|
136
130
|
|
|
137
|
-
function CreateAutomationButton() {
|
|
131
|
+
function CreateAutomationButton({ onCreated }: { onCreated: () => void }) {
|
|
138
132
|
const [open, setOpen] = useState(false);
|
|
139
|
-
const [scope, setScope] = useState<"personal" | "organization">("personal");
|
|
140
|
-
|
|
141
|
-
function handleSubmit(text: string) {
|
|
142
|
-
const trimmed = text.trim();
|
|
143
|
-
if (!trimmed) return;
|
|
144
|
-
window.dispatchEvent(
|
|
145
|
-
new CustomEvent("agent-panel:set-mode", {
|
|
146
|
-
detail: { mode: "chat" },
|
|
147
|
-
}),
|
|
148
|
-
);
|
|
149
|
-
sendToAgentChat({
|
|
150
|
-
message: trimmed,
|
|
151
|
-
context: `The user wants to create a new automation. Scope: ${scope}. Use manage-automations with action=define to create it. Ask clarifying questions if needed about what event to trigger on, conditions, and what actions to take.`,
|
|
152
|
-
submit: true,
|
|
153
|
-
newTab: true,
|
|
154
|
-
});
|
|
155
|
-
setOpen(false);
|
|
156
|
-
}
|
|
157
133
|
|
|
158
134
|
return (
|
|
159
|
-
|
|
160
|
-
<
|
|
161
|
-
<
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
draftScope="dispatch-automations:create"
|
|
174
|
-
onSubmit={handleSubmit}
|
|
175
|
-
/>
|
|
176
|
-
<select
|
|
177
|
-
value={scope}
|
|
178
|
-
onChange={(event) =>
|
|
179
|
-
setScope(event.target.value as "personal" | "organization")
|
|
180
|
-
}
|
|
181
|
-
className="mt-2 w-full cursor-pointer rounded-md border border-input bg-background px-3 py-1.5 text-xs text-foreground"
|
|
182
|
-
>
|
|
183
|
-
<option value="personal">Personal</option>
|
|
184
|
-
<option value="organization">Organization</option>
|
|
185
|
-
</select>
|
|
186
|
-
</PopoverContent>
|
|
187
|
-
</Popover>
|
|
135
|
+
<>
|
|
136
|
+
<Button size="sm" onClick={() => setOpen(true)}>
|
|
137
|
+
<IconPlus size={14} />
|
|
138
|
+
New automation
|
|
139
|
+
</Button>
|
|
140
|
+
<AutomationCreateDialog
|
|
141
|
+
open={open}
|
|
142
|
+
onOpenChange={setOpen}
|
|
143
|
+
onCreated={() => {
|
|
144
|
+
setOpen(false);
|
|
145
|
+
onCreated();
|
|
146
|
+
}}
|
|
147
|
+
/>
|
|
148
|
+
</>
|
|
188
149
|
);
|
|
189
150
|
}
|
|
190
151
|
|
|
@@ -194,6 +155,7 @@ export default function AutomationsRoute() {
|
|
|
194
155
|
const [query, setQuery] = useState("");
|
|
195
156
|
const automationsQuery = useAutomations();
|
|
196
157
|
const toggleAutomation = useToggleAutomation();
|
|
158
|
+
const queryClient = useQueryClient();
|
|
197
159
|
const automations = automationsQuery.data ?? [];
|
|
198
160
|
const visibleAutomations = useMemo(
|
|
199
161
|
() =>
|
|
@@ -215,6 +177,7 @@ export default function AutomationsRoute() {
|
|
|
215
177
|
item.event,
|
|
216
178
|
item.schedule,
|
|
217
179
|
item.scheduleDescription,
|
|
180
|
+
item.webhookPath,
|
|
218
181
|
item.body,
|
|
219
182
|
item.model,
|
|
220
183
|
item.domain,
|
|
@@ -259,7 +222,7 @@ export default function AutomationsRoute() {
|
|
|
259
222
|
return (
|
|
260
223
|
<DispatchShell
|
|
261
224
|
title="Automations"
|
|
262
|
-
description="
|
|
225
|
+
description="Create schedule, webhook, or app-event automations, then inspect their checks and past runs."
|
|
263
226
|
>
|
|
264
227
|
<section className="flex flex-col gap-4">
|
|
265
228
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
|
@@ -304,7 +267,14 @@ export default function AutomationsRoute() {
|
|
|
304
267
|
<SelectItem value="all">All apps</SelectItem>
|
|
305
268
|
</SelectContent>
|
|
306
269
|
</Select>
|
|
307
|
-
<CreateAutomationButton
|
|
270
|
+
<CreateAutomationButton
|
|
271
|
+
onCreated={() => {
|
|
272
|
+
toast.success("Automation created");
|
|
273
|
+
void queryClient.invalidateQueries({
|
|
274
|
+
queryKey: AUTOMATIONS_QUERY_KEY,
|
|
275
|
+
});
|
|
276
|
+
}}
|
|
277
|
+
/>
|
|
308
278
|
</div>
|
|
309
279
|
</div>
|
|
310
280
|
|
|
@@ -415,14 +415,16 @@ describe("askGrantedDispatchMcpApp", () => {
|
|
|
415
415
|
{ type: "text", text: "Build a weekly active users dashboard." },
|
|
416
416
|
],
|
|
417
417
|
},
|
|
418
|
-
{
|
|
418
|
+
expect.objectContaining({
|
|
419
419
|
async: true,
|
|
420
|
+
deadlineMs: expect.any(Number),
|
|
421
|
+
idempotencyKey: expect.stringMatching(/^ask-app:/),
|
|
420
422
|
metadata: {
|
|
421
423
|
userEmail: "owner@example.test",
|
|
422
424
|
orgDomain: "builder.io",
|
|
423
425
|
requestOrigin: "http://localhost:8092",
|
|
424
426
|
},
|
|
425
|
-
},
|
|
427
|
+
}),
|
|
426
428
|
);
|
|
427
429
|
expect(result).toMatchObject({
|
|
428
430
|
app: "analytics",
|
|
@@ -433,6 +435,31 @@ describe("askGrantedDispatchMcpApp", () => {
|
|
|
433
435
|
});
|
|
434
436
|
});
|
|
435
437
|
|
|
438
|
+
it("reuses the MCP request identity for transport retries", async () => {
|
|
439
|
+
const requestContext = {
|
|
440
|
+
userEmail: "owner@example.test",
|
|
441
|
+
orgId: "org-1",
|
|
442
|
+
requestOrigin: "http://localhost:8092",
|
|
443
|
+
mcpRequestId: "session-1:request-42",
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
await runWithRequestContext(requestContext, () =>
|
|
447
|
+
askGrantedDispatchMcpApp("analytics", "Retry this request.", {
|
|
448
|
+
async: true,
|
|
449
|
+
}),
|
|
450
|
+
);
|
|
451
|
+
await runWithRequestContext(requestContext, () =>
|
|
452
|
+
askGrantedDispatchMcpApp("analytics", "Retry this request.", {
|
|
453
|
+
async: true,
|
|
454
|
+
}),
|
|
455
|
+
);
|
|
456
|
+
|
|
457
|
+
const firstKey = mocks.a2aSend.mock.calls[0]?.[1].idempotencyKey;
|
|
458
|
+
const secondKey = mocks.a2aSend.mock.calls[1]?.[1].idempotencyKey;
|
|
459
|
+
expect(firstKey).toMatch(/^ask-app:v1:[0-9a-f]{64}$/);
|
|
460
|
+
expect(secondKey).toBe(firstKey);
|
|
461
|
+
});
|
|
462
|
+
|
|
436
463
|
it("preserves authenticated structured mutation receipts from the target app", async () => {
|
|
437
464
|
const orgSecret = "org-receipt-secret";
|
|
438
465
|
mocks.getOrgA2ASecret.mockResolvedValueOnce(orgSecret);
|
|
@@ -742,6 +769,14 @@ describe("askGrantedDispatchMcpApp", () => {
|
|
|
742
769
|
message:
|
|
743
770
|
'ask_app is still working. Call ask_app_status with taskId "task-working" to retrieve the final response.',
|
|
744
771
|
});
|
|
772
|
+
expect(mocks.a2aSend).toHaveBeenCalledWith(
|
|
773
|
+
expect.anything(),
|
|
774
|
+
expect.objectContaining({
|
|
775
|
+
async: true,
|
|
776
|
+
deadlineMs: expect.any(Number),
|
|
777
|
+
idempotencyKey: expect.stringMatching(/^ask-app:/),
|
|
778
|
+
}),
|
|
779
|
+
);
|
|
745
780
|
});
|
|
746
781
|
|
|
747
782
|
it("counts submission and every poll against one inline deadline", async () => {
|
|
@@ -53,7 +53,8 @@ const TARGET_EMBED_SESSION_RETRY_BASE_MS = 250;
|
|
|
53
53
|
const TARGET_EMBED_SESSION_CONNECT_TIMEOUT_MS = 90_000;
|
|
54
54
|
const TARGET_EMBED_SESSION_BUDGET_MS = 95_000;
|
|
55
55
|
const DISPATCH_ASK_APP_DEFAULT_INLINE_WAIT_MS = 20_000;
|
|
56
|
-
|
|
56
|
+
// Leave response headroom for the hosted MCP transport after the inline wait.
|
|
57
|
+
const DISPATCH_ASK_APP_MAX_INLINE_WAIT_MS = 20_000;
|
|
57
58
|
const DISPATCH_ASK_APP_POLL_INTERVAL_MS = 1_500;
|
|
58
59
|
const DISPATCH_A2A_REQUEST_TIMEOUT_MS = 10_000;
|
|
59
60
|
const DISPATCH_ASK_APP_STATUS_RETRY_DELAYS_MS = [250, 750, 1_500] as const;
|
|
@@ -98,6 +99,31 @@ function boundedDispatchAskAppWaitMs(raw: unknown): number {
|
|
|
98
99
|
);
|
|
99
100
|
}
|
|
100
101
|
|
|
102
|
+
async function dispatchAskAppIdempotencyKey(
|
|
103
|
+
target: DispatchMcpAccessibleApp,
|
|
104
|
+
message: string,
|
|
105
|
+
): Promise<string> {
|
|
106
|
+
const requestId = getRequestContext()?.mcpRequestId;
|
|
107
|
+
if (!requestId) return `ask-app:${globalThis.crypto.randomUUID()}`;
|
|
108
|
+
|
|
109
|
+
const digest = await globalThis.crypto.subtle.digest(
|
|
110
|
+
"SHA-256",
|
|
111
|
+
new TextEncoder().encode(
|
|
112
|
+
JSON.stringify({
|
|
113
|
+
requestId,
|
|
114
|
+
app: target.id,
|
|
115
|
+
targetUrl: target.url,
|
|
116
|
+
message,
|
|
117
|
+
}),
|
|
118
|
+
),
|
|
119
|
+
);
|
|
120
|
+
let hex = "";
|
|
121
|
+
for (const byte of new Uint8Array(digest)) {
|
|
122
|
+
hex += byte.toString(16).padStart(2, "0");
|
|
123
|
+
}
|
|
124
|
+
return `ask-app:v1:${hex}`;
|
|
125
|
+
}
|
|
126
|
+
|
|
101
127
|
function isTerminalDispatchTask(task: Task): boolean {
|
|
102
128
|
return DISPATCH_ASK_APP_TERMINAL_STATES.has(String(task.status.state));
|
|
103
129
|
}
|
|
@@ -801,20 +827,31 @@ export async function askGrantedDispatchMcpApp(
|
|
|
801
827
|
? 0
|
|
802
828
|
: boundedDispatchAskAppWaitMs(options?.maxWaitMs);
|
|
803
829
|
const deadline = inlineWaitMs > 0 ? Date.now() + inlineWaitMs : undefined;
|
|
830
|
+
const submissionDeadline =
|
|
831
|
+
deadline ?? Date.now() + DISPATCH_A2A_REQUEST_TIMEOUT_MS;
|
|
804
832
|
|
|
805
833
|
const { client, metadata } = await createDispatchA2AClient({
|
|
806
834
|
targetUrl: target.url,
|
|
807
835
|
userEmail,
|
|
808
836
|
orgDomain: orgDomain ?? undefined,
|
|
809
837
|
orgSecret: orgSecret ?? undefined,
|
|
810
|
-
deadline,
|
|
838
|
+
deadline: submissionDeadline,
|
|
811
839
|
});
|
|
840
|
+
const idempotencyKey = await dispatchAskAppIdempotencyKey(
|
|
841
|
+
target,
|
|
842
|
+
trimmedMessage,
|
|
843
|
+
);
|
|
812
844
|
const task = await client.send(
|
|
813
845
|
{
|
|
814
846
|
role: "user",
|
|
815
847
|
parts: [{ type: "text", text: trimmedMessage }],
|
|
816
848
|
},
|
|
817
|
-
{
|
|
849
|
+
{
|
|
850
|
+
async: true,
|
|
851
|
+
metadata,
|
|
852
|
+
idempotencyKey,
|
|
853
|
+
deadlineMs: submissionDeadline,
|
|
854
|
+
},
|
|
818
855
|
);
|
|
819
856
|
const finalOrRunning = await waitForDispatchA2ATask(client, task, deadline);
|
|
820
857
|
return dispatchAskAppTaskResult(target.id, finalOrRunning, {
|