Build a Coding Agent from Scratch
中文

06. The Agent Kernel and Its Lifecycle

By now you have a message protocol, providers, tools, and streaming events. The next step is to organize them into an agent kernel. The kernel is not a product interface, nor a wrapper around some provider. Its job is to maintain running state, advance turns, execute tools, emit events, and expose customization points to the layers above.

Four responsibility boundaries

A clean agent kernel usually splits into four parts:

  • State: current messages, tools, model, configuration, queues, and running status.
  • Runner: advances turns based on the stop reason.
  • Tool executor: validates, schedules, and executes tools, producing tool results.
  • Event bus: broadcasts lifecycle events to the UI, logs, extensions, and tests.

Do not let the UI modify messages directly, and do not let tools call the provider directly. Each layer interacts through explicit contracts. That is what allows you to add a JSON mode, an SDK, an extension system, or a test harness later without rewriting the core loop.

Lifecycle events

The kernel should emit stable lifecycle events:

agent_start
turn_start
model_request_start
model_response
tool_batch_start
tool_execution_start
tool_execution_end
tool_batch_end
turn_end
agent_idle

These events are not for decoration. They support several kinds of capability:

  • The UI displays the current status.
  • The session log accurately records the order of what happened.
  • Extensions run permission checks before a tool call.
  • Tests assert the execution trace of a task.
  • Metrics systems compute latency and token cost.

If you do not have a unified event stream, each of these capabilities will invent its own notion of state, and the system quickly loses consistency.

A hook surface, not an inheritance tree

Requirements can easily push an agent toward complex inheritance: a security agent, a test agent, an agent with compaction, an agent with extensions. A more robust approach is to expose hooks:

type AgentHooks = {
  beforeModelRequest?: (context: Message[]) => Promise<Message[]>;
  afterModelResponse?: (message: AssistantMessage) => Promise<AssistantMessage>;
  beforeToolCall?: (call: ToolCallBlock) => Promise<ToolCallBlock>;
  afterToolResult?: (result: ToolResultMessage) => Promise<ToolResultMessage>;
  shouldStop?: (state: AgentState) => Promise<boolean>;
};

Hooks let the product layer inject system prompts, permission confirmations, compaction, metrics, and extension logic without changing the kernel's control flow. Note that hooks must have well-defined timing and error semantics. For example, when beforeToolCall throws, does that block the tool execution and produce an error tool result, or terminate the entire task? This must be defined clearly at the kernel layer.

Parallel tools, sequential facts

Many models request multiple tools within a single assistant turn. Read-only tools can run in parallel; write tools usually need to be serialized per file. Whether or not execution is parallel, the session's facts must remain explainable. Write tool results in the order of the tool calls in the assistant message, or at least record the tool call id and completion order in the log.

The real problem parallel execution brings is file-write conflicts. Two tools read the old file at the same time, each computes its own modification, and the last one to write overwrites the other. That is not a model problem — it is the tool runtime lacking a write queue per file. The later chapter on coding tools deals with this specifically.

Kernel state

Do not stuff the kernel state with product information. It only needs to maintain the data required to advance the loop:

  • messages.
  • active model.
  • active tools.
  • isRunning.
  • queued steering and follow-up.
  • abort controller.
  • turn counters.

Session names, theme colors, and recent command history belong to the product layer. Putting them in the kernel lets the SDK, CLI, and TUI contaminate one another.

Observing a run

From the start of a user request to idle, the events might be:

agent_start
turn_start
model_request_start
model_response stopReason=toolUse
tool_batch_start count=1
tool_execution_start read
tool_execution_end read
tool_batch_end
turn_end
turn_start
model_request_start
model_response stopReason=stop
turn_end
agent_idle

This single sequence can simultaneously drive a terminal spinner, log writes, extension permission gates, and test assertions. A stable event sequence is the foundation for turning an agent into a product.

Exercises

Wrap the current loop into an Agent class or factory function.

Acceptance criteria:

  • The outside can only drive the kernel through prompt, abort, subscribe, and configuration methods.
  • Event subscribers cannot modify the internal messages directly.
  • A faux provider test can assert the complete lifecycle event sequence.
  • Unknown tools, tool errors, and user aborts all emit explicit events.