Corbie

Machine types

Agent

An agent calls an AI model in a loop, runs the tools the model calls, and ends when the model answers without calling a tool.

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: true waits for the definition's ask:, which gets asked: { does, args, said }: the tool's does:, 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 shelf and shelf.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: github and create_issue give github_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 entry and tool in asked: github and create_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.

  1. The model is shown a tool open, with the name and does: of each part it has not opened.
  2. The model calls open with a part's name, and a job named open runs.
  3. 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 as open.net.tools.files.list.
  • The ask for open gets asked: { part, does, args, said }, where does is the part's. An open that is not approved opens nothing.
  • The ask for a call of a part's tool gets part in asked, and so does the ask for the list of an entry in a part.
  • A part that has an ask: needs no ask: of the agent's. The permissions of the part's ask: are its own, as any ask's are.
  • An agent given a conversation opens no part: the model calls open for each part it needs, and a part marked approve: true waits for the ask again.

Blobs

  • The model gets a blob as a handle, { blob: "blob1", type, size, items, cost, preview }, and gives the name blob1 where a tool's args: has the type blob or format: blob.
  • items counts its lines, rows or entries, cost estimates the tokens needed to read it, and preview is its first items, given once.
  • Every agent has the tools blob_read, blob_search and blob_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 gets exec:claude, env:HOME and, for a runner on Windows, env:USERPROFILE. With secret:, the job gets exec:claude and secret:<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 named own.
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