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

  1. Session Retrieval: The runner fetches the session using the session service with provided identifiers.

  2. Agent Determination: It calls findAgentToRun to select the agent responsible for the next user message, based on session events and agent transfer policies.

  3. Invocation Context Setup: Builds an invocation context embedding artifacts, memory, session, the selected agent, user content, and run configuration.

  4. 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.

  5. Agent Execution: The selected agent's Run method is invoked with the context, yielding a stream of session events.

  6. Event Persistence: Non-partial events are appended to the session service, updating the session state.

  7. 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.

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.


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


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.