Action Runtime
Languages: English · 中文
Agently's action stack has three replaceable plugin layers below the orchestration layer:
TriggerFlow ◄── orchestration above actions (loops, branches, pause/resume)
│
▼
ActionRuntime ◄── planning + dispatch
│ (uses ActionFlow as the bridge to the orchestration layer)
▼
ActionExecutor ◄── atomic execution (local function, MCP, sandbox)Layers in detail
| Layer | What it owns | Default builtin |
|---|---|---|
TriggerFlow | high-level orchestration above actions (loops, branches, pause/resume, sub-flow) — see TriggerFlow | the TriggerFlow core |
ActionRuntime | planning protocol, action call normalization, default execution orchestration | AgentlyActionRuntime |
ActionFlow | bridge between an ActionRuntime and a flow representation | TriggerFlowActionFlow |
ActionExecutor | how one action actually runs | local function, MCP, Python/Bash sandbox, Search/Browse, Node.js, Docker, SQLite executors |
ExecutionEnvironment | managed execution dependencies required before an executor call | MCP, Bash, Python, Node, Docker, Browser, SQLite providers |
Action in agently.core is a façade that wires:
ActionRegistryandActionDispatcher(stable core primitives)- one active
ActionRuntimeplugin - one active
ActionFlowplugin
Default wiring:
Agent → ActionExtension → Action façade → ActionRuntime → ActionFlow → ActionExecutorPlugin types
The plugin types you can replace are:
ActionRuntime— for changing the planning protocol or call normalizationActionFlow— for changing the orchestration shape (e.g., custom flow representation)ActionExecutor— for adding a new backend (HTTP, gRPC, custom sandbox, remote worker)
Import the protocols and handler aliases from agently.types.plugins:
from agently.types.plugins import (
ActionExecutor,
ActionRuntime,
ActionFlow,
ActionPlanningHandler,
ActionExecutionHandler,
)The older
ToolManagerplugin type andAgentlyToolManagerclass are kept for explicit legacy use only and emit deprecation warnings once per deprecated API per Python process, unlessruntime.show_deprecation_warningsis disabled. Don't write new plugins againstToolManager.
The preferred surface — actions
For new code:
from agently import Agently
agent = Agently.create_agent()
@agent.action_func
async def add(a: int, b: int) -> int:
"""Add two integers."""
return a + b
@agent.action_func
async def python_code_executor(python_code: str):
"""Execute Python code and return the result."""
...
agent.use_actions([add, python_code_executor])
# Or register and run in one shot
@agent.auto_func
def calculate(formula: str) -> int:
"""Compute {formula}. Use available actions."""
...
print(calculate("3333+6666=?"))| Surface | Purpose |
|---|---|
@agent.action_func | mark a function as an action, derive its schema from signature + docstring |
agent.use_actions(actions) | register a list, single action, or string-named action with the agent |
agent.use_actions(["name1", "name2"]) | register pre-registered actions by name |
agent.use_actions(Search(...)) | mount the built-in Search package from agently.builtins.actions |
agent.use_actions(Browse(...)) | mount the built-in Browse package from agently.builtins.actions |
agent.enable_python(...) | mount a managed run_python action for deterministic code execution |
agent.enable_shell(...) | mount a managed run_bash action with workspace and command allowlists |
agent.enable_nodejs(...) | mount a managed run_nodejs action |
agent.enable_sqlite(...) | mount a managed query_sqlite action |
agent.enable_workspace_file_actions(...) | expose the current Workspace file area as list/search/read/write actions |
@agent.auto_func | turn a Python function signature + docstring into a model-backed implementation that uses the agent's actions |
agent.get_action_result(prompt=turn.prompt) | retrieve action call records for a request-scoped turn |
extra.action_logs | structured logs produced during the action loop |
agent.action.get_action_info() and agent.action.get_tool_info() return the visible action/tool schemas registered on that agent by default, including agent-scoped actions, MCP tools mounted through agent.use_mcp(...), and enable_* component helpers. Pass explicit tags=[...] only when you need a narrow subset.
For application code, prefer enable_* helpers when the goal is to give the model a common capability such as Python, shell, or workspace access. Use register_action(..., executor=..., execution_environments=[...]) when you are building a custom Action backend.
Built-in capability packages live under agently.builtins.actions. For example:
from agently.builtins.actions import Browse, Search
agent.use_actions(Search(timeout=15, backend="auto"))
agent.use_actions(Browse())Search is an Action-native package and does not use Execution Environment; proxy, timeout, backend, and region are package/executor configuration. Browse is also Action-native; its default path is Playwright + BS4, while pyautogui is kept as legacy/advanced configuration. If a Browse action needs a managed browser/page/session, register it with Browser Execution Environment enabled.
The desc= argument on enable_* helpers is optional. By default it is appended as additional guidance so the model still sees the baseline usage and safety constraints. Use desc_mode="override" when you intentionally want to replace the default description, or desc_mode="default" to ignore the supplied description and keep only the built-in one.
Execution recall
Instruction-heavy actions such as run_bash, run_python, run_nodejs, query_sqlite, browse, and search keep later model context compact by recording an execution digest plus artifact references.
The digest is what the next action-planning round normally sees. It includes the action id, call id, purpose, status, a compact instruction preview, result preview, redaction notes, and artifact refs. Full raw content such as complete code, shell output, SQL rows, page HTML, screenshots, or logs is retained as a redacted artifact instead of being inserted into every prompt.
When the model or application needs the omitted detail, read it explicitly:
turn = agent.input("Use the action and summarize the result.")
records = agent.get_action_result(prompt=turn.prompt)
artifact_ref = records[0]["artifact_refs"][0]
raw = agent.action.read_action_artifact(
artifact_id=artifact_ref["artifact_id"],
action_call_id=artifact_ref["action_call_id"],
)Action.to_action_results(records) uses the digest for instruction-heavy actions, so follow-up replies can reason about what happened without receiving the full payload by default.
Compatibility surface — tools
The older surface still works:
@agent.tool_func
def add(a: int, b: int) -> int:
return a + b
agent.use_tool(add)
agent.use_tools([add])
agent.use_mcp("https://...")
agent.use_sandbox(...)
extra.tool_logs # equivalent to extra.action_logs at the old surfaceThese remain valid public mounting surfaces. They map onto the new action runtime internally — they don't imply a ToolManager implementation. Migrate to the action surface when convenient; nothing breaks immediately.
Planning model key
Action planning is a model-owned step. When an Agent uses model_pool, set action.planning_model_key to the business model key that should plan action rounds:
agent.set_settings("model_pool", {"task-main": "deepseek-chat-prod"})
agent.set_settings("model_profiles", {
"deepseek-chat-prod": {
"provider": "OpenAICompatible",
"base_url": "https://api.deepseek.com/v1",
"model": "deepseek-chat",
"api_key_pool": "deepseek-prod",
}
})
agent.set_settings("action.planning_model_key", "task-main")This applies to the default structured-plan and native tool-call planning paths. It is especially important when a higher-level runtime such as SkillsExecutor or AgentTaskLoop delegates a bounded action round to ActionRuntime.
Handler interface
If you're writing a custom ActionRuntime or ActionFlow plugin, the planning and execution handlers use one stable two-argument contract:
async def planning_handler(
context: ActionRunContext,
request: ActionPlanningRequest,
) -> ActionDecision:
...
async def execution_handler(
context: ActionRunContext,
request: ActionExecutionRequest,
) -> list[ActionResult]:
...Context fields include prompt, settings, agent_name, round_index, max_rounds, done_plans, last_round_records, action, runtime. Request fields include action_list, planning_protocol, action_calls, async_call_action, concurrency, timeout.
Custom ActionFlow plugins may accept an optional runtime_observation_handler keyword. If present, the flow should send plain observation dictionaries to that handler instead of emitting official action.* or tool.* RuntimeEvents directly; core maps those observations to the official event stream.
There is no legacy positional handler signature — the public contract is (context, request) only.
Extension guidance
| You want to change | Replace |
|---|---|
| Just the backend (HTTP, gRPC, remote worker, sandbox) | ActionExecutor |
| The planning protocol or how calls are normalized | ActionRuntime |
| The orchestration shape between runtime and flow | ActionFlow |
| Higher-level flow control over many action calls | use TriggerFlow above the runtime — don't embed it inside an executor |
| Lifecycle for MCP/sandbox/process-like dependencies | declare an ExecutionEnvironment requirement — don't hide lifecycle inside an executor |
See also
- Actions Overview — where Action Runtime stops and orchestration starts
- Execution Environment — managed MCP/sandbox dependencies
- Tools — the compat surface in more detail
- MCP —
agent.use_mcp(...) - TriggerFlow Overview — orchestration above actions