CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-dev-langchain4j--langchain4j-agentic

LangChain4j Agentic Framework provides a comprehensive Java library for building multi-agent AI systems with support for workflow orchestration, supervisor agents, planning-based execution, declarative configuration, agent-to-agent communication, and human-in-the-loop workflows.

Overview
Eval results
Files

observability.mddocs/api/

Observability API

Complete API reference for monitoring and tracking agent execution through listeners that observe agent lifecycle, tool execution, and agentic scope events.

Quick Start

import dev.langchain4j.agentic.observability.*;

class SimpleListener implements AgentListener {
    @Override
    public void beforeAgentInvocation(AgentRequest request) {
        System.out.println("Starting: " + request.agentName());
    }

    @Override
    public void afterAgentInvocation(AgentResponse response) {
        System.out.println("Completed: " + response.agentName() +
                         " in " + response.duration().toMillis() + "ms");
    }
}

UntypedAgent agent = AgenticServices.agentBuilder()
    .chatModel(chatModel)
    .listener(new SimpleListener())
    .build();

AgentListener Interface

Central interface for observing all agent execution events.

/**
 * Listener for agent execution events
 * All methods are optional (default implementations provided)
 */
interface AgentListener {
    /**
     * Called before agent invocation
     * @param agentRequest Request information including name, type, arguments, and scope
     */
    default void beforeAgentInvocation(AgentRequest agentRequest) {
        // Override to handle before invocation
    }

    /**
     * Called after agent invocation completes successfully
     * @param agentResponse Response information including name, result, scope, and duration
     */
    default void afterAgentInvocation(AgentResponse agentResponse) {
        // Override to handle after invocation
    }

    /**
     * Called when agent invocation fails
     * @param agentInvocationError Error information including name, error, and scope
     */
    default void onAgentInvocationError(AgentInvocationError agentInvocationError) {
        // Override to handle errors
    }

    /**
     * Called after AgenticScope is created
     * @param agenticScope Created scope with memory ID
     */
    default void afterAgenticScopeCreated(AgenticScope agenticScope) {
        // Override to handle scope creation
    }

    /**
     * Called before AgenticScope is destroyed
     * @param agenticScope Scope being destroyed with final state
     */
    default void beforeAgenticScopeDestroyed(AgenticScope agenticScope) {
        // Override to handle scope destruction
    }

    /**
     * Called before tool execution
     * @param beforeToolExecution Tool execution details including name and arguments
     */
    default void beforeToolExecution(BeforeToolExecution beforeToolExecution) {
        // Override to handle before tool execution
    }

    /**
     * Called after tool execution completes
     * @param toolExecution Tool execution details including name, result, error, and duration
     */
    default void afterToolExecution(ToolExecution toolExecution) {
        // Override to handle after tool execution
    }

    /**
     * Whether listener is inherited by sub-agents
     * @return true if inherited, false otherwise (default: false)
     */
    default boolean inheritedBySubagents() {
        return false;
    }
}

Usage Examples:

import dev.langchain4j.agentic.observability.*;

class ComprehensiveListener implements AgentListener {
    @Override
    public void beforeAgentInvocation(AgentRequest request) {
        System.out.println("=== Agent Starting ===");
        System.out.println("Agent: " + request.agentName());
        System.out.println("Type: " + request.agentType().getSimpleName());
        System.out.println("Arguments: " + request.arguments());
    }

    @Override
    public void afterAgentInvocation(AgentResponse response) {
        System.out.println("=== Agent Completed ===");
        System.out.println("Agent: " + response.agentName());
        System.out.println("Result: " + response.result());
        System.out.println("Duration: " + response.duration().toMillis() + "ms");
    }

    @Override
    public void onAgentInvocationError(AgentInvocationError error) {
        System.err.println("=== Agent Error ===");
        System.err.println("Agent: " + error.agentName());
        System.err.println("Error: " + error.error().getMessage());
        error.error().printStackTrace();
    }

    @Override
    public void afterAgenticScopeCreated(AgenticScope scope) {
        System.out.println("AgenticScope created for memory ID: " + scope.memoryId());
    }

    @Override
    public void beforeAgenticScopeDestroyed(AgenticScope scope) {
        System.out.println("AgenticScope destroyed. Final state: " + scope.state());
        System.out.println("Total invocations: " + scope.agentInvocations().size());
    }

    @Override
    public void beforeToolExecution(BeforeToolExecution beforeExec) {
        System.out.println("Executing tool: " + beforeExec.toolName());
        System.out.println("Arguments: " + beforeExec.arguments());
    }

    @Override
    public void afterToolExecution(ToolExecution toolExec) {
        System.out.println("Tool completed: " + toolExec.toolName() +
                         " in " + toolExec.duration().toMillis() + "ms");

        if (toolExec.error() != null) {
            System.err.println("Tool error: " + toolExec.error().getMessage());
        } else {
            System.out.println("Tool result: " + toolExec.result());
        }
    }

    @Override
    public boolean inheritedBySubagents() {
        return true; // Propagate to sub-agents
    }
}

// Use the listener
UntypedAgent agent = AgenticServices.agentBuilder()
    .chatModel(chatModel)
    .listener(new ComprehensiveListener())
    .build();

Agent Lifecycle Events

AgentRequest Record

Information provided before agent invocation.

/**
 * Request information for agent invocation
 */
record AgentRequest(
    String agentName,
    Class<?> agentType,
    Map<String, Object> arguments,
    AgenticScope agenticScope
) {}

Usage Examples:

class DetailedRequestListener implements AgentListener {
    @Override
    public void beforeAgentInvocation(AgentRequest request) {
        // Access request details
        String name = request.agentName();
        Class<?> type = request.agentType();
        Map<String, Object> args = request.arguments();
        AgenticScope scope = request.agenticScope();

        // Log request details
        System.out.println(String.format(
            "Invoking agent '%s' of type %s with args: %s",
            name,
            type.getSimpleName(),
            args
        ));

        // Access scope state
        System.out.println("Current scope state: " + scope.state());
        System.out.println("Memory ID: " + scope.memoryId());

        // Track invocation in scope
        int count = scope.readState("invocation_count", 0);
        scope.writeState("invocation_count", count + 1);
        scope.writeState("last_agent", name);
    }
}

AgentResponse Record

Information provided after successful agent invocation.

/**
 * Response information from agent invocation
 */
record AgentResponse(
    String agentName,
    Class<?> agentType,
    Object result,
    AgenticScope agenticScope,
    Duration duration
) {}

Usage Examples:

class PerformanceTrackingListener implements AgentListener {
    private final Map<String, List<Long>> executionTimes = new ConcurrentHashMap<>();

    @Override
    public void afterAgentInvocation(AgentResponse response) {
        // Access response details
        String name = response.agentName();
        Object result = response.result();
        Duration duration = response.duration();
        AgenticScope scope = response.agenticScope();

        // Track execution time
        executionTimes.computeIfAbsent(name, k -> new ArrayList<>())
                     .add(duration.toMillis());

        // Log performance
        System.out.println(String.format(
            "Agent '%s' completed in %dms with result: %s",
            name,
            duration.toMillis(),
            result
        ));

        // Store metrics in scope
        scope.writeState("last_duration_ms", duration.toMillis());
        scope.writeState("last_result", result);

        // Calculate and log average
        List<Long> times = executionTimes.get(name);
        double average = times.stream().mapToLong(Long::longValue).average().orElse(0);
        System.out.println(String.format(
            "Average execution time for %s: %.2fms",
            name,
            average
        ));

        // Alert on slow execution
        if (duration.toMillis() > 5000) {
            System.err.println("WARNING: Slow agent execution detected!");
        }
    }
}

AgentInvocationError Record

Information provided when agent invocation fails.

/**
 * Error information from agent invocation
 */
record AgentInvocationError(
    String agentName,
    Class<?> agentType,
    Throwable error,
    AgenticScope agenticScope
) {}

Usage Examples:

class ErrorTrackingListener implements AgentListener {
    private final Map<String, Integer> errorCounts = new ConcurrentHashMap<>();
    private final Map<String, List<String>> errorMessages = new ConcurrentHashMap<>();

    @Override
    public void onAgentInvocationError(AgentInvocationError invocationError) {
        // Access error details
        String name = invocationError.agentName();
        Class<?> type = invocationError.agentType();
        Throwable error = invocationError.error();
        AgenticScope scope = invocationError.agenticScope();

        // Track error count
        errorCounts.merge(name, 1, Integer::sum);

        // Track error messages
        errorMessages.computeIfAbsent(name, k -> new ArrayList<>())
                    .add(error.getMessage());

        // Log error with context
        System.err.println(String.format(
            "Agent '%s' (type: %s) failed with error: %s",
            name,
            type.getSimpleName(),
            error.getMessage()
        ));
        error.printStackTrace();

        // Store error in scope for recovery
        scope.writeState("last_error", error.getMessage());
        scope.writeState("last_error_agent", name);
        scope.writeState("error_timestamp", System.currentTimeMillis());

        // Alert if too many errors
        int count = errorCounts.get(name);
        if (count > 5) {
            System.err.println(String.format(
                "CRITICAL: Agent %s has failed %d times!",
                name,
                count
            ));
            sendAlert(name, count);
        }

        // Log error pattern
        if (count > 1) {
            System.err.println("Recent errors for " + name + ":");
            errorMessages.get(name).forEach(msg ->
                System.err.println("  - " + msg)
            );
        }
    }

    private void sendAlert(String agentName, int errorCount) {
        // Send alert to monitoring system
    }
}

Tool Execution Events

BeforeToolExecution Record

Information provided before tool execution.

/**
 * Information before tool execution
 */
record BeforeToolExecution(
    String toolName,
    Map<String, Object> arguments
) {}

Usage Examples:

class ToolMonitoringListener implements AgentListener {
    @Override
    public void beforeToolExecution(BeforeToolExecution beforeExec) {
        System.out.println("=== Tool Starting ===");
        System.out.println("Tool: " + beforeExec.toolName());
        System.out.println("Arguments: " + beforeExec.arguments());

        // Log specific tools
        if ("database_query".equals(beforeExec.toolName())) {
            String query = (String) beforeExec.arguments().get("query");
            System.out.println("Executing database query: " + query);
        }
    }
}

ToolExecution Record

Information provided after tool execution.

/**
 * Information after tool execution
 */
record ToolExecution(
    String toolName,
    Map<String, Object> arguments,
    Object result,
    Throwable error,
    Duration duration
) {}

Usage Examples:

class ToolPerformanceListener implements AgentListener {
    private final Map<String, Statistics> toolStats = new ConcurrentHashMap<>();

    @Override
    public void afterToolExecution(ToolExecution toolExec) {
        System.out.println("=== Tool Completed ===");
        System.out.println("Tool: " + toolExec.toolName());
        System.out.println("Duration: " + toolExec.duration().toMillis() + "ms");

        // Track statistics
        toolStats.computeIfAbsent(toolExec.toolName(), k -> new Statistics())
                .record(toolExec.duration().toMillis());

        // Check for errors
        if (toolExec.error() != null) {
            System.err.println("Tool error: " + toolExec.error().getMessage());
            toolExec.error().printStackTrace();
        } else {
            System.out.println("Tool result: " + toolExec.result());
        }

        // Alert on slow tools
        if (toolExec.duration().toMillis() > 1000) {
            System.err.println(String.format(
                "SLOW TOOL: %s took %dms",
                toolExec.toolName(),
                toolExec.duration().toMillis()
            ));
        }

        // Log statistics periodically
        Statistics stats = toolStats.get(toolExec.toolName());
        if (stats.count() % 10 == 0) {
            System.out.println(String.format(
                "Tool %s statistics - Avg: %.2fms, Min: %dms, Max: %dms, Count: %d",
                toolExec.toolName(),
                stats.average(),
                stats.min(),
                stats.max(),
                stats.count()
            ));
        }
    }

    static class Statistics {
        private long sum = 0;
        private long min = Long.MAX_VALUE;
        private long max = 0;
        private int count = 0;

        void record(long value) {
            sum += value;
            min = Math.min(min, value);
            max = Math.max(max, value);
            count++;
        }

        double average() { return count > 0 ? (double) sum / count : 0; }
        long min() { return min != Long.MAX_VALUE ? min : 0; }
        long max() { return max; }
        int count() { return count; }
    }
}

AgenticScope Lifecycle Events

Scope Creation

Monitor when agentic scopes are created.

/**
 * Called after AgenticScope created
 * @param agenticScope Created scope
 */
default void afterAgenticScopeCreated(AgenticScope agenticScope) {
    // Override to handle scope creation
}

Usage Examples:

class ScopeLifecycleListener implements AgentListener {
    @Override
    public void afterAgenticScopeCreated(AgenticScope scope) {
        System.out.println("=== Scope Created ===");
        System.out.println("Memory ID: " + scope.memoryId());

        // Initialize tracking state
        scope.writeState("scope_created_at", System.currentTimeMillis());
        scope.writeState("operation_count", 0);
        scope.writeState("scope_id", UUID.randomUUID().toString());

        // Log creation
        System.out.println("Initialized scope with ID: " + scope.readState("scope_id"));
    }
}

Scope Destruction

Monitor when agentic scopes are destroyed to capture final metrics.

/**
 * Called before AgenticScope destroyed
 * @param agenticScope Scope being destroyed
 */
default void beforeAgenticScopeDestroyed(AgenticScope agenticScope) {
    // Override to handle scope destruction
}

Usage Examples:

class ScopeAnalyticsListener implements AgentListener {
    @Override
    public void beforeAgenticScopeDestroyed(AgenticScope scope) {
        System.out.println("=== Scope Destroying ===");

        // Log final state
        Map<String, Object> finalState = scope.state();
        System.out.println("Final state keys: " + finalState.keySet());
        System.out.println("Final state: " + finalState);

        // Calculate lifetime
        Long createdAt = (Long) scope.readState("scope_created_at");
        if (createdAt != null) {
            long lifetime = System.currentTimeMillis() - createdAt;
            System.out.println("Scope lifetime: " + lifetime + "ms");
        }

        // Log invocation summary
        List<AgentInvocation> invocations = scope.agentInvocations();
        System.out.println("Total agent invocations: " + invocations.size());

        // Group by agent
        Map<String, Long> invocationsByAgent = invocations.stream()
            .collect(Collectors.groupingBy(
                AgentInvocation::agentName,
                Collectors.counting()
            ));
        System.out.println("Invocations by agent: " + invocationsByAgent);

        // Calculate success rate
        long errors = finalState.keySet().stream()
            .filter(k -> k.contains("error"))
            .count();
        double successRate = invocations.isEmpty() ? 100.0 :
            ((invocations.size() - errors) * 100.0 / invocations.size());
        System.out.println("Success rate: " + String.format("%.2f%%", successRate));
    }
}

Listener Composition and Inheritance

ComposedAgentListener

Compose multiple listeners into one.

/**
 * Composes multiple listeners into one
 * All composed listeners receive all events
 */
class ComposedAgentListener implements AgentListener {
    private final List<AgentListener> listeners;

    /**
     * Create composed listener
     * @param listeners Listeners to compose
     */
    public ComposedAgentListener(AgentListener... listeners) {
        this.listeners = Arrays.asList(listeners);
    }

    @Override
    public void beforeAgentInvocation(AgentRequest agentRequest) {
        listeners.forEach(listener -> listener.beforeAgentInvocation(agentRequest));
    }

    @Override
    public void afterAgentInvocation(AgentResponse agentResponse) {
        listeners.forEach(listener -> listener.afterAgentInvocation(agentResponse));
    }

    @Override
    public void onAgentInvocationError(AgentInvocationError agentInvocationError) {
        listeners.forEach(listener -> listener.onAgentInvocationError(agentInvocationError));
    }

    @Override
    public void afterAgenticScopeCreated(AgenticScope agenticScope) {
        listeners.forEach(listener -> listener.afterAgenticScopeCreated(agenticScope));
    }

    @Override
    public void beforeAgenticScopeDestroyed(AgenticScope agenticScope) {
        listeners.forEach(listener -> listener.beforeAgenticScopeDestroyed(agenticScope));
    }

    @Override
    public void beforeToolExecution(BeforeToolExecution beforeToolExecution) {
        listeners.forEach(listener -> listener.beforeToolExecution(beforeToolExecution));
    }

    @Override
    public void afterToolExecution(ToolExecution toolExecution) {
        listeners.forEach(listener -> listener.afterToolExecution(toolExecution));
    }

    @Override
    public boolean inheritedBySubagents() {
        return listeners.stream().anyMatch(AgentListener::inheritedBySubagents);
    }
}

Usage Examples:

// Create individual listeners
AgentListener loggingListener = new LoggingListener();
AgentListener metricsListener = new MetricsListener();
AgentListener errorListener = new ErrorTrackingListener();

// Compose them
AgentListener composedListener = new ComposedAgentListener(
    loggingListener,
    metricsListener,
    errorListener
);

// Use composed listener
UntypedAgent agent = AgenticServices.agentBuilder()
    .chatModel(chatModel)
    .listener(composedListener)
    .build();

// All three listeners will receive all events
Object result = agent.invoke("Process this");

Listener Inheritance

Control whether listeners are inherited by sub-agents.

/**
 * Whether listener is inherited by sub-agents
 * @return true if inherited, false otherwise (default: false)
 */
default boolean inheritedBySubagents() {
    return false;
}

Usage Examples:

// Listener that propagates to sub-agents
class GlobalListener implements AgentListener {
    @Override
    public void beforeAgentInvocation(AgentRequest request) {
        System.out.println("Global: " + request.agentName() + " starting");
    }

    @Override
    public void afterAgentInvocation(AgentResponse response) {
        System.out.println("Global: " + response.agentName() + " completed");
    }

    @Override
    public boolean inheritedBySubagents() {
        return true; // This listener will be inherited by all sub-agents
    }
}

// Listener only for parent agent
class ParentOnlyListener implements AgentListener {
    @Override
    public void beforeAgentInvocation(AgentRequest request) {
        System.out.println("Parent only: " + request.agentName());
    }

    @Override
    public boolean inheritedBySubagents() {
        return false; // Sub-agents won't receive these events
    }
}

// Use in workflow
UntypedAgent workflow = AgenticServices.sequenceBuilder()
    .listener(new GlobalListener()) // Will propagate to sub-agents
    .listener(new ParentOnlyListener()) // Won't propagate
    .subAgents(agent1, agent2, agent3)
    .build();

// When workflow executes:
// - GlobalListener receives events from workflow AND all sub-agents
// - ParentOnlyListener only receives events from workflow itself

Common Listener Patterns

Pattern 1: Performance Monitoring

Track and analyze agent performance metrics.

class PerformanceMonitor implements AgentListener {
    private final Map<String, Statistics> stats = new ConcurrentHashMap<>();

    @Override
    public void afterAgentInvocation(AgentResponse response) {
        String agentName = response.agentName();
        long durationMs = response.duration().toMillis();

        stats.computeIfAbsent(agentName, k -> new Statistics())
             .record(durationMs);

        // Log if slow
        if (durationMs > 1000) {
            System.out.println(String.format(
                "SLOW: Agent %s took %dms",
                agentName,
                durationMs
            ));
        }
    }

    public void printReport() {
        System.out.println("=== Performance Report ===");
        stats.forEach((name, stat) -> {
            System.out.println(String.format(
                "Agent: %s\n" +
                "  Average: %.2fms\n" +
                "  Min: %dms\n" +
                "  Max: %dms\n" +
                "  Count: %d\n",
                name,
                stat.average(),
                stat.min(),
                stat.max(),
                stat.count()
            ));
        });
    }

    static class Statistics {
        private long sum = 0;
        private long min = Long.MAX_VALUE;
        private long max = 0;
        private int count = 0;

        void record(long value) {
            sum += value;
            min = Math.min(min, value);
            max = Math.max(max, value);
            count++;
        }

        double average() { return count > 0 ? (double) sum / count : 0; }
        long min() { return min != Long.MAX_VALUE ? min : 0; }
        long max() { return max; }
        int count() { return count; }
    }
}

// Usage
PerformanceMonitor monitor = new PerformanceMonitor();
UntypedAgent agent = AgenticServices.agentBuilder()
    .chatModel(chatModel)
    .listener(monitor)
    .build();

agent.invoke("Process this");
agent.invoke("Process that");

monitor.printReport();

Pattern 2: Audit Logging

Create detailed audit trails of all agent activity.

class AuditLogger implements AgentListener {
    private final PrintWriter auditLog;

    public AuditLogger(String logFilePath) throws IOException {
        this.auditLog = new PrintWriter(new FileWriter(logFilePath, true));
    }

    @Override
    public void beforeAgentInvocation(AgentRequest request) {
        auditLog.println(String.format(
            "[%s] INVOKE | Agent: %s | Type: %s | Args: %s | MemoryId: %s",
            Instant.now(),
            request.agentName(),
            request.agentType().getSimpleName(),
            request.arguments(),
            request.agenticScope().memoryId()
        ));
        auditLog.flush();
    }

    @Override
    public void afterAgentInvocation(AgentResponse response) {
        auditLog.println(String.format(
            "[%s] COMPLETE | Agent: %s | Duration: %dms | Result: %s",
            Instant.now(),
            response.agentName(),
            response.duration().toMillis(),
            truncate(String.valueOf(response.result()), 100)
        ));
        auditLog.flush();
    }

    @Override
    public void onAgentInvocationError(AgentInvocationError error) {
        auditLog.println(String.format(
            "[%s] ERROR | Agent: %s | Error: %s | StackTrace: %s",
            Instant.now(),
            error.agentName(),
            error.error().getMessage(),
            Arrays.toString(error.error().getStackTrace())
        ));
        auditLog.flush();
    }

    @Override
    public void beforeToolExecution(BeforeToolExecution beforeExec) {
        auditLog.println(String.format(
            "[%s] TOOL_START | Tool: %s | Args: %s",
            Instant.now(),
            beforeExec.toolName(),
            beforeExec.arguments()
        ));
        auditLog.flush();
    }

    @Override
    public void afterToolExecution(ToolExecution toolExec) {
        auditLog.println(String.format(
            "[%s] TOOL_END | Tool: %s | Duration: %dms | Error: %s",
            Instant.now(),
            toolExec.toolName(),
            toolExec.duration().toMillis(),
            toolExec.error() != null ? toolExec.error().getMessage() : "none"
        ));
        auditLog.flush();
    }

    public void close() {
        auditLog.close();
    }

    private String truncate(String text, int maxLength) {
        return text.length() > maxLength ?
            text.substring(0, maxLength) + "..." : text;
    }
}

// Usage
AuditLogger logger = new AuditLogger("/var/log/agents/audit.log");
try {
    UntypedAgent agent = AgenticServices.agentBuilder()
        .chatModel(chatModel)
        .listener(logger)
        .build();

    agent.invoke("Process data");
} finally {
    logger.close();
}

Pattern 3: Distributed Tracing

Integrate with distributed tracing systems.

class DistributedTracingListener implements AgentListener {
    private final Tracer tracer;

    public DistributedTracingListener(Tracer tracer) {
        this.tracer = tracer;
    }

    @Override
    public void beforeAgentInvocation(AgentRequest request) {
        // Start span
        Span span = tracer.buildSpan(request.agentName())
            .withTag("agent.type", request.agentType().getSimpleName())
            .withTag("memory.id", String.valueOf(request.agenticScope().memoryId()))
            .start();

        // Store span in scope for later retrieval
        request.agenticScope().writeState("trace.span", span);

        // Set as active span
        tracer.scopeManager().activate(span);
    }

    @Override
    public void afterAgentInvocation(AgentResponse response) {
        // Retrieve and finish span
        Span span = (Span) response.agenticScope().readState("trace.span");
        if (span != null) {
            span.setTag("agent.duration.ms", response.duration().toMillis());
            span.setTag("agent.success", true);
            span.finish();
        }
    }

    @Override
    public void onAgentInvocationError(AgentInvocationError error) {
        // Mark span as error
        Span span = (Span) error.agenticScope().readState("trace.span");
        if (span != null) {
            span.setTag("error", true);
            span.log(Map.of(
                "error.kind", error.error().getClass().getName(),
                "error.message", error.error().getMessage(),
                "error.stack", Arrays.toString(error.error().getStackTrace())
            ));
            span.finish();
        }
    }

    @Override
    public void beforeToolExecution(BeforeToolExecution beforeExec) {
        // Create child span for tool
        Span parentSpan = (Span) tracer.activeSpan();
        if (parentSpan != null) {
            Span toolSpan = tracer.buildSpan("tool:" + beforeExec.toolName())
                .asChildOf(parentSpan)
                .withTag("tool.name", beforeExec.toolName())
                .start();
            // Store for later
            parentSpan.setTag("current.tool.span", toolSpan);
        }
    }

    @Override
    public void afterToolExecution(ToolExecution toolExec) {
        Span parentSpan = (Span) tracer.activeSpan();
        if (parentSpan != null) {
            Span toolSpan = (Span) parentSpan.getBaggageItem("current.tool.span");
            if (toolSpan != null) {
                toolSpan.setTag("tool.duration.ms", toolExec.duration().toMillis());
                if (toolExec.error() != null) {
                    toolSpan.setTag("error", true);
                    toolSpan.log(Map.of("error.message", toolExec.error().getMessage()));
                }
                toolSpan.finish();
            }
        }
    }

    @Override
    public boolean inheritedBySubagents() {
        return true; // Trace sub-agents too
    }
}

Pattern 4: Real-time Notifications

Send notifications for critical events.

class NotificationListener implements AgentListener {
    private final NotificationService notificationService;

    public NotificationListener(NotificationService service) {
        this.notificationService = service;
    }

    @Override
    public void afterAgentInvocation(AgentResponse response) {
        // Notify on long-running agents
        if (response.duration().toSeconds() > 30) {
            notificationService.send(
                "Long Running Agent",
                String.format(
                    "Agent %s took %d seconds to complete",
                    response.agentName(),
                    response.duration().toSeconds()
                )
            );
        }
    }

    @Override
    public void onAgentInvocationError(AgentInvocationError error) {
        // Notify on errors
        notificationService.send(
            "Agent Error",
            String.format(
                "Agent %s failed: %s",
                error.agentName(),
                error.error().getMessage()
            ),
            NotificationPriority.HIGH
        );
    }

    @Override
    public void beforeAgenticScopeDestroyed(AgenticScope scope) {
        // Notify on completion
        int invocations = scope.agentInvocations().size();
        if (invocations > 20) {
            notificationService.send(
                "High Activity Detected",
                String.format(
                    "Workflow completed with %d agent invocations",
                    invocations
                )
            );
        }
    }
}

Pattern 5: State Validation

Validate state consistency and data integrity.

class StateValidationListener implements AgentListener {
    private final List<String> requiredKeys;

    public StateValidationListener(String... requiredKeys) {
        this.requiredKeys = Arrays.asList(requiredKeys);
    }

    @Override
    public void afterAgentInvocation(AgentResponse response) {
        AgenticScope scope = response.agenticScope();

        // Validate required state
        for (String key : requiredKeys) {
            if (!scope.hasState(key)) {
                System.err.println(String.format(
                    "WARNING: Agent %s did not set required state key: %s",
                    response.agentName(),
                    key
                ));
            }
        }

        // Validate state types
        if (scope.hasState("user_id") && !(scope.readState("user_id") instanceof String)) {
            System.err.println("ERROR: user_id must be a String");
        }

        if (scope.hasState("count") && !(scope.readState("count") instanceof Integer)) {
            System.err.println("ERROR: count must be an Integer");
        }
    }

    @Override
    public void beforeAgenticScopeDestroyed(AgenticScope scope) {
        // Final validation
        Map<String, Object> state = scope.state();

        // Check for null values
        state.forEach((key, value) -> {
            if (value == null) {
                System.err.println("WARNING: Null value for key: " + key);
            }
        });

        // Check for required keys at end
        for (String key : requiredKeys) {
            if (!scope.hasState(key)) {
                System.err.println(String.format(
                    "ERROR: Required key %s not found in final state",
                    key
                ));
            }
        }
    }
}

Best Practices

1. Keep Listeners Lightweight

Listeners should not perform heavy operations that slow down agent execution.

// Good - lightweight logging
class GoodListener implements AgentListener {
    @Override
    public void afterAgentInvocation(AgentResponse response) {
        System.out.println(response.agentName() + " completed");
    }
}

// Avoid - heavy computation in listener
class AvoidListener implements AgentListener {
    @Override
    public void afterAgentInvocation(AgentResponse response) {
        // Don't do this - it blocks agent execution
        performExpensiveAnalysis(response);
        sendToExternalService(response);
    }
}

// Better - offload to async processing
class BetterListener implements AgentListener {
    private final ExecutorService executor = Executors.newSingleThreadExecutor();

    @Override
    public void afterAgentInvocation(AgentResponse response) {
        // Offload to background thread
        executor.submit(() -> {
            performExpensiveAnalysis(response);
            sendToExternalService(response);
        });
    }
}

2. Handle Exceptions in Listeners

Don't let listener exceptions break agent execution.

class SafeListener implements AgentListener {
    @Override
    public void afterAgentInvocation(AgentResponse response) {
        try {
            // Listener logic
            processResponse(response);
        } catch (Exception e) {
            // Log but don't propagate
            System.err.println("Listener error: " + e.getMessage());
            e.printStackTrace();
        }
    }
}

3. Use Inheritance Wisely

Only propagate listeners to sub-agents when necessary.

// Propagate for global monitoring
class GlobalMonitor implements AgentListener {
    @Override
    public boolean inheritedBySubagents() {
        return true; // Monitor all agents
    }
}

// Don't propagate for parent-specific logic
class ParentSpecificListener implements AgentListener {
    @Override
    public boolean inheritedBySubagents() {
        return false; // Only parent agent
    }
}

4. Compose Listeners for Separation of Concerns

Create focused listeners and compose them.

// Each listener has single responsibility
AgentListener logger = new LoggingListener();
AgentListener metrics = new MetricsListener();
AgentListener errors = new ErrorTrackingListener();

// Compose them
AgentListener composed = new ComposedAgentListener(logger, metrics, errors);

UntypedAgent agent = AgenticServices.agentBuilder()
    .chatModel(chatModel)
    .listener(composed)
    .build();

See Also

  • Agent Builder API - Adding listeners to agents
  • Workflows Overview - Monitoring workflow execution
  • State Management API - Accessing state in listeners

Install with Tessl CLI

npx tessl i tessl/maven-dev-langchain4j--langchain4j-agentic@1.11.0

docs

index.md

tile.json