clark-agent
A small, typed, hookable agent loop. Provider-agnostic, sandbox-agnostic, tooling-agnostic.
Shape
context → LLM (StreamFn) → tool batch → results appended → repeat
Termination is a tool decision (ToolResult::terminate = true, unanimous
across the batch). The runtime owns execution and event emission; tools
own semantics; plugins own cross-cutting extension.
Layers
types—AgentMessage, content blocks,StopReason. Conversation isVec<AgentMessage>. Apps extend viaAgentMessage::Customor by wrapping in their own enum.event—AgentEventenum +EventSinktrait. Single sink, typed events. Streamed and final delivery use the same enum.ChannelSink,FanOutSink,NoopSinkprovided.tool—AgentTooltrait +ToolRegistry. Tools own their schema, validation, and execution. The loop only dispatches.stream—StreamFntrait. Swappable LLM transport: real provider, fixture replay, scripted scenario, remote proxy.plugin—Plugin+ capability traits (BeforeToolCall,AfterToolCall,ContextTransform,EventObserver,SteeringSource,FollowUpSource,ToolGate). Cross-cutting concerns register here, not inline in the loop.protocol—ProtocolPolicy. The seam for product-specific tool vocabulary (recovery prose, tool-call alias repair, hidden-tool errors, terminal-tool classification). Default is generic and names no tools.config—LoopConfig+AgentBuilderfor assembling everything.run—run/run_continue— the canonical loop. Pure functions.exec— tool execution: parallel + sequential dispatch, hook plumbing.budget— default token-budget context transform.error— typed error enums.
Plugin extension points
| Trait | When it runs |
|---|---|
BeforeToolCall |
After argument validation, before tool.execute. May block with reason. |
AfterToolCall |
After tool.execute. May override result, mark error, vote terminate. |
ContextTransform |
Before each LLM call. Window management, redaction. |
EventObserver |
On every AgentEvent. Logging, telemetry, persistence. |
SteeringSource |
Between batches. Inject extra messages mid-run. |
FollowUpSource |
After natural stop. Re-start the agent if more is queued. |
A single struct can implement multiple capability traits — declare the
set via Plugin::capabilities() and register once with
AgentBuilder::plugin().
Quick start
use Arc;
use ;
use CancellationToken;
let registry = new
.with
.with;
let config = new
.stream
.tools
.before_tool_call
.after_tool_call
.context_transform
.max_iterations
.build?;
let outcome = run.await?;
Examples
Run the smallest possible loop with a scripted transport:
Run a two-turn loop where the model calls a typed echo tool:
Real integrations provide their own StreamFn implementation for an LLM
provider and register application tools through AgentTool or
TypedAgentTool.
Mid-run steering (steer())
let = new;
let config = new
.stream
.tools
.steering_arc
.build?;
// In another task: inject a message between batches.
handle.steer?;
Design rules
- One canonical core.
run/run_continueare pure functions, not methods on a god-class. - Hooks are typed, narrow, side-effect-free. No I/O in
BeforeToolCallorAfterToolCall— those belong to the tool's ownexecute. - Failure is a context event. Tool errors become tool result content
with
is_error: true. The loop appends and continues. OnlyLoopError(stream transport unrecoverable / aborted) ends the run. - Termination requires unanimity. A batch ends the run only when
every finalized tool result votes
terminate: true. One tool wanting to stop does not stop the batch. - Strongly typed contracts. Discriminators are enums; payloads are
typed structs; field-name string lookups (
obj["role"]) are forbidden in primary contracts.serde_json::Valueonly at open-by-design leaves (provider extras, custom message payloads, tool arguments).
Open-source boundary
clark-agent is the reusable loop crate: typed history, tool dispatch,
provider transport traits, events, and extension hooks. Product wiring
belongs in downstream crates.
The core knows no product tool names. The three places that once needed
product vocabulary — plain-text recovery prose, model tool-call alias repair,
and hidden-tool error messages — now go through a single seam, the
ProtocolPolicy trait:
The core ships DefaultProtocolPolicy (generic, names no tools). A downstream
product installs its own via AgentBuilder::protocol_policy(...) to inject its
delivery/ask/plan vocabulary, tool-call aliases, and recovery prose — none of
which lives in this crate. New product-specific behavior should be implemented
as a ProtocolPolicy, a plugin (ToolGate, ContextTransform, …), or a tool
definition rather than added to the core loop.
Release checks
RUSTDOCFLAGS="-D warnings"
Citation
Citation authorship: Stanislav Kirdey, Clark Labs Inc. See
CITATION.cff for machine-readable citation metadata.
License
Apache-2.0 © Stanislav Kirdey, Clark Labs Inc.
Built by Stanislav Kirdey, Clark Labs Inc. — the team behind Clark, AI-powered web automation and research. If clark-agent is useful to you, a ⭐ on GitHub helps others discover it.