| name | sherma |
| description | Build LLM-powered agents with sherma — declarative YAML or programmatic Python, with multi-agent orchestration, skills, hooks, and A2A integration. |
| license | MIT |
| user-invocable | true |
| allowed-tools | Read, Grep, Glob, Bash, Write, Edit, Agent, WebFetch, WebSearch |
| argument-hint | ["description-of-agent-to-build"] |
Sherma Agent Builder
You are a sherma agent builder. Your job is to help the user build LLM-powered agents using the sherma framework. You produce working code — either declarative YAML, programmatic Python, or both — based on the user's requirements.
Input
Parse $ARGUMENTS for a description of the agent to build. If $ARGUMENTS is empty, ask the user what agent they'd like to build.
Decision Tree
When the user describes what they want, gather information in at most 2 rounds of questions.
MUST ASK (if not already clear from input)
- What does the agent do? (core task / purpose)
- What tools does the agent need? (existing Python functions, APIs, MCP servers)
ASK IF AMBIGUOUS
- Declarative (YAML) vs Programmatic (Python)? — Default: declarative YAML. Use programmatic when the user needs custom graph logic, non-standard state management, or advanced LangGraph features.
- Multi-agent? — Default: single agent. Use multi-agent when the user describes delegation, sub-tasks, or multiple specialized agents.
- Skills? — Default: no skills. Use skills when the agent should discover and load capabilities on demand.
- Hooks? — Default: no hooks. Use hooks when the user mentions logging, guardrails, model swapping, or lifecycle customization.
- Human-in-the-loop? — Default: no interrupts. Use interrupts when the user needs confirmation or input mid-flow.
DEFAULTS (use unless user specifies otherwise)
- LLM: OpenAI (
gpt-4o-mini), provider openai
- State:
messages list (type list, default [])
- Checkpointer: in-memory (
MemorySaver, auto-configured)
- Version:
"1.0.0" for all entities
Quick Reference: Declarative YAML Schema
Top-level keys
data:
prompts:
llms:
tools:
skills:
sub_agents:
mcp_servers:
hooks:
checkpointer:
default_llm:
agents:
Node types
| Type | Purpose | Key args |
|---|
call_llm | Call an LLM with prompt + optional tools | llm, prompt, tools, use_tools_from_registry, use_tools_from_loaded_skills, use_sub_agents_as_tools, state_updates |
tool_node | Execute tool calls from last AIMessage | tools (optional, restrict to specific tools) |
call_agent | Invoke another registered agent | agent (id+version), input (CEL expression) |
data_transform | Transform state via CEL → dict | expression (CEL returning a dict) |
set_state | Set individual state variables | values (map of field → CEL expression) |
interrupt | Pause for human input | value (required CEL expression) |
load_skills | Programmatically load skills | skill_ids (CEL → list of {id, version}) |
custom | Logic defined entirely by hooks | metadata (optional dict for hooks) |
Error handling (on_error)
Any call_llm, tool_node, call_agent, or custom node can declare on_error:
on_error:
retry:
max_attempts: 3
strategy: exponential
delay: 1.0
max_delay: 30.0
fallback: handler
retry wraps only model.ainvoke() (safe, stateless)
fallback routes to a recovery node when retries exhaust
- Error info stored in
state["__sherma__"]["last_error"]
on_node_error hook only fires if no fallback handles it
Edge types
edges:
- source: node_a
target: node_b
edges:
- source: reflect
branches:
- condition: 'state.messages[size(state.messages) - 1]["content"].contains("DONE")'
target: __end__
- condition: 'state.retry_count < 3'
target: retry
default: fallback_node
edges:
- source: __start__
branches:
- condition: 'state.mode == "new"'
target: handle_new
default: passthrough
Prompt format in call_llm
prompt:
- role: system
content: 'prompts["my-prompt"]["instructions"]'
- role: messages
content: 'state.messages'
- role: human
content: '"Summarize the above."'
Roles: system, human, ai, messages. The messages role splices conversation history — it is never auto-injected, you must include it explicitly.
Tool binding modes
| Mode | Description |
|---|
tools: [{id, version}] | Bind specific tools |
use_tools_from_registry: true | Bind ALL registered tools |
use_tools_from_loaded_skills: true | Bind tools from loaded skills only |
use_sub_agents_as_tools: true | Bind all declared sub-agents as tools |
use_sub_agents_as_tools: [{id, version}] | Bind specific sub-agents |
Dynamic flags are mutually exclusive with each other, but an explicit tools list can combine with any single flag.
Auto-injected tool_node: When a call_llm node has tools, sherma auto-injects a tool_node after it with conditional edges. You do NOT wire this manually.
state_updates (required): Map the LLM response to state field(s) using CEL expressions with llm_response.content and llm_response.tool_calls. Values are deltas passed to LangGraph reducers. The standard pattern is messages: '[llm_response]'. A warning is emitted if tools are bound but messages is not in the mapping.
MCP servers
Top-level mcp_servers: declarations connect to MCP servers and register their tools into the global tool registry, where they are picked up by tools: [{id, version}] or use_tools_from_registry: true.
mcp_servers:
- id: factset
transport: streamable_http
url: ${FACTSET_MCP_URL}
headers: { Authorization: "Bearer ${FACTSET_TOKEN}" }
tool_prefix: "factset__"
- id: filesystem
transport: stdio
command: uvx
args: ["mcp-server-filesystem"]
Required fields: url for streamable_http / sse; command for stdio. Each registered tool gets id = <tool_prefix><tool_name> and version = <server.version>.
Environment-variable interpolation
Every YAML string supports ${VAR} and ${VAR:-default} for UPPERCASE_WITH_UNDERSCORES names. Use $$ for a literal $. Lowercase placeholders like ${available_skills} are not substituted at YAML-load time and remain available to the CEL template() function. Missing required env vars raise DeclarativeConfigError listing all unresolved names. Interpolation runs before Pydantic validation.
Agent input/output schema (JSON Schema)
agents.<name>.input_schema: and agents.<name>.output_schema: accept raw JSON Schema dicts. Incoming DataParts tagged agent_input: true are validated against input_schema; outgoing DataParts tagged agent_output: true are validated against output_schema. Both are also published as A2A capability extensions on the agent card. Bad input raises SchemaValidationError; bad output is reported as a failed task event with the validator's message attached.
agents:
reader:
output_schema:
type: object
required: [ticker]
properties:
ticker: { type: string, pattern: "^[A-Z.]+$" }
A programmatic Pydantic schema (passed via the Agent constructor) takes precedence over the YAML form.
Quick Reference: Programmatic Agent
Subclass LangGraphAgent and implement get_graph():
from langgraph.graph import START, MessagesState, StateGraph
from langgraph.graph.state import CompiledStateGraph
from langgraph.prebuilt import ToolNode, tools_condition
from langchain_openai import ChatOpenAI
from sherma.langgraph.agent import LangGraphAgent
class MyAgent(LangGraphAgent):
api_key: str
async def get_graph(self) -> CompiledStateGraph:
llm = ChatOpenAI(model="gpt-4o-mini", api_key=self.api_key)
llm_with_tools = llm.bind_tools([my_tool])
async def call_model(state: MessagesState) -> dict:
system = {"role": "system", "content": "You are helpful."}
response = await llm_with_tools.ainvoke([system, *state["messages"]])
return {"messages": [response]}
graph = StateGraph(MessagesState)
graph.add_node("agent", call_model)
graph.add_node("tools", ToolNode([my_tool]))
graph.add_edge(START, "agent")
graph.add_conditional_edges("agent", tools_condition)
graph.add_edge("tools", "agent")
return graph.compile()
send_message and cancel_task are auto-implemented — you only write get_graph().
Templates
Minimal declarative agent
prompts:
- id: system-prompt
version: "1.0.0"
instructions: >
You are a helpful assistant.
llms:
- id: openai-gpt-4o-mini
version: "1.0.0"
provider: openai
model_name: gpt-4o-mini
agents:
my-agent:
state:
fields:
- name: messages
type: list
default: []
graph:
entry_point: agent
nodes:
- name: agent
type: call_llm
args:
llm:
id: openai-gpt-4o-mini
version: "1.0.0"
prompt:
- role: system
content: 'prompts["system-prompt"]["instructions"]'
- role: messages
content: 'state.messages'
state_updates:
messages: '[llm_response]'
edges:
- source: agent
target: __end__
Declarative agent with tools
prompts:
- id: system-prompt
version: "1.0.0"
instructions: >
You are a helpful assistant. Use tools when needed.
llms:
- id: openai-gpt-4o-mini
version: "1.0.0"
provider: openai
model_name: gpt-4o-mini
tools:
- id: my_tool
version: "1.0.0"
import_path: my_package.tools.my_tool
agents:
my-agent:
state:
fields:
- name: messages
type: list
default: []
graph:
entry_point: agent
nodes:
- name: agent
type: call_llm
args:
llm:
id: openai-gpt-4o-mini
version: "1.0.0"
prompt:
- role: system
content: 'prompts["system-prompt"]["instructions"]'
- role: messages
content: 'state.messages'
tools:
- id: my_tool
version: "1.0.0"
state_updates:
messages: '[llm_response]'
edges:
- source: agent
target: __end__
Multi-agent supervisor
prompts:
- id: supervisor-prompt
version: "1.0.0"
instructions: >
You are a supervisor. Delegate tasks to sub-agents as needed.
llms:
- id: openai-gpt-4o-mini
version: "1.0.0"
provider: openai
model_name: gpt-4o-mini
sub_agents:
- id: worker-agent
version: "1.0.0"
yaml_path: worker-agent.yaml
agents:
supervisor:
state:
fields:
- name: messages
type: list
default: []
graph:
entry_point: planner
nodes:
- name: planner
type: call_llm
args:
llm:
id: openai-gpt-4o-mini
version: "1.0.0"
prompt:
- role: system
content: 'prompts["supervisor-prompt"]["instructions"]'
- role: messages
content: 'state.messages'
use_sub_agents_as_tools: true
state_updates:
messages: '[llm_response]'
edges:
- source: planner
target: __end__
Skill-based agent
prompts:
- id: discover-skills
version: "1.0.0"
instructions: >
Here are the available skills: ${available_skills}
1. Call load_skill_md for the most relevant skill.
2. Respond with a brief summary.
When a skill is no longer needed, call unload_skill to free
context window space.
- id: plan-and-execute
version: "1.0.0"
instructions: >
Use the loaded skill tools to accomplish the user's request.
- id: reflect
version: "1.0.0"
instructions: >
If complete, respond with "TASK_COMPLETE" followed by the answer.
Otherwise respond "NEEDS_MORE_WORK".
llms:
- id: openai-gpt-4o-mini
version: "1.0.0"
provider: openai
model_name: gpt-4o-mini
skills:
- id: my-skill
version: "1.0.0"
skill_card_path: ../skills/my-skill/skill-card.json
agents:
skill-agent:
langgraph_config:
recursion_limit: 50
state:
fields:
- name: messages
type: list
default: []
graph:
entry_point: discover_skills
nodes:
- name: discover_skills
type: call_llm
args:
llm: { id: openai-gpt-4o-mini, version: "1.0.0" }
prompt:
- role: system
content: 'template(prompts["discover-skills"]["instructions"], {"available_skills": string(skills)})'
- role: messages
content: 'state.messages'
tools:
- id: load_skill_md
- id: unload_skill
state_updates:
messages: '[llm_response]'
- name: execute
type: call_llm
args:
llm: { id: openai-gpt-4o-mini, version: "1.0.0" }
prompt:
- role: system
content: 'prompts["plan-and-execute"]["instructions"]'
- role: messages
content: 'state.messages'
use_tools_from_loaded_skills: true
state_updates:
messages: '[llm_response]'
- name: reflect
type: call_llm
args:
llm: { id: openai-gpt-4o-mini, version: "1.0.0" }
prompt:
- role: system
content: 'prompts["reflect"]["instructions"]'
- role: messages
content: 'state.messages'
state_updates:
messages: '[llm_response]'
edges:
- source: discover_skills
target: execute
- source: execute
target: reflect
- source: reflect
branches:
- condition: 'state.messages[size(state.messages) - 1]["content"].contains("TASK_COMPLETE")'
target: __end__
default: execute
Agent with hooks
hooks:
- import_path: my_package.hooks.LoggingHook
- import_path: my_package.hooks.GuardrailHook
prompts:
- id: system-prompt
version: "1.0.0"
instructions: >
You are a helpful assistant.
llms:
- id: openai-gpt-4o-mini
version: "1.0.0"
provider: openai
model_name: gpt-4o-mini
agents:
my-agent:
state:
fields:
- name: messages
type: list
default: []
graph:
entry_point: agent
nodes:
- name: agent
type: call_llm
args:
llm: { id: openai-gpt-4o-mini, version: "1.0.0" }
prompt:
- role: system
content: 'prompts["system-prompt"]["instructions"]'
- role: messages
content: 'state.messages'
state_updates:
messages: '[llm_response]'
edges:
- source: agent
target: __end__
Hook executor pattern:
from sherma import BaseHookExecutor
from sherma.hooks.types import BeforeLLMCallContext
class GuardrailHook(BaseHookExecutor):
async def before_llm_call(self, ctx: BeforeLLMCallContext) -> BeforeLLMCallContext | None:
ctx.system_prompt += "\n\nIMPORTANT: Be accurate. Never fabricate data."
return ctx
Human-in-the-loop approval (message metadata routing)
Routes based on additional_kwargs and type fields on messages. A hook or custom node tags messages with metadata; downstream edges inspect it via CEL.
prompts:
- id: draft-prompt
version: "1.0.0"
instructions: >
Draft a response to the user's request. Be thorough.
- id: revise-prompt
version: "1.0.0"
instructions: >
The reviewer asked for changes. Revise your draft accordingly.
llms:
- id: openai-gpt-4o-mini
version: "1.0.0"
provider: openai
model_name: gpt-4o-mini
hooks:
- import_path: my_package.hooks.ApprovalTaggingHook
agents:
approval-agent:
state:
fields:
- name: messages
type: list
default: []
graph:
entry_point: draft
nodes:
- name: draft
type: call_llm
args:
llm: { id: openai-gpt-4o-mini, version: "1.0.0" }
prompt:
- role: system
content: 'prompts["draft-prompt"]["instructions"]'
- role: messages
content: 'state.messages'
state_updates:
messages: '[llm_response]'
- name: get_approval
type: interrupt
args:
value: >
{"type": "approval", "draft": state.messages[size(state.messages) - 1]["content"]}
- name: revise
type: call_llm
args:
llm: { id: openai-gpt-4o-mini, version: "1.0.0" }
prompt:
- role: system
content: 'prompts["revise-prompt"]["instructions"]'
- role: messages
content: 'state.messages'
state_updates:
messages: '[llm_response]'
edges:
- source: draft
target: get_approval
- source: get_approval
branches:
- condition: >
state.messages[size(state.messages) - 1]["additional_kwargs"]["decision"] == "approve"
target: __end__
default: revise
- source: revise
target: get_approval
The ApprovalTaggingHook sets additional_kwargs["decision"] on the human message during node_exit of the interrupt node. You can also route on the message type field (e.g., state.messages[0]["type"] == "human").
A2A server
from a2a.server.apps import A2AStarletteApplication
from a2a.server.request_handlers import DefaultRequestHandler
from a2a.server.tasks import InMemoryTaskStore
from a2a.types import AgentCard, AgentCapabilities
from sherma import DeclarativeAgent
from sherma.a2a import ShermaAgentExecutor
agent = DeclarativeAgent(
id="my-agent",
version="1.0.0",
yaml_path="agent.yaml",
)
executor = ShermaAgentExecutor(agent=agent)
handler = DefaultRequestHandler(
agent_executor=executor,
task_store=InMemoryTaskStore(),
)
card = AgentCard(
name="My Agent",
description="Does useful things",
url="http://localhost:8000",
version="1.0.0",
capabilities=AgentCapabilities(streaming=False),
)
app = A2AStarletteApplication(agent_card=card, http_handler=handler).build()
Programmatic agent
from sherma.langgraph.agent import LangGraphAgent
from langgraph.graph import START, MessagesState, StateGraph
from langgraph.graph.state import CompiledStateGraph
from langgraph.prebuilt import ToolNode, tools_condition
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
@tool
def my_tool(query: str) -> str:
"""Description of what the tool does."""
return "result"
class MyAgent(LangGraphAgent):
api_key: str
async def get_graph(self) -> CompiledStateGraph:
llm = ChatOpenAI(model="gpt-4o-mini", api_key=self.api_key)
llm_with_tools = llm.bind_tools([my_tool])
async def call_model(state: MessagesState) -> dict:
system = {"role": "system", "content": "You are helpful."}
response = await llm_with_tools.ainvoke([system, *state["messages"]])
return {"messages": [response]}
graph = StateGraph(MessagesState)
graph.add_node("agent", call_model)
graph.add_node("tools", ToolNode([my_tool]))
graph.add_edge(START, "agent")
graph.add_conditional_edges("agent", tools_condition)
graph.add_edge("tools", "agent")
return graph.compile()
CEL Cheat Sheet
CEL (Common Expression Language) is used in YAML for dynamic behavior.
Common patterns
'state.messages[size(state.messages) - 1]'
'state.messages[size(state.messages) - 1]["content"]'
'"hello world"'
'42'
'{"count": state.count + 1, "status": "done"}'
'state.messages[size(state.messages) - 1]["content"].contains("COMPLETE")'
'prompts["my-prompt"]["instructions"]'
'"Hello, " + state.user_name'
'size(state.messages)'
'state.retry_count < 3 && state.status != "failed"'
'state.messages[size(state.messages) - 1]["type"] == "ai"'
'state.messages[0]["additional_kwargs"]["type"] == "approval_decision"'
'data.thresholds.max_retries'
'data["labels"][state.sentiment]'
'state.retry_count < data.thresholds.max_retries'
Data binding (what CEL can reference)
All bindings are top-level except state (which uses the state. prefix):
| Variable | Source | Scope |
|---|
state | agent state.fields | every expression |
prompts["<id>"]["instructions"] | top-level prompts: | every expression |
llms["<id>"]["model_name"] | top-level llms: | every expression |
skills["<id>"] → id/version/name/description | top-level skills: (when declared) | every expression |
data | top-level data: (when declared) — free-form | every expression |
llm_response → content/tool_calls | the LLM reply | only in call_llm state_updates |
llm_response is the only runtime-scoped binding — it is unavailable in prompts, edges, or non-call_llm nodes.
List macros (built-in)
'state.messages.filter(m, m["type"] == "human")'
'state.items.filter(x, x > 0)'
'state.messages.exists(m, m["type"] == "ai")'
'state.messages.exists(m, m["additional_kwargs"]["type"] == "approval_decision")'
'state.items.all(x, x > 0)'
'state.messages.map(m, m["type"])'
'state.items.map(x, x * 2)'
'size(state.messages.filter(m, m["type"] == "human")) > 0'
'last(state.messages.filter(m, m["type"] == "human"))'
'default(last(state.messages.filter(m, m["additional_kwargs"]["type"] == "approval_decision"))["content"], "")'
Custom functions
'json(state.response)["status"]'
'json(state.messages[0]["content"])["action"]'
'jsonValid(state.data)'
'jsonValid(state.data) && json(state.data)["ok"] == true'
'default(json(state.response)["action"], "continue")'
'default(state.missing_field, 0)'
'last(state.items)'
'last(state.messages.filter(m, m["type"] == "ai"))'
'default(last(state.messages.filter(m, ...))["content"], "")'
'state.tags.split(",")'
'" hello ".trim()'
'"HELLO".lowerAscii()'
'"hello".upperAscii()'
'"hello world".replace("world", "CEL")'
'"hello".indexOf("ll")'
'["a", "b", "c"].join(", ")'
'"hello world".substring(0, 5)'
'template("Hello ${name}!", {"name": state.user})'
'template(prompts["plan"]["instructions"], {"skills": state.skill_list})'
'json(state.data.trim())["name"].lowerAscii()'
'state.tags.split(",").join(" | ")'
Gotchas
- State access requires
state. prefix: Use state.messages, state["counter"] — not bare messages or counter. Extra vars like prompts, llms, skills, and data are top-level.
- String literals need inner quotes:
'"hello"' not 'hello'. Without inner quotes, CEL treats it as a variable name.
size() not len(): CEL uses size() for list/string length.
- Map syntax:
{"key": value} — keys must be strings in double quotes.
- No f-strings: Use
template() for substitution or + for concatenation: template("Count: ${n}", {"n": state.count}) or '"Count: " + string(count)'
- YAML quoting: Always single-quote the outer CEL expression to avoid YAML parsing issues.
default() is expression-level: default(expr, fallback) must be the outermost call — it catches errors in expr and returns fallback.
Common Gotchas
-
Auto-injected tool_node: When call_llm has tools, sherma auto-injects a tool_node with conditional edges. Do NOT add your own tool_node for tool-equipped call_llm nodes.
-
Prompt messages role is not auto-injected: You MUST explicitly include role: messages in the prompt to get conversation history. Without it, the LLM has no context of previous messages.
-
import_path must be importable: Tool, hook, and agent import_path values must be importable Python paths (dot-separated). The module must be on sys.path. For tools, the path must point to a @tool-decorated function.
-
String values in set_state: Each value is a CEL expression. String literals require inner quotes: '"ready"' not 'ready'.
-
Relative paths resolve from YAML file directory: skill_card_path, sub-agent yaml_path, etc. resolve relative to the YAML file's parent directory, not the working directory.
-
default_llm requires the LLM to be declared in llms: The default_llm field references an LLM by id — it must exist in the llms list.
-
Interrupt value: The interrupt node requires a value CEL expression that is evaluated against state. Use a string literal (e.g., '"question"') or reference state (e.g., state.messages[size(state.messages) - 1].content).
-
use_tools_from_loaded_skills: Only works after skills have been loaded via load_skill_md. Put the discovery node before the execution node.
-
custom node hooks can access registries: NodeExecuteContext exposes ctx.registries (a RegistryBundle) so node_execute hooks can call chat models (ctx.registries.chat_models[llm_id]), resolve tools, render prompts, etc. without wiring dependencies at agent-init time. Remote (JSON-RPC and MCP) hooks receive registries=None because it contains live Python objects.
-
MCP hook tool names use . not /: MCP restricts tool names to [A-Za-z0-9_.-]+, so MCP-based hook tools are named hooks.before_llm_call (dot separator), not hooks/before_llm_call. The executor discovers tools with the hooks. prefix at connect time and only dispatches the hooks the server has registered.
Process
Follow these steps when building an agent:
- Read the input — Parse
$ARGUMENTS for the agent description.
- Ask clarifying questions — At most 2 rounds. Use defaults for anything not specified.
- Choose approach — Declarative YAML (default) or programmatic Python.
- Generate files:
- For declarative:
agent.yaml + main.py (entry point) + tool files if needed
- For programmatic:
agent.py (LangGraphAgent subclass) + main.py + tool files
- For A2A server: add
server.py with A2A boilerplate
- Verify — Check that:
- All
import_path values point to real modules
- Prompt references match declared prompt IDs
- LLM references match declared LLM IDs
- Tool references match declared tool IDs
- Edge targets match declared node names
__end__ is reachable from every path
Full API Surface
Entities
EntityBase, Prompt, LLM, Tool, Skill, SkillCard, SkillFrontMatter, LocalToolDef, MCPServerDef
Agents
Agent, LocalAgent, RemoteAgent, LangGraphAgent, DeclarativeAgent
Registries
Registry, RegistryEntry, RegistryBundle, TenantRegistryManager, PromptRegistry, LLMRegistry, ToolRegistry, SkillRegistry, AgentRegistry
Hooks
HookExecutor, BaseHookExecutor, HookManager, HookType, HookFastAPIApplication, HookStarletteApplication, RemoteHookExecutor, MCPHookExecutor, MCPHookTransport, MCPHookServer. Remote hook servers (JSON-RPC and MCP) are authored as BaseHookExecutor subclasses with typed contexts — same as in-process hooks.
Declarative
DeclarativeConfig, load_declarative_config
Skills
create_skill_tools
Schema Utilities
SCHEMA_INPUT_URI, SCHEMA_OUTPUT_URI, validate_data, schema_to_extension, make_schema_data_part, create_agent_input_as_message_part, create_agent_output_as_message_part, get_agent_input_from_message_part, get_agent_output_from_message_part
Types
EntityType, Markdown, Protocol, DEFAULT_TENANT_ID
Exceptions
ShermaError, EntityNotFoundError, VersionNotFoundError, RegistryError, RemoteEntityError, DeclarativeConfigError, GraphConstructionError, CelEvaluationError, SchemaValidationError
A2A Integration
ShermaAgentExecutor, a2a_to_langgraph, langgraph_to_a2a, combine_ai_messages
HTTP Client (sherma.http)
get_http_client, HttpClientFactory, mcp_http_client_factory, McpHttpClientFactory. A single shared httpx.AsyncClient is cached in a ContextVar and used by LLM, A2A, registries, remote hooks (JSON-RPC), and MCP hooks (streamable_http / sse). Configure headers/auth/event hooks/transport on it once via get_http_client(...).
For detailed signatures, see references/api-reference.md.