Plan and Execute — One Agent, Many Actions

Fixed pipelines like the chatbot’s classify respond only work when you know in advance how many steps a request needs. Real requests vary — “say hi” is one action, “write a haiku and translate it” is two. This page builds an agent that asks the AI to plan a variable-length list of actions, then executes each one — no separate planner agent, no network hop, all inside one process.

What you’ll build

An agent that:

  1. Turns a request into an ordered list of actions (the “plan”)

  2. Executes each action in turn, one at a time

  3. Returns every outcome together

Create the agent

abi-core add agent runner \
  --description "Plans and executes multi-step requests" \
  --with-web-interface

Tasks/skills when prompted:

plan_and_execute

Each execute_action call below is otherwise stateless — without shared memory, a later step (like “translate the haiku”) has no way to see what an earlier step (like “write the haiku”) produced. Add the built-in memory API so steps can pass context to each other:

abi-core add service agent-memory

This adds the Agent Memory Server + Redis to compose.yaml and wires AGENT_MEMORY_URL into runner automatically (retroactively, since the agent already exists).

Write the steps

Edit agents/runner/steps.py:

from app import agent
from config import config
from abi_core.agent.llm_provider import invoke
from abi_core.agent import add_short_term_memory, get_short_term_memory
from abi_core.common.utils import clean_llm_json


@agent.step(name="plan")
async def plan(query):
    """Ask the AI to break the request into an ordered list of actions."""
    prompt = f"""Break this request into an ordered list of concrete actions.
Request: {query}

Reply with ONLY this JSON, nothing else:
{{"actions": ["first action", "second action", ...]}}"""
    raw = await invoke(config.LLM_CONFIG, prompt)
    return {"actions": clean_llm_json(raw)["actions"]}


@agent.step(name="execute_action")
async def execute_action(action, context_id):
    """Carry out a single planned action. This is where real work happens —
    call a tool, write a file, hit an API. Here we just ask the AI to do it.

    Reads prior steps' outcomes from short-term memory so, e.g., a
    "translate the haiku" action can see the haiku a previous action wrote —
    then writes its own outcome back for the steps after it."""
    history = await get_short_term_memory(context_id=context_id)
    prompt = f"{history}\n\nDo this: {action}\nReport what you did." if history else f"Do this: {action}\nReport what you did."
    result = await invoke(config.LLM_CONFIG, prompt)
    await add_short_term_memory(
        "plan_and_execute", action, f"Action: {action}\nOutcome: {result}", context_id=context_id
    )
    return {"action": action, "outcome": result}

Write the task

The task is what turns a variable-length plan into a loop of step calls. Edit agents/runner/tasks.py:

from app import agent
from abi_core.agent.agent_response import AgentResponse


@agent.task(name="plan_and_execute", task_id="task-plan-exec")
async def plan_and_execute(query, context_id=None):
    """Plan the request, then execute every action the plan produced."""
    yield AgentResponse.status("Planning...")
    plan_result = await agent.execute_step("plan", query=query)
    actions = plan_result["actions"]

    outcomes = []
    for i, action in enumerate(actions, 1):
        yield AgentResponse.status(f"Step {i}/{len(actions)}: {action}")
        outcome = await agent.execute_step("execute_action", action=action, context_id=context_id)
        outcomes.append(outcome)

    yield AgentResponse.result({"actions": actions, "outcomes": outcomes})

The framework injects context_id into the task automatically (one per conversation/session) — declaring it in the signature is enough to receive it, and passing it on to execute_action is what lets memory tie every action in the same request together.

Run it

docker compose up --build -d

Talk to it

curl -X POST http://localhost:8002/stream \
  -H "Content-Type: application/json" \
  -d '{"query": "Write a haiku about the ocean, then translate it to Spanish"}'

You’ll see something like:

event: status → "Planning..."
event: status → "Step 1/2: Write a haiku about the ocean"
event: status → "Step 2/2: Translate the haiku to Spanish"
event: result → {"actions": [...], "outcomes": [...]}

What happened

  1. The task called the plan step, which asked the AI for a JSON list of actions

  2. The number of actions wasn’t fixed anywhere in the code — the AI decided there were two

  3. The task looped over that list, calling execute_action once per item, passing along the shared context_id

  4. Each agent.execute_step(...) call is a plain Python call inside the same process — no network hop, no other agent involved

  5. Before doing its work, each execute_action call read prior outcomes from short-term memory (via context_id), so “translate the haiku” actually saw the haiku “write a haiku” produced — then wrote its own outcome back for whatever runs next

  6. Every outcome accumulated into one final response

Without memory, each execute_action call is stateless — step 2 has no way to know what step 1 did. That’s the bug this page’s agent-memory service fixes: the same context_id ties every action in one request together.

Key rules

  • A step does one deterministic thing (call the AI, do the work). It never decides how many times it runs.

  • A task is what decides that: it’s ordinary Python, so it can loop, branch, or run steps in parallel with asyncio.gather.

  • Because the plan’s length comes from the AI at runtime, this can’t be a fixed @agent.step(depends_on=[...]) DAG (see Dependency Management) — a DAG’s shape is fixed when the agent starts. A task is imperative code, so it can shape itself around whatever the AI just produced.

  • Steps don’t share state on their own — if a later action needs to see what an earlier one produced, thread it through explicitly, e.g. with the built-in memory API keyed by context_id, as done here.

  • This is still one agent, one process. If execute_action needs to become a specialized agent of its own — a research expert, a code writer, anything with its own tools and model — you’re no longer looping in-process. See Plan, Then Execute — Two Agents for that split.

Next step

👉 Plan Confirmation — let the user approve the plan before this agent runs it.