AI engine
This is the engine guide — how the pieces of @netscript/ai fit together and
when to reach for each one. @netscript/ai is the provider-agnostic AI engine at
the design center of the stack: a zero-@netscript/*-dependency, ports-and-adapters
core that owns the domain vocabulary, the capability seams, the model registries,
the tool system, the agent loop, MCP transports, and opt-in provider adapters. It
wraps @tanstack/ai* and @standard-schema/spec and adds no schema DSL of its own.
We keep this page at the "which piece, and why" altitude; the exact symbol tables
live in the API reference, linked at the end.
The map: which subpath answers which question
| Subpath | Purpose |
|---|---|
. |
Runtime wiring + the model / embedding / vision registries. |
./contracts |
Domain vocabulary — pure types and the error hierarchy. |
./ports |
Capability seams (hexagonal) plus default port / registry factories. |
./tools |
Tool definition / validation / registry, plus the built-in render_ui contract. |
./agent |
The agent loop. |
./mcp |
MCP transport adapters (stdio + Streamable-HTTP). |
./anthropic |
Anthropic provider (side-effect self-register). |
./openai-compatible |
OpenAI-compatible chat provider. |
./openrouter |
OpenRouter chat provider (OpenAI-compatible base + reasoning option). |
./ollama |
Ollama local chat provider (OpenAI-compatible base + reachability preflight). |
./openai-embeddings |
OpenAI embeddings + vision provider. |
./testing |
Deterministic port fakes. |
A useful way to read that table: the top half is the engine (vocabulary, seams, tools, loop), the bottom half is what you opt into (providers) and how you test without them (fakes).
The runtime and registries
createAiRuntime(config) is a pure wiring function: it resolves every
capability port, defaulting each to a no-op or throwing implementation, with no IO
and no global mutation. getAiRuntime(config?) is the process singleton (shaped
like getKv()), with resetAiRuntime() and isAiRuntimeInitialized() alongside.
Use createAiRuntime when you want an isolated instance (tests, one runtime per
tenant); use getAiRuntime when the process should share one. AiRuntimeConfig
mirrors the resolved runtime field-for-field, all optional, so you inject only the
ports you have real implementations for — everything else stays a safe default
until you need it.
Three registries sit at the root — models, embeddings, and vision — each with the
same register / get / list / reset shape. Models are addressed by reference: a
ModelRef is string | ModelSelector, and the string form is
"<provider>:<model>", e.g. "anthropic:claude-sonnet-4-5". Errors are a small
hierarchy rooted at AiError; the two you will actually catch are
AiNotConfiguredError (you called a capability whose port was never injected) and
ModelProviderNotFoundError (the reference names a provider that never
registered).
Contracts — the shared vocabulary
@netscript/ai/contracts is pure types with no IO — the vocabulary every layer
above the engine speaks. It carries the Message / MessageRole conversation
model, the multimodal ContentPart union (text, image, audio, video, document,
each backed by a base64 or URL ContentSource), the model descriptors and
capability flags, and the tool-call shapes.
The one contract worth internalizing before anything else is the AgentChunk
union — the wire vocabulary the whole stack streams. A run yields text spans,
tool calls, tool results, message boundaries, usage, and errors as discriminated
chunks, always terminating in a final done chunk. The durable-chat runtime
persists these chunks and the chat UI renders them, which is why the layers compose
without adapters: they all speak AgentChunk.
The vocabulary also carries the generative-UI contract: RENDER_UI_TOOL_NAME = "render_ui", RenderUiResult, and a UiResource whose uri is a ui:// string
(mirroring the MCP resource shape).
Ports — the seams you wire
@netscript/ai/ports exposes the capability interfaces — telemetry, tool
registry, embeddings, vision, MCP transport, skill loading, the agent loop,
memory, and the chat/model provider seams — plus their registry and default
factories. Two are worth calling out because they shape how you wire things:
ModelProviderPortcovers discovery (listModels(),getModel(),supports()), andcreateChatClient?()is optional on it — so a discovery-only provider can omit chat entirely. The agent loop injects the narrowerChatModelProviderPort, which requirescreateChatClient(modelId).AgentMemoryPort—append(threadId, message)andload(threadId)are the base;recall?(threadId, query)is optional andundefinedby default. There is no built-in semantic recall — guardrecalland fall back toload.
Tools — Standard Schema, no DSL
The tool system validates with Standard Schema, so you bring any conforming validator (zod, valibot, arktype, or hand-rolled) — the core adds no schema language.
defineAiTool(name)returns a builder:.describe(),.parameters(jsonSchema),.input(schema), terminating in either.server(handler)or.client()(a deferred tool, e.g.render_ui).createToolRegistry(defs?)implementsToolRegistryPort; its.dispatch(name, input, ctx?)validates input before your handler runs and throwsToolNotFoundError/ToolInputValidationErrorwhen it should.renderUiToolis the built-inrender_uiwire contract: schema-only and client-deferred (result.deferred === true). The engine runs no renderer — rendering is the chat UI's job.
The agent loop
createAgentLoop(deps) builds the loop from injected collaborators: a required
modelProvider: ChatModelProviderPort, plus optional tools, history, and
defaultMaxSteps. loop.run(input, options?) returns an
AsyncIterable<AgentChunk>; the loop exposes loop.state and loop.stop().
The defaults are deliberately conservative: the built-in
slidingWindowHistory strategy keeps DEFAULT_HISTORY_WINDOW = 20 messages, and
DEFAULT_MAX_STEPS = 8 bounds how many tool-calling steps a run may take before
it must settle. Hitting that bound throws AgentMaxStepsExceededError inside the
run — the loop settles errored, yields an error chunk, then the final done,
so a consumer never hangs on an unterminated stream.
MCP transports
@netscript/ai/mcp adapts remote Model Context Protocol servers into the same
tool registry the loop dispatches. createMcpTransport(config) takes a
discriminated config (kind: "stdio" or kind: "streamable-http") and returns an
McpTransportPort; registerMcpTools(registry, transport) surfaces the remote
tools and returns a registration whose .stop() detaches. The Streamable-HTTP
transport reconnects with backoff, and auth (none / api-token / oauth) is
injected by your app's startup wiring rather than hardcoded. The full client
story — pooling multiple servers, auth modes, rendering ui:// resources — is
the MCP guide.
Provider adapters — opt-in side-effect imports
Providers self-register on import, mirroring @netscript/kv/redis. The base
engine pulls no provider SDK; you opt in per provider, and an import is all it
takes:
import "@netscript/ai/anthropic"; // self-registers the "anthropic" provider
const model = await getModel("anthropic:claude-sonnet-4-5");
Choosing between them is mostly a question of where your models live:
./anthropictalks to Anthropic directly, catalog taken verbatim from@tanstack/ai-anthropic,apiKeydefaulting toANTHROPIC_API_KEY../openai-compatibleis the workhorse for any endpoint that speaks the OpenAI API: no fixed catalog (the remote endpoint owns its model list), and it throwsAiNotConfiguredErrorrather than guessing whenbaseURL/apiKeyare missing../openrouterbuilds on the OpenAI-compatible base for OpenRouter (key fromOPENROUTER_API_KEY) and adds areasoningEffortoption ("low" | "medium" | "high") normalized to OpenRouter's wire format../ollamabuilds on the same base for a local endpoint and runs a reachability preflight against the host first — so a missing local daemon fails fast instead of at the first token../openai-embeddingsregisters for the embedding and vision seams, not chat:.embed()and.analyze().
Per-provider config fields and defaults are enumerated in the API reference.
Testing — deterministic fakes
@netscript/ai/testing supplies port fakes so an agent or tool can be exercised
without a network: fake chat model providers and agent loops that replay scripted
turns and chunks, fake memory, embedding, and vision ports, an in-memory tool
registry, and createFakeTelemetryPort() (whose .records capture emitted
telemetry). The full factory list lives in the API reference.
Wiring tool and agent registries
Today you wire the engine's registries directly at app startup:
createToolRegistry(defs?) from @netscript/ai/tools for tools and
createAgentLoop(deps) from @netscript/ai/agent for agent loops. No codegen
step sits between your files and the runtime — you register against the engine
factories yourself.
Where the exact tables live
This guide stops at the altitude of "which piece, and why". For the complete, generated symbol tables — every export, signature, and config field — use:
@netscript/ai— the engine itself: runtime, contracts, ports, tools, agent loop, MCP transports, provider adapters.@netscript/plugin-ai— the thin AI plugin delivery shell.@netscript/plugin-ai-core— the plugin's reusable/v1/aicontract core.
And for the rest of the stack: the AI overview tells the layering story, durable chat covers the Fresh runtime, and the chat UI covers the components that render what this engine streams.