Private · Go agent harness

An agent harness in plain Go that survives a restart.

török /ˈtørøk/ · Hungarian for Turk. Named after Kempelen's chess automaton, the machine that played well because a person sat inside. Torok keeps the person in the loop on purpose.

Torok is the harness: it runs the tool loop, checks typed answers and commits progress, so a run resumes after a person answers or the process comes back. You bring a model and ordinary Go functions. Desk runs on it.

What it gives you

The surface is small on purpose: generic methods where a type belongs on a value, plain functions for behaviour, named config fields. No graph DSL, no builder chain. The whole shape fits in three statements.

// An ordinary Go function becomes a tool.
quote := tool.Func("quote", "Last close for a symbol", lastClose, tool.ReadOnly())

// A model and its tools become an agent.
analyst, err := torok.New(torok.Config{Model: llm, Tools: []tool.Tool{quote}})

// A question returns a typed, checked answer. Interrupted? It resumes.
answer, err := analyst.RunAs[Close](ctx, "What did NVDA close at?")

Plain Go

An agent is configuration. A workflow is a function of a context, a Flow and your input. A tool is func(ctx, In) (Out, error). RunAs[T] names the answer type once.

Progress that survives

Model calls commit before any tool runs. Workflows replay from checkpoints. A question or an approval parks the run and frees the worker. The answer resumes it once, even if it arrives twice.

One set of limits

Calls, steps, depth, concurrency, active time and tokens are set on the root and shared by every child. A spend ledger reserves money before the request and settles on the receipt.

Effects with receipts

Intent commits before an effect starts. A lost result waits for reconciliation instead of a blind retry. An approval freezes the exact call it approves.

A resident service

For agents that stay on: a durable inbox, timers, bounded memory, observers with their own freshness, daily admission budgets, pause. Idle costs nothing.

Adapters, not a framework

Responses and Chat Completions. Stdio MCP with a frozen catalogue. Bounded HTTPS and processes. An HTTP and SSE surface you mount in your own server.

  • queued
  • running
  • waiting
  • completed
  • accepted
  • failed
  • cancelled

Runtime architecture

The harness sits between your functions and the store. Model and tool adapters are the only code that leaves the process, and both report to the same limits. The resident service and the HTTP adapter sit on the same runtime. There is no second engine.

Architecture diagram of Torok: the application's agents, workflows and tools on the left, the execution engine with its limits, checks and recovery in the middle, memory or SQLite storage below it, model and tool adapters reaching providers and external systems on the right, with the resident service and the HTTP adapter as optional layers
One engine, one store, two kinds of adapter, and the optional resident service around them. Amber marks a paid model call.

Checked rather than promised

  • Race tests cover storage, sessions, typed repair, shared child limits, nested concurrency, approvals, ambiguous commits and event replay.
  • Child-process tests kill the process after a real effect and before its receipt is saved, then require reconciliation without repeating the effect.
  • A Lean 4 model of the acceptance boundary proves that a result is accepted only with an explained, passing check for every declared criterion, bound to that exact candidate and its original input. CI runs it and also checks that removing the guard makes the proof fail.
  • Model quality is a separate question. Scripted fixtures prove execution. What a live model does with the same tools needs real tasks, a budget and a human grader.

Status

Private, experimental API, Go 1.27, three direct dependencies: SQLite, JSON Schema, a PDF reader. If you want to argue about what "accepted" should mean, get in touch.