| list_sessionsA | List discoverable sessions, optionally filtered by agent. Results are sorted by date (newest first) and paginated with
limit/offset so the payload stays small. The default limit
guards against dumping an unbounded number of sessions. Each summary carries kind ("agent" for a top-level session,
"subagent" for a spawned subagent/sidechain) and parent_uuid
(the parent session's uuid for subagents, else None). Subagent
detection covers Claude, OpenCode, Codex and Pi; Antigravity's format
has no parent signal, so it always reports kind="agent". Each summary also carries the F1.4 origin fields, None when the
source format has no signal (never fabricated): project_dir — the project directory the session ran in
(Claude: transcript cwd / Desktop metadata / verified slug
decode; Codex: session_meta.cwd; OpenCode:
session.directory; Pi: header cwd; Antigravity: no signal).
launch_surface — where the session was driven from (Claude:
"claude-cli" | "claude-desktop"; Codex: the raw
originator, e.g. "codex_vscode"; Antigravity:
"antigravity-ide" | "antigravity-cli"; OpenCode/Pi: no
signal).
Each summary also carries models — the unique model ids observed
in the session, in order of first appearance (Claude: assistant
message.model; Codex: turn_context.model; OpenCode:
message.data.modelID; Pi: assistant message.model;
Antigravity records no model signal). [] when the format carries
no signal — honest absence, never fabricated. Each summary also carries the A3 recency signal, measured against a
single wall-clock now sampled once for the whole call: last_activity — the last-activity timestamp as an explicit ISO
string (same instant as date; date is kept for backward
compatibility);
age_sec — whole seconds since last_activity (clamped at 0
when a future timestamp implies writer/reader clock skew);
activity — "fresh" if age_sec is at or under the
AI_R_STALL_SEC threshold (default 600 s = 10 min),
"stale" if past it.
Honest contract (F1.1): activity describes only the recency of the
last written record. It is not a claim about process liveness — a
session file cannot show whether its producer is still running.
"Running but silent" vs. "crashed" is a consumer-side inference
(correlate activity == "stale" with an OS pid-alive check); ai-r does
not fabricate it. Args:
agent: One of claude, codex, opencode, antigravity,
pi. When omitted, every supported agent is queried.
limit: Max sessions in this page. 0 means no cap (use with care:
may return a very large payload). Defaults to 100.
offset: Zero-based index of the first session to return. Use with
limit to page through total.
kind: Optional filter. "agent" returns only top-level sessions,
"subagent" returns only subagent sessions. When omitted
(default), both kinds are returned.
noise: Noise filter — a session is noise when it is a spawned
subagent (kind == "subagent" or parent_uuid set).
"include" (default) returns everything, "exclude" drops
noise sessions, "only" returns only noise sessions.
kind and noise compose (AND).
project_dir: Keep only sessions whose project_dir equals this
path or is a descendant of it (path-boundary aware:
/a/b matches /a/b and /a/b/sub, never /a/bc);
trailing slashes ignored. Sessions without a project_dir
signal never match. Composes with the other filters (AND).
redact: When True (default) secrets in emitted title /
extra values are masked as [REDACTED_<TYPE>] and the
response carries a redactions type→count dict when any
replacement happened; False returns raw titles. Returns:
{"sessions": [...], "total": int, "offset": int, "limit": int, "truncated": bool}. total is the full count matching the
agent (and kind/noise) filter; truncated is True
when more sessions remain beyond this page. When total == 0
the dict additionally carries diagnostics (scanned agents +
session counts, source-dir presence, cause hints) so an empty
inventory is explainable. |
| read_sessionA | Read a single session by uuid; agent is an optional hint. Args:
uuid: Session identifier.
agent: One of claude, codex, opencode, antigravity,
pi. Optional: when omitted, the uuid is looked up
across every parser (session ids are unique across agents in
practice). If — rarely — the same id exists under several
agents, a candidates list is returned (not an error) so
the caller can re-ask with an explicit agent.
offset: Zero-based index of the first message to return
(applied to the projected {role, content} list).
limit: Maximum number of messages to return. Defaults to
:data:_MESSAGES_CAP (100). A non-positive value means
"no upper bound".
redact: When True (default) secrets in the emitted title,
message content/intent/qa are masked as
[REDACTED_<TYPE>] and the response carries a
redactions type→count dict when any replacement happened
(see ai_r.redact); False returns the raw content.
with_tokens: When True (F3.3) attach token usage read at
request time (nothing runs in the background): * ``summary["tokens"]`` — the session's flat
:func:`ai_r.tokens.session_tokens` block (exact where the agent
records usage, a labeled estimate otherwise, honest
``source=None`` without any signal);
* ``summary["component_tokens"]`` — the
:func:`ai_r.tokens.component_tokens` breakdown: the transcript's
estimated token volume split across ai-r's event taxonomy
(``user_turn`` / ``assistant_turn`` / ``thinking`` / ``plan``
and a ``tool_call`` per-``tool_kind`` sub-dict), always
``source="estimate"`` (never merged with the exact tier),
``None`` on an empty transcript;
* per-message ``tokens`` on projected entries that carry exact
usage — Claude (deduplicated per streamed API call), OpenCode
and Pi. Codex / Antigravity / user turns carry **no**
``tokens`` key at all (absent, not ``null``). The dedup/attach
is decided on absolute message positions BEFORE pagination, so
page boundaries never shift which record is "first" for a call.
Default ``False``: output is byte-identical to the historical
shape (no ``tokens`` / ``component_tokens`` key on the summary or
any message). The token blocks carry only integers and
ai-r-authored labels (never raw session text), so they stay
outside the F2.1 redaction pass by construction.
include_subagents: When ``True`` attach
``summary["subagent_rollup"]`` — the parent session's
``component_tokens`` block plus one per spawned subagent child
(resolved via :func:`ai_r.session_stats.children_of` on
``parent_uuid``) and a ``total`` folding parent + children through
the ``aggregate`` ``component_tokens`` metric. Each child entry
carries ``uuid`` / ``agent`` / ``component_tokens`` (the estimate)
plus **what it actually cost**: ``tokens`` read from the child's own
transcript on the honest three-tier ``source`` ladder — billed
``"exact"`` where the child's transcript records usage, a labeled
``"estimate"`` where it does not (a truncated / reference-only run),
``source=None`` without any signal (never a fabricated zero) —
``models`` (the model(s) it ran on — a persona pinned to a cheaper
tier shows up here), ``subagent_type`` (its persona, from the
child's own spawn metadata, falling back to the spawning call's
sidecar) and ``status`` where the spawn recorded one
(``"async_launched"`` for a background spawn). A childless parent
(or an agent like Antigravity that never records ``parent_uuid``)
yields an empty ``children`` list and a ``total`` equal to the
parent's own block — honest, not an error. Independent of
``with_tokens``. Default ``False``.
include_thinking: When ``True`` attach the model's reasoning
(``message.thinking``) as a separate ``thinking`` string field on
each projected message that carried it — kept OUT of ``content``
so the historical text projection is unchanged. Default
``False``: output is byte-identical to the historical shape (no
``thinking`` key on any message). Reasoning is opt-in to save the
caller's budget; ``Event.has_thinking`` / the presence of the key
signals which messages have it. Fail-soft: a message without
reasoning never carries the key.
Returns:
A dict with session metadata plus: * ``messages`` — the projected ``{role, content}`` list, sliced
to ``[offset:offset+limit]``.
* ``total`` — the full uncapped projected message count (the
length the slice was taken from).
* ``offset`` / ``limit`` — the pagination echo values actually
used.
* ``messages_truncated`` — True when the MCP hard cap stopped
extraction before every projected message could be returned.
* ``outcome`` — session outcome classification (F2.3):
``{status: success|failure|mixed|unknown, signals, user_verdict,
markers, tool_results, tool_errors, error_rate,
error_rate_reliable}``. ``status`` combines the tool-call
error rate (real flag only for Claude/OpenCode —
``None`` elsewhere, never guessed) with a calibrated bilingual
success/failure dictionary over the tail user turns;
``"unknown"`` when neither signal exists (SSOT
:mod:`ai_r.outcome`).
On an id collision (agent omitted, several agents own the id):
``{"ambiguous": True, "uuid": ..., "candidates": [...],
"count": N, "message": ...}`` where each candidate is a session
summary carrying its ``agent``.
On a missing session, returns an ``error`` dict instead of
raising (``agents_scanned`` lists the parsers probed when the
agent was omitted).
|
| find_file_editsA | Find every file edit across sessions, cross-agent by default. redact=True (default) masks secrets in emitted record fields as
[REDACTED_<TYPE>] and adds a redactions type→count dict when
any replacement happened; redact=False returns raw content.
Reference-by-default: to keep an audit listing small, each record does
not inline the full edit body. Instead it carries a light-weight
reference — input_sha256 (hash of the body) and input_chars (its
length) — so you can see a body exists and how big it is. Fetch the body
on demand with get_body / read_session (keyed by session_uuid Size-bounded output: over-long intent / assistant fields are cut
with a …[truncated] marker (named in the per-record
truncated_fields) and emission stops at a total byte budget
(output_truncated — distinct from the count-based truncated). Default time window: a call with NO narrowing filter at all (no
agent / since / until) is scoped to the last
7 days instead of the whole corpus; the response then carries
default_since (the applied bound) plus a note saying so. Any
explicit scope disables the default — pass e.g. since="1970-01-01"
to deliberately scan the full history. Thin wrapper over :func:ai_r.find_file_edits.find_file_edits
that translates the core ValueError contract into the
{"error": "invalid_argument", "message": str(exc)} shape the
MCP client expects. |
| find_tool_callsA | Find every tool call across sessions, cross-agent by default. redact=True (default) masks secrets in emitted record fields as
[REDACTED_<TYPE>] and adds a redactions type→count dict when
any replacement happened; redact=False returns raw content.
Filters always match the RAW, pre-redaction text.
session scopes the scan to a single session uuid (or a list of
uuids) — same semantics as the query facet. None = every
session; a wide since/until with NO session therefore
surfaces calls from unrelated sessions, so pin it when auditing one
conversation.
Exactly one of tool_name (exact, case-insensitive) or
tool_name_pattern (substring, case-insensitive) must be set. Optional filters combine with AND: input_contains /
output_contains (case-insensitive substring on the full,
pre-cap input/output), output_excludes (drop records whose
output contains it) and is_error (tri-state: None all,
True failures only, False successes only). output_mode
selects output truncation — "head"/"tail"/"smart";
None is adaptive ("smart" on errors, "head" otherwise).
Each record also carries is_error_reliable (True only for
Claude/OpenCode) plus the wrapper-aware classification: tool_kind
(edit/write/read/bash/task/skill/mcp/
web/other) and tool_resolved — the real name under a
Skill/Task/MCP wrapper (subagent type, skill name, or
"<server>:<tool>"); None when there is no wrapper or the
input carries no name signal. A record whose call has a correlated result also carries
tool_use_id — the join key back to a spawned subagent's own session
(the child stores it as extra.spawn_tool_use_id). On a spawn
(tool_kind="task") it additionally carries subagent: what the
child COST — model (the model it actually resolved to, which may be a
cheaper pinned tier than the parent's), agent_type (persona),
tokens (EXACT billed usage, source="exact", full token-block
shape), status, duration_ms, tool_uses. Honest gaps: a
background spawn (status="async_launched", sidecar written before the
run exists) reports its model with no tokens key — never a
fabricated zero; its real cost and persona come from
read_session(include_subagents=True) → subagent_rollup.children.
A record carrying several tool results drops the sidecar rather than
billing it to the wrong subagent. with_subagent_cost=True (opt-in) recovers exactly that for the spawn
records here: each subagent sidecar is JOINED to the spawned child's
own files, adding the persona (agent_type) from the child's
agent-*.meta.json, the models it ran on, its EXACT billed
tokens (source="exact", an estimate is never merged into the
billing field) and child_uuid. So a background spawn — anonymous and
price-less in the launch-time sidecar — becomes a named, priced row. The
child is preferred over the sidecar, which stays the fallback for a child
that cannot be joined (not yet on disk, meta corrupt): its tokens are
then left absent, never zeroed. Default False reads no per-spawn child
file (a cross-corpus scan does not pay the join).
Thin wrapper over :func:ai_r.find_tool_calls.find_tool_calls
that translates the core ValueError contract into the
{"error": "invalid_argument", "message": str(exc)} shape the
MCP client expects. |
| session_statsA | Summarise sessions, grouped and ranked — the bird's-eye audit view. Where find_file_edits / find_tool_calls return flat record
streams, this rolls the sessions themselves up by one dimension so you
can see how the work is distributed in a single call. group_by is one of:
"agent" (default) — claude vs codex vs opencode vs ...
"dir" — by working directory / project (the normalized
project_dir first — one real directory = one bucket across
agents — then cwd for codex/pi / project slug for claude;
"(unknown)" for agents without any signal).
"date" — by calendar day (YYYY-MM-DD).
"kind" — top-level agent sessions vs spawned subagent sessions.
"model" — by the model that produced the session. A session that
mixed models buckets as "(mixed)" rather than being attributed to
one of them; one whose transcript records no model is "(unknown)".
Neither is guessed. Pair with with_tokens=True to see what each
model actually cost.
Each group carries its session count plus enrichment from the shared
find_file_edits core: edits (file edits attributed to the group's
sessions), intents (distinct requests behind those edits), the
distinct agents in the group, and total messages. RISK-4 note: subagent detection is currently Claude-only. When no
subagent sessions are in scope, a group_by="kind" result shows a
single agent bucket — so the result always carries
kind_split_available (False here) plus a note making clear
that this is NOT a verified "no subagents", just an absent split. with_tokens=True (F3.3) additionally reads every matched session's
token usage at request time (nothing runs in the background) and
adds a folded tokens block to each group and to totals:
{input, output, reasoning, cache_read, cache_write, total, exact, estimated, unknown}. Per session the numbers are exact where the
agent's own files record usage (Claude message.usage, Codex
token_count, OpenCode message.data.tokens, Pi usage); a
session without a recorded signal (e.g. Antigravity) gets a
transcript-volume estimate — tokenized with the optional
tiktoken dependency (pip install "ai-r[tokens]") when installed,
else a rough chars/4 heuristic — and counts under estimated, never
silently mixed in as exact; no signal at all counts under unknown.
Sums that no session carried stay null (never a fabricated 0).
The block contains only ai-r-computed integers and labels — no raw
session text — so it is outside the redaction surface by construction.
Default False: byte-identical historical output, no extra reads.
Scan guard (token_scan_limit): because with_tokens reads every
matched session's files at request time, an unscoped run over a huge
corpus is a multi-hour I/O storm. When with_tokens is set with no
narrowing filter (agent/since/until) and more than
token_scan_limit sessions match, the call returns
{"error": "scope_required", ...} (naming the count and the limit)
INSTEAD of scanning — the check runs on the cheap inventory count before
any file is read. Narrow the scope, or raise token_scan_limit (0
disables the cap) to force the full scan. A permitted-but-large scan runs
but carries a warning. Thin wrapper over :func:ai_r.session_stats.session_stats that
translates the core ValueError contract into the
{"error": "invalid_argument", "message": str(exc)} shape the MCP
client expects. |
| incidentsA | Dangerous shell commands + regret reactions — the incidents preset. One call answers "where did an agent run something destructive — and
did it then apologise?". A preset over the existing core, not a second
engine: ONE query scan (type="tool_call", tool_kind="bash")
supplies the candidates, a deterministic danger dictionary (harvested
from public agent-guardrail rule sets, calibrated on real history)
selects the dangerous commands, and a bilingual (ru + en) regret
dictionary scans the next reaction_window messages (default 6) for
an apology/rollback reaction — the two-step check behind the
confirmed flag. Zero LLM, zero guessing: no dictionary hit → no
incident; no reaction → confirmed: false, never inferred. Filters (all parameters): agent, session (uuid or list of
uuids), since/until (ISO bounds on the call ts), category
(fs/git/db/net — unknown values fail loud),
confirmed (include default | only | exclude), noise
and project_dir (session-level, same semantics as query). Each incident record carries the query event id (walk its context
via query(relative_to=...) / read_session), the matched
patterns + categories, a char-capped command fragment
centred on the hit (token budget — full context stays on-demand),
is_error (null when the agent's format has no correlated
outcome signal — honest, cross-agent), confirmed and reaction
(message_index/offset/role/marker labels/capped preview;
null when unconfirmed). count/confirmed_count/
by_pattern always reflect the FULL match set; limit (default
50, 0 = no cap) bounds only the emitted records (truncated). Dictionary caveat (documented, not hidden): patterns are a
deterministic dictionary, not a shell interpreter — a command that
merely mentions a dangerous string (e.g. echo "rm -rf /") can
still match. Matching runs on the extracted command field (a Bash
description alone never fires) and always on the RAW stored text;
redact=true (default) masks secrets only in the emitted
session_title/command/reaction.preview fields
(redactions type→count dict when anything was masked). When
count == 0 the response carries diagnostics so an empty result
is explainable (missing source dir vs all-excluding filter vs a
genuinely clean history). Thin wrapper over :func:ai_r.incidents.incidents that translates the
core ValueError contract into the {"error": "invalid_argument", "message": str(exc)} shape the MCP client expects. |
| networkA | Network-egress audit — the network preset (F4.3). One call answers "where did an agent reach out to the network — and
how risky did those requests look?". A preset over the existing core,
not a second engine: ONE query scan (type="tool_call",
tool_kind="web") supplies the candidates — Claude
WebFetch/WebSearch, OpenCode webfetch, Codex web_search
(surfaced from web_search_call rollout records),
Gemini/Antigravity web_fetch/google_web_search; Pi records no
web tool (honest absence). The request target (url/query) is
extracted from each call's own input and assessed with a deterministic
risk dictionary: plain_http, credentials_in_url,
secret_in_url / secret_in_query (the redaction patterns double
as the detector), ip_literal_host, private_or_local_host,
punycode_host. Zero LLM, zero guessing: no extractable target →
honest null fields; a risk fires only on parse/regex evidence. Filters (all parameters): agent, session (uuid or list of
uuids), since/until (ISO bounds on the call ts), kind
(fetch|search — derived from the extracted fields, unknown
values fail loud), risk (include default | only |
exclude), domain (host equals-or-subdomain match), noise
and project_dir (session-level, same semantics as query). Each request record carries the query event id (walk its context
via query(relative_to=...) / read_session), the derived
kind, char-capped url/query (token budget — full context
stays on-demand), domain, the risks labels and tri-state
is_error (null when the agent's format has no correlated
outcome signal — honest, cross-agent). count/risky_count/
by_domain/by_risk always reflect the FULL match set; limit
(default 50, 0 = no cap) bounds only the emitted records
(truncated). Honesty caveats (documented, not hidden): risk labels are a
deterministic dictionary, not a threat oracle; MCP-mediated network
access (browser-automation servers etc.) stays under
tool_kind="mcp" — a name alone cannot prove an MCP server touches
the network, so it is never guessed into this audit. Risk assessment
runs on the RAW stored strings; redact=true (default) masks
secrets only in the emitted url/query/session_title fields
(redactions type→count dict when anything was masked). When
count == 0 the response carries diagnostics so an empty result
is explainable. Thin wrapper over :func:ai_r.network.network that translates the
core ValueError contract into the {"error": "invalid_argument", "message": str(exc)} shape the MCP client expects. |
| quotesA | User quotes of prior in-session content — the quotes preset. One call answers "where did the user quote something the agent said, and
what did they say about it?". When a user selects a chunk of a prior
message and comments on it (the "attach selection as context" flow), the
quoted text is embedded VERBATIM in their turn — no agent records it as a
structured field — so it is recovered by matching the user turn against the
text before it. A preset over the existing core, not a second engine:
query scans supply user turns + assistant turns, the reviewed text is
normalized (_normalize_rendered_text, reused from the plan-feedback
anchorer) and :func:difflib.SequenceMatcher finds the longest verbatim
run; a run below the minimum is not a quote (honest null), and text
pasted from OUTSIDE the session matches nothing (never fabricated). This is the cross-agent, chat-wide generalization of plan(feedback)
(which surfaces «plan quote → user comment» only for Claude's plan-approval
flow): quotes surfaces «any prior message quote → user comment» for
every agent, operating on the normalized event stream, not client markup. Filters (all parameters): agent, session (uuid or list),
since/until (ISO bounds on the user turn's ts), source_kind
(currently assistant — a user quoting the agent's prose; unknown values
fail loud), noise and project_dir (session-level, same as
query). Each record carries the user_turn event id (context on-demand via
query(relative_to=...)), source_id (the quoted assistant turn),
source_kind, quote_chars, and the char-capped quote + comment
(the user's turn with the quote elided). count/by_source_kind
reflect the FULL matched set; limit (default 50, 0 = no cap) bounds
only the emitted records (truncated). Caveat (documented): v1 sources are assistant prose (the common case —
quoting a tool's raw output is a future extension); the emitted
quote/comment come from the NORMALIZED text (markdown stripped), so
they are readable but not byte-identical to the raw turn (raw bodies stay
reachable via the event ids). redact=true (default) masks secrets only
in the emitted session_title/quote/comment. When count == 0
the response carries diagnostics so an empty result is explainable. Thin wrapper over :func:ai_r.quotes.quotes that translates the core
ValueError contract into the {"error": "invalid_argument", "message": str(exc)} shape the MCP client expects. |
| audit_briefA | Token-lean, budgeted session digest for auditors — the audit_brief preset. One call answers "what happened in this session, verbatim where it
matters", inside a hard character budget. A preset over the existing
core, not a second engine (project preset rule): ONE query scan over
the session's events supplies the user turns (VERBATIM — the auditor's
ground truth) and the tool/file footprint (folded by
aggregate(group_by="tool_kind") + the edit/write rows' existing
file refs); the plan/plan_feedback projections supply the
decision trail; :mod:ai_r.tokens (the same SSOT behind
session_stats(with_tokens) / read_session(with_tokens)) supplies
the token breakdown. Deterministic budget algorithm: build the full digest, then tighten in a
FIXED ladder until the serialized JSON fits budget_chars (default
15000; 0 = unlimited) — (1) drop tool-call error details, (2) drop
the per-file edit list, (3) drop plan bodies + feedback quote/comment
texts (counts/references always stay; bodies on-demand via get_body).
User turns are NEVER truncated: if they alone exceed the budget the
response carries budget.over_budget: true + a note naming the
full projections — never a silently clipped ground truth. session accepts the full uuid or a unique id prefix (e.g. the 8-hex
head), resolved through the SAME id-prefix matching locate uses —
the digest's session.uuid echoes the full resolved id; an ambiguous
prefix is invalid_argument naming the candidates (capped), zero
matches is not_found with closest-title suggestions. agent is
an optional hint (None = the id resolves across every parser, like
read_session). redact=true (default) masks secrets in the
emitted title / user texts / plan bodies / feedback pairs. The
response is section-structured (session / user_turns / plans
/ tools / files / tokens / component_tokens / budget);
the CLI mirror is ai-r audit-brief <uuid> (markdown, --json for
this dict).
Thin wrapper over :func:ai_r.audit_brief.audit_brief: a ValueError
becomes {"error": "invalid_argument"}, an unknown session id
{"error": "not_found"}. |
| locateA | Find a session across all agents by uuid / id-prefix / title — locate. One call answers "I remember a session — where does it live and how do I
read it?". needle is a full uuid, an id prefix (e.g. the 8-hex
head), or a case-insensitive title substring. A thin preset over the
existing per-parser inventory (the same list_sessions walk — zero new
scanning code) with a deterministic algorithm inside: prefix-match on
uuid/path-stem OR substring-match on title, ranked by last activity
(mtime) descending. Each match carries where it lives (path /
agent / project_dir / date / size_bytes), the honest
local-content claim readable (false for a reference-only stub
whose transcript is not on this machine), and the ready-to-run commands:
read_command (ai-r read <uuid> --agent <agent>) +
resume_command (F2.2 — text only, never executed). limit bounds the emitted list (0 = no cap; count keeps the
full total, truncated flags the cut). Zero matches → an honest empty
with closest-title suggestions + diagnostics — never a fabricated
match.
web=true (v1, honest scope) adds web sessions KNOWN LOCALLY only:
materialized hook-export files ($SW_HOME/web-sessions, default
~/.session-watch/web-sessions) and ~/.claude.json → projects[*].lastSessionId teleport stubs (id known, transcript NOT
local — content_local: false). The fuller per-repo teleport-picker
sweep needs a PTY and is a documented follow-up, not guessed here.
Thin wrapper over :func:ai_r.locate.locate (ValueError →
{"error": "invalid_argument"}). |
| session_diffA | Reconstruct what the agent changed in one session — without git. Stitches the session's own edit records (Edit/MultiEdit
old_string→new_string, Write content, codex shell-exec
redirections) into a per-file, chronological diff. Returns
{"files": [...], "count": N, "caveats": [...]}; each file carries
its ordered edits (timestamp + intent + hunks) and a stitched,
readable diff. caveats always carries two honest blind spots: (1) this is a diff
of the agent's actions, not the git outcome — manual edits / partial
commits / merges are invisible; (2) RISK-3 — inherits the
find_file_edits shell-redirect gap (tee / sed -i / cp /
mv / heredoc writes are not detected and are silently skipped).
redact=True (default) masks secrets in the emitted diff/hunks/
intents as [REDACTED_<TYPE>] and adds a redactions type→count
dict when any replacement happened; redact=False returns raw.
Size-bounded output (mirrors find_file_edits): over-long intent
/ hunk bodies / per-file diff text are cut with a …[truncated]
marker and named in the per-file truncated_fields (indexed paths),
and whole-file emission stops at a total byte budget
(output_truncated; count keeps the true total). The full edit
body stays reachable on demand via get_body / read_session. Thin wrapper over :func:ai_r.session_diff.session_diff that
translates the core ValueError contract into the
{"error": "invalid_argument", "message": str(exc)} shape the MCP
client expects. |
| search_sessionsA | Case-insensitive search across sessions. Args:
query: Search string. Supports:
* Bare words: pwa manifest (AND default)
* Quoted phrases: "exact phrase"
* Negative prefix: -claude (Google-style, always excluded)
agent: Optional agent filter (claude/codex/opencode/antigravity/pi).
scope: Where to look.
* "title" — only session.title (default, backward-compat)
* "body" — message text + tool_use[*].input +
tool_result[*].content
* "all" — title OR body
operator: How to combine positive terms.
* "AND" — all positive terms must appear (default)
* "OR" — at least one positive term must appear
* "NOT" — no term (positive or negative) may appear
Negative -term prefixes are always excluded regardless
of operator.
limit: Maximum number of results. 0 or negative = no limit.
Applied after sorting, so it keeps the top-ranked matches.
sort: Result ordering.
* "relevance" — BM25 relevance over the matched text
(default). Pure-stdlib scoring; ties keep newest-first.
* "date" — newest-first by session date (the historical
pre-ranking order).
* "semantic" — F5.1 (optional ai-r[semantic]): the
BM25 top-50 candidates re-ranked by meaning with a local
multilingual embedding model (cross-lingual ru↔en,
synonyms); the response carries a semantic dict —
either the active ranking (active: true, model,
candidate count, blend weight) or the honest degradation
notice (active: false + plain-words reason +
fallback: "bm25", order stays BM25 — never a crash).
noise: Noise filter — a session is noise when it is a spawned
subagent (kind == "subagent" or parent_uuid set).
* "include" — no filtering (default).
* "exclude" — search only top-level agent sessions.
* "only" — search only subagent sessions.
Applied before matching, so excluded sessions never pay
the body-scan cost.
redact: When True (default) secrets in the emitted title /
snippet / extra fields are masked as
[REDACTED_<TYPE>] and the response carries a
redactions type→count dict when any replacement
happened; False returns raw content. Matching always
runs on the RAW stored text, so searching for a literal
secret still finds its session — only the displayed
snippet is masked.
include_thinking: When True fold model reasoning
(message.thinking) into the body/all search haystack
so a search matches text that lives only in the model's thoughts.
Default False: reasoning is excluded from matching to save
the caller's budget (turn it on only when a query must reach into
reasoning). No effect on scope="title". The two modes are
cached under separate keys, so toggling never serves a stale
haystack of the other mode. Returns:
A dict {"results": [...], "count": N} where results is the
list of session summaries and count is their total. When
scope is "body" or "all" and a match is found, each
summary includes a "snippet" field with the first matching
message excerpt (up to 200 chars) and may carry body_truncated.
When a scan matches nothing (count == 0), the dict additionally
carries diagnostics (scanned agents + session counts, corpus
date bounds, cause hints) so an empty result is explainable. With
sort="semantic" the dict also carries a semantic report
(active ranking vs BM25 fallback + reason). Errors are returned as a top-level {"error": ..., "message": ...}
dict (matches the existing convention). |
| queryA | Filter/search the unified session event stream — the workhorse verb. Every parser's messages + tool calls are normalized into one flat,
agent-neutral event stream (user_turn / assistant_turn /
tool_call(<sub>) / plan_event); this tool filters that stream
by facets — all behaviour is parameters, never hard-wired variants. Facets: type — user_turn | assistant_turn | tool_call |
tool_call(edit|write|read|bash|other) | plan_event. Bare
tool_call matches every subtype.
agent — one of claude/codex/opencode/antigravity/pi (all if omitted).
session — restrict to a single session uuid, OR a list of uuids
(the union of those sessions' events in one call — e.g. the ids
picked from a search_sessions / list_sessions result).
Duplicates collapse; an unknown uuid contributes nothing. An empty
list or a non-string item is a fail-loud invalid_argument —
never a silent unfiltered scan.
since / until — ISO-8601 bounds (inclusive) on the event ts.
file — substring matched against an event's referenced file path.
tool — substring (pattern) matched against the referenced tool
name OR the resolved name under a wrapper (tool_resolved) — so
tool="commit" also finds the Skill call that ran the commit
skill.
tool_kind — exact match against the wrapper-aware classification
of a tool call: edit / write / read / bash / task
(subagent spawn) / skill / mcp / web / other.
Every tool_call event carries tool_kind (in refs and as
a top-level field); wrappers whose input names the real actor also
carry tool_resolved — the subagent type under Task/Agent/
spawn_agent, the skill name under Skill/SlashCommand, or
"<server>:<tool>" for a Claude-style mcp__<server>__<tool>
call. No signal → no tool_resolved (never guessed). An
unknown tool_kind value is a fail-loud invalid_argument.
model — exact, case-insensitive match against the model that
produced the event's message: an assistant_turn /
tool_call / plan_event inherits the model of the assistant
message behind it and carries it as a top-level model field
(absent without a signal — user turns, Antigravity — so
aggregate(group_by="model") buckets those under
"(unknown)"). Model ids are agent-defined strings (no fixed
vocabulary); events without a signal never match; an empty string
is a fail-loud invalid_argument.
user_ref — filter user_turn events by the entity the user
referenced. A string with special values: "any" matches every
user turn that referenced some target; a bare kind
(e.g. "file" / "session") matches turns referencing that
kind of target; any other string is a substring matched against the
referenced target. Non-user events never match, so combining
user_ref with a non-user_turn type yields an honest empty
result.
has_thinking — filter by whether the event's message carried
model reasoning (Event.has_thinking). True keeps only events
with reasoning, False only those without; unset (None,
default) does not filter. Note the reasoning text itself is never
inlined — this only gates on its presence (fetch it with
get_body(id, include_thinking=True)).
text — substring matched against event text. With
sort="relevance" survivors are BM25-ranked using the same
scorer as search_sessions; sort="semantic" (F5.1,
optional ai-r[semantic]) re-ranks the BM25 top-50 candidates
by meaning with a local multilingual embedding model
(cross-lingual ru↔en, synonyms) — the response carries a
semantic dict reporting either the active ranking
(active: true, model, candidate count, blend weight) or the
honest degradation (active: false + plain-words reason +
fallback: "bm25" — the order is then plain BM25, never a
crash); sort="date" (default) orders by timestamp ascending.
relative_to (event id) + direction (prev|next) +
n (a positive integer, default 1, or "all") — the
neighbouring-turn walk. A numeric string ("3") is deprecated
and will be rejected in 0.6.0 — pass an int or "all".
Generalises the previous_user_intent used by find_file_edits
to both directions and any count. step_type chooses which
event type to collect (default user_turn). When
relative_to is set, other filter facets are ignored.
with_intent=True attaches a top-level intent (the request behind
the event, via the same previous_user_intent walk-back the legacy
tools use) to every returned event. Default False keeps the base
event shape unchanged.
noise filters at the session level before events are read — a
session is noise when it is a spawned subagent (kind == "subagent"
or parent_uuid set): "include" (default, no filtering),
"exclude" (top-level sessions only), "only" (subagent sessions
only). Ignored on the relative_to walk, like every other filter
facet.
project_dir also filters at the session level: keep only events
of sessions whose project_dir equals this path or is a
descendant of it (path-boundary aware, trailing slashes ignored) —
"events of this project". Sessions without a project_dir signal
never match. Ignored on the relative_to walk, like every other
filter facet.
parent also filters at the session level: keep only events of
sessions that are a descendant (transitively, any depth) of this
session uuid in the subagent parent_uuid tree — the whole spawned
subtree below parent (direct children plus nested). parent
itself is excluded (its own events are reachable via
session=<parent>). An unknown uuid matches nothing (honest empty
result). Ignored on the relative_to walk, like every other filter
facet.
group filters at the event level, plan_events only: keep only the
plan_events whose task_id (the plan-task grouping key — plan-file
slug or normalized title) equals this value. Non-plan events never
match when group is set, so combining group with a non-plan
type yields an honest empty result.
redact=True (default) masks secrets in the emitted text /
intent fields as [REDACTED_<TYPE>] and adds a top-level
redactions type→count dict when any replacement happened;
redact=False returns raw content. Redaction is emission-time only:
the text facet (and every other filter) matches the RAW stored text.
Events are reference-by-default: each emitted event's text is a
preview cut to ~160 chars (applied after redaction). A real cut is
marked with a trailing … and text_truncated: true (absent when
nothing was cut). id/refs/sha256 are untouched — fetch the
full body on demand with get_body(id). kind was removed — it duplicated noise (noise="only" for
subagents, noise="exclude" for top-level). It is kept in the signature
only as a fail-loud tombstone: passing any value returns an
invalid_argument error pointing at noise rather than silently
ignoring it (the MCP transport would otherwise drop an unknown argument and
return an unfiltered result — a silent wrong answer).
Returns {"events": [...], "count": N} or the standard
{"error": ..., "message": ...} dict on invalid arguments. When
count == 0 the dict additionally carries diagnostics (scanned
agents + session counts, corpus date bounds, cause hints) so an empty
result is explainable. |
| planA | Normalized plan atoms for a session — final vs drafts, grouped by task. Wraps query(type="plan_event", …) and normalizes every agent's plan
signal (Claude ExitPlanMode / Write plans/*.md, Codex
update_plan, Antigravity implementation_plan.md) into a single
:class:~ai_r.events.Plan shape — the per-agent signal is an internal
detail, never surfaced. Plans are grouped by task keyed on each plan's task_key — the
plan-file slug when the agent has one (Claude plans/<slug>.md,
Antigravity implementation_plan.md path), falling back to the
normalized title only when no plan file exists (Codex update_plan).
Within a task the latest plan is final and earlier revisions are
draft; plans of earlier completed tasks are completed_major. F3.4 default schema (measured ≈×3.7 cheaper than "everything inlined"):
the final plan's full text is inlined (body +
body_source — "approval_edited_by_user" when the user's
approval carried an edited plan, which is the AUTHORITATIVE text and
overrides the signal/file body, else "plan_signal"); drafts stay
references (bodies via get_body); every «plan quote → user comment»
pair extracted from the user's plan responses is returned under
feedback, each with a ref ("<session>:pf<N>") that
get_body resolves to the FULL raw response. Only agents with an
interactive plan-approval flow have the feedback signal (today: Claude —
an ExitPlanMode verdict or a rejected plan-file Write); others
honestly contribute nothing. Technical failures and bare no-comment
rejections are filtered out. F3.4 v2 additions: every plan atom carries version — its 1-based
revision number within the task group, chronological (drafts are
v1…vN-1, the final is vN); every feedback pair carries
plan_version (the answered revision's number), round (1-based
feedback-round number within the session — one round per user response
that produced pairs) and section — the heading of the plan section
the quote anchors to. Quotes are selected from the RENDERED plan, so
the anchor match strips markdown markup from both sides; a quote that
matches no section — or more than one — gets an honest null
anchor, never a nearest guess. Args:
session: Restrict to one session uuid (recommended).
kind: Optional filter — draft | final | completed_major.
group: Grouping strategy; only "task" is supported.
agent: Optional agent filter (claude/codex/opencode/antigravity/pi).
redact: When True (default) secrets in the emitted plan/feedback
fields (title/steps/body/quote/comment…)
are masked as [REDACTED_<TYPE>] and the response carries a
redactions type→count dict when any replacement happened;
False returns raw content.
bodies: "final" (default) inlines the final plan's full text;
"none" returns reference-only atoms.
feedback: True (default) adds the feedback pair list +
feedback_count; False omits both (historical shape).
rounds: "all" (default) returns every feedback round;
"last" keeps only each session's final round (v2). Any
other value fails loud. Returns:
{"plans": [...], "count": N, "feedback": [...], "feedback_count": M} — each plan carries
id/session_id/agent/title/task_id/kind/version/path/steps/status/ refs/sha256 (+ body/body_source on the final when
bodies="final"); each feedback pair carries
session_id/agent/plan_id/plan_version/verdict/round/quote/comment/ section/ref/ts (verdict ∈ rejected | stay_in_plan_mode;
quote is null for a free-text comment; plan_version/
section are null without a signal). Draft bodies and raw
responses stay on-demand via :func:get_body. Standard
{"error": ..., "message": ...} dict on invalid arguments. |
| get_bodyA | Return the on-demand body for an event / plan id. For a plan_event id: the full plan text and/or Codex steps
(bodies are deliberately kept off the event stream so callers pay for
them only when needed). For a user_turn / assistant_turn id:
the turn text. shallow=True (plans only) returns just the final plan of the id's
task, dropping the bodies of superseded draft revisions — the S6
case where a subagent receives one plan without the draft noise
(dropped_drafts lists the ids that were elided).
max_chars bounds the returned body/text (default 500_000,
generous enough that ordinary bodies are never cut; pass 0 to
disable). When it trips, the field is sliced with a …[truncated]
marker and body_truncated: true is set.
redact=True (default) masks secrets in the emitted
text/body/title/steps as [REDACTED_<TYPE>] and adds
a redactions type→count dict when any replacement happened;
redact=False returns the raw content.
include_thinking=True (turn ids only): attach the model's reasoning
(message.thinking) as a separate thinking field on the returned
body, re-read from the hosting message. Default False leaves the body
byte-identical to the historical shape (no thinking key). Fail-soft:
a turn without reasoning never carries the key.
Returns the body dict, or {"error": ..., "message": ...} on a bad id. |
| aggregateA | Roll a list of row dicts up by group_by — the generic stats verb. Reproduces session_stats (group_by ∈ agent/dir/date/
kind over a session inventory) and file_frequency
(group_by="file" over a find_file_edits record stream) as a pure
fold over already-materialized rows — no re-parsing. session_stats is
now a thin preset over this verb (rank_by="stats" + kind_split). Args:
rows: The row dicts to fold (query output, find_file_edits
records, or a session inventory).
group_by: The bucket key — a row field name (agent / dir /
date / kind / file / model — query rows carry
the producing model where the format records one / …).
Missing/empty values bucket under "(unknown)".
metrics: Which numbers each bucket carries. One or more of
count / sessions / edits / intents / agents /
messages / files / tokens / component_tokens.
Defaults to ["count"].
tokens (F3.3) folds per-row tokens blocks (the shape
session_stats(with_tokens=True) rows carry, or a bare int
total) into {input, output, reasoning, cache_read, cache_write, total, exact, estimated, unknown} — sums over
rows that carry each field (null when none does) plus
honest provenance counters (exact + estimated + unknown == len(rows)).
component_tokens (F3.3) folds per-row component_tokens
blocks (the shape :func:ai_r.tokens.component_tokens produces,
as read_session(with_tokens=True) attaches) into summed
event-taxonomy components (user_turn / assistant_turn /
thinking / plan and a tool_call per-kind sub-dict) +
total + provenance counters (estimated / unknown;
never exact — always an estimate). A component/kind no row
carried stays absent (never a fabricated 0).
rank_by: Group ordering — "default" (edits→sessions→count→label,
the file_frequency order) or "stats" (sessions→edits→label,
the session_stats order).
kind_split: When True, add the session_stats RISK-4 fields
(kind_split_available + a degenerate-split note). Returns:
{"group_by", "groups": [...], "totals": {...}} (plus
kind_split_available/note when kind_split) or the standard
{"error": ..., "message": ...} dict on an unknown metric/rank_by. |
| diffA | Stitch edit rows into a per-file chronological diff — the diff verb. Reproduces the synthesis of session_diff: given the edit events of a
session (query(type="tool_call(edit)", session=…) — plus write /
shell-redirect events), group them per file in chronological order and
render a stitched, readable diff. Bodies are fetched on demand (via each
event's stored message_index), never inlined on the row. Args:
rows: Edit-event dicts (query output). Each must carry an id
and a refs list with a file entry; unresolvable rows skip.
per_file: Group by file (the only mode today).
format: "unified" (the only rendering today).
redact: When True (default) secrets in the stitched output are
masked as [REDACTED_<TYPE>] and the result carries a
redactions type→count dict when any replacement happened;
False returns raw content. Returns:
{"files": [{"file", "edits", "diff", "hunks"}], "count", "caveats"}
(same shape + caveats as session_diff, size-bounded the same
way: capped fields named in the per-file truncated_fields, byte
budget → output_truncated) or the standard
{"error": ..., "message": ...} dict on an unsupported format. |
| detect_currentA | Return the current runtime identity (session + agent) from env/fs. NOT a session-query — this reads the runtime environment (env vars +
per-session flag files), reusing the exact cascade behind the
ai-r detect-agent / ai-r detect-session CLI subcommands. Args:
agent: Optional hint (accepted for symmetry with the CLI's
deprecated --agent flag); the cascade scans all agents. Returns:
{"session_id", "agent", "model", "resume_command", "candidates": [...], "verified", "self"} where session_id /
agent describe the highest-priority candidate, model is
the current session's model — the LAST assistant model
recorded in its transcript (null when identity is incomplete
or the format records no model signal — never guessed) —
resume_command is the ready-to-run shell one-liner that
reopens the detected session in its agent's CLI (F2.2, text
only, never executed; null when no real command exists) —
and candidates is the full cascade for disambiguation.
Returns {"error": ..., "message": ...} on an unknown
agent hint. |