It is not reasonable to implement a complex workflow using a single instance of core.agent.Agent and one req_loop call, no matter how complex the prompting and toolset, or "how reasoning" the model is.
Therefore, it is critical to build a way for agent instances to pass content to one another.
A Hatchery object represents a network of Baneling() or Drone(core.agent.Agent) objects ("nodes"), as described in a configuration JSON file (see hatchery/*).
- A Baneling object is a static tool call ("type":"tool").
- A Drone object is an LLM inference call (no type / default type).
Each node must implement:
- node.run(self,ctx)
- node.next
- node.save_output
- node.write_output
A drone can specify one or more "next" nodes:
- If a drone has zero next nodes (i.e. next is not present): hatchery will print the output of that drone and exit.
- If a drone has exactly one next node: hatchery will run the next drone.
- If a drone has more than one next node: hatchery will check the output of the current drone. If any of the next node names are present, it will route to that drone. If not, it will route to the last node as a default case. (todo: consider changing).
Output can be passed between nodes, using a shared context object which exists for the lifetime of the hatchery ("ctx"). Write to the output of ctx with "save_output", read from it with Jinja2 templating (ctx.varnamehere).
Fundamentally, there should be no difference (to the LLM) between invoking an MCP, a hatchery, another agent or a regular function - all of these are functions. Therefore, an arbirary hatchery can be included as a tool. To do this, simply include a tool using this syntax:
hatch:hatchery/bananwriter.json
This will expose a single tool, according to the JSON "name" and "desc" properties. This function takes a single string (saved in the context as "input". The output of the final node is taken as the hatchery-tool's output.
It is also possible to load one node as a tool for another within Hatchery, using the following syntax in the hatchery JSON definition:
"tools":["node:SomeOtherNode"]
These are lazy-loaded - the first node can load the second without the second being defined, it is looked up at runtime. The first node can pass an input string to the second node, which is loaded in the second node as ctx.input (note: there is no consideration for threads).