Skip to content

feat(ai): add agent hooks and OpenTelemetry tracing - #1536

Open
chaojixinren wants to merge 1 commit into
apache:aifrom
chaojixinren:feat/hook-system
Open

feat(ai): add agent hooks and OpenTelemetry tracing#1536
chaojixinren wants to merge 1 commit into
apache:aifrom
chaojixinren:feat/hook-system

Conversation

@chaojixinren

Copy link
Copy Markdown

Summary

Introduces a config-driven, telemetry-agnostic agent hooks system in ai/, with
logging and OpenTelemetry (OTLP) tracing as the first observers. It instruments
the full ReAct lifecycle — interaction, iteration, stage, model call, and tool call —
without coupling the strategy code to any specific backend.

Motivation

Resolves #1525. There was previously no consistent way to observe agent execution or trace
a request end-to-end across the Agent ↔ MCP boundary, making agent behavior hard to debug
and spans impossible to correlate.

What changed

  • Hook manager (ai/component/hooks/): immutable lifecycle events
    (interaction / iteration / stage / model_call / tool_call × start / end), read-only
    State snapshots, per-event registration, and panic isolation per hook.
  • Context derivation: hooks may derive a context.Context for nested work; exactly one
    DerivesContext registration is accepted (a Go context carries one span lineage — fan out
    via a Collector instead). Contexts returned by plain observational hooks are ignored.
  • Tracing hook: OTLP exporter (gRPC or HTTP/protobuf) with GenAI semantic attributes
    (gen_ai.operation.name, gen_ai.request.model, gen_ai.provider.name,
    gen_ai.conversation.id, gen_ai.tool.name, gen_ai.tool.call.id,
    gen_ai.input/output.messages, gen_ai.usage.*, agent.fallback.*, error.type, …).
    Content is serialized lazily only when the span IsRecording(), and content capture is
    opt-in (capture_content: none default; truncated ≤ 4096 bytes; full).
  • Logging hook: structured lifecycle logging with the same opt-in content capture.
  • Trace propagation: W3C traceparent / tracestate / baggage are honored inbound,
    propagated to MCP HTTP calls, and the active trace ID is returned on SSE responses via
    X-Trace-ID (CORS-exposed).
  • Fallback metadata: timeout vs parse-error are distinguished (FallbackReason), written
    to both the model-call span and the stage span, with correct Evidence text; tool failures
    are recorded as error.type + agent.degraded without faking a gen_ai.tool.call.result.
  • Cancellation semantics: context.Canceled propagates cleanly (only DeadlineExceeded
    is a timeout); SSE disconnect cancellation stays detached from the running interaction.
  • Configuration: type: hooks component with logging / tracing blocks, JSON schema
    validation, and standard OTel env vars for endpoint and credentials.

Design constraints

  • Hooks are observational: they read state and may derive context, but must never mutate
    Agent execution data.
  • Content capture defaults to none for credential/PII safety; payloads are not serialized
    on the hot path unless a matching hook opts in.
  • Tool-call hooks must explicitly select tool names ("*" for all).

Agent Hooks + OTel Tracing — Completed Test Checklist

1. Unit Tests

  • go test -count=1 ./... — all passed (hooks, Agent, server engine, MCP tools, runtime, etc.)

2. Integration Tests

  • go test -tags=integration -count=1 ./... — all passed

3. Race / Static Analysis

  • go test -race ./component/hooks/... ./component/agent/... — passed
  • go vet ./... — passed

4. E2E: Jaeger (OTLP)

  • Local Jaeger OTLP end-to-end passed
  • Trace ID: f6477cd5b8d5a5cce9ae07a0ad1d8470

5. E2E: Langfuse (Docker 4.11.0)

  • Full stack via official Compose; OTLP/HTTP ingestion succeeded
  • v2 Observations API: HTTP 200
  • Correct hierarchy: AGENT invoke_agentGENERATION chat qwen-maxTOOL lookup_service
  • Confirmed in ClickHouse events_full: model input/output, 7/5/12 tokens, session ID, tool call ID
  • 3 observations written in total
  • Trace ID: fdc809bacdcdbf5b7cd7798ba8f7c1cb

6. Performance Benchmarks (0 allocs)

Benchmark Result
BenchmarkDisabledHookFastPath 2.967 ns/op · 0 allocs
BenchmarkEmptyManagerFastPath 8.943 ns/op · 0 allocs
BenchmarkHookContentDisabled 34.23 ns/op · 0 allocs
BenchmarkHookContentLoggingOnly 34.68 ns/op · 0 allocs

7. Cleanup

  • Temporary test files removed
  • Jaeger / Langfuse containers and dedicated Docker network removed
  • No code or commit changes; HEAD remains 7bbcab1

⚠️ Environment Gaps (out of scope for this issue — not completed)

  • Hosted Langfuse cloud E2E — requires credentials
  • External DashScope E2E — requires credentials; TestMultiTurnConversation therefore shows 0/14
  • External Milvus E2E — requires credentials

Introduce immutable lifecycle observations for interactions, iterations, stages, model calls, and tool calls while allowing hooks to derive context for nested work. Preserve detached Agent cancellation semantics, propagate inbound W3C trace context through Agent and MCP calls, and expose trace IDs on SSE responses.

Add configurable OTLP tracing with GenAI semantic attributes, opt-in content capture, logging hooks, graceful interaction draining, CORS trace headers, and regression coverage for lifecycle ordering, panic isolation, fallback metadata, concurrency, and content immutability.

Constraint: Hooks may observe state and derive context but must not mutate Agent execution data.

Constraint: SSE disconnect cancellation must remain detached from the running interaction.

Rejected: Pass mutable Agent objects to hooks | violates the observational hook contract.

Confidence: high

Scope-risk: moderate

Directive: Keep tracing content capture opt-in and register content demand on the tracing hook, not the logging hook.

Tested: go test -count=1 ./...; integration-tag full suite; hooks and Agent race tests; go vet; local Jaeger OTLP E2E; local Langfuse 4.11.0 OTLP/HTTP ingestion and v4 Observations verification.

Not-tested: Hosted Langfuse cloud and external DashScope/Milvus E2E, which are outside this issue and require credentials.
@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant