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, and thinking level. One Agent owns one conversation for its lifetime; create another Agent to start or resume a conversation, or use Agent.fork to branch one. App owns view replacement. 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 receives the COMPLETE current context on every request; one IO instance is reused per qualified model and reconstructed after close() leaves it permanently closed. It reports usage PER TURN (the terminal event's usage envelope, see lib/context/usage.js) — nothing above it. - The CONTEXT is a Context object (lib/context.js): Agent names it from a session id (--session/--resume), or adopts a ready one unchanged; its identity never changes. Context owns edits and persistence to env.settings.sessions when saving is on. - Agent alone owns the CUMULATIVE usage total (the usage getter): it sums every terminal's canonical counters 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 eventDispatch/execution, the result contract; - readouts.js cumulative usage/context-window/connection readouts; - context-lifecycle.js constructor session settings and seeded system prompt; - thinking.js the thinking level.
Model identity is one qualified <endpoint>/<model> string throughout construction, property assignment, child creation and context persistence. Assigning model validates and records a whole state change. run() has no model override. Split endpoint options are rejected; presentation layers derive display fields.
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 settings.tools.timeout (duration) Read-only (tool schema safe:true) calls overlap in contiguous groups; mutations run in order. Every call is Agent-timed (including in-process). A schema-declared timeout argument is extracted and capped at settings.tools.timeoutLimit; schema.onTimeout gets at most 60s for cleanup or a final result.
WRITE CAPABILITY (options.can.write; assign agent.can.write at runtime): always publish the full available catalog, but execute ONLY read-only tools (schemas with safe: true) while write is denied. Agent passes the effective tool-context safe to Env.toolCall. A child inherits its parent's write restriction. Write denial is FORCED 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 after the complete tool-call batch and any queued user input. 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 Context class
Constructor namespace; see Agent.Context for its full contract.
static Env class
Constructor namespace; see Agent.Env for its full contract.
static IO class
Constructor namespace; see Agent.IO for its full contract.
static Jobs class
Constructor namespace; see Agent.Jobs for its full contract.
static EVENT field
The Symbol event vocabulary for Agent.onEvent — events about THIS agent only (Env reports membership: AGENT_ADDED/AGENT_REMOVED): REQUEST_START, TEXT_START, TEXT_DELTA, TEXT_END, THINKING_START, THINKING_DELTA, THINKING_END, TOOL_CALL_START, TOOL_CALL_DELTA, TOOL_CALL_END, REQUEST_DONE, REQUEST_ERROR, MESSAGE_COMMITTED, LOG, TOOL_EXECUTE, TOOL_DATA, TOOL_RESULT, CLOSE_MARKED, CLOSED, SENT_MESSAGE, CONTEXT_REPLACED — see Agent.onEvent's documentation for each value's meaning and payload.
static EVENT_CALLBACKS 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.
static NAMES field
Namespace-derived runtime names. Namespace is the canonical product name; the public library façades retain their own names (see lib/agent.js). The UPPER/lower variations supply prefixes for environment variables and namespace-specific runtime paths.
busy property
Read: get busy()
Whether an agent run is currently in progress. @returns {boolean}
can field
Live agent-owned capability map. read is always true; write is subject to sandbox and parent restrictions; a child's delegate and interact are always false. Tools may define their own keys; only boolean values allow or deny, others require asking. Assignments persist with session settings.
Returns {read: boolean, write: boolean, delegate: *, interact: boolean, [capability: string]: *} — Live agent-owned capability map. `read` is always true; `write` is subject to sandbox and parent restrictions; a child's `delegate` and `interact` are always false. Tools may define their own keys; only boolean values allow or deny, others require asking. Assignments persist with session settings.
cancel() method
Cancel active IO and every pending tool dispatch; queued calls never start. Dispatch owns sandbox teardown. Resolves after active dispatch resources are released; repeated cancellation is safe and needs no escalation.
childCreate(options = {…}) method
Construct one direct child through the environment factory. The parent relationship is authoritative for ownership and delegation denial.
[options]object
Returns Promise<Agent>
Throws — {Error} when this agent's env is closed (env.agentCreate)
children property
Read: get children()
Snapshot of directly owned workers.
close() method
Mark closure synchronously, refusing admission immediately. Finish an active turn without cancelling it; callers who need interruption call cancel(). Await completion for all owned IO, context persistence, background processes, registry and hook removal, never the borrowed Env. Concurrent and repeated calls await the same outcome. CLOSE_MARKED precedes cleanup; CLOSED follows every attempted release. All cleanup and listener failures are reported together after the remaining releases run.
Returns Promise<void> — Completion of owned teardown; rejects with AggregateError on failures.
closeMarked property
Read: get closeMarked()
Whether close has been requested, including while a turn finishes.
closed property
Read: get closed()
Whether close cleanup has completed.
compact(focus = "") method
Compact: ask the model to summarize the conversation (a structured, self-contained prompt), then continue in a new 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.
[focus]string- optional user guidance appended to the summary prompt
Returns Promise<{ok: boolean, before: number, summaryText?: string}>
constructor(options = {…}, construction) 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).
[options]Objectoptions.envEnv- required borrowed environment; use Agent.create() for asynchronous acquisition
[options.model]string- default `<endpoint>/<model>` selector
[options.context]Array|Context- seed messages (deep-copied into the owned Context), or a ready Context used as is (then no `session`) (seed arrays receive the system prompt ahead of them; a ready Context is adopted without changing its messages or stored settings)
[options.tools]string[]- availability selection (omitted/["*"]=all, []=none)
[options.parent]Agent- creating Agent, or unset for non-Agents
[options.name]string- display name (default: `agent-<counter>`); names that read as an address or a privileged identity are reserved (see `name`)
[options.description]string- display description (empty allowed)
[options.folder]string- existing env.folder-relative or absolute working folder; validated before the agent is registered (omitted uses env.folder)
[options.can]object- agent capabilities: read is always true; write false denies mutations while preserving the full catalog, and is forced false without OS sandbox or when the parent denies writes; delegate allows child creation (false denies; other values ask); interact defaults true but is false for children. Additional tool-owned keys are allowed.
[options.contextId]string|false- names the context: an existing id (in env.settings.sessions) is resumed — only a session recorded in env.folder; an absent id creates it. `false` (or omitted, or an anonymous spelling "0"/"false"/"anon") gives a memory-only context that is not logged — (agent.context.save = true) starts logging it at any time.
[options.contextSave=true]boolean- whether a context named by `contextId` is logged to disk
[options.createIO](opts:object)=>object- IO factory (tests inject fakes)
[options.onStart](payload: object) => void- response callback; onStart, onTextStart, onTextDelta, onTextEnd, onThinkingStart, onThinkingDelta, onThinkingEnd, onToolCallStart, onToolCallDelta, onToolCallEnd, onDone, and onError map to Agent.EVENT_CALLBACKS. Supplied callbacks register through onEvent before Env membership events; done/error fire after request bookkeeping.
Throws — {TypeError} Direct construction, missing or invalid Env, non-object options, unknown own keys (including symbols), non-function response callbacks, and unknown toolCall keys are rejected before Context adoption. Use can.write instead of the removed safe key; count limits and toolCall.async are unsupported.
contacts property
Read: get contacts()
Sorted contact addresses (alias@project); aliases never change.
Returns string[] — Sorted contact addresses (`alias@project`); aliases never change.
context property
Read: get context()
The live conversation owned and closed by this Agent. Mutate its messages/settings through Context; construct another Agent for another conversation. Identity is read-only for the Agent's lifetime.
Returns Context — The live conversation owned and closed by this Agent. Mutate its messages/settings through Context; construct another Agent for another conversation. Identity is read-only for the Agent's lifetime.
contextUsage property
Read: get contextUsage()
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).
Returns {used: number, total: number|null, approximate: boolean}
static create(options = {…}) method
Open an Agent with a ready Env. When env is omitted, acquire the Env for cwd (the process cwd by default) through Env.open. The returned Env is borrowed: the caller, not Agent.close(), decides when to close it. folder independently selects the Agent's tool root, never the Env path.
[options={}]object- Agent creation options plus cwd and envOptions.
[options.cwd=process.cwd()]string- Env project path, only when env is omitted.
[options.envOptions]object- Options forwarded to Env.open, only when env is omitted.
Returns Promise<Agent> — A fully constructed Agent; options are not mutated or retained.
Throws — {TypeError|Error} Invalid options or Env startup/Agent construction failure; fix inputs before retrying. Env acquisition is not undone if Agent construction fails.
description property
Read: get description()
Human-friendly Agent description; an empty string is valid.
Write: set description(value)
Describe the agent (recorded into a logged context's settings).
valuestring
Returns void — Assignment records the new description.
env property
Read: get env()
Borrowed, read-only environment identity; Agent never closes it.
Returns Env — Borrowed, read-only environment identity; Agent never closes it.
finishAdd(fn, { process: proc = process } = {…}) method
Register fn for synchronous execution on the configured process's exit event.
fn() => void- Cleanup callback; must be synchronous and may run more than once.
[options]Object- Registration options.
[options.process=process]NodeJS.Process- Process to attach the exit hook to.
Returns () => void — Idempotent function that unregisters `fn` from the shared cleanup set.
Throws — {TypeError} If `fn` is not a function.
finishRun() method
Invoke a snapshot of all registered cleanups synchronously; swallow callback errors.
Returns void
folder property
Read: get folder()
The agent-local root used for file tools and their OS sandbox.
Write: set folder(folder)
Set this agent's tool working folder to an existing directory. undefined restores the parent folder for children, or Env.folder for independent Agents. Embedded independent Agents may use another tree.
folderstring|undefined|null- absolute or env.folder-relative
Returns void — Read folder afterwards for the resolved root.
fork({ id, through } = {…}) method
Create an independent Agent from a deep copy of this conversation at a message boundary. The source is neither closed nor changed; the returned Agent owns its new Context and has no parent. Its runtime work, event listeners and usage are not copied.
Returns Promise<Agent> — A new Agent owned by the caller; close it independently.
Throws — {TypeError|RangeError|Error} Invalid options, boundary, existing session id, or a closing/running source; correct the input or wait for the source to become idle. Factory failures leave the source intact; retry after resolving the failure.
io property
Read: get io()
The selected IO session, lazily created for this Agent's model; undefined when no model is selected.
Returns IO|undefined — Agent-owned active provider IO; the Context is never stored in IO between requests.
Effects — Creates a provider IO on first read without sending a request.
Write: set io(value)
Adopt another idle IO session for subsequent requests using the same Context.
valueIO- IO using this Env, configured with this Agent's safety/tool policy.
Returns void — The adopted IO is closed with this Agent. The caller owns and closes any displaced IO obtained from the getter.
Throws — {TypeError} Invalid IO, another Env, busy/closed Agent or IO; correct configuration or await idle before retrying. Validation failures leave selection unchanged.
Effects — Selects and records value.model, then replaces the cached IO. Reassigning the same IO is idempotent.
ioState property
Read: get ioState()
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).
Returns "idle"|"working"|"disconnected"
model property
Read: get model()
The current qualified model selector. Assign to validate and record a whole model-state change; an active request finishes on its already-open IO.
Returns string|undefined — The current qualified model selector. Assign to validate and record a whole model-state change; an active request finishes on its already-open IO.
Write: set model(selector)
Select an exact, configured endpoint/model pair for subsequent turns. Validation happens before the live model changes, so a failed attempt leaves the current selection intact.
selectorstring|undefined- `<endpoint>/<model>`; undefined clears the selection
Returns void — Assign the selector; read model for the current value.
Throws — {TypeError} Invalid selector or unavailable endpoint/model; keep the old selection and choose a valid model.
Effects — Records the default model and session settings; undefined clears it. Repeating a valid selection is safe and updates its last-used timestamp.
name property
Read: get name()
Human-friendly Agent name.
Write: set name(value)
Rename the agent (recorded into a logged context's settings). Reserved (any case, look-alike letters and separators ignored): new, all, everyone, chat role names, the product name, and names containing user, admin, group, or another authority word (root, sudo, system, developer, owner, operator, supervisor, moderator, privilege, authority).
valuestring
Returns void — Assignment records the new name; read name for the current value.
Throws — {TypeError} For a non-string or reserved name.
offEvent(handle) method
Remove one registration; returns false when it is absent.
onEvent(event, callback) method
Register a synchronous listener for one Symbol 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 user message when it enters context. The possible event values (the Agent.EVENT constants):
Agent.EVENT.REQUEST_START— a provider request began (once per request; a tool-loop run makes several); 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.TOOL_CALL_START/TOOL_CALL_DELTA/TOOL_CALL_END—
the same lifecycle for one streamed tool call
Agent.EVENT.REQUEST_DONE— the provider request completed (after its bookkeeping)Agent.EVENT.REQUEST_ERROR— the provider request 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 user message entered contextAgent.EVENT.CONTEXT_REPLACED— compaction switched contexts; {previous, context}Agent.EVENT.THROTTLED— a delayed continuation was scheduled; {until} is epoch ms
eventsymbol- one Agent.EVENT constant (see the list above)
callback(payload: object) => void- synchronous listener; returned promises are not awaited. Async listeners must observe their own rejections and cannot delay teardown.
Returns number — deterministic opaque registration handle (offEvent removes it)
parent property
Read: get parent()
The creating Agent, or undefined when none was supplied.
pending property
Read: get pending()
Unsent messages queued while busy (array copy; message objects are shared). An error leaves them pending until a later run or pendingPop().
pendingPop() method
Remove ALL unsent pending messages, returning them (the TUI's Option+↑ recall: the queued messages go back into the input area, merged, for editing).
Returns Array — the drained messages
question property
Read: get question()
The live question bridge; a trusted host capability, never provider data.
Returns object|null — The live question bridge; a trusted host capability, never provider data.
Write: set question(callbacks)
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.
Returns void
request(request) method
Send a request (handoff, question or reply) to another root agent and return at once; replies arrive later as user-role messages labelled with the sender's contact address. Requires can.request === true; workers never may. to: "<name>@<project>" (see contacts) messages a known agent again: queued while busy, resumed from its saved session when closed, never duplicated. to: "new:<name>@<project>" starts a fresh root agent with that role name (a taken name gets -2, -3…) in this (this project) or a pinned project sharing a settings group. It inherits this Agent's capabilities, model, thinking, tools, IO factory and session logging; safe: true makes it read-only and model picks a known model with free capacity. On a known contact, safe: false and a model equal to that contact's current model are harmless; changing either setting requires a new agent. An empty string, false or null model means no override for both kinds of request. The receiver gains this Agent as a known agent, so it can reply.
Returns Promise<string> — The target's contact address (`<alias>@<project>`).
Throws — {TypeError|Error} Invalid input, worker caller, missing permission, unknown project/contact/model, unsaved closed contact, or creation failure.
run(options = {…}) method
Run the tool loop until done/error (lib/agent/run.js).
[options]Object[options.timeout]number|string- per-request provider-request timeout
[options.after]number- non-negative delay in ms before running; an immediate run cancels a pending delayed continuation
Returns Promise<object|null> — terminal done/error, or null if a delayed run is superseded
Throws — {TypeError} Unsupported options, including model/endpoint and internal loop controls; assign agent.model before running.
Effects — Executes provider requests and tools, updates this Context and usage, and persists completed turns. Concurrent calls share the active run.
send(message) method
Submit 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. User slash messages are interpreted just before IO, after all pending and direct context writes: /compact triggers compaction, and registered /prompt (or //prompt) expands with trailing text. Unknown slash commands remain text; other UI commands are not executed. A user message is TRIMMED of surrounding whitespace (trailing EOLs, tabs, every white space) before it is queued; a submission that is whitespace only carries no turn at all — it behaves exactly like /continue: no message is appended and the existing context runs.
messageobject|string- a context message, or plain text converted to a user message
Returns Promise<object> — the active run's terminal done/error event.
Invalid input and closed-agent submissions return a rejected Promise
without changing the queue or context. Awaiting a busy send waits for
that run, not a distinct reply; on failure queued messages remain pending.
sendFile(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.
fileNamestring- Agent-folder-relative file path
Returns Promise<object> — the run's terminal done/error event
skillLoad(names) method
Activate named skills in this Agent's Context, or list the Env catalog. Validate all requested names before injection; already active skills are no-ops.
[names]string|string[]- Names to activate; omitted lists available skills.
Returns string — Catalog text or activation summary; inputs are not retained or mutated.
Throws — {TypeError|Error} Invalid names, unknown skills, or closed Agent; correct names before retrying. Lookup failures inject nothing.
Effects — Appends durable system guidance; repeated activation in the same Context is idempotent.
thinking property
Read: get thinking()
the current thinking level (undefined = provider default)
Returns string|undefined — the current thinking level (undefined = provider default)
Write: set thinking(level)
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.
[level]string- none/low/medium/high/xhigh/max (undefined: the model's default)
throttledUntil property
Read: get throttledUntil()
Epoch-ms deadline of a pending continuation, or null.
toolMessages() method
tools with a live sticky message
Returns Array<{name: string, text: string}> — tools with a live sticky message
tools property
Read: get tools()
Current model-facing tools: const tools = await agent.tools; tools.get(name) returns a provider-neutral descriptor with its schema. Uses IO's publication filter: dynamic availability, configured selection, hidden-tool exclusion and the model's provider tools. Provider dialect conversion remains IO/provider-owned; this is the current catalog, not a capture of an earlier request's wire payload.
Returns Promise<Map<string, object>> — Fresh map/descriptors, no callables or execution metadata. Nested schemas are shared registry data.
Throws — {Error} Availability-check failures propagate; retry by reading again after correcting the tool/environment.
Effects — Rechecks dynamic availability; does not open a provider connection or mutate context. O(n) in catalog size.
usage property
Read: get usage()
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. Only canonical token/cache/reasoning counters and reported cost are summed; totalTokens is inputTokens + outputTokens. Cache counters are input subsets; reasoningTokens is an output subset. Optional serviceTier, inferenceGeo and bounded provider details describe only the latest request (missing metadata clears previous values). Unknown numeric details are snapshots, not additive counters. Quotas remain separate Context.planUsage snapshots. The returned deep snapshot shares no mutable data with this Agent; reading has no effects.
Returns {inputTokens: number, outputTokens: number, cost: number, totalTokens?: number, cacheReadTokens?: number, cacheWriteTokens?: number, cacheWrite5mTokens?: number, cacheWrite1hTokens?: number, uncachedInputTokens?: number, reasoningTokens?: number, serviceTier?: string, inferenceGeo?: string, details?: object}