Leia

Leia AI Dialect Reference

Leia’s AI support is an optional standard-library layer over host-installed LLM providers. It is one DSL implementation on Leia’s generic tagged-dialect mechanism, not the language itself and not a privileged AI runtime. The dialect surface is intentionally small: tagged model, tool, agent, and turn blocks plus ordinary llm.*, msg.*, and history.* helpers.

AI dialect syntax is not language intrinsic behavior. The tagged forms build ordinary values and call ordinary runtime helpers; they do not add hidden prompt memory, model-specific evaluation rules, or a separate agent engine. Provider I/O, tool dispatch, budgets, trace, record, and replay all pass through the same host-visible llm runtime paths whether the source uses dialect syntax or direct helper calls.

Host Contract

Embedders install providers through Go options:

Go API Purpose
leia.WithLLMProvider(provider) Provider for ordinary turn and agent calls.
leia.WithLLMProviderFactory(factory) Builds providers for script-declared model {} entries when allowed.
leia.WithLLMTrace(sink) Receives metadata-only trace events.
leia.WithLLMRecorder(sink) Records provider turns for offline replay.
leia.WithLLMReplay(records) Replays recorded turns deterministically.

Provider implementations use github.com/never-labs/leia/llm.Provider. The same package exposes ProviderConfig, ProviderFactory, TurnRequest, TurnResult, Tool, ToolCall, Message, replay helpers such as NewRecorder, LoadRecords, SaveRecords, NewReplayProvider, trace helpers such as NewTraceRecorder, and provider error classifications such as ProviderErrorNetwork.

Model Dialect

model { ... } registers model aliases for the current script.

model {
    default: "fast"
    fast: {
        protocol: "anthropic_compatible"
        base_url: os.getenv("LEIA_LLM_BASE_URL")
        api_key: os.getenv("LEIA_LLM_API_KEY")
        provider_model: os.getenv("LEIA_LLM_MODEL")
    }
}

Rules:

Tool Dialect

tool { ... } returns a model-facing tool descriptor backed by a Leia function.

search_runbook := tool {
    name: "search_runbook"
    params: ["service"]
    description: "Search local runbooks."
    requires: ["docs.read", "metrics.read"]
    fn: func(service) {
        return "runbook:" .. service, nil
    }
}

Tool fields:

Field Meaning
name Provider-visible tool name.
fn Leia function called when the tool is dispatched.
params Ordered parameter names.
description Provider-visible description.
requires Capability labels for host policy and audit.
capabilities Alias for requires in lower-level llm.tool option tables.
schema Optional provider/tool input schema metadata.
result / output Script-visible success shape for contract inventory and validation.
error Script-visible structured error shape for contract inventory.
replay_key Stable key template for deterministic tool fixtures and audit logs.

Runtime helpers remain available through llm.tool, llm.toolof, llm.agent_as_tool, llm.dispatch, llm.tool_caps, llm.check_tools, llm.tool_schema, llm.tool_info, and llm.validate_tools.

Tool contracts are metadata for hosts, tests, replay fixtures, and provider adapter schemas. They are not authorization by themselves and they do not coerce tool return values. Use llm.validate_tools(tools) to fail fast when a tool list is missing required contract metadata for a workflow:

lookup := llm.tool("lookup", func(query) {
    return {answer: "docs:" .. query}, nil
}, {
    params: ["query"]
    description: "Lookup local documents."
    capabilities: ["docs.read", "replay.local"]
    result: {answer: "string"}
    error: {kind: "validation", message: "string"}
    replay_key: "lookup:{query}"
})

info := llm.tool_info([lookup])
ok, err := llm.validate_tools([lookup])

llm.tool_info(tool_or_tools) returns a contract inventory with normalized name, kind, type, description, params, schema, capabilities, requires, result, error, and replay_key fields. llm.tool_schema(...) returns the provider-facing input schema and script-visible result/error shapes. Agent-as-tool values report type: "agent" and carry trace_contract: "agent_tool.v1"; their provider-facing input schema describes arguments only, not the delegated agent’s output shape.

Use llm.tool_outcome(call_or_tool_message, result_or_err, opts) to project a tool dispatch into a stable, metadata-only table. It records fields such as tool_call_id, tool_name, status, result_status, result_type, result_ref, arg_names, error_kind, replay_key, and correlation IDs. llm.tool_outcome_event(outcome, opts) or llm.tool_outcome_trace_event(...) turns that projection into a generic trace event. The projection is ref-only: it does not copy raw tool args, raw tool results, raw prompts, or provider completions into the trace payload.

For package-level inventories, use llm.tool_registry(tools, opts):

registry := llm.tool_registry([lookup], {registry_id: "research-tools"})
gate := llm.validate_tool_registry(registry)
trace := llm.tool_invocation_trace({tool_name: "lookup"}, {answer: "ok"})

The registry is a provider-free metadata projection. It contains normalized descriptors, unique capabilities, effect/approval counts, redaction metadata, and a summary. Validation returns a table with ok, status, findings, and finding_count; it reports duplicate names, missing descriptor fields, live network/provider credential boundaries, provider wire formats other than none, secret parameters, and effectful tools without approval policy. llm.tool_invocation_trace builds the registry trace envelope with ordered registered, schema_validated, approval_checked, invoked, and result_recorded events. These helpers do not execute tools or authorize side effects.

Messages And History

turn and llm.turn receive ordinary message arrays. Use llm.system, llm.user, msg.assistant, msg.assistant_call, msg.tool_result, and msg.tool_error to build normalized messages.

history := [llm.system("You are concise."), llm.user("Summarize this.")]
append(history, msg.assistant("draft"))

Message tables use normalized roles: system, user, assistant, and tool.

A prompt { role: "...", text: "..." } block is message-object shorthand. It produces the same kind of normalized table accepted by messages and by the msg helpers:

messages := {
    prompt { role: "system", text: "Answer from local evidence." }
    prompt { role: "user", text: "Summarize the release." }
}

Prompt message objects are data, not compiler directives. They may appear in message arrays, agent instructions, or generated config tables. Trace sinks may redact prompt text according to host policy.

Turn Dialect

turn { ... } performs exactly one provider request and returns (result, err). It never dispatches tools by itself.

result, err := turn {
    model: "fast"
    messages: [llm.user("Reply with ok.")]
    tools: [search_runbook]
    max_tokens: 32
    temperature: 0
}

Important fields:

Field Meaning
model Alias or provider model name.
messages Ordered message array.
tools Tool descriptors or supported agent-as-tool values.
force_tool Force one tool by name when supported by provider.
max_tokens Output token limit.
temperature / top_p Sampling controls.
response_format Provider response-format hint.
stream Request incremental provider output when supported.
on_stream / onStream Optional callback for streaming events.
stop Stop sequences.
metadata String metadata passed to the provider.

With streaming, callbacks and trace events can receive incremental token data, but the script still receives one complete final result table after the provider finishes.

Result fields include status, text, calls, reason, and usage. Errors are ordinary result values such as {kind: "provider", message: "..."}.

Agent Dialect

agent { ... } returns a callable workflow value. The config function builds the request configuration for each call.

answer := agent {
    name: "answer"
    params: ["question"]
    description: "Answer with local documentation when useful."
    config: func(question) {
        return {
            model: "fast"
            system: "Use local documentation when useful."
            user: question
            tools: [search_runbook]
        }, nil
    }
}

result, err := answer("What changed?")

For common single-input agents, the dialect also accepts a declarative shorthand. The shorthand synthesizes the same config function and still lowers through llm.agent; it does not introduce a separate execution engine.

summarize := agent {
    name: "summarize"
    params: ["topic"]
    model: "fast"
    instructions: prompt { role: "system", text: "Use evidence and be concise." }
    tools: [search_runbook]
    output: {summary: "short"}
}

result, err := summarize("release process")

When messages is omitted, the first call argument becomes user. The instructions field is treated as system unless system is already present. Prompt field blocks with role and text are ordinary message tables, so they can be placed directly in messages.

Use the shorthand when the agent is a prompt capsule: fixed model, fixed instructions, optional tools, sampling controls, metadata, budget, and expected output shape. Use an explicit config function when call arguments need custom mapping or dynamic request fields. Use a custom flow only when the script must own turn sequencing, message history, or dispatch.

For an agent without a custom flow function, Leia runs the built-in loop: synthesize messages from system and user, call one turn, dispatch returned tool calls, append assistant tool-call and tool-result messages, and repeat until the provider returns a final answer or the workflow stops.

For explicit custom control, use llm.agent(name, configFn, flowFn, opts). Custom flow functions call llm.turn and llm.dispatch directly; no hidden turn or dispatch occurs.

Use llm.agent_state_checkpoint(state, opts) when a custom flow needs a portable resume marker or trace checkpoint. The helper returns a provider-free, ref-only table with kind: "agent_state_checkpoint", schema/version metadata, agent/session/state identifiers, input/output/memory refs, deterministic checkpoint_key and cache_key values, a resume_token, redaction metadata, and trace correlation fields. llm.agent_state_checkpoint_event(checkpoint, opts) or llm.agent_state_checkpoint_trace_event(...) projects that checkpoint into a generic trace event. The helpers do not persist state, call providers, or copy raw prompts, raw inputs, raw outputs, credentials, or provider completions into payloads.

Agent As Tool

Use llm.agent_as_tool(agent_value) when a supervisor agent should call another agent as a tool.

supervisor := agent {
    name: "supervisor"
    params: ["question"]
    config: func(question) {
        return {
            model: "fast"
            user: question
            tools: [llm.agent_as_tool(answer)]
        }, nil
    }
}

llm.handoff(agent, opts) and llm.delegate(agent, opts) are agent-as-tool aliases for composition-oriented code. They wrap an existing agent as a tool descriptor, copy supported metadata such as name, description, params, requires, and output, and invoke the nested agent through the same dispatch path as other tools. They do not create a private scheduler or send the nested agent’s output shape as the provider-facing tool input schema. If the delegated agent pauses for approval, dispatch returns a structured pending error so the host can persist or resume the outer workflow.

Agent-as-tool dispatch also attaches a generic trace contract. A successful delegation result may contain trace or tool_trace with:

Field Meaning
type Node type such as agent_tool, agent, workflow, or workflow_step.
name Tool, agent, workflow, or step name.
status Runtime status such as ok, done, pending, stopped, or error.
parent Lightweight parent reference when nested.
children Ordered nested trace nodes.
error Structured error table when the node failed or paused.
budget / cancel Copied budget or cancellation error details when applicable.
metadata Host/script metadata table.

This trace contract is script-visible result metadata. It complements host trace sinks, which receive metadata events such as turn_start, turn_end, turn_error, and streaming events.

Model I/O Envelopes

Use llm.model_io_envelope(spec, opts) when a script or package needs a stable provider-free record of a model request/response boundary:

envelope := llm.model_io_envelope({
    model: "fast"
    request: {messages: [llm.user("Summarize ACME.")]}
    response: {finish_reason: "stop" usage: {prompt_tokens: 8 completion_tokens: 5}}
}, {capability: "generic.ai.turn"})

The envelope records routing metadata, redacted request headers/auth, message/tool counts, response presence, tool-call counts, usage totals, schema hints, refs, redaction policy, and a compact summary. It does not call a provider, execute tools, keep raw prompt/completion text, or require credentials. llm.modelIOEnvelope, llm.model_call_envelope, llm.modelCallEnvelope, llm.turn_envelope, and llm.turnEnvelope are equivalent aliases.

Structured Output

Agents and turns can request structured output with an output shape and can also pass provider-facing hints through response_format.

extract := agent {
    name: "extract"
    params: ["note"]
    model: "fast"
    instructions: prompt { role: "system", text: "Extract project and owner." }
    output: {project: "ORCHID", owner: "ADA"}
}

output is a validation contract over provider results. It does not make model text part of Leia syntax or add a new type system rule. Built-in agent execution validates configured shapes; custom flows should call llm.validate_output(value, schema) when they need the same check. Validation failures are structured errors, not provider answers.

The lower-level schema helpers are useful when the same shape is shared across tools, turns, agents, and section generation:

Helper Meaning
llm.schema(spec) Normalize a lightweight shape or JSON Schema-like table to a JSON Schema table.
llm.schema_info(spec) / llm.schemaInfo(spec) Return {schema, json_schema, kind} for inspection.
llm.output_schema(name, spec, opts) Build a provider-facing response_format table of type json_schema.

Lightweight object specs treat string values as field types, a trailing ? as optional, one-element arrays as array item specs, and descriptor tables as field schemas with metadata such as description.

contact_schema := llm.schema({
    name: {type: "string", description: "Display name"}
    score: "number"
    nickname: "string?"
    tags: ["string"]
})

format := llm.output_schema("contact", contact_schema)
result, err := llm.turn({
    model: "fast"
    messages: [llm.user("Extract the contact.")]
    response_format: format
})
ok, message := llm.validate_output(result.text, contact_schema)

Schemas are request and validation metadata. They do not make model output a typed Leia value until the script parses or validates it.

Retrieval Context And Evidence

The memory/RAG helpers build ordinary tables and messages for small local retrieval workflows:

Helper Meaning
llm.doc(value, opts) / llm.document(value, opts) Build a document table with text plus optional id, title, source, tags, sections, or other metadata.
llm.collection(docs) / llm.docs(docs) Build a collection table with docs and count.
llm.retrieve(collection, query, opts) / llm.search(...) Return ranked local matches; opts.limit caps results and opts.label labels generated context.
llm.context(matches, opts) Build a context message wrapper, default label Context.
llm.evidence(matches, opts) Build an evidence message wrapper, default label Evidence.
llm.memory_outcome(context_or_matches, opts) / llm.retrieval_outcome(...) Project retrieval/context matches into a redacted, ref-only outcome table.
llm.memory_outcome_event(outcome, opts) / llm.memory_outcome_trace_event(...) Project that outcome into the generic trace event shape.
llm.evidence_outcome(evidence_or_refs, opts) / llm.citation_outcome(...) Project evidence refs into a redacted, ref-only outcome table.
llm.evidence_outcome_event(outcome, opts) / llm.evidence_outcome_trace_event(...) Project evidence outcome metadata into the generic trace event shape.

context and evidence fields on turn, agent, and section request tables are expanded into user messages before the provider call. These helpers are for script-local packaging and simple lexical retrieval; they are not a vector database, persistence layer, citation verifier, or permission boundary. llm.memory_outcome is observational: it records match count, query/label, top match identity, citation refs, and correlation fields without copying raw memory text, snippets, prompts, or provider results into the payload. llm.evidence_outcome is the evidence/report-facing equivalent: it records evidence refs, citation/source/artifact counts, top evidence identity, redaction metadata, and correlation fields without copying raw evidence text, snippets, prompts, completions, or credentials.

docs := llm.collection([
    llm.doc("Checkout runbook says payment queue owns sev2 incidents.", {
        id: "runbook"
        title: "Checkout runbook"
        source: "local/runbook"
        tags: ["checkout", "payments"]
    })
])

ctx := llm.retrieve(docs, "checkout payment sev2", {limit: 1})
outcome := llm.memory_outcome(ctx, {workflow_run_id: "run-42"})
event := llm.memory_outcome_event(outcome, {trace_id: "memory-review"})
evidence_outcome := llm.evidence_outcome(llm.evidence(ctx.matches), {
    report_id: "report-42"
    section: "risk"
})
evidence_event := llm.evidence_outcome_event(evidence_outcome, {
    trace_id: "evidence-review"
})
result, err := llm.turn({
    model: "fast"
    user: "Who owns the incident?"
    evidence: llm.evidence(ctx.matches, {label: "Runbook evidence"})
})

Documents may carry section text and provenance metadata so report or RAG code can preserve source identity before prompt assembly:

policy_doc := llm.document({
    id: "policy"
    title: "Release policy"
    source: "docs/release/index.md"
    sections: {
        approvals: "Production releases require owner approval."
        rollback: "Rollback plans must name the on-call."
    }
})

The retrieval helpers rank and package only the collection passed to them. A host or tool must filter documents before collection construction when access control, tenant boundaries, licensing, freshness, or secret handling matter.

Workflows

llm.workflow(steps) creates a sequential runner for named AI or non-AI steps. Each step is built with llm.step(name, fn, opts), with the stage-shaped alias llm.stage(name, fn, opts), or supplied as a function. A workflow value exposes run(input, opts) and mock(fixtures).

flow := llm.workflow([
    llm.step("draft", func(ctx) {
        return llm.turn({model: "fast", messages: [llm.user(ctx.input)]})
    })
    llm.step("revise", func(ctx) {
        return llm.turn({model: "fast", messages: [llm.user(ctx.input)]})
    })
])

result, err := flow.run("release notes")

Step functions receive a context table with input, initial_input, previous, accumulated results, and named context. A step may return a provider/agent result table, a plain value, and optionally an error. The next step receives the prior step’s text or value as its input. The final workflow result includes status, text, value, ordered steps, and named context.

Use workflows for deterministic sequencing, replay, and test fixtures around agent calls. They are not parallel execution, durable orchestration, retries, or transaction management. flow.mock(fixtures) replaces steps with fixtures and is intended for tests and offline examples. Fixture lookup prefers a step or stage fixture_key, then the step name, then the 1-based numeric step index.

llm.workflow_graph(config) adds a static graph contract around the same sequential runner. It accepts ordered stages and optional edges, validates that dependencies point to earlier stages, exposes graph metadata, and then runs the stages in the supplied order. This is useful when an AI dialect package needs stable stage ids, capability metadata, input/output refs, and replayable fixtures without introducing a separate scheduler.

graph := llm.workflow_graph({
    workflow_id: "research-flow"
    entrypoint: "ai.workflow.orchestrate"
    stages: [
        llm.stage("plan", plan_fn, {output_ref: "plan_result"})
        llm.stage("finalize", final_fn, {
            depends_on: ["plan"]
            input_ref: "plan_result"
            output_ref: "final_result"
        })
    ]
    edges: [{from: "plan", to: "finalize"}]
})
result, err := graph.run("topic")

The graph helper is intentionally conservative: it validates and records graph shape, but it does not perform DAG scheduling, parallel execution, retries, persistence, or hidden provider calls. Stage metadata fields such as capability, fixture_key, input_ref, output_ref, input_schema, output_schema, and depends_on are also copied into each executed step’s trace.metadata, so replay trace events and workflow trace nodes can be correlated without inspecting raw step inputs or outputs.

For provider-free planning contracts that should not execute, use llm.plan_node, llm.planning_graph, llm.validate_planning_graph, and llm.planning_trace:

plan := llm.plan_node("plan", {
    node_type: "transform"
    retry_policy: {max_attempts: 1 retryable: false backoff: "none"}
})
contract := llm.planning_graph([plan], {}, {id: "research-plan"})
gate := llm.validate_planning_graph(contract)
trace := llm.planning_trace(contract, {}, {run_id: "research-plan:1"})

These helpers normalize plan nodes, edges, retry policy, branch/merge metadata, trace evidence, validation findings, and trace summaries. They do not schedule work, run branches, sleep for retries, call providers, persist state, or authorize side effects.

Section Generation

llm.sections(config) and llm.generate_sections(config) run the same agent request shape once per requested section and return ordered and name-indexed results.

generated, err := llm.sections({
    model: "fast"
    messages: [llm.system("Return JSON."), llm.user("Draft the report.")]
    evidence: "Evidence: launch checklist is complete."
    sections: [
        {
            name: "summary"
            instructions: "Create the summary section."
            output: {headline: "Short headline", confidence: 0.5}
        }
        {
            name: "risk"
            prompt: "Create the risk section."
            output: {risk: "Low", owner: "team"}
        }
    ]
})
headline := generated.values.summary.headline

Top-level request fields such as model, messages, tools, context, and evidence are shared. Each section must have name and may provide instructions or prompt, output, section-local evidence, and other request fields. Results are returned as sections in order, results by section name, and parsed values by section name when structured output is configured.

Sections are a convenience for independent report parts. They do not guarantee cross-section consistency, automatic citations, or shared hidden memory beyond the request fields supplied to each section.

Report Artifact Contract

llm.report_artifact_contract(opts) returns a generic, offline-verifiable contract table for report-producing workflows. It is useful when an agent or workflow produces sections, chart plans, source annotations, and artifact manifests that another renderer or CI check will validate.

contract := llm.report_artifact_contract({
    name: "release_report"
    version: "report.v1"
})

manifest := contract.manifest_template
section_schema := contract.schemas.report_section

The returned table includes name, version, offline_verifiable, renderer_required, schemas, manifest_template, and required_markers. The schemas cover:

Schema Purpose
report_section Ordered section content with chart/source references and AI disclosure flags.
chart_spec Chart intent plus source references and planned/rendered artifact metadata.
artifact_manifest Report id, generated time, sections, chart specs, artifacts, source annotations, warnings, and disclosure.
source_annotation Source identity, locator, freshness fields, optional license, retrieval time, and evidence hash.

This helper declares an artifact boundary; it does not render HTML/PDF, fetch data, verify citations, or make product-specific report APIs. Scripts should validate produced tables with llm.validate_output or ordinary assertions and leave rendering to explicit code or host tooling.

Budgets, Policy, Approval, Replay, And Trace

Budgets can be attached to agent config tables or managed with lower-level helpers such as llm.with_budget. Public dimensions include turns, calls, tokens, and time. Provider usage may include cost metadata, but Leia does not promise money accounting as a stable script-level budget dimension.

Budgets gate AI runtime work before or after provider turns and tool dispatch. They do not change ordinary expression evaluation outside the helper paths. Declarative agents, explicit agents, and direct turns share the same accounting when they lower through llm.

Use llm.budget_outcome(err, opts) to turn a budget or deadline error into a portable table with kind: "budget_outcome", source_kind, status, result_status, blocked, and safe budget fields such as dimension, limit, used, and message. Use llm.budget_outcome_event(outcome, opts) or llm.budget_outcome_trace_event to add that result to a generic trace envelope. These helpers are provider-free projections; they do not consume budget, mutate accounting, or provide stable money accounting.

Policy helpers operate on tool capability labels:

policy := llm.policy()
ok, err := llm.check_policy([search_runbook], policy)

The default policy is capability_policy.v1 with default: "deny_high_risk". It denies capability classes for trading, portfolio, generated-code, network, credential, and publish actions unless an exact capability is listed in allow. llm.check_policy(tool_or_tools, policy) returns (true, nil) or (nil, {kind: "policy", capability, class, policy, message}).

Use llm.policy_outcome(tool_or_tools, policy, opts) when scripts need a portable decision table instead of tuple-style control flow. It returns kind: "policy_outcome", status, result_status, capabilities, allowed, denied, clean_skip, approval_required, and side_effect_allowed. Passing {clean_skip: true, reason, dependency} records a skipped host/dependency gate without granting the requested capability. Use llm.policy_outcome_event(outcome, opts) to add that decision to a generic trace envelope. llm.policy_outcome_trace_event is an equivalent explicit trace-named alias. The helper emits a redacted policy_outcome event with policy summary, capability, status, skip metadata, and correlation IDs, without copying raw error tables or executable tool values into the payload.

Human approval and resume use the lower-level loop helpers. A pending snapshot is host-persistable data; loop.resume(token, approval) applies an approval table such as {ok: true} or {ok: false, reason: "..."}. Use llm.approval_trace({token, pending, approval, result, policy}) to record a portable approval replay trace with kind: "approval_replay_trace" and version: "approval_replay.v1".

Use llm.approval_trace_event(trace, opts) when the approval decision should join a generic trace envelope. It returns a redacted llm.trace_event-compatible event with the default event_type: "approval_replay_trace", correlation IDs, decision/result status, and policy summary fields. It does not copy approval tokens, raw tool arguments, or raw result values into the event payload.

Use host-side record/replay for deterministic provider behavior:

rec := llm.NewRecorder()
vm := leia.New(leia.WithLLMProvider(provider), leia.WithLLMRecorder(rec.Record))
// run script
_ = llm.SaveRecords("records/turns.json", rec.Records())

records, _ := llm.LoadRecords("records/turns.json")
vm = leia.New(leia.WithLLMReplay(records))
replay := llm.NewReplayProvider(records)
_ = replay

Use llm.NewTraceRecorder() or leia.WithLLMTrace for metadata events. Trace events intentionally omit prompt text and tool result values by default. Record/replay observes normalized requests after dialect lowering, so replay fixtures should not depend on whether the source used shorthand syntax or direct helper calls. Trace is for operational visibility and may redact content; replay is for deterministic provider behavior and must be strict about request matching.

Scripts can also build provider-free replay fixtures directly. llm.replay_record normalizes one record, fills operation, capability, request_hash, response_hash, and a replay table that can be passed to llm.turn. llm.replay_fixture(records, opts) wraps normalized records with the same strict ordered matcher used by llm.replay_index. fixture.replay(request, replay_key) matches a normalized turn request and returns replay options for llm.turn. llm.fixture_index(spec, opts) normalizes provider-free fixture index metadata for packages and replay catalogs. It fills stable offline defaults such as provider_free: true, live_network: false, real_dependency_imports: false, mode: "fixture_replay", strategy: "strict_ordered", normalized fixture entries, matching metadata, and a summary table. llm.validate_fixture_index returns {ok, status, findings} for shape and offline-flag checks. These helpers treat path, schema, and records_path as metadata only; they do not open files, scan directories, read schemas, or require a specific package layout. llm.provider_free_package_contract(spec, opts) normalizes package boundary metadata for provider-free AI packages. It fills package identity, provider/offline flags, default_policy, credentials, entrypoint/schema/ fixture/capability counts, and a summary table. llm.validate_package_contract returns a validation table for offline defaults, credential-free policy, and safe relative reference strings in entrypoints, schemas, and fixtures. These helpers do not load package files, resolve directories, inspect schemas, or require application-specific names. llm.replay_http_record(spec, opts) and llm.replay_artifact_record(spec, opts) normalize caller-supplied HTTP/API and downloaded-artifact replay metadata. They fill provider-free/offline flags, replay identity, request and response summaries, redaction metadata, rate-limit/pagination/terms fields, and artifact provenance fields. They redact sensitive headers and auth refs, and they do not keep raw response bodies or artifact content in the normalized record by default. They never perform HTTP requests, open artifact files, resolve paths, compute file hashes, or require referenced resources to exist. llm.replay_trace_event(match, opts) projects a fixture.match(...) or llm.replay_index(...).match(...) result into a redacted trace event. Matched, mismatched, and exhausted replay states become replay_record_matched, replay_record_mismatch, and replay_record_exhausted events with replay identity hashes and summary counters, not raw prompts or response bodies. llm.trace_summary(envelope_or_events) returns deterministic trace counts, event types, replay keys, and ordering diagnostics. llm.trace_assert(input, opts) turns those checks into {ok, status, summary, findings} without contacting a provider, so replay and workflow packages can gate trace evidence with ordinary assertions. Trace assertions may require event types, correlation fields, payload fields on all events, or event-type-specific payload fields with require_event_payload_fields or the equivalent required_payload_fields_by_event_type. It can require minimum status counts with require_status_counts, cap status counts with limit_status_counts or max_status_counts, and deny truthy redaction markers with deny_secret_values_present, deny_raw_prompt_stored, and deny_raw_completion_stored.

record, err := llm.replay_record({
    replay_key: "turn:1"
    request: {model: "fast", messages: [llm.user("hello")]}
    response: {status: "final_answer", text: "fixture answer"}
})
fixture, err := llm.replay_fixture([record], {fixture_id: "unit-fixture"})
index := llm.fixture_index({
    fixture_id: "unit-fixtures"
    fixtures: [
        {
            fixture_key: "turn:1"
            path: "fixtures/turns.json#/records/0"
            schema: "schemas/turn_record.schema.json"
        }
    ]
})
index_gate := llm.validate_fixture_index(index, {
    require_replay_ready: true
    require_path: true
})
package_contract := llm.provider_free_package_contract({
    id: "unit-package"
    package_name: "unit-package"
    entrypoints: {fixture_index: "fixtures/fixture_index.json"}
    fixtures: {index: "fixtures/fixture_index.json"}
})
package_gate := llm.validate_package_contract(package_contract, {
    require_fixture_index: true
})
http_record, err := llm.replay_http_record({
    replay_key: "api:metrics:1"
    request: {method: "GET" url: "https://example.invalid/metrics"}
    response: {status: 200 body: "[{\"x\":1}]" typed_as: "MetricRow[]"}
    rate_limit: {limit: 300 remaining: 1 retry_after_seconds: 2}
    terms: {usage: "offline-fixture" live_network: false}
})
artifact_record, err := llm.replay_artifact_record({
    replay_key: "download:report:1"
    artifact: {
        id: "report-html"
        media_type: "text/html"
        sha256: "provided-by-fixture"
        replay_uri: "mock://artifacts/report.html"
    }
})
request := {model: "fast", messages: [llm.user("hello")]}
replay, err := fixture.replay(request, "turn:1")
result, err := llm.turn({
    model: "fast"
    messages: [llm.user("hello")]
    replay: replay
})
event := llm.trace_event({
    trace_id: "unit-trace"
    event_type: "turn_end"
    replay_key: "turn:1"
    request_hash: replay.request_hash
    response_hash: replay.response_hash
    payload: {status: result.status}
})
trace := llm.trace_envelope([event], {trace_id: "unit-trace"})
gate := llm.trace_assert(trace, {
    require_provider_free: true
    deny_live_network: true
    deny_secret_values_present: true
    deny_raw_prompt_stored: true
    max_status_counts: {error: 0}
    required_event_types: ["turn_end"]
    require_event_payload_fields: {
        turn_end: ["status"]
    }
})
match := fixture.match({
    operation: "llm.turn"
    capability: "generic.ai.turn"
    replay_key: "turn:1"
    request_hash: replay.request_hash
})
replay_event := llm.replay_trace_event(match, {
    trace_id: "unit-trace"
    replay_session_id: "unit-fixture"
})

Evaluation Harness

evaluate "name" { ... } declares a regression case discovered by leia evaluate. The eval module is available only while the evaluation harness runs the block.

Function Meaning
eval.case(id, fn) Run one named subcase and continue after subcase failures.
eval.metric(name, value) Record bool, number, string, nil, or JSON-like metrics.
eval.load_jsonl(path) Load a JSON Lines corpus relative to the evaluate source.
eval.skip_if(cond, reason) Skip the active subcase when cond is truthy.
eval.fail_if(cond, message) Fail the active subcase when cond is truthy.
eval.usage() Return current case LLM usage.
eval.budget(table) Fail when current usage exceeds a positive limit.
eval.judge(options) Run a bounded judge turn through llm.turn(options).

The CLI supports --list, --filter, --parallel, --format json|text|html, --output/--report, --gate, --baseline, --compare, --regression-threshold, --llm-record, --llm-replay, and --update-golden. JSON reports preserve case metrics, usage, diagnostics, and optional baseline comparison. Boolean metrics become pass-rate summaries; numeric metrics become count/mean/min/max summaries; string metrics become category counts. LLM fixture modes are intended for deterministic provider behavior and may serialize execution even when --parallel is set.

Live Provider Tests

Live LLM tests must be opt-in and must not commit tokens. The repository uses:

LEIA_LLM_INTEGRATION=1
LEIA_ANTHROPIC_COMPAT_BASE_URL=...
LEIA_ANTHROPIC_COMPAT_API_KEY=...
LEIA_ANTHROPIC_COMPAT_MODEL=...

The GLM live-provider path also accepts LEIA_GLM_BASE_URL, LEIA_GLM_API_KEY, and LEIA_GLM_MODEL.

Run a live GLM example explicitly:

LEIA_LLM_INTEGRATION=1 \
LEIA_GLM_API_KEY=... \
LEIA_GLM_MODEL=glm-5.1 \
go run ./cmd/leia examples run repo-llm-glm_smoke

leia examples check skips live-provider examples by default. Directly running examples/llm/glm_smoke.leia or examples/llm/glm_direct_agent_tools.leia requires both LEIA_LLM_INTEGRATION=1 and a configured GLM key.

Validation

The optional LLM dialect is validated through provider-free examples, record/replay examples, and opt-in live provider runs. The main coverage areas are:

Surface Validation surface
Models and provider adapters Provider-free dialect examples plus opt-in GLM/OpenAI-compatible live runs.
Tools and agents Agent-as-tool examples, tool dispatch examples, and coding-agent replay examples.
Messages, turns, and streaming Direct-turn, prompt-tagged-message, history, and streaming examples.
Record, replay, and trace Replay fixtures, trace envelopes, judge replay, multiturn replay, and project regression examples.
Tagged AI dialects examples/ai/tagged_agent_workflow.leia, examples/dialects/ai_prompt_quote.leia