Skip to content

09 - Logging Strategy

This document specifies the logging approach used throughout AgentEnsemble.

Framework

  • SLF4J API (slf4j-api) for the logging facade
  • Users provide their own SLF4J implementation (Logback, Log4j2, etc.)
  • The agentensemble-examples module includes Logback for demonstration

Logger Naming Convention

Each class uses its own SLF4J logger:

private static final Logger log = LoggerFactory.getLogger(ClassName.class);

This follows standard Java logging conventions and allows fine-grained log level control per class.

Log Levels by Component

Ensemble.run()

Logger: net.agentensemble.Ensemble

INFO  : "Ensemble run started | Workflow: {workflow} | Tasks: {taskCount} | Agents: {agentCount}"
DEBUG : "Input variables: {inputs}"
DEBUG : "Task list: [{task1Description}, {task2Description}, ...]"
DEBUG : "Agent list: [{agent1Role}, {agent2Role}, ...]"
INFO  : "Ensemble run completed | Duration: {totalDuration} | Tasks: {taskCount} | Tool calls: {totalToolCalls}"
WARN  : "Agent '{role}' is registered but not assigned to any task"
ERROR : "Ensemble run failed: {exceptionMessage}"

SequentialWorkflowExecutor

Logger: net.agentensemble.workflow.SequentialWorkflowExecutor

INFO  : "Task {index}/{total} starting | Description: {truncated80} | Agent: {role}"
INFO  : "Task {index}/{total} completed | Duration: {duration} | Tool calls: {toolCallCount}"
DEBUG : "Task {index}/{total} context: {contextCount} prior outputs"
DEBUG : "Task {index}/{total} output preview: {truncated200}"
ERROR : "Task {index}/{total} failed: {exceptionMessage}"

AgentExecutor

Logger: net.agentensemble.agent.AgentExecutor

INFO  : "Agent '{role}' executing task | Tools: {toolCount}"
INFO  : "Tool call: {toolName}({truncatedInput200}) -> {truncatedOutput200} [{durationMs}ms]"
DEBUG : "System prompt ({charCount} chars):\n{systemPrompt}"
DEBUG : "User prompt ({charCount} chars):\n{userPrompt}"
DEBUG : "Agent '{role}' completed | Tool calls: {count} | Duration: {duration}"
WARN  : "Tool error: {toolName}({truncatedInput200}) -> {errorMessage}"
WARN  : "Agent '{role}' returned empty response for task '{truncatedDescription}'"
WARN  : "Agent '{role}' exceeded max iterations ({max}). Stop message sent ({stopCount}/3)."
WARN  : "Context from task '{description}' is {length} characters (>10000). Consider breaking into smaller tasks."
TRACE : "Full LLM response:\n{fullResponse}"

AgentPromptBuilder

Logger: net.agentensemble.agent.AgentPromptBuilder

DEBUG : "Built system prompt ({charCount} chars) for agent '{role}'"
DEBUG : "Built user prompt ({charCount} chars) for task '{truncatedDescription}'"

TemplateResolver

Logger: net.agentensemble.config.TemplateResolver

DEBUG : "Resolving template ({charCount} chars) with {inputCount} input variables"
DEBUG : "Resolved {variableCount} variables in template"

LangChain4jToolAdapter

Logger: net.agentensemble.tool.LangChain4jToolAdapter

DEBUG : "Adapted AgentTool '{name}' to LangChain4j ToolSpecification"
WARN  : "AgentTool '{name}' threw exception during execution: {message}"

Verbose Mode

When verbose mode is active (ensemble.verbose = true OR agent.verbose = true), certain log statements are elevated from DEBUG/TRACE to INFO level:

Normal Level Elevated To Content
DEBUG INFO System prompt text
DEBUG INFO User prompt text
TRACE INFO Full LLM response text
DEBUG INFO Task output preview

Resolution: effectiveVerbose = ensemble.verbose || task.agent().verbose()

Implementation Pattern

if (effectiveVerbose) {
    log.info("System prompt:\n{}", systemPrompt);
} else {
    log.debug("System prompt ({} chars):\n{}", systemPrompt.length(), systemPrompt);
}

Verbose mode is intended for development and debugging. In production, users should rely on standard log level configuration.

MDC (Mapped Diagnostic Context)

MDC values are set at the start of each task execution and cleared after completion (including on failure via try/finally).

MDC Keys

Key Value Example
ensemble.id UUID generated per run() call "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
task.index Current task position "2/5"
agent.role Current agent's role "Senior Research Analyst"

Usage in Log Patterns

Users can include MDC values in their logging configuration:

Logback example:

<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level [%X{ensemble.id:-}] [%X{task.index:-}] [%X{agent.role:-}] %logger{36} - %msg%n</pattern>

Example output:

14:23:01.456 [main] INFO  [a1b2c3d4] [1/3] [Researcher] i.a.w.SequentialWorkflowExecutor - Task 1/3 starting | Description: Research AI trends | Agent: Researcher
14:23:05.789 [main] INFO  [a1b2c3d4] [1/3] [Researcher] i.a.a.AgentExecutor - Tool call: web_search(AI agent frameworks 2026) -> Top results: 1. LangChain... [1234ms]
14:23:12.321 [main] INFO  [a1b2c3d4] [1/3] [Researcher] i.a.w.SequentialWorkflowExecutor - Task 1/3 completed | Duration: PT10.865S | Tool calls: 2

MDC Lifecycle

ensemble.run():
  MDC.put("ensemble.id", UUID.randomUUID().toString())
  try:
    for each task:
      MDC.put("task.index", ...)
      MDC.put("agent.role", ...)
      try:
        execute task
      finally:
        MDC.remove("task.index")
        MDC.remove("agent.role")
  finally:
    MDC.remove("ensemble.id")

Truncation Constants

To keep logs readable, certain values are truncated:

Context Default Configurable? Source
Task description in MDC 80 chars No MDC_DESCRIPTION_MAX_LENGTH constant
Tool input/output in INFO/WARN logs 200 chars YestoolLogTruncateLength Ensemble.builder() / RunOptions
Output preview in verbose logs 200 chars YestoolLogTruncateLength Ensemble.builder() / RunOptions
Template in error messages 100 chars No ERROR_TEMPLATE_MAX_LENGTH constant

Truncation appends "..." when text is cut.

Tool output truncation

There are two independent truncation knobs for tool output:

  • toolLogTruncateLength — controls what appears in log statements. -1 means full output; 0 suppresses content entirely. Does not affect what the LLM sees.
  • maxToolOutputLength — controls what the LLM sees in its message history. -1 (default) means no truncation. When positive, a "... [truncated, full length: N chars]" note is appended so the model knows output was cut. The full result is always stored in the trace and fired to listeners.

Both can be set on Ensemble.builder() (applies to every run() call) or overridden per-run via RunOptions:

// Full LLM context, terse logs
Ensemble.builder()
    .maxToolOutputLength(-1)       // LLM: unlimited (default)
    .toolLogTruncateLength(500)    // logs: first 500 chars
    .build();

// Ensemble caps LLM at 2000 chars, but a specific run needs full log output for debugging
ensemble.run(RunOptions.builder()
    .toolLogTruncateLength(-1)
    .build());

JsonTraceExporter

Logger: net.agentensemble.trace.export.JsonTraceExporter

DEBUG : "Execution trace written to {path}"
WARN  : "Failed to export execution trace to {path}: {message}"

CaptureMode

Logger: net.agentensemble.trace.CaptureMode

INFO  : "CaptureMode active: {STANDARD|FULL}"     (logged in Ensemble.runWithInputs)
DEBUG : "CaptureMode activated via system property agentensemble.captureMode={value}"
DEBUG : "CaptureMode activated via environment variable AGENTENSEMBLE_CAPTURE_MODE={value}"
DEBUG : "CaptureMode.FULL: auto-registering JsonTraceExporter at ./traces/"
WARN  : "Unrecognised CaptureMode value '{value}' in {source} -- ignoring. Valid values: OFF, STANDARD, FULL"

No Logging Dependencies

The agentensemble-core module depends only on slf4j-api (the facade). It does NOT include any SLF4J implementation. Users must provide their own:

  • Logback: ch.qos.logback:logback-classic
  • Log4j2: org.apache.logging.log4j:log4j-slf4j2-impl
  • JUL bridge: org.slf4j:slf4j-jdk14
  • Simple: org.slf4j:slf4j-simple (for quick testing)

If no implementation is provided, SLF4J outputs a warning and discards all log messages.