Agents in YAML
Omnigent has no "agent class" you subclass. An agent is a YAML document — identity, system prompt, which model/harness runs the loop, which tools and sub-agents it may call, and what policies gate it. Everything that makes an agent this agent is declarative data, and the runtime reads that data to wire up a runnable loop.
Skills-as-markdown flag: the agent definition itself is YAML, not markdown. (Skills attached to an agent are markdown —
SKILL.mdwith frontmatter — but the agent that loads them is YAML.)
There are two YAML shapes, distinguished by one key:
| Shape | Discriminator | Doc | Layout |
|---|---|---|---|
| Single-file launcher | name + prompt, no spec_version |
docs/AGENT_YAML_SPEC.md |
one .yaml file |
| Agent image / bundle | spec_version: 1 |
omnigent/spec/AGENTSPEC.md |
a directory with config.yaml |
The single-file shape is what omnigent run agent.yaml is built around
and is the focus here. The bundle shape is the portable artifact the
server stores as a tarball; the four shipped examples
(examples/polly, examples/debby, examples/scribe,
examples/kimi_hello.yaml) are mostly bundles using the same field
vocabulary.
The discriminator is enforced in code: spec_version is the
discriminator key (omnigent/spec/_omnigent_compat.py:140), and a file that declares
it is rejected as a single-file YAML and must be a directory bundle
(omnigent/spec/_omnigent_compat.py:217, :265-273).
1. The minimal agent
name: hello_agent
prompt: |
You are a concise assistant. Answer directly and ask a follow-up
question when the request is ambiguous.
executor:
harness: claude-sdk
model: databricks-claude-sonnet-4-6
auth:
type: databricks
profile: ossname + a system prompt + an executor is the whole contract. Detection
requires exactly this: .yaml/.yml extension, a top-level mapping, a
name key, at least one of prompt/instructions, and no
spec_version (omnigent/spec/_omnigent_compat.py:189-222,
is_omnigent_yaml).
prompt may be replaced by instructions: AGENTS.md — a path resolved
relative to the YAML file, or inline text if it doesn't match a file
(docs/AGENT_YAML_SPEC.md:28-29). At least one system-prompt key must
be present or the load fails loud (_OMNIGENT_SYSTEM_PROMPT_KEYS,
omnigent/spec/_omnigent_compat.py:139).
2. Field reference
Every field below is repo-grounded — each ties to the code that reads it.
Top-level
| Field | Purpose | Consumed at |
|---|---|---|
name |
Stable id shown in sessions/logs; required for detection. | omnigent/spec/_omnigent_compat.py:219 |
prompt |
Inline system prompt. | _OMNIGENT_SYSTEM_PROMPT_KEYS, omnigent/spec/_omnigent_compat.py:139 |
instructions |
Inline text or a path to an instructions file; takes precedence over prompt. |
docs/AGENT_YAML_SPEC.md:37 |
description |
Free-form summary. | omnigent/spec/_omnigent_compat.py (carried) |
executor |
Harness + model + auth. | omnigent/inner/loader.py:259-261, _parse_executor_spec omnigent/inner/loader.py:617 |
tools |
MCP servers, Python function tools, sub-agents, handoffs, inherited tools. | docs/AGENT_YAML_SPEC.md:198; tools.agents → omnigent/spec/omnigent.py:358-366 |
policies / guardrails.policies |
Guardrails inspecting requests/responses/tool-calls/tool-results. | omnigent/spec/omnigent.py:214-224; handlers omnigent/inner/nessie/policies.py |
params |
Typed user parameters readable by tools/skills. | docs/AGENT_YAML_SPEC.md:41 |
os_env |
Enables local OS tools (sys_os_read/write/edit/shell). |
_parse_os_env_spec omnigent/inner/loader.py:648; forwarded omnigent/spec/omnigent.py:195 |
terminals |
Named interactive shell environments the agent can launch. | docs/AGENT_YAML_SPEC.md:320 |
async |
Whether async work tools are exposed. Default true. |
docs/AGENT_YAML_SPEC.md:44 |
cancellable |
Whether the session can be cancelled. Default true. |
docs/AGENT_YAML_SPEC.md:45 |
timers |
Whether timer tools are exposed. Default false. |
docs/AGENT_YAML_SPEC.md:46 |
spawn |
Registers sys_session_create so the agent can author + launch child agents. |
examples/polly/config.yaml:16 |
executor
executor:
harness: claude-sdk # which agent loop runs the turn
model: databricks-claude-opus-4-7
auth:
type: databricks
profile: oss # Databricks routing profile_parse_executor_spec reads the flat harness / model / profile
and parses auth into a typed dataclass (omnigent/inner/loader.py:639-644). A bare
string executor: <model> is also accepted as just the model
(omnigent/inner/loader.py:620-621).
The accepted harness ids are a frozen set
(OMNIGENT_HARNESSES, omnigent/spec/_omnigent_compat.py:80-106) plus user-facing
aliases (OMNIGENT_HARNESS_ALIASES, :108-126):
antigravity claude-native claude-sdk codex codex-native copilot
cursor cursor-native goose hermes kimi kiro-native openai-agents
open-responses opencode-native pi qwen (+ *-native variants)
An unrecognised harness fails validation with must be one of [...]
(validate_omnigent_executor, omnigent/spec/_omnigent_compat.py:179-183). Aliases
(claude, kimi-code, opencode, github-copilot, …) are
canonicalised before dispatch (canonicalize_harness).
Bundle examples use a nested variant, executor.type: omnigent with the
harness under executor.config:
executor:
type: omnigent
config:
harness: claude-sdkHere executor.config.harness is required and validated against the
same set (omnigent/spec/_omnigent_compat.py:172-183). When no model is pinned, the
harness resolves the configured provider's default
(examples/debby/config.yaml:30-34); when no harness is set, it's
inferred from the model prefix (_infer_harness_from_model,
omnigent/spec/omnigent.py:164-166).
Three harnesses are special-cased in the spec doc:
cursor talks only to Cursor's backend (no Databricks gateway, use
CURSOR_API_KEY); antigravity is Gemini-native; copilot needs a
GitHub token with Copilot access; kimi authenticates through its own
kimi login and declares no executor.auth
(docs/AGENT_YAML_SPEC.md:62-141).
os_env — local OS access
os_env:
type: caller_process
cwd: .
sandbox:
type: linux_bwrap # omit to get the platform default
write_paths: [.]
allow_network: trueDeclaring os_env is what registers the filesystem/shell tools
(sys_os_read / sys_os_write / sys_os_edit / sys_os_shell —
filesystem comes bundled with a shell), parsed by _parse_os_env_spec
(omnigent/inner/loader.py:648) and forwarded onto the spec at omnigent/spec/omnigent.py:195.
Omit sandbox.type and Omnigent picks linux_bwrap on Linux /
darwin_seatbelt on macOS, so the same YAML is cross-platform
(docs/AGENT_YAML_SPEC.md:191-195). Trusted local dev uses
sandbox.type: none (every example does, to stay loadable on macOS).
Some harnesses own their tools internally and need no os_env —
kimi advertises handles_tools_internally=True, so adding os_env
would just duplicate every op against Omnigent's dispatch path
(examples/kimi_hello.yaml:36-40).
tools
Tools are declared by name under tools. Four kinds:
tools:
github: # MCP server (command or url)
type: mcp
command: uv
args: [run, python, -m, my_package.github_mcp]
tools: [search_issues, get_pull_request]
summarize_file: # Python function tool
type: function
description: Summarize a local text file.
callable: my_package.tools.summarize_file
parameters: { type: object, properties: { path: { type: string } }, required: [path] }
reviewer: # sub-agent tool — its own harness/model
type: agent
prompt: |
You are a careful code reviewer. Focus on correctness, tests, security.
executor: { harness: claude-sdk, model: databricks-claude-sonnet-4-6 }
os_env: inherit
pass_history: true
max_sessions: 2
agents: # bundle shorthand: sub-agent dir names
- claude_code
- codex
- pitools.agents lists sub-agent names; each becomes a callable AgentTool
the inner harness surfaces to the LLM
(omnigent/spec/omnigent.py:358-366). Listing a sub-agent is sufficient to call
it — no separate agent.call builtin
(omnigent/spec/AGENTSPEC.md:114-118). The model is allowlist-only: anything not
listed is rejected at call time (omnigent/spec/AGENTSPEC.md:345-349).
tools.<name>: inherit inherits a parent tool; spec: self clones the
parent spec (docs/AGENT_YAML_SPEC.md:292-293).
policies / guardrails
guardrails:
ask_timeout: 86400 # human-approval window for an ASK
policies:
blast_radius:
type: function
on: [tool_call]
function:
path: omnigent.inner.nessie.policies.blast_radius
arguments: { gate_pushes: false }A policy names a dotted-path handler, an on: phase list (request,
response, tool_call, tool_result), and arguments/factory_params
for a factory. The compiled engine runs before the harness receives a
turn (omnigent/spec/omnigent.py:214-224). Shipped handlers
(omnigent/inner/nessie/policies.py):
blast_radius(policies.py:346) — classifies shell commands by reversibility: catastrophic (force-push,rm -rf /, hard-reset to a remote ref) → DENY; recoverable-but-outward (git push,gh pr merge, deploy) → ASK; everything else → ALLOW. Matchessys_os_shelland nativeBash/bashso one policy covers every harness (policies.py:377-403).spawn_bounds(policies.py:408) — caps dispatches per turn so fan-out happens in bounded waves.headless_subagent_purpose_guard(policies.py:467) — restricts sub-agent dispatches to anallowed_purposesallowlist.
terminals
terminals:
shell:
command: bash
args: [-l]
os_env: inherit # same sandbox as the agent
allow_cwd_override: true
allow_sandbox_override: falseNamed interactive shells the agent launches with sys_terminal_launch
— for long-running processes (dev servers, watchers, log tails) that
don't fit sys_os_shell's one-shot blocking model
(examples/polly/config.yaml:261-269).
3. From YAML to a running loop
omnigent run agent.yaml
│
▼
spec.load(path) omnigent/spec/__init__.py:159
│ source.is_file() → is_omnigent_yaml(path)? :241-250
▼ yes
load_omnigent_yaml(path) omnigent/spec/_omnigent_compat.py:288
│
├─ load_agent_def(path) omnigent/inner/loader.py:75
│ parses executor/os_env/tools/policies → AgentDef
│
├─ agent_def_to_agent_spec(...) omnigent/spec/omnigent.py
│ translate to typed AgentSpec (executor.type="omnigent")
│
└─ validate(spec) omnigent/spec/validator.py
validate_omnigent_executor omnigent/spec/_omnigent_compat.py:146
│
▼
AgentSpec → harness factory → ExecutorAdapter(executor_factory=…)
e.g. omnigent/inner/claude_sdk_harness.py:309
Step by step:
-
Dispatch.
spec.loadbranches on the source. A file that passesis_omnigent_yamlroutes to the single-file adapter (omnigent/__init__.py:245-250); a directory parses as a bundle; a YAML that fails detection gets a precise diagnosis instead of a misleading tarball error (diagnose_yaml_rejection,omnigent/__init__.py:251-262). -
Parse.
load_agent_defreads the raw YAML into anAgentDef, pullingexecutor(omnigent/inner/loader.py:259-261),os_env,tools, andpoliciesinto typed sub-objects. -
Translate.
agent_def_to_agent_speclowers theAgentDefinto the canonical typedAgentSpecwithexecutor.type == "omnigent", inferring the harness from the model when unset (omnigent/spec/omnigent.py:164-166) and turningtools.agentsinto AgentTool entries (omnigent/spec/omnigent.py:358-366). -
Validate.
validateruns the shared validator;validate_omnigent_executorenforces the harness allowlist and rejects fields the harness manages itself (compaction— the harness owns its context window,omnigent/spec/_omnigent_compat.py:166-171). Failure raisesOmnigentErrornaming the exact field — fail-loud. -
Instantiate. The validated
AgentSpec'sexecutor.harnessselects a per-harnessExecutorAdapter, each built from anexecutor_factory(e.g.omnigent/inner/claude_sdk_harness.py:309,omnigent/inner/codex_harness.py:328,omnigent/inner/cursor_harness.py:144,omnigent/inner/copilot_harness.py:144). That adapter is the agent loop: it owns the turn, the tool dispatch, and the model client. The YAML'sos_env,policies, and sub-agents are wired around it. The harness id is normalised underscore→hyphen against_UCODE_HARNESS_CONFIGS(omnigent/runtime/workflow.py:182) on the way to the runtime.
So the harness id in YAML is a late-bound dispatch key: the same spec maps onto Claude Code, Codex, Cursor, Gemini, Kimi, Pi, etc. by swapping one string.
The bundle path differs at steps 2-4. A directory bundle
(spec_version: 1, the four shipped examples) skips
load_omnigent_yaml and parses directly: spec.load →
parse(root) (omnigent/spec/parser.py:115, reading config.yaml,
sub-objects via _parse_executor :487 / _parse_os_env :643 /
_parse_guardrails :2517 / _discover_sub_agents :2475) →
validate(spec) (omnigent/spec/validator.py:77). Function-policy
handlers stay unresolved dotted strings until build time, when
resolve_function_policy (omnigent/policies/function.py:256) imports
the callable. From the validated AgentSpec onward (step 5) both paths
converge.
4. Two worked examples
examples/scribe — a docs orchestrator (bundle)
A directory bundle: config.yaml plus agents/researcher/,
agents/reviewer/, and skills/{changelog,migration-guide,api-docs}/.
- Brain:
executor.type: omnigent/config.harness: claude-sdk, no model pinned (config.yaml:33-36) — runs on whatever Claude provider is configured. - Job: turns git diffs + commit history + PR metadata into release
notes and migration guides. Scribe authors prose itself and
delegates only read-only code investigation
(
config.yaml:38-45). - Two sub-agents via
tools.agents(config.yaml:141-147):researcheris a read-only explorer on the same vendor (claude-sdk,examples/scribe/agents/researcher/config.yaml),revieweris a fact-checker deliberately on a different vendor (codex,examples/scribe/agents/reviewer/config.yaml:13-16) so it catches claims the author's own model would wave through. os_env+sandbox: noneregisterssys_os_*so Scribe can read the repo and write docs (config.yaml:120-124).- One guardrail:
blast_radiuswithgate_pushes: false(config.yaml:131-139) — the catastrophic set is still denied, but a headless orchestrator can't answer an ASK prompt, so pushes don't gate.
The cross-vendor structure is the whole point: review is done by a model from a different vendor than the author.
examples/polly — a coding orchestrator (bundle)
Polly is the multi-agent coding pipeline — the closest thing in the repo to a Swisscheese-style writer→reviewer→fixer loop.
- Brain: claude-sdk,
context_window: 1000000(config.yaml:27-31). Polly writes no code — it plans, splits, and delegates everything to coding sub-agents (config.yaml:33-43). spawn: true(config.yaml:16) registerssys_session_createso Polly can also author and launch self-defined children, beyond the three declared intools.agents.- Three workers (
config.yaml:275-278):claude_code(claude-native),codex(codex-native), andpi(pi, the multi-model review/explore specialist). Each implements, reviews cross-vendor, and explores — each in its own git worktree, each opening its own PR. - The hard rule baked into the prompt: review is ALWAYS done by a
different vendor than the implementer (
config.yaml:141-147). The reviewer gets only the diff + contract, never the implementer's worktree, so its stray edits can't reach the deliverable. - Three guardrails (
config.yaml:281-311) compose the safety envelope:blast_radius(gate_pushes: false) — catastrophic DENY only.spawn_bounds(max_dispatches_per_turn: 5, counting bothsys_session_sendandsys_session_createsospawn: truecan't bypass the fan-out cap).headless_subagent_purpose_guard— every dispatch must carry apurposeofimplement | review | explore | search.ask_timeout: 86400— a day-long approval window so an ASK outlives a human stepping away.
- Skills compose (
config.yaml:193-201):investigate,fanout,cross-revieware markdownSKILL.mdfiles underexamples/polly/skills/loaded on demand.examples/polly/skills/cross-review/SKILL.mdencodes the procedure: get the diff, run deterministic gates, dispatch the opposite-vendor reviewer, turn blocking issues into fix-tasks, loop until clean.
examples/kimi_hello.yaml is the opposite extreme — a single-file
launcher with just name + executor.harness: kimi + prompt, the
exact shape omnigent run --harness kimi generates internally
(examples/kimi_hello.yaml:9-16). examples/debby sits between: a bundle that
fans every question out to a claude and a gpt sub-agent and shows
both, with an optional opencode third voice.
5. How YAML composes with native harnesses and policies
The YAML never implements an agent loop — it selects one and wraps it:
-
Native bridges.
executor.harnesspicks a real coding CLI (Claude Code viaclaude-native, Codex viacodex-native, Cursor, Kimi, OpenCode, Qwen, Copilot…). The YAML is a thin launcher; the harness owns the loop, the tools, and (often) its own auth. A sub-agent overrides the parent — Polly's brain isclaude-sdkwhile its workers areclaude-native/codex-native/pi, so one orchestrator mixes harnesses by role. -
Policies wrap the harness uniformly. Policy handlers match both Omnigent's
sys_os_shelland a harness's nativeBash/bash(policies.py:377-383), so the sameblast_radius/spawn_boundsdeclaration governs a Claude Code worker, a Codex worker, and a headless Pi worker identically. Guardrails are evaluated before the harness gets the turn (omnigent/spec/omnigent.py:214-224) — enforcement lives at the mechanism layer, not in any one harness. -
Skills layer on top. Markdown
SKILL.mdfiles (frontmattername≤64 chars[a-z0-9-]+matching the dir,description≤1024 chars —omnigent/spec/AGENTSPEC.md:215-221) are loaded on demand and compose with each other and with the policies.
The net: harness = how the loop runs, policy = what it may do, skill = how it should think, sub-agents = who else it can call — all four declared as data in one YAML tree.
6. Annotated reference YAML
# ── identity ────────────────────────────────────────────────
name: coding_agent # stable id in sessions/logs (required)
description: Plans and delegates coding work. # free-form summary
# ── system prompt ───────────────────────────────────────────
prompt: | # inline prompt; or `instructions: AGENTS.md`
You are a coding agent. Inspect files before editing, run targeted
tests, and summarize changes with validation results.
# ── which loop runs the turn ────────────────────────────────
executor:
harness: claude-sdk # dispatch key → an ExecutorAdapter
model: databricks-claude-sonnet-4-6 # omit to use provider default
auth:
type: databricks # provider auth; cursor/kimi use their own
profile: oss
# ── interaction toggles ─────────────────────────────────────
async: true # expose async work tools (default true)
cancellable: true # session can be cancelled (default true)
timers: false # timer tools off (default false)
spawn: false # set true to allow self-authored children
# ── local OS access (registers sys_os_* tools) ──────────────
os_env:
type: caller_process
cwd: .
sandbox:
type: linux_bwrap # omit → platform default
write_paths: [.]
allow_network: true
# ── interactive shells the agent can launch ─────────────────
terminals:
zsh:
command: zsh
args: [-l]
os_env: inherit # same sandbox as the agent
allow_cwd_override: true
# ── guardrails: evaluated before the harness gets the turn ──
guardrails:
ask_timeout: 86400 # day-long human-approval window
policies:
blast_radius:
type: function
on: [tool_call] # phase: request|response|tool_call|tool_result
function:
path: omnigent.inner.nessie.policies.blast_radius # dotted handler
arguments: { gate_pushes: false } # catastrophic DENY only
# ── tools: functions, MCP servers, and sub-agents ───────────
tools:
repo_search: # Python function tool
type: function
description: Search repository files for a pattern.
callable: my_package.tools.repo_search
parameters:
type: object
properties: { query: { type: string } }
required: [query]
agents: # sub-agents; listing one = allowed to call it
- reviewer # resolves to agents/reviewer/ (bundle)For Swisscheese
Polly is a writer→reviewer→fixer pipeline expressed entirely in YAML — worth lifting wholesale:
- Roles are declarative. A writer/reviewer/fixer pipeline is four
YAML files (
config.yaml+ three sub-agents underagents/), not four classes. New role = new directory + a line intools.agents. - Cross-vendor review is a spec invariant, not a convention. Give
the reviewer a different
executor.harnessthan the implementer (Polly: claude-native implements → codex/pi reviews) so the second opinion is genuinely independent — the failure mode Swisscheese exists to solve. spawn_bounds+headless_subagent_purpose_guardare your fan-out controls. Cap dispatches per wave and force every dispatch to carry animplement|review|fixpurpose — directly portable to a parallel review fleet (policies.py:408,:467).blast_radius(gate_pushes:false)is the unattended-orchestrator default: deny only the irreversible set, let recoverable pushes through, because a headless pipeline can't answer an ASK (policies.py:346).- One harness key swaps the whole agent loop — review the same diff with two vendors by changing one string.
For the AI Act platform
- The policy layer is audit-traceable by construction: every
tool-call passes a declared handler (
omnigent/spec/omnigent.py:214-224) whose decision (ALLOW/ASK/DENY) and reason are emitted before execution — a natural hook for Article 12 logging and Article 14 human oversight (the ASK gate is human-in-the-loop, withask_timeoutas the oversight window). - The agent definition is a single declarative artifact you can diff, version, and review — the audit trail starts at the spec, not the code.
Pitfalls
spec_versionis a trap door. Add it and your single-file YAML is rejected — it now must be a directory bundle (omnigent/spec/_omnigent_compat.py:265-273). The error is explicit, but the two shapes sharing field names invites confusion.${VAR}expansion is author-side only for uploaded bundles. References resolve against the spec author's environment at the registration boundary, never server-side at runtime — expanding an uploaded spec's${VAR}against the server env would let a tenant exfiltrate server secrets (omnigent/spec/AGENTSPEC.md:256-266).- No SSRF validation on MCP
url. The server makes outbound requests to whateverurla spec declares, with no private-IP / metadata-endpoint blocking — use network-level egress controls in multi-tenant setups (omnigent/spec/AGENTSPEC.md:267-272). - The harness allowlist is a hard wall. A harness id not in
OMNIGENT_HARNESSESfails validation; new harnesses must be registered in the frozen set (omnigent/spec/_omnigent_compat.py:80-106). os_envis opt-in. Forget it and the agent has no filesystem or shell tools — but a harness that owns its tools (kimi) needs you to omit it to avoid double-dispatch (examples/kimi_hello.yaml:36-40).