Skip to content

Latest commit

 

History

History
762 lines (629 loc) · 108 KB

File metadata and controls

762 lines (629 loc) · 108 KB
type reference
topic overview
audience
human
agent

Reyn Feature Map

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.

Visual overview

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
Loading

Feature index

OS Core

Workspace (P5)

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

Crash Recovery

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

Time-Travel / Rewind (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

Event System (P6)

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.


Chat Engine

Chat Compaction

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.

Router system prompt

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.

LLM router resilience

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.

LLM token streaming

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_CONTENTTEXT_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_acompletion call, the SAME OutboxHub/chat-event fan-out, and the SAME FlowView render 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.

Textual TUI: state + timing/token gutters (#3283 ①②④)

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 events live replay, a multi-session monitor, a cost dashboard — built on flowview's dashboard.py/compare.py example patterns. This would be a Reyn-internal live view over the SAME reyn events audit-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.

Textual TUI: conversation-pane interaction (#3476)

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

Textual TUI: session-switch reset + rehydrate (#3310 N1/N2)

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_attached on the wire, so a remote client's switch-hydrate is unimplemented today (RemoteReadModel.conversation_history degrades to empty regardless of a targeted session — the same frame-sufficiency boundary this arc follows throughout).


Control IR Ops

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_write remains removed. FP-0057 Phase 2b: semantic_search's query embed and index_update's ingestion embed BOTH now dispatch through the shared embed op (execute_op(EmbedIROp(...)), not provider-direct) — the PRE-embed redaction-egress seam applies to both paths symmetrically. The CodeAct-only ingestion entry reyn.api.safe.embed_index.embed_and_index was retired clean-break (deleted, no shim) in FP-0057 Phase 2b, replaced by the safe-mode reyn.api.safe.index_update() wrapper — which FP-0066 P1c then also retired clean-break (alongside the CLI reyn source command group): the in-core index is OS-internal only now (populated by internal index_update op callers, not a user-facing entry point); user RAG is the FP-0063 plugin. See Control IR.


Present layer

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 ingested annotation) rather than forbidden. Non-executable by construction sidesteps the UI-spoofing class that sandboxed-iframe UI protocols spend their complexity on.


Tool-Use Schemes

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_tool gate, swapping the LLM-facing tool surface never weakens the security or validation pipeline. The presentation is data; the gate is constant.


CLI

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

Config

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

Permissions

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).


Safety / limit-handling

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.


Content-layer defense

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_blockedcore/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.


Budget & Cost

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.


Memory & RAG

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_update op (add/update/remove/skip reconcile) over a pluggable IndexBackend, now OS-internal only (FP-0066 P1c retired the user-facing safe-mode index_update() entry point and the CLI reyn source command 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 just embed, and the pipeline you copy is the extension mechanism.


MCP

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.


Skills

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.yamlreyn.yamlreyn.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.


Pipeline

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.yamlreyn.yamlreyn.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.


Web & Protocol

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_modeTask 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.


Inline CUI

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.


Intervention

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_user routes to the operator identically whether the agent runs in the inline CUI, CLI, web / A2A, or MCP.


Sessions and identity

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.


Multi-Agent

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_id audit propagation — multi-agent reach is bounded and traceable, not free-form.


LLM org-design (runtime spawn primitives)

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).


Sandbox

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 the landlock_exec re-exec shim, which restricts itself and then execs the server.


Environment — ⚗ Stage 2 (experimental MVP)

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.)