Modularity
Four specs that share a sandbox variant, a toolset and a set of conventions should not drift apart by hand. Two mechanisms stop them, and they answer different questions:
extends: loop-base:0.0.1 # inheritance โ "the same agent, adjusted"
includes: # composition โ "this bundle, wherever needed"
- notebook-surfaces:0.0.1
Extension
Inheritance, with extends. Reach for it when the new spec is the old one
Composition
Mixing in capability, with includes. Reach for it when several unrelated
Which oneโ
Extension when the new spec is the old one with changes โ a specialist that behaves like the base agent but with a different model or an extra instruction. One parent, and the relationship is "a kind of".
Composition when several unrelated specs need the same capability โ the notebook surfaces, a standard toolset. Many includes, and the relationship is "has".
The distinction earns its keep in one specific way. If the notebook specialists
inherited from a base agent that declares subagents, each specialist would
inherit that subagent list โ and @NotebookCompactor could delegate to
@NotebookCompactor. Shared capability goes in a fragment; only the base agent
declares subagents. There is then no cycle for a depth cap to catch.
Resolved before anything runsโ
Both are resolved when the catalogs are generated (make specs), so the
generated Python and TypeScript stay flat, the runtime keeps no inheritance
logic, and the result of a merge is visible in a diff rather than inferred at
startup.
That last point is not only tidiness. A resolved spec is what travels to a
running pod, where the companion process forwards it without interpreting a
single field โ an unresolved extends would arrive somewhere that cannot
understand it.
Fragments are applied first and extends second, so a parent can override what
a fragment brought in.
Merge semanticsโ
The rules both mechanisms obey.
| Field kind | Rule |
|---|---|
Scalars (model, sandbox_variant, icon, memory) | the child wins |
Lists (tools, skills, mcp_servers, frontend_tools, tags, suggestions) | append, deduplicated by id โ a version suffix does not make an entry different |
Keyed collections (frontend_render_tools) | merged entry by entry, on tool |
system_prompt | replaced; system_prompt_prepend / system_prompt_append layer onto the parent's |
Lists append because the common case is "the parent's tools plus mine". Two markers cover the cases where that is wrong:
tools:
- "!remove delete-cells:0.0.1" # drop one thing the parent granted
- my-own-tool:0.0.1
tools:
- "!replace" # start from nothing
- only-this:0.0.1
!remove is what a least-privilege specialist needs: an agent that may read and
edit a notebook but must never delete from it.
Inspecting the resultโ
Composition is resolved at generation time, so the flattened spec is what the
generated catalog contains โ read it there, or diff it after make specs. A
merge rule that changes shows up as a diff in the generated file, which is the
point of resolving early rather than at runtime.