Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Rationale

Inspired by Recursive Language Models (Zhang, Kraska & Khattab, 2025). Built from the ground up. Works with any text-based model.

The model writes Clojure. A sandboxed SCI interpreter executes it. Results flow back as a compact journal. State lives in named vars and SQLite, not in the token budget.

The Problem

Every coding agent (Claude Code, OpenCode, Pi, Hermes) runs the same loop:

sequenceDiagram
    participant U as User
    participant H as Harness
    participant LLM
    participant T as Tools

    U->>H: "Fix the auth bug"
    H->>LLM: [system, user]
    LLM->>H: assistant + tool_use: read_file
    H->>T: execute
    T->>H: result
    H->>LLM: [system, user, asst+tool_use, tool_result]
    LLM->>H: assistant + tool_use: grep
    H->>T: execute
    T->>H: result
    H->>LLM: [system, user, asst, tool_res, asst, tool_res]
    LLM->>H: assistant + tool_use: edit_file
    H->>T: execute
    T->>H: result
    H->>LLM: [sys, user, asst, res, asst, res, asst, res]
    LLM->>H: final answer

Each tool call adds 2 messages to context (assistant tool_use + tool_result). 10 tool calls = 20 messages. Context grows monotonically. ~80% of tokens end up being tool results the model may never reference again.

When context fills up: compaction. Summarize old messages, drop results. Every compaction loses signal. The model forgets, re-reads, forgets.

Other costs: one tool per round-trip (no composition), requires function_calling API support, hallucinated tool names.

How Vis Works

No tool calls. No message accumulation. No compaction.

sequenceDiagram
    participant U as User
    participant V as Vis
    participant LLM
    participant SCI as SCI Sandbox

    U->>V: "Fix the auth bug"
    V->>LLM: [system, user requirement]

    rect rgb(240, 248, 255)
        Note over V,SCI: Iteration 1
        LLM->>V: JSON {thinking, code: [read, grep, edit]}
        V->>SCI: execute all 3 blocks
        SCI->>V: 3 results
    end

    V->>LLM: [system, user req, ONE msg: journal + var_index]

    rect rgb(240, 255, 240)
        Note over V,SCI: Iteration 2
        LLM->>V: JSON {thinking, code: [run-tests]}
        V->>SCI: execute
        SCI->>V: result
    end

    V->>LLM: [system, user req, ONE msg: journal + var_index]

    rect rgb(255, 248, 240)
        Note over V,SCI: Iteration 3
        LLM->>V: JSON {final: {answer: "Fixed..."}}
    end

    V->>U: "Fixed the auth bug"
Tool-call agentsVis
Messages per turn2 per tool call, grows O(n)1 per iteration, constant
Context at iter 50All 50 iterations accumulatedSame size as iter 1
CompactionRequiredNever needed
Ops per LLM callN tools (harness-dispatched)N code blocks (LLM-composed, async-native via futures)
API requirementfunction_calling / tool_useAny text-based model
StateIn context window onlyNamed vars + SQLite
AsyncHarness decides parallelismLLM decides via future/deref/pmap
SecurityPermission prompts / trust policiesDeny-by-default sandbox, extensions grant access

The model sees one context message per iteration:

  • <journal> — previous iteration’s results (not accumulated)
  • <var_index> — all named vars rendered as compact pseudo-source, e.g. (def ^{:v 3 :s :l :t :map :n 12} foo ...); :v means persisted version count and full history is available via (var-history 'sym)
  • [system_nudge] — budget, repetition, extension hints
  • <prior_thinking> — previous iteration’s reasoning only

Everything older is one function call away: (var-history 'x), (conversation-history).

Secure by Default

In tool-call agents, every tool has direct host access. bash runs shell commands. write_file writes anywhere. The harness adds permission prompts (“Allow write to /etc/passwd?”) or trust policies. Security is opt-in, bolted on.

Vis inverts this. The SCI sandbox is a deny-by-default environment:

  • eval, load-file, spit, sh, *in*, *out*blocked
  • File system, network, shell — no access unless an extension grants it
  • Java interop — only classes explicitly exposed (LocalDate, UUID, Pattern, etc.)
  • Per-block timeout — infinite loops get killed

The model can only do what extensions allow. An extension that registers read-file decides the allowed paths, size caps, and tracking. An extension that registers bash decides which commands are permitted. No extension = no capability. There are no permission prompts because there is nothing to permit — the sandbox boundary is the policy.

This is why the extension system is the only way to add capabilities. It’s not a plugin architecture for convenience — it’s the security model.

Why SCI

SCI — sandboxed Clojure interpreter on the JVM. Full Clojure semantics, deny-list sandboxing, per-block timeouts, selective Java interop, persistent vars across evaluations.

What We Took From Others

Pi — extension system design (activation guards, lifecycle hooks). Pi’s extensions persist state via appendEntry() into JSONL session files — append-only, per-session, no structured queries. Vis gives extensions a shared SQLite DB with versioned snapshots, queryable across conversations.

Claude Code / OpenCode — speed matters, permissions kill flow. Vis uses the sandbox as the permission system.

Hermes — ambitious 5-layer memory architecture. But a 10K-line monolith with undocumented heuristics. Vis keeps every iteration inspectable with full provenance in SQLite.