Modules
An agent core for Elixir apps: pi-agent-core's loop (Rho.Loop), omp/pi
JSONL sessions (Rho.Session), compaction, tools, and model sign-in
(subscription OAuth or API keys, Rho.Auth) on req_llm.
The output budget: a tool result over 16 KiB goes to
the model as its head and tail with a marker in the middle; the full
text is kept as an artifact the agent reads back with the read_artifact
tool, a page of lines at a time.
The model-provider credentials, one per provider, each pi's auth.json
entry as JSON in the credential store (Rho.CredentialStore) under
"auth:<provider>"
Signing in to a subscription (:anthropic, :openai_codex): the OAuth
flow (Rho.Auth.OAuthFlow) with the provider's own localhost redirect,
answered by this BEAM.
The OAuth authorization-code flows omp signs in with (/login anthropic,
/login openai-codex), built as omp's declarative engine builds them from
pi-catalog's rules/auth/anthropic.kdl and openai-codex.kdl
The per-day cost cap: what the model calls cost today, priced by
req_llm from each reply's usage (notional on a Claude or ChatGPT
subscription), against the host's Rho.Settings.daily_cap/1.
Context compaction, ported from pi's local summary method
(pi-agent-core/src/compaction/compaction.ts; omp writes it as
method: "soft"). When a request nears the model's context window, the
older part of the branch is summarized by the model into a compaction
entry (Rho.Session.compaction/4), and Rho.Session.context/1
sends that summary plus the recent tail from then on.
The host's settings for rho, config :rho, ..., each with a default that
works on a computer. :data_dir, :default_model and :system_prompt
may be given as {module, function, args} (or a 0-arity function),
called each time the value is read, so a host can keep them in its own
settings and change them at run time.
Where rho keeps secrets: the provider sign-ins and API keys
(Rho.Auth), a binary value per string key. A host seam:
config :rho, :credential_store, Module picks the backend.
The default backend: one 0600 file per key under <data dir>/secure/,
the key made filename-safe. Written through a temp file created 0600
before the secret goes in, then renamed over the old one.
The Dyn layer: code the agent writes, compiles and runs on the device itself (tools and screens), next to the fixed Core (the loop, sessions, sign-in, the core tools, the approval gate and the repair path, which ship in the app and which the agent can't change). The Core changes Dyn only through this module, one generation at a time
The approval gate on changing what runs (the pipeline's step 3, see
Rho.Dyn):
activating a proposed generation, or reverting to a chosen one by hand.
The production approval: the system screen-lock prompt (fingerprint,
face, or the phone's PIN, pattern, password or passcode) behind an
approve control the host shows with the change (Operator's approve chip).
This small process (Rho.Supervisor starts it with Dyn) holds what the
human confirmed; only the host's UI, after the prompt passed, calls
confirm/1.
The "approve all" setting: while on, the host's UI activates each candidate generation as it arrives instead of asking for approval (the same activation, so probation and revert-on-crash still apply). Only Dyn activations: reverting still asks.
Compiles a source set into generation n (part of the pipeline's step 2, see
Rho.Dyn) and loads, unloads and purges generations' modules.
The host's settings for the Dyn layer, config :rho, :dyn, [...] (see
Rho.Dyn). Every key has a default; a host that doesn't use Dyn sets
none of them.
Unified diff (as diff -u and git print it, three lines of context) between
two source sets %{relative_path => source}: what the approval card and
the rescue screen show for a generation.
One generation of the Dyn layer: what gens/<n>/manifest.json holds
(see Rho.Dyn.Store).
The Core process that decides which Dyn generation runs (activation,
probation, revert and safe mode, steps 3–4 in Rho.Dyn). It owns the
registry table (Rho.Dyn.Registry) and is the only writer of the
current pointer.
A primary :logger filter that keeps Dyn terms away from the logger's
formatting, in whatever process logs them.
Reads the Dyn registry: one ETS table owned (and only written) by
Rho.Dyn.Keeper, named like the Keeper. It maps logical names
to the current generation's modules
Incremental compiles: a proposal recompiles only the files that changed (or depend on a change at compile time) and reuses the rest from its parent generation, renamed. On a Moto G 2021 compiling the default front from source takes about 33 s, and recompiling a module from its stored Erlang code still about 350 ms (the Erlang compiler's passes): 24 s of a one-file change to the 68-module front. So a module is reused by renaming its compiled binary instead; on a Mac that takes 56 ms for the whole front, against 1.9 s through the Erlang compiler.
How Dyn screens plug into the host's UI: a host seam (config :rho, :dyn, screens: Module, Rho.Dyn.Config). rho builds, checks, selftests and
watches Dyn screens the same way whatever the UI; this says what a
screen is and how the UI tells the Keeper a launch got going
Mob screens as Dyn screens (config :rho, :dyn, screens: Rho.Dyn.Screens.Mob, see Rho.Dyn.Screens)
No screens (the default Rho.Dyn.Screens): Dyn code is tools and
modules. Without a UI there is no first frame to wait for, so
Rho.Dyn.Screens.after_first_render/1 applies the MFA at once: a launch's clock to
"stable" starts at Rho.Dyn.boot/1.
The seed: the Dyn sources an install starts from, which the host ships
(Operator's default front: a welcome screen, a component library, the
front's settings). The host embeds them at build time
(Application.app_dir/2 can't resolve priv/ on the device) and hands them
to start/2.
Selftests a compiled generation (the end of the pipeline's step 2, see Rho.Dyn),
one module at a time, each in a fresh process with a max_heap_size
(killed when exceeded, off-heap binaries included), a timeout and
trap_exit (a linked helper that crashes doesn't take the test down
unreported).
The Dyn layer as a Rho.ToolRegistry.Source: the current generation's
tools, read from the Keeper's registry table on every call, so an
activation or a revert (by hand, or automatic) changes the tools the
loop offers from its next model call. Every tool call's task is handed
to the Keeper first (Rho.ToolRegistry.Source.watch/2), so a Dyn tool's crash, or the kill at
its timeout, counts against its generation.
The Dyn layer on disk, under one root dir (<data dir>/dyn by default,
Rho.Config.data_dir/0)
An Elixir compiler tracer for Dyn compiles (Code.put_compiler_option(:tracers, ...)):
which modules of the generation a file needs at compile time, so
Rho.Dyn.Compiler knows when a file compiled for an earlier
generation can be reused as it is (Rho.Dyn.Reuse).
What a loop tells its subscribers, as {:rho, session_id, event}
messages (plain send, no PubSub). The lifecycle is pi's
(pi-agent-core/src/agent-loop.ts)
The model client the loop talks to. Rho.LLM.ReqLLM is the real
one (Anthropic, OpenAI, OpenAI Codex and Google through req_llm, with
the credential from Rho.Auth); tests inject a scripted fake.
The model calls through req_llm's providers, streamed, with the
provider's credential from Rho.Auth (fetched, and for a subscription
refreshed when due, per call)
Gives llm_db (the model catalog req_llm resolves models through) a snapshot that works on a phone.
The agent loop for one session: a GenServer with pi's lifecycle
(pi-agent-core/src/agent-loop.ts; events in Rho.Events).
The models rho can call right now (like omp's /models): the static
catalog filtered to the providers that have a credential (Rho.Auth).
Sessions in omp/pi's JSONL format (pi-coding-agent/src/session/ session-entries.ts, CURRENT_SESSION_VERSION = 3), so a session can move
between omp on a computer and a rho host (Operator on a phone, say).
The user settings the loop reads, a host seam
(config :rho, :settings, Module)
The default Rho.Settings: settings.json in the data dir (a missing
or unreadable file means the defaults; other keys in it are kept, so a
host may share the file). Each call reads the file.
rho's processes, for the host to start in its own tree (rho has no OTP application callback: a mob app starts its code by hand)
A tool the agent can call. Implementations are plain modules, so a host
can register one at runtime by module, or serve changing sets of them
from a source (see Rho.ToolRegistry).
The tools the agent can call, by name, from three places
A source of tools the registry asks on every call, for tool sets that
change while the app runs (the Dyn layer's generations, Rho.Dyn.Source:
tools the agent writes, compiles and activates on the device). Configure with
{Rho.Supervisor, tool_sources: [MySource]} or
config :rho, :tool_sources, [MySource].
Runs one tool call in its own task (under a Task.Supervisor, not linked
to the loop). The before_tool_call gate runs first, inside the task, so
a gate that waits (a biometric approval) never blocks the loop;
once the gate allows, the task tells the loop ({:tool_running, pid}),
which starts the tool's timeout clock from then.
Core tool: copy a staged Dyn source file to a new path under a new module
name (see Rho.Tools.DynTool).
Core tool: delete a staged Dyn source file (see Rho.Tools.DynTool).
Core tool: replace one exact, unique piece of a staged Dyn source file (see Rho.Tools.DynTool).
Core tool: list the Dyn layer's staged source files (see Rho.Tools.DynTool).
Core tool: turn the Dyn staging copy into a candidate generation
(Rho.Dyn.propose/2: static check, compile, selftests). It
never activates anything itself: the human approves on the phone, or has
turned approve all on (Rho.Dyn.AutoApprove), when the host's UI
activates it as it arrives. Description and result say which.
Core tool: read a staged Dyn source file with line numbers (see Rho.Tools.DynTool).
Core tool: put the Dyn staging copy back to the current generation's sources (see Rho.Tools.DynTool).
Core tool: what the Dyn layer runs, what's pending, and its recent crash reports (see Rho.Tools.DynTool).
What the dyn_* Core tools share: the agent edits its Dyn layer's
staging copy and proposes it (Rho.Dyn). There is deliberately
no tool to activate or revert: those need the human's approval (the screen lock).
Core tool: create or overwrite a staged Dyn source file (see Rho.Tools.DynTool).
Core tool: read a page of a tool output that was too big to give whole
(Rho.Artifacts). A page stays under the output budget, so
reading never spills again.
Core tool: the session's todo list (omp's todo), kept in
todos/<session_id>.json in the app's data dir. Writing the steps of a
multi-step job down and ticking them off keeps the agent from stopping
halfway, or forgetting a step after a compaction; and the list survives a
restart of the app. One item at a time is in progress: the first open one,
moved on whenever an item is done or removed.
Text and data from code rho doesn't trust, the Dyn layer's (Rho.Dyn),
for the processes it does: the loop, the Keeper, the caller of a compile
or a selftest.
How much of each provider subscription is left, and what the model calls used, for the host's usage page and status line.
Asks a provider's OAuth usage endpoint what its subscription has left,
with the sign-in's access token (Rho.Auth.credential/1), as omp
does (subscriptions only: an API key has no such endpoint)