Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Domain model

Every artefact the framework produces or exchanges is a plain Rust type in vibe-core, serialisable with serde. This page describes those types and shows how they look on disk. All types derive Serialize, Deserialize, Clone, Debug and PartialEq; most enums use #[serde(rename_all = "snake_case")], so TaskStatus::InProgress is written "in_progress".

Task ──1:1── Spec ──1:1── Plan ──1:n── PlanPhase ──1:n── Subtask
  │                                                        │
  └──1:n── QaReport (one per round) ──1:n── QaIssue        └── depends_on: [SubtaskId]

Run: RunId ──publishes──▶ Event (wrapped in Envelope) ──▶ EventBus
Agent session: SessionId, Message[] of ContentBlock, Usage

Identifiers

TaskId, SubtaskId, SessionId and RunId are generated by one macro in ids.rs: a newtype over a v4 uuid::Uuid, #[serde(transparent)], so each serialises as a bare string.

MethodPurpose
new() / default()fresh random id
parse(&str) / FromStrparse the canonical UUID form
short()first 8 characters, used in branch and directory names
Displayfull UUID

The ids are distinct types, so passing a TaskId where a SubtaskId is expected does not compile.

Task

A Task is the unit of work handed to the pipeline.

FieldTypeSerde
idTaskIdrequired
titleStringrequired
descriptionStringrequired
statusTaskStatusrequired
complexityOption<Complexity>required key, null until assessed
labelsVec<String>default
sourceTaskSourcedefault (manual)
created_at, updated_atDateTime<Utc>RFC 3339 strings
branchOption<String>default, omitted when None; the workspace branch recorded by the last run

Task::new creates a backlog task; set_status and touch bump updated_at; slug() applies slugify (lowercase ASCII alphanumerics separated by -, at most 48 characters, "task" when empty).

TaskStatusMeaningis_terminal()
backlog (default)created, not startedno
planningspec and plan being producedno
buildingsubtasks being implementedno
reviewQA reviewing or fixingno
readywaiting for a human to review and mergeno
donemerged or acceptedyes
failedthe pipeline gave upyes
cancelledcancelled by the useryes

Complexity is ordered (trivial < simple < standard < complex) and exposes two thresholds: needs_spec() is true from simple upwards, needs_research() from standard upwards.

TaskSource is internally tagged with kind: {"kind": "manual"}, {"kind": "issue", "provider": "github", "reference": "412"} or {"kind": "derived", "parent": "<task id>"}.

A task as serialised by serde_json:

{
  "id": "6f1c3e0a-9b2d-4c47-8a51-0e3b8f2d7c19",
  "title": "Add OAuth login with GitHub",
  "description": "Users should be able to sign in with their GitHub account.",
  "status": "planning",
  "complexity": "standard",
  "labels": ["auth"],
  "source": { "kind": "issue", "provider": "github", "reference": "412" },
  "created_at": "2026-09-27T08:12:44.120Z",
  "updated_at": "2026-09-27T08:15:02.871Z"
}

Spec

TypeFields
Spectask_id, summary, requirements (default []), context (default), body (default "", free markdown)
Requirementid ("R1"…), description, kind (default functional), priority (default 1, 1 = must have), acceptance (default [])
RequirementKindfunctional, non_functional, constraint
SpecContextrelevant_files, findings, assumptions, all default []
{
  "task_id": "6f1c3e0a-9b2d-4c47-8a51-0e3b8f2d7c19",
  "summary": "Add a GitHub OAuth sign-in flow next to the existing password login.",
  "requirements": [
    {
      "id": "R1",
      "description": "A 'Sign in with GitHub' button starts the OAuth flow.",
      "kind": "functional",
      "priority": 1,
      "acceptance": ["GET /auth/github redirects to github.com/login/oauth/authorize"]
    },
    {
      "id": "R2",
      "description": "Password login keeps working unchanged.",
      "kind": "constraint",
      "priority": 1,
      "acceptance": ["existing tests in tests/auth pass"]
    }
  ],
  "context": {
    "relevant_files": ["src/auth/mod.rs", "src/routes.rs"],
    "findings": ["sessions are stored with tower-sessions"],
    "assumptions": ["client id and secret come from environment variables"]
  },
  "body": ""
}

Because every field but task_id and summary has a default, a minimal spec is two keys.

Plan

TypeFields
Plantask_id, approach (default ""), phases (default [])
PlanPhasename, parallel (default false), subtasks (default [])
Subtaskid, title, description, files, depends_on: Vec<SubtaskId>, verification, status (default pending), attempts (default 0), notes (default "")
SubtaskStatusMeaning
pending (default)not started
in_progressan agent is working on it
doneimplemented
failedgave up after retries
skippeddependency failed or no longer needed

Phases always run in order; the subtasks of a phase may run concurrently when parallel is true. Plan offers subtasks() / subtasks_mut() (plan order), subtask(id), len(), is_empty() and is_complete() (every subtask done or skipped).

Plan::validate() rejects two kinds of broken plans with an ErrorKind::Config error:

  1. a depends_on entry naming a subtask that is not in the plan ("subtask <id> depends on unknown subtask <id>");
  2. a dependency cycle, found with Kahn’s algorithm: every subtask starts with an in-degree equal to its number of dependencies, subtasks at zero are dequeued, and each dequeue decrements the subtasks that depend on it. If fewer subtasks are dequeued than exist, the remainder forms a cycle ("plan contains a dependency cycle").

Dependencies may cross phase boundaries; validation looks at the whole plan.

{
  "task_id": "6f1c3e0a-9b2d-4c47-8a51-0e3b8f2d7c19",
  "approach": "Add an OAuth client module, then wire routes and the login page.",
  "phases": [
    {
      "name": "Foundations",
      "parallel": false,
      "subtasks": [
        {
          "id": "0b7e2f4c-1d3a-4e8b-9c6f-5a2d8e1f3b70",
          "title": "OAuth client",
          "description": "Create src/auth/github.rs exposing authorize_url() and exchange_code() (R1).",
          "files": ["src/auth/github.rs", "src/auth/mod.rs"],
          "depends_on": [],
          "verification": ["cargo test auth::github"],
          "status": "done",
          "attempts": 1,
          "notes": "Uses reqwest already present in Cargo.toml."
        }
      ]
    },
    {
      "name": "Wiring",
      "parallel": true,
      "subtasks": [
        {
          "id": "c41a9e07-6b52-4f1d-a3e8-2d9f0b7c6e15",
          "title": "Routes",
          "description": "Add GET /auth/github and GET /auth/github/callback (R1, R2).",
          "files": ["src/routes.rs"],
          "depends_on": ["0b7e2f4c-1d3a-4e8b-9c6f-5a2d8e1f3b70"],
          "verification": ["cargo test --test auth"],
          "status": "pending",
          "attempts": 0,
          "notes": ""
        }
      ]
    }
  ]
}

QA report

TypeFields
QaReporttask_id, round (1-based), verdict, summary (default), issues (default)
QaIssueseverity, title, detail, and optional requirement, file, line, suggested_fix
QaVerdictapproved, changes_requested, inconclusive
Severitylow < medium < high < critical (ordered)

issues_at_least(Severity::High) filters by severity; is_blocking() is true for any verdict other than approved; to_markdown() renders the human-readable report.

Messages

A conversation is a Vec<Message>; each Message has a role (user or assistant) and a list of ContentBlocks. There is no system role: the system prompt travels separately in CompletionRequest::system. Tool results are sent back in a user message.

ContentBlock (tag type)Fields
texttext
thinkingtext (only when the provider exposes reasoning)
tool_useid, name, input (JSON matching the tool schema)
tool_resulttool_use_id, content (text), is_error (default false)
{"role": "assistant", "content": [
  {"type": "text", "text": "Reading the router first."},
  {"type": "tool_use", "id": "toolu_01", "name": "read_file", "input": {"path": "src/routes.rs"}}
]}

Helpers: Message::user, Message::assistant, Message::tool_results, text() (joins text blocks), tool_uses(), has_tool_use(), and estimate_tokens(), a deliberately rough 4-characters-per-token estimate used for context budgeting without a tokenizer.

Usage counts input_tokens, output_tokens, cache_read_tokens and cache_write_tokens (the last two default to 0). It supports combined, += and total() (input plus output).

Phase

Phase lists the pipeline stages in canonical order: assess, spec, plan, build, qa, fix, merge (Phase::ALL). as_str() gives the stable lowercase name used in [phases.<name>] configuration and CLI flags; FromStr accepts it case-insensitively and returns a Config error otherwise. The enum does not impose an order at run time: a pipeline may skip spec or loop fix → qa.

Events

Event is internally tagged with type. Every variant except log carries the run id.

VariantExtra fields
run_startedtask
phase_startedphase
phase_finishedphase, success, summary
agent_startedrole, subtask (nullable)
agent_textrole, text
tool_calledrole, tool, input
tool_returnedrole, tool, is_error, duration_ms, preview
agent_finishedrole, steps, usage, stop (the AgentStop serialised as a JSON string)
subtask_updatedsubtask, status
retryingwhat, attempt, delay_ms
pausedreason
run_finishedsuccess, status (a TaskStatus)
logrun (nullable), level (info, warn, error), message

EventBus::publish wraps the event in an Envelope stamped with the current time; that is the shape appended to events.jsonl:

{"at":"2026-09-27T08:16:10.002Z","event":{"type":"tool_returned","run":"a3f0…","role":"coder","tool":"read_file","is_error":false,"duration_ms":3,"preview":"use axum::Router;…"}}

Errors

Every crate returns vibe_core::Result<T> with a single Error { kind, message, retry_after, source }. ErrorKind drives behaviour rather than display: RateLimited, ServerError and Network are the only kinds for which is_retryable() is true; ContextTooLong ends an agent run with AgentStop::ContextWindow; Denied is what policy checks return. io::Error converts to Storage, serde_json::Error to InvalidRequest, toml::de::Error to Config.

JSON is the source of truth

Complexity assessments, specs, plans and QA reports are produced by agents as JSON, parsed into these types, and saved as JSON. Markdown (Spec::to_markdown, QaReport::to_markdown) is rendered from the typed value for humans and is never parsed back. There is therefore one source of truth per artefact, and a malformed model answer is caught at the boundary by serde instead of deep in the pipeline. The rationale is in ADR-006; the extraction and repair strategy is in Agent runtime.

The JSON documents the built-in prompts ask for are the agent-facing shapes, not the persisted ones: they omit fields only the framework knows. The planner, for example, returns subtasks without id, status or attempts and names dependencies by subtask title, and the QA reviewer returns a report without task_id or round. Assigning ids, resolving titles to SubtaskIds and filling task_id and round is the caller’s job before a value becomes a Plan or a QaReport and before Plan::validate() runs.