Agent Loop · Deep Dive
The scratchpad is the agent's working memory. It holds the original instructions, every action, every observation, and intermediate reasoning. A well-designed scratchpad lets the agent track progress and connect observations across turns.
LLMs are pattern-matching engines. A clean, repeatable scratchpad format is the difference between an agent that finishes in 5 turns and one that loops for 20.
The scratchpad holds system instructions once, then a repeating block for each turn: Thought, Action, Action Input, Observation. Do not store state outside the scratchpad if the agent needs to reference it.
Separate each turn with a consistent delimiter like '---'. Label every section: Thought, Action, Action Input, Observation. Use the same format every time. Consistency reduces parsing errors and improves reasoning quality by 20-40%.
Check token usage before every turn. When the scratchpad exceeds 70-80% of the model's limit, trim old turns or compress them into a summary. The simplest approach: keep system prompt + last N turns.
Concrete Example
from dataclasses import dataclass
@dataclass
class Turn:
thought: str
action: str
action_input: str
observation: str
class Scratchpad:
DELIMITER = "---"
MAX_TOKENS = 4000
SUMMARY_TRIGGER = 3200
def __init__(self, system_prompt: str):
self.system_prompt = system_prompt
self.turns: list[Turn] = []
self.summary: str | None = None
def add_turn(self, turn: Turn) -> None:
self.turns.append(turn)
def should_trim(self) -> bool:
return len(self.render()) // 4 > self.SUMMARY_TRIGGER
def trim_oldest(self, keep: int = 5) -> None:
if len(self.turns) <= keep:
return
removed = self.turns[:-keep]
actions = ", ".join(f"{t.action}({t.action_input[:30]})" for t in removed)
self.summary = f"[Earlier context: {len(removed)} turns. Actions: {actions}]"
self.turns = self.turns[-keep:]
def render(self) -> str:
parts = [self.system_prompt]
if self.summary:
parts.append(f"\n{self.DELIMITER}\n{self.summary}")
for i, turn in enumerate(self.turns, 1):
parts.append(f"\n{self.DELIMITER}\nTurn {i}:\nThought: {turn.thought}\nAction: {turn.action}\nAction Input: {turn.action_input}\nObservation: {turn.observation}\n{self.DELIMITER}")
return "".join(parts)Enforces consistent formatting with delimiters and labeled sections. should_trim() checks token count, and trim_oldest() removes early turns while preserving a summary of what actions were performed.
A visible marker like '---' signals turn boundaries.
Thought, Action, Action Input, Observation each need a clear label.
Trim or summarize when exceeding 70-80% of the model's limit.
Keep a brief summary of removed turns so critical info is preserved.