mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: Improve DevUI, add Context Inspector view as new tab under traces (#2742)
* Improve DevUI, add Context Inspector view as new tab under traces * fix mypy errors * fix: Handle stale MCP connections in DevUI executor MCP tools can become stale when HTTP streaming responses end - the underlying stdio streams close but `is_connected` remains True. This causes subsequent requests to fail with `ClosedResourceError`. Add `_ensure_mcp_connections()` to detect and reconnect stale MCP tools before agent execution. This is a workaround for an upstream Agent Framework issue where connection state isn't properly tracked. Fixes MCP tools failing on second HTTP request in DevUI. fixes #1476 #1515 #2865 * fix #1572 report import dependency errors more clearly * Ensure there is streaming toggle where users can select streaming vs non streaming mode in devui . Fixes .NET: [Python] DevUI tool call rendering in non-streaming mode? * remove unused dead code * improve ux - workflows with agents show a chat component in execution timelien, also ensure magentic final output shows correctly * update ui build * update devui to use instrumentation instead of tracing, other instrumentation and type/instance check fixes
This commit is contained in:
committed by
GitHub
Unverified
parent
db283cd396
commit
2e1189ca65
@@ -102,12 +102,32 @@ agents/
|
||||
└── .env # Optional: shared environment variables
|
||||
```
|
||||
|
||||
## Viewing Telemetry (Otel Traces) in DevUI
|
||||
### Importing from External Modules
|
||||
|
||||
Agent Framework emits OpenTelemetry (Otel) traces for various operations. You can view these traces in DevUI by enabling tracing when starting the server.
|
||||
If your agents import tools or utilities from sibling directories (e.g., `from tools.helpers import my_tool`), you must set `PYTHONPATH` to include the parent directory:
|
||||
|
||||
```bash
|
||||
devui ./agents --tracing framework
|
||||
# Project structure:
|
||||
# backend/
|
||||
# ├── agents/
|
||||
# │ └── my_agent/
|
||||
# │ └── agent.py # contains: from tools.helpers import my_tool
|
||||
# └── tools/
|
||||
# └── helpers.py
|
||||
|
||||
# Run from project root with PYTHONPATH
|
||||
cd backend
|
||||
PYTHONPATH=. devui ./agents --port 8080
|
||||
```
|
||||
|
||||
Without `PYTHONPATH`, Python cannot find modules in sibling directories and DevUI will report an import error.
|
||||
|
||||
## Viewing Telemetry (Otel Traces) in DevUI
|
||||
|
||||
Agent Framework emits OpenTelemetry (Otel) traces for various operations. You can view these traces in DevUI by enabling instrumentation when starting the server.
|
||||
|
||||
```bash
|
||||
devui ./agents --instrumentation
|
||||
```
|
||||
|
||||
## OpenAI-Compatible API
|
||||
@@ -196,11 +216,12 @@ Options:
|
||||
--port, -p Port (default: 8080)
|
||||
--host Host (default: 127.0.0.1)
|
||||
--headless API only, no UI
|
||||
--config YAML config file
|
||||
--tracing none|framework|workflow|all
|
||||
--no-open Don't automatically open browser
|
||||
--instrumentation Enable OpenTelemetry instrumentation
|
||||
--reload Enable auto-reload
|
||||
--mode developer|user (default: developer)
|
||||
--auth Enable Bearer token authentication
|
||||
--auth-token Custom authentication token
|
||||
```
|
||||
|
||||
### UI Modes
|
||||
|
||||
@@ -94,7 +94,7 @@ def serve(
|
||||
auto_open: bool = False,
|
||||
cors_origins: list[str] | None = None,
|
||||
ui_enabled: bool = True,
|
||||
tracing_enabled: bool = False,
|
||||
instrumentation_enabled: bool = False,
|
||||
mode: str = "developer",
|
||||
auth_enabled: bool = False,
|
||||
auth_token: str | None = None,
|
||||
@@ -109,7 +109,7 @@ def serve(
|
||||
auto_open: Whether to automatically open browser
|
||||
cors_origins: List of allowed CORS origins
|
||||
ui_enabled: Whether to enable the UI
|
||||
tracing_enabled: Whether to enable OpenTelemetry tracing
|
||||
instrumentation_enabled: Whether to enable OpenTelemetry instrumentation
|
||||
mode: Server mode - 'developer' (full access, verbose errors) or 'user' (restricted APIs, generic errors)
|
||||
auth_enabled: Whether to enable Bearer token authentication
|
||||
auth_token: Custom authentication token (auto-generated if not provided with auth_enabled=True)
|
||||
@@ -172,22 +172,12 @@ def serve(
|
||||
os.environ["AUTH_REQUIRED"] = "true"
|
||||
os.environ["DEVUI_AUTH_TOKEN"] = auth_token
|
||||
|
||||
# Configure tracing environment variables if enabled
|
||||
if tracing_enabled:
|
||||
import os
|
||||
# Enable instrumentation if requested
|
||||
if instrumentation_enabled:
|
||||
from agent_framework.observability import enable_instrumentation
|
||||
|
||||
# Only set if not already configured by user
|
||||
if not os.environ.get("ENABLE_INSTRUMENTATION"):
|
||||
os.environ["ENABLE_INSTRUMENTATION"] = "true"
|
||||
logger.info("Set ENABLE_INSTRUMENTATION=true for tracing")
|
||||
|
||||
if not os.environ.get("ENABLE_SENSITIVE_DATA"):
|
||||
os.environ["ENABLE_SENSITIVE_DATA"] = "true"
|
||||
logger.info("Set ENABLE_SENSITIVE_DATA=true for tracing")
|
||||
|
||||
if not os.environ.get("OTLP_ENDPOINT"):
|
||||
os.environ["OTLP_ENDPOINT"] = "http://localhost:4317"
|
||||
logger.info("Set OTLP_ENDPOINT=http://localhost:4317 for tracing")
|
||||
enable_instrumentation(enable_sensitive_data=True)
|
||||
logger.info("Enabled Agent Framework instrumentation with sensitive data")
|
||||
|
||||
# Create server with direct parameters
|
||||
server = DevServer(
|
||||
|
||||
@@ -28,7 +28,7 @@ Examples:
|
||||
devui ./agents # Scan specific directory
|
||||
devui --port 8000 # Custom port
|
||||
devui --headless # API only, no UI
|
||||
devui --tracing # Enable OpenTelemetry tracing
|
||||
devui --instrumentation # Enable OpenTelemetry instrumentation
|
||||
""",
|
||||
)
|
||||
|
||||
@@ -53,7 +53,7 @@ Examples:
|
||||
|
||||
parser.add_argument("--reload", action="store_true", help="Enable auto-reload for development")
|
||||
|
||||
parser.add_argument("--tracing", action="store_true", help="Enable OpenTelemetry tracing for Agent Framework")
|
||||
parser.add_argument("--instrumentation", action="store_true", help="Enable OpenTelemetry instrumentation")
|
||||
|
||||
parser.add_argument(
|
||||
"--mode",
|
||||
@@ -182,7 +182,7 @@ def main() -> None:
|
||||
host=args.host,
|
||||
auto_open=not args.no_open,
|
||||
ui_enabled=ui_enabled,
|
||||
tracing_enabled=args.tracing,
|
||||
instrumentation_enabled=args.instrumentation,
|
||||
mode=mode,
|
||||
auth_enabled=args.auth,
|
||||
auth_token=args.auth_token, # Pass through explicit token only
|
||||
|
||||
@@ -176,6 +176,31 @@ class ConversationStore(ABC):
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def add_trace(self, conversation_id: str, trace_event: dict[str, Any]) -> None:
|
||||
"""Add a trace event to the conversation for context inspection.
|
||||
|
||||
Traces capture execution metadata like token usage, timing, and LLM context
|
||||
that isn't stored in the AgentThread but is useful for debugging.
|
||||
|
||||
Args:
|
||||
conversation_id: Conversation ID
|
||||
trace_event: Trace event data (from ResponseTraceEvent.data)
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_traces(self, conversation_id: str) -> list[dict[str, Any]]:
|
||||
"""Get all trace events for a conversation.
|
||||
|
||||
Args:
|
||||
conversation_id: Conversation ID
|
||||
|
||||
Returns:
|
||||
List of trace event dicts, or empty list if not found
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class InMemoryConversationStore(ConversationStore):
|
||||
"""In-memory conversation storage wrapping AgentThread.
|
||||
@@ -215,6 +240,7 @@ class InMemoryConversationStore(ConversationStore):
|
||||
"metadata": metadata or {},
|
||||
"created_at": created_at,
|
||||
"items": [],
|
||||
"traces": [], # Trace events for context inspection (token usage, timing, etc.)
|
||||
}
|
||||
|
||||
# Initialize item index for this conversation
|
||||
@@ -407,10 +433,20 @@ class InMemoryConversationStore(ConversationStore):
|
||||
elif content_type == "function_result":
|
||||
# Function result - create separate ConversationItem
|
||||
call_id = getattr(content, "call_id", None)
|
||||
# Output is stored in additional_properties
|
||||
output = ""
|
||||
if hasattr(content, "additional_properties"):
|
||||
output = content.additional_properties.get("output", "")
|
||||
# Output is stored in the 'result' field of FunctionResultContent
|
||||
result_value = getattr(content, "result", None)
|
||||
# Convert result to string (it could be dict, list, or other types)
|
||||
if result_value is None:
|
||||
output = ""
|
||||
elif isinstance(result_value, str):
|
||||
output = result_value
|
||||
else:
|
||||
import json
|
||||
|
||||
try:
|
||||
output = json.dumps(result_value)
|
||||
except (TypeError, ValueError):
|
||||
output = str(result_value)
|
||||
|
||||
if call_id:
|
||||
function_results.append(
|
||||
@@ -556,6 +592,34 @@ class InMemoryConversationStore(ConversationStore):
|
||||
conv_data = self._conversations.get(conversation_id)
|
||||
return conv_data["thread"] if conv_data else None
|
||||
|
||||
def add_trace(self, conversation_id: str, trace_event: dict[str, Any]) -> None:
|
||||
"""Add a trace event to the conversation for context inspection.
|
||||
|
||||
Traces capture execution metadata like token usage, timing, and LLM context
|
||||
that isn't stored in the AgentThread but is useful for debugging.
|
||||
|
||||
Args:
|
||||
conversation_id: Conversation ID
|
||||
trace_event: Trace event data (from ResponseTraceEvent.data)
|
||||
"""
|
||||
conv_data = self._conversations.get(conversation_id)
|
||||
if conv_data:
|
||||
traces = conv_data.get("traces", [])
|
||||
traces.append(trace_event)
|
||||
conv_data["traces"] = traces
|
||||
|
||||
def get_traces(self, conversation_id: str) -> list[dict[str, Any]]:
|
||||
"""Get all trace events for a conversation.
|
||||
|
||||
Args:
|
||||
conversation_id: Conversation ID
|
||||
|
||||
Returns:
|
||||
List of trace event dicts, or empty list if not found
|
||||
"""
|
||||
conv_data = self._conversations.get(conversation_id)
|
||||
return conv_data.get("traces", []) if conv_data else []
|
||||
|
||||
async def list_conversations_by_metadata(self, metadata_filter: dict[str, str]) -> list[Conversation]:
|
||||
"""Filter conversations by metadata (e.g., agent_id)."""
|
||||
results = []
|
||||
|
||||
@@ -666,7 +666,16 @@ class EntityDiscovery:
|
||||
logger.debug(f"Successfully imported {pattern}")
|
||||
return module, None
|
||||
|
||||
except ModuleNotFoundError:
|
||||
except ModuleNotFoundError as e:
|
||||
# Distinguish between "module pattern doesn't exist" vs "module has import errors"
|
||||
# If the missing module is the pattern itself, it's just not found (try next pattern)
|
||||
# If the missing module is something else (a dependency), capture the error
|
||||
missing_module = getattr(e, "name", None)
|
||||
if missing_module and missing_module != pattern and not pattern.endswith(f".{missing_module}"):
|
||||
# The module exists but has an import error (missing dependency)
|
||||
logger.warning(f"Error importing {pattern}: {e}")
|
||||
return None, e
|
||||
# The module pattern itself doesn't exist - this is expected, try next pattern
|
||||
logger.debug(f"Import pattern {pattern} not found")
|
||||
return None, None
|
||||
except Exception as e:
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from collections.abc import AsyncGenerator
|
||||
from typing import Any
|
||||
|
||||
@@ -45,8 +44,8 @@ class AgentFrameworkExecutor:
|
||||
"""
|
||||
self.entity_discovery = entity_discovery
|
||||
self.message_mapper = message_mapper
|
||||
self._setup_tracing_provider()
|
||||
self._setup_agent_framework_tracing()
|
||||
self._setup_instrumentation_provider()
|
||||
self._setup_agent_framework_instrumentation()
|
||||
|
||||
# Use provided conversation store or default to in-memory
|
||||
self.conversation_store = conversation_store or InMemoryConversationStore()
|
||||
@@ -56,7 +55,7 @@ class AgentFrameworkExecutor:
|
||||
|
||||
self.checkpoint_manager = CheckpointConversationManager(self.conversation_store)
|
||||
|
||||
def _setup_tracing_provider(self) -> None:
|
||||
def _setup_instrumentation_provider(self) -> None:
|
||||
"""Set up our own TracerProvider so we can add processors."""
|
||||
try:
|
||||
from opentelemetry import trace
|
||||
@@ -71,7 +70,7 @@ class AgentFrameworkExecutor:
|
||||
})
|
||||
provider = TracerProvider(resource=resource)
|
||||
trace.set_tracer_provider(provider)
|
||||
logger.info("Set up TracerProvider for server tracing")
|
||||
logger.info("Set up TracerProvider for instrumentation")
|
||||
else:
|
||||
logger.debug("TracerProvider already exists")
|
||||
|
||||
@@ -80,25 +79,86 @@ class AgentFrameworkExecutor:
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to setup TracerProvider: {e}")
|
||||
|
||||
def _setup_agent_framework_tracing(self) -> None:
|
||||
"""Set up Agent Framework's built-in tracing."""
|
||||
# Configure Agent Framework tracing only if ENABLE_INSTRUMENTATION is set
|
||||
if os.environ.get("ENABLE_INSTRUMENTATION"):
|
||||
try:
|
||||
from agent_framework.observability import OBSERVABILITY_SETTINGS, configure_otel_providers
|
||||
def _setup_agent_framework_instrumentation(self) -> None:
|
||||
"""Set up Agent Framework's built-in instrumentation."""
|
||||
try:
|
||||
from agent_framework.observability import OBSERVABILITY_SETTINGS, configure_otel_providers
|
||||
|
||||
# Only configure if not already executed
|
||||
# Configure if instrumentation is enabled (via enable_instrumentation() or env var)
|
||||
if OBSERVABILITY_SETTINGS.ENABLED:
|
||||
# Only configure providers if not already executed
|
||||
if not OBSERVABILITY_SETTINGS._executed_setup:
|
||||
# Run the configure_otel_providers
|
||||
# This ensures OTLP exporters are created even if env vars were set late
|
||||
configure_otel_providers(enable_sensitive_data=True)
|
||||
# Call configure_otel_providers to set up exporters.
|
||||
# If OTEL_EXPORTER_OTLP_ENDPOINT is set, exporters will be created automatically.
|
||||
# If not set, no exporters are created (no console spam), but DevUI's
|
||||
# TracerProvider from _setup_instrumentation_provider() remains active for local capture.
|
||||
configure_otel_providers(enable_sensitive_data=OBSERVABILITY_SETTINGS.SENSITIVE_DATA_ENABLED)
|
||||
logger.info("Enabled Agent Framework observability")
|
||||
else:
|
||||
logger.debug("Agent Framework observability already configured")
|
||||
else:
|
||||
logger.debug("Instrumentation not enabled, skipping observability setup")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to enable Agent Framework observability: {e}")
|
||||
|
||||
async def _ensure_mcp_connections(self, agent: Any) -> None:
|
||||
"""Ensure MCP tool connections are healthy before agent execution.
|
||||
|
||||
This is a workaround for an Agent Framework bug where MCP tool connections
|
||||
can become stale (underlying streams closed) but is_connected remains True.
|
||||
This happens when HTTP streaming responses end and GeneratorExit propagates.
|
||||
|
||||
This method detects stale connections and reconnects them. It's designed to
|
||||
be a no-op once the Agent Framework fixes this issue upstream.
|
||||
|
||||
Args:
|
||||
agent: Agent object that may have MCP tools
|
||||
"""
|
||||
if not hasattr(agent, "_local_mcp_tools"):
|
||||
return
|
||||
|
||||
for mcp_tool in agent._local_mcp_tools:
|
||||
if not getattr(mcp_tool, "is_connected", False):
|
||||
continue
|
||||
|
||||
tool_name = getattr(mcp_tool, "name", "unknown")
|
||||
|
||||
try:
|
||||
# Check if underlying write stream is closed
|
||||
session = getattr(mcp_tool, "session", None)
|
||||
if session is None:
|
||||
continue
|
||||
|
||||
write_stream = getattr(session, "_write_stream", None)
|
||||
if write_stream is None:
|
||||
continue
|
||||
|
||||
# Detect stale connection: is_connected=True but stream is closed
|
||||
is_closed = getattr(write_stream, "_closed", False)
|
||||
if not is_closed:
|
||||
continue # Connection is healthy
|
||||
|
||||
# Stale connection detected - reconnect
|
||||
logger.warning(f"MCP tool '{tool_name}' has stale connection (stream closed), reconnecting...")
|
||||
|
||||
# Clean up old connection
|
||||
try:
|
||||
if hasattr(mcp_tool, "close"):
|
||||
await mcp_tool.close()
|
||||
except Exception as close_err:
|
||||
logger.debug(f"Error closing stale MCP tool '{tool_name}': {close_err}")
|
||||
# Force reset state
|
||||
mcp_tool.is_connected = False
|
||||
mcp_tool.session = None
|
||||
|
||||
# Reconnect
|
||||
if hasattr(mcp_tool, "connect"):
|
||||
await mcp_tool.connect()
|
||||
logger.info(f"MCP tool '{tool_name}' reconnected successfully")
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to enable Agent Framework observability: {e}")
|
||||
else:
|
||||
logger.debug("ENABLE_INSTRUMENTATION not set, skipping observability setup")
|
||||
# If detection fails, log and continue - let it fail naturally during execution
|
||||
logger.debug(f"Error checking MCP tool '{tool_name}' connection: {e}")
|
||||
|
||||
async def discover_entities(self) -> list[EntityInfo]:
|
||||
"""Discover all available entities.
|
||||
@@ -192,11 +252,11 @@ class AgentFrameworkExecutor:
|
||||
|
||||
logger.info(f"Executing {entity_info.type}: {entity_id}")
|
||||
|
||||
# Extract session_id from request for trace context
|
||||
session_id = getattr(request.extra_body, "session_id", None) if request.extra_body else None
|
||||
# Extract response_id from request for trace context (added by _server.py)
|
||||
response_id = request.extra_body.get("response_id") if request.extra_body else None
|
||||
|
||||
# Use simplified trace capture
|
||||
with capture_traces(session_id=session_id, entity_id=entity_id) as trace_collector:
|
||||
with capture_traces(response_id=response_id, entity_id=entity_id) as trace_collector:
|
||||
if entity_info.type == "agent":
|
||||
async for event in self._execute_agent(entity_obj, request, trace_collector):
|
||||
yield event
|
||||
@@ -260,6 +320,12 @@ class AgentFrameworkExecutor:
|
||||
logger.debug(f"Executing agent with text input: {user_message[:100]}...")
|
||||
else:
|
||||
logger.debug(f"Executing agent with multimodal ChatMessage: {type(user_message)}")
|
||||
|
||||
# Workaround for MCP tool stale connection bug (GitHub issue pending)
|
||||
# When HTTP streaming ends, GeneratorExit can close MCP stdio streams
|
||||
# but is_connected stays True. Detect and reconnect before execution.
|
||||
await self._ensure_mcp_connections(agent)
|
||||
|
||||
# Check if agent supports streaming
|
||||
if hasattr(agent, "run_stream") and callable(agent.run_stream):
|
||||
# Use Agent Framework's native streaming with optional thread
|
||||
|
||||
@@ -12,6 +12,7 @@ from datetime import datetime
|
||||
from typing import Any, Union
|
||||
from uuid import uuid4
|
||||
|
||||
from agent_framework import ChatMessage, TextContent
|
||||
from openai.types.responses import (
|
||||
Response,
|
||||
ResponseContentPartAddedEvent,
|
||||
@@ -225,27 +226,128 @@ class MessageMapper:
|
||||
Final aggregated OpenAI response
|
||||
"""
|
||||
try:
|
||||
# Extract text content from events
|
||||
content_parts = []
|
||||
# Collect output items in order
|
||||
output_items: list[Any] = []
|
||||
|
||||
# Track text content parts per message (keyed by item_id)
|
||||
text_parts_by_message: dict[str, list[str]] = {}
|
||||
|
||||
# Track function calls (keyed by call_id) to accumulate arguments
|
||||
function_calls: dict[str, dict[str, Any]] = {}
|
||||
|
||||
# Track function results (keyed by call_id)
|
||||
function_results: dict[str, dict[str, Any]] = {}
|
||||
|
||||
for event in events:
|
||||
# Extract delta text from ResponseTextDeltaEvent
|
||||
if hasattr(event, "delta") and hasattr(event, "type") and event.type == "response.output_text.delta":
|
||||
content_parts.append(event.delta)
|
||||
event_type = getattr(event, "type", None)
|
||||
|
||||
# Combine content
|
||||
full_content = "".join(content_parts)
|
||||
# Handle text deltas - accumulate text per message
|
||||
if event_type == "response.output_text.delta":
|
||||
item_id = getattr(event, "item_id", "default")
|
||||
if item_id not in text_parts_by_message:
|
||||
text_parts_by_message[item_id] = []
|
||||
text_parts_by_message[item_id].append(event.delta)
|
||||
|
||||
# Create proper OpenAI Response
|
||||
response_output_text = ResponseOutputText(type="output_text", text=full_content, annotations=[])
|
||||
# Handle output_item.added events (function_call, message, etc.)
|
||||
elif event_type == "response.output_item.added":
|
||||
item = getattr(event, "item", None)
|
||||
if item:
|
||||
# Handle both object and dict formats
|
||||
item_type = item.get("type") if isinstance(item, dict) else getattr(item, "type", None)
|
||||
|
||||
response_output_message = ResponseOutputMessage(
|
||||
type="message",
|
||||
role="assistant",
|
||||
content=[response_output_text],
|
||||
id=f"msg_{uuid.uuid4().hex[:8]}",
|
||||
status="completed",
|
||||
)
|
||||
# Track function calls to accumulate their arguments
|
||||
if item_type == "function_call":
|
||||
# Handle both object and dict formats
|
||||
if isinstance(item, dict):
|
||||
call_id = item.get("call_id") or item.get("id")
|
||||
if call_id:
|
||||
function_calls[call_id] = {
|
||||
"id": item.get("id", call_id),
|
||||
"call_id": call_id,
|
||||
"name": item.get("name", ""),
|
||||
"arguments": item.get("arguments", ""),
|
||||
"type": "function_call",
|
||||
"status": item.get("status", "completed"),
|
||||
}
|
||||
else:
|
||||
call_id = getattr(item, "call_id", None) or getattr(item, "id", None)
|
||||
if call_id:
|
||||
function_calls[call_id] = {
|
||||
"id": getattr(item, "id", call_id),
|
||||
"call_id": call_id,
|
||||
"name": getattr(item, "name", ""),
|
||||
"arguments": getattr(item, "arguments", ""),
|
||||
"type": "function_call",
|
||||
"status": getattr(item, "status", "completed"),
|
||||
}
|
||||
|
||||
# Other output items (message, etc.) - track for later
|
||||
elif item_type == "message":
|
||||
# Messages will be built from text_parts_by_message
|
||||
pass
|
||||
|
||||
# Handle function call arguments delta - accumulate arguments
|
||||
elif event_type == "response.function_call_arguments.delta":
|
||||
item_id = getattr(event, "item_id", None)
|
||||
delta = getattr(event, "delta", "")
|
||||
# item_id for function calls is the call_id
|
||||
if item_id and item_id in function_calls:
|
||||
function_calls[item_id]["arguments"] += delta
|
||||
|
||||
# Handle function result complete events
|
||||
elif event_type == "response.function_result.complete":
|
||||
call_id = getattr(event, "call_id", None)
|
||||
if call_id:
|
||||
function_results[call_id] = {
|
||||
"type": "function_call_output",
|
||||
"call_id": call_id,
|
||||
"output": getattr(event, "output", ""),
|
||||
"status": getattr(event, "status", "completed"),
|
||||
}
|
||||
|
||||
# Build output array in order: function_calls, then final message
|
||||
|
||||
# Add function call items
|
||||
for _call_id, fc_data in function_calls.items():
|
||||
output_items.append(ResponseFunctionToolCall(**fc_data))
|
||||
|
||||
# Note: function_call_output items are NOT added to output array
|
||||
# In OpenAI's Responses API, function results are user inputs, not assistant outputs
|
||||
# The function_results dict is kept for potential future use or debugging
|
||||
# but we don't include them in the Response output
|
||||
_ = function_results # Acknowledge but don't use
|
||||
|
||||
# Build final text message from accumulated deltas
|
||||
# Combine all text parts (usually there's just one message)
|
||||
all_text_parts = []
|
||||
for _item_id, parts in text_parts_by_message.items():
|
||||
all_text_parts.extend(parts)
|
||||
|
||||
full_content = "".join(all_text_parts)
|
||||
|
||||
# Only add message if there's text content
|
||||
if full_content:
|
||||
response_output_text = ResponseOutputText(type="output_text", text=full_content, annotations=[])
|
||||
response_output_message = ResponseOutputMessage(
|
||||
type="message",
|
||||
role="assistant",
|
||||
content=[response_output_text],
|
||||
id=f"msg_{uuid.uuid4().hex[:8]}",
|
||||
status="completed",
|
||||
)
|
||||
output_items.append(response_output_message)
|
||||
|
||||
# If no output items at all, create an empty message
|
||||
if not output_items:
|
||||
response_output_text = ResponseOutputText(type="output_text", text="", annotations=[])
|
||||
response_output_message = ResponseOutputMessage(
|
||||
type="message",
|
||||
role="assistant",
|
||||
content=[response_output_text],
|
||||
id=f"msg_{uuid.uuid4().hex[:8]}",
|
||||
status="completed",
|
||||
)
|
||||
output_items.append(response_output_message)
|
||||
|
||||
# Get usage from accumulator (OpenAI standard)
|
||||
request_id = str(id(request))
|
||||
@@ -278,7 +380,7 @@ class MessageMapper:
|
||||
object="response",
|
||||
created_at=datetime.now().timestamp(),
|
||||
model=request.model or "devui",
|
||||
output=[response_output_message],
|
||||
output=output_items,
|
||||
usage=usage,
|
||||
parallel_tool_calls=False,
|
||||
tool_choice="none",
|
||||
@@ -501,7 +603,7 @@ class MessageMapper:
|
||||
return events
|
||||
|
||||
# Check if we're streaming text content
|
||||
has_text_content = any(content.__class__.__name__ == "TextContent" for content in update.contents)
|
||||
has_text_content = any(isinstance(content, TextContent) for content in update.contents)
|
||||
|
||||
# Check if we're in an executor context with an existing item
|
||||
executor_id = context.get("current_executor_id")
|
||||
@@ -791,17 +893,35 @@ class MessageMapper:
|
||||
|
||||
# Extract text from output data based on type
|
||||
text = None
|
||||
if hasattr(output_data, "__class__") and output_data.__class__.__name__ == "ChatMessage":
|
||||
if isinstance(output_data, ChatMessage):
|
||||
# Handle ChatMessage (from Magentic and AgentExecutor with output_response=True)
|
||||
text = getattr(output_data, "text", None)
|
||||
if not text:
|
||||
# Fallback to string representation
|
||||
text = str(output_data)
|
||||
elif isinstance(output_data, list):
|
||||
# Handle list of ChatMessage objects (from Magentic yield_output([final_answer]))
|
||||
text_parts = []
|
||||
for item in output_data:
|
||||
if isinstance(item, ChatMessage):
|
||||
item_text = getattr(item, "text", None)
|
||||
if item_text:
|
||||
text_parts.append(item_text)
|
||||
else:
|
||||
text_parts.append(str(item))
|
||||
elif isinstance(item, str):
|
||||
text_parts.append(item)
|
||||
else:
|
||||
try:
|
||||
text_parts.append(json.dumps(item, indent=2))
|
||||
except (TypeError, ValueError):
|
||||
text_parts.append(str(item))
|
||||
text = "\n".join(text_parts) if text_parts else str(output_data)
|
||||
elif isinstance(output_data, str):
|
||||
# String output
|
||||
text = output_data
|
||||
else:
|
||||
# Object/dict/list → JSON string
|
||||
# Object/dict → JSON string
|
||||
try:
|
||||
text = json.dumps(output_data, indent=2)
|
||||
except (TypeError, ValueError):
|
||||
@@ -1081,275 +1201,6 @@ class MessageMapper:
|
||||
|
||||
return [trace_event]
|
||||
|
||||
# Handle Magentic-specific events
|
||||
if event_class == "MagenticAgentDeltaEvent":
|
||||
agent_id = getattr(event, "agent_id", "unknown_agent")
|
||||
text = getattr(event, "text", None)
|
||||
|
||||
if text:
|
||||
# Check if we're inside an executor - route to executor's item
|
||||
# This prevents duplicate timeline entries (executor + inner agent)
|
||||
current_executor_id = context.get("current_executor_id")
|
||||
executor_item_key = f"exec_item_{current_executor_id}" if current_executor_id else None
|
||||
|
||||
if executor_item_key and executor_item_key in context:
|
||||
# Route delta to the executor's item instead of creating a new message item
|
||||
item_id = context[executor_item_key]
|
||||
|
||||
# Emit text delta event routed to the executor's item
|
||||
return [
|
||||
ResponseTextDeltaEvent(
|
||||
type="response.output_text.delta",
|
||||
output_index=context.get("output_index", 0),
|
||||
content_index=0,
|
||||
item_id=item_id,
|
||||
delta=text,
|
||||
logprobs=[],
|
||||
sequence_number=self._next_sequence(context),
|
||||
)
|
||||
]
|
||||
|
||||
# Fallback: No executor context - create separate message item (original behavior)
|
||||
# This handles cases where MagenticAgentDeltaEvent is emitted outside an executor
|
||||
events = []
|
||||
|
||||
# Track Magentic agent messages separately from regular messages
|
||||
# Use timestamp to ensure uniqueness for multiple runs of same agent
|
||||
magentic_key = f"magentic_message_{agent_id}"
|
||||
|
||||
# Check if this is the first delta from this agent (need to create message container)
|
||||
if magentic_key not in context:
|
||||
# Create a unique message ID for this agent's streaming session
|
||||
message_id = f"msg_{agent_id}_{uuid4().hex[:8]}"
|
||||
context[magentic_key] = message_id
|
||||
context["output_index"] = context.get("output_index", -1) + 1
|
||||
|
||||
# Import required types for creating message containers
|
||||
from openai.types.responses import ResponseOutputMessage, ResponseOutputText
|
||||
from openai.types.responses.response_content_part_added_event import (
|
||||
ResponseContentPartAddedEvent,
|
||||
)
|
||||
from openai.types.responses.response_output_item_added_event import ResponseOutputItemAddedEvent
|
||||
|
||||
# Emit message output item (container for the agent's message)
|
||||
# This matches what _convert_agent_update does for regular agents
|
||||
events.append(
|
||||
ResponseOutputItemAddedEvent(
|
||||
type="response.output_item.added",
|
||||
output_index=context["output_index"],
|
||||
sequence_number=self._next_sequence(context),
|
||||
item=ResponseOutputMessage(
|
||||
type="message",
|
||||
id=message_id,
|
||||
role="assistant",
|
||||
content=[],
|
||||
status="in_progress",
|
||||
# Add metadata to identify this as a Magentic agent message
|
||||
metadata={"agent_id": agent_id, "source": "magentic"}, # type: ignore[call-arg]
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
# Add content part for text (establishes the text container)
|
||||
events.append(
|
||||
ResponseContentPartAddedEvent(
|
||||
type="response.content_part.added",
|
||||
output_index=context["output_index"],
|
||||
content_index=0,
|
||||
item_id=message_id,
|
||||
sequence_number=self._next_sequence(context),
|
||||
part=ResponseOutputText(type="output_text", text="", annotations=[]),
|
||||
)
|
||||
)
|
||||
|
||||
# Get the message ID for this agent
|
||||
message_id = context[magentic_key]
|
||||
|
||||
# Emit text delta event using the message ID (matches regular agent behavior)
|
||||
events.append(
|
||||
ResponseTextDeltaEvent(
|
||||
type="response.output_text.delta",
|
||||
output_index=context["output_index"],
|
||||
content_index=0, # Always 0 for single text content
|
||||
item_id=message_id,
|
||||
delta=text,
|
||||
logprobs=[],
|
||||
sequence_number=self._next_sequence(context),
|
||||
)
|
||||
)
|
||||
return events
|
||||
|
||||
# Handle function calls from Magentic agents
|
||||
if getattr(event, "function_call_id", None) and getattr(event, "function_call_name", None):
|
||||
# Handle function call initiation
|
||||
function_call_id = getattr(event, "function_call_id", None)
|
||||
function_call_name = getattr(event, "function_call_name", None)
|
||||
function_call_arguments = getattr(event, "function_call_arguments", None)
|
||||
|
||||
# Track function call for accumulating arguments
|
||||
context["active_function_calls"][function_call_id] = {
|
||||
"item_id": function_call_id,
|
||||
"name": function_call_name,
|
||||
"arguments_chunks": [],
|
||||
}
|
||||
|
||||
# Emit function call output item
|
||||
return [
|
||||
ResponseOutputItemAddedEvent(
|
||||
type="response.output_item.added",
|
||||
item=ResponseFunctionToolCall(
|
||||
id=function_call_id,
|
||||
call_id=function_call_id,
|
||||
name=function_call_name,
|
||||
arguments=json.dumps(function_call_arguments) if function_call_arguments else "",
|
||||
type="function_call",
|
||||
status="in_progress",
|
||||
),
|
||||
output_index=context["output_index"],
|
||||
sequence_number=self._next_sequence(context),
|
||||
)
|
||||
]
|
||||
|
||||
# For other non-text deltas, emit as trace for debugging
|
||||
return [
|
||||
ResponseTraceEventComplete(
|
||||
type="response.trace.completed",
|
||||
data={
|
||||
"trace_type": "magentic_delta",
|
||||
"agent_id": agent_id,
|
||||
"function_call_id": getattr(event, "function_call_id", None),
|
||||
"function_call_name": getattr(event, "function_call_name", None),
|
||||
"function_result_id": getattr(event, "function_result_id", None),
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
},
|
||||
span_id=f"magentic_delta_{uuid4().hex[:8]}",
|
||||
item_id=context["item_id"],
|
||||
output_index=context.get("output_index", 0),
|
||||
sequence_number=self._next_sequence(context),
|
||||
)
|
||||
]
|
||||
|
||||
if event_class == "MagenticAgentMessageEvent":
|
||||
agent_id = getattr(event, "agent_id", "unknown_agent")
|
||||
message = getattr(event, "message", None)
|
||||
|
||||
# Check if we're inside an executor - if so, deltas were already routed there
|
||||
# We don't need to emit a separate message completion event
|
||||
current_executor_id = context.get("current_executor_id")
|
||||
executor_item_key = f"exec_item_{current_executor_id}" if current_executor_id else None
|
||||
|
||||
if executor_item_key and executor_item_key in context:
|
||||
# Deltas were routed to executor item - no separate message item to complete
|
||||
# The executor's output_item.done will mark completion
|
||||
logger.debug(
|
||||
f"MagenticAgentMessageEvent from {agent_id} - "
|
||||
f"deltas routed to executor {current_executor_id}, skipping"
|
||||
)
|
||||
return []
|
||||
|
||||
# Fallback: Handle case where we created a separate message item (no executor context)
|
||||
magentic_key = f"magentic_message_{agent_id}"
|
||||
|
||||
# Check if we were streaming for this agent
|
||||
if magentic_key in context:
|
||||
# Mark the streaming message as complete
|
||||
message_id = context[magentic_key]
|
||||
|
||||
# Import required types
|
||||
from openai.types.responses import ResponseOutputMessage
|
||||
from openai.types.responses.response_output_item_done_event import ResponseOutputItemDoneEvent
|
||||
|
||||
# Extract text from ChatMessage for the completed message
|
||||
text = None
|
||||
if message and hasattr(message, "text"):
|
||||
text = message.text
|
||||
|
||||
# Emit output_item.done to mark message as complete
|
||||
events = [
|
||||
ResponseOutputItemDoneEvent(
|
||||
type="response.output_item.done",
|
||||
output_index=context["output_index"],
|
||||
sequence_number=self._next_sequence(context),
|
||||
item=ResponseOutputMessage(
|
||||
type="message",
|
||||
id=message_id,
|
||||
role="assistant",
|
||||
content=[], # Content already streamed via deltas
|
||||
status="completed",
|
||||
metadata={"agent_id": agent_id, "source": "magentic"}, # type: ignore[call-arg]
|
||||
),
|
||||
)
|
||||
]
|
||||
|
||||
# Clean up context for this agent
|
||||
del context[magentic_key]
|
||||
|
||||
logger.debug(f"MagenticAgentMessageEvent from {agent_id} marked streaming message as complete")
|
||||
return events
|
||||
# No streaming occurred, create a complete message (shouldn't happen normally)
|
||||
# Extract text from ChatMessage
|
||||
text = None
|
||||
if message and hasattr(message, "text"):
|
||||
text = message.text
|
||||
|
||||
if text:
|
||||
# Emit as output item for this agent
|
||||
from openai.types.responses import ResponseOutputMessage, ResponseOutputText
|
||||
from openai.types.responses.response_output_item_added_event import ResponseOutputItemAddedEvent
|
||||
|
||||
context["output_index"] = context.get("output_index", -1) + 1
|
||||
|
||||
text_content = ResponseOutputText(type="output_text", text=text, annotations=[])
|
||||
|
||||
output_message = ResponseOutputMessage(
|
||||
type="message",
|
||||
id=f"msg_{agent_id}_{uuid4().hex[:8]}",
|
||||
role="assistant",
|
||||
content=[text_content],
|
||||
status="completed",
|
||||
metadata={"agent_id": agent_id, "source": "magentic"}, # type: ignore[call-arg]
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
f"MagenticAgentMessageEvent from {agent_id} converted to output_item.added (non-streaming)"
|
||||
)
|
||||
return [
|
||||
ResponseOutputItemAddedEvent(
|
||||
type="response.output_item.added",
|
||||
item=output_message,
|
||||
output_index=context["output_index"],
|
||||
sequence_number=self._next_sequence(context),
|
||||
)
|
||||
]
|
||||
|
||||
if event_class == "MagenticOrchestratorMessageEvent":
|
||||
orchestrator_id = getattr(event, "orchestrator_id", "orchestrator")
|
||||
message = getattr(event, "message", None)
|
||||
kind = getattr(event, "kind", "unknown")
|
||||
|
||||
# Extract text from ChatMessage
|
||||
text = None
|
||||
if message and hasattr(message, "text"):
|
||||
text = message.text
|
||||
|
||||
# Emit as trace event for orchestrator messages (typically task ledger, instructions)
|
||||
return [
|
||||
ResponseTraceEventComplete(
|
||||
type="response.trace.completed",
|
||||
data={
|
||||
"trace_type": "magentic_orchestrator",
|
||||
"orchestrator_id": orchestrator_id,
|
||||
"kind": kind,
|
||||
"text": text or "",
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
},
|
||||
span_id=f"magentic_orch_{uuid4().hex[:8]}",
|
||||
item_id=context["item_id"],
|
||||
output_index=context.get("output_index", 0),
|
||||
sequence_number=self._next_sequence(context),
|
||||
)
|
||||
]
|
||||
|
||||
# For unknown/legacy events, still emit as workflow event for backward compatibility
|
||||
# Get event data and serialize if it's a SerializationMixin
|
||||
raw_event_data = getattr(event, "data", None)
|
||||
|
||||
@@ -407,7 +407,7 @@ class DevServer:
|
||||
framework="agent_framework",
|
||||
runtime="python", # Python DevUI backend
|
||||
capabilities={
|
||||
"tracing": os.getenv("ENABLE_INSTRUMENTATION") == "true",
|
||||
"instrumentation": os.getenv("ENABLE_INSTRUMENTATION") == "true",
|
||||
"openai_proxy": openai_executor.is_configured,
|
||||
"deployment": True, # Deployment feature is available
|
||||
},
|
||||
@@ -748,6 +748,11 @@ class DevServer:
|
||||
response_id = f"resp_{uuid.uuid4().hex[:8]}"
|
||||
logger.info(f"[CANCELLATION] Creating response {response_id} for entity {entity_id}")
|
||||
|
||||
# Inject response_id into extra_body for trace context
|
||||
if request.extra_body is None:
|
||||
request.extra_body = {}
|
||||
request.extra_body["response_id"] = response_id
|
||||
|
||||
return StreamingResponse(
|
||||
self._stream_with_cancellation(executor, request, response_id),
|
||||
media_type="text/event-stream",
|
||||
@@ -1000,10 +1005,16 @@ class DevServer:
|
||||
logger.warning(f"Unexpected item type: {type(item)}, converting to dict")
|
||||
serialized_items.append(dict(item))
|
||||
|
||||
# Get stored traces for context inspection (DevUI extension)
|
||||
traces = executor.conversation_store.get_traces(conversation_id)
|
||||
|
||||
return {
|
||||
"object": "list",
|
||||
"data": serialized_items,
|
||||
"has_more": has_more,
|
||||
"metadata": {
|
||||
"traces": traces, # Trace events for token usage, timing, LLM context
|
||||
},
|
||||
}
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e)) from e
|
||||
@@ -1080,10 +1091,22 @@ class DevServer:
|
||||
# Collect events for final response.completed event
|
||||
events = []
|
||||
|
||||
# Get conversation_id for trace storage
|
||||
conversation_id = request._get_conversation_id()
|
||||
|
||||
# Stream all events
|
||||
async for event in executor.execute_streaming(request):
|
||||
events.append(event)
|
||||
|
||||
# Store trace events for context inspection (persisted with conversation)
|
||||
if conversation_id and hasattr(event, "type") and event.type == "response.trace.completed":
|
||||
try:
|
||||
trace_data = event.data if hasattr(event, "data") else None
|
||||
if trace_data:
|
||||
executor.conversation_store.add_trace(conversation_id, trace_data)
|
||||
except Exception as e:
|
||||
logger.debug(f"Failed to store trace event: {e}")
|
||||
|
||||
# IMPORTANT: Check model_dump_json FIRST because to_json() can have newlines (pretty-printing)
|
||||
# which breaks SSE format. model_dump_json() returns single-line JSON.
|
||||
if hasattr(event, "model_dump_json"):
|
||||
|
||||
@@ -18,14 +18,14 @@ logger = logging.getLogger(__name__)
|
||||
class SimpleTraceCollector(SpanExporter):
|
||||
"""Simple trace collector that captures spans for direct yielding."""
|
||||
|
||||
def __init__(self, session_id: str | None = None, entity_id: str | None = None) -> None:
|
||||
def __init__(self, response_id: str | None = None, entity_id: str | None = None) -> None:
|
||||
"""Initialize trace collector.
|
||||
|
||||
Args:
|
||||
session_id: Session identifier for context
|
||||
response_id: Response identifier for grouping traces by turn
|
||||
entity_id: Entity identifier for context
|
||||
"""
|
||||
self.session_id = session_id
|
||||
self.response_id = response_id
|
||||
self.entity_id = entity_id
|
||||
self.collected_events: list[ResponseTraceEvent] = []
|
||||
|
||||
@@ -93,7 +93,7 @@ class SimpleTraceCollector(SpanExporter):
|
||||
"duration_ms": duration_ms,
|
||||
"attributes": dict(span.attributes) if span.attributes else {},
|
||||
"status": str(span.status.status_code) if hasattr(span, "status") else "OK",
|
||||
"session_id": self.session_id,
|
||||
"response_id": self.response_id,
|
||||
"entity_id": self.entity_id,
|
||||
}
|
||||
|
||||
@@ -121,18 +121,18 @@ class SimpleTraceCollector(SpanExporter):
|
||||
|
||||
@contextmanager
|
||||
def capture_traces(
|
||||
session_id: str | None = None, entity_id: str | None = None
|
||||
response_id: str | None = None, entity_id: str | None = None
|
||||
) -> Generator[SimpleTraceCollector, None, None]:
|
||||
"""Context manager to capture traces during execution.
|
||||
|
||||
Args:
|
||||
session_id: Session identifier for context
|
||||
response_id: Response identifier for grouping traces by turn
|
||||
entity_id: Entity identifier for context
|
||||
|
||||
Yields:
|
||||
SimpleTraceCollector instance to get trace events from
|
||||
"""
|
||||
collector = SimpleTraceCollector(session_id, entity_id)
|
||||
collector = SimpleTraceCollector(response_id, entity_id)
|
||||
|
||||
try:
|
||||
from opentelemetry import trace
|
||||
@@ -146,7 +146,7 @@ def capture_traces(
|
||||
# Check if this is a real TracerProvider (not the default NoOpTracerProvider)
|
||||
if isinstance(provider, TracerProvider):
|
||||
provider.add_span_processor(processor)
|
||||
logger.debug(f"Added trace collector to TracerProvider for session: {session_id}, entity: {entity_id}")
|
||||
logger.debug(f"Added trace collector to TracerProvider for response: {response_id}, entity: {entity_id}")
|
||||
|
||||
try:
|
||||
yield collector
|
||||
|
||||
@@ -390,7 +390,7 @@ class MetaResponse(BaseModel):
|
||||
"""Backend runtime/language - 'python' or 'dotnet' for deployment guides and feature availability."""
|
||||
|
||||
capabilities: dict[str, bool] = {}
|
||||
"""Server capabilities (e.g., tracing, openai_proxy)."""
|
||||
"""Server capabilities (e.g., instrumentation, openai_proxy)."""
|
||||
|
||||
auth_required: bool = False
|
||||
"""Whether the server requires Bearer token authentication."""
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -19,5 +19,13 @@ export default tseslint.config([
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
rules: {
|
||||
// Allow exporting constants alongside components in specific patterns
|
||||
// This is common for shadcn/ui components (buttonVariants) and form utilities
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true }
|
||||
],
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
+4272
File diff suppressed because it is too large
Load Diff
@@ -19,6 +19,7 @@
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@tailwindcss/vite": "^4.1.12",
|
||||
"@xyflow/react": "^12.8.4",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
|
||||
@@ -270,6 +270,7 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) {
|
||||
const conversationUsage = useDevUIStore((state) => state.conversationUsage);
|
||||
const pendingApprovals = useDevUIStore((state) => state.pendingApprovals);
|
||||
const oaiMode = useDevUIStore((state) => state.oaiMode);
|
||||
const streamingEnabled = useDevUIStore((state) => state.streamingEnabled);
|
||||
|
||||
// Get conversation actions from Zustand (only the ones we actually use)
|
||||
const setCurrentConversation = useDevUIStore((state) => state.setCurrentConversation);
|
||||
@@ -570,6 +571,7 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) {
|
||||
let allItems: unknown[] = [];
|
||||
let hasMore = true;
|
||||
let after: string | undefined = undefined;
|
||||
let storedTraces: unknown[] = [];
|
||||
|
||||
while (hasMore) {
|
||||
const result = await apiClient.listConversationItems(
|
||||
@@ -578,7 +580,12 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) {
|
||||
);
|
||||
allItems = allItems.concat(result.data);
|
||||
hasMore = result.has_more;
|
||||
|
||||
|
||||
// Capture traces from metadata (only need from one response, they accumulate)
|
||||
if (result.metadata?.traces && result.metadata.traces.length > 0) {
|
||||
storedTraces = result.metadata.traces;
|
||||
}
|
||||
|
||||
// Get the last item's ID for pagination
|
||||
if (hasMore && result.data.length > 0) {
|
||||
const lastItem = result.data[result.data.length - 1] as { id?: string };
|
||||
@@ -590,6 +597,21 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) {
|
||||
setChatItems(allItems as import("@/types/openai").ConversationItem[]);
|
||||
setIsStreaming(false);
|
||||
|
||||
// Restore stored traces as debug events for context inspection
|
||||
if (storedTraces.length > 0) {
|
||||
// Clear any previous debug events first
|
||||
onDebugEvent("clear");
|
||||
for (const trace of storedTraces) {
|
||||
// Convert stored trace back to ResponseTraceComplete event format
|
||||
const traceEvent: ExtendedResponseStreamEvent = {
|
||||
type: "response.trace.completed",
|
||||
data: trace as Record<string, unknown>,
|
||||
sequence_number: 0, // Not used for display
|
||||
};
|
||||
onDebugEvent(traceEvent);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for incomplete stream and resume if needed
|
||||
const state = loadStreamingState(mostRecent.id);
|
||||
|
||||
@@ -724,6 +746,9 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) {
|
||||
useDevUIStore.setState({ conversationUsage: { total_tokens: 0, message_count: 0 } });
|
||||
accumulatedTextRef.current = "";
|
||||
|
||||
// Clear debug panel for fresh conversation
|
||||
onDebugEvent("clear");
|
||||
|
||||
// Update localStorage cache with new conversation
|
||||
const cachedKey = `devui_convs_${selectedAgent.id}`;
|
||||
const updated = [newConversation, ...availableConversations];
|
||||
@@ -736,7 +761,7 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) {
|
||||
type: "conversation_creation_error",
|
||||
});
|
||||
}
|
||||
}, [selectedAgent, setCurrentConversation, setAvailableConversations, setChatItems, setIsStreaming]);
|
||||
}, [selectedAgent, onDebugEvent, setCurrentConversation, setAvailableConversations, setChatItems, setIsStreaming]);
|
||||
|
||||
// Handle conversation deletion
|
||||
const handleDeleteConversation = useCallback(
|
||||
@@ -843,6 +868,7 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) {
|
||||
let allItems: unknown[] = [];
|
||||
let hasMore = true;
|
||||
let after: string | undefined = undefined;
|
||||
let storedTraces: unknown[] = [];
|
||||
|
||||
while (hasMore) {
|
||||
const result = await apiClient.listConversationItems(conversationId, {
|
||||
@@ -851,7 +877,12 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) {
|
||||
});
|
||||
allItems = allItems.concat(result.data);
|
||||
hasMore = result.has_more;
|
||||
|
||||
|
||||
// Capture traces from metadata (only need from one response, they accumulate)
|
||||
if (result.metadata?.traces && result.metadata.traces.length > 0) {
|
||||
storedTraces = result.metadata.traces;
|
||||
}
|
||||
|
||||
// Get the last item's ID for pagination
|
||||
if (hasMore && result.data.length > 0) {
|
||||
const lastItem = result.data[result.data.length - 1] as { id?: string };
|
||||
@@ -865,6 +896,19 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) {
|
||||
setChatItems(items);
|
||||
setIsStreaming(false);
|
||||
|
||||
// Restore stored traces as debug events for context inspection
|
||||
if (storedTraces.length > 0) {
|
||||
for (const trace of storedTraces) {
|
||||
// Convert stored trace back to ResponseTraceComplete event format
|
||||
const traceEvent: ExtendedResponseStreamEvent = {
|
||||
type: "response.trace.completed",
|
||||
data: trace as Record<string, unknown>,
|
||||
sequence_number: 0, // Not used for display
|
||||
};
|
||||
onDebugEvent(traceEvent);
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate usage from loaded items
|
||||
useDevUIStore.setState({
|
||||
conversationUsage: {
|
||||
@@ -1249,13 +1293,15 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) {
|
||||
|
||||
// Handle function calls as separate conversation items
|
||||
if (item.type === "function_call") {
|
||||
// Type assertion for function call - narrows from union type
|
||||
const funcCall = item as import("@/types/openai").ResponseFunctionToolCall;
|
||||
const functionCallItem: import("@/types/openai").ConversationFunctionCall = {
|
||||
id: item.id || `call-${Date.now()}`,
|
||||
id: funcCall.id || `call-${Date.now()}`,
|
||||
type: "function_call",
|
||||
name: item.name,
|
||||
arguments: item.arguments || "",
|
||||
call_id: item.call_id,
|
||||
status: (item.status === "failed" || item.status === "cancelled" ? "incomplete" : item.status) || "in_progress",
|
||||
name: funcCall.name,
|
||||
arguments: funcCall.arguments || "",
|
||||
call_id: funcCall.call_id,
|
||||
status: funcCall.status || "in_progress",
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
};
|
||||
|
||||
@@ -1414,6 +1460,209 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) {
|
||||
[selectedAgent, currentConversation, onDebugEvent, setChatItems, setIsStreaming, setCurrentConversation, setAvailableConversations, setPendingApprovals, updateConversationUsage, createAbortSignal, resetCancelling]
|
||||
);
|
||||
|
||||
// Handle non-streaming message sending
|
||||
const handleSendMessageSync = useCallback(
|
||||
async (request: RunAgentRequest) => {
|
||||
if (!selectedAgent) return;
|
||||
|
||||
// Check if this is a function approval response (internal, don't show in chat)
|
||||
const isApprovalResponse = request.input.some(
|
||||
(inputItem) =>
|
||||
inputItem.type === "message" &&
|
||||
Array.isArray(inputItem.content) &&
|
||||
inputItem.content.some((c) => c.type === "function_approval_response")
|
||||
);
|
||||
|
||||
// Extract content from OpenAI format to create ConversationMessage
|
||||
const messageContent: import("@/types/openai").MessageContent[] = [];
|
||||
|
||||
// Parse OpenAI ResponseInputParam to extract content
|
||||
for (const inputItem of request.input) {
|
||||
if (inputItem.type === "message" && Array.isArray(inputItem.content)) {
|
||||
for (const contentItem of inputItem.content) {
|
||||
if (contentItem.type === "input_text") {
|
||||
messageContent.push({
|
||||
type: "text",
|
||||
text: contentItem.text,
|
||||
});
|
||||
} else if (contentItem.type === "input_image") {
|
||||
messageContent.push({
|
||||
type: "input_image",
|
||||
image_url: contentItem.image_url || "",
|
||||
detail: "auto",
|
||||
});
|
||||
} else if (contentItem.type === "input_file") {
|
||||
const fileItem = contentItem as import("@/types/agent-framework").ResponseInputFileParam;
|
||||
messageContent.push({
|
||||
type: "input_file",
|
||||
file_data: fileItem.file_data,
|
||||
filename: fileItem.filename,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Capture timestamp once for both user and assistant messages
|
||||
const messageTimestamp = Math.floor(Date.now() / 1000); // Unix seconds
|
||||
|
||||
// Only add user message to UI if it's not an approval response (internal messages)
|
||||
if (!isApprovalResponse && messageContent.length > 0) {
|
||||
const userMessage: import("@/types/openai").ConversationMessage = {
|
||||
id: `user-${Date.now()}`,
|
||||
type: "message",
|
||||
role: "user",
|
||||
content: messageContent,
|
||||
status: "completed",
|
||||
created_at: messageTimestamp,
|
||||
};
|
||||
|
||||
setChatItems([...useDevUIStore.getState().chatItems, userMessage]);
|
||||
}
|
||||
|
||||
// Show loading state (but not streaming indicator)
|
||||
setIsSubmitting(true);
|
||||
|
||||
try {
|
||||
// If no conversation selected, create one automatically
|
||||
let conversationToUse = currentConversation;
|
||||
if (!conversationToUse) {
|
||||
try {
|
||||
conversationToUse = await apiClient.createConversation({
|
||||
agent_id: selectedAgent.id,
|
||||
});
|
||||
setCurrentConversation(conversationToUse);
|
||||
setAvailableConversations([conversationToUse, ...useDevUIStore.getState().availableConversations]);
|
||||
setConversationError(null);
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : "Failed to create conversation";
|
||||
setConversationError({
|
||||
message: errorMessage,
|
||||
type: "conversation_creation_error",
|
||||
});
|
||||
setIsSubmitting(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Call non-streaming API
|
||||
const response = await apiClient.runAgentSync(selectedAgent.id, {
|
||||
input: request.input,
|
||||
conversation_id: conversationToUse?.id,
|
||||
});
|
||||
|
||||
// Extract content from response output
|
||||
const assistantContent: import("@/types/openai").MessageContent[] = [];
|
||||
const toolCalls: import("@/types/openai").ConversationFunctionCall[] = [];
|
||||
const toolResults: import("@/types/openai").ConversationFunctionCallOutput[] = [];
|
||||
|
||||
if (response.output) {
|
||||
for (const outputItem of response.output) {
|
||||
if (outputItem.type === "message") {
|
||||
// Extract message content
|
||||
const msgItem = outputItem as import("@/types/openai").ResponseOutputMessage;
|
||||
if (msgItem.content) {
|
||||
for (const content of msgItem.content) {
|
||||
if (content.type === "output_text") {
|
||||
assistantContent.push({
|
||||
type: "text",
|
||||
text: (content as { text: string }).text,
|
||||
} as import("@/types/openai").MessageTextContent);
|
||||
} else if (content.type === "output_image") {
|
||||
assistantContent.push(content as unknown as import("@/types/openai").MessageOutputImage);
|
||||
} else if (content.type === "output_file") {
|
||||
assistantContent.push(content as unknown as import("@/types/openai").MessageOutputFile);
|
||||
} else if (content.type === "output_data") {
|
||||
assistantContent.push(content as unknown as import("@/types/openai").MessageOutputData);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (outputItem.type === "function_call") {
|
||||
const funcCall = outputItem as unknown as import("@/types/openai").ResponseFunctionToolCall;
|
||||
toolCalls.push({
|
||||
id: funcCall.id || `call-${Date.now()}`,
|
||||
type: "function_call",
|
||||
name: funcCall.name,
|
||||
arguments: funcCall.arguments || "",
|
||||
call_id: funcCall.call_id,
|
||||
status: funcCall.status || "completed",
|
||||
created_at: messageTimestamp,
|
||||
});
|
||||
} else if (outputItem.type === "function_call_output") {
|
||||
const resultItem = outputItem as unknown as { call_id: string; output: string };
|
||||
toolResults.push({
|
||||
id: `result-${Date.now()}`,
|
||||
type: "function_call_output",
|
||||
call_id: resultItem.call_id,
|
||||
output: resultItem.output,
|
||||
status: "completed",
|
||||
created_at: messageTimestamp,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create assistant message with all content
|
||||
const assistantMessage: import("@/types/openai").ConversationMessage = {
|
||||
id: `assistant-${Date.now()}`,
|
||||
type: "message",
|
||||
role: "assistant",
|
||||
content: assistantContent,
|
||||
status: "completed",
|
||||
created_at: messageTimestamp,
|
||||
usage: response.usage ? {
|
||||
input_tokens: response.usage.input_tokens,
|
||||
output_tokens: response.usage.output_tokens,
|
||||
total_tokens: response.usage.total_tokens,
|
||||
} : undefined,
|
||||
};
|
||||
|
||||
// Add all items to chat
|
||||
const currentItems = useDevUIStore.getState().chatItems;
|
||||
const newItems: import("@/types/openai").ConversationItem[] = [
|
||||
...currentItems,
|
||||
assistantMessage,
|
||||
...toolCalls,
|
||||
...toolResults,
|
||||
];
|
||||
setChatItems(newItems);
|
||||
|
||||
// Update conversation-level usage stats
|
||||
if (response.usage) {
|
||||
updateConversationUsage(response.usage.total_tokens);
|
||||
}
|
||||
|
||||
// Send debug event with response completed
|
||||
onDebugEvent({
|
||||
type: "response.completed",
|
||||
response: response,
|
||||
sequence_number: 0,
|
||||
} as ExtendedResponseStreamEvent);
|
||||
|
||||
} catch (error) {
|
||||
// Show error message
|
||||
const errorMessage = error instanceof Error ? error.message : "Failed to get response";
|
||||
const assistantMessage: import("@/types/openai").ConversationMessage = {
|
||||
id: `assistant-${Date.now()}`,
|
||||
type: "message",
|
||||
role: "assistant",
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `Error: ${errorMessage}`,
|
||||
} as import("@/types/openai").MessageTextContent],
|
||||
status: "incomplete",
|
||||
created_at: messageTimestamp,
|
||||
};
|
||||
|
||||
const currentItems = useDevUIStore.getState().chatItems;
|
||||
setChatItems([...currentItems, assistantMessage]);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
},
|
||||
[selectedAgent, currentConversation, onDebugEvent, setChatItems, setCurrentConversation, setAvailableConversations, updateConversationUsage, setIsSubmitting]
|
||||
);
|
||||
|
||||
|
||||
// Handle message submission from ChatMessageInput
|
||||
const handleChatInputSubmit = async (content: import("@/types/agent-framework").ResponseInputContent[]) => {
|
||||
@@ -1435,11 +1684,17 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) {
|
||||
},
|
||||
];
|
||||
|
||||
// Use pure OpenAI format
|
||||
await handleSendMessage({
|
||||
const request = {
|
||||
input: openaiInput,
|
||||
conversation_id: currentConversation?.id,
|
||||
});
|
||||
};
|
||||
|
||||
// Use streaming or non-streaming based on setting
|
||||
if (streamingEnabled) {
|
||||
await handleSendMessage(request);
|
||||
} else {
|
||||
await handleSendMessageSync(request);
|
||||
}
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,949 @@
|
||||
/**
|
||||
* ContextInspector - Token usage visualization and context analysis
|
||||
*
|
||||
* Features:
|
||||
* - Stacked bar chart showing input/output tokens per turn
|
||||
* - Composition view showing what fills the context (system, user, assistant, tools)
|
||||
* - Per-turn vs cumulative modes
|
||||
* - Summary statistics (total, average, peak)
|
||||
* - Pure CSS visualization (no external charting library)
|
||||
*/
|
||||
|
||||
import { useState, useMemo } from "react";
|
||||
import { useDevUIStore } from "@/stores/devuiStore";
|
||||
import {
|
||||
BarChart3,
|
||||
Layers,
|
||||
Info,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
} from "lucide-react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import type { ExtendedResponseStreamEvent } from "@/types";
|
||||
import {
|
||||
TraceAttributes,
|
||||
type TypedTraceAttributes,
|
||||
type TraceMessage,
|
||||
parseTraceMessages,
|
||||
isTextPart,
|
||||
isToolCallPart,
|
||||
isToolResultPart,
|
||||
} from "@/types/openai";
|
||||
|
||||
// Trace data interface matching debug-panel types
|
||||
interface TraceEventData {
|
||||
operation_name?: string;
|
||||
duration_ms?: number;
|
||||
status?: string;
|
||||
attributes?: TypedTraceAttributes;
|
||||
span_id?: string;
|
||||
trace_id?: string;
|
||||
parent_span_id?: string | null;
|
||||
start_time?: number;
|
||||
end_time?: number;
|
||||
entity_id?: string;
|
||||
response_id?: string | null;
|
||||
}
|
||||
|
||||
// Context composition breakdown
|
||||
interface ContextComposition {
|
||||
system: number; // character count
|
||||
user: number;
|
||||
assistant: number;
|
||||
toolCalls: number; // function definitions + arguments
|
||||
toolResults: number; // function outputs
|
||||
total: number;
|
||||
}
|
||||
|
||||
// Turn data extracted from traces
|
||||
interface TurnData {
|
||||
response_id: string;
|
||||
timestamp: number;
|
||||
input_tokens: number;
|
||||
output_tokens: number;
|
||||
total_tokens: number;
|
||||
model?: string;
|
||||
entity_id?: string;
|
||||
duration_ms: number;
|
||||
composition: ContextComposition;
|
||||
}
|
||||
|
||||
// Props for the component
|
||||
interface ContextInspectorProps {
|
||||
events: ExtendedResponseStreamEvent[];
|
||||
}
|
||||
|
||||
// Parse message content to extract composition using typed TraceMessage format
|
||||
function parseComposition(messagesJson: string | unknown): ContextComposition {
|
||||
const composition: ContextComposition = {
|
||||
system: 0,
|
||||
user: 0,
|
||||
assistant: 0,
|
||||
toolCalls: 0,
|
||||
toolResults: 0,
|
||||
total: 0,
|
||||
};
|
||||
|
||||
try {
|
||||
// Use the typed parser for string input
|
||||
let messages: TraceMessage[];
|
||||
|
||||
if (typeof messagesJson === "string") {
|
||||
messages = parseTraceMessages(messagesJson);
|
||||
} else if (Array.isArray(messagesJson)) {
|
||||
messages = messagesJson as TraceMessage[];
|
||||
} else {
|
||||
return composition;
|
||||
}
|
||||
|
||||
for (const message of messages) {
|
||||
if (!message || typeof message !== "object") continue;
|
||||
|
||||
const role = message.role;
|
||||
const parts = message.parts;
|
||||
|
||||
// Calculate character count for this message
|
||||
let charCount = 0;
|
||||
|
||||
// Handle parts array (Agent Framework format)
|
||||
// Using type guards for type-safe access to part properties
|
||||
if (Array.isArray(parts)) {
|
||||
for (const part of parts) {
|
||||
if (!part || typeof part !== "object") continue;
|
||||
|
||||
if (isTextPart(part)) {
|
||||
// Text content can be in either 'content' or 'text' field
|
||||
const text = part.content || part.text || "";
|
||||
charCount += text.length;
|
||||
} else if (isToolCallPart(part)) {
|
||||
// Tool call includes name and arguments
|
||||
const name = part.name || "";
|
||||
const args = part.arguments || "";
|
||||
composition.toolCalls += name.length + args.length;
|
||||
} else if (isToolResultPart(part)) {
|
||||
// Tool result - check both 'result' and 'response' fields
|
||||
const result = part.result || part.response || "";
|
||||
composition.toolResults += result.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Categorize by role
|
||||
if (role === "system") {
|
||||
composition.system += charCount;
|
||||
} else if (role === "user") {
|
||||
composition.user += charCount;
|
||||
} else if (role === "assistant") {
|
||||
composition.assistant += charCount;
|
||||
} else if (role === "tool") {
|
||||
composition.toolResults += charCount;
|
||||
}
|
||||
}
|
||||
|
||||
composition.total =
|
||||
composition.system +
|
||||
composition.user +
|
||||
composition.assistant +
|
||||
composition.toolCalls +
|
||||
composition.toolResults;
|
||||
|
||||
} catch {
|
||||
// Parsing failed, return empty composition
|
||||
}
|
||||
|
||||
return composition;
|
||||
}
|
||||
|
||||
// Extract turn data from trace events
|
||||
function extractTurnData(events: ExtendedResponseStreamEvent[]): TurnData[] {
|
||||
const traceEvents = events.filter(e => e.type === "response.trace.completed");
|
||||
|
||||
// Group by response_id
|
||||
const byResponseId = new Map<string, TraceEventData[]>();
|
||||
|
||||
for (const event of traceEvents) {
|
||||
if (!("data" in event)) continue;
|
||||
const data = event.data as TraceEventData;
|
||||
const responseId = data.response_id || "unknown";
|
||||
|
||||
if (!byResponseId.has(responseId)) {
|
||||
byResponseId.set(responseId, []);
|
||||
}
|
||||
byResponseId.get(responseId)!.push(data);
|
||||
}
|
||||
|
||||
const turns: TurnData[] = [];
|
||||
|
||||
for (const [responseId, traces] of byResponseId) {
|
||||
let inputTokens = 0;
|
||||
let outputTokens = 0;
|
||||
let model: string | undefined;
|
||||
let timestamp = Date.now() / 1000;
|
||||
let entity_id: string | undefined;
|
||||
let totalDuration = 0;
|
||||
let composition: ContextComposition = {
|
||||
system: 0, user: 0, assistant: 0, toolCalls: 0, toolResults: 0, total: 0
|
||||
};
|
||||
|
||||
for (const trace of traces) {
|
||||
const attrs = trace.attributes || {};
|
||||
|
||||
// Get token counts using typed attribute keys
|
||||
const traceInput = attrs[TraceAttributes.INPUT_TOKENS];
|
||||
const traceOutput = attrs[TraceAttributes.OUTPUT_TOKENS];
|
||||
|
||||
if (traceInput !== undefined) {
|
||||
inputTokens += Number(traceInput);
|
||||
}
|
||||
if (traceOutput !== undefined) {
|
||||
outputTokens += Number(traceOutput);
|
||||
}
|
||||
|
||||
// Get model using typed attribute key
|
||||
if (attrs[TraceAttributes.MODEL]) {
|
||||
model = String(attrs[TraceAttributes.MODEL]);
|
||||
}
|
||||
|
||||
// Get timestamp
|
||||
if (trace.start_time && trace.start_time < timestamp) {
|
||||
timestamp = trace.start_time;
|
||||
}
|
||||
|
||||
// Get entity_id
|
||||
if (trace.entity_id) {
|
||||
entity_id = trace.entity_id;
|
||||
}
|
||||
|
||||
// Sum durations
|
||||
if (trace.duration_ms) {
|
||||
totalDuration += Number(trace.duration_ms);
|
||||
}
|
||||
|
||||
// Parse composition from input messages using typed attribute key
|
||||
const inputMessages = attrs[TraceAttributes.INPUT_MESSAGES];
|
||||
if (inputMessages && composition.total === 0) {
|
||||
composition = parseComposition(inputMessages);
|
||||
}
|
||||
|
||||
// Also check for system instructions using typed attribute key
|
||||
const systemInstructions = attrs[TraceAttributes.SYSTEM_INSTRUCTIONS];
|
||||
if (systemInstructions && typeof systemInstructions === "string" && composition.system === 0) {
|
||||
composition.system = systemInstructions.length;
|
||||
composition.total += systemInstructions.length;
|
||||
}
|
||||
}
|
||||
|
||||
// Only include turns that have token data
|
||||
if (inputTokens > 0 || outputTokens > 0) {
|
||||
turns.push({
|
||||
response_id: responseId,
|
||||
timestamp,
|
||||
input_tokens: inputTokens,
|
||||
output_tokens: outputTokens,
|
||||
total_tokens: inputTokens + outputTokens,
|
||||
model,
|
||||
entity_id,
|
||||
duration_ms: totalDuration,
|
||||
composition,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by timestamp (oldest first)
|
||||
turns.sort((a, b) => a.timestamp - b.timestamp);
|
||||
|
||||
return turns;
|
||||
}
|
||||
|
||||
// Calculate summary stats
|
||||
function calculateStats(turns: TurnData[]) {
|
||||
if (turns.length === 0) {
|
||||
return {
|
||||
totalInput: 0,
|
||||
totalOutput: 0,
|
||||
totalTokens: 0,
|
||||
avgInput: 0,
|
||||
avgOutput: 0,
|
||||
avgTotal: 0,
|
||||
peakInput: 0,
|
||||
peakOutput: 0,
|
||||
peakTotal: 0,
|
||||
turnCount: 0,
|
||||
};
|
||||
}
|
||||
|
||||
const totalInput = turns.reduce((sum, t) => sum + t.input_tokens, 0);
|
||||
const totalOutput = turns.reduce((sum, t) => sum + t.output_tokens, 0);
|
||||
const totalTokens = totalInput + totalOutput;
|
||||
|
||||
const peakInput = Math.max(...turns.map(t => t.input_tokens));
|
||||
const peakOutput = Math.max(...turns.map(t => t.output_tokens));
|
||||
const peakTotal = Math.max(...turns.map(t => t.total_tokens));
|
||||
|
||||
return {
|
||||
totalInput,
|
||||
totalOutput,
|
||||
totalTokens,
|
||||
avgInput: Math.round(totalInput / turns.length),
|
||||
avgOutput: Math.round(totalOutput / turns.length),
|
||||
avgTotal: Math.round(totalTokens / turns.length),
|
||||
peakInput,
|
||||
peakOutput,
|
||||
peakTotal,
|
||||
turnCount: turns.length,
|
||||
};
|
||||
}
|
||||
|
||||
// Aggregate composition across all turns
|
||||
function aggregateComposition(turns: TurnData[]): ContextComposition {
|
||||
return turns.reduce(
|
||||
(acc, turn) => ({
|
||||
system: acc.system + turn.composition.system,
|
||||
user: acc.user + turn.composition.user,
|
||||
assistant: acc.assistant + turn.composition.assistant,
|
||||
toolCalls: acc.toolCalls + turn.composition.toolCalls,
|
||||
toolResults: acc.toolResults + turn.composition.toolResults,
|
||||
total: acc.total + turn.composition.total,
|
||||
}),
|
||||
{ system: 0, user: 0, assistant: 0, toolCalls: 0, toolResults: 0, total: 0 }
|
||||
);
|
||||
}
|
||||
|
||||
// Format large numbers with K suffix
|
||||
function formatTokenCount(n: number): string {
|
||||
if (n >= 1000) {
|
||||
return `${(n / 1000).toFixed(1)}k`;
|
||||
}
|
||||
return String(n);
|
||||
}
|
||||
|
||||
// Color constants - single source of truth for all visualizations
|
||||
const SEGMENT_COLORS = {
|
||||
// Token segments
|
||||
input: "bg-blue-500 dark:bg-blue-600",
|
||||
output: "bg-emerald-500 dark:bg-emerald-600",
|
||||
// Composition segments
|
||||
system: "bg-purple-500 dark:bg-purple-600",
|
||||
user: "bg-blue-500 dark:bg-blue-600",
|
||||
assistant: "bg-emerald-500 dark:bg-emerald-600",
|
||||
toolCalls: "bg-amber-500 dark:bg-amber-600",
|
||||
toolResults: "bg-orange-500 dark:bg-orange-600",
|
||||
} as const;
|
||||
|
||||
// Segment definition for the unified bar component
|
||||
interface BarSegment {
|
||||
key: string;
|
||||
value: number;
|
||||
color: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
// Unified segmented bar component with tooltips
|
||||
// Replaces both TokenBar and CompositionBar for consistency and maintainability
|
||||
function SegmentedBar({
|
||||
segments,
|
||||
maxValue,
|
||||
height = 20,
|
||||
renderLabel,
|
||||
}: {
|
||||
segments: BarSegment[];
|
||||
maxValue: number;
|
||||
height?: number;
|
||||
renderLabel?: (total: number, segments: BarSegment[]) => React.ReactNode;
|
||||
}) {
|
||||
const total = segments.reduce((sum, s) => sum + s.value, 0);
|
||||
|
||||
if (total === 0) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 w-full">
|
||||
<div
|
||||
className="rounded bg-muted/30 flex-1"
|
||||
style={{ height: `${height}px` }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// When maxValue is 0, use full width (100%) - focus on ratios within the bar
|
||||
// When maxValue > 0, scale relative to max - focus on size comparison
|
||||
const widthPercent = maxValue > 0 ? (total / maxValue) * 100 : 100;
|
||||
|
||||
// Pre-compute segment metadata for tooltips
|
||||
const segmentsWithMeta = segments
|
||||
.filter(s => s.value > 0)
|
||||
.map(seg => ({
|
||||
...seg,
|
||||
percent: Math.round((seg.value / total) * 100),
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 w-full">
|
||||
<div
|
||||
className="relative rounded overflow-hidden bg-muted/30 flex-1"
|
||||
style={{ height: `${height}px` }}
|
||||
>
|
||||
<TooltipProvider delayDuration={150}>
|
||||
<div
|
||||
className="h-full flex transition-all duration-300"
|
||||
style={{ width: `${widthPercent}%` }}
|
||||
>
|
||||
{segmentsWithMeta.map((seg) => (
|
||||
<Tooltip key={seg.key}>
|
||||
<TooltipTrigger asChild>
|
||||
<div
|
||||
className={`h-full ${seg.color} transition-all duration-150 hover:brightness-110 hover:scale-y-[1.15] origin-bottom cursor-default`}
|
||||
style={{ width: `${(seg.value / total) * 100}%` }}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" className="text-xs">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className={`w-2 h-2 rounded-sm ${seg.color} flex-shrink-0`} />
|
||||
<span className="font-medium">{seg.label}</span>
|
||||
<span className="opacity-80">{formatTokenCount(seg.value)} ({seg.percent}%)</span>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
|
||||
{renderLabel?.(total, segments)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Helper to create token segments (input/output)
|
||||
function createTokenSegments(input: number, output: number): BarSegment[] {
|
||||
return [
|
||||
{ key: "input", value: input, color: SEGMENT_COLORS.input, label: "Input" },
|
||||
{ key: "output", value: output, color: SEGMENT_COLORS.output, label: "Output" },
|
||||
];
|
||||
}
|
||||
|
||||
// Helper to create composition segments
|
||||
function createCompositionSegments(composition: ContextComposition): BarSegment[] {
|
||||
return [
|
||||
{ key: "system", value: composition.system, color: SEGMENT_COLORS.system, label: "System" },
|
||||
{ key: "user", value: composition.user, color: SEGMENT_COLORS.user, label: "User" },
|
||||
{ key: "assistant", value: composition.assistant, color: SEGMENT_COLORS.assistant, label: "Assistant" },
|
||||
{ key: "toolCalls", value: composition.toolCalls, color: SEGMENT_COLORS.toolCalls, label: "Tool Calls" },
|
||||
{ key: "toolResults", value: composition.toolResults, color: SEGMENT_COLORS.toolResults, label: "Tool Results" },
|
||||
];
|
||||
}
|
||||
|
||||
// Composition breakdown list
|
||||
function CompositionBreakdown({
|
||||
composition,
|
||||
className = "",
|
||||
}: {
|
||||
composition: ContextComposition;
|
||||
className?: string;
|
||||
}) {
|
||||
const { system, user, assistant, toolCalls, toolResults, total } = composition;
|
||||
|
||||
if (total === 0) {
|
||||
return (
|
||||
<div className={`text-xs text-muted-foreground ${className}`}>
|
||||
No composition data available
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const items = [
|
||||
{ label: "System", value: system, color: SEGMENT_COLORS.system },
|
||||
{ label: "User", value: user, color: SEGMENT_COLORS.user },
|
||||
{ label: "Assistant", value: assistant, color: SEGMENT_COLORS.assistant },
|
||||
{ label: "Tool Calls", value: toolCalls, color: SEGMENT_COLORS.toolCalls },
|
||||
{ label: "Tool Results", value: toolResults, color: SEGMENT_COLORS.toolResults },
|
||||
].filter(item => item.value > 0);
|
||||
|
||||
return (
|
||||
<div className={`space-y-1.5 ${className}`}>
|
||||
{items.map((item) => {
|
||||
const percent = Math.round((item.value / total) * 100);
|
||||
return (
|
||||
<div key={item.label} className="flex items-center gap-2 text-xs">
|
||||
<div className={`w-2 h-2 rounded-sm ${item.color}`} />
|
||||
<span className="text-muted-foreground w-20">{item.label}</span>
|
||||
<div className="flex-1 h-3 bg-muted/30 rounded overflow-hidden">
|
||||
<div
|
||||
className={`h-full ${item.color} transition-all duration-300`}
|
||||
style={{ width: `${percent}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="font-mono w-10 text-right text-muted-foreground">
|
||||
{percent}%
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Turn row component
|
||||
function TurnRow({
|
||||
turn,
|
||||
index,
|
||||
maxValue,
|
||||
maxCompositionValue,
|
||||
cumulativeInput,
|
||||
cumulativeOutput,
|
||||
cumulativeComposition,
|
||||
showCumulative,
|
||||
viewMode,
|
||||
}: {
|
||||
turn: TurnData;
|
||||
index: number;
|
||||
maxValue: number;
|
||||
maxCompositionValue: number;
|
||||
cumulativeInput: number;
|
||||
cumulativeOutput: number;
|
||||
cumulativeComposition: ContextComposition;
|
||||
showCumulative: boolean;
|
||||
viewMode: "tokens" | "composition";
|
||||
}) {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
const displayInput = showCumulative ? cumulativeInput : turn.input_tokens;
|
||||
const displayOutput = showCumulative ? cumulativeOutput : turn.output_tokens;
|
||||
const displayComposition = showCumulative ? cumulativeComposition : turn.composition;
|
||||
|
||||
const timestamp = new Date(turn.timestamp * 1000).toLocaleTimeString([], {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="border-b border-muted/50 last:border-0">
|
||||
<div
|
||||
className="flex items-center gap-3 py-2 px-2 hover:bg-muted/30 cursor-pointer transition-colors"
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
>
|
||||
{/* Turn number */}
|
||||
<div className="w-6 h-6 rounded-full bg-muted flex items-center justify-center text-xs font-medium flex-shrink-0">
|
||||
{index + 1}
|
||||
</div>
|
||||
|
||||
{/* Bar */}
|
||||
<div className="flex-1 min-w-0">
|
||||
{viewMode === "tokens" ? (
|
||||
<SegmentedBar
|
||||
segments={createTokenSegments(displayInput, displayOutput)}
|
||||
maxValue={maxValue}
|
||||
height={20}
|
||||
renderLabel={(_, segs) => (
|
||||
<div className="flex items-center gap-1 text-xs font-mono text-muted-foreground min-w-[80px] justify-end">
|
||||
<span className="text-blue-600 dark:text-blue-400">↑{formatTokenCount(segs[0]?.value || 0)}</span>
|
||||
<span>/</span>
|
||||
<span className="text-emerald-600 dark:text-emerald-400">↓{formatTokenCount(segs[1]?.value || 0)}</span>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<SegmentedBar
|
||||
segments={createCompositionSegments(displayComposition)}
|
||||
maxValue={maxCompositionValue}
|
||||
height={20}
|
||||
renderLabel={(total) => (
|
||||
<div className="text-xs font-mono text-muted-foreground min-w-[50px] text-right">
|
||||
{formatTokenCount(Math.round(total / 4))}~
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Expand icon */}
|
||||
<div className="text-muted-foreground flex-shrink-0">
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Expanded details */}
|
||||
{isExpanded && (
|
||||
<div className="pb-3">
|
||||
{/* Connector line */}
|
||||
<div className="flex items-start gap-3 px-2">
|
||||
<div className="w-6 flex justify-center flex-shrink-0">
|
||||
<div className="w-px h-full bg-muted" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
{/* L-connector and composition */}
|
||||
<div className="flex items-start gap-2">
|
||||
<div className="text-muted-foreground text-xs mt-1">└─</div>
|
||||
<div className="flex-1 space-y-3">
|
||||
{/* Basic info */}
|
||||
<div className="grid grid-cols-2 gap-x-4 gap-y-1 text-xs text-muted-foreground">
|
||||
<div>Time: <span className="font-mono text-foreground">{timestamp}</span></div>
|
||||
<div>Duration: <span className="font-mono text-foreground">{turn.duration_ms.toFixed(0)}ms</span></div>
|
||||
{turn.model && (
|
||||
<div>Model: <span className="font-mono text-foreground">{turn.model}</span></div>
|
||||
)}
|
||||
{turn.entity_id && (
|
||||
<div>Entity: <span className="font-mono text-foreground">{turn.entity_id}</span></div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Token counts - shown in tokens mode */}
|
||||
{viewMode === "tokens" && (
|
||||
<div className="flex gap-4 text-xs">
|
||||
<div>
|
||||
<span className="text-blue-600 dark:text-blue-400">Input:</span>{" "}
|
||||
<span className="font-mono">{turn.input_tokens.toLocaleString()}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-emerald-600 dark:text-emerald-400">Output:</span>{" "}
|
||||
<span className="font-mono">{turn.output_tokens.toLocaleString()}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">Total:</span>{" "}
|
||||
<span className="font-mono">{turn.total_tokens.toLocaleString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Composition breakdown - shown in composition mode */}
|
||||
{viewMode === "composition" && turn.composition.total > 0 && (
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground mb-2 flex items-center gap-1">
|
||||
<Info className="h-3 w-3" />
|
||||
Context Composition (estimated from ~{formatTokenCount(Math.round(turn.composition.total / 4))} tokens)
|
||||
</div>
|
||||
<CompositionBreakdown composition={turn.composition} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Summary stats card
|
||||
function StatCard({
|
||||
label,
|
||||
value,
|
||||
icon: Icon,
|
||||
color = "default",
|
||||
}: {
|
||||
label: string;
|
||||
value: string | number;
|
||||
icon: typeof BarChart3;
|
||||
color?: "default" | "blue" | "green";
|
||||
}) {
|
||||
const colorClass = {
|
||||
default: "text-muted-foreground",
|
||||
blue: "text-blue-600 dark:text-blue-400",
|
||||
green: "text-emerald-600 dark:text-emerald-400",
|
||||
}[color];
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 p-2 bg-muted/30 rounded">
|
||||
<Icon className={`h-4 w-4 ${colorClass}`} />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-xs text-muted-foreground truncate">{label}</div>
|
||||
<div className="font-mono text-sm font-medium">{value}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Main component
|
||||
export function ContextInspector({ events }: ContextInspectorProps) {
|
||||
// Use persisted store state instead of local useState
|
||||
const viewMode = useDevUIStore((state) => state.contextInspectorViewMode);
|
||||
const setViewMode = useDevUIStore((state) => state.setContextInspectorViewMode);
|
||||
const showCumulative = useDevUIStore((state) => state.contextInspectorCumulative);
|
||||
const setShowCumulative = useDevUIStore((state) => state.setContextInspectorCumulative);
|
||||
|
||||
// Extract turn data from traces
|
||||
const turns = useMemo(() => extractTurnData(events), [events]);
|
||||
|
||||
// Calculate stats
|
||||
const stats = useMemo(() => calculateStats(turns), [turns]);
|
||||
|
||||
// Aggregate composition
|
||||
const totalComposition = useMemo(() => aggregateComposition(turns), [turns]);
|
||||
|
||||
// Calculate max value for bar scaling (tokens)
|
||||
// In non-cumulative mode, use 0 to signal full-width bars (focus on ratios)
|
||||
// In cumulative mode, scale relative to total (focus on growth)
|
||||
const maxValue = useMemo(() => {
|
||||
if (turns.length === 0) return 0;
|
||||
|
||||
if (showCumulative) {
|
||||
return stats.totalTokens;
|
||||
} else {
|
||||
// Return 0 to signal "use full width" - each bar shows its own ratio
|
||||
return 0;
|
||||
}
|
||||
}, [turns, showCumulative, stats.totalTokens]);
|
||||
|
||||
// Calculate max value for composition bar scaling
|
||||
// Same logic: full-width in non-cumulative, scaled in cumulative
|
||||
const maxCompositionValue = useMemo(() => {
|
||||
if (turns.length === 0) return 0;
|
||||
|
||||
if (showCumulative) {
|
||||
return totalComposition.total;
|
||||
} else {
|
||||
// Return 0 to signal "use full width"
|
||||
return 0;
|
||||
}
|
||||
}, [turns, showCumulative, totalComposition.total]);
|
||||
|
||||
// Calculate cumulative values for tokens and composition
|
||||
const cumulativeData = useMemo(() => {
|
||||
let cumInput = 0;
|
||||
let cumOutput = 0;
|
||||
let cumComposition: ContextComposition = {
|
||||
system: 0, user: 0, assistant: 0, toolCalls: 0, toolResults: 0, total: 0
|
||||
};
|
||||
|
||||
return turns.map(t => {
|
||||
cumInput += t.input_tokens;
|
||||
cumOutput += t.output_tokens;
|
||||
cumComposition = {
|
||||
system: cumComposition.system + t.composition.system,
|
||||
user: cumComposition.user + t.composition.user,
|
||||
assistant: cumComposition.assistant + t.composition.assistant,
|
||||
toolCalls: cumComposition.toolCalls + t.composition.toolCalls,
|
||||
toolResults: cumComposition.toolResults + t.composition.toolResults,
|
||||
total: cumComposition.total + t.composition.total,
|
||||
};
|
||||
return {
|
||||
input: cumInput,
|
||||
output: cumOutput,
|
||||
composition: { ...cumComposition }
|
||||
};
|
||||
});
|
||||
}, [turns]);
|
||||
|
||||
// No data state
|
||||
if (turns.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-col items-center text-center p-6 pt-9">
|
||||
<BarChart3 className="h-8 w-8 text-muted-foreground mb-3" />
|
||||
<div className="text-sm font-medium mb-1">No Data</div>
|
||||
<div className="text-xs text-muted-foreground max-w-[200px]">
|
||||
Run{" "}
|
||||
<span className="font-mono bg-accent/10 px-1 rounded">
|
||||
devui --instrumentation
|
||||
</span>{" "}
|
||||
and start a conversation.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="p-3 border-b flex-shrink-0 space-y-2">
|
||||
{/* Title row */}
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<BarChart3 className="h-4 w-4" />
|
||||
<span className="font-medium text-sm">Context Inspector</span>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{turns.length} turn{turns.length !== 1 ? "s" : ""}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* Cumulative checkbox */}
|
||||
<label className="flex items-center gap-1.5 text-xs text-muted-foreground cursor-pointer">
|
||||
<Checkbox
|
||||
checked={showCumulative}
|
||||
onCheckedChange={(checked) => setShowCumulative(checked === true)}
|
||||
className="h-3.5 w-3.5"
|
||||
/>
|
||||
<span>Cumulative</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* View mode segmented control */}
|
||||
<div className="flex items-center bg-muted rounded-md p-1">
|
||||
<button
|
||||
onClick={() => setViewMode("tokens")}
|
||||
className={`flex-1 px-3 py-1.5 text-xs rounded transition-colors ${
|
||||
viewMode === "tokens"
|
||||
? "bg-background shadow-sm font-medium"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
Tokens
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setViewMode("composition")}
|
||||
className={`flex-1 px-3 py-1.5 text-xs rounded transition-colors ${
|
||||
viewMode === "composition"
|
||||
? "bg-background shadow-sm font-medium"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
Composition
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* View mode description */}
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{viewMode === "tokens"
|
||||
? "Token usage per turn"
|
||||
: "Context breakdown by message type (chars)"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ScrollArea className="flex-1">
|
||||
<div className="p-3 space-y-4">
|
||||
{/* Legend */}
|
||||
<div className="flex items-center gap-4 text-xs px-1 flex-wrap">
|
||||
{viewMode === "tokens" ? (
|
||||
<>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className={`w-3 h-3 rounded ${SEGMENT_COLORS.input}`} />
|
||||
<span className="text-muted-foreground">Input (↑)</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className={`w-3 h-3 rounded ${SEGMENT_COLORS.output}`} />
|
||||
<span className="text-muted-foreground">Output (↓)</span>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className={`w-2.5 h-2.5 rounded-sm ${SEGMENT_COLORS.system}`} />
|
||||
<span className="text-muted-foreground">System</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className={`w-2.5 h-2.5 rounded-sm ${SEGMENT_COLORS.user}`} />
|
||||
<span className="text-muted-foreground">User</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className={`w-2.5 h-2.5 rounded-sm ${SEGMENT_COLORS.assistant}`} />
|
||||
<span className="text-muted-foreground">Assistant</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className={`w-2.5 h-2.5 rounded-sm ${SEGMENT_COLORS.toolCalls}`} />
|
||||
<span className="text-muted-foreground">Tools</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className={`w-2.5 h-2.5 rounded-sm ${SEGMENT_COLORS.toolResults}`} />
|
||||
<span className="text-muted-foreground">Results</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="flex-1" />
|
||||
<div className="flex items-center gap-1 text-muted-foreground">
|
||||
<Info className="h-3 w-3" />
|
||||
<span>Click for details</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Turn bars */}
|
||||
<div className="border rounded-lg overflow-hidden">
|
||||
{turns.map((turn, index) => (
|
||||
<TurnRow
|
||||
key={turn.response_id}
|
||||
turn={turn}
|
||||
index={index}
|
||||
maxValue={maxValue}
|
||||
maxCompositionValue={maxCompositionValue}
|
||||
cumulativeInput={cumulativeData[index]?.input || 0}
|
||||
cumulativeOutput={cumulativeData[index]?.output || 0}
|
||||
cumulativeComposition={cumulativeData[index]?.composition || turn.composition}
|
||||
showCumulative={showCumulative}
|
||||
viewMode={viewMode}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Session summary */}
|
||||
<div className="border rounded-lg overflow-hidden">
|
||||
<div className="p-3 bg-muted/30 border-b">
|
||||
<span className="text-xs font-medium">Session Summary</span>
|
||||
</div>
|
||||
|
||||
<div className="p-3 space-y-3">
|
||||
{/* Token summary cards */}
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<StatCard
|
||||
label="Total Tokens"
|
||||
value={formatTokenCount(stats.totalTokens)}
|
||||
icon={Layers}
|
||||
/>
|
||||
<StatCard
|
||||
label="Input"
|
||||
value={formatTokenCount(stats.totalInput)}
|
||||
icon={BarChart3}
|
||||
color="blue"
|
||||
/>
|
||||
<StatCard
|
||||
label="Output"
|
||||
value={formatTokenCount(stats.totalOutput)}
|
||||
icon={BarChart3}
|
||||
color="green"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Per-turn statistics (only for multi-turn sessions) */}
|
||||
{turns.length > 1 && (
|
||||
<div className="grid grid-cols-2 gap-x-4 gap-y-1 text-xs pt-2 border-t border-muted/50">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Avg per turn:</span>
|
||||
<span className="font-mono">{formatTokenCount(stats.avgTotal)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Peak turn:</span>
|
||||
<span className="font-mono">{formatTokenCount(stats.peakTotal)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Avg input:</span>
|
||||
<span className="font-mono text-blue-600 dark:text-blue-400">{formatTokenCount(stats.avgInput)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Avg output:</span>
|
||||
<span className="font-mono text-emerald-600 dark:text-emerald-400">{formatTokenCount(stats.avgOutput)}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Total composition */}
|
||||
{totalComposition.total > 0 && (
|
||||
<div className="pt-3 border-t border-muted/50">
|
||||
<div className="flex items-start gap-2">
|
||||
<div className="text-muted-foreground text-xs mt-0.5">└─</div>
|
||||
<div className="flex-1">
|
||||
<div className="text-xs text-muted-foreground mb-2 flex items-center gap-1">
|
||||
<Info className="h-3 w-3" />
|
||||
Total Composition (all turns)
|
||||
</div>
|
||||
<CompositionBreakdown composition={totalComposition} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+13
-6
@@ -133,10 +133,10 @@ function useBase64ToBlobUrl(data: string | undefined, mimeType: string): string
|
||||
function FileContentRenderer({ content, className }: ContentRendererProps) {
|
||||
const [isExpanded, setIsExpanded] = useState(true);
|
||||
|
||||
if (content.type !== "input_file" && content.type !== "output_file") return null;
|
||||
|
||||
const fileUrl = content.file_url || content.file_data;
|
||||
const filename = content.filename || "file";
|
||||
// Determine file properties (must be before hooks for conditional logic)
|
||||
const isFileContent = content.type === "input_file" || content.type === "output_file";
|
||||
const fileUrl = isFileContent ? (content.file_url || content.file_data) : undefined;
|
||||
const filename = isFileContent ? (content.filename || "file") : undefined;
|
||||
|
||||
// Determine file type from filename or data URI
|
||||
const isPdf = filename?.toLowerCase().endsWith(".pdf") || fileUrl?.includes("application/pdf");
|
||||
@@ -144,9 +144,13 @@ function FileContentRenderer({ content, className }: ContentRendererProps) {
|
||||
|
||||
// Convert base64 to blob URL for PDFs (better browser compatibility)
|
||||
// Use file_data (raw base64) if available, otherwise try file_url
|
||||
const pdfData = isPdf ? (content.file_data || content.file_url) : undefined;
|
||||
// Hook must be called unconditionally - pass undefined if not a PDF
|
||||
const pdfData = (isFileContent && isPdf) ? (content.file_data || content.file_url) : undefined;
|
||||
const pdfBlobUrl = useBase64ToBlobUrl(pdfData, 'application/pdf');
|
||||
|
||||
// Early return after all hooks
|
||||
if (!isFileContent) return null;
|
||||
|
||||
// Use blob URL if available, otherwise fall back to original URL
|
||||
const effectivePdfUrl = pdfBlobUrl || fileUrl;
|
||||
|
||||
@@ -299,9 +303,12 @@ function DataContentRenderer({ content, className }: ContentRendererProps) {
|
||||
|
||||
// Function approval request renderer - compact version
|
||||
function FunctionApprovalRequestRenderer({ content, className }: ContentRendererProps) {
|
||||
// Hooks must be called unconditionally
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
// Early return after hooks
|
||||
if (content.type !== "function_approval_request") return null;
|
||||
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const { status, function_call } = content;
|
||||
|
||||
// Status styling - compact
|
||||
|
||||
+5
-5
@@ -23,7 +23,7 @@ import {
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { apiClient } from "@/services/api";
|
||||
import type { CheckpointItem, WorkflowSession } from "@/types";
|
||||
import type { CheckpointItem, WorkflowSession, FullCheckpoint, PendingRequestInfoEvent } from "@/types";
|
||||
|
||||
interface CheckpointInfoModalProps {
|
||||
session: WorkflowSession | null;
|
||||
@@ -39,7 +39,7 @@ export function CheckpointInfoModal({
|
||||
onOpenChange,
|
||||
}: CheckpointInfoModalProps) {
|
||||
const [selectedCheckpointId, setSelectedCheckpointId] = useState<string | null>(null);
|
||||
const [fullCheckpoint, setFullCheckpoint] = useState<any>(null);
|
||||
const [fullCheckpoint, setFullCheckpoint] = useState<FullCheckpoint | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [jsonExpanded, setJsonExpanded] = useState(true);
|
||||
|
||||
@@ -68,7 +68,7 @@ export function CheckpointInfoModal({
|
||||
session.conversation_id,
|
||||
`checkpoint_${selectedCheckpointId}`
|
||||
);
|
||||
setFullCheckpoint((item as CheckpointItem).metadata?.full_checkpoint);
|
||||
setFullCheckpoint((item as CheckpointItem).metadata?.full_checkpoint ?? null);
|
||||
} catch (error) {
|
||||
console.error("Failed to load checkpoint:", error);
|
||||
setFullCheckpoint(null);
|
||||
@@ -276,7 +276,7 @@ export function CheckpointInfoModal({
|
||||
)}
|
||||
|
||||
{/* Messages */}
|
||||
{messageExecutors.length > 0 && (
|
||||
{messageExecutors.length > 0 && fullCheckpoint && (
|
||||
<div>
|
||||
<div className="text-sm font-medium mb-3 flex items-center gap-2">
|
||||
<MessageSquare className="h-4 w-4" />
|
||||
@@ -311,7 +311,7 @@ export function CheckpointInfoModal({
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{Object.entries(fullCheckpoint.pending_request_info_events).map(
|
||||
([reqId, reqData]: [string, any]) => (
|
||||
([reqId, reqData]: [string, PendingRequestInfoEvent]) => (
|
||||
<div
|
||||
key={reqId}
|
||||
className="bg-muted/50 border border-border p-3 rounded-lg"
|
||||
|
||||
+63
-35
@@ -9,6 +9,8 @@ import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { HilTimelineItem } from "./hil-timeline-item";
|
||||
import { RunWorkflowButton } from "./run-workflow-button";
|
||||
import { ChatMessageInput } from "@/components/ui/chat-message-input";
|
||||
import { isChatMessageSchema } from "@/utils/workflow-utils";
|
||||
import {
|
||||
Loader2,
|
||||
CheckCircle,
|
||||
@@ -21,6 +23,7 @@ import {
|
||||
Square,
|
||||
} from "lucide-react";
|
||||
import type { ExtendedResponseStreamEvent, JSONSchemaProperty } from "@/types";
|
||||
import type { ResponseInputContent } from "@/types/agent-framework";
|
||||
import type { ExecutorState } from "./executor-node";
|
||||
import { truncateText } from "@/utils/workflow-utils";
|
||||
|
||||
@@ -262,8 +265,8 @@ export function ExecutionTimeline({
|
||||
const item = (event as import("@/types/openai").ResponseOutputItemAddedEvent).item;
|
||||
|
||||
// Handle both executor_action items AND message items from Magentic agents
|
||||
if (item && item.type === "executor_action" && item.executor_id && item.id) {
|
||||
const executorId = item.executor_id;
|
||||
if (item && item.type === "executor_action" && "executor_id" in item && item.id) {
|
||||
const executorId = String(item.executor_id);
|
||||
const itemId = item.id;
|
||||
const runNumber = (runCount.get(executorId) || 0) + 1;
|
||||
runCount.set(executorId, runNumber);
|
||||
@@ -277,22 +280,25 @@ export function ExecutionTimeline({
|
||||
timestamp: uiTimestamp,
|
||||
runNumber,
|
||||
});
|
||||
} else if (item && item.type === "message" && item.metadata?.agent_id && item.metadata?.source === "magentic" && item.id) {
|
||||
} else if (item && item.type === "message" && "metadata" in item && item.id) {
|
||||
// Handle message items from Magentic agents
|
||||
const executorId = item.metadata.agent_id;
|
||||
const itemId = item.id;
|
||||
const runNumber = (runCount.get(executorId) || 0) + 1;
|
||||
runCount.set(executorId, runNumber);
|
||||
const metadata = item.metadata as { agent_id?: string; source?: string } | undefined;
|
||||
if (metadata?.agent_id && metadata?.source === "magentic") {
|
||||
const executorId = metadata.agent_id;
|
||||
const itemId = item.id;
|
||||
const runNumber = (runCount.get(executorId) || 0) + 1;
|
||||
runCount.set(executorId, runNumber);
|
||||
|
||||
runs.push({
|
||||
executorId,
|
||||
executorName: truncateText(executorId, 35),
|
||||
itemId,
|
||||
state: "running",
|
||||
output: itemOutputs[itemId] || "",
|
||||
timestamp: uiTimestamp,
|
||||
runNumber,
|
||||
});
|
||||
runs.push({
|
||||
executorId,
|
||||
executorName: truncateText(executorId, 35),
|
||||
itemId,
|
||||
state: "running",
|
||||
output: itemOutputs[itemId] || "",
|
||||
timestamp: uiTimestamp,
|
||||
runNumber,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,7 +307,7 @@ export function ExecutionTimeline({
|
||||
const item = (event as import("@/types/openai").ResponseOutputItemDoneEvent).item;
|
||||
|
||||
// Handle both executor_action items AND message items from Magentic agents
|
||||
if (item && item.type === "executor_action" && item.executor_id && item.id) {
|
||||
if (item && item.type === "executor_action" && "executor_id" in item && item.id) {
|
||||
const itemId = item.id;
|
||||
// Find the run by ITEM ID (not executor ID!) to handle multiple runs correctly
|
||||
const existingRun = runs.find((r) => r.itemId === itemId);
|
||||
@@ -315,18 +321,21 @@ export function ExecutionTimeline({
|
||||
: "completed";
|
||||
// Use item-specific output, not executor-wide output
|
||||
existingRun.output = itemOutputs[itemId] || "";
|
||||
if (item.status === "failed" && item.error) {
|
||||
existingRun.error = item.error;
|
||||
if (item.status === "failed" && "error" in item && item.error) {
|
||||
existingRun.error = String(item.error);
|
||||
}
|
||||
}
|
||||
} else if (item && item.type === "message" && item.metadata?.agent_id && item.metadata?.source === "magentic" && item.id) {
|
||||
} else if (item && item.type === "message" && "metadata" in item && item.id) {
|
||||
// Handle message completion from Magentic agents
|
||||
const itemId = item.id;
|
||||
const existingRun = runs.find((r) => r.itemId === itemId);
|
||||
const metadata = item.metadata as { agent_id?: string; source?: string } | undefined;
|
||||
if (metadata?.agent_id && metadata?.source === "magentic") {
|
||||
const itemId = item.id;
|
||||
const existingRun = runs.find((r) => r.itemId === itemId);
|
||||
|
||||
if (existingRun) {
|
||||
existingRun.state = item.status === "completed" ? "completed" : "failed";
|
||||
existingRun.output = itemOutputs[itemId] || "";
|
||||
if (existingRun) {
|
||||
existingRun.state = item.status === "completed" ? "completed" : "failed";
|
||||
existingRun.output = itemOutputs[itemId] || "";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -625,16 +634,35 @@ export function ExecutionTimeline({
|
||||
{/* Bottom Control Bar - Sticky (hidden when HIL is active) */}
|
||||
{(onRun || onCancel) && pendingHilRequests.length === 0 && (
|
||||
<div className="border-t p-3 bg-background flex-shrink-0">
|
||||
<RunWorkflowButton
|
||||
inputSchema={inputSchema}
|
||||
onRun={onRun || (() => {})}
|
||||
onCancel={onCancel}
|
||||
isSubmitting={workflowState === "running"}
|
||||
isCancelling={isCancelling}
|
||||
workflowState={workflowState}
|
||||
checkpoints={checkpoints}
|
||||
showCheckpoints={false}
|
||||
/>
|
||||
{inputSchema && isChatMessageSchema(inputSchema) ? (
|
||||
<ChatMessageInput
|
||||
onSubmit={async (content: ResponseInputContent[]) => {
|
||||
// Wrap in OpenAI message format (same as run-workflow-button modal)
|
||||
const openaiInput = [
|
||||
{ type: "message", role: "user", content },
|
||||
];
|
||||
onRun?.(openaiInput as unknown as Record<string, unknown>);
|
||||
}}
|
||||
isSubmitting={workflowState === "running"}
|
||||
isStreaming={workflowState === "running"}
|
||||
onCancel={onCancel}
|
||||
isCancelling={isCancelling}
|
||||
placeholder="Message workflow..."
|
||||
showFileUpload={true}
|
||||
entityName="workflow"
|
||||
/>
|
||||
) : (
|
||||
<RunWorkflowButton
|
||||
inputSchema={inputSchema}
|
||||
onRun={onRun || (() => {})}
|
||||
onCancel={onCancel}
|
||||
isSubmitting={workflowState === "running"}
|
||||
isCancelling={isCancelling}
|
||||
workflowState={workflowState}
|
||||
checkpoints={checkpoints}
|
||||
showCheckpoints={false}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { memo } from "react";
|
||||
import { memo, useState } from "react";
|
||||
import { Handle, Position, type NodeProps } from "@xyflow/react";
|
||||
import {
|
||||
Workflow,
|
||||
Home,
|
||||
Loader2,
|
||||
ChevronRight,
|
||||
ChevronDown,
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { truncateText } from "@/utils/workflow-utils";
|
||||
@@ -70,8 +72,9 @@ const getExecutorStateConfig = (state: ExecutorState) => {
|
||||
export const ExecutorNode = memo(({ data, selected }: NodeProps) => {
|
||||
const nodeData = data as ExecutorNodeData;
|
||||
const config = getExecutorStateConfig(nodeData.state);
|
||||
const [isOutputExpanded, setIsOutputExpanded] = useState(false);
|
||||
|
||||
const hasData = nodeData.inputData || nodeData.outputData || nodeData.error;
|
||||
const hasOutput = nodeData.outputData || nodeData.error;
|
||||
const isRunning = nodeData.state === "running";
|
||||
const shouldAnimate = isRunning && (nodeData.isStreaming ?? true); // Default to true for backwards compatibility
|
||||
|
||||
@@ -80,19 +83,13 @@ export const ExecutorNode = memo(({ data, selected }: NodeProps) => {
|
||||
const targetPosition = isVertical ? Position.Top : Position.Left;
|
||||
const sourcePosition = isVertical ? Position.Bottom : Position.Right;
|
||||
|
||||
// Helper to safely render data with full details
|
||||
// Helper to render output/error details when expanded
|
||||
const renderDataDetails = () => {
|
||||
const details = [];
|
||||
|
||||
if (nodeData.error && typeof nodeData.error === "string") {
|
||||
// Truncate error to first 150 characters for node display
|
||||
const truncatedError = truncateText(nodeData.error, 150);
|
||||
details.push(
|
||||
<div key="error" className="mb-2">
|
||||
<div className="text-xs font-medium text-red-600 dark:text-red-400 mb-1">Error:</div>
|
||||
<div className="text-xs text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-950/20 p-2 rounded border border-red-200 dark:border-red-800 break-words">
|
||||
{truncatedError}
|
||||
</div>
|
||||
const truncatedError = truncateText(nodeData.error, 200);
|
||||
return (
|
||||
<div className="text-xs text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-950/20 p-2 rounded border border-red-200 dark:border-red-800 break-words max-h-32 overflow-auto">
|
||||
{truncatedError}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -103,53 +100,21 @@ export const ExecutorNode = memo(({ data, selected }: NodeProps) => {
|
||||
typeof nodeData.outputData === "string"
|
||||
? nodeData.outputData
|
||||
: JSON.stringify(nodeData.outputData, null, 2);
|
||||
details.push(
|
||||
<div key="output" className="mb-2">
|
||||
<div className="text-xs font-medium text-green-600 dark:text-green-400 mb-1">Output:</div>
|
||||
<div className="text-xs text-gray-700 dark:text-gray-300 bg-green-50 dark:bg-green-950/20 p-2 rounded border border-green-200 dark:border-green-800 max-h-20 overflow-auto">
|
||||
<pre className="whitespace-pre-wrap font-mono">{outputStr}</pre>
|
||||
</div>
|
||||
return (
|
||||
<div className="text-xs text-gray-700 dark:text-gray-300 bg-muted/50 p-2 rounded border max-h-32 overflow-auto">
|
||||
<pre className="whitespace-pre-wrap font-mono">{outputStr}</pre>
|
||||
</div>
|
||||
);
|
||||
} catch {
|
||||
details.push(
|
||||
<div key="output" className="mb-2">
|
||||
<div className="text-xs font-medium text-green-600 dark:text-green-400 mb-1">Output:</div>
|
||||
<div className="text-xs text-gray-600 dark:text-gray-400 bg-green-50 dark:bg-green-950/20 p-2 rounded border border-green-200 dark:border-green-800">
|
||||
[Unable to display output data]
|
||||
</div>
|
||||
return (
|
||||
<div className="text-xs text-gray-600 dark:text-gray-400 bg-muted/50 p-2 rounded border">
|
||||
[Unable to display output]
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (nodeData.inputData) {
|
||||
try {
|
||||
const inputStr =
|
||||
typeof nodeData.inputData === "string"
|
||||
? nodeData.inputData
|
||||
: JSON.stringify(nodeData.inputData, null, 2);
|
||||
details.push(
|
||||
<div key="input" className="mb-2">
|
||||
<div className="text-xs font-medium text-blue-600 dark:text-blue-400 mb-1">Input:</div>
|
||||
<div className="text-xs text-gray-700 dark:text-gray-300 bg-blue-50 dark:bg-blue-950/20 p-2 rounded border border-blue-200 dark:border-blue-800 max-h-20 overflow-auto">
|
||||
<pre className="whitespace-pre-wrap font-mono">{inputStr}</pre>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} catch {
|
||||
details.push(
|
||||
<div key="input" className="mb-2">
|
||||
<div className="text-xs font-medium text-blue-600 dark:text-blue-400 mb-1">Input:</div>
|
||||
<div className="text-xs text-gray-600 dark:text-gray-400 bg-blue-50 dark:bg-blue-950/20 p-2 rounded border border-blue-200 dark:border-blue-800">
|
||||
[Unable to display input data]
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return details.length > 0 ? details : null;
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -218,10 +183,28 @@ export const ExecutorNode = memo(({ data, selected }: NodeProps) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Data details */}
|
||||
{hasData && (
|
||||
<div className="mt-3">
|
||||
{renderDataDetails()}
|
||||
{/* Collapsible output section */}
|
||||
{hasOutput && (
|
||||
<div className="mt-2 border-t border-border/50 pt-2">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsOutputExpanded(!isOutputExpanded);
|
||||
}}
|
||||
className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors w-full"
|
||||
>
|
||||
{isOutputExpanded ? (
|
||||
<ChevronDown className="w-3 h-3" />
|
||||
) : (
|
||||
<ChevronRight className="w-3 h-3" />
|
||||
)}
|
||||
<span>{nodeData.error ? "Show error" : "Show output"}</span>
|
||||
</button>
|
||||
{isOutputExpanded && (
|
||||
<div className="mt-2">
|
||||
{renderDataDetails()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
+116
-17
@@ -110,6 +110,7 @@ export function WorkflowView({
|
||||
const removeSession = useDevUIStore((state) => state.removeSession);
|
||||
const addToast = useDevUIStore((state) => state.addToast);
|
||||
const runtime = useDevUIStore((state) => state.runtime);
|
||||
const streamingEnabled = useDevUIStore((state) => state.streamingEnabled);
|
||||
|
||||
// View options state
|
||||
const [viewOptions, setViewOptions] = useState(() => {
|
||||
@@ -304,8 +305,9 @@ export function WorkflowView({
|
||||
{ limit: 100 }
|
||||
);
|
||||
const checkpointItems = response.data.filter(
|
||||
(item: any) => item.type === "checkpoint"
|
||||
) as CheckpointItem[];
|
||||
(item): item is CheckpointItem =>
|
||||
typeof item === "object" && item !== null && "type" in item && (item as { type: string }).type === "checkpoint"
|
||||
);
|
||||
setSessionCheckpoints(checkpointItems);
|
||||
} catch (error) {
|
||||
console.error(`Failed to load checkpoints for session ${currentSession.conversation_id}:`, error);
|
||||
@@ -453,9 +455,9 @@ export function WorkflowView({
|
||||
| import("@/types/openai").ResponseOutputItemAddedEvent
|
||||
| import("@/types/openai").ResponseOutputItemDoneEvent
|
||||
).item;
|
||||
if (item && item.type === "executor_action" && item.executor_id) {
|
||||
if (item && item.type === "executor_action" && "executor_id" in item && item.executor_id) {
|
||||
history.push({
|
||||
executorId: item.executor_id,
|
||||
executorId: String(item.executor_id),
|
||||
message:
|
||||
event.type === "response.output_item.added"
|
||||
? "Executor started"
|
||||
@@ -624,7 +626,8 @@ export function WorkflowView({
|
||||
if (
|
||||
item &&
|
||||
item.type === "message" &&
|
||||
item.metadata?.source === "magentic" &&
|
||||
"metadata" in item &&
|
||||
(item.metadata as { source?: string } | undefined)?.source === "magentic" &&
|
||||
item.id
|
||||
) {
|
||||
// Track this message ID as the current streaming target for Magentic agents
|
||||
@@ -639,19 +642,21 @@ export function WorkflowView({
|
||||
if (
|
||||
item &&
|
||||
item.type === "message" &&
|
||||
!item.metadata?.source &&
|
||||
item.content
|
||||
(!("metadata" in item) || !(item.metadata as { source?: string } | undefined)?.source) &&
|
||||
"content" in item &&
|
||||
Array.isArray(item.content)
|
||||
) {
|
||||
// Extract text from message content
|
||||
for (const content of item.content) {
|
||||
for (const content of item.content as Array<{ type: string; text?: string }>) {
|
||||
if (content.type === "output_text" && content.text) {
|
||||
const text = content.text; // Capture for closure
|
||||
// Append to workflow result (support multiple yield_output calls)
|
||||
setWorkflowResult((prev) => {
|
||||
if (prev && prev.length > 0) {
|
||||
// If there's existing output, add separator
|
||||
return prev + "\n\n" + content.text;
|
||||
return prev + "\n\n" + text;
|
||||
}
|
||||
return content.text;
|
||||
return text;
|
||||
});
|
||||
|
||||
// Try to parse as JSON for structured metadata
|
||||
@@ -820,6 +825,99 @@ export function WorkflowView({
|
||||
]
|
||||
);
|
||||
|
||||
// Handle non-streaming workflow data sending
|
||||
const handleSendWorkflowDataSync = useCallback(
|
||||
async (inputData: Record<string, unknown>, checkpointId?: string) => {
|
||||
if (!selectedWorkflow || selectedWorkflow.type !== "workflow") return;
|
||||
|
||||
setIsStreaming(false); // Not actually streaming
|
||||
setWasCancelled(false);
|
||||
setOpenAIEvents([]);
|
||||
setWorkflowResult("");
|
||||
itemOutputs.current = {};
|
||||
currentStreamingItemId.current = null;
|
||||
workflowMetadata.current = null;
|
||||
setPendingHilRequests([]);
|
||||
setHilResponses({});
|
||||
onDebugEvent("clear");
|
||||
|
||||
try {
|
||||
const response = await apiClient.runWorkflowSync(selectedWorkflow.id, {
|
||||
input_data: inputData,
|
||||
conversation_id: currentSession?.conversation_id || undefined,
|
||||
checkpoint_id: checkpointId,
|
||||
});
|
||||
|
||||
// Extract workflow result from response output
|
||||
if (response.output) {
|
||||
for (const outputItem of response.output) {
|
||||
if (outputItem.type === "message" && "content" in outputItem && Array.isArray(outputItem.content)) {
|
||||
for (const content of outputItem.content as Array<{ type: string; text?: string }>) {
|
||||
if (content.type === "output_text" && content.text) {
|
||||
setWorkflowResult((prev) => {
|
||||
if (prev && prev.length > 0) {
|
||||
return prev + "\n\n" + content.text;
|
||||
}
|
||||
return content.text || "";
|
||||
});
|
||||
|
||||
// Try to parse as JSON for structured metadata
|
||||
try {
|
||||
const parsed = JSON.parse(content.text || "");
|
||||
if (typeof parsed === "object" && parsed !== null) {
|
||||
workflowMetadata.current = parsed;
|
||||
}
|
||||
} catch {
|
||||
// Not JSON, keep as text
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create a synthetic completion event for the timeline
|
||||
const completedEvent = {
|
||||
type: "response.completed",
|
||||
response: response,
|
||||
sequence_number: 0,
|
||||
} as ExtendedResponseStreamEvent;
|
||||
setOpenAIEvents([completedEvent]);
|
||||
onDebugEvent(completedEvent);
|
||||
|
||||
// Refetch checkpoints after completion
|
||||
await loadCheckpoints();
|
||||
} catch (error) {
|
||||
console.error("Workflow execution error:", error);
|
||||
|
||||
// Create a synthetic error event for the timeline
|
||||
const errorMessage = error instanceof Error ? error.message : "Workflow execution failed";
|
||||
const errorEvent: ExtendedResponseStreamEvent = {
|
||||
type: "response.failed",
|
||||
response: {
|
||||
error: { message: errorMessage },
|
||||
},
|
||||
sequence_number: 0,
|
||||
} as ExtendedResponseStreamEvent;
|
||||
setOpenAIEvents([errorEvent]);
|
||||
onDebugEvent(errorEvent);
|
||||
}
|
||||
},
|
||||
[selectedWorkflow, currentSession, onDebugEvent, loadCheckpoints]
|
||||
);
|
||||
|
||||
// Wrapper to choose between streaming and non-streaming
|
||||
const handleWorkflowRun = useCallback(
|
||||
async (inputData: Record<string, unknown>, checkpointId?: string) => {
|
||||
if (streamingEnabled) {
|
||||
await handleSendWorkflowData(inputData, checkpointId);
|
||||
} else {
|
||||
await handleSendWorkflowDataSync(inputData, checkpointId);
|
||||
}
|
||||
},
|
||||
[streamingEnabled, handleSendWorkflowData, handleSendWorkflowDataSync]
|
||||
);
|
||||
|
||||
// Check if all HIL responses are valid
|
||||
const areAllHilResponsesValid = useCallback(() => {
|
||||
// Check each pending request has a valid response
|
||||
@@ -979,22 +1077,23 @@ export function WorkflowView({
|
||||
}
|
||||
|
||||
// Handle workflow output messages
|
||||
if (item && item.type === "message" && item.content) {
|
||||
if (item && item.type === "message" && "content" in item && Array.isArray(item.content)) {
|
||||
// Extract text from message content
|
||||
for (const content of item.content) {
|
||||
for (const content of item.content as Array<{ type: string; text?: string }>) {
|
||||
if (content.type === "output_text" && content.text) {
|
||||
const text = content.text; // Capture for closure
|
||||
// Append to workflow result (support multiple yield_output calls)
|
||||
setWorkflowResult((prev) => {
|
||||
if (prev && prev.length > 0) {
|
||||
// If there's existing output, add separator
|
||||
return prev + "\n\n" + content.text;
|
||||
return prev + "\n\n" + text;
|
||||
}
|
||||
return content.text;
|
||||
return text;
|
||||
});
|
||||
|
||||
// Try to parse as JSON for structured metadata
|
||||
try {
|
||||
const parsed = JSON.parse(content.text);
|
||||
const parsed = JSON.parse(text);
|
||||
if (typeof parsed === "object" && parsed !== null) {
|
||||
workflowMetadata.current = parsed;
|
||||
}
|
||||
@@ -1296,7 +1395,7 @@ export function WorkflowView({
|
||||
{timelineMinimized && (
|
||||
<RunWorkflowButton
|
||||
inputSchema={workflowInfo.input_schema}
|
||||
onRun={handleSendWorkflowData}
|
||||
onRun={handleWorkflowRun}
|
||||
onCancel={handleCancel}
|
||||
isSubmitting={isStreaming}
|
||||
isCancelling={isCancelling}
|
||||
@@ -1481,7 +1580,7 @@ export function WorkflowView({
|
||||
inputSchema={workflowInfo?.input_schema}
|
||||
onRun={(data, checkpointId) => {
|
||||
// Use the form data from timeline
|
||||
handleSendWorkflowData(data, checkpointId);
|
||||
handleWorkflowRun(data, checkpointId);
|
||||
}}
|
||||
onCancel={handleCancel}
|
||||
isCancelling={isCancelling}
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
* Features: Real-time event streaming, trace visualization, tool call details
|
||||
*/
|
||||
|
||||
import { useRef, useState } from "react";
|
||||
import { useRef, useState, useMemo } from "react";
|
||||
import { useDevUIStore } from "@/stores/devuiStore";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
@@ -19,8 +20,9 @@ import {
|
||||
MessageSquare,
|
||||
ChevronRight,
|
||||
ChevronDown,
|
||||
Info,
|
||||
BarChart3,
|
||||
} from "lucide-react";
|
||||
import { ContextInspector } from "@/components/features/agent/context-inspector";
|
||||
import type { ExtendedResponseStreamEvent } from "@/types";
|
||||
|
||||
// Simple visual separator component
|
||||
@@ -88,7 +90,23 @@ interface TraceEventData extends EventDataBase {
|
||||
start_time?: number;
|
||||
end_time?: number;
|
||||
entity_id?: string;
|
||||
session_id?: string | null;
|
||||
response_id?: string | null;
|
||||
}
|
||||
|
||||
// Helper type for trace hierarchy
|
||||
interface TraceNode {
|
||||
event: ExtendedResponseStreamEvent;
|
||||
data: TraceEventData;
|
||||
children: TraceNode[];
|
||||
}
|
||||
|
||||
// Helper type for grouped traces by response
|
||||
interface TraceGroup {
|
||||
response_id: string;
|
||||
timestamp: number;
|
||||
traces: TraceNode[];
|
||||
totalDuration: number;
|
||||
entity_id?: string;
|
||||
}
|
||||
|
||||
interface DebugPanelProps {
|
||||
@@ -149,20 +167,22 @@ function processEventsForDisplay(
|
||||
const item = outputEvent.item;
|
||||
|
||||
// If it's a function call item, extract metadata
|
||||
if (item.type === "function_call" && item.call_id && item.name) {
|
||||
const callId = item.call_id;
|
||||
if (item.type === "function_call") {
|
||||
// Type assertion for function call
|
||||
const funcCall = item as import("@/types").ResponseFunctionToolCall;
|
||||
const callId = funcCall.call_id;
|
||||
|
||||
// Initialize function call tracking with REAL function name from backend!
|
||||
functionCalls.set(callId, {
|
||||
name: item.name, // ← REAL NAME! (not "unknown")
|
||||
name: funcCall.name, // ← REAL NAME! (not "unknown")
|
||||
arguments: "",
|
||||
callId: callId,
|
||||
itemId: item.id, // Track item_id for delta matching
|
||||
itemId: funcCall.id, // Track item_id for delta matching
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
// Also track in callIdToName map for result pairing
|
||||
callIdToName.set(callId, item.name);
|
||||
callIdToName.set(callId, funcCall.name);
|
||||
}
|
||||
|
||||
// Pass through the event for display
|
||||
@@ -955,27 +975,7 @@ function EventExpandedContent({
|
||||
</span>
|
||||
<div className="mt-1 max-h-32 overflow-auto">
|
||||
<pre className="text-xs bg-background border rounded p-2 whitespace-pre-wrap break-all">
|
||||
{(() => {
|
||||
try {
|
||||
// Try to pretty-print JSON, and unescape string values that contain JSON
|
||||
const attrs = { ...data.attributes };
|
||||
Object.keys(attrs).forEach((key) => {
|
||||
if (
|
||||
typeof attrs[key] === "string" &&
|
||||
attrs[key].startsWith("[")
|
||||
) {
|
||||
try {
|
||||
attrs[key] = JSON.parse(attrs[key]);
|
||||
} catch {
|
||||
// Keep original if parsing fails
|
||||
}
|
||||
}
|
||||
});
|
||||
return JSON.stringify(attrs, null, 2);
|
||||
} catch {
|
||||
return JSON.stringify(data.attributes, null, 2);
|
||||
}
|
||||
})()}
|
||||
{formatTraceAttributes(data.attributes)}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1149,257 +1149,418 @@ function EventsTab({
|
||||
);
|
||||
}
|
||||
|
||||
function TracesTab({ events }: { events: ExtendedResponseStreamEvent[] }) {
|
||||
// ONLY show actual trace events - handle both event type formats
|
||||
const traceEvents = events.filter(
|
||||
(e) =>
|
||||
e.type === "response.trace.completed" ||
|
||||
e.type === "response.trace.completed"
|
||||
);
|
||||
// Build hierarchical trace structure from flat trace events
|
||||
function buildTraceHierarchy(traceEvents: ExtendedResponseStreamEvent[]): TraceGroup[] {
|
||||
// Group by response_id first
|
||||
const groupedByResponse = new Map<string, ExtendedResponseStreamEvent[]>();
|
||||
|
||||
// Add separators between message rounds
|
||||
const tracesWithSeparators = addSeparatorsToEvents(traceEvents);
|
||||
for (const event of traceEvents) {
|
||||
if (!("data" in event)) continue;
|
||||
const data = event.data as TraceEventData;
|
||||
const responseId = data.response_id || "unknown";
|
||||
|
||||
// Reverse to show latest traces at the top
|
||||
const reversedTraceEvents = [...tracesWithSeparators].reverse();
|
||||
if (!groupedByResponse.has(responseId)) {
|
||||
groupedByResponse.set(responseId, []);
|
||||
}
|
||||
groupedByResponse.get(responseId)!.push(event);
|
||||
}
|
||||
|
||||
// Convert each group to hierarchical structure
|
||||
const groups: TraceGroup[] = [];
|
||||
|
||||
for (const [responseId, events] of groupedByResponse) {
|
||||
// Build tree from parent_span_id relationships
|
||||
const nodeMap = new Map<string, TraceNode>();
|
||||
const rootNodes: TraceNode[] = [];
|
||||
|
||||
// First pass: create all nodes
|
||||
for (const event of events) {
|
||||
if (!("data" in event)) continue;
|
||||
const data = (event as { data: TraceEventData }).data;
|
||||
const spanId = data.span_id || `span_${Math.random()}`;
|
||||
nodeMap.set(spanId, {
|
||||
event,
|
||||
data,
|
||||
children: [],
|
||||
});
|
||||
}
|
||||
|
||||
// Second pass: build parent-child relationships
|
||||
for (const event of events) {
|
||||
if (!("data" in event)) continue;
|
||||
const data = (event as { data: TraceEventData }).data;
|
||||
const spanId = data.span_id || "";
|
||||
const parentSpanId = data.parent_span_id;
|
||||
const node = nodeMap.get(spanId);
|
||||
|
||||
if (!node) continue;
|
||||
|
||||
if (parentSpanId && nodeMap.has(parentSpanId)) {
|
||||
// Has a parent in this group
|
||||
nodeMap.get(parentSpanId)!.children.push(node);
|
||||
} else {
|
||||
// Root node (no parent or parent not in this group)
|
||||
rootNodes.push(node);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort root nodes by start_time (earliest first)
|
||||
rootNodes.sort((a, b) => (a.data.start_time || 0) - (b.data.start_time || 0));
|
||||
|
||||
// Sort children recursively by start_time
|
||||
const sortChildren = (node: TraceNode) => {
|
||||
node.children.sort((a, b) => (a.data.start_time || 0) - (b.data.start_time || 0));
|
||||
node.children.forEach(sortChildren);
|
||||
};
|
||||
rootNodes.forEach(sortChildren);
|
||||
|
||||
// Calculate group metadata
|
||||
const firstEvent = events[0];
|
||||
const firstData = firstEvent && "data" in firstEvent ? (firstEvent.data as TraceEventData) : null;
|
||||
const timestamp = Math.min(...events.map(e => {
|
||||
const d = "data" in e ? (e.data as TraceEventData) : null;
|
||||
return d?.start_time || Date.now() / 1000;
|
||||
}));
|
||||
const totalDuration = events.reduce((sum, e) => {
|
||||
const d = "data" in e ? (e.data as TraceEventData) : null;
|
||||
return sum + (d?.duration_ms || 0);
|
||||
}, 0);
|
||||
|
||||
groups.push({
|
||||
response_id: responseId,
|
||||
timestamp,
|
||||
traces: rootNodes,
|
||||
totalDuration,
|
||||
entity_id: firstData?.entity_id,
|
||||
});
|
||||
}
|
||||
|
||||
// Sort groups by timestamp (newest first)
|
||||
groups.sort((a, b) => b.timestamp - a.timestamp);
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
// Recursively parse escaped JSON strings at any depth
|
||||
function parseEscapedJson(value: unknown): unknown {
|
||||
if (typeof value === "string") {
|
||||
// Try to parse JSON strings (arrays or objects)
|
||||
const trimmed = value.trim();
|
||||
if (trimmed.startsWith("[") || trimmed.startsWith("{")) {
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
// Recursively process the parsed result
|
||||
return parseEscapedJson(parsed);
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(parseEscapedJson);
|
||||
}
|
||||
|
||||
if (value !== null && typeof value === "object") {
|
||||
const result: Record<string, unknown> = {};
|
||||
for (const [k, v] of Object.entries(value)) {
|
||||
result[k] = parseEscapedJson(v);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
// Format trace attributes by parsing escaped JSON strings for better readability
|
||||
function formatTraceAttributes(attributes: Record<string, unknown>): string {
|
||||
try {
|
||||
const formatted = parseEscapedJson(attributes);
|
||||
return JSON.stringify(formatted, null, 2);
|
||||
} catch {
|
||||
return JSON.stringify(attributes, null, 2);
|
||||
}
|
||||
}
|
||||
|
||||
// Get operation type badge color
|
||||
function getOperationColor(operationName: string): string {
|
||||
if (operationName.includes("invoke_agent") || operationName.includes("Agent")) {
|
||||
return "bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200";
|
||||
}
|
||||
if (operationName.includes("chat") || operationName.includes("Chat")) {
|
||||
return "bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200";
|
||||
}
|
||||
if (operationName.includes("tool") || operationName.includes("execute")) {
|
||||
return "bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200";
|
||||
}
|
||||
return "bg-orange-100 dark:bg-orange-900 text-orange-800 dark:text-orange-200";
|
||||
}
|
||||
|
||||
// Recursive component for rendering trace tree nodes
|
||||
function TraceTreeNode({ node, depth = 0 }: { node: TraceNode; depth?: number }) {
|
||||
const [isExpanded, setIsExpanded] = useState(depth < 2); // Auto-expand first 2 levels
|
||||
const [showDetails, setShowDetails] = useState(false);
|
||||
|
||||
const { data } = node;
|
||||
const operationName = data.operation_name || "Unknown";
|
||||
const duration = data.duration_ms ? `${Number(data.duration_ms).toFixed(1)}ms` : "";
|
||||
const hasChildren = node.children.length > 0;
|
||||
|
||||
// Extract token usage from attributes if available
|
||||
const inputTokens = data.attributes?.["gen_ai.usage.input_tokens"];
|
||||
const outputTokens = data.attributes?.["gen_ai.usage.output_tokens"];
|
||||
const hasTokens = inputTokens !== undefined || outputTokens !== undefined;
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
<div className="flex items-center gap-2 p-3 border-b">
|
||||
<Search className="h-4 w-4" />
|
||||
<span className="font-medium">Traces</span>
|
||||
<Badge variant="outline">{traceEvents.length}</Badge>
|
||||
<div className="relative">
|
||||
{/* Vertical line for tree structure */}
|
||||
{depth > 0 && (
|
||||
<div
|
||||
className="absolute left-0 top-0 bottom-0 border-l-2 border-muted"
|
||||
style={{ marginLeft: `${(depth - 1) * 16 + 8}px` }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div
|
||||
className="flex items-center gap-2 py-1.5 hover:bg-muted/50 rounded transition-colors"
|
||||
style={{ paddingLeft: `${depth * 16}px` }}
|
||||
>
|
||||
{/* Expand/collapse for children OR details */}
|
||||
<button
|
||||
onClick={() => hasChildren ? setIsExpanded(!isExpanded) : setShowDetails(!showDetails)}
|
||||
className="w-4 h-4 flex items-center justify-center text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
{hasChildren ? (
|
||||
isExpanded ? <ChevronDown className="h-3 w-3" /> : <ChevronRight className="h-3 w-3" />
|
||||
) : (
|
||||
showDetails ? <ChevronDown className="h-3 w-3" /> : <ChevronRight className="h-3 w-3" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Operation badge */}
|
||||
<span className={`text-xs px-1.5 py-0.5 rounded font-medium ${getOperationColor(operationName)}`}>
|
||||
{operationName.replace("ChatAgent.", "").replace("invoke_agent ", "")}
|
||||
</span>
|
||||
|
||||
{/* Duration */}
|
||||
{duration && (
|
||||
<span className="text-xs text-muted-foreground font-mono">
|
||||
{duration}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Token usage */}
|
||||
{hasTokens && (
|
||||
<span className="text-xs text-muted-foreground font-mono">
|
||||
{inputTokens !== undefined && <span>↑{String(inputTokens)}</span>}
|
||||
{inputTokens !== undefined && outputTokens !== undefined && <span className="mx-0.5">/</span>}
|
||||
{outputTokens !== undefined && <span>↓{String(outputTokens)}</span>}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ScrollArea className="flex-1">
|
||||
<div className="p-3">
|
||||
{traceEvents.length === 0 ? (
|
||||
<div className="text-center text-muted-foreground text-sm py-8">
|
||||
No trace data available.
|
||||
<br />
|
||||
{events && events.length > 0 && (
|
||||
<div className="mt-3 text-xs border rounded p-2">
|
||||
{" "}
|
||||
<Info className="inline h-4 w-4 mr-1 " />
|
||||
You may have to set the environment variable{" "}
|
||||
<span className="font-mono bg-accent/10 px-1 rounded">
|
||||
ENABLE_INSTRUMENTATION=true
|
||||
</span>{" "}
|
||||
or restart devui with the tracing flag{" "}
|
||||
<div className="font-mono bg-accent/10 px-1 rounded">
|
||||
devui --tracing
|
||||
</div>
|
||||
to enable tracing.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{reversedTraceEvents.map((event, index) => {
|
||||
if ('type' in event && event.type === "separator") {
|
||||
return <MessageSeparator key={(event as { type: "separator"; id: string }).id} />;
|
||||
}
|
||||
return <TraceEventItem key={index} event={event as ExtendedResponseStreamEvent} />;
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{/* Details panel */}
|
||||
{showDetails && !hasChildren && (
|
||||
<div
|
||||
className="ml-4 mt-1 mb-2 p-2 bg-muted/30 rounded border text-xs"
|
||||
style={{ marginLeft: `${depth * 16 + 20}px` }}
|
||||
>
|
||||
<div className="space-y-1">
|
||||
{data.span_id && (
|
||||
<div className="flex gap-2">
|
||||
<span className="text-muted-foreground w-20">Span ID:</span>
|
||||
<span className="font-mono text-xs break-all">{data.span_id}</span>
|
||||
</div>
|
||||
)}
|
||||
{data.trace_id && (
|
||||
<div className="flex gap-2">
|
||||
<span className="text-muted-foreground w-20">Trace ID:</span>
|
||||
<span className="font-mono text-xs break-all">{data.trace_id}</span>
|
||||
</div>
|
||||
)}
|
||||
{data.status && (
|
||||
<div className="flex gap-2">
|
||||
<span className="text-muted-foreground w-20">Status:</span>
|
||||
<span className={`px-1.5 py-0.5 rounded text-xs ${
|
||||
data.status === "StatusCode.UNSET" || data.status === "OK"
|
||||
? "bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200"
|
||||
: "bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200"
|
||||
}`}>
|
||||
{data.status}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{data.attributes && Object.keys(data.attributes).length > 0 && (
|
||||
<div className="mt-2">
|
||||
<span className="text-muted-foreground block mb-1">Attributes:</span>
|
||||
<pre className="text-xs bg-background border rounded p-2 overflow-auto max-h-32 whitespace-pre-wrap break-all">
|
||||
{formatTraceAttributes(data.attributes)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
)}
|
||||
|
||||
{/* Children */}
|
||||
{hasChildren && isExpanded && (
|
||||
<div>
|
||||
{node.children.map((child, idx) => (
|
||||
<TraceTreeNode key={child.data.span_id || idx} node={child} depth={depth + 1} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TraceEventItem({ event }: { event: ExtendedResponseStreamEvent }) {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
// Component for a single trace group (one response/turn)
|
||||
function TraceGroupItem({ group }: { group: TraceGroup }) {
|
||||
const [isExpanded, setIsExpanded] = useState(true);
|
||||
|
||||
if (
|
||||
(event.type !== "response.trace.completed" &&
|
||||
event.type !== "response.trace.completed") ||
|
||||
!("data" in event)
|
||||
) {
|
||||
return (
|
||||
<div className="border rounded p-3 text-red-600 dark:text-red-400 text-xs">
|
||||
Error: Expected trace event but got {event.type}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const data = event.data as TraceEventData;
|
||||
|
||||
// Use stored UI timestamp first, then trace timestamps, then fallback to current time
|
||||
let timestamp: string;
|
||||
if ('_uiTimestamp' in event && typeof event._uiTimestamp === 'number') {
|
||||
// Use stored UI timestamp from when event was received
|
||||
timestamp = new Date(event._uiTimestamp * 1000).toLocaleTimeString();
|
||||
} else if (data.end_time) {
|
||||
timestamp = new Date(data.end_time * 1000).toLocaleTimeString();
|
||||
} else if (data.start_time) {
|
||||
timestamp = new Date(data.start_time * 1000).toLocaleTimeString();
|
||||
} else if (data.timestamp) {
|
||||
timestamp = new Date(data.timestamp).toLocaleTimeString();
|
||||
} else {
|
||||
timestamp = new Date().toLocaleTimeString();
|
||||
}
|
||||
|
||||
const operationName = data.operation_name || "Unknown Operation";
|
||||
const duration = data.duration_ms
|
||||
? `${Number(data.duration_ms).toFixed(1)}ms`
|
||||
: "";
|
||||
const entityId = data.entity_id || "";
|
||||
const timestamp = new Date(group.timestamp * 1000).toLocaleTimeString();
|
||||
const duration = group.totalDuration > 0 ? `${group.totalDuration.toFixed(0)}ms` : "";
|
||||
const spanCount = group.traces.reduce((count, node) => {
|
||||
const countNode = (n: TraceNode): number => 1 + n.children.reduce((c, child) => c + countNode(child), 0);
|
||||
return count + countNode(node);
|
||||
}, 0);
|
||||
|
||||
return (
|
||||
<div className="border-l-2 border-muted pl-3 py-2 hover:bg-muted/50 transition-colors">
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground mb-1">
|
||||
<Search className="h-3 w-3 text-orange-600 dark:text-orange-400" />
|
||||
<span className="font-mono">{timestamp}</span>
|
||||
<Badge variant="outline" className="text-xs py-0">
|
||||
trace
|
||||
</Badge>
|
||||
<div className="border rounded-lg overflow-hidden">
|
||||
{/* Group header */}
|
||||
<div
|
||||
className="flex items-center gap-2 p-2 bg-muted/50 cursor-pointer hover:bg-muted/70 transition-colors"
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
>
|
||||
<div className="text-muted-foreground">
|
||||
{isExpanded ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
|
||||
</div>
|
||||
|
||||
<span className="font-mono text-xs text-muted-foreground">{timestamp}</span>
|
||||
|
||||
{group.entity_id && (
|
||||
<Badge variant="outline" className="text-xs py-0">
|
||||
{group.entity_id.replace("agent_", "").replace("workflow_", "")}
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
<div className="flex-1" />
|
||||
|
||||
{duration && (
|
||||
<Badge variant="secondary" className="text-xs py-0">
|
||||
{duration}
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{spanCount} span{spanCount !== 1 ? "s" : ""}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="text-sm">
|
||||
<div
|
||||
className="flex items-center gap-2 cursor-pointer"
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
>
|
||||
<div className="text-muted-foreground">
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="h-3 w-3" />
|
||||
) : (
|
||||
<ChevronRight className="h-3 w-3" />
|
||||
)}
|
||||
</div>
|
||||
<div className="text-muted-foreground flex-1 break-all">
|
||||
<span className="font-medium">{operationName}</span>
|
||||
{entityId && <span className="ml-2 text-xs">({entityId})</span>}
|
||||
</div>
|
||||
{/* Group content - trace tree */}
|
||||
{isExpanded && (
|
||||
<div className="p-2 border-t">
|
||||
{group.traces.map((node, idx) => (
|
||||
<TraceTreeNode key={node.data.span_id || idx} node={node} depth={0} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
{/* Expandable content */}
|
||||
{isExpanded && (
|
||||
<div className="mt-2 ml-5 p-3 bg-muted/30 rounded border">
|
||||
<div className="space-y-2">
|
||||
function TracesTab({ events }: { events: ExtendedResponseStreamEvent[] }) {
|
||||
// Use persisted store state instead of local useState
|
||||
const subTab = useDevUIStore((state) => state.debugTraceSubTab);
|
||||
const setSubTab = useDevUIStore((state) => state.setDebugTraceSubTab);
|
||||
|
||||
// ONLY show actual trace events
|
||||
const traceEvents = events.filter(
|
||||
(e) => e.type === "response.trace.completed"
|
||||
);
|
||||
|
||||
// Build hierarchical structure grouped by response_id
|
||||
const traceGroups = buildTraceHierarchy(traceEvents);
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
{/* Sub-tab header */}
|
||||
<div className="flex items-center gap-2 p-3 border-b">
|
||||
<Search className="h-4 w-4" />
|
||||
<span className="font-medium">Traces</span>
|
||||
<Badge variant="outline">{traceEvents.length}</Badge>
|
||||
|
||||
{/* Sub-tab toggle */}
|
||||
<div className="flex-1" />
|
||||
<div className="flex items-center bg-muted rounded-md p-1 min-w-0">
|
||||
<button
|
||||
onClick={() => setSubTab("spans")}
|
||||
className={`px-3 py-1.5 text-xs rounded transition-colors truncate ${
|
||||
subTab === "spans"
|
||||
? "bg-background shadow-sm font-medium"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
OTel Spans
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setSubTab("context")}
|
||||
className={`px-3 py-1.5 text-xs rounded transition-colors flex items-center gap-1.5 min-w-0 ${
|
||||
subTab === "context"
|
||||
? "bg-background shadow-sm font-medium"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
<BarChart3 className="h-3.5 w-3.5 flex-shrink-0" />
|
||||
<span className="truncate">Context Inspector</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sub-tab content */}
|
||||
{subTab === "spans" ? (
|
||||
<div className="flex-1 flex flex-col min-h-0">
|
||||
{/* OTel Spans header - only show when we have data */}
|
||||
{traceEvents.length > 0 && (
|
||||
<div className="p-3 border-b flex-shrink-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<Search className="h-4 w-4 text-orange-500" />
|
||||
<span className="font-semibold text-sm">Trace Details</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-2 text-xs">
|
||||
<div>
|
||||
<span className="font-medium text-muted-foreground">
|
||||
Operation:
|
||||
</span>
|
||||
<span className="ml-2 font-mono bg-orange-100 dark:bg-orange-900 px-2 py-1 rounded">
|
||||
{operationName}
|
||||
</span>
|
||||
</div>
|
||||
{data.span_id && (
|
||||
<div>
|
||||
<span className="font-medium text-muted-foreground">
|
||||
Span ID:
|
||||
</span>
|
||||
<span className="ml-2 font-mono text-xs break-all">
|
||||
{data.span_id}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{data.trace_id && (
|
||||
<div>
|
||||
<span className="font-medium text-muted-foreground">
|
||||
Trace ID:
|
||||
</span>
|
||||
<span className="ml-2 font-mono text-xs break-all">
|
||||
{data.trace_id}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{data.parent_span_id && (
|
||||
<div>
|
||||
<span className="font-medium text-muted-foreground">
|
||||
Parent Span:
|
||||
</span>
|
||||
<span className="ml-2 font-mono text-xs break-all">
|
||||
{data.parent_span_id}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{data.duration_ms && (
|
||||
<div>
|
||||
<span className="font-medium text-muted-foreground">
|
||||
Duration:
|
||||
</span>
|
||||
<span className="ml-2 font-mono text-xs">
|
||||
{Number(data.duration_ms).toFixed(2)}ms
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{data.status && (
|
||||
<div>
|
||||
<span className="font-medium text-muted-foreground">
|
||||
Status:
|
||||
</span>
|
||||
<span
|
||||
className={`ml-2 px-2 py-1 rounded text-xs font-medium ${data.status === "StatusCode.UNSET" ||
|
||||
data.status === "OK"
|
||||
? "bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200"
|
||||
: "bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200"
|
||||
}`}
|
||||
>
|
||||
{data.status || "unknown"}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{data.entity_id && (
|
||||
<div>
|
||||
<span className="font-medium text-muted-foreground">
|
||||
Entity:
|
||||
</span>
|
||||
<span className="ml-2 font-mono text-xs break-all">
|
||||
{data.entity_id}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{data.attributes && Object.keys(data.attributes).length > 0 && (
|
||||
<div>
|
||||
<span className="font-medium text-muted-foreground">
|
||||
Attributes:
|
||||
</span>
|
||||
<div className="mt-1 max-h-32 overflow-auto">
|
||||
<pre className="text-xs bg-background border rounded p-2 whitespace-pre-wrap max-w-full break-all">
|
||||
{(() => {
|
||||
try {
|
||||
// Try to pretty-print JSON, and unescape string values that contain JSON
|
||||
const attrs = { ...data.attributes };
|
||||
Object.keys(attrs).forEach((key) => {
|
||||
if (
|
||||
typeof attrs[key] === "string" &&
|
||||
attrs[key].startsWith("[")
|
||||
) {
|
||||
try {
|
||||
attrs[key] = JSON.parse(attrs[key]);
|
||||
} catch {
|
||||
// Keep original if parsing fails
|
||||
}
|
||||
}
|
||||
});
|
||||
return JSON.stringify(attrs, null, 2);
|
||||
} catch {
|
||||
return JSON.stringify(data.attributes, null, 2);
|
||||
}
|
||||
})()}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<Search className="h-4 w-4" />
|
||||
<span className="font-medium text-sm">OTel Spans</span>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{traceGroups.length} turn{traceGroups.length !== 1 ? "s" : ""}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{traceEvents.length === 0 ? (
|
||||
<div className="flex flex-col items-center text-center p-6 pt-9">
|
||||
<BarChart3 className="h-8 w-8 text-muted-foreground mb-3" />
|
||||
<div className="text-sm font-medium mb-1">No Data</div>
|
||||
<div className="text-xs text-muted-foreground max-w-[200px]">
|
||||
Run{" "}
|
||||
<span className="font-mono bg-accent/10 px-1 rounded">
|
||||
devui --instrumentation
|
||||
</span>{" "}
|
||||
and start a conversation.
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<ScrollArea className="flex-1">
|
||||
<div className="p-3">
|
||||
<div className="space-y-3">
|
||||
{traceGroups.map((group) => (
|
||||
<TraceGroupItem key={group.response_id} group={group} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<ContextInspector events={events} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1590,19 +1751,48 @@ export function DebugPanel({
|
||||
isStreaming = false,
|
||||
onMinimize,
|
||||
}: DebugPanelProps) {
|
||||
// Use persisted store state for active tab
|
||||
const activeTab = useDevUIStore((state) => state.debugPanelTab);
|
||||
const setActiveTab = useDevUIStore((state) => state.setDebugPanelTab);
|
||||
|
||||
// Compute counts once for tab badges (memoized to avoid perf hits)
|
||||
const counts = useMemo(() => {
|
||||
const processedEvents = processEventsForDisplay(events);
|
||||
const eventsCount = processedEvents.length;
|
||||
const tracesCount = events.filter(e => e.type === "response.trace.completed").length;
|
||||
const toolsCount = processedEvents.filter(e => e.type === "response.function_call.complete").length
|
||||
+ events.filter(e => getFunctionResultFromEvent(e) !== null).length;
|
||||
return { eventsCount, tracesCount, toolsCount };
|
||||
}, [events]);
|
||||
|
||||
return (
|
||||
<div className="flex-1 border-l flex flex-col min-h-0">
|
||||
<Tabs defaultValue="events" className="flex-1 flex flex-col min-h-0">
|
||||
<Tabs value={activeTab} onValueChange={(v) => setActiveTab(v as "events" | "traces" | "tools")} className="flex-1 flex flex-col min-h-0">
|
||||
<div className="px-3 pt-3 flex items-center gap-2 flex-shrink-0">
|
||||
<TabsList className="flex-1">
|
||||
<TabsTrigger value="events" className="flex-1">
|
||||
<TabsTrigger value="events" className="flex-1 gap-1.5">
|
||||
Events
|
||||
{counts.eventsCount > 0 && (
|
||||
<span className="text-[10px] bg-muted-foreground/20 text-muted-foreground px-1.5 py-0.5 rounded-full min-w-[1.25rem] text-center">
|
||||
{counts.eventsCount}
|
||||
</span>
|
||||
)}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="traces" className="flex-1">
|
||||
<TabsTrigger value="traces" className="flex-1 gap-1.5">
|
||||
Traces
|
||||
{counts.tracesCount > 0 && (
|
||||
<span className="text-[10px] bg-muted-foreground/20 text-muted-foreground px-1.5 py-0.5 rounded-full min-w-[1.25rem] text-center">
|
||||
{counts.tracesCount}
|
||||
</span>
|
||||
)}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="tools" className="flex-1">
|
||||
<TabsTrigger value="tools" className="flex-1 gap-1.5">
|
||||
Tools
|
||||
{counts.toolsCount > 0 && (
|
||||
<span className="text-[10px] bg-muted-foreground/20 text-muted-foreground px-1.5 py-0.5 rounded-full min-w-[1.25rem] text-center">
|
||||
{counts.toolsCount}
|
||||
</span>
|
||||
)}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
{onMinimize && (
|
||||
|
||||
@@ -209,7 +209,7 @@ export function DeploymentModal({
|
||||
setCopiedTemplate(null);
|
||||
timeoutRef.current = null;
|
||||
}, 2000);
|
||||
} catch (err) {
|
||||
} catch {
|
||||
// Reset state on error - clipboard write failed
|
||||
setCopiedTemplate(null);
|
||||
}
|
||||
@@ -248,7 +248,7 @@ services:
|
||||
- AZURE_OPENAI_API_KEY=\${AZURE_OPENAI_API_KEY}
|
||||
- AZURE_OPENAI_ENDPOINT=\${AZURE_OPENAI_ENDPOINT}
|
||||
- AZURE_OPENAI_CHAT_DEPLOYMENT_NAME=\${AZURE_OPENAI_CHAT_DEPLOYMENT_NAME}
|
||||
# Optional: Enable tracing
|
||||
# Optional: Enable instrumentation
|
||||
- ENABLE_INSTRUMENTATION=\${ENABLE_INSTRUMENTATION:-false}
|
||||
ports:
|
||||
- "8080:8080"
|
||||
|
||||
@@ -41,8 +41,8 @@ export function SettingsModal({
|
||||
}: SettingsModalProps) {
|
||||
const [activeTab, setActiveTab] = useState<Tab>("general");
|
||||
|
||||
// OpenAI proxy mode, Azure deployment, auth status, server capabilities, and version from store
|
||||
const { oaiMode, setOAIMode, azureDeploymentEnabled, setAzureDeploymentEnabled, authRequired, serverCapabilities, serverVersion, runtime, uiMode } = useDevUIStore();
|
||||
// OpenAI proxy mode, Azure deployment, auth status, server capabilities, streaming, and version from store
|
||||
const { oaiMode, setOAIMode, azureDeploymentEnabled, setAzureDeploymentEnabled, authRequired, serverCapabilities, serverVersion, runtime, uiMode, streamingEnabled, setStreamingEnabled } = useDevUIStore();
|
||||
|
||||
// Get current backend URL from localStorage or default
|
||||
const defaultUrl = import.meta.env.VITE_API_BASE_URL !== undefined ? import.meta.env.VITE_API_BASE_URL : "";
|
||||
@@ -353,6 +353,37 @@ export function SettingsModal({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Streaming Mode Setting */}
|
||||
<div className="space-y-3 border-t pt-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<Label className="text-sm font-medium">
|
||||
Streaming Mode
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Stream responses token-by-token as they're generated
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={streamingEnabled}
|
||||
onCheckedChange={setStreamingEnabled}
|
||||
/>
|
||||
</div>
|
||||
{!streamingEnabled && (
|
||||
<div className="flex items-start gap-2 text-xs text-amber-600 dark:text-amber-400 bg-amber-500/10 p-3 rounded">
|
||||
<Info className="h-3.5 w-3.5 flex-shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<p className="font-medium">Non-streaming mode limitations:</p>
|
||||
<ul className="mt-1 space-y-0.5 list-disc list-inside text-amber-600/80 dark:text-amber-400/80">
|
||||
<li>Tool calls won't display in real-time</li>
|
||||
<li>No typing indicator during generation</li>
|
||||
<li>Response appears all at once when complete</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -628,11 +659,11 @@ export function SettingsModal({
|
||||
<div className="space-y-2 pt-2">
|
||||
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wide">Capabilities</p>
|
||||
<div className="space-y-1 text-sm">
|
||||
{serverCapabilities?.tracing !== undefined && (
|
||||
{serverCapabilities?.instrumentation !== undefined && (
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-muted-foreground">Tracing:</span>
|
||||
<span className={`text-xs px-2 py-0.5 rounded-full ${serverCapabilities.tracing ? 'bg-green-500/10 text-green-600 dark:text-green-400' : 'bg-muted text-muted-foreground'}`}>
|
||||
{serverCapabilities.tracing ? 'Enabled' : 'Disabled'}
|
||||
<span className="text-muted-foreground">Instrumentation:</span>
|
||||
<span className={`text-xs px-2 py-0.5 rounded-full ${serverCapabilities.instrumentation ? 'bg-green-500/10 text-green-600 dark:text-green-400' : 'bg-muted text-muted-foreground'}`}>
|
||||
{serverCapabilities.instrumentation ? 'Enabled' : 'Disabled'}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import * as React from "react";
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const TooltipProvider = TooltipPrimitive.Provider;
|
||||
|
||||
const Tooltip = TooltipPrimitive.Root;
|
||||
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger;
|
||||
|
||||
const TooltipContent = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</TooltipPrimitive.Portal>
|
||||
));
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
||||
@@ -398,7 +398,11 @@ class ApiClient {
|
||||
async listConversationItems(
|
||||
conversationId: string,
|
||||
options?: { limit?: number; after?: string; order?: "asc" | "desc" }
|
||||
): Promise<{ data: unknown[]; has_more: boolean }> {
|
||||
): Promise<{
|
||||
data: unknown[];
|
||||
has_more: boolean;
|
||||
metadata?: { traces?: unknown[] };
|
||||
}> {
|
||||
const params = new URLSearchParams();
|
||||
if (options?.limit) params.set("limit", options.limit.toString());
|
||||
if (options?.after) params.set("after", options.after);
|
||||
@@ -409,7 +413,11 @@ class ApiClient {
|
||||
queryString ? `?${queryString}` : ""
|
||||
}`;
|
||||
|
||||
return this.request<{ data: unknown[]; has_more: boolean }>(url);
|
||||
return this.request<{
|
||||
data: unknown[];
|
||||
has_more: boolean;
|
||||
metadata?: { traces?: unknown[] };
|
||||
}>(url);
|
||||
}
|
||||
|
||||
async getConversationItem(
|
||||
@@ -800,34 +808,68 @@ class ApiClient {
|
||||
yield* this.streamOpenAIResponse(openAIRequest, request.conversation_id, signal);
|
||||
}
|
||||
|
||||
// REMOVED: Legacy streaming methods - use streamAgentExecutionOpenAI and streamWorkflowExecutionOpenAI instead
|
||||
// ========================================
|
||||
// Non-Streaming Execution Methods
|
||||
// ========================================
|
||||
|
||||
// Non-streaming execution (for testing)
|
||||
async runAgent(
|
||||
// Non-streaming agent execution using /v1/responses with stream=false
|
||||
async runAgentSync(
|
||||
agentId: string,
|
||||
request: RunAgentRequest
|
||||
): Promise<{
|
||||
conversation_id: string;
|
||||
result: unknown[];
|
||||
message_count: number;
|
||||
}> {
|
||||
return this.request(`/agents/${agentId}/run`, {
|
||||
): Promise<import("@/types/openai").OpenAIResponse> {
|
||||
// Check if OAI proxy mode is enabled
|
||||
const { oaiMode } = await import("@/stores").then((m) => ({
|
||||
oaiMode: m.useDevUIStore.getState().oaiMode,
|
||||
}));
|
||||
|
||||
const openAIRequest: AgentFrameworkRequest = {
|
||||
metadata: { entity_id: agentId },
|
||||
input: request.input,
|
||||
stream: false,
|
||||
conversation: request.conversation_id,
|
||||
};
|
||||
|
||||
// Apply OAI mode settings if enabled
|
||||
if (oaiMode.enabled) {
|
||||
openAIRequest.model = oaiMode.model;
|
||||
if (oaiMode.temperature !== undefined) {
|
||||
openAIRequest.temperature = oaiMode.temperature;
|
||||
}
|
||||
if (oaiMode.max_output_tokens !== undefined) {
|
||||
openAIRequest.max_output_tokens = oaiMode.max_output_tokens;
|
||||
}
|
||||
}
|
||||
|
||||
const headers: Record<string, string> = {};
|
||||
if (oaiMode.enabled) {
|
||||
headers["X-Proxy-Backend"] = "openai";
|
||||
}
|
||||
|
||||
return this.request<import("@/types/openai").OpenAIResponse>("/v1/responses", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(request),
|
||||
headers,
|
||||
body: JSON.stringify(openAIRequest),
|
||||
});
|
||||
}
|
||||
|
||||
async runWorkflow(
|
||||
// Non-streaming workflow execution using /v1/responses with stream=false
|
||||
async runWorkflowSync(
|
||||
workflowId: string,
|
||||
request: RunWorkflowRequest
|
||||
): Promise<{
|
||||
result: string;
|
||||
events: number;
|
||||
message_count: number;
|
||||
}> {
|
||||
return this.request(`/workflows/${workflowId}/run`, {
|
||||
): Promise<import("@/types/openai").OpenAIResponse> {
|
||||
const openAIRequest: AgentFrameworkRequest = {
|
||||
metadata: { entity_id: workflowId },
|
||||
input: JSON.stringify(request.input_data || {}),
|
||||
stream: false,
|
||||
conversation: request.conversation_id,
|
||||
extra_body: request.checkpoint_id
|
||||
? { entity_id: workflowId, checkpoint_id: request.checkpoint_id }
|
||||
: undefined,
|
||||
};
|
||||
|
||||
return this.request<import("@/types/openai").OpenAIResponse>("/v1/responses", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(request),
|
||||
body: JSON.stringify(openAIRequest),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -60,6 +60,13 @@ interface DevUIState {
|
||||
debugEvents: ExtendedResponseStreamEvent[];
|
||||
isResizing: boolean;
|
||||
showToolCalls: boolean; // UI setting to show/hide tool calls in chat
|
||||
streamingEnabled: boolean; // Whether to use streaming mode for responses
|
||||
|
||||
// Debug Panel Preferences (persisted)
|
||||
debugPanelTab: "events" | "traces" | "tools"; // Main debug panel tab
|
||||
debugTraceSubTab: "spans" | "context"; // OTel Spans vs Context Inspector
|
||||
contextInspectorViewMode: "tokens" | "composition";
|
||||
contextInspectorCumulative: boolean;
|
||||
|
||||
// Modal Slice
|
||||
showAboutModal: boolean;
|
||||
@@ -82,7 +89,7 @@ interface DevUIState {
|
||||
uiMode: "developer" | "user";
|
||||
runtime: "python" | "dotnet";
|
||||
serverCapabilities: {
|
||||
tracing: boolean;
|
||||
instrumentation: boolean;
|
||||
openai_proxy: boolean;
|
||||
deployment: boolean;
|
||||
};
|
||||
@@ -146,6 +153,13 @@ interface DevUIActions {
|
||||
clearDebugEvents: () => void;
|
||||
setIsResizing: (resizing: boolean) => void;
|
||||
setShowToolCalls: (show: boolean) => void;
|
||||
setStreamingEnabled: (enabled: boolean) => void;
|
||||
|
||||
// Debug Panel Preference Actions
|
||||
setDebugPanelTab: (tab: "events" | "traces" | "tools") => void;
|
||||
setDebugTraceSubTab: (tab: "spans" | "context") => void;
|
||||
setContextInspectorViewMode: (mode: "tokens" | "composition") => void;
|
||||
setContextInspectorCumulative: (cumulative: boolean) => void;
|
||||
|
||||
// Modal Actions
|
||||
setShowAboutModal: (show: boolean) => void;
|
||||
@@ -166,7 +180,7 @@ interface DevUIActions {
|
||||
toggleOAIMode: () => void;
|
||||
|
||||
// Server Meta Actions
|
||||
setServerMeta: (meta: { uiMode: "developer" | "user"; runtime: "python" | "dotnet"; capabilities: { tracing: boolean; openai_proxy: boolean; deployment: boolean }; authRequired: boolean; version?: string }) => void;
|
||||
setServerMeta: (meta: { uiMode: "developer" | "user"; runtime: "python" | "dotnet"; capabilities: { instrumentation: boolean; openai_proxy: boolean; deployment: boolean }; authRequired: boolean; version?: string }) => void;
|
||||
|
||||
// Deployment Actions
|
||||
startDeployment: () => void;
|
||||
@@ -228,6 +242,13 @@ export const useDevUIStore = create<DevUIStore>()(
|
||||
debugEvents: [],
|
||||
isResizing: false,
|
||||
showToolCalls: true, // Default to showing tool calls
|
||||
streamingEnabled: true, // Default to streaming mode (recommended)
|
||||
|
||||
// Debug Panel Preferences (persisted)
|
||||
debugPanelTab: "events", // Default to events tab
|
||||
debugTraceSubTab: "spans", // Default to spans sub-tab
|
||||
contextInspectorViewMode: "tokens", // Default to tokens view
|
||||
contextInspectorCumulative: false, // Default to per-message view
|
||||
|
||||
// Modal State
|
||||
showAboutModal: false,
|
||||
@@ -248,7 +269,7 @@ export const useDevUIStore = create<DevUIStore>()(
|
||||
uiMode: "developer", // Default to developer mode
|
||||
runtime: "python", // Default to Python runtime
|
||||
serverCapabilities: {
|
||||
tracing: false,
|
||||
instrumentation: false,
|
||||
openai_proxy: false,
|
||||
deployment: false,
|
||||
},
|
||||
@@ -371,14 +392,16 @@ export const useDevUIStore = create<DevUIStore>()(
|
||||
setDebugPanelMinimized: (minimized) => set({ debugPanelMinimized: minimized }),
|
||||
setDebugPanelWidth: (width) => set({ debugPanelWidth: width }),
|
||||
setShowToolCalls: (show) => set({ showToolCalls: show }),
|
||||
setStreamingEnabled: (enabled) => set({ streamingEnabled: enabled }),
|
||||
addDebugEvent: (event) =>
|
||||
set((state) => {
|
||||
// Generate unique timestamp for each event
|
||||
// Use current time + small increment to ensure uniqueness even for rapid events
|
||||
const baseTimestamp = Math.floor(Date.now() / 1000);
|
||||
const lastTimestamp = state.debugEvents.length > 0
|
||||
? (state.debugEvents[state.debugEvents.length - 1] as any)._uiTimestamp || 0
|
||||
: 0;
|
||||
const lastEvent = state.debugEvents.length > 0
|
||||
? state.debugEvents[state.debugEvents.length - 1] as { _uiTimestamp?: number }
|
||||
: null;
|
||||
const lastTimestamp = lastEvent?._uiTimestamp ?? 0;
|
||||
// Ensure new timestamp is always greater than the last one
|
||||
const uniqueTimestamp = Math.max(baseTimestamp, lastTimestamp + 1);
|
||||
|
||||
@@ -399,6 +422,12 @@ export const useDevUIStore = create<DevUIStore>()(
|
||||
clearDebugEvents: () => set({ debugEvents: [] }),
|
||||
setIsResizing: (resizing) => set({ isResizing: resizing }),
|
||||
|
||||
// Debug Panel Preference Actions
|
||||
setDebugPanelTab: (tab) => set({ debugPanelTab: tab }),
|
||||
setDebugTraceSubTab: (tab) => set({ debugTraceSubTab: tab }),
|
||||
setContextInspectorViewMode: (mode) => set({ contextInspectorViewMode: mode }),
|
||||
setContextInspectorCumulative: (cumulative) => set({ contextInspectorCumulative: cumulative }),
|
||||
|
||||
// ========================================
|
||||
// Modal Actions
|
||||
// ========================================
|
||||
@@ -605,8 +634,14 @@ export const useDevUIStore = create<DevUIStore>()(
|
||||
debugPanelMinimized: state.debugPanelMinimized,
|
||||
debugPanelWidth: state.debugPanelWidth,
|
||||
showToolCalls: state.showToolCalls, // Persist tool calls visibility preference
|
||||
streamingEnabled: state.streamingEnabled, // Persist streaming mode preference
|
||||
oaiMode: state.oaiMode, // Persist OpenAI proxy mode settings
|
||||
azureDeploymentEnabled: state.azureDeploymentEnabled, // Persist Azure deployment preference
|
||||
// Debug panel tab preferences
|
||||
debugPanelTab: state.debugPanelTab,
|
||||
debugTraceSubTab: state.debugTraceSubTab,
|
||||
contextInspectorViewMode: state.contextInspectorViewMode,
|
||||
contextInspectorCumulative: state.contextInspectorCumulative,
|
||||
}),
|
||||
}
|
||||
),
|
||||
|
||||
@@ -130,6 +130,7 @@ export type {
|
||||
ResponseCompletedEvent,
|
||||
ResponseFailedEvent,
|
||||
ResponseFunctionResultComplete,
|
||||
ResponseFunctionToolCall,
|
||||
StructuredEvent,
|
||||
WorkflowItem,
|
||||
ExecutorActionItem,
|
||||
@@ -159,7 +160,7 @@ export interface MetaResponse {
|
||||
framework: string;
|
||||
runtime: "python" | "dotnet";
|
||||
capabilities: {
|
||||
tracing: boolean;
|
||||
instrumentation: boolean;
|
||||
openai_proxy: boolean;
|
||||
deployment: boolean;
|
||||
};
|
||||
@@ -266,6 +267,29 @@ export interface CheckpointInfo {
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
// Full checkpoint data structure
|
||||
export interface FullCheckpoint {
|
||||
checkpoint_id: string;
|
||||
workflow_id: string;
|
||||
timestamp: string;
|
||||
messages: Record<string, unknown[]>;
|
||||
shared_state: Record<string, unknown>;
|
||||
pending_request_info_events: Record<string, PendingRequestInfoEvent>;
|
||||
iteration_count: number;
|
||||
metadata: Record<string, unknown>;
|
||||
version: string;
|
||||
}
|
||||
|
||||
// Pending request info event data
|
||||
export interface PendingRequestInfoEvent {
|
||||
source_executor_id: string;
|
||||
request_type?: string;
|
||||
response_type?: string;
|
||||
request_data?: Record<string, unknown>;
|
||||
request_schema?: Record<string, unknown>;
|
||||
timestamp?: string;
|
||||
}
|
||||
|
||||
// Checkpoint item from conversation items API
|
||||
export interface CheckpointItem {
|
||||
id: string;
|
||||
@@ -281,16 +305,6 @@ export interface CheckpointItem {
|
||||
message_count: number;
|
||||
size_bytes?: number;
|
||||
version: string;
|
||||
full_checkpoint?: {
|
||||
checkpoint_id: string;
|
||||
workflow_id: string;
|
||||
timestamp: string;
|
||||
messages: Record<string, unknown[]>;
|
||||
shared_state: Record<string, unknown>;
|
||||
pending_request_info_events: Record<string, unknown>;
|
||||
iteration_count: number;
|
||||
metadata: Record<string, unknown>;
|
||||
version: string;
|
||||
};
|
||||
full_checkpoint?: FullCheckpoint;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,6 +3,45 @@
|
||||
* Based on OpenAI's official response types
|
||||
*/
|
||||
|
||||
// OpenAI Response Error (from response_error.py)
|
||||
export type ResponseErrorCode =
|
||||
| "server_error"
|
||||
| "rate_limit_exceeded"
|
||||
| "invalid_prompt"
|
||||
| "vector_store_timeout"
|
||||
| "invalid_image"
|
||||
| "invalid_image_format"
|
||||
| "invalid_base64_image"
|
||||
| "invalid_image_url"
|
||||
| "image_too_large"
|
||||
| "image_too_small"
|
||||
| "image_parse_error"
|
||||
| "image_content_policy_violation"
|
||||
| "invalid_image_mode"
|
||||
| "image_file_too_large"
|
||||
| "unsupported_image_media_type"
|
||||
| "empty_image_file"
|
||||
| "failed_to_download_image"
|
||||
| "image_file_not_found";
|
||||
|
||||
export interface ResponseError {
|
||||
code: ResponseErrorCode;
|
||||
message: string;
|
||||
}
|
||||
|
||||
// OpenAI Response Usage (from response_usage.py)
|
||||
export interface ResponseUsage {
|
||||
input_tokens: number;
|
||||
output_tokens: number;
|
||||
total_tokens: number;
|
||||
input_tokens_details?: {
|
||||
cached_tokens: number;
|
||||
};
|
||||
output_tokens_details?: {
|
||||
reasoning_tokens: number;
|
||||
};
|
||||
}
|
||||
|
||||
// Core OpenAI Response Stream Event
|
||||
export interface ResponseStreamEvent {
|
||||
type: string;
|
||||
@@ -28,7 +67,7 @@ export interface ResponseCreatedEvent {
|
||||
id: string;
|
||||
status: "in_progress";
|
||||
created_at: number;
|
||||
output?: any[];
|
||||
output?: ResponseOutputItem[];
|
||||
};
|
||||
sequence_number?: number;
|
||||
}
|
||||
@@ -47,9 +86,11 @@ export interface ResponseCompletedEvent {
|
||||
response: {
|
||||
id: string;
|
||||
status?: "completed";
|
||||
usage?: any; // Optional usage information
|
||||
usage?: ResponseUsage; // Optional usage information
|
||||
model?: string; // Optional model information
|
||||
[key: string]: any; // Allow any additional fields
|
||||
output?: ResponseOutputItem[]; // Output items
|
||||
error?: ResponseError; // Error if failed
|
||||
metadata?: Record<string, unknown>; // Additional metadata
|
||||
};
|
||||
sequence_number?: number;
|
||||
}
|
||||
@@ -59,7 +100,7 @@ export interface ResponseFailedEvent {
|
||||
response: {
|
||||
id: string;
|
||||
status: "failed";
|
||||
error?: any;
|
||||
error?: ResponseError;
|
||||
};
|
||||
sequence_number?: number;
|
||||
}
|
||||
@@ -157,16 +198,16 @@ export interface WorkflowItem {
|
||||
type: string; // "executor_action", "workflow_action", "message", or any future type
|
||||
id: string;
|
||||
status?: "in_progress" | "completed" | "failed" | "cancelled";
|
||||
[key: string]: any; // Allow any additional fields
|
||||
[key: string]: unknown; // Allow any additional fields with unknown type
|
||||
}
|
||||
|
||||
// Executor Action Item (DevUI specific)
|
||||
export interface ExecutorActionItem extends WorkflowItem {
|
||||
type: "executor_action";
|
||||
executor_id: string;
|
||||
metadata?: Record<string, any>;
|
||||
result?: any;
|
||||
error?: any;
|
||||
metadata?: Record<string, unknown>;
|
||||
result?: unknown;
|
||||
error?: unknown;
|
||||
}
|
||||
|
||||
// Type guard for executor actions
|
||||
@@ -364,18 +405,7 @@ export interface ResponseOutputText {
|
||||
annotations: Record<string, unknown>[];
|
||||
}
|
||||
|
||||
export interface ResponseUsage {
|
||||
input_tokens: number;
|
||||
output_tokens: number;
|
||||
total_tokens: number;
|
||||
input_tokens_details: {
|
||||
cached_tokens: number;
|
||||
};
|
||||
output_tokens_details: {
|
||||
reasoning_tokens: number;
|
||||
};
|
||||
}
|
||||
|
||||
// Note: ResponseUsage is defined at the top of this file
|
||||
|
||||
// Request format for Agent Framework
|
||||
// AgentFrameworkRequest moved to agent-framework.ts to avoid conflicts
|
||||
@@ -404,11 +434,62 @@ export interface MessageInputTextContent {
|
||||
text: string;
|
||||
}
|
||||
|
||||
// Annotation types for output text (from response_output_text.py)
|
||||
export interface AnnotationFileCitation {
|
||||
type: "file_citation";
|
||||
file_id: string;
|
||||
filename: string;
|
||||
index: number;
|
||||
}
|
||||
|
||||
export interface AnnotationURLCitation {
|
||||
type: "url_citation";
|
||||
url: string;
|
||||
title: string;
|
||||
start_index: number;
|
||||
end_index: number;
|
||||
}
|
||||
|
||||
export interface AnnotationContainerFileCitation {
|
||||
type: "container_file_citation";
|
||||
container_id: string;
|
||||
file_id: string;
|
||||
filename: string;
|
||||
start_index: number;
|
||||
end_index: number;
|
||||
}
|
||||
|
||||
export interface AnnotationFilePath {
|
||||
type: "file_path";
|
||||
file_id: string;
|
||||
index: number;
|
||||
}
|
||||
|
||||
export type OutputTextAnnotation =
|
||||
| AnnotationFileCitation
|
||||
| AnnotationURLCitation
|
||||
| AnnotationContainerFileCitation
|
||||
| AnnotationFilePath;
|
||||
|
||||
// Logprob types for output text
|
||||
export interface LogprobTopLogprob {
|
||||
token: string;
|
||||
bytes: number[];
|
||||
logprob: number;
|
||||
}
|
||||
|
||||
export interface Logprob {
|
||||
token: string;
|
||||
bytes: number[];
|
||||
logprob: number;
|
||||
top_logprobs: LogprobTopLogprob[];
|
||||
}
|
||||
|
||||
export interface MessageOutputTextContent {
|
||||
type: "output_text";
|
||||
text: string;
|
||||
annotations?: any[];
|
||||
logprobs?: any[];
|
||||
annotations?: OutputTextAnnotation[];
|
||||
logprobs?: Logprob[];
|
||||
}
|
||||
|
||||
export interface MessageInputImage {
|
||||
@@ -541,9 +622,218 @@ export interface Conversation {
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
// List response
|
||||
// ============================================================================
|
||||
// OpenTelemetry Trace Attribute Keys
|
||||
// Mirrored from Python: agent_framework/observability.py ObservabilityAttributes
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Standard attribute keys for OpenTelemetry traces.
|
||||
* These match the Python ObservabilityAttributes enum exactly.
|
||||
*/
|
||||
export const TraceAttributes = {
|
||||
// Request attributes
|
||||
MODEL: "gen_ai.request.model",
|
||||
MAX_TOKENS: "gen_ai.request.max_tokens",
|
||||
TEMPERATURE: "gen_ai.request.temperature",
|
||||
TOP_P: "gen_ai.request.top_p",
|
||||
SEED: "gen_ai.request.seed",
|
||||
FREQUENCY_PENALTY: "gen_ai.request.frequency_penalty",
|
||||
PRESENCE_PENALTY: "gen_ai.request.presence_penalty",
|
||||
STOP_SEQUENCES: "gen_ai.request.stop_sequences",
|
||||
|
||||
// Response attributes
|
||||
FINISH_REASONS: "gen_ai.response.finish_reasons",
|
||||
RESPONSE_ID: "gen_ai.response.id",
|
||||
|
||||
// Usage attributes
|
||||
INPUT_TOKENS: "gen_ai.usage.input_tokens",
|
||||
OUTPUT_TOKENS: "gen_ai.usage.output_tokens",
|
||||
|
||||
// Content attributes (messages sent/received)
|
||||
INPUT_MESSAGES: "gen_ai.input.messages",
|
||||
OUTPUT_MESSAGES: "gen_ai.output.messages",
|
||||
SYSTEM_INSTRUCTIONS: "gen_ai.system_instructions",
|
||||
OUTPUT_TYPE: "gen_ai.output.type",
|
||||
|
||||
// Tool attributes
|
||||
TOOL_CALL_ID: "gen_ai.tool.call.id",
|
||||
TOOL_NAME: "gen_ai.tool.name",
|
||||
TOOL_TYPE: "gen_ai.tool.type",
|
||||
TOOL_DEFINITIONS: "gen_ai.tool.definitions",
|
||||
TOOL_ARGUMENTS: "gen_ai.tool.call.arguments",
|
||||
TOOL_RESULT: "gen_ai.tool.call.result",
|
||||
|
||||
// Agent attributes
|
||||
AGENT_ID: "gen_ai.agent.id",
|
||||
AGENT_NAME: "gen_ai.agent.name",
|
||||
AGENT_DESCRIPTION: "gen_ai.agent.description",
|
||||
CONVERSATION_ID: "gen_ai.conversation.id",
|
||||
|
||||
// Workflow attributes
|
||||
WORKFLOW_ID: "workflow.id",
|
||||
WORKFLOW_NAME: "workflow.name",
|
||||
EXECUTOR_ID: "executor.id",
|
||||
EXECUTOR_TYPE: "executor.type",
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Type for trace attribute keys - ensures type safety when accessing attributes
|
||||
*/
|
||||
export type TraceAttributeKey = (typeof TraceAttributes)[keyof typeof TraceAttributes];
|
||||
|
||||
/**
|
||||
* Typed interface for known trace attributes.
|
||||
* Using this instead of Record<string, unknown> provides compile-time safety.
|
||||
*/
|
||||
export interface TypedTraceAttributes {
|
||||
// Request attributes
|
||||
[TraceAttributes.MODEL]?: string;
|
||||
[TraceAttributes.MAX_TOKENS]?: number;
|
||||
[TraceAttributes.TEMPERATURE]?: number;
|
||||
[TraceAttributes.TOP_P]?: number;
|
||||
[TraceAttributes.SEED]?: number;
|
||||
|
||||
// Usage attributes
|
||||
[TraceAttributes.INPUT_TOKENS]?: number;
|
||||
[TraceAttributes.OUTPUT_TOKENS]?: number;
|
||||
|
||||
// Content attributes (JSON strings that need parsing)
|
||||
[TraceAttributes.INPUT_MESSAGES]?: string;
|
||||
[TraceAttributes.OUTPUT_MESSAGES]?: string;
|
||||
[TraceAttributes.SYSTEM_INSTRUCTIONS]?: string;
|
||||
|
||||
// Tool attributes
|
||||
[TraceAttributes.TOOL_NAME]?: string;
|
||||
[TraceAttributes.TOOL_DEFINITIONS]?: string;
|
||||
[TraceAttributes.TOOL_ARGUMENTS]?: string;
|
||||
[TraceAttributes.TOOL_RESULT]?: string;
|
||||
|
||||
// Agent/workflow attributes
|
||||
[TraceAttributes.AGENT_NAME]?: string;
|
||||
[TraceAttributes.WORKFLOW_NAME]?: string;
|
||||
[TraceAttributes.EXECUTOR_ID]?: string;
|
||||
|
||||
// Allow additional unknown attributes
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Message part types used in gen_ai.input.messages / gen_ai.output.messages
|
||||
*
|
||||
* Source: Python agent_framework/observability.py _to_otel_part()
|
||||
*
|
||||
* Python produces:
|
||||
* - text: {"type": "text", "content": "..."}
|
||||
* - function_call: {"type": "tool_call", "id": "...", "name": "...", "arguments": "..."}
|
||||
* - function_result: {"type": "tool_call_response", "id": "...", "response": "..."}
|
||||
*/
|
||||
|
||||
// Text content part
|
||||
// Python: {"type": "text", "content": content.text}
|
||||
export interface TraceTextPart {
|
||||
type: "text";
|
||||
content?: string; // Agent Framework format (from Python)
|
||||
text?: string; // Alternative field name (OpenAI format)
|
||||
}
|
||||
|
||||
// Tool/function call part (from assistant)
|
||||
// Python: {"type": "tool_call", "id": content.call_id, "name": content.name, "arguments": content.arguments}
|
||||
export interface TraceToolCallPart {
|
||||
type: "tool_call" | "function_call";
|
||||
id?: string; // Tool call ID for correlation
|
||||
name?: string; // Function name
|
||||
arguments?: string; // JSON string of arguments
|
||||
}
|
||||
|
||||
// Tool/function result part (response to tool call)
|
||||
// Python: {"type": "tool_call_response", "id": content.call_id, "response": response}
|
||||
export interface TraceToolResultPart {
|
||||
type: "tool_call_response" | "tool_result" | "function_result";
|
||||
id?: string; // Tool call ID for correlation
|
||||
response?: string; // Agent Framework format (from Python)
|
||||
result?: string; // Alternative field name (other formats)
|
||||
}
|
||||
|
||||
// Union type for all message parts
|
||||
export type TraceMessagePart = TraceTextPart | TraceToolCallPart | TraceToolResultPart;
|
||||
|
||||
// Helper type guard functions
|
||||
export function isTextPart(part: TraceMessagePart): part is TraceTextPart {
|
||||
return part.type === "text";
|
||||
}
|
||||
|
||||
export function isToolCallPart(part: TraceMessagePart): part is TraceToolCallPart {
|
||||
return part.type === "tool_call" || part.type === "function_call";
|
||||
}
|
||||
|
||||
export function isToolResultPart(part: TraceMessagePart): part is TraceToolResultPart {
|
||||
return (
|
||||
part.type === "tool_result" ||
|
||||
part.type === "function_result" ||
|
||||
part.type === "tool_call_response"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Message structure in gen_ai.input.messages / gen_ai.output.messages
|
||||
* Format: [{role: "system"|"user"|"assistant"|"tool", parts: [...]}]
|
||||
*/
|
||||
export interface TraceMessage {
|
||||
role: "system" | "user" | "assistant" | "tool";
|
||||
parts: TraceMessagePart[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to safely get a typed attribute value
|
||||
*/
|
||||
export function getTraceAttribute<K extends keyof TypedTraceAttributes>(
|
||||
attributes: TypedTraceAttributes,
|
||||
key: K
|
||||
): TypedTraceAttributes[K] {
|
||||
return attributes[key];
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to parse JSON message array from trace attributes
|
||||
*/
|
||||
export function parseTraceMessages(jsonString: string | undefined): TraceMessage[] {
|
||||
if (!jsonString) return [];
|
||||
try {
|
||||
return JSON.parse(jsonString) as TraceMessage[];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Stored trace span (from conversation metadata)
|
||||
export interface TraceSpan {
|
||||
type?: string;
|
||||
span_id: string;
|
||||
trace_id: string;
|
||||
parent_span_id?: string | null;
|
||||
operation_name: string;
|
||||
start_time: number;
|
||||
end_time?: number;
|
||||
duration_ms?: number;
|
||||
attributes: TypedTraceAttributes;
|
||||
status: string;
|
||||
response_id?: string | null;
|
||||
entity_id?: string;
|
||||
events?: Array<{
|
||||
name: string;
|
||||
timestamp: number;
|
||||
attributes?: Record<string, unknown>;
|
||||
}>;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// List response with trace metadata (DevUI extension)
|
||||
export interface ConversationItemsListResponse {
|
||||
object: "list";
|
||||
data: ConversationItem[];
|
||||
has_more: boolean;
|
||||
metadata?: {
|
||||
traces?: TraceSpan[];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -99,54 +99,54 @@ export interface Workflow {
|
||||
* Type guards for runtime type checking
|
||||
*/
|
||||
export function isWorkflow(obj: unknown): obj is Workflow {
|
||||
if (typeof obj !== "object" || obj === null) return false;
|
||||
const record = obj as Record<string, unknown>;
|
||||
return (
|
||||
typeof obj === "object" &&
|
||||
obj !== null &&
|
||||
"id" in obj &&
|
||||
"edge_groups" in obj &&
|
||||
"executors" in obj &&
|
||||
"start_executor_id" in obj &&
|
||||
"max_iterations" in obj &&
|
||||
typeof (obj as any).id === "string" &&
|
||||
Array.isArray((obj as any).edge_groups) &&
|
||||
typeof (obj as any).executors === "object" &&
|
||||
typeof (obj as any).start_executor_id === "string" &&
|
||||
typeof (obj as any).max_iterations === "number"
|
||||
"id" in record &&
|
||||
"edge_groups" in record &&
|
||||
"executors" in record &&
|
||||
"start_executor_id" in record &&
|
||||
"max_iterations" in record &&
|
||||
typeof record.id === "string" &&
|
||||
Array.isArray(record.edge_groups) &&
|
||||
typeof record.executors === "object" &&
|
||||
typeof record.start_executor_id === "string" &&
|
||||
typeof record.max_iterations === "number"
|
||||
);
|
||||
}
|
||||
|
||||
export function isExecutor(obj: unknown): obj is Executor {
|
||||
if (typeof obj !== "object" || obj === null) return false;
|
||||
const record = obj as Record<string, unknown>;
|
||||
return (
|
||||
typeof obj === "object" &&
|
||||
obj !== null &&
|
||||
"id" in obj &&
|
||||
"type" in obj &&
|
||||
typeof (obj as any).id === "string" &&
|
||||
typeof (obj as any).type === "string"
|
||||
"id" in record &&
|
||||
"type" in record &&
|
||||
typeof record.id === "string" &&
|
||||
typeof record.type === "string"
|
||||
);
|
||||
}
|
||||
|
||||
export function isEdge(obj: unknown): obj is Edge {
|
||||
if (typeof obj !== "object" || obj === null) return false;
|
||||
const record = obj as Record<string, unknown>;
|
||||
return (
|
||||
typeof obj === "object" &&
|
||||
obj !== null &&
|
||||
"source_id" in obj &&
|
||||
"target_id" in obj &&
|
||||
typeof (obj as any).source_id === "string" &&
|
||||
typeof (obj as any).target_id === "string"
|
||||
"source_id" in record &&
|
||||
"target_id" in record &&
|
||||
typeof record.source_id === "string" &&
|
||||
typeof record.target_id === "string"
|
||||
);
|
||||
}
|
||||
|
||||
export function isEdgeGroup(obj: unknown): obj is EdgeGroup {
|
||||
if (typeof obj !== "object" || obj === null) return false;
|
||||
const record = obj as Record<string, unknown>;
|
||||
return (
|
||||
typeof obj === "object" &&
|
||||
obj !== null &&
|
||||
"id" in obj &&
|
||||
"type" in obj &&
|
||||
"edges" in obj &&
|
||||
typeof (obj as any).id === "string" &&
|
||||
typeof (obj as any).type === "string" &&
|
||||
Array.isArray((obj as any).edges)
|
||||
"id" in record &&
|
||||
"type" in record &&
|
||||
"edges" in record &&
|
||||
typeof record.id === "string" &&
|
||||
typeof record.type === "string" &&
|
||||
Array.isArray(record.edges)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ import type {
|
||||
import type {
|
||||
ExtendedResponseStreamEvent,
|
||||
ResponseWorkflowEventComplete,
|
||||
ResponseOutputItemAddedEvent,
|
||||
ResponseOutputItemDoneEvent,
|
||||
JSONSchemaProperty,
|
||||
} from "@/types";
|
||||
import type { Workflow } from "@/types/workflow";
|
||||
@@ -389,9 +391,10 @@ export function processWorkflowEvents(
|
||||
events.forEach((event) => {
|
||||
// Handle new standard OpenAI events
|
||||
if (event.type === "response.output_item.added" || event.type === "response.output_item.done") {
|
||||
const item = (event as any).item;
|
||||
if (item && item.type === "executor_action" && item.executor_id) {
|
||||
const executorId = item.executor_id;
|
||||
const outputEvent = event as ResponseOutputItemAddedEvent | ResponseOutputItemDoneEvent;
|
||||
const item = outputEvent.item;
|
||||
if (item && item.type === "executor_action" && "executor_id" in item) {
|
||||
const executorId = item.executor_id as string;
|
||||
const itemId = item.id;
|
||||
|
||||
// Track the latest item ID for this executor
|
||||
@@ -492,16 +495,17 @@ export function processWorkflowEvents(
|
||||
// This prevents setting to "running" after the executor has already completed
|
||||
const hasCompletionEvent = events.some((event) => {
|
||||
if (event.type === "response.output_item.done") {
|
||||
const item = (event as any).item;
|
||||
return item && item.type === "executor_action" && item.executor_id === startExecutorId;
|
||||
const outputEvent = event as ResponseOutputItemDoneEvent;
|
||||
const item = outputEvent.item;
|
||||
return item && item.type === "executor_action" && "executor_id" in item && item.executor_id === startExecutorId;
|
||||
}
|
||||
if (event.type === "response.workflow_event.completed" && "data" in event && event.data) {
|
||||
const data = event.data as any;
|
||||
const data = event.data as Record<string, unknown>;
|
||||
return data.executor_id === startExecutorId &&
|
||||
(data.event_type === "ExecutorCompletedEvent" ||
|
||||
data.event_type === "ExecutorFailedEvent" ||
|
||||
data.event_type?.includes("Error") ||
|
||||
data.event_type?.includes("Failed"));
|
||||
(typeof data.event_type === "string" && data.event_type.includes("Error")) ||
|
||||
(typeof data.event_type === "string" && data.event_type.includes("Failed")));
|
||||
}
|
||||
return false;
|
||||
});
|
||||
@@ -565,9 +569,10 @@ export function getCurrentlyExecutingExecutors(
|
||||
events.forEach((event) => {
|
||||
// Handle new standard OpenAI events
|
||||
if (event.type === "response.output_item.added" || event.type === "response.output_item.done") {
|
||||
const item = (event as any).item;
|
||||
if (item && item.type === "executor_action" && item.executor_id) {
|
||||
const executorId = item.executor_id;
|
||||
const outputEvent = event as ResponseOutputItemAddedEvent | ResponseOutputItemDoneEvent;
|
||||
const item = outputEvent.item;
|
||||
if (item && item.type === "executor_action" && "executor_id" in item) {
|
||||
const executorId = item.executor_id as string;
|
||||
|
||||
executorTimeline[executorId] = {
|
||||
lastEvent: event.type === "response.output_item.added" ? "ExecutorInvokedEvent" : "ExecutorCompletedEvent",
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz"
|
||||
integrity sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==
|
||||
|
||||
"@babel/core@^7.28.3":
|
||||
"@babel/core@^7.0.0", "@babel/core@^7.0.0-0", "@babel/core@^7.28.3":
|
||||
version "7.28.3"
|
||||
resolved "https://registry.npmjs.org/@babel/core/-/core-7.28.3.tgz"
|
||||
integrity sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==
|
||||
@@ -168,158 +168,11 @@
|
||||
"@babel/helper-string-parser" "^7.27.1"
|
||||
"@babel/helper-validator-identifier" "^7.27.1"
|
||||
|
||||
"@emnapi/core@^1.4.3", "@emnapi/core@^1.4.5":
|
||||
version "1.7.1"
|
||||
resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.7.1.tgz#3a79a02dbc84f45884a1806ebb98e5746bdfaac4"
|
||||
integrity sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==
|
||||
dependencies:
|
||||
"@emnapi/wasi-threads" "1.1.0"
|
||||
tslib "^2.4.0"
|
||||
|
||||
"@emnapi/runtime@^1.4.3", "@emnapi/runtime@^1.4.5":
|
||||
version "1.7.1"
|
||||
resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.7.1.tgz#a73784e23f5d57287369c808197288b52276b791"
|
||||
integrity sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==
|
||||
dependencies:
|
||||
tslib "^2.4.0"
|
||||
|
||||
"@emnapi/wasi-threads@1.1.0", "@emnapi/wasi-threads@^1.0.4":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz#60b2102fddc9ccb78607e4a3cf8403ea69be41bf"
|
||||
integrity sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==
|
||||
dependencies:
|
||||
tslib "^2.4.0"
|
||||
|
||||
"@esbuild/aix-ppc64@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz#bef96351f16520055c947aba28802eede3c9e9a9"
|
||||
integrity sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==
|
||||
|
||||
"@esbuild/android-arm64@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz#d2e70be7d51a529425422091e0dcb90374c1546c"
|
||||
integrity sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==
|
||||
|
||||
"@esbuild/android-arm@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.9.tgz#d2a753fe2a4c73b79437d0ba1480e2d760097419"
|
||||
integrity sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==
|
||||
|
||||
"@esbuild/android-x64@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.9.tgz#5278836e3c7ae75761626962f902a0d55352e683"
|
||||
integrity sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==
|
||||
|
||||
"@esbuild/darwin-arm64@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz#f1513eaf9ec8fa15dcaf4c341b0f005d3e8b47ae"
|
||||
resolved "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz"
|
||||
integrity sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==
|
||||
|
||||
"@esbuild/darwin-x64@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz#e27dbc3b507b3a1cea3b9280a04b8b6b725f82be"
|
||||
integrity sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==
|
||||
|
||||
"@esbuild/freebsd-arm64@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz#364e3e5b7a1fd45d92be08c6cc5d890ca75908ca"
|
||||
integrity sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==
|
||||
|
||||
"@esbuild/freebsd-x64@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz#7c869b45faeb3df668e19ace07335a0711ec56ab"
|
||||
integrity sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==
|
||||
|
||||
"@esbuild/linux-arm64@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz#48d42861758c940b61abea43ba9a29b186d6cb8b"
|
||||
integrity sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==
|
||||
|
||||
"@esbuild/linux-arm@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz#6ce4b9cabf148274101701d112b89dc67cc52f37"
|
||||
integrity sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==
|
||||
|
||||
"@esbuild/linux-ia32@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz#207e54899b79cac9c26c323fc1caa32e3143f1c4"
|
||||
integrity sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==
|
||||
|
||||
"@esbuild/linux-loong64@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz#0ba48a127159a8f6abb5827f21198b999ffd1fc0"
|
||||
integrity sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==
|
||||
|
||||
"@esbuild/linux-mips64el@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz#a4d4cc693d185f66a6afde94f772b38ce5d64eb5"
|
||||
integrity sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==
|
||||
|
||||
"@esbuild/linux-ppc64@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz#0f5805c1c6d6435a1dafdc043cb07a19050357db"
|
||||
integrity sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==
|
||||
|
||||
"@esbuild/linux-riscv64@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz#6776edece0f8fca79f3386398b5183ff2a827547"
|
||||
integrity sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==
|
||||
|
||||
"@esbuild/linux-s390x@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz#3f6f29ef036938447c2218d309dc875225861830"
|
||||
integrity sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==
|
||||
|
||||
"@esbuild/linux-x64@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz#831fe0b0e1a80a8b8391224ea2377d5520e1527f"
|
||||
integrity sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==
|
||||
|
||||
"@esbuild/netbsd-arm64@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz#06f99d7eebe035fbbe43de01c9d7e98d2a0aa548"
|
||||
integrity sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==
|
||||
|
||||
"@esbuild/netbsd-x64@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz#db99858e6bed6e73911f92a88e4edd3a8c429a52"
|
||||
integrity sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==
|
||||
|
||||
"@esbuild/openbsd-arm64@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz#afb886c867e36f9d86bb21e878e1185f5d5a0935"
|
||||
integrity sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==
|
||||
|
||||
"@esbuild/openbsd-x64@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz#30855c9f8381fac6a0ef5b5f31ac6e7108a66ecf"
|
||||
integrity sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==
|
||||
|
||||
"@esbuild/openharmony-arm64@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz#2f2144af31e67adc2a8e3705c20c2bd97bd88314"
|
||||
integrity sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==
|
||||
|
||||
"@esbuild/sunos-x64@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz#69b99a9b5bd226c9eb9c6a73f990fddd497d732e"
|
||||
integrity sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==
|
||||
|
||||
"@esbuild/win32-arm64@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz#d789330a712af916c88325f4ffe465f885719c6b"
|
||||
integrity sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==
|
||||
|
||||
"@esbuild/win32-ia32@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz#52fc735406bd49688253e74e4e837ac2ba0789e3"
|
||||
integrity sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==
|
||||
|
||||
"@esbuild/win32-x64@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz"
|
||||
integrity sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==
|
||||
|
||||
"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.7.0":
|
||||
version "4.7.0"
|
||||
resolved "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz"
|
||||
@@ -368,7 +221,7 @@
|
||||
minimatch "^3.1.2"
|
||||
strip-json-comments "^3.1.1"
|
||||
|
||||
"@eslint/js@9.33.0", "@eslint/js@^9.33.0":
|
||||
"@eslint/js@^9.33.0", "@eslint/js@9.33.0":
|
||||
version "9.33.0"
|
||||
resolved "https://registry.npmjs.org/@eslint/js/-/js-9.33.0.tgz"
|
||||
integrity sha512-5K1/mKhWaMfreBGJTwval43JJmkip0RmM+3+IuqupeSKNC/Th2Kc7ucaq5ovTSra/OOKB9c58CGSz3QMVbWt0A==
|
||||
@@ -482,15 +335,6 @@
|
||||
"@jridgewell/resolve-uri" "^3.1.0"
|
||||
"@jridgewell/sourcemap-codec" "^1.4.14"
|
||||
|
||||
"@napi-rs/wasm-runtime@^0.2.12":
|
||||
version "0.2.12"
|
||||
resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz#3e78a8b96e6c33a6c517e1894efbd5385a7cb6f2"
|
||||
integrity sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==
|
||||
dependencies:
|
||||
"@emnapi/core" "^1.4.3"
|
||||
"@emnapi/runtime" "^1.4.3"
|
||||
"@tybys/wasm-util" "^0.10.0"
|
||||
|
||||
"@nodelib/fs.scandir@2.1.5":
|
||||
version "2.1.5"
|
||||
resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz"
|
||||
@@ -499,7 +343,7 @@
|
||||
"@nodelib/fs.stat" "2.0.5"
|
||||
run-parallel "^1.1.9"
|
||||
|
||||
"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2":
|
||||
"@nodelib/fs.stat@^2.0.2", "@nodelib/fs.stat@2.0.5":
|
||||
version "2.0.5"
|
||||
resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz"
|
||||
integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
|
||||
@@ -742,12 +586,12 @@
|
||||
|
||||
"@radix-ui/react-separator@^1.1.7":
|
||||
version "1.1.7"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-separator/-/react-separator-1.1.7.tgz#a18bd7fd07c10fda1bba14f2a3032e7b1a2b3470"
|
||||
resolved "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz"
|
||||
integrity sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==
|
||||
dependencies:
|
||||
"@radix-ui/react-primitive" "2.1.3"
|
||||
|
||||
"@radix-ui/react-slot@1.2.3", "@radix-ui/react-slot@^1.2.3":
|
||||
"@radix-ui/react-slot@^1.2.3", "@radix-ui/react-slot@1.2.3":
|
||||
version "1.2.3"
|
||||
resolved "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz"
|
||||
integrity sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==
|
||||
@@ -756,7 +600,7 @@
|
||||
|
||||
"@radix-ui/react-switch@^1.2.6":
|
||||
version "1.2.6"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-switch/-/react-switch-1.2.6.tgz#ff79acb831f0d5ea9216cfcc5b939912571358e3"
|
||||
resolved "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz"
|
||||
integrity sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==
|
||||
dependencies:
|
||||
"@radix-ui/primitive" "1.1.3"
|
||||
@@ -781,6 +625,24 @@
|
||||
"@radix-ui/react-roving-focus" "1.1.11"
|
||||
"@radix-ui/react-use-controllable-state" "1.2.2"
|
||||
|
||||
"@radix-ui/react-tooltip@^1.2.8":
|
||||
version "1.2.8"
|
||||
resolved "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz"
|
||||
integrity sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==
|
||||
dependencies:
|
||||
"@radix-ui/primitive" "1.1.3"
|
||||
"@radix-ui/react-compose-refs" "1.1.2"
|
||||
"@radix-ui/react-context" "1.1.2"
|
||||
"@radix-ui/react-dismissable-layer" "1.1.11"
|
||||
"@radix-ui/react-id" "1.1.1"
|
||||
"@radix-ui/react-popper" "1.2.8"
|
||||
"@radix-ui/react-portal" "1.1.9"
|
||||
"@radix-ui/react-presence" "1.1.5"
|
||||
"@radix-ui/react-primitive" "2.1.3"
|
||||
"@radix-ui/react-slot" "1.2.3"
|
||||
"@radix-ui/react-use-controllable-state" "1.2.2"
|
||||
"@radix-ui/react-visually-hidden" "1.2.3"
|
||||
|
||||
"@radix-ui/react-use-callback-ref@1.1.1":
|
||||
version "1.1.1"
|
||||
resolved "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz"
|
||||
@@ -849,106 +711,11 @@
|
||||
resolved "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.32.tgz"
|
||||
integrity sha512-QReCdvxiUZAPkvp1xpAg62IeNzykOFA6syH2CnClif4YmALN1XKpB39XneL80008UbtMShthSVDKmrx05N1q/g==
|
||||
|
||||
"@rollup/rollup-android-arm-eabi@4.47.1":
|
||||
version "4.47.1"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.47.1.tgz#6e236cd2fd29bb01a300ad4ff6ed0f1a17550e69"
|
||||
integrity sha512-lTahKRJip0knffA/GTNFJMrToD+CM+JJ+Qt5kjzBK/sFQ0EWqfKW3AYQSlZXN98tX0lx66083U9JYIMioMMK7g==
|
||||
|
||||
"@rollup/rollup-android-arm64@4.47.1":
|
||||
version "4.47.1"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.47.1.tgz#808f2c9c7e68161add613ebcb0eac5a058a0df3c"
|
||||
integrity sha512-uqxkb3RJLzlBbh/bbNQ4r7YpSZnjgMgyoEOY7Fy6GCbelkDSAzeiogxMG9TfLsBbqmGsdDObo3mzGqa8hps4MA==
|
||||
|
||||
"@rollup/rollup-darwin-arm64@4.47.1":
|
||||
version "4.47.1"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.47.1.tgz#fa41e413c8e73d61039d6375b234595f24b1e5e3"
|
||||
resolved "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.47.1.tgz"
|
||||
integrity sha512-tV6reObmxBDS4DDyLzTDIpymthNlxrLBGAoQx6m2a7eifSNEZdkXQl1PE4ZjCkEDPVgNXSzND/k9AQ3mC4IOEQ==
|
||||
|
||||
"@rollup/rollup-darwin-x64@4.47.1":
|
||||
version "4.47.1"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.47.1.tgz#9aac64e886435493f2e3a0aa5e4aad098a90814c"
|
||||
integrity sha512-XuJRPTnMk1lwsSnS3vYyVMu4x/+WIw1MMSiqj5C4j3QOWsMzbJEK90zG+SWV1h0B1ABGCQ0UZUjti+TQK35uHQ==
|
||||
|
||||
"@rollup/rollup-freebsd-arm64@4.47.1":
|
||||
version "4.47.1"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.47.1.tgz#9fc804264f7b7a7cdad3747950299f990163be1f"
|
||||
integrity sha512-79BAm8Ag/tmJ5asCqgOXsb3WY28Rdd5Lxj8ONiQzWzy9LvWORd5qVuOnjlqiWWZJw+dWewEktZb5yiM1DLLaHw==
|
||||
|
||||
"@rollup/rollup-freebsd-x64@4.47.1":
|
||||
version "4.47.1"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.47.1.tgz#933feaff864feb03bbbcd0c18ea351ade957cf79"
|
||||
integrity sha512-OQ2/ZDGzdOOlyfqBiip0ZX/jVFekzYrGtUsqAfLDbWy0jh1PUU18+jYp8UMpqhly5ltEqotc2miLngf9FPSWIA==
|
||||
|
||||
"@rollup/rollup-linux-arm-gnueabihf@4.47.1":
|
||||
version "4.47.1"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.47.1.tgz#02915e6b2c55fe5961c27404aba2d9c8ef48ac6c"
|
||||
integrity sha512-HZZBXJL1udxlCVvoVadstgiU26seKkHbbAMLg7680gAcMnRNP9SAwTMVet02ANA94kXEI2VhBnXs4e5nf7KG2A==
|
||||
|
||||
"@rollup/rollup-linux-arm-musleabihf@4.47.1":
|
||||
version "4.47.1"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.47.1.tgz#1afef33191b26e76ae7f0d0dc767efc6be1285ce"
|
||||
integrity sha512-sZ5p2I9UA7T950JmuZ3pgdKA6+RTBr+0FpK427ExW0t7n+QwYOcmDTK/aRlzoBrWyTpJNlS3kacgSlSTUg6P/Q==
|
||||
|
||||
"@rollup/rollup-linux-arm64-gnu@4.47.1":
|
||||
version "4.47.1"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.47.1.tgz#6e7f38fb99d14143de3ce33204e6cd61e1c2c780"
|
||||
integrity sha512-3hBFoqPyU89Dyf1mQRXCdpc6qC6At3LV6jbbIOZd72jcx7xNk3aAp+EjzAtN6sDlmHFzsDJN5yeUySvorWeRXA==
|
||||
|
||||
"@rollup/rollup-linux-arm64-musl@4.47.1":
|
||||
version "4.47.1"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.47.1.tgz#25ab09f14bbcba85a604bcee2962d2486db90794"
|
||||
integrity sha512-49J4FnMHfGodJWPw73Ve+/hsPjZgcXQGkmqBGZFvltzBKRS+cvMiWNLadOMXKGnYRhs1ToTGM0sItKISoSGUNA==
|
||||
|
||||
"@rollup/rollup-linux-loongarch64-gnu@4.47.1":
|
||||
version "4.47.1"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.47.1.tgz#d3e3a3fd61e21b2753094391dee9b515a2bc9ecd"
|
||||
integrity sha512-4yYU8p7AneEpQkRX03pbpLmE21z5JNys16F1BZBZg5fP9rIlb0TkeQjn5du5w4agConCCEoYIG57sNxjryHEGg==
|
||||
|
||||
"@rollup/rollup-linux-ppc64-gnu@4.47.1":
|
||||
version "4.47.1"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.47.1.tgz#6b44445e2bd5866692010de241bf18d2ae8b0cb8"
|
||||
integrity sha512-fAiq+J28l2YMWgC39jz/zPi2jqc0y3GSRo1yyxlBHt6UN0yYgnegHSRPa3pnHS5amT/efXQrm0ug5+aNEu9UuQ==
|
||||
|
||||
"@rollup/rollup-linux-riscv64-gnu@4.47.1":
|
||||
version "4.47.1"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.47.1.tgz#3ff412d20d3b157e6aadabf84788e8c5cb221ba7"
|
||||
integrity sha512-daoT0PMENNdjVYYU9xec30Y2prb1AbEIbb64sqkcQcSaR0zYuKkoPuhIztfxuqN82KYCKKrj+tQe4Gi7OSm1ow==
|
||||
|
||||
"@rollup/rollup-linux-riscv64-musl@4.47.1":
|
||||
version "4.47.1"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.47.1.tgz#104f451497d53d82a49c6d08c13c59f5f30eed57"
|
||||
integrity sha512-JNyXaAhWtdzfXu5pUcHAuNwGQKevR+6z/poYQKVW+pLaYOj9G1meYc57/1Xv2u4uTxfu9qEWmNTjv/H/EpAisw==
|
||||
|
||||
"@rollup/rollup-linux-s390x-gnu@4.47.1":
|
||||
version "4.47.1"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.47.1.tgz#d04de7b21d181f30750760cb3553946306506172"
|
||||
integrity sha512-U/CHbqKSwEQyZXjCpY43/GLYcTVKEXeRHw0rMBJP7fP3x6WpYG4LTJWR3ic6TeYKX6ZK7mrhltP4ppolyVhLVQ==
|
||||
|
||||
"@rollup/rollup-linux-x64-gnu@4.47.1":
|
||||
version "4.47.1"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.47.1.tgz#a6ba88ff7480940a435b1e67ddbb3f207a7ae02f"
|
||||
integrity sha512-uTLEakjxOTElfeZIGWkC34u2auLHB1AYS6wBjPGI00bWdxdLcCzK5awjs25YXpqB9lS8S0vbO0t9ZcBeNibA7g==
|
||||
|
||||
"@rollup/rollup-linux-x64-musl@4.47.1":
|
||||
version "4.47.1"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.47.1.tgz#c912c8ffa0c242ed3175cd91cdeaef98109afa54"
|
||||
integrity sha512-Ft+d/9DXs30BK7CHCTX11FtQGHUdpNDLJW0HHLign4lgMgBcPFN3NkdIXhC5r9iwsMwYreBBc4Rho5ieOmKNVQ==
|
||||
|
||||
"@rollup/rollup-win32-arm64-msvc@4.47.1":
|
||||
version "4.47.1"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.47.1.tgz#ca5eaae89443554b461bb359112a056528cfdac0"
|
||||
integrity sha512-N9X5WqGYzZnjGAFsKSfYFtAShYjwOmFJoWbLg3dYixZOZqU7hdMq+/xyS14zKLhFhZDhP9VfkzQnsdk0ZDS9IA==
|
||||
|
||||
"@rollup/rollup-win32-ia32-msvc@4.47.1":
|
||||
version "4.47.1"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.47.1.tgz#34e76172515fb4b374eb990d59f54faff938246e"
|
||||
integrity sha512-O+KcfeCORZADEY8oQJk4HK8wtEOCRE4MdOkb8qGZQNun3jzmj2nmhV/B/ZaaZOkPmJyvm/gW9n0gsB4eRa1eiQ==
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc@4.47.1":
|
||||
version "4.47.1"
|
||||
resolved "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.47.1.tgz"
|
||||
integrity sha512-CpKnYa8eHthJa3c+C38v/E+/KZyF1Jdh2Cz3DyKZqEWYgrM1IHFArXNWvBLPQCKUEsAqqKX27tTqVEFbDNUcOA==
|
||||
|
||||
"@tailwindcss/node@4.1.12":
|
||||
version "4.1.12"
|
||||
resolved "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.12.tgz"
|
||||
@@ -962,73 +729,11 @@
|
||||
source-map-js "^1.2.1"
|
||||
tailwindcss "4.1.12"
|
||||
|
||||
"@tailwindcss/oxide-android-arm64@4.1.12":
|
||||
version "4.1.12"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.12.tgz#27920fe61fa2743afe8a8ca296fa640b609d17d5"
|
||||
integrity sha512-oNY5pq+1gc4T6QVTsZKwZaGpBb2N1H1fsc1GD4o7yinFySqIuRZ2E4NvGasWc6PhYJwGK2+5YT1f9Tp80zUQZQ==
|
||||
|
||||
"@tailwindcss/oxide-darwin-arm64@4.1.12":
|
||||
version "4.1.12"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.12.tgz#e8bd4798f26ec1d012bf0683aeb77449f71505cd"
|
||||
resolved "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.12.tgz"
|
||||
integrity sha512-cq1qmq2HEtDV9HvZlTtrj671mCdGB93bVY6J29mwCyaMYCP/JaUBXxrQQQm7Qn33AXXASPUb2HFZlWiiHWFytw==
|
||||
|
||||
"@tailwindcss/oxide-darwin-x64@4.1.12":
|
||||
version "4.1.12"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.12.tgz#8ddb7e5ddfd9b049ec84a2bda99f2b04a86859f5"
|
||||
integrity sha512-6UCsIeFUcBfpangqlXay9Ffty9XhFH1QuUFn0WV83W8lGdX8cD5/+2ONLluALJD5+yJ7k8mVtwy3zMZmzEfbLg==
|
||||
|
||||
"@tailwindcss/oxide-freebsd-x64@4.1.12":
|
||||
version "4.1.12"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.12.tgz#da1c0b16b7a5f95a1e400f299a3ec94fb6fd40ac"
|
||||
integrity sha512-JOH/f7j6+nYXIrHobRYCtoArJdMJh5zy5lr0FV0Qu47MID/vqJAY3r/OElPzx1C/wdT1uS7cPq+xdYYelny1ww==
|
||||
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf@4.1.12":
|
||||
version "4.1.12"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.12.tgz#34e558aa6e869c6fe9867cb78ed7ba651b9fcaa4"
|
||||
integrity sha512-v4Ghvi9AU1SYgGr3/j38PD8PEe6bRfTnNSUE3YCMIRrrNigCFtHZ2TCm8142X8fcSqHBZBceDx+JlFJEfNg5zQ==
|
||||
|
||||
"@tailwindcss/oxide-linux-arm64-gnu@4.1.12":
|
||||
version "4.1.12"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.12.tgz#0a00a8146ab6215f81b2d385056c991441bf390e"
|
||||
integrity sha512-YP5s1LmetL9UsvVAKusHSyPlzSRqYyRB0f+Kl/xcYQSPLEw/BvGfxzbH+ihUciePDjiXwHh+p+qbSP3SlJw+6g==
|
||||
|
||||
"@tailwindcss/oxide-linux-arm64-musl@4.1.12":
|
||||
version "4.1.12"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.12.tgz#b138f494068884ae0d8c343dc1904b22f5e98dc6"
|
||||
integrity sha512-V8pAM3s8gsrXcCv6kCHSuwyb/gPsd863iT+v1PGXC4fSL/OJqsKhfK//v8P+w9ThKIoqNbEnsZqNy+WDnwQqCA==
|
||||
|
||||
"@tailwindcss/oxide-linux-x64-gnu@4.1.12":
|
||||
version "4.1.12"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.12.tgz#5b9d5f23b15cdb714639f5b9741c0df5d610f794"
|
||||
integrity sha512-xYfqYLjvm2UQ3TZggTGrwxjYaLB62b1Wiysw/YE3Yqbh86sOMoTn0feF98PonP7LtjsWOWcXEbGqDL7zv0uW8Q==
|
||||
|
||||
"@tailwindcss/oxide-linux-x64-musl@4.1.12":
|
||||
version "4.1.12"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.12.tgz#f68ec530d3ca6875ea9015bcd5dd0762ee5e2f5d"
|
||||
integrity sha512-ha0pHPamN+fWZY7GCzz5rKunlv9L5R8kdh+YNvP5awe3LtuXb5nRi/H27GeL2U+TdhDOptU7T6Is7mdwh5Ar3A==
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi@4.1.12":
|
||||
version "4.1.12"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.12.tgz#9fd15a1ebde6076c42c445c5e305c31673ead965"
|
||||
integrity sha512-4tSyu3dW+ktzdEpuk6g49KdEangu3eCYoqPhWNsZgUhyegEda3M9rG0/j1GV/JjVVsj+lG7jWAyrTlLzd/WEBg==
|
||||
dependencies:
|
||||
"@emnapi/core" "^1.4.5"
|
||||
"@emnapi/runtime" "^1.4.5"
|
||||
"@emnapi/wasi-threads" "^1.0.4"
|
||||
"@napi-rs/wasm-runtime" "^0.2.12"
|
||||
"@tybys/wasm-util" "^0.10.0"
|
||||
tslib "^2.8.0"
|
||||
|
||||
"@tailwindcss/oxide-win32-arm64-msvc@4.1.12":
|
||||
version "4.1.12"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.12.tgz#938bcc6a82e1120ea4fe2ce94be0a8cdf3ae92c7"
|
||||
integrity sha512-iGLyD/cVP724+FGtMWslhcFyg4xyYyM+5F4hGvKA7eifPkXHRAUDFaimu53fpNg9X8dfP75pXx/zFt/jlNF+lg==
|
||||
|
||||
"@tailwindcss/oxide-win32-x64-msvc@4.1.12":
|
||||
version "4.1.12"
|
||||
resolved "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.12.tgz"
|
||||
integrity sha512-NKIh5rzw6CpEodv/++r0hGLlfgT/gFN+5WNdZtvh6wpU2BpGNgdjvj6H2oFc8nCM839QM1YOhjpgbAONUb4IxA==
|
||||
|
||||
"@tailwindcss/oxide@4.1.12":
|
||||
version "4.1.12"
|
||||
resolved "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.12.tgz"
|
||||
@@ -1059,13 +764,6 @@
|
||||
"@tailwindcss/oxide" "4.1.12"
|
||||
tailwindcss "4.1.12"
|
||||
|
||||
"@tybys/wasm-util@^0.10.0":
|
||||
version "0.10.1"
|
||||
resolved "https://registry.yarnpkg.com/@tybys/wasm-util/-/wasm-util-0.10.1.tgz#ecddd3205cf1e2d5274649ff0eedd2991ed7f414"
|
||||
integrity sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==
|
||||
dependencies:
|
||||
tslib "^2.4.0"
|
||||
|
||||
"@types/babel__core@^7.20.5":
|
||||
version "7.20.5"
|
||||
resolved "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz"
|
||||
@@ -1138,7 +836,7 @@
|
||||
"@types/d3-interpolate" "*"
|
||||
"@types/d3-selection" "*"
|
||||
|
||||
"@types/estree@1.0.8", "@types/estree@^1.0.6":
|
||||
"@types/estree@^1.0.6", "@types/estree@1.0.8":
|
||||
version "1.0.8"
|
||||
resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz"
|
||||
integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==
|
||||
@@ -1148,19 +846,19 @@
|
||||
resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz"
|
||||
integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==
|
||||
|
||||
"@types/node@^24.3.0":
|
||||
"@types/node@^20.19.0 || >=22.12.0", "@types/node@^24.3.0":
|
||||
version "24.3.0"
|
||||
resolved "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz"
|
||||
integrity sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==
|
||||
dependencies:
|
||||
undici-types "~7.10.0"
|
||||
|
||||
"@types/react-dom@^19.1.7":
|
||||
"@types/react-dom@*", "@types/react-dom@^19.1.7":
|
||||
version "19.1.7"
|
||||
resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.7.tgz"
|
||||
integrity sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw==
|
||||
|
||||
"@types/react@^19.1.10":
|
||||
"@types/react@*", "@types/react@^19.0.0", "@types/react@^19.1.10", "@types/react@>=16.8", "@types/react@>=18.0.0":
|
||||
version "19.1.10"
|
||||
resolved "https://registry.npmjs.org/@types/react/-/react-19.1.10.tgz"
|
||||
integrity sha512-EhBeSYX0Y6ye8pNebpKrwFJq7BoQ8J5SO6NlvNwwHjSj6adXJViPQrKlsyPw7hLBLvckEMO1yxeGdR82YBBlDg==
|
||||
@@ -1182,7 +880,7 @@
|
||||
natural-compare "^1.4.0"
|
||||
ts-api-utils "^2.1.0"
|
||||
|
||||
"@typescript-eslint/parser@8.40.0":
|
||||
"@typescript-eslint/parser@^8.40.0", "@typescript-eslint/parser@8.40.0":
|
||||
version "8.40.0"
|
||||
resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.40.0.tgz"
|
||||
integrity sha512-jCNyAuXx8dr5KJMkecGmZ8KI61KBUhkCob+SD+C+I5+Y1FWI2Y3QmY4/cxMCC5WAsZqoEtEETVhUiUMIGCf6Bw==
|
||||
@@ -1210,7 +908,7 @@
|
||||
"@typescript-eslint/types" "8.40.0"
|
||||
"@typescript-eslint/visitor-keys" "8.40.0"
|
||||
|
||||
"@typescript-eslint/tsconfig-utils@8.40.0", "@typescript-eslint/tsconfig-utils@^8.40.0":
|
||||
"@typescript-eslint/tsconfig-utils@^8.40.0", "@typescript-eslint/tsconfig-utils@8.40.0":
|
||||
version "8.40.0"
|
||||
resolved "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.40.0.tgz"
|
||||
integrity sha512-jtMytmUaG9d/9kqSl/W3E3xaWESo4hFDxAIHGVW/WKKtQhesnRIJSAJO6XckluuJ6KDB5woD1EiqknriCtAmcw==
|
||||
@@ -1226,7 +924,7 @@
|
||||
debug "^4.3.4"
|
||||
ts-api-utils "^2.1.0"
|
||||
|
||||
"@typescript-eslint/types@8.40.0", "@typescript-eslint/types@^8.40.0":
|
||||
"@typescript-eslint/types@^8.40.0", "@typescript-eslint/types@8.40.0":
|
||||
version "8.40.0"
|
||||
resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.40.0.tgz"
|
||||
integrity sha512-ETdbFlgbAmXHyFPwqUIYrfc12ArvpBhEVgGAxVYSwli26dn8Ko+lIo4Su9vI9ykTZdJn+vJprs/0eZU0YMAEQg==
|
||||
@@ -1306,7 +1004,7 @@ acorn-jsx@^5.3.2:
|
||||
resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz"
|
||||
integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
|
||||
|
||||
acorn@^8.15.0:
|
||||
"acorn@^6.0.0 || ^7.0.0 || ^8.0.0", acorn@^8.15.0:
|
||||
version "8.15.0"
|
||||
resolved "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz"
|
||||
integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==
|
||||
@@ -1367,7 +1065,7 @@ braces@^3.0.3:
|
||||
dependencies:
|
||||
fill-range "^7.1.1"
|
||||
|
||||
browserslist@^4.24.0:
|
||||
browserslist@^4.24.0, "browserslist@>= 4.21.0":
|
||||
version "4.25.3"
|
||||
resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.25.3.tgz"
|
||||
integrity sha512-cDGv1kkDI4/0e5yON9yM5G/0A5u8sf5TnmdX5C9qHzI9PPu++sQ9zjm1k9NiOrf3riY4OkK0zSGqfvJyJsgCBQ==
|
||||
@@ -1463,7 +1161,7 @@ csstype@^3.0.2:
|
||||
resolved "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz"
|
||||
integrity sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==
|
||||
|
||||
"d3-drag@2 - 3", d3-drag@^3.0.0:
|
||||
d3-drag@^3.0.0, "d3-drag@2 - 3":
|
||||
version "3.0.0"
|
||||
resolved "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz"
|
||||
integrity sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==
|
||||
@@ -1476,14 +1174,14 @@ csstype@^3.0.2:
|
||||
resolved "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz"
|
||||
integrity sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==
|
||||
|
||||
"d3-interpolate@1 - 3", d3-interpolate@^3.0.1:
|
||||
d3-interpolate@^3.0.1, "d3-interpolate@1 - 3":
|
||||
version "3.0.1"
|
||||
resolved "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz"
|
||||
integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==
|
||||
dependencies:
|
||||
d3-color "1 - 3"
|
||||
|
||||
"d3-selection@2 - 3", d3-selection@3, d3-selection@^3.0.0:
|
||||
d3-selection@^3.0.0, "d3-selection@2 - 3", d3-selection@3:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz"
|
||||
integrity sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==
|
||||
@@ -1620,7 +1318,7 @@ eslint-visitor-keys@^4.2.1:
|
||||
resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz"
|
||||
integrity sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==
|
||||
|
||||
eslint@^9.33.0:
|
||||
"eslint@^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0", "eslint@^6.0.0 || ^7.0.0 || >=8.0.0", "eslint@^8.57.0 || ^9.0.0", eslint@^9.33.0, eslint@>=8.40:
|
||||
version "9.33.0"
|
||||
resolved "https://registry.npmjs.org/eslint/-/eslint-9.33.0.tgz"
|
||||
integrity sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA==
|
||||
@@ -1866,7 +1564,7 @@ isexe@^2.0.0:
|
||||
resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz"
|
||||
integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
|
||||
|
||||
jiti@^2.5.1:
|
||||
jiti@*, jiti@^2.5.1, jiti@>=1.21.0:
|
||||
version "2.5.1"
|
||||
resolved "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz"
|
||||
integrity sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==
|
||||
@@ -1877,9 +1575,7 @@ js-tokens@^4.0.0:
|
||||
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
|
||||
|
||||
js-yaml@^4.1.0:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.1.tgz#854c292467705b699476e1a2decc0c8a3458806b"
|
||||
integrity sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==
|
||||
version "4.1.0"
|
||||
dependencies:
|
||||
argparse "^2.0.1"
|
||||
|
||||
@@ -1925,55 +1621,10 @@ levn@^0.4.1:
|
||||
|
||||
lightningcss-darwin-arm64@1.30.1:
|
||||
version "1.30.1"
|
||||
resolved "https://registry.yarnpkg.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz#3d47ce5e221b9567c703950edf2529ca4a3700ae"
|
||||
resolved "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz"
|
||||
integrity sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==
|
||||
|
||||
lightningcss-darwin-x64@1.30.1:
|
||||
version "1.30.1"
|
||||
resolved "https://registry.yarnpkg.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz#e81105d3fd6330860c15fe860f64d39cff5fbd22"
|
||||
integrity sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==
|
||||
|
||||
lightningcss-freebsd-x64@1.30.1:
|
||||
version "1.30.1"
|
||||
resolved "https://registry.yarnpkg.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz#a0e732031083ff9d625c5db021d09eb085af8be4"
|
||||
integrity sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==
|
||||
|
||||
lightningcss-linux-arm-gnueabihf@1.30.1:
|
||||
version "1.30.1"
|
||||
resolved "https://registry.yarnpkg.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz#1f5ecca6095528ddb649f9304ba2560c72474908"
|
||||
integrity sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==
|
||||
|
||||
lightningcss-linux-arm64-gnu@1.30.1:
|
||||
version "1.30.1"
|
||||
resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz#eee7799726103bffff1e88993df726f6911ec009"
|
||||
integrity sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==
|
||||
|
||||
lightningcss-linux-arm64-musl@1.30.1:
|
||||
version "1.30.1"
|
||||
resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz#f2e4b53f42892feeef8f620cbb889f7c064a7dfe"
|
||||
integrity sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==
|
||||
|
||||
lightningcss-linux-x64-gnu@1.30.1:
|
||||
version "1.30.1"
|
||||
resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz#2fc7096224bc000ebb97eea94aea248c5b0eb157"
|
||||
integrity sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==
|
||||
|
||||
lightningcss-linux-x64-musl@1.30.1:
|
||||
version "1.30.1"
|
||||
resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz#66dca2b159fd819ea832c44895d07e5b31d75f26"
|
||||
integrity sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==
|
||||
|
||||
lightningcss-win32-arm64-msvc@1.30.1:
|
||||
version "1.30.1"
|
||||
resolved "https://registry.yarnpkg.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz#7d8110a19d7c2d22bfdf2f2bb8be68e7d1b69039"
|
||||
integrity sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==
|
||||
|
||||
lightningcss-win32-x64-msvc@1.30.1:
|
||||
version "1.30.1"
|
||||
resolved "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz"
|
||||
integrity sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==
|
||||
|
||||
lightningcss@1.30.1:
|
||||
lightningcss@^1.21.0, lightningcss@1.30.1:
|
||||
version "1.30.1"
|
||||
resolved "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz"
|
||||
integrity sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==
|
||||
@@ -2144,7 +1795,7 @@ picomatch@^2.3.1:
|
||||
resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz"
|
||||
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
|
||||
|
||||
picomatch@^4.0.3:
|
||||
"picomatch@^3 || ^4", picomatch@^4.0.3:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz"
|
||||
integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==
|
||||
@@ -2173,7 +1824,7 @@ queue-microtask@^1.2.2:
|
||||
resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz"
|
||||
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
|
||||
|
||||
react-dom@^19.1.1:
|
||||
"react-dom@^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom@^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", react-dom@^19.1.1, react-dom@>=16.8.0, react-dom@>=17:
|
||||
version "19.1.1"
|
||||
resolved "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz"
|
||||
integrity sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==
|
||||
@@ -2212,7 +1863,7 @@ react-style-singleton@^2.2.2, react-style-singleton@^2.2.3:
|
||||
get-nonce "^1.0.0"
|
||||
tslib "^2.0.0"
|
||||
|
||||
react@^19.1.1:
|
||||
"react@^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react@^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", react@^19.1.1, react@>=16.8, react@>=16.8.0, react@>=17, react@>=18.0.0:
|
||||
version "19.1.1"
|
||||
resolved "https://registry.npmjs.org/react/-/react-19.1.1.tgz"
|
||||
integrity sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==
|
||||
@@ -2312,7 +1963,7 @@ tailwind-merge@^3.3.1:
|
||||
resolved "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz"
|
||||
integrity sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==
|
||||
|
||||
tailwindcss@4.1.12, tailwindcss@^4.1.12:
|
||||
tailwindcss@^4.1.12, tailwindcss@4.1.12:
|
||||
version "4.1.12"
|
||||
resolved "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.12.tgz"
|
||||
integrity sha512-DzFtxOi+7NsFf7DBtI3BJsynR+0Yp6etH+nRPTbpWnS2pZBaSksv/JGctNwSWzbFjp0vxSqknaUylseZqMDGrA==
|
||||
@@ -2354,7 +2005,7 @@ ts-api-utils@^2.1.0:
|
||||
resolved "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz"
|
||||
integrity sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==
|
||||
|
||||
tslib@^2.0.0, tslib@^2.1.0, tslib@^2.4.0, tslib@^2.8.0:
|
||||
tslib@^2.0.0, tslib@^2.1.0:
|
||||
version "2.8.1"
|
||||
resolved "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz"
|
||||
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
|
||||
@@ -2381,7 +2032,7 @@ typescript-eslint@^8.39.1:
|
||||
"@typescript-eslint/typescript-estree" "8.40.0"
|
||||
"@typescript-eslint/utils" "8.40.0"
|
||||
|
||||
typescript@~5.8.3:
|
||||
typescript@>=4.8.4, "typescript@>=4.8.4 <6.0.0", typescript@~5.8.3:
|
||||
version "5.8.3"
|
||||
resolved "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz"
|
||||
integrity sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==
|
||||
@@ -2421,12 +2072,12 @@ use-sidecar@^1.1.3:
|
||||
detect-node-es "^1.1.0"
|
||||
tslib "^2.0.0"
|
||||
|
||||
use-sync-external-store@^1.2.2:
|
||||
use-sync-external-store@^1.2.2, use-sync-external-store@>=1.2.0:
|
||||
version "1.5.0"
|
||||
resolved "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz"
|
||||
integrity sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==
|
||||
|
||||
vite@^7.1.11:
|
||||
"vite@^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0", "vite@^5.2.0 || ^6 || ^7", vite@^7.1.11:
|
||||
version "7.1.12"
|
||||
resolved "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz"
|
||||
integrity sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==
|
||||
|
||||
@@ -8,8 +8,10 @@ import pytest
|
||||
from agent_framework import (
|
||||
Executor,
|
||||
InMemoryCheckpointStorage,
|
||||
RequestInfoEvent,
|
||||
WorkflowBuilder,
|
||||
WorkflowContext,
|
||||
WorkflowStatusEvent,
|
||||
handler,
|
||||
response_handler,
|
||||
)
|
||||
@@ -426,14 +428,11 @@ class TestIntegration:
|
||||
# Run workflow until it reaches IDLE_WITH_PENDING_REQUESTS (after checkpoint is created)
|
||||
saw_request_event = False
|
||||
async for event in test_workflow.run_stream(WorkflowTestData(value="test")):
|
||||
if hasattr(event, "__class__"):
|
||||
if event.__class__.__name__ == "RequestInfoEvent":
|
||||
saw_request_event = True
|
||||
# Wait for IDLE_WITH_PENDING_REQUESTS status (comes after checkpoint creation)
|
||||
is_status_event = event.__class__.__name__ == "WorkflowStatusEvent"
|
||||
has_pending_status = hasattr(event, "status") and "IDLE_WITH_PENDING_REQUESTS" in str(event.status)
|
||||
if is_status_event and has_pending_status:
|
||||
break
|
||||
if isinstance(event, RequestInfoEvent):
|
||||
saw_request_event = True
|
||||
# Wait for IDLE_WITH_PENDING_REQUESTS status (comes after checkpoint creation)
|
||||
if isinstance(event, WorkflowStatusEvent) and "IDLE_WITH_PENDING_REQUESTS" in str(event.state):
|
||||
break
|
||||
|
||||
assert saw_request_event, "Test workflow should have emitted RequestInfoEvent"
|
||||
|
||||
|
||||
@@ -234,7 +234,7 @@ async def test_list_items_converts_function_calls():
|
||||
{
|
||||
"type": "function_result",
|
||||
"call_id": "call_test123",
|
||||
"output": '{"temperature": 65, "condition": "sunny"}',
|
||||
"result": '{"temperature": 65, "condition": "sunny"}',
|
||||
}
|
||||
],
|
||||
),
|
||||
|
||||
@@ -441,73 +441,119 @@ async def test_workflow_status_event(mapper: MessageMapper, test_request: AgentF
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# MagenticAgentDeltaEvent Tests
|
||||
# Magentic Event Tests - Testing REAL AgentRunUpdateEvent with additional_properties
|
||||
# =============================================================================
|
||||
|
||||
|
||||
async def test_magentic_agent_delta_creates_message_container(
|
||||
async def test_magentic_agent_run_update_event_with_agent_delta_metadata(
|
||||
mapper: MessageMapper, test_request: AgentFrameworkRequest
|
||||
) -> None:
|
||||
"""Test that MagenticAgentDeltaEvent creates message containers when no executor context."""
|
||||
from dataclasses import dataclass
|
||||
"""Test that AgentRunUpdateEvent with magentic_event_type='agent_delta' is handled correctly.
|
||||
|
||||
from agent_framework import WorkflowEvent
|
||||
This tests the ACTUAL event format Magentic emits - not a fake MagenticAgentDeltaEvent class.
|
||||
Magentic uses AgentRunUpdateEvent with additional_properties containing magentic_event_type.
|
||||
"""
|
||||
from agent_framework._types import AgentRunResponseUpdate, Role, TextContent
|
||||
from agent_framework._workflows._events import AgentRunUpdateEvent
|
||||
|
||||
@dataclass
|
||||
class MagenticAgentDeltaEvent(WorkflowEvent):
|
||||
agent_id: str
|
||||
text: str | None = None
|
||||
# Create the REAL event format that Magentic emits
|
||||
update = AgentRunResponseUpdate(
|
||||
contents=[TextContent(text="Hello from agent")],
|
||||
role=Role.ASSISTANT,
|
||||
author_name="Writer",
|
||||
additional_properties={
|
||||
"magentic_event_type": "agent_delta",
|
||||
"agent_id": "writer_agent",
|
||||
},
|
||||
)
|
||||
event = AgentRunUpdateEvent(executor_id="magentic_executor", data=update)
|
||||
|
||||
# First delta should create message container
|
||||
first_delta = MagenticAgentDeltaEvent(agent_id="test_agent", text="Hello ")
|
||||
events = await mapper.convert_event(first_delta, test_request)
|
||||
events = await mapper.convert_event(event, test_request)
|
||||
|
||||
# Should emit 3 events: message container, content part, and text delta
|
||||
assert len(events) == 3
|
||||
assert events[0].type == "response.output_item.added"
|
||||
assert events[0].item.type == "message"
|
||||
assert events[0].item.metadata["agent_id"] == "test_agent"
|
||||
message_id = events[0].item.id
|
||||
|
||||
# Second delta should NOT create new container
|
||||
second_delta = MagenticAgentDeltaEvent(agent_id="test_agent", text="world!")
|
||||
events = await mapper.convert_event(second_delta, test_request)
|
||||
|
||||
assert len(events) == 1
|
||||
assert events[0].type == "response.output_text.delta"
|
||||
assert events[0].item_id == message_id
|
||||
# Should be treated as a regular AgentRunUpdateEvent with text content
|
||||
# The mapper should emit text delta events
|
||||
assert len(events) >= 1
|
||||
text_events = [e for e in events if getattr(e, "type", "") == "response.output_text.delta"]
|
||||
assert len(text_events) >= 1
|
||||
assert text_events[0].delta == "Hello from agent"
|
||||
|
||||
|
||||
async def test_magentic_agent_delta_routes_to_executor_item(
|
||||
async def test_magentic_orchestrator_message_event(mapper: MessageMapper, test_request: AgentFrameworkRequest) -> None:
|
||||
"""Test that AgentRunUpdateEvent with magentic_event_type='orchestrator_message' is handled.
|
||||
|
||||
Magentic emits orchestrator planning/instruction messages using AgentRunUpdateEvent
|
||||
with additional_properties containing magentic_event_type='orchestrator_message'.
|
||||
"""
|
||||
from agent_framework._types import AgentRunResponseUpdate, Role, TextContent
|
||||
from agent_framework._workflows._events import AgentRunUpdateEvent
|
||||
|
||||
# Create orchestrator message event (REAL format from Magentic)
|
||||
update = AgentRunResponseUpdate(
|
||||
contents=[TextContent(text="Planning: First, the writer will create content...")],
|
||||
role=Role.ASSISTANT,
|
||||
author_name="Orchestrator",
|
||||
additional_properties={
|
||||
"magentic_event_type": "orchestrator_message",
|
||||
"orchestrator_message_kind": "task_ledger",
|
||||
"orchestrator_id": "magentic_orchestrator",
|
||||
},
|
||||
)
|
||||
event = AgentRunUpdateEvent(executor_id="magentic_orchestrator", data=update)
|
||||
|
||||
events = await mapper.convert_event(event, test_request)
|
||||
|
||||
# Currently, mapper treats this as regular AgentRunUpdateEvent (no special handling)
|
||||
# This test documents the current behavior
|
||||
assert len(events) >= 1
|
||||
text_events = [e for e in events if getattr(e, "type", "") == "response.output_text.delta"]
|
||||
assert len(text_events) >= 1
|
||||
assert "Planning:" in text_events[0].delta
|
||||
|
||||
|
||||
async def test_magentic_events_use_same_event_class_as_other_workflows(
|
||||
mapper: MessageMapper, test_request: AgentFrameworkRequest
|
||||
) -> None:
|
||||
"""Test that MagenticAgentDeltaEvent routes to executor item when executor context is present."""
|
||||
from dataclasses import dataclass
|
||||
"""Verify Magentic uses the same AgentRunUpdateEvent class as other workflows.
|
||||
|
||||
from agent_framework import WorkflowEvent
|
||||
This test documents that Magentic does NOT define separate event classes like
|
||||
MagenticAgentDeltaEvent - it reuses AgentRunUpdateEvent with metadata in
|
||||
additional_properties. Any mapper code checking for 'MagenticAgentDeltaEvent'
|
||||
class names is dead code.
|
||||
"""
|
||||
from agent_framework._types import AgentRunResponseUpdate, Role, TextContent
|
||||
from agent_framework._workflows._events import AgentRunUpdateEvent
|
||||
|
||||
@dataclass
|
||||
class MagenticAgentDeltaEvent(WorkflowEvent):
|
||||
agent_id: str
|
||||
text: str | None = None
|
||||
# Create events the way different workflows do it
|
||||
# 1. Regular workflow (no additional_properties)
|
||||
regular_update = AgentRunResponseUpdate(
|
||||
contents=[TextContent(text="Regular workflow response")],
|
||||
role=Role.ASSISTANT,
|
||||
)
|
||||
regular_event = AgentRunUpdateEvent(executor_id="regular_executor", data=regular_update)
|
||||
|
||||
# First, invoke an executor (sets current_executor_id in context)
|
||||
executor_event = create_executor_invoked_event(executor_id="agent_writer")
|
||||
executor_events = await mapper.convert_event(executor_event, test_request)
|
||||
# 2. Magentic workflow (with additional_properties)
|
||||
magentic_update = AgentRunResponseUpdate(
|
||||
contents=[TextContent(text="Magentic workflow response")],
|
||||
role=Role.ASSISTANT,
|
||||
additional_properties={"magentic_event_type": "agent_delta"},
|
||||
)
|
||||
magentic_event = AgentRunUpdateEvent(executor_id="magentic_executor", data=magentic_update)
|
||||
|
||||
assert len(executor_events) == 1
|
||||
assert executor_events[0].type == "response.output_item.added"
|
||||
executor_item_id = executor_events[0].item.id
|
||||
# Both should be the SAME class
|
||||
assert type(regular_event) is type(magentic_event)
|
||||
assert isinstance(regular_event, AgentRunUpdateEvent)
|
||||
assert isinstance(magentic_event, AgentRunUpdateEvent)
|
||||
|
||||
# Now send Magentic delta - should route to executor's item
|
||||
delta = MagenticAgentDeltaEvent(agent_id="writer", text="Hello world")
|
||||
delta_events = await mapper.convert_event(delta, test_request)
|
||||
# Both should be handled by the same isinstance check in mapper
|
||||
regular_events = await mapper.convert_event(regular_event, test_request)
|
||||
magentic_events = await mapper.convert_event(magentic_event, test_request)
|
||||
|
||||
# Should only emit 1 event: text delta routed to executor's item
|
||||
assert len(delta_events) == 1
|
||||
assert delta_events[0].type == "response.output_text.delta"
|
||||
assert delta_events[0].item_id == executor_item_id
|
||||
assert delta_events[0].delta == "Hello world"
|
||||
# Both produce text delta events
|
||||
regular_text = [e for e in regular_events if getattr(e, "type", "") == "response.output_text.delta"]
|
||||
magentic_text = [e for e in magentic_events if getattr(e, "type", "") == "response.output_text.delta"]
|
||||
|
||||
assert len(regular_text) >= 1
|
||||
assert len(magentic_text) >= 1
|
||||
|
||||
|
||||
# =============================================================================
|
||||
|
||||
Reference in New Issue
Block a user