Skip to main content

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:
ToolOperationRead-only
TaskCreateAppend a new task to the task listNo
TaskGetRetrieve full details (description, status, dependencies) for a single task by IDYes
TaskListList every task with its status, owner, and blocking relationshipsYes
TaskUpdateUpdate a task’s status, fields, or dependency edges; or delete itNo
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.
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.

Use Plan Tools

Equip the Tools

Instantiate the tools and register them on a Toolkit like any other built-in tool:
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:
1

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", …).
2

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

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).
4

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

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

Storage

All task state lives on the agent itself, under agent.state.tasks_context. The relevant types are:
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:
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")
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).
You can also clear or replace the plan at any time:
agent.state.tasks_context.tasks.clear()
The next agent turn will see an empty plan and start over.

Further Reading

  • Tool — the toolkit, the ToolBase interface, and how state-injected tools receive AgentState.
  • Agent — the agent lifecycle, including how AgentState is created, restored, and persisted.