@nathapp/nax-agent
v0.3.1
Published
nax's native coding agent: session contract, native loop, tools, permissions, sandbox, command-safety.
Maintainers
Readme
@nathapp/nax-agent
nax's native coding agent as a package: the session contract, the native session adapter and its turn loop over @nathapp/nax-ai, the tool set, permission resolution, the OS sandbox, command-safety and cost accounting.
Pre-1.0. Until
1.0the API of.may change in any minor release. Pin an exact version.
Install
npm install @nathapp/nax-agentRequires Node.js >= 22.19.0. The package is ESM only and ships no Bun code: it runs on Node, and on Bun, with the default runtime.
Entry points
@nathapp/nax-agentis the supported entry. Every export is named, and none starts with_. Its exact names are pinned inapi/nax-agent.api.txt, which CI compares with the built declarations.@nathapp/nax-agent/internalis nax-only and outside semver. nax bundles this package and reaches below the public entry for shared helpers,NaxError, deep modules and the_*Depstest seams. Names, shapes and behaviour there can change in any release, patch included, and a change there is not a breaking change. Do not import it from another project.
Process-wide slots
The host installs these once, near startup. They are module-level, so they apply to everything in the process.
| Slot | Install | When unset |
|:-----|:--------|:-----------|
| Logger | setAgentLogger(logger) (null clears) | Logging is silent: getLogger() is a no-op logger and getSafeLogger() is null. |
| Credentials | configureCredentials({ configDir, readAuthConfig }) | The first credential read throws NaxError with code CREDENTIALS_NOT_CONFIGURED. There is no default config directory. |
| Runtime | setAgentRuntime(runtime) (null clears) | getAgentRuntime() returns nodeRuntime, built on node:child_process and node:fs. |
AgentRuntime is the process and glob contract (spawn, glob, globSync). The Node default is complete; install your own only to change how the agent spawns processes or expands globs.
Ports the host supplies
Three pieces of host knowledge are passed in as data or functions, because the package cannot know them. Their behaviour when you omit them differs, so check each one.
runDeclaredCommandruns a command your project declared (a test or lint command), never one the model wrote. If you do not supply it, theRunCommandtool answersexit 1with "no declared-command runner is configured for this session" and starts no process. It fails closed.ProtectedPathsPolicynames the paths you own and want kept away from the agent: git pathspecs the Git tool excludes from its default view, gitignore patternsGitCommitrefuses to stage, the project state directory, the credential directory and the trust-store file the sandbox protects. If you do not supply it, the Git tool excludes nothing from its default view, andGitCommitfails closed: it refuses every path and stages nothing until the policy supplies a non-emptygitIgnorePatterns(an empty list refuses the same way). Supply it whenever the agent works in a directory that holds files you own. Building a sandboxed session requires it.commandInterceptormay rewrite a command before it runs (for example to prefix a wrapper binary). Rewrites are validated: an argv rewrite may only prefix the original argv with the provider's own binary, and a shell rewrite goes through the same narrowing. If you do not supply one, commands run unchanged. An interceptor that throws, or returns a rewrite that fails validation, is treated as a decline, and the original command runs.
Conversational sessions
createAgentSession gives an embedder (for example a long-running Node service) a multi-turn chat with a person in the loop. Events stream, tools come from the embedder, history lives in a store the embedder supplies, and a turn can be cancelled or answered with an approval.
import { createAgentSession, createFileTranscriptStore, type EmbedderTool, nativeBackend } from "@nathapp/nax-agent";
const lookupOrder: EmbedderTool = {
name: "lookup_order",
description: "Look up an order by id.",
inputSchema: { type: "object", properties: { id: { type: "number" } }, required: ["id"] },
approval: "always", // ask the person before every run
async run(input, { signal }) {
return { content: JSON.stringify(await orders.get(input, { signal })) };
},
};
const session = await createAgentSession({
backend: nativeBackend({ model: "anthropic/claude-sonnet-5-5" }),
profile: "ask",
workdir: "/abs/project",
tools: [lookupOrder],
transcriptStore: createFileTranscriptStore("/abs/state/sessions"),
});
for await (const event of session.send("Where is order 42?")) {
if (event.type === "text_delta") process.stdout.write(event.text);
if (event.type === "approval_requested") showApproval(event); // later: session.answer(event.requestId, { decision: "allow" })
if (event.type === "turn_end") console.log(event.status, event.costUsd);
}
await session.close();Profiles set what a session may do:
| Profile | Side effects |
|:--------|:-------------|
| none | no side effects; |
| read | read-only; |
| ask | every Write, Edit, Delete, GitCommit and Bash is put to answer(); |
| full | no prompts; bashApproval and the sandbox floor apply. |
createAgentSession takes a SessionBackend. nativeBackend(opts) takes model, credentials, catalogOverrides, loopHandlers, hostPorts, bashApproval and allowUnsandboxed. The ACP backend ships in @nathapp/nax-agent-acp.
usage.costSource is computed (native), reported or unpriced; never sum unpriced rows as cost.
- One turn at a time.
send()claims the session's turn slot at once. A secondsend()while a turn runs throwsAGENT_SESSION_BUSY. The returned iterable is single-use, and the turn starts on its firstnext(). Breaking out of the loop cancels the turn. - Events.
turn_start,text_delta,thinking_delta,stream_reset,tool_call,tool_result,approval_requested,approval_resolved,question,usage,compactionandturn_end. Sessions do not compact, socompactionis not emitted and a conversation that outgrows the model's context window endserrored. Each carriessessionId,turnId,atand yourmetadata.turn_endis always last, and a failed turn arrives asturn_end, never as a throw. - Deltas are provisional.
stream_resetmeans the deltas of that round so far are void: the provider call was retried after a transient fault (up to 3 attempts; a rate-limit retry waits the provider'sretryAftersilently, andcancel()ends the wait).turn_end.outputand the stored transcript are authoritative. While your consumer lags, adjacent deltas are merged; control events are never merged or dropped. - Approvals and questions. Answer them with
session.answer(requestId, { decision: "allow" | "deny" })or{ text }. An unanswered one is denied afterapprovalTimeoutMs(default 600000, range 30000..3600000).answerreturns"accepted","expired","cancelled"or"unknown". - Credentials. A session's
credentials(memoryorexec) andcatalogOverridesgive it its own client. Without them it uses the process-wideconfigureCredentialsslot. - History and restarts. The store holds one document per session, saved at the end of every turn.
close()keeps it. After a restart,resumeAgentSession(sessionId, options)reopens it with the same options you created it with; passinstructionsandtoolsagain, since they are not stored. If the process died mid-turn,session.lastTurnis{ turnId, status: "interrupted" }, and that turn's message is not in history. A resume with a different model throwsAGENT_SESSION_MODEL_MISMATCH; a different reasoning effort ([high]) is the same model. Resuming with a different backend kind throwsAGENT_SESSION_BACKEND_MISMATCH; a stored document with nobackendfield is native. Do not open one session id twice at once: the store has no lock. - Errors. Every error is an
AgentSessionErrorwith a codeAGENT_SESSION_*:INVALID_OPTIONS,EXISTS,BUSY,CLOSED,INVALID_ANSWER,NOT_FOUND,SCHEMA_UNSUPPORTED,MODEL_MISMATCH,SANDBOX_UNAVAILABLE,TOOL_NAME_RESERVED,BACKEND_UNAVAILABLE,AUTH_REQUIRED,CAPABILITY_UNSUPPORTEDorBACKEND_MISMATCH. A stored document that cannot be read is aNaxErrorwithTRANSCRIPT_CORRUPT.
Status and roadmap
0.x may reshape . with a minor bump. @nathapp/nax-agent-acp provides an ACP backend for the same API (0.3.0). See CHANGELOG.md.
Maintainers: see the release procedure for the manual 0.1.0 publish and OTP step, trusted-publisher setup and subsequent tagged releases.
License
MIT
