| Status | Draft |
| Authors | AttemptDB maintainers |
| Created | 2026-08-28 |
| Related | RFC 0001 (canonical event model), RFC 0002 (storage engine), RFC 0003 (fact/inference model), ADR 0002 (Arrow + DataFusion) |
| Implementation | crates/attemptdb-query (SQL over every table below; SHOW, WHY, TRACE, STATE, DIFF, WHAT IS, EXPLAIN; work units, decisions, corrections, retractions and INCLUDING RETRACTED implemented) |
AttemptQL is a small, statement-oriented query language for the questions people actually ask about agent work: what is this project doing now, why is it blocked, what caused this, what did the project look like at 14:00, which attempts failed, what was handed off between agents, what is the evidence for this claim. It is not a replacement for SQL. Every AttemptQL statement compiles to the same DataFusion logical plan that a SQL query over the public tables would produce, and plain SQL remains available for everything else.
Two properties are non-negotiable:
- Results are rows, never prose alone. Every result set has an
evidencecolumn (event ids) and anuncertaintycolumn (confidence and reason). A question with no supportable answer returns a row whose uncertainty reason isinsufficient_evidence. - Every derived value is traceable.
SHOW EVIDENCE FOR <inference>works on anything AttemptQL has ever shown.
The logical schema AttemptQL and SQL share. Physical storage is RFC 0002;
inference tables are RFC 0003. evidence columns are List<FixedSizeBinary(16)>.
| Table | Grain | Key columns |
|---|---|---|
events |
one canonical event | all RFC 0001 fields, flattened as in the segment schema (storage-format.md §8.2), ids as prefixed text, plus retracted (Boolean: the event or its session was retracted; correction/retraction events are never flagged) |
sessions |
one session | session_id, provider, provider_session_id, project_id, project_name, state (open/closed), started_at, ended_at, end_reason, start_source, event_count, turn_count, prompt_count, tool_call_count, failure_count, agents, coverage, first_event_id, last_event_id, last_event_at, start_event_id, end_event_id, evidence, confidence, retracted |
turns |
one turn | turn_id, session_id, provider, project_id, project_name, turn_index, started_at, ended_at, status (completed/failed/in_progress/unknown), prompt_event_id, stop_event_id, tool_call_ids, tool_call_count, objective, prompt_chars, first_event_id, last_event_id, evidence, confidence, corrected_by, corrected_at, inferred_objective, retracted |
tool_calls |
one paired tool call | tool_call_id, session_id, provider, project_id, project_name, turn_id, agent_id, tool_name, tool_category, provider_call_id, started_at, finished_at, duration_ms, outcome_status, outcome_class, exit_code, path_relative, paths, command_category, git_subcommand, start_event_id, end_event_id, evidence, confidence, retracted |
attempts |
one attempt | attempt_id, session_id, provider, project_id, project_name, turn_id, turn_index, attempt_index, objective (null without content), approach (content-free), started_at, ended_at, outcome (succeeded/failed/abandoned/superseded/in_progress/unknown), failure_class, tool_call_ids, tool_call_count, paths, superseded_by, supersedes, evidence, confidence, algorithm_version, work_unit_id, corrected_by, corrected_at, correction_type, inferred_outcome, inferred_failure_class, note, retracted |
work_units |
one work unit (tier1-v0, RFC 0003 §5.6) |
work_unit_id, version, project_id, project_name, objective_event_id, objective, phase, phase_reason, status, status_reason, started_at, updated_at, ended_at, sessions, session_count, turns, turn_count, attempts, attempt_count, failed_attempt_count, paths, actors, last_attempt, blocking_signal, evidence, confidence (≤ 0.7), algorithm_version |
decisions |
one derived decision (RFC 0003 §5.7) | decision_id, kind (approach_change/human_intervention), work_unit_id, session_id, provider, project_id, project_name, turn_id, selected, alternatives, rationale (content-free), rationale_source (always derived), decided_at, evidence, confidence (≤ 0.7), algorithm_version |
handoffs |
one handoff edge | from_session, to_session, from_provider, to_provider, project_id, handoff_at, gap_ms, shared_paths, evidence, confidence |
edges |
one causal edge | ordinal, edge_kind (RFC 0001 §8.2; parent_of also links work_unit → turn), from_type, from_id, to_type, to_id, evidence, confidence, edge_source (projection/derived) |
signals |
one pending-input signal | session_id, event_id, raised_at, kind, signal_type, cleared_at, cleared_by, pending, evidence, confidence |
corrections |
one Correction event (RFC 0003 §8.1) |
event_id, corrected_at, session_id, project_id, correction_type, target_type, target, outcome, failure_class, note, note_chars, status (applied/target_not_found/target_retracted/invalid), evidence, confidence |
retractions |
one Retraction event (RFC 0003 §8.2) |
event_id, retracted_at, project_id, target_type, target, reason, note, note_chars, matched, retracted_events, evidence, confidence |
inferences |
every version (planned) | all RFC 0003 §3 fields |
Lists (evidence, paths, sessions, …) are List<Utf8> with prefixed
ids; the _json columns of earlier drafts were not built. sessions,
turns, tool_calls and attempts also hold the rows a retraction removed
from the projection, flagged retracted = true (SELECT … WHERE NOT retracted is what SHOW does by default). work_units_history (planned)
exposes all versions for AS KNOWN AT.
EBNF (ISO 14977 style; terminals are case-insensitive keywords unless
quoted; whitespace separates tokens; -- starts a line comment):
statement = [ "EXPLAIN" ] command [ ";" ] ;
command = what_is | why | trace | state | show | diff ;
what_is = "WHAT" "IS" subject "DOING" "NOW" ;
why = "WHY" subject "STATUS" state_name ;
trace = "TRACE" subject "CAUSES" [ "DEPTH" integer ] [ "DIRECTION" ( "UP" | "DOWN" | "BOTH" ) ] ;
state = "STATE" subject "AT" timestamp [ "AS" "KNOWN" "AT" timestamp ] ;
diff = "DIFF" "STATE" [ subject ] timestamp timestamp ;
show = "SHOW" target [ "FOR" filter_list ] [ "WHERE" predicate ]
[ "SINCE" timestamp ] [ "UNTIL" timestamp ]
[ "ORDER" "BY" column [ "ASC" | "DESC" ] ] [ "LIMIT" integer ]
[ "INCLUDING" "RETRACTED" ] ;
target = "ATTEMPTS"
| "FAILED" "ATTEMPTS"
| "SUPERSEDED" "ATTEMPTS"
| "HANDOFFS" [ "BETWEEN" agent_filter "AND" agent_filter ]
| "WORK" "UNITS" | "DECISIONS"
| "EVIDENCE" "FOR" inference_ref
| "SESSIONS" | "TURNS" | "TOOL" "CALLS" | "EDGES" | "SIGNALS"
| "CORRECTIONS" | "RETRACTIONS" ;
agent_filter = "agent" "=" string ;
inference_ref = id | "attempt" id | "work_unit" id | "session" id | "turn" id | "event" id ;
subject = "project" [ string ]
| "session" id | "work_unit" id | "attempt" id
| "turn" id | "span" id | "event" id
| "agent" string ;
filter_list = filter { "AND" filter } ;
filter = filter_key "=" value ;
filter_key = "project" | "provider" | "agent" | "session" | "turn" | "path"
| "outcome" | "tool" | "phase" | "status" | "since" | "until" ;
predicate = (* a DataFusion SQL boolean expression over the target table's columns *) ;
state_name = identifier ; (* BLOCKED (session, project, work unit) or FAILED (attempt) today *)
id = prefixed_uuid | uuid | short_id ;
prefixed_uuid = prefix uuid ; (* ev_ ses_ trn_ spn_ att_ wu_ agt_ dec_ art_ inf_ cor_ prj_ dev_ *)
short_id = prefix hex { hex } ; (* prefix + ≥ 8 hex digits; must be unambiguous *)
timestamp = string | "NOW" | relative ; (* '2026-08-20T14:00:00Z', '2026-08-20', 'now', '-2h', '-3d' *)
relative = "-" integer ( "m" | "h" | "d" | "w" ) ;
value = string | integer | identifier ;
string = "'" { character } "'" ;Subject project without a name means "the project of the current working
directory" (resolved by the CLI, RFC 0005), or all projects when not
resolvable. Short ids (att_01a04762) are expanded against the identity
index; an ambiguous prefix is an error, never a guess.
Each statement is defined by the SQL it is equivalent to. evidence and
uncertainty are always projected; uncertainty is a struct
{ confidence: Float32, reason: Utf8, note: Utf8 }.
STATE <subject> AT NOW restricted to status IN ('active', 'waiting_on_human'),
ordered by updated_at DESC. Rows: work units (or, for a session/attempt
subject, the single unit containing it) with phase, status, latest
attempt, and last_event_at. If no unit is active, one row with
reason = 'insufficient_evidence' and note naming the last observed event
time.
Finds the latest inference asserting <state> for the subject, then joins
the edges that justify it: for BLOCKED, the blocked edges and their
source events (permission requests, denials, failed turns); for ACTIVE, the
most recent triggering prompt and tool calls; for ABANDONED, the last
attempt and the idle gap. Rows: one per justifying edge or event, with the
inference id, so SHOW EVIDENCE FOR can follow. If the subject does not
currently hold <state>, the result says which state it does hold (one row,
reason = 'state_mismatch'), and does not invent a justification.
Implemented today: BLOCKED for a session or project (uncleared
pending-input signal, or the last two attempts failed the same way) and for
a work unit (WHY wu_… STATUS BLOCKED: the unit's blocking_signal —
an uncleared pending-input signal in a member session, the same fact that
makes its phase blocked — else its last two attempts failing with the
same class; a unit that is not blocked answers state_mismatch with its
actual phase and status); FAILED for an attempt (names the failing event,
the superseding attempt, and any human correction that set the outcome).
Recursive traversal of edges from the subject over the causal types
(caused, triggered, blocked, resolved, superseded, contradicted,
handed_off), default direction UP (toward causes), default DEPTH 5.
Rows: (depth, edge_type, from_type, from_id, to_type, to_id, at, evidence, uncertainty) in traversal order. Cycles are cut; a row with
reason = 'depth_limit' marks where traversal stopped.
Work units and attempts whose valid_from ≤ t and (valid_to IS NULL OR valid_to > t), using the latest non-superseded inferences (RFC 0003 §4).
AS KNOWN AT t₂ (planned) uses work_units_history filtered by
inferred_at ≤ t₂. Rows: one per subject with its state at t.
Implemented: one row per session active at t (subject_type = 'session': open/closed, current turn, in-flight tool calls, last attempt
and its outcome as known at t, blocked flag) followed by one row per
work unit open at t (subject_type = 'work_unit': phase, status,
attempt_count, failed_attempt_count, sessions, last attempt outcome
as known at t). Units are recomputed at t by
Projection::work_units_at (only entities observed by t, outcomes
masked, corrections after t ignored, idleness judged against t); a unit
that was already completed or abandoned by t is not listed (its
valid_to has passed) and the note reports how many were. The subject may
be a project, a session (units containing it) or a work unit. Retracted
entities never appear.
Two STATE evaluations full-outer-joined on subject id. Rows:
(subject_type, subject_id, session_id, provider, change ∈ {added, removed, changed}, field, before, after, confidence, uncertainty, evidence) — one
row per changed field, for sessions (open, turn, in-flight calls, last
attempt and outcome, blocked) and for work units (phase, status, attempt
and failure counts, session count, last attempt, blocked). A unit that
completed between the two times shows as removed with its final state in
after. unchanged rows are omitted.
| Target | Equivalent |
|---|---|
ATTEMPTS |
SELECT * FROM attempts |
FAILED ATTEMPTS |
… WHERE outcome = 'failed' |
SUPERSEDED ATTEMPTS |
… WHERE superseded_by IS NOT NULL (joins the superseding attempt's outcome) |
HANDOFFS [BETWEEN agent = 'a' AND agent = 'b'] |
SELECT * FROM handoffs with from_agent/from_provider and to_agent/to_provider matched in either order |
WORK UNITS |
SELECT * FROM work_units |
DECISIONS |
SELECT * FROM decisions |
CORRECTIONS / RETRACTIONS |
the corresponding table |
EVIDENCE FOR <ref> |
the events named by the entity's evidence (attempt, turn, session, tool call, work unit, or a single event), as events rows in observed order |
SESSIONS / TURNS / TOOL CALLS / EDGES / SIGNALS |
the corresponding table |
FOR filters map to columns: project → project_name or project_id;
provider → provider (actors contains, for work units; either end of a
handoff); agent → an agt_ id (sessions, tool calls) or a provider;
session → session_id (sessions contains, for work units; either end of
a handoff); turn → turn_id (turns contains, for work units); path →
paths contains (* glob allowed); outcome, tool, status →
same-named columns (status is the work-unit status, outcome/status
of sessions is state); phase → work-unit phase (validated against
the phase vocabulary). SINCE/UNTIL apply to the table's primary time
column (started_at, handoff_at, raised_at, decided_at,
corrected_at, retracted_at). Default ORDER BY is the primary time
column descending; default LIMIT is 100.
Retracted rows. SHOW SESSIONS / TURNS / TOOL CALLS / ATTEMPTS
(and the FAILED / SUPERSEDED variants) add AND NOT retracted and note
how many rows were hidden; … INCLUDING RETRACTED returns them with
retracted = true. SHOW EVIDENCE FOR … INCLUDING RETRACTED likewise
includes retracted events. Tables without retracted rows accept the clause
and say it had no effect. Retracted sessions and attempts are not subjects:
WHY, TRACE, STATE and SHOW EVIDENCE FOR resolve ids against the
live projection only.
AttemptQL text ─► parser (hand-written recursive descent) ─► AST
─► planner: AST → DataFusion LogicalPlan over the public tables
─► DataFusion optimiser (pushdown of time range, project, provider into the segment table provider)
─► physical plan ─► Arrow RecordBatch stream
SQL text ─► DataFusion SQL parser ─► the same LogicalPlan type ─► same path
- Public tables are DataFusion
TableProviders over segments (RFC 0002) plus the MemTable, withmin/maxpruning from manifest statistics. TRACEuses a custom logical node (CausalTraverse) with a physical implementation that walks the causal adjacency index; in SQL it is exposed as the table functionattempt_trace(id, depth, direction).STATE ATcompiles to ordinary filters;state_at(ts)is the SQL table function equivalent.evidence(inference_id)is the SQL equivalent ofSHOW EVIDENCE FOR.- Result transport is Arrow (in-process, IPC over the local API, or Flight —
planned); the CLI renders tables with
evidencecollapsed to counts unless--evidenceis passed. - Limits: every query runs with a memory budget, a timeout (default 30 s), and cancellation; large results stream.
EXPLAIN <command> returns rows describing: the AttemptQL AST, the logical
plan, the physical plan, the segments scanned versus pruned (with the manifest
statistic that pruned each), the indexes used, the inference algorithm
versions consulted, and whether the MemTable was included. EXPLAIN ANALYZE
(planned) adds actual row counts and timings.
Errors are structured, positional, and never echo content-bearing data.
error[AQL0102]: expected DOING after subject
--> query:1:23
|
1 | WHAT IS project 'attemptdb' NOW
| ^^^ expected 'DOING'
| Code range | Category | Examples |
|---|---|---|
AQL01xx |
Lexical | unterminated string, bad relative timestamp |
AQL02xx |
Syntax | unexpected token, missing keyword (with the expected token) |
AQL03xx |
Resolution | unknown id, ambiguous short id (lists candidate ids), unknown project name (suggests SHOW SESSIONS) |
AQL04xx |
Semantics | unknown state name (lists valid names), BETWEEN with identical agents, DIFF with t₁ ≥ t₂ |
AQL05xx |
Execution | timeout, memory limit, cancelled, segment unreadable (names the file) |
AQL06xx |
Evidence | inference without evidence (internal invariant violation; reported, never rendered) |
Messages say what was expected, where, and what to do; they name ids and
column names but never render content, raw, prompts, paths beyond
repo_relative, or values from attrs.
The dataset is AttemptDB's own development history (planned; ids below are illustrative and follow the README's Codex hooks example).
SHOW FAILED ATTEMPTS FOR project = 'attemptdb' SINCE '-7d';| attempt_id | outcome | superseded_by | paths | started_at | evidence | uncertainty |
|---|---|---|---|---|---|---|
| att_01a03f10 | failed | att_01a03f9c | crates/attemptdb-capture/src/install/codex.rs | 2026-08-27T09:12:04Z | 14 events | 0.7 heuristic |
WHY work_unit wu_01a03e00 STATUS BLOCKED;| inference_id | edge_type | from_type | from_id | at | evidence | uncertainty |
|---|---|---|---|---|---|---|
| inf_01a03fa0 | blocked | event | ev_01a03f8e (permission_denied, tool Bash) | 2026-08-27T09:31:40Z | 1 event | 1.0 deterministic |
TRACE attempt att_01a03f9c CAUSES;| depth | edge_type | from | to | at | evidence | uncertainty |
|---|---|---|---|---|---|---|
| 1 | superseded | att_01a03f10 | att_01a03f9c | 2026-08-27T09:40:11Z | 3 events | 0.7 heuristic |
| 2 | caused | ev_01a03f77 (tool_call_failed, Edit) | att_01a03f10 | 2026-08-27T09:18:52Z | 1 event | 1.0 deterministic |
| 3 | triggered | ev_01a03f01 (prompt_submitted) | att_01a03f10 | 2026-08-27T09:12:04Z | 1 event | 1.0 deterministic |
STATE project AT '2026-08-27T09:30:00Z';| subject_type | subject_id | phase | status | attempts | failed | evidence | uncertainty |
|---|---|---|---|---|---|---|---|
| work_unit | wu_01a03e00 | DEBUG | active | 2 | 1 | 41 events | 0.5 heuristic |
SHOW HANDOFFS BETWEEN agent = 'claude_code' AND agent = 'codex';| from_session | to_session | from_provider | to_provider | at | shared_paths | evidence | uncertainty |
|---|---|---|---|---|---|---|---|
| ses_f35dc30f | ses_7a1e09b2 | claude_code | codex | 2026-08-27T10:02:15Z | 2 | 9 events | 0.8 heuristic |
SHOW EVIDENCE FOR att_01a03f10;Returns the 14 events (prompt, tool calls, the failed edit, the session
end) as events rows.
DIFF STATE '2026-08-27T09:00:00Z' '2026-08-27T11:00:00Z';| subject_type | subject_id | change | before | after | evidence | uncertainty |
|---|---|---|---|---|---|---|
| work_unit | wu_01a03e00 | changed | {phase: IMPLEMENT, status: active} |
{phase: VERIFY, status: active} |
63 events | 0.5 heuristic |
When the capture mode hid content, objective and approach are null and
uncertainty.reason = 'content_unavailable'; the structural columns are
unaffected.
The current position: AttemptQL owns the verbs (WHAT IS, WHY, TRACE,
STATE, DIFF, SHOW) and the evidence/uncertainty contract; SQL owns
arbitrary joins, aggregation, and ad-hoc analysis, with table functions
(attempt_trace, state_at, evidence) so nothing is reachable only from
AttemptQL. Whether to grow AttemptQL toward SQL (adding GROUP BY, joins) or
to keep it deliberately small and push power users to SQL is the main open
question of this RFC.
- AttemptQL is a small statement language, not a SQL dialect; the statements are those in §3.
- Every result is rows with
evidenceanduncertaintycolumns;insufficient_evidenceis a valid, first-class answer. - AttemptQL and SQL compile to the same DataFusion logical plan over the public tables of §2; DataFusion executes both.
- Subjects are
project,session,work_unit,attempt,turn,span,event,agent; ids accept display prefixes and unambiguous short forms. SHOWhides retracted rows by default;INCLUDING RETRACTEDshows them flagged. Theeventsview carries aretractedcolumn for SQL.commitsis an ordinary table: eachgit commitcall joined to the shaHEADmoved to (linkagesays how,shais null when unresolved);SHOW COMMITSlists it andattempts/work_unitscarrycommit_shas.work_units,decisions,correctionsandretractionsare ordinary tables; decisions are derived (rationale_source = 'derived') and, like work units, capped at confidence 0.7.EXPLAINreports segments scanned versus pruned and inference versions consulted.- Errors are coded, positional, and never echo content.
- The AttemptQL/SQL boundary (§9).
- Whether
WHEREshould accept full DataFusion SQL expressions or a restricted safe subset. AS KNOWN ATsyntax and whetherwork_units_historyshould be a separate table or aVERSIONSmodifier.- How
agent = 'claude-code'(display name) versus'claude_code'(provider id) versusagt_…should resolve; current plan: accept all, normalise to ids. - Whether
SHOW ATTEMPTS FOR path = 'src/**'needs a proper glob semantics document. - Result transport for large evidence lists (inline list vs lazy handle).