Files
tracer-cloud--opensre/docs/investigation-pipeline-architecture.md
T
wehub-resource-sync 4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

8.6 KiB

Investigation pipeline architecture

Contributor guide to how a single investigation runs end-to-end: the six-stage pipeline, the ReAct evidence-gathering loop, and the guardrails that keep it bounded. Companion to investigation-tool-calling.md, which covers tool schema / LLM invoke payload mechanics specifically — this doc covers the pipeline and loop control flow around that.

Where code lives

Concern Location
Stage ordering tools/investigation/lifecycle.py (run_connected_investigation)
Public runner entrypoint tools/investigation/capability.py
Integration discovery tools/investigation/stages/resolve_integrations/node.py
Alert classification/extraction tools/investigation/stages/intake/node.py
Pre-loop tool planning tools/investigation/stages/plan_evidence/node.py, core/domain/alerts/tool_planning.py
ReAct loop (the agent) tools/investigation/stages/gather_evidence/{agent,loop,tools,prompt}.py
Diagnosis parsing tools/investigation/stages/diagnose/node.py, core/domain/diagnosis
Report delivery tools/investigation/reporting/
Shared state contract core/state/ (AgentState, InvestigationState, EvidenceEntry)
Context budget enforcement core/context_budget.py

Pipeline overview

Each stage is a pure function — (state) -> dict of updates, merged into a shared AgentState via apply_state_updates. A stage exception is reported to Sentry and then re-raised; the pipeline never silently swallows a failure.

flowchart TD
    A[raw_alert] --> B[resolve_integrations]
    B --> C[extract_alert]
    C -->|is_noise = true| Z[Stop — no investigation]
    C -->|is_noise = false| D[plan_actions]
    D --> E["ReAct loop\n(ConnectedInvestigationAgent.run)"]
    E --> F[diagnose]
    F --> G[deliver]
    G --> H[Slack / GitLab / report.md]

Stage by stage

1. resolve_integrations — what tools exist

Looks up which vendor integrations (Datadog, Grafana, EKS, …) this org has connected and credentialed. Not alert-specific — establishes the universe of tools everything downstream can draw from.

2. extract_alert — is this worth investigating

One LLM call classifies the raw alert: noise (chat, greetings, replies in an existing thread) short-circuits the pipeline immediately with no tools run. A real alert gets structured fields extracted — alert_name, severity, alert_source, namespace, error message — plus a computed incident_window.

3. plan_actions — what to check first

Scores every available tool against the alert (score_tools, source match + tool metadata) and keeps the top tool_budget (default 10) as planned_actions, with a written rationale. Advisory: if nothing scores confidently, the loop falls back to its own relevance ranking instead of an empty plan.

4. The ReAct loop — the core evidence-gathering agent

ConnectedInvestigationAgent.run() in tools/investigation/stages/gather_evidence/agent.py. Before the model's first turn: the tool set is narrowed to a hard cap (select_investigation_tools, MAX_AGENT_TOOL_SCHEMAS = 32) using the plan from stage 3 if present, otherwise alert-source relevance ranking. A handful of "obviously needed" tools may fire as deterministic seed calls before the LLM gets a turn at all, so the loop starts with free evidence already in hand.

flowchart TD
    S0["Select ≤32 relevant tools\n(select_investigation_tools)"] --> S1[Build system prompt + alert context]
    S1 --> S2{Seed calls for\nthis alert_source?}
    S2 -->|yes| S3["Execute seed tool calls\nrecord as evidence"]
    S2 -->|no| S4
    S3 --> S4["Loop: iteration 0..19\n(MAX_INVESTIGATION_LOOPS)"]
    S4 --> S5["Enforce context budget\n(evict/truncate low-value evidence)"]
    S5 --> S6[llm.invoke]
    S6 --> S7{Tool calls\nreturned?}
    S7 -->|no| S8{Accept\nconclusion?}
    S8 -->|yes| S9[Done — exit loop]
    S8 -->|no: nudge| S4
    S7 -->|yes| S10{Identical to a\nprior call?}
    S10 -->|yes| S11["Replay cached result,\ntell LLM not to repeat"]
    S10 -->|no| S12[Execute tool, record evidence]
    S11 --> S13{Any fresh calls\nthis iteration?}
    S12 --> S13
    S13 -->|yes| S4
    S13 -->|no| S15["Send stagnation nudge\n(stagnant_iterations += 1)"]
    S15 --> S16{stagnant_iterations\n>= 2?}
    S16 -->|yes| S14["Strip tool access;\nloop back for one\nfinal llm.invoke"]
    S16 -->|no| S4
    S14 --> S4

Guardrails inside the loop:

  • Duplicate detection (InvestigationToolCallCache) — identical tool name + args is served from cache instead of re-executed, and the LLM is told explicitly it already has that result.
  • Stagnation breaker — any iteration where every tool call was a replayed duplicate (no fresh evidence) appends a nudge telling the model to stop repeating itself and try something different. After MAX_STAGNANT_ITERATIONS = 2 such iterations in a row (two nudges), tool access is stripped on the next turn to force a text-only conclusion rather than burning the rest of the loop budget.
  • CLI-backed models (Codex, Claude Code CLI) use a subclass, CLIBackedInvestigationAgent, that overrides conclusion acceptance to refuse an early stop until every planned tool has been called — these models tend to write a final answer as soon as they see some results.
  • LLM invoke failures degrade to a partial "investigation failed" state (degraded_investigation_from_llm_failure) instead of crashing, preserving whatever evidence was already gathered.

5. diagnose — structure the conclusion

The loop's final free-text answer is unstructured. A separate LLM call (structured output) parses it into root_cause, root_cause_category, causal_chain, validated_claims / non_validated_claims, remediation_steps, and a validity_score, with a legacy regex-based fallback (parse_root_cause) if structured parsing fails.

6. deliver — publish it

Formats and ships the report to the destinations configured in state — Slack, GitLab writeback, local report.md, etc. See tools/investigation/reporting/.

Guardrails at a glance

Guardrail Constant Defined in Purpose
Tool schema cap MAX_AGENT_TOOL_SCHEMAS = 32 tools/investigation/stages/gather_evidence/tools.py Bounds per-turn schema payload regardless of registry size.
Secondary tool reserve MAX_SECONDARY_FALLBACK_TOOLS = 3 tools/investigation/stages/gather_evidence/tools.py Guarantees cheap reasoning/knowledge tools survive the cap.
Loop iteration cap MAX_INVESTIGATION_LOOPS = 20 config/constants/investigation.py Worst-case runtime bound for the ReAct loop.
Stagnation breaker MAX_STAGNANT_ITERATIONS = 2 tools/investigation/stages/gather_evidence/tools.py Stops the loop from spinning on duplicate-only iterations.
Context budget context_budget_ceiling_for_model() core/context_budget.py Evicts/truncates lowest-value evidence before the model's context limit.
Pre-loop plan size tool_budget (default 10) tools/investigation/stages/plan_evidence/node.py Shortlist size the plan hands the loop before it even starts.
  • investigation-tool-calling.md — tool schema / LLM invoke payload mechanics, per provider.
  • AGENTS.md — "Changing the investigation pipeline" entry point and checklist for making changes here.