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.
| Method | Purpose |
|---|---|
new() / default() | fresh random id |
parse(&str) / FromStr | parse the canonical UUID form |
short() | first 8 characters, used in branch and directory names |
Display | full 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.
| Field | Type | Serde |
|---|---|---|
id | TaskId | required |
title | String | required |
description | String | required |
status | TaskStatus | required |
complexity | Option<Complexity> | required key, null until assessed |
labels | Vec<String> | default |
source | TaskSource | default (manual) |
created_at, updated_at | DateTime<Utc> | RFC 3339 strings |
branch | Option<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).
TaskStatus | Meaning | is_terminal() |
|---|---|---|
backlog (default) | created, not started | no |
planning | spec and plan being produced | no |
building | subtasks being implemented | no |
review | QA reviewing or fixing | no |
ready | waiting for a human to review and merge | no |
done | merged or accepted | yes |
failed | the pipeline gave up | yes |
cancelled | cancelled by the user | yes |
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
| Type | Fields |
|---|---|
Spec | task_id, summary, requirements (default []), context (default), body (default "", free markdown) |
Requirement | id ("R1"…), description, kind (default functional), priority (default 1, 1 = must have), acceptance (default []) |
RequirementKind | functional, non_functional, constraint |
SpecContext | relevant_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
| Type | Fields |
|---|---|
Plan | task_id, approach (default ""), phases (default []) |
PlanPhase | name, parallel (default false), subtasks (default []) |
Subtask | id, title, description, files, depends_on: Vec<SubtaskId>, verification, status (default pending), attempts (default 0), notes (default "") |
SubtaskStatus | Meaning |
|---|---|
pending (default) | not started |
in_progress | an agent is working on it |
done | implemented |
failed | gave up after retries |
skipped | dependency 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:
- a
depends_onentry naming a subtask that is not in the plan ("subtask <id> depends on unknown subtask <id>"); - 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
| Type | Fields |
|---|---|
QaReport | task_id, round (1-based), verdict, summary (default), issues (default) |
QaIssue | severity, title, detail, and optional requirement, file, line, suggested_fix |
QaVerdict | approved, changes_requested, inconclusive |
Severity | low < 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 |
|---|---|
text | text |
thinking | text (only when the provider exposes reasoning) |
tool_use | id, name, input (JSON matching the tool schema) |
tool_result | tool_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.
| Variant | Extra fields |
|---|---|
run_started | task |
phase_started | phase |
phase_finished | phase, success, summary |
agent_started | role, subtask (nullable) |
agent_text | role, text |
tool_called | role, tool, input |
tool_returned | role, tool, is_error, duration_ms, preview |
agent_finished | role, steps, usage, stop (the AgentStop serialised as a JSON string) |
subtask_updated | subtask, status |
retrying | what, attempt, delay_ms |
paused | reason |
run_finished | success, status (a TaskStatus) |
log | run (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.