Planning

Planning gives an agent route a todo list you can see. You seed it from a file, the model updates it, B4.run keeps it in route state, and clients get it over the stream. Use planning when a route does work in several steps and you want to see the model's plan.

Quick startCopy link to section: Quick start

Add plan.md next to an agent() route:

text
src/app/support/[tenant]/
  index.ts
  plan.md

Seed it with markdown checklist items:

src/app/support/[tenant]/plan.md
- [ ] Understand the customer request
- [ ] Check account context
- [ ] Decide whether to answer or escalate
- [ ] Write the final response

When the route runs, B4.run adds four things that let the model keep the plan current:

  • a writeTodos tool
  • a todos state channel
  • a planning prompt fragment
  • a plan_update stream event after writeTodos runs

The plan.md fileCopy link to section: The plan.md file

The file is route-local. Only the route with that plan.md gets planning.

The parser reads markdown checklist lines:

md
- [ ] pending item
- [x] completed item
- [X] also completed

The parser skips other markdown and empty checklist items. Seed items start as pending or completed, and the model can also set in_progress later.

An empty plan.md still turns on planning, with an empty todo list.

B4.run ignores the seed items in a file larger than 64 KiB. Planning still works, but the list starts empty.

The writeTodos toolCopy link to section: The writeTodos tool

Planning adds this capability tool:

ts
writeTodos({
  todos: [
    { content: "Understand the customer request", status: "completed" },
    { content: "Check account context", status: "in_progress" },
    { content: "Write the final response", status: "pending" },
  ],
})

Each todo has:

  • content: non-empty string
  • status: "pending", "in_progress", or "completed"

writeTodos replaces the whole list, so the agent passes every item each time.

The tool updates the run's state. plan.md is only the starting list and stays as you wrote it.

The tool returns the new todos and updates the route state:

ts
{
  result: { todos },
  state: { todos },
}

B4.run's LangChain adapter turns that into a state update, so the next model turn sees the new list.

State and promptsCopy link to section: State and prompts

Planning adds a todos state field with a replace reducer. Two names are taken:

  • Don't declare your own todos field in state.ts. The route fails to load, with an error telling you to remove the field or plan.md.
  • Don't create a tool named writeTodos. A tool of yours with the same name as a capability tool is a conflict.

B4.run rebuilds the planning prompt from the current state on each turn. If there are todos, the model sees:

text
# Planning
 
For tasks with multiple steps, maintain a plan using `writeTodos({ todos: [...] })`.
...
 
Current plan:
- [completed] Understand the customer request
- [in_progress] Check account context
- [pending] Write the final response

That prompt keeps the plan in front of the model between tool calls.

StreamingCopy link to section: Streaming

When runs/stream sees a writeTodos tool result, B4.run emits:

json
{
  "type": "plan_update",
  "data": {
    "todos": [
      { "content": "Check account context", "status": "in_progress" }
    ],
    "tool_call_id": "call_writeTodos_0_1"
  }
}

tool_call_id is the model's id for the writeTodos call behind the update (the same id that call's tool events carry). It's left out when the model didn't supply one.

When a subagent sends a planning event, the parent stream forwards it with a subagent. prefix, as subagent.plan_update.

Use /threads/:id/runs/wait when you only need the final output. Use /threads/:id/runs/stream when a UI should show progress.

Generated typesCopy link to section: Generated types

When a route directory has a plan.md, b4 typegen adds writeTodos to that route's generated tool types.

B4.run only runs planning in agent() routes, though. The todos field exists at runtime only, and the generated route state types still come from state.ts.

Under the hoodCopy link to section: Under the hood

Planning is implemented by createPlanningMarker().

When B4.run prepares an agent route, it finds plan.md and reads the seed todos. Then it adds the writeTodos tool, the todos state field, and a stream transformer that watches tool results.

The transformer handles both plain { todos } outputs and LangGraph Command outputs that keep the update in update.todos.