Skip to content

Commit f06c49d

Browse files
kapaleshreyasclaude
andcommitted
examples: full marketing-agent demo with streaming + multi-turn + file output
Runs github.com/shreyas-lyzr/marketing-agent (32-skill GAP agent) via ComputerAgent with LocalSubstrate. Demonstrates: - Multi-turn conversation (product context → cold email → pricing → launch) - Real-time streaming of assistant text and tool calls - File-backed SessionStore for cross-process resume - Harness FS API to fetch agent-produced files - `await using` for clean dispose Usage: ANTHROPIC_API_KEY=sk-... bun run examples/marketing-agent.ts ANTHROPIC_API_KEY=sk-... bun run examples/marketing-agent.ts resume Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent a355bbf commit f06c49d

3 files changed

Lines changed: 285 additions & 1 deletion

File tree

examples/marketing-agent.ts

Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
/**
2+
* Full-fledged marketing agent example.
3+
*
4+
* Runs the marketing-agent GAP repo (github.com/shreyas-lyzr/marketing-agent)
5+
* as a multi-turn session: first turn sets product context, subsequent turns
6+
* request specific marketing deliverables. Each response streams in real-time
7+
* and the final outputs are saved to markdown files under ./marketing-outputs/.
8+
*
9+
* Usage:
10+
* ANTHROPIC_API_KEY=sk-ant-... bun run examples/marketing-agent.ts
11+
*
12+
* On subsequent runs, the session is resumed from disk so the agent remembers
13+
* the product context set in the first turn.
14+
*
15+
* ANTHROPIC_API_KEY=sk-ant-... bun run examples/marketing-agent.ts resume
16+
*
17+
* What this demonstrates:
18+
* - Running a real public GAP repo via ComputerAgent
19+
* - Streaming every sdk_message and tool call as they arrive
20+
* - Multi-turn conversation with a file-backed SessionStore
21+
* - Fetching agent-produced files via the Harness FS API
22+
* - clean dispose() via `await using`
23+
*/
24+
25+
import { mkdir, writeFile, readFile } from "node:fs/promises";
26+
import { existsSync } from "node:fs";
27+
import { join } from "node:path";
28+
import { ComputerAgent, LocalSubstrate } from "computeragent";
29+
30+
const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
31+
if (!ANTHROPIC_API_KEY) {
32+
console.error("Error: ANTHROPIC_API_KEY is not set.");
33+
console.error(" export ANTHROPIC_API_KEY=sk-ant-...");
34+
process.exit(1);
35+
}
36+
37+
const OUTPUTS_DIR = join(import.meta.dir ?? __dirname, "marketing-outputs");
38+
const SESSION_FILE = join(OUTPUTS_DIR, ".session-id");
39+
const RESUME = process.argv[2] === "resume";
40+
41+
// ── Helpers ──────────────────────────────────────────────────────────────────
42+
43+
function hr(label: string): void {
44+
const line = "─".repeat(60);
45+
console.log(`\n${line}`);
46+
console.log(` ${label}`);
47+
console.log(`${line}`);
48+
}
49+
50+
function dim(s: string): string {
51+
return `\x1b[2m${s}\x1b[0m`;
52+
}
53+
54+
function green(s: string): string {
55+
return `\x1b[32m${s}\x1b[0m`;
56+
}
57+
58+
function yellow(s: string): string {
59+
return `\x1b[33m${s}\x1b[0m`;
60+
}
61+
62+
async function streamTurn(
63+
agent: ComputerAgent,
64+
message: string,
65+
): Promise<{ sessionId: string; result: string; harnessUrl: string }> {
66+
console.log(`\n${yellow("You:")} ${message}\n`);
67+
68+
let result = "";
69+
let sessionId = "";
70+
71+
const handle = agent.chat(message);
72+
73+
for await (const ev of handle) {
74+
if (ev.kind === "ca_session_started") {
75+
sessionId = ev.sessionId;
76+
console.log(dim(`[session ${sessionId} • engine=${ev.engine}]`));
77+
} else if (ev.kind === "sdk_message") {
78+
const p = ev.payload as Record<string, unknown>;
79+
80+
if (p.type === "assistant") {
81+
const msg = p.message as { content?: unknown[] } | undefined;
82+
for (const block of msg?.content ?? []) {
83+
const b = block as { type?: string; text?: string; name?: string; input?: unknown };
84+
if (b.type === "text" && b.text) {
85+
// Stream text progressively — print in chunks
86+
process.stdout.write(b.text);
87+
} else if (b.type === "tool_use") {
88+
const input = JSON.stringify(b.input ?? {});
89+
console.log(`\n${dim(` → ${b.name}(${input.slice(0, 120)}${input.length > 120 ? "…" : ""})`)}`);
90+
}
91+
}
92+
} else if (p.type === "tool") {
93+
// tool result
94+
const content = typeof p.content === "string" ? p.content : JSON.stringify(p.content);
95+
console.log(dim(` ← ${content.slice(0, 200)}${content.length > 200 ? "…" : ""}`));
96+
} else if (p.type === "result" && typeof p.result === "string") {
97+
result = p.result;
98+
}
99+
} else if (ev.kind === "ca_usage_snapshot") {
100+
const u = ev as unknown as { input_tokens?: number; output_tokens?: number; cost_usd?: number };
101+
if (u.cost_usd !== undefined) {
102+
console.log(`\n${dim(`[usage: in=${u.input_tokens} out=${u.output_tokens} cost=$${u.cost_usd?.toFixed(4)}]`)}`);
103+
}
104+
} else if (ev.kind === "ca_session_ended") {
105+
console.log(`\n${dim(`[ended: ${ev.reason}]`)}`);
106+
}
107+
}
108+
109+
const harnessUrl = await agent.harnessUrl();
110+
return { sessionId, result, harnessUrl };
111+
}
112+
113+
async function saveOutput(filename: string, content: string): Promise<void> {
114+
const path = join(OUTPUTS_DIR, filename);
115+
await writeFile(path, content, "utf8");
116+
console.log(`\n${green("✓")} Saved → ${path}`);
117+
}
118+
119+
async function fetchAgentFile(harnessUrl: string, sessionId: string, path: string): Promise<string | null> {
120+
try {
121+
const res = await fetch(`${harnessUrl}/v1/sessions/${sessionId}/fs/file?path=${encodeURIComponent(path)}`);
122+
if (!res.ok) return null;
123+
return await res.text();
124+
} catch {
125+
return null;
126+
}
127+
}
128+
129+
// ── Main ─────────────────────────────────────────────────────────────────────
130+
131+
await mkdir(OUTPUTS_DIR, { recursive: true });
132+
133+
let priorSessionId: string | undefined;
134+
if (RESUME && existsSync(SESSION_FILE)) {
135+
priorSessionId = (await readFile(SESSION_FILE, "utf8")).trim();
136+
console.log(`\nResuming session: ${priorSessionId}`);
137+
}
138+
139+
// Boot the marketing agent from the public GitHub repo.
140+
// LocalSubstrate spawns the harness in a subprocess — no Docker required.
141+
await using agent = new ComputerAgent({
142+
source: {
143+
type: "git",
144+
url: "github.com/shreyas-lyzr/marketing-agent",
145+
},
146+
harness: "claude-agent-sdk",
147+
runtime: new LocalSubstrate(),
148+
envs: { ANTHROPIC_API_KEY },
149+
options: {
150+
permissionMode: "bypassPermissions",
151+
settingSources: ["project"],
152+
},
153+
sessionStore: {
154+
kind: "file",
155+
options: { root: OUTPUTS_DIR },
156+
},
157+
...(priorSessionId ? { sessionId: priorSessionId } : {}),
158+
});
159+
160+
// ── Turn 1: Product context ───────────────────────────────────────────────────
161+
162+
if (!RESUME) {
163+
hr("Turn 1 — Product Context");
164+
165+
const { sessionId } = await streamTurn(
166+
agent,
167+
`Here is our product context. Please confirm you've loaded it and identify the top 3
168+
marketing challenges you'd recommend we tackle first.
169+
170+
Product: Lyzr AI — an enterprise platform for building, deploying, and governing AI agents at scale.
171+
Audience: Mid-market and enterprise engineering and IT leaders (CTOs, VP Engineering, Head of AI).
172+
Stage: Series A, $8M ARR, 120 customers, growing 15% MoM.
173+
Key differentiators: On-prem / VPC deployment, SOC2 compliant, LLM-agnostic (OpenAI, Anthropic, Gemini, Mistral).
174+
Main competitors: LangChain, CrewAI, Vertex AI Agent Builder.
175+
Current channels: mostly outbound sales, some inbound content, limited PLG motion.
176+
Top conversion barrier: enterprises want a PoC before committing — long evaluation cycles (60-90 days).`,
177+
);
178+
179+
await writeFile(SESSION_FILE, sessionId, "utf8");
180+
console.log(`\nSession ID saved → ${SESSION_FILE}`);
181+
}
182+
183+
// ── Turn 2: Cold email sequence ───────────────────────────────────────────────
184+
185+
hr("Turn 2 — Cold Email Sequence");
186+
187+
const { result: emailResult } = await streamTurn(
188+
agent,
189+
`Write a 4-email cold outreach sequence targeting VP Engineering at Series B+ SaaS companies
190+
(500–2000 employees). The goal of the sequence is to book a 30-minute PoC scoping call.
191+
Angle: they are likely already using LangChain or CrewAI and hitting reliability or governance issues at scale.
192+
Format each email with: Subject line, Body, Send timing.`,
193+
);
194+
195+
await saveOutput(
196+
"cold-email-sequence.md",
197+
`# Cold Email Sequence — VP Engineering @ Series B+ SaaS\n\nGenerated by marketing-agent via ComputerAgent\n\n---\n\n${emailResult}`,
198+
);
199+
200+
// ── Turn 3: Pricing strategy ──────────────────────────────────────────────────
201+
202+
hr("Turn 3 — Pricing Strategy");
203+
204+
const { result: pricingResult } = await streamTurn(
205+
agent,
206+
`Design a 3-tier pricing structure for Lyzr AI. We currently charge flat enterprise contracts
207+
($3k–$15k/month). We want to add a self-serve tier to capture PLG motion and reduce the
208+
60-90 day sales cycle for smaller deals. Include:
209+
- Tier names and positioning
210+
- Pricing metric recommendation (per-seat vs. per-agent vs. usage-based)
211+
- Feature gates between tiers
212+
- Recommended price points with reasoning
213+
- A/B test ideas for the pricing page`,
214+
);
215+
216+
await saveOutput(
217+
"pricing-strategy.md",
218+
`# Pricing Strategy — Lyzr AI\n\nGenerated by marketing-agent via ComputerAgent\n\n---\n\n${pricingResult}`,
219+
);
220+
221+
// ── Turn 4: Launch strategy for self-serve tier ───────────────────────────────
222+
223+
hr("Turn 4 — Self-Serve Launch Strategy");
224+
225+
const { result: launchResult, sessionId: finalSession, harnessUrl } = await streamTurn(
226+
agent,
227+
`Now create a 30-day launch plan for the new self-serve tier.
228+
We want 500 signups in the first 30 days. Channels available: Product Hunt, HN Show,
229+
our existing email list (4,200 subscribers), LinkedIn (8k followers), and developer communities.
230+
Include: Pre-launch checklist, launch day playbook, week-by-week activation plan,
231+
success metrics, and the top 3 risks with mitigations.`,
232+
);
233+
234+
await saveOutput(
235+
"launch-strategy.md",
236+
`# Self-Serve Launch Strategy — 30-Day Plan\n\nGenerated by marketing-agent via ComputerAgent\n\n---\n\n${launchResult}`,
237+
);
238+
239+
// ── Bonus: check if agent wrote any files to its workdir ─────────────────────
240+
241+
hr("Harness Filesystem");
242+
243+
try {
244+
const treeRes = await fetch(
245+
`${harnessUrl}/v1/sessions/${finalSession}/fs/tree?depth=2`,
246+
);
247+
if (treeRes.ok) {
248+
const tree = (await treeRes.json()) as { entries: { path: string; type: string; size: number }[] };
249+
const interesting = tree.entries.filter((e) => e.path !== "/" && !e.path.startsWith("/."));
250+
if (interesting.length > 0) {
251+
console.log("\nFiles the agent wrote to its workspace:");
252+
for (const e of interesting) {
253+
console.log(` ${e.type.padEnd(4)} ${e.path.padEnd(50)} ${e.size}b`);
254+
if (e.type === "file") {
255+
const content = await fetchAgentFile(harnessUrl, finalSession, e.path);
256+
if (content) {
257+
const outName = e.path.replace(/^\//, "").replace(/\//g, "-");
258+
await saveOutput(`agent-workdir-${outName}`, content);
259+
}
260+
}
261+
}
262+
} else {
263+
console.log(dim(" (agent workdir is empty — all output was inline text)"));
264+
}
265+
}
266+
} catch {
267+
// FS API optional — harness may not expose it in all configurations
268+
}
269+
270+
// ── Summary ───────────────────────────────────────────────────────────────────
271+
272+
hr("Done");
273+
console.log(`\nAll outputs saved to: ${OUTPUTS_DIR}/`);
274+
console.log(" cold-email-sequence.md — 4-email VP Engineering outreach sequence");
275+
console.log(" pricing-strategy.md — 3-tier SaaS pricing with feature gates");
276+
console.log(" launch-strategy.md — 30-day self-serve launch plan");
277+
console.log(`\nSession ID: ${finalSession}`);
278+
console.log("Run with 'resume' to continue this session in a fresh process:");
279+
console.log(` ANTHROPIC_API_KEY=sk-... bun run examples/marketing-agent.ts resume\n`);

examples/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44
"private": true,
55
"type": "module",
66
"scripts": {
7-
"wedge1": "bun run wedge1-server.ts"
7+
"wedge1": "bun run wedge1-server.ts",
8+
"marketing-agent": "bun run marketing-agent.ts"
89
},
910
"dependencies": {
11+
"computeragent": "workspace:*",
1012
"@computeragent/engine-claude-agent-sdk": "workspace:*",
1113
"@computeragent/engine-gitagent": "workspace:*",
1214
"@computeragent/harness-server": "workspace:*",

pnpm-lock.yaml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)