18 KiB
Usage
Server
Start and stop
olliesrv # start the server (foreground)
# Stop by sending SIGTERM or via the supervisor (e.g. rc-service ollie stop)
TCP listener
To also accept connections over TCP (e.g. for remote access):
olliesrv -tcp :9564
Debug logging
Log level is controlled by OLLIE_LOG (default: warn). Set to debug for verbose output:
OLLIE_LOG=debug olliesrv
Valid levels: debug, info, warn, error. Per-subsystem overrides follow the pattern
OLLIE_<TAG>_LOG=debug where <TAG> matches the logger's tag (e.g. OLLIE_SESSION_LOG).
o — Terminal CLI
o is a multi-call shell script that wraps the 9P namespace into a human-friendly interface.
Installed to ~/bin/o by just install-data.
Quick start
eval $(o env myproject default) # set session + agent context
o prompt # interactive prompt REPL
In another terminal:
eval $(o env myproject default)
o tail # stream chat output
Commands
| Command | Description |
|---|---|
o prompt |
Interactive multi-line prompt REPL |
o tail |
Stream chat output |
o read <path> |
Read any file in the namespace |
o write <path> [data] |
Write to any file |
o readloop <path> |
Read in a loop (re-reads after each return) |
o ls [path] |
List namespace entries |
o ctl <cmd> |
Send raw control command to agent |
o stop |
Interrupt the running agent |
o kill |
Kill the active session |
o tui |
Launch tmux TUI |
o env [session] [agent] |
Print export statements for context |
Context
Context is set via $session and $agent environment variables:
eval $(o env myproject default) # export session=myproject agent=default
# Now every o command addresses the right agent automatically:
o read state # → session/myproject/agent/default/state
o readloop statewait
Path resolution
Paths are slash-delimited 9P namespace paths. The o CLI resolves them based
on context:
| Path type | Example | Resolves to |
|---|---|---|
| Root-level | o read models |
models (no context needed) |
| Agent file | o read state |
session/$session/agent/$agent/state |
| Session file | o read env |
session/$session/env |
| Full path | o read session/foo/agent/bar/chat |
Used as-is |
Root-level paths (session/, agents, models, help, ctl, tools) and
absolute paths (starting with session/) bypass context resolution entirely.
Agent-level files (chat, state, prompt, statewait, cfg, plan,
log, cost, offset, context, systemprompt) require both $session
and $agent. Session-level files (env, ctl, id, name, paused)
require only $session.
Prompt REPL
o prompt is an interactive multi-line REPL with readline editing:
$ o prompt
[myproject/default]
Type . on a blank line to send, Ctrl+D to exit.
Slash commands: /stop /compact /kill /clear /model /backend /cwd /help
> write a fibonacci function in python
and include a test case
.
Type your prompt across multiple lines, then send with . on its own line.
Slash commands control the agent without sending a prompt:
| Command | Description |
|---|---|
/stop |
Interrupt running agent |
/compact |
Compact context window |
/kill |
Kill the session (exits REPL) |
/clear |
Clear history |
/model X |
Switch model |
/backend X |
Switch backend |
/cwd X |
Change working directory |
/quit or /q |
Kill the tmux TUI |
/help |
List commands |
One-shot generation
No session or agent needed — o generate sends a prompt and returns a response:
o generate "explain monads in one sentence"
cat error.log | o generate # pipe content as the prompt
echo "write a haiku about filesystems" | o generate | wc -w
Chain multiple steps:
echo "list 5 blog post ideas" | o generate | head -3 | o generate
o tui — tmux Terminal UI
o tui composes o tail and o prompt into a single tmux layout — two shell
commands in two panes, with agent state in the tmux status bar:
┌──────────────────────────────────┐
│ o tail | grep │ ← chat stream
│ │
├──────────────────────────────────┤
│ o prompt │ ← input REPL
└──────────────────────────────────┘
Agent state shown in tmux status bar
idle=green calling=orange thinking=blue paused=gray
Usage
eval $(o env myproject default)
o tui
- Requires
$sessionand$agentto be set. - Creates a tmux session named
o-<session>. - Two panes:
- Pane 0 (top, 80%):
o tail | grep— streams chat output, filters block markers. - Pane 1 (bottom, 20%):
o prompt— multi-line REPL with slash commands.
- Pane 0 (top, 80%):
- A background loop reads
statewaitand updates the tmux status bar instantly. - Focuses the prompt pane and attaches.
No polling. Each read blocks until data arrives. The status bar updates on every
state transition via statewait — zero CPU when idle.
tmux tips
Ctrl+b ;— toggle between last two panes (fast prompt ↔ tail switch)Ctrl+b z— zoom any pane to full screenCtrl+b [— scroll mode (arrow keys,qto exit)Ctrl+b d— detach (session keeps running, reattach withtmux attach -t o-<session>)
Generation parameters
Parameters are set in the agent config JSON and can be overridden at runtime by
writing key=value to the agent config. Runtime overrides take precedence:
echo "temperature=0.3" | ollie-9p write session/mysession/agent/{aname}/cfg
| Key | Type | Description |
|---|---|---|
maxTokens |
int | Max output tokens (0 = no limit) |
maxCompletionTokens |
int | OpenAI o-series alternative to maxTokens |
temperature |
float | Sampling temperature |
topP |
float | Nucleus sampling threshold |
topK |
int | Top-K sampling (Anthropic, Ollama) |
minP |
float | Min-P sampling (Ollama) |
topA |
float | Top-A sampling (Ollama) |
frequencyPenalty |
float | Frequency penalty |
presencePenalty |
float | Presence penalty |
repetitionPenalty |
float | Repetition penalty (Ollama) |
reasoning |
int | Thinking budget in tokens (Anthropic); 0 = disabled |
reasoningEffort |
string | low, medium, high (OpenAI o-series) |
includeReasoning |
bool | Include reasoning in response (OpenRouter) |
responseFormat |
string | json_object, text, etc. (OpenAI) |
stop |
string | Comma-separated stop sequences |
verbosity |
string | Output detail level (internal, not sent to API) |
Example agent config:
{
"prompt": "You are a helpful assistant.",
"temperature": 0.7,
"topP": 0.95,
"reasoning": 10000,
"stop": ["\n\n"]
}
Sandbox
Every shell invocation (and promoted tool call) runs inside a Landlock sandbox
configured by ~/.config/ollie/sandbox/<name>.yaml. The sandbox wraps each
invocation as a new command, so config changes (e.g. granting access to an
additional directory) take effect on the next call without restarting the server
or session.
Multi-agent workflows
Two patterns cover most multi-agent use cases: ephemeral subagent delegation for independent parallel subtasks, and persistent concurrent sessions for workflows that need coordination or long-lived state.
Subagent delegation
subagent_spawn forks N ephemeral agents with a shared prompt, waits for all to
finish, and returns their results concatenated. Use it when work splits into
independent subtasks:
{
"steps": [{"tool": "subagent_spawn", "args": "-n 3 summarize this module"}]
}
Flags
| Flag | Description |
|---|---|
-n N |
Number of subagents to spawn (default: 1) |
-agent A |
Agent config for each subagent |
-backend B |
Backend override |
-model M |
Model override |
-cwd DIR |
Working directory for subagents (default: pwd) |
-keep |
Do not remove sessions after collecting results |
Concurrent sessions
For long-lived parallel agents, create named sessions and pass prompts between them via the filesystem:
o write session/new "name=writer cwd=$PWD"
o write session/new "name=reviewer cwd=$PWD"
agent=$(o ls session/writer/agent | grep -v '^new$')
o write "session/writer/agent/$agent/prompt" "Draft a design doc"
o read "session/writer/agent/$agent/chat" # read writer's response
Key files per agent:
| File | Mode | Description |
|---|---|---|
prompt |
write | Queue a new turn; enqueued if agent is busy |
statewait |
read (blocking) | Blocks until state changes; returns new state |
state |
read | Current state: idle, running, failed: <reason> |
chat |
read | Full conversation history |
context |
read | Full message history as JSONL |
offset |
read | Byte position in chat after the last user prompt |
plan |
r/w | Scratch space for agent planning |
prompt.prev |
read | The last submitted prompt |
env |
read | Session environment variables |
cost |
read | Cumulative cost in USD |
Predefined workflows
When coordination logic is known in advance, a shell script can drive the entire
workflow using named sessions and statewait:
#!/bin/sh
cwd=${1:?usage: $0 <cwd>}
# Create named sessions with agent profiles
o write session/new "name=developer agent=developer cwd=$cwd"
o write session/new "name=reviewer agent=reviewer cwd=$cwd"
o write session/new "name=tester agent=tester cwd=$cwd"
# Discover agent names (first agent in each session)
agent_developer=$(o ls session/developer/agent | grep -v '^new$')
agent_reviewer=$(o ls session/reviewer/agent | grep -v '^new$')
agent_tester=$(o ls session/tester/agent | grep -v '^new$')
send() {
sid=$1; shift
aname=$1; shift
# Open statewait, send prompt, wait for completion, read response
exec 3< <(o readloop "session/$sid/agent/$aname/statewait" &)
sleep 0.1
o write "session/$sid/agent/$aname/prompt" "$*"
until o read "session/$sid/agent/$aname/state" | grep -q idle; do sleep 0.5; done
exec 3<&-
offset=$(o read "session/$sid/agent/$aname/offset" | tr -d '[:space:]')
o read "session/$sid/agent/$aname/chat" | tail -c +$((offset + 1))
}
reply=$(send developer "$agent_developer" "Implement a function that parses a JSON config file.")
while true; do
review=$(send reviewer "$agent_reviewer" "Review the following code. Reply LGTM if ready, or PTAL with feedback.
$reply")
if ! printf '%s' "$review" | grep -qi "LGTM"; then
reply=$(send developer "$agent_developer" "Revise based on this feedback:
$review")
continue
fi
result=$(send tester "$agent_tester" "Test the following code. Reply Approved if all tests pass, or Rejected with details.
$reply")
if printf '%s' "$result" | grep -qi "Approved"; then
printf '%s\n' "$reply"
break
fi
reply=$(send developer "$agent_developer" "Fix the following test failures:
$result")
done
o kill developer
o kill reviewer
o kill tester
The agents have no knowledge of each other — the script holds all the routing logic.
Each agent can use a different agent=, backend=, or model=.
9P filesystem reference
The 9P namespace is the lowest-level interface to ollie. Everything — sessions,
agents, prompts, tools, state — is a file. The o CLI wraps these operations,
but knowing the raw filesystem is useful for scripting, debugging, and
understanding how things work.
Mounting
ollie-9p mount unix!/tmp/ns.$USER.$DISPLAY/ollie ~/mnt/ollie
cd ~/mnt/ollie
ls session/
Or connect remotely:
ollie-9p mount tcp!server:9564 ~/mnt/ollie
Raw session operations
# Create a session
echo "name={sname}" > session/new
# Submit a prompt
echo "fix the bug in main.go" > session/{sname}/agent/{aname}/prompt
# Stream the response
cat session/{sname}/agent/{aname}/chat
# Check state
cat session/{sname}/agent/{aname}/state
# Block until state changes
cat session/{sname}/agent/{aname}/statewait
# Read offset (byte position after user prompt)
cat session/{sname}/agent/{aname}/offset
Network transparency
9P is a network protocol. Mount the agent namespace from any machine and every session, tool, and config value is accessible as if local:
# From another machine:
9p -a 'tcp!server:5640' read session/{sname}/agent/{aname}/state
No SSH tunneling, no port forwarding, no API gateway.
Shell composability
Because operations are file I/O, they compose with the full Unix toolkit:
# Fan out a prompt to all agents
for s in session/*/agent/*/prompt; do echo "run tests" > "$s"; done
# Wait for all agents to finish
for s in session/*/agent/*/statewait; do cat "$s" > /dev/null; done
# Grep all agent plans
grep -r "TODO" session/*/plan
# Monitor costs
cat session/*/agent/*/cost
# Strip block markup from chat output
cat session/{sname}/agent/{aname}/chat | grep -vE '^\[\[\[.*'
Pipe agents into awk. Filter with grep. Schedule with cron. Orchestrate
with a 10-line shell script instead of a framework.
Plan 9 patterns
/net/dnspattern —/complete,/generate,/routeare stateless per-fid: write a request, read back the resultctlfiles — control operations (stop, kill, rename, compact) are writes to a control file, not method calls- Blocking reads —
statewaitblocks until state changes, replacing event subscriptions with a simplecat - Multiplexing — multiple clients mount the same server simultaneously, each with an independent view through per-fid state
/generate — one-shot LLM
echo 'summarize this repo' | ollie-9p rdwr generate
cat main.go | ollie-9p rdwr generate
echo '{"prompt":"explain recursion"}' | ollie-9p rdwr generate
/complete — code completion
Fill-in-the-middle:
echo '{"file":"main.go","prefix":"func ","suffix":""}' | ollie-9p rdwr complete
/route — task routing
echo 'implement login page' | ollie-9p rdwr route
# backend=anthropic model=claude-sonnet-4-20250514
Key files per agent
| File | Mode | Description |
|---|---|---|
prompt |
write | Queue a new turn |
statewait |
read (blocking) | Blocks until state changes |
state |
read | Current state |
chat |
read | Full conversation history |
context |
read | Full message history as JSONL |
offset |
read | Byte position after last user prompt |
plan |
r/w | Scratch space for agent planning |
prompt.prev |
read | The last submitted prompt |
env |
read | Session environment variables |
cost |
read | Cumulative cost in USD |
Store federation
OLLIE_TOOLS_PATH is an ordinary filesystem path. Because the server speaks 9P
and any remote instance can be mounted locally, this var can point at a remote
directory with no code changes — the OS handles the proxying transparently.
Centralized tool distribution
Host one authoritative tool set and point all machines at it:
# On each client machine:
ollie-9p mount toolserver:9564 ~/mnt/toolserver
export OLLIE_TOOLS_PATH=~/mnt/toolserver/t
Set it in ~/.config/ollie/env for persistence:
OLLIE_TOOLS_PATH=~/mnt/toolserver/t
Tool discovery
Tools are executables in $OLLIE_TOOLS_PATH (default:
~/.config/ollie/tools/). At session startup, all .meta sidecar files are
scanned and a compact listing (name + one-line description) is injected into
the system prompt.
Metadata format
Each tool has a .meta JSON sidecar file alongside the executable:
{
"description": "Short description (used in system prompt listing).",
"prompt": "## my_tool\n\nFull documentation shown when tool is loaded.",
"args": {"type":"object","required":["path"],"properties":{"path":{"type":"string","description":"File path"}}},
"tier": "cold",
"readOnly": true
}
| Field | Purpose |
|---|---|
description |
One-liner shown in tool listing |
prompt |
Full documentation injected when tool is loaded |
args |
JSON Schema for tool input parameters |
tier |
Result cacheability: cold, warm, or hot (default: hot) |
readOnly |
Safe for parallel execution with other read tools |
cmd |
Executable path or name override |
sudo |
Requires root privileges; implies elevation |
variants |
Conditional definitions for heterogeneous hosts |
Runtime access
The full prompt for any tool is available on-demand:
echo 'file_edit' | 9p rdwr ollie/tools # returns file_edit's full documentation
Adding a tool
Drop an executable and its .meta file in $OLLIE_TOOLS_PATH.
The next session created will pick it up automatically. No restart required.
See doc/writing-tools.md for the full specification including host-conditional variants and privileged tools.
System prompt override
The default system prompt is compiled into olliesrv. To replace it, set the
systemPrompt field in an agent config JSON to a file path:
{
"systemPrompt": "~/.config/ollie/prompts/SYSTEM_PROMPT_QWEN.md",
"prompt": ["$OLLIE_CFG_PATH/prompts/agent-coding.md"],
"temperature": 0.7
}
Viewing model context
The rendered context window for any session is readable at
session/{sname}/agent/{aname}/context — the exact message array sent to the
backend on the last turn. Combined with session/{sname}/agent/{aname}/systemprompt,
this gives full visibility into what the model sees.
Alternative front-ends
- ellie — Emacs front-end (
ellie.el) - KDE — plasmoid, standalone GUI, Kate plugin, KRunner, system tray
- Web — built-in HTTP server (see
olliesrv -web)