Structured handoff protocol for multi-agent AI pipelines.
Handles passing context between agents, saving checkpoints, and rolling back to any prior state. Works with any LLM provider — agents are plain async functions.
npm install agent-handoffimport { Pipeline, MemoryStore } from 'agent-handoff'
import type { AgentFn } from 'agent-handoff'
// 1. Define your agents
const researchAgent: AgentFn = async (handoff) => {
const { topic } = handoff.input as { topic: string }
const findings = await fetchResearch(topic) // your logic here
return {
result: { findings },
summary: `Researched "${topic}"`,
artifacts: { research: findings },
}
}
const writerAgent: AgentFn = async (handoff) => {
const research = handoff.context.artifacts.research
const draft = await writeDraft(research) // your logic here
return {
result: { draft },
summary: 'Wrote draft from research',
}
}
// 2. Build and run the pipeline
const pipeline = new Pipeline({ id: 'content-pipeline' })
pipeline
.agent('researcher', researchAgent)
.checkpoint('research-complete')
.agent('writer', writerAgent)
const result = await pipeline.run({ topic: 'AI in healthcare' })
console.log(result.output) // writer's output
console.log(result.context.history) // step-by-step audit trail
console.log(result.context.artifacts) // named outputs from all agentsAn ordered sequence of agents with optional checkpoints between them.
const pipeline = new Pipeline({
id: 'my-pipeline',
store: new MemoryStore(), // optional, defaults to MemoryStore
})
pipeline
.agent('step-one', agentOneFn)
.checkpoint('after-step-one')
.agent('step-two', agentTwoFn)Every agent is an async function that receives a Handoff and returns a HandoffOutput:
const myAgent: AgentFn = async (handoff) => {
// What you receive:
// handoff.input — output from the previous agent (or pipeline.run() input)
// handoff.context.history — summaries of all prior steps
// handoff.context.artifacts — named outputs from prior agents
// handoff.context.annotations — messages left by prior agents for downstream agents
return {
result: { /* your output — must be JSON-serialisable */ },
summary: 'What this agent did (appears in audit trail)',
key_decisions: ['Decision 1', 'Decision 2'], // optional, for provenance
artifacts: { my_output: data }, // optional, named for later steps
annotations: [{ // optional, messages for downstream agents
from_step: 'my-step',
to_step: 'next-step', // or '*' for all downstream
content: 'Something the next agent should know',
type: 'instruction', // 'instruction' | 'warning' | 'context'
}],
}
}Saves pipeline state at a point in the pipeline. Enables rollback.
pipeline
.agent('researcher', researchAgent)
.checkpoint('research-complete') // state saved here
.agent('writer', writerAgent)Re-run from any saved checkpoint, optionally with different input:
// Re-run from checkpoint with same input
await pipeline.rollback(result.run_id, 'research-complete')
// Re-run from checkpoint with new input
await pipeline.rollback(result.run_id, 'research-complete', {
override_input: { topic: 'Different topic' },
})Rollback creates a new run — the original is preserved.
pipeline
.on('step:start', ({ run_id, step }) => console.log(`Starting ${step}`))
.on('step:complete', ({ run_id, step, handoff }) => console.log(`Done: ${step}`))
.on('checkpoint:saved', ({ run_id, checkpoint }) => console.log(`Checkpoint: ${checkpoint}`))
.on('pipeline:complete', ({ run_id, handoff }) => console.log(`Pipeline done`))
.on('pipeline:error', ({ run_id, step, error }) => console.error(`${step} failed`, error))| Error | When |
|---|---|
PipelineStepError |
An agent throws. Wraps the original error with step name and run ID. |
HandoffSerializationError |
An agent returns a non-JSON-serialisable value. |
CheckpointNotFoundError |
rollback() called with a checkpoint name that doesn't exist. |
HandoffError |
Base class for all library errors. |
import { PipelineStepError, CheckpointNotFoundError } from 'agent-handoff'
try {
await pipeline.run(input)
} catch (err) {
if (err instanceof PipelineStepError) {
console.error(`Step "${err.step}" failed in run "${err.run_id}":`, err.cause)
}
}- Agent outputs must be JSON-serialisable. No functions, class instances, or circular references in
resultorartifacts. The library enforces this at runtime. - Not an LLM framework. Agents are plain async functions — wire in Claude, OpenAI, or any other provider yourself.
- Sequential execution only in v1. Parallel fan-out/fan-in is planned for v2.
- Human approval gates are planned for v2.
- v1 (current): Sequential pipeline, in-memory store, checkpoints, rollback
- v2: Human-in-the-loop approval gates, SQLite persistent store
- v3: Full audit trail API, per-step observability hooks
- v4: Parallel agent execution, Redis store, Python port