Skip to content

feat(agent-sessions): filter by model, agent, tool, environment, id, errors, cost and usage, and sort - #738

Merged
JeremyFunk merged 1 commit into
mainfrom
feat/agent-sessions-filters
Sep 3, 2026
Merged

feat(agent-sessions): filter by model, agent, tool, environment, id, errors, cost and usage, and sort#738
JeremyFunk merged 1 commit into
mainfrom
feat/agent-sessions-filters

Conversation

@JeremyFunk

@JeremyFunk JeremyFunk commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

What

The Agent Sessions list could be sliced by framework and by service, and by nothing else. This adds the filters a reader reaches for first, and a sort.

Counted facets (sidebar, with session counts): environment, model, agent, tool — alongside framework and service, all now multi-select.

Session-level filters: id search (session id or trace id, prefix match — paste from a ticket or from the row itself), with-errors chip, session length, cost, tokens, LLM calls, tool calls, and "hide single-trace sessions" for orgs whose framework exposes no session key.

Sort: newest / oldest / longest / most expensive / most tokens / most errors / most LLM calls / most tool calls.

List row gains a model lane, N calls · N tools in the activity lane (traces/spans move to its tooltip), and a tokens/cost lane.

How

Built on #741's two-stage list, the way that PR's "interaction" note asked for: every measure the sidebar filters or sorts on is materialized into ai_trace_index, so stage 1 (aiSessionPageQuery, index-only, whole window) filters, ranks and pages on them, and stage 2 stays the page-bounded fan-out it is on main.

  • Migration 0026 adds twelve columns to ai_trace_index and recreates ai_trace_index_mv: the facet dimensions DeploymentEnv, Model, AgentName, ToolName, and the per-span measures IsLlmCall, IsToolCall, IsError, Tokens, Cost, plus SpanId/ParentSpanId/Duration. All are facts of the GenAI span, coalesced and classified at insert by the new packages/domain/src/tinybird/gen-ai-columns.ts (the dialect key lists, classifyAiSpan/isLlmCall and spanTokenBuckets transcribed to SQL), which the read side compiles from too. Nothing is backfilled; pre-0026 rows read ''/0 and drop out of the new facets and sums. Local store: v15 → v16.
  • Counted filters join perf(agent-sessions): rank the page on the index, then aggregate only that page #741's per-trace existence tests on the index level of both stages (HAVING countIf(Model IN (…)) > 0 …), so each selects exactly the population its facet counted and dimensions on different spans of one trace (model on the chat span, tool on the tool span, id on the turn span) combine. Facets grow from two UNION ALL branches to six, still index-only.
  • Session-level filters and sorts are HAVING / ORDER BY on stage 1's ranked row, over per-session sums of the index measures — no fan-out, so a 30-day window sorted by cost costs what perf(agent-sessions): rank the page on the index, then aggregate only that page #741's page costs. Two documented approximations: "with errors" means a failed agent span (the row's badge still counts every span), and the duration filter reads the agent-span extent (the true extent trails by minutes at most, per perf(agent-sessions): rank the page on the index, then aggregate only that page #741's measurement).
  • Usage (totalTokens, cost) is summed with the detail page's deepest-reporter rule, one level deep: a wrapper span that rolls up its children's usage keeps only its excess. A raw ClickHouse lambda over the trace's (SpanId, ParentSpanId, Tokens, Cost) reporters, capped at 2 000 per trace.
  • Stage 1's measures (models, agents, calls, usage) ride along onto the response row; the handler merges them with stage 2's facts by session id.
  • RangeFilterSection learns count and usd units.
  • The request schema bounds every new field (non-negative ranges, integer counts, a max-length search, a sort-key enum) so a bad value is a 400.

Verification

  • Real-ClickHouse e2e (ai-trace-index-materialization.clickhouse.e2e.test.ts, on top of perf(agent-sessions): rank the page on the index, then aggregate only that page #741's two-stage scenarios): every new MV column per dialect, the deepest-reporter arithmetic on a roll-up (150 tokens where a naive sum reads 300), every filter including cross-span combinations, the sorts, and the facet counts. The SQL catalog analyzer sweep passes with the new shapes.
  • Unit: query text, alias lists pinned to the integrations layer's own tables, HTTP boundary (passthrough + 400s), filter-input mapping, sidebar/toolbar rendering, range-unit round-trips, migration ordering.
  • Not verified in a browser: the local Tinybird workspace predates ai_trace_index entirely, so the page cannot render against it without rebuilding that workspace.

Deploy — manual Tinybird step required

tinybird-cd.yml is disabled (one failed run, Feb 2026; its GitHub environments do not exist), so nothing in CI applies datasource or materialized-view changes to production. After merge, from the merged checkout, for each long-lived workspace (staging first, then production and the us-east-1 mirror):

bun run --cwd apps/api tinybird:deploy

Then confirm GET /v0/datasources shows the twelve new ai_trace_index columns and ai_trace_index_mv carries the new SELECT. The SDK's in-place schema evolution has failed on a column add before (PR #452), which is why staging goes first. Until this runs, the Agent Sessions page fails with UNKNOWN_IDENTIFIER on the new columns — the read side has no fallback to the old schema.

Not in this PR

Turn counts (the client's three-rule turn detection has no faithful SQL form), failure-kind facets, prompt/workflow version, user id, free-text search over messages.

@JeremyFunk

Copy link
Copy Markdown
Collaborator Author

Rebased onto main after #741 (two-stage list). Rather than a full-scan fallback, the session-level filters and sorts now run on stage 1: migration 0025 materializes the per-span measures (IsLlmCall, IsToolCall, IsError, Tokens, Cost, SpanId/ParentSpanId/Duration) into ai_trace_index alongside the facet dimensions, so aiSessionPageQuery filters, ranks and pages on them index-only and stage 2 is unchanged. Description updated with the two approximations that buys (agent-span errors and extent).

…errors, cost and usage, and sort

The list could be sliced by framework and service and nothing else. This adds
the dimensions a reader reaches for first, and a sort.

Index (migration 0026): DeploymentEnv, Model, AgentName, ToolName, and the
per-span measures IsLlmCall, IsToolCall, IsError, Tokens, Cost (with
SpanId/ParentSpanId/Duration) on ai_trace_index — every one a fact of the
GenAI span, coalesced across dialects and classified at insert by the new
gen-ai-columns module the read side compiles the same expressions from.
Nothing is backfilled; pre-0026 rows read ''/0 and drop out of the new facets
and sums. Local store: v15 -> v16.

Query, on the two-stage list: the counted filters are per-trace existence
tests on the index level of both stages, so a model on the chat span and a
tool on the tool span combine. The session-level filters (errors, duration,
cost, tokens, LLM/tool calls, hide single-trace sessions) and the sort are
HAVING/ORDER BY on the page's ranked row over sums of the index measures —
no fan-out. Usage is summed with the detail page's deepest-reporter rule so a
wrapper's roll-up is not double counted. Id search is a prefix match on
either id column.

Web: multi-select facets, range sections (with count/usd units), a search
box, an errors chip, a sort menu, and model / calls / usage lanes on the row.
@JeremyFunk
JeremyFunk force-pushed the feat/agent-sessions-filters branch from a987844 to 2973afb Compare September 3, 2026 01:39
@JeremyFunk

Copy link
Copy Markdown
Collaborator Author

Rebased again onto main: #749 took migration 0025 and local schema v15, so this PR's migration is now 0026 and its local-store edge v15 → v16. No other changes.

@JeremyFunk
JeremyFunk merged commit ab9a8d1 into main Sep 3, 2026
42 checks passed
@JeremyFunk
JeremyFunk deleted the feat/agent-sessions-filters branch September 3, 2026 09:39
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown

🍁 Maple PR preview

Warning

Preview cleanup could not be confirmed. The Alchemy teardown outcome was skipped.

Final commit 2973afb · View workflow run

Makisuo added a commit that referenced this pull request Sep 3, 2026
Main landed 0026_ai_trace_index_filter_columns and local schema v16 (#738).
The error-events attribute fallback moves to ClickHouse migration 0027 and
the local chDB edge to v16 -> v17; generated artifacts regenerated on the
merged tree.
Makisuo added a commit that referenced this pull request Sep 3, 2026
Third version collision in a row. Main landed #738 (agent-session filter
columns), taking migration 0026 and local schema v16 — the slots this branch
moved into yesterday. As with the previous two, nothing semantic conflicts:
two unrelated changes each took the next free number.

- `0026_product_events_from_traces.ts` -> `0027_...`, `version: 26` -> `27`,
  export and every doc reference renamed, docs/product-events-funnels.md
  included.
- `migrations/index.ts` and its test carry 0024, 0025, 0026 and 0027.
  `clickHouseSchemaVersion` stays "21" — all four are
  `requiredForIngest: false`.
- Local edge re-derived via `local-schema:bump` for v17; the v16->v17 module
  body is the previous one unchanged apart from version plumbing.
- Restored the same four local-store-migration test assertions that taking
  main's copy of that file reverts on every one of these merges.

Schema is 39 tables / 42 MVs, local schema v17, 81 objects.

Verified on the merged tree: `bun typecheck` 41/41, `bun run lint` clean,
ClickHouse schema / local-manifest / Tinybird gates up to date, apps/api 2585
passed, apps/web 2382, packages/domain 703, query-engine 1373, apps/cli 534.
`bun.lock` is byte-identical to main's.

Note for the next merge: main's #750 adds a new `@maple/safe-fetch` workspace,
so a worktree installed before the merge needs `bun install` again afterwards —
without it, `@maple/scraper` typecheck and one ScrapeTargetsService lint rule
fail for missing types rather than for anything in the diff.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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