Architecture
zkit is the substrate beneath the applications in this repo. Everything here runs in real sessions daily; nothing is speculative API design. The packages are layered — each one is usable without the ones above it, and dependencies only ever point downward.
The stack
Section titled “The stack”The runner never imports an application; zkit/ai/* never imports
zkit/agent/*. Take any horizontal slice and it stands alone — if
you only want the tool system, take the tool system.
Putting it together
Section titled “Putting it together”A production agent is the runner plus whichever layers you opt into.
Each is one option on runner.New:
import ( "time"
"github.com/zarldev/zarlmono/zkit/agent/coderunner" "github.com/zarldev/zarlmono/zkit/agent/compact" "github.com/zarldev/zarlmono/zkit/agent/guardrails" "github.com/zarldev/zarlmono/zkit/agent/pursue" "github.com/zarldev/zarlmono/zkit/agent/runner" "github.com/zarldev/zarlmono/zkit/ai/llm/anthropic" "github.com/zarldev/zarlmono/zkit/ai/tools" "github.com/zarldev/zarlmono/zkit/ai/tools/code")
provider, _ := anthropic.NewProvider(apiKey)
// the standard coding toolset: read, write, edit, bash, grep, ls, planws, _ := code.NewWorkspace(root)reg := tools.NewRegistry()coderunner.RegisterStandardTools(reg, ws, code.NewProcessManager(ws))
// guardrails wrap the registry — a GuardedSource is itself a ToolSourceguarded := guardrails.NewGuardedSource(reg, guardrails.NewSchemaGuardrail(reg), // repair malformed args guardrails.NewShellGuardrail(code.ToolNameBash), // reject destructive shell guardrails.NewFanoutGuardrail(map[tools.ToolName]int{ // cap exploration code.ToolNameRead: 30, code.ToolNameGrep: 30, }),)
r := runner.New(runner.ClientFromProvider(provider), runner.WithTools(guarded), runner.WithPromptText(systemPrompt), runner.WithCompactor(compact.NewTiered(ctxWindow)), // shrink history under pressure runner.WithMaxIterations(40), runner.WithToolTimeout(60*time.Second),)
// drive it to a verified outcome instead of trusting "done"goal, watcher := pursue.Until(testsPass, "tests still failing — read the output and fix")out, _ := pursue.Drive(ctx, pursue.NewRequest(r.Run, runner.TaskSpec{Prompt: task}, pursue.WithGoal(goal), pursue.WithWatcher(watcher)), pursue.WithMaxAttempts(4),)Strip any line and it still runs — no guardrails, no compactor, no
pursue is a valid agent, just a barer one. Each page in this section
covers one of those layers in depth. For a coding agent specifically,
coderunner.GuardedSource assembles the whole standard chain in one
call — it’s what zarlcode and the eval harness both use, so they
can’t drift.
Extending it
Section titled “Extending it”The toolkit grows through six interfaces. Implement one and the runner takes it without changes:
| Interface | Shape | Implement it for |
|---|---|---|
runner.Client | one method (Complete) | a new LLM transport |
runner.ToolSource | iterate + execute | tool middleware — guardrails are exactly this |
compact.Compactor (+ Prober) | compact + cheap pre-check | a new compaction strategy |
pursue.Goal (+ Watcher) | decide + early-stop | a new completion oracle |
spawn.AgentResolver | name → runner | routing sub-agents to different models |
runner.EventSink | six typed sub-sinks (embed NopSink) | a new observability surface |
EventSink is compile-time exhaustive: add an event method and every
implementer fails to build until it handles the new kind, so a UI
can’t silently drop events.