app.js 51 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631
  1. const messagesEl = document.querySelector("#messages");
  2. const statusEl = document.querySelector("#connection-status");
  3. const chatForm = document.querySelector("#chat-form");
  4. const userMessage = document.querySelector("#user-message");
  5. const runButton = document.querySelector("#run-button");
  6. const sessionTitle = document.querySelector("#session-title");
  7. const sessionList = document.querySelector("#session-list");
  8. const sessionStatus = document.querySelector("#session-status");
  9. const auditReplay = document.querySelector("#audit-replay");
  10. const auditCount = document.querySelector("#audit-count");
  11. const auditSummaryEl = document.querySelector("#audit-summary");
  12. const auditDetail = document.querySelector("#audit-detail");
  13. const sessionUsageSummary = document.querySelector("#session-usage-summary");
  14. const sessionUsageTurns = document.querySelector("#session-usage-turns");
  15. const sessionUsageCalls = document.querySelector("#session-usage-calls");
  16. const snapshotName = document.querySelector("#workspace-snapshot-name");
  17. const savedSnapshots = document.querySelector("#saved-workspace-snapshots");
  18. const systemPrompts = document.querySelector("#system-prompts");
  19. const preMessages = document.querySelector("#pre-messages");
  20. const preMessageTemplate = document.querySelector("#pre-message-template");
  21. const toolList = document.querySelector("#tool-list");
  22. const toolCount = document.querySelector("#tool-count");
  23. const chatConfigDialog = document.querySelector("#chat-config-dialog");
  24. const eventConfigDialog = document.querySelector("#event-config-dialog");
  25. const auditDialog = document.querySelector("#audit-dialog");
  26. const toolInvocationMode = document.querySelector("#tool-invocation-mode");
  27. const toolModeHelp = document.querySelector("#tool-mode-help");
  28. const WORKSPACE_SNAPSHOTS_STORAGE_KEY = "agent-lab.workspace-snapshots.v1";
  29. const LEGACY_PROMPT_SETS_STORAGE_KEY = "agent-lab.prompt-sets.v1";
  30. let socket = null;
  31. let activeAssistant = null;
  32. let runStartedAt = 0;
  33. let firstTokenAt = 0;
  34. let elapsedTimer = 0;
  35. let hasBackendRoundStats = false;
  36. let pendingEnabledTools = null;
  37. let availableTools = [];
  38. let currentSessionId = null;
  39. let sessionReplayLoadToken = 0;
  40. let sessions = [];
  41. let auditEntries = [];
  42. let selectedAuditKey = null;
  43. let liveAuditSequence = 0;
  44. let turnActive = false;
  45. bindClick("#add-system-prompt", () => {
  46. systemPrompts.append(createSystemPrompt({ content: "" }));
  47. });
  48. bindClick("#add-pre-message", () => {
  49. preMessages.append(createPreMessage({ role: "user", content: "" }));
  50. });
  51. bindClick("#save-workspace-snapshot", saveWorkspaceSnapshot);
  52. bindClick("#load-workspace-snapshot", loadWorkspaceSnapshot);
  53. bindClick("#delete-workspace-snapshot", deleteWorkspaceSnapshot);
  54. bindClick("#new-session", createSession);
  55. bindClick("#refresh-sessions", () => {
  56. loadSessions();
  57. });
  58. bindClick("#load-session", () => {
  59. loadSelectedSession();
  60. });
  61. bindClick("#open-audit-dialog", () => {
  62. auditDialog.showModal();
  63. });
  64. bindClick("#close-audit-dialog", () => {
  65. auditDialog.close();
  66. });
  67. bindClick("#open-chat-config-panel", openChatConfig);
  68. bindClick("#close-chat-config", () => {
  69. chatConfigDialog.close();
  70. });
  71. bindClick("#open-event-config-panel", openEventConfig);
  72. bindClick("#close-event-config", () => {
  73. eventConfigDialog.close();
  74. });
  75. bindClick("#select-all-tools", () => {
  76. setAllTools(true);
  77. });
  78. bindClick("#clear-tools", () => {
  79. setAllTools(false);
  80. });
  81. toolInvocationMode.addEventListener("change", updateToolModeHelp);
  82. chatForm.addEventListener("submit", (event) => {
  83. event.preventDefault();
  84. runDebugSession();
  85. });
  86. initializePromptList();
  87. updateToolModeHelp();
  88. refreshWorkspaceSnapshotSelector();
  89. loadTools();
  90. loadSessions({ preserveStatus: true });
  91. renderAuditReplay([]);
  92. renderSessionUsage({ calls: [], turns: [], session: {} });
  93. function openChatConfig() {
  94. chatConfigDialog.showModal();
  95. }
  96. function openEventConfig() {
  97. eventConfigDialog.showModal();
  98. }
  99. function bindClick(selector, handler) {
  100. const element = document.querySelector(selector);
  101. if (element) {
  102. element.addEventListener("click", handler);
  103. }
  104. }
  105. async function loadTools() {
  106. try {
  107. const response = await fetch("/api/tools");
  108. if (!response.ok) {
  109. throw new Error(`Tool API failed: ${response.status}`);
  110. }
  111. renderTools(await response.json());
  112. } catch (error) {
  113. toolList.textContent = "Failed to load tools";
  114. }
  115. }
  116. async function loadSessions(options = {}) {
  117. if (!options.preserveStatus) {
  118. setSessionStatus("Refreshing sessions");
  119. }
  120. try {
  121. sessions = await fetchJson("/api/sessions");
  122. renderSessions();
  123. if (!options.preserveStatus) {
  124. setSessionStatus("Sessions refreshed");
  125. }
  126. } catch (error) {
  127. if (!options.preserveStatus) {
  128. setSessionStatus("Failed to load sessions");
  129. }
  130. }
  131. }
  132. function renderSessions() {
  133. const previous = currentSessionId || sessionList.value;
  134. sessionList.textContent = "";
  135. const placeholder = document.createElement("option");
  136. placeholder.value = "";
  137. placeholder.textContent = sessions.length ? "Select a session" : "No saved sessions";
  138. sessionList.append(placeholder);
  139. sessions.forEach((session) => {
  140. const option = document.createElement("option");
  141. option.value = session.id;
  142. option.textContent = `${session.title} (${session.turn_count} turns, ${session.total_tokens} tokens)`;
  143. sessionList.append(option);
  144. });
  145. if (previous && sessions.some((session) => session.id === previous)) {
  146. sessionList.value = previous;
  147. }
  148. }
  149. async function createSession() {
  150. if (hasActiveTurn()) {
  151. setSessionStatus("Finish current turn before switching sessions");
  152. return;
  153. }
  154. closeSocket();
  155. setSessionStatus("Creating session");
  156. try {
  157. const title = sessionTitle.value.trim() || "New session";
  158. const session = await fetchJson("/api/sessions", {
  159. method: "POST",
  160. headers: { "content-type": "application/json" },
  161. body: JSON.stringify({
  162. title,
  163. config: buildWorkspaceSnapshot(),
  164. }),
  165. });
  166. currentSessionId = session.id;
  167. sessionTitle.value = session.title;
  168. await loadSessions({ preserveStatus: true });
  169. await loadSessionReplay(session.id, { preserveStatus: true });
  170. setSessionStatus("Session created");
  171. } catch (error) {
  172. setSessionStatus("Failed to create session");
  173. }
  174. }
  175. async function loadSelectedSession() {
  176. if (hasActiveTurn()) {
  177. setSessionStatus("Finish current turn before switching sessions");
  178. return;
  179. }
  180. const sessionId = sessionList.value;
  181. if (!sessionId) {
  182. setSessionStatus("Select a session");
  183. return;
  184. }
  185. closeSocket();
  186. setSessionStatus("Loading session");
  187. currentSessionId = sessionId;
  188. await loadSessionReplay(sessionId);
  189. }
  190. async function loadSessionReplay(sessionId, options = {}) {
  191. const loadToken = ++sessionReplayLoadToken;
  192. const isCurrentLoad = () => (
  193. loadToken === sessionReplayLoadToken && sessionId === currentSessionId
  194. );
  195. try {
  196. const [session, messages, audit, usage] = await Promise.all([
  197. fetchJson(`/api/sessions/${sessionId}`),
  198. fetchJson(`/api/sessions/${sessionId}/messages`),
  199. fetchJson(`/api/sessions/${sessionId}/audit`),
  200. fetchJson(`/api/sessions/${sessionId}/usage`),
  201. ]);
  202. if (!isCurrentLoad()) {
  203. return;
  204. }
  205. restoreWorkspaceSnapshot(session.config || {});
  206. renderReplayMessages(messages);
  207. renderAuditReplay(audit);
  208. renderSessionUsage(usage);
  209. sessionTitle.value = session.title;
  210. sessionList.value = sessionId;
  211. if (!options.preserveStatus && !isSocketOpen()) {
  212. setSessionStatus("Session loaded");
  213. }
  214. } catch (error) {
  215. if (!isCurrentLoad()) {
  216. return;
  217. }
  218. if (!options.preserveStatus) {
  219. setSessionStatus("Failed to load session");
  220. }
  221. }
  222. }
  223. function setSessionStatus(message) {
  224. sessionStatus.textContent = message;
  225. statusEl.textContent = message;
  226. }
  227. async function fetchJson(url, options) {
  228. const response = await fetch(url, options);
  229. if (!response.ok) {
  230. throw new Error(`Request failed: ${response.status}`);
  231. }
  232. return response.json();
  233. }
  234. function renderTools(tools) {
  235. availableTools = tools;
  236. toolList.textContent = "";
  237. if (!tools.length) {
  238. toolList.textContent = "No tools available";
  239. updateToolCount();
  240. updateEventPromptItem(tools);
  241. return;
  242. }
  243. tools.forEach((tool) => {
  244. const item = document.createElement("div");
  245. item.className = "tool-card";
  246. const label = document.createElement("label");
  247. label.className = "checkbox";
  248. label.title = tool.description || tool.name;
  249. const checkbox = document.createElement("input");
  250. checkbox.type = "checkbox";
  251. checkbox.className = "tool-toggle";
  252. checkbox.value = tool.name;
  253. checkbox.checked = true;
  254. checkbox.addEventListener("change", () => {
  255. updateToolCount();
  256. updateEventPromptItem(availableTools);
  257. });
  258. label.append(checkbox, ` ${tool.name}`);
  259. const description = document.createElement("div");
  260. description.className = "tool-description";
  261. description.textContent = tool.description || "No description";
  262. const required = document.createElement("div");
  263. required.className = "tool-required";
  264. required.textContent = formatRequiredParameters(tool);
  265. item.append(label, description, required);
  266. toolList.append(item);
  267. });
  268. if (pendingEnabledTools) {
  269. applySelectedTools(pendingEnabledTools);
  270. pendingEnabledTools = null;
  271. }
  272. updateToolCount();
  273. updateEventPromptItem(tools);
  274. }
  275. function formatRequiredParameters(tool) {
  276. const required = tool.parameters && Array.isArray(tool.parameters.required)
  277. ? tool.parameters.required
  278. : [];
  279. return required.length ? `Required: ${required.join(", ")}` : "No required parameters";
  280. }
  281. function setAllTools(checked) {
  282. toolList.querySelectorAll(".tool-toggle").forEach((checkbox) => {
  283. checkbox.checked = checked;
  284. });
  285. updateToolCount();
  286. updateEventPromptItem(availableTools);
  287. }
  288. function updateToolCount() {
  289. const selected = selectedTools().length;
  290. const total = toolList.querySelectorAll(".tool-toggle").length;
  291. toolCount.textContent = `${selected}/${total} selected`;
  292. }
  293. function saveWorkspaceSnapshot() {
  294. const name = snapshotName.value.trim();
  295. if (!name) {
  296. statusEl.textContent = "Snapshot name required";
  297. return;
  298. }
  299. const store = readWorkspaceSnapshots();
  300. store.snapshots[name] = buildWorkspaceSnapshot();
  301. writeWorkspaceSnapshots(store);
  302. refreshWorkspaceSnapshotSelector(name);
  303. statusEl.textContent = "Workspace snapshot saved";
  304. }
  305. function loadWorkspaceSnapshot() {
  306. const name = savedSnapshots.value;
  307. if (!name) {
  308. statusEl.textContent = "Select a snapshot";
  309. return;
  310. }
  311. const snapshot = readWorkspaceSnapshots().snapshots[name];
  312. if (!snapshot) {
  313. statusEl.textContent = "Snapshot not found";
  314. refreshWorkspaceSnapshotSelector();
  315. return;
  316. }
  317. snapshotName.value = name;
  318. restoreWorkspaceSnapshot(snapshot);
  319. statusEl.textContent = "Workspace snapshot loaded";
  320. }
  321. function deleteWorkspaceSnapshot() {
  322. const name = savedSnapshots.value;
  323. if (!name) {
  324. statusEl.textContent = "Select a snapshot";
  325. return;
  326. }
  327. const store = readWorkspaceSnapshots();
  328. delete store.snapshots[name];
  329. writeWorkspaceSnapshots(store);
  330. if (snapshotName.value.trim() === name) {
  331. snapshotName.value = "";
  332. }
  333. refreshWorkspaceSnapshotSelector();
  334. statusEl.textContent = "Workspace snapshot deleted";
  335. }
  336. function buildWorkspaceSnapshot() {
  337. return {
  338. tool_invocation_mode: document.querySelector("#tool-invocation-mode").value,
  339. prompt_items: buildPromptItems(),
  340. system_prompts: buildCustomPromptItems()
  341. .map((item) => item.content)
  342. .filter(Boolean),
  343. pre_messages: [...document.querySelectorAll(".pre-message")]
  344. .map((row) => ({
  345. role: row.querySelector(".pre-role").value,
  346. content: row.querySelector(".pre-content").value.trim(),
  347. }))
  348. .filter((message) => message.content),
  349. chat_agent: {
  350. model: document.querySelector("#model").value.trim(),
  351. temperature: Number(document.querySelector("#temperature").value),
  352. max_tokens: Number(document.querySelector("#max-tokens").value),
  353. system_prompt: "",
  354. extra_body: buildExtraBody("chat"),
  355. },
  356. event_agent: {
  357. model: document.querySelector("#event-model").value.trim(),
  358. temperature: Number(document.querySelector("#event-temperature").value),
  359. max_tokens: Number(document.querySelector("#event-max-tokens").value),
  360. system_prompt: document.querySelector("#event-system-prompt").value.trim(),
  361. extra_body: buildExtraBody("event"),
  362. enabled_tools: selectedTools(),
  363. max_event_loops: Number(document.querySelector("#max-event-loops").value),
  364. max_parallel_events: Number(document.querySelector("#max-parallel-events").value),
  365. batch_timeout_seconds: Number(document.querySelector("#batch-timeout-seconds").value),
  366. },
  367. };
  368. }
  369. function restoreWorkspaceSnapshot(snapshot) {
  370. document.querySelector("#tool-invocation-mode").value =
  371. snapshot.tool_invocation_mode || "dual_agent";
  372. restoreSystemPrompts(snapshot.prompt_items || snapshot.system_prompts || []);
  373. restorePreMessages(snapshot.pre_messages || []);
  374. const chatAgent = snapshot.chat_agent || {};
  375. document.querySelector("#model").value = chatAgent.model || "";
  376. document.querySelector("#temperature").value = chatAgent.temperature ?? 0.2;
  377. document.querySelector("#max-tokens").value = chatAgent.max_tokens ?? 800;
  378. restoreExtraBody("chat", chatAgent.extra_body);
  379. const eventAgent = snapshot.event_agent || {};
  380. document.querySelector("#event-model").value = eventAgent.model || "";
  381. document.querySelector("#event-temperature").value = eventAgent.temperature ?? 0.2;
  382. document.querySelector("#event-max-tokens").value = eventAgent.max_tokens ?? 800;
  383. document.querySelector("#event-system-prompt").value = eventAgent.system_prompt || "";
  384. restoreExtraBody("event", eventAgent.extra_body);
  385. document.querySelector("#max-event-loops").value = eventAgent.max_event_loops ?? 1;
  386. document.querySelector("#max-parallel-events").value =
  387. eventAgent.max_parallel_events ?? 4;
  388. document.querySelector("#batch-timeout-seconds").value =
  389. eventAgent.batch_timeout_seconds ?? 15;
  390. const enabledTools = eventAgent.enabled_tools || [];
  391. pendingEnabledTools = applySelectedTools(enabledTools) ? null : enabledTools;
  392. updateEventPromptItem(availableTools);
  393. updateToolModeHelp();
  394. }
  395. function updateToolModeHelp() {
  396. if (toolInvocationMode.value === "chat_agent_tools") {
  397. toolModeHelp.textContent =
  398. "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.";
  399. return;
  400. }
  401. toolModeHelp.textContent =
  402. "Dual Agent may use the EventAgent model, system prompt, and extra body for missing-argument fallback.";
  403. }
  404. function buildExtraBody(prefix) {
  405. const thinkingDisabled = document.querySelector(`#${prefix}-thinking-disabled`).checked;
  406. const enableSearch = document.querySelector(`#${prefix}-enable-search`).checked;
  407. const forcedSearch = document.querySelector(`#${prefix}-forced-search`).checked;
  408. const extraBody = {
  409. enable_search: enableSearch,
  410. search_options: {
  411. forced_search: forcedSearch,
  412. },
  413. };
  414. if (thinkingDisabled) {
  415. extraBody.thinking = { type: "disabled" };
  416. }
  417. return extraBody;
  418. }
  419. function restoreExtraBody(prefix, extraBody = {}) {
  420. document.querySelector(`#${prefix}-thinking-disabled`).checked =
  421. !extraBody.thinking || extraBody.thinking.type === "disabled";
  422. document.querySelector(`#${prefix}-enable-search`).checked =
  423. Boolean(extraBody.enable_search);
  424. document.querySelector(`#${prefix}-forced-search`).checked =
  425. Boolean(extraBody.search_options && extraBody.search_options.forced_search);
  426. }
  427. function restoreSystemPrompts(prompts) {
  428. systemPrompts.textContent = "";
  429. const promptItems = normalizePromptItems(prompts);
  430. promptItems.forEach((prompt) => {
  431. systemPrompts.append(createSystemPrompt(prompt));
  432. });
  433. ensureEventPromptItem();
  434. }
  435. function restorePreMessages(messages) {
  436. preMessages.textContent = "";
  437. messages.forEach((message) => {
  438. preMessages.append(createPreMessage(message));
  439. });
  440. }
  441. function createSystemPrompt(prompt) {
  442. const promptItem = normalizePromptItem(prompt);
  443. const item = document.createElement("div");
  444. item.className = promptItem.prompt_kind === "event_agent_system"
  445. ? "prompt-item event-prompt"
  446. : "prompt-item";
  447. item.dataset.promptKind = promptItem.prompt_kind;
  448. item.dataset.promptRole = promptItem.role;
  449. item.draggable = true;
  450. const head = document.createElement("div");
  451. head.className = "prompt-item-head";
  452. const meta = document.createElement("div");
  453. meta.className = "prompt-meta";
  454. const type = document.createElement("span");
  455. type.className = "prompt-type";
  456. type.textContent = `type: ${promptItem.role}`;
  457. const title = document.createElement("span");
  458. title.className = "prompt-title";
  459. title.textContent = promptItem.prompt_kind === "event_agent_system"
  460. ? "EventAgent Rules"
  461. : "Custom";
  462. meta.append(type, title);
  463. const actions = document.createElement("div");
  464. actions.className = "prompt-actions";
  465. const moveUp = document.createElement("button");
  466. moveUp.className = "move-prompt-up";
  467. moveUp.type = "button";
  468. moveUp.textContent = "Up";
  469. moveUp.addEventListener("click", () => movePromptItem(item, -1));
  470. const moveDown = document.createElement("button");
  471. moveDown.className = "move-prompt-down";
  472. moveDown.type = "button";
  473. moveDown.textContent = "Down";
  474. moveDown.addEventListener("click", () => movePromptItem(item, 1));
  475. actions.append(moveUp, moveDown);
  476. if (promptItem.prompt_kind !== "event_agent_system") {
  477. const deleteButton = document.createElement("button");
  478. deleteButton.className = "delete-system-prompt";
  479. deleteButton.type = "button";
  480. deleteButton.textContent = "Delete";
  481. deleteButton.addEventListener("click", () => item.remove());
  482. actions.append(deleteButton);
  483. }
  484. head.append(meta, actions);
  485. const textarea = document.createElement("textarea");
  486. textarea.className = "system-prompt";
  487. textarea.rows = promptItem.prompt_kind === "event_agent_system" ? 7 : 3;
  488. textarea.value = promptItem.content;
  489. textarea.readOnly = promptItem.prompt_kind === "event_agent_system";
  490. item.append(head, textarea);
  491. bindPromptDrag(item);
  492. return item;
  493. }
  494. function createPreMessage(message) {
  495. const fragment = preMessageTemplate.content.cloneNode(true);
  496. fragment.querySelector(".pre-role").value = message.role || "user";
  497. fragment.querySelector(".pre-content").value = message.content || "";
  498. return fragment;
  499. }
  500. function applySelectedTools(toolNames) {
  501. const checkboxes = [...toolList.querySelectorAll(".tool-toggle")];
  502. if (!checkboxes.length) {
  503. return false;
  504. }
  505. const selected = new Set(toolNames);
  506. checkboxes.forEach((checkbox) => {
  507. checkbox.checked = selected.has(checkbox.value);
  508. });
  509. updateToolCount();
  510. updateEventPromptItem(availableTools);
  511. return true;
  512. }
  513. function initializePromptList() {
  514. [...systemPrompts.querySelectorAll(".prompt-item")].forEach((item) => {
  515. bindPromptItemControls(item);
  516. bindPromptDrag(item);
  517. });
  518. ensureEventPromptItem();
  519. }
  520. function buildPromptItems() {
  521. return [...systemPrompts.querySelectorAll(".prompt-item")]
  522. .map((item) => ({
  523. prompt_kind: item.dataset.promptKind || "custom",
  524. role: item.dataset.promptRole || "system",
  525. content: item.querySelector(".system-prompt").value.trim(),
  526. }))
  527. .filter((item) => item.prompt_kind === "event_agent_system" || item.content);
  528. }
  529. function buildCustomPromptItems() {
  530. return buildPromptItems().filter((item) => item.prompt_kind !== "event_agent_system");
  531. }
  532. function collectSystemPrompts() {
  533. return buildPromptItems()
  534. .map((item) => item.content)
  535. .filter(Boolean);
  536. }
  537. function normalizePromptItems(prompts) {
  538. const items = Array.isArray(prompts) ? prompts : [];
  539. const normalized = items
  540. .map((prompt) => normalizePromptItem(prompt))
  541. .filter((prompt) => prompt.prompt_kind === "event_agent_system" || prompt.content);
  542. if (!normalized.some((prompt) => prompt.prompt_kind === "event_agent_system")) {
  543. normalized.push({ prompt_kind: "event_agent_system", content: "" });
  544. }
  545. if (!normalized.some((prompt) => prompt.prompt_kind === "custom")) {
  546. normalized.unshift({ prompt_kind: "custom", role: "system", content: "" });
  547. }
  548. return normalized;
  549. }
  550. function normalizePromptItem(prompt) {
  551. if (prompt && typeof prompt === "object" && !Array.isArray(prompt)) {
  552. return {
  553. prompt_kind: prompt.prompt_kind === "event_agent_system"
  554. ? "event_agent_system"
  555. : "custom",
  556. role: "system",
  557. content: prompt.content || "",
  558. };
  559. }
  560. return {
  561. prompt_kind: "custom",
  562. role: "system",
  563. content: prompt || "",
  564. };
  565. }
  566. function buildAvailableEventsPrompt(tools) {
  567. const eventLines = tools.map((tool) => `- ${tool.name}: ${tool.description || "No description"}`);
  568. if (!eventLines.length) {
  569. return "";
  570. }
  571. return [
  572. "You may request EventAgent work using this text protocol.",
  573. "Available events:",
  574. ...eventLines,
  575. "First write the user-facing reply normally. If events are needed, append this block after the visible reply:",
  576. "<agent_events>",
  577. "event_name",
  578. "</agent_events>",
  579. "Use exact event names only, one per line. Do not include parameters.",
  580. ].join("\n");
  581. }
  582. function updateEventPromptItem(tools) {
  583. const eventPrompt = ensureEventPromptItem();
  584. const selected = new Set(selectedTools());
  585. const prompt = buildAvailableEventsPrompt(
  586. tools.filter((tool) => selected.has(tool.name)),
  587. );
  588. eventPrompt.querySelector(".system-prompt").value = prompt;
  589. }
  590. function ensureEventPromptItem() {
  591. const existing = systemPrompts.querySelector('[data-prompt-kind="event_agent_system"]');
  592. if (existing) {
  593. return existing;
  594. }
  595. const item = createSystemPrompt({
  596. prompt_kind: "event_agent_system",
  597. content: "",
  598. });
  599. systemPrompts.append(item);
  600. return item;
  601. }
  602. function bindPromptItemControls(item) {
  603. const moveUp = item.querySelector(".move-prompt-up");
  604. if (moveUp) {
  605. moveUp.addEventListener("click", () => movePromptItem(item, -1));
  606. }
  607. const moveDown = item.querySelector(".move-prompt-down");
  608. if (moveDown) {
  609. moveDown.addEventListener("click", () => movePromptItem(item, 1));
  610. }
  611. const deleteButton = item.querySelector(".delete-system-prompt");
  612. if (deleteButton) {
  613. deleteButton.addEventListener("click", () => item.remove());
  614. }
  615. }
  616. function movePromptItem(item, direction) {
  617. if (direction < 0 && item.previousElementSibling) {
  618. systemPrompts.insertBefore(item, item.previousElementSibling);
  619. }
  620. if (direction > 0 && item.nextElementSibling) {
  621. systemPrompts.insertBefore(item.nextElementSibling, item);
  622. }
  623. }
  624. function bindPromptDrag(item) {
  625. item.addEventListener("dragstart", () => {
  626. item.classList.add("dragging");
  627. });
  628. item.addEventListener("dragend", () => {
  629. item.classList.remove("dragging");
  630. });
  631. item.addEventListener("dragover", (event) => {
  632. event.preventDefault();
  633. const dragging = systemPrompts.querySelector(".prompt-item.dragging");
  634. if (!dragging || dragging === item) {
  635. return;
  636. }
  637. const rect = item.getBoundingClientRect();
  638. const afterMidpoint = event.clientY > rect.top + rect.height / 2;
  639. systemPrompts.insertBefore(dragging, afterMidpoint ? item.nextSibling : item);
  640. });
  641. }
  642. function refreshWorkspaceSnapshotSelector(selectedName = "") {
  643. const previousName = selectedName || savedSnapshots.value;
  644. const names = Object.keys(readWorkspaceSnapshots().snapshots).sort();
  645. savedSnapshots.textContent = "";
  646. const placeholder = document.createElement("option");
  647. placeholder.value = "";
  648. placeholder.textContent = names.length ? "Select a snapshot" : "No saved snapshots";
  649. savedSnapshots.append(placeholder);
  650. names.forEach((name) => {
  651. const option = document.createElement("option");
  652. option.value = name;
  653. option.textContent = name;
  654. savedSnapshots.append(option);
  655. });
  656. if (names.includes(previousName)) {
  657. savedSnapshots.value = previousName;
  658. }
  659. }
  660. function readWorkspaceSnapshots() {
  661. try {
  662. const rawValue = localStorage.getItem(WORKSPACE_SNAPSHOTS_STORAGE_KEY);
  663. if (!rawValue) {
  664. return readLegacyPromptSets();
  665. }
  666. const parsed = JSON.parse(rawValue);
  667. if (
  668. !parsed ||
  669. typeof parsed !== "object" ||
  670. !parsed.snapshots ||
  671. typeof parsed.snapshots !== "object" ||
  672. Array.isArray(parsed.snapshots)
  673. ) {
  674. return { snapshots: {} };
  675. }
  676. return parsed;
  677. } catch {
  678. return { snapshots: {} };
  679. }
  680. }
  681. function readLegacyPromptSets() {
  682. try {
  683. const rawValue = localStorage.getItem(LEGACY_PROMPT_SETS_STORAGE_KEY);
  684. if (!rawValue) {
  685. return { snapshots: {} };
  686. }
  687. const parsed = JSON.parse(rawValue);
  688. if (
  689. !parsed ||
  690. typeof parsed !== "object" ||
  691. !parsed.sets ||
  692. typeof parsed.sets !== "object" ||
  693. Array.isArray(parsed.sets)
  694. ) {
  695. return { snapshots: {} };
  696. }
  697. return { snapshots: parsed.sets };
  698. } catch {
  699. return { snapshots: {} };
  700. }
  701. }
  702. function writeWorkspaceSnapshots(store) {
  703. localStorage.setItem(WORKSPACE_SNAPSHOTS_STORAGE_KEY, JSON.stringify(store));
  704. }
  705. function runDebugSession() {
  706. const submittedMessage = userMessage.value.trim();
  707. if (!submittedMessage) {
  708. statusEl.textContent = "Message required";
  709. return;
  710. }
  711. if (isSocketOpen()) {
  712. beginTurn(submittedMessage, "Waiting for first token");
  713. socket.send(JSON.stringify({ type: "user_message", content: submittedMessage }));
  714. userMessage.value = "";
  715. return;
  716. }
  717. closeSocket();
  718. resetRun();
  719. beginTurn(submittedMessage, "Connecting");
  720. socket = new WebSocket(wsUrl());
  721. const activeSocket = socket;
  722. activeSocket.addEventListener("open", () => {
  723. if (socket !== activeSocket) {
  724. return;
  725. }
  726. statusEl.textContent = "Waiting for first token";
  727. activeSocket.send(JSON.stringify(buildRequest(submittedMessage)));
  728. userMessage.value = "";
  729. });
  730. activeSocket.addEventListener("message", (event) => {
  731. if (socket !== activeSocket) {
  732. return;
  733. }
  734. handleServerMessage(JSON.parse(event.data));
  735. });
  736. activeSocket.addEventListener("close", () => {
  737. if (socket !== activeSocket) {
  738. return;
  739. }
  740. socket = null;
  741. statusEl.textContent = "Idle";
  742. stopElapsedTimer();
  743. setRunState(false);
  744. updateElapsed();
  745. });
  746. activeSocket.addEventListener("error", () => {
  747. if (socket !== activeSocket) {
  748. return;
  749. }
  750. appendLog("error", "WebSocket connection failed");
  751. });
  752. }
  753. function isSocketOpen() {
  754. return socket && socket.readyState === WebSocket.OPEN;
  755. }
  756. function hasActiveTurn() {
  757. return turnActive;
  758. }
  759. function beginTurn(submittedMessage, statusText) {
  760. activeAssistant = null;
  761. firstTokenAt = 0;
  762. hasBackendRoundStats = false;
  763. resetTurnStats();
  764. appendLog("user", submittedMessage);
  765. setRunState(true);
  766. runStartedAt = performance.now();
  767. startElapsedTimer();
  768. statusEl.textContent = statusText;
  769. }
  770. function buildRequest(submittedMessage = userMessage.value) {
  771. return {
  772. session_id: currentSessionId || undefined,
  773. user_message: submittedMessage,
  774. tool_invocation_mode: document.querySelector("#tool-invocation-mode").value,
  775. system_prompts: collectSystemPrompts(),
  776. pre_messages: [...document.querySelectorAll(".pre-message")]
  777. .map((row) => ({
  778. role: row.querySelector(".pre-role").value,
  779. content: row.querySelector(".pre-content").value.trim(),
  780. }))
  781. .filter((message) => message.content),
  782. chat_agent: {
  783. model: document.querySelector("#model").value.trim(),
  784. temperature: Number(document.querySelector("#temperature").value),
  785. max_tokens: Number(document.querySelector("#max-tokens").value),
  786. system_prompt: "",
  787. extra_body: buildExtraBody("chat"),
  788. },
  789. event_agent: {
  790. model: document.querySelector("#event-model").value.trim(),
  791. temperature: Number(document.querySelector("#event-temperature").value),
  792. max_tokens: Number(document.querySelector("#event-max-tokens").value),
  793. system_prompt: document.querySelector("#event-system-prompt").value.trim(),
  794. extra_body: buildExtraBody("event"),
  795. enabled_tools: selectedTools(),
  796. max_event_loops: Number(document.querySelector("#max-event-loops").value),
  797. max_parallel_events: Number(document.querySelector("#max-parallel-events").value),
  798. batch_timeout_seconds: Number(document.querySelector("#batch-timeout-seconds").value),
  799. },
  800. };
  801. }
  802. function selectedTools() {
  803. return [...toolList.querySelectorAll(".tool-toggle:checked")].map(
  804. (input) => input.value,
  805. );
  806. }
  807. function handleServerMessage(message) {
  808. if (message.type === "session_started") {
  809. if (message.session_id) {
  810. currentSessionId = message.session_id;
  811. loadSessions({ preserveStatus: true });
  812. }
  813. appendLog("session", "Session started");
  814. return;
  815. }
  816. if (message.type === "turn_started") {
  817. statusEl.textContent = "Waiting for first token";
  818. return;
  819. }
  820. if (message.type === "message_delta") {
  821. appendAssistantDelta(message.content || "");
  822. return;
  823. }
  824. if (message.type === "usage") {
  825. updateUsage(message.usage || {});
  826. return;
  827. }
  828. if (message.type === "round_stats") {
  829. updateRoundStats(message);
  830. return;
  831. }
  832. if (message.type === "audit") {
  833. appendAuditEntry(message);
  834. return;
  835. }
  836. if (message.type === "event") {
  837. appendLog("event", message.event.name);
  838. return;
  839. }
  840. if (message.type === "tool_result") {
  841. appendLog("tool", message.message.content);
  842. return;
  843. }
  844. if (message.type === "error") {
  845. appendLog("error", message.message);
  846. stopElapsedTimer();
  847. setRunState(false);
  848. return;
  849. }
  850. if (message.type === "turn_completed") {
  851. appendLog("session", `Turn ${message.turn_index || ""} completed`.trim());
  852. statusEl.textContent = "Idle";
  853. stopElapsedTimer();
  854. setRunState(false);
  855. updateElapsed();
  856. if (currentSessionId) {
  857. loadSessions({ preserveStatus: true });
  858. loadSessionReplay(currentSessionId, { preserveStatus: true });
  859. }
  860. return;
  861. }
  862. if (message.type === "done") {
  863. appendLog("session", "Done");
  864. statusEl.textContent = "Idle";
  865. stopElapsedTimer();
  866. setRunState(false);
  867. if (currentSessionId) {
  868. loadSessions({ preserveStatus: true });
  869. loadSessionReplay(currentSessionId, { preserveStatus: true });
  870. }
  871. closeSocket();
  872. }
  873. }
  874. function appendAssistantDelta(content) {
  875. if (!firstTokenAt) {
  876. firstTokenAt = performance.now();
  877. statusEl.textContent = "Streaming";
  878. if (!hasBackendRoundStats) {
  879. document.querySelector("#stat-ttft").textContent = `${Math.round(firstTokenAt - runStartedAt)}ms`;
  880. }
  881. }
  882. if (!activeAssistant) {
  883. activeAssistant = document.createElement("div");
  884. activeAssistant.className = "message assistant";
  885. messagesEl.append(activeAssistant);
  886. }
  887. activeAssistant.textContent += content;
  888. updateElapsed();
  889. messagesEl.scrollTop = messagesEl.scrollHeight;
  890. }
  891. function appendLog(kind, content) {
  892. const item = document.createElement("div");
  893. item.className = `message ${kind}`;
  894. item.textContent = content;
  895. messagesEl.append(item);
  896. messagesEl.scrollTop = messagesEl.scrollHeight;
  897. }
  898. function renderReplayMessages(messages) {
  899. messagesEl.textContent = "";
  900. activeAssistant = null;
  901. if (!messages.length) {
  902. appendLog("session", "No messages yet");
  903. return;
  904. }
  905. messages.forEach((message) => {
  906. if (
  907. message.role === "assistant" &&
  908. !message.content.trim() &&
  909. Array.isArray(message.tool_calls) &&
  910. message.tool_calls.length > 0
  911. ) {
  912. return;
  913. }
  914. const item = document.createElement("div");
  915. const kind = ["user", "assistant", "tool"].includes(message.role)
  916. ? message.role
  917. : "session";
  918. item.className = `message ${kind}`;
  919. item.textContent = message.content;
  920. messagesEl.append(item);
  921. });
  922. messagesEl.scrollTop = messagesEl.scrollHeight;
  923. }
  924. function appendAuditEntry(message) {
  925. const entry = normalizeAuditEntry(message);
  926. renderAuditReplay([...auditEntries, entry], {
  927. selectedKey: auditEntryKey(entry),
  928. scrollToEnd: true,
  929. });
  930. }
  931. function normalizeAuditEntry(entry) {
  932. const details = entry.details || {};
  933. return {
  934. id: entry.id || `live-${++liveAuditSequence}`,
  935. event: entry.event || "unknown_event",
  936. details,
  937. turn_index: entry.turn_index ?? details.turn_index ?? null,
  938. round_index: entry.round_index ?? details.round_index ?? null,
  939. turn_elapsed_ms: entry.turn_elapsed_ms ?? details.turn_elapsed_ms ?? null,
  940. created_at: entry.created_at || new Date().toISOString(),
  941. };
  942. }
  943. function auditEntryKey(entry) {
  944. return String(entry.id);
  945. }
  946. function renderAuditReplay(entries, options = {}) {
  947. auditEntries = entries.map(normalizeAuditEntry);
  948. auditReplay.textContent = "";
  949. auditCount.textContent = `${auditEntries.length} events`;
  950. if (!auditEntries.length) {
  951. selectedAuditKey = null;
  952. auditSummaryEl.textContent = "No audit entries";
  953. auditReplay.append(emptyReplayRow("No audit entries"));
  954. renderAuditDetail(null);
  955. return;
  956. }
  957. const keys = auditEntries.map(auditEntryKey);
  958. selectedAuditKey = options.selectedKey || selectedAuditKey;
  959. if (!selectedAuditKey || !keys.includes(selectedAuditKey)) {
  960. selectedAuditKey = keys[keys.length - 1];
  961. }
  962. auditEntries.forEach((entry) => {
  963. const key = auditEntryKey(entry);
  964. const row = document.createElement("button");
  965. row.type = "button";
  966. row.className = `audit-event ${auditKindClass(entry.event)}`;
  967. row.dataset.auditKey = key;
  968. if (key === selectedAuditKey) {
  969. row.classList.add("is-selected");
  970. }
  971. row.addEventListener("click", () => {
  972. selectAuditEntry(key);
  973. });
  974. const marker = document.createElement("span");
  975. marker.className = "audit-marker";
  976. const body = document.createElement("span");
  977. body.className = "audit-event-body";
  978. const meta = document.createElement("span");
  979. meta.className = "audit-event-meta";
  980. meta.textContent = auditMeta(entry);
  981. const title = document.createElement("strong");
  982. title.textContent = auditTitle(entry.event);
  983. const summary = document.createElement("span");
  984. summary.className = "audit-event-summary";
  985. summary.textContent = auditSummary(entry);
  986. body.append(meta, title, summary);
  987. row.append(marker, body);
  988. auditReplay.append(row);
  989. });
  990. renderAuditDetail(
  991. auditEntries.find((entry) => auditEntryKey(entry) === selectedAuditKey) || null,
  992. );
  993. const selectedEntry = auditEntries.find(
  994. (entry) => auditEntryKey(entry) === selectedAuditKey,
  995. );
  996. auditSummaryEl.textContent = selectedEntry
  997. ? `${auditTitle(selectedEntry.event)} · ${auditMeta(selectedEntry)}`
  998. : `${auditEntries.length} audit events`;
  999. if (options.scrollToEnd) {
  1000. auditReplay.scrollTop = auditReplay.scrollHeight;
  1001. }
  1002. }
  1003. function selectAuditEntry(key) {
  1004. selectedAuditKey = key;
  1005. renderAuditReplay(auditEntries);
  1006. }
  1007. function renderAuditDetail(entry) {
  1008. auditDetail.textContent = "";
  1009. if (!entry) {
  1010. auditDetail.append(emptyReplayRow("Select an audit event to inspect details"));
  1011. return;
  1012. }
  1013. const header = document.createElement("div");
  1014. header.className = "audit-detail-header";
  1015. const title = document.createElement("h3");
  1016. title.className = "audit-detail-title";
  1017. title.textContent = auditTitle(entry.event);
  1018. const meta = document.createElement("dl");
  1019. meta.className = "audit-detail-meta";
  1020. appendMetaField(meta, "Turn / Round", auditTurnRound(entry));
  1021. appendMetaField(meta, "Since User Message", auditRelativeTime(entry));
  1022. appendMetaField(meta, "Time", formatTime(entry.created_at) || "-");
  1023. appendMetaField(meta, "Agent", auditKindLabel(entry.event));
  1024. header.append(title, meta);
  1025. auditDetail.append(header);
  1026. const details = entry.details || {};
  1027. if (entry.event === "chat_agent_request" || entry.event === "event_agent_request") {
  1028. appendKeyValueSection(auditDetail, "Model Parameters", details.params);
  1029. appendMessagesSection(auditDetail, details.messages ? "Messages" : "History", details.messages || details.history);
  1030. appendEventsSection(auditDetail, details.events);
  1031. appendToolsSection(auditDetail, details.tools);
  1032. appendKeyValueSection(auditDetail, "Extra Body", details.extra_body);
  1033. } else if (entry.event === "chat_agent_response") {
  1034. appendTextSection(auditDetail, "Assistant Text", details.content);
  1035. appendListSection(auditDetail, "Events", details.event_names);
  1036. appendKeyValueSection(auditDetail, "Usage", details.usage);
  1037. appendRawChunksSection(auditDetail, "Raw LLM Chunks", details.raw_chunks);
  1038. } else if (entry.event === "event_agent_response") {
  1039. appendRepliesSection(auditDetail, details.replies);
  1040. appendRawModelChunksSection(auditDetail, details.raw_model_chunks);
  1041. } else if (entry.event === "chat_event_detected") {
  1042. appendKeyValueSection(auditDetail, "Detected Event", {
  1043. name: details.event_name,
  1044. id: details.event_id,
  1045. });
  1046. } else if (entry.event === "event_agent_completed") {
  1047. appendListSection(auditDetail, "Completed Events", details.event_names);
  1048. appendKeyValueSection(auditDetail, "Results", {
  1049. result_count: details.result_count,
  1050. });
  1051. appendTextSection(auditDetail, "Tool Result Summary", details.result_summary);
  1052. } else if (entry.event === "chat_message_stream_started" || entry.event === "chat_message_stream_finished") {
  1053. appendKeyValueSection(auditDetail, "Message Stream", {
  1054. ttft_ms: details.ttft_ms,
  1055. delta_count: details.delta_count,
  1056. content_length: details.content_length,
  1057. });
  1058. } else if (entry.event === "chat_round_started") {
  1059. appendKeyValueSection(auditDetail, "Event State", {
  1060. event_generation_enabled: details.event_generation_enabled,
  1061. });
  1062. appendListSection(auditDetail, "Enabled This Round", details.events_enabled);
  1063. appendListSection(auditDetail, "Configured Events", details.configured_events);
  1064. } else if (entry.event === "chat_round_finished") {
  1065. appendKeyValueSection(auditDetail, "Round Result", {
  1066. had_event: details.had_event,
  1067. elapsed_ms: details.elapsed_ms,
  1068. });
  1069. } else if (entry.event === "turn_completed") {
  1070. appendKeyValueSection(auditDetail, "Turn Result", {
  1071. round_count: details.round_count,
  1072. });
  1073. } else if (details.raw_chunks) {
  1074. appendRawChunksSection(auditDetail, "Raw LLM Chunks", details.raw_chunks);
  1075. } else if (details.raw_model_chunks) {
  1076. appendRawModelChunksSection(auditDetail, details.raw_model_chunks);
  1077. } else {
  1078. appendKeyValueSection(auditDetail, "Details", details);
  1079. }
  1080. appendRawDetails(auditDetail, details);
  1081. }
  1082. function auditTitle(event) {
  1083. return event
  1084. .split("_")
  1085. .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
  1086. .join(" ");
  1087. }
  1088. function auditMeta(entry) {
  1089. const time = formatTime(entry.created_at);
  1090. const relativeTime = auditRelativeTime(entry);
  1091. return `${auditTurnRound(entry)} / ${relativeTime}${time ? ` / ${time}` : ""}`;
  1092. }
  1093. function auditTurnRound(entry) {
  1094. const turn = entry.turn_index === null || entry.turn_index === undefined ? "-" : entry.turn_index;
  1095. const round = entry.round_index === null || entry.round_index === undefined ? "-" : entry.round_index;
  1096. return `T${turn} / R${round}`;
  1097. }
  1098. function auditRelativeTime(entry) {
  1099. if (entry.turn_elapsed_ms === null || entry.turn_elapsed_ms === undefined) {
  1100. return "-";
  1101. }
  1102. return `+${formatMs(entry.turn_elapsed_ms)}`;
  1103. }
  1104. function appendMetaField(container, label, value) {
  1105. const item = document.createElement("div");
  1106. const term = document.createElement("dt");
  1107. term.textContent = label;
  1108. const description = document.createElement("dd");
  1109. description.textContent = value;
  1110. item.append(term, description);
  1111. container.append(item);
  1112. }
  1113. function auditSummary(entry) {
  1114. const details = entry.details || {};
  1115. if (entry.event === "chat_agent_request") {
  1116. return `${details.params?.model || "model default"} · ${details.messages?.length || 0} messages`;
  1117. }
  1118. if (entry.event === "event_agent_request") {
  1119. return `${details.params?.model || "model default"} · ${(details.events || []).map((event) => event.name).join(", ") || "no events"}`;
  1120. }
  1121. if (entry.event === "chat_agent_response") {
  1122. const rawCount = details.raw_chunks?.length || 0;
  1123. return `${details.usage?.total_tokens || 0} tokens · ${rawCount} raw chunks · ${(details.event_names || []).join(", ") || "no events"}`;
  1124. }
  1125. if (entry.event === "event_agent_response") {
  1126. const rawCount = (details.raw_model_chunks || []).reduce(
  1127. (count, item) => count + (item.chunks?.length || 0),
  1128. 0,
  1129. );
  1130. return `${details.replies?.length || 0} tool replies · ${rawCount} raw chunks`;
  1131. }
  1132. if (entry.event === "chat_event_detected") {
  1133. return details.event_name || "event detected";
  1134. }
  1135. if (entry.event === "event_agent_completed") {
  1136. return `${details.result_count || 0} results · ${(details.event_names || []).join(", ")}`;
  1137. }
  1138. if (entry.event === "chat_message_stream_started") {
  1139. return `visible text started · TTFT ${formatMs(details.ttft_ms)}`;
  1140. }
  1141. if (entry.event === "chat_message_stream_finished") {
  1142. return `${details.delta_count || 0} deltas · ${details.content_length || 0} chars`;
  1143. }
  1144. if (entry.event === "chat_round_started") {
  1145. const state = details.event_generation_enabled ? "event generation on" : "event generation off";
  1146. return `${details.events_enabled?.length || 0} enabled this round · ${state}`;
  1147. }
  1148. if (entry.event === "chat_round_finished") {
  1149. return `${formatMs(details.elapsed_ms)} · ${details.had_event ? "event" : "no event"}`;
  1150. }
  1151. if (entry.event === "turn_completed") {
  1152. return `${details.round_count || 0} rounds`;
  1153. }
  1154. return JSON.stringify(details).slice(0, 120);
  1155. }
  1156. function auditKindClass(event) {
  1157. if (event.includes("request")) {
  1158. return "audit-kind-request";
  1159. }
  1160. if (event.includes("response") || event.includes("completed")) {
  1161. return "audit-kind-response";
  1162. }
  1163. if (event.includes("event_agent")) {
  1164. return "audit-kind-event-agent";
  1165. }
  1166. if (event.includes("chat")) {
  1167. return "audit-kind-chat";
  1168. }
  1169. return "audit-kind-lifecycle";
  1170. }
  1171. function auditKindLabel(event) {
  1172. if (event.includes("event_agent")) {
  1173. return "EventAgent";
  1174. }
  1175. if (event.includes("chat_agent") || event.includes("chat_round") || event.includes("chat_event") || event.includes("chat_message")) {
  1176. return "ChatAgent";
  1177. }
  1178. return "Lifecycle";
  1179. }
  1180. function appendKeyValueSection(container, title, data) {
  1181. if (!data || !Object.keys(data).length) {
  1182. return;
  1183. }
  1184. const grid = document.createElement("div");
  1185. grid.className = "detail-kv";
  1186. Object.entries(data).forEach(([key, value]) => {
  1187. if (value === null || value === undefined || value === "") {
  1188. return;
  1189. }
  1190. const label = document.createElement("span");
  1191. label.textContent = key;
  1192. const content = document.createElement("strong");
  1193. content.textContent = typeof value === "object" ? JSON.stringify(value) : String(value);
  1194. grid.append(label, content);
  1195. });
  1196. if (grid.children.length) {
  1197. appendDetailSection(container, title, grid);
  1198. }
  1199. }
  1200. function appendRawChunksSection(container, title, chunks) {
  1201. if (!chunks || !chunks.length) {
  1202. return;
  1203. }
  1204. const list = document.createElement("div");
  1205. list.className = "detail-list";
  1206. chunks.forEach((chunk, index) => {
  1207. const item = document.createElement("div");
  1208. item.className = "detail-message raw-chunk";
  1209. const label = document.createElement("span");
  1210. label.textContent = `chunk ${index + 1}`;
  1211. const content = document.createElement("pre");
  1212. content.textContent = JSON.stringify(chunk, null, 2);
  1213. item.append(label, content);
  1214. list.append(item);
  1215. });
  1216. appendDetailSection(container, title, list);
  1217. }
  1218. function appendRawModelChunksSection(container, rawGroups) {
  1219. if (!rawGroups || !rawGroups.length) {
  1220. return;
  1221. }
  1222. const list = document.createElement("div");
  1223. list.className = "detail-list";
  1224. rawGroups.forEach((group) => {
  1225. const item = document.createElement("div");
  1226. item.className = "detail-message raw-chunk";
  1227. const label = document.createElement("span");
  1228. label.textContent = `${group.event_name || "event"} · ${group.event_id || "-"}`;
  1229. const content = document.createElement("pre");
  1230. content.textContent = JSON.stringify(group.chunks || [], null, 2);
  1231. item.append(label, content);
  1232. list.append(item);
  1233. });
  1234. appendDetailSection(container, "Raw EventAgent LLM Chunks", list);
  1235. }
  1236. function appendMessagesSection(container, title, messages) {
  1237. if (!messages || !messages.length) {
  1238. return;
  1239. }
  1240. const list = document.createElement("div");
  1241. list.className = "detail-list";
  1242. messages.forEach((message) => {
  1243. const item = document.createElement("div");
  1244. item.className = "detail-message";
  1245. const role = document.createElement("span");
  1246. role.textContent = message.role || "message";
  1247. const content = document.createElement("pre");
  1248. content.textContent = message.content || "";
  1249. item.append(role, content);
  1250. list.append(item);
  1251. });
  1252. appendDetailSection(container, title, list);
  1253. }
  1254. function appendToolsSection(container, tools) {
  1255. if (!tools || !tools.length) {
  1256. return;
  1257. }
  1258. const list = document.createElement("div");
  1259. list.className = "detail-list";
  1260. tools.forEach((tool) => {
  1261. const item = document.createElement("div");
  1262. item.className = "detail-message";
  1263. const name = document.createElement("span");
  1264. name.textContent = tool.function?.name || "tool";
  1265. const description = document.createElement("p");
  1266. description.textContent = tool.function?.description || "No description";
  1267. item.append(name, description);
  1268. list.append(item);
  1269. });
  1270. appendDetailSection(container, "Tools", list);
  1271. }
  1272. function appendEventsSection(container, events) {
  1273. if (!events || !events.length) {
  1274. return;
  1275. }
  1276. const values = events.map((event) => `${event.name}${event.id ? ` (${event.id})` : ""}`);
  1277. appendListSection(container, "Events", values);
  1278. }
  1279. function appendRepliesSection(container, replies) {
  1280. if (!replies || !replies.length) {
  1281. return;
  1282. }
  1283. const list = document.createElement("div");
  1284. list.className = "detail-list";
  1285. replies.forEach((reply) => {
  1286. const item = document.createElement("div");
  1287. item.className = "detail-message";
  1288. const role = document.createElement("span");
  1289. role.textContent = reply.role || "reply";
  1290. const content = document.createElement("pre");
  1291. content.textContent = reply.content || "";
  1292. item.append(role, content);
  1293. list.append(item);
  1294. });
  1295. appendDetailSection(container, "Tool Replies", list);
  1296. }
  1297. function appendListSection(container, title, items) {
  1298. if (!items || !items.length) {
  1299. return;
  1300. }
  1301. const list = document.createElement("ul");
  1302. list.className = "detail-tags";
  1303. items.forEach((item) => {
  1304. const tag = document.createElement("li");
  1305. const code = document.createElement("code");
  1306. code.textContent = String(item);
  1307. tag.append(code);
  1308. list.append(tag);
  1309. });
  1310. appendDetailSection(container, title, list);
  1311. }
  1312. function appendTextSection(container, title, value) {
  1313. if (!value) {
  1314. return;
  1315. }
  1316. const pre = document.createElement("pre");
  1317. pre.textContent = String(value);
  1318. appendDetailSection(container, title, pre);
  1319. }
  1320. function appendDetailSection(container, title, content) {
  1321. const section = document.createElement("section");
  1322. section.className = "audit-detail-block";
  1323. const heading = document.createElement("h3");
  1324. heading.className = "audit-detail-block-title";
  1325. heading.textContent = title;
  1326. section.append(heading, content);
  1327. container.append(section);
  1328. }
  1329. function appendRawDetails(container, details) {
  1330. if (!details || !Object.keys(details).length) {
  1331. return;
  1332. }
  1333. const raw = document.createElement("details");
  1334. raw.className = "raw-detail";
  1335. const summary = document.createElement("summary");
  1336. summary.textContent = "Raw JSON";
  1337. const pre = document.createElement("pre");
  1338. pre.textContent = JSON.stringify(details, null, 2);
  1339. raw.append(summary, pre);
  1340. container.append(raw);
  1341. }
  1342. function formatTime(value) {
  1343. if (!value) {
  1344. return "";
  1345. }
  1346. const date = new Date(value);
  1347. if (Number.isNaN(date.getTime())) {
  1348. return value;
  1349. }
  1350. return date.toLocaleTimeString([], {
  1351. hour: "2-digit",
  1352. minute: "2-digit",
  1353. second: "2-digit",
  1354. });
  1355. }
  1356. function renderSessionUsage(usage) {
  1357. const session = usage.session || {};
  1358. sessionUsageSummary.textContent = "";
  1359. [
  1360. ["Tokens", session.total_tokens || 0],
  1361. ["Cached", session.cached_tokens || 0],
  1362. ["Prompt", session.prompt_tokens || 0],
  1363. ["Elapsed", formatMs(session.elapsed_ms || 0)],
  1364. ].forEach(([label, value]) => {
  1365. const item = document.createElement("div");
  1366. const labelEl = document.createElement("span");
  1367. labelEl.textContent = label;
  1368. const valueEl = document.createElement("strong");
  1369. valueEl.textContent = value;
  1370. item.append(labelEl, valueEl);
  1371. sessionUsageSummary.append(item);
  1372. });
  1373. renderUsageRows(
  1374. sessionUsageTurns,
  1375. usage.turns || [],
  1376. (turn) => `Turn ${turn.turn_index}: ${turn.total_tokens} tokens, ${turn.elapsed_ms}ms`,
  1377. );
  1378. renderUsageRows(
  1379. sessionUsageCalls,
  1380. usage.calls || [],
  1381. (call) => `T${call.turn_index} R${call.round_index}: ${call.total_tokens} tokens, TTFT ${formatMs(call.ttft_ms)}, elapsed ${formatMs(call.elapsed_ms)}`,
  1382. );
  1383. }
  1384. function renderUsageRows(container, rows, formatter) {
  1385. container.textContent = "";
  1386. if (!rows.length) {
  1387. container.append(emptyReplayRow("No usage rows"));
  1388. return;
  1389. }
  1390. rows.forEach((row) => {
  1391. const item = document.createElement("div");
  1392. item.className = "replay-row";
  1393. item.textContent = formatter(row);
  1394. container.append(item);
  1395. });
  1396. }
  1397. function emptyReplayRow(content) {
  1398. const row = document.createElement("div");
  1399. row.className = "replay-row";
  1400. row.textContent = content;
  1401. return row;
  1402. }
  1403. function updateUsage(usage) {
  1404. if (hasBackendRoundStats) {
  1405. return;
  1406. }
  1407. document.querySelector("#stat-tokens").textContent = usage.total_tokens || 0;
  1408. document.querySelector("#stat-cached").textContent = usage.cached_tokens || 0;
  1409. }
  1410. function updateRoundStats(stats) {
  1411. if (firstTokenAt) {
  1412. hasBackendRoundStats = true;
  1413. }
  1414. document.querySelector("#stat-tokens").textContent = stats.total_tokens || 0;
  1415. document.querySelector("#stat-cached").textContent = stats.cached_tokens || 0;
  1416. const ttftText = formatMs(stats.ttft_ms);
  1417. const elapsedText = formatMs(stats.elapsed_ms);
  1418. document.querySelector("#stat-ttft").textContent = ttftText;
  1419. document.querySelector("#stat-elapsed").textContent = elapsedText;
  1420. appendLog(
  1421. "session",
  1422. `Round ${stats.round_index}: tokens ${stats.total_tokens || 0}, cached ${stats.cached_tokens || 0}, TTFT ${ttftText}, elapsed ${elapsedText}`,
  1423. );
  1424. }
  1425. function formatMs(value) {
  1426. return value === null || value === undefined ? "-" : `${value}ms`;
  1427. }
  1428. function updateElapsed() {
  1429. if (!runStartedAt || (hasBackendRoundStats && firstTokenAt)) {
  1430. return;
  1431. }
  1432. document.querySelector("#stat-elapsed").textContent = `${Math.round(performance.now() - runStartedAt)}ms`;
  1433. }
  1434. function startElapsedTimer() {
  1435. stopElapsedTimer();
  1436. elapsedTimer = window.setInterval(updateElapsed, 100);
  1437. }
  1438. function stopElapsedTimer() {
  1439. if (elapsedTimer) {
  1440. window.clearInterval(elapsedTimer);
  1441. elapsedTimer = 0;
  1442. }
  1443. }
  1444. function setRunState(isRunning) {
  1445. turnActive = isRunning;
  1446. runButton.disabled = isRunning;
  1447. }
  1448. function resetRun() {
  1449. stopElapsedTimer();
  1450. activeAssistant = null;
  1451. runStartedAt = 0;
  1452. firstTokenAt = 0;
  1453. hasBackendRoundStats = false;
  1454. messagesEl.textContent = "";
  1455. resetTurnStats();
  1456. }
  1457. function resetTurnStats() {
  1458. document.querySelector("#stat-tokens").textContent = "0";
  1459. document.querySelector("#stat-cached").textContent = "0";
  1460. document.querySelector("#stat-ttft").textContent = "-";
  1461. document.querySelector("#stat-elapsed").textContent = "-";
  1462. }
  1463. function closeSocket() {
  1464. const closingSocket = socket;
  1465. if (closingSocket && closingSocket.readyState < WebSocket.CLOSING) {
  1466. closingSocket.close();
  1467. }
  1468. if (socket === closingSocket) {
  1469. socket = null;
  1470. }
  1471. }
  1472. function wsUrl() {
  1473. const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
  1474. return `${protocol}//${window.location.host}/ws/debug`;
  1475. }