> ## Documentation Index
> Fetch the complete documentation index at: https://docs.agentscope.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Python Tool

> Build tools from classes or plain functions, and control how they run

A Python tool is any object satisfying the `ToolBase` interface. AgentScope ships built-in tools for common operations and exposes the same interface for developers to build their own:

| Topic                                                      | Covers                                                        |
| ---------------------------------------------------------- | ------------------------------------------------------------- |
| [ToolBase Interface](#toolbase-interface)                  | The attributes and methods every tool implements              |
| [Built-in Tools](#use-built-in-tools)                      | Ready-to-use tools: `Bash`, file tools, plan tools            |
| [Switch Tool Backend](#switch-tool-backend)                | Run built-in tools against Docker, E2B, or other environments |
| [Create Custom Tool](#create-custom-tool)                  | Subclass `ToolBase` with your own schema and permission logic |
| [Wrap Function as Tool](#wrap-function-as-tool)            | Turn a plain Python function into a tool with `FunctionTool`  |
| [External Execution Tool](#define-external-execution-tool) | Delegate execution to a human or an external system           |
| [Tool Middleware](#tool-middleware)                        | Attach onion-style hooks to a tool instance                   |

## ToolBase Interface

`ToolBase` is the abstract base class every tool satisfies. The tables below list its attributes and methods.

Attributes that describe the tool to the agent and the runtime:

| Attribute             | Type          | Description                                                                                                          |
| --------------------- | ------------- | -------------------------------------------------------------------------------------------------------------------- |
| `name`                | `str`         | The tool name presented to the agent                                                                                 |
| `description`         | `str`         | Agent-oriented description of what the tool does                                                                     |
| `input_schema`        | `dict`        | JSON Schema defining the tool's parameters                                                                           |
| `is_concurrency_safe` | `bool`        | Whether the tool is safe to call in parallel                                                                         |
| `is_read_only`        | `bool`        | Whether the tool only reads data without side effects                                                                |
| `is_external_tool`    | `bool`        | If `True`, execution is delegated externally (see [Define External Execution Tool](#define-external-execution-tool)) |
| `is_state_injected`   | `bool`        | If `True`, the agent state is injected via the `_agent_state` argument                                               |
| `is_mcp`              | `bool`        | Whether the tool comes from an MCP server                                                                            |
| `mcp_name`            | `str \| None` | The MCP server name when `is_mcp` is `True`                                                                          |

Methods that hook into execution and the permission system:

| Method                                   | Required | Description                                                                                                                                                                                                                                                      |
| ---------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `check_permissions(tool_input, context)` | Yes      | Runtime permission check before execution; returns `PermissionDecision`                                                                                                                                                                                          |
| `check_read_only(tool_input)`            | Optional | Per-invocation read-only check; returns `bool`. Defaults to `self.is_read_only`. Override when read-only-ness depends on the input (e.g. `Bash`: `ls` is read-only, `rm` is not). Used by the permission engine to decide auto-allow in EXPLORE / ACCEPT\_EDITS. |
| `match_rule(rule_content, tool_input)`   | Optional | Custom rule-matching logic for the permission system; returns `bool`                                                                                                                                                                                             |
| `generate_suggestions(tool_input)`       | Optional | Generate suggested permission rules from a tool call; returns `list[PermissionRule]`                                                                                                                                                                             |
| `call(**kwargs)`                         | Yes\*    | The tool's execution logic, the subclass override point. Returns `ToolChunk` or `AsyncGenerator[ToolChunk, None]`. Not required for external execution tools (`is_external_tool = True`).                                                                        |
| `__call__(**kwargs)`                     | —        | Framework dispatch entry point. Runs the middleware chain (if any), then delegates to `call()`. Do not override.                                                                                                                                                 |

## Use Built-in Tools

AgentScope ships a set of ready-to-use tools covering common agent operations. Instantiate them and pass into `Toolkit(tools=[...])`:

| Tool         | Description                                    | Read-only |
| ------------ | ---------------------------------------------- | --------- |
| `Bash`       | Execute shell commands                         | No        |
| `Read`       | Read file contents with line numbers           | Yes       |
| `Write`      | Create or overwrite files                      | No        |
| `Edit`       | Perform exact string replacements in files     | No        |
| `Glob`       | Find files by glob pattern                     | Yes       |
| `Grep`       | Search file contents using ripgrep             | Yes       |
| `TaskCreate` | Create a structured task for progress tracking | No        |
| `TaskGet`    | Retrieve task details by ID                    | Yes       |
| `TaskList`   | List all tasks and their status                | Yes       |
| `TaskUpdate` | Update task status or metadata                 | No        |

<Note>
  Two more tools, the `reset_tools` meta tool and the `Skill` viewer, are auto-registered by `Toolkit` whenever extra tool groups or skills exist. Developers do not instantiate them directly. See [Meta Tool](/versions/2.0.5dev/en/building-blocks/tool/manage-tools) and [Skill](/versions/2.0.5dev/en/building-blocks/tool/skill).
</Note>

### Bash

The `Bash` tool executes shell commands and returns stdout/stderr. It implements every optional interface method to provide fine-grained permission control.

`check_permissions()` runs a layered safety analysis on the command string:

1. **Injection risk detection**: flags dynamic shell structures (`$(...)`, backticks, process substitution) that cannot be statically analyzed → ASK
2. **Read-only command detection**: auto-allows safe commands (`git status`, `ls`, `cat`, `grep`, `docker ps`, etc.), including compound commands where every subcommand is read-only → ALLOW
3. **Dangerous command patterns**: detects destructive operations (e.g. `chmod 777`, `mkfs`) → ASK
4. **Sed constraint check**: blocks in-place `sed -i` against dangerous files → ASK
5. **Dangerous path protection**: checks if the command operates on sensitive config files (`.bashrc`, `.ssh/`, `.env`) → ASK
6. **Dangerous removal detection**: catches `rm` / `rmdir` targeting critical system paths (`/`, `~`, `/usr`) → ASK
7. **ACCEPT\_EDITS mode**: auto-allows filesystem commands (`mkdir`, `touch`, `rm`, `rmdir`, `mv`, `cp`, `sed`) **only when every target path resolves inside a configured working directory**. A command that touches any path outside the working set (e.g. `cp /etc/hosts /tmp/x`) falls through to PASSTHROUGH instead of auto-allowing.

`check_read_only()` returns `True` for any command identified by the read-only detector above (step 2), and `False` otherwise. The permission engine uses it to decide auto-allow in EXPLORE / ACCEPT\_EDITS without re-running the full safety analysis.

`match_rule()` uses prefix-based wildcard matching against the command string:

| Pattern        | Matches                         | Does not match |
| -------------- | ------------------------------- | -------------- |
| `npm run:*`    | `npm run build`, `npm run test` | `npm install`  |
| `git commit:*` | `git commit -m "fix"`           | `git push`     |
| `rm:*`         | `rm file.txt`, `rm -rf /tmp/x`  | `ls`           |

`generate_suggestions()` extracts the command prefix (first two tokens) and proposes a prefix rule. For example, `git commit -m "fix bug"` produces the suggestion `git commit:*`.

The constructor accepts optional extra entries for the dangerous-path lists:

```python theme={null}
from agentscope.tool import Bash

bash = Bash(
    additional_dangerous_files=[".secrets"],
    additional_dangerous_directories=[".credentials"],
)
```

### File Tools (Read, Write, Edit)

The file tools enforce a read-before-write rule: `Write` and `Edit` require the target file to have been read via `Read` first. This prevents blind overwrites and ensures the agent always operates on current content.

| Tool    | Operation                       | Key behavior                                                                                            |
| ------- | ------------------------------- | ------------------------------------------------------------------------------------------------------- |
| `Read`  | Read file contents              | Returns content with line numbers; supports offset/limit for large files; results cached in agent state |
| `Write` | Create or overwrite a file      | Fails if the file exists but has not been read first                                                    |
| `Edit`  | Replace exact strings in a file | Fails if `old_string` is not found or is not unique (unless `replace_all=True`); requires prior read    |

`check_permissions()`: `Write` and `Edit` share the same permission logic:

1. **Dangerous path protection**: operations on sensitive files (`.bashrc`, `.env`, `.ssh/`) return a bypass-immune ASK (`bypass_immune=True`), so allow rules cannot silently authorize them. The ASK is still skipped in `BYPASS` mode (which opts out of safety prompts by design) and converted to DENY in `DONT_ASK` mode. See the [permission system docs](/versions/2.0.5dev/en/building-blocks/permission-system/tool-check#safety-check-contract) for the full contract.
2. **ACCEPT\_EDITS mode**: auto-allows operations on files within configured working directories
3. **PASSTHROUGH**: falls through to the permission engine for rule matching

`Read` is read-only and always returns PASSTHROUGH (the engine handles EXPLORE-mode and ACCEPT\_EDITS-mode auto-allow via `check_read_only`).

`match_rule()`: all three tools use `fnmatch` glob matching against the `file_path` argument:

| Pattern       | Matches                   |
| ------------- | ------------------------- |
| `src/**`      | Any file under `src/`     |
| `src/**/*.py` | Python files under `src/` |
| `config.json` | Exact file match          |

`generate_suggestions()` proposes a glob covering the parent directory. For example, editing `/project/src/main.py` produces the suggestion `src/**`.

### Plan Tools (TaskCreate, TaskGet, TaskList, TaskUpdate)

The plan tools give the agent a structured task list it can append to, query, and update through normal tool calls. They share a single store on `agent.state.tasks_context`, are state-injected, and always pass permission checks: the agent treats them as free-cost coordination primitives for breaking complex work into trackable steps.

See [Plan](/versions/2.0.5dev/en/building-blocks/plan) for the full task lifecycle, the storage model, and how to seed or customize tasks programmatically.

## Switch Tool Backend

The `Bash`, `Grep`, `Glob`, `Read`, `Write`, and `Edit` tools in AgentScope support backend switching: delegating their execution to different runtime environments such as the local filesystem, a Docker container, an E2B sandbox, and so on.

A backend is selected via the `backend` argument. Backend instances are obtained from a `Workspace`, which defaults to the local environment. See [Workspace](/versions/2.0.5dev/en/building-blocks/workspace/overview) for more details.

<CodeGroup>
  ```python title="Switch to Docker backend" theme={null}
  from agentscope.workspace import DockerWorkspace

  # Obtain the backend from a Docker sandbox
  workspace = DockerWorkspace(...)
  await workspace.initialize()

  backend = workspace.get_backend()

  # Configure the tool
  bash = Bash(backend=backend)

  # Run logic
  ...

  await workspace.close()
  ```

  ```python title="Switch to E2B backend" theme={null}
  from agentscope.workspace import E2BWorkspace

  # Obtain the backend from an E2B sandbox
  workspace = E2BWorkspace(...)
  await workspace.initialize()

  backend = workspace.get_backend()

  # Configure the tool
  bash = Bash(backend=backend)

  # Run logic
  ...

  await workspace.close()
  ```
</CodeGroup>

## Create Custom Tool

To create a custom tool, subclass `ToolBase`, declare its schema, and implement `check_permissions` and `call`:

```python theme={null}
from agentscope.tool import ToolBase, ToolChunk
from agentscope.permission import (
    PermissionContext, PermissionDecision, PermissionBehavior,
)
from agentscope.message import TextBlock

class WebSearch(ToolBase):
    name = "WebSearch"
    description = "Search the web for information on a given query."
    input_schema = {
        "type": "object",
        "properties": {
            "query": {
                "type": "string",
                "description": "The search query.",
            },
        },
        "required": ["query"],
    }
    is_concurrency_safe = True
    is_read_only = True

    async def check_permissions(
        self, tool_input: dict, context: PermissionContext,
    ) -> PermissionDecision:
        return PermissionDecision(
            behavior=PermissionBehavior.ALLOW,
            message="Web search is read-only.",
        )

    async def call(self, query: str) -> ToolChunk:
        results = await do_search(query)
        return ToolChunk(content=[TextBlock(text=results)])
```

<Note>
  Two extension hooks worth knowing about when writing custom tools with safety logic:

  * **`check_read_only(tool_input)`**: override when whether an invocation modifies state depends on the input (like `Bash`: `ls` is read-only, `rm` is not). Defaults to returning the static `is_read_only` attribute. The permission engine calls it before deciding EXPLORE / ACCEPT\_EDITS auto-allow.
  * **`PermissionDecision(..., bypass_immune=True)`**: set on a returned ASK to mark it as a safety check that allow rules cannot silence (e.g. a `DeployTool` flagging `prod-*` targets). See the [safety check contract](/versions/2.0.5dev/en/building-blocks/permission-system/tool-check#safety-check-contract) for per-mode handling.
</Note>

## Wrap Function as Tool

For lightweight cases that don't justify a full subclass, wrap a plain Python function with the `FunctionTool` adapter. It auto-extracts the tool name from `func.__name__`, the description from the function docstring, and the input schema from type hints.

```python theme={null}
from agentscope.tool import FunctionTool, Toolkit

def get_weather(city: str, unit: str = "celsius") -> str:
    """Get the current weather for a city.

    Args:
        city: The city name to look up.
        unit: Temperature unit, either "celsius" or "fahrenheit".
    """
    return f"The weather in {city} is 22°{unit[0].upper()}"

toolkit = Toolkit(tools=[FunctionTool(get_weather)])
```

`FunctionTool` accepts overrides when the auto-extracted defaults are not what you want:

| Argument              | Type          | Description                                                             |
| --------------------- | ------------- | ----------------------------------------------------------------------- |
| `func`                | `Callable`    | The Python function to wrap                                             |
| `name`                | `str \| None` | Override the tool name (defaults to `func.__name__`)                    |
| `description`         | `str \| None` | Override the description (defaults to the docstring)                    |
| `is_concurrency_safe` | `bool`        | Whether parallel calls are safe (default `True`)                        |
| `is_read_only`        | `bool`        | Whether the function has side effects (default `False`)                 |
| `is_state_injected`   | `bool`        | Whether the agent state is injected as `_agent_state` (default `False`) |

<Note>
  Wrapped functions default to `ASK` permission behavior: the user must explicitly allow each call. Subclass `ToolBase` directly when you need custom permission logic.
</Note>

## Define External Execution Tool

An external execution tool delegates its actual execution outside the agent runtime, typically to a human operator or an external system. When the agent calls one, it emits a `RequireExternalExecutionEvent` and pauses until the result is delivered via `ExternalExecutionResultEvent`.

This pattern underlies the [human-in-the-loop](/versions/2.0.5dev/en/building-blocks/agent/human-in-the-loop) workflow, where certain actions require human approval or manual execution.

To create an external execution tool, set `is_external_tool = True`. There is no need to implement `call`:

```python theme={null}
from agentscope.tool import ToolBase
from agentscope.permission import (
    PermissionContext, PermissionDecision, PermissionBehavior,
)

class HumanApproval(ToolBase):
    name = "HumanApproval"
    description = "Request human approval for a sensitive operation."
    input_schema = {
        "type": "object",
        "properties": {
            "action": {"type": "string", "description": "The action requiring approval."},
            "reason": {"type": "string", "description": "Why this action needs approval."},
        },
        "required": ["action", "reason"],
    }
    is_concurrency_safe = True
    is_read_only = False
    is_external_tool = True

    async def check_permissions(
        self, tool_input: dict, context: PermissionContext,
    ) -> PermissionDecision:
        return PermissionDecision(
            behavior=PermissionBehavior.ALLOW,
            message="External tool dispatch is always allowed.",
        )
```

## Tool Middleware

Tool middleware attaches onion-style hooks directly to a tool instance. Every time that tool is invoked, whether by an agent or called directly, the registered middlewares fire in order, wrap the execution, and can observe or transform both inputs and outputs.

This is separate from agent-level middleware (`MiddlewareBase`): `on_acting` in agent middleware wraps the entire tool-call slot inside the ReAct loop (including permission checks and event emission), while `ToolMiddlewareBase` hooks only inside the tool's own `call()` execution chain and fires even when the tool is called outside any agent.

### ToolMiddlewareBase Interface

Subclass `ToolMiddlewareBase` and implement the single abstract async-generator method `on_tool_call`:

| Parameter      | Type                                             | Description                                                                                                                                                           |
| -------------- | ------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `tool`         | `ToolBase`                                       | The tool instance being invoked                                                                                                                                       |
| `input_kwargs` | `dict[str, Any]`                                 | Input arguments for this invocation. Pass (possibly modified) arguments to `next_handler` to control what the inner layers receive                                    |
| `next_handler` | `Callable[..., AsyncGenerator[ToolChunk, None]]` | Call as `next_handler(**input_kwargs)` to continue to the next layer. Always returns an async generator regardless of whether the underlying tool is streaming or not |

### Execution Model

* The **first** registered middleware is the **outermost** layer: its pre-logic runs first, its post-logic runs last.
* `next_handler(**input_kwargs)` always returns `AsyncGenerator[ToolChunk, None]`. Streaming and non-streaming tools are unified, so a middleware never needs to handle the two shapes separately.
* The innermost layer calls the tool's own `call()`.

```
middlewares[0].on_tool_call
  └─ middlewares[1].on_tool_call
       └─ ... → tool.call()
```

### Attach Middleware

Pass a list of middleware instances to the tool constructor via the `middlewares` argument:

```python theme={null}
from agentscope.tool import Bash

bash = Bash(middlewares=[LoggingMiddleware(), MetricsMiddleware()])
# Execution order: LoggingMiddleware → MetricsMiddleware → Bash.call()
```

### Example

A logging middleware that prints before and after each invocation, and a retry middleware that re-attempts on failure:

```python theme={null}
from typing import AsyncGenerator, Any, Callable
from agentscope.tool import ToolMiddlewareBase, ToolBase, ToolChunk, Bash


class LoggingMiddleware(ToolMiddlewareBase):
    async def on_tool_call(
        self,
        tool: ToolBase,
        input_kwargs: dict[str, Any],
        next_handler: Callable[..., AsyncGenerator[ToolChunk, None]],
    ) -> AsyncGenerator[ToolChunk, None]:
        print(f"→ Calling {tool.name} with {input_kwargs}")
        async for chunk in next_handler(**input_kwargs):
            yield chunk
        print(f"✓ {tool.name} finished")


class RetryMiddleware(ToolMiddlewareBase):
    def __init__(self, max_attempts: int = 3):
        self.max_attempts = max_attempts

    async def on_tool_call(
        self,
        tool: ToolBase,
        input_kwargs: dict[str, Any],
        next_handler: Callable[..., AsyncGenerator[ToolChunk, None]],
    ) -> AsyncGenerator[ToolChunk, None]:
        for attempt in range(1, self.max_attempts + 1):
            try:
                async for chunk in next_handler(**input_kwargs):
                    yield chunk
                return
            except Exception as e:
                if attempt == self.max_attempts:
                    raise
                print(f"Attempt {attempt} failed: {e}, retrying…")


bash = Bash(middlewares=[LoggingMiddleware(), RetryMiddleware(max_attempts=3)])
```

<Note>
  **Tool middleware vs. agent middleware**: use `ToolMiddlewareBase` for cross-cutting concerns that belong to the tool itself (logging, metrics, retry). Use `MiddlewareBase.on_acting` when you need access to the broader agent context, such as permission decisions, the tool-call event, or the surrounding ReAct round. See [Middleware](/versions/2.0.5dev/en/building-blocks/middleware) for the full agent-level hook reference.
</Note>
