Public API and ownership¶
Named framework failures are listed in the error reference. Application callback, allocator, runtime, and custom transport errors propagate alongside those named errors without wrapping.
ZigAI's supported API starts at @import("zigai"). Prefer its short aliases
for agent and model types, and use the named modules for larger subsystems:
const zigai = @import("zigai");
const Agent = zigai.Agent;
const Client = zigai.providers.openai.Client;
const Dataset = zigai.evals.Dataset;
Supported surface¶
The root exposes the agent loop, its configuration and result types, message and tool types, model settings and usage, streaming events, provider errors, history helpers, telemetry, MCP, evaluation, and model-routing modules.
Reusable history is typed as Message, a union of RequestMessage and
ResponseMessage. Construct prompts and tool returns with RequestPart;
model implementations return ResponsePart. PromptPart is the short alias
for rich UserContent accepted by RunOptions.prompt_parts. Part remains a
compatibility alias for ResponsePart.
The vocabulary follows PydanticAI's message model while staying idiomatic in Zig:
| Area | Types |
|---|---|
| Prompts | system, user, retry, instruction, speech, and tool-availability parts |
| User content | text, tagged text, image, audio, video, document, binary, uploaded file, and cache point |
| Tools | function, provider-native, tool-search, and capability-load calls and returns |
| Model output | text, thinking, compaction, files, speech, and tool parts |
zigai.tool.Policy is the orchestration extension point for function tools.
Its typed events cover preparation, argument validation, dynamic approval,
pre-call behavior, and return validation. Agent.tool_policies run first;
capability policies follow in capability order. Argument and return policies
may transform values or request a retry from the tool's own budget. Dynamic
approval is evaluated after valid arguments and persisted across pause/resume.
Tool.sequential provides scheduler-wide exclusivity. Return schemas are
local unless return_schema_visibility opts into provider descriptions.
Capabilities¶
Capability is a borrowed implementation bundle. It can contribute local and
provider-managed tools, toolsets, instructions, lifecycle hooks, tool policies,
history processors, output validators, model settings, and a model selector.
Anonymous capabilities are eager-only. A capability needs a stable id when
it is on demand or declares dependencies or conflicts.
Set loading = .on_demand to keep the bundle out of the first provider
request. ZigAI adds a catalog instruction and a sequential load_capability
function instead. A successful call resolves dependencies in declaration
order, returns their instructions in dependency-first order, and activates all
contributions together before the next model request. OpenAI Responses,
OpenAI-compatible Chat Completions, Anthropic Messages, and Google Gemini replay
typed capability-load history through their ordinary function-call protocols.
Loads are atomic: a conflict, callback failure, or allocation failure cannot
leave a partial bundle active. Structural errors are detected before the first
model request. CapabilityRegistry exposes the same bounded validation and
dependency planning without executing an agent; LoadResolution owns its
arena and must be deinitialized.
CapabilityUnloadPolicy.history reconstructs successful loads from canonical
history. .run_end forgets them between independent invocations while keeping
them active through serialized pause/resume. Unsuccessful load results never
restore state.
Capabilities compose in fixed scope order: inherited, agent, run, nested, then
subagent. Direct Agent.capabilities occupy the agent scope and
RunOptions.capabilities occupy the run scope. RunOptions.capability_layers
adds explicit scopes; declaration order is stable within a scope regardless of
the layer slice order.
CapabilitySnapshot is borrowed and exposes active IDs plus the subset loaded
on demand. It is available to dynamic instructions, toolsets, tools, tool
policies, output validators, and model selectors. Nested agents can use the
snapshot to construct an inherited CapabilityLayer; ZigAI does not infer
subagent ownership from an opaque dependency pointer.
The short system_prompt, user_prompt, retry_prompt, and text variants
are convenient when no part metadata is needed. Their *_part counterparts
retain timestamps, IDs, and provider replay data. Provider-bound parts carry a
ProviderPart; file IDs carry their owner in UploadedFile.
History preserves opaque provider data as ProviderDetails, a structured JSON
object separate from application Metadata. Current adapters reject
provider-bound part IDs or details they cannot replay, and validate ownership
before encoding uploaded files. They never silently flatten fields.
Create it from a parsed object with ProviderDetails.fromValue; nested slices,
arrays, and object maps follow the enclosing owner's lifetime.
All slices are borrowed. Agent.Result, history.Owned, and other explicitly
owned containers define the lifetime of copied or parsed messages. The message
types themselves never allocate and never store an allocator.
The same types live under zigai.messages. The namespace is the canonical home
for durable conversation data; the root aliases keep common application code
short, and zigai.model keeps compatibility aliases for model implementers.
Use these namespaces for the rest of the API:
| Namespace | Purpose |
|---|---|
zigai.messages |
Provider-neutral request/response messages and parts |
zigai.capability |
Capability descriptors, scopes, diagnostics, and dependency planning |
zigai.agent_spec |
Strict JSON/YAML agent configuration, dry-run validation, and resolution |
zigai.settings |
Portable model controls and tagged provider extensions |
zigai.usage |
Per-request and aggregate run usage, exact costs, and native counters |
zigai.pricing |
Explicit versioned price tables and deterministic estimates |
zigai.codecs.pydantic_ai |
Lossless PydanticAI stable-v2 JSON interchange |
zigai.security |
Outbound URL validation and diagnostic redaction |
zigai.provider |
Provider identity, authenticated operations, policy, discovery, files, and profiles |
zigai.providers |
Native and named OpenAI-compatible provider clients |
zigai.providers.http |
Reusable authenticated HTTP provider for model adapters |
zigai.models |
Fallback and application-selected model routing |
zigai.history |
Version-2 ZigAI history serialization, version-1 migration, and processors |
zigai.evals |
Datasets, evaluators, reports, and model grading |
zigai.eval_io |
Versioned JSON/YAML dataset and report documents |
zigai.eval_compare |
Baseline/candidate comparisons and stable CI JSON |
zigai.mcp |
MCP 2026 client, server, transports, and explicit durable request identities |
zigai.durable |
Versioned durable operations, records, payloads, and runtime bindings |
zigai.durable.checkpoint |
Restart-safe stream cursors and approval state stores |
zigai.durable_adapters.temporal |
Temporal sidecar adapter, worker registrations, retry policy, and payload limits |
zigai.graph |
Typed graph definitions, bounded execution, lifecycle events, and manual iteration |
zigai.graph_agent |
Explicit buffered and structured agent-node adapters for typed graphs |
zigai.multi_agent |
Bounded delegation, handoff, and subagent execution scopes |
zigai.embeddings |
Provider-neutral bounded text embedding and batching |
zigai.realtime |
Persistent speech, transcript, turn, tool, and reconnect sessions |
zigai.ui |
AG-UI and Vercel AI SDK browser event adapters |
zigai.harness |
Reusable bounded coder, researcher, and custom compositions |
zigai.execution |
Rooted filesystem, shell, remote sandbox, and disposable workspace contracts |
zigai.builtin_capabilities |
Reviewed web, browser, image, skill, and repository bundles |
zigai.memory |
Tenant-isolated conversation and semantic memory stores |
zigai.planning |
Bounded revisions, advisors, approval gates, and dynamic workflows |
zigai.runtime_services |
Step, blob, prompt, and bounded-executor service contracts |
zigai.acp |
Agent Client Protocol process, session, update, and client-operation support |
zigai.compatibility |
Public deprecations and persisted-format migration guarantees |
zigai.telemetry |
OpenTelemetry-shaped hooks and metrics |
zigai.diagnostics |
Backend-neutral structured lifecycle diagnostics |
zigai.reflect |
Compile-time tools and JSON Schema derivation |
zigai.transport |
Pluggable buffered and line-streaming HTTP transport |
zigai.json |
Pre-allocation validation and boundary-specific JSON limits |
zigai.testing |
Deterministic scripted models for application tests |
Provider Client.model() values borrow their client. Keep the client and its
transport alive for every agent run that uses the model. The same rule applies
to model routers, MCP clients, callback contexts, dependencies, and toolsets.
Provider values likewise borrow their concrete provider state. Model and file
discovery results own arenas and must be released with deinit.
zigai.providers.http.Configured is the standard concrete boundary for an
HTTP-backed provider. Its API root, credential, configured headers, transport,
request policy, and optional profile callbacks are borrowed. provider()
returns another borrowed view; keep the Configured value at a stable address
until every model and in-flight request using it has finished. Model adapters
receive only relative endpoints and never receive the provider credential.
Provider wrappers can attach discovery and file callbacks through
Configured.Operations; those callbacks remain behind the same borrowed
provider interface.
Provider requests may attach a borrowed ResponseHeaderSink. The standard
transport invokes it while response-head slices are valid and retains no
headers afterward. Callbacks must copy any selected value into their own
bounded storage; Google uses this mechanism for its resumable upload URL.
OpenAI uses that split directly. Create a stable openai.Provider, then pass
its borrowed interface to the Responses API client:
var provider = zigai.providers.openai.Provider.init(api_key, transport);
var client = zigai.providers.openai.Client{
.model_name = "gpt-5-mini",
.provider = provider.provider(),
};
Use Provider.initWithOptions for a custom API root, configured headers,
provider-wide request policy, or model profile callbacks. The provider and
transport must outlive the client and every in-flight model request.
Anthropic follows the same construction pattern with
zigai.providers.anthropic.Provider. Its provider owns the x-api-key header;
the client retains max_tokens and the Messages API wire behavior.
Google uses zigai.providers.google.Provider; it owns x-goog-api-key and
keeps the stable gcp.gen_ai provider identity. Its client builds model-bound
GenerateContent endpoints and encodes Gemini request and response bodies.
Vertex AI deliberately reuses that Gemini client with a different provider:
const base_url = try zigai.providers.vertex_ai.regionalApiBase(
allocator,
"europe-west1",
);
defer allocator.free(base_url);
var provider = zigai.providers.vertex_ai.Provider.initWithOptions(
access_token,
"my-project",
"europe-west1",
transport,
.{ .base_url = base_url },
);
var client = zigai.providers.vertex_ai.Client{
.model_name = "gemini-2.5-flash",
.provider = provider.provider(),
};
The provider maps the codec's relative model operations onto the Vertex v1
projects/{project}/locations/{location}/publishers/{publisher}/models/{model}
resource. It accepts only generateContent and SSE streamGenerateContent,
validates every resource segment before HTTP, and uses the stable
gcp.vertex_ai identity. The default API root is Google's global Vertex
endpoint; regionalApiBase returns an owned regional root. The access token is
borrowed, so refresh it by creating a new provider after expiry.
Vertex and Google AI Studio share .google extension bodies because the
GenerateContent schema is the same. Credentials, resource paths, discovery,
and file operations remain provider-owned: Vertex does not expose the AI
Studio Files API or send x-goog-api-key.
Amazon Bedrock uses a native regional provider and a separate Converse wire adapter:
var provider = try zigai.providers.bedrock.Provider.init(
api_key,
"eu-west-1",
transport,
);
var client = zigai.providers.bedrock.Client{
.model_name = "eu.anthropic.claude-sonnet-4-6",
.provider = provider.provider(),
};
The provider borrows the API key and transport. Its generated regional URL is
bound only when provider() is called, so keep the concrete provider at a
stable address until the client and every request are finished. It authenticates
with the Bedrock bearer token, while the client owns model-path encoding and
Converse JSON.
Converse supports instructions, system and text history, function tools and
results, structured output on recognized model profiles, common inference
settings, tagged Bedrock extensions, service tiers, reasoning replay, normalized
finish reasons, and cache-aware usage. Unknown model families use a fail-closed
client profile unless the application supplies a profile lookup or override.
The adapter is buffered: ConverseStream uses binary AWS EventStream frames,
which require a chunk-stream transport rather than the line-stream interface.
bedrock.MantleProvider, bedrock.MantleClient, and bedrock.mantleApiBase
remain available for Bedrock's OpenAI-compatible Chat Completions endpoint.
xAI uses a first-class Responses client and keeps compatibility explicit:
var provider = zigai.providers.xai.Provider.init(api_key, transport);
var client = zigai.providers.xai.Client{
.model_name = "grok-4.6",
.provider = provider.provider(),
};
The native client accepts typed web and X search configuration, provider-side
code execution, collection search, and multiple remote MCP servers. Remote MCP
servers are keyed by server_label; other provider-managed tools are
singletons. Use xai.ChatProvider with xai.ChatClient only when an existing
Chat Completions integration requires that compatibility protocol. xAI-only
extension JSON uses the .xai tag and cannot cross into another adapter.
Mistral keeps Chat Completions and Conversations explicit. mistral.Provider
and mistral.Client remain compatibility aliases; the native path uses
ConversationsProvider and ConversationsClient:
var provider = zigai.providers.mistral.ConversationsProvider.init(
api_key,
transport,
);
var client = zigai.providers.mistral.ConversationsClient{
.model_name = "mistral-small-latest",
.provider = provider.provider(),
};
client.model() is stateless. It sends the complete provider-neutral history
with store: false, so retries, fallback, and ordinary agent calls never mutate
hidden remote state. Portable web search and code execution map to Mistral's
managed tools. Mistral-only premium search, image generation, document
libraries, and connectors are configured through client.managed_tools;
connector authorization remains request-scoped and borrowed. Native extension
JSON uses the .mistral tag.
Stored conversations are deliberate:
const first = try client.start(arena, request);
const session = try zigai.providers.mistral.Session.init(
provider.provider(),
first.conversation_id.?,
);
const next = try session.append(arena, append_request);
_ = next;
var history = try session.history(gpa);
defer history.deinit();
try session.delete(gpa);
Client.start stores the initial conversation. Session.append accepts only
new entries and completion settings; instructions and tool declarations belong
to the initial request. The session borrows its provider and ID. history
returns an arena-owned native entry list with a typed family and complete
structured ProviderDetails, including unknown beta entry types. Call
deinit when finished. Conversation IDs are validated before path assembly.
Cohere keeps its existing Compatibility API aliases. Native v2 Chat uses an explicit provider and client:
var provider = zigai.providers.cohere.ChatProvider.init(api_key, transport);
var client = zigai.providers.cohere.ChatClient{
.model_name = "command-a-03-2025",
.provider = provider.provider(),
};
The native adapter supports chronological system, user, assistant, and tool
messages; buffered and streamed text, thinking, tool plans, and parallel tool
calls; structured output; normalized usage; and structured citation and
log-probability details. Cohere-only fields such as strict_tools, documents,
citation options, safety mode, and priority use the isolated .cohere
extension body. Authentication stays on ChatProvider; compatibility clients
continue to use Provider and Client.
OpenRouter uses Chat Completions as its wire format and keeps router policy on its own client:
var provider = zigai.providers.openrouter.Provider.init(api_key, transport);
var client = zigai.providers.openrouter.Client{
.model_name = "anthropic/claude-sonnet-4.5",
.provider = provider.provider(),
.routing = .{
.order = &.{ "anthropic", "google-vertex" },
.allow_fallbacks = false,
.require_parameters = true,
.data_collection = .deny,
.zdr = true,
},
.include_router_metadata = true,
};
Routing also supports allowlists, denylists, quantization filters, price,
latency, and throughput preferences, plus simple or cross-model partitioned
sorting. Invalid names, conflicting allow/deny rules, empty percentile or
price objects, and non-finite or negative limits fail before HTTP. Raw
OpenRouter-only fields use the .openrouter extension tag and cannot replace
the typed provider object. Metadata opt-in follows OpenRouter's
X-OpenRouter-Metadata
contract; returned openrouter_metadata remains structured in
ModelResponse.provider_details for buffered and streaming calls.
OpenAI-compatible modules follow the same rule. Each named module exports a
matching Provider and Client built from shared compile-time defaults. The
provider owns the API root, provider identity, authentication style, headers,
request policy, transport, and profile callbacks. The client owns only the
model name, model settings, Chat Completions behavior, and optional
gateway-specific idempotency header. Use
openai_compatible.Provider.initWithOptions when the endpoint or identity is
selected at runtime. Azure OpenAI exposes one provider state with
ResponsesClient for its native GA v1 Responses endpoint and ChatClient for
Chat Completions; both use apiBase and Azure api-key authentication. The
Responses client deliberately shares OpenAI's Responses codec because Azure v1
publishes the same wire contract. Bedrock Mantle exposes mantleApiBase for
its deployment-specific Chat Completions root.
Named compatible providers resolve model capabilities in four layers:
- An application
ModelProfiles.lookupFn, when it recognizes the model. - The provider's built-in upstream-family profile.
- The client's fallback profile when neither lookup recognizes the model.
- An application
ModelProfiles.overrideFn, applied to the resolved result.
Named clients use a fail-closed fallback for unknown families. The generic
openai_compatible.Client retains its configurable compatibility presets
because an arbitrary endpoint has no provider identity to resolve against.
Ollama has a named compatibility provider with no credential. Its default
http://localhost:11434/v1 endpoint is accepted only through the exported
ollama.local_request_policy; callers must apply that same policy to the
standard HTTP transport. Tagged and namespaced library models resolve through
Ollama-specific family profiles. Unknown model families retain the fail-closed
compatible profile, so unsupported settings and tools are rejected before I/O.
Crusoe's named provider uses bearer authentication from the caller and the
Serverless Inference /v1 root. Its profile lookup recognizes Crusoe's
vendor-qualified Meta, DeepSeek, Qwen, Google, OpenAI Harmony, Moonshot, and
Z.AI families. Crusoe's guided decoding enables both structured-output modes;
unknown deployment aliases remain fail-closed unless the application supplies
an explicit profile.
Snowflake Cortex has an account-scoped Chat Completions root. Build it with
snowflake.apiBase, which accepts a bare account identifier or canonical
Snowflake account hostname and rejects schemes, paths, ports, and malformed
labels before authentication. Cortex profiles distinguish OpenAI, Claude, and
text-only families so ignored or rejected tool and structured-output fields do
not reach the network. Unknown models remain fail-closed.
snowflake.Client.reasoning accepts exactly one of effort (low, medium,
or high) and max_tokens. It is limited to Claude model IDs. When set, the
client supplies Cortex's required temperature 1, or rejects a conflicting
temperature before I/O. Buffered and streaming requests delegate to the shared
Chat Completions codec after this typed preparation. Snowflake-only raw fields
use ProviderExtraBody.snowflake; reasoning is reserved to the typed API.
Z.AI's named provider uses the general /api/paas/v4 Chat Completions root and
native glm-* model IDs. zai.Client adds typed thinking and
clear_thinking controls. It decodes buffered and streamed
reasoning_content into provider-owned thinking parts and replays that content
unchanged on later tool turns. Unsupported GLM families and lossy thinking
parts fail before transport. zai.CompatibilityClient exposes only the
portable Chat Completions surface; Z.AI fields do not enter the generic public
codec or extension tag.
The compiled custom-provider example demonstrates the runtime extension path.
It validates a public HTTPS API root, configures bearer authentication, assigns
a stable provider identity, and supplies an exact application profile lookup.
Its client explicitly selects profiles.unknown as the fallback. Applications
should add only capabilities their server contract guarantees; this keeps
unsupported settings, tools, and content out of transport callbacks.
Provider.listModels is implemented by OpenAI, Anthropic, Google, and
OpenAI-compatible provider objects. It authenticates through the provider
boundary and returns OwnedProviderModels; call deinit after consuming its
identifiers and raw metadata. Provider.Options.discovery_limits bounds pages
and total models for paginated APIs. Model adapters are not involved in
discovery.
ModelCatalog.init validates a borrowed slice of ModelCatalogEntry values.
Each entry scopes one canonical ID and its aliases to provider_name, and may
carry ModelCatalogLimits, ModelDeprecation, and a trusted ModelProfile.
Resolution is exact and case-sensitive; no allocation, prefix matching, or
implicit provider fallback occurs. Canonical IDs and aliases occupy the same
provider-local namespace, so ambiguous catalogs fail at initialization.
ModelCatalog.resolve(provider_name, requested_id) returns a borrowed
ResolvedModel. Use canonicalId() before constructing a provider client,
wasAlias() for diagnostics, and replacement() to follow a validated
deprecation target. The catalog and every referenced entry must outlive the
resolution. Live provider metadata remains separate and untrusted; merging it
with catalog records is an explicit API rather than a capability-profile
override.
mergeModelDiscovery returns OwnedDiscoveredCatalog. Each joined item keeps
the original ProviderModelDescriptor, optional catalog entry, and canonical
ID distinct. trustedProfile(), limits(), and deprecation() read only from
the catalog; a profile attached to live discovery is never promoted. Alias and
canonical duplicates in one provider response fail instead of being silently
collapsed. The owned result stores only the joined index, so call deinit and
keep both the provider discovery arena and catalog entries alive while using
it.
builtin_model_catalog exposes the generated compatibility snapshot alongside
builtin_model_catalog_version and builtin_model_catalog_updated_at. The
runtime module contains static entries and performs no parsing or allocation.
data/model_catalog.json is the reviewable source; zig build
update-model-catalog regenerates the Zig module, and zig build
check-model-catalog compares it byte-for-byte. The source is sorted and
validated before generation, including aliases, enum sets, HTTPS provenance,
limits, and the core catalog invariants.
Provider file descriptors always include provider_name. Their
uploadedFile() view is the handle accepted by inspectFile, downloadFile,
and deleteFile, and it can also be passed directly as provider-owned rich
content. The provider validates that handle before dispatch, so a foreign or
empty ID never reaches provider code. Upload, inspection, and download results
are validated again before they are returned.
uploadFile and inspectFile return OwnedProviderFile. downloadFile
returns OwnedProviderFileDownload, which owns both its descriptor and bytes.
Call deinit on either result after use. Providers that cannot safely download
content leave that operation unsupported; applications receive
UnsupportedProviderOperation instead of a synthetic URL or a second
unauthenticated fetch.
OpenAI implements upload, inspection, reuse, download, and deletion. It uses
user_data when ProviderFileInput.purpose is null, while the API permits
content download only for supported purposes such as fine-tuning inputs.
Anthropic implements the same endpoints, but files uploaded by a caller are
marked downloadable: false; only files generated by a tool can be downloaded.
OwnedProviderFile.value.downloadable exposes that provider result so callers
can choose safely. Anthropic rejects an upload purpose, always sends the Files
API beta header, and both providers percent-encode file IDs before using them
in paths. ProviderFileLimits bounds upload bytes and multipart metadata before
transport I/O and is configurable through provider options.
Google implements resumable upload, inspection, reuse, and deletion. Its
returned handle is the authenticated file URI required by Gemini requests.
Upload session URLs are accepted only from the configured upload origin, and
the default Google upload limit matches the documented 2 GB file maximum.
downloadFile intentionally remains unsupported because the Gemini Files API
does not permit clients to download stored files.
agent_spec.Owned owns parsed configuration in an arena. It is data-only and
does not read secrets or construct clients. validateResolution uses
application-supplied provider and capability catalogs without building a
model. agent_spec.Resolved owns copied configuration and the model context
allocated through its resolver; its optional cleanup callback runs before the
arena is released. Capability implementations remain borrowed. See
Agent specifications for the complete contract.
ModelSettings also borrows stop strings, tool names, request headers, and
provider-extension JSON. Those values need to live only until the model request
returns; results and reusable history do not retain them. An empty non-null
slice is an explicit override, while null inherits the lower-precedence value.
Typed graphs¶
graph.Graph(State, Deps, Input, Value, Output) creates a workflow type without
erasing application values. Its Builder registers one typed start callback,
one typed end callback, named steps and decisions, and an entry node. A step has
exactly one unconditional route. A decision returns
DecisionResult { branch, value } and has one or more named routes registered
with branch or branchFinish. Duplicate names, invalid node kinds or IDs,
incomplete routing, unreachable nodes, and definition limits fail at build
time.
Graph.run executes to completion. Graph.iter executes the start adapter and
returns a Run; Run.next then advances one step and returns either the next
node or the final typed output. A terminal error is latched, so later calls
return the same failure instead of continuing a desynchronized workflow.
RunOptions.max_steps may tighten, but never widen, the definition ceiling.
Returning a branch name that was not registered latches UnmatchedRoute.
Builder.addFanOut registers an explicit parallel node. FanOutMode.broadcast
sends the current Value to every borrowed ParallelBranch.
FanOutMode.map invokes a Map callback with a bounded Emitter, then sends
each emitted value to every branch in item-major, branch-major source order.
The embedded typed Join initializes one accumulator even for an empty map and
borrows every successful branch result through reduce_fn.
RunOptions.max_concurrency defaults to one and is capped by
Limits.max_concurrency. A value above one requires RunOptions.io only when a
fan-out contains multiple tasks. The scheduler observes completions as they
arrive, cancels sibling tasks on the first failure, and retains results by
source index so reducer order is deterministic. max_fan_out_items,
max_fan_out_tasks, and max_parallel_branches bound every fork before more
work is admitted.
The built graph owns only its node and routing arrays and releases them with
deinit(gpa). Node names, registered branch names, callback contexts, and
definition metadata are borrowed for the graph's lifetime. A branch name returned by a decision is
borrowed only for that synchronous callback, route lookup, and event delivery.
State and dependencies are borrowed for the run's lifetime.
Intermediate and output ownership follows the application-defined Value and
Output types; callbacks receive the run allocator explicitly and must define
their own cleanup contract for allocations they return.
A fan-out borrows its input, owns values emitted by a map and values returned
by branches, and transfers the final accumulator back to the graph. When
FanOut.deinit_value_fn is set, the scheduler calls it for emitted values and
branch outputs after reduction, and for an accumulator abandoned by failure.
When this hook is used, branch outputs must own independent resources: they
must not alias the fan-out input, emitted values, or one another. The
application remains responsible for cleaning the successful accumulator after
a later step or end callback consumes it.
Event sinks are synchronous and infallible. They receive borrowed run/step start, end, and failure records in execution order. They must copy a node name or branch name before retaining it. The core supports linear, cyclic, named conditional, and bounded parallel routing.
Graph agent nodes¶
graph_agent.BufferedNode(Workflow) and
graph_agent.TypedNode(Workflow, AgentOutput) turn an Agent invocation into
an ordinary typed Workflow.Step. Each adapter borrows its agent, callback
context, observer, name, and metadata for the built graph's lifetime.
prepare_fn receives a node-scoped scratch arena, graph context, and borrowed
input. It returns a prompt plus Agent.RunOptions; when dependencies is
null, the adapter injects the graph's typed dependency pointer. apply_fn
receives the completed agent result and must copy anything retained in the
returned Value before the callback returns.
Set stream_sink to select the streaming agent API. It receives each borrowed
AgentStreamEvent synchronously in provider and tool-loop order. After a
successful callback, the node observer receives the matching borrowed stream
event. A callback failure emits the stream failure phase and aborts before
apply_fn, so the graph never commits a transition for a partially delivered
run.
Control.cancellation overrides the borrowed agent token for one node.
Control.timeout_ms can only tighten the agent and prepared run timeout. A
borrowed graph RunOptions.io becomes the agent runtime when Agent.io is
null. Cancellation and deadline work is drained by the agent before the node
returns.
Correlation supplies a stable graph run ID and optional trace parent. The
adapter derives the node ID, node name, provider request ID, and
Agent.RunCorrelation. Hooks and OpenTelemetry therefore share the graph
identity. A durable binding is namespaced with
graph.<node-id>.step.<step-number> beneath any existing
Binding.step_namespace; replay keeps the same identity, while two
agent nodes cannot collide. A configured correlation run ID must equal the
durable binding run ID.
The adapter's synchronous observer receives borrowed start, stream, end,
and failure events. Failure events preserve the original error name and
identify preparation, agent execution, stream delivery, or application. The
graph-facing error remains deliberately narrow: allocator failures stay
OutOfMemory, agent cancellation stays Cancelled, and other agent failures
become StepFailed.
graph_agent.Conversation is the reusable state helper for sequential agent
nodes. init and replace deep-copy the complete canonical message vocabulary
and usage detail names into one arena. appendRun replaces history while
adding request, tool, token, latency, detail, and cost counters to cumulative
usage. Its storage must be deinitialized by the application state owner.
Agent adapters produce ordinary graph steps, not fan-out branch callbacks.
They therefore do not execute concurrently unless separate graph runs invoke
them concurrently. In that case, the agent, callbacks, dependencies, stream
sink, and telemetry exporter must be immutable or thread-safe. A shared
Conversation still requires application synchronization.
Concurrent fan-out callbacks share State, Deps, branch contexts, and input
values. The scheduler makes only Context.gpa safe for concurrent use.
Applications must synchronize all other shared mutation, use immutable data,
or keep max_concurrency = 1.
Multi-agent scopes¶
multi_agent.Session.init creates one bounded, arena-owned execution tree with
a stable run ID. It owns cumulative usage and counts every started invocation,
including failures. Successful results contribute complete RunUsage. Failed
runs contribute observed request attempts, successful response usage, and tool
validation starts before their terminal error. Use recordUsage to include
parent work that you deliberately executed outside a session scope.
multi_agent.Scope.root creates the application scope. delegate and
subagent run at the next depth and return control to their caller. handoff
runs another programmatic turn at the same depth. Their Typed variants retain
Agent.TypedResult(Output) without erasing the application type. child
creates the scope occupied by a completed nested run. withHistory continues a
same-depth handoff with replacement canonical history.
CallOptions.context = .shared passes the scope's dependency pointer and
history. .isolated uses an explicit dependency pointer and history boundary.
Scope.fromToolContext copies an active contextual tool's dependencies,
history, cancellation token, std.Io, and absolute deadline. This is the
explicit bridge for delegation from an agent tool.
The scope copies the target agent for one invocation. It fills a missing agent
runtime, propagates the tree cancellation token, tightens RunOptions.timeout_ms
to the remaining absolute deadline, and preserves an explicitly supplied
shorter timeout. It also passes the scope trace parent and emits an
Agent.RunCorrelation containing the session ID, transition kind, depth, run
index, and target name. Agent hooks and OpenTelemetry can reconstruct the
complete tree.
Limits.max_depth bounds nested delegation and subagents; handoffs do not
increase depth. max_runs counts successful and failed starts. Optional
request, tool, token, and exact nano-USD ceilings apply to cumulative usage.
The session is intentionally single-threaded. Use one session per concurrent
execution tree so ordering, budgets, and correlation IDs remain deterministic.
All returned agent results keep their normal independent deinit boundary.
Embeddings¶
embeddings.Model is a small provider vtable with stable provider/model names,
a maximum batch size, an optional dimension ceiling, one batch callback, and an
optional retry classifier. Provider callbacks return an arena-owned
BatchResult. They must return one vector per input in source order.
embeddings.Embedder validates all inputs before the first callback. Limits
bounds input count, bytes per input, total bytes, dimensions, and batch count.
Options.max_batch_size can only tighten the model batch size.
Options.dimensions must fit both model and embedder ceilings. Empty inputs,
non-finite values, count drift, and inconsistent dimensions fail explicitly.
Use embedQuery for one search query and embedDocuments for indexable text.
embed accepts an explicit InputType. All forms preserve source order and
return one arena-owned Result containing copied inputs and vectors, one exact
dimension, model identity, and aggregate RunUsage. vectorFor performs an
exact first-input lookup. Call deinit when the result is no longer needed.
Each batch shares one RunControl, so retries and later batches consume the
same absolute deadline. Provider callbacks receive the remaining timeout and
tree cancellation token. Retry classification defaults to connection, rate
limit, server, and request-timeout failures. RetryPolicy bounds attempts and
cumulative delay, applies full jitter, and exposes a fallible pre-retry hook.
A positive retry delay requires Options.io. Losing controlled callbacks
allocate in a disposable attempt arena and are drained before it is released.
embeddings.openai.Client encodes OpenAI's /embeddings API through any
configured Provider, including compatible providers. It sends float encoding,
optional dimensions, and source inputs in one batch. Decoding restores provider
index order, rejects missing or duplicate indexes, and maps prompt-token usage.
embeddings.google.Client encodes Gemini batchEmbedContents. It distinguishes
RETRIEVAL_QUERY from RETRIEVAL_DOCUMENT, supports document titles and
output dimensions, and aggregates Vertex-style per-vector token statistics.
Both adapters reuse provider authentication, request policy, cancellation,
timeouts, safe error observation, and stable status classification.
cosineSimilarity validates equal finite nonzero vectors before ranking.
examples/retrieval.zig uses a deterministic local model to show the complete
index, query, similarity, and best-document flow without credentials or network
access.
Realtime sessions¶
realtime.Connector opens a provider Connection. The connection declares
whether it uses a WebSocket or WebRTC sideband, its provider/model identity, its
audio rates and feature Profile, and callbacks for sending normalized input,
receiving owned codec events, closing, and classifying transport failures.
Provider protocol framing stays below this boundary.
Session.init connects immediately and deep-copies optional canonical history.
Call sendAudio with raw little-endian mono PCM16, sendText with a complete
turn, or sendImage with bounded in-memory image content. Manual
commitAudio, clearAudio, and createResponse calls require the profile flag.
interrupt sends cancellation or output truncation according to the model
profile. Unsupported operations fail before a frame is sent.
next returns one independently arena-owned event. Audio chunks, input/output
transcripts, tool completions, response interruption, turn completion,
recoverable errors, and reconnects have explicit variants. Usage events update
session state and do not recurse through the consumer stream. A configured tool
handler receives bounded JSON arguments under the session cancellation/deadline
control; its bounded result is returned to the provider and recorded beside the
call in canonical history.
AudioRetention controls whether raw input/output PCM is copied into finalized
SpeechPart history. Transcripts are always retained. allMessages returns an
independent deep copy suitable for a text-agent handoff. Limits bound text,
chunks, images, history, retained audio, tool payloads, and cumulative tokens.
A reconnect policy bounds per-drop attempts and total reconnects; classified
transport failures close and redial before a reconnect event reports whether
the provider restored state.
realtime.wire.Channel is the raw frame boundary. Applications or optional
transport packages provide authenticated WebSocket/WebRTC dialing and complete
text/binary/close frames. realtime.openai translates the shared OpenAI
Realtime protocol; its dialect selects OpenAI, Azure OpenAI, or xAI profiles.
realtime.azure.init and realtime.xai.init are explicit constructors for
those dialects. realtime.google translates Gemini Live setup, client content,
realtime media, transcriptions, tools, usage, interruption, and turn terminals.
Credentials, HTTP upgrades, SDP exchange, ICE, and media tracks remain owned by
the raw dialer rather than leaking into session state.
Set Options.observer for synchronous connected, input, public-event, and usage
observations. realtime.telemetry.Config.start adapts that stream to correlated
OpenTelemetry events and token metrics. Keep its Run alive through the
session, then call finish to export one parented realtime invoke_agent span.
Exporter failures follow the configured fail-open policy.
Browser UI protocols¶
ui.Bridge translates an AgentStreamSink into borrowed run, text, reasoning,
tool, approval, result, error, and typed custom events. It validates thread,
run, part, tool, approval, and custom identities before delivery. Deferred tool
requests become explicit approval requests; parsed browser decisions convert to
Agent.ResumeDecision for the existing pause/resume API.
ui.ag_ui.encode emits official AG-UI event objects. Approval requests use a
RUN_FINISHED interrupt outcome, and parseApproval consumes the matching
RunAgentInput.resume entry. ui.vercel.encode emits AI SDK UI message stream
v1 SSE records and exposes the required response header constants. It supports
text/reasoning blocks, complete tool input, tool results/errors, standard tool
approval request/response chunks, data-* custom values, and the [DONE]
terminal marker.
customValue(T, ...) serializes a typed Zig value under explicit JSON and name
limits. sanitizeMessages accepts bounded user text and optionally assistant
history, ignores browser IDs, rejects system/tool/activity/reasoning roles, and
returns arena-owned canonical messages. Client-controlled history is therefore
never mistaken for instructions or tool authorization.
ReplayBuffer owns a bounded source-ordered window of encoded protocol records.
Every append gets a monotonic sequence; replayAfter redelivers only newer
records after browser reconnect. Oldest records are evicted by event and byte
limits. examples/ui_server.zig emits a complete Vercel SSE stream, while
examples/ui_browser.ts reads chunks and posts bidirectional tool approvals.
Graph snapshots¶
Graph snapshots are opt-in. Set Builder.definition_id to a stable borrowed
identity before build; an absent identity keeps Run.snapshot and
Graph.resumeSnapshot disabled. The identity, node kinds and names, fan-out
mode and branch names, routes, typed graph parameters, entry node, and runtime
ceilings produce one SHA-256 definition fingerprint. Names, branch slices, and
the definition identity must remain immutable for the built graph's lifetime.
Change the identity when callback behavior or types change incompatibly.
Run.snapshot(gpa, codec) is valid only while a run is settled and running:
after iter returns or between completed next calls. It calls the
application's SnapshotCodec to produce complete JSON documents for State
and the current Value, then returns one gpa-owned versioned envelope. It
does not serialize Deps, callback contexts, event sinks, std.Io, or
in-flight fan-out work. A next call drains every parallel task before it
returns, so snapshots never contain an ambiguous partial fork.
The version 1 envelope contains definition_sha256, payload_version, the
next node's index and name, completed step count, original run step ceiling,
and encoded state/value documents. Parsing rejects unknown or duplicate fields,
malformed JSON, mismatched node identity, definition drift, future envelope or
payload versions, and configured byte/depth/collection limits before invoking
decoders. Snapshot limits are controlled by Limits.max_snapshot_bytes,
max_snapshot_payload_bytes, max_snapshot_depth, and
max_snapshot_collection_items.
Graph.resumeSnapshot(gpa, state_out, deps, source, codec, options) decodes
state and value atomically, injects fresh dependencies and run-only options,
and returns a Run positioned at the saved next node without invoking Start
or replaying completed nodes. state_out must point to uninitialized storage;
it remains untouched on failure and owns the decoded state on success. A
resume emits run_resume. RunOptions.max_steps may narrow the stored total
ceiling only when it does not fall below the completed step count.
Each codec has a nonzero payload version. To restore an older payload, provide
SnapshotMigration.run_fn; it receives both old JSON documents and returns one
gpa-owned JSON object with exactly state_json and value_json string fields
for the codec's current version. The graph validates the migrated documents
before decoding them. If decoded state can own resources, set
deinit_state_fn so a later value-decoding failure can release it.
Graph visualization metadata¶
NodeMetadata attaches optional borrowed label, description, group, and
SourceLocation values to start, step, decision, fan-out, and end definitions.
EdgeMetadata carries a label, description, and source location. Existing
connect, finish, branch, and branchFinish calls keep empty metadata;
their *WithMetadata counterparts register it explicitly. Empty or oversized
values fail during definition assembly according to the graph Limits.
Graph.visualization(gpa) returns an owned Visualization with schema
visualization_format_version, the definition fingerprint, synthetic start
and end nodes, registered nodes in source order, and deterministic edges. Node
IDs use the tagged VisualizationNodeId; decision branch identities remain
separate from presentation labels. Only the node and edge arrays are owned by
the result. Every string is borrowed from the graph and the view must be
deinitialized before the graph or its metadata storage is released.
Graph.renderMermaid(gpa, options) returns a gpa-owned
stateDiagram-v2 document capped by Limits.max_visualization_bytes.
MermaidOptions controls an optional title, MermaidDirection, and edge-label
visibility. Nodes use definition-order IDs, groups use first-appearance IDs,
decisions and fan-outs retain choice/fork shapes, and descriptions plus source
locations render as notes. Edge labels prefer explicit metadata and otherwise
use the decision branch identity. Newlines, quotes, markup characters, and
control bytes are escaped before borrowed text reaches Mermaid syntax.
Evaluations¶
evals.Dataset.run executes each case once and propagates task, evaluator,
hook, and allocation errors. runWithOptions additionally accepts
evals.ExecutionOptions: repetitions, independent task_retry and
evaluator_retry policies, bounded max_concurrency with an optional io
runtime, and borrowed lifecycle hooks. Zero repetitions, concurrency, or retry
attempts return evals.Error.InvalidExecutionOptions before a model is called.
More than one effective concurrent run without io returns
ConcurrentExecutionRequiresIo.
RetryPolicy.shouldRetryFn classifies the error from the completed attempt.
When it returns true, beforeRetryFn runs before the next attempt and may
implement sleeping, rate-limit coordination, or test-controlled waiting.
CaseResult records the source case_index, one-based repetition, total
repetitions, and task_attempts; each EvaluationResult records its own
attempt count. Report order is source case first, then repetition.
With max_concurrency > 1, the Model implementation, evaluators, retry
callbacks, and lifecycle hooks must be thread-safe. ZigAI locks access to the
caller allocator and report arena; application callback state remains outside
that boundary. All admitted work is joined before success or failure returns,
and the lowest source-order task/evaluator failure is propagated.
Dataset.report_evaluators run once each after all cases have joined. A
ReportEvaluator receives a borrowed ReportView and returns an Analysis
with optional assertion, scalar value, unit, and reason. Returned strings are
copied into Report.analyses. A false analysis assertion participates in
Report.passed(); a non-finite value returns InvalidReportAnalysis.
Report.summary() returns total and passed run counts plus a null-on-empty pass
rate. caseSummary(case_index) groups repetitions by their stable source index.
scoreStatistics(evaluator_name) ignores absent and non-finite scores and
returns count, minimum, maximum, mean, and population standard deviation.
Dataset.trace_evaluators are TraceEvaluator callbacks over a
TraceContext. They require Agent.telemetry; otherwise the run returns
TraceEvaluationRequiresTelemetry before calling the model. The per-case
CaseResult.spans slice owns a deep copy of every span exported during that
agent run, including typed attributes. The configured exporter still receives
the original spans and all metrics. Trace results follow ordinary evaluator
results and use ExecutionOptions.evaluator_retry.
eval_io.stringifyDatasetJson and stringifyDatasetYaml encode version-1
dataset documents. Files contain cases, metadata, and ordered evaluator names.
parseDatasetJson and parseDatasetYaml require an EvaluatorRegistry and
return an arena-owned runnable dataset. A missing name returns
UnknownEvaluator; more than one registry entry with that name returns
AmbiguousEvaluator. Names must also be unique across ordinary, trace, and
report evaluator categories.
Dataset serialization accepts only default per-case RunOptions. Those options
may contain executable callbacks, pointers, queues, or credentials, so a
non-default value returns UnsupportedCaseOptions instead of silently dropping
state.
eval_io.stringifyReportJson, stringifyReportYaml, parseReportJson, and
parseReportYaml round-trip complete reports. The version-1 shape includes
case/repetition identities, attempts, output, usage, evaluations, aggregate
analyses, and typed OpenTelemetry spans. Trace and span IDs are lowercase hex.
Parsers are strict, bounded by the CLI-config JSON limits, reject duplicate YAML
keys, and return arena-owned values that must be deinitialized.
eval_compare.compareReports matches case runs by stable (case_index,
repetition) identity and evaluator or analysis entries by name. Baseline order
is retained, followed by candidate-only entries in candidate order. A reused
identity with a different case name fails with CaseIdentityMismatch; duplicate
identities or evaluator names are rejected before comparison.
Each comparison classifies entries as unchanged, improved, regressed,
added, or removed. Removed assertions and newly failing entries count as
regressions; newly passing entries count as improvements. The report also
contains pass-rate and finite score/value deltas plus signed token, request,
tool, latency, and cost deltas. regressionFree() is true only when every
regression category is zero.
eval_compare.stringifyCiJson emits stable indented version-1 JSON without
timestamps or environment-specific fields. Its top-level conclusion is
pass exactly when regressionFree() is true, so callers can persist the JSON
as an artifact and map the same predicate to their process exit status.
Ownership¶
ZigAI follows one rule for high-level operations: a returned type with a
deinit method owns its complete result graph.
| Value | Ownership rule |
|---|---|
Agent.Result |
Owns output and message history until deinit |
TypedResult(T) |
Owns the decoded value, JSON, and history until deinit |
RunOutcome / PausedRun |
Owns completed or serialized paused state until deinit |
OwnedResumeDecisions |
Owns parsed decisions until deinit |
eval_io.OwnedDataset |
Owns a parsed dataset graph; registry callbacks remain borrowed |
Parsed evals.Report |
Owns the complete deserialized report until deinit |
eval_compare.Report |
Owns the complete comparison graph until deinit |
history.Owned |
Owns parsed history until deinit |
codecs.pydantic_ai.Owned |
Owns the complete PydanticAI JSON value graph until deinit |
capability.LoadResolution |
Owns its dependency plan and diagnostic until deinit |
evals.Report |
Owns every case and evaluation result until deinit |
transport.Response |
Caller frees body with the allocator passed to send |
ResponseMessage.usage is RequestUsage. Agent.Result, PausedRun,
TypedResult, and evals.Report expose RunUsage. Cached and modality fields
are inclusive subsets of the input/output totals, so totalTokens() is always
input_tokens + output_tokens. Result arenas own usage detail names and price
table version strings. reasoning_tokens normalizes OpenAI reasoning tokens,
Anthropic thinking tokens, and Google thought tokens for both buffered and
streamed responses.
UsageCost stores nano-USD exactly. PriceTable.estimate returns null for an
unknown model or any non-empty bucket without a rate. pricing.builtin is an
offline snapshot generated from the pinned pydantic/genai-prices v2 data. Its
version, source commit, checksum, and coverage counts are public constants.
Applications may provide a different immutable table through
Agent.price_table.
transport.HttpTransport.init uses bounded decompressed response defaults:
16 MiB per buffered body and 1 MiB per streaming line. Pass a
transport.Limits value to HttpTransport.initWithLimits to change them.
The limit excludes the streaming newline. A body or line exactly at its limit
is accepted; the next byte returns error.ResponseTooLarge or
error.StreamLineTooLarge.
Inputs, callback events, stream events, lifecycle events, and provider error
observer values are borrowed unless their documentation says otherwise. Copy
data inside the callback if it must outlive the call. Functions such as
history.stringify, codecs.pydantic_ai.stringify,
stringifyResumeDecisions, and provider request encoders return a slice owned
by the caller's allocator.
Direct Model.request, Model.stream, and provider decoder calls build nested
response data with the supplied allocator. Use an arena and release the arena
as one unit. Normal Agent calls already provide this ownership boundary.
Agent.diagnostics borrows its sink and configured sensitive values. The sink
receives a sanitized event whose strings live only for the callback. Content
capture is opt-in; configured sensitive values are replaced before the value
limit is applied, so truncation cannot expose a secret prefix. max_attributes,
max_key_bytes, and max_value_bytes bound every event before delivery.
Output strategies¶
Agent.output is an OutputSpec, separate from the provider wire
OutputFormat. The concise .json_schema form accepts one named schema;
.native accepts named alternatives and combines them with anyOf;
.prompted accepts the same alternatives plus an optional template containing
one {schema} marker. Prompted output requires system instructions, selects
JSON-object mode when the profile supports it, and otherwise uses text mode.
Its result is always validated locally.
Specification slices are borrowed for the run. Combined schemas and rendered instructions live in the run arena and follow the result ownership boundary.
.tool exposes each choice as a separate function-tool definition and requires
tool support. Non-object schemas receive a provider-facing { "value": ... }
wrapper that the agent removes after local validation. Result.output_name
identifies the chosen branch. An OutputFunction receives borrowed
OutputRunContext state and validated JSON; returned slices must be static or
allocated with its supplied run-arena allocator. Its explicit .retry result
is safe to send to the model, while thrown errors stop the run.
Agent.output_validators runs ordered OutputValidator callbacks after schema
validation and after an output function. Each callback receives the selected
choice name, borrowed OutputRunContext, and the previous callback's output.
It returns .output to accept or transform the value, or .retry with a safe
model-visible correction message. Transformed structured output is checked
against its schema again. A thrown callback error aborts the run. Capabilities
can contribute validators through Capability.output_validators; agent-level
validators run first.
Agent.end_strategy defaults to .graceful: ordinary calls before the first
successful output run, later calls are closed as skipped. .early evaluates
output calls first and skips ordinary calls after one succeeds. .exhaustive
runs every emitted call and keeps the first successful output by emission
order. Deferred approval or external calls still pause before finalization.
Local JSON Schema dialect¶
Every hand-written output schema is checked before the first model request.
ZigAI implements a fail-closed subset of JSON Schema Draft 2020-12: unknown
validation keywords and non-local references are rejected instead of ignored.
zigai.json_schema.supported_dialect contains the accepted $schema URI, and
validateSchema can preflight a schema independently of an agent run.
The supported assertions are:
type,enum,const,allOf,anyOf,oneOf,not, andif/then/else;$defswith#or single-segment#/$defs/...references, including JSON Pointer~0and~1escapes;properties,required,additionalProperties,propertyNames,dependentRequired, and minimum/maximum property counts;items,prefixItems,contains, minimum/maximum contains and item counts, anduniqueItems;- Unicode-codepoint string lengths; and
- inclusive/exclusive numeric bounds and
multipleOf.
Standard descriptive annotations such as title, description, default,
examples, format, and read/write/deprecation flags are accepted but do not
change validation. Regex, remote or general JSON Pointer references, dynamic
references, and unevaluated-value keywords are currently unsupported.
Malformed supported vocabulary returns InvalidJsonSchema; unsupported valid
vocabulary returns UnsupportedJsonSchema; a valid output that misses an
assertion returns OutputSchemaValidationFailed.
Tool.origin is ToolOrigin.application by default. Tools prepared by an MCP
client use ToolOrigin.mcp; deterministic orchestration tools may explicitly
use ToolOrigin.workflow. Providers see all three as ordinary function tools.
Durable agents route application and MCP tools through tool_call and
mcp_request workers, while workflow tools execute inline during replay and
must not perform external side effects. ZigAI's internal load_capability tool
uses the workflow origin.
Streaming events¶
ModelStreamEvent represents provider response parts with part_start,
part_delta, and part_end; every event for a part uses the same index.
ResponsePartDelta covers text, thinking, function and native tools, native
tool returns, media, speech, and compaction. Usage is reported separately.
AgentStreamEvent wraps model events and adds function-tool call/result,
tool-availability, deferred request/result, enqueued-message,
partial_output, and final_result events. A partial_output is the complete
output accumulated so far, not the latest delta. The corresponding raw model
event is delivered first. Text snapshots are accumulated directly. Structured
snapshots are repaired into valid JSON, then checked against present types,
properties/items, forbidden extras, and maximum bounds. Assertions that later
bytes can satisfy or change, such as required, minimum bounds, const, and
numeric limits, are deferred to final validation.
Wrap a sink with CheckpointedStreamSink to resume one deterministic stream
segment after a worker restart. Committed event ordinals are skipped on replay.
Use a new checkpoint ID after an approval resume or whenever event ordering
changes. Delivery is at least once across the small sink-success/store-write
window; use durable.deliverEvent for deduplicated business side effects.
Output functions and validators receive every useful partial snapshot with
OutputRunContext.partial_output = true; .retry suppresses that snapshot and
a thrown error stops the stream. They run again on the final candidate with
partial_output = false. Structured transformations are partially checked
again before delivery. Only final_result is accepted output and receives full
validation. JSON object and schema events attach a std.json.Value snapshot.
All values are borrowed for the callback; copy them before returning if they
must be retained.
runUntilPauseStream and resumeRunStream preserve the event flow across
approval and external-tool boundaries. Providers restart part indexes for each
model response in a multi-request agent run.
PendingMessageQueue is an allocator-owned, thread-safe, one-run FIFO. It
deep-copies RequestMessage batches and requires a std.Io runtime for its
mutex. Attach it through RunOptions.pending_messages; keep it alive until the
run returns, then call deinit. Accepted batches enter history only at safe
boundaries: before a model request, after tool results, or after a provisional
final response. Each insertion emits enqueued_messages for streaming runs.
If a run pauses, accepted messages are stored separately in the serialized
pause state and inserted immediately after deferred tool results on resume.
Cancellation or another terminal failure closes the queue and discards batches
that did not enter history. Finalization checks and closes the queue under the
same lock, so an enqueue is either accepted for another model step or rejected
with PendingMessageQueueClosed; it cannot disappear behind a final result.
Errors¶
The public named error categories are:
Agent.Error(alsozigai.AgentError) for agent validation, limits, and lifecycle failures;durable.Errorfor invalid durable records, missing worker registrations, runtime mismatches, and persisted failure or suspension outcomes;zigai.ProviderRequestErrorfor normalized rate-limit, server, and other non-success provider responses;providers.<name>.Errorfor provider encoding and decoding failures plus the normalized provider request errors;history.Error,codecs.pydantic_ai.Error,json_schema.Error,evals.Error,mcp.Error, andtransport.Errorfor their subsystem-defined failures.
transport.Error.ResponseTooLarge and transport.Error.StreamLineTooLarge
are stable policy failures. They apply to decompressed bytes, so compression
cannot bypass the configured allocation boundary.
Security policy¶
zigai.UrlPolicy is HTTPS-only and public-network-only by default. Agents apply
it to provider endpoints and rich-content URLs. MCP Streamable HTTP and the
standard HTTP transport enforce it independently, so direct and custom-model
use keeps the same boundary.
HttpTransport.initWithOptions configures URL, redirect, and response-size
policy together. Redirects are never followed; the explicit choices are reject
or return the 3xx response. See Security for local development,
allowlists, DNS limitations, credential redaction, and trust boundaries.
JSON limits¶
zigai.json.validate scans one complete document without retaining a value
graph. It checks the encoded document size, decoded string or encoded number
size, container depth, and the number of fields or elements in each collection.
Its stable ValidationError distinguishes each limit from malformed JSON.
json.parse and json.parseLeaky combine that preflight with owned or
arena-owned decoding and map invalid input to a caller-selected stable error.
json.validateAs provides the same mapping without building a value graph,
while json.isValid is useful for boolean validation such as evals.
ZigAI applies these reviewed defaults before its own decoders allocate:
| Boundary | Document | Value | Depth | Items per collection |
|---|---|---|---|---|
| History | 16 MiB | 1 MiB | 64 | 65,536 |
| Paused state | 32 MiB | 16 MiB | 64 | 65,536 |
| Resume decisions | 4 MiB | 1 MiB | 32 | 16,384 |
| Provider response | 16 MiB | 4 MiB | 128 | 65,536 |
| Tool payload | 1 MiB | 1 MiB | 64 | 4,096 |
| MCP message | 4 MiB | 1 MiB | 64 | 16,384 |
| JSON Schema | 2 MiB | 512 KiB | 64 | 16,384 |
| CLI manifest | 1 MiB | 256 KiB | 32 | 4,096 |
The named profiles live under zigai.json.defaults. Applications can use a
custom json.Limits value when validating their own untrusted JSON entry
points.
Every Tool execution and validation entry point requires arguments_json to
contain one complete JSON document within the tool-payload profile. Invalid or
oversized arguments return error.InvalidToolArguments before the callback is
invoked.
Agent harness¶
harness.Harness.init borrows any Agent and applies a Config for one or
more reusable runs. Preset.coder adds evidence-driven code-change guidance;
Preset.researcher adds evidence/source guidance; Preset.custom adds no
implicit instruction. Explicit Instruction values and enabled
CapabilityConfig entries compose after the borrowed agent's own values.
Disabled capability entries are omitted before agent validation.
Harness limits tighten model requests, tool calls, total tokens, output bytes,
artifact count, artifact bytes, and one absolute timeout. The same
RunControl bounds both the agent invocation and post-run artifact producers,
so cancellation or timeout drains a losing producer before returning.
ArtifactProducer receives the completed borrowed Agent.Result and a
run-scoped ArtifactCollector. add validates names/media types and copies
bytes into the result artifact arena before observation. harness.Result owns
that arena and the normal Agent.Result; one deinit closes both boundaries.
Fallible start, artifact, end, and failure observations are synchronous.
Harness.fromResolved borrows the runnable agent inside an
agent_spec.Resolved. The resolution must outlive the harness and every run.
Provider clients, secrets, and capability implementation lifetimes remain
owned by the existing spec resolver rather than the harness.
Execution environments¶
execution.Environment is a small vtable for bounded read, write, remove,
command, and dispose operations. Its Profile declares filesystem, shell,
network-isolation, and disposable guarantees. RemoteSandbox pairs any
application implementation with a stable sandbox ID; the remote backend must
enforce the same request policy before returning owned results.
LocalWorkspace borrows an explicit std.Io.Dir root. It rejects absolute,
empty, NUL, empty-component, and parent-traversal paths. Reads disable symlink
following and request resolve-beneath behavior. Writes request resolve-beneath
creation. A disposable local workspace creates a child directory, owns its
handle, and removes the tree when dispose runs.
FilesystemPolicy controls mutability and file bytes. CommandPolicy
allowlists executables, fixes required network access, limits arguments,
environment count, and stdout/stderr, and controls sensitive environment use.
Local execution refuses network = denied because the standard process API
cannot prove network isolation; use a remote sandbox whose profile guarantees
it. This fails closed instead of claiming an unenforced sandbox.
Command cancellation and timeout use RunControl; a losing process is killed
and drained before return. CommandResult owns stdout/stderr in one arena.
Sensitive environment values are replaced with [REDACTED] in both streams by
default. Audit events contain paths, executables, statuses, error names, and
sensitive variable names only - never secret values.
Built-in capability catalog¶
Keep a builtin_capabilities.Catalog at a stable address, configure optional
backends, then pass catalog.resolver() to agent_spec.resolve or request
individual capability values by ID. The catalog exposes web_search,
web_fetch, browser, image_generation, skills, and
repository_context. Missing optional backends make their capability ID
unavailable instead of silently degrading behavior.
Web search and fetch use the existing provider-neutral BuiltinTool vocabulary.
Browser, image, skill, and repository features use ordinary local Tool
callbacks. BrowserBackend, ImageBackend, SkillStore, and
RepositorySearch are structural interfaces, so optional vendor packages can
implement them without becoming ZigAI dependencies.
Browser URLs pass the configured UrlPolicy before a callback. Browser, skill,
repository, and image input/output are bounded. Generated media must be a
non-empty image/* payload and is returned as explicit base64 JSON. Skill names
accept only stable identifier bytes. Repository reads go through an
execution.Environment, retaining its root, secret, audit, and cancellation
policy; repository search is a separate optional backend.
Durable memory¶
memory.Store is the application persistence boundary for put, search,
entry deletion, tenant deletion, retention, and compaction. Implementations can
use a database, vector service, or durable workflow store. Every method takes an
explicit tenant ID; no query or delete API has an unscoped form.
memory.Entry distinguishes conversation, semantic, and compaction records.
Conversation entries retain canonical Message slices instead of inventing a
second transcript shape. Semantic entries can carry finite vectors. Search uses
bounded case-insensitive text matching or cosine similarity, filters by kind,
and returns an arena-owned SearchResult. Each match contains a copied record
and citation with entry/tenant identity, excerpt, score, and timestamp.
Retention receives an explicit clock value and can remove records by age and
per-tenant count. delete requires both tenant and record ID;
deleteTenant removes one tenant only. compact keeps the newest configured
records, passes older records in deterministic oldest-first order to a
Compactor, creates a bounded summary before mutation, reserves store capacity,
and only then replaces the sources atomically.
InMemoryStore is the deterministic, allocation-checked test/local backend. It
owns each record independently so deletion releases storage. It is deliberately
single-threaded; production concurrent stores provide their own synchronization
behind the same Store vtable.
Planning and dynamic workflows¶
planning.Plan.init creates one arena-owned immutable revision with bounded
step IDs, titles, dependencies, approval markers, and cycle validation.
revise copies a complete replacement and increments the stable revision; the
previous revision stays readable until its own deinit. Observers receive a
user-visible plan_revised event before advisory or execution work.
planning.run evaluates approval gates before side effects. Missing decisions
return an owned paused outcome listing every gated step. Explicit denial is
an error. Advisors run under the shared control and return bounded copied
advice. Approved steps execute in deterministic dependency order and return one
arena-owned complete outcome with step outputs and aggregate RunUsage.
Every StepContext contains the plan/revision, completed outputs, shared
RunControl, optional parent trace, and optional multi_agent.Scope. Nested
agents can therefore share cancellation, deadline, trace, and usage sessions
without ambient globals. Trace-linked observer events cover revision, advice,
approval, step start/end, completion, and failure.
CapabilityAdapter exposes one plan as dynamic_workflow with an
execute_dynamic_plan function tool. Plans with gated steps make that tool
approval-required. Once the outer agent approval succeeds, the adapter supplies
all per-step approvals, propagates the active tool deadline/cancellation, and
returns structured step output and usage.
Execution runtime services¶
runtime_services.StepStore persists versioned step records with optimistic
revision checks. New records start at revision one; replacements require the
exact prior revision and increment by one. Input/output payloads are bounded
JSON, and succeeded/failed states require their matching output/error fields.
BlobStore owns tenant-qualified media and artifacts with explicit kind, media
type, bytes, lookup, and deletion. PromptStore resolves an exact or latest
versioned prompt. renderPrompt replaces explicit {{name}} variables,
rejects missing values or malformed templates, and bounds literal plus expanded
output.
BoundedExecutor runs a bounded task slice with structured std.Io concurrency,
one shared cancellation/deadline, disposable per-task arenas, and source-
ordered owned results. It drains cancelled work before releasing arenas and
rejects per-task output beyond its ceiling.
InMemoryStores provides deterministic local implementations for tests and
single-thread workers. Production databases, object stores, prompt services,
and thread/remote executors implement the same small vtables. No hosted
Pydantic client or credential is imported by the core runtime-services module.
Agent Client Protocol¶
acp.Client.init opens a reconnectable newline JSON-RPC transport.
initialize requests ACP major version 2, validates the negotiated version, and
records session, prompt-content, MCP, delete, additional-directory, and v1
compatibility capabilities. Lifecycle methods cover session create, list,
resume with optional full replay, delete, close, text prompts, session cancel,
and $/cancel_request.
Requests accept interleaved agent-to-client calls and session/update
notifications while waiting for their response ID. PermissionHandler must
select one advertised option. FilesystemHandler maps required absolute ACP
paths into one rooted execution.Environment and rejects prefix/traversal
escapes. Legacy v1 filesystem and terminal requests use explicit handlers; v2
terminal state/output remains streamed through session updates.
nextUpdate dispatches owned bounded frames to a borrowed update sink. On a
classified transport failure, a reconnect policy closes and redials,
renegotiates capabilities, and resumes the active session without replaying or
resending the accepted prompt. Cancellation and request deadlines use
RunControl and drain late transport calls.
acp.StdioTransport launches an agent process with piped stdin/stdout, writes
one UTF-8 JSON-RPC message per line, bounds reads, rejects embedded newlines,
and kills/reaps the child during close. The tests replay the official ACP v2
initialize/session/prompt examples through both an in-memory transcript and a
real stdio subprocess fixture.
Production CLI¶
zig-out/bin/zigai selects OpenAI, Anthropic, or Google with --provider and
--model. A strict bounded JSON config can set provider defaults, the API-key
environment-variable name, base URL, system prompt, history/paused paths,
output mode, approval policy, local tool manifest, and multiple MCP stdio
commands. require_tool_approval converts manifest tools into deferred tools
before the run. CLI flags have higher precedence. Credentials remain
environment-owned and are never accepted
in config files.
A positional - reads a bounded prompt from stdin. --output text, --json,
and --events select plain final output, one structured result, or newline
AG-UI lifecycle/agent events. --history atomically loads and saves canonical
ZigAI history. Configured MCP servers become dynamic agent toolsets and are
shut down with the CLI.
Approval mode deferred persists a versioned paused envelope and exits with
status 10. A later --resume --approval approve|deny validates the envelope,
constructs decisions for every call, and continues without replaying the model
request. External calls are denied unless an explicit external result is
available; approval cannot accidentally execute them.
zigai completion bash|zsh|fish prints shell completion. Exit codes are stable:
0 success, 1 other runtime failure, 2 usage, 3 config/file, 4 missing
credential, 5 provider failure, and 10 approval required.
Run deadlines and cancellation¶
Agent.run_timeout_ms creates one absolute monotonic deadline for an
invocation. RunOptions.timeout_ms may tighten that budget for one buffered,
streaming, pause, or resume call. Agent.request_timeout_ms is a separate
per-model-attempt ceiling; each attempt receives the smaller of that value and
the remaining run time, so retries never restart the run budget.
The run control covers model requests and streams, retry waits, dynamic instructions, capability model selection, history processors, toolset preparation, argument validation, local and MCP tools, output validators, lifecycle hooks, stream sinks, and resumed tool decisions. Official HTTP transports apply the remaining timeout to the complete operation, including DNS, connect, write, read, and stream consumption. A timed-out or cancelled task is cancelled and drained before the public call returns.
Deadlines require Agent.io. Cancellation remains cooperative without a
runtime; with Agent.io, ZigAI also races the token against in-flight work.
Terminal control errors are RunTimedOut, Cancelled,
RunControlRequiresIo, and RunControlConcurrencyUnavailable. Callback
contexts expose the shared RunControl, and ToolRunContext exposes its
absolute deadline for nested work.
Preemptive control requires the runtime to schedule the operation and each
active deadline or cancellation watcher concurrently. If it cannot,
RunControlConcurrencyUnavailable or ToolConcurrencyUnavailable is returned
instead of running a watcher inline and risking a stalled caller.
A paused run ends its invocation. resumeRunWithOptions starts a new monotonic
budget because a process-local monotonic timestamp cannot be serialized safely
across restarts.
Retry policy¶
Retry decisions classify rate limits, server errors, request timeouts,
connection failures, and provider-response decoding independently. Optional
exponential backoff uses full jitter from the application's Io, honors
numeric and HTTP-date Retry-After, and remains interruptible. The policy's
max_total_delay_ms bounds cumulative backoff across the run. A fallible
before-retry hook observes the planned and cumulative delay, rate-limit values,
and the bounded provider request ID.
The HTTP transport copies provider request IDs into bounded inline storage.
Provider error observers and retry hooks receive borrowed views; copy a value
to retain it. ProviderConnectionError and ProviderResponseDecodeError are
stable retry categories.
ProviderError.body is empty by default. Set
Agent.provider_error_policy.capture_body or ModelRequest.error_policy to
opt in, and choose max_body_bytes; exact-limit bodies are complete and larger
bodies set body_truncated. Provider messages and codes have independent caps.
The observer is synchronous and infallible, so none of its borrowed fields may
be retained without copying.
RunOptions.correlation supplies a stable orchestration run ID, node ID, and
node name. Lifecycle hooks receive it on run_start. OpenTelemetry exports it
as zigai.run.id, zigai.graph.node.id, and zigai.graph.node.name on the run
span and start event. RunOptions.telemetry_parent remains the trace parent.
RunOptions.request_id supplies provider-facing correlation. OpenAI and
OpenAI-compatible adapters send it as x-client-request-id. Generation APIs
do not share a portable idempotency header, so official adapters do not claim
one. A compatible gateway may configure Client.idempotency_header; its model
profile then requests a generated key that remains stable across one logical
request's retries. This requires Agent.io.
Context budgets¶
Agent.context_budget measures the provider-facing request after history
processors and dynamic toolsets have run. Separate limits cover prompt text,
tool traffic and definitions, JSON schemas, raw media sources, estimated input
tokens, and combined input/output capacity. reserve_output_tokens is removed
from max_total_tokens; when it is null, the resolved ModelSettings.max_tokens
is reserved instead. Exact boundaries are accepted.
The default estimate is deliberately provider-neutral: four measured bytes per
token plus fixed message and tool framing. TokenEstimator receives borrowed
ContextBudget.Input and ByteUsage values for provider-specific counting.
Estimator and overflow callbacks run under the invocation's deadline and
cancellation control.
On overflow, ContextOverflowHook receives the measured snapshot, first failed
boundary, and borrowed request view. It may return a borrowed history subslice
or allocate replacement messages from the supplied arena. ZigAI measures the
result once more; a remaining overflow becomes ContextPromptTooLarge,
ContextToolsTooLarge, ContextSchemaTooLarge, ContextMediaTooLarge, or
ContextTokenLimitExceeded. Callback rejection errors pass through unchanged.
Only the provider-facing view is compacted; owned result history remains
complete. RunOptions.context_budget replaces the agent policy for one run.
Tool execution limits¶
Agent.tool_limits applies zigai.ToolLimits to local calls. The defaults are
eight concurrent calls, a 64-call queue, a 1 MiB result, 16 follow-up messages,
and 1 MiB of aggregate follow-up strings and binary data. Tool.limits may
tighten that policy for one tool, including an optional timeout_ms; it can
never loosen the agent-wide envelope.
Parallel batches preserve provider call order while the scheduler respects
both the agent-wide and per-tool concurrency and queue limits. ToolTimedOut,
ToolQueueOverflow, ToolResultTooLarge, and ToolFollowUpOverflow are
recoverable tool failures by default and are sent to the model through the
normal bounded retry path. Run deadline failures, ToolIsolationRequiresIo,
cancellation, allocation failure, and unavailable runtime concurrency remain
fatal. Tools should use ToolRunContext.io for blocking I/O and propagate
cancellation errors instead of swallowing them.
Public operations intentionally use inferred error unions. Allocator, network, process, application callback, tool, hook, exporter, and custom model errors pass through unchanged, so callers can handle their own errors without ZigAI erasing them. Match the named errors that matter and propagate the remainder.
Compatibility imports¶
zigai.providers.<name> is the preferred provider spelling. The original
zigai.openai, zigai.anthropic, zigai.google, and
zigai.openai_compatible aliases remain supported for the 0.x series.
The package also exports standalone zopenai, zanthropic, zgoogle, and
zopenai_compatible modules. They expose the same provider constants, error
set, client, and public codec functions, and can be imported together with
zigai in one executable.
Versioning¶
ZigAI follows semantic versioning. During 0.x, a minor release may make a documented public API change. Patch releases preserve source compatibility for the supported surface described here. Provider wire behavior may evolve when an upstream API changes, with real recorded cassettes covering the supported adapters and model families.