Agent Lifecycle
How Daydreams agents process information and execute tasks.
Simple Overview
Think of an agent as following a simple loop:
- Something happens (input arrives)
- Agent thinks (uses LLM to decide what to do)
- Agent acts (performs actions or sends responses)
- Agent remembers (saves what happened)
- Repeat
This loop continues as long as the agent is running, handling new inputs and responding intelligently based on its context and memory.
Agent Boot Process
Before handling any inputs, agents go through an initialization phase:
// Agent creation
const agent = createDreams({
model: openai("gpt-4o"),
contexts: [chatContext, taskContext],
// ... other config
});
// Boot sequence
await agent.start();
Boot steps:
- Container Setup - Dependency injection container initialized
- Memory System - KV store, vector store, working memory initialized
- TaskRunner - Task queues and concurrency management setup
- Context Registry - All provided contexts registered with type validation
- Extension Loading - Input/output handlers and services activated
- Agent Context - If agent has its own context, it's created and prepared
The Basic Flow
Here's what happens when your agent receives a Discord message:
Discord Message Arrives
↓
Agent loads chat context & memory
↓
Agent thinks: "What should I do?"
↓
Agent decides: "I'll check the weather and respond"
↓
Agent calls weather API (action)
↓
Agent sends Discord reply (output)
↓
Agent saves conversation to memory
Detailed Technical Explanation
The core of the Daydreams framework is the agent's execution lifecycle. This loop manages how an agent receives input, reasons with an LLM, performs actions, and handles results. Understanding this flow is crucial for building and debugging agents.
Let's trace the lifecycle of a typical request:
1. Input Reception
- Source: An external system (like Discord, Telegram, CLI, or an API) sends
information to the agent. This is usually configured via an
extension
. - Listener: An
input
definition within the agent or an extension listens for these events (e.g., a new message arrives). - Trigger: When the external event occurs, the input listener is triggered.
- Invocation: The listener typically calls
agent.send(...)
, providing:- The target
context
definition (which part of the agent should handle this?). args
to identify the specific context instance (e.g., which chat session?).- The input
data
itself (e.g., the message content).
- The target
2. agent.send
- Starting the Process
- Log Input: The framework logs the incoming information as an
InputRef
(a record of the input). - Initiate Run: It then calls the internal
agent.run
method to start or continue the processing cycle for the specified context instance, passing the newInputRef
along.
3. agent.run
- Managing the Execution Cycle
- Context Queue Management: Uses TaskRunner to ensure only one execution per context instance at a time. Multiple inputs to the same context get queued.
- Load/Create Context: The framework finds the specific
ContextState
for the target instance (e.g., the state for chat session #123). If it's the first time, it creates the state and persistent memory (ContextState.memory
). - Working Memory Setup: Retrieves or creates temporary
WorkingMemory
for this execution containing arrays for inputs, outputs, actions, results, thoughts, events, steps, and runs. - Context Preparation: If the context uses
.use()
composition, all composer functions are executed to determine which additional contexts to prepare. These are prepared in parallel. - Engine Initialization: Creates an Engine instance that will orchestrate the step-by-step execution with its router system for handling inputs, outputs, and actions.
- Start Step Loop: Begins the main reasoning loop that continues until the LLM stops generating actions or hits step limits.
4. Inside the Step Loop - Perception, Reasoning, Action
Each iteration (step) within the agent.run
loop represents one turn of the
agent's core reasoning cycle:
- Prepare State: The agent gathers the latest information, including:
- The current persistent state of the active
Context
(s) (via theirrender
functions). - The history of the current interaction from
WorkingMemory
(processed inputs, outputs, action results from previous steps).
- Any new unprocessed information (like the initial
InputRef
or results from actions completed in the previous step). - The list of currently available
actions
andoutputs
.
- The current persistent state of the active
- Generate Prompt: This information is formatted into a structured prompt (using XML) for the LLM. The prompt clearly tells the LLM its instructions, what tools (actions/outputs) it has, the current state, and what new information needs attention. (See Prompting).
- LLM Call: The agent sends the complete prompt to the configured LLM.
- Stream XML Parsing: As the LLM generates its response token by token:
- The framework uses
xmlStreamParser
to process chunks in real-time - Detects and extracts elements:
<think>
,<action_call>
,<output>
- Each element becomes a typed reference (ThoughtRef, ActionCall, OutputRef) immediately pushed to WorkingMemory
- Incomplete elements are held until closing tags are found
- The framework uses
- Engine Router Processing: The Engine routes each parsed element:
- ThoughtRef → Added to
workingMemory.thoughts
array - ActionCall → Validated, template-resolved, queued via TaskRunner
- OutputRef → Validated and processed through output handlers
- ThoughtRef → Added to
- Action Execution: For each
<action_call>
:- Arguments parsed and validated against Zod schema
- Template resolution for
{{calls[0].data}}
style references - TaskRunner queues action with proper concurrency control
- Results returned as ActionResult and added to
workingMemory.results
- Step Completion: Engine checks if more steps needed:
- Continues if LLM generated actions that might produce new information
- Stops if no unprocessed elements remain or step limit reached
5. Run Completion
- Exit Loop: Once the loop condition determines no further steps are needed, the loop exits.
- Final Tasks: Any final cleanup logic or
onRun
hooks defined in the context are executed. - Save State: The final persistent state (
ContextState.memory
) of all involved contexts is saved to theMemoryStore
. - Return Results: The framework resolves the promise originally returned by
agent.send
oragent.run
, providing the complete log (chain
) of the interaction.
Practical Example: Complete Lifecycle Trace
Here's a real example showing the complete lifecycle when a user asks for weather:
// User message triggers input
await agent.send({
context: chatContext,
args: { userId: "alice" },
input: { type: "text", data: "What's the weather in NYC?" }
});
Execution trace:
1. INPUT RECEPTION
InputRef created: { type: "text", data: "What's the weather in NYC?" }
2. CONTEXT PREPARATION
ContextState loaded/created: chat:alice
Working memory retrieved with conversation history
Composed contexts prepared (if any .use() definitions)
3. ENGINE STEP 1
Prompt generated with:
- Context render: "Chat with alice, 5 previous messages..."
- Available actions: [getWeather, ...]
- New input: "What's the weather in NYC?"
LLM Response (streamed):
<think>User wants weather for NYC. I should use getWeather action.</think>
<action_call name="getWeather">
{"location": "New York City"}
</action_call>
Parsed elements:
- ThoughtRef → workingMemory.thoughts[]
- ActionCall → validated, queued via TaskRunner
Action execution:
- getWeather handler called with {"location": "New York City"}
- Returns: {temperature: 72, condition: "sunny"}
- ActionResult → workingMemory.results[]
4. ENGINE STEP 2
Prompt includes action result from Step 1
LLM Response:
<think>Got weather data, now I'll respond to the user.</think>
<output type="text">
The weather in NYC is 72°F and sunny! Perfect day to go outside.
</output>
Parsed elements:
- ThoughtRef → workingMemory.thoughts[]
- OutputRef → processed through text output handler
5. RUN COMPLETION
- No more unprocessed elements
- Context onRun hooks executed
- Context memory saved: chat:alice state updated
- Working memory persisted
- Chain of all logs returned: [InputRef, ThoughtRef, ActionCall, ActionResult, ThoughtRef, OutputRef]
This shows how the agent cycles through reasoning steps, executing actions and generating outputs until the interaction is complete.
Error Handling in Lifecycle
When errors occur during the lifecycle:
- Action Failures: ActionResult contains error information, LLM can see failure and retry or handle gracefully
- LLM Errors: Automatic retry with exponential backoff
- Context Errors: onError hooks called, execution can continue or abort
- Stream Parsing Errors: Invalid XML is logged but doesn't crash execution
- Memory Errors: Fallback to in-memory storage, logging for debugging
This detailed cycle illustrates how Daydreams agents iteratively perceive (inputs, results), reason (LLM prompt/response), and act (outputs, actions), using streaming and asynchronous task management to handle complex interactions efficiently.