Count Claude Code tool calls in the AI Governance tool calls card and tile

The tool calls card and the summary tile now count Claude Code tool calls, and the agent filter can classify them. Before, the card skipped them: it did not read their event name, and they keep the tool name under a different key. It builds on the tool calls card (Add the tool calls card, ClickHouse reader and ... (!256578 - merged)), now merged. This is for #630971+s.

Changes

  • The ClickHouse reader accepts ai_claude_tool_invoked events from the Claude Compliance API ingest.
  • The tool name is read from details['tool_name'], then from details['name']. Events with neither are still dropped.
  • With the ai_governance_sessions_api flag off, the DAP and Connected filters use the card's rule: an event with a workflow is classified by that workflow's agent type, and an event with a session and no workflow counts as Connected.
  • With the flag on, an event with a session is classified by the session's own source and agent type: a Duo session without an agent type is DAP, anything else is Connected, and deleted sessions are left out. An event without a session falls back to its workflow. This keeps the filters right once Duo events move to governance sessions. Two concrete differences from the flag-off rule: when an event names a workflow and a session that disagree, the session wins, so DAP plus Connected still equals All; and an event whose session was deleted is not counted.
  • The summary tile counts the same events with the same tool-name keys, and classifies DAP and Connected through governance sessions behind the same flag, so the card and the tile agree.
  • glab posts ai_tool_invoked with details['tool_name'] (gitlab-org/cli, internal/commands/govern/audit/sync/sync.go), so its calls count like Duo's.
  • glab events are session-only since Update the external agent session API to use ai... (!257427 - merged) merged, and the Claude Compliance API ingest is expected to write the same shape. Both rules count them as Connected. Specs cover the session-only case for the card and the tile under both flag states.

ClickHouse only.

Out of scope

ClickHouse review

The DAP and Connected filters add one IN (subquery) to the deduplicated event scan. With the flag off, only the workflow subquery runs; with it on, an event with a session uses the session subquery and an event without one uses the workflow subquery.

Workflow subquery (flag off, and events without a session)
workflow_id IN (
  SELECT id FROM (
    SELECT id,
      argMax(agent_type, _siphon_replicated_at) AS agent_type,
      argMax(_siphon_deleted, _siphon_replicated_at) AS deleted
    FROM siphon_duo_workflows_workflows
    WHERE startsWith(traversal_path, {traversal_path:String})
      AND created_at < {to:DateTime64(6, 'UTC')}
    GROUP BY traversal_path, created_at, id
  )
  WHERE deleted = false
    AND agent_type IS NULL          -- DAP; `IS NOT NULL` for Connected
)
Session subquery (flag on, events with a session)
ai_governance_session_id IN (
  SELECT id FROM (
    SELECT id,
      argMax(source, _siphon_replicated_at) AS source,
      argMax(agent_type, _siphon_replicated_at) AS agent_type,
      argMax(_siphon_deleted, _siphon_replicated_at) AS deleted
    FROM siphon_ai_governance_sessions
    WHERE startsWith(traversal_path, {traversal_path:String})
      AND session_started_at < {to:DateTime64(6, 'UTC')}
    GROUP BY traversal_path, session_started_at, id
  )
  WHERE deleted = false
    AND (source = 0 AND coalesce(agent_type, '') = '')      -- DAP; `NOT (...)` for Connected
)

With the flag off, the card's rule applies: the workflow subquery, plus OR (workflow_id = 0 AND ai_governance_session_id != 0) for Connected. Combined, with the flag on: (ai_governance_session_id != 0 AND <session subquery>) OR (ai_governance_session_id = 0 AND <workflow subquery>).

EXPLAIN indexes=1 for the session subquery (GDK ClickHouse, 269 session rows)
ReadFromMergeTree (siphon_ai_governance_sessions)
  Prewhere filter column: startsWith(traversal_path, '<prefix>') AND session_started_at < '2026-10-05 00:00:00'
  Indexes:
    PrimaryKey
      Keys: traversal_path, session_started_at
      Condition: and((session_started_at in (-Inf, '1791158400')), (traversal_path in ['<prefix>', '<prefix-next>')))
      Parts: 2/2, Granules: 2/2

Both primary key columns prune, then the GROUP BY on the full key (traversal_path, session_started_at, id) collapses the ReplacingMergeTree versions. There is no lower bound on session_started_at on purpose: a session can start long before the events it emits into the window. The workflow subquery has the same shape and was reviewed with Add the tool calls card, ClickHouse reader and ... (!256578 - merged) • Dheeraj Joshi • 19.5.

Testing

  • ClickHouse tool calls service and tool calls service specs (card and dispatcher).
  • ClickHouse metrics service and metrics service specs (tile and dispatcher).
  • Local ClickHouse runs hit the shared memory cap in the test hooks, so CI is the check.

Screenshots

No frontend change in this MR. The card and the filter look the same; the card lists more tools once Claude Code events exist.

Edited by Dheeraj Joshi

Merge request reports

Loading
Loading