esengine/DeepSeek-Reasonix
Single-binary Go coding agent built around prefix-cache-stable prompts, an optional planner/executor split, OS sandboxing and per-turn rewind.
Overview
Reasonix began as a DeepSeek-focused agent and is now a general coding agent written from scratch in Go. It is not a fork of another CLI. The pinned commit is on the studio branch, “Reasonix 2.x”, which the README calls the active line. The 1.x line lives on another branch in maintenance mode. One reasonix binary serves four front ends: a terminal UI, a browser UI (web/serve), an Electron desktop app under desktop/, and editors over ACP.
The design is shaped by one constraint: provider prefix caching, which makes DeepSeek in particular very cheap on cache hits. Comments throughout the loop protect a byte-stable prompt prefix. Tool schemas are frozen once per sampling attempt. Context-pressure notices are appended to the tail and never rewrite the prefix. Project instructions (REASONIX.md/AGENTS.md/CLAUDE.md) are delivered inside a <project-instructions> block in the conversation rather than in the system prompt, because their project-specific text would change the prefix from its first byte (policies.go). The plan-mode marker also rides the user turn “so plan toggles preserve cache shape” (planmode/policy.go). Compaction is described as a “low-frequency cache-reset point” (compact.go). Around that core sits a large amount of defensive engineering: stream recovery, perseveration detection, stale-anchor guards, write receipts, permission rules, an OS sandbox and git-free per-turn checkpoints.
Architecture
flowchart LR
FE["TUI / web / desktop / ACP"] --> CTRL["session control (Controller)"]
CTRL --> COORD["Coordinator (optional planner)"]
COORD --> EXE["Executor Agent"]
CTRL --> EXE
EXE --> LOOP["runToolLoop"]
LOOP --> PROV["provider: openai / anthropic / responses"]
LOOP --> ONE["executeOne: parse, policy, prepare, finish"]
ONE --> PERM["permission Gate + approvals"]
ONE --> TOOLS["builtin tools / MCP / extensions"]
TOOLS --> SBX["sandbox: bwrap / Seatbelt"]
TOOLS --> CKPT["checkpoint store (rewind)"]
LOOP --> CMP["compaction checkpoint"]
| Component | Path | Role |
|---|---|---|
| Entry | cmd/reasonix/main.go, internal/frontend/cli/cli.go |
Blank-imports providers and tools; dispatches run, tui, serve, web, acp, mcp, … |
| Front ends | internal/frontend/{tui,serve,acp}, desktop/ |
Terminal, HTTP/browser, ACP and Electron surfaces over one engine |
| Session control | internal/session/control/ |
Approvals, attachments, branches, rewind, memory commands |
| Agent loop | internal/runtime/agent/ |
runToolLoop, sampling recovery, tool rounds, compaction, sub-agents |
| Coordinator | internal/runtime/coordinator/ |
Planner model → submit_plan → executor model |
| Providers | internal/model/{openai,anthropic,responses} |
Self-registering provider kinds with per-vendor reasoning knobs |
| Built-in tools | internal/tools/builtin/ |
read_file, edit_file, multi_edit, write_file, grep, glob, bash, code_index, web_fetch, … |
| Safety | internal/safety/{permission,sandbox,egress} |
Allow/ask/deny rules, OS jail for shell, egress proxy |
| Checkpoints | internal/state/checkpoint/ |
Pre-edit snapshots per user turn for rewind |
| Extensions | internal/ext/, sdk/go/ |
MCP, hooks, skills, plugin packages, Extension Protocol v2 |
How a request flows
- Entry.
mainblank-imports theanthropic,openaiandresponsesproviders and the built-in tools, so they register themselves. Then it hands off tocli.RunWithBuildInfo(main.go), which dispatches on the subcommand (cli.go). - Optional planning. When a planner model is configured,
Coordinator.Runasks the planner policy for a route.executor_onlyskips planning. Otherwise the planner researches with read-only tools and must deliver a structured plan throughsubmit_plan. A planner failure falls back to running the executor alone, except in plan-only or plan-for-approval routes, which fail closed (coordinator.go). - Tool loop.
runToolLoopiterates up to the step budget. Each step lands any queued steer message, appends context-budget notices at the tail, captures the frozen tool schemas and prefix shape, and callsstreamWithSamplingRecovery(run_loop.go). Interrupted streams are retried, up to six sampling attempts in total. - Branch. A response without tool calls goes to
handleFinalResponse. OtherwisehandleToolRoundruns the calls. When the budget runs out, a grace round or a pause takes over (run_loop.go). - Execute one call.
executeOneis pure with respect to events, so calls can run in parallel. It runs four stages: parse (resolve the tool, reject unknown or ambiguous names, apply repeat-success and stale-anchor guards), extensiontool.beforeintercepts, policy (the permission gate), prepare (sandbox, checkpoint capture), and finish (execute_one.go). - Edit.
edit_fileconfines the path to the write roots, requiresold_stringto match exactly once (a fuzzy match is allowed and reported), writes, updates the file view and appends a bounded post-write receipt showing what landed (editfile.go). - Loop. Tool results join the conversation and the next step samples again. Past 85% of the window, compaction installs a summary checkpoint before continuing.
Key components
Providers
There are three provider kinds. openai covers any /chat/completions endpoint and picks the reasoning wire shape from the base URL: DeepSeek thinking plus reasoning_effort, MiniMax adaptive|disabled, Zhipu GLM, LongCat, Ollama Cloud, and Kimi K3’s max_completion_tokens (openai.go). anthropic speaks /v1/messages and replays signed thinking blocks. responses (also registered as dashscope-responses) targets the OpenAI Responses API (responses.go). Providers are config instances in reasonix.toml, not code. There is no bundled local-inference path. A local server is used as an OpenAI-compatible endpoint.
Context and compaction
Repository context is pulled on demand: read_file with paging, grep (ripgrep), glob, and a tree-sitter code_index for outlines and symbol search in Go, JS/TS, Python and Rust. There is no embedding index. Compaction triggers at 85% of the window and keeps a stable prefix, one structured digest of at most 16K tokens produced at low effort, and a verbatim recent tail of 10% of the window, clamped to 32K–96K tokens (compact.go). Instruction files REASONIX.md, AGENTS.md and CLAUDE.md, plus *.local.md variants, are resolved at user, ancestor, project and local scope, with @path imports up to five levels deep (resolver.go). #<note> appends a note to them.
Safety
Permission modes are read-only, ask/manual, acceptEdits, auto, dontAsk, plan and bypassPermissions. The 1.x names workspace-write and danger-full-access still parse (cli.go). Rules evaluate to allow, ask or deny. Ask defers to an interactive approver, or resolves to allow when there is none (permission.go). Under the rules, bash runs in an OS jail: Seatbelt on macOS, bubblewrap on Linux. The host is mounted read-only, with writable roots, optional forbid-read roots, a network flag and host-authority grants. When enforcement is requested but no backend exists (Windows today), the tool fails closed (sandbox.go). The file-writing built-ins confine themselves in-process.
Checkpoints
Before a writer tool changes a file, its pre-edit content is stored under the current user turn. A front end can then rewind the workspace, and the conversation, to an earlier turn. Checkpoints never touch git. Shell side effects are not captured, because only tools that can preview their change are hooked (checkpoint.go).
Extending it
- MCP.
[mcp_servers]in config, with stdio and SSE, OAuth, a registry browser, and a per-server sandbox spec. - Hooks. Claude-Code-style events (
PreToolUse,PostToolUse,PermissionRequest,UserPromptSubmit,Stop,SessionStart/End,SubagentStart/Stop,PreCompact, plusPostLLMCall) (hook.go). - Skills and slash commands. Built-in and filesystem skills that can be invoked as slash commands.
- Extensions. Sidecar processes speaking Extension Protocol v2 over stdio can intercept tool calls, observe events, host provider streams and add UI surfaces.
sdk/gois a dependency-free Go SDK for writing them (sdk/go README).
Running it
- Install a 2.x “Studio” release (the 1.x line ships on npm as
reasonix). Runreasonix setup, thenreasonixorreasonix tuiin a repository, orreasonix run "<task>"headless. - Configure providers, models, permissions, sandbox and MCP in
reasonix.toml(reasonix.example.tomlis the reference). reasonix serveorwebstarts the browser UI.reasonix acpserves editors. The desktop app wraps the same engine.- Shell sandboxing needs
bwrapon Linux. On macOS it uses the built-insandbox-exec.
Strengths and caveats
- Strength: cost-aware loop design. Frozen schemas, append-only context notices and rare compaction keep cache hit rates high. On DeepSeek-class pricing that is the biggest cost lever.
- Strength: defence in depth. Permission rules, an OS jail that fails closed, in-process write confinement, stale-write detection and per-turn rewind together make long autonomous runs recoverable.
- Strength: honest planner fallback. The two-model mode degrades to single-model on planner failure but never skips an approval boundary.
- Caveat: rewind does not cover the shell. Checkpoints capture edit-tool writes only. Anything
bashchanges is outside the undo. - Caveat: no Windows shell sandbox. Enforcement is unavailable there, so
bashruns only with the sandbox turned off. - Caveat: a fast-moving 2.x. The README itself steers users who want stability to 1.x. The agent package alone has hundreds of files, so internals change often.
Sources: code at 7f30fcb, deepwiki-open wiki (11 pages), verified Q&A.
How it answers the Open-source coding agents questions
Each answer was drafted by a code-reading agent at commit 7f30fcb. Its citations were checked mechanically. Compare with the other open-source coding agents →
How is the agent loop implemented?
answeredThe agent loop is a tool-round loop inside Agent.runToolLoop (internal/runtime/agent/run_loop.go:261). There is no separate planner model by default — the single model both plans and executes. An optional two-model Coordinator (internal/runtime/coordinator/coordinator.go:97) runs a planner model first (via submit_plan, planner tools are read-only), then hands its plan to an executor Agent. The loop in runToolLoop iterates steps up to maxSteps (default from config). Each step: (a) freezes the provider request via prepareSamplingRequest — tool schemas are captured once (ProviderSchemas), prefix shape recorded; (b) streams the model response with streamWithSamplingRecovery (run_loop.go:407), which retries up to maxSamplingAttempts=6 on interruption or context overflow; (c) classifies the response boundary via classifyResponseBoundary (response_boundary.go:27) to drop half-written tool calls; (d) commits assistant message to conversation; (e) for tool calls, runs handleToolRound → executeOne (execute_one.go:95), which resolves the tool (parse, policy, permission gate, sandbox extend, execute, post-write receipt); (f) loops back for another step. Stop conditions: no tool calls → final response handled by handleFinalResponse; max steps reached → armFinalizationRound/gracePause (finalization.go:40,55); task budget exhausted → taskBudgetPause; perseveration detected → cut-and-retry with nudge (settlePerseveration). Sub-agents are spawned via RunSubAgentWithSession (child_run.go:36), each getting its own Agent instance, session, event sink, and session-private temp directory.
How is repository context gathered and kept within the context window?
answeredRepository context is gathered through compile-time-builtin tools, not embeddings or a repo-map pass. The primary tools are: read_file (internal/tools/builtin/readfile.go:52) reads files with offset/limit paging and reports total-length/pagination trailer; grep (internal/tools/builtin/grep.go:30) runs ripgrep underneath, capped at 200 results and 300s timeout; glob finds files by glob pattern; code_index (internal/tools/builtin/codeindex.go:51) provides outline/search for Go/JS/Python/Rust/TypeScript via tree-sitter AST parsing without a language server. The model selectively invokes these on demand — there is no automatic repository indexing. Context window management is handled by windowState (internal/runtime/agent/context_window.go:13) with a compaction system (internal/runtime/agent/compact.go:20). At 85% capacity (defaultCompactRatio), the agent installs a summary checkpoint: earlier conversation is replaced by a structured brief under headings (Standing facts, Goal, Decisions, Files, Commands, Errors, Pending) generated by the model itself with a low-effort summarizer prompt (compact.go:57). The recent tail (at least 10% of window, 32K–96K tokens) and a small number of verbatim user turns are preserved. Compaction is one-shot per maintenance boundary; the model sees a <compaction-summary>...</compaction-summary> block. The workspace scan (workspace_scan.go:22) walks the workspace tree (skipping VCS dirs, symlinks) in parallel (16 readers) up to 50K files, used for delivery-verification scans rather than continuous context. There is no embedding-based retrieval or RAG system.
How are code edits applied?
answeredEdits are applied through three compile-time-builtin tools. write_file (internal/tools/builtin/writefile.go:43) replaces an entire file; it detects no-op writes (same content) and reports them, and creates parent directories. edit_file (internal/tools/builtin/editfile.go:30) does a search-and-replace: old_string must match exactly once in the file, new_string replaces it. A fuzzy-match fallback is available (whitespace-tolerant). multi_edit (internal/tools/builtin/multiedit.go:43) applies a batch of edits atomically to one file — each step runs against the result of the previous one in memory, and the file is only rewritten if every edit succeeds. Each edit tool has a Preview method (internal/tools/builtin/preview.go:25,50) that computes the diff without executing; preview_test.go asserts Preview matches Execute exactly. After every write, a bounded "post-write receipt" (post_write_receipt.go:20) is appended to the conversation — it shows the matched and replacement spans (capped at ~2KB total) so the model can confirm what landed. The FileViews mechanism (internal/tools/builtin/fileviews.go:23) tracks SHA-256 hashes of files the agent read or wrote; write_file checks that the file hasn't changed since the agent last saw it, refusing the overwrite (ErrFileChangedSinceSeen) to prevent clobbering simultaneous edits. There is no lint or test validation between edits and the model's reasoning — that is left to the model's own judgement and the subsequent tool round. Git integration is available via platform/gitcmd for commits and status queries, but edits themselves do not auto-commit; the bash tool can be used to run lint/test commands. All write tools pass through confineWrite which checks write roots, session-data guards, and managed config path protections.
How are shell commands and file writes kept safe?
answeredShell commands and file writes have three independent layers of protection. Permission policy (internal/safety/permission/permission.go:1): each tool call is evaluated against a Policy of rules (allow/ask/deny). Rules are configured as [permission] entries in reasonix.toml (e.g. allow = ["Bash:git*"], deny = ["Bash:rm -rf /"]). A Gate wraps the policy with an interactive Approver that lets the user allow/deny/always-allow on each call. The default is Ask (prompt user). Read-only mode denies all writers with a clear refusal code (RefusalReadOnly). OS sandbox (internal/safety/sandbox/sandbox.go:1): on Linux, bash commands run under bubblewrap; on macOS, under sandbox-exec/Seatbelt. The Spec declares write roots (workspace + configured extras), forbid-read roots, a Network boolean, HostAuthorities for SSH/Docker/Podman, an Egress proxy route for external HTTP, and MinimalWrites for MCP processes. On Windows, OS-level bash sandboxing is not available and enforced sandbox fails closed (UnavailableMessage). Path confinement (internal/tools/builtin/confine.go:29): ConfineBash binds the bash tool to the sandbox spec; BindSessionTemp attaches session-private temp dirs. The write tools (write_file, edit_file, multi_edit, move_file, delete operations) apply confineWrite to ensure the target path falls within allowed write roots and outside protected Reasonix data directories (SessionDataGuard). Network access for tool calls is further governed by the egress policy (internal/safety/egress/policy.go) which routes external HTTP through a proxy. The sandbox enforces three dimensions independently: Integrity (write roots), Confidentiality (forbid-read roots), and Authority (host services). Checkpoints (session snapshots) are stored separately in the session store for rewind, not used for sandbox isolation.
Which models are supported and how are they called?
answeredModels are supported through two provider implementations registered in a provider.Registry. OpenAI-compatible (internal/model/openai/openai.go:1): registered as kind "openai", calls /chat/completions with SSE streaming. Per-vendor wire-shape detection selects different reasoning parameters: api.deepseek.com uses thinking.type=enabled with reasoning_effort; api.minimaxi.com uses thinking.type=adaptive|disabled (M3's binary knob); open.bigmodel.cn / api.z.ai (Zhipu GLM) have documented depth fields; api.longcat.chat uses thinking.type=enabled|disabled without effort scale; ollama.com accepts hosted Ollama Cloud's effort scale including max; Kimi K3 preserves complete messages and uses max_completion_tokens. Everything else uses vanilla reasoning_effort. Anthropic (internal/model/anthropic/anthropic.go:1): registered as kind "anthropic", calls POST /v1/messages with SSE streaming. Supports extended thinking via signed reasoning blocks that are replayed on multi-turn. No temperature/top_p (Claude rejects sampling params). DeepSeek's compatible Anthropic endpoint uses unsigned thinking blocks with binary enabled|disabled and output_config.effort. Configuration is via reasonix.toml [provider] blocks with kind, base_url, model, api_key, and kind-specific extras. Providers resolve via config.go (internal/contract/provider/config.go:6) and the provider broker serves them. local.go (internal/model/providerbroker/local.go) provides no local-model fallback; there is no Ollama-hosted local inference path — the Ollama integration reaches hosted cloud API. Cost tracking uses pricing.rates.go (internal/contract/pricing/rates.go:36) with official rates for DeepSeek, Anthropic Claude, OpenAI GPT, MiniMax, LongCat, Moonshot (Kimi), and others, stored as oldest-first generations per vendor. make pricecheck reads vendor pages to verify rates.
responses (also dashscope-responses, internal/model/responses/responses.go) targets the OpenAI Responses API alongside openai and anthropic.How can it be extended and customised?
answeredReasonix provides two extension systems. MCP (Model Context Protocol) servers (internal/ext/plugin/plugin.go): configured via [mcp_servers] in reasonix.toml, supports stdio and SSE transports, OAuth, URI handlers, prompt templates, and resource listing. MCP servers are launched and managed by mcplaunch/plugin packages, with per-server sandbox specs (write roots, network, egress). The MCP registry client (internal/ext/mcpregistry/registry.go:33) provides browse/install from the official registry. MCP tool results can include images (toolresult_image.go). Extension packages (internal/ext/extension/): a protocol for runtime-loadable packages that can register new providers, tools, themes, skills, and frontend hooks through a JSON-wire RPC mechanism (packages like internal/ext/extension/protocol, dispatch, providerext). The extensioncontract package defines the contract between host and extension. Skills (internal/ext/skill/): slash-command invocable workflows registered from filesystem or built-in content; resolved by the skill package and routed by slash.go/slash_catalog.go. Hooks (internal/ext/hook/): lifecycle hooks that fire on session events (start, turn begin/end). Instruction/rule files: multi-file loading from REASONIX.md, AGENTS.md, CLAUDE.md, personal *.local.md variants, and ancestor-directory search. @path imports and #<note> quick-add are supported. Plugin packaging (internal/ext/pluginpkg/): package/unpackage MCP servers into redistributable archives with declared metadata. There is no SDK or headless-agent library exposed to external Go consumers — the sdk/ directory at the repo root contains stubs only. The desktop frontend (desktop/) is a separate Electron app that drives the same Controller.
sdk/go is not a stub; it is a dependency-free Go SDK for writing Extension Protocol v2 sidecars that intercept tool calls, observe events, host provider streams and add UI surfaces.