Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
a0b8ea5
feat: add agent tracing lifecycle contract
Aug 11, 2026
484f7b8
fix: require span ID for linked remote traces
Aug 11, 2026
7c9c767
feat: export AppKit agent traces to MLflow UC
Aug 12, 2026
bda2f9f
fix: isolate and release MLflow UC traces
Aug 12, 2026
605e1d4
fix: buffer split MLflow UC export batches
Aug 12, 2026
4112f5a
fix: resolve MLflow UC exporter ancestry
Aug 12, 2026
e7b0596
feat: trace every AppKit model step
Aug 12, 2026
0a0573f
fix: harden AppKit model lifecycle tracing
Aug 12, 2026
78ecc8e
fix: bound cancelled model stream draining
Aug 12, 2026
c91bb5c
feat: trace every AppKit agent invocation
Aug 12, 2026
53ee534
fix: trace early AppKit agent failures
Aug 12, 2026
7dd8161
feat: trace AppKit tools approvals and memory
Aug 12, 2026
8767e07
fix: complete AppKit tracing semantic attributes
Aug 12, 2026
29b377c
feat: trace retrieval and local sub-agents
Aug 12, 2026
fe802e1
fix: harden agent and retriever tracing
Aug 12, 2026
61963cd
feat: propagate agent trace context
Aug 12, 2026
9eba2f2
fix: inject trace context after SDK auth
Aug 12, 2026
dd10b0f
feat: provision and surface MLflow UC tracing
Aug 12, 2026
bad5a7b
fix: address MLflow UC tracing review
Aug 12, 2026
126a6a8
test: gate AppKit agent trace coverage
Aug 13, 2026
b01b729
test: close AppKit trace conformance gaps
Aug 13, 2026
efc8c2b
fix: harden generated trace conformance proof
Aug 13, 2026
dab18fe
fix: strengthen deployed trace conformance
Aug 13, 2026
4149277
fix: harden trace conformance lifecycle
Aug 13, 2026
c65d4a6
fix: close trace parity and shutdown races
Aug 13, 2026
9df0b9f
fix: close final tracing conformance gaps
Aug 13, 2026
9c6f1c5
docs: refresh createApp return type
Aug 13, 2026
c220ed0
Merge remote-tracking branch 'origin/main' into feat/mlflow-uc-agent-…
Aug 13, 2026
c29bee4
fix: bound tool-call validation time
Aug 13, 2026
6f14e55
fix: harden MLflow UC tracing integration
Aug 13, 2026
616ef33
fix: address AppKit AI review findings
Aug 13, 2026
967dc70
fix: bound MLflow export pressure and preserve diagnostics
Aug 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions apps/dev-playground/client/src/routes/agent.route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,8 @@ function AgentRoute() {
const [input, setInput] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [threadId, setThreadId] = useState<string | null>(null);
const [mlflowTraceId, setMlflowTraceId] = useState<string | null>(null);
const [mlflowTraceUrl, setMlflowTraceUrl] = useState<string | null>(null);
const [agent, setAgent] = useState<string>(AGENT_OPTIONS[0].value);
const [pendingApprovals, setPendingApprovals] = useState<PendingApproval[]>(
[],
Expand Down Expand Up @@ -217,6 +219,8 @@ function AgentRoute() {
{ id: ++msgIdCounter.current, role: "user", content: userMessage },
]);
setEvents([]);
setMlflowTraceId(null);
setMlflowTraceUrl(null);
setIsLoading(true);

try {
Expand Down Expand Up @@ -287,6 +291,12 @@ function AgentRoute() {
if (event.type === "appkit.metadata" && event.data?.threadId) {
setThreadId(event.data.threadId as string);
}
if (event.type === "appkit.metadata") {
const traceId = event.data?.traceId;
const traceUrl = event.data?.traceUrl;
if (typeof traceId === "string") setMlflowTraceId(traceId);
if (typeof traceUrl === "string") setMlflowTraceUrl(traceUrl);
}

if (event.type === "response.output_text.delta" && event.delta) {
assistantContent += event.delta;
Expand Down Expand Up @@ -469,6 +479,23 @@ function AgentRoute() {
</div>

<div className="border-t p-4">
{mlflowTraceId && (
<div className="mb-3 flex items-center justify-between gap-3 rounded-md bg-muted px-3 py-2 text-xs">
<code className="truncate" title={mlflowTraceId}>
{mlflowTraceId}
</code>
{mlflowTraceUrl && (
<a
href={mlflowTraceUrl}
target="_blank"
rel="noreferrer"
className="shrink-0 font-medium text-primary hover:underline"
>
Open trace in MLflow
</a>
)}
</div>
)}
{hasAutocomplete && (suggestion || isAutocompleting) && (
<div className="flex items-center gap-2 mb-2 text-xs text-muted-foreground">
{isAutocompleting && (
Expand Down
29 changes: 29 additions & 0 deletions apps/dev-playground/client/src/routes/smart-dashboard.route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ function SmartDashboardRoute() {
);
const [lastAction, setLastAction] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [mlflowTraceId, setMlflowTraceId] = useState<string | null>(null);
const [mlflowTraceUrl, setMlflowTraceUrl] = useState<string | null>(null);

// Multi-turn chat history. Messages accumulate across sends so the user
// can scroll back through the conversation rather than having the UI
Expand Down Expand Up @@ -177,6 +179,13 @@ function SmartDashboardRoute() {
(event: SSEEvent) => {
handleDispatcherEvent(event);

if (event.type === "appkit.metadata") {
const traceId = event.data?.traceId;
const traceUrl = event.data?.traceUrl;
if (typeof traceId === "string") setMlflowTraceId(traceId);
if (typeof traceUrl === "string") setMlflowTraceUrl(traceUrl);
}

// Capture pending approvals and pin them to the user turn that
// triggered them so the ChatDrawer can render the card inline.
if (event.type === "appkit.approval_pending") {
Expand Down Expand Up @@ -250,6 +259,8 @@ function SmartDashboardRoute() {

const dispatchToAgent = useCallback(
(message: string) => {
setMlflowTraceId(null);
setMlflowTraceUrl(null);
const userMsgId = nextMessageId();
const assistantMsgId = nextMessageId();
lastUserMessageIdRef.current = userMsgId;
Expand Down Expand Up @@ -479,6 +490,24 @@ function SmartDashboardRoute() {
<QuickActionsBar onSend={dispatchToAgent} disabled={agentLoading} />
</div>

{mlflowTraceId && (
<div className="mb-4 flex items-center justify-between gap-3 rounded-lg border bg-card px-3 py-2 text-xs">
<code className="truncate" title={mlflowTraceId}>
{mlflowTraceId}
</code>
{mlflowTraceUrl && (
<a
href={mlflowTraceUrl}
target="_blank"
rel="noreferrer"
className="shrink-0 font-medium text-primary hover:underline"
>
Open trace in MLflow
</a>
)}
</div>
)}

{(error || dataError) && (
<div className="mb-4 rounded-lg border border-red-500/40 bg-red-500/5 p-3 text-xs text-red-700 dark:text-red-400">
<div className="flex items-start justify-between gap-3">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { context, trace } from "@opentelemetry/api";
import { AsyncLocalStorageContextManager } from "@opentelemetry/context-async-hooks";
import {
BasicTracerProvider,
InMemorySpanExporter,
SimpleSpanProcessor,
} from "@opentelemetry/sdk-trace-base";
import type { AgentAdapter } from "shared";
import { z } from "zod";
import { createAgent } from "../../../../packages/appkit/src/core/agent/create-agent";
import { runAgent } from "../../../../packages/appkit/src/core/agent/run-agent";
import { tool } from "../../../../packages/appkit/src/core/agent/tools/tool";

export interface SmartDashboardSpanFixture {
spanId: string;
parentSpanId?: string;
traceId: string;
spanType: string;
agentName?: string;
toolName?: string;
}

export interface SmartDashboardWireEvent {
type: string;
data?: { threadId: string; traceId: string; traceUrl?: string };
delta?: string;
response?: Record<string, never>;
}

export interface SmartDashboardTracingFixture {
traceId: string;
spans: SmartDashboardSpanFixture[];
events: SmartDashboardWireEvent[];
}

export async function runSmartDashboardTracingFixture(options?: {
includeTraceUrl?: boolean;
}): Promise<SmartDashboardTracingFixture> {
context.disable();
trace.disable();
context.setGlobalContextManager(
new AsyncLocalStorageContextManager().enable(),
);

const exporter = new InMemorySpanExporter();
const provider = new BasicTracerProvider({
spanProcessors: [new SimpleSpanProcessor(exporter)],
});
trace.setGlobalTracerProvider(provider);

try {
const filterByDateRange = tool({
name: "filter_by_date_range",
description: "Filter the dashboard to a date range",
schema: z.object({ start: z.string(), end: z.string() }),
execute: async ({ start, end }) => `Filtered ${start} through ${end}.`,
});
const pilotAdapter: AgentAdapter = {
async *run(_input, runContext) {
const result = await runContext.executeTool("filter_by_date_range", {
start: "2016-11-01",
end: "2016-11-30",
});
yield { type: "message_delta", content: String(result) };
},
};
const queryAdapter: AgentAdapter = {
async *run(_input, runContext) {
const result = await runContext.executeTool("agent-dashboard_pilot", {
input: "Show November 2016",
});
yield { type: "message_delta", content: String(result) };
},
};
const query = createAgent({
name: "query",
instructions: "Delegate dashboard changes to the dashboard pilot.",
model: queryAdapter,
agents: {
dashboard_pilot: createAgent({
name: "dashboard_pilot",
instructions: "Manipulate the Smart Dashboard.",
model: pilotAdapter,
tools: { filter_by_date_range: filterByDateRange },
}),
},
});

const result = await runAgent(query, {
appName: "dev-playground",
messages: "Show November 2016",
requestId: "smart-dashboard-request",
sessionId: "smart-dashboard-session",
threadId: "smart-dashboard-thread",
userId: "fixture-user",
});
await provider.forceFlush();

const spans = exporter
.getFinishedSpans()
.filter((span) => {
const spanType = span.attributes["mlflow.spanType"];
return spanType === "AGENT" || spanType === "TOOL";
})
.map((span) => ({
spanId: span.spanContext().spanId,
...(span.parentSpanContext?.spanId
? { parentSpanId: span.parentSpanContext.spanId }
: {}),
traceId: span.spanContext().traceId,
spanType: String(span.attributes["mlflow.spanType"]),
...(typeof span.attributes["appkit.agent.name"] === "string"
? { agentName: span.attributes["appkit.agent.name"] }
: {}),
...(typeof span.attributes["appkit.tool.name"] === "string"
? { toolName: span.attributes["appkit.tool.name"] }
: {}),
}));
const traceUrl = options?.includeTraceUrl
? `https://example.cloud.databricks.com/ml/experiments/123456789/traces?selectedTraceId=${encodeURIComponent(result.traceId)}`
: undefined;

return {
traceId: result.traceId,
spans,
events: [
{
type: "appkit.metadata",
data: {
threadId: "smart-dashboard-thread",
traceId: result.traceId,
...(traceUrl ? { traceUrl } : {}),
},
},
{
type: "response.output_text.delta",
delta: "Applied the November filter.",
},
{ type: "response.completed", response: {} },
],
};
} finally {
await provider.shutdown();
trace.disable();
context.disable();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { describe, expect, test } from "vitest";
import { runSmartDashboardTracingFixture } from "./smart-dashboard-agent-tracing.fixture";

describe("Smart Dashboard semantic tracing fixture", () => {
test("emits the exact query → delegation → pilot → action tree", async () => {
const observed = await runSmartDashboardTracingFixture();
const root = observed.spans.find(
(span) => span.spanType === "AGENT" && span.agentName === "query",
);
const delegation = observed.spans.find(
(span) =>
span.spanType === "TOOL" && span.toolName === "agent-dashboard_pilot",
);
const pilot = observed.spans.find(
(span) =>
span.spanType === "AGENT" && span.agentName === "dashboard_pilot",
);
const action = observed.spans.find(
(span) =>
span.spanType === "TOOL" && span.toolName === "filter_by_date_range",
);

expect(
observed.spans.filter(
(span) => span.spanType === "AGENT" && span.parentSpanId === undefined,
),
).toHaveLength(1);
expect(root).toBeDefined();
expect(delegation?.parentSpanId).toBe(root?.spanId);
expect(pilot?.parentSpanId).toBe(delegation?.spanId);
expect(action?.parentSpanId).toBe(pilot?.spanId);
expect(new Set(observed.spans.map((span) => span.traceId))).toEqual(
new Set([observed.traceId]),
);
expect(observed.events[0]).toMatchObject({
type: "appkit.metadata",
data: { traceId: observed.traceId },
});
});
});
68 changes: 68 additions & 0 deletions apps/dev-playground/tests/agent-tracing.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { expect, test } from "@playwright/test";

const traceId = `trace:/main.agent_traces.appkit/${"a".repeat(32)}`;
const traceUrl = `https://example.cloud.databricks.com/ml/experiments/123456789/traces?selectedTraceId=${encodeURIComponent(traceId)}`;

test("agent invocation surfaces its V4 trace identity and direct MLflow link", async ({
page,
}) => {
await page.route("**/api/agents/chat", async (route) => {
const body = [
{
type: "appkit.metadata",
data: { threadId: "thread-1", traceId, traceUrl },
},
{ type: "response.output_text.delta", delta: "Traced answer" },
{ type: "response.completed", response: {} },
]
.map((event) => `data: ${JSON.stringify(event)}\n\n`)
.join("");
await route.fulfill({
status: 200,
headers: { "Content-Type": "text/event-stream" },
body,
});
});

await page.goto("/agent");
await page.getByPlaceholder("Ask a question...").fill("Trace this request");
await page.getByRole("button", { name: "Send" }).click();

await expect(
page.getByRole("paragraph").filter({ hasText: "Traced answer" }),
).toBeVisible();
await expect(page.getByText(traceId)).toBeVisible();
const link = page.getByRole("link", { name: "Open trace in MLflow" });
await expect(link).toHaveAttribute("href", traceUrl);
});

test("agent invocation surfaces its trace ID without a workspace link", async ({
page,
}) => {
await page.route("**/api/agents/chat", async (route) => {
const body = [
{
type: "appkit.metadata",
data: { threadId: "thread-2", traceId },
},
{ type: "response.output_text.delta", delta: "Unlinked trace" },
{ type: "response.completed", response: {} },
]
.map((event) => `data: ${JSON.stringify(event)}\n\n`)
.join("");
await route.fulfill({
status: 200,
headers: { "Content-Type": "text/event-stream" },
body,
});
});

await page.goto("/agent");
await page.getByPlaceholder("Ask a question...").fill("Trace without a URL");
await page.getByRole("button", { name: "Send" }).click();

await expect(page.getByText(traceId)).toBeVisible();
await expect(
page.getByRole("link", { name: "Open trace in MLflow" }),
).toHaveCount(0);
});
Loading