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