Agent Execution Runner
Overview
The Agent Execution Runner serves as the central coordinator for running AI agents within user interaction sessions. It is responsible for managing the flow of execution, including appending user messages to sessions, selecting the appropriate agent to handle the current input, invoking the agent's logic, and updating the session state with generated events. This module ensures smooth orchestration between agents, session storage, artifact management, and memory services during agent execution.
The runner is essential to bridging user inputs with agent responses, maintaining context and state across multi-turn conversations, and enabling features such as artifact saving and agent transfer within complex agent hierarchies.
Core Responsibilities
Session Message Handling and Event Management
When a user sends input, the runner appends this message to the corresponding session. This includes optionally saving any embedded blob data in the message as artifacts (see Input Blob Artifact Saving). The runner creates a new session event for the user message and persists it using the session service.
As the agent executes and generates outputs, the runner receives events from the agent's run sequence and appends them to the session. Partial events (e.g., streaming LLM responses) are handled without commit, while complete events are persisted to maintain accurate session history.
Agent Selection Logic
The runner identifies which agent should process the incoming user message. Instead of always running the root agent, it analyzes recent session events to find the last agent that produced an event and checks whether control can be transferred to that agent. This is critical for complex agent trees, where multiple sub-agents may be active during a session.
The selection algorithm walks backward through the session's event history, skipping user events, and tries to find an agent matching the event's author. It then verifies if the agent allows transfer up the agent tree (via a flag DisallowTransferToParent) to decide if the agent can be resumed or if the root agent should be used. This dynamic agent selection supports smooth agent handoffs and multi-agent conversations.
Interaction with Other Components
Session Service
The runner relies heavily on the session service to retrieve and update session state. It loads the current session by appName, userID, and sessionID, appends user messages as events, and persists agent-generated events to maintain the conversation history.
Artifact Service
When enabled, the runner supports saving inline blob data from user messages as artifacts stored via the artifact service. This integration allows agents to later reference or load those artifacts during execution.
Memory Service
The runner optionally integrates with a memory service that can provide long-term or contextual memory support for agents. It constructs a memory interface tied to the session and user, which the agent can query or update during its run.
Agent Tree and Parent Map
The runner maintains a map of agent parent relationships, enabling it to traverse the agent tree for transfer checks and agent lookups by name. This parent map is constructed once at runner initialization.
Invocation Context
For each run, the runner creates a rich invocation context encapsulating the session, artifacts, memory, user input content, and run configuration. This context is passed to the agent to provide all necessary runtime dependencies and state.
Key Functional Workflow
Session Retrieval: The runner fetches the session using the session service with provided identifiers.
Agent Determination: It calls
findAgentToRunto select the agent responsible for the next user message, based on session events and agent transfer policies.Invocation Context Setup: Builds an invocation context embedding artifacts, memory, session, the selected agent, user content, and run configuration.
Appending User Message: If the user message contains inline blob data and saving is enabled, blobs are saved as artifacts and the message parts replaced with textual placeholders. The modified user message event is appended to the session.
Agent Execution: The selected agent's
Runmethod is invoked with the context, yielding a stream of session events.Event Persistence: Non-partial events are appended to the session service, updating the session state.
Event Streaming: Events (and any errors) are yielded back to the caller, supporting streaming or batch processing.
Agent Selection Logic
The agent selection is a critical feature enabling multi-agent workflows and smooth control transfer within agent hierarchies.
The runner scans session events starting from the most recent.
It skips events authored by the user, focusing on agent-generated events.
For each agent event, the runner attempts to find the corresponding sub-agent by name via recursive search in the agent tree.
It then checks if this agent (and its parents) allow transfer by inspecting the
DisallowTransferToParentflag on LLM agents.If transferable, the runner selects that agent to handle the current input.
Otherwise, it defaults to the root agent.
This logic supports scenarios where conversations involve multiple agents, and the session naturally resumes with the most relevant agent.
Input Blob Artifact Saving
The runner can optionally save embedded blob data from user messages as artifacts before agent execution.
It iterates over message parts and identifies those containing inline blob data.
Each blob is saved to the artifact service with a generated filename based on the invocation ID.
The original blob-containing part in the message is replaced with a text placeholder indicating the artifact upload.
This approach offloads large or binary data from session events into dedicated artifact storage, improving efficiency and reusability.
Agents can later load these artifacts during their run to access the original data.
Code References
Runner Initialization
The runner is created via New(cfg Config), which validates input, builds the agent parent map, and initializes internal references:
func New(cfg Config) (*Runner, error) {
if cfg.Agent == nil {
return nil, fmt.Errorf("root agent is required")
}
if cfg.SessionService == nil {
return nil, fmt.Errorf("session service is required")
}
parents, err := parentmap.New(cfg.Agent)
if err != nil {
return nil, fmt.Errorf("failed to create agent tree: %w", err)
}
return &Runner{
appName: cfg.AppName,
rootAgent: cfg.Agent,
sessionService: cfg.SessionService,
artifactService: cfg.ArtifactService,
memoryService: cfg.MemoryService,
parents: parents,
}, nil
}
Running an Agent
Run is the main entrypoint to execute an agent run for a user message. It retrieves the session, selects the agent, sets up context, appends the user message, then iterates over the agent's event stream:
func (r *Runner) Run(ctx context.Context, userID, sessionID string, msg *genai.Content, cfg agent.RunConfig) iter.Seq2[*session.Event, error] {
// ...
resp, err := r.sessionService.Get(ctx, &session.GetRequest{...})
// error handling omitted for brevity
agentToRun, err := r.findAgentToRun(session)
// error handling omitted
ctx = parentmap.ToContext(ctx, r.parents)
// Setup invocation context with artifacts, memory, session, user content
if err := r.appendMessageToSession(ctx, session, msg, cfg.SaveInputBlobsAsArtifacts); err != nil {
yield(nil, err)
return
}
for event, err := range agentToRun.Run(ctx) {
if err != nil {
if !yield(event, err) {
return
}
continue
}
if !event.LLMResponse.Partial {
if err := r.sessionService.AppendEvent(ctx, session, event); err != nil {
yield(nil, fmt.Errorf("failed to add event to session: %w", err))
return
}
}
if !yield(event, nil) {
return
}
}
}
Agent Selection
The runner finds the agent to run by inspecting session events in reverse order:
func (r *Runner) findAgentToRun(session session.Session) (agent.Agent, error) {
events := session.Events()
for i := events.Len() - 1; i >= 0; i-- {
event := events.At(i)
if event.Author == "user" {
continue
}
subAgent := findAgent(r.rootAgent, event.Author)
if subAgent == nil {
continue
}
if r.isTransferableAcrossAgentTree(subAgent) {
return subAgent, nil
}
}
return r.rootAgent, nil
}
The isTransferableAcrossAgentTree method walks the agent's parent chain to check transfer restrictions:
func (r *Runner) isTransferableAcrossAgentTree(agentToRun agent.Agent) bool {
for curAgent := agentToRun; curAgent != nil; curAgent = r.parents[curAgent.Name()] {
llmAgent, ok := curAgent.(llminternal.Agent)
if !ok {
return false
}
if llminternal.Reveal(llmAgent).DisallowTransferToParent {
return false
}
}
return true
}
Visualization of Execution Flow
flowchart TD
UserInput[User Input Message]
AppendSession[Append User Message to Session]
DetermineAgent[Determine Agent to Run]
SetupContext[Setup Invocation Context]
RunAgent[Run Agent]
AgentEvent[Agent Generates Event]
PersistEvent{Is Event Partial?}
AppendEvent[Append Event to Session]
YieldEvent[Yield Event to Caller]
End[End Execution]
UserInput --> AppendSession --> DetermineAgent --> SetupContext --> RunAgent
RunAgent --> AgentEvent --> PersistEvent
PersistEvent -->|No| AppendEvent --> YieldEvent --> RunAgent
PersistEvent -->|Yes| YieldEvent --> RunAgent
RunAgent -->|No more events| End
Relations to Subtopics and Other Modules
The Agent Selection Logic subtopic details the mechanism by which the runner identifies the appropriate agent from the agent tree and session history for the incoming message.
The Input Blob Artifact Saving subtopic describes how the runner processes user messages containing inline blobs, saving them as artifacts before agent execution.
The runner interacts with services in the broader system including the Session Management, Artifact Management, and Memory Service subsystems.
It also connects with the agent framework in the AI Agent Framework topic by orchestrating agent execution lifecycle and state mutation.
This detailed explanation of the Agent Execution Runner clarifies how it manages session state, agent invocation, and artifact handling to maintain coherent, persistent conversations across complex agent hierarchies.