Gnosh: an AI harness that thinks in Gno

An AI harness that thinks in Gno.

Gnosh is functional AI: the agent's context is a call stack. It runs the Gno VM in the AI harness and keeps the agent's plan, what it found and the facts you gave it in mem, in Gno on your disk. Each step of a task is a frame: when it returns, its work leaves the model's context and its result stays, as a function's locals go when it returns. A new session starts with what is still open and the newest facts, so it picks up where the last one stopped.

Underneath, it is a security-first fork of Charm's Crush: any model, hosted or on your own machine, and nothing phones home unless you turn it on.

Install★ Star on GitHub▶ Functional AI is faster▶ Three sessions, 2.2–3.4× faster▶ 1,000 agents on gno.land

Functional AI is faster: an ordinary agent's context beside Gnosh's, head to head; Gnosh finishes first
https://gnosh.ai/head-to-head.html
A real Gnosh run, replayed beside an ordinary agent doing the same calls on the same box. The ordinary agent's context fills up and stops to be summarized; Gnosh's is a stack, and each step it finishes leaves it, so it finishes first.
https://gnosh.ai/peers.html
Three Gnosh sessions working one plan in mem, measured on a rented H100: 2.2× and 3.4× faster than one session, every test passing.
https://gnosh.ai/swarm.html
The same plan as a realm on gno.land, worked by up to 1,000 agents at once: every todo done exactly once, even when agents are killed or the chain stops.
https://gnosh.ai/priorart.md
Who else folds an agent's context, from Context-Folding to NVIDIA, and how Gnosh differs.
A real run, recorded in Ghostty, with the waiting cut short: Qwen3.8 27B on a home AI box works out the backoff in Gno, plans a task in mem, and a new session finishes it.

Install

Built from source, with Go 1.27 or newer:

git clone https://github.com/gnosh-ai/gnosh
cd gnosh && go build -o gnosh .
mkdir -p ~/.local/bin
install -m 755 gnosh ~/.local/bin/gnosh

The code and its issues are on GitHub; a star there helps others find it.

Pick a model

Run gnosh in a project and press ctrl+l to pick a hosted one. For a model on your own machine, add it in ~/.config/gnosh/gnoshrc:

provider add local --type llamacpp --base-url "http://127.0.0.1:8080"
# a local model can take minutes to read a long prompt
option request-timeout 900

Thinks in Gno: an answer worked out, not estimated

youA client retries a failed request with exponential backoff: it waits 200 ms before the first retry, doubles the wait each time, and never waits more than 30 s. How long after the first failure does the 10th retry go out?

The agent works it out in the session's scratch machine instead of adding it up in its head, printing every wait, and answers "111 s". In five real runs on 30 October with Qwen thinking, as it does by default, it printed every wait in a machine and got 111 s every time; with its thinking turned off it was right in 4 runs of 5, and the one miss ran its loop one wait short. When a loop prints only its result, a note under the output tells the model to print the terms too and run it again: over 28 scored runs, the answer was right in 18 of 18 when the machine printed every term, and in 2 of 10 when it showed one line.

In Gno:

w := 200
total := 0
for i := 1; i <= 10; i++ {
	if w > 30000 {
		w = 30000
	}
	total += w
	fmt.Println("retry", i, "wait", w, "ms  cumulative", total, "ms")
	w *= 2
}
fmt.Println("the 10th retry goes out after", total, "ms =", total/1000, "s")retry 1 wait 200 ms  cumulative 200 ms
retry 2 wait 400 ms  cumulative 600 ms
retry 3 wait 800 ms  cumulative 1400 ms
…
retry 9 wait 30000 ms  cumulative 81000 ms
retry 10 wait 30000 ms  cumulative 111000 ms
the 10th retry goes out after 111000 ms = 111 s// one call, in the session's scratch machine

Plans in Gno: a todo that outlives the session

youRename the project from klash to gnosh everywhere: the code, the tests, the README, the Makefile and the config example. Plan it as todos and do the first one now; we'll carry on in another session. It's done when nothing in the project mentions klash and go test ./... passes.

The agent puts the task in mem as a todo with five todos under it, the check last, steps into the task and, with mem.Next(), into its first todo. It renames the code and the tests, pops the todo with what came of it, and keeps what the next ones need as a thought.

A frame is a todo seen as a stretch of the conversation. mem.Next(), or .Push() on a todo, marks where it begins; mem.Pop("what came of it") ends it, and from then on Gnosh leaves every message between the two out of what the model is sent, with the pop and its result as the last line. Nothing is deleted: you still see all of it, and the transcript keeps it. Only the model's context shrinks, the way a stack frame goes when a function returns and only its value comes back. No more compactions to get a long task through: a todo's work leaves the context as the todo finishes, and when one todo's own work outgrows the context, the oldest rounds of its frame slide out instead of a summary.

Line by line, where the agent stands (its stack, bottom to top) and what mem holds:

mem.Todo("rename the project").Push()stack: rename-project · tree: rename-project
a todo made at the root, and stepped intomem.Todo("rename it in the code")stack: rename-project · tree: rename-project › rename-code
a todo goes under the frame you stand inmem.Todo("rename it in the tests")stack: rename-project · tree: rename-project › rename-code, rename-testsmem.Next()stack: rename-project › rename-code
steps into the next open todo under where you standmem.Pop("go.mod and main.go say gnosh")stack: rename-project · rename-code is done, its result kept
everything since its push leaves the model's contextmem.Next()stack: rename-project › rename-tests

A todo cannot be popped while one under it is open: nothing mentioning klash and the tests passing is something the task has to reach, not claim. The next session is told, with your first message, what is open and what is next, and carries on.

In Gno, the first session, a real run on 31 October 2026:

mem.Todo("rename the project from klash to gnosh everywhere").Push()
mem.Todo("rename klash to gnosh in the code and tests")
mem.Todo("rename klash to gnosh in the README")
mem.Todo("rename klash to gnosh in the Makefile")
mem.Todo("rename klash to gnosh in the config example file")
mem.Todo("verify nothing in the project mentions klash and go test ./... passes")
mem.Fact("cmd/test", "go test ./...")
mem.Next()added `project-rename-to-gnosh` "rename the project from klash to gnosh everywhere"
in `project-rename-to-gnosh` "rename the project from klash to gnosh everywhere": when it is done, pop it with what came of it and what the next todo needs. Everything from here until the pop is removed from your context then, what you were told included; what matters after, keep as a thought first. Your result stays, and nothing you did is undone.
added `project-rename-to-gnosh/code-tests-rename-klash` "rename klash to gnosh in the code and tests"
added `project-rename-to-gnosh/readme-rename-klash` "rename klash to gnosh in the README"
added `project-rename-to-gnosh/makefile-rename-klash` "rename klash to gnosh in the Makefile"
added `project-rename-to-gnosh/config-example-rename-klash` "rename klash to gnosh in the config example file"
added `project-rename-to-gnosh/verify-no-klash-mentions` "verify nothing in the project mentions klash and go test ./... passes"
fact `cmd/test` is new
in `project-rename-to-gnosh/code-tests-rename-klash` "rename klash to gnosh in the code and tests": when it is done, pop it with what came of it and what the next todo needs. …// go.mod, main.go and main_test.go edited, the tests run; thenmem.Fact("cmd/test", "/usr/local/go/bin/go test ./... (go is not in PATH on this machine)")
mem.Pop("go.mod module is gnosh; main.go doc comment and const name, main_test.go expected strings all renamed to gnosh; /usr/local/go/bin/go test ./... passes (ok gnosh); no klash left in *.go/go.mod")
mem.Think("where-stand", "code+tests done. Remaining: readme-rename-klash (README.md), makefile-rename-klash (Makefile builds -o klash), config-example-rename-klash (file klash.example.toml itself must be renamed to gnosh.example.toml plus its contents, and README references klash.toml too), then verify-no-klash-mentions. Use /usr/local/go/bin/go.")
mem.Next()fact `cmd/test` changed (was "go test ./...")
done `project-rename-to-gnosh/code-tests-rename-klash` "rename klash to gnosh in the code and tests" → go.mod module is gnosh; main.go doc comment and const name, main_test.go expected strings all renamed to gnosh; /usr/local/go/bin/go test ./... passes (ok gnosh); no klash left in *.go/go.mod
thought `where-stand` kept on `project-rename-to-gnosh`
in `project-rename-to-gnosh/readme-rename-klash` "rename klash to gnosh in the README": when it is done, pop it with what came of it and what the next todo needs. …
Everything since you entered `project-rename-to-gnosh/code-tests-rename-klash` was removed from your context: this result stays; the user still sees all of it. In it you edited go.mod, main.go, main_test.go. Your reads inside it are forgotten: view a file before you edit it.

The next session, told only “Let's carry on with the rename.”, looks, then takes the todos left one frame at a time:

mem.Show()mem: 1 todo open · 1 fact
todos:
  [ ] `project-rename-to-gnosh` "rename the project from klash to gnosh everywhere" (1/5 done · session 39e9ec42 · 1m ago · held by session 39e9ec42, 1m ago)
    [x] `project-rename-to-gnosh/code-tests-rename-klash` "rename klash to gnosh in the code and tests" → go.mod module is gnosh; main.go doc comment and const name, main_test.go expected strings all renamed to gnosh; /usr/local/go/bin/go test ./... passes (ok gnosh…
    [ ] `project-rename-to-gnosh/readme-rename-klash` "rename klash to gnosh in the README" (held by session 39e9ec42, 1m ago)
    [ ] `project-rename-to-gnosh/makefile-rename-klash` "rename klash to gnosh in the Makefile"  ← next
    [ ] `project-rename-to-gnosh/config-example-rename-klash` "rename klash to gnosh in the config example file"
    [ ] `project-rename-to-gnosh/verify-no-klash-mentions` "verify nothing in the project mentions klash and go test ./... passes"
facts, newest first:
  `cmd/test` = /usr/local/go/bin/go test ./... (go is not in PATH on this machine)mem.Frame("project-rename-to-gnosh/makefile-rename-klash").Push()in `project-rename-to-gnosh/makefile-rename-klash` "rename klash to gnosh in the Makefile": when it is done, pop it with what came of it and what the next todo needs. …// the Makefile edited, the build run; thenmem.Pop("Makefile build target now emits the gnosh binary (Makefile:2, `go build -o gnosh .`); verified by running that exact command; no klash mentions remain in the Makefile; note: make is not installed on this machine")done `project-rename-to-gnosh/makefile-rename-klash` "rename klash to gnosh in the Makefile" → Makefile build target now emits the gnosh binary (Makefile:2, `go build -o gnosh .`); verified by running that exact command; no klash mentions remain in the Makefile; note: make is not installed on this machine
next: `project-rename-to-gnosh/config-example-rename-klash` "rename klash to gnosh in the config example file": mem.Next()
Everything since you entered project-rename-to-gnosh/makefile-rename-klash was removed from your context: this result stays; the user still sees all of it. In it you edited Makefile. Your reads inside it are forgotten: view a file before you edit it.mem.Next()in `project-rename-to-gnosh/config-example-rename-klash` "rename klash to gnosh in the config example file": when it is done, pop it with what came of it and what the next todo needs. …// the config example renamed and edited; a thought for the README todomem.Think("readme-crossref", "README.md line 8 references klash.example.toml — point it at gnosh.example.toml when the readme todo runs")thought `readme-crossref` kept on `project-rename-to-gnosh/config-example-rename-klash`mem.Pop("klash.example.toml renamed to gnosh.example.toml; contents now say gnosh/gnosh.toml/gnosh.log; main.go does not parse the toml (defaults only), so no code change needed; README still references the old filename (held by other session)")done `project-rename-to-gnosh/config-example-rename-klash` "rename klash to gnosh in the config example file" → klash.example.toml renamed to gnosh.example.toml; contents now say gnosh/gnosh.toml/gnosh.log; main.go does not parse the toml (defaults only), so no code change needed; README still references the old filename (held by other session)
next: `project-rename-to-gnosh/verify-no-klash-mentions` "verify nothing in the project mentions klash and go test ./... passes": mem.Next()
Everything since you entered project-rename-to-gnosh/config-example-rename-klash was removed from your context: this result stays, and its 1 thought is in mem (mem.Show("project-rename-to-gnosh/config-example-rename-klash") lists it when you need it); the user still sees all of it. In it you edited gnosh.example.toml. Your reads inside it are forgotten: view a file before you edit it.mem.Next()in `project-rename-to-gnosh/verify-no-klash-mentions` "verify nothing in the project mentions klash and go test ./... passes": when it is done, pop it with what came of it and what the next todo needs. …// the README's five mentions renamed, the grep empty, the tests green; thenmem.Pop("README.md: all 5 klash mentions renamed to gnosh (title, description, build/run commands, tol filename refs); case-insensitive grep for klash across the project now finds zero files; go test -count=1 ./... passes (ok gnosh 0.003s)")done `project-rename-to-gnosh/verify-no-klash-mentions` "verify nothing in the project mentions klash and go test ./... passes" → README.md: all 5 klash mentions renamed to gnosh (title, description, build/run commands, tol filename refs); case-insensitive grep for klash across the project now finds zero files; go test -count=1 ./... passes (ok gnosh 0.003s)
next: nothing in `project-rename-to-gnosh` can be done now: `project-rename-to-gnosh/readme-rename-klash` is held by session 39e9ec42
Everything since you entered `project-rename-to-gnosh/verify-no-klash-mentions` was removed from your context: this result stays; the user still sees all of it. In it you edited README.md. Your reads inside it are forgotten: view a file before you edit it.

It did the todos left, renamed the README on the way, and reported the rename complete: nothing mentions klash and the tests pass, every check of the run's harness green. The first session's thought where-stand was in mem for it too. The first session had stepped into the README todo before it ended, and a todo is held by the session that entered it until that session pops it; so the second session worked the free ones and left the holder a fact.

Plans in Gno: a todo done is a todo out of the context

youMake go test ./... pass by fixing parse.go, not the test. Keep the goal in mem, note each approach you try and why it fails, and re-plan when one does not work.

What a todo learns beyond its result, the agent keeps first as a thought, mem.Think("the shared port is 8080"): a line in mem that outlives the pop, read back by name or by meaning. Everything else inside the todo, a file it read, a test's output, a question you answered there, leaves with it, so the context holds the plan, the results so far and the present todo's work.

In Gno, a real run on 31 October 2026; the last lines of a result are Gnosh's, saying what each pop removed:

mem.Todo("make go test ./... pass by fixing parse.go").Push()added `fix-go-tests` "make go test ./... pass by fixing parse.go"
in `fix-go-tests` "make go test ./... pass by fixing parse.go": when it is done, pop it with what came of it and what the next todo needs. Everything from here until the pop is removed from your context then, what you were told included; what matters after, keep as a thought first. Your result stays, and nothing you did is undone.// strconv tried in the scratch machine; then the first approach, a todo under the taskmem.Todo("approach 1: trim spaces, strconv.ParseInt base 0").Push()added `fix-go-tests/parse-int-base-zero` "approach 1: trim spaces, strconv.ParseInt base 0"
in `fix-go-tests/parse-int-base-zero` "approach 1: trim spaces, strconv.ParseInt base 0": when it is done, pop it with what came of it and what the next todo needs. …// the fix in parse.go, the suite green at the first try; thenmem.Pop("parse.go fixed: TrimSpace + strconv.ParseInt(base 0) passes all tests. Approach 1 worked, no re-plan needed. My Gno check had predicted failure because Gno's strconv rejects underscores (\"1_000\" -> invalid syntax), but Go's strconv accepts single underscores between digits and rejects 1__0 and 0x; TrimSpace covers \" 12 \".")done `fix-go-tests/parse-int-base-zero` "approach 1: trim spaces, strconv.ParseInt base 0" → parse.go fixed: TrimSpace + strconv.ParseInt(base 0) passes all tests. Approach 1 worked, no re-plan needed. My Gno check had predicted failure because Gno's strconv rejects underscores ("1_000" -> invalid syntax), but Go's strconv accepts single underscores between digits and rejects 1__0 and 0x; TrimSpace covers " 12 ".
next: pop `fix-go-tests` with the evidence that it holds
Everything since you entered `fix-go-tests/parse-int-base-zero` was removed from your context: this result stays; the user still sees all of it. In it you edited parse.go. Your reads inside it are forgotten: view a file before you edit it.mem.Fact("cmd/go", "/usr/local/go/bin/go (go is not in PATH; vet and test with the full path)")
mem.Pop("go test ./... passes: ok numparse (go vet clean). parse.go now trims spaces then strconv.ParseInt(s, 0, 64); test file untouched.")fact `cmd/go` is new
done `fix-go-tests` "make go test ./... pass by fixing parse.go" → go test ./... passes: ok numparse (go vet clean). parse.go now trims spaces then strconv.ParseInt(s, 0, 64); test file untouched.
Everything since you entered `fix-go-tests` was removed from your context: this result stays; the user still sees all of it. In it you edited parse.go. In it you also changed mem: Todo `approach 1: trim spaces, strconv.ParseInt base 0`. Your reads inside it are forgotten: view a file before you edit it.

The first approach held, so there was nothing to re-plan; the model had tried strconv in the scratch machine first, where Gno's strconv rejects underscores, and kept the difference in the todo's result. The tests are green and the test file untouched. The model server's cache is a prefix of the conversation, so a pop truncates it back to the push and the next request re-reads only the pop: in the server's log of an earlier run of this task, the request after the 3K-token pop re-read 2,572 tokens instead of the whole 20K-token conversation, and the requests after it were back to a few hundred each. The cache survived the cut.

Remembers in Gno: facts that stay current

youRemember these for later sessions: the staging database is db-stg-7, and the on-call engineer is Sam Okafor.

you, a week laterAn update for what you remember: the staging database moved to db-stg-9.

you, any later sessionWhich staging database should I use, and who is on call?

A new session is given the newest facts with your first message, so a fact that changes is set again under its name, not kept twice. In a real run the last session answered without a single call to mem: "Staging database: db-stg-9. On call: Sam Okafor."

In Gno:

mem.Fact("fact/staging-db", "the staging database is db-stg-7")
mem.Fact("fact/on-call", "the on-call engineer is Sam Okafor")fact `fact/staging-db` is new
fact `fact/on-call` is new; `fact/` also has `fact/staging-db`// a week latermem.Fact("fact/staging-db", "the staging database is db-stg-9")fact `fact/staging-db` changed (was "the staging database is db-stg-7")// any later session: no call; it starts with the newest facts

Remembers in Gno: a reminder for next time

youBefore we wrap up: remind me next time we talk to rotate the staging API key. It expires this week.

"Morning! Anything I should know before I start?" in the next session brings it back: in a real run on 31 October 2026 the new session answered, with no call to mem, "Two things from mem: Staging API key expires this week (~2026-11-06), that's your reminder. Goal rotate-staging-api-key is open and waiting until then. Today is 2026-10-31, so you've got ~5 days of runway." A session that starts on something else raises it too. It is a reminder for the next session, not an alarm.

In Gno, the first session; the next one starts with what is due and the newest facts, and needs no call:

mem.Todo("rotate the staging API key").Wait("2026-11-06")added `rotate-staging-api-key` "rotate the staging API key"
`rotate-staging-api-key` "rotate the staging API key" waits until 2026-11-06mem.Fact("staging-api-key", "staging API key expires this week (~2026-11-06); user asked to be reminded next time we talk")fact `staging-api-key` is new

These are real runs with an open-weight model, and GNOMEM has them, with how often each passed.

How the agent uses Gno

Each of those calls is Gno code the agent writes and Gnosh runs in the Gno VM, against mem, a realm kept on your disk for the project. A call changes mem only through mem's own methods; every change is journaled before it is kept, and a call that fails keeps nothing. A new session starts with your rules, what is due, what waits on you, what is open with its next todo, and the newest facts, and stands in the frame it stopped in.

The calls, as the agent writes them:

// a task and the todos under it; a small model names each from its wordsr := mem.Todo("release v2")
r.Todo("bump the version")
r.Todo("tag v2.0.0")
r.Todo("CI is green")// step into the task, then its first todo: from here the context will be cutmem.Frame("release-v2").Push()
mem.Next()// a thought: a line on the todo, kept in mem after the todo is gone; named the same waymem.Think("the version lives in version.go, not in the Makefile")// what came of the todo; everything since the push leaves the context, and the next todo beginsmem.Pop("now 2.0.0")
mem.Next()// what it found, a todo that cannot work and why, and the one that replaces itmem.Note("build-greet/make-test", "make is not installed here")
mem.Drop("build-greet/make-test", "no make on this machine")
mem.Frame("build-greet").Todo("test.sh runs go test ./...")// a fact, under a name; a search by words; a recall by meaning, thoughts includedmem.Fact("cmd/ship", "make ship ENV=prod")
mem.Find("sign-off")
mem.Recall("where the version is set")// what is open, and what is nextmem.Show()

You can look too: /mem shows the outline, /mem find searches, /mem log lists every change, and /mem undo takes one back.

Also in the fork