This repository has been archived on 2026-08-16. You can view files and clone it, but cannot push or open issues or pull requests.
Go to file
Levi Neely 5eb93f181b Add concurrent Go sub-agent example to README
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 03:07:37 +02:00
internal/p9 Add reply file for agent chaining 2026-04-10 02:10:36 +02:00
LICENSE Initial commit: olliesrv 9P server for ollie agent sessions 2026-04-10 01:46:48 +02:00
README.md Add concurrent Go sub-agent example to README 2026-04-10 03:07:37 +02:00
go.mod Initial commit: olliesrv 9P server for ollie agent sessions 2026-04-10 01:46:48 +02:00
go.sum Initial commit: olliesrv 9P server for ollie agent sessions 2026-04-10 01:46:48 +02:00
main.go Initial commit: olliesrv 9P server for ollie agent sessions 2026-04-10 01:46:48 +02:00
mkfile Initial commit: olliesrv 9P server for ollie agent sessions 2026-04-10 01:46:48 +02:00

README.md

olliesrv

A 9P server that exposes ollie agent sessions as a virtual filesystem. Mount it with 9pfuse and interact with AI sessions using ordinary shell tools.

The goal is integration, not self-sufficiency. Rather than providing orchestration, scheduling, or workflow primitives, olliesrv exposes a stable surface — sessions as directories, conversation as files — and defers everything else to the surrounding environment. Scripting, chaining, monitoring, and automation come from composing olliesrv with tools that already exist, not from building those capabilities into the server.

Filesystem layout

ollie/
  ctl                   write: "new [backend=x] [model=x] [agent=x]" | "kill <session-id>"
  <session-id>/
    prompt              write: submit a prompt to the agent (clears reply)
    chat                read:  cumulative conversation history
    reply               read:  assistant text from the most recent turn only
    state               read:  current agent state (idle, thinking, calling: <tool>)
    ctl                 write: stop | interrupt | /<slash-command>
    backend             r/w:   active backend name
    agent               r/w:   active agent name
    model               r/w:   active model name

Building

mk

Installs olliesrv to $HOME/bin.

Usage

olliesrv start       # start daemon (backgrounds itself)
olliesrv fgstart     # start in foreground
olliesrv stop        # stop daemon
olliesrv status      # check if running

The server listens on a Unix socket in the Plan 9 namespace ($NAMESPACE/ollie) and optionally mounts via 9pfuse to $HOME/mnt/ollie (or $OLLIE_9MOUNT).

Sessions

Create a session

echo new > ~/mnt/ollie/ctl                                        # all defaults
echo "new backend=ollama" > ~/mnt/ollie/ctl                       # specific backend
echo "new backend=ollama model=qwen3:8b" > ~/mnt/ollie/ctl        # backend + model
echo "new backend=ollama model=qwen3:8b agent=myagent" > ~/mnt/ollie/ctl

All options are optional and can be specified in any order. Unrecognised keys are rejected. Valid keys: backend, model, agent.

A new session directory appears under the mount point named by timestamp + random suffix (e.g. 20260410-014002-ba70fc).

Send a prompt

echo "what files are in the current directory?" > ~/mnt/ollie/<session-id>/prompt

Writes dispatch asynchronously on close, so the shell returns immediately. The agent runs in the background.

Read the conversation

cat ~/mnt/ollie/<session-id>/chat             # full history snapshot
tail -f ~/mnt/ollie/<session-id>/chat         # follow output as it arrives

The chat file is an append-only log of the full conversation. Format:

user: <prompt>
assistant: <response>
-> <tool>(<args>)
= <result>

Check agent state

cat ~/mnt/ollie/<session-id>/state
# idle | thinking | calling: <toolname>

Control a session

echo stop > ~/mnt/ollie/<session-id>/ctl          # interrupt the current turn
echo /compact > ~/mnt/ollie/<session-id>/ctl      # summarize context
echo /clear > ~/mnt/ollie/<session-id>/ctl        # clear session history
echo /model qwen3:8b > ~/mnt/ollie/<session-id>/ctl

ctl accepts stop/interrupt or any /slash-command supported by the agent. Arbitrary text is rejected.

Switch backend, model, or agent

echo ollama > ~/mnt/ollie/<session-id>/backend
echo qwen3:8b > ~/mnt/ollie/<session-id>/model
echo myagent > ~/mnt/ollie/<session-id>/agent

Kill a session

echo "kill <session-id>" > ~/mnt/ollie/ctl

Ideas

Automation & Scripting

Scripting and automation — shell scripts that submit prompts, poll state until idle, then read chat for the result. No SDK, no HTTP client, just file I/O. Works in any language that can write to a file.

Multiplexing sessions — run several agents in parallel, each in their own session directory, and fan work out to them from a shell script. Coordinate by watching their state files.

Hooks and watchers — poll state in a loop to trigger actions when the agent finishes, or use tail -f chat to react to new output. Build lightweight event-driven pipelines with standard shell tools.

Tooling & Integration

Unix tools — pipe chat into grep, awk, sed. Diff two sessions' outputs. Log conversations with cp. Search history across sessions with grep -r.

Editor integration — any editor that can read/write files gets agent access for free. In acme or sam, you write to prompt and read back chat with no plugin required. Fits naturally into the Plan 9 workflow.

Lightweight TUI alternatives — watch cat state as a status bar, tail -f chat in one pane, prompt submission in another. Compose a working interface entirely from standard tools.

Network & Remote Access

Remote access — 9P is a network protocol. Export the namespace over the network and access agent sessions from another machine using the same file interface, with no additional daemon or API layer.

Multi-Agent Workflows

Each session exposes a reply file containing only the assistant text from the most recently completed turn. Writing to prompt clears reply as a side-effect. Together these make agent-to-agent handoffs expressible as plain file operations — no message queue, no orchestration framework, no shared memory.

A shell script moves text between sessions. The agents communicate only through what the script explicitly passes between them, so information flow is fully under your control. Each agent can use a different backend, model, or tool configuration. A human can intervene at any point by writing directly to a session's prompt.

Example: develop → review → test

Three sessions with specialized agent configs run a feedback loop. The reviewer ends its response with LGTM (proceed) or PTAL (revise). The tester ends with Approved (done) or Rejected (revise). A shell script reads the verdict with grep and routes accordingly.

#!/bin/sh
cd ~/mnt/ollie

echo "new agent=developer" > ctl
echo "new agent=reviewer"  > ctl
echo "new agent=tester"    > ctl

dev=$(ls -d [0-9]* | sed -n '1p')
rev=$(ls -d [0-9]* | sed -n '2p')
tst=$(ls -d [0-9]* | sed -n '3p')

wait_reply() {
    while [ "$(wc -c < $1/reply)" -eq 0 ]; do sleep 1; done
}

echo "implement a function that parses a JSON config file" > $dev/prompt

while true; do
    wait_reply $dev
    code=$(cat $dev/reply)

    # review phase
    printf "Review the following code. End your response with LGTM if it is ready, or PTAL if it needs revision.\n\n%s" "$code" > $rev/prompt
    wait_reply $rev

    if ! grep -qi "LGTM" $rev/reply; then
        { echo "revise this code based on the feedback below."
          echo "--- code ---";     echo "$code"
          echo "--- feedback ---"; cat $rev/reply
        } > $dev/prompt
        continue
    fi

    # test phase
    printf "Test the following code. End your response with Approved if all tests pass, or Rejected if they do not.\n\n%s" "$code" > $tst/prompt
    wait_reply $tst

    if grep -qi "Approved" $tst/reply; then
        echo "done."
        echo "$code"
        break
    fi

    { echo "fix the failures reported below."
      echo "--- code ---";        echo "$code"
      echo "--- test report ---"; cat $tst/reply
    } > $dev/prompt
done

Each agent only sees what the script explicitly sends it. Each can use a different backend, model, or tool configuration. A human can intervene at any point by writing directly to a session's prompt. The boundary between fully automated and human-in-the-loop is just whether the script pauses to ask.

Sub-Agents

A session can spawn ephemeral child sessions to handle a focused task, then tear them down when done. The parent uses execute_code to write new to ctl, send an initial context and task to the child's prompt, poll until reply is ready, read the result, and write kill <id> to ctl. From the child's perspective it is just a normal session; it has no knowledge of its own ephemerality.

Sub-agents as function calls — the parent primes the child with exactly the context it needs (excerpts from its own chat, relevant beads, local file contents) and receives a single focused reply. Failures are isolated; a child that errors or stalls can be killed and retried without affecting the parent. The pattern composes naturally: a sub-agent can itself spawn sub-agents, building a call tree bounded only by the number of sessions open at once.

The coordination logic can be written in any language that reads and writes files. The following Go example fans out N sub-agents concurrently, waits for all replies, and collects the results:

package main

import (
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"time"
)

var base = filepath.Join(os.Getenv("HOME"), "mnt", "ollie")

func main() {
	tasks := []string{
		"summarize the key ideas in functional programming",
		"summarize the key ideas in object-oriented programming",
		"summarize the key ideas in logic programming",
	}
	for i, reply := range runSubAgents(tasks) {
		fmt.Printf("=== agent %d ===\n%s\n", i+1, reply)
	}
}

func runSubAgents(tasks []string) []string {
	type result struct {
		idx   int
		reply string
	}
	results := make([]string, len(tasks))
	ch := make(chan result, len(tasks))
	var wg sync.WaitGroup

	for i, task := range tasks {
		wg.Add(1)
		go func(idx int, task string) {
			defer wg.Done()
			sid := spawnSession()
			defer killSession(sid)
			os.WriteFile(filepath.Join(base, sid, "prompt"), []byte(task), 0644)
			ch <- result{idx, waitReply(filepath.Join(base, sid, "reply"))}
		}(i, task)
	}
	go func() { wg.Wait(); close(ch) }()
	for r := range ch {
		results[r.idx] = r.reply
	}
	return results
}

// spawnSession is serialised to avoid a race between snapshot and detection.
var spawnMu sync.Mutex

func spawnSession() string {
	spawnMu.Lock()
	defer spawnMu.Unlock()
	before := sessionIDs()
	os.WriteFile(filepath.Join(base, "ctl"), []byte("new\n"), 0644)
	for {
		for id := range sessionIDs() {
			if !before[id] {
				return id
			}
		}
		time.Sleep(100 * time.Millisecond)
	}
}

func sessionIDs() map[string]bool {
	entries, _ := os.ReadDir(base)
	ids := make(map[string]bool)
	for _, e := range entries {
		if e.IsDir() {
			ids[e.Name()] = true
		}
	}
	return ids
}

func waitReply(path string) string {
	for {
		if info, err := os.Stat(path); err == nil && info.Size() > 0 {
			data, _ := os.ReadFile(path)
			return strings.TrimSpace(string(data))
		}
		time.Sleep(500 * time.Millisecond)
	}
}

func killSession(sid string) {
	os.WriteFile(filepath.Join(base, "ctl"), []byte("kill "+sid+"\n"), 0644)
}

Each goroutine spawns, primes, waits, and cleans up independently. The only serialised step is session creation, to avoid a race between snapshotting existing sessions and detecting the new one.

Self-Generating Workflows

Since agents have access to execute_code, a session can write and execute a workflow script without any human involvement. Given a task and knowledge of the filesystem layout, an agent can decompose the work, spawn sessions, write the coordination script, and run it — all in a single turn. The README you are reading is essentially its system prompt. Conductor functionality, for free.

Persistent Memory

Task tracking — Agent sessions are ephemeral by nature: context windows fill, processes crash, work gets lost. 9beads addresses this by exposing a structured, version-controlled task database as a 9P filesystem. Any session can read the task list, claim work, track dependencies, and mark completion through ordinary file operations. If a session is interrupted, another can pick up exactly where it left off. Task lists are scoped per-project, keeping them focused and bounded. Combined with olliesrv, agents get both a communication surface and a memory layer, with no special integration required between them.

Example shell session

$ echo new > ~/mnt/ollie/ctl
$ ls ~/mnt/ollie/
20260410-014002-ba70fc  ctl
$ sid=20260410-014002-ba70fc
$ tail -f ~/mnt/ollie/$sid/chat &
$ echo "list the go files in /home/lkn/src/ollie" > ~/mnt/ollie/$sid/prompt
user: list the go files in /home/lkn/src/ollie
assistant: -> execute_code({"code":"find /home/lkn/src/ollie -name '*.go'","language":"bash"})
= pkg/agent/core.go
pkg/agent/loop.go
...
assistant: The Go source files are: core.go, loop.go, ...
$ cat ~/mnt/ollie/$sid/state
idle