The kernel
The kernel is the turn loop at the heart of @lite-agent/core: encode → call → decode → execute → feed back, repeated until the model stops. Understanding it pays off everywhere — it tells you exactly where a strategy plugs in, where a middleware wraps, and when an event fires. The kernel itself knows nothing about permissions or compaction — those are middleware. The loop body is just "encode → call → decode → execute → feed back".
Usage
createAgent(config) assembles a KernelConfig and returns an Agent with two entry points:
run(input, opts?)— an async generator that yields everyAgentEventand returns theRunResult.send(input, opts?)— drains the same generator and resolves with just theRunResult.
Both accept { signal, sessionId, steer } via RunOptions.
One run, step by step
- Load the session. If a
checkpointeris configured, the event log is replayed andfoldEventsrebuilds the message list; withcrashRecovery: "safe", tools that started but never finished get a synthetic errortool_result. - Run
beforeAgenthooks (once per run), then drain the event queue. - Start a turn — yield
turn_start, apply pending steers and background-task completions, then runbeforeModelhooks (this is where compaction middleware lives). - Call the model. The request is encoded by the
ToolCallCodec, streamed through thewrapModelCallmiddleware chain, and surfaced astext_deltaevents. A context-overflow error before any chunk streamed triggers one emergency compaction + retry when the ContextEngine is active. - Decode the response. The codec normalizes the assistant message into text +
ToolCall[]. Malformed prompt-codec output throwsCodecError; the kernel appends the codec'srepairPromptand retries (default 2 attempts viamaxDecodeRetries). - Stop or execute tools. No tool calls →
turn_end(stop)and the loop exits (unless steers/background tasks resurrect it). Otherwise alltool_useevents are announced up front, then each call runs through thewrapToolCallchain — schema-validated, executed, and turned into aToolResult. Up tomaxParallelTools(default 10) run concurrently; tool-phase events stream live in completion order, while the model-facing message is assembled in input order. - Feed results back as one user message of
tool_resultblocks, yieldturn_end(tool_use), and loop — untilstop,aborted, ormaxTurns. - Finish. Run
afterAgenthooks, yielddonewith theRunResult(messages,text,usage,stopReason).
Abort is observed at turn boundaries: pass an AbortSignal via run(input, { signal }) and the generator finishes with done(reason: "aborted").
Drain semantics
Two properties matter when you consume or emit events:
- Events are observational, not control flow. Middleware and tools call
ctx.emit(ev); the kernel buffers those events in a queue and drains it at loop boundaries (after hooks, after the model call, before the next turn). Emitting never pauses the loop, and a slow consumer never blocks the kernel. - Interactive handlers block on their own I/O. When an
ApprovalHandlerorInputHandleris in play, the kernel emitsapproval_request/input_requestand thenawaitshandler.request(...). The loop genuinely parks on that promise — your CLI reads stdin, your web handler waits for a button click — and resumes when you resolve it. The event stream and the pause live in the same process; nothing is persisted mid-question.
During the tool phase the queue is replaced by a live channel so concurrent tools (and forwarded subagent events) surface in real time, in completion order.
See also
- Strategies — the roles that plug into each step.
- Middleware — the hooks and wrappers the loop invokes.
- Events — every event the loop yields.
- Persistence — the
checkpointerbehind step 1.