const messagesEl = document.querySelector("#messages"); const statusEl = document.querySelector("#connection-status"); const chatForm = document.querySelector("#chat-form"); const userMessage = document.querySelector("#user-message"); const runButton = document.querySelector("#run-button"); const sessionTitle = document.querySelector("#session-title"); const sessionList = document.querySelector("#session-list"); const sessionStatus = document.querySelector("#session-status"); const auditReplay = document.querySelector("#audit-replay"); const auditCount = document.querySelector("#audit-count"); const auditSummaryEl = document.querySelector("#audit-summary"); const auditDetail = document.querySelector("#audit-detail"); const sessionUsageSummary = document.querySelector("#session-usage-summary"); const sessionUsageTurns = document.querySelector("#session-usage-turns"); const sessionUsageCalls = document.querySelector("#session-usage-calls"); const snapshotName = document.querySelector("#workspace-snapshot-name"); const savedSnapshots = document.querySelector("#saved-workspace-snapshots"); const systemPrompts = document.querySelector("#system-prompts"); const preMessages = document.querySelector("#pre-messages"); const preMessageTemplate = document.querySelector("#pre-message-template"); const toolList = document.querySelector("#tool-list"); const toolCount = document.querySelector("#tool-count"); const chatConfigDialog = document.querySelector("#chat-config-dialog"); const eventConfigDialog = document.querySelector("#event-config-dialog"); const auditDialog = document.querySelector("#audit-dialog"); const toolInvocationMode = document.querySelector("#tool-invocation-mode"); const toolModeHelp = document.querySelector("#tool-mode-help"); const WORKSPACE_SNAPSHOTS_STORAGE_KEY = "agent-lab.workspace-snapshots.v1"; const LEGACY_PROMPT_SETS_STORAGE_KEY = "agent-lab.prompt-sets.v1"; let socket = null; let activeAssistant = null; let runStartedAt = 0; let firstTokenAt = 0; let elapsedTimer = 0; let hasBackendRoundStats = false; let pendingEnabledTools = null; let availableTools = []; let currentSessionId = null; let sessionReplayLoadToken = 0; let sessions = []; let auditEntries = []; let selectedAuditKey = null; let liveAuditSequence = 0; let turnActive = false; bindClick("#add-system-prompt", () => { systemPrompts.append(createSystemPrompt({ content: "" })); }); bindClick("#add-pre-message", () => { preMessages.append(createPreMessage({ role: "user", content: "" })); }); bindClick("#save-workspace-snapshot", saveWorkspaceSnapshot); bindClick("#load-workspace-snapshot", loadWorkspaceSnapshot); bindClick("#delete-workspace-snapshot", deleteWorkspaceSnapshot); bindClick("#new-session", createSession); bindClick("#refresh-sessions", () => { loadSessions(); }); bindClick("#load-session", () => { loadSelectedSession(); }); bindClick("#open-audit-dialog", () => { auditDialog.showModal(); }); bindClick("#close-audit-dialog", () => { auditDialog.close(); }); bindClick("#open-chat-config-panel", openChatConfig); bindClick("#close-chat-config", () => { chatConfigDialog.close(); }); bindClick("#open-event-config-panel", openEventConfig); bindClick("#close-event-config", () => { eventConfigDialog.close(); }); bindClick("#select-all-tools", () => { setAllTools(true); }); bindClick("#clear-tools", () => { setAllTools(false); }); toolInvocationMode.addEventListener("change", updateToolModeHelp); chatForm.addEventListener("submit", (event) => { event.preventDefault(); runDebugSession(); }); initializePromptList(); updateToolModeHelp(); refreshWorkspaceSnapshotSelector(); loadTools(); loadSessions({ preserveStatus: true }); renderAuditReplay([]); renderSessionUsage({ calls: [], turns: [], session: {} }); function openChatConfig() { chatConfigDialog.showModal(); } function openEventConfig() { eventConfigDialog.showModal(); } function bindClick(selector, handler) { const element = document.querySelector(selector); if (element) { element.addEventListener("click", handler); } } async function loadTools() { try { const response = await fetch("/api/tools"); if (!response.ok) { throw new Error(`Tool API failed: ${response.status}`); } renderTools(await response.json()); } catch (error) { toolList.textContent = "Failed to load tools"; } } async function loadSessions(options = {}) { if (!options.preserveStatus) { setSessionStatus("Refreshing sessions"); } try { sessions = await fetchJson("/api/sessions"); renderSessions(); if (!options.preserveStatus) { setSessionStatus("Sessions refreshed"); } } catch (error) { if (!options.preserveStatus) { setSessionStatus("Failed to load sessions"); } } } function renderSessions() { const previous = currentSessionId || sessionList.value; sessionList.textContent = ""; const placeholder = document.createElement("option"); placeholder.value = ""; placeholder.textContent = sessions.length ? "Select a session" : "No saved sessions"; sessionList.append(placeholder); sessions.forEach((session) => { const option = document.createElement("option"); option.value = session.id; option.textContent = `${session.title} (${session.turn_count} turns, ${session.total_tokens} tokens)`; sessionList.append(option); }); if (previous && sessions.some((session) => session.id === previous)) { sessionList.value = previous; } } async function createSession() { if (hasActiveTurn()) { setSessionStatus("Finish current turn before switching sessions"); return; } closeSocket(); setSessionStatus("Creating session"); try { const title = sessionTitle.value.trim() || "New session"; const session = await fetchJson("/api/sessions", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ title, config: buildWorkspaceSnapshot(), }), }); currentSessionId = session.id; sessionTitle.value = session.title; await loadSessions({ preserveStatus: true }); await loadSessionReplay(session.id, { preserveStatus: true }); setSessionStatus("Session created"); } catch (error) { setSessionStatus("Failed to create session"); } } async function loadSelectedSession() { if (hasActiveTurn()) { setSessionStatus("Finish current turn before switching sessions"); return; } const sessionId = sessionList.value; if (!sessionId) { setSessionStatus("Select a session"); return; } closeSocket(); setSessionStatus("Loading session"); currentSessionId = sessionId; await loadSessionReplay(sessionId); } async function loadSessionReplay(sessionId, options = {}) { const loadToken = ++sessionReplayLoadToken; const isCurrentLoad = () => ( loadToken === sessionReplayLoadToken && sessionId === currentSessionId ); try { const [session, messages, audit, usage] = await Promise.all([ fetchJson(`/api/sessions/${sessionId}`), fetchJson(`/api/sessions/${sessionId}/messages`), fetchJson(`/api/sessions/${sessionId}/audit`), fetchJson(`/api/sessions/${sessionId}/usage`), ]); if (!isCurrentLoad()) { return; } restoreWorkspaceSnapshot(session.config || {}); renderReplayMessages(messages); renderAuditReplay(audit); renderSessionUsage(usage); sessionTitle.value = session.title; sessionList.value = sessionId; if (!options.preserveStatus && !isSocketOpen()) { setSessionStatus("Session loaded"); } } catch (error) { if (!isCurrentLoad()) { return; } if (!options.preserveStatus) { setSessionStatus("Failed to load session"); } } } function setSessionStatus(message) { sessionStatus.textContent = message; statusEl.textContent = message; } async function fetchJson(url, options) { const response = await fetch(url, options); if (!response.ok) { throw new Error(`Request failed: ${response.status}`); } return response.json(); } function renderTools(tools) { availableTools = tools; toolList.textContent = ""; if (!tools.length) { toolList.textContent = "No tools available"; updateToolCount(); updateEventPromptItem(tools); return; } tools.forEach((tool) => { const item = document.createElement("div"); item.className = "tool-card"; const label = document.createElement("label"); label.className = "checkbox"; label.title = tool.description || tool.name; const checkbox = document.createElement("input"); checkbox.type = "checkbox"; checkbox.className = "tool-toggle"; checkbox.value = tool.name; checkbox.checked = true; checkbox.addEventListener("change", () => { updateToolCount(); updateEventPromptItem(availableTools); }); label.append(checkbox, ` ${tool.name}`); const description = document.createElement("div"); description.className = "tool-description"; description.textContent = tool.description || "No description"; const required = document.createElement("div"); required.className = "tool-required"; required.textContent = formatRequiredParameters(tool); item.append(label, description, required); toolList.append(item); }); if (pendingEnabledTools) { applySelectedTools(pendingEnabledTools); pendingEnabledTools = null; } updateToolCount(); updateEventPromptItem(tools); } function formatRequiredParameters(tool) { const required = tool.parameters && Array.isArray(tool.parameters.required) ? tool.parameters.required : []; return required.length ? `Required: ${required.join(", ")}` : "No required parameters"; } function setAllTools(checked) { toolList.querySelectorAll(".tool-toggle").forEach((checkbox) => { checkbox.checked = checked; }); updateToolCount(); updateEventPromptItem(availableTools); } function updateToolCount() { const selected = selectedTools().length; const total = toolList.querySelectorAll(".tool-toggle").length; toolCount.textContent = `${selected}/${total} selected`; } function saveWorkspaceSnapshot() { const name = snapshotName.value.trim(); if (!name) { statusEl.textContent = "Snapshot name required"; return; } const store = readWorkspaceSnapshots(); store.snapshots[name] = buildWorkspaceSnapshot(); writeWorkspaceSnapshots(store); refreshWorkspaceSnapshotSelector(name); statusEl.textContent = "Workspace snapshot saved"; } function loadWorkspaceSnapshot() { const name = savedSnapshots.value; if (!name) { statusEl.textContent = "Select a snapshot"; return; } const snapshot = readWorkspaceSnapshots().snapshots[name]; if (!snapshot) { statusEl.textContent = "Snapshot not found"; refreshWorkspaceSnapshotSelector(); return; } snapshotName.value = name; restoreWorkspaceSnapshot(snapshot); statusEl.textContent = "Workspace snapshot loaded"; } function deleteWorkspaceSnapshot() { const name = savedSnapshots.value; if (!name) { statusEl.textContent = "Select a snapshot"; return; } const store = readWorkspaceSnapshots(); delete store.snapshots[name]; writeWorkspaceSnapshots(store); if (snapshotName.value.trim() === name) { snapshotName.value = ""; } refreshWorkspaceSnapshotSelector(); statusEl.textContent = "Workspace snapshot deleted"; } function buildWorkspaceSnapshot() { return { tool_invocation_mode: document.querySelector("#tool-invocation-mode").value, prompt_items: buildPromptItems(), system_prompts: buildCustomPromptItems() .map((item) => item.content) .filter(Boolean), pre_messages: [...document.querySelectorAll(".pre-message")] .map((row) => ({ role: row.querySelector(".pre-role").value, content: row.querySelector(".pre-content").value.trim(), })) .filter((message) => message.content), chat_agent: { model: document.querySelector("#model").value.trim(), temperature: Number(document.querySelector("#temperature").value), max_tokens: Number(document.querySelector("#max-tokens").value), system_prompt: "", extra_body: buildExtraBody("chat"), }, event_agent: { model: document.querySelector("#event-model").value.trim(), temperature: Number(document.querySelector("#event-temperature").value), max_tokens: Number(document.querySelector("#event-max-tokens").value), system_prompt: document.querySelector("#event-system-prompt").value.trim(), extra_body: buildExtraBody("event"), enabled_tools: selectedTools(), max_event_loops: Number(document.querySelector("#max-event-loops").value), max_parallel_events: Number(document.querySelector("#max-parallel-events").value), batch_timeout_seconds: Number(document.querySelector("#batch-timeout-seconds").value), }, }; } function restoreWorkspaceSnapshot(snapshot) { document.querySelector("#tool-invocation-mode").value = snapshot.tool_invocation_mode || "dual_agent"; restoreSystemPrompts(snapshot.prompt_items || snapshot.system_prompts || []); restorePreMessages(snapshot.pre_messages || []); const chatAgent = snapshot.chat_agent || {}; document.querySelector("#model").value = chatAgent.model || ""; document.querySelector("#temperature").value = chatAgent.temperature ?? 0.2; document.querySelector("#max-tokens").value = chatAgent.max_tokens ?? 800; restoreExtraBody("chat", chatAgent.extra_body); const eventAgent = snapshot.event_agent || {}; document.querySelector("#event-model").value = eventAgent.model || ""; document.querySelector("#event-temperature").value = eventAgent.temperature ?? 0.2; document.querySelector("#event-max-tokens").value = eventAgent.max_tokens ?? 800; document.querySelector("#event-system-prompt").value = eventAgent.system_prompt || ""; restoreExtraBody("event", eventAgent.extra_body); document.querySelector("#max-event-loops").value = eventAgent.max_event_loops ?? 1; document.querySelector("#max-parallel-events").value = eventAgent.max_parallel_events ?? 4; document.querySelector("#batch-timeout-seconds").value = eventAgent.batch_timeout_seconds ?? 15; const enabledTools = eventAgent.enabled_tools || []; pendingEnabledTools = applySelectedTools(enabledTools) ? null : enabledTools; updateEventPromptItem(availableTools); updateToolModeHelp(); } function updateToolModeHelp() { if (toolInvocationMode.value === "chat_agent_tools") { toolModeHelp.textContent = "ChatAgent Tools uses provider-resolved arguments. EventAgent model, temperature, max tokens, system prompt, and extra body are inactive; enabled tools, loop budget, batch concurrency, and timeout remain active."; return; } toolModeHelp.textContent = "Dual Agent may use the EventAgent model, system prompt, and extra body for missing-argument fallback."; } function buildExtraBody(prefix) { const thinkingDisabled = document.querySelector(`#${prefix}-thinking-disabled`).checked; const enableSearch = document.querySelector(`#${prefix}-enable-search`).checked; const forcedSearch = document.querySelector(`#${prefix}-forced-search`).checked; const extraBody = { enable_search: enableSearch, search_options: { forced_search: forcedSearch, }, }; if (thinkingDisabled) { extraBody.thinking = { type: "disabled" }; } return extraBody; } function restoreExtraBody(prefix, extraBody = {}) { document.querySelector(`#${prefix}-thinking-disabled`).checked = !extraBody.thinking || extraBody.thinking.type === "disabled"; document.querySelector(`#${prefix}-enable-search`).checked = Boolean(extraBody.enable_search); document.querySelector(`#${prefix}-forced-search`).checked = Boolean(extraBody.search_options && extraBody.search_options.forced_search); } function restoreSystemPrompts(prompts) { systemPrompts.textContent = ""; const promptItems = normalizePromptItems(prompts); promptItems.forEach((prompt) => { systemPrompts.append(createSystemPrompt(prompt)); }); ensureEventPromptItem(); } function restorePreMessages(messages) { preMessages.textContent = ""; messages.forEach((message) => { preMessages.append(createPreMessage(message)); }); } function createSystemPrompt(prompt) { const promptItem = normalizePromptItem(prompt); const item = document.createElement("div"); item.className = promptItem.prompt_kind === "event_agent_system" ? "prompt-item event-prompt" : "prompt-item"; item.dataset.promptKind = promptItem.prompt_kind; item.dataset.promptRole = promptItem.role; item.draggable = true; const head = document.createElement("div"); head.className = "prompt-item-head"; const meta = document.createElement("div"); meta.className = "prompt-meta"; const type = document.createElement("span"); type.className = "prompt-type"; type.textContent = `type: ${promptItem.role}`; const title = document.createElement("span"); title.className = "prompt-title"; title.textContent = promptItem.prompt_kind === "event_agent_system" ? "EventAgent Rules" : "Custom"; meta.append(type, title); const actions = document.createElement("div"); actions.className = "prompt-actions"; const moveUp = document.createElement("button"); moveUp.className = "move-prompt-up"; moveUp.type = "button"; moveUp.textContent = "Up"; moveUp.addEventListener("click", () => movePromptItem(item, -1)); const moveDown = document.createElement("button"); moveDown.className = "move-prompt-down"; moveDown.type = "button"; moveDown.textContent = "Down"; moveDown.addEventListener("click", () => movePromptItem(item, 1)); actions.append(moveUp, moveDown); if (promptItem.prompt_kind !== "event_agent_system") { const deleteButton = document.createElement("button"); deleteButton.className = "delete-system-prompt"; deleteButton.type = "button"; deleteButton.textContent = "Delete"; deleteButton.addEventListener("click", () => item.remove()); actions.append(deleteButton); } head.append(meta, actions); const textarea = document.createElement("textarea"); textarea.className = "system-prompt"; textarea.rows = promptItem.prompt_kind === "event_agent_system" ? 7 : 3; textarea.value = promptItem.content; textarea.readOnly = promptItem.prompt_kind === "event_agent_system"; item.append(head, textarea); bindPromptDrag(item); return item; } function createPreMessage(message) { const fragment = preMessageTemplate.content.cloneNode(true); fragment.querySelector(".pre-role").value = message.role || "user"; fragment.querySelector(".pre-content").value = message.content || ""; return fragment; } function applySelectedTools(toolNames) { const checkboxes = [...toolList.querySelectorAll(".tool-toggle")]; if (!checkboxes.length) { return false; } const selected = new Set(toolNames); checkboxes.forEach((checkbox) => { checkbox.checked = selected.has(checkbox.value); }); updateToolCount(); updateEventPromptItem(availableTools); return true; } function initializePromptList() { [...systemPrompts.querySelectorAll(".prompt-item")].forEach((item) => { bindPromptItemControls(item); bindPromptDrag(item); }); ensureEventPromptItem(); } function buildPromptItems() { return [...systemPrompts.querySelectorAll(".prompt-item")] .map((item) => ({ prompt_kind: item.dataset.promptKind || "custom", role: item.dataset.promptRole || "system", content: item.querySelector(".system-prompt").value.trim(), })) .filter((item) => item.prompt_kind === "event_agent_system" || item.content); } function buildCustomPromptItems() { return buildPromptItems().filter((item) => item.prompt_kind !== "event_agent_system"); } function collectSystemPrompts() { return buildPromptItems() .map((item) => item.content) .filter(Boolean); } function normalizePromptItems(prompts) { const items = Array.isArray(prompts) ? prompts : []; const normalized = items .map((prompt) => normalizePromptItem(prompt)) .filter((prompt) => prompt.prompt_kind === "event_agent_system" || prompt.content); if (!normalized.some((prompt) => prompt.prompt_kind === "event_agent_system")) { normalized.push({ prompt_kind: "event_agent_system", content: "" }); } if (!normalized.some((prompt) => prompt.prompt_kind === "custom")) { normalized.unshift({ prompt_kind: "custom", role: "system", content: "" }); } return normalized; } function normalizePromptItem(prompt) { if (prompt && typeof prompt === "object" && !Array.isArray(prompt)) { return { prompt_kind: prompt.prompt_kind === "event_agent_system" ? "event_agent_system" : "custom", role: "system", content: prompt.content || "", }; } return { prompt_kind: "custom", role: "system", content: prompt || "", }; } function buildAvailableEventsPrompt(tools) { const eventLines = tools.map((tool) => `- ${tool.name}: ${tool.description || "No description"}`); if (!eventLines.length) { return ""; } return [ "You may request EventAgent work using this text protocol.", "Available events:", ...eventLines, "First write the user-facing reply normally. If events are needed, append this block after the visible reply:", "", "event_name", "", "Use exact event names only, one per line. Do not include parameters.", ].join("\n"); } function updateEventPromptItem(tools) { const eventPrompt = ensureEventPromptItem(); const selected = new Set(selectedTools()); const prompt = buildAvailableEventsPrompt( tools.filter((tool) => selected.has(tool.name)), ); eventPrompt.querySelector(".system-prompt").value = prompt; } function ensureEventPromptItem() { const existing = systemPrompts.querySelector('[data-prompt-kind="event_agent_system"]'); if (existing) { return existing; } const item = createSystemPrompt({ prompt_kind: "event_agent_system", content: "", }); systemPrompts.append(item); return item; } function bindPromptItemControls(item) { const moveUp = item.querySelector(".move-prompt-up"); if (moveUp) { moveUp.addEventListener("click", () => movePromptItem(item, -1)); } const moveDown = item.querySelector(".move-prompt-down"); if (moveDown) { moveDown.addEventListener("click", () => movePromptItem(item, 1)); } const deleteButton = item.querySelector(".delete-system-prompt"); if (deleteButton) { deleteButton.addEventListener("click", () => item.remove()); } } function movePromptItem(item, direction) { if (direction < 0 && item.previousElementSibling) { systemPrompts.insertBefore(item, item.previousElementSibling); } if (direction > 0 && item.nextElementSibling) { systemPrompts.insertBefore(item.nextElementSibling, item); } } function bindPromptDrag(item) { item.addEventListener("dragstart", () => { item.classList.add("dragging"); }); item.addEventListener("dragend", () => { item.classList.remove("dragging"); }); item.addEventListener("dragover", (event) => { event.preventDefault(); const dragging = systemPrompts.querySelector(".prompt-item.dragging"); if (!dragging || dragging === item) { return; } const rect = item.getBoundingClientRect(); const afterMidpoint = event.clientY > rect.top + rect.height / 2; systemPrompts.insertBefore(dragging, afterMidpoint ? item.nextSibling : item); }); } function refreshWorkspaceSnapshotSelector(selectedName = "") { const previousName = selectedName || savedSnapshots.value; const names = Object.keys(readWorkspaceSnapshots().snapshots).sort(); savedSnapshots.textContent = ""; const placeholder = document.createElement("option"); placeholder.value = ""; placeholder.textContent = names.length ? "Select a snapshot" : "No saved snapshots"; savedSnapshots.append(placeholder); names.forEach((name) => { const option = document.createElement("option"); option.value = name; option.textContent = name; savedSnapshots.append(option); }); if (names.includes(previousName)) { savedSnapshots.value = previousName; } } function readWorkspaceSnapshots() { try { const rawValue = localStorage.getItem(WORKSPACE_SNAPSHOTS_STORAGE_KEY); if (!rawValue) { return readLegacyPromptSets(); } const parsed = JSON.parse(rawValue); if ( !parsed || typeof parsed !== "object" || !parsed.snapshots || typeof parsed.snapshots !== "object" || Array.isArray(parsed.snapshots) ) { return { snapshots: {} }; } return parsed; } catch { return { snapshots: {} }; } } function readLegacyPromptSets() { try { const rawValue = localStorage.getItem(LEGACY_PROMPT_SETS_STORAGE_KEY); if (!rawValue) { return { snapshots: {} }; } const parsed = JSON.parse(rawValue); if ( !parsed || typeof parsed !== "object" || !parsed.sets || typeof parsed.sets !== "object" || Array.isArray(parsed.sets) ) { return { snapshots: {} }; } return { snapshots: parsed.sets }; } catch { return { snapshots: {} }; } } function writeWorkspaceSnapshots(store) { localStorage.setItem(WORKSPACE_SNAPSHOTS_STORAGE_KEY, JSON.stringify(store)); } function runDebugSession() { const submittedMessage = userMessage.value.trim(); if (!submittedMessage) { statusEl.textContent = "Message required"; return; } if (isSocketOpen()) { beginTurn(submittedMessage, "Waiting for first token"); socket.send(JSON.stringify({ type: "user_message", content: submittedMessage })); userMessage.value = ""; return; } closeSocket(); resetRun(); beginTurn(submittedMessage, "Connecting"); socket = new WebSocket(wsUrl()); const activeSocket = socket; activeSocket.addEventListener("open", () => { if (socket !== activeSocket) { return; } statusEl.textContent = "Waiting for first token"; activeSocket.send(JSON.stringify(buildRequest(submittedMessage))); userMessage.value = ""; }); activeSocket.addEventListener("message", (event) => { if (socket !== activeSocket) { return; } handleServerMessage(JSON.parse(event.data)); }); activeSocket.addEventListener("close", () => { if (socket !== activeSocket) { return; } socket = null; statusEl.textContent = "Idle"; stopElapsedTimer(); setRunState(false); updateElapsed(); }); activeSocket.addEventListener("error", () => { if (socket !== activeSocket) { return; } appendLog("error", "WebSocket connection failed"); }); } function isSocketOpen() { return socket && socket.readyState === WebSocket.OPEN; } function hasActiveTurn() { return turnActive; } function beginTurn(submittedMessage, statusText) { activeAssistant = null; firstTokenAt = 0; hasBackendRoundStats = false; resetTurnStats(); appendLog("user", submittedMessage); setRunState(true); runStartedAt = performance.now(); startElapsedTimer(); statusEl.textContent = statusText; } function buildRequest(submittedMessage = userMessage.value) { return { session_id: currentSessionId || undefined, user_message: submittedMessage, tool_invocation_mode: document.querySelector("#tool-invocation-mode").value, system_prompts: collectSystemPrompts(), pre_messages: [...document.querySelectorAll(".pre-message")] .map((row) => ({ role: row.querySelector(".pre-role").value, content: row.querySelector(".pre-content").value.trim(), })) .filter((message) => message.content), chat_agent: { model: document.querySelector("#model").value.trim(), temperature: Number(document.querySelector("#temperature").value), max_tokens: Number(document.querySelector("#max-tokens").value), system_prompt: "", extra_body: buildExtraBody("chat"), }, event_agent: { model: document.querySelector("#event-model").value.trim(), temperature: Number(document.querySelector("#event-temperature").value), max_tokens: Number(document.querySelector("#event-max-tokens").value), system_prompt: document.querySelector("#event-system-prompt").value.trim(), extra_body: buildExtraBody("event"), enabled_tools: selectedTools(), max_event_loops: Number(document.querySelector("#max-event-loops").value), max_parallel_events: Number(document.querySelector("#max-parallel-events").value), batch_timeout_seconds: Number(document.querySelector("#batch-timeout-seconds").value), }, }; } function selectedTools() { return [...toolList.querySelectorAll(".tool-toggle:checked")].map( (input) => input.value, ); } function handleServerMessage(message) { if (message.type === "session_started") { if (message.session_id) { currentSessionId = message.session_id; loadSessions({ preserveStatus: true }); } appendLog("session", "Session started"); return; } if (message.type === "turn_started") { statusEl.textContent = "Waiting for first token"; return; } if (message.type === "message_delta") { appendAssistantDelta(message.content || ""); return; } if (message.type === "usage") { updateUsage(message.usage || {}); return; } if (message.type === "round_stats") { updateRoundStats(message); return; } if (message.type === "audit") { appendAuditEntry(message); return; } if (message.type === "event") { appendLog("event", message.event.name); return; } if (message.type === "tool_result") { appendLog("tool", message.message.content); return; } if (message.type === "error") { appendLog("error", message.message); stopElapsedTimer(); setRunState(false); return; } if (message.type === "turn_completed") { appendLog("session", `Turn ${message.turn_index || ""} completed`.trim()); statusEl.textContent = "Idle"; stopElapsedTimer(); setRunState(false); updateElapsed(); if (currentSessionId) { loadSessions({ preserveStatus: true }); loadSessionReplay(currentSessionId, { preserveStatus: true }); } return; } if (message.type === "done") { appendLog("session", "Done"); statusEl.textContent = "Idle"; stopElapsedTimer(); setRunState(false); if (currentSessionId) { loadSessions({ preserveStatus: true }); loadSessionReplay(currentSessionId, { preserveStatus: true }); } closeSocket(); } } function appendAssistantDelta(content) { if (!firstTokenAt) { firstTokenAt = performance.now(); statusEl.textContent = "Streaming"; if (!hasBackendRoundStats) { document.querySelector("#stat-ttft").textContent = `${Math.round(firstTokenAt - runStartedAt)}ms`; } } if (!activeAssistant) { activeAssistant = document.createElement("div"); activeAssistant.className = "message assistant"; messagesEl.append(activeAssistant); } activeAssistant.textContent += content; updateElapsed(); messagesEl.scrollTop = messagesEl.scrollHeight; } function appendLog(kind, content) { const item = document.createElement("div"); item.className = `message ${kind}`; item.textContent = content; messagesEl.append(item); messagesEl.scrollTop = messagesEl.scrollHeight; } function renderReplayMessages(messages) { messagesEl.textContent = ""; activeAssistant = null; if (!messages.length) { appendLog("session", "No messages yet"); return; } messages.forEach((message) => { if ( message.role === "assistant" && !message.content.trim() && Array.isArray(message.tool_calls) && message.tool_calls.length > 0 ) { return; } const item = document.createElement("div"); const kind = ["user", "assistant", "tool"].includes(message.role) ? message.role : "session"; item.className = `message ${kind}`; item.textContent = message.content; messagesEl.append(item); }); messagesEl.scrollTop = messagesEl.scrollHeight; } function appendAuditEntry(message) { const entry = normalizeAuditEntry(message); renderAuditReplay([...auditEntries, entry], { selectedKey: auditEntryKey(entry), scrollToEnd: true, }); } function normalizeAuditEntry(entry) { const details = entry.details || {}; return { id: entry.id || `live-${++liveAuditSequence}`, event: entry.event || "unknown_event", details, turn_index: entry.turn_index ?? details.turn_index ?? null, round_index: entry.round_index ?? details.round_index ?? null, turn_elapsed_ms: entry.turn_elapsed_ms ?? details.turn_elapsed_ms ?? null, created_at: entry.created_at || new Date().toISOString(), }; } function auditEntryKey(entry) { return String(entry.id); } function renderAuditReplay(entries, options = {}) { auditEntries = entries.map(normalizeAuditEntry); auditReplay.textContent = ""; auditCount.textContent = `${auditEntries.length} events`; if (!auditEntries.length) { selectedAuditKey = null; auditSummaryEl.textContent = "No audit entries"; auditReplay.append(emptyReplayRow("No audit entries")); renderAuditDetail(null); return; } const keys = auditEntries.map(auditEntryKey); selectedAuditKey = options.selectedKey || selectedAuditKey; if (!selectedAuditKey || !keys.includes(selectedAuditKey)) { selectedAuditKey = keys[keys.length - 1]; } auditEntries.forEach((entry) => { const key = auditEntryKey(entry); const row = document.createElement("button"); row.type = "button"; row.className = `audit-event ${auditKindClass(entry.event)}`; row.dataset.auditKey = key; if (key === selectedAuditKey) { row.classList.add("is-selected"); } row.addEventListener("click", () => { selectAuditEntry(key); }); const marker = document.createElement("span"); marker.className = "audit-marker"; const body = document.createElement("span"); body.className = "audit-event-body"; const meta = document.createElement("span"); meta.className = "audit-event-meta"; meta.textContent = auditMeta(entry); const title = document.createElement("strong"); title.textContent = auditTitle(entry.event); const summary = document.createElement("span"); summary.className = "audit-event-summary"; summary.textContent = auditSummary(entry); body.append(meta, title, summary); row.append(marker, body); auditReplay.append(row); }); renderAuditDetail( auditEntries.find((entry) => auditEntryKey(entry) === selectedAuditKey) || null, ); const selectedEntry = auditEntries.find( (entry) => auditEntryKey(entry) === selectedAuditKey, ); auditSummaryEl.textContent = selectedEntry ? `${auditTitle(selectedEntry.event)} · ${auditMeta(selectedEntry)}` : `${auditEntries.length} audit events`; if (options.scrollToEnd) { auditReplay.scrollTop = auditReplay.scrollHeight; } } function selectAuditEntry(key) { selectedAuditKey = key; renderAuditReplay(auditEntries); } function renderAuditDetail(entry) { auditDetail.textContent = ""; if (!entry) { auditDetail.append(emptyReplayRow("Select an audit event to inspect details")); return; } const header = document.createElement("div"); header.className = "audit-detail-header"; const title = document.createElement("h3"); title.className = "audit-detail-title"; title.textContent = auditTitle(entry.event); const meta = document.createElement("dl"); meta.className = "audit-detail-meta"; appendMetaField(meta, "Turn / Round", auditTurnRound(entry)); appendMetaField(meta, "Since User Message", auditRelativeTime(entry)); appendMetaField(meta, "Time", formatTime(entry.created_at) || "-"); appendMetaField(meta, "Agent", auditKindLabel(entry.event)); header.append(title, meta); auditDetail.append(header); const details = entry.details || {}; if (entry.event === "chat_agent_request" || entry.event === "event_agent_request") { appendKeyValueSection(auditDetail, "Model Parameters", details.params); appendMessagesSection(auditDetail, details.messages ? "Messages" : "History", details.messages || details.history); appendEventsSection(auditDetail, details.events); appendToolsSection(auditDetail, details.tools); appendKeyValueSection(auditDetail, "Extra Body", details.extra_body); } else if (entry.event === "chat_agent_response") { appendTextSection(auditDetail, "Assistant Text", details.content); appendListSection(auditDetail, "Events", details.event_names); appendKeyValueSection(auditDetail, "Usage", details.usage); appendRawChunksSection(auditDetail, "Raw LLM Chunks", details.raw_chunks); } else if (entry.event === "event_agent_response") { appendRepliesSection(auditDetail, details.replies); appendRawModelChunksSection(auditDetail, details.raw_model_chunks); } else if (entry.event === "chat_event_detected") { appendKeyValueSection(auditDetail, "Detected Event", { name: details.event_name, id: details.event_id, }); } else if (entry.event === "event_agent_completed") { appendListSection(auditDetail, "Completed Events", details.event_names); appendKeyValueSection(auditDetail, "Results", { result_count: details.result_count, }); appendTextSection(auditDetail, "Tool Result Summary", details.result_summary); } else if (entry.event === "chat_message_stream_started" || entry.event === "chat_message_stream_finished") { appendKeyValueSection(auditDetail, "Message Stream", { ttft_ms: details.ttft_ms, delta_count: details.delta_count, content_length: details.content_length, }); } else if (entry.event === "chat_round_started") { appendKeyValueSection(auditDetail, "Event State", { event_generation_enabled: details.event_generation_enabled, }); appendListSection(auditDetail, "Enabled This Round", details.events_enabled); appendListSection(auditDetail, "Configured Events", details.configured_events); } else if (entry.event === "chat_round_finished") { appendKeyValueSection(auditDetail, "Round Result", { had_event: details.had_event, elapsed_ms: details.elapsed_ms, }); } else if (entry.event === "turn_completed") { appendKeyValueSection(auditDetail, "Turn Result", { round_count: details.round_count, }); } else if (details.raw_chunks) { appendRawChunksSection(auditDetail, "Raw LLM Chunks", details.raw_chunks); } else if (details.raw_model_chunks) { appendRawModelChunksSection(auditDetail, details.raw_model_chunks); } else { appendKeyValueSection(auditDetail, "Details", details); } appendRawDetails(auditDetail, details); } function auditTitle(event) { return event .split("_") .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) .join(" "); } function auditMeta(entry) { const time = formatTime(entry.created_at); const relativeTime = auditRelativeTime(entry); return `${auditTurnRound(entry)} / ${relativeTime}${time ? ` / ${time}` : ""}`; } function auditTurnRound(entry) { const turn = entry.turn_index === null || entry.turn_index === undefined ? "-" : entry.turn_index; const round = entry.round_index === null || entry.round_index === undefined ? "-" : entry.round_index; return `T${turn} / R${round}`; } function auditRelativeTime(entry) { if (entry.turn_elapsed_ms === null || entry.turn_elapsed_ms === undefined) { return "-"; } return `+${formatMs(entry.turn_elapsed_ms)}`; } function appendMetaField(container, label, value) { const item = document.createElement("div"); const term = document.createElement("dt"); term.textContent = label; const description = document.createElement("dd"); description.textContent = value; item.append(term, description); container.append(item); } function auditSummary(entry) { const details = entry.details || {}; if (entry.event === "chat_agent_request") { return `${details.params?.model || "model default"} · ${details.messages?.length || 0} messages`; } if (entry.event === "event_agent_request") { return `${details.params?.model || "model default"} · ${(details.events || []).map((event) => event.name).join(", ") || "no events"}`; } if (entry.event === "chat_agent_response") { const rawCount = details.raw_chunks?.length || 0; return `${details.usage?.total_tokens || 0} tokens · ${rawCount} raw chunks · ${(details.event_names || []).join(", ") || "no events"}`; } if (entry.event === "event_agent_response") { const rawCount = (details.raw_model_chunks || []).reduce( (count, item) => count + (item.chunks?.length || 0), 0, ); return `${details.replies?.length || 0} tool replies · ${rawCount} raw chunks`; } if (entry.event === "chat_event_detected") { return details.event_name || "event detected"; } if (entry.event === "event_agent_completed") { return `${details.result_count || 0} results · ${(details.event_names || []).join(", ")}`; } if (entry.event === "chat_message_stream_started") { return `visible text started · TTFT ${formatMs(details.ttft_ms)}`; } if (entry.event === "chat_message_stream_finished") { return `${details.delta_count || 0} deltas · ${details.content_length || 0} chars`; } if (entry.event === "chat_round_started") { const state = details.event_generation_enabled ? "event generation on" : "event generation off"; return `${details.events_enabled?.length || 0} enabled this round · ${state}`; } if (entry.event === "chat_round_finished") { return `${formatMs(details.elapsed_ms)} · ${details.had_event ? "event" : "no event"}`; } if (entry.event === "turn_completed") { return `${details.round_count || 0} rounds`; } return JSON.stringify(details).slice(0, 120); } function auditKindClass(event) { if (event.includes("request")) { return "audit-kind-request"; } if (event.includes("response") || event.includes("completed")) { return "audit-kind-response"; } if (event.includes("event_agent")) { return "audit-kind-event-agent"; } if (event.includes("chat")) { return "audit-kind-chat"; } return "audit-kind-lifecycle"; } function auditKindLabel(event) { if (event.includes("event_agent")) { return "EventAgent"; } if (event.includes("chat_agent") || event.includes("chat_round") || event.includes("chat_event") || event.includes("chat_message")) { return "ChatAgent"; } return "Lifecycle"; } function appendKeyValueSection(container, title, data) { if (!data || !Object.keys(data).length) { return; } const grid = document.createElement("div"); grid.className = "detail-kv"; Object.entries(data).forEach(([key, value]) => { if (value === null || value === undefined || value === "") { return; } const label = document.createElement("span"); label.textContent = key; const content = document.createElement("strong"); content.textContent = typeof value === "object" ? JSON.stringify(value) : String(value); grid.append(label, content); }); if (grid.children.length) { appendDetailSection(container, title, grid); } } function appendRawChunksSection(container, title, chunks) { if (!chunks || !chunks.length) { return; } const list = document.createElement("div"); list.className = "detail-list"; chunks.forEach((chunk, index) => { const item = document.createElement("div"); item.className = "detail-message raw-chunk"; const label = document.createElement("span"); label.textContent = `chunk ${index + 1}`; const content = document.createElement("pre"); content.textContent = JSON.stringify(chunk, null, 2); item.append(label, content); list.append(item); }); appendDetailSection(container, title, list); } function appendRawModelChunksSection(container, rawGroups) { if (!rawGroups || !rawGroups.length) { return; } const list = document.createElement("div"); list.className = "detail-list"; rawGroups.forEach((group) => { const item = document.createElement("div"); item.className = "detail-message raw-chunk"; const label = document.createElement("span"); label.textContent = `${group.event_name || "event"} · ${group.event_id || "-"}`; const content = document.createElement("pre"); content.textContent = JSON.stringify(group.chunks || [], null, 2); item.append(label, content); list.append(item); }); appendDetailSection(container, "Raw EventAgent LLM Chunks", list); } function appendMessagesSection(container, title, messages) { if (!messages || !messages.length) { return; } const list = document.createElement("div"); list.className = "detail-list"; messages.forEach((message) => { const item = document.createElement("div"); item.className = "detail-message"; const role = document.createElement("span"); role.textContent = message.role || "message"; const content = document.createElement("pre"); content.textContent = message.content || ""; item.append(role, content); list.append(item); }); appendDetailSection(container, title, list); } function appendToolsSection(container, tools) { if (!tools || !tools.length) { return; } const list = document.createElement("div"); list.className = "detail-list"; tools.forEach((tool) => { const item = document.createElement("div"); item.className = "detail-message"; const name = document.createElement("span"); name.textContent = tool.function?.name || "tool"; const description = document.createElement("p"); description.textContent = tool.function?.description || "No description"; item.append(name, description); list.append(item); }); appendDetailSection(container, "Tools", list); } function appendEventsSection(container, events) { if (!events || !events.length) { return; } const values = events.map((event) => `${event.name}${event.id ? ` (${event.id})` : ""}`); appendListSection(container, "Events", values); } function appendRepliesSection(container, replies) { if (!replies || !replies.length) { return; } const list = document.createElement("div"); list.className = "detail-list"; replies.forEach((reply) => { const item = document.createElement("div"); item.className = "detail-message"; const role = document.createElement("span"); role.textContent = reply.role || "reply"; const content = document.createElement("pre"); content.textContent = reply.content || ""; item.append(role, content); list.append(item); }); appendDetailSection(container, "Tool Replies", list); } function appendListSection(container, title, items) { if (!items || !items.length) { return; } const list = document.createElement("ul"); list.className = "detail-tags"; items.forEach((item) => { const tag = document.createElement("li"); const code = document.createElement("code"); code.textContent = String(item); tag.append(code); list.append(tag); }); appendDetailSection(container, title, list); } function appendTextSection(container, title, value) { if (!value) { return; } const pre = document.createElement("pre"); pre.textContent = String(value); appendDetailSection(container, title, pre); } function appendDetailSection(container, title, content) { const section = document.createElement("section"); section.className = "audit-detail-block"; const heading = document.createElement("h3"); heading.className = "audit-detail-block-title"; heading.textContent = title; section.append(heading, content); container.append(section); } function appendRawDetails(container, details) { if (!details || !Object.keys(details).length) { return; } const raw = document.createElement("details"); raw.className = "raw-detail"; const summary = document.createElement("summary"); summary.textContent = "Raw JSON"; const pre = document.createElement("pre"); pre.textContent = JSON.stringify(details, null, 2); raw.append(summary, pre); container.append(raw); } function formatTime(value) { if (!value) { return ""; } const date = new Date(value); if (Number.isNaN(date.getTime())) { return value; } return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit", }); } function renderSessionUsage(usage) { const session = usage.session || {}; sessionUsageSummary.textContent = ""; [ ["Tokens", session.total_tokens || 0], ["Cached", session.cached_tokens || 0], ["Prompt", session.prompt_tokens || 0], ["Elapsed", formatMs(session.elapsed_ms || 0)], ].forEach(([label, value]) => { const item = document.createElement("div"); const labelEl = document.createElement("span"); labelEl.textContent = label; const valueEl = document.createElement("strong"); valueEl.textContent = value; item.append(labelEl, valueEl); sessionUsageSummary.append(item); }); renderUsageRows( sessionUsageTurns, usage.turns || [], (turn) => `Turn ${turn.turn_index}: ${turn.total_tokens} tokens, ${turn.elapsed_ms}ms`, ); renderUsageRows( sessionUsageCalls, usage.calls || [], (call) => `T${call.turn_index} R${call.round_index}: ${call.total_tokens} tokens, TTFT ${formatMs(call.ttft_ms)}, elapsed ${formatMs(call.elapsed_ms)}`, ); } function renderUsageRows(container, rows, formatter) { container.textContent = ""; if (!rows.length) { container.append(emptyReplayRow("No usage rows")); return; } rows.forEach((row) => { const item = document.createElement("div"); item.className = "replay-row"; item.textContent = formatter(row); container.append(item); }); } function emptyReplayRow(content) { const row = document.createElement("div"); row.className = "replay-row"; row.textContent = content; return row; } function updateUsage(usage) { if (hasBackendRoundStats) { return; } document.querySelector("#stat-tokens").textContent = usage.total_tokens || 0; document.querySelector("#stat-cached").textContent = usage.cached_tokens || 0; } function updateRoundStats(stats) { if (firstTokenAt) { hasBackendRoundStats = true; } document.querySelector("#stat-tokens").textContent = stats.total_tokens || 0; document.querySelector("#stat-cached").textContent = stats.cached_tokens || 0; const ttftText = formatMs(stats.ttft_ms); const elapsedText = formatMs(stats.elapsed_ms); document.querySelector("#stat-ttft").textContent = ttftText; document.querySelector("#stat-elapsed").textContent = elapsedText; appendLog( "session", `Round ${stats.round_index}: tokens ${stats.total_tokens || 0}, cached ${stats.cached_tokens || 0}, TTFT ${ttftText}, elapsed ${elapsedText}`, ); } function formatMs(value) { return value === null || value === undefined ? "-" : `${value}ms`; } function updateElapsed() { if (!runStartedAt || (hasBackendRoundStats && firstTokenAt)) { return; } document.querySelector("#stat-elapsed").textContent = `${Math.round(performance.now() - runStartedAt)}ms`; } function startElapsedTimer() { stopElapsedTimer(); elapsedTimer = window.setInterval(updateElapsed, 100); } function stopElapsedTimer() { if (elapsedTimer) { window.clearInterval(elapsedTimer); elapsedTimer = 0; } } function setRunState(isRunning) { turnActive = isRunning; runButton.disabled = isRunning; } function resetRun() { stopElapsedTimer(); activeAssistant = null; runStartedAt = 0; firstTokenAt = 0; hasBackendRoundStats = false; messagesEl.textContent = ""; resetTurnStats(); } function resetTurnStats() { document.querySelector("#stat-tokens").textContent = "0"; document.querySelector("#stat-cached").textContent = "0"; document.querySelector("#stat-ttft").textContent = "-"; document.querySelector("#stat-elapsed").textContent = "-"; } function closeSocket() { const closingSocket = socket; if (closingSocket && closingSocket.readyState < WebSocket.CLOSING) { closingSocket.close(); } if (socket === closingSocket) { socket = null; } } function wsUrl() { const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; return `${protocol}//${window.location.host}/ws/debug`; }