@agentwonderland/mcp 0.1.53 → 0.1.55

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.
@@ -0,0 +1,1221 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import { apiGet, apiPost, apiPostWithApprovedLinkSpendRequest, apiPostWithPayment } from "../core/api-client.js";
4
+ import { uploadLocalFiles } from "../core/file-upload.js";
5
+ import { hasWalletConfigured, getConfiguredMethods, getWalletAddress } from "../core/payments.js";
6
+ import { getLinkConfig, setPendingLinkSpendRequest, requiresSpendConfirmation } from "../core/config.js";
7
+ import { canSpend, recordSpend, requiresPolicyConfirmation } from "../core/spend-policy.js";
8
+ import { formatRunResult } from "../core/formatters.js";
9
+ import {
10
+ decodeMppNetworkId,
11
+ ensureApprovedLinkSpendRequest,
12
+ LinkApprovalRequiredError,
13
+ } from "../core/link-cli.js";
14
+ import {
15
+ formatPaymentLabel,
16
+ resolveConfirmationMethod,
17
+ } from "./_payment-confirmation.js";
18
+
19
+ const POLL_INTERVAL_MS = 3000;
20
+ const POLL_MAX_MS = 300000;
21
+
22
+ type PlaybookStepQuote = {
23
+ id: string;
24
+ source_step_id?: string;
25
+ index: number;
26
+ node_type: "aw_agent" | "external_x402";
27
+ agent_slug: string | null;
28
+ agent_id: string | null;
29
+ provider_id: string | null;
30
+ agent_name: string;
31
+ description: string;
32
+ quantity: number;
33
+ iteration?: number;
34
+ default_input?: Record<string, unknown>;
35
+ unit_price_usd: number;
36
+ quoted_cost_usd: number;
37
+ support_status: string;
38
+ };
39
+
40
+ type PlaybookFanoutControl = {
41
+ key: string;
42
+ default: number;
43
+ max: number;
44
+ description: string;
45
+ };
46
+
47
+ type PlaybookRecord = {
48
+ id: string;
49
+ slug: string;
50
+ version: number;
51
+ name: string;
52
+ persona: string;
53
+ tags: string[];
54
+ description: string;
55
+ outcome: string;
56
+ support_status: "supported" | "catalog_only";
57
+ support_note?: string;
58
+ default_limits?: Record<string, number>;
59
+ applied_limits?: Record<string, number>;
60
+ fanout_controls?: PlaybookFanoutControl[];
61
+ input_schema: Record<string, unknown>;
62
+ sample_input: Record<string, unknown>;
63
+ budget_notes: string[];
64
+ risks: string[];
65
+ stats?: {
66
+ favorite_count?: number;
67
+ rating_count?: number;
68
+ average_rating?: number | null;
69
+ };
70
+ current_quote: {
71
+ estimated_cost_usd: number;
72
+ step_count: number;
73
+ blocking_issues: string[];
74
+ default_limits?: Record<string, number>;
75
+ applied_limits?: Record<string, number>;
76
+ fanout_controls?: PlaybookFanoutControl[];
77
+ steps: PlaybookStepQuote[];
78
+ };
79
+ };
80
+
81
+ type PlaybookRunReceipt = {
82
+ run_id: string;
83
+ status: string;
84
+ playbook_id: string;
85
+ playbook_slug: string;
86
+ playbook_version: number;
87
+ input?: Record<string, unknown>;
88
+ limits?: Record<string, number>;
89
+ budget_usd: number;
90
+ quoted_cost_usd: number;
91
+ charged_usd: number;
92
+ refunded_usd: number;
93
+ remaining_budget_usd: number;
94
+ output?: unknown;
95
+ error_code?: string;
96
+ failure_message?: string;
97
+ steps: Array<{
98
+ playbook_step_id: string;
99
+ step_index: number;
100
+ node_type: string;
101
+ agent_slug: string | null;
102
+ agent_id: string | null;
103
+ provider_id: string | null;
104
+ job_id: string | null;
105
+ consumption_mode?: "direct_charge" | "credit_pack" | "not_charged";
106
+ status: string;
107
+ quoted_cost_usd: number;
108
+ charged_usd: number;
109
+ refunded_usd: number;
110
+ output?: unknown;
111
+ error_code?: string;
112
+ failure_message?: string;
113
+ }>;
114
+ };
115
+
116
+ type AgentSchemaDetails = {
117
+ id?: string;
118
+ schema?: {
119
+ input?: {
120
+ inputSchema?: unknown;
121
+ };
122
+ };
123
+ mcpSchema?: {
124
+ inputSchema?: unknown;
125
+ };
126
+ };
127
+
128
+ const pendingPlaybookRuns = new Map<string, {
129
+ playbook: PlaybookRecord;
130
+ input: Record<string, unknown>;
131
+ budget: number;
132
+ method?: string;
133
+ limits?: Record<string, number>;
134
+ }>();
135
+
136
+ function text(t: string) {
137
+ return { content: [{ type: "text" as const, text: t }] };
138
+ }
139
+
140
+ function multiText(...blocks: string[]) {
141
+ return { content: blocks.map((t) => ({ type: "text" as const, text: t })) };
142
+ }
143
+
144
+ function money(value: number): string {
145
+ return `$${value.toFixed(value < 1 ? 4 : 2)}`;
146
+ }
147
+
148
+ function cents(valueUsd: number): string {
149
+ return String(Math.max(1, Math.ceil(valueUsd * 100)));
150
+ }
151
+
152
+ function nonEmptyLimits(limits?: Record<string, number>): Record<string, number> | undefined {
153
+ return limits && Object.keys(limits).length > 0 ? limits : undefined;
154
+ }
155
+
156
+ function appliedPlaybookLimits(playbook: PlaybookRecord): Record<string, number> | undefined {
157
+ return nonEmptyLimits(playbook.current_quote.applied_limits ?? playbook.applied_limits ?? playbook.default_limits);
158
+ }
159
+
160
+ function fanoutControls(playbook: PlaybookRecord): PlaybookFanoutControl[] {
161
+ return playbook.current_quote.fanout_controls ?? playbook.fanout_controls ?? [];
162
+ }
163
+
164
+ function buildPlaybookLinkApprovalContext(params: {
165
+ playbook: PlaybookRecord;
166
+ budget: number;
167
+ stepCount: number;
168
+ runId: string;
169
+ }): string {
170
+ return [
171
+ `Approve up to ${money(params.budget)} for the Agent Wonderland "${params.playbook.name}" playbook.`,
172
+ `This playbook run ${params.runId} has ${params.stepCount} child-agent steps and will stop before exceeding the approved budget.`,
173
+ "Each child agent is still charged at its exact quoted price, unused budget is not spent, and failed child runs keep the existing Agent Wonderland refund behavior.",
174
+ ].join(" ");
175
+ }
176
+
177
+ async function collectWalletAddresses(paymentMethod?: string): Promise<string[]> {
178
+ const addresses = new Set<string>();
179
+ const primary = await getWalletAddress(paymentMethod);
180
+ if (primary) addresses.add(primary);
181
+ for (const chain of ["tempo", "base", "solana"]) {
182
+ const addr = await getWalletAddress(chain);
183
+ if (addr) addresses.add(addr);
184
+ }
185
+ return [...addresses];
186
+ }
187
+
188
+ async function pollJobUntilDone(
189
+ jobId: string,
190
+ paymentMethod?: string,
191
+ ): Promise<{ status: string; output?: unknown; error_code?: string }> {
192
+ const deadline = Date.now() + POLL_MAX_MS;
193
+ const walletAddresses = await collectWalletAddresses(paymentMethod);
194
+ const walletParams = walletAddresses.length > 0
195
+ ? walletAddresses.map((a) => `?wallet=${encodeURIComponent(a)}`)
196
+ : [""];
197
+
198
+ while (Date.now() < deadline) {
199
+ await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
200
+ for (const walletParam of walletParams) {
201
+ try {
202
+ const job = await apiGet<{
203
+ status: string;
204
+ output?: unknown;
205
+ error_code?: string;
206
+ }>(`/jobs/${jobId}${walletParam}`);
207
+
208
+ if (job.status === "completed") return { status: "completed", output: job.output };
209
+ if (job.status === "failed") return { status: "failed", output: job.output, error_code: job.error_code };
210
+ break;
211
+ } catch (err) {
212
+ const status = (err as { status?: number })?.status;
213
+ if (status !== 404) break;
214
+ }
215
+ }
216
+ }
217
+
218
+ return { status: "failed", error_code: "POLL_TIMEOUT" };
219
+ }
220
+
221
+ function formatPlaybookList(playbooks: PlaybookRecord[], query?: string) {
222
+ if (playbooks.length === 0) {
223
+ return query
224
+ ? `No playbooks found matching "${query}". Try search_playbooks({ persona: "gtm" }) or search_playbooks({ tag: "research" }).`
225
+ : "No playbooks found.";
226
+ }
227
+
228
+ const lines = playbooks.map((playbook) => {
229
+ const support = playbook.support_status === "supported" ? "supported" : `catalog-only: ${playbook.support_note ?? "not launch executable"}`;
230
+ const limits = appliedPlaybookLimits(playbook);
231
+ const fanout = limits ? ` · default limits ${JSON.stringify(limits)}` : "";
232
+ return [
233
+ ` ${playbook.name} (${playbook.slug})`,
234
+ ` ${playbook.outcome}`,
235
+ ` ${playbook.current_quote.step_count} step${playbook.current_quote.step_count === 1 ? "" : "s"} · ${money(playbook.current_quote.estimated_cost_usd)} est. · ${support}${fanout}`,
236
+ ` Favorites: ${playbook.stats?.favorite_count ?? 0} · Rating: ${playbook.stats?.average_rating ? `${playbook.stats.average_rating}/5` : "unrated"} (${playbook.stats?.rating_count ?? 0})`,
237
+ ` Inspect: get_playbook({ slug: "${playbook.slug}" })`,
238
+ ].join("\n");
239
+ });
240
+
241
+ return [
242
+ query ? `Found ${playbooks.length} playbook${playbooks.length === 1 ? "" : "s"} matching "${query}":` : `Found ${playbooks.length} playbook${playbooks.length === 1 ? "" : "s"}:`,
243
+ "",
244
+ ...lines,
245
+ ].join("\n");
246
+ }
247
+
248
+ function formatPlaybook(playbook: PlaybookRecord) {
249
+ const q = playbook.current_quote;
250
+ const lines = [
251
+ `${playbook.name} (${playbook.slug})`,
252
+ `${playbook.outcome}`,
253
+ "",
254
+ playbook.description,
255
+ "",
256
+ `Support: ${playbook.support_status}${playbook.support_note ? ` — ${playbook.support_note}` : ""}`,
257
+ `Favorites: ${playbook.stats?.favorite_count ?? 0}`,
258
+ `Rating: ${playbook.stats?.average_rating ? `${playbook.stats.average_rating}/5` : "unrated"} (${playbook.stats?.rating_count ?? 0})`,
259
+ `Estimated cost: ${money(q.estimated_cost_usd)} across ${q.step_count} paid step${q.step_count === 1 ? "" : "s"}`,
260
+ ...(q.blocking_issues.length ? [`Blocking issues: ${q.blocking_issues.join("; ")}`] : []),
261
+ "",
262
+ "Inputs:",
263
+ ];
264
+
265
+ const schema = playbook.input_schema as { properties?: Record<string, { type?: string; description?: string }>; required?: string[] };
266
+ const required = new Set(schema.required ?? []);
267
+ for (const [name, def] of Object.entries(schema.properties ?? {})) {
268
+ lines.push(` ${name}: ${def.type ?? "string"}${required.has(name) ? " (required)" : ""}${def.description ? ` — ${def.description}` : ""}`);
269
+ }
270
+
271
+ lines.push("", "Execution plan:");
272
+ for (const step of q.steps) {
273
+ const quantity = step.quantity > 1 ? ` × ${step.quantity}` : "";
274
+ const status = step.support_status === "ready" ? "" : " [blocking]";
275
+ lines.push(` ${step.index + 1}. ${step.agent_name} (${step.agent_slug ?? step.node_type})${quantity} — ${money(step.quoted_cost_usd)}${status}`);
276
+ }
277
+
278
+ if (playbook.budget_notes.length) lines.push("", "Budget notes:", ...playbook.budget_notes.map((note) => ` - ${note}`));
279
+ const limits = appliedPlaybookLimits(playbook);
280
+ const controls = fanoutControls(playbook);
281
+ if (controls.length) {
282
+ lines.push("", "Fanout controls:");
283
+ for (const control of controls) {
284
+ const current = limits?.[control.key] ?? control.default;
285
+ lines.push(` - ${control.key}: default ${control.default}, current ${current}, max ${control.max} — ${control.description}`);
286
+ }
287
+ if (limits) lines.push(` Override by passing limits, for example limits: ${JSON.stringify(limits)}`);
288
+ }
289
+ if (playbook.risks.length) lines.push("", "Risks:", ...playbook.risks.map((risk) => ` - ${risk}`));
290
+
291
+ const runLimits = formatLimitsField(limits);
292
+ lines.push(
293
+ "",
294
+ "Sample input:",
295
+ JSON.stringify(playbook.sample_input, null, 2),
296
+ "",
297
+ `Run quote: run_playbook({ slug: "${playbook.slug}", input: <input>, budget: ${Math.max(1, Math.ceil(q.estimated_cost_usd + 1))}${runLimits ? `, ${runLimits}` : ""} })`,
298
+ );
299
+ return lines.join("\n");
300
+ }
301
+
302
+ function formatLimitsField(limits?: Record<string, number>): string | undefined {
303
+ return limits && Object.keys(limits).length > 0
304
+ ? `limits: ${JSON.stringify(limits)}`
305
+ : undefined;
306
+ }
307
+
308
+ function formatRunConfirmationCommand(slug: string, method: string | undefined, budget: number, limits?: Record<string, number>) {
309
+ const args = [
310
+ `slug: "${slug}"`,
311
+ "input: <same>",
312
+ `budget: ${budget}`,
313
+ formatLimitsField(limits),
314
+ method ? `pay_with: "${method}"` : undefined,
315
+ "confirmed: true",
316
+ ].filter(Boolean);
317
+ return `run_playbook({ ${args.join(", ")} })`;
318
+ }
319
+
320
+ function formatQuoteNotes(playbook: PlaybookRecord): string[] {
321
+ const lines: string[] = [];
322
+ const controls = fanoutControls(playbook);
323
+ if (controls.length > 0) {
324
+ const limits = appliedPlaybookLimits(playbook) ?? {};
325
+ lines.push("", "Fanout controls:");
326
+ for (const control of controls) {
327
+ const current = limits[control.key] ?? control.default;
328
+ lines.push(` - ${control.key}: current ${current}, default ${control.default}, max ${control.max}`);
329
+ }
330
+ if (Object.keys(limits).length > 0) {
331
+ lines.push(` Override by editing limits in the confirmation call: ${JSON.stringify(limits)}`);
332
+ }
333
+ }
334
+ if (playbook.budget_notes.length > 0) {
335
+ lines.push("", "Budget and fanout notes:", ...playbook.budget_notes.map((note) => ` - ${note}`));
336
+ }
337
+ if (playbook.risks.length > 0) {
338
+ lines.push("", "Risks and limitations:", ...playbook.risks.map((risk) => ` - ${risk}`));
339
+ }
340
+ return lines;
341
+ }
342
+
343
+ function formatResumeCommand(receipt: PlaybookRunReceipt, budget = receipt.budget_usd): string {
344
+ const args = [
345
+ `resume_run_id: "${receipt.run_id}"`,
346
+ "confirmed: true",
347
+ `budget: ${budget}`,
348
+ formatLimitsField(receipt.limits),
349
+ ].filter(Boolean);
350
+ return `run_playbook({ ${args.join(", ")} })`;
351
+ }
352
+
353
+ async function formatCheaperAlternatives(currentSlug: string, budget: number): Promise<string> {
354
+ try {
355
+ const params = new URLSearchParams();
356
+ params.set("max_budget", String(budget));
357
+ params.set("limit", "4");
358
+ params.set("sort", "price");
359
+ const result = await apiGet<{ playbooks?: PlaybookRecord[] }>(`/playbooks?${params}`);
360
+ const alternatives = (result.playbooks ?? [])
361
+ .filter((candidate) => candidate.slug !== currentSlug)
362
+ .filter((candidate) => candidate.support_status === "supported")
363
+ .filter((candidate) => candidate.current_quote.blocking_issues.length === 0)
364
+ .filter((candidate) => candidate.current_quote.estimated_cost_usd <= budget)
365
+ .slice(0, 3);
366
+
367
+ if (alternatives.length === 0) return "";
368
+ return [
369
+ "Cheaper supported alternatives under this budget:",
370
+ ...alternatives.map((candidate) => ` - ${candidate.name} (${candidate.slug}) — ${money(candidate.current_quote.estimated_cost_usd)} est.; inspect with get_playbook({ slug: "${candidate.slug}" })`),
371
+ ].join("\n");
372
+ } catch {
373
+ return "";
374
+ }
375
+ }
376
+
377
+ function stepInput(baseInput: Record<string, unknown>, step: PlaybookStepQuote, iteration: number): Record<string, unknown> {
378
+ const input: Record<string, unknown> = { ...(step.default_input ?? {}), ...baseInput };
379
+ const itemIndex = Math.max(0, (step.iteration ?? 1) - 1);
380
+
381
+ if (Array.isArray(input.leads) && input.leads[itemIndex] && typeof input.leads[itemIndex] === "object") {
382
+ const lead = input.leads[itemIndex] as Record<string, unknown>;
383
+ input.lead = lead;
384
+ input.name = input.name ?? lead.name;
385
+ input.company = input.company ?? lead.company;
386
+ input.email = input.email ?? lead.email;
387
+ }
388
+
389
+ if (step.agent_slug === "serpanalyzer" && !("queries" in input)) {
390
+ input.queries = input.domain ? [`${String(input.domain)} alternatives`, `${String(input.domain)} pricing`] : ["category keywords"];
391
+ }
392
+ if (step.agent_slug === "ad-strategy-intel" && input.domain && !("startUrls" in input)) {
393
+ input.startUrls = [{
394
+ url: normalizeUrl(input.domain) ?? String(input.domain),
395
+ }];
396
+ input.maxAds = input.maxAds ?? 1;
397
+ delete input.platforms;
398
+ }
399
+ if (step.agent_slug === "web-search" && !("q" in input)) {
400
+ const company = (input.counterparty ?? input.company ?? input.domain ?? input.category ?? input.icp ?? "company") as string;
401
+ input.q = `${company} recent news`;
402
+ }
403
+ if (step.agent_slug === "company-enrichment-deep" && !("name" in input) && !("domain" in input)) {
404
+ input.name = input.counterparty ?? input.company ?? input.domain;
405
+ }
406
+ if (step.agent_slug === "company-enrichment-deep" && !("company_name" in input) && !("domain" in input)) {
407
+ input.company_name = input.company ?? input.name ?? input.counterparty;
408
+ }
409
+ if (step.agent_slug === "cold-email-council" && !("brief" in input)) {
410
+ const recipient = [
411
+ input.name ? String(input.name) : undefined,
412
+ input.company ? `at ${String(input.company)}` : undefined,
413
+ input.email ? `<${String(input.email)}>` : undefined,
414
+ ].filter(Boolean).join(" ");
415
+ input.brief = `Write a concise first-touch cold email${recipient ? ` to ${recipient}` : ""}.`;
416
+ input.context = {
417
+ lead: input.lead,
418
+ company: input.company,
419
+ email: input.email,
420
+ research: input.previous_outputs,
421
+ };
422
+ input.num_outputs = input.num_outputs ?? 1;
423
+ }
424
+ if (step.agent_slug === "email-verification" && !("emails" in input) && input.email) {
425
+ input.emails = [String(input.email)];
426
+ }
427
+ if (step.agent_slug === "scan-contract-for-risks" && !("file" in input) && input.contract) {
428
+ input.file = input.contract;
429
+ }
430
+ if (step.agent_slug === "marketing-copy-council" && !("brief" in input)) {
431
+ input.brief = input.product ?? input.context ?? input.text;
432
+ }
433
+ if (step.agent_slug === "write-landing-page-copy") {
434
+ const previousOutputs = Array.isArray(input.previous_outputs) ? input.previous_outputs : [];
435
+ const councilOutput = previousOutputs.find((item) => {
436
+ const record = item && typeof item === "object" ? item as Record<string, unknown> : null;
437
+ return record?.step === "council" || record?.step === "marketing-copy-council";
438
+ }) ?? previousOutputs[previousOutputs.length - 1];
439
+ const context = {
440
+ brief: input.brief,
441
+ product_slug: input.product_slug,
442
+ council_output: councilOutput,
443
+ previous_outputs: previousOutputs,
444
+ };
445
+ input.product = input.product ?? input.product_name ?? input.product_slug ?? "Landing page";
446
+ input.context = input.context ?? JSON.stringify(context);
447
+ }
448
+ if (step.agent_slug === "publish-html-to-a-public-url") {
449
+ const previousOutputs = Array.isArray(input.previous_outputs) ? input.previous_outputs : [];
450
+ const writerOutput = previousOutputs[previousOutputs.length - 1];
451
+ const outputRecord = writerOutput && typeof writerOutput === "object"
452
+ ? writerOutput as Record<string, unknown>
453
+ : null;
454
+ const output = outputRecord?.output && typeof outputRecord.output === "object"
455
+ ? outputRecord.output as Record<string, unknown>
456
+ : outputRecord?.output;
457
+ input.html = input.html
458
+ ?? (output && typeof output === "object" ? (output as Record<string, unknown>).html : undefined)
459
+ ?? (typeof output === "string" ? output : undefined)
460
+ ?? JSON.stringify(output ?? writerOutput ?? {});
461
+ input.slug = input.slug ?? input.product_slug;
462
+ }
463
+ if (step.agent_slug === "place-search") {
464
+ input.location = input.location ?? input.zip;
465
+ }
466
+ if (step.quantity > 1) {
467
+ input.limit = Math.min(Number(input.limit ?? step.quantity), step.quantity);
468
+ }
469
+ input.item_index = itemIndex;
470
+
471
+ return input;
472
+ }
473
+
474
+ function valueAtPath(input: Record<string, unknown>, path: string): unknown {
475
+ return path.split(".").reduce<unknown>((current, key) => {
476
+ if (!current || typeof current !== "object" || Array.isArray(current)) return undefined;
477
+ return (current as Record<string, unknown>)[key];
478
+ }, input);
479
+ }
480
+
481
+ function present(value: unknown): boolean {
482
+ return value !== undefined && value !== null && !(typeof value === "string" && value.trim() === "");
483
+ }
484
+
485
+ function liveInputSchema(agent: AgentSchemaDetails): { properties?: Record<string, unknown>; required?: string[] } | null {
486
+ const raw = agent.schema?.input?.inputSchema ?? agent.mcpSchema?.inputSchema;
487
+ return raw && typeof raw === "object" && !Array.isArray(raw)
488
+ ? raw as { properties?: Record<string, unknown>; required?: string[] }
489
+ : null;
490
+ }
491
+
492
+ function aliasCandidates(field: string): string[] {
493
+ const aliases: Record<string, string[]> = {
494
+ q: ["query", "search", "domain", "company", "counterparty", "category", "icp", "vertical", "ticker", "compound"],
495
+ query: ["q", "search", "domain", "company", "counterparty", "category", "icp", "vertical", "ticker", "compound"],
496
+ search: ["q", "query", "domain", "company", "counterparty", "category", "icp", "vertical"],
497
+ name: ["company", "counterparty", "domain", "lead.name"],
498
+ company: ["name", "counterparty", "domain", "lead.company"],
499
+ company_name: ["company", "name", "counterparty", "lead.company"],
500
+ domain: ["company", "website", "site"],
501
+ website: ["domain", "company"],
502
+ emails: ["email", "lead.email"],
503
+ url: ["domain", "website"],
504
+ urls: ["url", "website", "domain"],
505
+ startUrls: ["startUrl", "url", "website", "domain"],
506
+ tickers: ["ticker", "symbol"],
507
+ ticker: ["tickers", "symbol"],
508
+ file: ["contract", "bill", "document", "path"],
509
+ text: ["brief", "content", "context"],
510
+ brief: ["text", "content", "context"],
511
+ context: ["previous_outputs", "brief", "text", "content"],
512
+ location: ["zip", "city", "address"],
513
+ email: ["lead.email"],
514
+ };
515
+ return aliases[field] ?? [];
516
+ }
517
+
518
+ function normalizeUrl(value: unknown): string | null {
519
+ if (typeof value !== "string" || !value.trim()) return null;
520
+ const trimmed = value.trim();
521
+ return /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
522
+ }
523
+
524
+ function repairValueForField(field: string, value: unknown, property: unknown): unknown {
525
+ const propertyDef = property as { type?: unknown; items?: { type?: unknown } } | undefined;
526
+ const type = propertyDef?.type;
527
+ if (field === "startUrls" || field === "urls") {
528
+ const toStartUrl = (candidate: unknown) => {
529
+ const url = normalizeUrl(candidate);
530
+ return field === "startUrls" && propertyDef?.items?.type === "object" && url ? { url } : url;
531
+ };
532
+ if (Array.isArray(value)) {
533
+ return value.map(toStartUrl).filter(Boolean);
534
+ }
535
+ const url = toStartUrl(value);
536
+ return url ? [url] : value;
537
+ }
538
+ if (type === "array" && !Array.isArray(value)) return [value];
539
+ return value;
540
+ }
541
+
542
+ function coerceKnownFieldValue(field: string, value: unknown, property: unknown): { value: unknown; changed: boolean } {
543
+ const propertyDef = property as { type?: unknown } | undefined;
544
+ if (field === "startUrls" || field === "urls") {
545
+ const repaired = repairValueForField(field, value, property);
546
+ return { value: repaired, changed: JSON.stringify(repaired) !== JSON.stringify(value) };
547
+ }
548
+ if (propertyDef?.type === "string" && Array.isArray(value)) {
549
+ const joined = value
550
+ .filter((item) => item != null && (typeof item === "string" || typeof item === "number" || typeof item === "boolean"))
551
+ .map(String)
552
+ .join("; ");
553
+ return joined ? { value: joined, changed: true } : { value, changed: false };
554
+ }
555
+ if ((propertyDef?.type === "number" || propertyDef?.type === "integer") && typeof value === "string" && value.trim()) {
556
+ const parsed = Number(value);
557
+ if (Number.isFinite(parsed)) return { value: parsed, changed: true };
558
+ }
559
+ if (propertyDef?.type === "array" && !Array.isArray(value)) {
560
+ return { value: [value], changed: true };
561
+ }
562
+ return { value, changed: false };
563
+ }
564
+
565
+ function repairInputFromSchema(input: Record<string, unknown>, schema: { properties?: Record<string, unknown>; required?: string[] }) {
566
+ const repaired = { ...input };
567
+ const required = schema.required ?? [];
568
+ const applied: string[] = [];
569
+ const missing: string[] = [];
570
+ const properties = schema.properties ?? {};
571
+
572
+ for (const field of required) {
573
+ if (present(repaired[field])) continue;
574
+
575
+ const property = properties[field] as { default?: unknown } | undefined;
576
+ if (property && Object.prototype.hasOwnProperty.call(property, "default")) {
577
+ repaired[field] = property.default;
578
+ applied.push(`${field}=default`);
579
+ continue;
580
+ }
581
+
582
+ const candidate = aliasCandidates(field).find((alias) => present(valueAtPath(input, alias)));
583
+ if (candidate) {
584
+ repaired[field] = repairValueForField(field, valueAtPath(input, candidate), property);
585
+ applied.push(`${field}←${candidate}`);
586
+ continue;
587
+ }
588
+
589
+ missing.push(field);
590
+ }
591
+
592
+ for (const [field, property] of Object.entries(properties) as Array<[string, { default?: unknown }]>) {
593
+ if (present(repaired[field]) || !Object.prototype.hasOwnProperty.call(property, "default")) continue;
594
+ repaired[field] = property.default;
595
+ applied.push(`${field}=default`);
596
+ }
597
+
598
+ for (const [field, property] of Object.entries(properties)) {
599
+ if (!present(repaired[field])) continue;
600
+ const coerced = coerceKnownFieldValue(field, repaired[field], property);
601
+ if (coerced.changed) {
602
+ repaired[field] = coerced.value;
603
+ applied.push(`${field}=coerced`);
604
+ }
605
+ }
606
+
607
+ const propertyNames = Object.keys(properties);
608
+ const filtered = propertyNames.length > 0
609
+ ? Object.fromEntries(Object.entries(repaired).filter(([key]) => Object.prototype.hasOwnProperty.call(properties, key)))
610
+ : repaired;
611
+
612
+ return { input: filtered, applied, missing };
613
+ }
614
+
615
+ async function recordStep(runId: string, stepId: string, body: Record<string, unknown>) {
616
+ let lastError: unknown;
617
+ const encodedRunId = encodeURIComponent(runId);
618
+ const encodedStepId = encodeURIComponent(stepId);
619
+ for (let attempt = 1; attempt <= 3; attempt += 1) {
620
+ try {
621
+ await apiPost(`/playbook-runs/${encodedRunId}/steps/${encodedStepId}`, body);
622
+ return;
623
+ } catch (err) {
624
+ lastError = err;
625
+ if (attempt < 3) {
626
+ await new Promise((resolve) => setTimeout(resolve, 250 * attempt));
627
+ }
628
+ }
629
+ }
630
+ throw lastError instanceof Error ? lastError : new Error("Failed to record playbook step receipt");
631
+ }
632
+
633
+ async function updateRun(runId: string, body: Record<string, unknown>) {
634
+ await apiPost(`/playbook-runs/${runId}/status`, body);
635
+ }
636
+
637
+ function formatReceipt(receipt: PlaybookRunReceipt) {
638
+ const lines = [
639
+ `Playbook run ${receipt.run_id}`,
640
+ `${receipt.playbook_slug} v${receipt.playbook_version} — ${receipt.status}`,
641
+ "",
642
+ `Budget: ${money(receipt.budget_usd)}`,
643
+ `Charged: ${money(receipt.charged_usd)}`,
644
+ `Refunded: ${money(receipt.refunded_usd)}`,
645
+ `Remaining unspent budget: ${money(receipt.remaining_budget_usd)}`,
646
+ "",
647
+ "Steps:",
648
+ ];
649
+
650
+ for (const step of [...receipt.steps].sort((a, b) => a.step_index - b.step_index)) {
651
+ const job = step.job_id ? ` · job ${step.job_id}` : "";
652
+ const label = step.agent_slug ?? step.playbook_step_id;
653
+ const mode = step.consumption_mode === "credit_pack"
654
+ ? " · credit pack"
655
+ : step.consumption_mode === "not_charged"
656
+ ? " · not charged"
657
+ : "";
658
+ lines.push(` ${step.step_index + 1}. ${label}: ${step.status}${mode} · charged ${money(step.charged_usd)} · refunded ${money(step.refunded_usd)}${job}`);
659
+ if (step.error_code) lines.push(` error: ${step.error_code}`);
660
+ }
661
+
662
+ if (receipt.error_code) lines.push("", `Error: ${receipt.error_code}${receipt.failure_message ? ` — ${receipt.failure_message}` : ""}`);
663
+ if (receipt.status === "paused") {
664
+ lines.push("", `Resume: ${formatResumeCommand(receipt)}`);
665
+ }
666
+ if (receipt.output) {
667
+ lines.push("", "Output:", JSON.stringify(receipt.output, null, 2));
668
+ }
669
+
670
+ return lines.join("\n");
671
+ }
672
+
673
+ export function registerPlaybookTools(server: McpServer): void {
674
+ server.tool(
675
+ "search_playbooks",
676
+ "Search Agent Wonderland Playbooks by outcome, persona, tag, budget, or popularity. Playbooks are budget-capped multi-agent workflows backed by Agent Wonderland agents.",
677
+ {
678
+ query: z.string().optional(),
679
+ persona: z.string().optional(),
680
+ tag: z.string().optional(),
681
+ limit: z.number().optional().default(10),
682
+ max_budget: z.number().optional(),
683
+ sort: z.enum(["relevance", "price", "rating", "popularity", "newest"]).optional(),
684
+ },
685
+ async ({ query, persona, tag, limit, max_budget, sort }) => {
686
+ const params = new URLSearchParams();
687
+ if (query) params.set("q", query);
688
+ if (persona) params.set("persona", persona);
689
+ if (tag) params.set("tag", tag);
690
+ if (limit) params.set("limit", String(limit));
691
+ if (max_budget != null) params.set("max_budget", String(max_budget));
692
+ if (sort) params.set("sort", sort);
693
+ const result = await apiGet<{ playbooks: PlaybookRecord[] }>(`/playbooks?${params}`);
694
+ return text(formatPlaybookList(result.playbooks, query));
695
+ },
696
+ );
697
+
698
+ server.tool(
699
+ "get_playbook",
700
+ "Inspect an Agent Wonderland Playbook before running it. Shows input schema, live child-agent quote, support status, budget notes, and execution plan.",
701
+ {
702
+ playbook_id: z.string().optional(),
703
+ slug: z.string().optional(),
704
+ },
705
+ async ({ playbook_id, slug }) => {
706
+ const id = slug ?? playbook_id;
707
+ if (!id) return text("Provide slug or playbook_id.");
708
+ const playbook = await apiGet<PlaybookRecord>(`/playbooks/${encodeURIComponent(id)}`);
709
+ return text(formatPlaybook(playbook));
710
+ },
711
+ );
712
+
713
+ server.tool(
714
+ "run_playbook",
715
+ "Quote, confirm, execute, or resume an Agent Wonderland Playbook. One user approval sets a max budget; each child agent is still paid through the existing exact-charge run_agent payment flow. Unused budget is never charged.",
716
+ {
717
+ playbook_id: z.string().optional(),
718
+ slug: z.string().optional(),
719
+ input: z.record(z.string(), z.unknown()).optional().default({}),
720
+ budget: z.number().positive().optional(),
721
+ pay_with: z.string().trim().min(1).optional(),
722
+ confirmed: z.boolean().optional(),
723
+ resume_run_id: z.string().optional(),
724
+ limits: z.record(z.string(), z.number().int().positive()).optional(),
725
+ },
726
+ async ({ playbook_id, slug, input, budget, pay_with, confirmed, resume_run_id, limits }) => {
727
+ let resumeReceipt: PlaybookRunReceipt | null = null;
728
+ if (resume_run_id) {
729
+ resumeReceipt = await apiGet<PlaybookRunReceipt>(`/playbook-runs/${resume_run_id}`);
730
+ if (!confirmed) {
731
+ const displayReceipt = { ...resumeReceipt, limits: limits ?? resumeReceipt.limits };
732
+ return text(`${formatReceipt(displayReceipt)}\n\nTo resume, call ${formatResumeCommand(displayReceipt, budget ?? resumeReceipt.budget_usd)}.`);
733
+ }
734
+ if (resumeReceipt.status === "completed") return text(formatReceipt(resumeReceipt));
735
+ slug = resumeReceipt.playbook_slug;
736
+ input = Object.keys(input ?? {}).length ? input : (resumeReceipt.input ?? {});
737
+ budget = budget ?? resumeReceipt.budget_usd;
738
+ limits = limits ?? resumeReceipt.limits;
739
+ }
740
+
741
+ const id = slug ?? playbook_id;
742
+ if (!id) return text("Provide slug or playbook_id.");
743
+ if (!budget) return text("Provide a budget. Example: run_playbook({ slug: \"competitor-ads\", input: { domain: \"notion.so\" }, budget: 5 })");
744
+
745
+ let playbook = await apiGet<PlaybookRecord>(`/playbooks/${encodeURIComponent(id)}`);
746
+ const pending = pendingPlaybookRuns.get(playbook.slug);
747
+ const method = resolveConfirmationMethod(pay_with, pending?.method, getConfiguredMethods());
748
+ const spendMethod = method ?? getConfiguredMethods()[0] ?? "auto";
749
+ const effectiveInput = Object.keys(input ?? {}).length ? input : pending?.input ?? {};
750
+ const effectiveBudget = budget ?? pending?.budget ?? 0;
751
+ const effectiveLimits = limits ?? pending?.limits;
752
+
753
+ if (!confirmed || effectiveLimits) {
754
+ try {
755
+ const quoted = await apiPost<{
756
+ playbook: PlaybookRecord;
757
+ budget_usd: number;
758
+ budget_sufficient: boolean;
759
+ remaining_budget_usd: number;
760
+ }>("/playbook-quotes", {
761
+ playbook_id: playbook.id,
762
+ slug: playbook.slug,
763
+ input: effectiveInput,
764
+ budget: effectiveBudget,
765
+ limits: effectiveLimits,
766
+ });
767
+ if (quoted?.playbook) {
768
+ playbook = quoted.playbook;
769
+ }
770
+ } catch {
771
+ // Quote analytics should not block the user-facing quote.
772
+ }
773
+ }
774
+
775
+ if (playbook.support_status !== "supported") {
776
+ return text(`Playbook ${playbook.slug} is catalog-only for MVP execution.${playbook.support_note ? `\n\n${playbook.support_note}` : ""}`);
777
+ }
778
+ if (playbook.current_quote.blocking_issues.length > 0) {
779
+ return text(`Cannot run ${playbook.slug}; child-agent issues must be fixed first:\n${playbook.current_quote.blocking_issues.map((issue) => `- ${issue}`).join("\n")}`);
780
+ }
781
+
782
+ const runLimits = appliedPlaybookLimits(playbook) ?? effectiveLimits;
783
+ const estimatedCost = playbook.current_quote.estimated_cost_usd;
784
+ const spendCheck = canSpend({ method: spendMethod, amountUsd: Math.min(effectiveBudget, estimatedCost) });
785
+ if (!spendCheck.ok) return text(spendCheck.message);
786
+
787
+ if (estimatedCost > effectiveBudget) {
788
+ pendingPlaybookRuns.set(playbook.slug, { playbook, input: effectiveInput, budget: effectiveBudget, method, limits: runLimits });
789
+ const cheaperAlternatives = await formatCheaperAlternatives(playbook.slug, effectiveBudget);
790
+ return text([
791
+ `${playbook.name} quote exceeds the budget.`,
792
+ "",
793
+ `Estimated cost: ${money(estimatedCost)}`,
794
+ `Budget: ${money(effectiveBudget)}`,
795
+ "",
796
+ "No paid execution was attempted.",
797
+ ...(cheaperAlternatives ? ["", cheaperAlternatives] : []),
798
+ "",
799
+ `Try a higher budget or reduced limits, then call:`,
800
+ formatRunConfirmationCommand(playbook.slug, method, Math.ceil(estimatedCost + 1), runLimits),
801
+ ].join("\n"));
802
+ }
803
+
804
+ if ((requiresSpendConfirmation() || requiresPolicyConfirmation(spendMethod, estimatedCost)) && !confirmed) {
805
+ pendingPlaybookRuns.set(playbook.slug, { playbook, input: effectiveInput, budget: effectiveBudget, method, limits: runLimits });
806
+ return text([
807
+ `Ready to run ${playbook.name}`,
808
+ "",
809
+ ` Max budget: ${money(effectiveBudget)}`,
810
+ ` Estimated child-agent charges: ${money(estimatedCost)}`,
811
+ ` Payment: ${formatPaymentLabel(method)}`,
812
+ ` Steps: ${playbook.current_quote.step_count}`,
813
+ "",
814
+ "Unused budget is not charged. The runner stops before exceeding the cap.",
815
+ "",
816
+ "Execution plan:",
817
+ ...playbook.current_quote.steps.map((step) => ` ${step.index + 1}. ${step.agent_name} (${step.agent_slug ?? step.node_type}) — ${money(step.quoted_cost_usd)}`),
818
+ ...formatQuoteNotes(playbook),
819
+ "",
820
+ "To proceed, call:",
821
+ formatRunConfirmationCommand(playbook.slug, method, effectiveBudget, runLimits),
822
+ "",
823
+ "To cancel, do nothing.",
824
+ ].join("\n"));
825
+ }
826
+
827
+ if (!hasWalletConfigured()) {
828
+ return text([
829
+ "No payment method configured.",
830
+ "",
831
+ "Run wallet_setup({ action: \"start\" }) to configure Tempo, Base, or Solana before executing a paid playbook.",
832
+ ].join("\n"));
833
+ }
834
+
835
+ let processedInput: Record<string, unknown>;
836
+ let uploadSummary = "";
837
+ try {
838
+ const uploadResult = await uploadLocalFiles(effectiveInput);
839
+ processedInput = uploadResult.input;
840
+ if (uploadResult.uploads.length > 0) {
841
+ uploadSummary = uploadResult.uploads
842
+ .map((upload) => `Uploaded ${upload.field}: ${upload.originalPath} -> ${upload.url}`)
843
+ .join("\n");
844
+ }
845
+ } catch (err) {
846
+ const msg = err instanceof Error ? err.message : "File upload failed";
847
+ return text(`Error: ${msg}`);
848
+ }
849
+
850
+ const runState = resumeReceipt
851
+ ? {
852
+ run_id: resumeReceipt.run_id,
853
+ steps: playbook.current_quote.steps,
854
+ completedStepIds: new Set(resumeReceipt.steps
855
+ .filter((step) => step.status === "succeeded")
856
+ .map((step) => step.playbook_step_id)),
857
+ existingOutputs: resumeReceipt.steps
858
+ .filter((step) => step.status === "succeeded")
859
+ .map((step) => ({ step: step.playbook_step_id, job_id: step.job_id ?? undefined, output: step.output }))
860
+ .filter((step) => step.output !== undefined),
861
+ chargedUsd: Math.max(0, resumeReceipt.charged_usd - resumeReceipt.refunded_usd),
862
+ }
863
+ : {
864
+ ...await apiPost<{
865
+ run_id: string;
866
+ steps: PlaybookStepQuote[];
867
+ }>("/playbook-runs", {
868
+ playbook_id: playbook.id,
869
+ slug: playbook.slug,
870
+ input: processedInput,
871
+ budget: effectiveBudget,
872
+ limits: runLimits,
873
+ }),
874
+ completedStepIds: new Set<string>(),
875
+ existingOutputs: [] as Array<{ step: string; job_id?: string; output?: unknown }>,
876
+ chargedUsd: 0,
877
+ };
878
+
879
+ await apiPost(`/playbook-runs/${runState.run_id}/status`, { status: "running" });
880
+
881
+ let charged = runState.chargedUsd;
882
+ const childOutputs: Array<{ step: string; job_id?: string; output?: unknown }> = [...runState.existingOutputs];
883
+ let linkSpendRequestId: string | undefined;
884
+ let linkNetworkId: string | undefined;
885
+ const linkApprovalContext = buildPlaybookLinkApprovalContext({
886
+ playbook,
887
+ budget: effectiveBudget,
888
+ stepCount: playbook.current_quote.step_count,
889
+ runId: runState.run_id,
890
+ });
891
+
892
+ for (const [iteration, step] of runState.steps.entries()) {
893
+ if (runState.completedStepIds.has(step.id)) {
894
+ continue;
895
+ }
896
+ if (!step.agent_id || step.node_type !== "aw_agent") {
897
+ await recordStep(runState.run_id, step.id, {
898
+ status: "skipped",
899
+ consumption_mode: "not_charged",
900
+ error_code: step.node_type === "aw_agent" ? "MISSING_CHILD_AGENT" : "UNSUPPORTED_NODE_TYPE",
901
+ failure_message: step.node_type === "aw_agent"
902
+ ? `Missing child agent ${step.agent_slug}`
903
+ : `Unsupported playbook node type ${step.node_type}`,
904
+ });
905
+ await updateRun(runState.run_id, { status: "paused", error_code: step.node_type === "aw_agent" ? "MISSING_CHILD_AGENT" : "UNSUPPORTED_NODE_TYPE" });
906
+ const receipt = await apiGet<PlaybookRunReceipt>(`/playbook-runs/${runState.run_id}`);
907
+ return text(formatReceipt(receipt));
908
+ }
909
+
910
+ const nextCost = step.quoted_cost_usd;
911
+ if (charged + nextCost > effectiveBudget) {
912
+ await recordStep(runState.run_id, step.id, {
913
+ status: "skipped",
914
+ consumption_mode: "not_charged",
915
+ error_code: "BUDGET_EXHAUSTED",
916
+ failure_message: "Skipped before payment because the next child step would exceed the budget cap.",
917
+ });
918
+ await updateRun(runState.run_id, { status: "paused", error_code: "BUDGET_EXHAUSTED" });
919
+ const receipt = await apiGet<PlaybookRunReceipt>(`/playbook-runs/${runState.run_id}`);
920
+ return text(formatReceipt(receipt));
921
+ }
922
+
923
+ await recordStep(runState.run_id, step.id, {
924
+ status: "running",
925
+ agent_id: step.agent_id,
926
+ provider_id: step.provider_id,
927
+ quoted_cost_usd: step.quoted_cost_usd,
928
+ });
929
+
930
+ const payload = stepInput({ ...processedInput, previous_outputs: childOutputs }, step, iteration);
931
+ const playbookContext = {
932
+ playbook_run_id: runState.run_id,
933
+ playbook_id: playbook.id,
934
+ playbook_slug: playbook.slug,
935
+ playbook_step_id: step.id,
936
+ playbook_step_index: step.index,
937
+ };
938
+ let result: Record<string, unknown> | undefined;
939
+ let schemaError: { status?: number; message?: string } | undefined;
940
+ let usedPaidMethod = false;
941
+ const executeChild = async (childInput: Record<string, unknown>) => {
942
+ const body = { input: childInput, playbook_context: playbookContext };
943
+ try {
944
+ return await apiPost<Record<string, unknown>>(
945
+ `/agents/${step.agent_id}/run`,
946
+ body,
947
+ { ensureConsumerPrincipal: true },
948
+ );
949
+ } catch (err) {
950
+ const status = (err as { status?: number })?.status;
951
+ if (status !== 402) throw err;
952
+ usedPaidMethod = true;
953
+ if ((method ?? spendMethod) === "link") {
954
+ const linkConfig = getLinkConfig();
955
+ if (!linkConfig) {
956
+ throw new Error('Payment method "link" is not configured. Run wallet_setup({ action: "add-link" }).');
957
+ }
958
+ const challenge = (err as { headers?: Headers })?.headers?.get("www-authenticate");
959
+ if (!challenge && !linkNetworkId) {
960
+ throw new Error("Link payment challenge did not include a WWW-Authenticate header.");
961
+ }
962
+ linkNetworkId = linkNetworkId ?? await decodeMppNetworkId(challenge!);
963
+ linkSpendRequestId = linkSpendRequestId ?? await ensureApprovedLinkSpendRequest({
964
+ amount: cents(effectiveBudget),
965
+ currency: "usd",
966
+ context: linkApprovalContext,
967
+ expiresAt: Math.floor(Date.now() / 1000) + 3600,
968
+ networkId: linkNetworkId,
969
+ paymentMethodId: linkConfig.paymentMethodId,
970
+ });
971
+ return await apiPostWithApprovedLinkSpendRequest<Record<string, unknown>>(
972
+ `/agents/${step.agent_id}/run`,
973
+ body,
974
+ linkSpendRequestId,
975
+ );
976
+ }
977
+ return await apiPostWithPayment<Record<string, unknown>>(
978
+ `/agents/${step.agent_id}/run`,
979
+ body,
980
+ method,
981
+ );
982
+ }
983
+ };
984
+ try {
985
+ result = await executeChild(payload);
986
+ } catch (err) {
987
+ if (err instanceof LinkApprovalRequiredError) {
988
+ await recordStep(runState.run_id, step.id, {
989
+ status: "pending",
990
+ agent_id: step.agent_id,
991
+ provider_id: step.provider_id,
992
+ consumption_mode: "not_charged",
993
+ error_code: null,
994
+ failure_message: null,
995
+ });
996
+ await updateRun(runState.run_id, {
997
+ status: "paused",
998
+ error_code: "LINK_APPROVAL_REQUIRED",
999
+ failure_message: "Approve the Link spend request, then resume the playbook run.",
1000
+ });
1001
+ const receipt = await apiGet<PlaybookRunReceipt>(`/playbook-runs/${runState.run_id}`);
1002
+ return text(`${formatReceipt(receipt)}\n\n${err.message}`);
1003
+ }
1004
+ const apiErr = err as { status?: number; message?: string };
1005
+ const validationFailed = (apiErr?.status === 400 || apiErr?.status === 422)
1006
+ && /missing required field|validation failed|invalid input|schema|provide at least one/i.test(apiErr.message ?? "");
1007
+ if (validationFailed) {
1008
+ try {
1009
+ const agent = await apiGet<AgentSchemaDetails>(`/agents/${step.agent_id}`);
1010
+ const schema = liveInputSchema(agent);
1011
+ if (schema) {
1012
+ const repaired = repairInputFromSchema(payload, schema);
1013
+ if (repaired.applied.length > 0 && repaired.missing.length === 0) {
1014
+ result = await executeChild(repaired.input);
1015
+ } else {
1016
+ schemaError = apiErr;
1017
+ }
1018
+ } else {
1019
+ schemaError = apiErr;
1020
+ }
1021
+ } catch {
1022
+ schemaError = apiErr;
1023
+ }
1024
+ } else {
1025
+ const errorCode = apiErr?.status === 402 ? "PAYMENT_REJECTED" : "CHILD_AGENT_FAILED";
1026
+ await recordStep(runState.run_id, step.id, {
1027
+ status: apiErr?.status === 402 ? "skipped" : "failed",
1028
+ agent_id: step.agent_id,
1029
+ provider_id: step.provider_id,
1030
+ consumption_mode: apiErr?.status === 402 ? "not_charged" : "direct_charge",
1031
+ error_code: errorCode,
1032
+ failure_message: apiErr?.message ?? "Child agent execution failed",
1033
+ });
1034
+ await updateRun(runState.run_id, {
1035
+ status: "paused",
1036
+ error_code: errorCode,
1037
+ failure_message: apiErr?.message,
1038
+ });
1039
+ const receipt = await apiGet<PlaybookRunReceipt>(`/playbook-runs/${runState.run_id}`);
1040
+ const help = apiErr?.status === 402
1041
+ ? "Use wallet_status to inspect payment methods before retrying."
1042
+ : "The child agent failed before a paid run completed.";
1043
+ return text(`${formatReceipt(receipt)}\n\n${help}`);
1044
+ }
1045
+ }
1046
+
1047
+ if (!result) {
1048
+ await recordStep(runState.run_id, step.id, {
1049
+ status: "skipped",
1050
+ agent_id: step.agent_id,
1051
+ provider_id: step.provider_id,
1052
+ consumption_mode: "not_charged",
1053
+ error_code: "SCHEMA_MISMATCH",
1054
+ failure_message: schemaError?.message ?? "Child agent input schema mismatch",
1055
+ });
1056
+ await updateRun(runState.run_id, {
1057
+ status: "paused",
1058
+ error_code: "SCHEMA_MISMATCH",
1059
+ failure_message: schemaError?.message,
1060
+ });
1061
+ const receipt = await apiGet<PlaybookRunReceipt>(`/playbook-runs/${runState.run_id}`);
1062
+ return text(`${formatReceipt(receipt)}\n\nTried the live child schema but could not safely repair the input. Use get_agent({ agent_id: "${step.agent_id}" }) to inspect the child agent schema before retrying.`);
1063
+ }
1064
+
1065
+ const jobId = (result.job_id as string) ?? "";
1066
+ const status = result.status as string;
1067
+ let output = result.output;
1068
+ let errorCode = result.error_code as string | undefined;
1069
+
1070
+ if (status === "processing" && jobId) {
1071
+ const pollResult = await pollJobUntilDone(jobId, method);
1072
+ output = pollResult.output;
1073
+ errorCode = pollResult.error_code;
1074
+ if (pollResult.error_code === "POLL_TIMEOUT") {
1075
+ await recordStep(runState.run_id, step.id, {
1076
+ status: "running",
1077
+ agent_id: step.agent_id,
1078
+ provider_id: step.provider_id,
1079
+ job_id: jobId,
1080
+ consumption_mode: usedPaidMethod ? "direct_charge" : "not_charged",
1081
+ error_code: "POLL_TIMEOUT",
1082
+ failure_message: "Child job is still processing after the MCP poll window.",
1083
+ });
1084
+ await updateRun(runState.run_id, {
1085
+ status: "needs_reconciliation",
1086
+ error_code: "POLL_TIMEOUT",
1087
+ failure_message: "A child job is still processing. Use get_playbook_run before retrying this playbook.",
1088
+ });
1089
+ const receipt = await apiGet<PlaybookRunReceipt>(`/playbook-runs/${runState.run_id}`);
1090
+ return multiText(
1091
+ formatReceipt(receipt),
1092
+ `Child job ${jobId} is still processing after the MCP poll window. Do not rerun this step until get_job({ job_id: "${jobId}" }) or get_playbook_run({ run_id: "${runState.run_id}" }) shows a terminal status.`,
1093
+ );
1094
+ }
1095
+ result.status = pollResult.status === "completed" ? "success" : "failed";
1096
+ }
1097
+
1098
+ const usedCreditPack = result.consumption_mode === "credit_pack";
1099
+ const actualCost = usedCreditPack ? 0 : Number(result.cost ?? (result.status === "success" ? step.quoted_cost_usd : 0));
1100
+ if (result.status === "success") {
1101
+ charged += actualCost;
1102
+ if (usedPaidMethod) recordSpend(spendMethod, actualCost);
1103
+ childOutputs.push({ step: step.id, job_id: jobId, output });
1104
+ try {
1105
+ await recordStep(runState.run_id, step.id, {
1106
+ status: "succeeded",
1107
+ agent_id: step.agent_id,
1108
+ provider_id: step.provider_id,
1109
+ job_id: jobId,
1110
+ consumption_mode: usedCreditPack ? "credit_pack" : "direct_charge",
1111
+ charged_usd: actualCost,
1112
+ output,
1113
+ error_code: null,
1114
+ failure_message: null,
1115
+ });
1116
+ } catch (receiptErr) {
1117
+ const message = receiptErr instanceof Error ? receiptErr.message : "Failed to record playbook step receipt";
1118
+ try {
1119
+ await updateRun(runState.run_id, {
1120
+ status: "needs_reconciliation",
1121
+ error_code: "RECEIPT_WRITE_FAILED",
1122
+ failure_message: message,
1123
+ output: {
1124
+ summary: `${playbook.name} needs reconciliation after a paid child step completed.`,
1125
+ child_outputs: childOutputs,
1126
+ },
1127
+ });
1128
+ } catch {
1129
+ // The paid child result is still returned below even if the run status update also fails.
1130
+ }
1131
+ return multiText(
1132
+ formatRunResult({ status: "success", job_id: jobId, output }, { paymentMethod: method }),
1133
+ [
1134
+ `Playbook run ${runState.run_id} needs reconciliation.`,
1135
+ `Child step ${step.id} completed${usedCreditPack ? " using a credit pack" : ` after charging ${money(actualCost)}`}, but the gateway receipt write failed after retries.`,
1136
+ "Do not rerun this step until get_playbook_run shows it reconciled.",
1137
+ ].join("\n"),
1138
+ );
1139
+ }
1140
+ } else {
1141
+ const failedCharge = usedCreditPack ? 0 : step.quoted_cost_usd;
1142
+ const stepStatus = failedCharge > 0 ? "refunded" : "failed";
1143
+ await recordStep(runState.run_id, step.id, {
1144
+ status: stepStatus,
1145
+ agent_id: step.agent_id,
1146
+ provider_id: step.provider_id,
1147
+ job_id: jobId,
1148
+ consumption_mode: usedCreditPack ? "credit_pack" : "direct_charge",
1149
+ charged_usd: failedCharge,
1150
+ refunded_usd: failedCharge,
1151
+ output,
1152
+ error_code: errorCode ?? "CHILD_AGENT_FAILED",
1153
+ });
1154
+ await updateRun(runState.run_id, { status: "paused", error_code: errorCode ?? "CHILD_AGENT_FAILED" });
1155
+ const receipt = await apiGet<PlaybookRunReceipt>(`/playbook-runs/${runState.run_id}`);
1156
+ return multiText(
1157
+ formatReceipt(receipt),
1158
+ "The child agent failed. Existing per-agent refund behavior applies to that child step.",
1159
+ );
1160
+ }
1161
+ }
1162
+
1163
+ const finalOutput = {
1164
+ summary: `${playbook.name} completed ${childOutputs.length} child step${childOutputs.length === 1 ? "" : "s"}.`,
1165
+ child_outputs: childOutputs,
1166
+ };
1167
+ await updateRun(runState.run_id, {
1168
+ status: "completed",
1169
+ output: finalOutput,
1170
+ error_code: null,
1171
+ failure_message: null,
1172
+ });
1173
+ const receipt = await apiGet<PlaybookRunReceipt>(`/playbook-runs/${runState.run_id}`);
1174
+ pendingPlaybookRuns.delete(playbook.slug);
1175
+ if ((method ?? spendMethod) === "link" && linkSpendRequestId) {
1176
+ setPendingLinkSpendRequest(null);
1177
+ }
1178
+
1179
+ const runBlocks = childOutputs.length > 0
1180
+ ? childOutputs.map((item) => item.output ? formatRunResult({ status: "success", job_id: item.job_id, output: item.output }, { paymentMethod: method }) : "").filter(Boolean)
1181
+ : [];
1182
+ return multiText(
1183
+ uploadSummary ? `${uploadSummary}\n\n${formatReceipt(receipt)}` : formatReceipt(receipt),
1184
+ ...runBlocks
1185
+ );
1186
+ },
1187
+ );
1188
+
1189
+ server.tool(
1190
+ "favorite_playbook",
1191
+ "Save an Agent Wonderland Playbook for later. Favorites are associated with the configured consumer principal when available.",
1192
+ {
1193
+ playbook_id: z.string().optional(),
1194
+ slug: z.string().optional(),
1195
+ },
1196
+ async ({ playbook_id, slug }) => {
1197
+ const id = slug ?? playbook_id;
1198
+ if (!id) return text("Provide slug or playbook_id.");
1199
+
1200
+ const result = await apiPost<{ ok: boolean; favorite_id: string; playbook_slug: string }>(
1201
+ `/playbooks/${encodeURIComponent(id)}/favorite`,
1202
+ {},
1203
+ { ensureConsumerPrincipal: true },
1204
+ );
1205
+
1206
+ return text(`Saved ${result.playbook_slug} to favorites.`);
1207
+ },
1208
+ );
1209
+
1210
+ server.tool(
1211
+ "get_playbook_run",
1212
+ "Inspect current or historical Agent Wonderland Playbook run state, including completed steps, charged/refunded amount, partial output, and resume command when paused.",
1213
+ {
1214
+ run_id: z.string(),
1215
+ },
1216
+ async ({ run_id }) => {
1217
+ const receipt = await apiGet<PlaybookRunReceipt>(`/playbook-runs/${run_id}`);
1218
+ return text(formatReceipt(receipt));
1219
+ },
1220
+ );
1221
+ }