Map: exarch / builtins
exarch’s resident host atoms and the thin ral helpers over them — the
search, line-witness, and edit surface the model reaches through the ral
tool. The Rust atoms register above ral-core and core never inspects them
(builtins-registry;
repl-builtins-stay-in-repl). The
why of witnessed editing is
hash-addressed-editing.
The shared identity is the witness: the letter h followed by six hex of a
Blake3 digest (trailing whitespace stripped), computed over the smallest
symmetric window of neighbouring lines — at least ±MIN_RADIUS (5), grown until
it names the line uniquely, falling back to the absolute index past
MAX_RADIUS — with the target’s offset and the radius folded in
(window_hashes, an adaptive-context witness). The hashing is private Rust:
the model never constructs a witness, only copies one from view-hash into
edit-hash, and both derive identical witnesses from identical content. The h
prefix keeps the witness un-lexable as an integer, so a hash never elaborates to
Val::Int and silently fails to compare against the recomputed String
(witness-hash-h-prefix).
Rust atoms — shell_eval/builtins.rs
EXARCH_BUILTINS is the largest set on builtins::host_surface() — the one
HostSurface value declaring exarch’s builtins beyond core’s, alongside the
harness verbs. Core’s boot_shell
takes the surface and installs it at construction (a half-dressed production
shell is unrepresentable), and the wire engine boots the same dressing
through its EngineInstaller’s two halves
(boot::engine_runtime and boot::engine_root), named on the wire by INSTALLER_TAG in
Frame::Attach.
The bulk-I/O atoms read in Rust, below the redirect frame, so each is one
logical operation with one surface (io-surface).
The readers and editors share one line-ending policy (Eol): a file whose
every \n follows a \r is CRLF, read as \n text and written back in CRLF;
any other file, mixed ones included, is taken raw. So an unedited round trip is
byte-exact, a multi-line from matches a CRLF file, and an edit never leaves
mixed endings behind.
view-text <path> <start> <end>→[{line, text}]. The read primitive: the half-open line range[start, end), each row carrying its 1-based line number and its text, verbatim — which is whatedit-replacematches on. Surfaces oneObserved::Read { path }observation.view-hash <path> <start> <end>→[{line, hash, text}]. The same range with each row’s witness, the handleedit-hashchecks. Reads and hashes the whole file, since the witness depends on file-wide uniqueness; both readers share one range reader and differ only in the column.grep-files <pattern> <path>→[{ file, line, text }]. An ignore-aware Rust regex search ofpath, a file or a directory walked recursively (search_tree; a hit names its file in the shapepathwas written, so a reader takes it back unchanged; a missing path is an error, not an empty search; CRLF-aware, so$anchors before\r\nand a hit’s text is the rowview-textshows; binary detection quits at NUL, the walk the shared one below, kept topath’s filesystem). Each file is streamed, never read whole: every read polls the shell’s scope, the line ending is learnt as the bytes pass, and a line past 16 MiB skips its file with a note on stderr. Surfaces exactly oneObserved::Grep { scope, pattern }observation for the whole search (io-surface).edit-hash <path> <edits>→Unit. The edit verb:editsis a list of[hash: …, line: …]records. Read the file once, resolve each hash to the one line whose witness matches (zero matches, several matches, a stale witness, or two records on one line all fail before any write), splice every named line in a single pass over the original rows (a real newline in the replacement splits the line, an empty string deletes it), and write back through core’s atomic write site (Shell::atomic_write). Resolving against one snapshot makes the batch atomic and non-interfering. The Rust read raises no read card and the atomic write observes nothing; the builtin surfaces one whole-file diff card of the original against the final text (cards), and nothing at all if the two agree. A stderr note names the replaced lines and warns on suspicious\n-style escapes (the replacement text is verbatim).edit-replace <path> <from> <to>→Unit. Replace the one literal occurrence offrom, erroring (file untouched) on zero or several matches; same silent read, same atomic write, same diff card. Counting is overlap-aware (core’soccurrence_starts), so a needle that overlaps itself cannot pass for unique. It speaks its own diagnostics rather than relabelling another builtin’s: several matches name the lines, and a miss names the mangling it can prove — a literal\n-style escape infrom, or a line matching apart from its indentation.explore-dir <n> <path>→[String]. List the entries beneathpathto depthn, ignore-aware, skipping the root and any denied path.find-files <query> <path>→[String]. The files under PATH (resolved and existence-checked asgrep-filesdoes), walked asgrep-fileswalks, fuzzy-ranked by path through core’stext::rank(nucleo), cut toFIND_LIMIT. It filters before the cut: a denied match neither takes a slot nor shows in the count. No index is kept, so every name the model learns has passed the door this call.
Each walk resolves its path through walk_root (grant-checked; missing is
an error) and runs core’s admitted Walk over it
(capabilities), as glob does: cancel is polled per
entry, each entry is anchored by its own path (so a file named ~ or xdg:x
is that file, not a sigil’s expansion) and admitted silently, and a denied or
unreadable entry is skipped, so neither a refusal nor an error names a withheld
path. grep-files then reads each listed file through locate_if_admitted,
judging the object it opens.
skill-list/skill <name>— Agent Skills with progressive disclosure: list the available skills (fresh scan each call, filtered by the grant), then load one skill’s fullSKILL.mdbody on demand; the scan and frontmatter parse live inskill.rs. Startup discovery skips invalid files quietly;skill-listlists valid skills, then names each skippedSKILL.mdby path with its cause; its count covers only valid skills.skillreports a read or frontmatter failure with the skill’s name and path.
Reads resolve through checked_read_path / check_fs_read; the edit writes go
through core’s atomic write site under the run’s pushed grant
frame (surface-reads-writes-execs).
Listing and reacquisition: workers, worker
A listing is data; worker <id> is the door. Both verbs are core’s,
present in every host (map: core builtins), so exarch
declares nothing of its own here. Every worker is legible by its id: the
birth trail card (errand #7 started · building) and the completion notice
name it (shell-eval), and /resources counts the
running ones.
workers→[WorkerRow]. The registry as records —id,title,owner,state,started,elapsed,settled— one per worker, running or with a retained result, and never a handle, so a top-levelworkerscrosses the engine protocol and theVALUEsection prints it as aligned rows, by the printer’s own rule. Reading it claims nothing.worker <id>→Handle. The one door back to a worker’s handle, whatever started it, running or with a retained result, by the id its row, birth card or completion notice carries; nothing looks a worker up by title. A bare top-levelworker Nresult cannot cross the engine protocol (aHandleis not ground): it exists to be composed with an eliminator in the same run,await !{worker 3},cancel !{worker 3}.
ral helpers — agent.ral
Sourced into the shell at boot:
view-text-around path line peek/view-hash-around path line peek— the two thin helpers over the atoms: the2*peek + 1lines centred online, clamped at the top of the file.- the tasks kit —
task-newand friends, a pure-ral task list that reads its own state back throughexarch-pins `readand writes the rendered rollup forward throughexarch-pins `set/`clear(tasks-sync) rather than threading a bound list through every mutator;exarch-tasks`listis the read point, and the kit ships no query functions —filter/first-whereover it is how you query (cards). exarch-goal`set/`clear—exarch-pins`set/`clearunder thegoalregister key, kept visible by the nudge reminder.
Harness verbs — context, spawn, schedule, reply
Every verb below is a BuiltinEntry in
exarch/src/shell_eval/builtins/harness.rs
(HARNESS_BUILTINS, carried on host_surface() beside the atoms above — one
surface for the boot install and the prompt’s builtin_index alike), landed by
agent-tool-to-exarch-builtin
over the rail engine-protocol built. A verb’s
body reads the model’s argument, as first-order data, into a Request and
calls shell.enquire with it; exarch/src/agent/desk.rs’s ExarchDesk
decodes the same Request and matches it exhaustively, answering from shared
handles (HostServices) captured at install — never &mut Agent — installed
per ral call in Agent::run_shell and swapped back to an absent desk
immediately after. The door then admits the host’s answer only in the shape
the request owes (Request::owed). A closed label set is an open row checked
at the door instead of a closed variant type: an unknown label errors before
any enquiry crosses, naming the legal set, rather than a static
row-unification error with no room for a didactic message.
One vocabulary, typed once: exarch/src/enquiry.rs. The desk
answers five classes, each a Family of tags — `exarch-agents,
`exarch-schedules, `exarch-context, `exarch-transcript and
`exarch-pins — every class, tag, field and answer a Datum, with one
encode and one strict, total decode. An unrecognised tag is as loud one
level down as an unrecognised class is at the top, the refusal offering the
tags the family takes; a tag’s record crosses by field name, strict in its
keys (record!), `evict’s and `grep’s included. Because door and
desk decode with the one function, they refuse in the one wording. The
desk’s decode is still not a duplicate of the door’s but the trust
boundary: the door refuses engine-side so a bad value reaches the model with
the parser’s own message, and the desk refuses again because a guest can send
whatever it likes — which is why a cron expression is parsed
(CronSchedule::parse, inside the one decode) on both sides.
Context stewardship
The explain guides in shell_eval/builtins/harness.rs document each
operation with examples, return shapes, limits, and recovery advice.
data/context.md keeps the working strategy: evict irrelevant retrievals and
failed attempts once retried, each as a run [from, to] under a required
note; cut at the bottom or in batch; cover a residue whole or not at all;
expect the a forced eviction to take the current prompt too, so evict
first under one’s own note, saying what was asked; and read
residues back through exarch-transcript. The command reference explicitly
distinguishes bytes from tokens, requires nonempty single-line UTF-8
eviction notes, and warns that transcript reads are unbounded.
Two verbs over two things: exarch-context is what the provider is sent and the
model pays for, and exarch-transcript is the record, every turn this session or
its ancestors ever recorded
(context-rollover,
the-turn-is-the-atom). exarch-context
edits and surveys; exarch-transcript only reads. Both speak turns, and every
turn carries a role: a user turn is a prompt (or an import’s opening) and
anything before the first reply, an assistant turn is the assistant message
with the tool results it called for. The turns answering a prompt — those
after it, up to the next prompt — are a function of role and order, named in
no answer
(an-eviction-is-a-set-of-turns).
An eviction leaves a residue standing for its run: a span [from, to] of
turn ids, both inclusive, and the model’s note, addressed by the ids it covers
and never by an id of its own (an-eviction-is-a-span).
Every ral tool result ends with TURN: <id>, the id of the turn it closes,
so the model has the address before it asks for one.
-
exarch-context <tag>→∀ρ. <survey | evict [from: Int, to: Int, note: Str] | ρ> → F [rows: [[from: Int, to: Int, role: Str, kind: Str, label: Str, bytes: Int]], total-bytes: Int]. One verb per addressable state: the tag selects the transition, and every tag answers the survey afterwards. That is not a shared prefix collapsed but the rule the registries already follow, and it fits this surface better than either, because an edit changes what is addressable — an evicted run becomes one residue — so the edit is also the resurvey the next edit must be written against.`surveydescribes the context, one span row per frontier item — a resident turn (from == to,kindown,importorinherited) or a resident residue (kindresidue, roleuser, its note aslabel) — the very rowsexarch-transcript `indexanswers with, lesslocation, besidetotal-bytes, which is what is actually sent rather than the sum of the rows. What lies under a residue istranscript `index’s to say. It changes nothing.`evict [from, to, note]is the one context edit: it takes one contiguous run of the context — turns and residues alike, both ends inclusive — and leaves one bracketed user-voice residue standing for it, carrying the note and how to read the run back (an-eviction-is-a-span).[from: 41, to: 60]names turns 41–60 and is the address of the residue it leaves. A run that reaches into a residue must cover it whole; a larger run that does replaces the old notes with the new one, and the same span over one residue rewrites its note. Refused by name: an end the transcript never recorded, an end already withdrawn, a run with the unclosed turn — the one being written — within, and a run cutting a residue (turns 41–60 stand together under one note: name from: 41, to: 60 whole, or leave it). A prompt may be evicted, the live one included; the note should say what was asked.noteis the model’s own line to its future self and is required: a forced eviction is this same edit under another authority and the only one without a note;/rewindis a different record,Protocol::Rewound, which withdraws turns and leaves no residue at all. The desk refuses an empty note, and its size and shape are the desk’s too — at mostNote::CAP(240) bytes and no line break, since the residue keeps one short line, the whole of what an eviction leaves in the context.
The edit records a
Protocol::Evicted { residue }model event (Residue { span, note, by }) at the desk immediately, underDeskAct::ContextEvict, whose subject is the span —turns 41–60— and whose result says the turns now stand under the note, with the context’s serialized bytes. There is no byte-delta receipt: the decision-relevant number istotal-bytesnow against the budget (context-is-a-projection). -
exarch-transcript <tag>→∀α ρ. <index | read [turns: [Int]] | grep [pattern: Str, turns: <all | only [Int]>] | ρ> → F α. Read-only: no tag records a protocol event, though each records aDisplay::HarnessCallfor the screen. The answer type is a bareαbecause the three tags answer three shapes. Every narrowing is a list of turn ids, and the ids are the ones`evict’sfromandtoname: the context family speaks a run by its ends, the transcript family a[Int](an-eviction-is-a-span). Both records are closed;`grep’sturnsis`allor`only [Int], absence being data.`index→ the survey’s own rows over every turn and every residue the transcript holds, pluslocation—`resident,`under [from: Int, to: Int]naming the innermost residue over the item, a named record like every other span, or`withdrawn— ordered byfrom, wider span first, later-recorded first, so a residue precedes what it covers.kindisown,import,inherited(an ancestor’s) orresidue. A departed turn is listed at the weight it carried when it left.`read [turns: [Int]]→[[turn: Int, role: Str, messages: [Message]]], one element per turn named, in transcript order, each addressed by its ownturnfield and carrying theroleit was taken in rather than a=== … ===header a reader had to re-parse. A turn’s material is what the model was sent, whether it is resident, departed to this log’s file, or an ancestor’s (Placed::offinds its records,turn_messagesrenders them).!{range a b}is half-open, so the turns under[from: 41, to: 60]are!{range 41 61}; a residue itself is never read, its note being the whole of it.`grep [pattern, turns]→[hits: [[from: Int, to: Int, kind: Str, role: Str, line: Int, text: Str]], total: Int]. A Rust regex — ral’s ownre-*dialect, compiled at the desk so a bad pattern is refused in the regex crate’s words — over prompts, programs, results, and reasoning, per line, and over every model residue’s note (line: 1; never the residue’s boilerplate, and a harness residue has nothing to search). A hit names the item it lies in, a turn hit beingfrom == to.`turns: `allsearches every turn but the one in hand and every noted residue;`onlynarrows it to the turns named and the residues whose ids are all named. At mostGREP_HITS(100) hits, in`index’s order, each line clipped at 200 bytes, withtotalthe true count so a large one says narrow, not page.
Only the turn being written now is unreadable: the earlier turns of the work in hand have closed and read back like any other, which is what the unclosed-turn refusal names when it points at them.
`readis the one harness answer whose size is the size of the thing it describes: the survey spends a few hundred bytes to describe a 200 KB context, and this returns the 200 KB. That is why it is a tag ofexarch-transcriptand not ofexarch-context— the distinct name is the cheapest safety mechanism a model-facing surface has, and the only one that acts before the call rather than after — and why the docstring points a long search at amnemonchild, which shares the transcript and spends its own context on it.A
Messageis[role:system|user|assistant|tool, parts: [Part]], one per message the turns hold — the meta half of taking a turn carries no message of its own, the same as it contributes none to a live provider request. APartis a variant, one arm pergenai::chat::ContentPart—`text,`program(the ral tool call itself: the script source for exarch’s own tool, or a name and argument keys for any other),`result,`reasoning,`binary(media metadata only),`custom— matched exhaustively inexarch/src/record/model/transcript.rs, so a genai variant this arm list has not met is a compile error rather than a serialization of the provider’s struct leaking through as content.`ThoughtSignaturecarries no part at all: an opaque continuation token, dropped rather than rendered. Narrowing this material — truncation, elision, byte caps — is deliberately not this builtin’s job: it isfilter/take/view-textover the records, the wayexarch-tasks `listputs querying on the caller rather than the kit (the-transcript-is-a-value for the transcript-as-value law this reads under: a turn’s own recordedProtocolmaterial, converted rather than re-rendered).`grepsearches that same narrowing —`text,`program’s source,`result,`reasoning, a binary payload and a provider extension carrying no text a pattern could mean — so nothing is searchable that is not readable.
The exarch-agents guide documents its operations, grant profiles,
write restrictions with inherited execution, role selection, and fan-out
with literal binding references. data/agents.md
keeps point/call guidance for delegation, disjoint write grants, waiting,
reading, follow-ups, and cancellation. Each child’s system prompt renders its
own grant, including the parent’s restriction. Replying parks a child and starts
its one-hour follow-up lease; working time is outside that lease
(agent).
-
exarch-agents <tag>→∀α. F α. One verb for the fleet, over an open row of six tags —`list,`start,`message,`cancel,`reply <value>,`read <name>. The acting tags answer[live: Int, replied: Int]afterwards — other live agents in the reader’s tree, and how many of its own children hold a deposited reply (reading does not consume it) — rather than a receipt of its own. The rule is unchanged (the answer is the world after the transition); what changed is its cost, since a roster on every spawn of a fan-out is O(fleet) restating what the caller mostly chose, whilerepliedis the one count it could not derive and the one asking for an action.`listanswers the roster rows,[[name: Str, spawner: <root|agent Str>, state: <busy|waiting-on-agents|replied|waiting>, idle-s: Int, elapsed-s: Int, log-dir: Str]]— the reader’s whole tree, itself among them, since what it may message is wider than what it spawned. The climb stops at a root, so one root session never lists another’s.`readanswers[name: Str, reply: α], the value a replied child deposited, which is why the family’s answer type is a bareα(theexarch-pins `readprecedent) rather than a roster (reply-parks).`replyis the sole return path of a returning agent — first-orderness checked at the door, refused on every non-returning agent with the desk’s own didactic text, last write wins within a run, deposited once the enclosingralbatch drains. The outer row is open so an unknown tag reaches a door that enumerates the tags; each known tag’s payload keeps its exact type, so the closed record inside`startstill makes a missing or misspelled field a static error naming it, while thetype/grant/modelrows inside that record stay open for the same reason one level down. Every family door refuses a block or a handle at any depth of its argument (first_order), so the type variables its argument mentions aredataand its row variables deep (doorinharness.rs; operators-are-kinded).`start [prompt: …, name: …, type: …, grant: …, model: …]is the one spawn: launch-only and always asynchronous, a one-line notice arriving through the inbox when the child replies, and the child’s row in the answer carrying thenameandlog-dirthe old receipt did.nameis the child’s identity — the tab-bar contract (check_name, inenquiry.rsbeside the rule it belongs with), unique among live agents or the call is refused; this door refuses a malformed one early and in the model’s own words, andFleet::enrolrefuses it again for peers that never came through a door (agent);typeis`amnemon(blank context) or`mnemon(imports the parent’s model-visible context before the fresh final prompt);grantis the one layer the child gets —`inherit, one of the four spawnable base names (confined,read-only,edit-only,reasonable), or`restrict RwithRa capability record ingrant [...] { body }’s own vocabulary; the engine reads it offLaunch::split, and the desk’sStartcarries only the remainingSpec.Grant::decode(enquiry.rs) closes the row, naming all six shapes andR’s own keys in its refusal and holdingRto first-order data, since the ceiling crosses to the far side as data; the record itself is decoded bydecode_capability_mapoff theForm::Granttable whenSpawnGrant::layerresolves it against the child’s cwd, so an unknown key is refused in that table’s words (a-spawn-is-one-layer). There is nosearchfield: hosted web search is a tool, present in a request or not, so the model never reasons about it in prose; a child inherits its parent’s bit verbatim (agent).modelis`inheritor`role <Str>, and is always written: ral has no optional field, so absence is data carried by a variant (optionality-via-variants) and the record row stays closed.`inheritshares the parent’sArc<Provider>verbatim;`roletakes the provider the human bound to that name at launch (--role NAME=MODEL, provider), and is refused when none is configured or none bears the name. The agent never names a provider or a model, so it cannot discover one. Fuel bounds delegation depth, not fan-out — refused only once the caller’s ownfuelreaches zero.`cancel <name>is descendant-only, resolved by name and enforced at the desk; a scope violation raises.`message [to: …, text: …]is not: the fleet is one mailbox space, so a note runs in any direction across the tree and only a note to oneself refuses. The roster is the fleet to match — every live agent, the reader included — so what a model may list and what it may message are one set rather than two that drift. Each row’sspawner(`rootfor one a human started,`agent <name>otherwise) carries the scope`canceland`readstill enforce: the listing states the rule it does not impose. Where a removed schedule is simply gone from its answer, a cancelled agent is not:Agent::cancel_treeonly sets the cooperative token (and stamps the eval reach), so a successful cancel answers with a roster that still lists the target — a request, not a transaction, and the one place the rule needs a sentence of its own. -
exarch-schedules <tag>→F [[label: Str, trigger: Str, next-s: Int, fires: Int]]. The same shape over`list,`add,`remove.`add [trigger: …, label: …, prompt: …]takes a closed record — a record literal infers an exact row, so a missing or surplus field is a static error naming it, which also sidesteps ral’s grammar footgun where a nullary tag would otherwise absorb the following positional atom;triggeris`cron '<expr>'or`after '<dur>'over an open row,labela requiredStrand the schedule’s identity. The receipt’snext-sis not lost but recomputed: the new schedule’s row in the answer carries it, which is still how a mis-meant cron is caught at arm time.`remove <label>really does answer by absence, sinceScheduleRegistry::unscheduleremoves the entry under the lock — the half of the rule the fleet cannot honour.next-ssaturates toi64::MAXfor a cron with no next occurrence. Every tag is gated on the--allow-schedulegrant, refused with a didactic text that names the tag the model actually typed — never a spelling it was never taught.Both verbs issue their transition and then the list, so a raise does not imply nothing happened — the act may have landed and the re-read failed. The audit is unchanged:
`listcommits noDeskActin either family, so each transition is still one act. -
exarch-pins <tag>→∀α. F α. One family over the caller’s own pin register:`set/`clearwrite it,`read/`listread it back, and it is the register’s only door — with the same foreground-only disciplineexarch-agentsandexarch-contextalready have: a call insidespawn "…" { … }errors rather than degrading.`set [key: Str, body: Card]→Unit.`clear <key>→Unit.`read <key>→∀α. F α.`somethe card pinned atkey, canonically re-encoded (cards) so a kit can destructure it whether or not the bytes it wrote match what comes back;`noneon a miss or an absent register. Its answer type is the script’s to decide, because the register is schemaless by design (register-is-read-write): a boundary, admitted at the door against how the script uses the card (a-boundary-is-checked-against-its-type).`list→F [String]. Silent; the keys currently occupied on the caller’s register, inBTreeMaporder — a key names a slot for`read, not its content.
Receipts and listings are ral records the model can bind, filter, and fan out
over, rather than stringly-typed JSON it re-parses — the composability a
provider-advertised tool’s JSON lacks. Acting verbs render as acts — the
Display::HarnessCall/Forensic::HarnessResult rail pair
(harness-calls-are-acts; a spawn
additionally derives a child tab);
listings stay silent, since their value is the returned record.
`ral` is what remains a tool for all of them; thinking
is a second, narration-only tool outside this harness/tool boundary
(agent).
Where to look
exarch/src/shell_eval/builtins.rs— the Rust atoms, their type schemes, andEXARCH_BUILTINS.exarch/src/shell_eval/builtins/harness.rs— the harness verbs above,HARNESS_BUILTINS.exarch/src/skill.rs— the skill scan behindskill-list/skill.exarch/src/agent/desk.rsandagent/desk/—HostServices,ExarchDesk, and the handler for each enquiry class.exarch/data/agent.ral— the helper library; installed bylibrary::install_agent_library, whichboot::engine_rootcalls (exarch hub).- The model-facing tool that carries every one of these calls is `ral`.
- There is no model-facing network builtin: a guest reaches the network itself, policed host-side — egress, agent, synod.