Skip to content

Agent Usage Documentation

Agent is the core execution unit of the tRPC-Agent-Go framework, responsible for processing user input and generating corresponding responses. Each Agent implements a unified interface, supporting streaming output and callback mechanisms.

The framework provides multiple types of Agents, including LLMAgent, ChainAgent, ParallelAgent, CycleAgent, and GraphAgent. This document focuses on LLMAgent. For detailed information about other Agent types and multi-Agent systems, please refer to Multi-Agent.

Quick Start

Recommended Usage: Runner

We strongly recommend using Runner to execute Agents instead of directly calling Agent interfaces. Runner provides a more user-friendly interface, integrating services like Session and Memory, making usage much simpler.

📖 Learn More: For detailed usage methods, please refer to Runner

This example uses OpenAI's GPT-4o-mini model. Before starting, please ensure you have prepared the corresponding OPENAI_API_KEY and exported it through environment variables:

export OPENAI_API_KEY="your_api_key"

Additionally, the framework supports OpenAI API-compatible models, which can be configured through environment variables:

export OPENAI_BASE_URL="your_api_base_url"
export OPENAI_API_KEY="your_api_key"

Creating Model Instance

First, you need to create a model instance. Here we use OpenAI's GPT-4o-mini model:

1
2
3
4
5
6
import "trpc.group/trpc-go/trpc-agent-go/model/openai"

modelName := flag.String("model", "gpt-4o-mini", "Name of the model to use")
flag.Parse()
// Create OpenAI model instance.
modelInstance := openai.New(*modelName, openai.Options{})

Configuring Generation Parameters

Set the model's generation parameters, including maximum tokens, temperature, and whether to use streaming output:

1
2
3
4
5
6
7
8
9
import "trpc.group/trpc-go/trpc-agent-go/model"

maxTokens := 1000
temperature := 0.7
genConfig := model.GenerationConfig{
    MaxTokens:   &maxTokens,   // Maximum number of tokens to generate.
    Temperature: &temperature, // Temperature parameter, controls output randomness.
    Stream:      true,         // Enable streaming output.
}

Creating LLMAgent

Use the model instance and configuration to create an LLMAgent, while setting the Agent's Description and Instruction.

Description is used to describe the basic functionality and characteristics of the Agent, while Instruction defines the specific instructions and behavioral guidelines that the Agent should follow when executing tasks.

import "trpc.group/trpc-go/trpc-agent-go/agent/llmagent"

llmAgent := llmagent.New(
    "demo-agent",                      // Agent name.
    llmagent.WithModel(modelInstance), // Set model.
    llmagent.WithDescription("A helpful AI assistant for demonstrations"),              // Set description.
    llmagent.WithInstruction("Be helpful, concise, and informative in your responses"), // Set instruction.
    llmagent.WithGenerationConfig(genConfig),                                           // Set generation parameters.
    // Set the filter mode for messages passed to the model. The final messages passed to the model must satisfy both WithMessageTimelineFilterMode and WithMessageBranchFilterMode conditions.
    // Timeline dimension filter conditions
    // Default: llmagent.TimelineFilterAll
    // Optional values:
    //  - llmagent.TimelineFilterAll: Includes historical messages as well as messages generated in the current request
    //  - llmagent.TimelineFilterCurrentRequest: Only includes messages generated in the current request
    //  - llmagent.TimelineFilterCurrentInvocation: Only includes messages generated in the current invocation context
    llmagent.WithMessageTimelineFilterMode(llmagent.TimelineFilterAll),

    // Branch dimension filter conditions
    // Default: llmagent.BranchFilterModePrefix
    // Optional values:
    //  - llmagent.BranchFilterModeAll: Includes messages from all agents. Use this when the current agent interacts with the model and needs to synchronize all valid content messages generated by all agents to the model.
    //  - llmagent.BranchFilterModePrefix: Filters messages by prefix matching Event.FilterKey with Invocation.eventFilterKey. Use this when you want to pass messages generated by the current agent and related upstream/downstream agents to the model.
    //  - llmagent.BranchFilterModeExact: Filters messages where Event.FilterKey == Invocation.eventFilterKey. Use this when the current agent interacts with the model and only needs to use messages generated by the current agent.
    llmagent.WithMessageBranchFilterMode(llmagent.BranchFilterModeAll),
)

Placeholder Variables (Session State Injection)

LLMAgent automatically injects session state into Instruction and the optional SystemPrompt via placeholder variables. Supported patterns:

  • {key}: Replaced with the string value corresponding to the key key in the session state (write via invocation.Session.SetState("key", ...) or SessionService)
  • {key?}: Optional; if missing, replaced with an empty string
  • {user:subkey} / {app:subkey} / {temp:subkey}: Use user/app/temp scoped keys (session services merge app/user state into session with these prefixes)
  • {invocation:subkey}: Replaces with the value of fmt.Sprintf("%+v", invocation.state["subkey"]). (The state can be set via invocation.SetState(k, v))

Notes:

  • If a non-optional key is not found, the original {key} is preserved (helps the LLM notice missing context)
  • Values are read from session state (Runner + SessionService set/merge this automatically)

Example:

llm := llmagent.New(
  "research-agent",
  llmagent.WithModel(modelInstance),
  llmagent.WithInstruction(
    "You are a research assistant. Focus: {research_topics}. " +
    "User interests: {user:topics?}. App banner: {app:banner?}.",
  ),
)

inv := agent.NewInvoction()
inv.SetState("case", "case-1")

// Initialize session state (Runner + SessionService)
_ = sessionService.UpdateUserState(ctx, session.UserKey{AppName: app, UserID: user}, session.StateMap{
  "topics": []byte("quantum computing, cryptography"),
})
_ = sessionService.UpdateAppState(ctx, app, session.StateMap{
  "banner": []byte("Research Mode"),
})
// Unprefixed keys live directly in session.State
_, _ = sessionService.CreateSession(ctx, session.Key{AppName: app, UserID: user, SessionID: sid}, session.StateMap{
  "research_topics": []byte("AI, ML, DL"),
})

See also:

  • Examples: examples/placeholder, examples/outputkey
  • Session API: docs/mkdocs/en/session.md

Using Runner to Execute Agent

Use Runner to execute the Agent, which is the recommended usage:

import "trpc.group/trpc-go/trpc-agent-go/runner"

// Create Runner.
runner := runner.NewRunner("demo-app", llmAgent)

// Send message directly without creating complex Invocation.
message := model.NewUserMessage("Hello! Can you tell me about yourself?")
eventChan, err := runner.Run(ctx, "user-001", "session-001", message)
if err != nil {
    log.Fatalf("Failed to execute Agent: %v", err)
}

Message Visibility Options

The Agent can dynamically manage the visibility of messages generated by other Agents and historical session messages based on different scenarios. This is configurable through relevant options. When interacting with the model, only the visible content is passed as input.

TIPS: - Messages from different sessionIDs are never visible to each other under any circumstances. The following control strategies only apply to messages sharing the same sessionID. - Invocation.Message always visible regardless of the configuration. - When the option is not configured, the default value is FullContext.

Config: - llmagent.WithMessageFilterMode(MessageFilterMode): - FullContext: Includes historical messages and messages generated in the current request, filtered by prefix matching with the filterKey. - RequestContext: Only includes messages generated in the current request, filtered by prefix matching with the filterKey. - IsolatedRequest: Only includes messages generated in the current request, filtered by exact matching with the filterKey. - IsolatedInvocation: Only includes messages generated in the current Invocation context, filtered by exact matching with the filterKey.

Recommended Usage Examples (These examples are simplified configurations based on advanced usage):

taskagentA := llmagent.New(
  "coordinator",
  llmagent.WithModel(modelInstance),
  // Makes all messages generated by taskagentA and taskagentB visible (including historical session messages under the same sessionID)
  llmagent.WithMessageFilterMode(llmagent.FullContext),
  // Makes all messages generated during the current runner.Run of taskagentA and taskagentB visible (excluding historical session messages)
  llmagent.WithMessageFilterMode(llmagent.RequestContext),
  // Only makes messages generated during the current runner.Run of taskagentA visible (excluding its own historical session messages)
  llmagent.WithMessageFilterMode(llmagent.IsolatedRequest),
  // Agent execution order: taskagentA-invocation1 -> taskagentB-invocation2 -> taskagentA-invocation3 (current execution phase)
  // Only makes messages generated during the current taskagentA-invocation3 phase visible (excluding its own historical session messages and messages generated during taskagentA-invocation1)
  llmagent.WithMessageFilterMode(llmagent.IsolatedInvocation),
)

taskagentB := llmagent.New(
  "coordinator",
  llmagent.WithModel(modelInstance),
  // Makes all messages generated by taskagentA and taskagentB visible (including historical session messages under the same sessionID)
  llmagent.WithMessageFilterMode(llmagent.FullContext),
  // Makes all messages generated during the current runner.Run of taskagentA and taskagentB visible (excluding historical session messages)
  llmagent.WithMessageFilterMode(llmagent.RequestContext),
  // Only makes messages generated during the current runner.Run of taskagentB visible (excluding its own historical session messages)
  llmagent.WithMessageFilterMode(llmagent.IsolatedRequest),
  // Agent execution order: taskagentA-invocation1 -> taskagentB-invocation2 -> taskagentA-invocation3 -> taskagentB-invocation4 (current execution phase)
  // Only makes messages generated during the current taskagentB-invocation4 phase visible (excluding its own historical session messages and messages generated during taskagentB-invocation2)
  llmagent.WithMessageFilterMode(llmagent.IsolatedInvocation),
)

// Cyclically execute taskagentA and taskagentB
cycleAgent := cycleagent.New(
  "coordinator",
  llmagent.WithModel(modelInstance),
  llmagent.WithSubAgents([]agent.Agent{taskagentA, taskagentB}),
  llmagent.WithMessageFilterMode(llmagent.FullContext)
)

// Create Runner
runner := runner.NewRunner("demo-app", cycleAgent)

// Send message directly without creating complex Invocation
message := model.NewUserMessage("Hello! Can you tell me about yourself?")
eventChan, err := runner.Run(ctx, "user-001", "session-001", message)
if err != nil {
    log.Fatalf("Failed to run Agent: %v", err)
}

Advanced Usage Examples: You can independently control the visibility of historical messages and messages generated by other Agents for the current agent using WithMessageTimelineFilterModeand WithMessageBranchFilterMode. When the current agent interacts with the model, only messages satisfying both conditions are input to the model. (invocation.Messageis always visible in any scenario.) - WithMessageTimelineFilterMode: Controls visibility from a temporal dimension - TimelineFilterAll: Includes historical messages and messages generated in the current request. - TimelineFilterCurrentRequest: Only includes messages generated in the current request (one runner.Runcounts as one request). - TimelineFilterCurrentInvocation: Only includes messages generated in the current invocation context. - WithMessageBranchFilterMode: Controls visibility from a branch dimension (used to manage visibility of messages generated by other agents). - BranchFilterModePrefix: Uses prefix matching between Event.FilterKeyand Invocation.eventFilterKey. - BranchFilterModeAll: Includes messages from all agents. - BranchFilterModeExact: Only includes messages generated by the current agent.

llmAgent := llmagent.New(
    "demo-agent",                      // Agent name
    llmagent.WithModel(modelInstance), // Set the model
    llmagent.WithDescription("A helpful AI assistant for demonstrations"),              // Set description
    llmagent.WithInstruction("Be helpful, concise, and informative in your responses"), // Set instruction
    llmagent.WithGenerationConfig(genConfig),                                           // Set generation parameters

    // Set the message filtering mode for input to the model. The final messages passed to the model must satisfy both WithMessageTimelineFilterMode and WithMessageBranchFilterMode conditions.
    // Temporal dimension filtering condition
    // Default: llmagent.TimelineFilterAll
    // Options:
    //  - llmagent.TimelineFilterAll: Includes historical messages and messages generated in the current request.
    //  - llmagent.TimelineFilterCurrentRequest: Only includes messages generated in the current request.
    //  - llmagent.TimelineFilterCurrentInvocation: Only includes messages generated in the current invocation context.
    llmagent.WithMessageTimelineFilterMode(llmagent.TimelineFilterAll),
    // Branch dimension filtering condition
    // Default: llmagent.BranchFilterModePrefix
    // Options:
    //  - llmagent.BranchFilterModeAll: Includes messages from all agents. Use this when the current agent needs to sync valid content messages generated by all agents to the model during interaction.
    //  - llmagent.BranchFilterModePrefix: Filters messages by prefix matching Event.FilterKey with Invocation.eventFilterKey. Use this when you want to pass messages generated by the current agent and related upstream/downstream agents to the model.
    //  - llmagent.BranchFilterModeExact: Filters messages where Event.FilterKey exactly matches Invocation.eventFilterKey. Use this when the current agent only needs to use messages generated by itself during model interaction.
    llmagent.WithMessageBranchFilterMode(llmagent.BranchFilterModeAll),
)

Reasoning Content Mode (DeepSeek Thinking Mode)

When using models with thinking/reasoning capabilities (such as DeepSeek), the model outputs both reasoning_content (thinking chain) and content (final answer). According to DeepSeek API documentation, in multi-turn conversations, you should not send the previous turn's reasoning_content to the model.

LLMAgent provides WithReasoningContentMode to control how reasoning_content is handled in conversation history:

Available Modes:

Mode Constant Description
Discard Previous Turns ReasoningContentModeDiscardPreviousTurns Discard reasoning_content from previous request turns, keep for current request. (Default, recommended)
Keep All ReasoningContentModeKeepAll Keep all reasoning_content in history (for debugging).
Discard All ReasoningContentModeDiscardAll Discard all reasoning_content from history for maximum bandwidth savings.

Usage Example:

1
2
3
4
5
6
7
8
// Recommended configuration for DeepSeek models with thinking mode.
agent := llmagent.New(
    "deepseek-agent",
    llmagent.WithModel(deepseekModel),
    llmagent.WithInstruction("You are a helpful assistant."),
    // Discard reasoning_content from previous turns (recommended for DeepSeek).
    llmagent.WithReasoningContentMode(llmagent.ReasoningContentModeDiscardPreviousTurns),
)

How It Works:

  • keep_all: All reasoning_content is preserved in session history. Use this if you need to retain thinking chains for debugging or analysis.
  • discard_previous_turns: When building the message list for a new request, reasoning_content from messages belonging to previous requests is cleared. Messages within the current request (e.g., during tool call loops) retain their reasoning_content. This follows DeepSeek's recommendation.
  • discard_all: All reasoning_content is stripped from historical messages before sending to the model.

Note: This option only affects how historical messages are processed before sending to the model. The current response's reasoning_content is always captured and stored in session events.

Delegation Visibility Options

When building multi‑Agent systems (task delegation between Agents), LLMAgent provides a unified fallback option for delegation events. Transfer events always include announcement text and are tagged transfer so UIs (User Interfaces) can filter them if desired.

  • llmagent.WithDefaultTransferMessage(string)
    • Configure the default message used when a model calls a SubAgent without a message.
    • Pass an empty string to disable injecting a default message; pass a non‑empty string to enable and override it.

Usage example:

1
2
3
4
5
6
7
8
coordinator := llmagent.New(
  "coordinator",
  llmagent.WithModel(modelInstance),
  llmagent.WithSubAgents([]agent.Agent{mathAgent, weatherAgent}),
  // Transfer announcement events are always emitted (tagged `transfer`). Filter in the UI if needed.
  // Customize the default message when the model omits it (empty string disables)
  llmagent.WithDefaultTransferMessage("Handing off to the specialist"),
)

Notes:

  • These options do not change the actual handoff logic; they only affect user‑visible texts or whether a fallback message is injected.
  • Transfer announcements are emitted as Events with Response.Object == "agent.transfer". If your UI should not display system‑level notices, filter this object type at the renderer/service layer.

Handling Event Stream

The eventChan returned by runner.Run() is an event channel. The Agent continuously sends Event objects to this channel during execution.

Each Event contains execution state information at a specific moment: LLM-generated content, tool call requests and results, error messages, etc. By iterating through the event channel, you can get real-time execution progress (see Event section below for details).

Receive execution results through the event channel:

// 1. Get event channel (returns immediately, starts async execution)
eventChan, err := runner.Run(ctx, userID, sessionID, message)
if err != nil {
    log.Fatalf("Failed to start: %v", err)
}

// 2. Handle event stream (receive execution results in real-time)
for event := range eventChan {
    // Check for errors
    if event.Error != nil {
        log.Printf("Execution error: %s", event.Error.Message)
        continue
    }

    // Handle response content
    if len(event.Response.Choices) > 0 {
        choice := event.Response.Choices[0]

        // Streaming content (real-time display)
        if choice.Delta.Content != "" {
            fmt.Print(choice.Delta.Content)
        }

        // Tool call information
        for _, toolCall := range choice.Message.ToolCalls {
            fmt.Printf("Calling tool: %s\n", toolCall.Function.Name)
        }
    }

    // Check if completed (note: should not break on tool call completion)
    if event.IsFinalResponse() {
        fmt.Println()
        break
    }
}

The complete code for this example can be found at examples/runner

Why is Runner recommended?

  1. Simpler Interface: No need to create complex Invocation objects
  2. Integrated Services: Automatically integrates Session, Memory and other services
  3. Better Management: Unified management of Agent execution flow
  4. Production Ready: Suitable for production environment use

💡 Tip: Want to learn more about Runner's detailed usage and advanced features? Please check Runner

Advanced Usage: Direct Agent Usage

If you need more fine-grained control, you can also use the Agent interface directly, but this requires creating Invocation objects:

Core Concepts

Invocation (Advanced Usage)

Invocation is the context object for Agent execution flow, containing all information needed for a single call. Note: This is advanced usage, we recommend using Runner to simplify operations.

import "trpc.group/trpc-go/trpc-agent-go/agent"

// Create Invocation object (advanced usage).
invocation := agent.NewInvocation(
    agent.WithAgentName(agent),                                                                  // Agent.
    agent.WithInvocationMessage(model.NewUserMessage("Hello! Can you tell me about yourself?")), // User message.
    agent.WithInvocationSession(&session.Session{ID: "session-001"}),                            // session object.
    agent.WithInvocationEndInvocation(false),                                                    // Whether to end invocation.
    agent.WithInvocationModel(modelInstance),                                                    // Model to use.
)

// Call Agent directly (advanced usage).
ctx := context.Background()
eventChan, err := llmAgent.Run(ctx, invocation)
if err != nil {
    log.Fatalf("Failed to execute Agent: %v", err)
}

When to use direct calls?

  • Need complete control over execution flow
  • Custom Session and Memory management
  • Implement special invocation logic
  • Debugging and testing scenarios
// Invocation is the context object for Agent execution flow, containing
// all information needed for a single call.
type Invocation struct {
    // Agent specifies the Agent instance to call.
    Agent Agent
    // AgentName identifies the name of the Agent instance to call.
    AgentName string
    // InvocationID provides a unique identifier for each call.
    InvocationID string
    // Branch is a branch identifier for hierarchical event filtering.
    Branch string
    // EndInvocation indicates whether to end the invocation.
    EndInvocation bool

    // Session maintains the context state of the conversation.
    Session *session.Session
    // Model specifies the model instance to use.
    Model model.Model
    // Message is the specific content sent by the user to the Agent.
    Message model.Message
    // RunOptions are option configurations for the Run call.
    RunOptions RunOptions
    // TransferInfo supports control transfer between Agents.
    TransferInfo *TransferInfo

    // Structured output configuration (optional).
    StructuredOutput     *model.StructuredOutput
    StructuredOutputType reflect.Type

    // Services injected for this invocation.
    MemoryService   memory.Service
    ArtifactService artifact.Service

    // Internal signaling: notify when events are appended.
    noticeChanMap map[string]chan any
    noticeMu      *sync.Mutex

    // Internal: event filter key and parent linkage for nested flows.
    eventFilterKey string
    parent         *Invocation

    // Invocation-scoped state (lazy-init, thread-safe via stateMu).
    state   map[string]any
    stateMu sync.RWMutex

    // Optional per-invocation safety limits (usually set by LLMAgent).
    MaxLLMCalls      int
    MaxToolIterations int

    // Internal counters used to enforce MaxLLMCalls / MaxToolIterations.
    llmCallCount       int
    toolIterationCount int
}

Invocation State

Invocation provides a general-purpose state storage mechanism for sharing data within the lifecycle of a single invocation. This is useful for callbacks, middleware, or any scenario that requires storing temporary data at the invocation level.

Core Methods:

1
2
3
4
5
6
7
8
// Set a state value
inv.SetState(key string, value any)

// Get a state value
value, ok := inv.GetState(key string)

// Delete a state value
inv.DeleteState(key string)

Features:

  • Invocation-scoped: State is automatically scoped to a single invocation
  • Thread-safe: Built-in RWMutex protection for concurrent access
  • Lazy initialization: Memory allocated only on first use
  • General-purpose: Can be used for callbacks, middleware, custom logic, and more

Usage Example:

Version Requirement
The structured callback API (recommended) requires trpc-agent-go >= 0.6.0.

// Store data in BeforeAgentCallback
// Note: Structured callback API requires trpc-agent-go >= 0.6.0
callbacks := agent.NewCallbacks()
callbacks.RegisterBeforeAgent(func(ctx context.Context, args *agent.BeforeAgentArgs) (*agent.BeforeAgentResult, error) {
    args.Invocation.SetState("agent:start_time", time.Now())
    args.Invocation.SetState("custom:request_id", "req-123")
    return nil, nil
})

// Read data in AfterAgentCallback
callbacks.RegisterAfterAgent(func(ctx context.Context, args *agent.AfterAgentArgs) (*agent.AfterAgentResult, error) {
    if startTime, ok := args.Invocation.GetState("agent:start_time"); ok {
        duration := time.Since(startTime.(time.Time))
        log.Printf("Execution took: %v", duration)
        args.Invocation.DeleteState("agent:start_time")
    }
    return nil, nil
})

Recommended Key Naming Convention:

  • Agent callbacks: "agent:xxx"
  • Model callbacks: "model:xxx"
  • Tool callbacks: "tool:toolName:xxx"
  • Middleware: "middleware:xxx"
  • Custom logic: "custom:xxx"

For detailed usage and more examples, please refer to Callbacks.

Event

Event is the real-time feedback generated during Agent execution, reporting execution progress in real-time through Event streams.

Events mainly include the following types:

  • Model conversation events
  • Tool call and response events
  • Agent transfer events
  • Error events
// Event is the real-time feedback generated during Agent execution, reporting execution progress in real-time through Event streams.
type Event struct {
    // Response contains model response content, tool call results and statistics.
    *model.Response
    // InvocationID is associated with a specific invocation.
    InvocationID string `json:"invocationId"`
    // Author is the source of the event, such as Agent or tool.
    Author string `json:"author"`
    // ID is the unique identifier of the event.
    ID string `json:"id"`
    // Timestamp records the time when the event occurred.
    Timestamp time.Time `json:"timestamp"`
    // Branch is a branch identifier for hierarchical event filtering.
    Branch string `json:"branch,omitempty"`
    // RequiresCompletion identifies whether this event requires a completion signal.
    RequiresCompletion bool `json:"requiresCompletion,omitempty"`
    // LongRunningToolIDs is a set of IDs for long-running function calls. Agent clients can understand which function calls are long-running through this field, only valid for function call events.
    LongRunningToolIDs map[string]struct{} `json:"longRunningToolIDs,omitempty"`
}

The streaming nature of Events allows you to see the Agent's working process in real-time, just like having a natural conversation with a real person. You only need to iterate through the Event stream, check the content and status of each Event, and you can completely handle the Agent's execution results.

Agent Interface

The Agent interface defines the core behaviors that all Agents must implement. This interface allows you to uniformly use different types of Agents while supporting tool calls and sub-Agent management.

type Agent interface {
    // Run receives execution context and invocation information, returns an event channel. Through this channel, you can receive Agent execution progress and results in real-time.
    Run(ctx context.Context, invocation *Invocation) (<-chan *event.Event, error)
    // Tools returns the list of tools that this Agent can access and execute.
    Tools() []tool.Tool
    // Info method provides basic information about the Agent, including name and description, for easy identification and management.
    Info() Info
    // SubAgents returns the list of sub-Agents available to this Agent.
    // SubAgents and FindSubAgent methods support collaboration between Agents. An Agent can delegate tasks to other Agents, building complex multi-Agent systems.
    SubAgents() []Agent
    // FindSubAgent finds sub-Agent by name.
    FindSubAgent(name string) Agent
}

The framework provides multiple types of Agent implementations, including LLMAgent, ChainAgent, ParallelAgent, CycleAgent, and GraphAgent. For detailed information about different types of Agents and multi-Agent systems, please refer to Multi-Agent.

Callbacks

Callbacks provide a rich callback mechanism that allows you to inject custom logic at key points during Agent execution.

Version Requirement
The structured callback API (recommended) requires trpc-agent-go >= 0.6.0.

Callback Types

The framework provides three types of callbacks:

Agent Callbacks: Triggered before and after Agent execution

// Create callbacks using agent.NewCallbacks()
callbacks := agent.NewCallbacks()

Model Callbacks: Triggered before and after model calls

// Create callbacks using model.NewCallbacks()
callbacks := model.NewCallbacks()

Tool Callbacks: Triggered before and after tool calls

// Create callbacks using tool.NewCallbacks()
callbacks := tool.NewCallbacks()

Usage Example

// Create Agent callbacks (using structured API)
// Note: Structured callback API requires trpc-agent-go >= 0.6.0
callbacks := agent.NewCallbacks()
callbacks.RegisterBeforeAgent(func(ctx context.Context, args *agent.BeforeAgentArgs) (*agent.BeforeAgentResult, error) {
    log.Printf("Agent %s started execution", args.Invocation.AgentName)
    return nil, nil
})
callbacks.RegisterAfterAgent(func(ctx context.Context, args *agent.AfterAgentArgs) (*agent.AfterAgentResult, error) {
    if args.Error != nil {
        log.Printf("Agent %s execution error: %v", args.Invocation.AgentName, args.Error)
    } else {
        log.Printf("Agent %s execution completed", args.Invocation.AgentName)
    }
    return nil, nil
})

// Use callbacks in llmAgent
llmagent := llmagent.New("llmagent", llmagent.WithAgentCallbacks(callbacks))

The callback mechanism allows you to precisely control the Agent's execution process and implement more complex business logic.

Structured Output

Structured output ensures that agent responses conform to a predefined format, making them easier to parse and process programmatically. The framework provides multiple methods for structured output, each suited for different use cases.

Comparison of Structured Output Methods

Feature WithStructuredOutputJSONSchema WithStructuredOutputJSON WithOutputSchema WithOutputKey
Tool Usage ✅ Allowed ✅ Allowed ❌ Disabled ✅ Enabled
Schema Type User-provided JSON Schema Auto-generated from Go struct User-provided JSON Schema N/A
Output Type Untyped (map/interface{}) Typed (Go struct) Untyped (map/interface{}) String/Bytes
Schema Validation ✅ By LLM ✅ By LLM ✅ By LLM ❌ None
Data Location Event.StructuredOutput Event.StructuredOutput Model response content Session State
Primary Use Case Flexible schema with tools Type-safe structured output Simple structured responses State storage & flow control

WithStructuredOutputJSONSchema

Provides a user-defined JSON schema for structured output while allowing tool usage. This is the most flexible option for agents that need both structured output and tool capabilities.

Example:

schema := map[string]any{
    "type": "object",
    "properties": map[string]any{
        "name": map[string]any{
            "type": "string",
            "description": "Product name",
        },
        "price": map[string]any{
            "type": "number",
            "minimum": 0,
        },
        "category": map[string]any{
            "type": "string",
            "enum": []string{"electronics", "clothing", "food"},
        },
    },
    "required": []string{"name", "price"},
}

agent := llmagent.New(
    "shopping-agent",
    llmagent.WithModel(modelInstance),
    llmagent.WithStructuredOutputJSONSchema(
        "shopping_output",      // Name
        schema,                 // JSON schema
        true,                   // Strict mode
        "Product information",  // Description
    ),
    llmagent.WithTools([]tool.Tool{searchTool, calculatorTool}), // Tools are allowed!
)

// Access untyped output from events
for event := range eventCh {
    if event.StructuredOutput != nil {
        data := event.StructuredOutput.(map[string]any)
        name := data["name"].(string)
        price := data["price"].(float64)
        fmt.Printf("Product: %s, Price: $%.2f\n", name, price)
    }
}

Best for: - Complex agents requiring both structured output and tool usage - Working with external JSON schemas (from APIs, databases, config files) - Prototyping with dynamic schemas - Gradual typing scenarios

WithStructuredOutputJSON

Auto-generates JSON schema from a Go struct and returns typed output. Provides compile-time type safety.

Example:

type ProductInfo struct {
    Name     string  `json:"name"`
    Price    float64 `json:"price"`
    Category string  `json:"category"`
}

agent := llmagent.New(
    "shopping-agent",
    llmagent.WithModel(modelInstance),
    llmagent.WithStructuredOutputJSON(
        new(ProductInfo),           // Auto-generates schema
        true,                       // Strict mode
        "Product information",      // Description
    ),
    llmagent.WithTools([]tool.Tool{searchTool}), // Tools are allowed
)

// Access typed output from events
for event := range eventCh {
    if event.StructuredOutput != nil {
        product := event.StructuredOutput.(*ProductInfo)
        fmt.Printf("Product: %s, Price: $%.2f\n", product.Name, product.Price)
    }
}

Best for: - Type-safe applications with well-defined Go structs - Clean code integration - Compile-time type checking

WithOutputSchema (Legacy)

Similar to WithStructuredOutputJSONSchema but disables all tools. This is a legacy method kept for backward compatibility.

Example:

1
2
3
4
5
6
agent := llmagent.New(
    "weather-agent",
    llmagent.WithModel(modelInstance),
    llmagent.WithOutputSchema(weatherSchema),
    // llmagent.WithTools(...) // ❌ Tools are disabled!
)

Limitations: - ❌ Cannot use tools, function calling, or RAG - ❌ Response in model content (needs parsing)

Migration Tip: If you need tool capabilities, migrate to WithStructuredOutputJSONSchema:

// Old: Tools disabled
agent := llmagent.New(
    "agent",
    llmagent.WithOutputSchema(schema),
)

// New: Tools enabled
agent := llmagent.New(
    "agent",
    llmagent.WithStructuredOutputJSONSchema(
        "agent_output",  // Name
        schema,          // JSON schema
        true,            // Strict mode
        "Agent output",  // Description
    ),
    llmagent.WithTools([]tool.Tool{myTool1, myTool2}), // ✅ Now works!
)

WithOutputKey

Stores agent output in session state under a specific key, useful for agent workflows where output needs to be accessed by downstream agents.

Example:

researchAgent := llmagent.New(
    "researcher",
    llmagent.WithModel(modelInstance),
    llmagent.WithOutputKey("research_findings"),
)

writerAgent := llmagent.New(
    "writer",
    llmagent.WithModel(modelInstance),
    llmagent.WithInstruction("Based on the research: {research_findings}, write a summary."),
)

// Chain agents using session state
chain := chainagent.New("pipeline", chainagent.WithSubAgents([]agent.Agent{
    researchAgent,
    writerAgent,
}))

Best for: - Multi-agent workflows with data passing - Session state management - Placeholder variable access in downstream agents

Choosing the Right Method

Scenario Recommended Method
Need tools + structured output WithStructuredOutputJSONSchema or WithStructuredOutputJSON
Type safety is critical WithStructuredOutputJSON
Working with external schemas WithStructuredOutputJSONSchema
Simple structured responses (no tools) WithOutputSchema
Multi-agent workflows WithOutputKey
Rapid prototyping WithStructuredOutputJSONSchema

Examples: - examples/structuredoutput/ - Demonstrates WithStructuredOutputJSON (typed) - examples/outputschema/ - Demonstrates WithOutputSchema (legacy) - examples/outputkey/ - Demonstrates WithOutputKey (session state)

Advanced Usage

The framework provides advanced features like Runner, Session, and Memory for building more complex Agent systems.

Runner is the recommended usage, responsible for managing Agent execution flow, connecting Session/Memory Service capabilities, and providing a more user-friendly interface.

Session Service is used to manage session state, supporting conversation history and context maintenance.

Memory Service is used to record user preference information, supporting personalized experiences.

Recommended Reading Order:

  1. Runner - Learn the recommended usage
  2. Session - Understand session management
  3. Multi-Agent - Learn multi-Agent systems

Runtime Instruction Updates

You can update an Agent’s behavior-defining text at runtime without rebuilding or restarting the Agent.

What changes dynamically

  • Instruction: the behavior guideline appended to the system message.
  • Global Instruction (system prompt): the system-level preface prepended to the request.

Both can be updated on an existing LLMAgent instance and take effect on subsequent model requests.

Example

import (
    "context"

    "trpc.group/trpc-go/trpc-agent-go/agent/llmagent"
    "trpc.group/trpc-go/trpc-agent-go/model"
    "trpc.group/trpc-go/trpc-agent-go/model/openai"
    "trpc.group/trpc-go/trpc-agent-go/runner"
)

// 1) Build model and agent once at startup.
mdl := openai.New("gpt-4o-mini", openai.Options{})
llm := llmagent.New(
    "support-bot",
    llmagent.WithModel(mdl),
    llmagent.WithInstruction("Be helpful and concise."),
)
run := runner.NewRunner("my-app", llm)

// 2) Later, change behavior at runtime (e.g., user updates prompt in UI).
llm.SetInstruction("Translate all user inputs to French.")
llm.SetGlobalInstruction("System: Safety first. No PII leakage.")

// 3) Subsequent runs use the new instructions.
msg := model.NewUserMessage("Where is the nearest museum?")
ch, err := run.Run(context.Background(), "u1", "s1", msg)
_ = ch; _ = err

Notes

  • Thread‑safe: the setters are concurrency‑safe and can be called while the service is handling requests.
  • Mid‑turn behavior: if an Agent’s current turn triggers more than one model request (e.g., due to tool calls), updates may apply to subsequent requests in the same turn. If you need per‑run stability, set or freeze the text at the start of the run.
  • Per‑run override: pass agent.WithInstruction(...) and/or agent.WithGlobalInstruction(...) to Runner.Run(...) to override prompts for a single request without mutating the Agent instance.
  • Model‑specific prompts: if an Agent can switch models, use llmagent.WithModelInstructions / llmagent.WithModelGlobalInstructions (or the corresponding setters) to override prompts by model.Info().Name, falling back to the Agent defaults when no mapping exists.
  • Per‑session personalization: for per‑user or per‑session data, prefer placeholders in the instruction and session state injection (see the “Placeholder Variables” section above).

Model-specific Prompts

If a single Agent can switch between different models, you can define a different Instruction/system prompt for each model.

The key used for matching is the model name returned by model.Info().Name.

Example

import (
    "trpc.group/trpc-go/trpc-agent-go/agent/llmagent"
    "trpc.group/trpc-go/trpc-agent-go/model"
    "trpc.group/trpc-go/trpc-agent-go/model/openai"
)

models := map[string]model.Model{
    "gpt-4o-mini": openai.New("gpt-4o-mini"),
    "gpt-4o":      openai.New("gpt-4o"),
}

llm := llmagent.New(
    "support-bot",
    llmagent.WithModels(models),
    llmagent.WithModel(models["gpt-4o-mini"]), // Default model.

    // Fallback prompts when no mapping exists.
    llmagent.WithGlobalInstruction("System: You are a helpful assistant."),
    llmagent.WithInstruction("Start every answer with DEFAULT:"),

    // Per-model prompt mapping.
    llmagent.WithModelGlobalInstructions(map[string]string{
        "gpt-4o-mini": "System: You are in FAST mode.",
        "gpt-4o":      "System: You are in SMART mode.",
    }),
    llmagent.WithModelInstructions(map[string]string{
        "gpt-4o-mini": "Start every answer with FAST:",
        "gpt-4o":      "Start every answer with SMART:",
    }),
)

See also: examples/model/promptmap.

Alternative: Placeholder‑Driven Dynamic System Prompts

If you’d rather not call setters, you can make the instruction itself a template and feed values via session state. The instruction processor replaces placeholders using session/app/user state on each turn.

Patterns

  • Persistent per‑user value: store under user:* and reference {user:key}.
  • Persistent per‑app value: store under app:* and reference {app:key}.
  • Session-scoped temporary value: write into the session’s temp:* namespace and reference {temp:key} (not user:*/app:*).

Example: per‑user dynamic instruction

import (
    "context"

    "trpc.group/trpc-go/trpc-agent-go/agent/llmagent"
    "trpc.group/trpc-go/trpc-agent-go/runner"
    "trpc.group/trpc-go/trpc-agent-go/session"
    "trpc.group/trpc-go/trpc-agent-go/session/inmemory"
)

svc := inmemory.NewSessionService()
app, user, sid := "my-app", "u1", "s1"

// 1) Instruction template references a user-scoped key.
llm := llmagent.New(
  "dyn-agent",
  llmagent.WithInstruction("{user:system_prompt}"),
)
run := runner.NewRunner(app, llm, runner.WithSessionService(svc))

// 2) Update the user-scoped state when the user changes settings.
_ = svc.UpdateUserState(context.Background(), session.UserKey{AppName: app, UserID: user}, session.StateMap{
  "system_prompt": []byte("You are a helpful assistant. Always answer in English."),
})

// 3) Runs now read the latest prompt via placeholder injection.
_, _ = run.Run(context.Background(), user, sid, model.NewUserMessage("Hi!"))

Example: per‑turn temp value via a before‑agent callback

Version Requirement
The structured callback API (recommended) requires trpc-agent-go >= 0.6.0.

// Note: Structured callback API requires trpc-agent-go >= 0.6.0
callbacks := agent.NewCallbacks()
callbacks.RegisterBeforeAgent(func(ctx context.Context, args *agent.BeforeAgentArgs) (*agent.BeforeAgentResult, error) {
  if args.Invocation != nil && args.Invocation.Session != nil {
    // Write a temporary instruction for this run
    args.Invocation.Session.SetState("temp:sys", []byte("Translate to French."))
  }
  return nil, nil
})

llm := llmagent.New(
  "temp-agent",
  llmagent.WithInstruction("{temp:sys}"),
  llmagent.WithAgentCallbacks(callbacks), // requires trpc-agent-go >= 0.6.0
)

Caveats

  • In-memory UpdateUserState intentionally forbids temp:* updates; write temp:* via invocation.Session.SetState (e.g., via a callback) when you need session-scoped temporary values.
  • Placeholders are resolved at request time; changing the stored value updates behavior on the next model request without recreating the agent.