Skip to main content
This page will document the code-first API in the ziro package. ziro.create_agent(...) assembles an agent from pluggable objects (an LLMAdapter, guardrail backends, BaseTool instances, a BaseStore, a BaseCheckpointSaver, and policy configs) into the frozen AgentSpec tree defined in ziro/spec.py. A SpecProfile adapts that spec to the same AgentProfile duck-type the engine consumes, so a code-first agent runs through the identical graph as a YAML one. Chatting is agent.chat("hi", user="alice"), with the user bound at chat time. ziro.profiles.load(<agent-id-or-path>) goes the other way: it parses a YAML agent folder into an AgentSpec, and is tested for equivalence against the shipped agents. An on_interrupt handler that answers automatically must return an InterruptResponse with source="policy". Only responses marked source="human" can create a session permission grant. The defaults are deliberately hermetic, so the same script behaves the same on two machines. A spec-driven agent reads no project config layers (only spec fields and library defaults), runs outside flavours, and surfaces zero plugins. Opt into plugins explicitly with create_agent(plugins=True), an allowlist such as plugins=["cc/x"], or a full PluginConfig; a bare string raises TypeError.

Embedding ziro-core

ziro-core embeds as the chat engine of a host application, with the host deciding which skills and subagent definitions each agent sees. create_agent(sources=..., skills=..., agents=...) takes three inputs, concatenated in that order: sources= takes Source objects (DirectorySource, MappingSource, StoreSource, ZiroDefaults), skills= takes directory paths (each mapped to DirectorySource(path, layout="skills")), and agents= takes AgentSpec objects or directory paths (each path mapped with layout="agents"). When any of the three is given, that is the agent’s full set; ZiroDefaults() is the only way to include the ambient layers (project .ziro, user ~/.ziro, bundled, enabled plugins). A missing path raises ValueError. StoreSource(store, namespace) reads one host-chosen namespace of a LangGraph BaseStore. Keys are relative posix paths (skills/<id>/SKILL.md plus its reference files, and agents/<id>.agent.md), values are {"text": "<file contents>"}, and StoreSource.put(store, namespace, path, text) writes one file in that shape. A write is picked up on the agent’s next turn. skill_discovery selects the skills delivery mode (legacy, auto, catalog, search) and skill_catalog_limits=(30, 6000) bounds auto (max skills, max rendered chars). legacy (the default) reproduces today’s prompt exactly; catalog renders - <id>: <description> lines with descriptions cut to 200 chars; search renders N skills available; call search_skills before producing any artifact; auto picks catalog while the library is within the limits. The catalog rides the context channel at the turn’s first model call and is pinned for the whole turn: a skill removed mid-turn answers skill '<id>' was removed after this turn started, and one added mid-turn answers skill '<id>' is not available yet; it will appear next turn. A spawn permission ceiling keeps an approval-gated parent from producing a shell-capable or write-capable child: a mutating or dangerous tool whose parent verdict is ask or deny is withheld at spawn, read-only ask-gated tools pass through, and inherit_permissions is a no-op. The full rule lives in docs/subagents.md, section “The spawn permission ceiling”. subagents= sets the spawn policy (which children are allowed) but does not bind spawn_subagent. It stays deferred, so the model only reaches it through search_tools and load_tools, and often says the tool is missing instead. With an explicit tools= list, add "subagent:*" so the spawn tools are bound every turn:
The BaseTool objects you pass in tools= register under the custom namespace. An MCP server whose name is a namespace already in use (custom once you pass tools, or a built-in one such as fs or shell) is not started: Ziro only logs a warning and its tools are missing. Give the server another name in mcp_servers=. agent.submit(prompt) (or await agent.asubmit(prompt)) enqueues a durable background job and returns its job id; agent.job(job_id) reads the record. A job of a spec-driven agent carries its own profile, llm and libraries, and one recovered after a restart fails with host sources unavailable after restart instead of falling back to config resolution.
This page is in progress. See architecture for the graph the SDK builds against and agent files for the YAML equivalents of each spec field.

Architecture

The agent loop an AgentSpec is compiled into.

Agent files

The YAML side of the same configuration surface.