@nifrajs/agent
v5.5.1
Published
Provider-neutral, typed, resumable agent turns built on Nifra tool contracts.
Maintainers
Readme
@nifrajs/agent
Provider-neutral, typed, resumable agent turns over Nifra tool contracts.
The package owns one bounded turn at a time. Model providers, state, approvals, budgets, idempotency, and telemetry are ports. The package never imports a model vendor and its state adapter stores only token-only turn evidence.
import { createAgentState, runAgent } from "@nifrajs/agent"
const result = await runAgent(definition, { value: input }, ports, {
state: createAgentState("support-1"),
maxTurns: 8,
})The public in-memory adapters are for local development and tests. Durable state, provider credentials, and operated policy remain adapter concerns.
For transient prompt assembly, @nifrajs/agent/context provides a deterministic, token-budgeted
selection seam:
import { assembleContext } from "@nifrajs/agent/context"
const context = assembleContext(
[
{ id: "system", kind: "instruction", content: "You are concise.", required: true },
{ id: "memory", kind: "memory", content: "The user prefers short answers.", priority: 10 },
],
{ budget: { maxTokens: 2_000, reserveTokens: 256 } },
)The assembler owns selection, ordering, and accounting only. Persistence, embeddings, retrieval indexes, provider tokenizers, redaction, and product policy stay in caller-owned adapters.
Gateway and deployment contracts are deliberately provider-neutral. Retry, fallback, budget, deadline, workspace, and hostile-code isolation claims are admitted only from a host-approved capability report. The local and replay reference profiles are not hostile-code sandboxes, and no public package loads provider credentials or promises durable execution.
Several telemetry consumers can observe the same run: combineAgentTelemetry(a, b, ...) fans step
evidence out to every port in order, so an SSE evidence stream and an exporter (for example
@nifrajs/agent-telemetry's traceAgentRun) compose instead of displacing each other. The HTTP
seams (mountAgent, @nifrajs/a2a, @nifrajs/ag-ui) compose their streaming evidence with any
telemetry port supplied through ports.
A model port can stream: when the caller wires an AgentDeltaSink into ports.deltas, every model
request carries an optional onDelta callback the port may call per chunk - user-visible text,
reasoning text, the raw argument text of the tool call being formed, or a usage delta reporting
the decision's settled token counts (optionally attributed to a provider and model, for observers
to sum across the run). Deltas are transient
observer data for live UIs (@nifrajs/ag-ui projects them onto TEXT_MESSAGE_*, REASONING_*,
and TOOL_CALL_ARGS frames): they are never validated, persisted, or replayed, and a failing sink
never fails the model step. combineAgentDeltaSinks(a, b, ...) fans deltas out the way
combineAgentTelemetry fans evidence.
createAgentSharedState(initial) is the run's shared UI state channel: a JSON document any port or
tool executor patches with RFC 6902 operations (add, replace, remove), with subscribers
observing every applied batch. Patches apply atomically - an invalid op rejects the whole batch -
and prototype-grafting pointer segments are refused. The document is per-run observer data, not
turn state: nothing in it is persisted by the runtime. @nifrajs/ag-ui projects the channel onto
AG-UI STATE_SNAPSHOT/STATE_DELTA events.
SSE streams are resumable: give mountAgent (or @nifrajs/ag-ui's mountAgUI) an evidenceLog
and step frames carry id: <seq>. A client that loses the connection re-POSTs the same turnId
with a Last-Event-ID header and receives the missed evidence, rejoining a still-running turn live
or replaying the stored terminal frame - the run is never re-executed. A disconnected run keeps
going and records its real result for that reconnect. Without an evidenceLog nothing can rejoin a
dropped stream, so the disconnect aborts ports.signal and the turn suspends as cancelled.
createMemoryAgentEvidenceLog (from @nifrajs/agent/events) is the single-process dev/test
reference; a durable, multi-process log is an adapter implementing the same AgentEvidenceLog
interface.
One SSE response emits at most maxOutputBytes (default 4 MiB), so a client that stops reading
cannot make the server queue a whole run. Past the cap the response ends with an error frame
(output_limit): with an evidenceLog the run keeps recording and a Last-Event-ID reconnect reads
on from where the response stopped; without one the run is cancelled.
When more than one caller shares the route, also pass evidenceOwner: (c) => <the caller's user or
tenant id>: turns are recorded and replayed under that owner, so another caller's reconnect or
reused turnId finds none of them. Without it a turn id works as a bearer capability. The owner
function runs before any replay; throwing from it refuses the request with 403, and a thrown
Response answers the request as built, as one thrown from ports does. A JSON-mode run that
throws answers 500 run_failed, or 409 resume_unavailable when the POST resumed a turn.
Execution policies can be required by a tool contract. createLocalProcessAdapter applies cwd and
environment filtering, timeouts, and cancellation to child processes. On POSIX each run leads its
own process group: a timeout, a cancel, or the run's own end stops every process the command
started, background ones included, and runs still in flight end when the host exits. On Windows a
timeout or cancel ends the command's whole process tree (taskkill /T /F), but a background process
still running after the command exits normally is not tracked. The local adapter is NOT a
security boundary. Without OS-level sandboxing it contains crashes and accidents, not hostile code.
For the compact agent contract, see LLM.md. The full machine-readable corpus is
../../llms-full.txt.
