Agents Overview

loom.ai turns a declarative, vendor-neutral agent artifact into a running, governed HTTP service. An agent is authored as a YAML file, validated offline with no credentials and no network, compiled into an immutable plan at start-up, and served under the authentication the rest of your application already uses.

The pillar is deliberately narrow about what belongs where:

Concern

Lives in

Why

What the agent is — its name, instructions, answer shape, granted capabilities

the artifact (agent.yaml)

Portable. The same file moves between environments unchanged.

Where things are — model provider, endpoints, credentials, server URLs

the ai: config section

Deployment facts. They differ per environment and must never travel in the artifact.

Which engine actually runs it

an entry point (loom.ai.engines)

Swappable. The compiler never imports an engine.

That split is the whole design. An artifact names a model role (reasoning), never a model id; it names an MCP server (runbooks), never a URL; it names a skill library (./skills), never an absolute path. Nothing in the artifact can locate a resource or carry a secret, because those fields do not exist in the format.

Stability

Two different promises, and conflating them is the mistake this section exists to prevent.

The artifact format is stable; the Python API is not

The programmatic contracts are experimental (FR-056). The classes, protocols and functions exported from loom.ai may change within a major line. Pin your loom version if you import them.

The artifact format is not experimental (FR-056a). An agent definition declaring spec_version: 1 keeps validating and compiling for the whole major line. The version-1 corpus is append-only: an existing entry may never be edited to make a change pass, only added to.

Author against the artifact. Pin the Python API.

This is why the JSON Schema is shipped inside the distribution and the artifact reference is the longest page in this section: the artifact is the surface we ask you to build on.

Install

The base wheel imports no engine and no vendor SDK — import loom.ai works on a bare install. Capabilities arrive as extras:

pip install "loom-kernel[rest,ai-pydantic]"          # the engine
pip install "loom-kernel[ai-bedrock]"                # + AWS Bedrock models
pip install "loom-kernel[ai-openai]"                 # + OpenAI and every compatible endpoint
pip install "loom-kernel[ai-anthropic]"              # + Anthropic models
pip install "loom-kernel[ai-a2a]"                    # + the A2A interoperability surface
pip install "loom-kernel[ai-harness]"                # + the `skills` capability
pip install "loom-kernel[logfire]"                   # + the Logfire OTel distribution

A missing extra is reported at start-up, by name, with the extra to install — never as an ImportError on the first request.

The shape of a deployment

# config/api.yaml
ai:
  engine: pydantic-ai
  specs: ["ai/agents/*/agent.yaml"]

  models:
    default:
      provider: openai
      model: gpt-4o-mini
      credentials_ref: OPENAI_API_KEY
    reasoning:
      provider: bedrock
      model: anthropic.claude-sonnet-4-20250514-v1:0
      region: eu-west-1

  mcp_servers:
    runbooks:
      url: https://runbooks.internal.example.com/mcp
      headers_ref: ${secrets:/loom/runbooks/api-key}
      timeout_ms: 20000

  endpoints:
    incident-triage:
      enabled: true
      auth: jwt

Every agent matched by specs is compiled. Only the agents named in endpoints, with enabled and a named auth, are mounted on the HTTP and A2A doors. An agent absent from endpoints is still reachable — from application code, through an Agent() marker declared on a use case’s own execute signature, bound to that use case’s own verified caller. That is a separate door with its own default: a marker-filled handle always refuses an anonymous caller, with no allow_anonymous to relax it, unlike the HTTP door an operator explicitly opts an agent into. Exposure over the network is always an explicit opt-in, never a default; the code door is a distinct decision, not derived from it.

Nesting: how deep an agent may call another

ai:
  max_agent_depth: 1   # default

max_agent_depth bounds the longest chain of nested agent runs one task may open, counting the top-level run itself. A use case reaches a named agent through an Agent() marker, and that handle’s run counts as one more entry in the same chain the run that reached the use case already opened.

Defaults to 1: the top-level run alone already consumes the whole budget, so an on_output hook — or any other use case an agent’s run invokes — that itself declares an Agent() marker finds no depth left, and the whole class of agent-calls-agent cycles is unreachable without deliberately raising this value. Raising it buys deliberate nesting (a hook that escalates by asking a second agent, say) at the cost of a chain that can grow that much deeper before AGENT_CALL_TOO_DEEP catches a runaway one — see what the marker refuses.

Conversation memory belongs to the application

An artifact may declare a conversation use case beside on_output: for every run that carries a conversation_id, the runtime executes it before the model starts and it returns the prior turns as opaque, engine-native bytes — or None on the first turn. The engine replays them and hands the turn’s new messages to the on_output use case as messages, so the application persists them under its own conversation_id, in its own store, under its own tenancy rules. Loom stores nothing, caches nothing and never reads a message; no history travels on the wire in either direction. See conversation.

An application that is only agents

An application does not need a model, a use case or a REST interface to run an agent. Its manifest module declares AGENTS and nothing else, discovery runs in manifest mode, and persistence is switched off:

# myapp/manifest.py
AGENTS = ["agents/*.yaml"]          # globs resolve against app.code_path
# config/api.yaml
app:
  name: incident-triage
  discovery:
    mode: manifest
    manifest:
      module: myapp.manifest

persistence:
  backend: none

ai:
  engine: pydantic-ai
  models:
    default:
      provider: bedrock
      model: <model id>
      region: eu-west-1
      output_mode: native          # optional, see Model providers
  endpoints:
    incident-triage:
      enabled: true
      auth: jwt
pip install "loom-kernel[rest,ai-pydantic,ai-bedrock]"   # swap the provider extra for yours

The sqlalchemy extra is not needed: create_app imports the SQLAlchemy backend only inside the sqlalchemy wiring, so an agents-only process never loads it. (loom.celery still imports it at module level; this recipe is about the REST application, not every loom module.)

What persistence.backend: none means:

  • No unit of work and no repositories. A granted usecase capability still runs through the kernel executor, just without a transaction around it.

  • Discovered models are accepted but not compiled, and they get no repository. Declaring them under none is harmless and pointless.

  • A database: section is ignored, silently.

  • Deferred job dispatch does not fire. JobService.dispatch queues the call to run after the unit of work commits, and with no unit of work nothing flushes the queue, so a job dispatched from a use case is dropped without an error. An agents-only application that needs background jobs keeps a real backend.

Only manifest discovery can describe this application: the interfaces and modules engines require at least one module path, and their errors — like the RuntimeError raised when discovery finds no use case, no interface and no agent — say so by naming app.discovery.mode: manifest and AGENTS.

persistence.backend still defaults to sqlalchemy; loom never infers none from the absence of models or of a database: section. Leaving the default in place with no models starts the application with a WARNING that names persistence.backend: none as the way out, and still requires a reachable database.

Where to go next

  • Artifact reference — the complete spec_version: 1 format, field by field, and the folder layout an agent lives in. It includes on_output, the use case the runtime executes once per completed run with the validated output, and conversation, the use case that loads the prior turns of a conversation_id before the model starts.

  • Model providers — the provider matrix, and why an OpenAI-compatible endpoint is one provider covering many vendors.

  • A2A surface — publishing an agent to other agents, and exactly what the card does and does not say.

  • MCP deployment — where an MCP server goes in your topology, and the rule that decides mcp versus usecase.

  • Observability — OTel and Logfire, including an honest account of what the traces do not yet give you.