How Agent State and Message History Management Works in Codebuff
Codebuff manages agent state and message history through an immutable AgentState object that tracks every conversation turn, tool result, and sub-agent interaction in a serializable messageHistory array.
The Codebuff runtime treats every LLM interaction as a stateful session centered on the AgentState interface. This architecture enables deterministic replay, hierarchical agent spawning, and tool-driven conversation flow while maintaining strict ordering guarantees across asynchronous boundaries.
Core Architecture of Agent State
The AgentState Interface
The foundation of agent state and message history management in Codebuff lives in .agents/types/agent-definition.ts. This file defines the AgentState interface that encapsulates everything needed to persist and resume a conversation:
export interface AgentState {
agentId: string
runId: string
parentId: string | undefined
/** The agent's conversation history: messages from the user and the assistant. */
messageHistory: Message[]
/** The last value set by the set_output tool. */
output: Record<string, any> | undefined
/** The system prompt for this agent. */
systemPrompt: string
/** The tool definitions for this agent. */
toolDefinitions: Record<string, { description: string | undefined; inputSchema: {} }>
/** Token count from the Anthropic API (updated each step). */
contextTokenCount: number
}
The messageHistory field is an ordered array of Message objects (defined in common/types/messages/codebuff-message.ts) that records every user query, assistant response, and tool execution result.
Message History Lifecycle
Codebuff's message history follows a strict lifecycle from initialization through sub-agent merging:
Initialization and System Messages
When an agent session begins, messageHistory starts as an empty array ([]). The runtime immediately injects a system message containing the systemPrompt defined in the agent configuration.
Appending User and Assistant Messages
The add_message tool handler in packages/agent-runtime/src/tools/handlers/tool/add-message.ts appends new messages to the history:
// Tool payload
{
toolName: 'add_message',
input: {
role: 'user',
content: 'Explain the differences between GPT‑4 and Claude.'
}
}
// Handler (simplified)
agentState.messageHistory.push(userMessage(toolCall.input.content));
Handling Tool Results
After the assistant generates a tool call, the runtime in packages/agent-runtime/src/run-programmatic-step.ts (lines 476-478) creates a tool-role message and pushes it immediately after the originating assistant turn:
agentState.messageHistory = [...agentState.messageHistory]
agentState.messageHistory.push(assistantMessage(toolCallPart))
Explicit History Replacement
The set_messages tool in packages/agent-runtime/src/tools/handlers/tool/set-messages.ts allows wholesale replacement of the conversation history:
// Tool payload
{
toolName: 'set_messages',
input: {
messages: [
{ role: 'system', content: 'You are a helpful assistant.' },
{ role: 'user', content: 'What is the weather?' }
]
}
}
// Handler
agentState.messageHistory = toolCall.input.messages;
Sub-Agent Spawning and History Merging
When spawning sub-agents via packages/agent-runtime/src/tools/handlers/tool/spawn-agent-utils.ts, Codebuff handles history inheritance and merging:
- Filtering: The parent filters out unfinished tool calls before passing history to the child
- Inheritance: The child receives a copy of the parent's filtered history
- Merging: When the child completes, its final
messageHistorymerges back into the parent
// Parent prepares filtered history
const parentHistory = filterUnfinishedToolCalls(parentAgentState.messageHistory);
// Child runs...
const childResult = await runSubAgent(...);
// Merge back
parentAgentState.messageHistory = result.agentState.messageHistory;
parentAgentState.childRunIds.push(result.agentState.runId);
Output Extraction from Message History
The getAgentOutput helper in packages/agent-runtime/src/util/agent-output.ts extracts the final output from messageHistory based on the agent template configuration. For last_message output mode, it uses getLastAssistantTurnMessages:
function getLastAssistantTurnMessages(messageHistory: Message[]): Message[] {
// Find the index of the last assistant message
let lastAssistantIndex = -1
for (let i = messageHistory.length - 1; i >= 0; i--) {
if (messageHistory[i].role === 'assistant') {
lastAssistantIndex = i
break
}
}
// Collect that assistant message and all subsequent tool messages
const result: Message[] = []
for (let i = lastAssistantIndex; i < messageHistory.length; i++) {
const message = messageHistory[i]
if (message.role === 'assistant' || message.role === 'tool') {
result.push(message)
} else {
break // stop on user/system
}
}
return result
}
This function walks the history backwards to locate the most recent assistant message, then collects that message plus any following tool messages until encountering a user or system entry.
Token Accounting and Immutability
Context Token Counting
Every step execution updates AgentState.contextTokenCount via the /api/v1/token-count endpoint. This field tracks credit usage and enables cost budgeting across the agent hierarchy.
Handling Read-Only Constraints
The runtime in packages/agent-runtime/src/run-programmatic-step.ts notes that agentState.messageHistory becomes read-only after certain async boundaries. The workaround involves cloning the array before mutation:
// NOTE: agentState.messageHistory is readonly for some reason (?!). Recreating the array is a workaround.
agentState.messageHistory = [...agentState.messageHistory]
agentState.messageHistory.push(assistantMessage(toolCallPart))
Practical Code Examples
Adding Messages with add_message
// Tool invocation payload
{
toolName: 'add_message',
input: {
role: 'user',
content: 'Refactor this function to use async/await'
}
}
// Implementation pushes to history
agentState.messageHistory.push(userMessage(toolCall.input.content));
Source: add-message.ts
Replacing History with set_messages
// Tool invocation payload
{
toolName: 'set_messages',
input: {
messages: [
{ role: 'system', content: 'You are a code reviewer.' },
{ role: 'user', content: 'Review this PR' }
]
}
}
// Implementation replaces array
agentState.messageHistory = toolCall.input.messages;
Source: set-messages.ts
Extracting the Last Assistant Turn
const lastTurn = getLastAssistantTurnMessages(agentState.messageHistory);
// Returns:
// [
// { role: 'assistant', content: 'Here is the solution...' },
// { role: 'tool', content: '{"status":"ok"}' }
// ]
Source: agent-output.ts
Spawning Sub-Agents with History Inheritance
// Filter parent history to remove unfinished calls
const parentHistory = filterUnfinishedToolCalls(parentAgentState.messageHistory);
// After child execution, merge histories
parentAgentState.messageHistory = childResult.agentState.messageHistory;
parentAgentState.childRunIds.push(childResult.agentState.runId);
Source: spawn-agent-utils.ts
Summary
- AgentState serves as the single source of truth for conversation state, defined in
.agents/types/agent-definition.tswith fields formessageHistory,output,systemPrompt, and token accounting. - Message history follows a strict lifecycle: initialization → user/assistant appends via
add_message→ tool result injection → optional wholesale replacement viaset_messages→ sub-agent inheritance and merging. - Output extraction uses
getLastAssistantTurnMessagesinagent-output.tsto isolate the final assistant response and associated tool results based on template configuration. - Immutability constraints require array cloning before mutation in
run-programmatic-step.tsto work around read-only boundaries after async operations. - Hierarchical agents share history through
spawn-agent-utils.ts, filtering unfinished tool calls for children and merging final states back to parents.
Frequently Asked Questions
How does Codebuff handle message history when spawning sub-agents?
When spawning sub-agents, Codebuff filters the parent’s messageHistory to remove unfinished tool calls via filterUnfinishedToolCalls in spawn-agent-utils.ts. The child receives this filtered copy and operates independently. Upon completion, the child’s final messageHistory merges back into the parent, and the child’s runId is tracked in the parent’s childRunIds array.
What is the difference between add_message and set_messages tools?
The add_message tool appends a single message to the existing messageHistory array, preserving all previous conversation context. In contrast, the set_messages tool performs a wholesale replacement of the entire history, allowing agents to reset context, trim old messages, or load predefined conversation states. Both tools are implemented in add-message.ts and set-messages.ts respectively.
How does Codebuff extract the final output from message history?
Codebuff uses the getAgentOutput utility in agent-output.ts to extract results based on the agent template configuration. For last_message output mode, it calls getLastAssistantTurnMessages, which walks backward through messageHistory to find the most recent assistant message, then collects that message plus any subsequent tool messages until encountering a user or system entry.
Why does Codebuff clone the messageHistory array during execution?
According to the source code in run-programmatic-step.ts, the agentState.messageHistory array becomes read-only after certain async boundaries. To work around this immutability constraint, the runtime clones the array using the spread operator ([...agentState.messageHistory]) before pushing new assistant or tool messages, ensuring mutations succeed without violating TypeScript readonly protections.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →