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.
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: []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.
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.
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.
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)" --quietBy 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)
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"}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 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 stdoutTest 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.
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- Outputs are merged into input — declared dependencies run first so their data is available:
merged = {**caller_input, **dep1_output, **dep2_output, ...} - Fail fast — required input fields are validated after the merge; missing fields raise
CHAIN_INPUT_MISSINGbefore the model is called - Linear chains only — no branching, no conditions, no loops; if you need those, express them outside OA
- Cycle detection — circular references raise
CHAIN_CYCLE_ERRORat run time
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.
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_tokensare normalised across engines (OpenAI'sprompt/completionand Anthropic'sinput/outputshapes both map to these keys).estimated_cost_usdis 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, sousageisnullthere.) usageisnullonly 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}
}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:
-
Per-spec —
intelligence.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
-
Global — the
OA_PRICINGenv var, applied to every spec (good for org-wide negotiated rates). JSON mapping model-id → rates, ornone: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_PRICINGentries override the built-in table. -
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.
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_effortis 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'seffortrequires 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_effortis set, OpenAI reasoning models needmax_completion_tokens(notmax_tokens) and reject a non-defaulttemperature, so OA switches the token field and omitstemperature; Anthropic likewise dropstemperaturealongside adaptive thinking. Standard models and OpenAI-compatible servers are unaffected. - Status: experimental spike. Validated live against Opus 4.8 and OpenAI
o4-miniwithpython scripts/verify_reasoning.py(needs API keys) — it fireslowvshighper engine and prints the token deltas (--repeat Nto average,--sleep Nto space out low-rate-limit accounts). Mappings live inoas_cli/reasoning.py,openai_http._apply_sampling_and_reasoning, andanthropic_http._apply_reasoning. oa validatechecks the value against thelow|medium|highenum.
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.
pip install 'open-agent-spec[contracts]'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 }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.
| Priority | Source |
|---|---|
| Base | Top-level behavioural_contract |
| Override (merged) | tasks.<name>.behavioural_contract |
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.
| 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": "Missing required field: 'confidence'", "code": "CONTRACT_VIOLATION", "stage": "contract", "task": "summarize"}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.
| 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.
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 timeA 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]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.
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:
- Entry to
run_task_from_spec(public API boundary) - Each dependency call in
_resolve_chain - Start of
_run_single_task(guards delegated specs too)
| 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 |
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.
See examples/sandboxed-agent/ for a working demo that exercises tool allowlists, domain restrictions, file path restrictions, and per-task sandbox overrides.
| 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.
intelligence:
type: "llm"
engine: "openai"
endpoint: "https://api.openai.com/v1"
model: "gpt-4o"
config: { temperature: 0.7, max_tokens: 1000 }intelligence:
type: "llm"
engine: "anthropic"
endpoint: "https://api.anthropic.com"
model: "claude-3-5-sonnet-20241022"
config: { temperature: 0.7, max_tokens: 1000 }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 neededexport XAI_API_KEY=xai-...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"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.2To 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"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 execconfig keys are passed as CLI flags to codex exec. Common options: sandbox (workspace-write, workspace-read, none) and cwd.
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 stringExample:
# 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 stringThe 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.
oa init aac # creates .agents/example.yaml, review.yaml, and README
oa init aac --directory ./my-repo # target a different rootoa run --spec .agents/example.yaml --task greet \
--input '{"name":"Alice"}' --quietoa init --spec .agents/ci-failure-repair.yaml --output ./repair-agentoa 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 |
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 |
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.
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.
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 automaticallyThe 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.
| 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) |
output/
├── agent.py
├── models.py # if outputs are modelled
├── prompts/
│ ├── <task>.jinja2
│ └── agent_prompt.jinja2
├── requirements.txt
├── .env.example
└── README.md
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.
git clone https://github.com/prime-vector/open-agent-spec.git
cd open-agent-spec
pip install -e ".[dev]"
pytestBuild: python -m build. Release: bump version in pyproject.toml, tag, push — CI publishes to PyPI.