| type | reference | ||
|---|---|---|---|
| topic | overview | ||
| audience |
|
Full feature inventory of the Reyn Agent OS, extracted from implementation. Each entry links to its reference or concept documentation.
Per-group Differentiation vs general agents callouts position each capability against self-hosted general agents (OpenClaw / Hermes) — Skill is one feature among many, not the headline. Maturity marks: entries are production unless tagged ⚗ experimental / MVP or noted as an optional dependency.
mindmap
root((Reyn<br/>Agent OS))
🧩 OS Core
🗂️ Workspace P5
Permission-gated IO
♻️ Crash Recovery
WAL state log
Forward-replay resume
CommittedStep memo
⏱️ Time-Travel
/rewind picker
Consistent-cut rewind
Branch registry
checkout-seq primitive
Multi-fork UX
Live-fork gate
📜 Event System P6
171 event types
Append-only JSONL
Replay
🗜️ Chat Compaction
Head+tail+body budget
Overflow retry loop
Adaptive token estimation
Multimodal token estimation
⚙️ Control IR Ops
file
ask_user
sandboxed_exec
web_search
web_fetch
mcp
mcp_install
embed
index_query
semantic_search
index_drop
index_update
compact
🔧 Tool-Use Schemes
Pluggable chat-layer
enumerate-all default
enumerate-all
retrieval
CodeAct
Per-call gate unchanged
⌨️ CLI
reyn chat
reyn agent
reyn topology
reyn memory
reyn permissions
reyn events
reyn mcp
reyn secret
reyn config
reyn auth
reyn cron
reyn web
reyn init
🔧 Config
3-layer cascade
safety
cost
sandbox
web
eval
plan
chat
embedding
voice
events
models
auth
mcp
multimodal
python
cron
action_retrieval
hooks
🔒 Permissions
Tier 0-3 model
4-layer resolution
CLI gates
🛡️ Safety
Force-close wrap-up
limit_denied event
On-limit modes
🔄 LLM Provider Resilience
litellm.Router delegation
Cross-model fallback chain
Retry-After aware retry
Per-deployment cooldown
Default OFF byte-identical
Credential rotation
🧪 Content-layer defense
Threat-pattern library
Content fence
Tool-result guard
Memory-write block
Exec command scan
Inbound peer fence
Compaction secret redact
💰 Budget and Cost
Per-agent caps
Per-chain caps
Rate limits
Daily/monthly quotas
High-cost model warn
🧠 Memory and RAG
Embedding
SQLite index
Recall
Chat compaction
🔌 MCP
Transports
mcp serve
mcp install
📘 Skills
SKILL.md registry
Three-layer exposure
Hot-reload
Session visibility toggle
install_local
install_source
🔗 Pipeline
Step kinds
Primitives
Invocation tools
Driver-as-session
Crash recovery
Registration
🌐 Web and Protocol
FastAPI gateway
AG-UI SSE chat
A2A sync message/send
A2A async tasks
Webhook push
MCP-over-SSE
REST API
🙋 Intervention
ask_user routing
InterventionBus family
InterventionRegistry
🧬 Sessions and identity
Three-level model
Multiple Sessions per Agent
Per-session persistence
Global-cut rewind
Transport routing-key
🤝 Multi-Agent
Agent registry
Topology system
MessageBus
delegate_to_agent
🖥️ Inline CUI
Conversation view
Bottom-chrome drawer (Model/Agent/History/Cost/Ctx/Tool/MCP/Skill/Pipe/Hook/Cron/Menu/Help)
Tool-result one-line summaries
Above-input region (interventions, command UIs)
Input + slash-command completion
🐳 Environment
EnvironmentBackend
HostBackend
Container backend
🏖️ Sandbox
SeatbeltBackend
LandlockBackend
NoopBackend
SandboxPolicy
| Feature | Description | Documentation |
|---|---|---|
| Permission-gated IO | permissions.file.read / file.write is the readable / writable path set — one source read by both the runtime gate and the tool advertisement (#3458); unset = the schema default (<zone-root> / <zone-root>/.reyn), deny = empty, a list = that set. Outside it: JIT ask or deny |
Concepts: Workspace · Permissions |
| Feature | Description | Documentation |
|---|---|---|
.reyn/ layout + recovery-core classification |
Which .reyn/ subtrees are recovery-core (state/ + config/) vs persist / audit / cache / outside; the recovery-core write-gate (mutate config via dedicated ops, never raw file.write) |
.reyn/ directory layout |
| Config recovery (config-as-snapshot) | Config registries (.reyn/config/: mcp/cron/hooks/index) reconstruct from truncation-surviving config generations (full-state snapshots written by the durability worker, seq-keyed) — replacing the former config_changed-WAL-event replay, which a WAL truncation below the floor could silently drop (#2259 PR-1). The .yaml IS the durable snapshot, not a derived projection |
.reyn/ directory layout |
| WAL state log | step_started / step_completed / step_failed written to .reyn/state/wal.jsonl (StateLog); fsync'd off the event loop via the shared DurabilityWorker. #2259: durable-RECORD writes (snapshots / config / identity) are async fire-and-forget — the task loop never blocks on durability; step_started BLOCKS by design (durable-before-side-effect, so a crash-mid-op is detected as ambiguous for non-idempotent ops — #2275). Truncatable after snapshot. Not the audit trail — see Event System (P6). |
Skill Resume |
| Async-decoupled durability (recover-to-last-durable) | In-memory state mutates immediately on the task loop; the seq-keyed durable record is submitted fire-and-forget to the serial DurabilityWorker (the seq is assigned IN the worker). Recovery restores to the last durable record — a consistent prefix; the un-durable tail at crash is lost (relaxed durability). A persistent (§4-exhausted) durable-write failure latches durability_failed → the session fail-stops (DurabilityHaltError on new ops + run-loop halt) so in-memory cannot race a dead disk (#2259). #2280: the fail-stop also emits a session_halted chat-event so an IDLE operator learns the halt proactively — the Textual TUI's always-visible status line and the plain --cui renderers' bottom toolbar both show a HALTED banner with the reason, not only the next-op's raised error |
.reyn/ directory layout, Session construction |
| Forward-replay resume | SkillResumeAnalyzer reconstructs run state from state log |
Skill Resume |
CommittedStep memo |
Replay recorded op results on resume without re-invoking | Skill Resume |
| World-op bypass | Transient ops (web_search, web_fetch) re-execute fresh on resume | Skill Resume |
User-facing point-in-time rewind with branching. Phase 1 and Phase 2 (2a/2b/2c/2d) are production. Concurrent-live-fork (parallel live branches) is owner-rejected out-of-scope. Full design: ADR-0038.
| Feature | Description | Documentation |
|---|---|---|
/rewind picker |
Interactive current-branch checkpoint picker (seq / timestamp / kind columns) in the Textual TUI; a plain text list over --connect. Selecting a row runs the same /rewind <seq> checkout a typed one does |
How-to: rewind |
| Per-checkpoint anchor preview | Each picker row shows a rendered scroll-hint anchor | How-to: rewind |
| PITR reconstruct | Point-in-time snapshot + WAL-diff reconstruction to target seq | Time-Travel concepts · Crash Recovery |
| Consistent-cut rewind | Both substrates (runtime state + workspace shadow-git as-of-N) rewound atomically |
Time-Travel concepts |
| Append-only reset-record | Undo appends a reset-record at seq R; history before R is preserved on the current branch (no destructive rewrite) | Time-Travel concepts |
| Retention window + GC | Configurable checkpoint retention window; stale snapshots GC'd automatically | How-to: rewind |
| Branch registry | Abandoned-interval lineage: each fork receives a registry entry with origin seq | Time-Travel concepts |
checkout(seq) unified primitive |
Active-branch seq → undo; inactive-branch seq → fork-switch. One primitive for both directions | Time-Travel concepts |
| Multi-fork tree UX | Always-tree picker with per-branch anchor labels | How-to: rewind |
| Act-turn runtime-only rewind | Ghost-Replay memo truncate for rewind within an in-flight turn (no substrate round-trip) | Time-Travel concepts |
| Container-mode shadow-git | Shadow-git as-of-N rewind supported inside the container environment backend |
How-to: rewind |
| Deterministic CI rewind gate | test_live_rewind_gate.py — Phase-1 rewind deterministic gate |
— |
| Deterministic CI live-fork gate | test_live_fork_gate.py — Phase-2 fork / checkout deterministic gate |
— |
| tmux live e2e | P1 undo + P2 fork-switch verified on real terminal | — |
| Phase 2c: fork-then-edit | New branch on edit via ctrl+t |
How-to: rewind |
| Phase 2d: web surface | /rewind picker over AG-UI SSE / A2A; web edit via AskUserMessage UX (original message presented for edit + submit) |
How-to: rewind |
Agent archive-delete (reyn agent rm) |
Archive by default (soft-delete): data preserved — PITR generations + topology membership kept (agent dormant, not destroyed). --purge permanently hard-deletes (topology cascade fires immediately; no rewind possible). WAL-window GC auto-purges archived agents once archival seq leaves the retention window. |
CLI: reyn agent |
| Feature | Description | Documentation |
|---|---|---|
| 171 event types | Complete taxonomy: workflow / phase / LLM / tool / budget / permission / etc. | Events reference · Concepts: Events |
| Append-only JSONL | .reyn/events/<run_id>.jsonl per-run (EventStore); audit trail — append-only, rotation-based (not per-append fsync). Separate log and lifecycle from the recovery WAL (.reyn/state/wal.jsonl). |
Events reference |
| Replay | reyn events <path> streams events for audit and debug |
reyn events CLI |
Differentiation vs general agents: the agent loop is an OS-enforced contract — every side effect the LLM emits is a schema-validated, typed Control IR op (never a free-form string), every op routes through the same exclude → permission → dispatch gate regardless of which tool-use scheme is active, every value the agent produces lives in the workspace (P5), and every state change emits an append-only, replayable event (P6). Constrained and auditable by construction, not by developer discipline.
| Feature | Description | Documentation |
|---|---|---|
| Head+tail+body budget | Keeps the most-recent turns (tail) and earliest context (head) within per-component token budgets; turns between them are replaced by an LLM-generated summary | Chat Compaction |
| Overflow retry loop | When the compacted context still exceeds the model limit, budgets for head / tail / summary shrink monotonically per iteration until the prompt fits; fails fast with a structured error when no further reduction is possible | Chat Compaction |
| Adaptive token estimation | Learns a per-model token-count multiplier over time, reducing estimation drift across sessions | Chat Compaction |
| Multimodal token estimation | Estimates tokens for text and image content; image parts use a fixed per-part cost | Chat Compaction |
| Compaction lock | Async mutex prevents concurrent turn appends from racing with an in-flight compaction call | Chat Compaction |
Differentiation vs general agents: instead of naive truncation or an unbounded growing memory, Reyn budgets context as head + tail + LLM summary with a monotonic overflow-shrink retry, adaptive per-model token estimation, and multimodal estimation — predictable context management under a hard model limit.
| Feature | Description | Documentation |
|---|---|---|
| Static / dynamic SP split | The router system prompt separates a stable, cache-prefix-friendly head from per-turn dynamic sections | LLM invocation surfaces |
| Task-completion guidance | Anti-fabrication guidance steering the model to finish and verify rather than claim completion prematurely | SP-improvements study |
| Model-family-gated steering | A coarse model-family classifier gates non-Claude operational-steering hygiene — added only when the router model is non-Claude, kept off the Claude path | SP-improvements study |
| Memory-quality guidance (gated) | Guidance on what makes a good memory entry, rendered only when memory is in scope | SP-improvements study |
Differentiation vs general agents: these SP improvements are adopted by design-judgment (sound + low-cost + non-harmful), not gated on a limited-environment A/B — a measured null on one environment cannot prove a universal negative, so structurally-sound guidance is adopted while genuinely measurable wins are verified separately.
Config-gated litellm.Router slot-in for provider-resilience. Default OFF (llm.router.use: false) — the direct litellm.acompletion path is byte-identical. When enabled the Router owns infra retry, Retry-After handling, cooldown, and cross-model fallback; Reyn does not re-implement any of these.
| Feature | Description | Documentation |
|---|---|---|
| litellm.Router delegation | When llm.router.use: true, LLM calls route through a litellm.Router; Reyn delegates infra-exception retry / Retry-After / cooldown / fallback entirely to the Router |
Config: llm block · Reliability |
| Default OFF — byte-identical | use: false (default) keeps the direct litellm.acompletion path with no routing overhead; the on/off switch is the only code-path change |
Config: llm block |
| Cross-model fallback chain | llm.router.fallbacks maps primary deployments to an ordered fallback list; on primary failure the Router tries each fallback model in order |
Config: llm block |
| Retry-After aware retry | llm.router.num_retries caps infra retries; the Router natively honours provider Retry-After headers (fold of retry-engineering gap) |
Reliability |
| Per-deployment cooldown | llm.router.cooldown_time + allowed_fails cools a deployment after repeated failures; subsequent calls route to the fallback chain until recovery |
Config: llm block |
| Accurate cost on fallback | On fallback the actual responding model is recorded from response.model so cost attribution reflects which deployment served the call |
Budget config |
| Config-fingerprint Router cache | Router is cached per event-loop with a (model, config-fingerprint) key; a changed llm.router.* rebuilds the Router rather than silently reusing a stale instance |
Config: llm block |
llm.router.credentials rotation |
Per-model list of API-key env-var names; the Router cycles through active keys; a declared model with zero resolvable keys fails loudly — never a silent keyless deployment | Config: llm block |
Differentiation vs general agents: provider-resilience is delegated entirely to litellm.Router (Retry-After, jitter, cooldown, cross-model fallback chain, credential rotation) rather than re-implemented — the on/off gate keeps the direct path byte-identical, so replay and cost-recording work unchanged whether or not the Router is active.
Capability-gated, provider-agnostic token streaming from the LLM call through to the rendered reply (#3288, phases ③a–③d). Streaming is a rendering optimization only — history/persistence and the final reconstructed result are byte-identical to the non-streaming path (stream ≡ whole equivalence).
| Feature | Description | Documentation |
|---|---|---|
| Capability-gated streaming loop | recorded_acompletion streams only when the resolved provider/model declares streaming capability (a litellm inline capability query, never a hardcoded per-provider allow/deny list); usage is chunk-summed and emitted once through the existing single-chokepoint record_llm, never per-chunk |
AG-UI transport |
agent_delta chat-event |
Each streamed content-delta chunk rides a agent_delta chat-event (correlated by chain_id), never an OutboxMessage display kind — the completed reply still persists exactly once via the terminal kind="agent" outbox message (L9 whole-persist unchanged) |
AG-UI transport § reyn.event.<etype> |
| AG-UI generic multi-CONTENT | On the AG-UI wire, a streamed reply rides a REAL TEXT_MESSAGE_START → N TEXT_MESSAGE_CONTENT → TEXT_MESSAGE_END sequence (never a re-sent full-text CONTENT at completion, which would double-render on an accumulating generic client); a mid-stream-joining connection is closed by the snapshot/restore path always supplying the authoritative full text, never the deltas |
AG-UI transport § Text lifecycle |
| Textual TUI streamed-reply coalescing | The Textual TUI's L7 pump coalesces N deltas for one reply into exactly ONE FlowView entry (keyed by chain_id, updated in place via Entry.set_item), finalized by the terminal completion frame rather than appended a second time — the plain/repl renderer has no consumer and silently drops agent_delta (opt-in draw, no visible-garbage window) |
AG-UI transport § Textual TUI streamed-reply rendering |
| Visibility-gated streamed-reply updates (#3283 ③) | A streamed reply's live re-render is gated on whether its row is on screen (FlowView.track_visibility): off-screen deltas still accumulate in full but issue no Entry.set_item, and on_show replays the accumulated text in ONE update when the row scrolls back — O(1) model→view updates for a scrolled-away reply instead of O(deltas), with scroll-away-then-back showing the COMPLETE reply. An optimisation only (accumulation is unconditional); distinct from flowview's own off-screen render skip, which does not gate the update feed |
AG-UI transport § Textual TUI streamed-reply rendering |
Differentiation vs general agents: streaming is layered onto the SAME single-chokepoint
recorded_acompletioncall, the SAMEOutboxHub/chat-event fan-out, and the SAMEFlowViewrender model every other frame uses — no parallel streaming-only pipeline, no per-provider hardcoding, and no risk of a streamed reply diverging from its non-streamed reconstruction.
Two fixed-width FlowView columns per row: the left keeps entry STATE, the
right shows per-entry ELAPSED TIME plus the row's TURN's real prompt/completion
token split. A state chip, the third candidate, stays dropped — it would
duplicate the left gutter. Neither right-gutter figure is ever fabricated: a row
whose turn the runtime holds no figure for renders —, never 0.
| Feature | Description | Documentation |
|---|---|---|
| Left gutter — state-coloured glyph (#3273, #3283 ①) | Kind-driven glyph, EntryState-driven colour (RUNNING amber / SUCCESS green / ERROR coral / CANCELLED dim); a RUNNING glyph blinks off flowview's native animation_fps clock, no app-side timer |
AG-UI transport § Textual TUI gutters |
| Right gutter — addressed-row rail (#3490/#3491/#3493/#3496/#3526) | The ADDRESSED row is marked by a thin ▏ rail in the RIGHT gutter's leading cell — the edge facing the body — spanning the entry's whole post-wrap height, so one entry reads as one marked block. It sat in the LEFT gutter's trailing cell until #3526 moved it on the owner's instruction; both positions cost no body column and both divide the body from a gutter, and what changed is distance from the text, since most lines stop short of the right margin. There is exactly ONE addressed position — the entry highlight (highlight= since flowview 0.7.0; cursor= before it), which is also what ctrl+f search moves rather than holding a selection of its own (#3493) — so two rows can never both be marked by construction; selectable is left off for the same reason. The state glyph's own colour is unchanged — being addressed is a POSITION, not an outcome. Gutter CONTENT rather than a flowview--selected/--highlight component style, because flowview applies a component style as a base beneath each segment's own attributes, which a whole-row background (the user/failure ROW TINT) swallows. Both classes are left UNDECLARED, which flowview 0.6.1 honours by painting nothing (the row overlay uses the partial component style). Under 0.6.0 an undeclared class resolved to a concrete inherited style that flowview painted, rendering the addressed row near-black (#3496 → fixed upstream as textual-flowview#5, letting reyn drop the subclass that suppressed it). The rail shows only while the pane is being addressed (FlowView focused, or the search bar open); the position persists either way |
AG-UI transport § Textual TUI gutters |
| Right gutter — per-entry elapsed time (#3283 ④) | A tool-call row shows its LIVE elapsed while RUNNING or its captured FINAL elapsed once SETTLED, via flowview's additive right_decorator/right_gutter_width; every other entry (user/agent/intervention, and any RESTORED row) shows nothing — no placeholder, no "0s" |
AG-UI transport § Textual TUI gutters |
| Gutter show/hide (#3352) | Either column can be switched off at runtime — ctrl+g (left) / ctrl+t (right), both listed in the Help pane — handing its whole width back to the conversation body (an 80-column body goes 66 → 78 with the right gutter hidden, → 80 with both). The two sides are independent, following flowview's own two-flag granularity. Start state is config-backed (chat.gutters.left / chat.gutters.right); a keypress is session-scoped and never writes back |
AG-UI transport § Hiding a gutter · reyn.yaml § chat.gutters fields |
| Right gutter — per-turn token split (#3283 ④) | The agent-reply row that concludes a turn shows that turn's real prompt/completion token split (↑12k ↓1.8k), read by chain_id through BudgetTracker's bounded per-turn buckets (#3339's source-captured attribution, extended with the split) — one figure per turn, never repeated per row and never differenced out of cumulative counters. A row naming a turn with no held figure (no LLM call, evicted bucket, unknown chain_id, or a remote client where the buckets are not on the wire) renders —, never 0, and a turn that really used 0 renders ↑0 ↓0; a restored conversation shows no figures at all rather than reconstructed ones. The turn's USD cost is returned by the lookup but deliberately not drawn in this column |
AG-UI transport § Textual TUI gutters |
Unstarted direction (#3283 ⑤, not yet designed or scoped): a LIVE dashboard surface outside the conversation pane —
reyn eventslive replay, a multi-session monitor, a cost dashboard — built on flowview'sdashboard.py/compare.pyexample patterns. This would be a Reyn-internal live view over the SAMEreyn eventsaudit-event log the rest of the OS already emits, not the external, queryable-database/time-series/alerting "observability dashboard" Care boundary § Concrete examples from the landscape (Observability dashboards) deliberately keeps out of scope — that boundary is about NOT shipping BI-style cross-run aggregation infrastructure, not about the TUI never rendering a live in-process view of its own events.
| Feature | Description | Documentation |
|---|---|---|
| Empty-state welcome hint (#3476 ②) | A fresh session with no history draws a hint (reyn / Type a message to start / / commands · : skills · Help tab for keys) centered in the viewport instead of a blank composer-topped void. Uses flowview 0.6.0's empty=/empty_align=, which the library itself clears the instant the first entry lands — the app has no show/hide wiring of its own, so it cannot drift out of sync. Restore hydration was also collapsed to a single extend reflow in this same PR |
AG-UI transport § Textual TUI empty state |
| Lazy history paging (#3476 ④) | On restore, only the most recent 200 frames are materialised; older prefix pages in one page at a time via FlowView.ReachedTop (reach_threshold=3, re-arms once you scroll away from the edge) with a single insert_many(0, …) per page — flowview holds the scroll position across the reflow, so the row you're reading doesn't move. A paged-in tool frame still carries its restored terminal state (the same transition hydrate uses). Measured cost of hydrating everything instead: small (≈41ms for 40k frames) — this is forward infrastructure by owner decision, not a performance fix |
AG-UI transport § Textual TUI lazy history paging |
| In-conversation search (#3476 ⑤) | ctrl+f opens a one-line bar above the composer for case-insensitive incremental substring search over entry text. The cursor moves to the newest match and it is scrolled to center (scroll_to_entry(align="center")); Enter/↑ moves to an older match, Shift+Enter/↓ to a newer one, both wrapping; a n/M counter shows position; Esc closes the bar and returns focus to the composer, KEEPING the cursor on the hit so Shift+Tab resumes navigating from what was found (#3493). Opening the bar fully materialises any lazily-paged-in older history first, so a real match never reads as "not found" |
AG-UI transport § Textual TUI in-conversation search |
| Keyboard cursor / entry highlight (#3476 ⑥) | The conversation pane (reached via Shift+Tab, returned via Esc) gets a per-entry highlight (flowview 0.7.0 renamed this from cursor to highlight, freeing "cursor" for copy mode's text cursor). ↑/↓/PgUp/PgDn/Home/End move it (flowview built-in); it auto-arms on the newest entry the moment the pane gains focus. Enter/Space copies the cursor's entry text straight to the clipboard (any kind, bypassing /copy's ring); r sends a bare /rewind over the normal submit seam (not a direct jump to that entry — the conversation log's seq and the WAL seq are different spaces with no correlation between them) |
AG-UI transport § Textual TUI keyboard cursor |
| Copy mode — vim text cursor (#3507) | c from the conversation pane enters flowview 0.7.0's copy mode: a per-character text cursor over the rendered content, so part of a long reply can be selected and yanked from the keyboard (the entry highlight's finest position is a whole entry). The motions are flowview's own defaults (hjkl w b e 0 $ ^ gg G v V y zz zt zb Ctrl-E Ctrl-Y Esc, plus */n/N to search the selection) and reyn declares NO motion binding of its own, so the keymap cannot drift from upstream; c is the single key reyn adds, matching upstream's own example. Copy mode starts on the highlighted entry and holds that highlight fixed, so the addressed-row rail stays put while the text cursor moves |
AG-UI transport § Textual TUI copy mode |
| Feature | Description | Documentation |
|---|---|---|
session_attached switch barrier |
AgentRegistry.attach/attach_session emit a session_attached chat-event ({agent, session_id}) as an EventFrame put directly on repl_outbox with NO await between the attach flip and the put — a stream barrier holding BY CONSTRUCTION (before = old session's frames, after = new session's) |
AG-UI transport § The session-switch barrier |
| Local client reset + rehydrate on switch | TextualChatApp treats the barrier as a reconnect: clears every per-session client state (retained FlowModel, running-tool tracking, pending-intervention tabs, sent-queue view/widget + item-meta, in-flight streamed-reply tracking) and rehydrates from the NEW session's history.jsonl — never from a client-side cache, which is stale-by-construction while a session is detached (the forwarder drops its frames entirely) |
AG-UI transport § The session-switch barrier |
| Targeted hydrate seam | ChatReadModel.conversation_history accepts an optional (agent, session_id) — omitted hydrates the currently attached session (pre-N2 behavior unchanged); given, hydrates that specific session (possibly never attached in this client run) via AgentRegistry.get_session, no duplicated history.jsonl path literal |
AG-UI transport § The session-switch barrier |
Remote parity gap (N3, open): the AG-UI/SSE remote transport does not yet re-emit
session_attachedon the wire, so a remote client's switch-hydrate is unimplemented today (RemoteReadModel.conversation_historydegrades to empty regardless of a targeted session — the same frame-sufficiency boundary this arc follows throughout).
All ops are documented in the single reference page: Control IR
The op kinds below mirror OP_KIND_MODEL_MAP in schemas/models.py.
| Op | Description |
|---|---|
file |
read / write / edit / delete / glob / grep / regenerate_index (six fine-grained registry kinds) |
ask_user |
Pause the run, collect the user's answer via the intervention bus |
present |
Route bulk data + a declarative display template to the user surface without the data passing through LLM output tokens (Tier 0, fire-and-continue) |
sandboxed_exec |
argv under SandboxPolicy via platform-selected backend |
web_search |
DuckDuckGo search — Tier 1, default-allow |
web_fetch |
URL fetch + text extract — Tier 1, default-allow |
mcp |
Call a configured MCP server tool by name |
mcp_read_resource |
Read one MCP resource by URI (permission-gated, same axis as mcp) |
mcp_subscribe_resource / mcp_unsubscribe_resource |
Subscribe/unsubscribe to server-pushed resources/updated for one URI (requires a persistent connection; push lands as an mcp_resource_updated hook-event) |
mcp_get_prompt |
Fetch one rendered MCP prompt's messages by name (permission-gated, same axis as mcp) |
mcp_install |
Install / register an MCP server (registry / package / local source) |
embed |
Raw embedding primitive: batch texts → vectors (FP-0057 Phase 1). User-facing (compose with an external MCP vector-DB via pipeline) AND the shared logic internal RAG ops call; default-allow; PRE-embed redaction-egress seam |
index_query |
Vector similarity search over one indexed source |
semantic_search |
Macro (FP-0057 Phase 2a; renamed from recall — clean-break, fixes the recall/search_actions/memory naming collision): per-source-model embed query → index_query per source → merge top-K. Multi-model correct: each source's model is auto-adopted from its own recorded index (never caller-supplied); cross-model scores are never directly compared |
index_drop |
Destructive source removal — requires approval |
index_update |
Incremental/delta-reconcile ingestion into a source's index (FP-0057 Phase 2a): add/update/remove/skip against content_hash, source-model-bound, cost-warn surfacing; no full-rebuild mode |
compact |
Summarise / compact conversation history within budget |
index_writeremains removed. FP-0057 Phase 2b:semantic_search's query embed andindex_update's ingestion embed BOTH now dispatch through the sharedembedop (execute_op(EmbedIROp(...)), not provider-direct) — the PRE-embed redaction-egress seam applies to both paths symmetrically. The CodeAct-only ingestion entryreyn.api.safe.embed_index.embed_and_indexwas retired clean-break (deleted, no shim) in FP-0057 Phase 2b, replaced by the safe-modereyn.api.safe.index_update()wrapper — which FP-0066 P1c then also retired clean-break (alongside the CLIreyn sourcecommand group): the in-core index is OS-internal only now (populated by internalindex_updateop callers, not a user-facing entry point); user RAG is the FP-0063 plugin. See Control IR.
Show bulk data to the user without the data passing through LLM output tokens — the agent routes a data ref + a declarative display template to the user surface directly. The LLM sees the data's shape (schema + preview) and binds paths; the renderer joins the template against the full data the LLM never ingested. Declarative + non-executable by construction (a vetted component catalog + JSON-Pointer bindings, no code) — safety from the primitive's shape, not layered policy.
| Feature | Description | Documentation |
|---|---|---|
present op |
Tier 0 (ask_user's sibling), fire-and-continue; data_ref XOR data_inline, template XOR blueprint. data_ref read authority == file.read |
Present reference · Concepts: Present layer |
| v1 component catalog | Display-only read-only components: text / markdown / code / diff / keyvalue / table / list / image; $bind JSON-Pointer (RFC 6901) bindings, row-relative for table/list |
Present reference § catalog |
| Presentation-guard + renderer discipline | Surface-universal guard strips ESC/control sequences at one seam (every leaf, incl. never-ingested data); Rich-markup safety is structural in the renderer (markup-inert sinks, no escaping) | Concepts: Present layer § guard vs renderer |
| 4-stage fallback | Registered template → inline blueprint → default viewer (from data shape) → generic (always renders); degrade-never-fail, drops audited in the ack | Present reference § fallback |
presentations.yaml registry |
Operator-registered named templates (presentations.entries), hot-reloaded at the turn boundary; the LLM authors inline blueprints only |
reyn.yaml § presentations |
presented event (P6) + replay-as-cache |
Audit event carries refs + stats, never content bytes; replay/rewind re-renders best-effort from the ref, or an expiry placeholder when it is gone (display-only, no reconstructed state) | Present reference § replay |
Differentiation vs general agents: general agents reproduce bulk tool data as LLM output tokens (expensive, and lossy when the model summarizes to fit). reyn's present layer routes the data's handle + a declarative template to the surface directly — display costs ~0 output tokens, the user sees full fidelity, and blind presentation is audited (an OS-computed
ingestedannotation) rather than forbidden. Non-executable by construction sidesteps the UI-spoofing class that sandboxed-iframe UI protocols spend their complexity on.
How tools are presented to the LLM and how its calls are dispatched is a pluggable scheme, selectable for the chat layer (tool_use.scheme x tool_use.transport in reyn.yaml, FP-0066 P4b #3247). The chat layer defaults to scheme=enumerate-all / transport=tool_calls. Non-default schemes are opt-in. All schemes route every tool call through the same OS gate (exclude → permission → dispatch), so the security and validation pipeline is unchanged whichever scheme is active.
| Feature | Description | Documentation |
|---|---|---|
| Pluggable scheme protocol | ToolUseScheme seam — tool presentation + interpretation + dispatch + feedback behind one interface; schemes are swapped by config, no OS change |
Tool-Use Schemes |
| Per-layer selection | Independent scheme per layer — chat / step — via tool_use config |
Tool-Use Schemes · reyn.yaml § tool_use |
universal-category (step default) |
The universal action catalog — 4 wrappers over every category, discover + dispatch by the action's ONE name (#3429 abolished the second, <category>__<verb> spelling). Every category enumerates a FIXED set of verbs, so the catalog enumeration is a constant: a resource (memory entry / MCP tool / registered pipeline) is an ARGUMENT to a verb, never an action of its own, and the payload the LLM is sent does not grow with what the operator has accumulated. Where collapsing a resource category removed the only surface NAMING those resources, a constant-count discovery verb replaced it (list_memory, pipeline_list, list_mcp_tools); FP-0066 P1b retired the rag_operation category (and its list_sources discovery verb) outright, along with the rest of the layer-1 agent-facing RAG tools |
Tool-Use Schemes · Universal catalog |
enumerate-all (chat default) |
Flat-native-JSON baseline — every usable tool presented flatly, dispatched by name. Best for small tool sets where determinism matters | Tool-Use Schemes |
retrieval |
RAG-over-tools — present a search tool, the LLM searches, the OS re-presents matched tools as callable. Supported opt-in for very large tool sets where full-catalog token cost is prohibitive; requires embedding.enabled: true (the FP-0066 P1a opt-in gate, default off) |
Tool-Use Schemes |
CodeAct |
Code-as-tools — the LLM writes a Python snippet whose in-code tool() calls run in a sandboxed subprocess under the same permission gate as a JSON call. Strongest for weak models |
Tool-Use Schemes |
category x content_fence |
The two axes composed — code-as-tools over the wrapper surface instead of the flat catalog, so the code-API stays constant as the catalog grows. For a weak model on a large catalog, where CodeAct's full enumeration costs too much | Tool-Use Schemes · reyn.yaml § tool_use |
retrieval x content_fence |
The search-first code-API: search_actions / describe_action / invoke_action and no list_actions — discovery is a search, not a listing. Costs one round trip less than the tool_calls retrieval cell, because the search result is a value inside the snippet rather than a re-presented tools= payload; requires embedding.enabled: true, and falls back to the flat catalog when the index is not ready |
Tool-Use Schemes · reyn.yaml § tool_use |
Differentiation vs general agents: the tool-use strategy is a swappable scheme —
enumerate-all/retrieval/CodeAct/ the default catalog — chosen per layer by config, without changing the OS. Because every scheme dispatches through the same exclude → permission →dispatch_toolgate, swapping the LLM-facing tool surface never weakens the security or validation pipeline. The presentation is data; the gate is constant.
| Command | Description | Documentation |
|---|---|---|
reyn chat |
Interactive multi-turn chat with a named agent | Reference |
reyn agent |
Create and manage named persistent agents | Reference |
reyn topology |
Create and manage communication topologies | Reference |
reyn memory |
CRUD + search + export/import for agent memories | Reference |
reyn permissions |
Inspect and revoke saved approval entries | Reference |
reyn events |
Replay event JSONL files or purge old files by date | Reference |
reyn mcp |
Serve, search, install, and manage MCP servers | Reference |
reyn secret |
Set / list / clear secrets in ~/.reyn/secrets.env |
Reference |
reyn embeddings |
status / rebuild / clear for the action embedding index (search_actions) |
Reference |
reyn config |
Show, query, and set effective configuration | Reference |
reyn auth |
Manage OAuth credentials — login (RFC 8628 device grant against auth.providers) / list / revoke |
reyn.yaml § auth |
reyn cron |
Manage and run cron-scheduled skill jobs — foreground scheduler / list jobs + next-run / status | reyn.yaml § cron |
reyn web |
Start FastAPI gateway server (HTTP + SSE) | Reference |
reyn init |
Scaffold reyn.yaml and .reyn/ in current directory |
Reference |
Main reference: reyn.yaml
| Block | Description | Documentation |
|---|---|---|
| 3-layer cascade | user-global / project / project-local + CLI flags | reyn-yaml |
${VAR} interpolation |
Env var expansion in all string fields via secrets.env |
reyn-yaml § interpolation |
safety |
Loop caps / timeout caps / on-limit policy | reyn-yaml § safety |
cost |
Per-agent / per-chain / daily / monthly token+USD caps | Budget config |
sandbox |
Backend selection (auto/seatbelt/landlock/noop) + on_unsupported |
reyn-yaml § sandbox |
web |
web.fetch SSL verify_ssl and ca_bundle override |
reyn-yaml § web |
chat |
Compaction trigger / head+tail retention / section token caps | Chat Compaction |
embedding |
Model classes / batch_size / cost_warn_threshold | RAG concepts |
voice |
Whisper model / language / device config still parses, but has no consumer since the Textual TUI it was built for was deleted (replaced by the inline CUI) — currently unavailable | Voice concepts |
events |
Rotation size/age + cleanup_period_days | Events reference |
models |
Class → LiteLLM model string with extends chain |
reyn-yaml § models |
permissions |
Project-wide default capability policy | Permissions config |
multi-agent |
Agent and topology defaults | Multi-agent config |
state_dir |
Runtime state directory (default .reyn/) |
State dir |
observability |
OTLP endpoint / headers / service name / content-capture toggle for the opt-in OpenTelemetry exporter | reyn-yaml § observability · Observability reference |
auth |
OAuth provider definitions for reyn auth login (RFC 8628 device grant) |
reyn-yaml |
mcp |
Configured external MCP server connections (transport + env) | Concepts: MCP |
multimodal |
Media handling caps (max_bytes, per-part token cost) |
reyn-yaml |
python |
python-step execution policy (safe / unsafe subprocess) |
Preprocessor |
cron |
Cron-scheduled skill job definitions | reyn-yaml |
action_retrieval |
Action-catalog search_actions retrieval tuning |
Universal catalog |
hooks |
Agent-lifecycle push/shell/pipeline hooks at 4 lifecycle points (turn_start/end, session_start/end) plus 4 external-event points fired outside the session's own run-loop: mcp_resource_updated, file_changed, cron_fired, webhook_received (the latter two non-blocking relative to their own ingress — dispatch never delays cron delivery or the webhook's HTTP response; webhook_received's vars carry only routing metadata, never the raw request body). push mode: wake:false passive context ride-along, or wake:true self-continuation bounded by safety.loop.max_hook_driven_turns. exec (renamed from shell_exec in #3226 Phase 4 — naming honesty, argv-list-only payload): sandbox-gated side-effect, output ignored. pipeline_launch: async/detached launch of a registered pipeline, input Jinja2-rendered from the firing hook-event's template vars. matcher: optional per-field filter (exact match, except uri/path which glob) narrowing which hook-events fire a hook. Cross-session push routes to another session's inbox via the session field. Shell-hook consent routes through the intervention bus → an above-input closed-set intervention in the inline CUI ([A]lways / [y]es / [n]o; Always persists to ~/.reyn/shell-hooks-allowlist.json); falls back to stdin on non-interactive surfaces. All exec runs emit hook_shell_executed P6 event ("tool" group; prefix exec: or exec_capture: — renamed from shell_exec:/shell_push: in #3226 Phase 4). Hooks emit attributed [hook:name] messages — history is never silently mutated. |
reyn-yaml § hooks · Concepts: hooks |
fs_watch |
Operator-declared filesystem watch paths (paths, debounce_seconds) firing the file_changed external-event hook on create/modify/delete. Restart-only (OUT-set) — no op/tool verb lets an agent register or widen a watch. Requires the watchdog extra; degrades to a no-op warning without it. |
reyn-yaml § fs_watch · Concepts: hooks |
| Hook-Event Bus + Composer (Phase 4a/4b/5, proposal 0059) | Per-Session pub/sub HookBus (broadcast, no consume; independent of Sync hook dispatch) plus a Composer (reyn.hooks.composer) that correlates multiple bus-observed hook-events into one composed:<name> event via 8 ops (all/any/seq/window/debounce/correlate_by/count/deadline), config-parsed with a load-time cycle-check (DAG). deadline (issue #3166) is the dead-man op — the only one that fires on ABSENCE: it arms on an on pattern and fires if its until pattern does not arrive within ttl for that key, reusing the same per-key PendingStore/TTL-sweep/QueuePolicy/correlate_by machinery (the sweep's discard branch becomes a fire branch, no new mechanism); crash-durable by default (#3180): ComposerDef.durable defaults to True for deadline alone, routing its pending set to DurablePendingStore (<per-session state dir>/composer_pending.json — a full-state snapshot file, never WAL-derived, so it survives WAL truncation structurally; carries the CLAUDE.md truncate-falsify witness) so an armed monitor comes back after a restart with its original arm instant; an explicit durable: false is allowed but re-arms the load-time UserWarning. Every other op keeps best-effort/crash-non-durable pending state (InMemoryPendingStore — losing a debounce buffer costs one notification, not the monitoring itself); overflow (drop_oldest/drop_newest/reject) and ttl-eviction always emit a metadata-only composer_dropped P6 event (payload content never recorded); a fire emits composer_fired. Composers are startup-only — a composers: config change takes effect on the next session start, not via the hooks hot-reload seam (a live Composer's in-flight PendingStore correlation state has no reload-time reconciliation yet; known limitation, originally recorded only in closed #2881, tracked here per #2890 F8). Phase 4+ hardening (#2886/#2890): a HookBus subscriber-queue drop is fail-visible — a per-subscriber drop counter (snapshot_drop_counts()) plus a metadata-only bus_subscriber_dropped P6 event on first-drop/every-Nth (never per-drop, publish stays a sync/never-raises hot path); policy.max_events_per_key bounds a single correlation key's buffered-events list length (drop-oldest + composer_dropped(reason=per_key_event_cap)) so an external-event storm hammering one key cannot grow it unboundedly during the ttl window; emit_hook_event's event_name is now pattern/length-constrained (^[A-Za-z0-9_.-]*$, max_length=200) so control characters/newlines/unbounded length can never reach the constructed kind. Full reachability path wired (Phase 5 part 1, #2881, closed): a Session reads composers: from the same 4-layer additive combine as hooks: and auto-starts every configured Composer (start_composers, called from run()); composed:<name> is now a loadable Sync on: target (an open namespace, prefix-accepted in reyn.hooks.loader, not enumerated in the fixed ALLOWED_HOOK_POINTS); a dedicated bridge (reyn.hooks.composed_consumer.ComposedEventConsumer) subscribes to the Bus and runs any Sync-registered hook matching an observed composed:* event via HookDispatcher.dispatch_bus_event — without re-publishing to the bus (Composer itself still never calls HookDispatcher directly, keeping the Bus-only invariant true). A composed→wake chain is bounded by the existing max_hook_driven_turns loop-valve with zero new bounding logic (every wake traverses inbox kind="hook") — pinned by a flip-witness Tier-2 test (a self-stimulating, naturally-unbounded composed→wake→turn_end→composed loop force-closes at the cap; falsified by raising the cap and observing the trip disappear). emit_hook_event (LLM-emit, Phase 5 part 2, #2885, delivered): a Control-IR op letting the LLM publish an llm:<session_id>:<event_name> event onto its own session's Bus — the first LLM-reachable producer in the pipeline, gated by a static kind allowlist (own-session llm:* only; builtin:*/composed:*/webhook:*/mcp:*/another session's llm:* all rejected) enforced before HookBus.publish. Reaches Sync dispatch only via a Composer correlating it into a composed:* event (no direct llm:* → hooks: path). Remaining out of scope: valve-persist (a recovery-gated follow-up). |
Concepts: hooks § Async Bus and Composer · Reference: control-ir.md § emit_hook_event |
| Config hot-reload | Runtime re-read of the IN-set (.reyn/config/mcp.yaml / cron.yaml / hooks.yaml) at the turn boundary without a process restart. OUT-set (reyn.yaml: security / budget / loop valve) is restart-only — the file-split is the structural write-gate. Two triggers: operator /reload and agent hooks_add LLM-op — scope-aware (#2088): the default/unnamed agent writes the GLOBAL layer, a named-agent session writes its OWN .reyn/agents/<name>/hooks.yaml layer, additive (never overriding) with the global + startup + per-session layers. Validate-before-apply + per-layer boot resilience + sandbox/loop-valve = safe-by-construction. |
Concepts: Config hot-reload |
| Feature | Description | Documentation |
|---|---|---|
| Tier 0 — always allowed | ask_user — no gate |
Permission model |
| Tier 1 — default-allow | web_search / web_fetch — deny-only gate |
Permission model · Permissions config |
| Tier 2/3 — declaration + 4-layer approval | exec (renamed from shell #3226 Phase 3) / mcp / file (out-of-zone) / python |
Permission model |
| Layer 1: config pre-approval | reyn.yaml hard allow / deny |
Permissions config |
| Layer 2: saved approvals | .reyn/approvals.yaml — persisted per path/server |
reyn permissions CLI |
| Layer 3: session approvals | In-memory for current invocation only | Permission model |
| Layer 4: interactive prompt | Ask user with persist choices (yes / always / just-this-path) | Permission model |
| Capability profile | Per-agent MCP / tool / category capability restriction (ProfileLayer in the ∩ model); agent can self-edit .reyn/agents/<name>/profile.yaml within the default write zone. A narrowed tool is withheld from the LLM's tools= catalog AND rejected if called anyway — both derived from the same resolved narrowing, re-derived when it changes mid-turn, so the model is never offered a capability the gate will refuse (#3378) |
Concepts: Capability profile · Reference: profile.yaml |
| Delegation policy | Config-selectable default-deny for delegated agents: delegation.capability_default=deny narrows any unbound delegate with the restrictive _delegate floor (same deny taxonomy as _untrusted). Binding replaces the floor (= the re-grant). Recursive: no laundering via re-granted coordinators. reyn audit (gateway:delegation-unsafe) flags re-grants with OPT-A reachability precision (HIGH exit on re-delegation/exec). |
Concepts: Delegation policy · Concepts: Capability profile |
Differentiation vs general agents: autonomous agents typically execute tools with minimal gating. Reyn requires per-capability declaration + 4-layer just-in-time approval (config → saved → session → interactive), a
.reyn/write zone, and per-skill credential scoping (Confused Deputy mitigation).
Bounded-operation checkpoints that stop the agent gracefully rather than hard-failing. See Safety framework.
| Feature | Description | Documentation |
|---|---|---|
handle_limit_exceeded unified checkpoint |
Single shared function runtime/limits/limit_handler.py that all seven loop / timeout / budget checkpoints call; owns the 3-mode dispatch, bus interaction, extension bookkeeping, and audit-event emission — callers only decide what limit fired |
Safety framework |
On-limit modes (OnLimitConfig) |
interactive (ask) / auto_extend (budgeted N times) / unattended (abort) via safety.on_limit.mode; applies uniformly to loop caps, timeout caps, and budget exceed paths |
Safety framework · reyn.yaml § safety |
| Force-close wrap-up | On a denied limit the LLM gets one final tool-less turn to summarise what was accomplished; delivered as a kind="agent" message with meta.limit_stopped |
Safety framework |
limit_denied event |
P6 audit event on every deny path (max_iterations / router_cap) |
Events reference |
| Decision-enabling fallback | When the wrap-up fails or is empty, a structured error states the limit hit, the config key to change, and partial-data availability | Safety framework |
Differentiation vs general agents: where free-running agents hard-stop or run away at a limit, Reyn's force-close turns a denied limit into a graceful LLM wrap-up plus an operator decision — it reports what it accomplished instead of vanishing or looping unbounded.
Scanning untrusted content (memory, tool results, context files, inbound peer messages) for prompt-injection / exfiltration / role-hijack patterns at the seams where it enters the prompt — a security transform at a content boundary, not OS decision logic. Design: content-threat scan proposal.
| Feature | Description | Documentation |
|---|---|---|
| Threat-pattern library ✅ | Security-domain regexes (injection / exfiltration / role-hijack / exec) applied to untrusted content across all scopes — security/threat_patterns.py |
Design |
| Content fence ✅ | Wraps untrusted content in explicit delimiters so model-visible boundaries are unambiguous — security/content_fence.py |
Design |
| Unified tool-result guard ✅ | One seam scans + fences tool-result content before it reaches the prompt — security/content_guard.py |
Design |
| Memory-write BLOCK ✅ | Memory writes that match threat patterns are blocked before reaching the agent's memory store — runtime/router_loop.py |
Design |
| Pre-exec command scan ✅ | sandboxed_exec scans the full joined argv against exec-scope threat patterns before any shell is launched; blocked commands emit exec_threat_blocked — core/op_runtime/sandboxed_exec.py |
Design |
| Context-file + A2A-inbound fence ✅ | Operator-editable context files (REYN.md/AGENTS.md) and untrusted inbound A2A peer messages are fenced + scanned on arrival — router_host_adapter.py (EP3) / inter_agent_messaging.py (S4b) |
Design |
| Compaction secret redaction ✅ | Secret-looking content is stripped from compaction input before summaries are persisted — security/secret_redaction.py |
Design |
| Untrusted capability narrowing (opt-in) ✅ | The CAPABILITY half of the same defense: while external_source-tagged content is live, apply the _untrusted profile. Default off (`safety.threat_scan.capability_narrowing: off |
turn |
Differentiation vs general agents: Reyn places content-layer scanning at the OS seams — the same content boundaries where secret interpolation already sits — as a security-domain transform that keeps OS decision logic free of skill strings (P7). Structural redundancy means checks already enforced by the sandbox / permission layer (e.g. absolute-path or pipe-to-shell writes) are not re-implemented as ad-hoc per-call scans.
| Feature | Description | Documentation |
|---|---|---|
| Per-agent caps | Token + USD hard limits with warn_ratio |
Budget config |
| Per-chain caps | Skill spawn count + token total per chain | Budget config |
| Rate limits | Per-model calls-per-minute sliding window | Budget config |
| Daily quotas | Persistent JSONL ledger, resets at local midnight | Budget config |
| Monthly quotas | Persistent JSONL ledger, resets at month boundary | Budget config |
| Per-turn token/cost attribution | Each LLM call is keyed with the turn's chain_id (ambient turn scope + ledger field), so a turn's tokens/USD is the real sum of that turn's own calls — tool-loop iterations included — never a difference of cumulative counters. A sub-agent's turn is billed to its own chain_id, not the parent's; a call with no turn in scope at all is attributed to no turn |
Budget config § Per-turn attribution |
| Token-count provenance | Every recorded token count says where it came from — provider (the provider reported it) vs estimated (LiteLLM's local tokenizer filled it because the stream carried no usage) vs unknown (not stated; how pre-existing records read). The estimate is kept, not suppressed; the origin rides the same ledger record, the same llm_response_received audit-event, and the same per-turn figure as the number itself, so an estimated turn is identifiable after the fact and a cap decision is auditable. A turn reads estimated if any one of its calls was |
Budget config § Token-count provenance · Events reference |
| Crash-durable cap counters | Every cap counter (daily / monthly / per-agent token+USD / per-chain spawn count) is reconstructed on startup from the fsync-per-append ledger — a crash inside the throttled budget_state.json save window cannot under-count a cap and re-allow over-budget calls or spawns. The state file is a best-effort cache; the ledger wins on recovery |
Budget config · state-dir |
extension_calls (+ safety.on_limit.mode) |
Budget-extension flow on hard cap hit; extension_calls > 0 opts the dimension into the unified safety.on_limit policy (ask / auto-extend / deny). The per-dimension ask_on_exceed bool was removed. |
Budget config |
High-cost model warn (cost_warn) |
cost_warn.enabled (default true) emits a model_cost_warn audit-event + inline conv-pane marker when the resolved model's input cost per 1M tokens exceeds model_threshold_per_1m_input_usd (default 5.0); fires at /model switch and session startup, de-duped once per model per session |
reyn.yaml § cost_warn |
Differentiation vs general agents: token + USD caps per agent / chain / model with refuse-on-exceed and a
safety.on_limit-driven extension flow, plus a pre-selection high-cost model warning — runaway spend is structurally bounded, not merely observed after the fact.
| Feature | Description | Documentation |
|---|---|---|
| LiteLLM embedding backend | Any provider via named model class config | RAG concepts |
| Local / offline embedding models | No in-process backend — reyn depends on litellm exclusively for embeddings (#3128 removed the in-process sentence-transformers backend, local-mini / local-e5, and the reyn[local-embed] extras). A local/offline setup runs a model server (Ollama / TEI / infinity) behind an operator-run litellm proxy and reaches it via a custom embedding.classes entry + LITELLM_API_BASE |
RAG concepts § Local and offline embedding models · Guide: enable semantic search § Case B |
| Batch embed | Configurable batch_size with concurrency semaphore |
RAG concepts |
| Dimension table | Static lookup for OpenAI / Voyage / Cohere | RAG concepts |
| SQLite index per source | .reyn/index/<source>/index.db with WAL mode |
RAG concepts |
| Chunk dedup | content_hash upsert prevents re-indexing |
RAG concepts |
Builtin user RAG (proposal 0063, now the builtin rag PLUGIN — ADR 0064 P5) |
Turnkey RAG over the operator's own documents, into an external sqlite vector store they name — distinct from the in-core IndexBackend rows above, which is OS-internal only as of FP-0066 P1b (FP-0057 C2: reyn hosts no user store; this plugin is the agent-callable way to search your own documents). Two pipelines (rag_ingest.ingest / rag_query.query) + two MCP servers + one skill (build-and-query-rag-corpus: routing/install in its router SKILL.md, with embedding-provider setup, the pipeline calls, and schema/tuning/backend-swap bundled as references/ files — #3162 consolidated the original five-skill split, itself #3162 part 1's fix for the original single skill exceeding the default read_file inline cap, back into the standard one-skill-plus-bundled-references shape), all shipped together as src/reyn/builtin/plugins/rag/ and installed in one call — install_plugin(source={"kind": "builtin", "name": "rag"}). reyn contributes exactly one thing to the chain — embed (C1: reyn is the sole embedder, so the per-chunk embedding_model column can never disagree with the vectors, C4) — and runs no python of its own: every other step is an MCP call or a reyn op (#2972). Ingest is incremental by content_hash (add/update/remove, C5) and reports metered spend (tokens_embedded/cost_usd/priced) plus an estimated dedup saving. Ships uninstalled — nothing registers until the operator/LLM explicitly installs the plugin (preserving #2932's trusted-by-configuration premise); until then ingest returns a decision-enabling pre-flight message (X1, the require_mcp shape) before any embedding spend. Discoverable before install (#3202 symptom 3): list_plugins enumerates every BUILTIN_PLUGINS-advertised name (an explicit-dict allowlist, no directory auto-scan) with its description/capabilities derived live from its own manifest — so an LLM learns "rag" exists from the ordinary tool-call flow, not from an install-time error message |
Guide: Build a RAG corpus · Cookbook config · Proposal 0064 §3.9a |
Builtin rag plugin MCP servers (ADR 0064 P5, formerly proposal 0063 P2) |
vector_store_server (sqlite-vec via apsw: upsert/query/list_metadata/delete, items↔vectors paired by index server-side; metadata filtering is an allowlisted plain-SQL WHERE, values parameterized) and chunker_server (chonkie; size/overlap_ratio are real tool parameters, never baked-in constants — R2/R4). Neither imports an embedding library — C1 holds structurally, not by comment. Shipped as self-contained scripts in src/reyn/builtin/plugins/rag/scripts/; install_plugin registers them only (ADR 0064 §3.11b, #3209 — install never provisions runtime deps). The operator/LLM creates the plugin's OWN venv (pip install -r its requirements.txt — chonkie/apsw/sqlite-vec/fastmcp) per the installing skill's SETUP steps, and points the registered spawn command at that venv's own interpreter — no console scripts, no pip install "reyn[builtin-rag]" into reyn's own env (that extra survives only for this repo's own dev/test direct-import coverage). An incomplete venv fails fast at spawn (#3060 preserved), never a runtime fetch |
Guide: Build a RAG corpus § Setup |
semantic_search op |
per-source-model embed → index_query per source → merge top-K globally (FP-0057 Phase 2a; renamed from recall). OS-internal as of FP-0066 P1b — no agent-facing tool wraps it |
Control IR |
index_update op |
incremental/delta-reconcile ingestion (add/update/remove/skip), source-model-bound, cost-warn surfacing (FP-0057 Phase 2a). OS-internal as of FP-0066 P1b — no agent-facing tool wraps it | Control IR |
| Action embedding index | ActionEmbeddingIndex (class-swap detection, cross-process build lock) — backs the search_actions tool the chat LLM uses. FP-0057 Phase 0: a thin domain adapter over the same pluggable IndexBackend doc-RAG uses (unified cosine + advisory-lock + storage; was a separately-implemented SQLite-WAL index pre-consolidation) |
Universal catalog § search_actions · reyn embeddings |
| Memory CRUD | list / read / remember_shared / remember_agent / forget |
Memory concepts · reyn memory CLI |
Differentiation vs general agents: beyond chat memory, Reyn ships a RAG framework — the
index_updateop (add/update/remove/skip reconcile) over a pluggableIndexBackend, now OS-internal only (FP-0066 P1c retired the user-facing safe-modeindex_update()entry point and the CLIreyn sourcecommand group; the in-core store is populated by internal callers, not a user-facing surface). A foundation the OS builds on, not a fixed memory feature. Embedding is litellm-delegated (a provider's own API, or a local model behind an operator-run litellm proxy), so a credential-free / offline setup is a proxy-configuration choice, not a bundled backend. User RAG is, separately, a turnkey path (proposal 0063): builtin pipelines that ingest a document folder into a sqlite store the operator owns — where reyn's contribution is deliberately justembed, and the pipeline you copy is the extension mechanism.
| Feature | Description | Documentation |
|---|---|---|
| stdio transport | Subprocess StdioServerParameters — implemented |
Concepts: MCP |
| HTTP transport | Streamable HTTP with request headers — implemented | Concepts: MCP |
| SSE transport | Reserved — raises NotImplementedError |
Concepts: MCP |
mcp serve |
Expose Reyn agents as an MCP server over stdio JSON-RPC 2.0 | reyn mcp CLI |
mcp install |
Fetch from registry, gate permissions, write config, store secrets. Three chat verbs: mcp_install_registry (official registry), mcp_install_package (npm/pypi/docker/github URL), mcp_install_local (direct command). CLI: reyn mcp install <SERVER_ID> or --source <SPEC>. |
Concepts: MCP · reyn mcp CLI |
| Secret management | Per-server env vars in ~/.reyn/secrets.env |
reyn secret CLI |
| Tool dispatch | Lazy-load and cache MCPClient per server connection |
Concepts: MCP |
| Resources consumption | List/read MCP resources + resource templates (list_mcp_resources / read_mcp_resource / list_mcp_resource_templates), gated by the negotiated resources capability |
Concepts: MCP · Control IR: mcp_read_resource |
| Resource subscriptions | Subscribe/unsubscribe to server-pushed resources/updated (subscribe_mcp_resource / unsubscribe_mcp_resource), gated by the negotiated resources.subscribe sub-capability; runtime-only subscribed-URI set survives a transport-death reconnect (re-subscribed, with a synthetic resync firing per re-subscribed URI); push lands as an mcp_resource_updated EventLog event and is also wired into the hook dispatcher as an external-event hook-point |
Concepts: MCP · Concepts: hooks · Control IR: mcp_subscribe_resource |
| Prompts consumption | List/get MCP prompts (list_mcp_prompts / get_mcp_prompt), gated by the negotiated prompts capability; no subscribe concept |
Concepts: MCP · Control IR: mcp_get_prompt |
| Elicitation | Server→client structured-input requests (elicitation/create) surfaced through reyn's own consent path — server-attributed prompt text, extra warning + no-autofill guarantee on sensitive-named fields, per-server elicitation: prompt|auto_decline + elicitation_timeout_seconds config; timeout/decline/headless all resolve to a clean cancel/decline response, never a hang; audit records field key names only, never values |
Concepts: MCP · reyn-yaml § MCP servers |
| OAuth 2.1 | Per-server auth: oauth (or {type: oauth, scopes, client_id, client_secret}) config, Streamable HTTP only (stdio/sse reject it); first auth is interactive (browser + localhost callback); tokens cached in ~/.reyn/oauth_tokens.json (outside bucket, mode 0600, per-server, never rewound — reuses the existing RFC-8628 device-grant store); headless with no cached token fails clearly instead of hanging; static bearer via headers unaffected |
Concepts: MCP · reyn-yaml § MCP servers |
Differentiation vs general agents: Reyn is both an MCP client (consumes external servers) and an MCP server (exposes its own agents) — standard-protocol interop in both directions, with stdio MCP servers subprocess-sandboxed under Seatbelt.
| Feature | Description | Documentation |
|---|---|---|
SKILL.md registry |
Explicit skills.entries declarations (no directory scan) — same registration model as mcp.servers |
Concepts: Skills |
| Three-layer exposure | L1 system-prompt ## Skills menu (name — description [path]) → L2 on-demand SKILL.md load (the dedicated load_skill op, FP-0066 P0/#3247) → L3 bundled-asset file-read (ordinary file read op) |
Concepts: Skills |
| Config cascade | ~/.reyn/config.yaml ⊕ reyn.yaml ⊕ reyn.local.yaml ⊕ dynamic .reyn/config/skills.yaml, later tier wins on name collision |
Reference: reyn.yaml |
| Hot-reload | .reyn/config/skills.yaml edits apply at the next turn boundary via the "skills" reload seam |
Concepts: Config hot-reload |
| Session visibility toggle | set_capability_visible("skill", name, visible) — restrict-only, cannot re-grant beyond the registered set |
Concepts: Skills |
skill_install_local |
Register a local skill directory into .reyn/config/skills.yaml; threat-scanned, permission-gated, config-generation recorded for crash-recovery |
Concepts: Skills |
skill_install_source |
Fetch + shallow-clone a skill from a git/GitHub URL into .reyn/skills/<name>/; same threat-scan/gate/recovery pipeline, plus path-traversal-hardened name sanitization and containment checks |
Concepts: Skills |
Builtin tier (reyn.builtin, proposal 0060) |
A code-shipped, package-data-packaged config tier merged BELOW every operator config file (build_builtin_config, F3a) — populated with the reyn-cheat-sheet skill (F3b): the reyn-specific usage guide (mechanism decision tree, present/self-review-via-agent+schema essentials, a worked flagship-pipeline example, a hook-example) named by the SP's mechanism-routing frame and existence-gated (D5e), and the draft-judge-revise workflow skill (the Evaluation-lens draft → schema-validated self-review → revise loop). (The RAG skill is NOT in this map — proposal 0063's build-and-query-rag-corpus moved out under ADR 0064 P5 into the rag plugin's own install-time registration, see the "Builtin user RAG" row above.) Every builtin skill ships provenance="builtin", visibility="on_demand" (#2971: out of the L1 menu, reachable via the skill_list tool → load the returned path with the dedicated load_skill op, FP-0066 P0/#3247) |
Concepts: Skills · Reference: Control IR |
Operator-explicit :skill invocation (#3100) |
A : namespace separate from / slash commands — :name [:name2 ...] [trailing] (stacked up to 6, one LLM wake) calls the SAME shared skill-load primitives the dedicated load_skill op wraps, directly (no skill__<name> op, and never routed through file.read either before or after FP-0066 P0/#3247). $ARGUMENTS/$0/$1/$name substitution (frontmatter arguments:), a same-name-across-config-tiers collision fires a skill_invoke_collision audit-event + operator-visible warning (never a silent shadow), an unknown :name errors with a suggestion, :list/bare : lists every menu/on_demand skill. TUI : completion mirrors the existing / completer. |
Concepts: Skills § Operator-explicit invocation |
Differentiation vs general agents: skills are instructions the model chooses to read, not programs the OS executes — the same layered-disclosure shape (menu → on-demand load) as MCP tool discovery, applied to task-specific technique instead of external APIs.
| Feature | Description | Documentation |
|---|---|---|
| Step kinds | transform (pure R1 expression), tool (dispatches any registered tool, e.g. sandboxed_exec for argv-only sandboxed exec with optional pipe-data→STDIN JSON threading; !expr YAML tag marks an expression arg vs a literal), agent (LLM leaf-worker, capability-narrowed to ⊆ the invoker) |
Reference: Pipeline DSL |
| Compositional primitives | call (sub-pipeline), match (runtime-value-selected sub-pipeline), fold (sequential accumulator), for_each (concurrent fan-out over a list + collect, S5-bounded), parallel (concurrent heterogeneous named branches + collect) — the full Appendix-B primitive set |
Reference: Pipeline DSL |
| R1 expression language | Field refs, comparisons, map/filter/all/any/count/join, lambdas in combinator slots — the total expression language transform.value / tool.args (!expr) / match.on resolve against |
Reference: Pipeline DSL |
Nested schemas + verify: schema |
SchemaRegistry-backed schema documents a tool/agent step's result is validated against |
Reference: Pipeline DSL |
| Registration | Explicit pipelines.entries.<key>: {path, description?, enabled?} declarations (no directory scan) — same registration model as skills.entries / mcp.servers; auto-loaded + registered at session start; surfaces as pipeline__<name>. Declared pipeline: name is authoritative — the entry key must match it exactly, or session start fails loudly |
Concepts: Pipeline registration |
| Config cascade | ~/.reyn/config.yaml ⊕ reyn.yaml ⊕ reyn.local.yaml ⊕ dynamic .reyn/config/pipelines.yaml, later tier wins on name collision |
Reference: reyn.yaml |
| Hot-reload | .reyn/config/pipelines.yaml edits apply at the next turn boundary via the "pipelines" reload seam |
Concepts: Config hot-reload |
pipeline_install_local |
Register a local pipeline DSL file into .reyn/config/pipelines.yaml; threat-scanned, permission-gated, config-generation recorded for crash-recovery |
Concepts: Pipeline registration |
pipeline_install_source |
Fetch + shallow-clone a pipeline from a git/GitHub URL into .reyn/pipelines/<name>/; same threat-scan/gate/recovery pipeline, plus path-traversal-hardened name sanitization and containment checks |
Concepts: Pipeline registration |
run_pipeline / run_pipeline_async |
Launch a registered pipeline by name — sync-attached (live step-progress audit-events, Ctrl-C cancel) or detached (result delivered later as an inbox message) | Reference: Pipeline DSL |
run_pipeline_inline / run_pipeline_inline_async |
Launch an ad-hoc, agent-generated DSL string — parsed and passed through a static-analysis gate (schema refs resolve, tool names resolve, no nested pipeline/delegate launch, agent steps run only under the invoker's own identity) before anything spawns | Reference: Pipeline DSL |
| Driver-as-session architecture | A pipeline run executes inside a spawned PipelineExecutorDriver session — reuses the ordinary session's run-loop, inbox, and WAL/crash-restore substrate rather than a bespoke execution path |
Concepts: Pipelines |
| Crash recovery | Per-run work-order (invocation.json) persisted before step 0; step-boundary generation snapshots give exactly-once, truncation-surviving resume (including mid-call/fold/for_each state) |
Concepts: Pipelines |
| S5 spawn bounds | safety.spawn.max_pipeline_fan_out_depth (for_each nesting depth, default 5) and safety.spawn.max_pipeline_spawns (ephemeral sessions per run, default 100) — both 0 = unlimited (operator opt-out) |
Reference: Pipeline DSL |
| Security floor | Launching a pipeline (any of the 4 launch tools) sits on the same HIGH-severity spawn-adjacent floor as delegate_to_agent; the 2 install tools sit on the same floor as skill_install_local / skill_install_source — an _untrusted- or _delegate-narrowed context cannot launch or register one |
Concepts: Pipeline registration § Security |
Flagship builtin pipeline (flagship.research_and_report, proposal 0060 F3b) |
The through-chain composition-thesis exemplar: web_search (input) → agent summarize (workflow) → agent self-review, schema-validated (workflow) → present (output), ships builtin + inert (invoke-by-name only). Self-review composes from the agent + schema primitives, with the threshold comparison done by a plain transform step |
Reference: Pipeline DSL § AgentStep |
Builtin RAG pipelines (rag_ingest.ingest / rag_query.query, proposal 0063 P3) |
The turnkey user-RAG chain, and the DSL's most substantial worked example: glob_files → per-file for_each fan-out (markitdown convert → chunker) → fold flatten → content_hash diff → embed only the new/changed → MCP upsert/delete. Ships builtin + inert (invoke-by-name). This file IS the extension mechanism (R2) — every backend is a *_server input with a default, so swapping vector-DB/chunker/parser means copying the YAML and re-pointing it, not patching reyn. Every tunable (chunk_size/chunk_overlap_ratio/file_extensions/max_files) is an input with a default, never a step constant |
Guide: Build a RAG corpus · Reference: Pipeline DSL |
Differentiation vs general agents: a pipeline is a deterministic, Turing-incomplete control-plane DSL, not another agent loop — the composition primitives are structurally closed (no nested launch, no arbitrary recursion), so safety and crash-recovery come from the DSL's shape rather than runtime policy layered on top of an unbounded execution graph.
| Feature | Description | Documentation |
|---|---|---|
| FastAPI gateway | REST + SSE server on localhost:8080 |
reyn web CLI |
| Surface opt-in/opt-out (FP-0058 P2) | A SurfaceSpec registry resolves each hosted surface's mount decision (CLI --enable/--disable > web.surfaces config > secure-default), unifying core surfaces onto the same conditional-mount seam the FP-0041 webhook plugin loader already used. Secure-default ON: AG-UI, the web UI, /health, REST /api, resources. Secure-default OFF (opt-in): A2A, MCP (broad machine-integration ports) |
reyn web CLI · reyn-yaml § web.surfaces · How-to: choosing which surfaces are hosted |
| AG-UI browser chat | The openui browser streams the session over the AG-UI SSE endpoint (/agui/chat/<name>/events) and submits turns / HITL answers via POST — the same single UI transport as the local CUI and the remote thin client |
Reference: AG-UI transport |
AG-UI remote chat (reyn chat --connect) |
Attach a thin CUI client to a single-writer server over AG-UI/SSE: display + turn submit + human-in-the-loop answering (answer by id), an active-driver token with symmetric seize, and fail-close with a grace window when the last operator surface is lost. Renders the SAME inline CUI as local on an interactive TTY (renderer selection + input/output loop are one shared driver; the main status bar — agent / model / cost / ctx% / working indicator — streams over STATE_* via a client-side read-model), degrading session-local dropdowns / pickers to empty/0/text on the wire. With 2+ clients attached (local + --connect, or several --connect), a submitted turn and a resolved HITL answer broadcast to every OTHER attached client too (same OutboxHub fan-out as agent output), carrying optional auth_user_id / auth_connection_id attribution — not just the agent's replies |
Reference: AG-UI transport |
| A2A Agent Card | Per-agent /.well-known/agent-card.json capability declaration |
reyn web CLI |
A2A message/send |
Synchronous JSON-RPC 2.0 single-turn endpoint per agent | reyn web CLI |
| A2A agent discovery | GET /a2a/agents server-level listing |
reyn web CLI |
| A2A async tasks | async_mode → Task envelope; GET /a2a/tasks/{run_id} poll, …/events SSE stream, …/cancel; mid-run ask_user surfaces as input-required |
A2A concepts |
| Webhook push | Status-transition POSTs to params.webhook_url for async tasks (reyn.web.notifications) |
A2A concepts |
| MCP-over-SSE | /mcp/sse + /mcp/messages for MCP client connections |
reyn web CLI · reyn mcp CLI |
| REST API | /api/* for agents / skills / runs / topologies / budget / permissions |
reyn web CLI |
| OpenTelemetry (OTLP) export | Opt-in, fail-open subscriber on the P6 audit-event log — maps events to OTLP spans/metrics/log records (GenAI semantic conventions, pinned version), off unless an endpoint is configured, content-capture off by default. Never a recovery source: .reyn/events + the WAL are unaffected and unchanged whether or not it is attached |
Reference: Observability |
Differentiation vs general agents: competitors specialise in broad, deep connectivity to the messaging apps you already use. Reyn keeps connectivity to standard protocols — MCP (client + server), A2A (sync + async tasks with webhook push), and a REST / AG-UI SSE gateway — rather than per-app integrations.
The default interactive reyn chat interface for a TTY: a Claude Code-style inline
renderer (src/reyn/interfaces/inline/, InlineChatRenderer) that streams the
conversation into the terminal's own scrollback rather than a full-screen app.
Replaced the earlier Textual-based TUI (with its full-screen Right Panel tabs and
a pluggable tool-result viewer registry) in full; --cui / non-TTY invocations
still use the plain ConsoleChatRenderer.
| Feature | Description | Documentation |
|---|---|---|
| Conversation view | Streaming conversation in scrollback with terracotta-accented ●/⎿ markers per message kind (agent/status/error/intervention/trace) |
— |
| Bottom-chrome drawer | Below the input: a live model │ agent │ cost │ ctx status line plus a focusable tab row that expands a drawer downward — Model / Agent (with the session tree beneath each agent) / History / Cost / Ctx / Tool / MCP / Skill / Pipe / Hook / Cron / Menu / Help. Cost shows a Session/Agent/Project × Total/Input/Output/Saved/Saved% breakdown (component cells marked ~ under >200k tiered pricing, — when the breakdown is unavailable — never conflated) plus the cumulative cache-hit rate; Ctx shows current context size vs the model's context window as a Claude Code-style %, the window source (litellm catalog vs fallback), free headroom, the last call's cache-hit rate, and the compaction subsystem's own separate estimate. Selecting a row dispatches its slash (/model, /attach, /session switch, /visibility, /hook). The Tool / MCP / Skill panes distinguish the three reasons a capability is unavailable — [off] = you turned it off with /visibility (flippable, so the row still dispatches), [--] · denied by capability profile = your envelope denies it durably (not flippable, so no slash), [--] · denied while untrusted content is in context = the ephemeral _untrusted narrowing, re-derived from the live conversation at read time and gone as soon as that entry compacts out (#3380) — and an empty pane says whether nothing is narrowed or the frame carries no visibility state at all (#3378). Status line and the open pane both refresh on every frame. The tab row shows WHERE the keyboard is: it dims while the composer holds focus, and the active tab inverts (text-style: reverse) once focus arrives, so entering the menu adds a marker rather than only removing the composer's cursor (#3528). Both cues are text-style rather than colour — under the ansi-dark theme the $text-muted/$text step this replaced resolved to one value and painted nothing |
— |
| Working indicator states | The spinner row names WHAT the turn is currently blocked on, not just "something is happening": Thinking… Ns (waiting on the model), Running <tool>… Ns (a tool is executing — sequential, one at a time), Waiting for you… Ns (a static amber line — any of ask_user / a permission confirm / cost-warn / a safety-limit checkpoint / an MCP install confirm / a hook confirm is pending). The elapsed seconds shown reset on each state change (time-in-this-state, not turn-total) — e.g. Running grep_files… 45s means this specific tool call has been running 45s, not that the whole turn has |
— |
| Tool-result summaries | summarize_tool_result renders a best-effort one-line, per-tool summary (e.g. Read 42 lines, 3 matches); always degrades gracefully to a truncated repr, never a full content preview |
— |
| Above-input region | Closed-set interventions (confirm/select/grant-deny) and command UIs (e.g. the /rewind checkpoint picker) render as a selectable row list above the input, rather than a modal |
Permission model |
| Input + slash-command completion | Input bar with /-prefixed command autocomplete (/rewind, /compact, /model, /help, /clear-history, …) and :-prefixed skill completion. Once the command name is settled by a space the menu switches to that command's arguments — its CompleterFn's candidates where one exists (5 commands), otherwise the command's registered usage line as a non-selectable ↳ usage: hint row (20 of 25 non-hidden commands declare one). A hint-only popup claims no keys, so ↑/Tab/Esc keep their normal meanings while it is up |
TUI keyboard shortcuts |
Differentiation vs general agents: Reyn's chat surface is a local, inspectable CLI with a live audit drawer (agents / cost / context / permissions) beneath the conversation — the operator sees what the agent is doing and spending in real time.
Cross-surface ask_user and permission routing — the same prompt reaches the operator over whichever surface is active (chat/services/intervention_registry.py).
| Feature | Description | Documentation |
|---|---|---|
| InterventionBus family | ChatInterventionBus (inline CUI) / StdinInterventionBus (CLI) / A2AInterventionBus (web) / _MCPInterventionBus (MCP) |
Permission model |
| InterventionRegistry | Tracks pending interventions and pairs each answer back to the waiting run | — |
ask_user lifecycle |
Pause run → surface prompt → resume on answer; async wait works across surfaces | Control IR — ask_user |
Differentiation vs general agents: human-in-the-loop is a first-class, surface-agnostic primitive — a permission ask or
ask_userroutes to the operator identically whether the agent runs in the inline CUI, CLI, web / A2A, or MCP.
| Feature | Description | Documentation |
|---|---|---|
| Two-level model | Agent (identity) → Session (conversation) |
Concepts: Sessions |
| Multiple Sessions per Agent | One identity, many parallel conversations; AgentRegistry maps name → {sid → Session} with a shared Agent identity |
Concepts: Sessions |
| Identity vs conversation scope | Memory / permissions / workspace / peer-addressing live on the Agent; history / inbox-outbox / current task stay per-Session | Concepts: Sessions |
| Per-session persistence | Each Session is snapshotted and restored independently (WAL-backed; snapshot re-keyed per Session) | Concepts: Sessions |
| Global-cut time-travel | /rewind moves every Session and Agent to the target checkpoint atomically (one global single-seq WAL) — per-Session granularity is in persistence, not the rewind |
Concepts: time-travel |
| Multi-session crash recovery | On restart the full name → {sid → Session} structure is reconstructed from the WAL + snapshots, not just one conversation | Concepts: time-travel |
| Transport routing-key | Default: native conversation-id → Session (namespaced, auto-spawn/resume). Explicit: join an existing Session by id (non-existent = error). Scoped within one Agent | Concepts: Sessions |
Differentiation vs general agents: the Agent / Session / runtime split is the mainstream agent-platform shape (cf. Assistant / Thread / Run); Reyn's distinction is what sits beneath it — every Session is WAL-event-sourced, permission-gated, and independently persisted, so one identity can hold many isolated conversations, with a single global consistent-cut rewind across them all.
| Feature | Description | Documentation |
|---|---|---|
| Agent registry | Named agents with role profiles + history.jsonl |
reyn agent CLI |
network topology |
Full mesh — any member to any member | reyn topology CLI |
team topology |
Star around leader — member-to-member forbidden | — |
pipeline topology |
Ordered — each member sends only to next | — |
_default topology |
Auto-synthesized full mesh for unassigned agents | Multi-agent config |
| MessageBus | Quiescence-based coordination with reply_to correlation |
Multi-agent config |
delegate_to_agent |
Async-dispatch to peer with topology permission gate | Multi-agent concepts |
| Agent hops cap | Max delegation depth via safety.loop.max_agent_hops |
reyn-yaml § safety |
chain_id propagation |
Trace multi-hop chains in P6 events | Events reference |
Differentiation vs general agents: delegation is topology-gated (network / team / pipeline) with a hop-depth cap and
chain_idaudit propagation — multi-agent reach is bounded and traceable, not free-form.
Three router-only tools the LLM uses to build a live organisation at runtime — distinct from the operator CLI / Topology YAML surface (which defines structure up front in configuration).
| Feature | Description | Documentation |
|---|---|---|
agent_spawn |
Create a new agent (name + role) under the calling agent's authority; capabilities capped at ⊆ the spawner's by construction; spawn lineage is OS-set / identity-keyed (forge-guarded) | Concepts: LLM org-design tools |
session_spawn |
Start a fresh-context sub-session under the calling agent to run a task in isolation; mode=ephemeral auto-vanishes after the task, mode=persistent stays; optional narrowing (restrict-only) at spawn time |
Concepts: LLM org-design tools |
topology_create |
Wire agents in the caller's spawn subtree into a named topology (network / team / pipeline) and optionally bind members to capability profiles (narrowing within the ⊆-parent envelope); subtree-restriction gate enforced by OS |
Concepts: LLM org-design tools |
| ⊆-parent capability model | Spawned agent effective capability = parent's live effective ∩ assigned profile; recursive no-escalation-via-spawn; closed across four stale-lineage axes (live, rewind-drop, absent-parent, name-reuse) | Concepts: permission model § LLM spawn |
| Operator spawn-tree bounds | safety.spawn.max_depth (chain depth) + safety.spawn.max_children (fan-out + topology member count) — DoS guard; exceeding either fires the safety.on_limit checkpoint (interactive=operator-prompt / unattended=reject / auto_extend); depth and children carry separate per-spawner extension keys; LLM cannot self-raise the base limit |
reyn-yaml § safety.spawn |
Differentiation vs general agents: the LLM designs the org structure at runtime — not free-form (every spawned agent is capability-capped at ⊆ the spawner, recursively), not pre-wired (the org emerges from the task), and fully rewind-safe (lineage is WAL-tracked; spawn and topology WAL-events survive crash recovery).
| Feature | Description | Documentation |
|---|---|---|
SeatbeltBackend |
macOS sandbox-exec SBPL profile generation |
Concepts: Sandbox |
LandlockBackend |
Linux 5.13+ Landlock LSM + seccomp-BPF stacking | Concepts: Sandbox |
NoopBackend |
Fallback audit-only with one-time WARN log | Concepts: Sandbox |
SandboxPolicy |
network / read_paths / write_paths / subprocess / env_passthrough / timeout |
Control IR — sandboxed_exec |
| Auto-selection | Platform detection + enforcement self-test (below) + on_unsupported: warn|error|ignore — the policy applies both when a backend is ABSENT and when one is present but does not enforce |
reyn-yaml § sandbox · Concepts: Sandbox |
| Enforcement self-test | A backend is selected only after it FIRES a real deny on this host, on every axis it claims: at resolution, subprocesses launched through the backend's own wrap_command attempt a write outside write_paths, and a process spawn under allow_subprocess: false, and both must be refused (a positive control on a granted action runs first in each, so "nothing happened" is never read as "denied"). Two probes, not one, because the axes need contradictory policies and fail independently — the write boundary is Landlock's alone, so a never-loading seccomp filter passes a write-only check. A backend that does not deny is treated exactly as an absent one, so on_unsupported — including the fail-closed error — fires on a present-but-inert backend. Cached per process on the backend name; paid only by a run that resolves a real backend, never at chat startup. NoopBackend is exempt (it claims no enforcement and is the fallback target). Witnesses the write + spawn axes — not network, not read_deny_paths, not the run() preexec path, not container backends |
Concepts: Sandbox § enforcement self-test · Guide: configure-sandbox |
| Launcher-shim argv0 resolution | A bare command resolving to a version-manager shim (pyenv/rbenv) is rewritten to the real binary by reading the manager's on-disk versions/<v>/bin layout — filesystem-only, no subprocess, strict version-token validation — so it runs directly under (deny process-fork) instead of dying on the shim's own launch fork. Fails open (asdf/mise, unknown shims) leaving the denial legible |
Events reference — sandboxed_exec |
| Launcher-fork denial classification | A (deny process-fork) failure at a PATH launcher/shim is classified denial_class=fork_denied, rendered to the model as an explicit "environment/config, not tool-availability" note, and recorded with argv0_resolved on the sandboxed_exec audit-events |
Events reference |
| Per-server MCP subprocess/network | Stdio MCP servers carry operator-declared subprocess: (default true — fork-based npx/uvx launchers can start) and network: sandbox knobs, same operator-ownership model (the model cannot set them) |
reyn-yaml § MCP servers |
| Per-hook shell sandbox triad | A shell hook's sandbox is scoped per-hook by operator-declared subprocess: / network: / write_paths: keys — each omitted key keeps its floor (no fork / no network / no writes), and only an explicit value moves that axis. Same operator-ownership model as the MCP triad: hooks_add (the model's only hook-authoring surface) can create template_push hooks only. Declaring a key on a non-shell scheme, or ill-typed, is a load-time HookConfigError rather than a silently-ignored security field |
reyn-yaml § hooks · Concepts: hooks § Sandbox |
| Hook-scope policy legibility | The agent-level sandbox.policy is op-scoped and does not reach a hook shell (a hook's floor must not move because a run's ops are unsandboxed). That scoping is never silent: an axis the operator declared there and the hook did not re-declare emits a sandbox_policy_not_applied audit-event + a WARNING naming the per-hook key that reaches it, so their expressed will is applied or refused, never dropped. An explicit per-hook value is a decision and reports nothing |
reyn-yaml § sandbox · Concepts: hooks § Sandbox |
| MCP write-denial diagnosis | A sandbox write denial names itself and the write_paths knob instead of surfacing as an opaque OS/library error — on both channels a denial can take: the launch path (denied launcher cache → the hint rides the init error, ahead of the stderr dump so fault-summary truncation cannot eat it) and the tool-call path (server running, a caller-passed path outside its scope → the denial returns as JSON-RPC tool-error content, never stderr, so the same predicate is applied to the error payload and the hint is appended as a content block the LLM and operator both read). Both remedies are named — the zero-config one (a path inside the server's working directory) first, the grant second. Diagnosability is only as good as the signature the denial carries: apsw reports a denied open as a marker-free unable to open database file, so the builtin vector store restores the OS errno on the failure path rather than let the sandbox denial read as a typo |
reyn-yaml § MCP servers |
Differentiation vs general agents: tool / code execution runs under an OS-level sandbox (Seatbelt / Landlock + seccomp-BPF) with an explicit
SandboxPolicy, rather than unsandboxed tool calls. Stdio MCP servers are also subprocess-wrapped — under Seatbelt on macOS, and on Linux through thelandlock_execre-exec shim, which restricts itself and then execs the server.
Repo-filesystem mechanism abstraction decoupling the workspace from where the repo FS lives. The host backend is production; the container backend is an exec-per-op MVP. See src/reyn/environment/.
| Feature | Description | Documentation |
|---|---|---|
EnvironmentBackend protocol |
Abstracts repo-FS read / write / exec away from the OS + permission layer | — |
HostBackend |
Default — identity over the local filesystem (production) | — |
DockerEnvironmentBackend |
⚗ Stage 2 MVP — repo FS + exec inside a Docker container (--container attach); exec-per-op |
— |
| Mount-mode launcher | ⚗ container launch with the repo mounted + devcontainer.json awareness / build-on-demand |
— |
Differentiation vs general agents: Reyn adopts the container-exec pattern those agents popularised (e.g. Hermes docker-exec), but keeps the OS + permission + audit layer on the host while only the repo FS lives in the container — sandboxed execution without surrendering governance. (⚗ Stage 2 / experimental.)