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

LayerWhat it ownsDefault builtin
TriggerFlowhigh-level orchestration above actions (loops, branches, pause/resume, sub-flow) — see TriggerFlowthe TriggerFlow core
ActionRuntimeplanning protocol, action call normalization, default execution orchestrationAgentlyActionRuntime
ActionFlowbridge between an ActionRuntime and a flow representationTriggerFlowActionFlow
ActionExecutorhow one action actually runslocal function, MCP, Python/Bash sandbox, Search/Browse, Node.js, common code runtime, Docker, SQLite executors
ExecutionResourcemanaged execution dependencies required before an executor callMCP, Bash, Python, Node, Docker, Browser, SQLite providers

Action in agently.core is a façade that wires:

  • ActionRegistry and ActionDispatcher (stable core primitives)
  • one active ActionRuntime plugin
  • one active ActionFlow plugin

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 normalization
  • ActionFlow — 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 ToolManager plugin type and AgentlyToolManager class are kept for explicit legacy use only and emit deprecation warnings once per deprecated API per Python process, unless runtime.show_deprecation_warnings is disabled. Don't write new plugins against ToolManager.

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=?"))
SurfacePurpose
@agent.action_funcmark 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_functurn 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_logsstructured 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 changeReplace
Just the backend (HTTP, gRPC, remote worker, sandbox)ActionExecutor
The planning protocol or how calls are normalizedActionRuntime
The orchestration shape between runtime and flowActionFlow
Higher-level flow control over many action callsuse TriggerFlow above the runtime — don't embed it inside an executor
Lifecycle for MCP/sandbox/process-like dependenciesdeclare an ExecutionResource requirement — don't hide lifecycle inside an executor

See also