Skip to content

Embedding Subagent Programs in Python

The language declares the profile graph. Python supplies the adapter, host tools, global values, resource limits, and runtime lifetime. It does not need to reach into the coordinator or call an invented kedi.subagent() function.

Compile, Execute, Close

This complete embedding uses a profile-only program with a raw text result. Configure provider credentials before running it:

import asyncio

from kedi import SubagentUsageLimits
from kedi.agent_adapter import PydanticAdapter
from kedi.lang import compile_program, parse_program

source = """
> profile: reviewer:
    > adapter: pydantic
    > instructions: Identify what the supplied evidence does not establish.

> profile: coordinator:
    > adapter: pydantic
    > subagent: reviewer
    > max_agents: 2

> use: coordinator
[answer] << Ask reviewer to assess: unit tests passed but integration tests were not run. Summarize uncertainty.
= <answer>
"""

limits = SubagentUsageLimits(
    request_limit=4,
    tool_calls_limit=6,
    input_tokens_limit=20000,
    output_tokens_limit=2000,
    total_tokens_limit=22000,
)
runtime = compile_program(
    parse_program(source),
    adapter=PydanticAdapter("openai:gpt-5.6-luna"),
    subagent_max_depth=2,
    subagent_max_concurrency=2,
    subagent_timeout_seconds=60,
    subagent_usage_limits=limits,
    subagent_state_path=".kedi-subagent-state.json",
)
try:
    print(runtime.run_main())
finally:
    asyncio.run(runtime.aclose())

This is a synchronous script. In an asynchronous application, keep execution and cleanup under that application's lifetime; do not call asyncio.run inside an already running event loop. The same subagent configuration is accepted by KediRuntime and the public kedi.context(...) runtime context.

Input/total token budgets can require pre-request token counting from the model integration. If the selected model cannot provide it, the child fails rather than pretending that the limit was enforced. The local documentation fixture implements token counting explicitly; a bare Pydantic FunctionModel does not.

The state path is optional. Exclude it from version control because task text, results, and adapter continuation state may be sensitive. See Persistence.

Delegate Directly From Python

When application code knows the child task, it can use the same runtime without a parent model call. The scope owns every child it starts:

async with runtime.subagents(parent="coordinator") as agents:
    job = await agents.start(
        "reviewer",
        task="Assess this evidence: unit tests passed; integration tests were not run.",
    )
    result = await job.wait()
    print(result.task_summary)

start admits the child and returns a SubagentTask; wait blocks for its SubagentResult. The result exposes output, task_summary, run_id, and subagent. If the child has no > output: declaration, output is None. Repeated waits return the same result. The handle cannot be reused after its scope closes or from another invocation. Leaving a task unawaited cancels it and fails the scope; child failures and schema errors raise at wait.

A failure or child cancellation caught at wait counts as observed: closing the scope does not raise that same outcome again. You can handle the failure and start a replacement within the remaining budget. The failed or cancelled invocation still consumes its max_agents slot. Cancelling only a waiter does not cancel or observe the running child; await that child later or let scope cleanup reject the unawaited work.

The parent must be the effective profile when this scope is called inside Kedi execution. An independent embedding scope may select a compiled profile, but it cannot use that choice to bypass a running child profile's permissions. Use await runtime.aclose() when the application is done with the runtime.

Inspect a Child From Its Tool

Running tasks also expose await job.send(message, interrupt=False). For receipts, redirection, and explicit owner message reception, see Task Input and Redirection.

current_subagent_execution() returns a SubagentExecutionContext inside a child execution, or None outside it. Its public fields are run_id, profile, and conversation_id. It is task-local, so concurrent children do not share one global "current child" value.

from kedi import current_subagent_execution, tool

@tool(risk="read_only")
def execution_identity() -> dict[str, str]:
    """Return the identity of the child currently inspecting evidence."""
    child = current_subagent_execution()
    if child is None:
        raise RuntimeError("This tool requires a child invocation")
    return {
        "run_id": child.run_id,
        "conversation_id": child.conversation_id,
        "profile": child.profile.name or "",
    }

Pass this callable in runtime_globals and expose it with > use: in the child's profile. Registration does not invoke it. The reviewed evidence example demonstrates a real child tool using this API and verifies its identity against the delegation result.

Keep the Surfaces Separate

Surface Intended caller
Profile directives Kedi source declares permissions and graph structure.
delegate_task, lifecycle tools, run_workflow The parent model through its advertised tools.
compile_program, KediRuntime, kedi.context The embedding application.
runtime.subagents(...), SubagentTask, SubagentResult Program-controlled child work in Python.
SubagentUsageLimits, execution context helper Application configuration and child-aware tools.

The coordinator's underscored runtime methods are implementation details, not an application API. A parent task's final text is not a substitute for observing its required child outcomes and host effects.