> ## 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.

# Plan

> Give agents a structured task list to plan, track, and coordinate complex work

## Overview

Planning is how an agent breaks a complex request into discrete, ordered, and trackable steps. Instead of letting the model juggle a multi-step goal entirely in free-form reasoning, AgentScope exposes a small set of built-in tools that let the agent maintain an **explicit, structured task list** — created, queried, and updated through normal tool calls.

AgentScope ships four planning tools out of the box:

| Tool         | Operation                                                                         | Read-only |
| ------------ | --------------------------------------------------------------------------------- | --------- |
| `TaskCreate` | Append a new task to the task list                                                | No        |
| `TaskGet`    | Retrieve full details (description, status, dependencies) for a single task by ID | Yes       |
| `TaskList`   | List every task with its status, owner, and blocking relationships                | Yes       |
| `TaskUpdate` | Update a task's status, fields, or dependency edges; or delete it                 | No        |

All four are state-injected tools (`is_state_injected = True`): the agent runtime hands each call the live `AgentState`, and the tools read from / write to `agent.state.tasks_context`. That means the task list is **scoped per agent** and persists naturally across reasoning steps, ReAct rounds, and human-in-the-loop pauses, alongside the rest of the agent's saved state.

<Tip>
  The planning tools are most useful when the work spans **three or more non-trivial steps**, involves **dependencies between subtasks**, or benefits from making progress **visible to the user**. For one-shot or purely conversational requests, equip them and the agent will skip them by design — the tool prompts explicitly tell it not to plan trivial work.
</Tip>

## Use Plan Tools

### Equip the Tools

Instantiate the tools and register them on a `Toolkit` like any other built-in tool:

```python theme={null}
from agentscope.agent import Agent
from agentscope.tool import (
    Toolkit,
    TaskCreate,
    TaskGet,
    TaskList,
    TaskUpdate,
)

toolkit = Toolkit(
    tools=[
        TaskCreate(),
        TaskGet(),
        TaskList(),
        TaskUpdate(),
    ],
)

agent = Agent(
    name="planner",
    system_prompt="You are a planning assistant.",
    model=model,
    toolkit=toolkit,
)
```

Each tool's `description` already contains a detailed prompt describing when to call it, when to skip it, and how to interpret its output, so no additional system-prompt engineering is required. `check_permissions()` is hard-wired to `ALLOW` — the planning tools are pure in-memory state mutations and never trigger user prompts.

### Task Lifecycle

A typical planning loop looks like this:

<Steps>
  <Step title="Capture the work">
    On a new instruction, the agent calls `TaskCreate` once per discrete step, providing a short imperative `subject` and a richer `description`. New tasks are appended in creation order; their `id` is a stable, monotonically increasing numeric string (`"1"`, `"2"`, …).
  </Step>

  <Step title="Inspect the queue">
    `TaskList` returns a compact one-line-per-task summary (id, status, subject, owner, blocked-by), which the agent uses to pick the next available task — typically the lowest-ID `pending` task with no unresolved `blocked_by`.
  </Step>

  <Step title="Claim and start">
    Before starting work, the agent calls `TaskUpdate` to set the task's `status` to `in_progress` (and optionally an `owner` for multi-agent scenarios).
  </Step>

  <Step title="Get full context">
    `TaskGet` returns the full description, dependency edges, and metadata for a specific task — useful right before execution if the description is long.
  </Step>

  <Step title="Finish or re-plan">
    On completion, `TaskUpdate` flips the status to `completed`. If the agent uncovers new work, it loops back to `TaskCreate`; if a task becomes obsolete, it sets status `deleted` (a hard removal that also rewires the dependency edges of any tasks that referenced it).
  </Step>
</Steps>

The status workflow is intentionally linear:

```
pending → in_progress → completed
                          (or)
                      ↘ deleted (any state, hard remove)
```

### Express Dependencies

Tasks expose two symmetric dependency edges:

* `blocks` — the IDs of tasks that cannot start until this one is completed.
* `blocked_by` — the IDs of tasks that must complete before this one can start.

`TaskUpdate` takes `add_blocks` and `add_blocked_by` arguments. Each one mutates **both sides** of the edge automatically, so the data stays consistent:

```python theme={null}
# After creating task "1" and task "2", make "2" depend on "1":
await TaskUpdate()(
    task_id="2",
    add_blocked_by=["1"],
    _agent_state=agent.state,
)
# Now: task "2".blocked_by == ["1"] AND task "1".blocks == ["2"]
```

When a task is deleted, its ID is removed from every other task's `blocks` and `blocked_by` lists, so the dependency graph remains valid.

<Note>
  `TaskList` annotates every task that still has unresolved `blocked_by` entries, and `TaskGet` returns the full edge list. The agent uses these hints to prefer unblocked work, but **enforcement is advisory** — nothing in the runtime prevents the model from working on a blocked task. Treat the dependency edges as a coordination signal, not a hard gate.
</Note>

## Storage

All task state lives on the agent itself, under `agent.state.tasks_context`. The relevant types are:

```python theme={null}
class Task(BaseModel):
    id: str                       # Monotonic numeric string, assigned by TaskCreate
    subject: str                  # Imperative one-liner
    description: str              # Detailed requirements / context
    state: Literal["pending", "in_progress", "completed"] = "pending"
    owner: str | None = None
    blocks: list[str] = []        # Task IDs blocked by this task
    blocked_by: list[str] = []    # Task IDs blocking this task
    metadata: dict[str, Any] = {}
    created_at: str               # ISO-8601 timestamp, set on creation

class TaskContext(BaseModel):
    tasks: list[Task] = []
```

`AgentState.tasks_context` is a regular field on the agent state model, which means:

* **It survives serialization.** Calling `agent.state_dict()` (or whatever the host platform uses to persist `AgentState`) captures the task list verbatim, and restoring the state restores the plan.
* **It is per-agent.** Two agents do not share a task list by default; multi-agent coordination is the developer's job (e.g. by routing all planning tools through one designated planner agent, or by manually syncing state between agents).
* **It is mutable from outside the LLM loop.** Anything that can reach `agent.state` — middleware, application code, evaluators — can read and write tasks directly. The planning tools have no privileged access; they are simply a convenient LLM-facing surface over the same data structure.

## Customize Tasks

Because tasks live on `agent.state.tasks_context`, developers can manage them programmatically without going through the LLM. This is useful for:

* **Seeding** the agent with a pre-baked plan generated elsewhere (e.g. by another agent, a workflow engine, or static analysis).
* **Importing** existing work items from an external tracker (Jira, GitHub issues, an internal task DB).
* **Migrating** state across agent instances or restoring partially completed plans.
* **Evaluation** of planning behavior, where the harness needs to inject ground-truth tasks before the agent reasons over them.

The example below seeds two dependent tasks before the agent's first reply:

```python theme={null}
from agentscope.agent import Agent
from agentscope.state import Task
from agentscope.tool import Toolkit, TaskCreate, TaskGet, TaskList, TaskUpdate

agent = Agent(
    name="planner",
    system_prompt="You are a planning assistant.",
    model=model,
    toolkit=Toolkit(
        tools=[TaskCreate(), TaskGet(), TaskList(), TaskUpdate()],
    ),
)

agent.state.tasks_context.tasks.extend(
    [
        Task(
            id="1",
            subject="Fetch project requirements",
            description="Read README.md and CONTRIBUTING.md in the repo root.",
            metadata={"source": "seed"},
        ),
        Task(
            id="2",
            subject="Draft an implementation plan",
            description="Produce a step-by-step plan based on the requirements.",
            blocked_by=["1"],
            metadata={"source": "seed"},
        ),
    ],
)
# Keep the reverse edge consistent:
agent.state.tasks_context.tasks[0].blocks.append("2")
```

<Warning>
  When mutating `tasks_context` directly, you are responsible for:

  * **Unique, parseable IDs.** `TaskCreate` derives the next ID by taking `max(int(task.id) for task in tasks) + 1`. Non-numeric IDs are ignored when computing the next ID, but they will not be revisited — assign numeric string IDs (`"1"`, `"2"`, …) to keep auto-generation working.
  * **Bidirectional dependency edges.** `blocks` and `blocked_by` must stay in sync. `TaskUpdate` does this automatically; manual edits do not.
  * **Valid status values.** Only `pending`, `in_progress`, and `completed` are valid for `Task.state`. `deleted` is an *operation* exposed by `TaskUpdate`, not a stored state — to drop a task by hand, simply remove it from the list (and clean up its edges).
</Warning>

You can also clear or replace the plan at any time:

```python theme={null}
agent.state.tasks_context.tasks.clear()
```

The next agent turn will see an empty plan and start over.

## Further Reading

* [Tool](/versions/2.0.4dev/en/building-blocks/tool) — the toolkit, the `ToolBase` interface, and how state-injected tools receive `AgentState`.
* [Agent](/versions/2.0.4dev/en/building-blocks/agent) — the agent lifecycle, including how `AgentState` is created, restored, and persisted.
