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.
Under the hood this is the tool-use loop, but the model also reflects on progress and decides when it is done.
Example
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.
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.
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.
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