This page documents the Python SDK interface for composing clients from other clients or from DNA artifacts. It focuses on how to use the public classes and their methods, and what behavior to expect when you call them.
This module provides two related utilities built on top of SummonerClient:
ClientMerger: build a single composite client by replaying handlers from multiple sources (live imported clients and/or DNA).ClientTranslation: reconstruct a fresh client from a DNA list by compiling handlers into an isolated sandbox module.
Both classes may execute code found in DNA via exec() / eval() (context imports, recipes, handler bodies). Use only with trusted DNA (typically produced by your own agents). Do not run untrusted DNA.
def __init__(
self,
named_clients: list[Any],
name: Optional[str] = None,
rebind_globals: Optional[dict[str, Any]] = None,
allow_context_imports: bool = True,
verbose_context_imports: bool = False,
close_subclients: bool = True,
) -> NoneCreates a composite client that can later replay handlers from multiple sources.
A "source" can be any of:
- an imported
SummonerClientinstance (live object), - a DNA list (
list[dict]), - a DNA JSON file path,
- or a dict wrapper with one of:
{"client": ...},{"dna_list": ...},{"dna_path": ...}.
The merger does not automatically register handlers on construction. You must call initiate_all() (or individual initiate_* methods) before calling run(...).
If the replayed client depends on flow-aware sender activation, configure flow on the merger before replaying senders. In practice, that means enabling flow before initiate_all() or initiate_senders().
If close_subclients=True, the merger attempts to clean up imported template clients after extracting their handlers (to reduce pending-task and event-loop warnings when importing agent scripts as templates).
-
Type:
list[Any] -
Meaning: List of sources to merge.
-
Accepted entry formats:
-
SummonerClientinstance -
DNA list (
list[dict]) -
dict with one of:
{"client": SummonerClient, "var_name": Optional[str]}{"dna_list": list[dict], "var_name": Optional[str]}{"dna_path": str, "var_name": Optional[str]}
-
- Type:
Optional[str] - Meaning: Logical name used for logging.
- Default behavior: Falls back to
SummonerClient's default placeholder if not provided.
-
Type:
Optional[dict[str, Any]] -
Meaning: Extra globals injected into:
- sandbox globals for DNA sources, and
- handler globals for imported-client sources.
-
Typical use: supply "missing" symbols referenced by handlers (shared objects, Trigger/Action-like bindings, utilities).
- Type:
bool - Meaning: Whether to execute import lines recorded in DNA
__context__headers. - Default:
True
- Type:
bool - Meaning: Whether to log successful context imports as well as failures.
- Default:
False
- Type:
bool - Meaning: Whether to attempt best-effort cleanup of imported template clients after extraction.
- Default:
True
Creates a ClientMerger instance (subclass of SummonerClient).
from summoner.client.client import SummonerClient
from summoner.client.merger import ClientMerger
a = SummonerClient(name="a")
b = SummonerClient(name="b")
agent = ClientMerger([a, b], name="merged")
agent.initiate_all()
agent.run(host="127.0.0.1", port=8888)from summoner.client.client import SummonerClient
from summoner.client.merger import ClientMerger
template = SummonerClient(name="template")
agent = ClientMerger(
[
template,
{"dna_path": "agent_dna.json"},
],
name="merged",
rebind_globals={"SOME_SHARED": object()},
)
agent.initiate_all()
agent.run(host="127.0.0.1", port=8888)def initiate_all(self) -> NoneReplays all supported handler types from every source onto the merged client, in a standard order:
upload_statesdownload_stateshookreceivesend
If the replayed sender behavior depends on flow-aware activation, call agent.flow().activate() before initiate_all() so the later send replay step can succeed.
This should be called before run(...).
None.
Returns None.
from summoner.client.merger import ClientMerger
agent = ClientMerger([{"dna_path": "a.json"}, {"dna_path": "b.json"}], name="merged")
agent.initiate_all()
agent.run(host="127.0.0.1", port=8888)def initiate_upload_states(self) -> NoneReplays @upload_states() handlers from every source onto the merged client.
- For imported-client sources: clones the handler, rebinding the original client variable name (commonly
"agent") to the merged client and injectingrebind_globals. - For DNA sources: compiles the function in the DNA sandbox and registers it onto the merged client.
None.
Returns None.
from summoner.client.merger import ClientMerger
agent = ClientMerger([{"dna_path": "a.json"}], name="merged")
agent.initiate_upload_states()def initiate_download_states(self) -> NoneReplays @download_states() handlers from every source onto the merged client.
None.
Returns None.
from summoner.client.merger import ClientMerger
agent = ClientMerger([{"dna_path": "a.json"}], name="merged")
agent.initiate_download_states()def initiate_hooks(self) -> NoneReplays @hook(Direction, priority=...) handlers from every source onto the merged client.
- Imported-client sources keep module-backed execution (their original globals dict).
- DNA sources compile hooks into the per-source sandbox and then register them normally.
None.
Returns None.
from summoner.client.merger import ClientMerger
agent = ClientMerger([{"dna_path": "a.json"}], name="merged")
agent.initiate_hooks()def initiate_receivers(self) -> NoneReplays @receive(route, priority=...) handlers from every source onto the merged client.
None.
Returns None.
from summoner.client.merger import ClientMerger
agent = ClientMerger([{"dna_path": "a.json"}], name="merged")
agent.initiate_receivers()def initiate_senders(self) -> NoneReplays @send(...) handlers from every source onto the merged client.
For everyday usage, this simply means the merged client regains the outbound handler behavior recorded on its sources. The extra work during replay is resolving any named trigger/action references from DNA and rebuilding optional sender guards and payload filters such as run_while and when_data.
Sender replay details
The merger replays sender fields such as:
routemultion_triggerson_actionsuse_datadata_modewhen_dataeveryrun_while
For DNA sources, triggers/actions are stored by name and are resolved as follows:
-
Triggers:
- prefers a
Triggerbinding present in the sandbox globals (often provided by context orrebind_globals), - otherwise falls back to default trigger loading (
load_triggers()).
- prefers a
-
Actions:
- resolved by name against the protocol
Actioncontainer.
- resolved by name against the protocol
Callable run_while guards and when_data predicates are also rehydrated from serialized metadata when possible. The merger first tries the direct callable stored on imported-client DNA, then falls back to serialized name/source reconstruction for DNA-based sources.
Important replay rules:
-
If the merger is not flow-enabled, replay fails early when any source requires:
use_data=True, or- a reactive timed sender (
everytogether withon_triggersand/oron_actions).
-
DNA replay now reads
routeas a required field. Missing routes fail clearly instead of producing a partially replayed sender. -
If trigger/action names or serialized
run_while/when_datacallables cannot be resolved, replay may fail for that sender.
None.
Returns None.
from summoner.client.merger import ClientMerger
agent = ClientMerger([{"dna_path": "agent_dna.json"}], name="merged")
agent.initiate_senders()from summoner.client.merger import ClientMerger
from summoner.protocol.triggers import load_triggers
Trigger = load_triggers()
agent = ClientMerger(
[{"dna_path": "agent_dna.json"}],
name="merged",
rebind_globals={"Trigger": Trigger},
)
agent.initiate_senders()from summoner.client.merger import ClientMerger
from summoner.protocol.triggers import load_triggers
Trigger = load_triggers()
agent = ClientMerger(
[{"dna_path": "agent_dna.json"}],
name="merged",
rebind_globals={"Trigger": Trigger},
)
agent.flow().activate()
agent.initiate_senders()def __init__(
self,
dna_list: list[dict[str, Any]],
name: Optional[str] = None,
var_name: Optional[str] = None,
rebind_globals: Optional[dict[str, Any]] = None,
allow_context_imports: bool = True,
verbose_context_imports: bool = False,
) -> NoneConstructs a new SummonerClient from a DNA list by compiling handlers into a dedicated sandbox module, then preparing them for replay via initiate_*.
Key properties of translation:
-
Handlers are not executed in their original modules.
-
Handlers run in the translation sandbox with explicit bindings:
var_name(often"agent") is bound to the translated client,- optional context (imports/globals/recipes) may be applied,
- optional
rebind_globalsmay be injected.
This class also attempts best-effort cleanup of "template clients" that may have been created as a side effect of importing modules referenced by DNA entries.
If the translated client depends on flow-aware sender activation, activate flow before initiate_all() or initiate_senders().
- Type:
list[dict[str, Any]] - Meaning: Parsed DNA entries (already JSON-decoded).
- Note: If the DNA begins with a
__context__entry, it may be applied into the sandbox.
- Type:
Optional[str] - Meaning: Logical name used for logging.
- Type:
Optional[str] - Meaning: The global name used inside handler source code to reference the client (for example
"agent"). - Default behavior: uses
__context__.var_nameif present, otherwise"agent".
- Type:
Optional[dict[str, Any]] - Meaning: Extra globals injected into the sandbox to satisfy referenced symbols (shared objects, Trigger bindings, etc.).
- Type:
bool - Meaning: Whether to execute import lines from a DNA
__context__entry. - Default:
True
- Type:
bool - Meaning: Whether to log successful context imports.
- Default:
False
Creates a ClientTranslation instance (subclass of SummonerClient).
import json
from summoner.client.merger import ClientTranslation
dna_list = json.loads(open("agent_dna.json", "r", encoding="utf-8").read())
agent = ClientTranslation(dna_list, name="translated")
agent.initiate_all()
agent.run(host="127.0.0.1", port=8888)def initiate_all(self) -> NoneReplays all handler types from the DNA list onto this translated client, in the standard order:
upload_statesdownload_stateshookreceivesend
Call this before run(...).
If the replayed sender behavior depends on flow-aware activation, call agent.flow().activate() before initiate_all().
None.
Returns None.
from summoner.client.merger import ClientTranslation
agent = ClientTranslation(dna_list, name="translated")
agent.initiate_all()
agent.run(host="127.0.0.1", port=8888)def initiate_upload_states(self) -> NoneReplays @upload_states() from DNA onto this translated client.
None.
Returns None.
from summoner.client.merger import ClientTranslation
agent = ClientTranslation(dna_list, name="translated")
agent.initiate_upload_states()def initiate_download_states(self) -> NoneReplays @download_states() from DNA onto this translated client.
None.
Returns None.
from summoner.client.merger import ClientTranslation
agent = ClientTranslation(dna_list, name="translated")
agent.initiate_download_states()def initiate_hooks(self) -> NoneReplays @hook(...) entries from DNA onto this translated client.
Direction and priorities are interpreted from the DNA entries and applied to the normal SummonerClient.hook(...) decorator.
None.
Returns None.
from summoner.client.merger import ClientTranslation
agent = ClientTranslation(dna_list, name="translated")
agent.initiate_hooks()def initiate_receivers(self) -> NoneReplays @receive(...) entries from DNA onto this translated client.
Routes and priorities are interpreted from the DNA entries and applied to the normal SummonerClient.receive(...) decorator.
None.
Returns None.
from summoner.client.merger import ClientTranslation
agent = ClientTranslation(dna_list, name="translated")
agent.initiate_receivers()def initiate_senders(self) -> NoneReplays @send(...) entries from DNA onto this translated client.
For everyday usage, this means the translated client regains the outbound handler behavior recorded in DNA. The extra replay work is resolving named trigger/action references and rebuilding optional sender guards and payload filters such as run_while and when_data.
Sender replay details
Triggers and actions are stored in DNA by name and are resolved as follows:
-
Triggers:
- uses a
Triggerbinding found in the sandbox globals (possibly from context orrebind_globals), - otherwise uses
load_triggers().
- uses a
-
Actions:
- resolved by name against the protocol
Actioncontainer.
- resolved by name against the protocol
The translated client also replays sender fields such as use_data, data_mode, when_data, every, and serialized run_while / when_data callables when possible.
Important replay rules:
-
If translation is not flow-enabled, replay fails early when DNA requires:
use_data=True, or- a reactive timed sender.
-
routeis treated as a required DNA field. -
Callable
run_whileguards andwhen_datapredicates are reconstructed from the serialized*_kind,*_value,*_name, and*_sourcefields when possible.
None.
Returns None.
from summoner.client.merger import ClientTranslation
from summoner.protocol.triggers import load_triggers
Trigger = load_triggers()
agent = ClientTranslation(
dna_list,
name="translated",
rebind_globals={"Trigger": Trigger},
)
agent.flow().activate()
agent.initiate_senders()import json
from summoner.client.merger import ClientMerger
a = json.loads(open("a.json", "r", encoding="utf-8").read())
b = json.loads(open("b.json", "r", encoding="utf-8").read())
agent = ClientMerger([a, b], name="merged")
agent.initiate_all()
agent.run(host="127.0.0.1", port=8888)import json
from summoner.client.merger import ClientTranslation
dna_list = json.loads(open("agent_dna.json", "r", encoding="utf-8").read())
agent = ClientTranslation(dna_list, name="translated")
agent.initiate_all()
agent.run(host="127.0.0.1", port=8888)
« Previous: Summoner.client configuration guide | Next: Summoner.client »