Skip to main content

SAIA

Framework-agnostic verb vocabulary for LLM agents

Python Dependencies Coverage Typed Linting: Ruff CI License

SAIA provides a fixed vocabulary of semantic verbs for LLM interactions. Instead of writing raw prompts, you express intent through verbs like ask, verify, critique, and refine.

Why a fixed vocabulary? The same insight that made SCUMM work for adventure games in 1987: constraints enable tooling. A finite set of operations means every interaction is debuggable, testable, and composable.

Why SAIA?

"Can't I just type this into Claude?" For a one-off question, yes. SAIA is for when you're building software, not chatting:

  • Structured outputs - verify() returns VerifyResult(passed: bool, reason: str), not text you parse
  • Composable - chain verify → critique → refine in 3 lines of code
  • Testable - mock the backend, unit test your verbs
  • Traceable - every verb call logged with inputs/outputs for debugging production issues
  • Backend-agnostic - same code works with Anthropic, OpenAI, or local models
  • Zero dependencies - pure Python core; bring your own LLM client

"Why not raw tool calling?" You could write the iteration loop yourself (~50-100 lines). SAIA gives you complete() with terminal detection, tracing, timeouts, and max iterations built in. It's requests vs urllib - both work, one is cleaner.

SAIA's value compounds when combining verbs, switching backends, or building a team around consistent patterns.

"Is this novel?" Perhaps not. These are the patterns that emerge when you build LLM agents that need to actually work. SAIA extracts them into ~2500 lines anyone can use, inspect, and build on.

Supported Python versions

CI tests every push against the full test suite:

  • Linux (Ubuntu): CPython 3.11, 3.12, 3.13, 3.14

requires-python = ">=3.11" is declared in package metadata; newer Python versions are opt-in and validated against the full test suite before being added to the CI matrix.

Installation

pip install llm-saia

Quick Start

Verbs return a VerbResult.value holds the typed result, .trace the per-step execution trace. Run the installed quick-start with python -m llm_saia.examples.quickstart (source at llm_saia/examples/quickstart.py); it uses an in-process canned backend so no API keys or network are needed.

from llm_saia import SAIA


async def main():
    saia = SAIA.builder().backend(your_backend).build()

    # Verify a claim
    result = await saia.verify(
        "def add(a, b): return a + b",
        "the function returns the sum of its two arguments",
    )
    print(f"Passed: {result.value.passed}, Reason: {result.value.reason}")

    # Generate counter-arguments
    critique = await saia.critique(
        "Adding more programmers to a late project always makes it finish faster."
    )
    print(f"Counter: {critique.value.counter_argument}")

    # Break down a complex task
    subtasks = await saia.decompose("Build a REST API with authentication")
    for task in subtasks.value:
        print(f"- {task}")

Verb Reference

Verb Purpose Returns
ask Query an artifact with a question str
verify Check if artifact satisfies predicate VerifyResult(passed, reason)
critique Generate strongest counter-argument Critique(counter_argument, weaknesses, strength)
refine Improve artifact based on feedback str
synthesize Combine multiple artifacts into one T (structured)
decompose Break complex task into subtasks list[str]
extract Pull structured data from text T (structured)
classify Categorize into predefined classes ClassifyResult(label, confidence)
choose Select best option from choices ChooseResult(choice, reasoning)
find Filter items matching criteria FindResult(indices, reason) (0-indexed)
constrain Rewrite text to comply with rules str
ground Anchor claims to source evidence list[Evidence]
instruct Execute open-ended instructions str

Memory Verbs

Verb Purpose
store Save value to memory
recall Retrieve from memory by query

Configuration

Builder Pattern

saia = (
    SAIA.builder()
    .backend(backend)  # Required: LLM backend
    .tools(tool_defs, executor)  # Optional: tool calling
    .logger(logger)  # Optional: logging
    .system("You are a helpful assistant")  # Optional: system prompt
    .max_iterations(10)  # Optional: tool loop limit
    .max_call_tokens(4096)  # Optional: per-call token limit
    .build()
)

Runtime Modifiers

# Single LLM call (no tool loop)
result = await saia.with_single_call().verify(code, "compiles")

# Custom iteration limit
result = await saia.with_max_iterations(5).instruct(task)

# Timeout
result = await saia.with_timeout(30).decompose(problem)

# Correlation ID for tracing
result = await saia.with_request_id("req-123").ask(doc, question)

Output Guards

Guards validate LLM output and automatically retry with feedback if validation fails.

from llm_saia.guards import english_only, max_length, no_preamble

# Apply guards to any verb
result = await saia.with_guards(english_only(), no_preamble(), max_length(500)).ask(
    article, "summarize this"
)

Field-level guards for structured output:

from typing import Annotated
from llm_saia import Guarded
from llm_saia.guards import english_only, max_length


@dataclass
class Summary:
    title: Annotated[str, Guarded(english_only(), max_length(100))]
    body: Annotated[str, Guarded(english_only())]


result = await saia.extract(article, Summary)

Pre-built guards: english_only(), ascii_only(), max_length(n), no_emoji(), no_markdown(), no_preamble().

Iteration Guards

Iteration guards enforce behavioral constraints during a tool-calling loop, rather than validating the final result. When a guard fires, its feedback is injected into the conversation and the loop continues.

from llm_saia import IterationGuard


# Require the LLM to explain what it's doing before calling tools
def require_narrative(response):
    if response.tool_calls and not (response.content or "").strip():
        return "Explain what you're doing and why before calling tools."
    return None


result = await saia.with_guard(IterationGuard(require_narrative, name="narrative")).complete(task)

with_guard() and with_guards() accept both OutputGuard and IterationGuard — each is routed to the correct bucket automatically.

Tool Gates

Tool gates dynamically control which tools are visible to the model per iteration. Blocked tools are stripped from the outbound schema — the model never sees them and cannot spend output tokens generating a call that would be rejected.

from llm_saia import ToolGateContext

# Hide terminal tool until iteration 3 (shortcut via TerminalConfig)
saia = (
    SAIA.builder()
    .backend(backend)
    .tools(tools, executor)
    .terminal("complete_task", min_iterations=3)
    .build()
)


# Custom gate: only allow expensive tool after cheap tool has been called
def require_cheap_first(ctx: ToolGateContext) -> bool | str:
    if ctx.tool_call_counts.get("cheap_tool", 0) == 0:
        return "must call cheap_tool first"
    return True


result = await saia.with_tool_gate("expensive_tool", require_cheap_first).complete(task)

Gates return True/None to allow, False to block silently, or a str reason recorded in the trace (never surfaced to the model). See docs/tool-gates.md for advanced patterns including context factories for shared computation.

Examples

See the examples/ directory:

  • investigate.py - Investigate a claim (verify → critique → refine)
  • build.py - Build an app (decompose → instruct → synthesize)
  • build_multi.py - Two LLMs collaborate (local generates, smart verifies)

Design Philosophy

  1. Verbs express intent - not implementation details
  2. Structured over strings - type-safe dataclass responses
  3. Composable primitives - build complex flows from simple verbs
  4. Backend-agnostic - same code works with any LLM
  5. Zero dependencies - pure Python core, you control your LLM client
  6. Debuggable - every operation is traceable

Research Directions

SAIA's structured verb outputs create opportunities beyond inference:

  • Consistency tuning - traces capture (prompt, decision) pairs that can fine-tune models for stable verb behavior. Same semantic question → same semantic answer.
  • Structured generation - backends can use grammar-constrained decoding (xgrammar, outlines) to guarantee valid outputs from verbs like extract, verify, and classify without retry loops.

License

Apache 2.0 - see LICENSE

Maintained by LLM Works LLC and contributors.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

llm_saia-0.6.2.tar.gz (262.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

llm_saia-0.6.2-py3-none-any.whl (125.3 kB view details)

Uploaded Python 3

File details

Details for the file llm_saia-0.6.2.tar.gz.

File metadata

  • Download URL: llm_saia-0.6.2.tar.gz
  • Upload date:
  • Size: 262.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for llm_saia-0.6.2.tar.gz
Algorithm Hash digest
SHA256 6e446b3e51fede09c410a878b89a26d1947c379cb1cbcfbfaa2371aa0769ba07
MD5 d9db1916c1ea75b1022b8a5c8aad6b24
BLAKE2b-256 b86c65ac8d925c90686fd3406eabfc8cfa5ddbaefea7b0e7b31f5e1e1310aeb8

See more details on using hashes here.

Provenance

The following attestation bundles were made for llm_saia-0.6.2.tar.gz:

Publisher: release.yml on llm-works/llm-saia

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file llm_saia-0.6.2-py3-none-any.whl.

File metadata

  • Download URL: llm_saia-0.6.2-py3-none-any.whl
  • Upload date:
  • Size: 125.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for llm_saia-0.6.2-py3-none-any.whl
Algorithm Hash digest
SHA256 979fd4214934f6b7f60a73881b9c76f2092f7d4b641315ea06c961baeec4aa48
MD5 94f518a94498e8a668ccbb35fcf30f6f
BLAKE2b-256 0930e4f9e90de129470390f1400e35a7b6a574bfd43619f17ae6b54e58ea2eb7

See more details on using hashes here.

Provenance

The following attestation bundles were made for llm_saia-0.6.2-py3-none-any.whl:

Publisher: release.yml on llm-works/llm-saia

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.6.2 This release

2 files

0.6.1

2 files

0.6.0

2 files

0.5.0

2 files

0.4.0

2 files

0.3.0

2 files

0.2.0

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page