Run actions requested by an interaction¶
When a model requests tools, its assistant Interaction
contains action_calls and the corresponding action_inputs. The included
runner turns those records back into live nested A11 actions, waits for them,
and returns output fragments. Those outputs belong on a new user
interaction, which follows the assistant interaction containing the calls.
import a11
from a11.sdk.llm import Interaction, Role
from a11.sdk.llm_tools.runner import execute_actions_from_interaction
# `parent_action` is the running model action. Its registry and deadline are
# inherited by each nested tool call.
outputs = await execute_actions_from_interaction(
assistant_interaction,
parent_action,
)
tool_result_interaction = Interaction(
previous_interaction_id=assistant_interaction.id,
role=Role.USER,
action_outputs=outputs,
content=[
a11.to_chunk(
{
"role": "user",
# This adapter returns the provider's tool-result blocks.
"content": await build_tool_result_blocks(outputs),
}
)
],
)
Do not mutate assistant_interaction.action_outputs. The assistant interaction
is the model's request to use tools; the new user interaction is the
application's response. Keeping them as separate, ordered turns preserves the
conversation structure expected by Claude and the other included handlers.
The runner performs the lifecycle work that is easy to get subtly wrong:
- It checks every requested name against
x-a11-allowed-llm-actions. - It resolves the schema and handler from the action registry.
- It creates a nested action, preserves the model’s call ID, and propagates the parent deadline.
- It streams input fragments to their ports while leaving autofilled ports to the runtime.
- It waits for completion and maps output port names into the JSON-facing fields declared by the action schema.
The runner returns those fragments; it does not create the user interaction or
encode provider content. For example, interact_with_claude builds Anthropic
tool_result blocks, places them in a new Role.USER interaction alongside
action_outputs, emits that interaction, and only then asks Claude to
continue.
Make the allow-list explicit¶
The registry defines what the application can run. The header defines what this particular model call may run. Both are required boundaries.
from a11.sdk.llm import LlmHeaders
parent_action.set_header(
LlmHeaders.ALLOWED_LLM_ACTIONS.value,
# Patterns are full-match regular expressions.
r"look_up_order|search_catalog",
)
Do not build this value from untrusted model output. Choose it from application
policy, user permissions, and the needs of the current workflow. A call outside
the allow-list fails with PERMISSION_DENIED before an action is started.
Most applications do not call execute_actions_from_interaction directly:
the included interact_with_* handlers call it while continuing the model/tool
loop. Use it directly when building a custom model provider or an interaction
orchestrator with its own loop.