Repository Public Research preview
1import { CURRENT, resolveModel } from "./knowledge.mjs";23/**4 * Starter code for the main ways to build with the Claude API. Every template is executed against5 * the live API by test/live.test.mjs (TypeScript) and test/scaffold-python.sh (Python), so what6 * this module emits is known to run. Templates adapt to the model: effort, thinking and refusal7 * handling are only emitted where the model supports them.8 */9export const APPROACHES = {10 single_call: "One request, one response (classification, extraction, Q&A).",11 streaming: "Token streaming for chat UIs and long outputs.",12 structured_output: "JSON that is guaranteed to match a schema.",13 manual_loop: "You own the tool loop: maximum control, no beta dependency.",14 tool_runner: "SDK-driven tool loop over tools you define (beta helper).",15 batch: "Asynchronous bulk processing at half price.",16 managed_agent: "Anthropic runs the loop and a per-session sandbox (beta).",17};18export const LANGUAGES = ["typescript", "python"];1920function profile(modelId, effort) {21 const m = resolveModel(modelId) ?? resolveModel(CURRENT.opus);22 const e = m.effort.supported ? (m.effort.levels.includes(effort) ? effort : m.effort.levels.includes("medium") ? "medium" : m.effort.levels[0]) : null;23 return { id: m.id, effort: e, adaptive: m.thinking.adaptive, refusal: ["fable", "opus", "sonnet"].includes(m.tier) && m.thinking.disabled !== "accepted" || m.id === "claude-opus-5" };24}2526const TS = {27 single_call: (p) => `import Anthropic from "@anthropic-ai/sdk";2829const client = new Anthropic(); // reads ANTHROPIC_API_KEY3031const message = await client.messages.create({32 model: "${p.id}",33 max_tokens: 16000,${p.effort ? `\n output_config: { effort: "${p.effort}" },` : ""}34 system: "You are a concise assistant.",35 messages: [{ role: "user", content: process.argv[2] ?? "Say hello in one short sentence." }],36});3738// Always check stop_reason before trusting the content.39if (message.stop_reason === "refusal") throw new Error(\`declined: \${message.stop_details?.category ?? "unspecified"}\`);40if (message.stop_reason === "max_tokens") console.error("warning: output was truncated at max_tokens");41for (const block of message.content) if (block.type === "text") console.log(block.text);42console.error(\`tokens: \${message.usage.input_tokens} in / \${message.usage.output_tokens} out\`);43`,44 streaming: (p) => `import Anthropic from "@anthropic-ai/sdk";4546const client = new Anthropic();4748const stream = client.messages.stream({49 model: "${p.id}",50 max_tokens: 64000, // streaming removes the HTTP-timeout reason to keep this low${p.effort ? `\n output_config: { effort: "${p.effort}" },` : ""}51 messages: [{ role: "user", content: process.argv[2] ?? "Count from one to five, one word per line." }],52});53stream.on("text", (delta) => process.stdout.write(delta));5455const message = await stream.finalMessage(); // full Message once the stream ends56process.stdout.write("\\n");57if (message.stop_reason !== "end_turn") console.error(\`stop_reason: \${message.stop_reason}\`);58`,59 structured_output: (p) => `import Anthropic from "@anthropic-ai/sdk";6061const client = new Anthropic();6263// Object schemas must set additionalProperties: false and list every property in required.64const schema = {65 type: "object",66 properties: {67 sentiment: { type: "string", enum: ["positive", "neutral", "negative"] },68 confidence: { type: "number" },69 reasons: { type: "array", items: { type: "string" } },70 },71 required: ["sentiment", "confidence", "reasons"],72 additionalProperties: false,73} as const;7475const message = await client.messages.create({76 model: "${p.id}",77 max_tokens: 4000,${p.effort ? `\n output_config: { effort: "${p.effort}", format: { type: "json_schema", schema } },` : `\n output_config: { format: { type: "json_schema", schema } },`}78 messages: [{ role: "user", content: \`Classify the sentiment of: \${process.argv[2] ?? "The install was painless and the docs are clear."}\` }],79});8081if (message.stop_reason !== "end_turn") throw new Error(\`no complete JSON: stop_reason \${message.stop_reason}\`);82const text = message.content.find((b) => b.type === "text");83if (!text || text.type !== "text") throw new Error("no text block");84const result = JSON.parse(text.text) as { sentiment: string; confidence: number; reasons: string[] };85console.log(result);86`,87 manual_loop: (p) => `import Anthropic from "@anthropic-ai/sdk";8889const client = new Anthropic();9091const tools: Anthropic.Tool[] = [92 {93 name: "get_weather",94 description: "Get the current weather for a city. Use it whenever the user asks about weather. Returns a short text summary.",95 strict: true,96 input_schema: {97 type: "object",98 properties: { city: { type: "string", description: "City name, e.g. Montréal" } },99 required: ["city"],100 additionalProperties: false,101 },102 },103];104105// Your tool implementations. Validate inputs: they are model output, not trusted data.106async function runTool(name: string, input: unknown): Promise<string> {107 if (name === "get_weather") {108 const { city } = input as { city: string };109 return \`Sunny, 21°C in \${city}\`;110 }111 throw new Error(\`unknown tool: \${name}\`);112}113114const messages: Anthropic.MessageParam[] = [{ role: "user", content: process.argv[2] ?? "What is the weather in Montréal and in Québec City?" }];115116for (let turn = 0; turn < 10; turn++) { // hard cap: never loop unbounded117 const response = await client.messages.create({118 model: "${p.id}",119 max_tokens: 16000,${p.effort ? `\n output_config: { effort: "${p.effort}" },` : ""}120 tools,121 messages,122 });123 // Append the FULL content (thinking, tool_use, …), not just the text.124 messages.push({ role: "assistant", content: response.content });125126 if (response.stop_reason === "pause_turn") continue; // a server tool paused: resend to resume127 if (response.stop_reason !== "tool_use") {128 if (response.stop_reason !== "end_turn") console.error(\`stop_reason: \${response.stop_reason}\`);129 for (const b of response.content) if (b.type === "text") console.log(b.text);130 break;131 }132133 // Run every requested tool (in parallel) and return ALL results in ONE user message.134 const calls = response.content.filter((b): b is Anthropic.ToolUseBlock => b.type === "tool_use");135 const results: Anthropic.ToolResultBlockParam[] = await Promise.all(136 calls.map(async (call) => {137 try {138 return { type: "tool_result" as const, tool_use_id: call.id, content: await runTool(call.name, call.input) };139 } catch (err) {140 return { type: "tool_result" as const, tool_use_id: call.id, content: String(err), is_error: true };141 }142 }),143 );144 messages.push({ role: "user", content: results });145}146`,147 tool_runner: (p) => `import Anthropic from "@anthropic-ai/sdk";148import { betaZodTool } from "@anthropic-ai/sdk/helpers/beta/zod";149import { z } from "zod";150151const client = new Anthropic();152153const getWeather = betaZodTool({154 name: "get_weather",155 description: "Get the current weather for a city. Use it whenever the user asks about weather.",156 inputSchema: z.object({ city: z.string().describe("City name, e.g. Montréal") }),157 run: async ({ city }) => \`Sunny, 21°C in \${city}\`, // input is validated against the schema first158});159160// The runner calls the API, executes tools and loops until Claude stops asking for them.161const finalMessage = await client.beta.messages.toolRunner({162 model: "${p.id}",163 max_tokens: 16000,${p.effort ? `\n output_config: { effort: "${p.effort}" },` : ""}164 max_iterations: 10,165 tools: [getWeather],166 messages: [{ role: "user", content: process.argv[2] ?? "What is the weather in Montréal and in Québec City?" }],167});168169if (finalMessage.stop_reason !== "end_turn") console.error(\`stop_reason: \${finalMessage.stop_reason}\`);170for (const b of finalMessage.content) if (b.type === "text") console.log(b.text);171`,172 batch: (p) => `import Anthropic from "@anthropic-ai/sdk";173174const client = new Anthropic();175const inputs = ["Summarise: the cat sat on the mat.", "Summarise: rain is expected tomorrow."];176177// 50% cheaper, asynchronous (usually under an hour, always under 24 h). Not for interactive use.178const batch = await client.messages.batches.create({179 requests: inputs.map((content, i) => ({180 custom_id: \`item-\${i}\`, // your key: results come back in ANY order181 params: { model: "${p.id}", max_tokens: 1024, messages: [{ role: "user", content }] },182 })),183});184console.error(\`batch \${batch.id}: \${batch.processing_status}\`);185186let current = batch;187while (current.processing_status !== "ended") {188 await new Promise((r) => setTimeout(r, 15_000));189 current = await client.messages.batches.retrieve(batch.id);190}191192for await (const entry of await client.messages.batches.results(batch.id)) {193 if (entry.result.type === "succeeded") {194 const text = entry.result.message.content.find((b) => b.type === "text");195 console.log(entry.custom_id, "→", text?.type === "text" ? text.text : "");196 } else {197 console.error(entry.custom_id, "→", entry.result.type); // errored | canceled | expired198 }199}200`,201 managed_agent: (p) => `import Anthropic from "@anthropic-ai/sdk";202203// Managed Agents (beta): Anthropic runs the agent loop and a per-session sandbox.204// Agents and environments are persistent: create them ONCE (setup), store the ids, reuse them.205const client = new Anthropic();206207async function setup() {208 const environment = await client.beta.environments.create({209 name: "starter-env",210 config: { type: "cloud", networking: { type: "limited" } },211 });212 const agent = await client.beta.agents.create({213 name: "Starter agent",214 model: "${p.id}",215 system: "You are a careful assistant working in a sandbox. Be brief.",216 tools: [217 {218 type: "agent_toolset_20260401",219 default_config: { enabled: true, permission_policy: { type: "auto" } },220 configs: [{ name: "web_fetch", enabled: false }, { name: "web_search", enabled: false }],221 },222 ],223 });224 return { agentId: agent.id, agentVersion: agent.version, environmentId: environment.id };225}226227// In production read these from configuration instead of calling setup() on every run.228const ids = process.env.AGENT_ID && process.env.ENVIRONMENT_ID229 ? { agentId: process.env.AGENT_ID, agentVersion: undefined, environmentId: process.env.ENVIRONMENT_ID }230 : await setup();231console.error(\`AGENT_ID=\${ids.agentId} ENVIRONMENT_ID=\${ids.environmentId}\`);232233const session = await client.beta.sessions.create({234 agent: ids.agentVersion ? { type: "agent", id: ids.agentId, version: ids.agentVersion } : ids.agentId,235 environment_id: ids.environmentId,236});237238// Open the stream BEFORE sending, so no early event is missed.239const stream = await client.beta.sessions.events.stream(session.id);240await client.beta.sessions.events.send(session.id, {241 events: [{ type: "user.message", content: [{ type: "text", text: process.argv[2] ?? "Create hello.txt containing 'hi', then print its contents." }] }],242});243244loop: for await (const event of stream) {245 switch (event.type) {246 case "agent.message":247 for (const block of event.content) if (block.type === "text") process.stdout.write(block.text);248 break;249 case "agent.tool_use":250 case "agent.mcp_tool_use":251 if (event.evaluated_permission === "ask") {252 // Nobody is watching this run: deny anything the policy could not decide.253 await client.beta.sessions.events.send(session.id, { events: [{ type: "user.tool_confirmation", tool_use_id: event.id, result: "deny" }] });254 }255 break;256 case "session.status_idle":257 if (event.stop_reason.type !== "requires_action") break loop;258 break;259 case "session.status_terminated":260 break loop;261 }262}263process.stdout.write("\\n");264`,265};266267const PY = {268 single_call: (p) => `import sys269import anthropic270271client = anthropic.Anthropic() # reads ANTHROPIC_API_KEY272273message = client.messages.create(274 model="${p.id}",275 max_tokens=16000,${p.effort ? `\n output_config={"effort": "${p.effort}"},` : ""}276 system="You are a concise assistant.",277 messages=[{"role": "user", "content": sys.argv[1] if len(sys.argv) > 1 else "Say hello in one short sentence."}],278)279280# Always check stop_reason before trusting the content.281if message.stop_reason == "refusal":282 raise SystemExit(f"declined: {getattr(message.stop_details, 'category', None) or 'unspecified'}")283if message.stop_reason == "max_tokens":284 print("warning: output was truncated at max_tokens", file=sys.stderr)285for block in message.content:286 if block.type == "text":287 print(block.text)288print(f"tokens: {message.usage.input_tokens} in / {message.usage.output_tokens} out", file=sys.stderr)289`,290 streaming: (p) => `import sys291import anthropic292293client = anthropic.Anthropic()294295with client.messages.stream(296 model="${p.id}",297 max_tokens=64000, # streaming removes the HTTP-timeout reason to keep this low${p.effort ? `\n output_config={"effort": "${p.effort}"},` : ""}298 messages=[{"role": "user", "content": sys.argv[1] if len(sys.argv) > 1 else "Count from one to five, one word per line."}],299) as stream:300 for text in stream.text_stream:301 print(text, end="", flush=True)302 message = stream.get_final_message() # full Message once the stream ends303304print()305if message.stop_reason != "end_turn":306 print(f"stop_reason: {message.stop_reason}", file=sys.stderr)307`,308 structured_output: (p) => `import json309import sys310import anthropic311312client = anthropic.Anthropic()313314# Object schemas must set additionalProperties: false and list every property in required.315schema = {316 "type": "object",317 "properties": {318 "sentiment": {"type": "string", "enum": ["positive", "neutral", "negative"]},319 "confidence": {"type": "number"},320 "reasons": {"type": "array", "items": {"type": "string"}},321 },322 "required": ["sentiment", "confidence", "reasons"],323 "additionalProperties": False,324}325326text_in = sys.argv[1] if len(sys.argv) > 1 else "The install was painless and the docs are clear."327message = client.messages.create(328 model="${p.id}",329 max_tokens=4000,330 output_config={${p.effort ? `"effort": "${p.effort}", ` : ""}"format": {"type": "json_schema", "schema": schema}},331 messages=[{"role": "user", "content": f"Classify the sentiment of: {text_in}"}],332)333334if message.stop_reason != "end_turn":335 raise SystemExit(f"no complete JSON: stop_reason {message.stop_reason}")336result = json.loads(next(b.text for b in message.content if b.type == "text"))337print(result)338`,339 manual_loop: (p) => `import sys340import anthropic341342client = anthropic.Anthropic()343344tools = [345 {346 "name": "get_weather",347 "description": "Get the current weather for a city. Use it whenever the user asks about weather. Returns a short text summary.",348 "strict": True,349 "input_schema": {350 "type": "object",351 "properties": {"city": {"type": "string", "description": "City name, e.g. Montréal"}},352 "required": ["city"],353 "additionalProperties": False,354 },355 }356]357358359def run_tool(name: str, tool_input: dict) -> str:360 """Your tool implementations. Validate inputs: they are model output, not trusted data."""361 if name == "get_weather":362 return f"Sunny, 21°C in {tool_input['city']}"363 raise ValueError(f"unknown tool: {name}")364365366messages = [{"role": "user", "content": sys.argv[1] if len(sys.argv) > 1 else "What is the weather in Montréal and in Québec City?"}]367368for _ in range(10): # hard cap: never loop unbounded369 response = client.messages.create(370 model="${p.id}",371 max_tokens=16000,${p.effort ? `\n output_config={"effort": "${p.effort}"},` : ""}372 tools=tools,373 messages=messages,374 )375 # Append the FULL content (thinking, tool_use, ...), not just the text.376 messages.append({"role": "assistant", "content": response.content})377378 if response.stop_reason == "pause_turn":379 continue # a server tool paused: resend to resume380 if response.stop_reason != "tool_use":381 if response.stop_reason != "end_turn":382 print(f"stop_reason: {response.stop_reason}", file=sys.stderr)383 for block in response.content:384 if block.type == "text":385 print(block.text)386 break387388 # Run every requested tool and return ALL results in ONE user message.389 results = []390 for block in response.content:391 if block.type != "tool_use":392 continue393 try:394 results.append({"type": "tool_result", "tool_use_id": block.id, "content": run_tool(block.name, block.input)})395 except Exception as err: # report the failure to the model instead of dropping the result396 results.append({"type": "tool_result", "tool_use_id": block.id, "content": str(err), "is_error": True})397 messages.append({"role": "user", "content": results})398`,399 tool_runner: (p) => `import sys400import anthropic401from anthropic import beta_tool402403client = anthropic.Anthropic()404405406@beta_tool407def get_weather(city: str) -> str:408 """Get the current weather for a city. Use it whenever the user asks about weather.409410 Args:411 city: City name, e.g. Montréal.412 """413 return f"Sunny, 21°C in {city}"414415416# The runner calls the API, executes tools and loops until Claude stops asking for them.417runner = client.beta.messages.tool_runner(418 model="${p.id}",419 max_tokens=16000,${p.effort ? `\n output_config={"effort": "${p.effort}"},` : ""}420 max_iterations=10,421 tools=[get_weather],422 messages=[{"role": "user", "content": sys.argv[1] if len(sys.argv) > 1 else "What is the weather in Montréal and in Québec City?"}],423)424final = runner.until_done()425426if final.stop_reason != "end_turn":427 print(f"stop_reason: {final.stop_reason}", file=sys.stderr)428for block in final.content:429 if block.type == "text":430 print(block.text)431`,432 batch: (p) => `import time433import anthropic434435client = anthropic.Anthropic()436inputs = ["Summarise: the cat sat on the mat.", "Summarise: rain is expected tomorrow."]437438# 50% cheaper, asynchronous (usually under an hour, always under 24 h). Not for interactive use.439batch = client.messages.batches.create(440 requests=[441 {442 "custom_id": f"item-{i}", # your key: results come back in ANY order443 "params": {"model": "${p.id}", "max_tokens": 1024, "messages": [{"role": "user", "content": content}]},444 }445 for i, content in enumerate(inputs)446 ]447)448print(f"batch {batch.id}: {batch.processing_status}")449450while batch.processing_status != "ended":451 time.sleep(15)452 batch = client.messages.batches.retrieve(batch.id)453454for entry in client.messages.batches.results(batch.id):455 if entry.result.type == "succeeded":456 text = next((b.text for b in entry.result.message.content if b.type == "text"), "")457 print(entry.custom_id, "->", text)458 else:459 print(entry.custom_id, "->", entry.result.type) # errored | canceled | expired460`,461 managed_agent: (p) => `import os462import sys463import anthropic464465# Managed Agents (beta): Anthropic runs the agent loop and a per-session sandbox.466# Agents and environments are persistent: create them ONCE (setup), store the ids, reuse them.467client = anthropic.Anthropic()468469470def setup() -> tuple[str, str]:471 environment = client.beta.environments.create(472 name="starter-env",473 config={"type": "cloud", "networking": {"type": "limited"}},474 )475 agent = client.beta.agents.create(476 name="Starter agent",477 model="${p.id}",478 system="You are a careful assistant working in a sandbox. Be brief.",479 tools=[480 {481 "type": "agent_toolset_20260401",482 "default_config": {"enabled": True, "permission_policy": {"type": "auto"}},483 "configs": [{"name": "web_fetch", "enabled": False}, {"name": "web_search", "enabled": False}],484 }485 ],486 )487 return agent.id, environment.id488489490# In production read these from configuration instead of calling setup() on every run.491if os.environ.get("AGENT_ID") and os.environ.get("ENVIRONMENT_ID"):492 agent_id, environment_id = os.environ["AGENT_ID"], os.environ["ENVIRONMENT_ID"]493else:494 agent_id, environment_id = setup()495print(f"AGENT_ID={agent_id} ENVIRONMENT_ID={environment_id}", file=sys.stderr)496497session = client.beta.sessions.create(agent=agent_id, environment_id=environment_id)498prompt = sys.argv[1] if len(sys.argv) > 1 else "Create hello.txt containing 'hi', then print its contents."499500# Open the stream BEFORE sending, so no early event is missed.501with client.beta.sessions.events.stream(session.id) as stream:502 client.beta.sessions.events.send(503 session.id,504 events=[{"type": "user.message", "content": [{"type": "text", "text": prompt}]}],505 )506 for event in stream:507 if event.type == "agent.message":508 for block in event.content:509 if block.type == "text":510 print(block.text, end="", flush=True)511 elif event.type in ("agent.tool_use", "agent.mcp_tool_use"):512 if getattr(event, "evaluated_permission", None) == "ask":513 # Nobody is watching this run: deny anything the policy could not decide.514 client.beta.sessions.events.send(515 session.id,516 events=[{"type": "user.tool_confirmation", "tool_use_id": event.id, "result": "deny"}],517 )518 elif event.type == "session.status_idle":519 if event.stop_reason.type != "requires_action":520 break521 elif event.type == "session.status_terminated":522 break523print()524`,525};526527export function scaffold({ approach, language = "typescript", model, effort }) {528 if (!APPROACHES[approach]) return { error: `Unknown approach "${approach}". One of: ${Object.keys(APPROACHES).join(", ")}.` };529 if (!LANGUAGES.includes(language)) return { error: `Unknown language "${language}". One of: ${LANGUAGES.join(", ")}.` };530 if (model && !resolveModel(model)) return { error: `Unknown model "${model}". Call list_models.` };531 const p = profile(model ?? CURRENT.opus, effort);532 const ts = language === "typescript";533 const needsZod = approach === "tool_runner" && ts;534 return {535 approach, language, model: p.id, description: APPROACHES[approach],536 filename: `${approach.replace(/_/g, "-")}.${ts ? "ts" : "py"}`,537 install: ts ? `npm install @anthropic-ai/sdk${needsZod ? " zod" : ""}` : "pip install anthropic",538 run: ts ? `ANTHROPIC_API_KEY=… npx tsx ${approach.replace(/_/g, "-")}.ts` : `ANTHROPIC_API_KEY=… python ${approach.replace(/_/g, "-")}.py`,539 code: (ts ? TS : PY)[approach](p),540 notes: [541 approach === "managed_agent" ? "Beta surface: the SDK adds the managed-agents beta header itself. Agents and environments persist; do not create them per request." : null,542 approach === "tool_runner" ? "The tool runner is an SDK beta helper (client.beta.messages)." : null,543 approach === "batch" ? "Batches cost 50% of standard rates; results arrive in any order." : null,544 p.effort ? `Effort "${p.effort}" is explicit because defaults differ between models.` : `${p.id} has no effort parameter.`,545 ].filter(Boolean),546 };547}548