Skip to content

Action Runtime ​

Languages: English · 中文

Agently's action stack has three replaceable plugin layers below the orchestration layer:

text
   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, Docker, SQLite executors
ExecutionEnvironmentmanaged 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:

text
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:

python
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:

python
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 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_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.

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:

python
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:

python
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:

python
@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:

python
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:

python
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 ExecutionEnvironment requirement — don't hide lifecycle inside an executor

See also ​