Agents

Give any model the read and propose tools: AI SDK, Anthropic, OpenAI or MCP.

The tools

Every store makes two tools for an agent. by is the name people see on its proposals.

const tools = store.tools({ by: 'Claude' })
// [{ name: 'pipeline_read', description, inputSchema, execute },
//  { name: 'pipeline_propose', description, inputSchema, execute }]

That is the shape MCP and most model SDKs use, so wiring them up is a few lines. execute returns plain JSON; a refused proposal comes back as { error } for the model to read and fix. See Concepts for what the model sends and gets back.

Vercel AI SDK

npm install ai @ai-sdk/anthropic
import { jsonSchema, stepCountIs, streamText, tool } from 'ai'
import { anthropic } from '@ai-sdk/anthropic'

const tools = Object.fromEntries(
  store.tools({ by: 'Claude' }).map((t) => [
    t.name,
    tool({ description: t.description, inputSchema: jsonSchema(t.inputSchema), execute: async (input) => t.execute(input) }),
  ]),
)

const result = streamText({
  model: anthropic('claude-opus-5-5'),
  tools,
  stopWhen: stepCountIs(5),
  prompt: 'Fill in the next step for every candidate.',
})

Anthropic SDK

npm install @anthropic-ai/sdk
import Anthropic from '@anthropic-ai/sdk'

const client = new Anthropic()
const tools = store.tools({ by: 'Claude' })
const messages: Anthropic.MessageParam[] = [{ role: 'user', content: 'Fill in the next step for every candidate.' }]

while (true) {
  const response = await client.messages.create({
    model: 'claude-opus-5-5',
    max_tokens: 16000,
    tools: tools.map((t) => ({ name: t.name, description: t.description, input_schema: t.inputSchema as Anthropic.Tool.InputSchema })),
    messages,
  })
  messages.push({ role: 'assistant', content: response.content })
  if (response.stop_reason !== 'tool_use') break

  const results: Anthropic.ToolResultBlockParam[] = []
  for (const block of response.content) {
    if (block.type !== 'tool_use') continue
    const output = tools.find((t) => t.name === block.name)?.execute(block.input)
    results.push({ type: 'tool_result', tool_use_id: block.id, content: JSON.stringify(output) })
  }
  messages.push({ role: 'user', content: results })
}

OpenAI

npm install openai
import OpenAI from 'openai'

const client = new OpenAI()
const tools = store.tools({ by: 'GPT' })
let input: OpenAI.Responses.ResponseInput = [{ role: 'user', content: 'Fill in the next step for every candidate.' }]

while (true) {
  const response = await client.responses.create({
    model: 'gpt-5',
    tools: tools.map((t) => ({ type: 'function', name: t.name, description: t.description, parameters: t.inputSchema, strict: false })),
    input,
  })
  const calls = response.output.filter((item) => item.type === 'function_call')
  if (!calls.length) break

  input = [
    ...input,
    ...response.output,
    ...calls.map((call) => ({
      type: 'function_call_output' as const,
      call_id: call.call_id,
      output: JSON.stringify(tools.find((t) => t.name === call.name)?.execute(JSON.parse(call.arguments))),
    })),
  ]
}

MCP

Serve the same two tools to any MCP client, such as Claude Code, Cursor or Codex.

npm install @modelcontextprotocol/sdk
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'

const tools = store.tools({ by: 'Claude' })
const server = new Server({ name: 'pipeline', version: '1.0.0' }, { capabilities: { tools: {} } })

server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: tools.map(({ name, description, inputSchema }) => ({ name, description, inputSchema })),
}))

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const tool = tools.find((t) => t.name === request.params.name)
  if (!tool) throw new Error(`Unknown tool: ${request.params.name}`)
  const output = tool.execute(request.params.arguments ?? {})
  return { content: [{ type: 'text', text: JSON.stringify(output) }] }
})

await server.connect(new StdioServerTransport())

Where the store lives

The tools act on the store in the process that runs them. Run the agent where the store is: in the browser next to the component, as in these docs, or on your server, with the snapshot sent to the page and the person's accepts and rejects sent back.