diff --git a/python/packages/ag-ui/agent_framework_ag_ui/_agent_run.py b/python/packages/ag-ui/agent_framework_ag_ui/_agent_run.py
index 67d99ae6252..3d005bbc391 100644
--- a/python/packages/ag-ui/agent_framework_ag_ui/_agent_run.py
+++ b/python/packages/ag-ui/agent_framework_ag_ui/_agent_run.py
@@ -10,6 +10,7 @@
import uuid
from collections import OrderedDict
from collections.abc import AsyncIterable, Awaitable, Mapping
+from functools import partial
from typing import TYPE_CHECKING, Any, TypedDict, cast
from ag_ui.core import (
@@ -48,6 +49,9 @@
)
from agent_framework._types import ResponseStream
from agent_framework.exceptions import AgentInvalidResponseException
+from agent_framework.observability import (
+ _use_telemetry_conversation_id, # pyright: ignore[reportPrivateUsage]
+)
from ._approval_state import _APPROVAL_SCOPE_INPUT_KEY, InMemoryAGUIApprovalStateStore, approval_state_thread_id
from ._message_adapters import normalize_agui_input_messages
@@ -63,6 +67,7 @@
_extract_resume_payload, # type: ignore
_extract_tool_result_display, # type: ignore
_has_only_tool_calls, # type: ignore
+ _iterate_with_context, # type: ignore
_normalize_resume_interrupts, # type: ignore
_reconstruct_messages_from_thread_snapshot, # type: ignore
_resume_contract_error, # type: ignore
@@ -1815,8 +1820,10 @@ async def run_agent_stream(
AG-UI events
"""
# Parse IDs
- thread_id = input_data.get("thread_id") or input_data.get("threadId") or str(uuid.uuid4())
- run_id = input_data.get("run_id") or input_data.get("runId") or str(uuid.uuid4())
+ supplied_thread_id = input_data.get("thread_id") or input_data.get("threadId")
+ supplied_run_id = input_data.get("run_id") or input_data.get("runId")
+ thread_id = supplied_thread_id or str(uuid.uuid4())
+ run_id = supplied_run_id or str(uuid.uuid4())
snapshot_scope = cast(str | None, input_data.get(_SNAPSHOT_SCOPE_INPUT_KEY))
approval_scope = cast(str | None, input_data.get(_APPROVAL_SCOPE_INPUT_KEY))
approval_thread_id = approval_state_thread_id(scope=approval_scope, thread_id=thread_id)
@@ -1959,7 +1966,6 @@ async def run_agent_stream(
# Create session (with service session support)
if config.use_service_session:
- supplied_thread_id = input_data.get("thread_id") or input_data.get("threadId")
session = AgentSession(session_id=thread_id, service_session_id=supplied_thread_id)
else:
session = AgentSession(session_id=thread_id)
@@ -2062,23 +2068,33 @@ async def run_agent_stream(
# Stream from agent - emit RunStarted after first update to get service IDs
run_started_emitted = False
+ provider_thread_id: str | None = None
all_updates: list[Any] = [] # Collect for structured output processing
latest_state_snapshot: dict[str, Any] | None = (
cast(dict[str, Any], make_json_safe(flow.current_state)) if flow.current_state else None
)
- response_stream = agent.run(messages, stream=True, **run_kwargs)
- stream = await _normalize_response_stream(response_stream)
- async for update in stream:
+ # Agent middleware can defer the inner run until streaming begins, so the
+ # telemetry override must cover construction, stream resolution, and every pull.
+ telemetry_conversation_id = str(supplied_thread_id) if supplied_thread_id is not None else None
+ telemetry_context = partial(_use_telemetry_conversation_id, telemetry_conversation_id)
+ with telemetry_context():
+ response_stream = agent.run(messages, stream=True, **run_kwargs)
+ stream = await _normalize_response_stream(response_stream)
+
+ async for update in _iterate_with_context(stream, telemetry_context):
# Collect updates for structured output processing
if response_format is not None:
all_updates.append(update)
- # Update IDs from service response on first update and emit RunStarted
+ # Use service-generated IDs only when the AG-UI request omitted them. Client-supplied
+ # IDs remain authoritative for lifecycle correlation and thread-scoped persistence.
if not run_started_emitted:
conv_id = get_conversation_id_from_update(update)
if conv_id:
+ provider_thread_id = conv_id
+ if supplied_thread_id is None and conv_id:
thread_id = conv_id
- if update.response_id:
+ if supplied_run_id is None and update.response_id:
run_id = update.response_id
# NOW emit RunStarted with proper IDs
yield RunStartedEvent(run_id=run_id, thread_id=thread_id)
@@ -2118,7 +2134,10 @@ async def run_agent_stream(
if content_type == "function_approval_request" and pending_approvals is not None:
if content.id and content.function_call and content.function_call.name:
canonical_interrupt_id = content.function_call.call_id or content.id
- provider_approval_thread_id = approval_state_thread_id(scope=approval_scope, thread_id=thread_id)
+ provider_approval_thread_id = approval_state_thread_id(
+ scope=approval_scope,
+ thread_id=provider_thread_id or thread_id,
+ )
_register_pending_approval(
pending_approvals,
[approval_thread_id, provider_approval_thread_id],
diff --git a/python/packages/ag-ui/agent_framework_ag_ui/_run_common.py b/python/packages/ag-ui/agent_framework_ag_ui/_run_common.py
index e59ea0941f4..ace78176a1f 100644
--- a/python/packages/ag-ui/agent_framework_ag_ui/_run_common.py
+++ b/python/packages/ag-ui/agent_framework_ag_ui/_run_common.py
@@ -7,9 +7,10 @@
import copy
import json
import logging
-from collections.abc import Mapping
+from collections.abc import AsyncGenerator, AsyncIterable, Callable, Mapping
+from contextlib import AbstractContextManager
from dataclasses import dataclass, field
-from typing import Any, cast
+from typing import Any, TypeVar, cast
from ag_ui.core import (
BaseEvent,
@@ -32,7 +33,7 @@
ToolCallResultEvent,
ToolCallStartEvent,
)
-from agent_framework import Content
+from agent_framework import Content, ResponseStream
from ._orchestration._predictive_state import PredictiveStateHandler
from ._state import TOOL_RESULT_DISPLAY_KEY, TOOL_RESULT_STATE_KEY
@@ -40,10 +41,33 @@
logger = logging.getLogger(__name__)
+_StreamItemT = TypeVar("_StreamItemT")
+
# Sentinel for an unset display_result; distinguishes "caller didn't pass" from None/{}/"".
_UNSET = object()
+async def _iterate_with_context(
+ stream: AsyncIterable[_StreamItemT],
+ context_factory: Callable[[], AbstractContextManager[Any]],
+) -> AsyncGenerator[_StreamItemT]:
+ """Advance a response stream with a fresh execution context for every pull."""
+ if isinstance(stream, ResponseStream):
+ stream.with_pull_context_manager(context_factory)
+ async for item in stream:
+ yield item
+ return
+
+ stream_iterator = aiter(stream)
+ while True:
+ with context_factory():
+ try:
+ item = await anext(stream_iterator)
+ except StopAsyncIteration:
+ return
+ yield item
+
+
def _has_only_tool_calls(contents: list[Any]) -> bool:
"""Check if contents have only tool calls (no text)."""
has_tool_call = any(getattr(c, "type", None) == "function_call" for c in contents)
diff --git a/python/packages/ag-ui/agent_framework_ag_ui/_workflow_run.py b/python/packages/ag-ui/agent_framework_ag_ui/_workflow_run.py
index 3f6e76e5fd3..bd3089775ea 100644
--- a/python/packages/ag-ui/agent_framework_ag_ui/_workflow_run.py
+++ b/python/packages/ag-ui/agent_framework_ag_ui/_workflow_run.py
@@ -9,6 +9,7 @@
import logging
import uuid
from collections.abc import AsyncGenerator
+from functools import partial
from typing import Any, cast, get_args, get_origin
from ag_ui.core import (
@@ -25,6 +26,9 @@
ToolCallStartEvent,
)
from agent_framework import AgentResponse, AgentResponseUpdate, Content, Message, Workflow, WorkflowRunState
+from agent_framework.observability import (
+ _use_telemetry_conversation_id, # pyright: ignore[reportPrivateUsage]
+)
from ._message_adapters import normalize_agui_input_messages
from ._run_common import (
@@ -33,6 +37,7 @@
_close_reasoning_block,
_emit_content,
_extract_resume_payload,
+ _iterate_with_context,
_normalize_resume_interrupts,
_resume_contract_error,
)
@@ -777,7 +782,8 @@ async def run_workflow_stream(
workflow: Workflow,
) -> AsyncGenerator[BaseEvent]:
"""Run a Workflow and emit AG-UI protocol events."""
- thread_id = input_data.get("thread_id") or input_data.get("threadId") or str(uuid.uuid4())
+ supplied_thread_id = input_data.get("thread_id") or input_data.get("threadId")
+ thread_id = supplied_thread_id or str(uuid.uuid4())
run_id = input_data.get("run_id") or input_data.get("runId") or str(uuid.uuid4())
available_interrupts = input_data.get("available_interrupts") or input_data.get("availableInterrupts")
if available_interrupts:
@@ -890,12 +896,15 @@ def _drain_open_message() -> list[TextMessageEndEvent]:
fwd_kwargs = {}
try:
- if responses:
- event_stream = workflow.run(responses=responses, stream=True, **fwd_kwargs)
- else:
- event_stream = workflow.run(message=messages, stream=True, **fwd_kwargs)
-
- async for event in event_stream:
+ telemetry_conversation_id = str(supplied_thread_id) if supplied_thread_id is not None else None
+ telemetry_context = partial(_use_telemetry_conversation_id, telemetry_conversation_id)
+ with telemetry_context():
+ if responses:
+ event_stream = workflow.run(responses=responses, stream=True, **fwd_kwargs)
+ else:
+ event_stream = workflow.run(message=messages, stream=True, **fwd_kwargs)
+
+ async for event in _iterate_with_context(event_stream, telemetry_context):
event_type = getattr(event, "type", None)
if event_type == "started":
diff --git a/python/packages/ag-ui/tests/ag_ui/test_endpoint.py b/python/packages/ag-ui/tests/ag_ui/test_endpoint.py
index 0408de841d8..35ade8e90cc 100644
--- a/python/packages/ag-ui/tests/ag_ui/test_endpoint.py
+++ b/python/packages/ag-ui/tests/ag_ui/test_endpoint.py
@@ -15,6 +15,7 @@
from ag_ui.core import MessagesSnapshotEvent, RunStartedEvent, StateSnapshotEvent
from agent_framework import (
Agent,
+ AgentContext,
AgentResponseUpdate,
AgentSession,
ChatResponseUpdate,
@@ -3724,6 +3725,145 @@ async def stream_fn(messages: Any, options: Any, **kwargs: Any):
assert state_snapshots[0]["snapshot"] == {"recipe": "pasta"}
+async def test_agent_endpoint_keeps_request_thread_key_when_provider_returns_conversation_id(
+ streaming_chat_client_stub: Any,
+) -> None:
+ """A provider conversation id must not move snapshots away from the requested AG-UI thread."""
+ app = FastAPI()
+ captured_messages: list[list[tuple[str, str]]] = []
+
+ async def stream_fn(messages: Any, options: Any, **kwargs: Any) -> AsyncIterator[ChatResponseUpdate]:
+ del options, kwargs
+ captured_messages.append([(message.role, message.text) for message in messages])
+ yield ChatResponseUpdate(
+ contents=[Content.from_text(text=f"Reply {len(captured_messages)}")],
+ conversation_id="conv_foundry_123",
+ response_id=f"resp_foundry_{len(captured_messages)}",
+ )
+
+ agent = Agent(name="test", instructions="Test agent", client=streaming_chat_client_stub(stream_fn))
+ store = InMemoryAGUIThreadSnapshotStore()
+ add_agent_framework_fastapi_endpoint(
+ app,
+ agent,
+ path="/snapshots",
+ snapshot_store=store,
+ snapshot_scope_resolver=lambda _request: "tenant-a",
+ )
+ client = TestClient(app)
+
+ first_response = client.post(
+ "/snapshots",
+ json={
+ "thread_id": "ag-ui-thread-1",
+ "run_id": "run-1",
+ "messages": [{"id": "user-1", "role": "user", "content": "Remember LANTERN-482"}],
+ },
+ )
+ assert first_response.status_code == 200
+ first_events = _decode_sse_events(first_response)
+ assert (first_events[0]["threadId"], first_events[0]["runId"]) == ("ag-ui-thread-1", "run-1")
+ assert (first_events[-1]["threadId"], first_events[-1]["runId"]) == ("ag-ui-thread-1", "run-1")
+
+ second_response = client.post(
+ "/snapshots",
+ json={
+ "thread_id": "ag-ui-thread-1",
+ "run_id": "run-2",
+ "messages": [{"id": "user-2", "role": "user", "content": "What token?"}],
+ },
+ )
+
+ assert second_response.status_code == 200
+ second_events = _decode_sse_events(second_response)
+ assert (second_events[0]["threadId"], second_events[0]["runId"]) == ("ag-ui-thread-1", "run-2")
+ assert (second_events[-1]["threadId"], second_events[-1]["runId"]) == ("ag-ui-thread-1", "run-2")
+ assert captured_messages[1] == [
+ ("user", "Remember LANTERN-482"),
+ ("assistant", "Reply 1"),
+ ("user", "What token?"),
+ ]
+
+
+async def test_agent_endpoint_correlates_agent_spans_with_supplied_thread_id(
+ streaming_chat_client_stub: Any,
+ monkeypatch: pytest.MonkeyPatch,
+) -> None:
+ """Agent spans use the stable AG-UI thread id as their OTel conversation id."""
+ from types import SimpleNamespace
+
+ import agent_framework.observability as observability
+ from opentelemetry.sdk.trace import TracerProvider
+ from opentelemetry.sdk.trace.export import SimpleSpanProcessor
+ from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter
+
+ exporter = InMemorySpanExporter()
+ tracer_provider = TracerProvider()
+ tracer_provider.add_span_processor(SimpleSpanProcessor(exporter))
+ monkeypatch.setattr(
+ observability,
+ "OBSERVABILITY_SETTINGS",
+ SimpleNamespace(ENABLED=True, SENSITIVE_DATA_ENABLED=False),
+ )
+ monkeypatch.setattr(observability, "get_tracer", lambda *args, **kwargs: tracer_provider.get_tracer("test"))
+
+ call_count = 0
+
+ async def stream_fn(messages: Any, options: Any, **kwargs: Any) -> AsyncIterator[ChatResponseUpdate]:
+ nonlocal call_count
+ del messages, options, kwargs
+ call_count += 1
+ yield ChatResponseUpdate(
+ contents=[Content.from_text(text=f"Reply {call_count}")],
+ conversation_id=f"resp_foundry_{call_count}",
+ )
+
+ app = FastAPI()
+
+ async def passthrough_middleware(_context: AgentContext, call_next: Any) -> None:
+ await call_next()
+
+ agent = Agent(
+ name="test",
+ instructions="Test agent",
+ client=streaming_chat_client_stub(stream_fn),
+ middleware=[passthrough_middleware],
+ )
+ add_agent_framework_fastapi_endpoint(app, agent, path="/agent")
+ client = TestClient(app)
+
+ for run_number in (1, 2):
+ response = client.post(
+ "/agent",
+ json={
+ "thread_id": "ag-ui-thread-1",
+ "run_id": f"run-{run_number}",
+ "messages": [{"role": "user", "content": f"Turn {run_number}"}],
+ },
+ )
+ assert response.status_code == 200
+
+ agent_spans = []
+ for span in exporter.get_finished_spans():
+ if span.attributes is not None and span.attributes.get("gen_ai.operation.name") == "invoke_agent":
+ agent_spans.append(span)
+
+ assert len(agent_spans) == 2
+ trace_ids: set[int] = set()
+ conversation_ids = []
+ for span in agent_spans:
+ assert span.context is not None
+ assert span.attributes is not None
+ trace_ids.add(span.context.trace_id)
+ conversation_ids.append(span.attributes.get("gen_ai.conversation.id"))
+
+ assert len(trace_ids) == 2
+ assert conversation_ids == [
+ "ag-ui-thread-1",
+ "ag-ui-thread-1",
+ ]
+
+
async def test_agent_endpoint_deduplicates_full_history_and_merges_fresh_state(streaming_chat_client_stub):
"""Stored prior history is authoritative while incoming full history and fresh state remain supported."""
app = FastAPI()
diff --git a/python/packages/ag-ui/tests/ag_ui/test_workflow_run.py b/python/packages/ag-ui/tests/ag_ui/test_workflow_run.py
index 8af9a33be2b..a68da6c6d0f 100644
--- a/python/packages/ag-ui/tests/ag_ui/test_workflow_run.py
+++ b/python/packages/ag-ui/tests/ag_ui/test_workflow_run.py
@@ -8,9 +8,11 @@
from types import SimpleNamespace
from typing import Any, cast
-from ag_ui.core import EventType, StateSnapshotEvent
+import pytest
+from ag_ui.core import EventType, RunFinishedEvent, RunStartedEvent, StateSnapshotEvent
from agent_framework import (
Agent,
+ AgentContext,
AgentResponse,
AgentResponseUpdate,
ChatResponseUpdate,
@@ -114,6 +116,122 @@ async def start(message: Any, ctx: WorkflowContext[Any, str]) -> None:
assert custom_events[0].value == {"progress": 10} # type: ignore[attr-defined] # ty: ignore[unresolved-attribute]
+async def test_workflow_and_agent_spans_use_supplied_agui_thread_id(monkeypatch: pytest.MonkeyPatch) -> None:
+ """Workflow spans use supplied AG-UI threads without replacing provider fallback behavior."""
+ import agent_framework.observability as observability
+ from opentelemetry.sdk.trace import TracerProvider
+ from opentelemetry.sdk.trace.export import SimpleSpanProcessor
+ from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter
+
+ exporter = InMemorySpanExporter()
+ tracer_provider = TracerProvider()
+ tracer_provider.add_span_processor(SimpleSpanProcessor(exporter))
+ monkeypatch.setattr(
+ observability,
+ "OBSERVABILITY_SETTINGS",
+ SimpleNamespace(ENABLED=True, SENSITIVE_DATA_ENABLED=False),
+ )
+ monkeypatch.setattr(observability, "get_tracer", lambda *args, **kwargs: tracer_provider.get_tracer("test"))
+
+ call_count = 0
+
+ async def scripted_stream(
+ messages: Any,
+ options: Any,
+ **kwargs: Any,
+ ) -> AsyncIterator[ChatResponseUpdate]:
+ nonlocal call_count
+ del messages, options, kwargs
+ call_count += 1
+ yield ChatResponseUpdate(
+ contents=[Content.from_text(text=f"Reply {call_count}")],
+ conversation_id="provider-conversation",
+ )
+
+ async def passthrough_middleware(_context: AgentContext, call_next: Any) -> None:
+ await call_next()
+
+ participant = Agent(
+ client=StreamingChatClientStub(scripted_stream),
+ name="workflow-agent",
+ middleware=[passthrough_middleware],
+ )
+ workflow = WorkflowBuilder(start_executor=participant, output_from="all").build()
+
+ for run_number in (1, 2):
+ events = [
+ event
+ async for event in run_workflow_stream(
+ {
+ "thread_id": "ag-ui-workflow-thread",
+ "run_id": f"run-{run_number}",
+ "messages": [{"role": "user", "content": f"Turn {run_number}"}],
+ },
+ workflow,
+ )
+ ]
+ run_started = next(event for event in events if isinstance(event, RunStartedEvent))
+ run_finished = next(event for event in events if isinstance(event, RunFinishedEvent))
+ assert (run_started.thread_id, run_started.run_id) == (
+ "ag-ui-workflow-thread",
+ f"run-{run_number}",
+ )
+ assert (run_finished.thread_id, run_finished.run_id) == (
+ "ag-ui-workflow-thread",
+ f"run-{run_number}",
+ )
+
+ agent_spans = []
+ for span in exporter.get_finished_spans():
+ if span.attributes is not None and span.attributes.get("gen_ai.operation.name") == "invoke_agent":
+ agent_spans.append(span)
+
+ assert len(agent_spans) == 2
+ conversation_ids = []
+ for span in agent_spans:
+ assert span.attributes is not None
+ conversation_ids.append(span.attributes.get("gen_ai.conversation.id"))
+
+ assert conversation_ids == [
+ "ag-ui-workflow-thread",
+ "ag-ui-workflow-thread",
+ ]
+
+ workflow_spans = [span for span in exporter.get_finished_spans() if span.name == "workflow.run"]
+ assert len(workflow_spans) == 2
+ workflow_conversation_ids = []
+ for span in workflow_spans:
+ assert span.attributes is not None
+ workflow_conversation_ids.append(span.attributes.get("gen_ai.conversation.id"))
+
+ assert workflow_conversation_ids == [
+ "ag-ui-workflow-thread",
+ "ag-ui-workflow-thread",
+ ]
+
+ exporter.clear()
+ events = [
+ event
+ async for event in run_workflow_stream(
+ {"messages": [{"role": "user", "content": "Provider fallback"}]},
+ workflow,
+ )
+ ]
+ assert any(isinstance(event, RunFinishedEvent) for event in events)
+
+ fallback_agent_span = next(
+ span
+ for span in exporter.get_finished_spans()
+ if span.attributes is not None and span.attributes.get("gen_ai.operation.name") == "invoke_agent"
+ )
+ assert fallback_agent_span.attributes is not None
+ assert fallback_agent_span.attributes.get("gen_ai.conversation.id") == "provider-conversation"
+
+ fallback_workflow_span = next(span for span in exporter.get_finished_spans() if span.name == "workflow.run")
+ assert fallback_workflow_span.attributes is not None
+ assert "gen_ai.conversation.id" not in fallback_workflow_span.attributes
+
+
async def test_workflow_run_request_info_emits_interrupt_and_resume_works():
"""request_info should emit interrupt metadata and resume should continue run."""
diff --git a/python/packages/core/agent_framework/observability.py b/python/packages/core/agent_framework/observability.py
index 20629b76b81..cd120a2fd8c 100644
--- a/python/packages/core/agent_framework/observability.py
+++ b/python/packages/core/agent_framework/observability.py
@@ -127,6 +127,29 @@
"inner_accumulated_usage", default=None
)
+# Allows protocol adapters to supply an application-managed conversation identity for one execution
+# without putting that value into a service-owned continuation field.
+_TELEMETRY_CONVERSATION_ID: Final[contextvars.ContextVar[str | None]] = contextvars.ContextVar(
+ "telemetry_conversation_id", default=None
+)
+
+
+@contextlib.contextmanager
+def _use_telemetry_conversation_id( # pyright: ignore[reportUnusedFunction]
+ conversation_id: str | None,
+) -> Generator[None]:
+ """Set an application-managed OTel conversation id for the current execution."""
+ if conversation_id is None:
+ yield
+ return
+
+ token = _TELEMETRY_CONVERSATION_ID.set(conversation_id)
+ try:
+ yield
+ finally:
+ _TELEMETRY_CONVERSATION_ID.reset(token)
+
+
OTEL_METRICS: Final[str] = "__otel_metrics__"
TOKEN_USAGE_BUCKET_BOUNDARIES: Final[tuple[float, ...]] = (
1,
@@ -1824,11 +1847,13 @@ def _trace_agent_invocation(
"Callable[[AgentSession | None], str | None] | None",
getattr(self, "_get_otel_conversation_id", None),
)
- conversation_id = (
- get_otel_conversation_id(session)
- if callable(get_otel_conversation_id)
- else (session.service_session_id if (session and isinstance(session.service_session_id, str)) else None)
- )
+ conversation_id = _TELEMETRY_CONVERSATION_ID.get()
+ if conversation_id is None:
+ conversation_id = (
+ get_otel_conversation_id(session)
+ if callable(get_otel_conversation_id)
+ else (session.service_session_id if (session and isinstance(session.service_session_id, str)) else None)
+ )
attributes = _get_span_attributes(
operation_name=OtelAttr.AGENT_INVOKE_OPERATION,
provider_name=provider_name,
@@ -2903,7 +2928,11 @@ def create_workflow_span(
kind: trace.SpanKind = trace.SpanKind.INTERNAL,
) -> _AgnosticContextManager[trace.Span]:
"""Create a generic workflow span."""
- return workflow_tracer().start_as_current_span(name, kind=kind, attributes=attributes)
+ span_attributes = dict(attributes) if attributes is not None else {}
+ conversation_id = _TELEMETRY_CONVERSATION_ID.get()
+ if name == OtelAttr.WORKFLOW_RUN_SPAN and conversation_id is not None:
+ span_attributes.setdefault(OtelAttr.CONVERSATION_ID, conversation_id)
+ return workflow_tracer().start_as_current_span(name, kind=kind, attributes=span_attributes or None)
def create_processing_span(
diff --git a/python/packages/core/tests/core/test_observability.py b/python/packages/core/tests/core/test_observability.py
index 53e99dc1395..c0e0710248b 100644
--- a/python/packages/core/tests/core/test_observability.py
+++ b/python/packages/core/tests/core/test_observability.py
@@ -608,6 +608,36 @@ class MockChatClientAgent(AgentTelemetryLayer, _MockChatClientAgent): # type: i
return MockChatClientAgent
+async def test_agent_telemetry_conversation_override_is_scoped(
+ mock_chat_agent: SupportsAgentRun,
+ span_exporter: InMemorySpanExporter,
+) -> None:
+ """An application-managed conversation id overrides provider continuation for one run only."""
+ from agent_framework import AgentSession
+ from agent_framework.observability import (
+ _use_telemetry_conversation_id, # pyright: ignore[reportPrivateUsage]
+ )
+
+ agent = mock_chat_agent() # type: ignore[operator] # pyrefly: ignore[not-callable] # ty: ignore[call-non-callable]
+ session = AgentSession(service_session_id="provider-conversation")
+ span_exporter.clear()
+
+ with _use_telemetry_conversation_id("application-thread"):
+ await agent.run("First turn", session=session)
+ await agent.run("Second turn", session=session)
+
+ spans = span_exporter.get_finished_spans()
+ conversation_ids = []
+ for span in spans:
+ assert span.attributes is not None
+ conversation_ids.append(span.attributes.get(OtelAttr.CONVERSATION_ID))
+
+ assert conversation_ids == [
+ "application-thread",
+ "provider-conversation",
+ ]
+
+
@pytest.mark.parametrize("enable_sensitive_data", [True, False], indirect=True)
async def test_agent_span_captures_response_telemetry_without_inner_chat_span(
mock_chat_agent: SupportsAgentRun, span_exporter: InMemorySpanExporter, enable_sensitive_data
@@ -2081,6 +2111,46 @@ def test_create_workflow_span(span_exporter):
assert spans[0].attributes["key"] == "value"
+def test_create_workflow_span_uses_scoped_conversation_id(span_exporter: InMemorySpanExporter) -> None:
+ """An ambient conversation id is applied only within its workflow execution scope."""
+ from agent_framework.observability import (
+ OtelAttr,
+ _use_telemetry_conversation_id, # pyright: ignore[reportPrivateUsage]
+ create_workflow_span,
+ )
+
+ span_exporter.clear() # type: ignore[attr-defined]
+ with _use_telemetry_conversation_id("application-thread"):
+ with create_workflow_span(OtelAttr.WORKFLOW_RUN_SPAN):
+ pass
+ with create_workflow_span(
+ OtelAttr.WORKFLOW_RUN_SPAN,
+ attributes={OtelAttr.CONVERSATION_ID: "explicit-thread"},
+ ):
+ pass
+ with create_workflow_span(OtelAttr.MESSAGE_SEND_SPAN):
+ pass
+ with create_workflow_span(OtelAttr.WORKFLOW_RUN_SPAN):
+ pass
+
+ spans = span_exporter.get_finished_spans() # type: ignore[attr-defined]
+ workflow_spans = [span for span in spans if span.name == OtelAttr.WORKFLOW_RUN_SPAN]
+ assert len(workflow_spans) == 3
+ ambient_attributes = workflow_spans[0].attributes
+ explicit_attributes = workflow_spans[1].attributes
+ unscoped_attributes = workflow_spans[2].attributes
+ assert ambient_attributes is not None
+ assert explicit_attributes is not None
+ assert unscoped_attributes is not None
+ assert ambient_attributes[OtelAttr.CONVERSATION_ID] == "application-thread"
+ assert explicit_attributes[OtelAttr.CONVERSATION_ID] == "explicit-thread"
+ assert OtelAttr.CONVERSATION_ID not in unscoped_attributes
+ message_send_span = next(span for span in spans if span.name == OtelAttr.MESSAGE_SEND_SPAN)
+ message_send_attributes = message_send_span.attributes
+ assert message_send_attributes is not None
+ assert OtelAttr.CONVERSATION_ID not in message_send_attributes
+
+
def test_create_processing_span(span_exporter):
"""Test create_processing_span creates a span with correct attributes."""
from agent_framework.observability import OtelAttr, create_processing_span
diff --git a/python/samples/05-end-to-end/ag_ui_single_agent/README.md b/python/samples/05-end-to-end/ag_ui_single_agent/README.md
new file mode 100644
index 00000000000..b1d2bfb1ff6
--- /dev/null
+++ b/python/samples/05-end-to-end/ag_ui_single_agent/README.md
@@ -0,0 +1,96 @@
+# AG-UI Single Agent Demo
+
+The simplest possible AG-UI integration: a **single chat agent** with **no tools** and **no context providers**,
+served over the AG-UI protocol and consumed by a small React client.
+
+Use this sample as the starting point for AG-UI. For a richer, multi-agent example with tool-approval checkpoints
+and human-in-the-loop resumes, see [`../ag_ui_workflow_handoff`](../ag_ui_workflow_handoff/README.md).
+
+## Folder Layout
+
+- `backend/server.py` - FastAPI + AG-UI endpoint wrapping a single `Agent`
+- `frontend/` - Vite + React AG-UI client UI
+
+## Prerequisites
+
+- Python 3.10+
+- Node.js 20.19+ or 22.12+
+- npm 9+
+- Azure AI project + model deployment configured in environment variables:
+ - `FOUNDRY_PROJECT_ENDPOINT`
+ - `FOUNDRY_MODEL`
+- Azure CLI authenticated with `az login`
+
+## 1) Run Backend
+
+From the repository root:
+
+```bash
+cd python
+uv sync
+uv run python samples/05-end-to-end/ag_ui_single_agent/backend/server.py
+```
+
+Backend default URL:
+
+- `http://127.0.0.1:8892`
+- AG-UI endpoint: `POST http://127.0.0.1:8892/agent`
+
+To export traces to the Application Insights resource connected to the Foundry project, run the backend with:
+
+```bash
+ENABLE_AZURE_MONITOR=true uv run python samples/05-end-to-end/ag_ui_single_agent/backend/server.py
+```
+
+Each user turn is a separate run and trace. The stable AG-UI `thread_id` is recorded as
+`gen_ai.conversation.id`, which lets Foundry group those turns into one conversation.
+
+## 2) Install Frontend Packages (npm)
+
+From the `python/` directory (where Step 1 left you):
+
+```bash
+cd samples/05-end-to-end/ag_ui_single_agent/frontend
+npm install
+```
+
+## 3) Run Frontend Locally
+
+```bash
+npm run dev
+```
+
+Frontend default URL:
+
+- `http://127.0.0.1:5173`
+
+If you changed backend host/port, run with:
+
+```bash
+VITE_BACKEND_URL=http://127.0.0.1:8892 npm run dev
+```
+
+## 4) Demo Flow to Verify
+
+1. Click one of the starter prompts (or type your own message).
+2. Watch the assistant response stream in token by token.
+3. Send a follow-up that depends on the previous turn (for example: "summarize what you just told me").
+ The client only sends the newest message plus the `thread_id`; the server replays the stored history.
+4. Click **New Thread** to start a fresh conversation (a new `thread_id`).
+
+## Conversation History
+
+The client only ever sends the **newest message** plus a `thread_id`. The backend retains history **server-side**,
+keyed by that `thread_id`, using an `InMemoryAGUIThreadSnapshotStore`. Because an AG-UI thread id is not an
+authorization boundary, a `snapshot_scope_resolver` is required whenever a snapshot store is configured; this
+single-tenant demo maps every request to one shared `"demo"` scope.
+
+The in-memory store is process-local and not durable. Swap in your own `AGUIThreadSnapshotStore` implementation
+(and a real scope resolver) for production.
+
+## What This Validates
+
+- `add_agent_framework_fastapi_endpoint(...)` with a plain `Agent` (no `AgentFrameworkWorkflow` wrapper)
+- Streaming assistant text via `TEXT_MESSAGE_START` / `TEXT_MESSAGE_CONTENT` / `TEXT_MESSAGE_END` AG-UI events
+- Server-side conversation history keyed by `thread_id` via a snapshot store
+- Foundry trace correlation across runs using the stable AG-UI `thread_id`
diff --git a/python/samples/05-end-to-end/ag_ui_single_agent/backend/server.py b/python/samples/05-end-to-end/ag_ui_single_agent/backend/server.py
new file mode 100644
index 00000000000..a399a0372bb
--- /dev/null
+++ b/python/samples/05-end-to-end/ag_ui_single_agent/backend/server.py
@@ -0,0 +1,148 @@
+# /// script
+# requires-python = ">=3.10"
+# dependencies = [
+# "agent-framework-ag-ui",
+# "agent-framework-foundry",
+# "azure-identity",
+# "azure-monitor-opentelemetry",
+# "fastapi",
+# "python-dotenv",
+# "uvicorn",
+# ]
+# ///
+
+# Copyright (c) Microsoft. All rights reserved.
+
+"""AG-UI single-agent demo backend.
+
+This sample exposes one Foundry-backed Agent over AG-UI and pairs it with the
+React frontend in `../frontend`.
+
+Environment variables:
+ FOUNDRY_PROJECT_ENDPOINT: Microsoft Foundry project endpoint.
+ FOUNDRY_MODEL: Model deployment name.
+ ENABLE_AZURE_MONITOR: Set to true to export traces to the project's Application Insights resource.
+"""
+
+from __future__ import annotations
+
+import asyncio
+import logging
+import os
+from collections.abc import AsyncIterator
+from contextlib import asynccontextmanager
+
+import uvicorn
+from agent_framework import Agent
+from agent_framework.ag_ui import (
+ InMemoryAGUIThreadSnapshotStore,
+ add_agent_framework_fastapi_endpoint,
+)
+from agent_framework.foundry import FoundryChatClient
+from azure.identity import AzureCliCredential
+from dotenv import load_dotenv
+from fastapi import FastAPI
+from fastapi.middleware.cors import CORSMiddleware
+
+load_dotenv()
+
+logger = logging.getLogger(__name__)
+
+
+# 1. Create one Foundry-backed agent with no tools or context providers.
+def create_client() -> FoundryChatClient:
+ """Create the Foundry chat client used by the sample."""
+
+ return FoundryChatClient(
+ project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
+ model=os.environ["FOUNDRY_MODEL"],
+ credential=AzureCliCredential(),
+ )
+
+
+def create_agent(client: FoundryChatClient) -> Agent:
+ """Create a single chat agent with no tools and no context providers."""
+
+ return Agent(
+ id="assistant",
+ name="assistant",
+ instructions="You are a helpful, concise assistant. Answer the user's questions directly.",
+ client=client,
+ )
+
+
+# 2. Configure the AG-UI endpoint, thread history, and optional trace export.
+def create_app() -> FastAPI:
+ """Create and configure the FastAPI application."""
+
+ client = create_client()
+ agent = create_agent(client)
+
+ @asynccontextmanager
+ async def lifespan(_app: FastAPI) -> AsyncIterator[None]:
+ if os.getenv("ENABLE_AZURE_MONITOR", "false").casefold() in {"1", "true", "yes", "on"}:
+ await client.configure_azure_monitor()
+ logger.info("Azure Monitor telemetry export is enabled")
+ yield
+
+ app = FastAPI(title="AG-UI Single Agent Demo", lifespan=lifespan)
+
+ cors_origins = [
+ origin.strip() for origin in os.getenv("CORS_ORIGINS", "http://127.0.0.1:5173").split(",") if origin.strip()
+ ]
+ app.add_middleware(
+ CORSMiddleware,
+ allow_origins=cors_origins,
+ allow_credentials=True,
+ allow_methods=["*"],
+ allow_headers=["*"],
+ )
+
+ add_agent_framework_fastapi_endpoint(
+ app=app,
+ agent=agent,
+ path="/agent",
+ # Persist conversation history server-side, keyed by thread_id, so the
+ # client only ever sends the newest message plus its thread_id.
+ snapshot_store=InMemoryAGUIThreadSnapshotStore(),
+ # AG-UI thread ids are not an authorization boundary, so a scope is required
+ # when a snapshot store is configured. This demo is single-tenant, so every
+ # request maps to one shared scope.
+ snapshot_scope_resolver=lambda _request: "demo",
+ )
+
+ @app.get("/healthz")
+ async def healthz() -> dict[str, str]:
+ return {"status": "ok"}
+
+ return app
+
+
+app = create_app()
+
+
+# 3. Run the backend for the React frontend.
+async def main() -> None:
+ """Run the AG-UI single-agent demo backend."""
+
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
+
+ host = os.getenv("HOST", "127.0.0.1")
+ port = int(os.getenv("PORT", "8892"))
+
+ print(f"AG-UI single-agent demo backend running at http://{host}:{port}")
+ print("AG-UI endpoint: POST /agent")
+
+ server = uvicorn.Server(uvicorn.Config(app, host=host, port=port))
+ await server.serve()
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
+
+
+"""
+Sample output:
+AG-UI single-agent demo backend running at http://127.0.0.1:8892
+AG-UI endpoint: POST /agent
+"""
diff --git a/python/samples/05-end-to-end/ag_ui_single_agent/frontend/.gitignore b/python/samples/05-end-to-end/ag_ui_single_agent/frontend/.gitignore
new file mode 100644
index 00000000000..16c69217c02
--- /dev/null
+++ b/python/samples/05-end-to-end/ag_ui_single_agent/frontend/.gitignore
@@ -0,0 +1,7 @@
+# dependencies
+/node_modules
+
+# build artifacts
+*.tsbuildinfo
+vite.config.js
+vite.config.d.ts
diff --git a/python/samples/05-end-to-end/ag_ui_single_agent/frontend/index.html b/python/samples/05-end-to-end/ag_ui_single_agent/frontend/index.html
new file mode 100644
index 00000000000..fa3da04d3eb
--- /dev/null
+++ b/python/samples/05-end-to-end/ag_ui_single_agent/frontend/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ AG-UI Single Agent Demo
+
+
+
+
+
+
diff --git a/python/samples/05-end-to-end/ag_ui_single_agent/frontend/package-lock.json b/python/samples/05-end-to-end/ag_ui_single_agent/frontend/package-lock.json
new file mode 100644
index 00000000000..883f8129f76
--- /dev/null
+++ b/python/samples/05-end-to-end/ag_ui_single_agent/frontend/package-lock.json
@@ -0,0 +1,1054 @@
+{
+ "name": "ag-ui-single-agent-demo-frontend",
+ "version": "0.1.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "ag-ui-single-agent-demo-frontend",
+ "version": "0.1.0",
+ "dependencies": {
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1"
+ },
+ "devDependencies": {
+ "@types/node": "^22.10.1",
+ "@types/react": "^18.3.3",
+ "@types/react-dom": "^18.3.0",
+ "@vitejs/plugin-react": "^6.0.2",
+ "typescript": "^5.5.4",
+ "vite": "^8.0.16"
+ }
+ },
+ "node_modules/@emnapi/core": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz",
+ "integrity": "sha1-ueEGTzprFjHiQeY460jXNr/TcqY=",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/wasi-threads": "1.2.2",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/runtime": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz",
+ "integrity": "sha1-WPHz1dgamxL3k6tojJY3GQECfCQ=",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/wasi-threads": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
+ "integrity": "sha1-TJO+z1v6OxPRu9zAau44MhrYE5o=",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@napi-rs/wasm-runtime": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz",
+ "integrity": "sha1-7TOAbQ+b6Y3HbQw9T9hy/acBtdU=",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@tybys/wasm-util": "^0.10.3"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ },
+ "peerDependencies": {
+ "@emnapi/core": "^1.7.1",
+ "@emnapi/runtime": "^1.7.1"
+ }
+ },
+ "node_modules/@oxc-project/types": {
+ "version": "0.139.0",
+ "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz",
+ "integrity": "sha1-ONdrnb+TTCoCvhdPsyzuvxgv50I=",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/Boshen"
+ }
+ },
+ "node_modules/@rolldown/binding-android-arm64": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz",
+ "integrity": "sha1-9Yy5oKgSjtBYIoJyBShUf8XANfM=",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-arm64": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz",
+ "integrity": "sha1-RBFEwFpKgxqnUmmrw6SjJDdOpwc=",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-x64": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz",
+ "integrity": "sha1-yC4wZSzvUsSvkl1cZsiVWkAxmBY=",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-freebsd-x64": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz",
+ "integrity": "sha1-wy6c5/ocD7K4CROio6BcPpB9BrA=",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz",
+ "integrity": "sha1-zpC14iMWretQLqAQWC9JjNBgTyc=",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-gnu": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz",
+ "integrity": "sha1-kZRxEMTdqk7vsATlJogHCXcIUgE=",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-musl": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz",
+ "integrity": "sha1-6zKy1BCMHHArkejN6KBD6uXKqU0=",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-ppc64-gnu": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz",
+ "integrity": "sha1-zLlDwR5acmVcuwL8EWNUHOtkB4I=",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-s390x-gnu": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz",
+ "integrity": "sha1-ozcx7lZ+kLdfrG5eVTB+iiswOPA=",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-gnu": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz",
+ "integrity": "sha1-p0wBqqzt/BHDm2/rozpfoMZUlJ8=",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-musl": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz",
+ "integrity": "sha1-KM0XhJT6HmXbpBIim1zlXE3Vy9E=",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-openharmony-arm64": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz",
+ "integrity": "sha1-98dfqRP8IIhNJqfUiNT1xZfNccA=",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-wasm32-wasi": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz",
+ "integrity": "sha1-w3lYGUd4cIHfNj6hBhQPzV/sJS0=",
+ "cpu": [
+ "wasm32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "1.11.1",
+ "@emnapi/runtime": "1.11.1",
+ "@napi-rs/wasm-runtime": "^1.1.6"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-arm64-msvc": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz",
+ "integrity": "sha1-8jyIaU96cpoS85UCSu4434Cha6M=",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-x64-msvc": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz",
+ "integrity": "sha1-M3fI3g5WqIV/ESF1YRRwkOh0y0Y=",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
+ "integrity": "sha1-4/zuCT+7XOdl4a0Ij/TeKIn2+b4=",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@tybys/wasm-util": {
+ "version": "0.10.3",
+ "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz",
+ "integrity": "sha1-AVy6np3UfOFNA9KoxdVHv7FpZl0=",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@types/node": {
+ "version": "22.20.1",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz",
+ "integrity": "sha1-hOfN9jzaogwTSqMXzMkBqiHhbw4=",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
+ "node_modules/@types/prop-types": {
+ "version": "15.7.15",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
+ "integrity": "sha1-5uWobWAr6spxzlFj+t9fldcJMcc=",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/react": {
+ "version": "18.3.31",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz",
+ "integrity": "sha1-teleKP/M6rjZgvM/LrB24XZTwqQ=",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/prop-types": "*",
+ "csstype": "^3.2.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "18.3.7",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
+ "integrity": "sha1-uJ3fLNg7T+r8xOLqQa/fuVoNGU8=",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^18.0.0"
+ }
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "6.0.4",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.4.tgz",
+ "integrity": "sha1-o1g0um5Gi8rWheyNBKU78M68wv8=",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@rolldown/pluginutils": "^1.0.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "peerDependencies": {
+ "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0",
+ "babel-plugin-react-compiler": "^1.0.0",
+ "vite": "^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@rolldown/plugin-babel": {
+ "optional": true
+ },
+ "babel-plugin-react-compiler": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha1-7EjA8+mT5QZIyG2lWeJhCZXPmJo=",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha1-aJxdzcGQDvVYOky59te0c3QgdK0=",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha1-7Sq5Z6MxreYvGNB32uGSaE1Q01A=",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha1-ysZAd4XQNnWipeGlMFxpezR9kNY=",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha1-GSA/tZmR35jjoocFDUZHzerzJJk=",
+ "license": "MIT"
+ },
+ "node_modules/lightningcss": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz",
+ "integrity": "sha1-wIhn1xp5OFxuGQIU/XL+8+X5Xws=",
+ "dev": true,
+ "license": "MPL-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.33.0",
+ "lightningcss-darwin-arm64": "1.33.0",
+ "lightningcss-darwin-x64": "1.33.0",
+ "lightningcss-freebsd-x64": "1.33.0",
+ "lightningcss-linux-arm-gnueabihf": "1.33.0",
+ "lightningcss-linux-arm64-gnu": "1.33.0",
+ "lightningcss-linux-arm64-musl": "1.33.0",
+ "lightningcss-linux-x64-gnu": "1.33.0",
+ "lightningcss-linux-x64-musl": "1.33.0",
+ "lightningcss-win32-arm64-msvc": "1.33.0",
+ "lightningcss-win32-x64-msvc": "1.33.0"
+ }
+ },
+ "node_modules/lightningcss-android-arm64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz",
+ "integrity": "sha1-mmhB+IrlD8g1ApA4krQa9BvCuQc=",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-arm64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz",
+ "integrity": "sha1-wPLDHAv9GfpN0/GOlXofGhUgl9Y=",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-x64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz",
+ "integrity": "sha1-ywcFllrLU4xmg5Sc5pJfs833w2E=",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-freebsd-x64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz",
+ "integrity": "sha1-djU4gosmurJoDa2vzITueLDrUCs=",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz",
+ "integrity": "sha1-aGLjF2ozGu297B7TUrTX0N0HhN4=",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz",
+ "integrity": "sha1-xqOi7RUUHa9r3CYokw+OOb30c6o=",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz",
+ "integrity": "sha1-f6EzSXH8goRfmCffbvigsgkUusY=",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz",
+ "integrity": "sha1-i5J4YuqMK7xoMaRlCSRLUNmTblU=",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz",
+ "integrity": "sha1-DFJbsHff2UQEwFnP5C2teX6Wrq8=",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz",
+ "integrity": "sha1-hQ7hED2smJz6tQ46wi0aaeOU5j0=",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz",
+ "integrity": "sha1-40OuFS7tNgncbhGUnRo785oclG8=",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha1-ce5R+nvkyuwaY4OffmgtgTLTDK8=",
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.16",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
+ "integrity": "sha1-oE2OxLHxAAnS1TOUeu/kKTc3gWw=",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha1-PTIa8+q5ObCDyPkpodEs2oHCa2s=",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
+ "integrity": "sha1-UepXoX2G9gX4EDlZX7xA7QalX6s=",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.22",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.22.tgz",
+ "integrity": "sha1-CGoNVoWnFaz1lQ67yxU4+vMFFpc=",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.16",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/react": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
+ "integrity": "sha1-SauJIAnFOTNiW9FrJTP8dUyrKJE=",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
+ "integrity": "sha1-wiZdeVEbV9R5s90/36UVNklMXLQ=",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0",
+ "scheduler": "^0.23.2"
+ },
+ "peerDependencies": {
+ "react": "^18.3.1"
+ }
+ },
+ "node_modules/rolldown": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz",
+ "integrity": "sha1-M5quJQhENR/FW3TiZS0+vW+6OJ0=",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@oxc-project/types": "=0.139.0",
+ "@rolldown/pluginutils": "^1.0.0"
+ },
+ "bin": {
+ "rolldown": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "optionalDependencies": {
+ "@rolldown/binding-android-arm64": "1.1.5",
+ "@rolldown/binding-darwin-arm64": "1.1.5",
+ "@rolldown/binding-darwin-x64": "1.1.5",
+ "@rolldown/binding-freebsd-x64": "1.1.5",
+ "@rolldown/binding-linux-arm-gnueabihf": "1.1.5",
+ "@rolldown/binding-linux-arm64-gnu": "1.1.5",
+ "@rolldown/binding-linux-arm64-musl": "1.1.5",
+ "@rolldown/binding-linux-ppc64-gnu": "1.1.5",
+ "@rolldown/binding-linux-s390x-gnu": "1.1.5",
+ "@rolldown/binding-linux-x64-gnu": "1.1.5",
+ "@rolldown/binding-linux-x64-musl": "1.1.5",
+ "@rolldown/binding-openharmony-arm64": "1.1.5",
+ "@rolldown/binding-wasm32-wasi": "1.1.5",
+ "@rolldown/binding-win32-arm64-msvc": "1.1.5",
+ "@rolldown/binding-win32-x64-msvc": "1.1.5"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.23.2",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
+ "integrity": "sha1-QUumSjsoKJLpRM8hCOzAeNEVzcM=",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha1-HOVlD93YerwJnto33P8CTCZnrkY=",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.17",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
+ "integrity": "sha1-ViqabJ6ys7Ej05cZ+a9btE/NdjE=",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha1-YS7+TtI11Wfoq6Xypfq3AoCt6D8=",
+ "dev": true,
+ "license": "0BSD",
+ "optional": true
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha1-W09Z4VMQqxeiFvXWz1PuR27eZw8=",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha1-aR0ArzkJvpOn+qE75hs6W1DvEss=",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/vite": {
+ "version": "8.1.5",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz",
+ "integrity": "sha1-zP/OPuSHsYhiI7JOuye5FnSCfTA=",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "lightningcss": "^1.32.0",
+ "picomatch": "^4.0.5",
+ "postcss": "^8.5.17",
+ "rolldown": "~1.1.5",
+ "tinyglobby": "^0.2.17"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "@vitejs/devtools": "^0.3.0",
+ "esbuild": "^0.27.0 || ^0.28.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "@vitejs/devtools": {
+ "optional": true
+ },
+ "esbuild": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ }
+ }
+}
diff --git a/python/samples/05-end-to-end/ag_ui_single_agent/frontend/package.json b/python/samples/05-end-to-end/ag_ui_single_agent/frontend/package.json
new file mode 100644
index 00000000000..e0eee696588
--- /dev/null
+++ b/python/samples/05-end-to-end/ag_ui_single_agent/frontend/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "ag-ui-single-agent-demo-frontend",
+ "private": true,
+ "version": "0.1.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc -b && vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1"
+ },
+ "devDependencies": {
+ "@types/node": "^22.10.1",
+ "@types/react": "^18.3.3",
+ "@types/react-dom": "^18.3.0",
+ "@vitejs/plugin-react": "^6.0.2",
+ "typescript": "^5.5.4",
+ "vite": "^8.0.16"
+ }
+}
diff --git a/python/samples/05-end-to-end/ag_ui_single_agent/frontend/src/App.tsx b/python/samples/05-end-to-end/ag_ui_single_agent/frontend/src/App.tsx
new file mode 100644
index 00000000000..3ce6419b7ea
--- /dev/null
+++ b/python/samples/05-end-to-end/ag_ui_single_agent/frontend/src/App.tsx
@@ -0,0 +1,282 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+import { FormEvent, useEffect, useMemo, useRef, useState } from "react";
+
+type AgUiEvent = Record & { type: string };
+
+interface ChatMessage {
+ id: string;
+ role: "assistant" | "user" | "system";
+ text: string;
+}
+
+const BACKEND_URL = import.meta.env.VITE_BACKEND_URL ?? "http://127.0.0.1:8892";
+const ENDPOINT = `${BACKEND_URL}/agent`;
+
+const STARTER_PROMPTS = [
+ "Explain the AG-UI protocol in two sentences.",
+ "Give me three tips for writing clear commit messages.",
+];
+
+function randomId(): string {
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
+ return crypto.randomUUID();
+ }
+ return `id-${Math.random().toString(16).slice(2)}`;
+}
+
+function isObject(value: unknown): value is Record {
+ return typeof value === "object" && value !== null;
+}
+
+function safeParseJson(value: string): unknown {
+ try {
+ return JSON.parse(value);
+ } catch {
+ return null;
+ }
+}
+
+export default function App() {
+ const [messages, setMessages] = useState([]);
+ const [draft, setDraft] = useState("");
+ const [isRunning, setIsRunning] = useState(false);
+ const [statusText, setStatusText] = useState("Ready");
+
+ const threadIdRef = useRef(randomId());
+ const streamingMessageIdRef = useRef(null);
+ const transcriptRef = useRef(null);
+
+ const canSend = useMemo(() => draft.trim().length > 0 && !isRunning, [draft, isRunning]);
+
+ useEffect(() => {
+ const node = transcriptRef.current;
+ if (node) {
+ node.scrollTop = node.scrollHeight;
+ }
+ }, [messages]);
+
+ const pushMessage = (message: ChatMessage): void => {
+ setMessages((prev) => [...prev, message]);
+ };
+
+ const appendToStreamingMessage = (messageId: string, delta: string): void => {
+ setMessages((prev) => {
+ const existing = prev.find((message) => message.id === messageId);
+ if (existing) {
+ return prev.map((message) =>
+ message.id === messageId ? { ...message, text: `${message.text}${delta}` } : message,
+ );
+ }
+ return [...prev, { id: messageId, role: "assistant", text: delta }];
+ });
+ };
+
+ const handleEvent = (event: AgUiEvent): void => {
+ switch (event.type) {
+ case "RUN_STARTED":
+ setStatusText("Thinking");
+ break;
+ case "TEXT_MESSAGE_START": {
+ const messageId = typeof event.messageId === "string" ? event.messageId : randomId();
+ streamingMessageIdRef.current = messageId;
+ break;
+ }
+ case "TEXT_MESSAGE_CONTENT": {
+ const messageId =
+ typeof event.messageId === "string" ? event.messageId : streamingMessageIdRef.current ?? randomId();
+ const delta = typeof event.delta === "string" ? event.delta : "";
+ if (delta.length > 0) {
+ setStatusText("Responding");
+ appendToStreamingMessage(messageId, delta);
+ }
+ break;
+ }
+ case "TEXT_MESSAGE_END":
+ streamingMessageIdRef.current = null;
+ break;
+ case "RUN_FINISHED":
+ setStatusText("Ready");
+ setIsRunning(false);
+ break;
+ case "RUN_ERROR": {
+ const errorText = typeof event.message === "string" ? event.message : "The run failed.";
+ pushMessage({ id: randomId(), role: "system", text: `Error: ${errorText}` });
+ setStatusText("Error");
+ setIsRunning(false);
+ break;
+ }
+ default:
+ break;
+ }
+ };
+
+ const streamRun = async (body: Record): Promise => {
+ const response = await fetch(ENDPOINT, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ Accept: "text/event-stream",
+ },
+ body: JSON.stringify(body),
+ });
+
+ if (!response.ok || !response.body) {
+ throw new Error(`Request failed: ${response.status}`);
+ }
+
+ const reader = response.body.getReader();
+ const decoder = new TextDecoder("utf-8");
+ let buffer = "";
+
+ const processSseChunk = (rawChunk: string): void => {
+ const dataLines = rawChunk
+ .split(/\r?\n/)
+ .filter((line) => line.startsWith("data:"))
+ .map((line) => line.slice(5).trim());
+
+ if (dataLines.length === 0) {
+ return;
+ }
+
+ const parsed = safeParseJson(dataLines.join("\n"));
+ if (isObject(parsed) && typeof parsed.type === "string") {
+ handleEvent(parsed as AgUiEvent);
+ }
+ };
+
+ while (true) {
+ const { value, done } = await reader.read();
+ if (done) {
+ break;
+ }
+
+ buffer += decoder.decode(value, { stream: true });
+
+ while (true) {
+ const boundary = /\r?\n\r?\n/.exec(buffer);
+ if (boundary === null) {
+ break;
+ }
+ const boundaryIndex = boundary.index;
+ const rawEvent = buffer.slice(0, boundaryIndex);
+ buffer = buffer.slice(boundaryIndex + boundary[0].length);
+ processSseChunk(rawEvent);
+ }
+ }
+
+ const tail = buffer.trim();
+ if (tail.length > 0) {
+ processSseChunk(tail);
+ }
+ };
+
+ const sendMessage = async (text: string): Promise => {
+ const trimmed = text.trim();
+ if (trimmed.length === 0 || isRunning) {
+ return;
+ }
+
+ pushMessage({ id: randomId(), role: "user", text: trimmed });
+ setDraft("");
+ setIsRunning(true);
+ setStatusText("Connecting");
+ streamingMessageIdRef.current = null;
+
+ try {
+ await streamRun({
+ thread_id: threadIdRef.current,
+ run_id: randomId(),
+ messages: [{ role: "user", content: trimmed }],
+ });
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Unknown error";
+ pushMessage({ id: randomId(), role: "system", text: `Network error: ${message}` });
+ setStatusText("Network error");
+ setIsRunning(false);
+ }
+ };
+
+ const handleSubmit = (event: FormEvent): void => {
+ event.preventDefault();
+ void sendMessage(draft);
+ };
+
+ const startNewThread = (): void => {
+ threadIdRef.current = randomId();
+ streamingMessageIdRef.current = null;
+ setMessages([]);
+ setDraft("");
+ setStatusText("Ready");
+ setIsRunning(false);
+ };
+
+ return (
+
+
+
+
+
+
Conversation
+
+
+
+
+ {messages.length === 0 ? (
+
+
Start the conversation with a prompt:
+
+ {STARTER_PROMPTS.map((prompt) => (
+
+ ))}
+
+
+ ) : (
+ messages.map((message) => (
+
+
{message.role}
+
{message.text}
+
+ ))
+ )}
+
+
+
+
+
+ );
+}
diff --git a/python/samples/05-end-to-end/ag_ui_single_agent/frontend/src/main.tsx b/python/samples/05-end-to-end/ag_ui_single_agent/frontend/src/main.tsx
new file mode 100644
index 00000000000..4daf5e19b35
--- /dev/null
+++ b/python/samples/05-end-to-end/ag_ui_single_agent/frontend/src/main.tsx
@@ -0,0 +1,13 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+import React from "react";
+import ReactDOM from "react-dom/client";
+
+import App from "./App";
+import "./styles.css";
+
+ReactDOM.createRoot(document.getElementById("root")!).render(
+
+
+ ,
+);
diff --git a/python/samples/05-end-to-end/ag_ui_single_agent/frontend/src/styles.css b/python/samples/05-end-to-end/ag_ui_single_agent/frontend/src/styles.css
new file mode 100644
index 00000000000..adab4d7f456
--- /dev/null
+++ b/python/samples/05-end-to-end/ag_ui_single_agent/frontend/src/styles.css
@@ -0,0 +1,259 @@
+/* Copyright (c) Microsoft. All rights reserved. */
+
+:root {
+ --page-bg: #edf4f8;
+ --panel-bg: #fdfdfd;
+ --ink: #132534;
+ --muted: #607487;
+ --line: #c6d6e2;
+ --teal: #1f9d8b;
+ --teal-dark: #11756a;
+ --shadow: 0 20px 45px rgb(15 35 51 / 14%);
+}
+
+* {
+ box-sizing: border-box;
+}
+
+body {
+ margin: 0;
+ font-family: "IBM Plex Sans", "Avenir Next", "Helvetica Neue", sans-serif;
+ color: var(--ink);
+ background:
+ radial-gradient(circle at 12% 8%, rgb(31 157 139 / 20%) 0%, transparent 28%),
+ radial-gradient(circle at 88% 18%, rgb(255 154 60 / 20%) 0%, transparent 30%),
+ linear-gradient(150deg, #eff6fa 0%, #dceaf3 46%, #e7f1f6 100%);
+}
+
+.page-shell {
+ min-height: 100vh;
+ max-width: 860px;
+ margin: 0 auto;
+ padding: 28px;
+ animation: fade-in 320ms ease-out;
+}
+
+.hero {
+ display: flex;
+ gap: 20px;
+ justify-content: space-between;
+ align-items: flex-end;
+ margin-bottom: 24px;
+}
+
+.eyebrow {
+ margin: 0;
+ text-transform: uppercase;
+ letter-spacing: 0.16em;
+ font-size: 0.72rem;
+ color: var(--teal-dark);
+ font-weight: 700;
+}
+
+.hero h1 {
+ margin: 6px 0 8px;
+ font-size: clamp(1.6rem, 2.8vw, 2.4rem);
+ line-height: 1.15;
+}
+
+.subtitle {
+ margin: 0;
+ max-width: 60ch;
+ color: var(--muted);
+ line-height: 1.45;
+}
+
+.status-pill {
+ border: 1px solid var(--line);
+ border-radius: 999px;
+ padding: 10px 16px;
+ background: #fff;
+ display: flex;
+ flex-direction: column;
+ min-width: 150px;
+ box-shadow: 0 8px 20px rgb(19 37 52 / 8%);
+}
+
+.status-pill span {
+ font-size: 0.72rem;
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ color: var(--muted);
+}
+
+.status-pill strong {
+ font-size: 1rem;
+}
+
+.status-pill[data-running="true"] {
+ border-color: var(--teal);
+}
+
+.card {
+ background: var(--panel-bg);
+ border: 1px solid var(--line);
+ border-radius: 18px;
+ box-shadow: var(--shadow);
+ padding: 18px;
+}
+
+.chat-card {
+ display: flex;
+ flex-direction: column;
+ gap: 14px;
+ min-height: 60vh;
+}
+
+.chat-toolbar {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.chat-toolbar h2 {
+ margin: 0;
+ font-size: 1.1rem;
+}
+
+.ghost-button {
+ border: 1px solid var(--line);
+ background: #fff;
+ color: var(--teal-dark);
+ border-radius: 999px;
+ padding: 6px 14px;
+ font-weight: 600;
+ cursor: pointer;
+}
+
+.ghost-button:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+.transcript {
+ flex: 1;
+ overflow-y: auto;
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ padding: 6px 2px;
+ max-height: 52vh;
+}
+
+.empty-state {
+ color: var(--muted);
+ display: grid;
+ gap: 12px;
+}
+
+.starter-prompts {
+ display: grid;
+ gap: 10px;
+}
+
+.starter-prompt {
+ text-align: left;
+ border: 1px dashed var(--line);
+ background: #f6fafc;
+ border-radius: 12px;
+ padding: 12px 14px;
+ color: var(--ink);
+ cursor: pointer;
+}
+
+.starter-prompt:hover:not(:disabled) {
+ border-color: var(--teal);
+}
+
+.starter-prompt:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+}
+
+.bubble {
+ border-radius: 14px;
+ padding: 10px 14px;
+ max-width: 82%;
+ border: 1px solid var(--line);
+ background: #fff;
+}
+
+.bubble p {
+ margin: 4px 0 0;
+ white-space: pre-wrap;
+ line-height: 1.45;
+}
+
+.bubble-role {
+ font-size: 0.68rem;
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ color: var(--muted);
+}
+
+.bubble-user {
+ align-self: flex-end;
+ background: var(--teal);
+ border-color: var(--teal-dark);
+ color: #fff;
+}
+
+.bubble-user .bubble-role {
+ color: rgb(255 255 255 / 80%);
+}
+
+.bubble-assistant {
+ align-self: flex-start;
+}
+
+.bubble-system {
+ align-self: center;
+ background: #fff4e6;
+ border-color: #ffcf99;
+ color: #8a5200;
+ max-width: 100%;
+}
+
+.composer {
+ display: flex;
+ gap: 10px;
+}
+
+.composer input {
+ flex: 1;
+ border: 1px solid var(--line);
+ border-radius: 12px;
+ padding: 12px 14px;
+ font-size: 1rem;
+}
+
+.composer input:focus {
+ outline: none;
+ border-color: var(--teal);
+}
+
+.send-button {
+ border: none;
+ background: var(--teal);
+ color: #fff;
+ border-radius: 12px;
+ padding: 12px 22px;
+ font-weight: 700;
+ cursor: pointer;
+}
+
+.send-button:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+@keyframes fade-in {
+ from {
+ opacity: 0;
+ transform: translateY(6px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
diff --git a/python/samples/05-end-to-end/ag_ui_single_agent/frontend/src/vite-env.d.ts b/python/samples/05-end-to-end/ag_ui_single_agent/frontend/src/vite-env.d.ts
new file mode 100644
index 00000000000..948b8a3ecfd
--- /dev/null
+++ b/python/samples/05-end-to-end/ag_ui_single_agent/frontend/src/vite-env.d.ts
@@ -0,0 +1,3 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+///
diff --git a/python/samples/05-end-to-end/ag_ui_single_agent/frontend/tsconfig.json b/python/samples/05-end-to-end/ag_ui_single_agent/frontend/tsconfig.json
new file mode 100644
index 00000000000..d1faad8f5f1
--- /dev/null
+++ b/python/samples/05-end-to-end/ag_ui_single_agent/frontend/tsconfig.json
@@ -0,0 +1,20 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+ "moduleResolution": "Bundler",
+ "allowImportingTsExtensions": false,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true
+ },
+ "include": ["src"],
+ "references": [{ "path": "./tsconfig.node.json" }]
+}
diff --git a/python/samples/05-end-to-end/ag_ui_single_agent/frontend/tsconfig.node.json b/python/samples/05-end-to-end/ag_ui_single_agent/frontend/tsconfig.node.json
new file mode 100644
index 00000000000..6c68264f0b3
--- /dev/null
+++ b/python/samples/05-end-to-end/ag_ui_single_agent/frontend/tsconfig.node.json
@@ -0,0 +1,13 @@
+{
+ "compilerOptions": {
+ "composite": true,
+ "target": "ES2020",
+ "lib": ["ES2020"],
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "allowSyntheticDefaultImports": true,
+ "types": ["node"],
+ "skipLibCheck": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/python/samples/05-end-to-end/ag_ui_single_agent/frontend/vite.config.ts b/python/samples/05-end-to-end/ag_ui_single_agent/frontend/vite.config.ts
new file mode 100644
index 00000000000..e8620a6bb68
--- /dev/null
+++ b/python/samples/05-end-to-end/ag_ui_single_agent/frontend/vite.config.ts
@@ -0,0 +1,12 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+import { defineConfig } from "vite";
+import react from "@vitejs/plugin-react";
+
+export default defineConfig({
+ plugins: [react()],
+ server: {
+ host: "127.0.0.1",
+ port: 5173,
+ },
+});
diff --git a/python/samples/README.md b/python/samples/README.md
index 4a585db7813..769a12e6f64 100644
--- a/python/samples/README.md
+++ b/python/samples/README.md
@@ -10,7 +10,7 @@ This directory contains samples demonstrating the capabilities of Microsoft Agen
| [`02-agents/`](./02-agents/) | Deep-dive by concept: tools, middleware, providers, orchestrations |
| [`03-workflows/`](./03-workflows/) | Workflow patterns: sequential, concurrent, state, declarative, explicit output designation |
| [`04-hosting/`](./04-hosting/) | Deployment: Azure Functions, Durable Tasks, A2A |
-| [`05-end-to-end/`](./05-end-to-end/) | Full applications, evaluation, demos |
+| [`05-end-to-end/`](./05-end-to-end/) | Full applications, evaluation, demos, including the [AG-UI single-agent demo](./05-end-to-end/ag_ui_single_agent/) using `FoundryChatClient` |
## Getting Started