Skip to content

Latest commit

 

History

History
990 lines (767 loc) · 34.2 KB

File metadata and controls

990 lines (767 loc) · 34.2 KB

Open Agent Spec — reference

For a short intro, see the README. This page is the longer reference moved out of the main README so PyPI stays simple.

Formal specification: The normative definition of OA 1.5.0 — what a conforming runtime MUST do — lives at spec/open-agent-spec-1.4.md. This reference guide covers practical usage of the CLI and reference implementation. When this guide and the spec disagree, the spec is authoritative.

Spec file structure

open_agent_spec: "1.5.0"

agent:
  name: "hello-world-agent"
  description: "A simple agent"
  role: "chat"   # optional, free-form; well-known: analyst, reviewer, chat, retriever, planner, executor, commander, coordinator

intelligence:
  type: "llm"
  engine: "openai"   # openai | anthropic | grok | cortex | codex | local | custom
  endpoint: "https://api.openai.com/v1"
  model: "gpt-4"
  config:
    temperature: 0.7
    max_tokens: 150
  # module: "MyModule.MyClass"   # required for engine: custom

tasks:
  greet:
    description: "Say hello"
    input:
      type: "object"
      properties:
        name: { type: "string", description: "Name to greet" }
      required: ["name"]
    output:
      type: "object"
      properties:
        response: { type: "string" }
      required: ["response"]

# optional
behavioural_contract:
  pii: "..."
  compliance_tags: []
  allowed_tools: []

Per-task prompts

Each task can define its own prompts block directly inside the task definition. This keeps system and user prompts co-located with the task they belong to, making multi-task agents much easier to reason about.

Style A — per-task prompts (preferred)

tasks:
  edit:
    description: Apply targeted edits to code
    prompts:
      system: |
        You are a precise code editor. Apply only the requested changes.
        Output a unified diff.
      user: "{{ instructions }}"
    input:
      type: object
      properties:
        instructions: { type: string }
      required: [instructions]
    output:
      type: object
      properties:
        diff: { type: string }
      required: [diff]

  ask:
    description: Answer a question about the codebase
    prompts:
      system: |
        You are a helpful code assistant. Be concise and accurate.
      user: "{{ question }}"
    input:
      type: object
      properties:
        question: { type: string }
      required: [question]
    output:
      type: object
      properties:
        answer: { type: string }
      required: [answer]

# Global fallback — used only when a task has no per-task prompts
prompts:
  system: "You are a general-purpose agent."
  user: "{{ input }}"

The prompts section at the top level becomes an optional fallback for any task that doesn't declare its own prompts.

Resolution order

For each run, the system and user prompt are resolved in this priority order (highest wins):

Priority Source How
1 (highest) CLI override --system-prompt / --user-prompt flag
2 Per-task inline tasks.<name>.prompts.system / .user
3 Per-task map (legacy) prompts.<name>.system / .user
4 (fallback) Global prompts.system / prompts.user

Each prompt dimension (system, user) resolves independently — you can override just the system prompt and let the user template fall through from the task or global block.

CLI prompt overrides

oa run accepts two runtime override flags:

--system-prompt TEXT    Replace the system prompt for this invocation
--user-prompt TEXT      Replace the user prompt template for this invocation

Supports the same {{ field }} placeholders as spec-defined user templates.

Examples:

# Run a task using its own per-task system prompt (no override needed)
oa run --spec .agents/code-assistant.yaml --task edit \
  --input '{"instructions":"Remove all debug print statements"}' --quiet

# Override the system prompt for a one-off targeted instruction
oa run --spec .agents/code-assistant.yaml --task ask \
  --input '{"question":"What does the auth module do?"}' \
  --system-prompt "You are a senior Go engineer. Be terse." --quiet

# Load a long system prompt from a file
oa run --spec .agents/code-assistant.yaml --task edit \
  --input '{"instructions":"..."}' \
  --system-prompt "$(cat .prompts/edit-system.txt)" --quiet

response_format: text

By default every task expects the model to return valid JSON, which the runner parses and validates against the task's output schema. For tasks where the desired output is natural prose (explanations, summaries, diffs, etc.) you can opt out of JSON parsing entirely:

tasks:
  explain:
    description: Explain what this function does
    response_format: text          # raw string output, no JSON parsing
    output:
      type: object
      properties:
        explanation: { type: string }
    prompts:
      system: |
        You are a helpful code explainer. Be concise.
      user: "{{ code }}"
    input:
      type: object
      properties:
        code: { type: string }
      required: [code]

When response_format: text:

  • The model's raw output string is returned directly as result["output"]
  • Output schema validation is skipped — the task author owns the contract
  • Markdown fences and JSON-parsing are both bypassed
  • Default is "json" (or omitting the field entirely)

Structured errors

When oa run --quiet encounters a failure it emits a machine-readable JSON object to stderr (stdout stays clean for piping). Example:

{"error": "Task 'explain' not found in spec", "code": "TASK_NOT_FOUND", "stage": "routing", "task": "explain"}

Error codes

code stage Trigger
SPEC_LOAD_ERROR load File not found, YAML parse error
TASK_NOT_FOUND routing --task name absent from spec, or unknown depends_on reference
RUN_ERROR run invoke_intelligence raises an exception
CHAIN_CYCLE_ERROR routing Circular depends_on chain detected
CHAIN_INPUT_MISSING input_validation Required input field missing after dependency merge
CONTRACT_VIOLATION contract Task output failed behavioural contract validation
PRICING_CONFIG_ERROR cost A cost-rate override (config.pricing / OA_PRICING) is present but invalid

Verbose mode (oa run without --quiet) prints the error to the terminal as plain text, unchanged from prior behaviour.

oa test (eval cases)

oa test loads a YAML file that points at a spec and lists cases. Each case runs one task (with the same depends_on resolution as oa run), then asserts on the parsed task output using expect rules. Use this for regression checks and CI gates; use oa validate for schema-only checks.

oa test path/to/agent.test.yaml
oa test path/to/agent.test.yaml --quiet   # single JSON summary on stdout

Test file shape

spec: ./agent.yaml          # relative to this file’s directory
cases:
  - name: optional label
    task: greet             # optional; defaults like `oa run`
    input: { name: "CI" }
    expect:
      output.response: { contains: "hello" }
      output.items: { min_length: 1, type: array }
      output.items[0].id: { type: string }

Keys under expect must be output or start with output.; the remainder is a dotted path with optional [index] segments (e.g. output.questions[0]).

Rules (all rules under a path must pass):

Rule Meaning
min_length / max_length For strings or lists, len(value) bound
contains Substring (default case-insensitive; set case_sensitive: true otherwise)
equals Exact equality
type One of string, number, boolean, object / dict, array / list

Omit expect or use {} for a smoke case that only checks the task completes without error.


depends_on — data dependencies between tasks

Design principle

OA describes what data a task needs and how it is implemented — never how execution should proceed.

depends_on is a data contract, not a workflow instruction. When a task declares depends_on: [extract], it is saying:

"I require extract's output fields as part of my input."

Execution ordering is a side-effect of satisfying that data dependency — not the purpose.

This distinction matters. OA intentionally does not support and will not add:

Feature Why it's out of scope
Branching / conditionals Execution control — belongs in the calling platform
Loops / retries Runtime policy — belongs in the calling platform
Parallel execution Scheduling — belongs in the calling platform
Fallback tasks Dynamic routing — belongs in the calling platform
Dynamic task selection Orchestration — belongs in the calling platform

If you need any of the above, express them outside OA in whatever platform or orchestrator is invoking the spec. OA stays composable and inspectable precisely because it refuses to become a workflow engine.


A task can declare that it needs the output of another task before it can run:

tasks:
  extract:
    description: Extract key facts from a document
    output:
      type: object
      properties:
        facts: { type: string }
      required: [facts]
    prompts:
      system: "Extract the three most important facts."
      user: "{{ document }}"
    input:
      type: object
      properties:
        document: { type: string }
      required: [document]

  summarize:
    description: Summarize the extracted facts
    depends_on: [extract]          # runs extract first
    output:
      type: object
      properties:
        summary: { type: string }
    prompts:
      system: "Summarize the following facts in one sentence."
      user: "{{ facts }}"          # facts injected from extract's output

Execution rules

  1. Outputs are merged into input — declared dependencies run first so their data is available:
    merged = {**caller_input, **dep1_output, **dep2_output, ...}
    
  2. Fail fast — required input fields are validated after the merge; missing fields raise CHAIN_INPUT_MISSING before the model is called
  3. Linear chains only — no branching, no conditions, no loops; if you need those, express them outside OA
  4. Cycle detection — circular references raise CHAIN_CYCLE_ERROR at run time

Result envelope

The final result includes all intermediate results in a chain key:

{
  "task": "summarize",
  "output": {"summary": "The sky is blue, water is wet, and ice is cold."},
  "chain": {
    "extract": {
      "task": "extract",
      "output": {"facts": "sky=blue; water=wet; ice=cold"}
    }
  }
}

Tasks with no depends_on do not include a chain key.

Token usage and cost

When the engine reports token counts, the result envelope includes a usage key so you can track and budget spend without a separate accounting layer:

{
  "task": "summarize",
  "output": {"summary": "..."},
  "usage": {
    "prompt_tokens": 1200,
    "completion_tokens": 350,
    "total_tokens": 1550,
    "estimated_cost_usd": 0.0065
  }
}
  • prompt_tokens / completion_tokens / total_tokens are normalised across engines (OpenAI's prompt/completion and Anthropic's input/output shapes both map to these keys).
  • estimated_cost_usd is best-effort — present only for models in the built-in price table (oas_cli/usage.py), and omitted otherwise rather than guessed. List prices drift, so treat it as indicative, not billing.
  • It is a pay-as-you-go API list-price estimate (tokens × public $/token). It does not reflect a subscription/seat plan (ChatGPT, Claude Max, etc.), committed-use or enterprise-negotiated rates, Bedrock/Vertex pricing, or a local model (zero marginal cost). Under those arrangements the dollar figure is not your real spend — but the token counts are always accurate, and are the right number to track usage/quota against any plan. (Subscription-routed paths like the Codex CLI report no tokens at all, so usage is null there.)
  • usage is null only when the engine does not report counts (e.g. some local servers, the Codex CLI, custom routers). Multi-turn tool-calling is covered: usage is summed across every turn of the loop (each turn re-sends the growing history, so the sum reflects what is actually billed).

oa run (without --quiet) shows a compact <total> tok · ~$<cost> summary in the result panel. --quiet still emits only the task output on stdout (clean for | jq); pass --usage PATH to write a JSON file with the leaf usage, any depends_on chain, and a rolled-up total so a script can meter spend without under-counting chained tasks:

oa run --spec .agents/example.yaml --task greet \
  --input '{"name":"Alice"}' --quiet --usage /tmp/usage.json
{
  "task": "summarize",
  "usage": {"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15},
  "chain": {
    "extract": {
      "task": "extract",
      "usage": {"prompt_tokens": 20, "completion_tokens": 10, "total_tokens": 30}
    }
  },
  "total": {"prompt_tokens": 30, "completion_tokens": 15, "total_tokens": 45}
}

Overriding the cost rate

Because the default is a list price, you can substitute your own rate — or turn the dollar figure off where it isn't meaningful (subscription, local model). Rates resolve in layers, first match wins:

  1. Per-specintelligence.config.pricing:

    intelligence:
      engine: anthropic
      model: claude-opus-4-8
      config:
        pricing:
          input_per_1m: 4.20      # your negotiated rate, USD per 1M tokens
          output_per_1m: 21.00
        # or:  pricing: none      # tokens-only, no dollar estimate
  2. Global — the OA_PRICING env var, applied to every spec (good for org-wide negotiated rates). JSON mapping model-id → rates, or none:

    export OA_PRICING='{"claude-opus-4-8": {"input": 4.2, "output": 21}}'
    export OA_PRICING=none        # disable cost everywhere

    Model ids match by longest prefix (so a dated suffix still resolves), and OA_PRICING entries override the built-in table.

  3. Built-in — the hand-maintained table in oas_cli/usage.py.

A per-spec value overrides OA_PRICING, which overrides the built-in table; a none at any layer disables cost from that layer down (a more specific layer can still re-enable it with explicit rates).

Invalid overrides fail closed. A pricing override that is present but malformed — a negative rate, a pricing string other than none, or an OA_PRICING value that isn't valid JSON / none — raises rather than silently reverting to the built-in list price. Through the runner it surfaces as the dedicated structured error PRICING_CONFIG_ERROR (stage cost), distinct from a model/run failure, so operator misconfiguration is actionable. (oa validate also rejects a bad per-spec pricing up front; the runtime check additionally guards the Python API and OA_PRICING.) A model simply not listed in a valid OA_PRICING map is not an error — it falls through to the built-in table.

Reasoning effort (cost tiering) — experimental

Frontier models increasingly expose a reasoning-effort control: spend more compute on hard tasks, less on easy ones. Declare one portable tier in the spec and OA maps it to each engine's native knob:

intelligence:
  type: "llm"
  engine: "anthropic"
  model: "claude-opus-4-8"
  config:
    reasoning_effort: "low"   # low | medium | high
Engine Mapped to
openai (Chat Completions) reasoning_effort request field
openai (Responses API) reasoning.effort
codex -c model_reasoning_effort=<tier> CLI override
anthropic output_config.effort (a near 1:1 tier mapping), paired with adaptive thinking. OA also drops temperature, which current Claude models reject alongside adaptive thinking.

Notes:

  • Opt-in and model-specific. reasoning_effort is only meaningful for reasoning-capable models — the author pairs it with a suitable model/engine. Non-reasoning OpenAI models (e.g. gpt-4o) reject the parameter; Anthropic's effort requires a capable model (Opus 4.5+, Sonnet 4.6, Fable 5 — it errors on Sonnet 4.5 / Haiku 4.5).
  • Provider-specific request shaping. When reasoning_effort is set, OpenAI reasoning models need max_completion_tokens (not max_tokens) and reject a non-default temperature, so OA switches the token field and omits temperature; Anthropic likewise drops temperature alongside adaptive thinking. Standard models and OpenAI-compatible servers are unaffected.
  • Status: experimental spike. Validated live against Opus 4.8 and OpenAI o4-mini with python scripts/verify_reasoning.py (needs API keys) — it fires low vs high per engine and prints the token deltas (--repeat N to average, --sleep N to space out low-rate-limit accounts). Mappings live in oas_cli/reasoning.py, openai_http._apply_sampling_and_reasoning, and anthropic_http._apply_reasoning.
  • oa validate checks the value against the low|medium|high enum.

Behavioural contracts (optional BCE integration)

Behavioural contracts let you declare constraints on a task's output — required fields, policy rules, behavioural flags — and have them enforced automatically at run time by the behavioural-contracts library.

Contracts are entirely optional. Specs without them run exactly as before. When a resolved task declares a contract but the enforcement library is not installed, OA fails closed with CONTRACTS_UNAVAILABLE before the affected task makes a model call. Locally delegated tasks are recursively preflighted before their containing chain starts; remote delegated tasks are checked immediately after fetch.

Install

pip install 'open-agent-spec[contracts]'

Per-task contract (preferred)

Declare a behavioural_contract block inside any task definition:

tasks:
  summarize:
    description: Summarise extracted facts
    depends_on: [extract]
    behavioural_contract:
      version: "1.0"
      description: "Summarize task must always return a summary field"
      response_contract:
        output_format:
          required_fields: [summary]
    output:
      type: object
      properties:
        summary: { type: string }

Global contract + per-task contract — merge semantics

A top-level behavioural_contract block acts as a baseline that applies to every task. Per-task contracts are merged on top — not replaced. Arrays (like required_fields) are unioned; scalars use per-task-wins.

# Global: every task must output 'confidence'
behavioural_contract:
  version: "1.0"
  description: "Global baseline"
  response_contract:
    output_format:
      required_fields: [confidence]

tasks:
  summarize:
    behavioural_contract:
      version: "1.0"
      description: "Also requires summary"
      response_contract:
        output_format:
          required_fields: [summary]
    # Effective required_fields for this task: [confidence, summary]

This lets you enforce cross-cutting guarantees (e.g. every task must include confidence) in one place without repeating them per task.

Contract resolution order

Priority Source
Base Top-level behavioural_contract
Override (merged) tasks.<name>.behavioural_contract

Where validation runs

Contracts are enforced after output parsing, before the result is returned — and for chain dependencies, before the dep output is merged into the next task's input:

extract → [parse] → [contract check] → merge into summarize input
summarize → [parse] → [contract check] → return result

A contract violation on a dependency stops the chain immediately and raises CONTRACT_VIOLATION before the dependent task ever runs.

Skipped and unavailable cases

Condition Behaviour
response_format: text Validation skipped — field checks are meaningless on raw strings
behavioural-contracts not installed Execution fails before the affected task's model call with CONTRACTS_UNAVAILABLE
Output is not a dict (JSON parse failed) Validation skipped with warning

Error on violation

{"error": "Missing required field: 'confidence'", "code": "CONTRACT_VIOLATION", "stage": "contract", "task": "summarize"}

Immutable Inference Sandboxing (IIS)

OA gives every spec a sandbox: block that defines what a task is mechanically permitted to do. The runner enforces these constraints before any tool call reaches the I/O layer — no network connection is opened, no file handle is created, no exception needs to be caught.

The boundary

Layer Concern Mechanism
OA sandbox: Hard execution constraints Runner blocks before dispatch
BCE behavioural_contract: Policy / quality contract Validated after the run

OA controls what a task can do. BCE controls what a task should do. They are complementary and never overlap.

Root-level sandbox (applies to all tasks)

sandbox:
  tools:
    allow: [file.read, http.get]   # allowlist — anything else is SANDBOX_TOOL_VIOLATION
    # deny: [file.write]           # denylist alternative (use one or the other)
  http:
    allow_domains:
      - api.example.com            # exact match or any subdomain, any port
      - localhost:3000             # optional port pinning
  file:
    allow_paths:
      - ./data/                    # resolved to absolute paths at check time

Per-task sandbox (overrides root)

A sandbox: key inside a task definition completely overrides the root sandbox for that task. Use this to tighten constraints on sensitive tasks without changing the global default:

sandbox:
  tools:
    allow: [file.read, http.get]

tasks:
  check_status:
    sandbox:
      tools:
        allow: [http.get]          # file.read now also blocked for this task
      http:
        allow_domains: [status.openai.com]

Error codes

All sandbox violations raise OARunError immediately with one of three structured codes:

Code Trigger
SANDBOX_TOOL_VIOLATION Tool name not in allow list, or in deny list
SANDBOX_DOMAIN_VIOLATION HTTP, MCP, or declared/resolved remote delegated-spec destination not in allow_domains; host:port rules require that port
SANDBOX_PATH_VIOLATION File path outside allow_paths (for file.read / file.write)

Path traversal (../../) is caught automatically — paths are resolved to absolute before comparison.

MCP endpoints and declared remote delegated-spec URLs are static spec configuration and are checked against the current task's effective http.allow_domains policy before discovery, the initial fetch, or model execution. For oa:// references, allow the resolved registry host (openagentspec.dev). Redirect destinations and sandbox inheritance across delegated documents are separate concerns. A bare hostname preserves the original any-port behaviour; use host:port when the agent must reach only one service on that host.

Input immutability

Every task receives a deep copy of its input. Chain outputs merged into downstream inputs never mutate the caller's original dict. This is enforced at three levels:

  1. Entry to run_task_from_spec (public API boundary)
  2. Each dependency call in _resolve_chain
  3. Start of _run_single_task (guards delegated specs too)

What IIS deliberately excludes (belongs in BCE)

Concern Why it's BCE
Prompt injection detection Requires interpretation — subjective, evolving
PII scanning Team-specific policy
Compliance tagging Audit concern, not execution constraint
Session / memory management Out of scope for a stateless runner

Future BCE rename note

The BCE library currently uses allowed_tools in behavioural_contract as an audit field. A future BCE release will rename this to expected_tools to make it unambiguous that this is a post-run audit assertion, not an enforcement rule. The OA sandbox.tools.allow list is the enforcement mechanism; BCE's audit field is observational only.

Example

See examples/sandboxed-agent/ for a working demo that exercises tool allowlists, domain restrictions, file path restrictions, and per-task sandbox overrides.


Engines (quick)

Engine Env var Notes
openai OPENAI_API_KEY Chat Completions or Responses API
anthropic ANTHROPIC_API_KEY Messages API
grok / xai XAI_API_KEY OpenAI-compatible; routes to api.x.ai
cortex OPENAI_API_KEY OpenAI-compatible; set endpoint in spec
local (none) OpenAI-compatible local server (Ollama, LM Studio, …)
codex Codex CLI on PATH codex login required
custom (user-defined) HTTP endpoint or Python class via module:

All engines except anthropic and codex speak the OpenAI Chat Completions API. oa run uses raw HTTP — no SDK required.

OpenAI

intelligence:
  type: "llm"
  engine: "openai"
  endpoint: "https://api.openai.com/v1"
  model: "gpt-4o"
  config: { temperature: 0.7, max_tokens: 1000 }

Anthropic

intelligence:
  type: "llm"
  engine: "anthropic"
  endpoint: "https://api.anthropic.com"
  model: "claude-3-5-sonnet-20241022"
  config: { temperature: 0.7, max_tokens: 1000 }

Grok / xAI (OpenAI-compatible)

engine: grok and engine: xai are aliases — both route to https://api.x.ai/v1 with XAI_API_KEY. Endpoint and model can be overridden in the spec.

intelligence:
  type: "llm"
  engine: "grok"          # or "xai"
  model: "grok-3-latest"  # default; override as needed
export XAI_API_KEY=xai-...

Cortex (OpenAI-compatible, user-hosted)

cortex is treated as an OpenAI-compatible endpoint. Provide your endpoint in the spec; OPENAI_API_KEY is used by default but can be overridden via config.api_key_env.

intelligence:
  type: "llm"
  engine: "cortex"
  endpoint: "https://cortex.mycompany.com/v1"
  model: "my-cortex-model"
  config:
    api_key_env: "CORTEX_API_KEY"

Local LLM (Ollama, LM Studio, vLLM, …)

engine: local points to a local OpenAI-compatible server. No API key is required. The default endpoint is http://localhost:11434/v1 (Ollama). Override endpoint and model as needed.

intelligence:
  type: "llm"
  engine: "local"
  model: "llama3.2"     # default; match whatever model you have pulled
# Start Ollama (example)
ollama serve
ollama pull llama3.2

To use a different local server (e.g. LM Studio on port 1234):

intelligence:
  type: "llm"
  engine: "local"
  endpoint: "http://localhost:1234/v1"
  model: "mistral-7b"

Codex

Runs Codex CLI non-interactively via the built-in adapter (oas_cli/adapters/codex_adapter.py). Requires codex on PATH and codex login.

intelligence:
  type: "llm"
  engine: "codex"
  model: "gpt-4.1-codex"
  config:
    sandbox: "workspace-write"   # codex sandbox mode
    cwd: "."                     # working directory for codex exec

config keys are passed as CLI flags to codex exec. Common options: sandbox (workspace-write, workspace-read, none) and cwd.

Custom router

Two modes:

HTTP mode — no Python glue, just an OpenAI-compatible endpoint:

intelligence:
  type: "llm"
  engine: "custom"
  endpoint: "https://my-llm-proxy.internal/v1"
  model: "my-model"

Class mode — point to a Python class for full control:

intelligence:
  type: "llm"
  engine: "custom"
  endpoint: "http://localhost:1234/invoke"
  model: "my-model"
  module: "my_package.router.MyRouter"

The class must implement:

class MyRouter:
    def __init__(self, endpoint: str, model: str, config: dict): ...
    def run(self, prompt: str, **kwargs) -> str: ...  # returns JSON string

Example:

# my_package/router.py
import json, requests

class MyRouter:
    def __init__(self, endpoint, model, config):
        self.endpoint = endpoint
        self.model = model

    def run(self, prompt, **kwargs):
        resp = requests.post(self.endpoint, json={"prompt": prompt, "model": self.model})
        return resp.text  # must be a JSON string

Agents as code (.agents/)

The agent-as-code pattern stores spec files in a .agents/ directory at the root of your repository — similar to how .github/workflows/ stores CI pipelines. Specs in .agents/ are treated as infrastructure-as-code: check them into version control, run them directly with oa run, or generate full project scaffolds from them.

Scaffold the layout

oa init aac                          # creates .agents/example.yaml, review.yaml, and README
oa init aac --directory ./my-repo    # target a different root

Run an agent directly

oa run --spec .agents/example.yaml --task greet \
  --input '{"name":"Alice"}' --quiet

Generate code from an agent spec

oa init --spec .agents/ci-failure-repair.yaml --output ./repair-agent

Generated by oa init aac

oa init aac creates these files in your project (they are not shipped in the repo at install time):

File Role Engine Description
example.yaml chat openai Minimal hello-world spec — good starting point
review.yaml reviewer openai Reviews a git diff and returns a decision plus summary
README.md Quick usage notes for the .agents/ directory

Shipped in this repository

This repository's own .agents/ directory contains four specs used for development and CI:

File Role Engine Description
hello-world-agent.yaml chat openai Simple greeting — mirrors the generated example.yaml
ci-failure-repair.yaml analyst openai Diagnoses GitHub Actions failures and emits remediation commands. Used by .github/workflows/ci-failure-repair.yml.
codex-runner.yaml executor codex Runs Codex CLI non-interactively for arbitrary instructions
review.yaml reviewer openai Reviews a git diff and returns a decision plus summary

Memory management

OA is stateless by design — it never stores, summarises, or manages conversation history. This keeps specs portable and infrastructure-agnostic. Two patterns cover the common memory use-cases without breaking that boundary.

Pattern 1 — Short-term history (within a session)

Pass prior turns as the reserved history input field. The runner automatically injects them between the system prompt and the current user message before calling the model. Your application code manages the list; OA just forwards it.

tasks:
  chat:
    input:
      type: object
      properties:
        message:  { type: string }
        history:
          type: array
          description: >
            Prior turns — [{"role": "user"|"assistant", "content": "…"}, …].
            Injected by the runner automatically. OA never writes to this field.
          items:
            type: object
            properties:
              role:    { type: string, enum: [user, assistant] }
              content: { type: string }
      required: [message]

Caller example (Python):

history = []

while True:
    message = input("You: ")
    result = run_task(spec_path, "chat", {"message": message, "history": history})
    reply = result["reply"]
    print(f"Agent: {reply}")

    history.append({"role": "user",      "content": message})
    history.append({"role": "assistant", "content": reply})

See examples/chat-agent/ for a full working example.

Pattern 2 — Long-term memory (across sessions)

OA specs are pure LLM interfaces — they cannot make HTTP calls during prompt rendering. The long-term memory pattern therefore has two distinct layers:

Layer Owner Responsibility
Memory store Your infrastructure Persist, index, and search prior turns
Memory re-ranker oa://prime-vector/memory-retriever Use the LLM to select the most relevant candidates

Your application code fetches raw candidate turns from the store, then passes them as the candidates input field. The memory-retriever spec uses the LLM to re-rank and select the most relevant ones, returning a history array ready for depends_on chaining.

tasks:
  recall:
    spec: oa://prime-vector/memory-retriever
    task: retrieve
    # input: { query, candidates: [...pre-fetched turns], top_k }

  respond:
    depends_on: [recall]
    spec: ../chat-agent/spec.yaml
    task: chat
    # history from recall is merged in automatically

The runner merges recall's output (including history) into respond's input automatically — no glue code required in the spec.

See examples/memory-chat/ for a runnable pipeline with a pre-populated candidates input.

What OA deliberately does NOT do

Capability Where it belongs
Session persistence Your application / infrastructure
History summarisation A dedicated summarisation spec (oa://prime-vector/summariser)
Memory write / upsert Your memory store's write endpoint
Branching on memory content Outside OA (OA has no conditionals)

Generated project layout

output/
├── agent.py
├── models.py            # if outputs are modelled
├── prompts/
│   ├── <task>.jinja2
│   └── agent_prompt.jinja2
├── requirements.txt
├── .env.example
└── README.md

Bundled templates

YAMLs ship inside the package; from a clone you can do:

oa init --spec oas_cli/templates/minimal-multi-task-agent.yaml --output my-multi-agent/
oa init --spec oas_cli/templates/minimal-agent-tool-usage.yaml --output tool-agent/

Or point --spec at any file on disk.

Development

git clone https://github.com/prime-vector/open-agent-spec.git
cd open-agent-spec
pip install -e ".[dev]"
pytest

Build: python -m build. Release: bump version in pyproject.toml, tag, push — CI publishes to PyPI.