SPB Git

Repository Public Research preview

scaffold.mjs 22.7 KB 548 lines javascript

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
Files main
Menu

Theme