Plan, Act, Observe

Agents run a loop: plan the next step, act with a tool, observe, repeat.

Most agents follow a plan / act / observe loop until the goal is met.

An agent repeats a loop of planning, acting and observing until the goal is reached1. Plan2. Act (tool)3. ObserveGoal met?yes: finish
Each observation feeds the next plan. The loop ends when the goal is satisfied or a limit is hit.

Under the hood this is the tool-use loop, but the model also reflects on progress and decides when it is done.

Example

Example · javascript
let steps = 0;
while (steps++ < 10) {                 // hard limit on iterations
  const res = await chat({ messages, tools });
  if (!res.tool_call) { console.log(res.message.content); break; } // done
  const result = await runTool(res.tool_call);   // act
  messages.push(res.message);
  messages.push({ role: 'tool', content: JSON.stringify(result) }); // observe
}

When to use it

  • A research agent runs Plan-Act-Observe for 8 turns: it plans which source to check, searches, reads the result, then decides whether more research is needed.
  • A test-writing agent plans which function to test, writes the test, runs it via a tool, observes the failure, then revises until it passes.
  • An email-triage agent plans which label to apply to an email, calls the label tool, observes confirmation, then moves to the next email.

More examples

Plan step printed before each action

Asks the model to emit a PLAN: line before each tool call so the plan step is observable in the loop output.

Example · python
from openai import OpenAI
import json

client = OpenAI()

def search(query): return f'Results for {query}: [article1, article2]'
tools = [{'type':'function','function':{'name':'search','description':'Search the web.','parameters':{'type':'object','properties':{'query':{'type':'string'}},'required':['query']}}}]
messages = [
    {'role':'system','content':'Before each action, print your plan as PLAN: <text>. Then use a tool.'},
    {'role':'user','content':'Find recent news about large language models.'}
]
for turn in range(4):
    r = client.chat.completions.create(model='gpt-4o-mini', messages=messages, tools=tools)
    msg = r.choices[0].message
    messages.append(msg)
    if msg.content and msg.content.startswith('PLAN:'):
        print(msg.content)
    if not msg.tool_calls:
        print('Answer:', msg.content)
        break
    for tc in msg.tool_calls:
        result = search(**json.loads(tc.function.arguments))
        print('Observed:', result[:60])
        messages.append({'role':'tool','tool_call_id':tc.id,'content':result})

Observe and adapt based on tool result

Shows the observe step: when the tool returns NOT_FOUND, the model can adapt its plan and respond accordingly.

Example · python
from openai import OpenAI
import json

client = OpenAI()

# Tool that sometimes returns 'not found'
def lookup_user(user_id):
    db = {'u1': 'Alice', 'u2': 'Bob'}
    return db.get(user_id, 'NOT_FOUND')

tools = [{'type':'function','function':{'name':'lookup_user','description':'Look up user by ID.','parameters':{'type':'object','properties':{'user_id':{'type':'string'}},'required':['user_id']}}}]
messages = [{'role':'user','content':'Get info for user u99.'}]

for _ in range(4):
    r = client.chat.completions.create(model='gpt-4o-mini', messages=messages, tools=tools)
    msg = r.choices[0].message
    messages.append(msg)
    if not msg.tool_calls:
        print(msg.content); break
    for tc in msg.tool_calls:
        result = lookup_user(**json.loads(tc.function.arguments))
        messages.append({'role':'tool','tool_call_id':tc.id,'content':result})

Loop with turn counter and exit condition

Guards the loop with a MAX_TURNS counter and prints a safety message if the model loops too many times.

Example · python
from openai import OpenAI
import json

client = OpenAI()
MAX_TURNS = 6
turn = 0

def count_words(text): return str(len(text.split()))
tool = {'type':'function','function':{'name':'count_words','description':'Count words in text.','parameters':{'type':'object','properties':{'text':{'type':'string'}},'required':['text']}}}
messages = [{'role':'user','content':'How many words are in "the quick brown fox"?'}]

while turn < MAX_TURNS:
    r = client.chat.completions.create(model='gpt-4o-mini', messages=messages, tools=[tool])
    msg = r.choices[0].message
    messages.append(msg)
    turn += 1
    if not msg.tool_calls:
        print(f'Done in {turn} turns:', msg.content); break
    tc = msg.tool_calls[0]
    result = count_words(**json.loads(tc.function.arguments))
    messages.append({'role':'tool','tool_call_id':tc.id,'content':result})
else:
    print('Safety limit reached')

Discussion

  • Be the first to comment on this lesson.