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.mcp |
MCP 2026 client, server, Streamable HTTP, and stdio |
zigai.telemetry |
OpenTelemetry-shaped hooks and metrics |
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.
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 |
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
opt-in snapshot identified by pricing.builtin_version; 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.
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.
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.
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;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.
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.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.