@recombine-ai/bosun 0.13.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (224) hide show
  1. package/bin/bosun.js +7 -0
  2. package/build/client/_app/immutable/assets/0.b0jhAoSC.css +1 -0
  3. package/build/client/_app/immutable/assets/0.b0jhAoSC.css.br +0 -0
  4. package/build/client/_app/immutable/assets/0.b0jhAoSC.css.gz +0 -0
  5. package/build/client/_app/immutable/chunks/BTX1tKMW.js +1 -0
  6. package/build/client/_app/immutable/chunks/BTX1tKMW.js.br +0 -0
  7. package/build/client/_app/immutable/chunks/BTX1tKMW.js.gz +0 -0
  8. package/build/client/_app/immutable/chunks/BVxLHCs8.js +1 -0
  9. package/build/client/_app/immutable/chunks/BVxLHCs8.js.br +0 -0
  10. package/build/client/_app/immutable/chunks/BVxLHCs8.js.gz +0 -0
  11. package/build/client/_app/immutable/chunks/{De8YgKS3.js → ChVBNPUb.js} +1 -1
  12. package/build/client/_app/immutable/chunks/ChVBNPUb.js.br +0 -0
  13. package/build/client/_app/immutable/chunks/ChVBNPUb.js.gz +0 -0
  14. package/build/client/_app/immutable/chunks/E9rdsnHb.js +1 -0
  15. package/build/client/_app/immutable/chunks/E9rdsnHb.js.br +0 -0
  16. package/build/client/_app/immutable/chunks/E9rdsnHb.js.gz +0 -0
  17. package/build/client/_app/immutable/chunks/Z1_c4us_.js +3 -0
  18. package/build/client/_app/immutable/chunks/Z1_c4us_.js.br +0 -0
  19. package/build/client/_app/immutable/chunks/Z1_c4us_.js.gz +0 -0
  20. package/build/client/_app/immutable/chunks/{Bj8jfBTi.js → lmnC3bvv.js} +1 -1
  21. package/build/client/_app/immutable/chunks/lmnC3bvv.js.br +0 -0
  22. package/build/client/_app/immutable/chunks/lmnC3bvv.js.gz +0 -0
  23. package/build/client/_app/immutable/entry/{app.DK_wVDeR.js → app.BY5DP2K_.js} +2 -2
  24. package/build/client/_app/immutable/entry/app.BY5DP2K_.js.br +0 -0
  25. package/build/client/_app/immutable/entry/app.BY5DP2K_.js.gz +0 -0
  26. package/build/client/_app/immutable/entry/start.BeIIwvvV.js +1 -0
  27. package/build/client/_app/immutable/entry/start.BeIIwvvV.js.br +0 -0
  28. package/build/client/_app/immutable/entry/start.BeIIwvvV.js.gz +0 -0
  29. package/build/client/_app/immutable/nodes/{0.5fEbn14Q.js → 0.Cao0LJwW.js} +1 -1
  30. package/build/client/_app/immutable/nodes/0.Cao0LJwW.js.br +0 -0
  31. package/build/client/_app/immutable/nodes/0.Cao0LJwW.js.gz +0 -0
  32. package/build/client/_app/immutable/nodes/{1.CJrSN8zL.js → 1.Kjgu6yxL.js} +1 -1
  33. package/build/client/_app/immutable/nodes/1.Kjgu6yxL.js.br +0 -0
  34. package/build/client/_app/immutable/nodes/1.Kjgu6yxL.js.gz +0 -0
  35. package/build/client/_app/immutable/nodes/{10.SZwTlOEA.js → 10.yW2mHq85.js} +1 -1
  36. package/build/client/_app/immutable/nodes/10.yW2mHq85.js.br +0 -0
  37. package/build/client/_app/immutable/nodes/10.yW2mHq85.js.gz +0 -0
  38. package/build/client/_app/immutable/nodes/{17.D-KFl7Vk.js → 17.COHkD98p.js} +3 -3
  39. package/build/client/_app/immutable/nodes/17.COHkD98p.js.br +0 -0
  40. package/build/client/_app/immutable/nodes/17.COHkD98p.js.gz +0 -0
  41. package/build/client/_app/immutable/nodes/18.DQVit7Md.js +11 -0
  42. package/build/client/_app/immutable/nodes/18.DQVit7Md.js.br +0 -0
  43. package/build/client/_app/immutable/nodes/18.DQVit7Md.js.gz +0 -0
  44. package/build/client/_app/immutable/nodes/2.DVwYttKv.js +35 -0
  45. package/build/client/_app/immutable/nodes/2.DVwYttKv.js.br +0 -0
  46. package/build/client/_app/immutable/nodes/2.DVwYttKv.js.gz +0 -0
  47. package/build/client/_app/immutable/nodes/{3.B1RGTOJ8.js → 3.BhmyTb3R.js} +1 -1
  48. package/build/client/_app/immutable/nodes/3.BhmyTb3R.js.br +0 -0
  49. package/build/client/_app/immutable/nodes/3.BhmyTb3R.js.gz +0 -0
  50. package/build/client/_app/immutable/nodes/{4.D2jcIUpD.js → 4.CxATX0ci.js} +1 -1
  51. package/build/client/_app/immutable/nodes/4.CxATX0ci.js.br +0 -0
  52. package/build/client/_app/immutable/nodes/4.CxATX0ci.js.gz +0 -0
  53. package/build/client/_app/immutable/nodes/{5.CUyCIyRd.js → 5.Dal1qxB1.js} +1 -1
  54. package/build/client/_app/immutable/nodes/5.Dal1qxB1.js.br +0 -0
  55. package/build/client/_app/immutable/nodes/5.Dal1qxB1.js.gz +0 -0
  56. package/build/client/_app/immutable/nodes/{6.B8HsGA2V.js → 6.iAOxBhJw.js} +1 -1
  57. package/build/client/_app/immutable/nodes/6.iAOxBhJw.js.br +0 -0
  58. package/build/client/_app/immutable/nodes/6.iAOxBhJw.js.gz +0 -0
  59. package/build/client/_app/immutable/nodes/{8.jY56-_OU.js → 8.DFhKu3dp.js} +1 -1
  60. package/build/client/_app/immutable/nodes/8.DFhKu3dp.js.br +0 -0
  61. package/build/client/_app/immutable/nodes/8.DFhKu3dp.js.gz +0 -0
  62. package/build/client/_app/immutable/nodes/9.DRIEv5m9.js +2 -0
  63. package/build/client/_app/immutable/nodes/9.DRIEv5m9.js.br +0 -0
  64. package/build/client/_app/immutable/nodes/9.DRIEv5m9.js.gz +0 -0
  65. package/build/client/_app/version.json +1 -1
  66. package/build/client/_app/version.json.br +0 -0
  67. package/build/client/_app/version.json.gz +0 -0
  68. package/build/server/chunks/{0-4QCErf9j.js → 0-CIzAMtA3.js} +3 -3
  69. package/build/server/chunks/{0-4QCErf9j.js.map → 0-CIzAMtA3.js.map} +1 -1
  70. package/build/server/chunks/{1-Dsjy8Hzc.js → 1-D1heuNbf.js} +2 -2
  71. package/build/server/chunks/{1-Dsjy8Hzc.js.map → 1-D1heuNbf.js.map} +1 -1
  72. package/build/server/chunks/{10-8061c8ui.js → 10-CjLtjXyZ.js} +2 -2
  73. package/build/server/chunks/{10-8061c8ui.js.map → 10-CjLtjXyZ.js.map} +1 -1
  74. package/build/server/chunks/{17-3_5wsIEX.js → 17-DklEmmOx.js} +4 -4
  75. package/build/server/chunks/{17-3_5wsIEX.js.map → 17-DklEmmOx.js.map} +1 -1
  76. package/build/server/chunks/{18-D73Mw140.js → 18-BJS-eRcz.js} +4 -4
  77. package/build/server/chunks/18-BJS-eRcz.js.map +1 -0
  78. package/build/server/chunks/{2-DM0VjzFi.js → 2-DCrAVYXs.js} +4 -4
  79. package/build/server/chunks/2-DCrAVYXs.js.map +1 -0
  80. package/build/server/chunks/{3-B3zlFGkL.js → 3-BpTqa4DU.js} +2 -2
  81. package/build/server/chunks/{3-B3zlFGkL.js.map → 3-BpTqa4DU.js.map} +1 -1
  82. package/build/server/chunks/{4-tESdMZpI.js → 4-B7GjwQzI.js} +2 -2
  83. package/build/server/chunks/{4-tESdMZpI.js.map → 4-B7GjwQzI.js.map} +1 -1
  84. package/build/server/chunks/{5-BJMTlq4U.js → 5-XjS__bcp.js} +2 -2
  85. package/build/server/chunks/{5-BJMTlq4U.js.map → 5-XjS__bcp.js.map} +1 -1
  86. package/build/server/chunks/{6-CDK2Aia6.js → 6-DWkWptmB.js} +2 -2
  87. package/build/server/chunks/{6-CDK2Aia6.js.map → 6-DWkWptmB.js.map} +1 -1
  88. package/build/server/chunks/{8-BJbDytoU.js → 8-_swAx6gL.js} +2 -2
  89. package/build/server/chunks/{8-BJbDytoU.js.map → 8-_swAx6gL.js.map} +1 -1
  90. package/build/server/chunks/{9-BNfIeSqk.js → 9-NEe24O4u.js} +4 -4
  91. package/build/server/chunks/9-NEe24O4u.js.map +1 -0
  92. package/build/server/chunks/{ChatStepTrace-DQL3qlna.js → ChatStepTrace-CX2Zolmp.js} +21 -8
  93. package/build/server/chunks/ChatStepTrace-CX2Zolmp.js.map +1 -0
  94. package/build/server/chunks/{_layout.svelte-DK7yl8vI.js → _layout.svelte-PtdpfgwJ.js} +66 -4
  95. package/build/server/chunks/_layout.svelte-PtdpfgwJ.js.map +1 -0
  96. package/build/server/chunks/{_page.svelte-ZWNBigwS.js → _page.svelte-C3gizoSD.js} +7 -4
  97. package/build/server/chunks/_page.svelte-C3gizoSD.js.map +1 -0
  98. package/build/server/chunks/{_page.svelte-D6bSXGHX.js → _page.svelte-CoCBnE2l.js} +8 -5
  99. package/build/server/chunks/_page.svelte-CoCBnE2l.js.map +1 -0
  100. package/build/server/chunks/{_page.svelte-ej3vdYeF.js → _page.svelte-DZvcjON6.js} +3 -2
  101. package/build/server/chunks/_page.svelte-DZvcjON6.js.map +1 -0
  102. package/build/server/chunks/steps-DzZCg39h.js +8 -0
  103. package/build/server/chunks/steps-DzZCg39h.js.map +1 -0
  104. package/build/server/index.js +1 -1
  105. package/build/server/index.js.map +1 -1
  106. package/build/server/manifest.js +13 -13
  107. package/build/server/manifest.js.map +1 -1
  108. package/package.json +4 -4
  109. package/src/backend/lib/backend-restart.ts +15 -0
  110. package/src/backend/roleplay/router.ts +42 -5
  111. package/src/backend/roleplay/schema-file.spec.ts +38 -0
  112. package/src/backend/roleplay/schema-file.ts +33 -0
  113. package/src/backend/tests/academy.ts +4 -2
  114. package/src/backend/tests/analyze.ts +2 -1
  115. package/src/backend/tests/emulate-agents.ts +13 -21
  116. package/src/backend/tests/pii-sanitized-test-drafts.spec.ts +162 -0
  117. package/src/backend/tests/pii-sanitized-test-drafts.ts +106 -0
  118. package/src/backend/tests/pii-sanitizer.spec.ts +145 -0
  119. package/src/backend/tests/pii-sanitizer.ts +171 -0
  120. package/src/backend/tests/router.ts +37 -0
  121. package/src/lib/agent-catalog.ts +4 -0
  122. package/src/lib/agents-context.ts +8 -0
  123. package/src/lib/api-telescope/client.ts +174 -0
  124. package/src/lib/api-telescope/index.ts +4 -0
  125. package/src/lib/api-telescope/latency-timeline-view-state.spec.ts +25 -0
  126. package/src/lib/api-telescope/latency-timeline-view-state.ts +86 -0
  127. package/src/lib/api-telescope/latency.ts +252 -0
  128. package/src/lib/api-telescope/source-selection.ts +20 -0
  129. package/src/lib/api-telescope/sources.server.ts +94 -0
  130. package/src/lib/api-telescope/types.ts +143 -0
  131. package/src/lib/components/Button.svelte +62 -0
  132. package/src/lib/components/CollapseExpand.svelte +425 -0
  133. package/src/lib/components/CopyTracesDropdown.svelte +118 -0
  134. package/src/lib/components/Dialog.svelte +69 -0
  135. package/src/lib/components/Dropdown.svelte +111 -0
  136. package/src/lib/components/History.svelte +174 -0
  137. package/src/lib/components/Icon.svelte +94 -0
  138. package/src/lib/components/Input.svelte +43 -0
  139. package/src/lib/components/Link.svelte +45 -0
  140. package/src/lib/components/Modal.svelte +45 -0
  141. package/src/lib/components/Panel.svelte +12 -0
  142. package/src/lib/components/PopoverModal.svelte +34 -0
  143. package/src/lib/components/Popper.svelte +109 -0
  144. package/src/lib/components/Tab.svelte +91 -0
  145. package/src/lib/components/Tabs.svelte +140 -0
  146. package/src/lib/components/agent-colors.ts +70 -0
  147. package/src/lib/dom/text-selection.ts +97 -0
  148. package/src/lib/edit/edit-vm.spec.ts +115 -0
  149. package/src/lib/edit/edit-vm.ts +169 -0
  150. package/src/lib/edit/edit-workspace-context.ts +56 -0
  151. package/src/lib/edit/highlight-styles.spec.ts +14 -0
  152. package/src/lib/edit/prompt-highlighter.spec.ts +80 -0
  153. package/src/lib/edit/prompt-highlighter.ts +155 -0
  154. package/src/lib/edit/quote-matching.ts +97 -0
  155. package/src/lib/monaco/bosun-theme.ts +125 -0
  156. package/src/lib/monaco/font.ts +2 -0
  157. package/src/lib/monaco-workers.ts +28 -0
  158. package/src/lib/steps.ts +19 -0
  159. package/src/lib/tests/pii-sanitized-test-draft.ts +50 -0
  160. package/src/lib/trpc-client.ts +16 -0
  161. package/src/lib/ws-client.ts +144 -0
  162. package/src/lib/ws-message.ts +150 -0
  163. package/build/client/_app/immutable/assets/0.B-Br6U4N.css +0 -1
  164. package/build/client/_app/immutable/assets/0.B-Br6U4N.css.br +0 -0
  165. package/build/client/_app/immutable/assets/0.B-Br6U4N.css.gz +0 -0
  166. package/build/client/_app/immutable/chunks/BOuo-4gP.js +0 -1
  167. package/build/client/_app/immutable/chunks/BOuo-4gP.js.br +0 -0
  168. package/build/client/_app/immutable/chunks/BOuo-4gP.js.gz +0 -0
  169. package/build/client/_app/immutable/chunks/Bj8jfBTi.js.br +0 -0
  170. package/build/client/_app/immutable/chunks/Bj8jfBTi.js.gz +0 -0
  171. package/build/client/_app/immutable/chunks/CX0Ekq8t.js +0 -1
  172. package/build/client/_app/immutable/chunks/CX0Ekq8t.js.br +0 -0
  173. package/build/client/_app/immutable/chunks/CX0Ekq8t.js.gz +0 -0
  174. package/build/client/_app/immutable/chunks/Cyj62X2W.js +0 -2
  175. package/build/client/_app/immutable/chunks/Cyj62X2W.js.br +0 -0
  176. package/build/client/_app/immutable/chunks/Cyj62X2W.js.gz +0 -0
  177. package/build/client/_app/immutable/chunks/DSDSeO7L.js +0 -1
  178. package/build/client/_app/immutable/chunks/DSDSeO7L.js.br +0 -0
  179. package/build/client/_app/immutable/chunks/DSDSeO7L.js.gz +0 -0
  180. package/build/client/_app/immutable/chunks/De8YgKS3.js.br +0 -1
  181. package/build/client/_app/immutable/chunks/De8YgKS3.js.gz +0 -0
  182. package/build/client/_app/immutable/entry/app.DK_wVDeR.js.br +0 -0
  183. package/build/client/_app/immutable/entry/app.DK_wVDeR.js.gz +0 -0
  184. package/build/client/_app/immutable/entry/start.xQy-2aGo.js +0 -1
  185. package/build/client/_app/immutable/entry/start.xQy-2aGo.js.br +0 -2
  186. package/build/client/_app/immutable/entry/start.xQy-2aGo.js.gz +0 -0
  187. package/build/client/_app/immutable/nodes/0.5fEbn14Q.js.br +0 -0
  188. package/build/client/_app/immutable/nodes/0.5fEbn14Q.js.gz +0 -0
  189. package/build/client/_app/immutable/nodes/1.CJrSN8zL.js.br +0 -0
  190. package/build/client/_app/immutable/nodes/1.CJrSN8zL.js.gz +0 -0
  191. package/build/client/_app/immutable/nodes/10.SZwTlOEA.js.br +0 -0
  192. package/build/client/_app/immutable/nodes/10.SZwTlOEA.js.gz +0 -0
  193. package/build/client/_app/immutable/nodes/17.D-KFl7Vk.js.br +0 -0
  194. package/build/client/_app/immutable/nodes/17.D-KFl7Vk.js.gz +0 -0
  195. package/build/client/_app/immutable/nodes/18.DqRdJY9A.js +0 -11
  196. package/build/client/_app/immutable/nodes/18.DqRdJY9A.js.br +0 -0
  197. package/build/client/_app/immutable/nodes/18.DqRdJY9A.js.gz +0 -0
  198. package/build/client/_app/immutable/nodes/2.B3Uuk25v.js +0 -35
  199. package/build/client/_app/immutable/nodes/2.B3Uuk25v.js.br +0 -0
  200. package/build/client/_app/immutable/nodes/2.B3Uuk25v.js.gz +0 -0
  201. package/build/client/_app/immutable/nodes/3.B1RGTOJ8.js.br +0 -0
  202. package/build/client/_app/immutable/nodes/3.B1RGTOJ8.js.gz +0 -0
  203. package/build/client/_app/immutable/nodes/4.D2jcIUpD.js.br +0 -0
  204. package/build/client/_app/immutable/nodes/4.D2jcIUpD.js.gz +0 -0
  205. package/build/client/_app/immutable/nodes/5.CUyCIyRd.js.br +0 -0
  206. package/build/client/_app/immutable/nodes/5.CUyCIyRd.js.gz +0 -0
  207. package/build/client/_app/immutable/nodes/6.B8HsGA2V.js.br +0 -0
  208. package/build/client/_app/immutable/nodes/6.B8HsGA2V.js.gz +0 -0
  209. package/build/client/_app/immutable/nodes/8.jY56-_OU.js.br +0 -0
  210. package/build/client/_app/immutable/nodes/8.jY56-_OU.js.gz +0 -0
  211. package/build/client/_app/immutable/nodes/9.v5dX6L9n.js +0 -2
  212. package/build/client/_app/immutable/nodes/9.v5dX6L9n.js.br +0 -0
  213. package/build/client/_app/immutable/nodes/9.v5dX6L9n.js.gz +0 -0
  214. package/build/server/chunks/18-D73Mw140.js.map +0 -1
  215. package/build/server/chunks/2-DM0VjzFi.js.map +0 -1
  216. package/build/server/chunks/9-BNfIeSqk.js.map +0 -1
  217. package/build/server/chunks/ChatStepTrace-DQL3qlna.js.map +0 -1
  218. package/build/server/chunks/_layout.svelte-DK7yl8vI.js.map +0 -1
  219. package/build/server/chunks/_page.svelte-D6bSXGHX.js.map +0 -1
  220. package/build/server/chunks/_page.svelte-ZWNBigwS.js.map +0 -1
  221. package/build/server/chunks/_page.svelte-ej3vdYeF.js.map +0 -1
  222. /package/build/client/_app/immutable/assets/{Popper.CLTyHc88.css → steps.CLTyHc88.css} +0 -0
  223. /package/build/client/_app/immutable/assets/{Popper.CLTyHc88.css.br → steps.CLTyHc88.css.br} +0 -0
  224. /package/build/client/_app/immutable/assets/{Popper.CLTyHc88.css.gz → steps.CLTyHc88.css.gz} +0 -0
@@ -0,0 +1,86 @@
1
+ export type LatencyTimelineViewport = {
2
+ startMs: number
3
+ endMs: number
4
+ originMs: number
5
+ }
6
+
7
+ const STORAGE_KEY = 'bosun.traces.latencyTimelineViewports.v2'
8
+
9
+ const viewports = new Map<string, LatencyTimelineViewport>()
10
+ let loadedFromSession = false
11
+
12
+ function isValidViewport(value: unknown): value is LatencyTimelineViewport {
13
+ if (!value || typeof value !== 'object') return false
14
+ const next = value as Record<string, unknown>
15
+ return (
16
+ typeof next.startMs === 'number' &&
17
+ Number.isFinite(next.startMs) &&
18
+ typeof next.endMs === 'number' &&
19
+ Number.isFinite(next.endMs) &&
20
+ next.endMs > next.startMs &&
21
+ typeof next.originMs === 'number' &&
22
+ Number.isFinite(next.originMs)
23
+ )
24
+ }
25
+
26
+ export function rebaseLatencyTimelineViewport(
27
+ viewport: LatencyTimelineViewport,
28
+ nextOriginMs: number
29
+ ): LatencyTimelineViewport {
30
+ if (!Number.isFinite(nextOriginMs)) return { ...viewport }
31
+
32
+ const originDeltaMs = nextOriginMs - viewport.originMs
33
+ return {
34
+ startMs: viewport.startMs + originDeltaMs,
35
+ endMs: viewport.endMs + originDeltaMs,
36
+ originMs: nextOriginMs
37
+ }
38
+ }
39
+
40
+ function ensureLoadedFromSession() {
41
+ if (loadedFromSession) return
42
+ loadedFromSession = true
43
+ if (typeof window === 'undefined') return
44
+
45
+ try {
46
+ const raw = window.sessionStorage.getItem(STORAGE_KEY)
47
+ if (!raw) return
48
+
49
+ const parsed = JSON.parse(raw) as unknown
50
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return
51
+
52
+ for (const [key, value] of Object.entries(parsed)) {
53
+ if (!key || !isValidViewport(value)) continue
54
+ viewports.set(key, value)
55
+ }
56
+ } catch {
57
+ // Ignore stale or unavailable session state.
58
+ }
59
+ }
60
+
61
+ function persistToSession() {
62
+ if (typeof window === 'undefined') return
63
+
64
+ try {
65
+ window.sessionStorage.setItem(
66
+ STORAGE_KEY,
67
+ JSON.stringify(Object.fromEntries(viewports.entries()))
68
+ )
69
+ } catch {
70
+ // Ignore storage failures; the in-memory map still works for this app session.
71
+ }
72
+ }
73
+
74
+ export function getLatencyTimelineViewport(key: string): LatencyTimelineViewport | null {
75
+ if (!key) return null
76
+ ensureLoadedFromSession()
77
+ const viewport = viewports.get(key)
78
+ return viewport ? { ...viewport } : null
79
+ }
80
+
81
+ export function persistLatencyTimelineViewport(key: string, viewport: LatencyTimelineViewport) {
82
+ if (!key || !isValidViewport(viewport)) return
83
+ ensureLoadedFromSession()
84
+ viewports.set(key, { ...viewport })
85
+ persistToSession()
86
+ }
@@ -0,0 +1,252 @@
1
+ import type { LatencyTrace, Trace } from './types'
2
+
3
+ export const RESPONSE_LATENCY_THRESHOLD_MS = 800
4
+ const RESPONSE_LATENCY_TRACE_MATCH_TOLERANCE_MS = 250
5
+
6
+ export type ResponseLatencySpan = {
7
+ id: string
8
+ start: number
9
+ end: number
10
+ durationMs: number
11
+ isHigh: boolean
12
+ startTrace: LatencyTrace
13
+ speechCreatedTrace: LatencyTrace | null
14
+ endTrace: LatencyTrace
15
+ text: string | null
16
+ }
17
+
18
+ function normalizeNonEmptyString(value: unknown): string | null {
19
+ if (typeof value !== 'string') return null
20
+ const trimmed = value.trim()
21
+ return trimmed ? trimmed : null
22
+ }
23
+
24
+ function finiteNumber(value: unknown): number | null {
25
+ return typeof value === 'number' && Number.isFinite(value) ? value : null
26
+ }
27
+
28
+ function latencyPayload(trace: LatencyTrace): Record<string, unknown> {
29
+ const payload = trace.payload
30
+ return payload && typeof payload === 'object' && !Array.isArray(payload)
31
+ ? (payload as Record<string, unknown>)
32
+ : {}
33
+ }
34
+
35
+ function latencyText(trace: LatencyTrace): string | null {
36
+ return normalizeNonEmptyString(latencyPayload(trace).text)
37
+ }
38
+
39
+ function isCancellationValue(value: unknown): boolean {
40
+ if (value === true) return true
41
+ if (typeof value === 'number') return value === 1
42
+ if (typeof value !== 'string') return false
43
+
44
+ const normalized = value.trim().toLowerCase()
45
+ return (
46
+ normalized === 'true' ||
47
+ normalized === '1' ||
48
+ normalized === 'cancelled' ||
49
+ normalized === 'canceled' ||
50
+ normalized === 'dropped' ||
51
+ normalized === 'discarded' ||
52
+ normalized === 'aborted' ||
53
+ normalized === 'superseded'
54
+ )
55
+ }
56
+
57
+ function hasCancellationMarker(payload: Record<string, unknown>): boolean {
58
+ const flagKeys = [
59
+ 'cancelled',
60
+ 'canceled',
61
+ 'isCancelled',
62
+ 'isCanceled',
63
+ 'wasCancelled',
64
+ 'wasCanceled',
65
+ 'dropped',
66
+ 'discarded',
67
+ 'aborted',
68
+ 'superseded'
69
+ ]
70
+ if (flagKeys.some((key) => isCancellationValue(payload[key]))) return true
71
+
72
+ const timestampKeys = [
73
+ 'cancelledAt',
74
+ 'canceledAt',
75
+ 'droppedAt',
76
+ 'discardedAt',
77
+ 'abortedAt',
78
+ 'supersededAt'
79
+ ]
80
+ if (
81
+ timestampKeys.some((key) => {
82
+ const value = payload[key]
83
+ if (value == null) return false
84
+ if (typeof value === 'string') return value.trim() !== ''
85
+ return true
86
+ })
87
+ ) {
88
+ return true
89
+ }
90
+
91
+ const status = normalizeNonEmptyString(payload.status)
92
+ const state = normalizeNonEmptyString(payload.state)
93
+ const lifecycle = normalizeNonEmptyString(payload.lifecycle)
94
+ return [status, state, lifecycle].some((value) => value != null && isCancellationValue(value))
95
+ }
96
+
97
+ function isFinalSttTranscript(trace: LatencyTrace): boolean {
98
+ return trace.eventType === 'stt.final' || trace.eventType === 'stt.final_transcript'
99
+ }
100
+
101
+ function isUncancelledSpeechCreated(trace: LatencyTrace): boolean {
102
+ if (trace.eventType !== 'speech.created') return false
103
+ const payload = latencyPayload(trace)
104
+ if (hasCancellationMarker(payload)) return false
105
+
106
+ const speech = payload.speech
107
+ if (speech && typeof speech === 'object' && !Array.isArray(speech)) {
108
+ return !hasCancellationMarker(speech as Record<string, unknown>)
109
+ }
110
+
111
+ return true
112
+ }
113
+
114
+ function isAudioSpeechStart(trace: LatencyTrace): boolean {
115
+ return (
116
+ trace.eventType === 'audio.output.wait.completed' ||
117
+ trace.eventType === 'audio.playback.started'
118
+ )
119
+ }
120
+
121
+ function compareLatencyTraces(a: LatencyTrace, b: LatencyTrace): number {
122
+ return (
123
+ a.seq - b.seq ||
124
+ a.atMs - b.atMs ||
125
+ (a.createdAt ?? 0) - (b.createdAt ?? 0) ||
126
+ String(a.traceId ?? '').localeCompare(String(b.traceId ?? ''))
127
+ )
128
+ }
129
+
130
+ function findNextLatencyEvent(
131
+ events: LatencyTrace[],
132
+ startIndex: number,
133
+ match: (trace: LatencyTrace) => boolean,
134
+ stop: (trace: LatencyTrace) => boolean
135
+ ): LatencyTrace | null {
136
+ for (let index = startIndex + 1; index < events.length; index += 1) {
137
+ const next = events[index]
138
+ if (stop(next)) return null
139
+ if (match(next)) return next
140
+ }
141
+ return null
142
+ }
143
+
144
+ function isUserConversationalTrace(trace: Trace): boolean {
145
+ return (
146
+ trace.traceType === 'CONVERSATIONAL' &&
147
+ trace.role.toLowerCase() === 'user' &&
148
+ (trace.eventName === 'received-message' || trace.eventName === 'llm-handler-stream-requested')
149
+ )
150
+ }
151
+
152
+ function isAgentConversationalTrace(trace: Trace): boolean {
153
+ return (
154
+ trace.traceType === 'CONVERSATIONAL' &&
155
+ (trace.eventName === 'finished-main-streaming-step' || trace.eventName === 'sent-message')
156
+ )
157
+ }
158
+
159
+ function findPreviousConversationalTrace(items: Trace[], beforeIndex: number): Trace | null {
160
+ for (let index = beforeIndex - 1; index >= 0; index -= 1) {
161
+ const candidate = items[index]
162
+ if (candidate.traceType === 'CONVERSATIONAL') return candidate
163
+ }
164
+ return null
165
+ }
166
+
167
+ export function findResponseLatencyUserTurn(
168
+ items: Trace[],
169
+ span: ResponseLatencySpan
170
+ ): Trace | null {
171
+ const startCreatedAt = finiteNumber(span.startTrace.createdAt)
172
+ const endCreatedAt = finiteNumber(span.endTrace.createdAt)
173
+ if (startCreatedAt == null) return null
174
+
175
+ for (let index = 0; index < items.length; index += 1) {
176
+ const candidate = items[index]
177
+ if (!isAgentConversationalTrace(candidate)) continue
178
+ if (candidate.createdAt < startCreatedAt - RESPONSE_LATENCY_TRACE_MATCH_TOLERANCE_MS) {
179
+ continue
180
+ }
181
+ if (
182
+ endCreatedAt != null &&
183
+ candidate.createdAt < endCreatedAt - RESPONSE_LATENCY_TRACE_MATCH_TOLERANCE_MS
184
+ ) {
185
+ continue
186
+ }
187
+
188
+ const previous = findPreviousConversationalTrace(items, index)
189
+ if (!previous || !isUserConversationalTrace(previous)) continue
190
+ return previous
191
+ }
192
+
193
+ return null
194
+ }
195
+
196
+ export function buildResponseLatencySpans(input: LatencyTrace[]): ResponseLatencySpan[] {
197
+ const events = input
198
+ .slice()
199
+ .filter(
200
+ (trace) =>
201
+ typeof trace.atMs === 'number' &&
202
+ Number.isFinite(trace.atMs) &&
203
+ typeof trace.seq === 'number' &&
204
+ Number.isFinite(trace.seq)
205
+ )
206
+ .sort(compareLatencyTraces)
207
+
208
+ const built: ResponseLatencySpan[] = []
209
+
210
+ events.forEach((trace, index) => {
211
+ if (!isFinalSttTranscript(trace)) return
212
+
213
+ const speechCreated = findNextLatencyEvent(
214
+ events,
215
+ index,
216
+ isUncancelledSpeechCreated,
217
+ (candidate) =>
218
+ candidate.eventType === 'call.ended' ||
219
+ candidate.eventType === 'vad.activated' ||
220
+ candidate.eventType === 'stt.started' ||
221
+ isFinalSttTranscript(candidate)
222
+ )
223
+
224
+ const speechCreatedIndex = speechCreated ? events.indexOf(speechCreated) : index
225
+ const next = findNextLatencyEvent(
226
+ events,
227
+ speechCreatedIndex,
228
+ isAudioSpeechStart,
229
+ (candidate) =>
230
+ candidate.eventType === 'call.ended' ||
231
+ candidate.eventType === 'vad.activated' ||
232
+ candidate.eventType === 'stt.started' ||
233
+ isFinalSttTranscript(candidate)
234
+ )
235
+ if (!next || next.atMs < trace.atMs) return
236
+
237
+ const durationMs = next.atMs - trace.atMs
238
+ built.push({
239
+ id: `response-latency:${trace.seq}:${next.seq}`,
240
+ start: trace.atMs,
241
+ end: next.atMs,
242
+ durationMs,
243
+ isHigh: durationMs > RESPONSE_LATENCY_THRESHOLD_MS,
244
+ startTrace: trace,
245
+ speechCreatedTrace: speechCreated,
246
+ endTrace: next,
247
+ text: latencyText(trace)
248
+ })
249
+ })
250
+
251
+ return built
252
+ }
@@ -0,0 +1,20 @@
1
+ export const TELESCOPE_SOURCE_STORAGE_KEY = 'bosun.traces.telescopeSourceId'
2
+
3
+ export function getPersistedTelescopeSourceId(): string | null {
4
+ if (typeof window === 'undefined') return null
5
+ try {
6
+ return window.localStorage.getItem(TELESCOPE_SOURCE_STORAGE_KEY)
7
+ } catch {
8
+ return null
9
+ }
10
+ }
11
+
12
+ export function persistTelescopeSourceId(sourceId: string) {
13
+ if (typeof window === 'undefined') return
14
+ if (!sourceId || !sourceId.trim()) return
15
+ try {
16
+ window.localStorage.setItem(TELESCOPE_SOURCE_STORAGE_KEY, sourceId)
17
+ } catch {
18
+ // ignore
19
+ }
20
+ }
@@ -0,0 +1,94 @@
1
+ import { env } from '$env/dynamic/private'
2
+ import { z } from 'zod'
3
+
4
+ const TelescopeSourceInputSchema = z
5
+ .object({
6
+ id: z.string().optional(),
7
+ label: z.string().min(1),
8
+ // Accept either camelCase or the user-friendly names ("url"/"secret")
9
+ baseUrl: z.string().optional(),
10
+ url: z.string().optional(),
11
+ apiKey: z.string().optional(),
12
+ secret: z.string().optional()
13
+ })
14
+ .superRefine((value, ctx) => {
15
+ const baseUrl = (value.baseUrl ?? value.url ?? '').trim()
16
+ const apiKey = (value.apiKey ?? value.secret ?? '').trim()
17
+
18
+ if (!baseUrl) {
19
+ ctx.addIssue({
20
+ code: z.ZodIssueCode.custom,
21
+ path: ['baseUrl'],
22
+ message: 'baseUrl/url is required'
23
+ })
24
+ }
25
+ if (!apiKey) {
26
+ ctx.addIssue({
27
+ code: z.ZodIssueCode.custom,
28
+ path: ['apiKey'],
29
+ message: 'apiKey/secret is required'
30
+ })
31
+ }
32
+ })
33
+
34
+ const TelescopeSourcesJsonSchema = z.array(TelescopeSourceInputSchema)
35
+
36
+ export interface TelescopeSourceConfig {
37
+ id: string
38
+ label: string
39
+ baseUrl: string
40
+ apiKey: string
41
+ }
42
+
43
+ function slugify(value: string): string {
44
+ return value
45
+ .trim()
46
+ .toLowerCase()
47
+ .replace(/[^a-z0-9]+/g, '-')
48
+ .replace(/^-+|-+$/g, '')
49
+ }
50
+
51
+ function cleanBaseUrl(value: string): string {
52
+ return value.trim().replace(/\/+$/, '')
53
+ }
54
+
55
+ function normalizeSources(
56
+ inputs: z.infer<typeof TelescopeSourcesJsonSchema>
57
+ ): TelescopeSourceConfig[] {
58
+ const usedIds = new Set<string>()
59
+
60
+ return inputs.map((input, idx) => {
61
+ const label = input.label.trim()
62
+ const baseUrl = cleanBaseUrl(input.baseUrl ?? input.url ?? '')
63
+ const apiKey = (input.apiKey ?? input.secret ?? '').trim()
64
+
65
+ const preferred = typeof input.id === 'string' && input.id.trim() ? input.id.trim() : label
66
+ let id = slugify(preferred) || `source-${idx + 1}`
67
+ let deduped = id
68
+ let i = 2
69
+ while (usedIds.has(deduped)) {
70
+ deduped = `${id}-${i++}`
71
+ }
72
+ id = deduped
73
+ usedIds.add(id)
74
+
75
+ return { id, label, baseUrl, apiKey }
76
+ })
77
+ }
78
+
79
+ export function getTelescopeSources(): TelescopeSourceConfig[] {
80
+ const rawJson = env.TELESCOPE_SOURCES_JSON
81
+
82
+ if (typeof rawJson === 'string' && rawJson.trim()) {
83
+ let parsed: unknown
84
+ try {
85
+ parsed = JSON.parse(rawJson)
86
+ } catch {
87
+ throw new Error('TELESCOPE_SOURCES_JSON is not valid JSON')
88
+ }
89
+
90
+ const inputs = TelescopeSourcesJsonSchema.parse(parsed)
91
+ return normalizeSources(inputs)
92
+ }
93
+ return []
94
+ }
@@ -0,0 +1,143 @@
1
+ import { z } from 'zod'
2
+
3
+ export const TranscriptSegmentSchema = z.object({
4
+ text: z.string(),
5
+ role: z.string(), // 'Agent' or 'User'
6
+ time: z.string(), // ISO timestamp
7
+ duration: z.number()
8
+ })
9
+
10
+ export const ConversationSchema = z
11
+ .object({
12
+ conversationId: z.string(),
13
+ medium: z.string(), // 'phone' | 'whatsapp' | ...
14
+ agentId: z.string().optional(),
15
+ phoneNumber: z.string().optional(),
16
+ duration: z.number().optional(),
17
+ audioUrl: z.string().url().optional(),
18
+ transcript: z.union([z.string(), z.array(TranscriptSegmentSchema)]).optional(),
19
+ createdAt: z.number()
20
+ })
21
+ .passthrough()
22
+
23
+ export type Conversation = z.infer<typeof ConversationSchema>
24
+
25
+ export const PaginatedConversationsResponseSchema = z.object({
26
+ limit: z.number(),
27
+ items: z.array(ConversationSchema),
28
+ lastConversationId: z.string().optional()
29
+ })
30
+
31
+ export type PaginatedConversationsResponse = z.infer<typeof PaginatedConversationsResponseSchema>
32
+
33
+ export type GetConversationsRequest = {
34
+ phoneNumber?: string
35
+ agentId?: string
36
+ limit?: number
37
+ lastConversationId?: string
38
+ }
39
+
40
+ export const LlmUsageSchema = z
41
+ .object({
42
+ promptTokens: z.number().optional(),
43
+ completionTokens: z.number().optional(),
44
+ totalTokens: z.number().optional(),
45
+ cachedPromptTokens: z.number().optional()
46
+ })
47
+ .passthrough()
48
+
49
+ export const StepTraceSchema = z
50
+ .object({
51
+ traceType: z.literal('STEP'),
52
+ traceId: z.string().optional(),
53
+ conversationId: z.string().optional(),
54
+ scopeId: z.string().optional(),
55
+ workflowId: z.string().optional(),
56
+ workflowRunId: z.string().optional(),
57
+ name: z.string().optional(),
58
+ renderedPrompt: z.string().optional(),
59
+ receivedContext: z.any().optional(),
60
+ receivedPrompt: z.any().optional(),
61
+ stringifiedConversation: z.string().optional(),
62
+ response: z.string().optional(),
63
+ model: z.string().optional(),
64
+ llmUsage: LlmUsageSchema.optional(),
65
+ createdAt: z.number(),
66
+ error: z.any().optional()
67
+ })
68
+ .passthrough()
69
+
70
+ export const ApiCallTraceSchema = z
71
+ .object({
72
+ traceType: z.literal('API_CALL'),
73
+ traceId: z.string().optional(),
74
+ conversationId: z.string().optional(),
75
+ scopeId: z.string().optional(),
76
+ operationName: z.string(),
77
+ method: z.string(),
78
+ path: z.string(),
79
+ baseUrl: z.string().optional(),
80
+ request: z.any().optional(),
81
+ response: z.any().optional(),
82
+ status: z.number().optional(),
83
+ ok: z.boolean().optional(),
84
+ durationMs: z.number().optional(),
85
+ createdAt: z.number(),
86
+ error: z.any().optional()
87
+ })
88
+ .passthrough()
89
+
90
+ export const ConversationalTraceSchema = z
91
+ .object({
92
+ traceType: z.literal('CONVERSATIONAL'),
93
+ traceId: z.string().optional(),
94
+ conversationId: z.string().optional(),
95
+ scopeId: z.string().optional(),
96
+ eventName: z.string(),
97
+ role: z.string(),
98
+ medium: z.string(),
99
+ content: z.string(),
100
+ createdAt: z.number(),
101
+ error: z.any().optional()
102
+ })
103
+ .passthrough()
104
+
105
+ export const LatencyTraceSchema = z
106
+ .object({
107
+ traceType: z.literal('LATENCY'),
108
+ traceId: z.string().optional(),
109
+ conversationId: z.string().optional(),
110
+ seq: z.number(),
111
+ atMs: z.number(),
112
+ eventType: z.string(),
113
+ payload: z.any().optional(),
114
+ createdAt: z.number()
115
+ })
116
+ .passthrough()
117
+
118
+ export const TraceSchema = z.discriminatedUnion('traceType', [
119
+ StepTraceSchema,
120
+ ApiCallTraceSchema,
121
+ ConversationalTraceSchema
122
+ ])
123
+
124
+ export type StepTrace = z.infer<typeof StepTraceSchema>
125
+ export type ApiCallTrace = z.infer<typeof ApiCallTraceSchema>
126
+ export type ConversationalTrace = z.infer<typeof ConversationalTraceSchema>
127
+ export type LatencyTrace = z.infer<typeof LatencyTraceSchema>
128
+ export type Trace = z.infer<typeof TraceSchema>
129
+
130
+ export const PaginatedTracesResponseSchema = z.object({
131
+ items: z.array(TraceSchema),
132
+ limit: z.number(),
133
+ lastTraceId: z.string().optional()
134
+ })
135
+
136
+ export const PaginatedLatencyTracesResponseSchema = z.object({
137
+ items: z.array(LatencyTraceSchema),
138
+ limit: z.number(),
139
+ lastTraceId: z.string().optional()
140
+ })
141
+
142
+ export type PaginatedTracesResponse = z.infer<typeof PaginatedTracesResponseSchema>
143
+ export type PaginatedLatencyTracesResponse = z.infer<typeof PaginatedLatencyTracesResponseSchema>
@@ -0,0 +1,62 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte'
3
+ import type { HTMLButtonAttributes } from 'svelte/elements'
4
+
5
+ type Variant = 'primary' | 'secondary' | 'outlined' | 'ghost'
6
+ type Size = 'big' | 'medium' | 'small'
7
+
8
+ interface Props extends HTMLButtonAttributes {
9
+ variant?: Variant
10
+ size?: Size
11
+ type?: 'button' | 'submit' | 'reset'
12
+ disabled?: boolean
13
+ class?: string
14
+ children: Snippet
15
+ el?: HTMLButtonElement | null
16
+ }
17
+
18
+ let {
19
+ variant = 'primary',
20
+ size = 'medium',
21
+ type = 'button',
22
+ disabled = false,
23
+ class: className = '',
24
+ children,
25
+ el = $bindable(),
26
+ ...rest
27
+ }: Props = $props()
28
+
29
+ const variantClasses = {
30
+ primary:
31
+ 'border border-thin border-rc-green-450 backdrop-blur-xl rounded-lg text-rc-gray-100 font-bold bg-rc-green-350 hover:bg-rc-green-250 focus:bg-rc-green-200 focus:outline-none active:bg-rc-green-250',
32
+ secondary:
33
+ 'border border-thin border-rc-gray-600 backdrop-blur-2xl rounded-lg bg-rc-gray-800 text-rc-gray-100 font-bold hover:bg-rc-gray-700 focus:bg-rc-gray-700 focus:outline-none active:bg-rc-gray-650',
34
+ outlined:
35
+ 'border border-rc-gray-500 bg-rc-gray-900 font-semibold hover:bg-rc-gray-700 hover:border-rc-gray-400 focus:outline-none focus:ring-0 focus:border-rc-gray-400 active:border-rc-gray-400',
36
+ ghost: 'bg-transparent rounded-lg hover:text-gray-100 focus:text-gray-100 active:text-gray-100'
37
+ }
38
+
39
+ const sizeClasses = {
40
+ big: 'px-4 py-2 text-base',
41
+ medium: 'px-4 py-2 text-base',
42
+ small: 'px-4 py-2 text-base'
43
+ }
44
+
45
+ let buttonClasses = $derived(
46
+ `text-gray-200
47
+ rounded
48
+ transition-colors
49
+ disabled:opacity-50
50
+ disabled:cursor-not-allowed
51
+ disabled:pointer-events-none
52
+ ${variantClasses[variant]}
53
+ ${sizeClasses[size]}
54
+ cursor-pointer
55
+ ${className}
56
+ `.trim()
57
+ )
58
+ </script>
59
+
60
+ <button bind:this={el} {type} class={buttonClasses} {disabled} {...rest}>
61
+ {@render children()}
62
+ </button>