- Go 99.4%
- Shell 0.4%
- Makefile 0.2%
|
|
||
|---|---|---|
| .forgejo/workflows | ||
| agent | ||
| cmd/bob-agent | ||
| db | ||
| detect | ||
| docs | ||
| llm | ||
| netcfg | ||
| packaging | ||
| rpc | ||
| scripts | ||
| toolhost | ||
| ui | ||
| verbs | ||
| .gitignore | ||
| AGENTS.md | ||
| go.mod | ||
| go.sum | ||
| Makefile | ||
| nfpm.yaml | ||
| README.md | ||
bob-agent
A personal, unix-only coding agent. One server owns all knowledge (SQLite) and all LLM calls; thin clients on your machines execute tools locally.
Documentation:
docs/manual.md— the user manual (what things are and how to use them). Start here.docs/commands.md— the full verb / slash-command matrix.docs/workspaces.md— the normative model: tool hosts, workspaces, projects, sessions, and how isolation follows from it.docs/research.md— the P0 research and settled decisions.
The roadmap (phases P7–P12, checkpoint C1, open questions) is tracked as issues in this repository; the repo history is the progress log.
Status: P1–P6 — chat loop, disconnect survival, tool use, project config
- P1 — server skeleton + chat loop: see commit history; the vertical slice (client -> server -> LLM -> back) works, persisted server-side.
- P2 — conversations survive disconnects: conversation state is
server-side; a UI can be killed and a new one reattaches by session id.
In-flight tool work dies with its tool host: a host killed mid-tool-call
leaves the call marked
failedand the turn ends with an error-aware reply. - P3 — tool use: server-side registry (
shell,read_file,write_file,edit,git) with per-call timeouts; execution happens on the client (tool host); output streams back and is persisted incrementally; the agent loop feeds results to the model until it answers. Verified end-to-end with the local qwen3-vl-8b: "read README.md and summarize it" calls the tool, streams output, and answers. - P6 — explicit project configuration: per-project config in the server
DB (workflow, test/lint/build commands, git remotes, agent git identity,
explicit skills dir, languages), templates (
project template), local auto-detect (project detect), session binding, and two new agent tools:project-config(server-side, fetched on demand) andsystem-info(uname-like, runs on the tool host).
Go code in this repo follows the idioms in
spf13/go-skills,
vendored at docs/go-skill.md: flat domain packages (agent/, db/,
llm/, rpc/), interfaces at the consumer, slog, stdlib-first, modern
test idioms.
One binary (cmd/bob-agent), three roles: server (owns the SQLite DB —
the single source of truth — and makes every LLM call; JSON-RPC 2.0 over
WebSocket, TLS with a self-signed cert pinned by clients, bearer-token auth,
online backups), client (Bubble Tea TUI or plain REPL; is the session's
tool host), and non-interactive verbs (same operations as the slash
commands). A built-in mock provider demos the chat slice with no API key.
Networking: one per-user DB, login blobs
Clients and servers share one per-user SQLite database,
$HOME/.bob-agent/agent.db, on every machine a user runs bob-agent on. The
DB holds the server's own identity (alias, externally visible host:port,
self-signed TLS cert, bearer token) and, on client machines, the configured
servers (alias → host, token, pinned cert fingerprint). One server runs per
unix user; each server serves exactly one identity, so there is no
per-client authorization to set up — a login blob is the whole story.
First run on the server machine — bob-agent server with no config
prompts for the server alias and externally visible host:port, generates a
self-signed TLS cert + bearer token, stores them in the per-user DB, and
prints a copy-pasteable login blob. Non-interactive (systemd, scripts):
bob-agent server -alias home -host agent.example.com:7373.
On a client machine — paste the blob (or bob-agent login blob.txt):
bob-agent login # paste the blob, Ctrl-D
bob-agent auth list / auth del <alias> / auth default <alias> manage
configured servers. The first login becomes the default; a bare
bob-agent client connects to it. When client and server run as the same
unix user on the same machine, no login is needed at all: the server's
identity lives in the shared per-user DB and doubles as the client's
endpoint (this is what make dev-server/dev-client rely on).
Clients reach the server over wss:// and verify it by the pinned cert
fingerprint from the blob — MITM protection without any CA. A raw
ws:///wss:// URL still works for scripts (no token, no pinning), and
BOB_SERVER/BOB_DB env vars mirror the -server/-db flags.
Build and run
One binary, every role:
go build -o bin/bob-agent ./cmd/bob-agent
./bin/bob-agent server -alias home -host agent.example.com:7373 # first run: sets up + prints login blob
./bin/bob-agent server # later runs: just serve
./bin/bob-agent client # on a client machine, after `login`
./bin/bob-agent -server home client # pick a specific server by alias
Non-interactive verbs do the same as the slash commands (see
docs/commands.md for the full justification of each):
./bin/bob-agent -server home provider add openai https://api.openai.com/v1 sk-...
./bin/bob-agent -server home model add openai gpt-4o-mini main
./bin/bob-agent -server home session new
./bin/bob-agent -server home chat 1 "hello world"
Inside the interactive client (same operations as the verbs):
/provider add mock http://localhost:1 # built-in demo provider
/model add mock mock-model main
hello world # chat; reply streams back
/new # new session
/sessions, /attach <id>, /messages # conversation survives restarts
Client (P4)
./bin/bob-agent client # default server from the per-user DB
./bin/bob-agent client --text # plain REPL
./bin/bob-agent -server home client # a specific configured server
Bubble Tea UI: chat transcript + streaming tool output + slash commands
(same operations as the CLI verbs). Assistant replies render as markdown
(glamour) once complete; user/tool/system items stay plain text so selecting
a shell command and pasting it keeps its whitespace (the P4 copy-paste
acceptance test). Attach to a specific session with client -session <id>.
A usage status bar glued above the input textarea shows the session's model usage:
──── deepseek/deepseek-v4-flash * high - Ctx: 45231 I: 38992, C: 812, O: 100 ──── cost: $0.0146
I/C/O are provider-reported tokens in / tokens in cached / tokens out,
summed over every model call in the session. Cache hits are parsed from
either usage shape providers use: prompt_tokens_details.cached_tokens
(OpenAI; also what DeepSeek v4-flash returns today) or
prompt_cache_hit_tokens (older DeepSeek). Ctx is the input token count
of the last request sent to the model — the size of the context it saw, not
the model's context window. Effort defaults to high until per-model
configuration exists.
Cost config is optional, per provider+model, in the DB (migration v6):
bob-agent ... model cost deepseek deepseek-v4-flash 0.28 0.07 0.42 $
Rates are per 1M tokens: input (cache miss), cache read, output, currency
symbol. When set for the model the session uses, the TUI appends the
session cost to the usage line — (I-C)*input + C*cache_read + O*output,
divided by 1M. No cost configured, no cost shown.
Every session also keeps a rolling raw wire log of the last 5 MiB of provider request/response payloads for offline debugging:
./bin/bob-agent -server home raw-log 1
Knowledge (P5): prompt, memories, skills
All three live in the server DB and are shared across machines automatically:
./bin/bob-agent -server home prompt show # versioned, revertible
./bin/bob-agent ... prompt set < prompt.txt # new version, becomes active
./bin/bob-agent ... prompt list / prompt use 1 # revert
./bin/bob-agent ... memory add "user prefers short replies" # injected into every turn
./bin/bob-agent ... skill add greet "greet someone" 'echo hello {{who}}' '{"type":"object","properties":{"who":{"type":"string"}}}'
A skill is a parameterized shell recipe: {{param}} placeholders are filled
from the model's call arguments and executed on the tool host as the shell
tool. Slash commands (/prompt, /memory, /skill) do the same from the
interactive clients.
Project configuration (P6)
Per-project config lives in the server DB — there is nothing in local files to forget. It is created before work starts on a project:
What a project is. A project is a named set of constraints on how the agent works, applied to the sessions bound to it — concretely, a workflow of a clone of a repository. It is not the repository and not the clone: one checkout can have several projects (a formal
issue-to-prproject and afreehandproject over the same directory), and a project survives the checkout being deleted. A session is bound to at most one project. Seedocs/manual.md§3 for the full definition.
What constraining a session means. The workflow rule is a standing
constraint, injected into the system prompt on every turn (an addendum to
the global prompt, like memories) — it governs the agent's very first
decision, so it can't be fetched on demand. The config details (commands,
remotes, identity) are fetched with the project-config tool when needed.
The two workflows: issue-to-pr = work on issues one at a time (or a
user-specified queue), each on its own branch, pushed as a PR — never
commit to the default branch directly, refuse free-form work, and free text
is a filter over issues ("pick up all issues about Norway"), never a work
request. freehand = commit freehand prompts directly to main, no
branches/PRs. The mechanism is planned to become deeper and configurable
(CHANGELOG requirements, semver policies, ...) — for now: two options,
KISS.
# explicit config
bob-agent ... project add myproj /path/to/checkout
bob-agent ... project set myproj workflow issue-to-pr # freehand | issue-to-pr
bob-agent ... project set myproj test "go test ./..."
bob-agent ... project set myproj lint "go vet ./..."
bob-agent ... project set myproj build "go build ./..."
bob-agent ... project set myproj identity "Agent <agent@example.com>"
bob-agent ... project set myproj git_remotes https://github.com/you/myproj.git,git@github.com:you/myproj.git
bob-agent ... project set myproj languages Go,Make
bob-agent ... project set myproj skills_dir .bob/skills
bob-agent ... project set myproj context_files AGENTS.md,docs/SPEC.md
# contents injected at the start of every session
# (NEVER injected unless configured; re-bind the
# session to re-sync changed files)
bob-agent ... project show myproj
# auto-detect most of it from a local checkout (Go, Rust, Node, Python,
# Makefile; git remotes; Makefile targets win over manifest defaults)
bob-agent ... project detect myproj /path/to/checkout
# templates: project B is developed the same way as project A
bob-agent ... project template myproj-b myproj
# bind a session to a project so the agent knows what it is working on
bob-agent ... session project <session-id> myproj
# (interactive: /project use myproj)
The agent does not get the config in every context (that would bloat it);
instead it fetches it on demand with the project-config tool — the
server-side source of truth — and is nudged to do so by a hint injected when
a session is bound to a project. The second new tool, system-info,
runs on the tool host (uname-like: OS, kernel, arch, installed tools such as
gh/glab/tea/yq/jq/go/make) so the agent can plan for what a machine can do.
Dev workflow (Makefile)
The dev database is always dev/agent.db (gitignored), shared between your
shell and agent development runs.
make dev-server # first run: sets up alias "dev" on dev/agent.db; prints login blob
make dev-server # later runs: just serve
make dev-client # CLI attached to the dev server (by alias, no login needed)
Configure the local test model once (qwen3-vl-8b via llama.cpp/ollama — no API key needed for a local endpoint):
make dev-qwen QWEN_URL=http://<host>:<port>/v1 # defaults to http://127.0.0.1:18090/v1
Then just type in the client. Other targets: make demo (scripted end-to-end
demo on a throwaway DB, mock provider, no key), make deb (build the .deb
into dist/ with nfpm; version from VERSION, default 0.0.0+git<ts>.<sha>),
make check (fmt+vet+test), make backup, make clean (removes bin/, dist/
and dev/). The Forgejo Actions workflow (.forgejo/workflows/deb.yml) runs
the same build on every push to main and publishes the package that
sudo bob-agent-update on machines installs — see the manual §2 for the
deployment story.
Tests
make check
Backups (P1 requirement)
The server DB is the crown jewel. Online backup while the server runs:
scripts/backup.sh /var/lib/bob-agent/agent.db /var/lib/bob-agent/backups
Restore: stop the server, replace the db file with a snapshot, start the
server. A restore test lives in db/backup_test.go.