Action Runtime
July 8, 2026 · View on GitHub
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, common code runtime, Docker, SQLite executors |
ExecutionResource | 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 → ActionExecutor
Plugin 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 Docker-backed run_python action for deterministic code execution |
agent.enable_shell(...) | mount a Docker-backed run_bash action with workspace roots, command allowlists, timeouts, and bounded output previews |
agent.enable_nodejs(...) | mount a Docker-backed run_nodejs action |
agent.enable_code_runtime(...) | mount a Docker-backed common-language code runtime action for Python, JavaScript/Node.js, TypeScript, C, C++, Go, Rust, Java, C#/.NET, PHP, Ruby, Perl, R, Lua, or Bash |
agent.enable_sqlite(...) | mount a managed query_sqlite action |
agent.enable_workspace_file_actions(...) | expose the current Workspace file area as handler-backed list/search/read/write actions, plus export_file when export=True and write=True |
agent.enable_coding_agent_actions(...) | expose coding-agent Workspace actions for file readback, glob/grep search, targeted edit, unified-diff patch, and guarded full-file writes |
@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. Managed execution environment metadata redacts raw env values
in this visible schema while preserving key names; providers still receive the
raw env only through the execution path.
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_resources=[...]) when you are
building a custom Action backend.
agent.enable_python(...), agent.enable_shell(...), and
agent.enable_nodejs(...) default to sandbox="auto" and
provisioning_profile="strict", which uses a Docker-backed ExecutionResource
profile after checking both the local Docker CLI and daemon. Missing images use
image_pull_policy="never" by default and fail closed with structured
diagnostics such as execution_resource.docker_image_missing; they do not
silently fall back to host execution. Use sandbox="trusted_local" only for
trusted compatibility paths that intentionally use the legacy in-process Python
sandbox, local shell runner, or local Node.js runner.
For Coding Agent, Agently Skills, examples, and framework tests, use
provisioning_profile="developer" or "ci". These profiles default to
image_pull_policy="if_missing" and dependency_policy="install", so the
provider may pull missing Docker images and prepare dependencies from standard
manifest files before running the fixed entrypoint. Dependency installation is
not a model-visible action input; do not ask the model to run pip, npm,
cargo, or other package-manager commands through code action schemas.
For coding-agent style local file work, prefer
agent.enable_coding_agent_actions(...). It exposes read_file,
glob_files, grep_files, edit_file, apply_patch, and guarded
write_file over the current Workspace file root. edit_file(...) can use an
expected_sha256 stale guard, apply_patch(...) applies a unified diff and can
require exact expected_files, and write_file(...) in coding-agent mode
requires either prior read state or an expected hash unless the host disables
that guard. Use shell for tests, builds, git inspection, and read-only
diagnostics; use Workspace file actions for file reading, search, editing, and
writing.
When agent.enable_shell(...) is called without an explicit commands=...
allowlist, Agently uses a small safe shell profile for commands such as pwd,
ls, rg, cat, git status, git diff, git log, python -m pytest, and
python -m pyright. Stdout and stderr are returned as bounded previews; if a
stream exceeds max_output_chars, the full stream is written under the
Workspace root at artifacts/shell/ and referenced from the action result.
allow_unsafe is a host-only direct execution grant; it is not exposed in
model-visible shell action schemas and is stripped from model-planned action
inputs. If a model-selected command is outside the safe profile, route it
through an approval-required action or an ExecutionExchange provider instead of
letting model output grant its own bypass.
Custom actions that need direct-call-only parameters can mark them with
meta={"host_only_input_keys": [...]}; Action Runtime strips those keys from
model-planned structured_plan and native tool-call inputs while preserving
host/direct calls.
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 ExecutionResource;
proxy, timeout, backend, and region are package/executor configuration. Browse
is also Action-native; its default path is Jina Reader -> Playwright -> BS4 ->
restricted curl, while pyautogui is kept as legacy/advanced configuration. The
curl backend is a Browse-internal URL fetch fallback, not model-visible shell
access. Jina Reader delegates the target public URL to https://r.jina.ai/ for
URL-to-Markdown recovery and automatically tries the official alternate endpoint
https://r.jinaai.cn/ when the primary Reader endpoint has a transport or
service failure. Disable it explicitly with
Browse(enable_jina_reader=False, fallback_order=("playwright", "bs4", "curl"))
when that external service boundary is not acceptable.
If a Browse action needs a managed browser/page/session, register it with a
Browser ExecutionResource provider enabled.
Agent Client Protocol (ACP) coding agents are exposed as Action capability, not
as an AgentExecution route. Use agent.use_acp(on_missing="skip") to
scan local ACP endpoints and built-in local coding-agent CLI adapters, then
register acp_list_agents plus acp_run_task only when a runnable agent is
verified. acp_list_agents also returns non-binding adapter-name hints for
common ACP adapters such as codex, claude code / cc, openclaw,
hermes / hermes agent, and gemini; these hints do not make an agent
runnable. Built-in local CLI adapters cover common Codex and Claude Code command
locations in addition to the current process PATH; they use fixed
framework-owned argv templates and do not expose model-visible shell execution.
The default on_missing="skip" records diagnostics and avoids fake runnable
agents; on_missing="error" fails closed. ACP run actions declare
ExecutionResource(kind="acp") so root scope and lifecycle facts stay in the
resource layer. If root is omitted, agent.use_acp() uses the Agent's bound
Workspace files_root as the coding-agent project root; pass root=... only
when the host intentionally authorizes a different project directory. ACP
session reuse is an internal AgentExecution resource policy, not an ordinary
task-start option. CLI adapters are marked
acp_session.persistence="stateless_cli" unless a real protocol session is
available.
AgentTask can also use ACP as an opt-in recovery fallback after a bounded
step or TaskBoard card fails and configured retries are exhausted. This still
uses the registered acp_run_task Action plus ExecutionResource(kind="acp");
ACP is not a route that bypasses AgentExecution or task strategy policy. If the
host never called agent.use_acp(...), the fallback records skipped diagnostics
instead of importing ACP dependencies or inventing an agent.
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.
Model-sourced input safety
Action commands produced by model planning are treated as untrusted input at the
Action boundary. For structured_plan and native_tool_calls commands,
ActionDispatcher filters action_input to the keys declared in the registered
ActionSpec.kwargs before the executor is called. Direct host calls keep their
existing behavior and are not filtered this way.
Filtered calls keep structured diagnostics on the ActionResult, including
action.input.unexpected_keys_stripped, the stripped keys, and bounded previews
of the original and executed kwargs. Timeout and executor exceptions also return
structured Action failures with diagnostics. RuntimeEvent consumers may observe
those facts, but RuntimeEvent does not enforce input safety or authorization.
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, preview truncation metadata, redaction notes, artifact refs, and any
Workspace file refs returned by the Action. 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. Artifact refs
include role, media type, size/bytes, preview size, SHA-256, and truncation
flags so consumers can tell that a preview is not complete evidence.
Actions that explicitly return artifacts or artifact_refs use the same
contract even when the output is small. This includes MCP resource/content
blocks surfaced by MCPActionExecutor; Agently records the declared artifact
metadata, but it does not infer undeclared file writes by scanning directories.
Host actions that create files for later AgentTask or TaskBoard consumption
should return typed file_refs or artifact_refs with the path, size or bytes,
media type, and SHA-256 when available. A path-only payload such as
{"filename": "...", "path": "...", "size": ...} stays visible as bounded
Action result evidence and a ref pointer, but it is not treated as a trusted
Workspace file unless the path is inside the Workspace files root and Workspace
readback succeeds.
Built-in web actions such as Search and Browse do not prompt for package
installation while running. Missing optional dependencies surface as structured
Action failures so service hosts can decide whether to install, retry, or fall
back.
When the digest is still too large for later planning or reply hot paths,
Agently compacts the model-visible digest again: result keeps the bounded
digest, duplicate data / model_digest fields may become same_as="result"
pointers, and artifact refs omit preview bodies while keeping readback ids.
That compaction only applies to hot-path model context; full redacted content
stays in the Action artifact store for explicit readback.
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.
max_output_bytes is an output evidence policy, not a destructive storage
operation. When an Action output exceeds it, Agently records diagnostics and
keeps the full value behind an artifact ref while the model-visible path
continues to use bounded previews.
When a host explicitly calls agent.get_action_result(prompt=...), Agently
marks that prompt as having consumed the action loop even if the returned record
list is empty. A later response read for the same prompt will not re-enter
ActionRuntime just to materialize final text.
Use agent.action.summarize_records(records) when host code needs an
authoritative action-evidence summary:
summary = agent.action.summarize_records(
records,
validation_command_markers=["pytest", "pyright"],
)
assert summary["latest_validation"]["status"] in {"passed", "failed"}
The summary reports failed actions, commands attempted, successful commands, and the latest matching validation command.
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 surface
These 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 SkillsManager-backed Skills execution or AgentTask delegates a bounded action round to ActionRuntime.
agent.get_action_result(..., timeout=N) bounds the full action loop,
including structured planning and native tool-call selection. If the loop
cannot finish before the deadline, Agently raises RuntimeStageStallError with
stage="action_loop_close".
When planning_protocol="native_tool_calls" returns no provider-native tool
calls, Agently emits a skipped diagnostic action record with code
action_runtime.native_tool_calls.empty. Host code should treat that diagnostic
as planning evidence, not as executed work.
When consecutive rounds select the same failing action ids and none of those
records shows progress, the default TriggerFlowActionFlow closes the current
bounded action step and returns the failure evidence it already has. The default
threshold is action.loop.max_consecutive_failed_rounds_per_action = 2
(tool.loop... remains a compatibility alias). This is not a task budget; a
higher-level owner such as AgentTask can then verify, replan, or block from the
structured failure records.
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 ExecutionResource requirement — don't hide lifecycle inside an executor |
See also
- Actions Overview — where Action Runtime stops and orchestration starts
- ExecutionResource — managed MCP/sandbox dependencies
- Tools — the compat surface in more detail
- MCP —
agent.use_mcp(...) - TriggerFlow Overview — orchestration above actions