PUBLIC MODULE · lib/agent.js
Agent
lib/agent.js — Agent: owns context, provider/model selection, the tool loop, and persistence. Headless: no stdio, no process.exit, no TUI.
PUBLIC CORE ENTRY POINT. Agent coordinates the JSONL session store, crash-safe finish hooks, the forked tool sandbox, the tool loop, tool-call execution, status readouts, session lifecycle, and thinking level. It composes Context ← Env ← IO ← Agent and publishes that core tree as Agent.Context, Agent.Env, and Agent.IO. It does not import CLI, Markdown, or application/UI layers.
Ownership: - Agent alone owns the ordered in-memory context, provider/model selection per request, the tool loop, and persistence. - IO stays stateless: every request passes the COMPLETE current context; one IO instance is reused per active provider and reconstructed after kill() leaves it permanently closed. It reports usage PER TURN (the terminal event's usage envelope, see lib/context/usage.js) — nothing above it. - Session wiring (--session/--resume) lives wholly inside Agent; bindings pass an id, nothing more. Persistence is injectable (any SessionStore-shaped object); the file store is the default. - Agent alone owns the CUMULATIVE usage total (the usage getter): it sums every terminal's envelope in memory as run() sees them. Never persisted (no usage.json) — a fresh Agent starts at zero; bindings that show it (the TUI footer, a one-shot CLI's final line) read it fresh rather than tracking their own copy.
Agent's implementation is split across helper modules in lib/agent/; this file provides the public façade: - run.js the tool loop and its runaway guards; - tool-exec.js tool-call dispatch/execution, the result contract; - readouts.js usage/context-window/plan/connection readouts; - sessions.js fork/new/resume/list, the seeded system prompt; - thinking.js the thinking level.
Core rules (see run.js/tool-exec.js for the full contracts): every tool call in the LIVE response executes — identical repeats included; calls execute ONLY in real time, never from history. Tool-call ids belong to AGENT, never to the model (generated when missing, regenerated on collision). Tool-execution lifecycle is observable: TOOL_EXECUTE fires just before a call runs and TOOL_RESULT after each result is appended.
Tool-call SANDBOX (options.toolCall): file-scanned tools execute in a FORKED child process by default (fork: true); built-ins, programmatic tools, and INTERACTIVE tools stay in-process. Options: - fork: false disables the sandbox (in-process execution) - timeout: explicit HOST override of Env.toolTimeout (duration) - async: true executes ONE message's tool calls concurrently Every call is Agent-timed (including in-process). A schema-declared timeout argument is extracted and capped at Env.toolTimeoutLimit; schema.onTimeout gets at most 60s for cleanup or a final result.
SAFE MODE (options.safe; setSafe() toggles it at runtime — /safe, the ^X Settings row — applying from the next request): publish and execute ONLY read-only tools (schemas with safe: true) through the environment's SAFE VIEW (Env.safe): the Agent reads tools through this._safe ? env.safe : env, so one Env serves any number of Agents in either mode — safety is a view, never global state. Safe mode is FORCED (construction and setSafe both honor this._forcedSafe) when no supported OS sandbox is available — there is NO opt-out: mutation tools run only under an active OS sandbox, so on a mechanism-less platform the agent is read-only.
SYSTEM PAYLOADS: a tool returning { result, system } answers briefly via its tool result while system (string or string[]) appends as System messages right after it — before any queued user messages. DISPLAY PAYLOADS: { result, display } — display rides the outcome to TOOL_RESULT subscribers and is shown to the user WITHOUT ever joining the context: a tool can display data without sending it to the model (the edit tool's git-style diff).
class Agent class
The agent: the ordered context, the provider/model selection, the tool loop, the pending queue and the session persistence, behind one headless object.
static EVENT field
The numeric event vocabulary for Agent.onEvent: START, TEXT_START, TEXT_DELTA, TEXT_END, THINKING_START, THINKING_DELTA, THINKING_END, TOOLCALL_START, TOOLCALL_DELTA, TOOLCALL_END, DONE, ERROR, MESSAGE_COMMITTED, LOG, TOOL_EXECUTE, TOOL_DATA, TOOL_RESULT, CLOSE_MARKED, CLOSED, SENT_MESSAGE — see Agent.onEvent's documentation for each value's meaning and payload.
static RESPONSE_CALLBACK_EVENTS field
The [responseCallbackName, Agent.EVENT] pairs: every EVENT value's corresponding option-callback name ("onTextDelta" for Agent.EVENT.TEXT_DELTA, …), for hosts that prefer per-event callbacks over one onEvent listener.
constructor({ env, model, url, timeout, settings, context, tools, parent, name, description, session, sessionDir, sessionSave = true, createIO, toolCall, safe, spawnPermission, question, } = {…}) constructor
Build an agent over an environment; wires the session store (a named file session, a resumed one, an injected store, or none) and takes ownership of the seed context — a NEW (non-resumed) context starts with the seeded system prompt as its FIRST message(s).
onEvent(event, callback) method
Register a synchronous listener for one numeric Agent.EVENT value. Response payloads deliberately omit IO's string type. Indexed payloads carry contentIndex plus content, the assembled block after that IO event was consumed. End-event text, when present, is the provider-normalized authoritative full block snapshot. MESSAGE_COMMITTED receives the stored message after persistence. SENT_MESSAGE receives a queued user message as it enters context. The possible event values (the Agent.EVENT constants):
Agent.EVENT.START— a run began; payload is the request startAgent.EVENT.TEXT_START/TEXT_DELTA/TEXT_END— one
assistant text block began / grew / completed; indexed payloads (contentIndex, content)
Agent.EVENT.THINKING_START/THINKING_DELTA/THINKING_END—
the same lifecycle for a thinking (reasoning) block
Agent.EVENT.TOOLCALL_START/TOOLCALL_DELTA/TOOLCALL_END—
the same lifecycle for one streamed tool call
Agent.EVENT.DONE— the turn completed successfullyAgent.EVENT.ERROR— the turn failed; payload carries the errorAgent.EVENT.MESSAGE_COMMITTED— a message was persisted to the
context (payload is the stored message)
Agent.EVENT.LOG— one diagnostic log line (payload is the line)Agent.EVENT.TOOL_EXECUTE— a tool call is about to runAgent.EVENT.TOOL_DATA— one chunk of a tool's live outputAgent.EVENT.TOOL_RESULT— a tool call's outcome was appendedAgent.EVENT.CLOSE_MARKED— close() was requested (no new work)Agent.EVENT.CLOSED— close cleanup completed; the agent is deadAgent.EVENT.SENT_MESSAGE— a queued user message entered context
offEvent(handle) method
Remove one registration; returns false when it is absent.
toolStorage(toolname) method
Return this agent's mutable, transient storage object for one tool.
toolStorageClear(toolname) method
Clear one tool's transient storage, or every tool store when omitted.
get parent() getter
The creating Agent, or undefined when none was supplied.
get children() getter
Snapshot of direct child Agents.
createChild(options = {…}) method
Construct one direct child through the environment factory. The parent relationship is authoritative for ownership and delegation denial.
childAdd(child) method
Register one direct child Agent. @param {Agent} child @returns {Agent}
childRemove(child) method
Remove one direct child Agent. @param {Agent} child @returns {boolean}
get name() getter
Human-friendly Agent name.
set name(value) setter
Set the human-friendly Agent name. @param {string} value
get description() getter
Human-friendly Agent description; an empty string is valid.
set description(value) setter
Set the human-friendly Agent description. @param {string} value
get safe() getter
safe mode: only read-only (safe) tools publish and execute
setModel(selector) method
Select an exact, configured endpoint/model pair for subsequent turns. Validation happens before either live field changes, so a failed attempt leaves the current selection intact.
setSafe(value) method
Switch safe mode at runtime (the /safe command, the ^X menu). Applies from the NEXT request: idle cached provider connections are dropped (their tool selection was fixed at construction); an in-flight request finishes with the old catalog.
get sessionSave() getter
whether the current SessionStore saves to disk
sessionSaveSet(value) method
Enable or disable saving for the current SessionStore. Anonymous agents have no store and cannot acquire one implicitly.
get spawnPermission() getter
Generic delegation permission. A child Agent may never delegate further, regardless of its stored host/user policy. Tools decide how to ask when an independent Agent's permission is unset; Agent performs no spawning.
setSpawnPermission(value) method
Set generic delegation permission; non-booleans restore tool-owned asking.
setFolder(folder) method
Narrow this agent's tool working folder to an existing folder inside its environment project. undefined restores the environment root.
get folder() getter
The agent-local root used for file tools and their OS sandbox.
setQuestion(callbacks) method
Set (or replace) the QUESTION BRIDGE at runtime — the binding's rendering engine wires it once its overlays exist (the TUI hands its questionnaire overlay to the Agent after construction; see the constructor's question option for the contract). Applies to the next tool call.
static toolContext({ question = null, env, call, agent, storage, trusted = false, resetTimeout } = {…}) method
Construct the public tool-call context. The object is ordinary in-process data: it is never serialized or published to a provider.
updateToolMessage(name, text) method
Set (or clear) a tool's sticky MESSAGE on THIS agent — a compact live text the TUI renders above the input area (collected from the VIEWED agent; lib/agent/tool-messages.js).
toolMessages() method
tools with a live sticky message
detectToolMessages() method
Re-detect tool-provided display information from the current context.
run(options = {…}) method
Run the tool loop until done/error (lib/agent/run.js).
compact() method
/context-compact: ask the model to summarize the conversation (a structured, self-contained prompt), then replace the context with the surviving SYSTEM messages plus one ASSISTANT message holding the marked summary (lib/agent/compact.js). Compact is an ordinary turn observed through Agent events; a no-op (context untouched) when the model's turn returns no usable summary text.
get usage() getter
Cumulative usage across every request THIS Agent has made — every IO terminal event's usage envelope, summed in memory. Never persisted: a fresh Agent starts at zero.
get contextUsage() getter
The context-window readout for the status surface (most exact first: the provider's own report, the last provider-reported envelope, the word-count estimate marked approximate).
get planUsage() getter
The provider-reported PLAN/QUOTA readout of the current endpoint ({label?, quotas}; in-memory, last-known). null until a request reports one.
get busy() getter
Whether an agent run is currently in progress. @returns {boolean}
get endRequested() getter
Whether this agent asked to end after its current turn settles. @returns {boolean}
get closeMarked() getter
Whether close has been requested, including while a turn finishes.
get closed() getter
Whether close cleanup has completed.
close() method
Refuse new messages now and close after the current turn, or immediately when idle. CLOSE_MARKED precedes CLOSED; repeated calls are no-ops.
requestEnd() method
Ask to END this agent (its job is done): the current turn finishes first; the run loop then closes the agent instead of idling forever. Idempotent.
get ioState() getter
The connection/work state for the TUI's status indicator: "working" (a run is in flight), "disconnected" (the last turn failed connection-class), "idle" (otherwise).
cancel() method
Cancel the in-flight request (IO kill → terminal partial). Tool children in flight are signalled on an ESCALATION LADDER: the first cancel interrupts (SIGINT — a well-behaved command exits on its own; note a BUN worker shrugs SIGINT off — the next press is what lands), the SECOND forces the child out (SIGTERM), and a still-surviving child gets SIGKILL from the third press on. The run loop stops after the current tool outcome.
fork(id) method
Fork the current session into a NEW session id: the live context continues under a fresh store (flushed immediately); the old file stays behind as a snapshot. "0"/"false"/"anon" forks into an ANONYMOUS (hidden, unpersisted) session.
newSession(id) method
Start a NEW session with an EMPTY context (re-seeded with the system prompt): the old session file is closed (its flushed content stays on disk — fork() first to keep a snapshot). With no id a random UUID is chosen (an anonymous session STAYS anonymous); "0"/"false"/"anon" always switches to an ANONYMOUS (unpersisted) session.
renameSession(name) method
Rename the current session: the session file takes the proper name (session-<name>.jsonl; the old name's file is gone) — /session-name. An anonymous session has no file to name.
enqueue(message) method
Deliver a user message: while a request is in flight it is queued for the next request; while idle it is appended and starts a request immediately. Pending messages send only after the in-flight IO turn settles (never mid-response). The flush appends them AFTER any tool results (tool calls answer first), append-merged into one user message (consecutive same-type merging). An identical user submission immediately following another user submission is ignored: it is normally an accidental second submit while a run is starting.
enqueueFile(fileName) method
Read an existing file as a binary user message and deliver it. The path is resolved inside the Agent folder; missing files, folders, and paths outside that folder fail before any message is queued.
pathInfo(path, options = {…}) method
Inspect a path using the Agent's file-security boundary.
get pending() getter
the pending queue (a copy — drainPending to remove)
drainPending() method
Remove EVERY pending message, returning them (the TUI's Option+↑ recall: the queued messages go back into the input area, merged, for editing).
resumeSession(id) method
Resume an EXISTING session: the live context is replaced with the session's stored context under its store; the old session file is closed (its flushed content stays on disk).
listSessions() method
Every session in the store's folder, latest first, each with a first-user-message preview (the ^X menu's Resume sub-menu, /resume's Tab completion).
listSessionsAsync() method
Nonblocking counterpart of listSessions().
latestSessionId() method
the latest session's id (undefined: no sessions)
get thinking() getter
the current thinking level (undefined = provider default)
setThinking(level) method
Set the thinking level for subsequent requests (THINKING_LEVELS; each provider translates it to the nearest symbol the model accepts). Applies to already-open provider connections too.
append(message) method
Append a caller-built message (e.g. AI user input), mirrored to the session store when one is wired. Library consumers use this to grow the context between turns; an active run flushes it at its durability points.
edit(i, message) method
Replace context[i] (Context edit semantics: rebuilt from recognized fields, stale provider identifiers dropped), mirrored to the session store when one is wired.
editBlock(i, j, block) method
Replace context[i].content[j] (the containing message is rebuilt).
rollback(i) method
Remove every message at index >= i (RangeError when i is not an existing index).
pop() method
Remove and return the last message (undefined on an empty context).
removeMessages(indexes) method
Remove selected context messages.
class SessionStore class
A JSONL session store: the live context array plus its atomic file rewrite.
Defined in lib/agent/session.js
constructor({ id, dir, context = [], origin, process: proc, uuid, name, save = true } = {…}) constructor
Open a store for a session id (the file is created on the first flush of a non-empty context); registers the crash-safe finish hook.
get save() getter
whether this store writes its context to disk
saveSet(value) method
Enable or disable persistence without replacing the live context. Enabling saving makes the complete current context eligible for the next flush.
append(message, options) method
Append a message, MERGING with the last one when possible (consecutive same-type messages / same-sub-type blocks fold — Context appendMessage). Returns the stored message.
prepend(messages) method
Insert messages at the FRONT of the context — Agent's seeded system prompt, which must always be the first message(s) of a fresh context.
edit(i, message) method
Replace context[i] through Context (stale identifiers dropped).
editBlock(i, j, block) method
Replace context[i].content[j] through Context.
rollback(i) method
Remove all messages at index >= i.
pop() method
Remove the last message.
removeMessages(indexes) method
Remove selected messages.
flush() method
Synchronously make the CURRENT context durable (see _planFlush for the remove/none/append/full decision). Idempotent; a no-op when nothing changed since the last flush.
close() method
Flush and detach the finish hook.
rename(newId) method
Rename the session: BOTH the stable id and the file's NAME segment become newId (the same session — the date/uuid8 prefix carries over unchanged) and the old file is gone. Refuses to clobber an EXISTING other session (a directory scan by id, the file name no longer being a direct function of it).
static originOf({ id, dir } = {…}) method
The ORIGIN FOLDER recorded in a session file's metadata line (the cwd the session ran in), or undefined (no such session). The resume-anywhere contract: an explicit --resume <id> makes this folder the process cwd.
static resume({ id, dir, process: proc, save = true } = {…}) method
Load a session file into a fresh store holding its live context. The file is found by a directory scan matching id against each file's metadata line (the file name is a presentation detail, not a direct function of id — see the module doc). Non-message records in the file are quietly ignored (tolerant reader — see loadMessages).
static list({ dir, cwd, limit = 50 } = {…}) method
Every session in the folder, LATEST FIRST, each with a small preview: a snippet of the first user message (whitespace-folded, capped). With cwd, only sessions whose metadata records THAT origin folder list (the resume contract: a project sees its own sessions; foreign metadata-less files are skipped by the FIRST-LINE scan, never parsed in full). Previews are read only for the newest limit files (stats are cheap; parsing is not). Unreadable files list without a preview, never an error.
static listAsync({ dir, cwd, limit = 50 } = {…}) method
Nonblocking counterpart of list(). It deliberately has its own async folder identity rather than calling sameFolder(), whose realpathSync would stall the interactive loop.
static latest({ dir, cwd } = {…}) method
The id of the most recently modified session in the folder (of the cwd origin when given), or undefined when the folder has none.
static deleteAll({ dir } = {…}) method
Delete EVERY session file in the folder (the /sessions-delete-all! contract — the user confirmed; foreign files stay untouched).
sessionDir() function
The default namespace session folder under settings (created when missing) — outside the project tree by design (see the header).
Defined in lib/agent/session.js
loadMessages(data) function
Parse session data (file text or an already-parsed value) into a context array. Tolerant reader: accepts a JSON array, or JSONL (one JSON value per line); quietly drops every value that isn't a core-shaped message OR a metadata RECORD (an object with a string type — isRecord, lib/context/validate.js): records (the note tool's store backup, user annotations) ride into the context, while the file's own session-metadata header record stays OUT (it maps the file to its folder — it is not context content, and re-loading it would duplicate it on the next flush).
datastring|Array- raw file text or a parsed array
Returns Array<object> — the messages and records found, in order
Defined in lib/agent/session.js
sameFolder(a, b) function
Two folder paths are the same place (realpath when it exists).
Defined in lib/agent/session.js
findSessionFile(folder, id) function
Find a session file in folder by its STABLE id (a directory scan reading each file's first line — cheap even at hundreds of files; the file name does not encode id directly).
folderstringidstring
Returns string|undefined — the file path, or undefined
Defined in lib/agent/session.js
resolveAgentPath(path, { folder = process.cwd(), boundary = folder } = {…}) function
Resolve a relative path from folder, bounded by boundary (the project root by default).
Defined in lib/agent/path-info.js
async rejectAgentSymlinks(resolved, { folder = process.cwd() } = {…}) function
Reject existing symbolic links along a resolved, in-root path.
Defined in lib/agent/path-info.js
async pathInfo(path, { folder = process.cwd(), requireExists = true } = {…}) function
Validate and inspect a path rooted at an Agent folder.
Returns Promise<{path:string,isFolder:boolean,mimetype?:string}>
Defined in lib/agent/path-info.js
onFinish(fn, { process: proc = process } = {…}) function
Register a synchronous cleanup to run at process finish.
fn() => void[options]Object[options.process]NodeJS.Process- injectable for tests
Returns () => void — unregister (idempotent)
Defined in lib/agent/finish.js
runFinish() function
Run every registered cleanup. Errors never propagate.
Defined in lib/agent/finish.js
armFinishSignals({ process: proc = process, signals = ["SIGINT", "SIGTERM", "SIGHUP"], } = {…}) function
Trap termination signals: run cleanups synchronously, then re-raise the signal with our handlers removed so the process dies by the signal itself. Idempotent; returns a disarm function.
[options]Object[options.process]NodeJS.Process- injectable for tests
[options.signals]string[]
Returns () => void — disarm
Defined in lib/agent/finish.js
_resetFinish() function
Test-only reset: forget cleanups and arming state.
Defined in lib/agent/finish.js
callToolSandboxed({ env, name, args, timeout = DEFAULT_TOOL_TIMEOUT, sandbox = false, detached = true, cwd, spawnImpl = spawn, onChild, onData, onQuestion, questionBridge = false, }) function
Run one tool call in a forked child process; never throws — every failure (spawn error, crash, timeout, bad result line) resolves as {ok: false, error}.
optionsObjectoptions.envobject- the parent's registry surface (dir/settings/tool roots are handed to the child)
options.namestring- flattened tool name
options.argsobject- the tool call's arguments
[options.timeout]number- ms before the child is killed (default 120s; explicit 0 disables — Agent's outer timeout wrapper uses 0 so it alone owns callback grace + termination)
[options.sandbox]boolean- run the worker under the OS write sandbox (lib/env/os-sandbox.js — the kernel denies writes outside Env's project folder; the tool's `sandbox: true` metadata)
[options.detached]boolean- default true; false inherits the hosts process group
[options.cwd]string- agent-local tool root (defaults env.cwd)
[options.onData](chunk: string) => void- LIVE streaming: the worker's stderr line channel ("{"data": ...}" records — a tool's incremental output, e.g. a running bash command) rides back here DURING execution, capped on arrival; the result line keeps sole ownership of stdout.
[options.onQuestion](questions: Array) => Promise<Array|null>|Array|null- Trusted-host bridge for a sandbox worker's typed fd-3 `ask` request. fd 3 is worker→host and fd 4 host→worker JSONL; both close at teardown.
[options.questionBridge]boolean- whether the worker receives a callable question bridge (false preserves the ordinary headless refusal).
[options.spawnImpl]typeof spawn- injectable for tests
Returns Promise<{ok: true, value: any} | {ok: false, error: string}>
Defined in lib/agent/tool-sandbox.js
DEFAULT_TOOL_TIMEOUT = ENV_DEFAULT_TOOL_TIMEOUT constant
Default Agent-enforced tool-call duration (120 seconds).
Defined in lib/agent/tool-sandbox.js
resultContent(value) function
Normalize a tool return value into result content blocks — the full tool-answer contract. A tool may return: - a string → one text block; - an array of content blocks → used as-is; - {content: [...]} → the content array; - any other JSON value → JSON.stringify'd into one text block; - {result, system?, display?} → result is normalized by the rules above, while system (string or string[]) appends as System messages right after the tool result and display is retained on it for context viewers (handled by the caller, not here). Throwing fails the call: the error message becomes the tool-result error content (error: true on the result message).
value*- the tool's return value
Returns Array<object> — content blocks for the tool-result message
Defined in lib/agent/tool-exec.js
thinkValue(level) function
Map a thinking-level word to the request option value: off/false/0 → false, on/true/1 → true, level words (low/medium/high/xhigh) pass through, undefined/"default" → undefined (the model's default).
Defined in lib/agent/thinking.js
reseatAgent(agent, { id } = {…}) function
Build the FRESH Agent that replaces agent for a new session.
agentobject- the agent being replaced (the TUI's viewed one)
[options]Object[options.id]string- the new session's id; "0"/"false"/"anon" (or no id while the old session is anonymous) starts ANONYMOUS
Returns object — the fresh Agent, seated in the old one's group
Defined in lib/agent/reseat.js
isAnonymousId = (id) => id === "0" || id === "false" || id === "anon" constant
The anonymous-session id spellings: "0", "false", "anon" (sessions.js).
Defined in lib/agent/reseat.js
RESPONSE_CALLBACK_EVENTS = Object.freeze([…] constant
IO callback property paired directly with its numeric Agent event.
Defined in lib/agent/events.js