eAgent reference
The single-account agent core shared by Exponential and spezl (EXP-1426, epic EXP-1420).
It spawns the user’s own, unmodified claude / codex binary over ACP, streams what the agent
does, lists the CLIs’ own session history (with their own session names), runs the agent’s login
in a PTY and wires MCP servers. One local account, no server.
Consuming
Section titled “Consuming”publish = false (it compiles a fixture from packages/ in with include_str!):
depend on a release tag, eagent-v<version>, per the git-tag policy in
docs/libraries.md (“Consuming from another repo”):
eagent = { git = "https://github.com/Niach/exponential", tag = "eagent-v0.1.0" }Renamed from exponential-agent-runtime (#EXP-1505). For one release a shim crate under the
old name re-exports this one (pub use eagent::*;), and the release workflow still accepts
agent-runtime-v* tags; move to eagent (see docs/libraries.md, “Migrating”).
- No server coupling. No tRPC, token store, device id, steer relay or pickup loop. The crate
depends on no Exponential app crate and no UI toolkit (
tests/dependency_gate.rs). - Never touch an agent’s credentials. The core spawns the vendor’s binary and lets it own its
login: it never reads, copies, moves or refreshes a token. Anthropic’s Claude Code terms forbid
token intermediation, so
coding/src/claude_oauth.rsis NOT part of this crate and never will be. Exponential’s multi-account staging and rotation stay incoding.
Server coupling becomes traits the hosts implement. Each lands with the first module that needs it.
| Trait | What it answers | Exponential (coding + engine) |
spezl / local |
|---|---|---|---|
LaunchSpec |
what to spawn and how (program, env, cwd, agent, picks, resume, MCP servers, context layers, confine root) | engine::adapters::AdapterSpec → LaunchSpec |
built directly |
EventSink |
where mapped events go | the steer publisher | the voice loop / UI |
RunBackend |
the side-car a run brings up and its end sequence, the run record, reply cards, consumed messages, open cards | engine::lifecycle::RunLifecycle (outbox, publisher, tickers, kill pump, sessions.* over tRPC) |
NoBackend (in memory) |
PromptLocalizer |
a host prompt’s attachment embeds, as the agent reads them | engine::host::SteerLocalizer (steer::localize_message) |
none |
AccountEnv |
config dir + env for the spawn | multi-account rotation | the CLI’s default home |
McpProvider |
the MCP servers a run gets | team + device servers | the user’s own list |
HostTools |
the host’s OWN tools: display row, subject key, preview spec, code-mode script log | engine::ExpHostTools (contract expToolDisplay / expToolPreview, codemode reports) |
NoHostTools |
What stays in Exponential
Section titled “What stays in Exponential”The pickup loop, the launcher, worktrees / git / clones / GitHub tokens, TrpcClient and
TokenStore, the steer relay, account rotation and usage, prune / reaper, remote admin and
claude_oauth.
Extraction order
Section titled “Extraction order”Stacked PRs, each behaviour-preserving: the old module paths re-export the moved modules, so no caller changes until a later cleanup.
- This crate, the dependency gate and the pure leaves: atomic config writes (
api::atomic_file), the lock file, the claude / codex trust seeders, the codex session index, the confinement check, the command head, the Linux run scope, the backgroundCommandconstructor (terminal::process). - Done. The event vocabulary out of
steer, aswire:wire::frames(ActivityEventand its pieces,TurnState,ConfigValue,QueuedMessage,BackgroundTask, the relay frames),wire::workflow(WorkflowState/WorkflowStatus/WorkflowAgent*),wire::tool_outputandwire::tool_diff, and the pure half ofsteer::activityaswire::activity(Redactor,secrets_from_worktree,worktree_diff,truncate*,SessionAgent, the blocked-wall vocabulary,normalize_agent_title,normalize_compaction_trigger,clamp_config_state, the relay caps).steerre-exports every module under its old path and keeps the contract checks (domain::contract) for the caps the core inlines. Still insteer: the links, signals and forwarders,SessionBlocked(itsapireport shape) andDiffSnapshots. - Done. Agent identity, detection and the single-account login PTY (no credential moves):
agent(CodingAgent,claude_model_alias, the model / effort vocabularies; the settings default stays incodingasCodingAgentExt),detect(Tool, a leanToolCheck,check_tool, the version and auth-status parsers, the boundedclaude auth status/codex login statusprobes,is_signed_in,probe_codex_acp, the min versions, the boundedoutput_with_timeout),codex_app_server(the codex handshake the probes use), the login-screen scrapers out ofsteer(login_picker,codex_login_picker,login_screen=steer::agent_login_driver) and the newlogin: the sign-in plan,LocalLogin(the CLI’s own login in a PTY against its DEFAULT home, URL / device-code scraping intoLoginProgress), logout.run_doctor, the profiles, the device caps and Exponential’sToolCheck(the lean one plus the account fields), the staging / commit / import flow and its emulator-driven PTY stay incoding/cli/uiand call the core. - The ACP engine core behind
RunBackend/EventSink;enginebecomes Exponential’s adapter.-
4a, done.
spawn(SpawnSpec,ChildExit,login_path, out ofterminal::pty, which re-exports them; the PTY emulator stays interminal) andengine::= the child-process line transport (transport), the child’s exit link (child_exit, out ofengine::host), theEventSinkseam onwire::ActivityEventplusRecordingSink(sink;steerimplements it for itsActivitySender), the local feed vocabulary (local) and the pure run policies (stall,api_retry,compaction).enginere-exports each under its old path. -
4b, done. The mapper and its leaves.
engine::mapper(the ONESessionUpdate→ wire mapper;MapperConfigcarrieshost_tools, andMapper::newtakesimpl Into<MapperConfig>so Exponential’s session-shapedengine::MapperConfigfills inExpHostTools),engine::host_tools(theHostToolsseam plusScriptReport),engine::reply(ReplyCard/ReplyOption, out ofapi::session_device, which re-exports them),engine::context_layout(plusContextLayers/ProjectMemoryFile/CarriedBaseout ofcoding, which re-exports them),engine::exp_tool_refs(the rule; the spec comes fromHostTools::preview_spec, andengine::exp_tool_refskeeps the name-keyed entry and the fixture lock), andengine::terminals(the ACPterminal/*capability). The headless PTY (Pty,open_pty,build_command,winsize) moved out ofterminal::ptyintospawn; the emulator stays interminal. The wire gotRemoteAnswer,SessionBlocked(thesessions.reportconversion issteer::SessionBlockedReport),now_unix_millis,iso_from_unix_millis/now_iso(chrono-free, pinned tocoding’s formatter in steer) andJOURNAL_WORKFLOW_CAP. The contract values the mapper and refs inline are pinned inengine::mapper/engine::exp_tool_refstests. -
4c, done (the adapters).
engine::adapters(mod,claude,claude_wire,codex,codex_wire, moved whole): a host starts the user’s ownclaude/codexACP adapter withAdapter::new(impl Into<LaunchSpec>)and drives it as aConnectTo<Client>.LaunchSpecis the core launch spec (program / args / env / cwd asSpawnSpec,CodingAgent,RunOptions, resume handle,AgentMcp+McpServerWireservers, system append,ContextLayers, confine root,ChildExitLink); Exponential’sengine::adapters::AdapterSpec(the launcher’scoding::LaunchOptions) converts into it, the account slot becoming the profile id. Moved with them, re-exported under the old paths:mcp(AgentMcp,McpServerWire,McpWireTransport,McpActor,MCP_TOKEN_ENV,MCP_JSON_FILE, the codex entry / TOML renderings, out ofcoding::argv,coding::mcp_json,api::mcp_servers),usage(UsageWindow, the claude / codex live-limit parsers,merge_live_windowsand the process-globalliveregistry, out ofcoding::agent_usage; the OAuth fetch and the collector stay),claude_transcript(locate_claude_transcript),engine::launch(RunOptions,ResumeHandle,EngineErrorwhoseApiarm boxes the backend error, the ACP_metakeys, the aside tool posture) andchild_exit::until_either_closes.codingconvertsResumeSeed,apiitsApiError. -
4d, done (the host and the session).
engine::hostandengine::sessionmoved whole (enginere-exports both;lifecycle,outboxandaside’s posture stay there). The knot is untied byRunBackend: the context holds it, the host calls it (record_session_ids,record_context_base,recorded_native_session_id,post_replies/resolve_question/consume/open_cards,fresh_context_kept,turn_started), and the core attaches it before the connection thread and hands it the end sequence. Exponential’sRunLifecycleimplements it (tRPC client, steer runtime, outbox,lost, the image hook stay on it). Moved with them:KillFeed/KillReason,EngineCommand,SessionIds,RunFacts,SessionCtx(public for a backend to read),ExitState,EngineExit<End = ()>(Exponential’s isEngineExit<Result<(), ApiError>>with its ownEngineHost),end_phases, the aside’s one-shot state (engine::aside), and intowiretheTurnSignal/CaptionSignal/AgentTitleSignaland the answer tap (wire::answer_tap,steerre-exports both). The steer runtime is atokio::runtime::Handle;domain::plan_card’s gate line isPLAN_MODE_ON_LINE, pinned to the fixture inengine::host’s tests. What stays inengine:EngineStartfrom acoding::PreparedLaunch,open_transcript, the pickup’sLiveEngine, the run-record reads,fresh_context_downgrade, the steer image localizer andblocked()’s expiry (chrono), on anengine::EngineSessionthat derefs to the core one.The server-free API:
engine::start(LaunchSpec, SessionSpec, Arc<dyn RunBackend>) -> EngineSession(orstart_with(SessionSpec, adapter, backend)for a scriptedConnectTo<Client>).SessionSpec::for_launch(&launch, runtime)fills it from the launch; setsink(anEventSink, e.g.RecordingSink) andlocal_sink.NoBackend::with_hostreports the exit. TheEngineSessiontakessend_prompt/deliver(queued while a turn or a compaction runs) /answer/answer_card/set_config/set_mode/cancel_turn/unqueue/kill, and readssubscribe/phase/turn_signal/wait; a resume is a new start withResumeHandle(session/load).tests/server_free_start.rsdrives it withNoBackend+RecordingSinkagainst the fake agent.
-
- Done. The server-free facade
local(below), the CLIs’ own session history (local::history),examples/local_chat.rs, and one additive MCP posture,mcp::AgentMcp::ServersOnly(the run’s own servers, noexponentialentry, never strict). Exponential never builds it;codingonly names it in one exhaustive match.
Using it without a server
Section titled “Using it without a server”eagent::local is the whole surface a single-account host needs:
use eagent::{agent::CodingAgent, local::{LocalAgent, RunRequest}};
let agent = LocalAgent::new(CodingAgent::Claude); // or ::with_program(agent, "/abs/claude")agent.check(); // `<cli> --version` -> ToolCheckagent.signed_in(); // the CLI's own auth status: Option<bool>agent.login(false)?; // LocalLogin: the CLI's own sign-in in a PTYagent.login_command_line(); // "claude auth login --claudeai"let run = agent.start( RunRequest::new(cwd) .prompt("…") // the first message .resume(past.id) // or reopen a past conversation .mcp_server(server).env("NOTES_TOKEN", tok) // extra servers, `${VAR}` refs into env .system_append("…"), // optional: .sink(..), .runtime(handle))?;run.events(); // flume::Receiver<wire::ActivityEvent>run.send("…"); run.answer(question_id, keys, text); run.interrupt(); run.stop();run.native_session_id(); run.title(); // the CLI's own id and nameagent.sessions(Some(&cwd), Some(20)); // Vec<PastSession>, newest first- One account, the default home. No config-dir variable is set, no credential is read,
copied or refreshed; the spawn env is the login shell’s PATH plus
RunRequest::env. TheAccountEnvseam’s local side is exactly that. - Events. Narration, tool rows,
Turn { Started | Ended }, and a question or permission asActivityEvent::Question(answer it withLocalRun::answer).RunRequest::sinktakes your ownEventSinkinstead of the channel. Underneath it isengine::start+NoBackend;LocalRun::session()reaches the rest ofEngineSession. - MCP. The CLI keeps the servers the user configured for it;
RunRequest::serversadds more. - History (
local::history, read-only over the CLIs’ own files). claude:<CLAUDE_CONFIG_DIR|~/.claude>/projects/*/<id>.jsonl, titled by the latestcustom-title, elseai-title, elsesummary, else the first prompt. codex: the rollouts under<CODEX_HOME|~/.codex>/sessions, titled bysession_index.jsonl’sthread_name, else the firstuser_message; subagent threads are left out.updated= the file’s mtime.
cd apps/desktopcargo run -p eagent --example local_chat -- "reply with the word ok"cargo run -p eagent --example local_chat -- --agent codex "hi"Still Exponential-only
Section titled “Still Exponential-only”- The pickup loop, the launcher (
coding::PreparedLaunch, worktrees, git, clones, GitHub tokens). -
RunLifecycle: the run record, outbox, steer publisher and relay,sessions.*over tRPC. - Multi-account: profiles, staging / import, rotation, usage collection,
claude_oauth. - The
exponentialMCP server posture (ClaudeInline/CodexOverrides), code mode, team servers. -
ExpHostTools, the steer image localizer, the plan card’spr_opengate, the run playbook. -
open_transcript,fresh_context_downgrade, prune / reaper, remote admin, the doctor and device caps.
Named per the shared-libs table (docs/libraries.md, EXP-1421): crate eagent (#EXP-1505, was exponential-agent-runtime), release tag eagent-v*.