agent

package
v0.4.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 11, 2026 License: MIT Imports: 31 Imported by: 0

Documentation

Overview

Approval broker — routes tool-approval prompts to whatever interface can actually answer them.

Mutating commands and tools prompt for approval (y/N/a). Historically that prompt was a terminal read (askLine), which works for the REPL but hangs headless (no terminal) and can't reach the web UI. The broker abstracts "ask the user to approve X" so it can be answered from the terminal OR the browser, depending on which front-end is driving.

  • Terminal mode (default, plain REPL): prompts via askLine as before.
  • Web mode (headless, or -serve-write when chosen): emits an EvApproval event the dashboard renders as Approve/Deny/Always buttons, and blocks on a response delivered via POST /api/approve.

Session budgets — a gentle ceiling on time and tokens.

The per-turn iteration cap stops ONE runaway turn. This stops the other failure mode: twenty reasonable turns that quietly add up to ninety minutes and a novel's worth of generated tokens before you notice. On a local box the cost isn't dollars, it's your afternoon and your GPU — so the budget is a WARNING, never a hard stop. Aborting a session mid-thought to even enforce a ceiling would be worse than the overrun.

Config: "budget_minutes" and/or "budget_ktokens" (thousands of generated + reasoning tokens — reasoning counts because on a reasoning model it's most of the spend). Zero/unset = that dimension is off. Crossing a threshold prints one warning; every further 50% prints a reminder. /budget shows current usage against the ceilings anytime.

Git checkpointing — repo-level protection where .bak is file-level.

Before every model turn, `git stash create` snapshots the working tree into an unreferenced commit WITHOUT touching the worktree or the stash list — the cheapest possible checkpoint. /rewind restores any turn's state with `git checkout <hash> -- .`.

Honest limitations, stated because they matter: `git stash create` only records TRACKED files, so a brand-new untracked file the model created after a checkpoint is not deleted by rewinding (its content is simply not managed); and the snapshot commits are unreferenced, so git gc can eventually collect them — checkpoints are session-scale protection, not history. /commit is the durable path, and its message generation enforces the Conventional Commits standard the system prompt already mandates.

Command tools — user-defined tools without recompiling.

MCP is the heavyweight plugin path (a running server, its own protocol); this is the lightweight one: drop a manifest in .agent/tools/<name>.json describing a tool and the executable that backs it, and it joins the registry at startup. Wrap a PowerShell deploy script, a curl one-liner, a Python analyzer — anything runnable — as a first-class tool the model can call, with typed arguments it can't typo.

.agent/tools/deploy_status.json
{
  "name": "deploy_status",
  "description": "Check the deploy status of a service in an environment.",
  "command": ["pwsh", "-File", "scripts/DeployStatus.ps1"],
  "parameters": {
    "service": {"type": "string", "description": "Service name", "required": true},
    "env":     {"type": "string", "description": "dev|stage|prod"}
  },
  "read_only": true,
  "timeout_sec": 60
}

The model's typed arguments are passed to the executable two ways, so the script can consume whichever is convenient: as --key value flags appended to the command, AND as a JSON object on stdin. The command runs in the workdir; stdout+stderr (capped) come back as the tool result.

Safety: read_only tools skip the approval prompt and may run in plan mode (declare it only for genuinely side-effect-free scripts). Everything else goes through the same y/N approval as run_command. Manifests are the user's own files under their repo — trusted like a Makefile, not like model output.

Custom slash commands — prompt templates as files.

Your recurring runbook-style prompts become commands: a markdown file at <repo>/.agent/commands/<name>.md (or ~/.agent/commands/<name>.md for personal ones) turns into /<name>. The file body is the prompt; the placeholder $ARGUMENTS is replaced with whatever follows the command:

.agent/commands/migrate.md:
  Migrate $ARGUMENTS to the reusable build workflow. Read the current
  workflow first, keep OTCOM versioning intact, and verify with a dry run.

> /migrate proj5

Repo-local commands shadow personal ones of the same name. Built-in commands always win — a template can't override /verify. Discovered at startup; shown in /help and Tab-completed like any other command.

Shared slash-command dispatch for the read-only / informational commands.

The REPL historically handled every slash command inline in its input loop, printing directly. That's fine for the REPL but leaves the TUI unable to run any command (it has no access to that loop). This file extracts the SAFE subset — commands that only READ and report state — into runInfoCommand, which returns its output as a string instead of printing. The REPL prints the string; the TUI emits it to the bus. One source of truth, and the TUI gains real slash-command support for everything that doesn't mutate session state.

STATEFUL commands (/plan, /commit, /rewind, /compact, /fork, /reload, /model, /init, /undo, /allow, /ctx, /resume, /diff, /verify) are NOT here: they mutate messages/session/tree and are entangled with the REPL loop. Routing those through a shared dispatcher safely is a larger refactor; they remain REPL-only, and the TUI reports as much for them.

Context construction v2 — relevance over recency, opt-in.

v1 (the default) sends the whole transcript, append-only, and trims with pruning/shrinking/compaction. Its virtue is KV-cache prefix reuse: within and across turns the server reprocesses only new suffixes (~25s measured for a cold 13k prompt, near-zero warm). Its vice is accumulation: a long session drags every stale tool output behind it forever.

v2 REBUILDS the wire context at each turn boundary from what matters now:

[system prompt]
[session state: modified files, checklist, elision note]
[the session's original goal, if it scrolled out of the window]
[recent dialogue: user messages + final assistant replies only]
[the new user instruction]

Old tool outputs are deliberately absent — the model re-reads what it needs, and reads are cheap. The trade is a full prompt reprocess once per turn (bounded, small prompt) versus v1's growing-but-cached prompt. Which wins depends on session length and hardware; that's why this is a live toggle (/ctx v2, /ctx v1) and the per-turn cost prints dim — measure, then decide.

INVARIANTS the distiller guarantees: never emits tool-role messages or assistant messages carrying tool_calls (so the wire can't contain orphaned halves of a call/result pair), always includes the newest user message, and never touches the canonical transcript — sessions persist full fidelity regardless of mode. One honest cost: mid-turn crash persistence is weaker in v2 (the turn's messages land in the session file at turn end, not per-iteration).

A real diff engine — first-class change display.

The previous "diff preview" printed all old lines then all new lines: a six-line struct edit showed twelve lines of which eleven were identical noise, and the actual String→string change was a spot-the-difference puzzle. This produces proper unified-style hunks: unchanged context dim, removals red, additions green, and — for modified line pairs — the changed SPAN inside the line highlighted, so a one-token edit reads at a glance.

Used by: edit_file and write_file previews (terminal AND the tool result, so the model sees exactly what its edit did), and the /diff command (session changes per file, current content vs the pre-session .bak).

Algorithm: trim common prefix/suffix lines, LCS-align the middle when it's small enough (the overwhelmingly common case for tool edits — a single contiguous region), degrade to one replace-hunk when it isn't. O(n) for the trim, bounded DP for the middle, no dependencies.

One-shot mode (-p) and the eval harness (-eval).

One-shot makes the agent pipeable and scriptable:

agent -p "fix the failing test" -yes ~/proj && go test ./...

The eval harness is how harness changes stop being judged by vibes: a JSON file of cases, each a prompt plus a shell check, run N times against the current model and harness. "Did the loop breaker help" and "is Gemma better than Qwen at this" become pass rates instead of impressions.

Eval file format (JSON array):

[
  {"name": "add-position", "prompt": "set the position column on insert in cmd/add.go",
   "check": "go build ./... && grep -q position cmd/add.go"},
  ...
]

Each run gets a FRESH context (system prompt rebuilt, empty history) and a fresh Sandbox, but shares the working directory — checks that mutate state should clean up after themselves (e.g. via git checkout in the check).

stdout subscriber — renders bus events to the terminal.

This is the first (and during migration, primary) event subscriber. It reproduces the exact terminal styling the direct fmt.Println sites use, so when a print site is migrated from fmt.Println("x") to emitLine("x") the visible output is unchanged. The TUI and dashboard are alternate subscribers that will render the same events differently.

The color hint on an event maps to the same ANSI codes tint() uses. A subscriber may be silenced (the TUI installs itself and silences stdout, since it owns the screen).

In-place lines (EvOverwrite, e.g. a spinner or progress counter) are a special case: at most one is "open" at a time. Opening one erases and replaces whatever was there before; any OTHER event first erases it, the same way term.go's spinner erases itself the moment real output starts, so a fast-moving status line never leaves stale text mixed into the transcript.

A tool-calling coding agent in pure Go (stdlib only), speaking the OpenAI-compatible chat completions API. Works with LM Studio, llama.cpp's llama-server, vLLM, Ollama's /v1 endpoint, or any other OpenAI-compatible server.

v7 design philosophy — built for a capable model (Gemma 4 12B class):

  • Trust the model: minimal system prompt (identity, workdir, hard rules), no sampling overrides, no iteration cap, no call-pattern babysitting.
  • Guards are tripwires, not walls: backups, empty-write rejection, the repeat-call loop breaker, and y/N gating on mutating commands protect against consequences without distorting what the model attempts.
  • Close the loop against reality: a configured verify command runs after any turn that modified files, and failures are fed back automatically. Hooks enforce user invariants (gofmt, lint gates) without model turns.
  • Repo-level safety: a git checkpoint before every turn, /rewind to any of them, /commit with a model-written Conventional Commits message.
  • Plan mode (/plan): read-only exploration → numbered plan → your approval → execution. Cheaper to review a plan than flailed edits.
  • Context is managed, not just accumulated: stale reads are pruned and old tool outputs elided — in rare big batches, because history edits invalidate the server's KV-cache prefix (see session.go).
  • Streaming: tokens print as they generate — no dead air.

Layout:

main.go     flags, config, system prompt, REPL
loop.go     the agent loop (runTurn), verify loop, spawn_task
tools.go    tool registry + handlers, command approval
session.go  session persistence, /compact, context management
api.go      OpenAI-compatible types, streaming chat, SSE parsing
mcp.go      MCP client (stdio transport) — external tool servers
eval.go     one-shot mode (-p) and the eval harness (-eval)
term.go     stdin ownership, colors, spinner, input reading

Usage:

go run . [flags] [working-dir]
go run . -url http://172.22.208.1:1235 -model google/gemma-4-12b ~/myproject
go run . -p "fix the failing test" -yes ~/myproject     (one-shot)
go run . -eval evals.json -runs 3 ~/myproject           (benchmark)

Flags:

-url     base URL of the server (default: auto-detect Windows host in WSL2)
-model   model id (default: first non-embedding model from GET /v1/models)
-resume  resume a session: 'latest' or a name from /sessions
-p       run one prompt non-interactively and exit
-yes     auto-approve mutating commands and fetches (for -p / scripts)
-eval    run an eval file (JSON array of {name,prompt,check}) and report
-runs    runs per eval case (default 1)

~/.agent/config.json (all optional): url, model, compact_tokens, command_timeout_sec, verify_command, mcp_servers.

Input: single lines as usual; type """ alone to start a multi-line block (paste code freely), and """ alone again to send it.

Trace legend:

⚙  tool call requested by the model
✏  file written or edited (absolute path shown)
✗  write/edit rejected by a guard (nothing touched disk)
$  command executed (auto-approved) or awaiting your y/N
⧉  subtask running in a fresh context
✓  verify command passed

MCP (Model Context Protocol) client — built on the official Go SDK (github.com/modelcontextprotocol/go-sdk/mcp).

This used to be a hand-rolled JSON-RPC client. It now delegates to the canonical SDK, which tracks the spec (protocol version negotiation, both transports, session management, notifications, cancellation) so we don't chase it by hand. The config surface and registry behavior are unchanged: each configured server's tools/list result is registered alongside the built-ins, calls proxy through as tools/call, and the model can't tell an MCP tool from a native one.

Config (~/.agent/config.json):

"mcp_servers": {
  "pg":    {"command": "npx", "args": ["-y", "@modelcontextprotocol/server-postgres", "postgres://..."]},
  "engram": {"url": "http://localhost:8088/mcp/", "no_prefix": true, "prefer": true},
  "sbx":   {"command": "sandbox", "args": ["mcp", "-image", "python:3-alpine"], "prefer": true,
           "prefer_hint": "a locked-down, network-less sandbox. Run untrusted or unfamiliar code here (sbx_run_script / sbx_run_sandbox) before running it on the host."}
}

Two transports: "command" (stdio child process) or "url" (Streamable HTTP). Tool names are prefixed with the server name (pg_query) unless "no_prefix" is set. "prefer" surfaces the server in the system prompt; "prefer_hint" overrides the default notes-server wording for non-notes servers (e.g. a sandbox).

Notifications — because at local token rates a turn can take minutes, and the worst UX in this agent is alt-tabbing away and missing a y/N prompt.

Two layers: a terminal BEL (\a — Windows Terminal flashes/badges the tab) and a Windows toast via powershell.exe interop, WSL's bridge to the host. The toast uses a NotifyIcon balloon — no modules to install, works on a stock Windows box. It's fired async and best-effort: a notification is never worth blocking the agent for, and never worth an error message.

Config: "notify_sec" — a turn longer than this notifies on completion (default 10; 0 disables everything including approval pings). Quick turns stay silent: you're still looking at the terminal anyway.

Prompt construction — every model-facing prompt string lives here, separate from the execution loop that decides WHEN to send it. Extracted from loop.go, session.go, context.go, and checkpoint.go as a pure text-building layer: no I/O, no globals mutated, explicit inputs. See docs/PHASE4_LOGIC_DECOUPLING.md for the rationale and rollout plan.

buildSystemPrompt (main.go) stays where it is — it was already its own function and moving it would be pure churn.

Named agent roles + auxiliary model routing.

ROLES — .agent/agents/<name>.md (repo) or ~/.agent/agents/<name>.md (personal; repo shadows personal) define specialists the model can delegate to via spawn_task's "role" argument:

.agent/agents/reviewer.md:
  model: qwen2.5-coder-14b-instruct

  You are a strict code reviewer. Point out bugs, race conditions, and
  deviations from the repo's conventions. Do not fix anything — report.

The optional "model: <id>" header routes the subtask to a different model on the same server; the rest of the file is appended to the subtask's system prompt. /agents lists what's loaded.

AUX MODEL — "aux_model" in config routes housekeeping (compaction summaries, session titles, commit messages) to a smaller/faster model. None of those need the 12B, and every VRAM-second they don't take is one the main model gets.

/stats — throughput made visible.

You're tuning VRAM, speculative decoding, and KV-cache behavior by feel; this turns feel into numbers. Everything here is measured at the harness (token counts are the same chars/4 estimate the context budget uses, so they're consistent with /context), because local servers are inconsistent about reporting usage on streamed responses.

The number that matters most on local hardware: TTFB (time to first byte of generation) ≈ prompt processing time. Append-only turns keep it small via KV-cache prefix reuse; anything that edited history (shrink, prune, compact, resume) shows up here as a spike. When /stats says the last request spent 90 seconds before the first token, that was the cache-invalidation bill.

Index

Constants

View Source
const (
	EvLine      = core.EvLine
	EvStatus    = core.EvStatus
	EvError     = core.EvError
	EvToken     = core.EvToken
	EvThinking  = core.EvThinking
	EvBusy      = core.EvBusy
	EvUser      = core.EvUser
	EvAssistant = core.EvAssistant
	EvToolCall  = core.EvToolCall
	EvToolDone  = core.EvToolDone
	EvStats     = core.EvStats
	EvApproval  = core.EvApproval
	EvOverwrite = core.EvOverwrite
)

--- event-kind constants ---

View Source
const (
	ApproveOnce   = approveOnce
	ApproveAlways = approveAlways
	ApproveDeny   = approveDeny
)

Approval decision constants for the web UI.

Variables

View Source
var ToolRegistrations []func()

ToolRegistrations are functions that register tools into the engine at startup. Tool packages append their registrar here (via cmd wiring) so the engine never imports them. Run invokes each after built-ins are registered.

View Source
var UseTTY = useTTY

Functions

func AnswerWebApproval

func AnswerWebApproval(id string, d ApprovalDecision) bool

AnswerWebApproval resolves a pending browser-routed approval. Returns true if the id matched a pending request.

func ClosestLines

func ClosestLines(content, needle string) string

ClosestLines returns the lines in content most similar to needle (for edit rejection diagnostics).

func CompleteLine

func CompleteLine(line string, pos int, key rune) (string, int, bool)

CompleteLine exposes the REPL tab-completer for the TUI input.

func ConfigPath added in v0.3.0

func ConfigPath() (string, error)

ConfigPath returns the absolute path to the agent config file (~/.agent/config.json), creating no files. It is the single source of truth for the config location, used by both the agent and the `config` command.

func CurBaseURL

func CurBaseURL() string

CurBaseURL returns the current model server base URL.

func DeleteTool

func DeleteTool(name string)

DeleteTool removes a tool from the registry by name.

func ExecArgv added in v0.2.0

func ExecArgv(dir string, name string, args ...string) (string, int, error)

execShell runs a command via bash in dir with the shared timeout, killing the WHOLE process group on timeout — exec.CommandContext alone kills only bash itself, leaving grandchildren (a hung test binary, a dev server) running orphaned. With live=true, output streams to the terminal (dimmed) as it happens — no dead air during a two-minute build — while still being captured in full for the tool result. Returns combined output and the exit code; exitCode is -1 for timeouts and other non-exit errors. (Setpgid is Linux/WSL2-only, which is where this agent lives.) ExecArgv runs a command as a direct argv vector — NO shell — so arguments are passed verbatim and shell metacharacters (;, |, `, $(), &) in an argument are inert. Use this (not ExecShell) for tools that invoke a FIXED program with data arguments derived from model/user input (e.g. the git tools): it eliminates command injection by construction. Returns combined stdout+stderr, the exit code, and any error. The command timeout and the VetCommand backstop still apply.

func ExecShell

func ExecShell(cmdStr, dir string, live bool) (string, int, error)

func HTTPClient

func HTTPClient() *http.Client

HTTPClient is the shared HTTP client (long timeout for model calls), exported for tool packages that make their own requests.

func Hooks

func Hooks() map[string]string

Hooks returns the current lifecycle hooks map (used by tests to save/restore).

func IsProtected

func IsProtected(rel string) bool

IsProtected reports whether a repo-relative path is protected from edits.

func MarkReadOnly

func MarkReadOnly(name string)

MarkReadOnly flags a tool name as read-only (safe under plan mode, eligible for parallel execution).

func RebuildToolSchemas

func RebuildToolSchemas()

RebuildToolSchemas regenerates the cached JSON schemas after tools are registered. Tool packages call this at the end of their registrar.

func RegisterTools

func RegisterTools(ts ...Tool)

RegisterTools adds tools to the engine's registry (idempotent by name).

func ResetRegistry

func ResetRegistry()

ResetRegistry clears the registry (tests set up a known state after).

func RestoreRegistry

func RestoreRegistry(s RegistrySnapshot)

RestoreRegistry restores a previously captured registry state.

func Run

func Run(opts Options) int

Run is the agent entry point. It preserves the exact behavior of the former func main(): resolve config, apply globals, register tools, start MCP, then dispatch to eval / one-shot / TUI / headless / REPL. Returns a process exit code (0 = normal). The cmd/ layer populates Options and calls this.

func RunHook

func RunHook(name string, repl map[string]string) (string, bool)

RunHook runs a configured lifecycle hook by name.

func RunInfoCommand

func RunInfoCommand(cmd, baseURL, model string, messages []Message, st *SessionStore) (string, bool)

runInfoCommand executes a read-only command and returns (output, handled). handled is false if the command isn't a known read-only one (the caller should then treat it as a stateful/REPL command). It needs a bit of context (model, messages, session) to render some reports.

func SaveConfig added in v0.3.0

func SaveConfig(cfg Config) (backup string, err error)

SaveConfig writes cfg to ConfigPath as indented JSON, creating ~/.agent if needed. If the existing file contains keys the struct doesn't model (e.g. "// comment" keys or fields from another version), the current file is first copied to <path>.bak so a clean rewrite never silently drops data. Returns the backup path if one was made (else "").

func SetActiveSessionStore added in v0.2.0

func SetActiveSessionStore(st *SessionStore)

SetActiveSessionStore records the running agent's session store. Called once at startup.

func SetApprovalMode

func SetApprovalMode(m approvalModeT)

setApprovalMode is called at startup once the front-end is known.

func SetApprovalWeb

func SetApprovalWeb()

SetApprovalWeb routes tool approvals to the browser (headless mode).

func SetHooks

func SetHooks(h map[string]string)

SetHooks replaces the lifecycle hooks map (used by tests).

func SetTodos

func SetTodos(items []TodoItem)

SetTodos replaces the current todo list. Primarily for tests and headless setup; normal updates flow through the update_todos tool.

func SilenceStdout

func SilenceStdout(silent bool)

SilenceStdout tells the stdout subscriber whether to suppress output (the TUI owns the screen and silences it).

func Tail

func Tail(s string, n int) string

tail returns the last n bytes of s (for showing the end of long output, where the actual error usually lives).

func UnregisterTools

func UnregisterTools(names ...string)

UnregisterTools removes tools by name from the registry (used by tests and the tool-dedup path).

func VerifyCommand

func VerifyCommand() string

VerifyCommand returns the configured post-turn verify command ("" if none).

func WriteAtomic

func WriteAtomic(path string, data []byte) error

WriteAtomic writes data to path atomically (temp + rename).

Types

type ApprovalDecision

type ApprovalDecision = approvalDecision

ApprovalDecision mirrors the internal approval outcome type for the web UI.

type ChatError added in v0.4.0

type ChatError struct {
	Kind      ChatErrorKind
	Retryable bool
	Status    int   // HTTP status code, 0 if not applicable
	Err       error // the underlying error
}

ChatError wraps a chat() failure with enough context for the caller to decide retryable vs. fatal without string-matching error messages.

func (*ChatError) Error added in v0.4.0

func (e *ChatError) Error() string

func (*ChatError) Unwrap added in v0.4.0

func (e *ChatError) Unwrap() error

type ChatErrorKind added in v0.4.0

type ChatErrorKind int

ChatErrorKind classifies why a chat request failed, so callers can decide whether retrying is worth it instead of treating every failure alike.

const (
	ErrKindUnknown    ChatErrorKind = iota
	ErrKindEncode                   // building the request failed — a code/config bug, not the network
	ErrKindConnection               // dial/TLS/DNS failure reaching the server
	ErrKindTimeout                  // context deadline exceeded
	ErrKindClient                   // HTTP 4xx — the request itself is bad
	ErrKindRateLimit                // HTTP 429 — bad timing, not a bad request
	ErrKindServer                   // HTTP 5xx — the server is unhealthy
	ErrKindStream                   // connection dropped mid-SSE-stream
	ErrKindModel                    // server reported an error object mid-stream (e.g. context length)
)

type ChatRequest

type ChatRequest struct {
	Model           string           `json:"model"`
	Messages        []Message        `json:"messages"`
	Tools           []map[string]any `json:"tools,omitempty"`
	Stream          bool             `json:"stream"`
	MaxTokens       int              `json:"max_tokens,omitempty"`
	ReasoningEffort string           `json:"reasoning_effort,omitempty"` // low|medium|high — attacks the measured ~90%-of-wall-time thinking cost
}

type Config

type Config struct {
	URL               string                     `json:"url,omitempty"`
	APIKey            string                     `json:"api_key,omitempty"` // Bearer token for authenticated endpoints (cloud/proxied OpenAI-compatible); empty for local servers
	Model             string                     `json:"model,omitempty"`
	CompactTokens     int                        `json:"compact_tokens,omitempty"`
	CommandTimeoutSec int                        `json:"command_timeout_sec,omitempty"`   // run_command limit (default 300)
	MaxTokens         int                        `json:"max_tokens,omitempty"`            // per-generation cap (default 8192)
	MaxTurnIters      int                        `json:"max_turn_iters,omitempty"`        // hard per-turn tool-call budget (default 40)
	Protected         []string                   `json:"protected,omitempty"`             // extra write-protected glob patterns (e.g. ".env", "secrets/*")
	ContextV2         bool                       `json:"context_v2,omitempty"`            // distilled per-turn wire context (see context.go) — experimental
	BudgetMinutes     int                        `json:"budget_minutes,omitempty"`        // warn when a session exceeds this wall-clock (0 = off)
	BudgetKTokens     int                        `json:"budget_ktokens,omitempty"`        // warn when generated+reasoning tokens exceed this many thousand (0 = off)
	VerifyCommand     string                     `json:"verify_command,omitempty"`        // e.g. "go build ./... && go test ./..."
	AuxModel          string                     `json:"aux_model,omitempty"`             // smaller model for compaction/titles/commit messages
	PlanModel         string                     `json:"plan_model,omitempty"`            // stronger model for /plan turns; falls back to the main model when unset
	FastModel         string                     `json:"fast_model,omitempty"`            // cheaper/faster model for trivial follow-up turns (empty = always use the main model)
	PriceInPerM       float64                    `json:"price_in_per_m,omitempty"`        // USD per 1M input (prompt) tokens — enables session cost in /stats (0 = off, e.g. local)
	PriceOutPerM      float64                    `json:"price_out_per_m,omitempty"`       // USD per 1M output (generated+reasoning) tokens
	EmbedModel        string                     `json:"embed_model,omitempty"`           // embedding model id — enables semantic code_search
	ReasoningEffort   string                     `json:"reasoning_effort,omitempty"`      // low|medium|high — thinking budget for normal turns
	PlanEffort        string                     `json:"plan_reasoning_effort,omitempty"` // thinking budget for /plan turns (deep thinking earns its time there)
	NotifySec         *int                       `json:"notify_sec,omitempty"`            // toast+bell for turns longer than this (default 10; 0 = off)
	NoCheckpoints     bool                       `json:"no_checkpoints,omitempty"`        // disable per-turn git snapshots
	Hooks             map[string]string          `json:"hooks,omitempty"`                 // post_edit ({file}), pre_command ({cmd}), post_turn
	MCPServers        map[string]MCPServerConfig `json:"mcp_servers,omitempty"`
	MaxFixAttempts    int                        `json:"max_fix_attempts,omitempty"`    // verify-loop fix retries (default 2)
	ChatRetryAttempts int                        `json:"chat_retry_attempts,omitempty"` // streamChat retry attempts on transient failure (default 2)
	CapNudgeLimit     int                        `json:"cap_nudge_limit,omitempty"`     // cap-stall recovery nudges per turn (default 2)
	ResumeTail        int                        `json:"resume_tail,omitempty"`         // messages restored by -resume (default 30)
	SubtaskMaxDepth   int                        `json:"subtask_max_depth,omitempty"`   // subtask nesting cap (default 1)
	ContextV2Budget   int                        `json:"context_v2_budget,omitempty"`   // v2 distilled-context token budget (default 6000)
	ContextV2Window   int                        `json:"context_v2_window,omitempty"`   // v2 recent-dialogue message window (default 12)
}

~/.agent/config.json persists defaults so you don't pass flags every launch. Command-line flags always override the file. Every field optional.

func LoadConfigFrom added in v0.3.0

func LoadConfigFrom() (cfg Config, found bool, err error)

LoadConfigFrom reads and parses the config at ConfigPath. A missing file is not an error — it returns a zero Config and found=false. A malformed file is reported so the caller (the `config` command) can warn instead of silently discarding it, unlike the agent's startup path which tolerates it.

type Event

type Event = core.Event

--- type aliases ---

type EventKind

type EventKind = core.EventKind

type InputKind

type InputKind int

InputKind classifies a submitted line so any front-end (REPL, TUI, headless web) routes it the same way.

const (
	InputPrompt  InputKind = iota // normal message to the model
	InputShell                    // !cmd — run directly
	InputFile                     // @path — attach a file
	InputCommand                  // /cmd — slash command
	InputBlank                    // empty
)

func ClassifyInput

func ClassifyInput(line string) (InputKind, string)

ClassifyInput determines how a submitted line should be handled.

type MCPServerConfig

type MCPServerConfig struct {
	// stdio transport: a child process speaking JSON-RPC on stdin/stdout.
	Command string            `json:"command,omitempty"`
	Args    []string          `json:"args,omitempty"`
	Env     map[string]string `json:"env,omitempty"` // extra environment for the child (e.g. SEARXNG_URL, API keys)
	// HTTP (Streamable HTTP) transport: a URL instead of a command.
	URL      string            `json:"url,omitempty"`
	Token    string            `json:"token,omitempty"`
	Headers  map[string]string `json:"headers,omitempty"`
	Insecure bool              `json:"insecure,omitempty"`  // skip TLS verify (self-signed loopback certs)
	NoPrefix bool              `json:"no_prefix,omitempty"` // register tools under their own names
	Prefer   bool              `json:"prefer,omitempty"`    // steer the model to this server in the system prompt
	// PreferHint overrides the default (notes-server) steering text used when
	// Prefer is set, so a non-notes server (e.g. a code sandbox) can describe
	// how the model should use it. Empty = the default knowledge/notes wording.
	PreferHint string `json:"prefer_hint,omitempty"`
}

type Message

type Message = core.Message

--- core data types (moved to core) ---

func Compact

func Compact(baseURL, model string, messages []Message, st *SessionStore) []Message

compact distills the conversation into a summary written by the model itself, then restarts the context (and the session file) from that summary. The full transcript remains on disk in the rotated-out file. This resets the KV-cache prefix — the first request after a compact reprocesses from scratch, which is the one-time price of a small context.

func RunTurn

func RunTurn(baseURL, model string, sb *Sandbox, st *SessionStore, messages []Message) []Message

runTurn is the agent loop: send the conversation, stream the reply, execute any tool calls (read-only ones concurrently), append results, and repeat until the model answers with plain text. No iteration cap by design — the loop breaker and the user's Ctrl+C are the exits.

func RunVerifyLoop

func RunVerifyLoop(baseURL, model string, sb *Sandbox, st *SessionStore, messages []Message, modifiedBefore int) []Message

runVerifyLoop closes the loop against ground truth: when a turn modified files and a verify command is configured, the HARNESS (not the model) runs it, and failures are fed back as a new turn — up to maxFixAttempts times. This converts "a model that edits files" into "a system that converges on working code": the model's claim of being done is checked, every time. If a fix attempt modifies nothing, retrying is pointless and control returns to the user immediately.

type Options

type Options struct {
	URL            string // -url
	Model          string // -model
	Resume         string // -resume
	Prompt         string // -p (one-shot)
	Yes            bool   // -yes
	Eval           string // -eval
	ForceEval      bool   // -force-eval
	Runs           int    // -runs
	Serve          string // -serve
	ServeWrite     bool   // -serve-write
	Tui            bool   // -tui
	Headless       bool   // -headless
	PositionalRoot string // optional workdir arg (was flag.Arg(0))

	// Presentation hooks, supplied by the cmd layer so the engine never
	// imports tui/web (which would create an import cycle). Any may be nil.
	StartDashboard func(addr string)
	RunTUI         func(baseURL, model string, sb *Sandbox, st *SessionStore, messages []Message)
	RunHeadless    func(baseURL, model string, sb *Sandbox, st *SessionStore, messages []Message)
	SetDashWrite   func(bool)
}

Options carries the resolved CLI inputs from the cmd/ (Cobra) layer into the agent. It replaces the old flag.* globals; each field maps 1:1 to a former flag. PositionalRoot is the optional working-directory argument.

type RegistrySnapshot

type RegistrySnapshot struct {
	// contains filtered or unexported fields
}

RegistrySnapshot captures the tool registry state for save/restore in tests.

func SnapshotRegistry

func SnapshotRegistry() RegistrySnapshot

SnapshotRegistry returns the current registry state.

type Sandbox

type Sandbox struct {
	Root     string
	Modified []string // absolute paths of every file written this session
}

func (*Sandbox) Backup

func (s *Sandbox) Backup(path string) (bool, error)

Backup snapshots a file before modification (for undo). Exported for out-of-package tool handlers.

func (*Sandbox) Execute

func (s *Sandbox) Execute(name string, args map[string]any) (result string)

Execute dispatches a tool call by name through the registry. A panic in a handler is converted to an ERROR result instead of killing the process — the model sees the failure and the session (and its context) survives.

func (*Sandbox) Resolve

func (s *Sandbox) Resolve(p string) (string, error)

Resolve resolves a possibly-relative path against the sandbox root, applying the sandbox's safety checks. Exported for out-of-package tool handlers.

func (*Sandbox) Summary

func (s *Sandbox) Summary()

Summary prints every file modified this session, deduplicated in order.

type SessionStore

type SessionStore struct {
	// contains filtered or unexported fields
}

func ActiveSessionStore added in v0.2.0

func ActiveSessionStore() *SessionStore

ActiveSessionStore returns the running agent's session store, or nil if none is set (e.g. persistence unavailable).

func NewSessionStore

func NewSessionStore(root string) *SessionStore

NewSessionStore creates a session store rooted at the given directory.

func (*SessionStore) Append

func (st *SessionStore) Append(messages []Message)

Append persists any messages beyond what's already on disk.

func (*SessionStore) Fork

func (st *SessionStore) Fork(messages []Message) (string, error)

Fork starts a NEW session file seeded with the current conversation and switches persistence to it — the original file stays frozen where it is, so a risky direction can be explored and abandoned by /resume-ing the original. Returns the new session name.

func (*SessionStore) Path

func (st *SessionStore) Path() string

func (*SessionStore) Rotate

func (st *SessionStore) Rotate()

Rotate closes the current file so the next Append starts a fresh session (used by /compact so the compacted context begins a new transcript).

func (*SessionStore) SessionSummaries added in v0.2.0

func (st *SessionStore) SessionSummaries() []SessionSummary

SessionSummaries returns the persisted sessions for this store's directory, newest first — the data behind the dashboard's session browser. Returns nil when persistence is unavailable or there are no sessions.

func (*SessionStore) SessionTranscript added in v0.2.0

func (st *SessionStore) SessionTranscript(name string) ([]Message, error)

SessionTranscript returns the messages of a named session (without the .jsonl suffix) for display in the dashboard. The name is validated to be a bare session name — no path separators — so it can't escape the store dir.

type SessionSummary added in v0.2.0

type SessionSummary struct {
	Name     string `json:"name"`     // session file name without .jsonl
	Title    string `json:"title"`    // model-written title, or a prompt excerpt
	Messages int    `json:"messages"` // message count
	Latest   bool   `json:"latest"`   // true for the newest session
}

SessionSummary is a structured, exported view of one persisted session for surfaces outside the agent package (the web dashboard). It mirrors what the CLI's `/sessions` list shows, but as data instead of printed text.

type StatsSnapshot

type StatsSnapshot struct {
	Requests   int
	PromptTk   int
	GenTk      int
	ThinkTk    int
	LastTTFBms int64
	CostUSD    float64 // estimated session cost; 0 when pricing isn't configured
}

StatsSnapshot is an immutable view of the cumulative request stats, safe to read from another package without touching the recorder's lock.

func Stats

func Stats() StatsSnapshot

Stats returns a locked snapshot of the current stats recorder.

type Subscriber

type Subscriber = core.Subscriber

type SubscriberFunc

type SubscriberFunc = core.SubscriberFunc

type TodoItem

type TodoItem = todoItem

TodoItem is one entry in the agent's working todo list.

func Todos

func Todos() []TodoItem

Todos returns a copy of the current todo list.

type Tool

type Tool struct {
	Name     string
	Desc     string
	Props    map[string]any
	Required []string
	// RawSchema, when set, is used verbatim as the tool's JSON-schema
	// parameters (MCP servers ship their own); Props/Required are ignored.
	RawSchema map[string]any
	Handler   func(s *Sandbox, args toolArgs) string
}

func LookupTool

func LookupTool(name string) (Tool, bool)

LookupTool returns a registered tool by name.

type ToolArgs

type ToolArgs = toolArgs

ToolArgs is the argument map passed to a tool handler.

func (ToolArgs) Num

func (a ToolArgs) Num(key string) int

Num returns the int value for key.

func (ToolArgs) Str

func (a ToolArgs) Str(key string) string

Str returns the string value for key (exported accessor for out-of-package tool handlers).

type ToolCall

type ToolCall = core.ToolCall

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL