@agentwonderland/mcp 0.1.56 → 0.1.57
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/version.d.ts +1 -1
- package/dist/core/version.js +1 -1
- package/dist/index.js +4 -0
- package/dist/tools/__tests__/playbook-adapters.test.d.ts +1 -0
- package/dist/tools/__tests__/playbook-adapters.test.js +67 -0
- package/dist/tools/__tests__/playbooks.test.js +3 -2
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.js +1 -0
- package/dist/tools/playbook-adapters.d.ts +19 -0
- package/dist/tools/playbook-adapters.js +164 -0
- package/dist/tools/playbooks.js +54 -19
- package/package.json +1 -1
- package/src/core/version.ts +1 -1
- package/src/index.ts +4 -0
- package/src/tools/__tests__/playbook-adapters.test.ts +74 -0
- package/src/tools/__tests__/playbooks.test.ts +3 -2
- package/src/tools/index.ts +1 -0
- package/src/tools/playbook-adapters.ts +199 -0
- package/src/tools/playbooks.ts +55 -21
package/dist/core/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const MCP_PACKAGE_VERSION = "0.1.
|
|
1
|
+
export declare const MCP_PACKAGE_VERSION = "0.1.57";
|
package/dist/core/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const MCP_PACKAGE_VERSION = "0.1.
|
|
1
|
+
export const MCP_PACKAGE_VERSION = "0.1.57";
|
package/dist/index.js
CHANGED
|
@@ -16,6 +16,7 @@ import { registerRebateTools } from "./tools/rebates.js";
|
|
|
16
16
|
import { registerUploadTools } from "./tools/upload.js";
|
|
17
17
|
import { registerProbeTools } from "./tools/probe.js";
|
|
18
18
|
import { registerProviderTools } from "./tools/providers.js";
|
|
19
|
+
import { registerPlaybookTools } from "./tools/playbooks.js";
|
|
19
20
|
// ── Resources ────────────────────────────────────────────────────
|
|
20
21
|
import { registerAgentResources } from "./resources/agents.js";
|
|
21
22
|
import { registerWalletResources } from "./resources/wallet.js";
|
|
@@ -41,8 +42,10 @@ export async function startMcpServer() {
|
|
|
41
42
|
"WORKFLOW:",
|
|
42
43
|
"1. solve(intent, input, budget) — one call: find best agent + pay + run. Use when the task is clear.",
|
|
43
44
|
" OR: search_agents() → get_agent() to inspect schema → run_agent() with required fields.",
|
|
45
|
+
" For multi-agent workflows, use search_playbooks() → get_playbook() → run_playbook().",
|
|
44
46
|
"2. If the agent returns status 'processing', poll with get_job(). Async runs resolve automatically.",
|
|
45
47
|
"3. After a successful agent run, rate_agent() and optionally tip_agent() if the result was useful.",
|
|
48
|
+
" Playbooks return one run-level receipt; do not ask users to rate or tip individual child-agent steps.",
|
|
46
49
|
"4. Use list_jobs() to recover state across sessions (it checks every configured wallet).",
|
|
47
50
|
"",
|
|
48
51
|
"PAYMENT:",
|
|
@@ -85,6 +88,7 @@ export async function startMcpServer() {
|
|
|
85
88
|
registerUploadTools(server);
|
|
86
89
|
registerProbeTools(server);
|
|
87
90
|
registerProviderTools(server);
|
|
91
|
+
registerPlaybookTools(server);
|
|
88
92
|
// Register resources
|
|
89
93
|
registerAgentResources(server);
|
|
90
94
|
registerWalletResources(server);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { buildExplicitPlaybookStepInput, playbookRunBlocker } from "../playbook-adapters.js";
|
|
3
|
+
describe("playbook step adapters", () => {
|
|
4
|
+
it("maps ipo-brief child steps to their live input schemas", () => {
|
|
5
|
+
expect(buildExplicitPlaybookStepInput("ipo-brief", { ticker: "CRCL" }, {
|
|
6
|
+
id: "s1",
|
|
7
|
+
agent_slug: "ipo-s-1-analysis",
|
|
8
|
+
})).toMatchObject({ status: "ready", input: { ticker: "CRCL" } });
|
|
9
|
+
expect(buildExplicitPlaybookStepInput("ipo-brief", { ticker: "CRCL" }, {
|
|
10
|
+
id: "insiders",
|
|
11
|
+
agent_slug: "insider-trading-tracker",
|
|
12
|
+
})).toMatchObject({ status: "ready", input: { ticker: "CRCL", days_back: 365 } });
|
|
13
|
+
expect(buildExplicitPlaybookStepInput("ipo-brief", { ticker: "CRCL", peer_tickers: ["COIN", "HOOD"] }, {
|
|
14
|
+
id: "comps",
|
|
15
|
+
agent_slug: "stock-comparison",
|
|
16
|
+
})).toMatchObject({ status: "ready", input: { tickers: ["CRCL", "COIN", "HOOD"] } });
|
|
17
|
+
});
|
|
18
|
+
it("maps launch-landing council, writer, and publisher steps explicitly", () => {
|
|
19
|
+
const council = buildExplicitPlaybookStepInput("launch-landing", {
|
|
20
|
+
brief: "Agent marketplace for builders",
|
|
21
|
+
product_slug: "agent-wonderland",
|
|
22
|
+
}, {
|
|
23
|
+
id: "council",
|
|
24
|
+
agent_slug: "marketing-copy-council",
|
|
25
|
+
});
|
|
26
|
+
expect(council).toMatchObject({ status: "ready", input: { brief: "Agent marketplace for builders" } });
|
|
27
|
+
const writer = buildExplicitPlaybookStepInput("launch-landing", {
|
|
28
|
+
brief: "Agent marketplace for builders",
|
|
29
|
+
product_slug: "agent-wonderland",
|
|
30
|
+
previous_outputs: [{ step: "council", output: { headline: "Hire agents on demand" } }],
|
|
31
|
+
}, {
|
|
32
|
+
id: "structure",
|
|
33
|
+
agent_slug: "write-landing-page-copy",
|
|
34
|
+
});
|
|
35
|
+
expect(writer).toMatchObject({
|
|
36
|
+
status: "ready",
|
|
37
|
+
input: {
|
|
38
|
+
product: "agent-wonderland",
|
|
39
|
+
audience: "builders and operators",
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
expect(writer.input.context).toContain("Hire agents on demand");
|
|
43
|
+
const publisher = buildExplicitPlaybookStepInput("launch-landing", {
|
|
44
|
+
brief: "Agent marketplace for builders",
|
|
45
|
+
product_slug: "agent-wonderland",
|
|
46
|
+
previous_outputs: [{ step: "structure", output: { html: "<html><body>Ship</body></html>" } }],
|
|
47
|
+
}, {
|
|
48
|
+
id: "publish",
|
|
49
|
+
agent_slug: "publish-html-to-a-public-url",
|
|
50
|
+
});
|
|
51
|
+
expect(publisher).toMatchObject({
|
|
52
|
+
status: "ready",
|
|
53
|
+
input: {
|
|
54
|
+
html: "<html><body>Ship</body></html>",
|
|
55
|
+
slug: "agent-wonderland",
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
it("blocks known broken fanout playbooks before paid child execution", () => {
|
|
60
|
+
expect(playbookRunBlocker("icp-hunter")).toContain("prospect-search schema");
|
|
61
|
+
expect(playbookRunBlocker("no-web-leads")).toContain("no-web/place/phone schemas");
|
|
62
|
+
expect(buildExplicitPlaybookStepInput("icp-hunter", { icp: "VP Eng" }, {
|
|
63
|
+
id: "prospects",
|
|
64
|
+
agent_slug: "find-prospects",
|
|
65
|
+
})).toMatchObject({ status: "blocked", code: "PLAYBOOK_NOT_CERTIFIED" });
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -533,7 +533,7 @@ describe("playbook MCP tools", () => {
|
|
|
533
533
|
expect(text).not.toContain("favorite_playbook({ slug: \"competitor-ads\" })");
|
|
534
534
|
expect(text).not.toContain("rate_playbook");
|
|
535
535
|
});
|
|
536
|
-
it("uses
|
|
536
|
+
it("uses a separate approved Link spend request for the paid child step", async () => {
|
|
537
537
|
mockRequiresSpendConfirmation.mockReturnValue(false);
|
|
538
538
|
mockGetConfiguredMethods.mockReturnValue(["link"]);
|
|
539
539
|
mockApiGet
|
|
@@ -599,8 +599,9 @@ describe("playbook MCP tools", () => {
|
|
|
599
599
|
const text = flattenToolText(result);
|
|
600
600
|
expect(mockDecodeMppNetworkId).toHaveBeenCalledWith("Payment challenge");
|
|
601
601
|
expect(mockEnsureApprovedLinkSpendRequest).toHaveBeenCalledWith(expect.objectContaining({
|
|
602
|
-
amount: "
|
|
602
|
+
amount: "125",
|
|
603
603
|
currency: "usd",
|
|
604
|
+
context: expect.stringContaining("step 1"),
|
|
604
605
|
networkId: "profile_test",
|
|
605
606
|
paymentMethodId: "csmrpd_test_123",
|
|
606
607
|
}));
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -12,3 +12,4 @@ export { registerRebateTools } from "./rebates.js";
|
|
|
12
12
|
export { registerUploadTools } from "./upload.js";
|
|
13
13
|
export { registerProbeTools } from "./probe.js";
|
|
14
14
|
export { registerProviderTools } from "./providers.js";
|
|
15
|
+
export { registerPlaybookTools } from "./playbooks.js";
|
package/dist/tools/index.js
CHANGED
|
@@ -12,3 +12,4 @@ export { registerRebateTools } from "./rebates.js";
|
|
|
12
12
|
export { registerUploadTools } from "./upload.js";
|
|
13
13
|
export { registerProbeTools } from "./probe.js";
|
|
14
14
|
export { registerProviderTools } from "./providers.js";
|
|
15
|
+
export { registerPlaybookTools } from "./playbooks.js";
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
type StepLike = {
|
|
2
|
+
id: string;
|
|
3
|
+
agent_slug: string | null;
|
|
4
|
+
default_input?: Record<string, unknown>;
|
|
5
|
+
quantity?: number;
|
|
6
|
+
iteration?: number;
|
|
7
|
+
};
|
|
8
|
+
export type PlaybookStepInputResult = {
|
|
9
|
+
status: "ready";
|
|
10
|
+
input: Record<string, unknown>;
|
|
11
|
+
certification: "explicit";
|
|
12
|
+
} | {
|
|
13
|
+
status: "blocked";
|
|
14
|
+
code: string;
|
|
15
|
+
message: string;
|
|
16
|
+
};
|
|
17
|
+
export declare function playbookRunBlocker(slug: string): string | null;
|
|
18
|
+
export declare function buildExplicitPlaybookStepInput(playbookSlug: string, baseInput: Record<string, unknown>, step: StepLike): PlaybookStepInputResult | null;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
const BLOCKED_PLAYBOOKS = {
|
|
2
|
+
"icp-hunter": "Blocked until the friendly ICP input is mapped to the live prospect-search schema and the prospect agent is verified to honor those filters.",
|
|
3
|
+
"no-web-leads": "Blocked until zip/category inputs are mapped to the live no-web/place/phone schemas and the no-web agent is verified to honor the requested local filters.",
|
|
4
|
+
};
|
|
5
|
+
function asString(value) {
|
|
6
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
7
|
+
}
|
|
8
|
+
function outputRecord(value) {
|
|
9
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
10
|
+
? value
|
|
11
|
+
: undefined;
|
|
12
|
+
}
|
|
13
|
+
function previousOutputs(input) {
|
|
14
|
+
return Array.isArray(input.previous_outputs)
|
|
15
|
+
? input.previous_outputs.map(outputRecord).filter((item) => Boolean(item))
|
|
16
|
+
: [];
|
|
17
|
+
}
|
|
18
|
+
function stepOutput(entry) {
|
|
19
|
+
return outputRecord(entry)?.output ?? entry;
|
|
20
|
+
}
|
|
21
|
+
function extractHtml(value) {
|
|
22
|
+
if (typeof value === "string" && /<html|<!doctype|<body|<section|<main/i.test(value))
|
|
23
|
+
return value;
|
|
24
|
+
const record = outputRecord(value);
|
|
25
|
+
if (!record)
|
|
26
|
+
return undefined;
|
|
27
|
+
const direct = record.html;
|
|
28
|
+
if (typeof direct === "string" && direct.trim())
|
|
29
|
+
return direct;
|
|
30
|
+
const output = record.output;
|
|
31
|
+
if (typeof output === "string" && /<html|<!doctype|<body|<section|<main/i.test(output))
|
|
32
|
+
return output;
|
|
33
|
+
const nested = outputRecord(output);
|
|
34
|
+
return typeof nested?.html === "string" && nested.html.trim() ? nested.html : undefined;
|
|
35
|
+
}
|
|
36
|
+
function launchLandingInput(baseInput, step) {
|
|
37
|
+
const brief = asString(baseInput.brief) ?? asString(baseInput.context) ?? asString(baseInput.product);
|
|
38
|
+
if (!brief) {
|
|
39
|
+
return {
|
|
40
|
+
status: "blocked",
|
|
41
|
+
code: "PLAYBOOK_INPUT_MISSING_BRIEF",
|
|
42
|
+
message: "launch-landing requires a brief before any child agent is called.",
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
if (step.agent_slug === "marketing-copy-council") {
|
|
46
|
+
return {
|
|
47
|
+
status: "ready",
|
|
48
|
+
certification: "explicit",
|
|
49
|
+
input: {
|
|
50
|
+
brief,
|
|
51
|
+
context: {
|
|
52
|
+
product_slug: asString(baseInput.product_slug),
|
|
53
|
+
},
|
|
54
|
+
num_outputs: 1,
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
const outputs = previousOutputs(baseInput);
|
|
59
|
+
if (step.agent_slug === "write-landing-page-copy") {
|
|
60
|
+
const council = outputs.find((item) => item.step === "council" || item.step === "marketing-copy-council") ?? outputs.at(-1);
|
|
61
|
+
const product = asString(baseInput.product)
|
|
62
|
+
?? asString(baseInput.product_name)
|
|
63
|
+
?? asString(baseInput.product_slug)
|
|
64
|
+
?? "Landing page";
|
|
65
|
+
return {
|
|
66
|
+
status: "ready",
|
|
67
|
+
certification: "explicit",
|
|
68
|
+
input: {
|
|
69
|
+
product,
|
|
70
|
+
audience: asString(baseInput.audience) ?? "builders and operators",
|
|
71
|
+
tone: asString(baseInput.tone) ?? "confident and practical",
|
|
72
|
+
context: JSON.stringify({
|
|
73
|
+
brief,
|
|
74
|
+
product_slug: asString(baseInput.product_slug),
|
|
75
|
+
council_output: stepOutput(council),
|
|
76
|
+
}),
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
if (step.agent_slug === "publish-html-to-a-public-url") {
|
|
81
|
+
const writer = outputs.find((item) => item.step === "structure" || item.step === "write-landing-page-copy") ?? outputs.at(-1);
|
|
82
|
+
const html = extractHtml(stepOutput(writer)) ?? extractHtml(writer);
|
|
83
|
+
if (!html) {
|
|
84
|
+
return {
|
|
85
|
+
status: "blocked",
|
|
86
|
+
code: "PLAYBOOK_INPUT_MISSING_HTML",
|
|
87
|
+
message: "launch-landing publish step requires generated HTML from the writer step.",
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
status: "ready",
|
|
92
|
+
certification: "explicit",
|
|
93
|
+
input: {
|
|
94
|
+
html,
|
|
95
|
+
slug: asString(baseInput.product_slug) ?? asString(baseInput.slug),
|
|
96
|
+
title: asString(baseInput.title) ?? asString(baseInput.product) ?? asString(baseInput.product_slug),
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
status: "blocked",
|
|
102
|
+
code: "PLAYBOOK_STEP_NOT_CERTIFIED",
|
|
103
|
+
message: `launch-landing step ${step.id} (${step.agent_slug ?? "unknown"}) has no explicit adapter.`,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function ipoBriefInput(baseInput, step) {
|
|
107
|
+
const ticker = asString(baseInput.ticker) ?? asString(baseInput.symbol);
|
|
108
|
+
if (!ticker) {
|
|
109
|
+
return {
|
|
110
|
+
status: "blocked",
|
|
111
|
+
code: "PLAYBOOK_INPUT_MISSING_TICKER",
|
|
112
|
+
message: "ipo-brief requires ticker before any child agent is called.",
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
if (step.agent_slug === "ipo-s-1-analysis") {
|
|
116
|
+
return {
|
|
117
|
+
status: "ready",
|
|
118
|
+
certification: "explicit",
|
|
119
|
+
input: { ticker },
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
if (step.agent_slug === "insider-trading-tracker") {
|
|
123
|
+
return {
|
|
124
|
+
status: "ready",
|
|
125
|
+
certification: "explicit",
|
|
126
|
+
input: {
|
|
127
|
+
ticker,
|
|
128
|
+
days_back: Number(baseInput.days_back ?? 365),
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
if (step.agent_slug === "stock-comparison") {
|
|
133
|
+
const peers = Array.isArray(baseInput.peer_tickers) ? baseInput.peer_tickers : [];
|
|
134
|
+
const tickers = [ticker, ...peers].filter((item) => typeof item === "string" && Boolean(item.trim())).slice(0, 5);
|
|
135
|
+
return {
|
|
136
|
+
status: "ready",
|
|
137
|
+
certification: "explicit",
|
|
138
|
+
input: { tickers },
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
status: "blocked",
|
|
143
|
+
code: "PLAYBOOK_STEP_NOT_CERTIFIED",
|
|
144
|
+
message: `ipo-brief step ${step.id} (${step.agent_slug ?? "unknown"}) has no explicit adapter.`,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
export function playbookRunBlocker(slug) {
|
|
148
|
+
return BLOCKED_PLAYBOOKS[slug] ?? null;
|
|
149
|
+
}
|
|
150
|
+
export function buildExplicitPlaybookStepInput(playbookSlug, baseInput, step) {
|
|
151
|
+
const blocker = playbookRunBlocker(playbookSlug);
|
|
152
|
+
if (blocker) {
|
|
153
|
+
return {
|
|
154
|
+
status: "blocked",
|
|
155
|
+
code: "PLAYBOOK_NOT_CERTIFIED",
|
|
156
|
+
message: blocker,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
if (playbookSlug === "launch-landing")
|
|
160
|
+
return launchLandingInput(baseInput, step);
|
|
161
|
+
if (playbookSlug === "ipo-brief")
|
|
162
|
+
return ipoBriefInput(baseInput, step);
|
|
163
|
+
return null;
|
|
164
|
+
}
|
package/dist/tools/playbooks.js
CHANGED
|
@@ -7,6 +7,7 @@ import { canSpend, recordSpend, requiresPolicyConfirmation } from "../core/spend
|
|
|
7
7
|
import { formatRunResult } from "../core/formatters.js";
|
|
8
8
|
import { decodeMppNetworkId, ensureApprovedLinkSpendRequest, LinkApprovalRequiredError, } from "../core/link-cli.js";
|
|
9
9
|
import { formatPaymentLabel, resolveConfirmationMethod, } from "./_payment-confirmation.js";
|
|
10
|
+
import { buildExplicitPlaybookStepInput, playbookRunBlocker } from "./playbook-adapters.js";
|
|
10
11
|
const POLL_INTERVAL_MS = 3000;
|
|
11
12
|
const POLL_MAX_MS = 300000;
|
|
12
13
|
const pendingPlaybookRuns = new Map();
|
|
@@ -33,9 +34,9 @@ function fanoutControls(playbook) {
|
|
|
33
34
|
}
|
|
34
35
|
function buildPlaybookLinkApprovalContext(params) {
|
|
35
36
|
return [
|
|
36
|
-
`Approve
|
|
37
|
-
`
|
|
38
|
-
"Each child agent is
|
|
37
|
+
`Approve ${money(params.step.quoted_cost_usd)} for step ${params.step.index + 1} of the Agent Wonderland "${params.playbook.name}" playbook.`,
|
|
38
|
+
`Run ${params.runId}, step ${params.step.id}: ${params.step.agent_name}.`,
|
|
39
|
+
"Each child agent is approved separately, and failed child runs keep the existing Agent Wonderland refund behavior.",
|
|
39
40
|
].join(" ");
|
|
40
41
|
}
|
|
41
42
|
async function collectWalletAddresses(paymentMethod) {
|
|
@@ -217,7 +218,15 @@ async function formatCheaperAlternatives(currentSlug, budget) {
|
|
|
217
218
|
return "";
|
|
218
219
|
}
|
|
219
220
|
}
|
|
220
|
-
function stepInput(baseInput, step, iteration) {
|
|
221
|
+
function stepInput(playbookSlug, baseInput, step, iteration) {
|
|
222
|
+
const explicit = buildExplicitPlaybookStepInput(playbookSlug, baseInput, step);
|
|
223
|
+
if (explicit?.status === "ready")
|
|
224
|
+
return explicit.input;
|
|
225
|
+
if (explicit?.status === "blocked") {
|
|
226
|
+
const error = new Error(explicit.message);
|
|
227
|
+
error.code = explicit.code;
|
|
228
|
+
throw error;
|
|
229
|
+
}
|
|
221
230
|
const input = { ...(step.default_input ?? {}), ...baseInput };
|
|
222
231
|
const itemIndex = Math.max(0, (step.iteration ?? 1) - 1);
|
|
223
232
|
if (Array.isArray(input.leads) && input.leads[itemIndex] && typeof input.leads[itemIndex] === "object") {
|
|
@@ -591,6 +600,10 @@ export function registerPlaybookTools(server) {
|
|
|
591
600
|
if (playbook.current_quote.blocking_issues.length > 0) {
|
|
592
601
|
return text(`Cannot run ${playbook.slug}; child-agent issues must be fixed first:\n${playbook.current_quote.blocking_issues.map((issue) => `- ${issue}`).join("\n")}`);
|
|
593
602
|
}
|
|
603
|
+
const blocker = playbookRunBlocker(playbook.slug);
|
|
604
|
+
if (blocker) {
|
|
605
|
+
return text(`Playbook ${playbook.slug} is temporarily paused for certification.\n\n${blocker}`);
|
|
606
|
+
}
|
|
594
607
|
const runLimits = appliedPlaybookLimits(playbook) ?? effectiveLimits;
|
|
595
608
|
const estimatedCost = playbook.current_quote.estimated_cost_usd;
|
|
596
609
|
const spendCheck = canSpend({ method: spendMethod, amountUsd: Math.min(effectiveBudget, estimatedCost) });
|
|
@@ -684,14 +697,7 @@ export function registerPlaybookTools(server) {
|
|
|
684
697
|
await apiPost(`/playbook-runs/${runState.run_id}/status`, { status: "running" });
|
|
685
698
|
let charged = runState.chargedUsd;
|
|
686
699
|
const childOutputs = [...runState.existingOutputs];
|
|
687
|
-
let linkSpendRequestId;
|
|
688
700
|
let linkNetworkId;
|
|
689
|
-
const linkApprovalContext = buildPlaybookLinkApprovalContext({
|
|
690
|
-
playbook,
|
|
691
|
-
budget: effectiveBudget,
|
|
692
|
-
stepCount: playbook.current_quote.step_count,
|
|
693
|
-
runId: runState.run_id,
|
|
694
|
-
});
|
|
695
701
|
for (const [iteration, step] of runState.steps.entries()) {
|
|
696
702
|
if (runState.completedStepIds.has(step.id)) {
|
|
697
703
|
continue;
|
|
@@ -727,7 +733,28 @@ export function registerPlaybookTools(server) {
|
|
|
727
733
|
provider_id: step.provider_id,
|
|
728
734
|
quoted_cost_usd: step.quoted_cost_usd,
|
|
729
735
|
});
|
|
730
|
-
|
|
736
|
+
let payload;
|
|
737
|
+
try {
|
|
738
|
+
payload = stepInput(playbook.slug, { ...processedInput, previous_outputs: childOutputs }, step, iteration);
|
|
739
|
+
}
|
|
740
|
+
catch (err) {
|
|
741
|
+
const blocked = err;
|
|
742
|
+
await recordStep(runState.run_id, step.id, {
|
|
743
|
+
status: "skipped",
|
|
744
|
+
agent_id: step.agent_id,
|
|
745
|
+
provider_id: step.provider_id,
|
|
746
|
+
consumption_mode: "not_charged",
|
|
747
|
+
error_code: blocked.code ?? "PLAYBOOK_STEP_INPUT_BLOCKED",
|
|
748
|
+
failure_message: blocked.message,
|
|
749
|
+
});
|
|
750
|
+
await updateRun(runState.run_id, {
|
|
751
|
+
status: "paused",
|
|
752
|
+
error_code: blocked.code ?? "PLAYBOOK_STEP_INPUT_BLOCKED",
|
|
753
|
+
failure_message: blocked.message,
|
|
754
|
+
});
|
|
755
|
+
const receipt = await apiGet(`/playbook-runs/${runState.run_id}`);
|
|
756
|
+
return text(`${formatReceipt(receipt)}\n\n${blocked.message}`);
|
|
757
|
+
}
|
|
731
758
|
const playbookContext = {
|
|
732
759
|
playbook_run_id: runState.run_id,
|
|
733
760
|
playbook_id: playbook.id,
|
|
@@ -738,6 +765,7 @@ export function registerPlaybookTools(server) {
|
|
|
738
765
|
let result;
|
|
739
766
|
let schemaError;
|
|
740
767
|
let usedPaidMethod = false;
|
|
768
|
+
let stepLinkSpendRequestId;
|
|
741
769
|
const executeChild = async (childInput) => {
|
|
742
770
|
const body = { input: childInput, playbook_context: playbookContext };
|
|
743
771
|
try {
|
|
@@ -758,15 +786,19 @@ export function registerPlaybookTools(server) {
|
|
|
758
786
|
throw new Error("Link payment challenge did not include a WWW-Authenticate header.");
|
|
759
787
|
}
|
|
760
788
|
linkNetworkId = linkNetworkId ?? await decodeMppNetworkId(challenge);
|
|
761
|
-
|
|
762
|
-
amount: cents(
|
|
789
|
+
stepLinkSpendRequestId = await ensureApprovedLinkSpendRequest({
|
|
790
|
+
amount: cents(step.quoted_cost_usd),
|
|
763
791
|
currency: "usd",
|
|
764
|
-
context:
|
|
792
|
+
context: buildPlaybookLinkApprovalContext({
|
|
793
|
+
playbook,
|
|
794
|
+
runId: runState.run_id,
|
|
795
|
+
step,
|
|
796
|
+
}),
|
|
765
797
|
expiresAt: Math.floor(Date.now() / 1000) + 3600,
|
|
766
798
|
networkId: linkNetworkId,
|
|
767
799
|
paymentMethodId: linkConfig.paymentMethodId,
|
|
768
800
|
});
|
|
769
|
-
return await apiPostWithApprovedLinkSpendRequest(`/agents/${step.agent_id}/run`, body,
|
|
801
|
+
return await apiPostWithApprovedLinkSpendRequest(`/agents/${step.agent_id}/run`, body, stepLinkSpendRequestId);
|
|
770
802
|
}
|
|
771
803
|
return await apiPostWithPayment(`/agents/${step.agent_id}/run`, body, method);
|
|
772
804
|
}
|
|
@@ -818,6 +850,9 @@ export function registerPlaybookTools(server) {
|
|
|
818
850
|
}
|
|
819
851
|
else {
|
|
820
852
|
const errorCode = apiErr?.status === 402 ? "PAYMENT_REJECTED" : "CHILD_AGENT_FAILED";
|
|
853
|
+
if ((method ?? spendMethod) === "link" && stepLinkSpendRequestId) {
|
|
854
|
+
setPendingLinkSpendRequest(null);
|
|
855
|
+
}
|
|
821
856
|
await recordStep(runState.run_id, step.id, {
|
|
822
857
|
status: apiErr?.status === 402 ? "skipped" : "failed",
|
|
823
858
|
agent_id: step.agent_id,
|
|
@@ -838,6 +873,9 @@ export function registerPlaybookTools(server) {
|
|
|
838
873
|
return text(`${formatReceipt(receipt)}\n\n${help}`);
|
|
839
874
|
}
|
|
840
875
|
}
|
|
876
|
+
if (result && (method ?? spendMethod) === "link" && stepLinkSpendRequestId) {
|
|
877
|
+
setPendingLinkSpendRequest(null);
|
|
878
|
+
}
|
|
841
879
|
if (!result) {
|
|
842
880
|
await recordStep(runState.run_id, step.id, {
|
|
843
881
|
status: "skipped",
|
|
@@ -957,9 +995,6 @@ export function registerPlaybookTools(server) {
|
|
|
957
995
|
});
|
|
958
996
|
const receipt = await apiGet(`/playbook-runs/${runState.run_id}`);
|
|
959
997
|
pendingPlaybookRuns.delete(playbook.slug);
|
|
960
|
-
if ((method ?? spendMethod) === "link" && linkSpendRequestId) {
|
|
961
|
-
setPendingLinkSpendRequest(null);
|
|
962
|
-
}
|
|
963
998
|
const runBlocks = childOutputs.length > 0
|
|
964
999
|
? childOutputs.map((item) => item.output ? formatRunResult({ status: "success", job_id: item.job_id, output: item.output }, { paymentMethod: method }) : "").filter(Boolean)
|
|
965
1000
|
: [];
|
package/package.json
CHANGED
package/src/core/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const MCP_PACKAGE_VERSION = "0.1.
|
|
1
|
+
export const MCP_PACKAGE_VERSION = "0.1.57";
|
package/src/index.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { registerRebateTools } from "./tools/rebates.js";
|
|
|
18
18
|
import { registerUploadTools } from "./tools/upload.js";
|
|
19
19
|
import { registerProbeTools } from "./tools/probe.js";
|
|
20
20
|
import { registerProviderTools } from "./tools/providers.js";
|
|
21
|
+
import { registerPlaybookTools } from "./tools/playbooks.js";
|
|
21
22
|
|
|
22
23
|
// ── Resources ────────────────────────────────────────────────────
|
|
23
24
|
import { registerAgentResources } from "./resources/agents.js";
|
|
@@ -48,8 +49,10 @@ export async function startMcpServer(): Promise<void> {
|
|
|
48
49
|
"WORKFLOW:",
|
|
49
50
|
"1. solve(intent, input, budget) — one call: find best agent + pay + run. Use when the task is clear.",
|
|
50
51
|
" OR: search_agents() → get_agent() to inspect schema → run_agent() with required fields.",
|
|
52
|
+
" For multi-agent workflows, use search_playbooks() → get_playbook() → run_playbook().",
|
|
51
53
|
"2. If the agent returns status 'processing', poll with get_job(). Async runs resolve automatically.",
|
|
52
54
|
"3. After a successful agent run, rate_agent() and optionally tip_agent() if the result was useful.",
|
|
55
|
+
" Playbooks return one run-level receipt; do not ask users to rate or tip individual child-agent steps.",
|
|
53
56
|
"4. Use list_jobs() to recover state across sessions (it checks every configured wallet).",
|
|
54
57
|
"",
|
|
55
58
|
"PAYMENT:",
|
|
@@ -94,6 +97,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
94
97
|
registerUploadTools(server);
|
|
95
98
|
registerProbeTools(server);
|
|
96
99
|
registerProviderTools(server);
|
|
100
|
+
registerPlaybookTools(server);
|
|
97
101
|
|
|
98
102
|
// Register resources
|
|
99
103
|
registerAgentResources(server);
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { buildExplicitPlaybookStepInput, playbookRunBlocker } from "../playbook-adapters.js";
|
|
3
|
+
|
|
4
|
+
describe("playbook step adapters", () => {
|
|
5
|
+
it("maps ipo-brief child steps to their live input schemas", () => {
|
|
6
|
+
expect(buildExplicitPlaybookStepInput("ipo-brief", { ticker: "CRCL" }, {
|
|
7
|
+
id: "s1",
|
|
8
|
+
agent_slug: "ipo-s-1-analysis",
|
|
9
|
+
})).toMatchObject({ status: "ready", input: { ticker: "CRCL" } });
|
|
10
|
+
|
|
11
|
+
expect(buildExplicitPlaybookStepInput("ipo-brief", { ticker: "CRCL" }, {
|
|
12
|
+
id: "insiders",
|
|
13
|
+
agent_slug: "insider-trading-tracker",
|
|
14
|
+
})).toMatchObject({ status: "ready", input: { ticker: "CRCL", days_back: 365 } });
|
|
15
|
+
|
|
16
|
+
expect(buildExplicitPlaybookStepInput("ipo-brief", { ticker: "CRCL", peer_tickers: ["COIN", "HOOD"] }, {
|
|
17
|
+
id: "comps",
|
|
18
|
+
agent_slug: "stock-comparison",
|
|
19
|
+
})).toMatchObject({ status: "ready", input: { tickers: ["CRCL", "COIN", "HOOD"] } });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("maps launch-landing council, writer, and publisher steps explicitly", () => {
|
|
23
|
+
const council = buildExplicitPlaybookStepInput("launch-landing", {
|
|
24
|
+
brief: "Agent marketplace for builders",
|
|
25
|
+
product_slug: "agent-wonderland",
|
|
26
|
+
}, {
|
|
27
|
+
id: "council",
|
|
28
|
+
agent_slug: "marketing-copy-council",
|
|
29
|
+
});
|
|
30
|
+
expect(council).toMatchObject({ status: "ready", input: { brief: "Agent marketplace for builders" } });
|
|
31
|
+
|
|
32
|
+
const writer = buildExplicitPlaybookStepInput("launch-landing", {
|
|
33
|
+
brief: "Agent marketplace for builders",
|
|
34
|
+
product_slug: "agent-wonderland",
|
|
35
|
+
previous_outputs: [{ step: "council", output: { headline: "Hire agents on demand" } }],
|
|
36
|
+
}, {
|
|
37
|
+
id: "structure",
|
|
38
|
+
agent_slug: "write-landing-page-copy",
|
|
39
|
+
});
|
|
40
|
+
expect(writer).toMatchObject({
|
|
41
|
+
status: "ready",
|
|
42
|
+
input: {
|
|
43
|
+
product: "agent-wonderland",
|
|
44
|
+
audience: "builders and operators",
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
expect((writer as { input: Record<string, unknown> }).input.context).toContain("Hire agents on demand");
|
|
48
|
+
|
|
49
|
+
const publisher = buildExplicitPlaybookStepInput("launch-landing", {
|
|
50
|
+
brief: "Agent marketplace for builders",
|
|
51
|
+
product_slug: "agent-wonderland",
|
|
52
|
+
previous_outputs: [{ step: "structure", output: { html: "<html><body>Ship</body></html>" } }],
|
|
53
|
+
}, {
|
|
54
|
+
id: "publish",
|
|
55
|
+
agent_slug: "publish-html-to-a-public-url",
|
|
56
|
+
});
|
|
57
|
+
expect(publisher).toMatchObject({
|
|
58
|
+
status: "ready",
|
|
59
|
+
input: {
|
|
60
|
+
html: "<html><body>Ship</body></html>",
|
|
61
|
+
slug: "agent-wonderland",
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("blocks known broken fanout playbooks before paid child execution", () => {
|
|
67
|
+
expect(playbookRunBlocker("icp-hunter")).toContain("prospect-search schema");
|
|
68
|
+
expect(playbookRunBlocker("no-web-leads")).toContain("no-web/place/phone schemas");
|
|
69
|
+
expect(buildExplicitPlaybookStepInput("icp-hunter", { icp: "VP Eng" }, {
|
|
70
|
+
id: "prospects",
|
|
71
|
+
agent_slug: "find-prospects",
|
|
72
|
+
})).toMatchObject({ status: "blocked", code: "PLAYBOOK_NOT_CERTIFIED" });
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -621,7 +621,7 @@ describe("playbook MCP tools", () => {
|
|
|
621
621
|
expect(text).not.toContain("rate_playbook");
|
|
622
622
|
});
|
|
623
623
|
|
|
624
|
-
it("uses
|
|
624
|
+
it("uses a separate approved Link spend request for the paid child step", async () => {
|
|
625
625
|
mockRequiresSpendConfirmation.mockReturnValue(false);
|
|
626
626
|
mockGetConfiguredMethods.mockReturnValue(["link"]);
|
|
627
627
|
mockApiGet
|
|
@@ -690,8 +690,9 @@ describe("playbook MCP tools", () => {
|
|
|
690
690
|
|
|
691
691
|
expect(mockDecodeMppNetworkId).toHaveBeenCalledWith("Payment challenge");
|
|
692
692
|
expect(mockEnsureApprovedLinkSpendRequest).toHaveBeenCalledWith(expect.objectContaining({
|
|
693
|
-
amount: "
|
|
693
|
+
amount: "125",
|
|
694
694
|
currency: "usd",
|
|
695
|
+
context: expect.stringContaining("step 1"),
|
|
695
696
|
networkId: "profile_test",
|
|
696
697
|
paymentMethodId: "csmrpd_test_123",
|
|
697
698
|
}));
|
package/src/tools/index.ts
CHANGED
|
@@ -12,3 +12,4 @@ export { registerRebateTools } from "./rebates.js";
|
|
|
12
12
|
export { registerUploadTools } from "./upload.js";
|
|
13
13
|
export { registerProbeTools } from "./probe.js";
|
|
14
14
|
export { registerProviderTools } from "./providers.js";
|
|
15
|
+
export { registerPlaybookTools } from "./playbooks.js";
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
type StepLike = {
|
|
2
|
+
id: string;
|
|
3
|
+
agent_slug: string | null;
|
|
4
|
+
default_input?: Record<string, unknown>;
|
|
5
|
+
quantity?: number;
|
|
6
|
+
iteration?: number;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type PlaybookStepInputResult =
|
|
10
|
+
| {
|
|
11
|
+
status: "ready";
|
|
12
|
+
input: Record<string, unknown>;
|
|
13
|
+
certification: "explicit";
|
|
14
|
+
}
|
|
15
|
+
| {
|
|
16
|
+
status: "blocked";
|
|
17
|
+
code: string;
|
|
18
|
+
message: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const BLOCKED_PLAYBOOKS: Record<string, string> = {
|
|
22
|
+
"icp-hunter": "Blocked until the friendly ICP input is mapped to the live prospect-search schema and the prospect agent is verified to honor those filters.",
|
|
23
|
+
"no-web-leads": "Blocked until zip/category inputs are mapped to the live no-web/place/phone schemas and the no-web agent is verified to honor the requested local filters.",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function asString(value: unknown): string | undefined {
|
|
27
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function outputRecord(value: unknown): Record<string, unknown> | undefined {
|
|
31
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
32
|
+
? value as Record<string, unknown>
|
|
33
|
+
: undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function previousOutputs(input: Record<string, unknown>): Array<Record<string, unknown>> {
|
|
37
|
+
return Array.isArray(input.previous_outputs)
|
|
38
|
+
? input.previous_outputs.map(outputRecord).filter((item): item is Record<string, unknown> => Boolean(item))
|
|
39
|
+
: [];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function stepOutput(entry: Record<string, unknown> | undefined): unknown {
|
|
43
|
+
return outputRecord(entry)?.output ?? entry;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function extractHtml(value: unknown): string | undefined {
|
|
47
|
+
if (typeof value === "string" && /<html|<!doctype|<body|<section|<main/i.test(value)) return value;
|
|
48
|
+
const record = outputRecord(value);
|
|
49
|
+
if (!record) return undefined;
|
|
50
|
+
const direct = record.html;
|
|
51
|
+
if (typeof direct === "string" && direct.trim()) return direct;
|
|
52
|
+
const output = record.output;
|
|
53
|
+
if (typeof output === "string" && /<html|<!doctype|<body|<section|<main/i.test(output)) return output;
|
|
54
|
+
const nested = outputRecord(output);
|
|
55
|
+
return typeof nested?.html === "string" && nested.html.trim() ? nested.html : undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function launchLandingInput(baseInput: Record<string, unknown>, step: StepLike): PlaybookStepInputResult {
|
|
59
|
+
const brief = asString(baseInput.brief) ?? asString(baseInput.context) ?? asString(baseInput.product);
|
|
60
|
+
if (!brief) {
|
|
61
|
+
return {
|
|
62
|
+
status: "blocked",
|
|
63
|
+
code: "PLAYBOOK_INPUT_MISSING_BRIEF",
|
|
64
|
+
message: "launch-landing requires a brief before any child agent is called.",
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (step.agent_slug === "marketing-copy-council") {
|
|
69
|
+
return {
|
|
70
|
+
status: "ready",
|
|
71
|
+
certification: "explicit",
|
|
72
|
+
input: {
|
|
73
|
+
brief,
|
|
74
|
+
context: {
|
|
75
|
+
product_slug: asString(baseInput.product_slug),
|
|
76
|
+
},
|
|
77
|
+
num_outputs: 1,
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const outputs = previousOutputs(baseInput);
|
|
83
|
+
if (step.agent_slug === "write-landing-page-copy") {
|
|
84
|
+
const council = outputs.find((item) => item.step === "council" || item.step === "marketing-copy-council") ?? outputs.at(-1);
|
|
85
|
+
const product = asString(baseInput.product)
|
|
86
|
+
?? asString(baseInput.product_name)
|
|
87
|
+
?? asString(baseInput.product_slug)
|
|
88
|
+
?? "Landing page";
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
status: "ready",
|
|
92
|
+
certification: "explicit",
|
|
93
|
+
input: {
|
|
94
|
+
product,
|
|
95
|
+
audience: asString(baseInput.audience) ?? "builders and operators",
|
|
96
|
+
tone: asString(baseInput.tone) ?? "confident and practical",
|
|
97
|
+
context: JSON.stringify({
|
|
98
|
+
brief,
|
|
99
|
+
product_slug: asString(baseInput.product_slug),
|
|
100
|
+
council_output: stepOutput(council),
|
|
101
|
+
}),
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (step.agent_slug === "publish-html-to-a-public-url") {
|
|
107
|
+
const writer = outputs.find((item) => item.step === "structure" || item.step === "write-landing-page-copy") ?? outputs.at(-1);
|
|
108
|
+
const html = extractHtml(stepOutput(writer)) ?? extractHtml(writer);
|
|
109
|
+
if (!html) {
|
|
110
|
+
return {
|
|
111
|
+
status: "blocked",
|
|
112
|
+
code: "PLAYBOOK_INPUT_MISSING_HTML",
|
|
113
|
+
message: "launch-landing publish step requires generated HTML from the writer step.",
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
status: "ready",
|
|
118
|
+
certification: "explicit",
|
|
119
|
+
input: {
|
|
120
|
+
html,
|
|
121
|
+
slug: asString(baseInput.product_slug) ?? asString(baseInput.slug),
|
|
122
|
+
title: asString(baseInput.title) ?? asString(baseInput.product) ?? asString(baseInput.product_slug),
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
status: "blocked",
|
|
129
|
+
code: "PLAYBOOK_STEP_NOT_CERTIFIED",
|
|
130
|
+
message: `launch-landing step ${step.id} (${step.agent_slug ?? "unknown"}) has no explicit adapter.`,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function ipoBriefInput(baseInput: Record<string, unknown>, step: StepLike): PlaybookStepInputResult {
|
|
135
|
+
const ticker = asString(baseInput.ticker) ?? asString(baseInput.symbol);
|
|
136
|
+
if (!ticker) {
|
|
137
|
+
return {
|
|
138
|
+
status: "blocked",
|
|
139
|
+
code: "PLAYBOOK_INPUT_MISSING_TICKER",
|
|
140
|
+
message: "ipo-brief requires ticker before any child agent is called.",
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (step.agent_slug === "ipo-s-1-analysis") {
|
|
145
|
+
return {
|
|
146
|
+
status: "ready",
|
|
147
|
+
certification: "explicit",
|
|
148
|
+
input: { ticker },
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
if (step.agent_slug === "insider-trading-tracker") {
|
|
152
|
+
return {
|
|
153
|
+
status: "ready",
|
|
154
|
+
certification: "explicit",
|
|
155
|
+
input: {
|
|
156
|
+
ticker,
|
|
157
|
+
days_back: Number(baseInput.days_back ?? 365),
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
if (step.agent_slug === "stock-comparison") {
|
|
162
|
+
const peers = Array.isArray(baseInput.peer_tickers) ? baseInput.peer_tickers : [];
|
|
163
|
+
const tickers = [ticker, ...peers].filter((item): item is string => typeof item === "string" && Boolean(item.trim())).slice(0, 5);
|
|
164
|
+
return {
|
|
165
|
+
status: "ready",
|
|
166
|
+
certification: "explicit",
|
|
167
|
+
input: { tickers },
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
status: "blocked",
|
|
173
|
+
code: "PLAYBOOK_STEP_NOT_CERTIFIED",
|
|
174
|
+
message: `ipo-brief step ${step.id} (${step.agent_slug ?? "unknown"}) has no explicit adapter.`,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function playbookRunBlocker(slug: string): string | null {
|
|
179
|
+
return BLOCKED_PLAYBOOKS[slug] ?? null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function buildExplicitPlaybookStepInput(
|
|
183
|
+
playbookSlug: string,
|
|
184
|
+
baseInput: Record<string, unknown>,
|
|
185
|
+
step: StepLike,
|
|
186
|
+
): PlaybookStepInputResult | null {
|
|
187
|
+
const blocker = playbookRunBlocker(playbookSlug);
|
|
188
|
+
if (blocker) {
|
|
189
|
+
return {
|
|
190
|
+
status: "blocked",
|
|
191
|
+
code: "PLAYBOOK_NOT_CERTIFIED",
|
|
192
|
+
message: blocker,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (playbookSlug === "launch-landing") return launchLandingInput(baseInput, step);
|
|
197
|
+
if (playbookSlug === "ipo-brief") return ipoBriefInput(baseInput, step);
|
|
198
|
+
return null;
|
|
199
|
+
}
|
package/src/tools/playbooks.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
formatPaymentLabel,
|
|
16
16
|
resolveConfirmationMethod,
|
|
17
17
|
} from "./_payment-confirmation.js";
|
|
18
|
+
import { buildExplicitPlaybookStepInput, playbookRunBlocker } from "./playbook-adapters.js";
|
|
18
19
|
|
|
19
20
|
const POLL_INTERVAL_MS = 3000;
|
|
20
21
|
const POLL_MAX_MS = 300000;
|
|
@@ -163,14 +164,13 @@ function fanoutControls(playbook: PlaybookRecord): PlaybookFanoutControl[] {
|
|
|
163
164
|
|
|
164
165
|
function buildPlaybookLinkApprovalContext(params: {
|
|
165
166
|
playbook: PlaybookRecord;
|
|
166
|
-
budget: number;
|
|
167
|
-
stepCount: number;
|
|
168
167
|
runId: string;
|
|
168
|
+
step: PlaybookStepQuote;
|
|
169
169
|
}): string {
|
|
170
170
|
return [
|
|
171
|
-
`Approve
|
|
172
|
-
`
|
|
173
|
-
"Each child agent is
|
|
171
|
+
`Approve ${money(params.step.quoted_cost_usd)} for step ${params.step.index + 1} of the Agent Wonderland "${params.playbook.name}" playbook.`,
|
|
172
|
+
`Run ${params.runId}, step ${params.step.id}: ${params.step.agent_name}.`,
|
|
173
|
+
"Each child agent is approved separately, and failed child runs keep the existing Agent Wonderland refund behavior.",
|
|
174
174
|
].join(" ");
|
|
175
175
|
}
|
|
176
176
|
|
|
@@ -374,7 +374,15 @@ async function formatCheaperAlternatives(currentSlug: string, budget: number): P
|
|
|
374
374
|
}
|
|
375
375
|
}
|
|
376
376
|
|
|
377
|
-
function stepInput(baseInput: Record<string, unknown>, step: PlaybookStepQuote, iteration: number): Record<string, unknown> {
|
|
377
|
+
function stepInput(playbookSlug: string, baseInput: Record<string, unknown>, step: PlaybookStepQuote, iteration: number): Record<string, unknown> {
|
|
378
|
+
const explicit = buildExplicitPlaybookStepInput(playbookSlug, baseInput, step);
|
|
379
|
+
if (explicit?.status === "ready") return explicit.input;
|
|
380
|
+
if (explicit?.status === "blocked") {
|
|
381
|
+
const error = new Error(explicit.message) as Error & { code?: string };
|
|
382
|
+
error.code = explicit.code;
|
|
383
|
+
throw error;
|
|
384
|
+
}
|
|
385
|
+
|
|
378
386
|
const input: Record<string, unknown> = { ...(step.default_input ?? {}), ...baseInput };
|
|
379
387
|
const itemIndex = Math.max(0, (step.iteration ?? 1) - 1);
|
|
380
388
|
|
|
@@ -778,6 +786,10 @@ export function registerPlaybookTools(server: McpServer): void {
|
|
|
778
786
|
if (playbook.current_quote.blocking_issues.length > 0) {
|
|
779
787
|
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
788
|
}
|
|
789
|
+
const blocker = playbookRunBlocker(playbook.slug);
|
|
790
|
+
if (blocker) {
|
|
791
|
+
return text(`Playbook ${playbook.slug} is temporarily paused for certification.\n\n${blocker}`);
|
|
792
|
+
}
|
|
781
793
|
|
|
782
794
|
const runLimits = appliedPlaybookLimits(playbook) ?? effectiveLimits;
|
|
783
795
|
const estimatedCost = playbook.current_quote.estimated_cost_usd;
|
|
@@ -880,14 +892,7 @@ export function registerPlaybookTools(server: McpServer): void {
|
|
|
880
892
|
|
|
881
893
|
let charged = runState.chargedUsd;
|
|
882
894
|
const childOutputs: Array<{ step: string; job_id?: string; output?: unknown }> = [...runState.existingOutputs];
|
|
883
|
-
let linkSpendRequestId: string | undefined;
|
|
884
895
|
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
896
|
|
|
892
897
|
for (const [iteration, step] of runState.steps.entries()) {
|
|
893
898
|
if (runState.completedStepIds.has(step.id)) {
|
|
@@ -927,7 +932,27 @@ export function registerPlaybookTools(server: McpServer): void {
|
|
|
927
932
|
quoted_cost_usd: step.quoted_cost_usd,
|
|
928
933
|
});
|
|
929
934
|
|
|
930
|
-
|
|
935
|
+
let payload: Record<string, unknown>;
|
|
936
|
+
try {
|
|
937
|
+
payload = stepInput(playbook.slug, { ...processedInput, previous_outputs: childOutputs }, step, iteration);
|
|
938
|
+
} catch (err) {
|
|
939
|
+
const blocked = err as Error & { code?: string };
|
|
940
|
+
await recordStep(runState.run_id, step.id, {
|
|
941
|
+
status: "skipped",
|
|
942
|
+
agent_id: step.agent_id,
|
|
943
|
+
provider_id: step.provider_id,
|
|
944
|
+
consumption_mode: "not_charged",
|
|
945
|
+
error_code: blocked.code ?? "PLAYBOOK_STEP_INPUT_BLOCKED",
|
|
946
|
+
failure_message: blocked.message,
|
|
947
|
+
});
|
|
948
|
+
await updateRun(runState.run_id, {
|
|
949
|
+
status: "paused",
|
|
950
|
+
error_code: blocked.code ?? "PLAYBOOK_STEP_INPUT_BLOCKED",
|
|
951
|
+
failure_message: blocked.message,
|
|
952
|
+
});
|
|
953
|
+
const receipt = await apiGet<PlaybookRunReceipt>(`/playbook-runs/${runState.run_id}`);
|
|
954
|
+
return text(`${formatReceipt(receipt)}\n\n${blocked.message}`);
|
|
955
|
+
}
|
|
931
956
|
const playbookContext = {
|
|
932
957
|
playbook_run_id: runState.run_id,
|
|
933
958
|
playbook_id: playbook.id,
|
|
@@ -938,6 +963,7 @@ export function registerPlaybookTools(server: McpServer): void {
|
|
|
938
963
|
let result: Record<string, unknown> | undefined;
|
|
939
964
|
let schemaError: { status?: number; message?: string } | undefined;
|
|
940
965
|
let usedPaidMethod = false;
|
|
966
|
+
let stepLinkSpendRequestId: string | undefined;
|
|
941
967
|
const executeChild = async (childInput: Record<string, unknown>) => {
|
|
942
968
|
const body = { input: childInput, playbook_context: playbookContext };
|
|
943
969
|
try {
|
|
@@ -960,10 +986,14 @@ export function registerPlaybookTools(server: McpServer): void {
|
|
|
960
986
|
throw new Error("Link payment challenge did not include a WWW-Authenticate header.");
|
|
961
987
|
}
|
|
962
988
|
linkNetworkId = linkNetworkId ?? await decodeMppNetworkId(challenge!);
|
|
963
|
-
|
|
964
|
-
amount: cents(
|
|
989
|
+
stepLinkSpendRequestId = await ensureApprovedLinkSpendRequest({
|
|
990
|
+
amount: cents(step.quoted_cost_usd),
|
|
965
991
|
currency: "usd",
|
|
966
|
-
context:
|
|
992
|
+
context: buildPlaybookLinkApprovalContext({
|
|
993
|
+
playbook,
|
|
994
|
+
runId: runState.run_id,
|
|
995
|
+
step,
|
|
996
|
+
}),
|
|
967
997
|
expiresAt: Math.floor(Date.now() / 1000) + 3600,
|
|
968
998
|
networkId: linkNetworkId,
|
|
969
999
|
paymentMethodId: linkConfig.paymentMethodId,
|
|
@@ -971,7 +1001,7 @@ export function registerPlaybookTools(server: McpServer): void {
|
|
|
971
1001
|
return await apiPostWithApprovedLinkSpendRequest<Record<string, unknown>>(
|
|
972
1002
|
`/agents/${step.agent_id}/run`,
|
|
973
1003
|
body,
|
|
974
|
-
|
|
1004
|
+
stepLinkSpendRequestId,
|
|
975
1005
|
);
|
|
976
1006
|
}
|
|
977
1007
|
return await apiPostWithPayment<Record<string, unknown>>(
|
|
@@ -1023,6 +1053,9 @@ export function registerPlaybookTools(server: McpServer): void {
|
|
|
1023
1053
|
}
|
|
1024
1054
|
} else {
|
|
1025
1055
|
const errorCode = apiErr?.status === 402 ? "PAYMENT_REJECTED" : "CHILD_AGENT_FAILED";
|
|
1056
|
+
if ((method ?? spendMethod) === "link" && stepLinkSpendRequestId) {
|
|
1057
|
+
setPendingLinkSpendRequest(null);
|
|
1058
|
+
}
|
|
1026
1059
|
await recordStep(runState.run_id, step.id, {
|
|
1027
1060
|
status: apiErr?.status === 402 ? "skipped" : "failed",
|
|
1028
1061
|
agent_id: step.agent_id,
|
|
@@ -1044,6 +1077,10 @@ export function registerPlaybookTools(server: McpServer): void {
|
|
|
1044
1077
|
}
|
|
1045
1078
|
}
|
|
1046
1079
|
|
|
1080
|
+
if (result && (method ?? spendMethod) === "link" && stepLinkSpendRequestId) {
|
|
1081
|
+
setPendingLinkSpendRequest(null);
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1047
1084
|
if (!result) {
|
|
1048
1085
|
await recordStep(runState.run_id, step.id, {
|
|
1049
1086
|
status: "skipped",
|
|
@@ -1172,9 +1209,6 @@ export function registerPlaybookTools(server: McpServer): void {
|
|
|
1172
1209
|
});
|
|
1173
1210
|
const receipt = await apiGet<PlaybookRunReceipt>(`/playbook-runs/${runState.run_id}`);
|
|
1174
1211
|
pendingPlaybookRuns.delete(playbook.slug);
|
|
1175
|
-
if ((method ?? spendMethod) === "link" && linkSpendRequestId) {
|
|
1176
|
-
setPendingLinkSpendRequest(null);
|
|
1177
|
-
}
|
|
1178
1212
|
|
|
1179
1213
|
const runBlocks = childOutputs.length > 0
|
|
1180
1214
|
? childOutputs.map((item) => item.output ? formatRunResult({ status: "success", job_id: item.job_id, output: item.output }, { paymentMethod: method }) : "").filter(Boolean)
|