Support multiple parents in bottle extends: #268

Closed
opened 2026-06-25 02:30:25 -04:00 by didericis-claude · 0 comments
Collaborator

Summary

Today a bottle's extends: accepts only a single parent (a string). It would be useful to extend from multiple bottles so that orthogonal concerns (e.g. a base env bottle, a networking/egress bottle, an agent_provider bottle) can be composed independently rather than forced into one linear chain.

Current behavior

bot_bottle/manifest_extends.py reads extends: as a single value and requires it to be a string:

parent_name_raw = raw.get("extends")
...
if not isinstance(parent_name_raw, str):
    raise ManifestError(
        f"bottle '{name}' extends must be a string "
        f"(was {type(parent_name_raw).__name__})"
    )

Passing a list (e.g. extends: [base, networking]) dies with "extends must be a string". The only way to combine multiple bottles today is a linear chain (my-agent -> networking -> base), which couples the parents to each other and can't express true multiple inheritance.

Proposed behavior

Allow extends: to be either a string (current) or a list of bottle names:

my-agent:
  extends: [base, networking]
  agent_provider: { ... }

Parents are resolved and folded left-to-right, then the child is merged on top. Precedence: later parents win over earlier parents on conflict, and the child wins over all parents (last-wins, consistent with the existing child-wins rules).

Implementation sketch

The merge primitives in manifest_extends.py are already written as binary parent (+) child operations, so they compose for a fold:

  • _resolve_one_bottle: accept str | list[str]; normalize to a list; resolve each parent (reusing the existing cycle detection across all parents in the chain); fold the resolved parents left-to-right into a single effective parent, then run the existing _merge_bottles(parent, child_raw, ...).
  • _resolve_repos_raw: today folds parent repos with child repos; fold across the multiple parents first to get the combined parent repo set, then merge the child against that.
  • _merge_egress: route lists already concatenate; folding multiple parents concatenates their routes in declaration order before the child's.
  • Validation/error messages: report the offending bottle and the full chain; a list entry that isn't a string, or names an undefined bottle, should die with a clear message (mirror the current string/undefined checks).

Open questions

  • Conflict precedence among parents: confirm last-wins is the desired rule (matches child-wins convention) vs. first-wins.
  • Diamond inheritance (two parents share a common ancestor): the resolution cache already dedupes by name, so a shared ancestor is resolved once; confirm this is the intended semantics.
  • Cycle detection must cover the multi-parent graph, not just a linear chain.

Tests

  • list with two parents: env / egress.routes / git-gate.repos compose in order
  • last-parent-wins on a scalar field (e.g. agent_provider, supervise)
  • child still wins over all parents
  • diamond (shared ancestor resolved once)
  • list entry that is not a string -> ManifestError
  • list entry naming an undefined bottle -> ManifestError
  • cycle through a multi-parent edge -> ManifestError
  • backward compat: extends: as a plain string still works

See PRD 0025 (docs/prds/0025-bottle-extends.md) for the original merge rules.

## Summary Today a bottle's `extends:` accepts only a single parent (a string). It would be useful to extend from multiple bottles so that orthogonal concerns (e.g. a `base` env bottle, a `networking`/egress bottle, an `agent_provider` bottle) can be composed independently rather than forced into one linear chain. ## Current behavior `bot_bottle/manifest_extends.py` reads `extends:` as a single value and requires it to be a string: ```python parent_name_raw = raw.get("extends") ... if not isinstance(parent_name_raw, str): raise ManifestError( f"bottle '{name}' extends must be a string " f"(was {type(parent_name_raw).__name__})" ) ``` Passing a list (e.g. `extends: [base, networking]`) dies with "extends must be a string". The only way to combine multiple bottles today is a linear chain (`my-agent` -> `networking` -> `base`), which couples the parents to each other and can't express true multiple inheritance. ## Proposed behavior Allow `extends:` to be either a string (current) or a list of bottle names: ```yaml my-agent: extends: [base, networking] agent_provider: { ... } ``` Parents are resolved and folded **left-to-right**, then the child is merged on top. Precedence: later parents win over earlier parents on conflict, and the child wins over all parents (last-wins, consistent with the existing child-wins rules). ## Implementation sketch The merge primitives in `manifest_extends.py` are already written as binary `parent (+) child` operations, so they compose for a fold: - `_resolve_one_bottle`: accept `str | list[str]`; normalize to a list; resolve each parent (reusing the existing cycle detection across all parents in the chain); fold the resolved parents left-to-right into a single effective parent, then run the existing `_merge_bottles(parent, child_raw, ...)`. - `_resolve_repos_raw`: today folds parent repos with child repos; fold across the multiple parents first to get the combined parent repo set, then merge the child against that. - `_merge_egress`: route lists already concatenate; folding multiple parents concatenates their routes in declaration order before the child's. - Validation/error messages: report the offending bottle and the full chain; a list entry that isn't a string, or names an undefined bottle, should die with a clear message (mirror the current string/undefined checks). ## Open questions - Conflict precedence among parents: confirm **last-wins** is the desired rule (matches child-wins convention) vs. first-wins. - Diamond inheritance (two parents share a common ancestor): the resolution cache already dedupes by name, so a shared ancestor is resolved once; confirm this is the intended semantics. - Cycle detection must cover the multi-parent graph, not just a linear chain. ## Tests - list with two parents: env / egress.routes / git-gate.repos compose in order - last-parent-wins on a scalar field (e.g. agent_provider, supervise) - child still wins over all parents - diamond (shared ancestor resolved once) - list entry that is not a string -> ManifestError - list entry naming an undefined bottle -> ManifestError - cycle through a multi-parent edge -> ManifestError - backward compat: `extends:` as a plain string still works See PRD 0025 (`docs/prds/0025-bottle-extends.md`) for the original merge rules.
didericis added the Kind/Enhancement
Priority
High
2
labels 2026-06-25 02:46:04 -04:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: didericis/bot-bottle#268