> Nivel 12 de Agent Harness Patterns, un recorrido de patrones sobre cómo funcionan los agentes de IA. Versión web: https://harnesspatterns.dev/es/patterns/plan-and-reflect · Todos los patrones (en inglés): https://harnesspatterns.dev/llms.txt

# Planificar y reflexionar

Escribe el plan antes de tocar nada, y revisa el trabajo antes de aceptar la respuesta. El plan mantiene al agente encaminado; la revisión atrapa lo que dice que hizo pero no hizo.

## El problema

Dale a un agente un trabajo con varias partes y empieza por lo que tenga enfrente. A mitad de camino, los primeros pasos quedaron muy atrás en el historial, y se olvida de uno. Al final responde “¡listo!” con total seguridad, porque nada lo obliga a mirar.

Ese es Scatterbrain. Dos fallas en una: sin plan, así que se pierden pasos; sin verificación, así que un paso que salió mal se informa como hecho. En la calle Tango, la herramienta dijo claramente que el diario cayó entre los arbustos. El modelo lo leyó y marcó la casilla igual.

## La solución

**Primero, planificar.** Antes de la primera acción real, el agente escribe el trabajo como una lista de tareas, y marca cada una a medida que avanza. El truco que lo hace funcionar: el plan completo se agrega a cada pedido, así el modelo siempre ve qué está hecho y qué falta, por más largo que se ponga el historial. En astorlm eso es `pattern: 'PLAN_EXECUTE'`: dos herramientas, `add_plan_item` y `update_plan_item`, y el plan en el system prompt en cada turno.

**Reflexionar antes de aceptar.** Una casilla marcada es solo lo que dice el modelo. Cuando `run()` devuelve, tu código revisa el resultado antes de aceptarlo. Si falta algo, el hallazgo vuelve como un mensaje nuevo en la *misma* sesión: el agente conserva su historial y su plan, y arregla solo lo que está mal. Limita la cantidad de rondas. Hay tres formas de revisar, de la más fuerte a la más débil:

- **Revisar el mundo**
   Tu código mira el resultado en sí: los porches, las filas de la base de datos, la suite de tests, el archivo en disco.
   El mejor revisor cuando puedes tenerlo: barato, exacto, y nadie puede convencerlo de nada. Necesita que el trabajo se pueda verificar con código.
- **Un modelo crítico**
   Una segunda llamada lee la tarea, la respuesta y una lista de verificación, y enumera lo que está mal o falta.
   Para trabajo que ningún código puede verificar: un resumen, un email, un plan. Cuesta una llamada, también puede pasar cosas por alto, y necesita criterios concretos, no “¿esto está bien?”.
- **Preguntarle al agente**
   El system prompt le dice al agente que relea su trabajo antes de responder.
   Gratis, y a veces alcanza. Pero es el mismo modelo calificándose a sí mismo, con los mismos puntos ciegos: ya marcó el #14 una vez.

Es la misma idea que una evaluación del nivel 11, usada en tiempo de ejecución: una evaluación califica ejecuciones después del hecho para mejorar el agente; una revisión califica esta ejecución antes de que el usuario la vea.

## El elenco

El mismo elenco de siempre, esta vez en un reparto de diarios.

- **El kiosco** (el modelo): El Oráculo, detrás del mostrador. Decide cada paso, y nunca pedalea.
- **La hoja de ruta** (el plan): Las tareas y sus casillas. Brilla en dorado en cada turno: va en cada pedido.
- **La bici de Astor** (el bucle): Lleva cada llamada a herramienta de ida y vuelta, con el historial en su bandoneón.
- **Un lanzamiento** (deliver): Una herramienta común. Dice dónde cayó el diario.
- **El editor** (tu código): Entrega el trabajo, y revisa los porches antes de aceptar la respuesta. Su lupa es `review()`.

## El código

**Con astorlm:** `pattern: 'PLAN_EXECUTE'` agrega las herramientas del plan y pone el plan en cada pedido; `getPlan()` lo lee de vuelta. La revisión es código común después de `run()`, y un segundo `run()` sobre el mismo agente continúa la misma sesión.

**Desde cero:** Una lista, dos herramientas que la editan, y un system prompt que se reconstruye con la lista en cada turno. El historial vive fuera de `run()`, así que la corrección continúa la misma conversación.

**Con astorlm**

```ts
import { OpenAIProvider, createLocalAgent, tool } from 'astorlm'
import { z } from 'zod'

// Any OpenAI-compatible endpoint: OpenAI, Ollama, LM Studio, vLLM, a proxy…
const LLM = { baseURL: 'http://localhost:11434/v1', apiKey: 'YOUR_API_KEY' } // local servers usually ignore the key

const SUBSCRIBERS = [12, 14, 18]
const porches = new Set<number>() // the real world: which porches have a paper

const deliver = tool({
  name: 'deliver',
  description: 'Ride to a house and throw today’s paper onto its porch. Says where the paper landed.',
  schema: z.object({ house: z.number() }),
  execute: async ({ house }) => {
    const landed = throwPaper(house) // your code: 'porch' or 'bushes'
    if (landed === 'porch') porches.add(house)
    return landed === 'porch' ? `Paper on the porch at #${house}.` : `Paper landed in the bushes at #${house}.`
  },
})

// PLAN: the agent gets add_plan_item and update_plan_item,
// and the current plan is added to the system prompt on every turn.
const agent = await createLocalAgent({
  provider: new OpenAIProvider({ ...LLM, model: 'your-model' }), // e.g. 'llama3.1', 'gpt-4o-mini'
  pattern: 'PLAN_EXECUTE',
  systemPrompt: 'You deliver newspapers. Plan every stop before you start, and tick each task as you go.',
  tools: [deliver],
  maxTurns: 20,
})

// REFLECT: check the work itself before accepting the answer. Deterministic when you can;
// a second model with a rubric when you can't.
const review = (): string[] => SUBSCRIBERS.filter((house) => !porches.has(house)).map((house) => `#${house} has no paper on the porch`)

let answer = await agent.run(`Deliver today’s paper to every subscriber on Tango Street: ${SUBSCRIBERS.join(', ')}.`)
for (let round = 1; round <= 2; round++) {
  const problems = review()
  if (problems.length === 0) break
  // Same agent, same session: it keeps its history and its plan, and fixes what's missing.
  answer = await agent.run(`Review found: ${problems.join('; ')}. Fix it.`)
}

console.log(agent.getPlan()) // [{ id: '1', description: 'Deliver to #12', status: 'completed' }, …]
console.log(answer.content)
```

**TypeScript**

```ts
// Plan and reflect, from scratch. Plain fetch, no SDK.

// Any OpenAI-compatible endpoint: OpenAI, Ollama, LM Studio, vLLM, a proxy…
const LLM = {
  baseURL: 'http://localhost:11434/v1', // e.g. Ollama's default address
  model: 'your-model', // e.g. 'llama3.1', 'gpt-4o-mini'
  apiKey: 'YOUR_API_KEY', // local servers usually ignore it
}

const SUBSCRIBERS = [12, 14, 18]
const porches = new Set<number>() // the real world: which porches have a paper

// 1. The plan: a list the model writes and ticks with two tools.
type Task = { id: number; description: string; status: 'pending' | 'completed' }
const plan: Task[] = []

type ToolFn = (args: Record<string, string | number>) => string
const tools: Record<string, ToolFn> = {
  add_plan_item: ({ description }) => {
    plan.push({ id: plan.length + 1, description: String(description), status: 'pending' })
    return `Task added with ID: ${plan.length}`
  },
  update_plan_item: ({ id, status }) => {
    const task = plan.find((t) => t.id === Number(id))
    if (!task) return `No task ${id}`
    task.status = status === 'completed' ? 'completed' : 'pending'
    return `Task ${id} status updated to ${task.status}.`
  },
  deliver: ({ house }) => {
    const landed = throwPaper(Number(house)) // your code: 'porch' or 'bushes'
    if (landed === 'porch') porches.add(Number(house))
    return landed === 'porch' ? `Paper on the porch at #${house}.` : `Paper landed in the bushes at #${house}.`
  },
}
const toolSchemas = [/* one JSON Schema per tool: add_plan_item(description), update_plan_item(id, status), deliver(house) */]

// 2. The plan goes into the system prompt on EVERY turn, so the model never loses track.
const system = (): string =>
  'You deliver newspapers. Plan every stop with add_plan_item before you start, and tick each task as you go.\n' +
  (plan.length ? plan.map((t) => `- [${t.status}] ${t.description} (id ${t.id})`).join('\n') : '(no plan yet)')

type ToolCall = { id: string; function: { name: string; arguments: string } }
type Message =
  | { role: 'user'; content: string }
  | { role: 'assistant'; content: string | null; tool_calls?: ToolCall[] }
  | { role: 'tool'; tool_call_id: string; content: string }

// The history lives outside run(), so a second run continues the same session.
const messages: Message[] = []

async function run(prompt: string, maxTurns = 20): Promise<string> {
  messages.push({ role: 'user', content: prompt })
  for (let turn = 1; turn <= maxTurns; turn++) {
    const res = await fetch(`${LLM.baseURL}/chat/completions`, {
      method: 'POST',
      headers: { 'content-type': 'application/json', authorization: `Bearer ${LLM.apiKey}` },
      body: JSON.stringify({ model: LLM.model, messages: [{ role: 'system', content: system() }, ...messages], tools: toolSchemas }),
    })
    const [choice] = (await res.json()).choices
    const reply: Message = choice.message
    messages.push(reply)
    if (choice.finish_reason !== 'tool_calls') return reply.content ?? ''

    for (const call of reply.tool_calls ?? []) {
      const fn = tools[call.function.name]
      const output = fn ? fn(JSON.parse(call.function.arguments)) : `Unknown tool: ${call.function.name}`
      messages.push({ role: 'tool', tool_call_id: call.id, content: output })
    }
  }
  throw new Error(`No answer after ${maxTurns} turns`)
}

// 3. Reflect: check the work itself before accepting the answer, and send what's missing back.
const review = (): string[] => SUBSCRIBERS.filter((house) => !porches.has(house)).map((house) => `#${house} has no paper on the porch`)

let answer = await run(`Deliver today’s paper to every subscriber on Tango Street: ${SUBSCRIBERS.join(', ')}.`)
for (let round = 1; round <= 2; round++) {
  const problems = review()
  if (problems.length === 0) break
  answer = await run(`Review found: ${problems.join('; ')}. Fix it.`)
}
console.log(answer)
```

**Python**

```python
# Plan and reflect, from scratch. Standard library only, no SDK.
import json
import urllib.request

# Any OpenAI-compatible endpoint: OpenAI, Ollama, LM Studio, vLLM, a proxy...
LLM = {
    "base_url": "http://localhost:11434/v1",  # e.g. Ollama's default address
    "model": "your-model",  # e.g. "llama3.1", "gpt-4o-mini"
    "api_key": "YOUR_API_KEY",  # local servers usually ignore it
}

def post(path, payload):
    request = urllib.request.Request(
        f"{LLM['base_url']}{path}",
        data=json.dumps(payload).encode(),
        headers={"Content-Type": "application/json", "Authorization": f"Bearer {LLM['api_key']}"},
    )
    with urllib.request.urlopen(request) as response:
        return json.load(response)

SUBSCRIBERS = [12, 14, 18]
PORCHES = set()  # the real world: which porches have a paper

# 1. The plan: a list the model writes and ticks with two tools.
PLAN = []

def add_plan_item(description):
    PLAN.append({"id": len(PLAN) + 1, "description": description, "status": "pending"})
    return f"Task added with ID: {len(PLAN)}"

def update_plan_item(id, status):
    task = next((t for t in PLAN if t["id"] == int(id)), None)
    if task is None:
        return f"No task {id}"
    task["status"] = "completed" if status == "completed" else "pending"
    return f"Task {id} status updated to {task['status']}."

def deliver(house):
    landed = throw_paper(house)  # your code: "porch" or "bushes"
    if landed == "porch":
        PORCHES.add(house)
        return f"Paper on the porch at #{house}."
    return f"Paper landed in the bushes at #{house}."

TOOLS = {"add_plan_item": add_plan_item, "update_plan_item": update_plan_item, "deliver": deliver}
TOOL_SCHEMAS = [...]  # one JSON Schema per tool: add_plan_item(description), update_plan_item(id, status), deliver(house)

# 2. The plan goes into the system prompt on EVERY turn, so the model never loses track.
def system():
    lines = [f"- [{t['status']}] {t['description']} (id {t['id']})" for t in PLAN] or ["(no plan yet)"]
    return "You deliver newspapers. Plan every stop with add_plan_item before you start, and tick each task as you go.\n" + "\n".join(lines)

# The history lives outside run(), so a second run continues the same session.
MESSAGES = []

def run(prompt, max_turns=20):
    MESSAGES.append({"role": "user", "content": prompt})
    for _ in range(max_turns):
        request = [{"role": "system", "content": system()}, *MESSAGES]
        choice = post("/chat/completions", {"model": LLM["model"], "messages": request, "tools": TOOL_SCHEMAS})["choices"][0]
        reply = choice["message"]
        MESSAGES.append(reply)
        if choice["finish_reason"] != "tool_calls":
            return reply.get("content") or ""

        for call in reply.get("tool_calls", []):
            output = TOOLS[call["function"]["name"]](**json.loads(call["function"]["arguments"]))
            MESSAGES.append({"role": "tool", "tool_call_id": call["id"], "content": output})

    raise RuntimeError(f"No answer after {max_turns} turns")

# 3. Reflect: check the work itself before accepting the answer, and send what's missing back.
def review():
    return [f"#{house} has no paper on the porch" for house in SUBSCRIBERS if house not in PORCHES]

answer = run(f"Deliver today's paper to every subscriber on Tango Street: {', '.join(map(str, SUBSCRIBERS))}.")
for _ in range(2):
    problems = review()
    if not problems:
        break
    answer = run(f"Review found: {'; '.join(problems)}. Fix it.")
print(answer)
```

## Qué vigilar

- **Mantén las tareas chicas y verificables.** “Entregar en el #14” se puede verificar; “encargarse de la calle”, no. Una tarea que puedes verificar es una tarea que la revisión puede atrapar.
- **Deja que el plan cambie.** Los planes chocan con la realidad: una calle está cortada, un cliente cancela. El agente debería poder agregar, quitar o reordenar tareas, no seguir una lista desactualizada.
- **Revisa el mundo, no el plan.** La hoja decía tres marcas. Revisar la hoja habría pasado. La revisión tiene que mirar el resultado en sí.
- **Limita las rondas.** Una revisión que nunca puede pasar, o un agente que no puede arreglar lo que encuentra, da vueltas para siempre. Dos o tres rondas, y después pásaselo a una persona (nivel 13).
- **No planifiques algo de una línea.** Planificar cuesta turnos y tokens. Para una sola consulta, sáltatelo. Vale la pena cuando el trabajo tiene varios pasos fáciles de perder.

## Patrones relacionados

- [2 · El bucle del agente](https://harnesspatterns.dev/es/patterns/agent-loop.md)
- [11 · Observabilidad y evaluaciones](https://harnesspatterns.dev/es/patterns/observability.md)
- [10 · Vueltas limpias](https://harnesspatterns.dev/es/patterns/fresh-laps.md)
- [13 · Humano en el bucle](https://harnesspatterns.dev/es/patterns/human-in-the-loop.md)
- [15 · Subagentes](https://harnesspatterns.dev/es/patterns/subagents.md)
