Skip to content

Latest commit

 

History

History
261 lines (217 loc) · 15.7 KB

concepts.md

File metadata and controls

261 lines (217 loc) · 15.7 KB

Concepts

agent-backbone brings agents across AI CLIs and projects into one personal team. It is a control plane for terminal AI agents on one machine. You run Claude Code, Codex, Gemini CLI, OpenCode, Deep Code or Aider in tmux sessions; the backbone starts those sessions, knows whether each one is ready to receive input, delivers text to them safely, and connects them to GitHub Issues, to inbound sources they subscribe to (Gmail today), to people through integrations (Telegram today), and to each other.

It is not an agent framework (it does not call models), not a workflow engine (there are no DAGs), and not a dashboard (it feeds one).

Communication, coordination and continuity let specialists collaborate and carry experience into later projects. Starting practices help agents retain reasoning, apply lessons in context and revise them after feedback. You can change those policies as your team develops. Expertise depends on the models, useful records, investigation and judgment; Backbone does not automatically learn or validate knowledge on their behalf.

Agent

A directory plus a runtime. Agents are discovered, not declared: the first backbone agent start from a directory records it.

$ cd ~/code/app && backbone agent start
app: ready — claude repo acme/app
  • Name — the directory name (pass agent start NAME to override). It is the tmux session name, the value of for:<name> labels, and the from: identity in messages the agent sends. Discovery removes leading punctuation so the name starts with a letter or digit (_app becomes app); a name containing only punctuation becomes agent. Internal underscores and dots are preserved.
  • Runtime — claude, codex, gemini, opencode, deepcode, aider or shell; a known agent reuses its saved runtime. New agents use agents.default_runtime (initially claude) unless explicitly selected.
  • Repository — read from git remote origin. An agent whose directory is a GitHub checkout owns that repository.
  • Watches — other repositories the agent wants to hear about (backbone agent watch orch acme/app acme/web). Inside its own session an agent can subscribe itself: backbone agent watch acme/app (the name defaults to $BACKBONE_AGENT).
  • Subscriptions — inbound events from a source the agent wants (backbone agent subscribe desk gmail "from:alerts@example.com" --priority high): a source, a filter in that source's own query language, and a priority (normal waits for the prompt, batched; high reaches a working Claude Code, Codex or OpenCode agent on its next tool call through hook context, or on Codex when a long-running command exits; other runtimes first when ready).

There are no prescribed roles or hierarchies. An orchestrator is an ordinary agent whose directory is its own repository and which watches the others. The one grouping that exists is a swarm: a coordinator plus members sharing a worktree to complete a single issue — and its members are still ordinary agents.

The database is the only record of agents; backbone agent set|watch|forget edit it and the running backbone picks the change up immediately.

Repository

Every repository an agent owns or watches is tracked on its own. There is no coordination repository and nothing to configure per repository: GitHub credentials are set once, and the backbone polls (or accepts webhooks for) every tracked repository.

Four relationships decide routing for an issue in repository R:

Relationship How it comes about Effect
owner the agent's directory is R (swarm members excepted: their worktree is R, but they are not owners) unlabelled issues are its work (sole owner) or are announced to all owners
for:<agent> a label on the issue goes to that agent's queue
from:<agent> a label on the issue comments and the close are reported back to the opener
watch backbone agent watch informational notice about new issues; for: labels in R route to it

State

What an agent is doing, in a vocabulary shared by every runtime:

State Meaning
starting Session exists; runtime not at its prompt yet
idle At the prompt, nothing running
busy Working on a prompt
waiting_for_human Blocked on a person — reason is plan (plan approval), permission (tool permission prompt) or question (AskUserQuestion, or any dialog seen on the terminal)
blocked Waiting on a usage limit (reason: quota) or a provider capacity/rate-limit failure (reason: provider). detail preserves the error and retry/reset time. Recovery may require waiting or choosing another model.
unknown No trustworthy signal
offline No tmux session (reported by the API; not a stored state)

Where it comes from, in order: agent start writes starting when the session is created (trusted for two minutes at most; the first hook write replaces it, and a visible prompt clears it). Hooks the runtime itself runs (Claude Code, Codex, Gemini CLI and OpenCode) write <data_dir>/state/<agent>.json on every transition; a fresh hook state is authoritative. When there is no hook state or it is older than timing.stale_threshold_seconds (5 min), the backbone reads the terminal through the runtime's module (prompt visible, busy marker, permission prompt). Every reading keeps its evidence, shown by backbone agent inspect.

Delivery condition

Derived from the state plus the terminal, right before anything is pasted:

Condition Meaning Deliverable?
offline no session, or an inbox-only agent no — queued (an inbox-only agent reads its messages with backbone inbox)
waiting_for_human agent is asking a person something no — queued
agent_working starting, busy, or blocked on a usage limit or provider failure no — queued (never bypassed)
human_reading someone is selecting text or reading tmux scrollback no — queued, even with priority
unattended_copy_mode the pane is in tmux copy mode and no client is viewing it no — queued, even with priority, until someone attaches and presses q
human_typing someone typed text into the prompt no — queued, unless priority
settling hook reported idle less than timing.grace_period_seconds ago, or a fresh session's brief (Aider) is not delivered yet not yet, unless priority; the brief goes first even then
ready idle, empty prompt yes
unknown no signal either way yes, best effort

tmux copy mode preserves scrollback and text selection. Backbone observes it without cancelling it: otherwise-ready agents report human_reading, and messages wait until the human leaves copy mode (normally q or Escape). Monitoring and priority delivery never clear a selection. The agent's own state still takes precedence: a busy agent remains agent_working while you read.

tmux keeps a pane in copy mode after its last client detaches. When no client is viewing the pane's window, the agent reports unattended_copy_mode instead: messages still wait, and backbone agent inspect and backbone doctor say to attach to the session and press q. A sender whose message expires in the queue is told the condition that held it, so they can tell whether a resend would meet it too.

Message

The only thing that ever enters an agent's terminal. Every message carries a provenance envelope so the agent — and anyone reading the transcript — knows where it came from:

[via:backbone from:elias] review PR 12 and summarise the risks
[via:github issue:42] New issue targeting you: acme/app#42 [bug] "Fix flaky auth test" (from planner, blocking). Link: https://…
[via:telegram from:alice] status?
[via:gmail] New gmail messages matching your subscriptions. …
- 199a4f2c3d1e0b7a · from «Alerts <alerts@example.com>» · subject «Outage: build queue stalled» · 2026-09-17 14:02Z · https://mail.google.com/mail/#all/199a4f2c3d1e0b7a

Delivery

One attempt to hand a message to a session, recorded with its kind (issue, comment, review, pull_request, direct_message, watch, escalation, subscription, steer), repository, outcome and a preview — direct messages included. A steer (backbone tell --steer) is never queued or pasted: it is offered to a working agent's hook and its record moves from offered to handed_off, not_taken or cancelled. What cannot be delivered now is queued in the database and delivered by the background jobs — a message that waited at least two minutes is delivered with (queued N min ago) (N h from two hours) after its envelope, so a review or comment drained after a long busy stretch does not read as current; queued messages expire after timing.queue_expiry_minutes (30), except direct messages, active swarm coordination, inbox holds, an agent's startup brief and subscription batches (facts, not conversation: they grow while the agent is busy and are retired only when delivered). Nothing expires for an agent whose queue is held by an unconfirmed (uncertain) delivery: that wait measures the hold, not whether the message is still wanted. Once the hold is acknowledged, the waiting messages get a full expiry window from that moment. Direct messages wait until delivered or acknowledged through the inbox, even across service restarts; retiring the recipient registration retires its pending messages. An expired notification leaves a delivery with outcome expired (kind, source and preview kept), so agent inspect shows what never arrived, and the recipient and each registered agent that sent one get a [via:backbone] notice listing them (source queue-expiry; notices never expire). A sender without a Backbone inbox, such as GitHub or an external caller, keeps only that record. The sender is told whether a row exists (stored), whether the same message from them was already waiting (already_queued), or whether storing it failed (failed) — "queued" is never claimed for a message that is not in the database. The same message means the same source event (a comment or review id) or the same sender with the same text; two senders with identical text are two messages. Issue deliveries are additionally claimed so concurrent jobs cannot deliver the same issue twice. Delivery checks and pastes are serialized per session within the running server; different sessions can proceed concurrently. A blocked queue drain retains its leased row instead of inserting an age-stamped copy.

Event

Every inbound GitHub event (webhook or poll) and every subscribed source item (a matched Gmail message) is stored before it is routed, with what the backbone did about it. That table is the activity feed (GET /api/events, backbone status shows the last event per repository) and the dedup record used by overlapping polls. A separate durable cursor per repository preserves an incomplete batch across restarts, independent of event retention. A durable outbox records every eligible recipient before the first delivery, then records delivered or queued receipts. A failed queue write leaves that recipient pending; replay and the retry job resume only unresolved recipients. See delivery receipts for retention and the external-terminal crash boundary.

Progress report

An agent's short account of its goal, accomplishments, blockers and next steps. Agents publish at meaningful milestones; the database preserves their reports so people and orchestrators can read a shared feed without interrupting work. Report age and missing reports are explicit. This authored account is separate from the runtime's measured state and from operational diagnostics. See Agent progress reports for publishing tools and enforced length limits.

Skill

A directory with a SKILL.md that an agent's CLI loads on demand. The store (skills.store, ~/skills) holds the one shared copy of each; a skill's frontmatter names the tags of the agents that receive it (all for everyone, agent:NAME for one). At launch, and for a running agent whenever a skill or its tags change, the backbone links the selected skills into the directory the runtime reads. It never changes a skill the repository owns, and links one kept where only another CLI reads it, so the repository's skills reach whichever CLI runs the agent. See Skills.

Settings

Everything tunable is a key in the database with a built-in default: backbone config list. Secrets (API key, tokens) are never in the database; they live in <data_dir>/.env or the environment, and are kept out of the agent sessions the backbone starts.

Jobs

Background loops inside the backbone process:

Job Every Does
agent-monitor timing.monitor_interval_seconds (60 s) refresh agents/settings, read every agent's state once and mirror it to the database, stall and dead-session reports, plan-waiting alerts, queue drain, next pending issue to idle agents, Socket.IO snapshot
delivery-retry timing.retry_interval_seconds (5 min) retry failed issue deliveries, drain the queue
steer-settle 15 s record what became of each steer offer: handed_off, not_taken when the turn ended first or after 300 s, cancelled when the session was replaced
agent-transitions 5 s explicit one-time stop/restart requests (agent restart): stop the session, start the replacement when its wait is over, hand it the continuation message
github-poll github.poll_interval_seconds (60 s) poll intake only
sources-poll sources.poll_interval_seconds (60 s) only with a configured source: search each source for new items matching any subscription filter and deliver them
github-backfill starts at startup webhook intake only: catch up on downtime; delivery-retry retries unfinished repositories until catch-up succeeds
dependency-sync 10 min, and at startup with GitHub: record the sub-issue links of open queued issues, for dependency priority and "Dependencies resolved" notices
prune 6 h delete old deliveries, events and completed queue bodies; rotate the hook action log
integrations-sync 5 min re-provision each integration's per-agent surfaces, such as Telegram topics
report-notifications 30 s post new progress reports to the integrations
report-audio 30 s send report voice messages, when telegram.report_audio is on
upgrade-watch 60 s restart the backbone itself onto new code when the installed version changes, or the checkout moves on the branch it started on (not when it switches branches), with no active upgrade hold and nothing being routed; off with backbone.restart_on_upgrade

What the backbone does not decide

  • What agents do. It hands an agent text; the agent's instructions (CLAUDE.md, AGENTS.md, …) decide how it reacts. GitHub → What an agent is expected to do is the whole protocol.
  • Who may talk to whom. Any agent can message any agent through the API.
  • Whether to restart a dead agent. It reports; it never restarts on its own. The one restart it performs is the explicit agent restart an agent or a person asked for.

Runtime registration is declared in services/runtimes/catalog.json: IDs, detection order and hook files feed configuration, the runtime registry and hook installation from the same data. Runtime behavior remains in each runtime's module; shipped hook scripts stay standalone.

See message checkpoints for safe mid-turn coordination, acknowledgement and retention.