Skip to content

symfonic.agent.subagents.facade_child

facade_child

Delegating to a kernel-native Agent.

A parent's roster calls a child as run(query=..., scope=..., run_id=..., agent_depth=...) and reads an :class:AgentResponse back. The simple facade answers run(prompt, *, attachments, history, output_type) and returns an AgentResult, so a facade agent handed straight to a roster raises TypeError on the first delegated call.

This adapter closes that at the edge instead of widening the facade, which deliberately has neither tenant scope nor delegation depth.

Terminal by construction. A facade child is a leaf. It accepts agent_depth so the roster's call type-checks and it never opens a level of its own, which leaves the ceiling exactly where it already works: the parent's DepthPolicy, consulted before dispatch. Re-checking it here would put one rule in two places, and two places is where two answers come from. But leaf is checked rather than assumed -- Agent accepts any object with a contribute(), delegation included -- so an agent that could itself delegate is refused here rather than wrapped into a chain the parent's ceiling cannot see.

Translated, not widened. AgentResult becomes AgentResponse at the boundary, so no public type changes and the parent reads what it always read.

FacadeChild

FacadeChild(agent: Any)

Adapt a simple :class:~symfonic.Agent into a delegation child.

Source code in src/symfonic/agent/subagents/facade_child.py
def __init__(self, agent: Any) -> None:
    if not callable(getattr(agent, "run", None)):
        raise ConfigurationError(
            "FacadeChild needs an object with an async run(); got "
            f"{type(agent).__name__}."
        )
    for name in _declared_capabilities(agent):
        if _DELEGATION_MARKER in str(name).lower():
            raise ConfigurationError(
                "this Agent declares a delegation capability, so it is not "
                "a leaf: wrapping it would put a child that can itself "
                "delegate behind an adapter that reports no depth, and the "
                "parent's ceiling would stop seeing the chain. Delegate to "
                "it directly, or give the roster a child that does not "
                "delegate."
            )
    self._agent = agent

run async

run(query: str, **_ignored: Any) -> AgentResponse

Answer one delegated task.

Everything the roster passes beyond the query is accepted and dropped on purpose. scope and agent_depth are platform concepts the facade does not have, and inventing values for them here would report a tenancy and a depth the child never actually ran under.

Source code in src/symfonic/agent/subagents/facade_child.py
async def run(self, query: str, **_ignored: Any) -> AgentResponse:
    """Answer one delegated task.

    Everything the roster passes beyond the query is accepted and dropped
    on purpose. ``scope`` and ``agent_depth`` are platform concepts the
    facade does not have, and inventing values for them here would report a
    tenancy and a depth the child never actually ran under.
    """
    result = await self._agent.run(query)
    return AgentResponse(
        final_response=getattr(result, "text", "") or "",
        messages=[_as_mapping(m) for m in getattr(result, "messages", ()) or ()],
        # The parent's correlation id, echoed rather than re-minted: a child
        # that renamed the run would break the pair's only shared handle.
        run_id=_ignored.get("run_id") or getattr(result, "run_id", None),
        duration_ms=getattr(result, "duration_ms", 0.0) or 0.0,
        structured=getattr(result, "output", None),
    )