sandbox
  • Go 99.4%
  • Shell 0.4%
  • Makefile 0.2%
Find a file
Agent Bob 5538058ad1
Some checks failed
deb / build (push) Has been cancelled
docs: point the remote at this sandbox
2026-08-30 15:13:16 +00:00
.forgejo/workflows ci: persistent Go caches via runner bind mounts 2026-08-28 13:45:06 +00:00
agent #72: PR 99 review round 7 — failure records consume the whole respond-due suffix; message roles get one enumeration 2026-08-29 19:10:42 +00:00
cmd/bob-agent #74: auto-retry transient provider errors in the turn loop; /retry resumes a failed turn 2026-08-29 18:45:27 +00:00
db #72: PR 99 review round 7 — failure records consume the whole respond-due suffix; message roles get one enumeration 2026-08-29 19:10:42 +00:00
detect #14: headless tool hosts — tool-host verb, /detach, monitoring 2026-08-26 13:45:09 +00:00
docs docs: correct the cohort boundary — the switch precedes PR 86, not PR 87 2026-08-29 23:14:20 +00:00
llm #74 review round 2 — Transient excludes genuinely canceled requests (real cancelled Chat test); 429 honors Retry-After (capped, backoff jittered); request logged once per retry iteration (responses per attempt); subagent refusal before the turn lock; failed retry leaves no blank assistant line 2026-08-29 18:47:15 +00:00
netcfg #57: review round — collision exit via RestartPreventExitStatus, shared name marker, netcfg.IsURL 2026-08-29 12:58:30 +00:00
packaging #57: review round — server-side dead-host reap (keepalive ping), collision retry on a wall-clock budget 2026-08-29 12:58:30 +00:00
rpc #72: PR 99 review round 7 — failure records consume the whole respond-due suffix; message roles get one enumeration 2026-08-29 19:10:42 +00:00
scripts P0+P1: research doc, Go module, server skeleton with working chat loop 2026-08-22 11:06:06 +00:00
toolhost #51: stage 2b review — no-option questions can't panic, free-row only with allow_custom, horizontally scrolling text field, window scrolls at edges; tool rejects unanswerable questions 2026-08-27 13:45:13 +00:00
ui #72: PR 99 review round 7 — failure records consume the whole respond-due suffix; message roles get one enumeration 2026-08-29 19:10:42 +00:00
verbs #74: auto-retry transient provider errors in the turn loop; /retry resumes a failed turn 2026-08-29 18:45:27 +00:00
.gitignore #12: deployment — graceful drain, .deb packaging, Forgejo CI, updater 2026-08-25 20:02:25 +00:00
AGENTS.md docs: point the remote at this sandbox 2026-08-30 15:13:16 +00:00
go.mod #51: stage 5 review — raw-mode detection, auto without re-query, no stock glamour backgrounds, full-width input band 2026-08-27 18:14:16 +00:00
go.sum #51: TUI stage 1 — layout arithmetic: delete the header, exact row budget, no 80-col floor, dynamic input height 2026-08-27 13:45:08 +00:00
Makefile #12: deployment — graceful drain, .deb packaging, Forgejo CI, updater 2026-08-25 20:02:25 +00:00
nfpm.yaml Tool hosts were never supervised: after every bob-agent-update the 2026-08-29 12:58:30 +00:00
README.md prompt set: read the prompt from stdin instead of an argument 2026-08-28 23:48:21 +00:00

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 failed and 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) and system-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-pr project and a freehand project over the same directory), and a project survives the checkout being deleted. A session is bound to at most one project. See docs/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.