Each call to the model is one turn, and each tool is a job. An agent ignores events sent to it.
# librarian.yaml
agent: librarian
input: { dir: string, question: string }
model: { name: claude-sonnet-5-5, secret: ANTHROPIC_KEY }
system: You answer questions about a library of files.
prompt: ${input.question}
tools:
shelf:
does: Give back every file on one shelf
args: { shelf: string }
job:
script: ./shelf.js
input: { dir: "${input.dir}", shelf: "${args.shelf}" }
allow: ["fs.read:${input.dir}/**"]// shelf.js
return fs.list(input.dir + "/" + input.shelf).map((file) => file.name)
corbie test librarian.yaml --input '{"dir": "/srv/books", "question": "What is on the top shelf?"}' --secret-env ANTHROPIC_KEY --watch step 1 -> job turn: a script, on runner step 2 -> job shelf: a script, on runner step 3 <- done from shelf ["dune.txt","emma.txt"] step 3 -> job turn#2: a script, on runner step 4 done
Fields
modelthe model, needed even when a turn: script calls none
systemthe system prompt, which may hold ${input...}
promptthe first message to the model, an expression of the input
toolsthe tools, by name
openparts: groups of tools the model opens, as under Parts
replya shape, as under Tools: the reply is data in this shape
conversationthe conversation of an earlier agent's result: this agent continues it, and each blob keeps its name
turna script that makes each turn in place of Corbie's own, as in agents; with one that calls no model, and no secret:, corbie test runs the tools with nothing set up
limits: { turns: 20 } is the number of turns, default 20. At the limit the agent fails with turn limit reached. Its error's output holds the conversation, and another agent given it as conversation: carries on.
The model
namethe model's name, as its service knows it
secretthe name of the secret that is the service's API key
serviceanthropic, openai (the Responses API), openai-chat (the Chat Completions API) or claude-code; by default anthropic for a name that begins claude, and openai for gpt, o1, o3 and o4
addressthe URL of a service that has the same API somewhere else: a self-hosted model, or one that a cloud offers
effortlow, medium or high; claude-code also takes xhigh and max
reply_tokensthe most tokens a reply may hold
ownfor claude-code: tools of the program that the model may use
The key is a secret. --secret-env ANTHROPIC_KEY reads it from that environment variable for one run, and corbie controller secret set ANTHROPIC_KEY sets it for every run. Each turn's job gets net:<host> for the service and secret:<secret>@net:<host>, and nothing from the definition's allow:.
# elsewhere.yaml
agent: elsewhere
model: { name: my-model, service: openai-chat, address: https://models.example, secret: MODELS_KEY }
prompt: Say hello.corbie test elsewhere.yaml --secret-env MODELS_KEY output: {"conversation":{"blobs":{},"turns":[{"$blob":"conversation.turns[0]","size":91,"type":"application/json"}]},"reply":"Hello.","tokens":{"input":10,"output":5}} used: net:models.example:443 secret:MODELS_KEY@net:models.example:443
The result
replythe model's last reply, as text or in the shape of reply:
tokens{ input, output }, counted over the agent's own turns
conversation{ turns, blobs }: a blob for each turn, and each blob the model was shown by its handle's name
A reply that does not fit reply: is returned to the model, which takes another turn. A failed agent's output has conversation and tokens.
Tools
doeswhat the tool does: the model chooses a tool by this text
argsthe shape of its arguments: { name: type }, or JSON Schema
joba script or a machine, as in Definitions; it reads input, the agent's input, and args, the model's arguments
lista job that returns tools, [{ name, does, args }], in place of does: and args:; job: runs each, and reads tool, its name
A shape is written as input: is, in Definitions: args: { shelf: string, limit: "number?" }. A name the shape does not give is refused, and a blob is the name the model was shown.
A shape written with type: object is JSON Schema, which can give an argument a description, an enum or fields of its own. format: blob marks a blob at any depth.
- A tool whose job has
approve: truewaits for the definition'sask:, which getsasked: { does, args, said }: the tool'sdoes:, the model's arguments and the model's text. A call that is not approved is returned to the model as denied, with the ask's note. - A tool whose job fails is returned to the model as an error.
- A tool whose job is an agent is a sub-agent. The model gets the sub-agent's reply, and the sub-agent's conversation stays in its own record.
- A tool called twice in one turn has jobs named
shelfandshelf.1. - No tool is named turn, preview, open, blob_read, blob_search or blob_code.
An entry with list: gives tools that come from elsewhere, such as the tools of an MCP server (mcp), which change without the definition changing.
- The list runs once, before the first turn, as a job named for the entry.
- The model gets each tool as the entry's name,
_and the tool's name:githubandcreate_issuegivegithub_create_issue. Other characters than letters, digits,_and-become_. job:reads${args}only whole, and${tool}is the tool's name as the list gave it.- Every listed tool has the permissions of
job:. - The ask gets
entryandtoolinasked:githubandcreate_issue. - The agent fails if the list fails, or gives a tool with no name, args that are not JSON Schema with type: object, args that mark a blob, or a name that is longer than 64 characters or that another tool has.
Parts
A part is a group of tools that the model is not shown until it opens the part. An agent with many tools shows the model the ones it has opened.
- The model is shown a tool
open, with the name anddoes:of each part it has not opened. - The model calls
openwith a part's name, and a job namedopenruns. - From the next turn, the model is shown the part's tools as well.
open:
net: { does: The firewall and the switch., tools: { … } }
A part is often kept in its own file, which include: reads, as Definitions says.
open:
net: { include: ./net.yaml }
pay: { include: ./pay.yaml, approve: true } its open waits for the ask
A part has these fields.
doeswhat the part is for, which the model reads before it opens it
toolsthe part's tools, written as under Tools
askthe ask for opening the part, for its tools' jobs and lists and for the machines below them, before the agent's own; its expressions read the agent's input
approvetrue holds the part's open for the ask
metadata Corbie keeps and never reads
- No tool of a part has the name of another tool of the agent.
- The list of an entry with
list:in a part runs before the first turn. An error names it asopen.net.tools.files.list. - The ask for
opengetsasked: { part, does, args, said }, wheredoesis the part's. An open that is not approved opens nothing. - The ask for a call of a part's tool gets
partinasked, and so does the ask for the list of an entry in a part. - A part that has an
ask:needs noask:of the agent's. The permissions of the part'sask:are its own, as any ask's are. - An agent given a
conversationopens no part: the model callsopenfor each part it needs, and a part markedapprove: truewaits for the ask again.
Blobs
- The model gets a blob as a handle,
{ blob: "blob1", type, size, items, cost, preview }, and gives the nameblob1where a tool'sargs:has the type blob orformat: blob. itemscounts its lines, rows or entries,costestimates the tokens needed to read it, andpreviewis its first items, given once.- Every agent has the tools
blob_read,blob_searchandblob_code, which read part of a blob. A large answer from one is returned as a blob. - A tool's result over about 32 KiB is stored as a blob by a job named
preview, which also reads each new blob before the next turn. - A blob with no items, such as a document or an image, needs a tool of the definition's that reads its format.
- An image or a PDF that the service accepts is also sent to the model whole.
Claude Code
service: claude-code starts the claude program on the runner's computer for each turn, in place of a request to a service. It takes no address: and no reply_tokens:.
# searcher.yaml
agent: searcher
model: { name: sonnet, service: claude-code, own: [WebSearch, WebFetch] }
prompt: Say hello.corbie test searcher.yaml used: env:HOME exec:claude network
- With no
secret:, the program is signed in as the runner's user, and the turn's job getsexec:claude,env:HOMEand, for a runner on Windows,env:USERPROFILE. Withsecret:, the job getsexec:claudeandsecret:<secret>@exec:claude. - The program starts with none of its own tools, no saved session and none of its user's settings, so the model's tools are the definition's.
- Corbie does not run a tool named in
own:and cannot limit it to a scope, so it needs a whole group allowed in every layer of permission, the ceilings included. Each use is written to the record as an emitted value namedown.
networkWebSearch and WebFetch
diskRead, Glob, Grep, Edit, Write and NotebookEdit
allBash, Skill and any other tool; Skill also loads what the runner's user has set up for the program