How PageAgent Manages Memory and History Events Across Execution Steps

PageAgent maintains a persistent history array inside PageAgentCore that stores every execution step as a typed HistoricalEvent, emitting historychange events to synchronize LLM context and UI rendering in real time.

The alibaba/page-agent repository implements a centralized memory architecture where all task progress is recorded in a single source of truth. This system enables the LLM to retain context across multi-step automation tasks while providing immediate visual feedback through event-driven UI updates.

History Data Structure

PageAgent defines all memory records through the HistoricalEvent union type in packages/core/src/types.ts. This type encompasses five distinct event categories that capture the complete execution lifecycle:

// packages/core/src/types.ts
export type HistoricalEvent =
  | AgentStepEvent          // step with reflection & tool action
  | ObservationEvent       // persistent observation text
  | UserTakeoverEvent      // user manually edited the page
  | RetryEvent             // LLM retry information
  | AgentErrorEvent;       // fatal error info

Each step event (AgentStepEvent) stores critical metadata including stepIndex for ordering, a reflection object containing the three-part evaluation model (evaluation_previous_goal, memory, next_goal), and the action details with tool name, input, and LLM output. Observation events (ObservationEvent) capture transient notes such as URL changes or wait-time warnings that provide context without representing agent decisions.

Recording Events During Execution

The PageAgentCore class in packages/core/src/PageAgentCore.ts appends events to the history array at three distinct phases of the execution loop.

Observations (Pre-Step)

Before each LLM call, the private method #handleObservations generates contextual notes and pushes them to memory:

await this.#handleObservations(step);

This method constructs temporary warnings—such as step-remaining notifications or URL change alerts—and records them via:

this.history.push({ type: 'observation', content });
this.#emitHistoryChange();

Step Results

After the LLM returns a macro-tool result, the core creates a comprehensive step record:

this.history.push({
  type: 'step',
  stepIndex: step,
  reflection,
  action,
  usage: result.usage,
  rawResponse: result.rawResponse,
  rawRequest: result.rawRequest,
});
this.#emitHistoryChange();

Retry and Error Handling

When the LLM requires retry attempts or encounters fatal errors, dedicated events preserve the failure context:

this.history.push({
  type: 'retry',
  message: `LLM retry attempt ${attempt} of ${maxAttempts}`,
  attempt,
  maxAttempts,
});
this.#emitHistoryChange();

this.history.push({
  type: 'error',
  message,
  rawResponse: error,
});
this.#emitHistoryChange();

Emitting the historychange Event

Every mutation to the history array triggers the private #emitHistoryChange method:

#emitHistoryChange(): void {
  this.dispatchEvent(new Event('historychange'));
}

This event-driven architecture ensures that any registered EventTarget listener receives immediate notification of state changes. The UI panel and other external components subscribe to this event to maintain synchronization with the internal memory state without polling.

Using History as LLM Context

When constructing prompts for the next execution step, PageAgentCore transforms the history array into an XML-formatted <agent_history> block. This serialization provides the LLM with chronological access to previous reflections and actions:

prompt += '<agent_history>\n';
for (const event of this.history) {
  if (event.type === 'step') { /* render step fields */ }
  else if (event.type === 'observation') {
    prompt += `<sys>${event.content}</sys>\n`;
  }
  // user_takeover, retry, error are omitted from LLM context
}
prompt += '</agent_history>\n\n';

Because the reflection object within each step contains the agent's previous evaluation, memory, and next-goal planning, the LLM receives a structured memory of its own reasoning chain. User takeovers, retries, and errors are intentionally excluded from this context to prevent contamination of the reasoning trajectory.

UI Rendering of Persistent Memory

The panel component in packages/ui/src/panel/Panel.ts maintains visual consistency by subscribing to the same historychange events:

this.#agent.addEventListener('historychange', this.#onHistoryChange);

When the event fires, the #renderHistory method rebuilds the DOM directly from agent.history:

const history = this.#agent.history;
for (const event of history) {
  items.push(...this.#createHistoryCards(event));
}
this.#historySection.innerHTML = items.join('');

This ensures the visual timeline presented to users remains a faithful representation of the internal history array, with every observation, step, and error rendered in chronological order.

Memory vs. Activity Distinction

PageAgent strictly separates persistent memory from transient activity:

  • Memory (history): Stored in PageAgentCore.history, included in LLM prompts, and emitted via historychange events. This data survives across execution steps and influences future agent decisions.
  • Activity (activity events): Transient UI feedback (states like thinking, executing, executed, retrying, error) that provides visual status indicators without affecting the LLM context or long-term memory.

This distinction is defined in packages/core/src/types.ts and implemented through separate emission methods (#emitHistoryChange versus #emitActivity).

Practical Implementation Examples

Adding Custom Observations Inside Tools

Tools can inject contextual notes into the persistent memory using the pushObservation method:

// packages/core/src/tools/myTool.ts
export const myTool = tool({
  description: 'Clicks an element and records why.',
  inputSchema: z.object({ selector: z.string() }),
  async execute(this: PageAgentCore, { selector }) {
    await this.pageController.clickElementBySelector(selector);
    
    // Record reasoning for future LLM context
    this.pushObservation(`Clicked element matching "${selector}"`);
  },
});

This observation will be included in the next <agent_history> block sent to the LLM.

Reading Memory from Execution Hooks

Developers can inspect the persistent history through the onAfterStep hook:

const agent = new PageAgentCore({ ... });
agent.config.onAfterStep = async (core, history) => {
  const lastStep = history[history.length - 1] as AgentStepEvent;
  console.log('Memory after step:', lastStep.reflection.memory);
};
await agent.execute('Summarize the page');

The hook receives the complete history array, enabling custom logging, analysis, or conditional logic based on the agent's accumulated memory.

Summary

  • PageAgent stores all execution state in a single history array within PageAgentCore, ensuring consistent memory across steps.
  • The HistoricalEvent union type provides type-safe storage for steps, observations, user takeovers, retries, and errors.
  • Every mutation emits a historychange event, enabling real-time synchronization between the core engine and UI components.
  • The LLM receives historical context through an XML-serialized <agent_history> block built from the history array, excluding transient retries and errors from the reasoning context.
  • The architecture cleanly separates persistent memory (affecting LLM decisions) from transient activity events (providing UI feedback only).

Frequently Asked Questions

How does PageAgent ensure the LLM remembers previous steps?

PageAgent serializes the entire history array into an <agent_history> XML block within each prompt. This block contains previous step reflections—including the agent's self-evaluation, memory notes, and next-goal planning—giving the LLM direct access to its own reasoning chain from prior execution steps.

What is the difference between an ObservationEvent and an AgentStepEvent?

An ObservationEvent captures external state changes or system notes (such as URL navigation or wait warnings) that provide context but do not represent agent decisions. An AgentStepEvent represents a complete LLM reasoning cycle, including the reflection output, tool action taken, and token usage statistics.

Can custom tools modify the agent's memory?

Yes. Custom tools can call this.pushObservation() to append ObservationEvent entries to the history. These observations persist across steps and appear in subsequent LLM prompts within the <agent_history> block, allowing tools to inject domain-specific context into the agent's memory.

Why are retry and error events excluded from LLM context?

Retry events (RetryEvent) and fatal errors (AgentErrorEvent) are recorded in history for debugging and UI display purposes, but intentionally omitted from the <agent_history> prompt block. This exclusion prevents the LLM from incorporating transient failure loops or error states into its permanent reasoning context, maintaining focus on successful execution trajectories.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →