diff --git a/apps/web/src/components/agent-sessions/agent-sessions-list.tsx b/apps/web/src/components/agent-sessions/agent-sessions-list.tsx
index 28fbaceff..77d96dac5 100644
--- a/apps/web/src/components/agent-sessions/agent-sessions-list.tsx
+++ b/apps/web/src/components/agent-sessions/agent-sessions-list.tsx
@@ -4,6 +4,7 @@ import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "@m
import { formatRelativeTimeOrDate, toEpochMs } from "@maple/ui/lib/time-format"
import { formatSessionDuration } from "@maple/ui/lib/replay-format"
import { ChatBubbleSparkleIcon } from "@/components/icons"
+import { vendorIcon } from "@/lib/agent-sessions/vendor-icon"
import { vendorLabel } from "@/lib/agent-sessions/vendor-label"
/** The wire row from `listAiSessions` — one AI agent session, newest first. */
@@ -58,6 +59,7 @@ export function AgentSessionsList({ sessions, limit }: AgentSessionsListProps) {
{sessions.map((session) => {
const hasErrors = session.errorSpanCount > 0
+ const VendorIcon = vendorIcon(session.vendorId)
const vendor = vendorLabel(session.vendorId)
const secondary =
session.vendorVersion && session.vendorVersion !== "0"
@@ -95,7 +97,10 @@ export function AgentSessionsList({ sessions, limit }: AgentSessionsListProps) {
{formatRelativeTimeOrDate(session.startTime)}
-
diff --git a/apps/web/src/components/icons/claude.tsx b/apps/web/src/components/icons/claude.tsx
new file mode 100644
index 000000000..5f03fa9a6
--- /dev/null
+++ b/apps/web/src/components/icons/claude.tsx
@@ -0,0 +1,20 @@
+import type { IconProps } from "./icon"
+
+/** The Claude spark mark — Claude-branded agent frameworks. */
+function ClaudeIcon({ size = 24, className, ...props }: IconProps) {
+ return (
+
+ )
+}
+export { ClaudeIcon }
diff --git a/apps/web/src/components/icons/crewai.tsx b/apps/web/src/components/icons/crewai.tsx
new file mode 100644
index 000000000..c8717deb1
--- /dev/null
+++ b/apps/web/src/components/icons/crewai.tsx
@@ -0,0 +1,20 @@
+import type { IconProps } from "./icon"
+
+/** The CrewAI mark. */
+function CrewAiIcon({ size = 24, className, ...props }: IconProps) {
+ return (
+
+ )
+}
+export { CrewAiIcon }
diff --git a/apps/web/src/components/icons/effect.tsx b/apps/web/src/components/icons/effect.tsx
new file mode 100644
index 000000000..17c704f9f
--- /dev/null
+++ b/apps/web/src/components/icons/effect.tsx
@@ -0,0 +1,20 @@
+import type { IconProps } from "./icon"
+
+/** The Effect mark, for Effect AI. */
+function EffectIcon({ size = 24, className, ...props }: IconProps) {
+ return (
+
+ )
+}
+export { EffectIcon }
diff --git a/apps/web/src/components/icons/haystack.tsx b/apps/web/src/components/icons/haystack.tsx
new file mode 100644
index 000000000..69417320c
--- /dev/null
+++ b/apps/web/src/components/icons/haystack.tsx
@@ -0,0 +1,20 @@
+import type { IconProps } from "./icon"
+
+/** The Haystack mark. */
+function HaystackIcon({ size = 24, className, ...props }: IconProps) {
+ return (
+
+ )
+}
+export { HaystackIcon }
diff --git a/apps/web/src/components/icons/huggingface.tsx b/apps/web/src/components/icons/huggingface.tsx
new file mode 100644
index 000000000..43cf20ed2
--- /dev/null
+++ b/apps/web/src/components/icons/huggingface.tsx
@@ -0,0 +1,20 @@
+import type { IconProps } from "./icon"
+
+/** The Hugging Face mark, for smolagents. */
+function HuggingFaceIcon({ size = 24, className, ...props }: IconProps) {
+ return (
+
+ )
+}
+export { HuggingFaceIcon }
diff --git a/apps/web/src/components/icons/index.ts b/apps/web/src/components/icons/index.ts
index a4aa9e638..5085cd809 100644
--- a/apps/web/src/components/icons/index.ts
+++ b/apps/web/src/components/icons/index.ts
@@ -73,8 +73,10 @@ export { ChartBarTrendUpIcon } from "./chart-bar-trend-up"
export { ChartLineIcon } from "./chart-line"
export { CirclePercentageIcon } from "./circle-percentage"
export { CircleQuestionIcon } from "./circle-question"
+export { ClaudeIcon } from "./claude"
export { ClickhouseIcon } from "./clickhouse"
export { CloudflareIcon, CloudflareMonoIcon } from "./cloudflare"
+export { CrewAiIcon } from "./crewai"
export { ChromeIcon } from "./chrome"
export { CompactLinesIcon } from "./compact-lines"
export { CornerDownLeftIcon } from "./corner-down-left"
@@ -83,6 +85,7 @@ export { CursorPointerIcon } from "./cursor-pointer"
export { CreditCardIcon } from "./credit-card"
export { CubeIcon } from "./cube"
export { DiscordIcon } from "./discord"
+export { EffectIcon } from "./effect"
export { DotsVerticalIcon } from "./dots-vertical"
export { DownloadIcon } from "./download"
export { EdgeIcon } from "./edge"
@@ -96,6 +99,8 @@ export { FolderIcon } from "./folder"
export { GearIcon } from "./gear"
export { GithubIcon } from "./github"
export { GoogleIcon } from "./google"
+export { HaystackIcon } from "./haystack"
+export { HuggingFaceIcon } from "./huggingface"
export { GridIcon } from "./grid"
export { GridSquareCirclePlusIcon } from "./grid-square-circle-plus"
export { GripDotsIcon } from "./grip-dots"
@@ -107,6 +112,7 @@ export { HouseIcon } from "./house"
export { IdBadgeIcon } from "./id-badge"
export { ImageIcon } from "./image"
export { KafkaIcon } from "./kafka"
+export { LangchainIcon } from "./langchain"
export { KeyIcon } from "./key"
export { KeyboardIcon } from "./keyboard"
export { KubernetesIcon } from "./kubernetes"
@@ -118,18 +124,21 @@ export { LogoutIcon } from "./logout"
export { MaximizeIcon } from "./maximize"
export { MediaPauseIcon } from "./media-pause"
export { MediaPlayIcon } from "./media-play"
+export { MicrosoftIcon } from "./microsoft"
export { MinimizeIcon } from "./minimize"
export { MongodbIcon } from "./mongodb"
export { MoonIcon } from "./moon"
export { MysqlIcon } from "./mysql"
export { NatsIcon } from "./nats"
export { NodejsIcon } from "./nodejs"
+export { OpenAiIcon } from "./openai"
export { OpenjdkIcon } from "./openjdk"
export { OperaIcon } from "./opera"
export { PaletteIcon } from "./palette"
export { PaperPlaneIcon } from "./paper-plane"
export { PencilIcon } from "./pencil"
export { PlanetScaleIcon } from "./planetscale"
+export { PydanticIcon } from "./pydantic"
// Nucleo Pixel Essential — a deliberately separate family, used where a row
// needs a type marker rather than an action affordance (the session replay
// event rail). Don't mix them with the outline icons inside one control.
@@ -151,6 +160,7 @@ export { SafariIcon } from "./safari"
export { ServerIcon } from "./server"
export { ShieldIcon } from "./shield"
export { SlackIcon, SlackMonoIcon } from "./slack"
+export { SpringIcon } from "./spring"
export { SlidersIcon } from "./sliders"
export { SpinnerIcon } from "./spinner"
export { SquareIcon } from "./square"
@@ -160,6 +170,7 @@ export { SunIcon } from "./sun"
export { TelegramIcon, TelegramMonoIcon } from "./telegram"
export { TranscriptIcon } from "./transcript"
export { TruckIcon } from "./truck"
+export { VercelIcon } from "./vercel"
export { WarpStreamIcon } from "./warpstream"
export { UploadIcon } from "./upload"
export { WorkflowRingIcon, WORKFLOW_LABEL, WORKFLOW_COLOR } from "./workflow-ring"
diff --git a/apps/web/src/components/icons/langchain.tsx b/apps/web/src/components/icons/langchain.tsx
new file mode 100644
index 000000000..9473b3eeb
--- /dev/null
+++ b/apps/web/src/components/icons/langchain.tsx
@@ -0,0 +1,20 @@
+import type { IconProps } from "./icon"
+
+/** The LangChain chain-link mark. */
+function LangchainIcon({ size = 24, className, ...props }: IconProps) {
+ return (
+
+ )
+}
+export { LangchainIcon }
diff --git a/apps/web/src/components/icons/microsoft.tsx b/apps/web/src/components/icons/microsoft.tsx
new file mode 100644
index 000000000..89851817f
--- /dev/null
+++ b/apps/web/src/components/icons/microsoft.tsx
@@ -0,0 +1,20 @@
+import type { IconProps } from "./icon"
+
+/** The Microsoft four-square mark, flattened to one colour. */
+function MicrosoftIcon({ size = 24, className, ...props }: IconProps) {
+ return (
+
+ )
+}
+export { MicrosoftIcon }
diff --git a/apps/web/src/components/icons/openai.tsx b/apps/web/src/components/icons/openai.tsx
new file mode 100644
index 000000000..ebbafba8a
--- /dev/null
+++ b/apps/web/src/components/icons/openai.tsx
@@ -0,0 +1,19 @@
+import type { IconProps } from "./icon"
+
+function OpenAiIcon({ size = 24, className, ...props }: IconProps) {
+ return (
+
+ )
+}
+export { OpenAiIcon }
diff --git a/apps/web/src/components/icons/pydantic.tsx b/apps/web/src/components/icons/pydantic.tsx
new file mode 100644
index 000000000..761213a5a
--- /dev/null
+++ b/apps/web/src/components/icons/pydantic.tsx
@@ -0,0 +1,20 @@
+import type { IconProps } from "./icon"
+
+/** The Pydantic mark, for Pydantic AI. */
+function PydanticIcon({ size = 24, className, ...props }: IconProps) {
+ return (
+
+ )
+}
+export { PydanticIcon }
diff --git a/apps/web/src/components/icons/spring.tsx b/apps/web/src/components/icons/spring.tsx
new file mode 100644
index 000000000..bf3a43753
--- /dev/null
+++ b/apps/web/src/components/icons/spring.tsx
@@ -0,0 +1,20 @@
+import type { IconProps } from "./icon"
+
+/** The Spring mark, for Spring AI. */
+function SpringIcon({ size = 24, className, ...props }: IconProps) {
+ return (
+
+ )
+}
+export { SpringIcon }
diff --git a/apps/web/src/components/icons/vercel.tsx b/apps/web/src/components/icons/vercel.tsx
new file mode 100644
index 000000000..d411d0646
--- /dev/null
+++ b/apps/web/src/components/icons/vercel.tsx
@@ -0,0 +1,19 @@
+import type { IconProps } from "./icon"
+
+function VercelIcon({ size = 24, className, ...props }: IconProps) {
+ return (
+
+ )
+}
+export { VercelIcon }
diff --git a/apps/web/src/lab/agent-session-fixture.ts b/apps/web/src/lab/agent-session-fixture.ts
index 44ad9990f..fec60dcdd 100644
--- a/apps/web/src/lab/agent-session-fixture.ts
+++ b/apps/web/src/lab/agent-session-fixture.ts
@@ -256,12 +256,16 @@ function toolResult(tool: string): string | undefined {
}
}
+/** The gateway stamps a vendor on every AI span, so the fixture carries one too
+ * — it is what puts a framework mark beside the label in the header and rows. */
+const VENDOR_ID = "claude_agent_sdk"
+
export function buildAgentSessionFixture(): readonly AiSessionSpan[] {
const base = buildBaseTurns()
const baseEndMs = Math.max(...base.map((span) => spanStartMs(span) + span.durationMs)) - T0
- return [...base, ...buildTranscriptTurns(baseEndMs + 2 * MINUTE)].sort(
- (a, b) => spanStartMs(a) - spanStartMs(b),
- )
+ return [...base, ...buildTranscriptTurns(baseEndMs + 2 * MINUTE)]
+ .sort((a, b) => spanStartMs(a) - spanStartMs(b))
+ .map((span) => ({ ...span, vendorId: VENDOR_ID }))
}
/**
diff --git a/apps/web/src/lib/agent-sessions/vendor-icon.test.ts b/apps/web/src/lib/agent-sessions/vendor-icon.test.ts
new file mode 100644
index 000000000..c35c1e6bb
--- /dev/null
+++ b/apps/web/src/lib/agent-sessions/vendor-icon.test.ts
@@ -0,0 +1,20 @@
+import { describe, expect, it } from "vitest"
+
+import { ChatBubbleSparkleIcon, ClaudeIcon, LangchainIcon, OpenAiIcon } from "@/components/icons"
+import { vendorIcon } from "./vendor-icon"
+
+describe("vendorIcon", () => {
+ it("gives a recognised framework its brand mark", () => {
+ expect(vendorIcon("claude_agent_sdk")).toBe(ClaudeIcon)
+ expect(vendorIcon("langchain")).toBe(LangchainIcon)
+ // Two ids, one framework: the OpenInference instrumentation is still OpenAI.
+ expect(vendorIcon("openai_agents_sdk")).toBe(OpenAiIcon)
+ expect(vendorIcon("openinference-openai")).toBe(OpenAiIcon)
+ })
+
+ it("falls back for frameworks without a mark, and for no vendor at all", () => {
+ expect(vendorIcon("litellm")).toBe(ChatBubbleSparkleIcon)
+ expect(vendorIcon("unknown:genai")).toBe(ChatBubbleSparkleIcon)
+ expect(vendorIcon("")).toBe(ChatBubbleSparkleIcon)
+ })
+})
diff --git a/apps/web/src/lib/agent-sessions/vendor-icon.ts b/apps/web/src/lib/agent-sessions/vendor-icon.ts
new file mode 100644
index 000000000..eb621fc67
--- /dev/null
+++ b/apps/web/src/lib/agent-sessions/vendor-icon.ts
@@ -0,0 +1,50 @@
+import { MapleMark } from "@maple/ui/components/icons"
+
+import {
+ ChatBubbleSparkleIcon,
+ ClaudeIcon,
+ CrewAiIcon,
+ EffectIcon,
+ GoogleIcon,
+ HaystackIcon,
+ HuggingFaceIcon,
+ type IconComponent,
+ LangchainIcon,
+ MicrosoftIcon,
+ OpenAiIcon,
+ PydanticIcon,
+ SpringIcon,
+ VercelIcon,
+} from "@/components/icons"
+
+/**
+ * Brand marks for the vendor ids the ingest gateway stamps. A framework is
+ * listed only when its logo is one we can draw faithfully — the rest fall back
+ * to the generic mark on purpose. An approximated logo names the wrong company;
+ * the fallback merely declines to name one, and `vendorLabel` says it in words
+ * either way.
+ */
+const VENDOR_ICONS: Record
= {
+ claude_agent_sdk: ClaudeIcon,
+ crewai: CrewAiIcon,
+ effect_ai: EffectIcon,
+ // eve is Vercel's agent framework and has no mark of its own.
+ eve: VercelIcon,
+ google_adk: GoogleIcon,
+ haystack: HaystackIcon,
+ langchain: LangchainIcon,
+ maple: MapleMark,
+ microsoft_agent_framework: MicrosoftIcon,
+ openai_agents_sdk: OpenAiIcon,
+ "openinference-openai": OpenAiIcon,
+ pydantic_ai: PydanticIcon,
+ semantic_kernel: MicrosoftIcon,
+ smolagents: HuggingFaceIcon,
+ spring_ai: SpringIcon,
+ vercel_ai_sdk: VercelIcon,
+} satisfies Record
+
+/** The mark that belongs beside `vendorLabel(vendorId)`. */
+export function vendorIcon(vendorId: string): IconComponent {
+ return VENDOR_ICONS[vendorId] ?? ChatBubbleSparkleIcon
+}
diff --git a/apps/web/src/routes/agent-sessions/$sessionId.tsx b/apps/web/src/routes/agent-sessions/$sessionId.tsx
index 4f7337d25..26b6e7fd3 100644
--- a/apps/web/src/routes/agent-sessions/$sessionId.tsx
+++ b/apps/web/src/routes/agent-sessions/$sessionId.tsx
@@ -29,6 +29,7 @@ import {
} from "@/lib/agent-sessions/session-window"
import { buildSessionSummary, type SessionSummary } from "@/lib/agent-sessions/session-summary"
import { buildSessionTurns } from "@/lib/agent-sessions/session-turns"
+import { vendorIcon } from "@/lib/agent-sessions/vendor-icon"
import { vendorLabel } from "@/lib/agent-sessions/vendor-label"
import { Result, useAtomValue } from "@/lib/effect-atom"
import { displayError } from "@/lib/error-messages"
@@ -225,6 +226,8 @@ function SessionDetailBody({
// Message content is opt-in and off by default, so most sessions have no
// opening user message to title the page with.
const title = summary.title ?? breadcrumbSessionId(sessionId)
+ // The framework that emitted the session, as its mark — the subtitle names it.
+ const VendorIcon = vendorIcon(summary.vendorIds[0] ?? "")
return (
@@ -233,6 +236,7 @@ function SessionDetailBody({
+
{summary.title === undefined ? (
// The id fallback title copies the full session id.