@agenthippo-ai/agenthippo 0.1.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 (480) hide show
  1. package/LICENSE +21 -0
  2. package/bin/agenthippo.js +2 -0
  3. package/out/agentpack/github.js +547 -0
  4. package/out/agentpack/index.js +66 -0
  5. package/out/agentpack/install.js +297 -0
  6. package/out/agentpack/manager.js +441 -0
  7. package/out/agentpack/manifest.js +135 -0
  8. package/out/agentpack/paths.js +475 -0
  9. package/out/agentpack/publish.js +248 -0
  10. package/out/agentpack/stores.js +554 -0
  11. package/out/agentpack/types.js +5 -0
  12. package/out/analytics/dashboard/artifactManager.js +439 -0
  13. package/out/analytics/dashboard/fleetDisplayUtils.js +159 -0
  14. package/out/analytics/dashboard/fleetOrchestrator.js +142 -0
  15. package/out/analytics/dashboard/fleetRegistry.js +79 -0
  16. package/out/analytics/dashboard/fleetRowDefaults.js +87 -0
  17. package/out/analytics/dashboard/fleetScriptAdapter.js +146 -0
  18. package/out/analytics/dashboard/fleetStatusUtils.js +30 -0
  19. package/out/analytics/dashboard/fleetTitleUtils.js +68 -0
  20. package/out/analytics/dashboard/fleetTypes.js +29 -0
  21. package/out/analytics/dashboard/index.js +17 -0
  22. package/out/analytics/dashboard/indexingService.js +422 -0
  23. package/out/analytics/dashboard/localFleetAdapter.js +222 -0
  24. package/out/analytics/dashboard/performanceMonitor.js +76 -0
  25. package/out/analytics/dashboard/processManager.js +209 -0
  26. package/out/analytics/dashboard/provider.js +3885 -0
  27. package/out/analytics/dashboard/queryRunner.js +994 -0
  28. package/out/analytics/dashboard/refreshPolicy.js +16 -0
  29. package/out/analytics/dashboard/scheduledFleetFingerprint.js +22 -0
  30. package/out/analytics/dashboard/scheduledFleetStorage.js +241 -0
  31. package/out/analytics/dashboard/scheduledFleetUtils.js +22 -0
  32. package/out/analytics/dashboard/sessionEventFleetStats.js +118 -0
  33. package/out/analytics/dashboard/sessionJsonlFleet.js +204 -0
  34. package/out/analytics/dashboard/teamViewDiscovery.js +106 -0
  35. package/out/analytics/dashboard/viewsConfigMerge.js +221 -0
  36. package/out/analytics/dashboard/webview/analytics-panel.html +155 -0
  37. package/out/analytics/dashboard/webview/codicon.ttf +0 -0
  38. package/out/analytics/dashboard/webview/index.html +78 -0
  39. package/out/analytics/dashboard/webview/panel.js +2295 -0
  40. package/out/analytics/dashboard/webview/runtime-bridge.js +82 -0
  41. package/out/analytics/dashboard/webview/styles.css +1009 -0
  42. package/out/analytics/lifecycle.js +43 -0
  43. package/out/analytics/service.js +1279 -0
  44. package/out/analytics/sessionEvents.js +231 -0
  45. package/out/auth/bootstrapApply.js +72 -0
  46. package/out/auth/brokerClient.js +126 -0
  47. package/out/auth/callbackUri.js +59 -0
  48. package/out/auth/callbackUriParse.js +59 -0
  49. package/out/auth/githubStoreGate.js +70 -0
  50. package/out/auth/loopbackAuthServer.js +165 -0
  51. package/out/auth/pkce.js +9 -0
  52. package/out/auth/provider.js +239 -0
  53. package/out/auth/sessionStore.js +193 -0
  54. package/out/auth/signInFlow.js +182 -0
  55. package/out/auth/types.js +12 -0
  56. package/out/auth-broker/config.js +62 -0
  57. package/out/auth-broker/jwt.js +105 -0
  58. package/out/auth-broker/pkce.js +57 -0
  59. package/out/auth-broker/server.js +539 -0
  60. package/out/auth-broker/store.js +273 -0
  61. package/out/base-prompts/gateway/openclaw-starter.json5 +144 -0
  62. package/out/base-prompts/gateway/slack-socket-mode-minimal.manifest.json +44 -0
  63. package/out/base-prompts/mcp-servers/.mcp.json +13 -0
  64. package/out/base-prompts/mcp-servers/langfuse-analytics/README.md +142 -0
  65. package/out/base-prompts/mcp-servers/langfuse-analytics/mcp.json +13 -0
  66. package/out/base-prompts/mcp-servers/langfuse-analytics/package-lock.json +1192 -0
  67. package/out/base-prompts/mcp-servers/langfuse-analytics/package.json +23 -0
  68. package/out/base-prompts/mcp-servers/langfuse-analytics/scripts/run-with-env.sh +12 -0
  69. package/out/base-prompts/mcp-servers/langfuse-analytics/scripts/sdk-test.js +102 -0
  70. package/out/base-prompts/mcp-servers/langfuse-analytics/scripts/simple-test.js +137 -0
  71. package/out/base-prompts/mcp-servers/langfuse-analytics/scripts/test-client.js +118 -0
  72. package/out/base-prompts/mcp-servers/langfuse-analytics/scripts/test-server.js +209 -0
  73. package/out/base-prompts/mcp-servers/langfuse-analytics/src/client.ts +431 -0
  74. package/out/base-prompts/mcp-servers/langfuse-analytics/src/index.ts +270 -0
  75. package/out/base-prompts/mcp-servers/langfuse-analytics/tsconfig.json +18 -0
  76. package/out/base-prompts/rules/AGENTS.md +204 -0
  77. package/out/base-prompts/skills/agent-browser/SKILL.md +102 -0
  78. package/out/base-prompts/skills/agent-builder/SKILL.md +73 -0
  79. package/out/base-prompts/skills/agent-builder/references/agent-pack.md +63 -0
  80. package/out/base-prompts/skills/agent-builder/references/crewai.md +66 -0
  81. package/out/base-prompts/skills/agent-builder/references/langchain.md +60 -0
  82. package/out/base-prompts/skills/agent-builder/references/raw-sdk.md +73 -0
  83. package/out/base-prompts/skills/agent-engine-creator/SKILL.md +586 -0
  84. package/out/base-prompts/skills/agent-engine-installer/SKILL.md +106 -0
  85. package/out/base-prompts/skills/agent-engine-installer/scripts/install-engine.mjs +259 -0
  86. package/out/base-prompts/skills/agent-engine-installer/scripts/install-engine.ps1 +189 -0
  87. package/out/base-prompts/skills/agenthippo-cli/SKILL.md +69 -0
  88. package/out/base-prompts/skills/agenthippo-cli/scripts/install.sh +63 -0
  89. package/out/base-prompts/skills/agentpack-creator/SKILL.md +375 -0
  90. package/out/base-prompts/skills/agentpack-creator/references/agent-templates.md +261 -0
  91. package/out/base-prompts/skills/agentpack-creator/references/agents-md-guide.md +195 -0
  92. package/out/base-prompts/skills/agentpack-creator/scripts/init_pack.py +391 -0
  93. package/out/base-prompts/skills/agentpack-creator/scripts/init_pack_test.py +294 -0
  94. package/out/base-prompts/skills/agentpack-testbench/SKILL.md +135 -0
  95. package/out/base-prompts/skills/algorithmic-art/LICENSE.txt +202 -0
  96. package/out/base-prompts/skills/algorithmic-art/SKILL.md +405 -0
  97. package/out/base-prompts/skills/algorithmic-art/templates/generator_template.js +223 -0
  98. package/out/base-prompts/skills/algorithmic-art/templates/viewer.html +599 -0
  99. package/out/base-prompts/skills/debug-agent/SKILL.md +83 -0
  100. package/out/base-prompts/skills/desktop-automation/SKILL.md +166 -0
  101. package/out/base-prompts/skills/desktop-automation/references/agenthippo-ui-recipes.md +441 -0
  102. package/out/base-prompts/skills/desktop-automation/scripts/windows-desktop-uia.psm1 +280 -0
  103. package/out/base-prompts/skills/docx/LICENSE.txt +30 -0
  104. package/out/base-prompts/skills/docx/SKILL.md +197 -0
  105. package/out/base-prompts/skills/docx/docx-js.md +350 -0
  106. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
  107. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
  108. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
  109. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
  110. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
  111. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
  112. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
  113. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
  114. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
  115. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
  116. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
  117. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
  118. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
  119. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
  120. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
  121. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
  122. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
  123. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
  124. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
  125. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
  126. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
  127. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
  128. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
  129. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
  130. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
  131. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
  132. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
  133. package/out/base-prompts/skills/docx/ooxml/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
  134. package/out/base-prompts/skills/docx/ooxml/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
  135. package/out/base-prompts/skills/docx/ooxml/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
  136. package/out/base-prompts/skills/docx/ooxml/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
  137. package/out/base-prompts/skills/docx/ooxml/schemas/mce/mc.xsd +75 -0
  138. package/out/base-prompts/skills/docx/ooxml/schemas/microsoft/wml-2010.xsd +560 -0
  139. package/out/base-prompts/skills/docx/ooxml/schemas/microsoft/wml-2012.xsd +67 -0
  140. package/out/base-prompts/skills/docx/ooxml/schemas/microsoft/wml-2018.xsd +14 -0
  141. package/out/base-prompts/skills/docx/ooxml/schemas/microsoft/wml-cex-2018.xsd +20 -0
  142. package/out/base-prompts/skills/docx/ooxml/schemas/microsoft/wml-cid-2016.xsd +13 -0
  143. package/out/base-prompts/skills/docx/ooxml/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
  144. package/out/base-prompts/skills/docx/ooxml/schemas/microsoft/wml-symex-2015.xsd +8 -0
  145. package/out/base-prompts/skills/docx/ooxml/scripts/pack.py +159 -0
  146. package/out/base-prompts/skills/docx/ooxml/scripts/unpack.py +29 -0
  147. package/out/base-prompts/skills/docx/ooxml/scripts/validate.py +69 -0
  148. package/out/base-prompts/skills/docx/ooxml/scripts/validation/__init__.py +15 -0
  149. package/out/base-prompts/skills/docx/ooxml/scripts/validation/base.py +951 -0
  150. package/out/base-prompts/skills/docx/ooxml/scripts/validation/docx.py +274 -0
  151. package/out/base-prompts/skills/docx/ooxml/scripts/validation/pptx.py +315 -0
  152. package/out/base-prompts/skills/docx/ooxml/scripts/validation/redlining.py +279 -0
  153. package/out/base-prompts/skills/docx/ooxml.md +610 -0
  154. package/out/base-prompts/skills/docx/scripts/__init__.py +1 -0
  155. package/out/base-prompts/skills/docx/scripts/document.py +1276 -0
  156. package/out/base-prompts/skills/docx/scripts/templates/comments.xml +3 -0
  157. package/out/base-prompts/skills/docx/scripts/templates/commentsExtended.xml +3 -0
  158. package/out/base-prompts/skills/docx/scripts/templates/commentsExtensible.xml +3 -0
  159. package/out/base-prompts/skills/docx/scripts/templates/commentsIds.xml +3 -0
  160. package/out/base-prompts/skills/docx/scripts/templates/people.xml +3 -0
  161. package/out/base-prompts/skills/docx/scripts/utilities.py +374 -0
  162. package/out/base-prompts/skills/gateway-outbound/SKILL.md +136 -0
  163. package/out/base-prompts/skills/mcp-builder/LICENSE.txt +202 -0
  164. package/out/base-prompts/skills/mcp-builder/SKILL.md +328 -0
  165. package/out/base-prompts/skills/mcp-builder/reference/evaluation.md +602 -0
  166. package/out/base-prompts/skills/mcp-builder/reference/mcp_best_practices.md +249 -0
  167. package/out/base-prompts/skills/mcp-builder/reference/node_mcp_server.md +970 -0
  168. package/out/base-prompts/skills/mcp-builder/reference/python_mcp_server.md +719 -0
  169. package/out/base-prompts/skills/mcp-builder/scripts/connections.py +151 -0
  170. package/out/base-prompts/skills/mcp-builder/scripts/evaluation.py +373 -0
  171. package/out/base-prompts/skills/mcp-builder/scripts/example_evaluation.xml +22 -0
  172. package/out/base-prompts/skills/mcp-builder/scripts/requirements.txt +2 -0
  173. package/out/base-prompts/skills/openclaw/SKILL.md +406 -0
  174. package/out/base-prompts/skills/openclaw/scripts/run-smoke.sh +61 -0
  175. package/out/base-prompts/skills/pdf/LICENSE.txt +30 -0
  176. package/out/base-prompts/skills/pdf/SKILL.md +294 -0
  177. package/out/base-prompts/skills/pdf/forms.md +205 -0
  178. package/out/base-prompts/skills/pdf/reference.md +612 -0
  179. package/out/base-prompts/skills/pdf/scripts/check_bounding_boxes.py +70 -0
  180. package/out/base-prompts/skills/pdf/scripts/check_bounding_boxes_test.py +226 -0
  181. package/out/base-prompts/skills/pdf/scripts/check_fillable_fields.py +12 -0
  182. package/out/base-prompts/skills/pdf/scripts/convert_pdf_to_images.py +35 -0
  183. package/out/base-prompts/skills/pdf/scripts/create_validation_image.py +41 -0
  184. package/out/base-prompts/skills/pdf/scripts/extract_form_field_info.py +152 -0
  185. package/out/base-prompts/skills/pdf/scripts/fill_fillable_fields.py +114 -0
  186. package/out/base-prompts/skills/pdf/scripts/fill_pdf_form_with_annotations.py +108 -0
  187. package/out/base-prompts/skills/pptx/LICENSE.txt +30 -0
  188. package/out/base-prompts/skills/pptx/SKILL.md +484 -0
  189. package/out/base-prompts/skills/pptx/html2pptx.md +625 -0
  190. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
  191. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
  192. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
  193. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
  194. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
  195. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
  196. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
  197. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
  198. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
  199. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
  200. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
  201. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
  202. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
  203. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
  204. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
  205. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
  206. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
  207. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
  208. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
  209. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
  210. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
  211. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
  212. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
  213. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
  214. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
  215. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
  216. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
  217. package/out/base-prompts/skills/pptx/ooxml/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
  218. package/out/base-prompts/skills/pptx/ooxml/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
  219. package/out/base-prompts/skills/pptx/ooxml/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
  220. package/out/base-prompts/skills/pptx/ooxml/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
  221. package/out/base-prompts/skills/pptx/ooxml/schemas/mce/mc.xsd +75 -0
  222. package/out/base-prompts/skills/pptx/ooxml/schemas/microsoft/wml-2010.xsd +560 -0
  223. package/out/base-prompts/skills/pptx/ooxml/schemas/microsoft/wml-2012.xsd +67 -0
  224. package/out/base-prompts/skills/pptx/ooxml/schemas/microsoft/wml-2018.xsd +14 -0
  225. package/out/base-prompts/skills/pptx/ooxml/schemas/microsoft/wml-cex-2018.xsd +20 -0
  226. package/out/base-prompts/skills/pptx/ooxml/schemas/microsoft/wml-cid-2016.xsd +13 -0
  227. package/out/base-prompts/skills/pptx/ooxml/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
  228. package/out/base-prompts/skills/pptx/ooxml/schemas/microsoft/wml-symex-2015.xsd +8 -0
  229. package/out/base-prompts/skills/pptx/ooxml/scripts/pack.py +159 -0
  230. package/out/base-prompts/skills/pptx/ooxml/scripts/unpack.py +29 -0
  231. package/out/base-prompts/skills/pptx/ooxml/scripts/validate.py +69 -0
  232. package/out/base-prompts/skills/pptx/ooxml/scripts/validation/__init__.py +15 -0
  233. package/out/base-prompts/skills/pptx/ooxml/scripts/validation/base.py +951 -0
  234. package/out/base-prompts/skills/pptx/ooxml/scripts/validation/docx.py +274 -0
  235. package/out/base-prompts/skills/pptx/ooxml/scripts/validation/pptx.py +315 -0
  236. package/out/base-prompts/skills/pptx/ooxml/scripts/validation/redlining.py +279 -0
  237. package/out/base-prompts/skills/pptx/ooxml.md +427 -0
  238. package/out/base-prompts/skills/pptx/scripts/html2pptx.js +984 -0
  239. package/out/base-prompts/skills/pptx/scripts/inventory.py +1020 -0
  240. package/out/base-prompts/skills/pptx/scripts/rearrange.py +231 -0
  241. package/out/base-prompts/skills/pptx/scripts/replace.py +385 -0
  242. package/out/base-prompts/skills/pptx/scripts/thumbnail.py +450 -0
  243. package/out/base-prompts/skills/prompt-lab/SKILL.md +149 -0
  244. package/out/base-prompts/skills/prompt-lab/scripts/query-analytics.sh +2551 -0
  245. package/out/base-prompts/skills/skill-creator/LICENSE.txt +202 -0
  246. package/out/base-prompts/skills/skill-creator/SKILL.md +371 -0
  247. package/out/base-prompts/skills/skill-creator/references/output-patterns.md +82 -0
  248. package/out/base-prompts/skills/skill-creator/references/workflows.md +28 -0
  249. package/out/base-prompts/skills/skill-creator/scripts/init_skill.py +303 -0
  250. package/out/base-prompts/skills/skill-creator/scripts/package_skill.py +110 -0
  251. package/out/base-prompts/skills/skill-creator/scripts/quick_validate.py +95 -0
  252. package/out/base-prompts/skills/skill-installer/SKILL.md +251 -0
  253. package/out/base-prompts/skills/skill-installer/references/store-api.md +219 -0
  254. package/out/base-prompts/skills/spotlight-dashboard/SKILL.md +83 -0
  255. package/out/base-prompts/skills/spotlight-insights/SKILL.md +101 -0
  256. package/out/base-prompts/skills/spotlight-optimize/SKILL.md +93 -0
  257. package/out/base-prompts/skills/spotlight-optimize/scripts/query-analytics.sh +2551 -0
  258. package/out/base-prompts/skills/spotlight-query/SKILL.md +314 -0
  259. package/out/base-prompts/skills/spotlight-query/references/otel-schema.md +163 -0
  260. package/out/base-prompts/skills/spotlight-query/scripts/query-analytics.sh +2551 -0
  261. package/out/base-prompts/skills/spotlight-query/scripts/query-analytics.test.sh +744 -0
  262. package/out/base-prompts/skills/spotlight-query/scripts/test-fixtures/analytics/sessions/sess-test-1.jsonl +14 -0
  263. package/out/base-prompts/skills/spotlight-query/scripts/test-fixtures/analytics/traces/.duckdb-index.json +7 -0
  264. package/out/base-prompts/skills/spotlight-query/scripts/test-fixtures/analytics/traces/sample.jsonl +5 -0
  265. package/out/base-prompts/skills/spotlight-report/SKILL.md +86 -0
  266. package/out/base-prompts/skills/spotlight-visualize/SKILL.md +92 -0
  267. package/out/base-prompts/skills/spotlight-visualize/scripts/query-analytics.sh +2551 -0
  268. package/out/base-prompts/skills/xlsx/LICENSE.txt +30 -0
  269. package/out/base-prompts/skills/xlsx/SKILL.md +289 -0
  270. package/out/base-prompts/skills/xlsx/recalc.py +178 -0
  271. package/out/branding.js +23 -0
  272. package/out/cli/commands/anywhere.js +66 -0
  273. package/out/cli/commands/ask.js +243 -0
  274. package/out/cli/commands/auth-broker.js +24 -0
  275. package/out/cli/commands/auth.js +219 -0
  276. package/out/cli/commands/chat.js +354 -0
  277. package/out/cli/commands/deploy.js +223 -0
  278. package/out/cli/commands/gateway.js +160 -0
  279. package/out/cli/commands/pack.js +220 -0
  280. package/out/cli/commands/probe-custom-engine.js +82 -0
  281. package/out/cli/commands/serve.js +583 -0
  282. package/out/cli/commands/store.js +919 -0
  283. package/out/cli/credential-context.js +113 -0
  284. package/out/cli/deploy-init.js +2087 -0
  285. package/out/cli/deps.js +238 -0
  286. package/out/cli/index.js +390 -0
  287. package/out/cli/pack-loader.js +376 -0
  288. package/out/cli/run-custom-engine-runner.js +318 -0
  289. package/out/cli/runtime.js +409 -0
  290. package/out/cli/serve-hardening.js +325 -0
  291. package/out/cli/state-sync.js +135 -0
  292. package/out/cli/utils.js +220 -0
  293. package/out/cli/vscode-shim.js +86 -0
  294. package/out/engines/base.js +159 -0
  295. package/out/engines/claude.js +1215 -0
  296. package/out/engines/codex.js +759 -0
  297. package/out/engines/custom.js +632 -0
  298. package/out/engines/customEngineIpc.js +95 -0
  299. package/out/engines/customEngineRunnerEmbed.js +107 -0
  300. package/out/engines/customRegistry.js +274 -0
  301. package/out/engines/embeddedCustomEngineRunner.generated.js +9 -0
  302. package/out/engines/engineApiKey.js +138 -0
  303. package/out/engines/factory.js +45 -0
  304. package/out/engines/install.js +372 -0
  305. package/out/engines/persistentEngineWorker.js +320 -0
  306. package/out/engines/publish.js +341 -0
  307. package/out/engines/types.js +17 -0
  308. package/out/examples/run-headless-repl.js +291 -0
  309. package/out/examples/run-headless.js +187 -0
  310. package/out/extension.js +4472 -0
  311. package/out/gateway/bridge.js +805 -0
  312. package/out/gateway/cli-agent-id.js +36 -0
  313. package/out/gateway/client.js +303 -0
  314. package/out/gateway/client.test.js +395 -0
  315. package/out/gateway/gateway-scope-errors.js +14 -0
  316. package/out/gateway/gatewayBackup.js +76 -0
  317. package/out/gateway/gatewayRouteIndex.js +371 -0
  318. package/out/gateway/gatewaySessionStore.js +243 -0
  319. package/out/gateway/index.js +31 -0
  320. package/out/gateway/lifecycle.js +3056 -0
  321. package/out/gateway/openclaw-channel-routing.js +317 -0
  322. package/out/gateway/outboundHandles.js +373 -0
  323. package/out/gateway/outboundPaths.js +72 -0
  324. package/out/gateway/outboundSend.js +254 -0
  325. package/out/gateway/protocol.js +48 -0
  326. package/out/gateway/resetMappings.js +52 -0
  327. package/out/gateway/service.js +1244 -0
  328. package/out/gateway/sessionRouting.js +77 -0
  329. package/out/headless/index.js +505 -0
  330. package/out/headless/sync.js +473 -0
  331. package/out/mcp/publish.js +143 -0
  332. package/out/mcp.js +597 -0
  333. package/out/permissions.js +108 -0
  334. package/out/permissions.mapping.yaml +68 -0
  335. package/out/registry/index.js +332 -0
  336. package/out/remote/handleRemoteChat.js +122 -0
  337. package/out/remote/remotePackStatusBar.js +103 -0
  338. package/out/remote/serveClient.js +216 -0
  339. package/out/remoteSkills.js +191 -0
  340. package/out/runtime/index.js +32 -0
  341. package/out/runtime/interfaces/config-provider.js +32 -0
  342. package/out/runtime/interfaces/edit-tracker.js +22 -0
  343. package/out/runtime/interfaces/index.js +29 -0
  344. package/out/runtime/interfaces/logger.js +40 -0
  345. package/out/runtime/interfaces/permission-handler.js +66 -0
  346. package/out/runtime/interfaces/session-store.js +342 -0
  347. package/out/runtime/vscode/index.js +20 -0
  348. package/out/runtime/vscode/vscode-config-provider.js +77 -0
  349. package/out/runtime/vscode/vscode-edit-tracker.js +120 -0
  350. package/out/runtime/vscode/vscode-logger.js +30 -0
  351. package/out/runtime/vscode/vscode-permission-handler.js +72 -0
  352. package/out/runtime/vscode/vscode-session-store.js +22 -0
  353. package/out/scripts/agentide-litellm.ps1 +501 -0
  354. package/out/scripts/agentide-litellm.sh +700 -0
  355. package/out/scripts/agentide_litellm_hooks_v1.py +604 -0
  356. package/out/scripts/analytics/agent-fleet-widget.js +954 -0
  357. package/out/scripts/analytics/agentide-litellm-otel.config.yaml +160 -0
  358. package/out/scripts/analytics/default-seed-scripts.json +12 -0
  359. package/out/scripts/analytics/duckdbLocator.js +29 -0
  360. package/out/scripts/analytics/fleet-local-adapter.js +246 -0
  361. package/out/scripts/analytics/fleet-workflow.sh +263 -0
  362. package/out/scripts/analytics/generate-default-charts.py +685 -0
  363. package/out/scripts/analytics/hotspot-analysis-widget.js +909 -0
  364. package/out/scripts/analytics/index-traces.py +1394 -0
  365. package/out/scripts/analytics/install-analytics.sh +286 -0
  366. package/out/scripts/analytics/live-fleet-4-collector.cjs +315 -0
  367. package/out/scripts/analytics/live-fleet-4-daemon.mjs +134 -0
  368. package/out/scripts/analytics/live-fleet-4-widget.cjs +433 -0
  369. package/out/scripts/analytics/otel-collector.yaml +55 -0
  370. package/out/scripts/analytics/query-analytics.ps1 +396 -0
  371. package/out/scripts/analytics/query-analytics.sh +2551 -0
  372. package/out/scripts/analytics/session-comparison-widget.js +1468 -0
  373. package/out/scripts/analytics/session-timeline-widget.js +2137 -0
  374. package/out/scripts/analytics/setup-analytics.ps1 +432 -0
  375. package/out/scripts/analytics/setup-analytics.sh +854 -0
  376. package/out/scripts/analytics/start-analytics.ps1 +169 -0
  377. package/out/scripts/analytics/start-analytics.sh +323 -0
  378. package/out/scripts/analytics/token-cost-flamegraph-widget.js +954 -0
  379. package/out/scripts/analytics/token-cost-trend-widget.js +1062 -0
  380. package/out/scripts/analytics/workflows-collector.cjs +315 -0
  381. package/out/scripts/analytics/workflows-daemon.mjs +186 -0
  382. package/out/scripts/analytics/workflows-widget.cjs +433 -0
  383. package/out/scripts/analytics/wow-model-widget.js +191 -0
  384. package/out/scripts/gateway/Dockerfile.serve +45 -0
  385. package/out/scripts/gateway/docker-serve.sh +248 -0
  386. package/out/scripts/gateway/setup-gateway-server.ps1 +953 -0
  387. package/out/scripts/gateway/setup-gateway-server.sh +935 -0
  388. package/out/scripts/gateway/setup-gateway.ps1 +1813 -0
  389. package/out/scripts/gateway/setup-gateway.sh +1541 -0
  390. package/out/scripts/setup-cli.ps1 +86 -0
  391. package/out/scripts/setup-cli.sh +117 -0
  392. package/out/scripts/tools-manifest.json +83 -0
  393. package/out/scripts/vendor-tools.mjs +234 -0
  394. package/out/session.js +272 -0
  395. package/out/shared/agentEngines.js +170 -0
  396. package/out/shared/index.js +109 -0
  397. package/out/shared/models.js +235 -0
  398. package/out/shared/providers.js +202 -0
  399. package/out/skills/publish.js +178 -0
  400. package/out/spotlight/install.js +339 -0
  401. package/out/spotlight/publish.js +544 -0
  402. package/out/spotlight/yaml.js +31 -0
  403. package/out/store/github/setup.js +235 -0
  404. package/out/store/github/templates/build-index.ts +339 -0
  405. package/out/store/github/templates/build-index.yml +97 -0
  406. package/out/store/github/templates/package.json +11 -0
  407. package/out/store/index.js +755 -0
  408. package/out/store/oramaIndex.js +884 -0
  409. package/out/store/shared.js +241 -0
  410. package/out/sync.js +1827 -0
  411. package/out/terminalLogger.js +358 -0
  412. package/out/test/agentEnvFile.test.js +97 -0
  413. package/out/test/agentYaml.test.js +373 -0
  414. package/out/test/agentpack.test.js +639 -0
  415. package/out/test/analytics-lifecycle.test.js +174 -0
  416. package/out/test/analytics.test.js +524 -0
  417. package/out/test/auth-bootstrapApply.test.js +102 -0
  418. package/out/test/auth-broker.test.js +434 -0
  419. package/out/test/auth-callback.test.js +62 -0
  420. package/out/test/branding.test.js +229 -0
  421. package/out/test/cli-agent-id.test.js +55 -0
  422. package/out/test/credential-context.test.js +105 -0
  423. package/out/test/customEngineInstall.test.js +100 -0
  424. package/out/test/customRegistry.test.js +122 -0
  425. package/out/test/deploy-init.test.js +532 -0
  426. package/out/test/deps-auto-install.test.js +81 -0
  427. package/out/test/engine-integration.test.js +297 -0
  428. package/out/test/enginePaths.test.js +195 -0
  429. package/out/test/enginePublish.test.js +82 -0
  430. package/out/test/fleetDisplayUtils.test.js +96 -0
  431. package/out/test/gateway-client.test.js +381 -0
  432. package/out/test/gateway-lifecycle.test.js +608 -0
  433. package/out/test/gateway-outbound.test.js +531 -0
  434. package/out/test/gateway-reset-mappings.test.js +221 -0
  435. package/out/test/gateway-route-index.test.js +235 -0
  436. package/out/test/gateway-session-routing.test.js +47 -0
  437. package/out/test/gateway-session-store.test.js +216 -0
  438. package/out/test/github.test.js +232 -0
  439. package/out/test/headless-sync.test.js +279 -0
  440. package/out/test/helpers/manifest-fixtures.js +27 -0
  441. package/out/test/mcp-obo.test.js +82 -0
  442. package/out/test/mcp.test.js +307 -0
  443. package/out/test/openclaw-channel-routing.test.js +191 -0
  444. package/out/test/pathSafety.test.js +63 -0
  445. package/out/test/permissions.test.js +175 -0
  446. package/out/test/persistent-engine-worker.test.js +220 -0
  447. package/out/test/refreshPolicy.test.js +60 -0
  448. package/out/test/registry.test.js +351 -0
  449. package/out/test/runtime.test.js +349 -0
  450. package/out/test/scheduledFleetStorage.test.js +72 -0
  451. package/out/test/serve-hardening.test.js +192 -0
  452. package/out/test/serveClient.test.js +173 -0
  453. package/out/test/sessionJsonlFleet.test.js +78 -0
  454. package/out/test/shared.test.js +173 -0
  455. package/out/test/skills-mcp.test.js +299 -0
  456. package/out/test/spotlightPublish.test.js +299 -0
  457. package/out/test/spotlightYaml.test.js +58 -0
  458. package/out/test/state-sync-peer.test.js +74 -0
  459. package/out/test/store.test.js +331 -0
  460. package/out/test/stores.test.js +504 -0
  461. package/out/test/sync-switch.test.js +251 -0
  462. package/out/test/teamViewDiscovery.test.js +90 -0
  463. package/out/test/toolPath.test.js +120 -0
  464. package/out/test/utils.test.js +308 -0
  465. package/out/test/viewsConfigMerge.test.js +180 -0
  466. package/out/test/windows-command-line.test.js +75 -0
  467. package/out/utils/agentEnvFile.js +131 -0
  468. package/out/utils/binary.js +175 -0
  469. package/out/utils/index.js +37 -0
  470. package/out/utils/network.js +74 -0
  471. package/out/utils/pathSafety.js +60 -0
  472. package/out/utils/paths.js +116 -0
  473. package/out/utils/pid.js +180 -0
  474. package/out/utils/pythonRunner.js +258 -0
  475. package/out/utils/resolveNodeBinary.js +168 -0
  476. package/out/utils/toolPath.js +83 -0
  477. package/out/utils/windowsCommandLine.js +29 -0
  478. package/out/utils.js +915 -0
  479. package/out/workspaceFolders.js +59 -0
  480. package/package.json +41 -0
@@ -0,0 +1,2551 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Copyright (c) AgentHippo.ai. All rights reserved.
4
+ #
5
+ # Query AgentHippo Analytics with DuckDB
6
+ #
7
+ # Run with --help for full usage. Key queries:
8
+ #
9
+ # Cost: --cost, --cost-by-session, --cost-by-model, --cost-by-pack
10
+ # Debug: --session <id>, --request <id>, --latest-session
11
+ # --session-messages <id> (parse messages directly from traces)
12
+ # Regress: --compare-versions <agent>, --top-sessions <agent>
13
+ # Health: --problems, --errors, --largest-requests, --tool-loops <id>
14
+ # Custom: --sql "SELECT ..." (can use ${BASE_QUERY} and ${EXTRACT_ATTRS} CTEs)
15
+ #
16
+
17
+ set -euo pipefail
18
+
19
+ # AGENT_HOME_DIR is set by the extension with the branded directory path
20
+ # Falls back to ~/.agent-hippo for manual script runs
21
+ AGENT_HOME_DIR="${AGENT_HOME_DIR:-$HOME/.agent-hippo}"
22
+ ANALYTICS_DIR="${AGENT_HOME_DIR}/analytics"
23
+ BIN_DIR="${AGENT_HOME_DIR}/bin"
24
+ # Prefer explicit traces dir from dashboard env; fall back to default analytics path.
25
+ TRACES_DIR="${AGENTIDE_TRACES_DIR:-${ANALYTICS_DIR}/traces}"
26
+ INDEX_DB_PATH="${AGENTIDE_INDEX_DB_PATH:-${TRACES_DIR}/analytics.duckdb}"
27
+ TMP_DIR="${AGENTIDE_ANALYTICS_TMP_DIR:-${ANALYTICS_DIR}/tmp}"
28
+ MESSAGE_PREVIEW_CHARS="${AGENTIDE_SPOTLIGHT_MESSAGE_PREVIEW_CHARS:-800}"
29
+ MESSAGE_PART_PREVIEW_CHARS="${AGENTIDE_SPOTLIGHT_MESSAGE_PART_PREVIEW_CHARS:-400}"
30
+ MESSAGE_OUTPUT_BUDGET_CHARS="${AGENTIDE_SPOTLIGHT_OUTPUT_BUDGET_CHARS:-12000}"
31
+ TRACE_EVENT_PREVIEW_LIMIT="${AGENTIDE_SPOTLIGHT_TRACE_EVENT_PREVIEW_LIMIT:-12}"
32
+ SESSION_DEBUG_REQUEST_LIMIT="${AGENTIDE_SPOTLIGHT_SESSION_DEBUG_REQUEST_LIMIT:-6}"
33
+ SESSION_DEBUG_TOOL_LIMIT="${AGENTIDE_SPOTLIGHT_SESSION_DEBUG_TOOL_LIMIT:-8}"
34
+
35
+ # Find duckdb binary
36
+ # Use indexed DB when available for fast session/message queries.
37
+ find_duckdb() {
38
+ if command -v duckdb &> /dev/null; then
39
+ echo "duckdb"
40
+ elif [[ -x "${BIN_DIR}/duckdb" ]]; then
41
+ echo "${BIN_DIR}/duckdb"
42
+ else
43
+ echo "ERROR: DuckDB not found. Run install-analytics.sh first." >&2
44
+ exit 1
45
+ fi
46
+ }
47
+
48
+ DUCKDB=$(find_duckdb)
49
+
50
+ # Check if traces exist
51
+ check_traces() {
52
+ if ! ls "${TRACES_DIR}"/*.jsonl &> /dev/null; then
53
+ echo "No trace files found in ${TRACES_DIR}/"
54
+ echo "Make sure OTEL Collector is running and LiteLLM has the 'otel' callback enabled."
55
+ exit 0
56
+ fi
57
+ }
58
+
59
+ has_indexed_db() {
60
+ if [[ ! -f "${INDEX_DB_PATH}" ]]; then
61
+ return 1
62
+ fi
63
+
64
+ local table_count
65
+ table_count="$(${DUCKDB} -readonly "${INDEX_DB_PATH}" -csv -c "
66
+ SELECT COUNT(DISTINCT table_name)
67
+ FROM information_schema.tables
68
+ WHERE table_name IN ('model_calls', 'sessions', 'turns');
69
+ " 2>/dev/null | tail -n 1)"
70
+
71
+ [[ "${table_count}" == "3" ]]
72
+ }
73
+
74
+ has_indexed_table() {
75
+ local table_name="$1"
76
+ if [[ ! -f "${INDEX_DB_PATH}" ]]; then
77
+ return 1
78
+ fi
79
+ local found
80
+ found="$(${DUCKDB} -readonly "${INDEX_DB_PATH}" -csv -c "
81
+ SELECT COUNT(*)
82
+ FROM information_schema.tables
83
+ WHERE table_name = '$(escape_sql_literal "${table_name}")';
84
+ " 2>/dev/null | tail -n 1)"
85
+ [[ "${found}" == "1" ]]
86
+ }
87
+
88
+ run_duckdb_sql() {
89
+ local sql="$1"
90
+ if [[ -f "${INDEX_DB_PATH}" ]]; then
91
+ ${DUCKDB} -readonly "${INDEX_DB_PATH}" -c "${sql}"
92
+ else
93
+ ${DUCKDB} -c "${sql}"
94
+ fi
95
+ }
96
+
97
+ run_duckdb_json_sql() {
98
+ local sql="$1"
99
+ if [[ -f "${INDEX_DB_PATH}" ]]; then
100
+ ${DUCKDB} -readonly "${INDEX_DB_PATH}" -json -c "${sql}"
101
+ else
102
+ ${DUCKDB} -json -c "${sql}"
103
+ fi
104
+ }
105
+
106
+ escape_sql_literal() {
107
+ local value="${1:-}"
108
+ printf "%s" "${value//\'/'\'}"
109
+ }
110
+
111
+ sanitize_artifact_label() {
112
+ local value="${1:-artifact}"
113
+ printf "%s" "${value}" | tr -cs 'A-Za-z0-9._-' '_'
114
+ }
115
+
116
+ ensure_tmp_dir() {
117
+ mkdir -p "${TMP_DIR}"
118
+ }
119
+
120
+ make_artifact_path() {
121
+ local prefix="${1:-spotlight-query}"
122
+ local suffix="${2:-txt}"
123
+ local path
124
+ ensure_tmp_dir
125
+ path="$(mktemp "${TMP_DIR}/$(sanitize_artifact_label "${prefix}")-XXXXXX")"
126
+ if [[ -n "${suffix}" ]]; then
127
+ mv "${path}" "${path}.${suffix}"
128
+ path="${path}.${suffix}"
129
+ fi
130
+ printf "%s\n" "${path}"
131
+ }
132
+
133
+ format_messages_rows() {
134
+ python3 -c '
135
+ import os
136
+ import json
137
+ import sys
138
+ import textwrap
139
+
140
+ role_colors = {
141
+ "system": "\033[33m",
142
+ "user": "\033[36m",
143
+ "assistant": "\033[32m",
144
+ "tool": "\033[35m",
145
+ }
146
+ reset = "\033[0m"
147
+ budget = max(1000, int(os.environ.get("MESSAGE_OUTPUT_BUDGET_CHARS", "12000")))
148
+ artifact_path = os.environ.get("MESSAGE_ARTIFACT_PATH", "")
149
+
150
+ def format_content(content, indent=2):
151
+ if content is None:
152
+ content = ""
153
+ content = str(content)
154
+ prefix = " " * indent
155
+ return textwrap.fill(content, width=100, initial_indent=prefix, subsequent_indent=prefix)
156
+
157
+ payload = sys.stdin.read().strip()
158
+ if not payload:
159
+ print("(no messages found)")
160
+ sys.exit(0)
161
+
162
+ rows = json.loads(payload)
163
+ if not rows:
164
+ print("(no messages found)")
165
+ sys.exit(0)
166
+
167
+ rendered_blocks = []
168
+ artifact_lines = []
169
+ truncated_rows = 0
170
+ for row in rows:
171
+ seq = row.get("seq_in_session")
172
+ role = str(row.get("role") or "unknown")
173
+ ts = row.get("ts") or ""
174
+ artifact_content = str(row.get("artifact_content_text") or row.get("content_text") or "")
175
+ content = str(row.get("content_text") or "")
176
+ content_chars = len(artifact_content)
177
+ preview_part_count = max(1, int(row.get("preview_part_count") or 1))
178
+ truncated_part_count = max(0, int(row.get("truncated_part_count") or 0))
179
+ is_truncated = bool(row.get("is_truncated"))
180
+ color = role_colors.get(role, "")
181
+ if seq is None:
182
+ header = f"\n{color}{role.upper()}{reset} {ts}"
183
+ else:
184
+ header = f"\n{color}[{seq}] {role.upper()}{reset} {ts}"
185
+ suffix = ""
186
+ artifact_suffix = ""
187
+ if is_truncated:
188
+ truncated_rows += 1
189
+ if preview_part_count > 1:
190
+ suffix = (
191
+ f"\n [per-part tail preview: {preview_part_count} parts, "
192
+ f"{truncated_part_count} truncated, {content_chars} chars total]"
193
+ )
194
+ artifact_suffix += (
195
+ f"\n [preview used per-part tail excerpts across {preview_part_count} parts; "
196
+ f"full extracted content retained]"
197
+ )
198
+ else:
199
+ visible_chars = len(content) - 3 if content.startswith("...") else len(content)
200
+ suffix = f"\n [tail preview truncated: showing last {min(content_chars, visible_chars)} of {content_chars} chars]"
201
+ artifact_suffix += f"\n [tail preview truncated from {content_chars} chars]"
202
+ block = f"{header}\n{format_content(content)}{suffix}"
203
+ rendered_blocks.append(block)
204
+ artifact_lines.append(f"{header}\n{format_content(artifact_content)}{artifact_suffix}")
205
+
206
+ total_chars = 0
207
+ printed = 0
208
+ for block in rendered_blocks:
209
+ projected = total_chars + len(block)
210
+ if printed > 0 and projected > budget:
211
+ break
212
+ print(block)
213
+ total_chars = projected
214
+ printed += 1
215
+
216
+ omitted = len(rendered_blocks) - printed
217
+ artifact_needed = bool(artifact_path) and (
218
+ omitted > 0 or truncated_rows > 0 or len(rendered_blocks) > 8
219
+ )
220
+ if artifact_needed:
221
+ with open(artifact_path, "w", encoding="utf-8") as handle:
222
+ handle.write("Spotlight Query message artifact\n")
223
+ handle.write(f"rows={len(rendered_blocks)} truncated_rows={truncated_rows}\n")
224
+ handle.write("\n".join(artifact_lines))
225
+ handle.write("\n")
226
+
227
+ if omitted > 0:
228
+ print(f"\nOutput clipped after {printed} rows to stay within {budget} chars.")
229
+ if artifact_path:
230
+ print(f"Artifact: {artifact_path}")
231
+ elif artifact_needed:
232
+ print(f"\nArtifact: {artifact_path}")
233
+ '
234
+ }
235
+
236
+ query_session_last_messages() {
237
+ local session_id="$1"
238
+ local limit="${2:-8}"
239
+ local escaped_session_id
240
+ local artifact_path
241
+ escaped_session_id="$(escape_sql_literal "${session_id}")"
242
+
243
+ if ! [[ "${limit}" =~ ^[0-9]+$ ]]; then
244
+ echo "Limit must be numeric"
245
+ exit 1
246
+ fi
247
+
248
+ if has_indexed_db; then
249
+ artifact_path="$(make_artifact_path "session-last-$(sanitize_artifact_label "${session_id}")" "txt")"
250
+ echo "📊 Last ${limit} User/Assistant Messages (safe preview): ${session_id}"
251
+ echo "================================================================"
252
+ run_duckdb_json_sql "
253
+ WITH latest_call AS (
254
+ SELECT
255
+ trace_id,
256
+ turn_id,
257
+ request_id,
258
+ start_ms,
259
+ input_messages_raw,
260
+ output_messages_raw
261
+ FROM model_calls
262
+ WHERE session_id = '${escaped_session_id}'
263
+ ORDER BY start_ms DESC
264
+ LIMIT 1
265
+ ),
266
+ input_messages AS (
267
+ SELECT
268
+ CAST(message.key AS BIGINT) + 1 AS seq_in_session,
269
+ LOWER(COALESCE(
270
+ json_extract_string(message.value, '$.role'),
271
+ json_extract_string(message.value, '$.message.role'),
272
+ 'unknown'
273
+ )) AS role,
274
+ message.value AS message_json,
275
+ to_timestamp((SELECT start_ms FROM latest_call) / 1000.0) AS ts,
276
+ (SELECT turn_id FROM latest_call) AS turn_id,
277
+ (SELECT trace_id FROM latest_call) AS trace_id,
278
+ COALESCE(
279
+ NULLIF(json_extract_string(message.value, '$.text'), ''),
280
+ NULLIF(json_extract_string(message.value, '$.message.content'), ''),
281
+ NULLIF(json_extract_string(message.value, '$.content'), ''),
282
+ ''
283
+ ) AS fallback_text
284
+ FROM latest_call,
285
+ json_each(COALESCE(input_messages_raw, '[]'::JSON)) message
286
+ ),
287
+ input_parts AS (
288
+ SELECT
289
+ raw_parts.seq_in_session,
290
+ raw_parts.part_index,
291
+ raw_parts.part_text,
292
+ CASE
293
+ WHEN length(raw_parts.part_text) > ${MESSAGE_PART_PREVIEW_CHARS}
294
+ THEN '...' || right(raw_parts.part_text, ${MESSAGE_PART_PREVIEW_CHARS})
295
+ ELSE raw_parts.part_text
296
+ END AS part_preview_text,
297
+ length(raw_parts.part_text) > ${MESSAGE_PART_PREVIEW_CHARS} AS part_is_truncated
298
+ FROM (
299
+ SELECT
300
+ input_messages.seq_in_session,
301
+ CAST(part.key AS BIGINT) AS part_index,
302
+ LOWER(COALESCE(json_extract_string(part.value, '$.type'), '')) AS part_type,
303
+ part_text
304
+ FROM input_messages
305
+ LEFT JOIN json_each(
306
+ CASE
307
+ WHEN json_type(json_extract(input_messages.message_json, '$.parts')) = 'ARRAY'
308
+ THEN json_extract(input_messages.message_json, '$.parts')
309
+ WHEN json_type(json_extract(input_messages.message_json, '$.message.parts')) = 'ARRAY'
310
+ THEN json_extract(input_messages.message_json, '$.message.parts')
311
+ WHEN json_type(json_extract(input_messages.message_json, '$.content')) = 'ARRAY'
312
+ THEN json_extract(input_messages.message_json, '$.content')
313
+ WHEN json_type(json_extract(input_messages.message_json, '$.message.content')) = 'ARRAY'
314
+ THEN json_extract(input_messages.message_json, '$.message.content')
315
+ ELSE '[]'::JSON
316
+ END
317
+ ) part ON TRUE
318
+ CROSS JOIN (
319
+ SELECT COALESCE(
320
+ NULLIF(json_extract_string(part.value, '$.text'), ''),
321
+ NULLIF(json_extract_string(part.value, '$.content'), ''),
322
+ ''
323
+ ) AS part_text
324
+ ) extracted
325
+ ) raw_parts
326
+ WHERE raw_parts.part_type NOT IN ('thinking', 'signature', 'tool_result')
327
+ AND raw_parts.part_text <> ''
328
+ ),
329
+ input_expanded AS (
330
+ SELECT
331
+ input_messages.seq_in_session,
332
+ input_messages.role,
333
+ COALESCE(
334
+ NULLIF(string_agg(input_parts.part_text, '\n' ORDER BY input_parts.part_index), ''),
335
+ input_messages.fallback_text
336
+ ) AS content_text,
337
+ COALESCE(
338
+ NULLIF(string_agg(input_parts.part_preview_text, '\n...\n' ORDER BY input_parts.part_index), ''),
339
+ CASE
340
+ WHEN length(input_messages.fallback_text) > ${MESSAGE_PART_PREVIEW_CHARS}
341
+ THEN '...' || right(input_messages.fallback_text, ${MESSAGE_PART_PREVIEW_CHARS})
342
+ ELSE input_messages.fallback_text
343
+ END
344
+ ) AS preview_text,
345
+ CASE
346
+ WHEN COUNT(input_parts.part_text) > 0 THEN COUNT(input_parts.part_text)
347
+ WHEN input_messages.fallback_text <> '' THEN 1
348
+ ELSE 0
349
+ END AS preview_part_count,
350
+ CASE
351
+ WHEN COUNT(input_parts.part_text) > 0 THEN SUM(CASE WHEN input_parts.part_is_truncated THEN 1 ELSE 0 END)
352
+ WHEN length(input_messages.fallback_text) > ${MESSAGE_PART_PREVIEW_CHARS} THEN 1
353
+ ELSE 0
354
+ END AS truncated_part_count,
355
+ input_messages.ts,
356
+ input_messages.turn_id,
357
+ input_messages.trace_id
358
+ FROM input_messages
359
+ LEFT JOIN input_parts
360
+ ON input_parts.seq_in_session = input_messages.seq_in_session
361
+ GROUP BY
362
+ input_messages.seq_in_session,
363
+ input_messages.role,
364
+ input_messages.fallback_text,
365
+ input_messages.ts,
366
+ input_messages.turn_id,
367
+ input_messages.trace_id
368
+ ),
369
+ output_messages AS (
370
+ SELECT
371
+ (SELECT COALESCE(MAX(seq_in_session), 0) FROM input_expanded) + CAST(message.key AS BIGINT) + 1 AS seq_in_session,
372
+ LOWER(COALESCE(
373
+ json_extract_string(message.value, '$.role'),
374
+ json_extract_string(message.value, '$.message.role'),
375
+ 'assistant'
376
+ )) AS role,
377
+ message.value AS message_json,
378
+ to_timestamp((SELECT start_ms FROM latest_call) / 1000.0) AS ts,
379
+ (SELECT turn_id FROM latest_call) AS turn_id,
380
+ (SELECT trace_id FROM latest_call) AS trace_id,
381
+ COALESCE(
382
+ NULLIF(json_extract_string(message.value, '$.text'), ''),
383
+ NULLIF(json_extract_string(message.value, '$.message.content'), ''),
384
+ NULLIF(json_extract_string(message.value, '$.content'), ''),
385
+ ''
386
+ ) AS fallback_text
387
+ FROM latest_call,
388
+ json_each(COALESCE(output_messages_raw, '[]'::JSON)) message
389
+ ),
390
+ output_parts AS (
391
+ SELECT
392
+ raw_parts.seq_in_session,
393
+ raw_parts.part_index,
394
+ raw_parts.part_text,
395
+ CASE
396
+ WHEN length(raw_parts.part_text) > ${MESSAGE_PART_PREVIEW_CHARS}
397
+ THEN '...' || right(raw_parts.part_text, ${MESSAGE_PART_PREVIEW_CHARS})
398
+ ELSE raw_parts.part_text
399
+ END AS part_preview_text,
400
+ length(raw_parts.part_text) > ${MESSAGE_PART_PREVIEW_CHARS} AS part_is_truncated
401
+ FROM (
402
+ SELECT
403
+ output_messages.seq_in_session,
404
+ CAST(part.key AS BIGINT) AS part_index,
405
+ LOWER(COALESCE(json_extract_string(part.value, '$.type'), '')) AS part_type,
406
+ part_text
407
+ FROM output_messages
408
+ LEFT JOIN json_each(
409
+ CASE
410
+ WHEN json_type(json_extract(output_messages.message_json, '$.parts')) = 'ARRAY'
411
+ THEN json_extract(output_messages.message_json, '$.parts')
412
+ WHEN json_type(json_extract(output_messages.message_json, '$.message.parts')) = 'ARRAY'
413
+ THEN json_extract(output_messages.message_json, '$.message.parts')
414
+ WHEN json_type(json_extract(output_messages.message_json, '$.content')) = 'ARRAY'
415
+ THEN json_extract(output_messages.message_json, '$.content')
416
+ WHEN json_type(json_extract(output_messages.message_json, '$.message.content')) = 'ARRAY'
417
+ THEN json_extract(output_messages.message_json, '$.message.content')
418
+ ELSE '[]'::JSON
419
+ END
420
+ ) part ON TRUE
421
+ CROSS JOIN (
422
+ SELECT COALESCE(
423
+ NULLIF(json_extract_string(part.value, '$.text'), ''),
424
+ NULLIF(json_extract_string(part.value, '$.content'), ''),
425
+ ''
426
+ ) AS part_text
427
+ ) extracted
428
+ ) raw_parts
429
+ WHERE raw_parts.part_type NOT IN ('thinking', 'signature', 'tool_result')
430
+ AND raw_parts.part_text <> ''
431
+ ),
432
+ output_expanded AS (
433
+ SELECT
434
+ output_messages.seq_in_session,
435
+ output_messages.role,
436
+ COALESCE(
437
+ NULLIF(string_agg(output_parts.part_text, '\n' ORDER BY output_parts.part_index), ''),
438
+ output_messages.fallback_text
439
+ ) AS content_text,
440
+ COALESCE(
441
+ NULLIF(string_agg(output_parts.part_preview_text, '\n...\n' ORDER BY output_parts.part_index), ''),
442
+ CASE
443
+ WHEN length(output_messages.fallback_text) > ${MESSAGE_PART_PREVIEW_CHARS}
444
+ THEN '...' || right(output_messages.fallback_text, ${MESSAGE_PART_PREVIEW_CHARS})
445
+ ELSE output_messages.fallback_text
446
+ END
447
+ ) AS preview_text,
448
+ CASE
449
+ WHEN COUNT(output_parts.part_text) > 0 THEN COUNT(output_parts.part_text)
450
+ WHEN output_messages.fallback_text <> '' THEN 1
451
+ ELSE 0
452
+ END AS preview_part_count,
453
+ CASE
454
+ WHEN COUNT(output_parts.part_text) > 0 THEN SUM(CASE WHEN output_parts.part_is_truncated THEN 1 ELSE 0 END)
455
+ WHEN length(output_messages.fallback_text) > ${MESSAGE_PART_PREVIEW_CHARS} THEN 1
456
+ ELSE 0
457
+ END AS truncated_part_count,
458
+ output_messages.ts,
459
+ output_messages.turn_id,
460
+ output_messages.trace_id
461
+ FROM output_messages
462
+ LEFT JOIN output_parts
463
+ ON output_parts.seq_in_session = output_messages.seq_in_session
464
+ GROUP BY
465
+ output_messages.seq_in_session,
466
+ output_messages.role,
467
+ output_messages.fallback_text,
468
+ output_messages.ts,
469
+ output_messages.turn_id,
470
+ output_messages.trace_id
471
+ ),
472
+ expanded AS (
473
+ SELECT * FROM input_expanded
474
+ UNION ALL
475
+ SELECT * FROM output_expanded
476
+ ),
477
+ filtered AS (
478
+ SELECT *
479
+ FROM expanded
480
+ WHERE role IN ('user', 'assistant')
481
+ AND content_text <> ''
482
+ ),
483
+ last_rows AS (
484
+ SELECT *
485
+ FROM filtered
486
+ ORDER BY seq_in_session DESC
487
+ LIMIT ${limit}
488
+ )
489
+ SELECT
490
+ seq_in_session,
491
+ role,
492
+ preview_text AS content_text,
493
+ content_text AS artifact_content_text,
494
+ length(content_text) AS content_chars,
495
+ truncated_part_count > 0 AS is_truncated,
496
+ preview_part_count,
497
+ truncated_part_count,
498
+ ts,
499
+ turn_id,
500
+ trace_id
501
+ FROM last_rows
502
+ ORDER BY seq_in_session ASC;
503
+ " | MESSAGE_PREVIEW_CHARS="${MESSAGE_PREVIEW_CHARS}" MESSAGE_OUTPUT_BUDGET_CHARS="${MESSAGE_OUTPUT_BUDGET_CHARS}" MESSAGE_ARTIFACT_PATH="${artifact_path}" format_messages_rows
504
+ return
505
+ fi
506
+
507
+ echo "Indexed analytics DB not found; falling back to trace parsing."
508
+ query_session_messages "${session_id}" "${limit}"
509
+ }
510
+
511
+ query_session_tools() {
512
+ local session_id="$1"
513
+ local escaped_session_id
514
+ escaped_session_id="$(escape_sql_literal "${session_id}")"
515
+
516
+ if has_indexed_db; then
517
+ echo "🔧 Session Tools: ${session_id}"
518
+ echo "=============================="
519
+ run_duckdb_sql "
520
+ SELECT
521
+ call_ts,
522
+ tool_name,
523
+ status,
524
+ ROUND(COALESCE(duration_ms, 0), 2) AS duration_ms,
525
+ LEFT(COALESCE(arguments_text, ''), 200) AS arguments_preview,
526
+ LEFT(COALESCE(result_text, ''), 200) AS result_preview,
527
+ COALESCE(error_message, '') AS error_message
528
+ FROM tool_calls
529
+ WHERE session_id = '${escaped_session_id}'
530
+ ORDER BY call_ts;
531
+ "
532
+ return
533
+ fi
534
+
535
+ echo "Indexed analytics DB not found; tool-level session history is unavailable without index DB."
536
+ exit 1
537
+ }
538
+
539
+ query_session_summary_fast() {
540
+ local session_id="$1"
541
+ local escaped_session_id
542
+ escaped_session_id="$(escape_sql_literal "${session_id}")"
543
+
544
+ if has_indexed_db; then
545
+ echo "📊 Session Summary: ${session_id}"
546
+ echo "==============================="
547
+ run_duckdb_sql "
548
+ SELECT
549
+ session_id,
550
+ COALESCE(agent_id, '') AS agent_id,
551
+ COALESCE(agent_version, '') AS agent_version,
552
+ COALESCE(engine, '') AS engine,
553
+ COALESCE(status, '') AS status,
554
+ started_at,
555
+ ended_at,
556
+ total_turns,
557
+ total_requests,
558
+ ROUND(COALESCE(total_cost_usd, 0), 6) AS total_cost_usd,
559
+ COALESCE(total_tokens, 0) AS total_tokens
560
+ FROM sessions
561
+ WHERE session_id = '${escaped_session_id}'
562
+ LIMIT 1;
563
+ "
564
+ return
565
+ fi
566
+
567
+ query_session "${session_id}"
568
+ }
569
+
570
+ query_latest_session_fast() {
571
+ if has_indexed_db; then
572
+ echo "📊 Latest Session"
573
+ echo "================"
574
+ run_duckdb_sql "
575
+ SELECT
576
+ session_id,
577
+ COALESCE(agent_id, '') AS agent_id,
578
+ COALESCE(agent_version, '') AS agent_version,
579
+ COALESCE(engine, '') AS engine,
580
+ COALESCE(status, '') AS status,
581
+ started_at,
582
+ ended_at,
583
+ total_turns,
584
+ total_requests,
585
+ ROUND(COALESCE(total_cost_usd, 0), 6) AS total_cost_usd,
586
+ COALESCE(total_tokens, 0) AS total_tokens
587
+ FROM sessions
588
+ ORDER BY ended_at DESC NULLS LAST, started_at DESC NULLS LAST
589
+ LIMIT 1;
590
+ "
591
+ return
592
+ fi
593
+
594
+ query_latest_session
595
+ }
596
+
597
+ query_request_fast() {
598
+ local request_id="$1"
599
+ local escaped_request_id
600
+ escaped_request_id="$(escape_sql_literal "${request_id}")"
601
+
602
+ if has_indexed_db; then
603
+ echo "📊 Request: ${request_id}"
604
+ echo "========================="
605
+ run_duckdb_sql "
606
+ SELECT
607
+ model,
608
+ ROUND(COALESCE(total_cost_usd, 0), 6) AS cost,
609
+ prompt_tokens AS input_tokens,
610
+ completion_tokens AS output_tokens,
611
+ ROUND(COALESCE(duration_ms, 0), 2) AS latency_ms,
612
+ COALESCE(turn_id, '') AS turn_id,
613
+ to_timestamp(start_ms / 1000.0) AS timestamp
614
+ FROM model_calls
615
+ WHERE request_id = '${escaped_request_id}'
616
+ ORDER BY start_ms;
617
+ "
618
+ return
619
+ fi
620
+
621
+ query_request "${request_id}"
622
+ }
623
+
624
+ query_session_fast() {
625
+ local session_id="$1"
626
+ local escaped_session_id
627
+ escaped_session_id="$(escape_sql_literal "${session_id}")"
628
+
629
+ if has_indexed_db; then
630
+ echo "📊 Session: ${session_id}"
631
+ echo "========================="
632
+ run_duckdb_sql "
633
+ SELECT
634
+ model,
635
+ ROUND(COALESCE(total_cost_usd, 0), 6) as cost,
636
+ prompt_tokens as input_tokens,
637
+ completion_tokens as output_tokens,
638
+ ROUND(COALESCE(duration_ms, 0), 2) as latency_ms,
639
+ COALESCE(request_id, '') as request_id,
640
+ to_timestamp(start_ms / 1000.0) as timestamp
641
+ FROM model_calls
642
+ WHERE session_id = '${escaped_session_id}'
643
+ ORDER BY start_ms;
644
+ "
645
+ return
646
+ fi
647
+
648
+ query_session "${session_id}"
649
+ }
650
+
651
+ query_cost_by_session_fast() {
652
+ if has_indexed_db; then
653
+ echo "📊 Cost by Session"
654
+ echo "=================="
655
+ run_duckdb_sql "
656
+ SELECT
657
+ session_id,
658
+ ROUND(COALESCE(total_cost_usd, 0), 6) as total_cost,
659
+ COALESCE(total_tokens, 0) as total_tokens,
660
+ total_requests as requests,
661
+ total_turns as turns,
662
+ started_at,
663
+ ended_at
664
+ FROM sessions
665
+ ORDER BY total_cost DESC
666
+ LIMIT 20;
667
+ "
668
+ return
669
+ fi
670
+
671
+ query_cost_by_session
672
+ }
673
+
674
+ query_cost_by_model_fast() {
675
+ if has_indexed_db; then
676
+ echo "📊 Cost by Model"
677
+ echo "================"
678
+ run_duckdb_sql "
679
+ SELECT
680
+ COALESCE(model, '(unknown)') as model,
681
+ ROUND(SUM(COALESCE(total_cost_usd, 0)), 6) as total_cost,
682
+ SUM(COALESCE(prompt_tokens, 0)) as input_tokens,
683
+ SUM(COALESCE(completion_tokens, 0)) as output_tokens,
684
+ COUNT(*) as requests,
685
+ ROUND(AVG(COALESCE(duration_ms, 0)), 2) as avg_latency_ms
686
+ FROM model_calls
687
+ GROUP BY model
688
+ ORDER BY total_cost DESC;
689
+ "
690
+ return
691
+ fi
692
+
693
+ query_cost_by_model
694
+ }
695
+
696
+ query_cost_by_pack_fast() {
697
+ if has_indexed_db; then
698
+ echo "📊 Cost by Agent"
699
+ echo "================"
700
+ run_duckdb_sql "
701
+ SELECT
702
+ COALESCE(agent_id, '(no agent)') AS agent,
703
+ COALESCE(agent_version, '?') AS version,
704
+ ROUND(SUM(COALESCE(total_cost_usd, 0)), 6) AS total_cost,
705
+ SUM(COALESCE(prompt_tokens, 0)) AS input_tokens,
706
+ SUM(COALESCE(completion_tokens, 0)) AS output_tokens,
707
+ COUNT(*) AS requests
708
+ FROM model_calls
709
+ GROUP BY agent_id, agent_version
710
+ ORDER BY total_cost DESC;
711
+ "
712
+ return
713
+ fi
714
+
715
+ query_cost_by_pack
716
+ }
717
+
718
+ query_cost_by_engine_fast() {
719
+ if has_indexed_db; then
720
+ echo "📊 Cost by Engine"
721
+ echo "================="
722
+ run_duckdb_sql "
723
+ SELECT
724
+ COALESCE(engine, '(unknown)') AS engine,
725
+ COALESCE(model, '(unknown)') AS model,
726
+ ROUND(SUM(COALESCE(total_cost_usd, 0)), 6) AS total_cost,
727
+ COUNT(*) AS requests,
728
+ SUM(COALESCE(prompt_tokens, 0)) AS input_tokens,
729
+ SUM(COALESCE(completion_tokens, 0)) AS output_tokens
730
+ FROM model_calls
731
+ GROUP BY engine, model
732
+ ORDER BY total_cost DESC;
733
+ "
734
+ return
735
+ fi
736
+
737
+ query_cost_by_engine
738
+ }
739
+
740
+ query_cost_by_source_fast() {
741
+ if has_indexed_db; then
742
+ echo "📊 Cost by Source"
743
+ echo "================="
744
+ run_duckdb_sql "
745
+ SELECT
746
+ COALESCE(source, '(unknown)') AS source,
747
+ COALESCE(model, '(unknown)') AS model,
748
+ ROUND(SUM(COALESCE(total_cost_usd, 0)), 6) AS total_cost,
749
+ COUNT(*) AS requests,
750
+ SUM(COALESCE(prompt_tokens, 0)) AS input_tokens,
751
+ SUM(COALESCE(completion_tokens, 0)) AS output_tokens
752
+ FROM model_calls
753
+ GROUP BY source, model
754
+ ORDER BY total_cost DESC;
755
+ "
756
+ return
757
+ fi
758
+
759
+ query_cost_by_source
760
+ }
761
+
762
+ query_total_cost_fast() {
763
+ if has_indexed_db; then
764
+ echo "📊 Total Cost Summary"
765
+ echo "===================="
766
+ run_duckdb_sql "
767
+ SELECT
768
+ ROUND(COALESCE(SUM(total_cost_usd), 0), 6) as total_cost,
769
+ COALESCE(SUM(prompt_tokens), 0) as total_input_tokens,
770
+ COALESCE(SUM(completion_tokens), 0) as total_output_tokens,
771
+ COUNT(*) as total_requests,
772
+ ROUND(AVG(COALESCE(duration_ms, 0)), 2) as avg_latency_ms
773
+ FROM model_calls;
774
+ "
775
+ return
776
+ fi
777
+
778
+ query_total_cost
779
+ }
780
+
781
+ query_cost_per_request_fast() {
782
+ local session_id="${1:-}"
783
+ if has_indexed_db; then
784
+ local where_clause="WHERE request_id IS NOT NULL"
785
+ echo "📊 Cost per Request"
786
+ echo "==================="
787
+ if [[ -n "${session_id}" ]]; then
788
+ where_clause="WHERE session_id = '$(escape_sql_literal "${session_id}")' AND request_id IS NOT NULL"
789
+ echo " Session: ${session_id}"
790
+ fi
791
+ run_duckdb_sql "
792
+ SELECT
793
+ request_id,
794
+ COUNT(*) AS llm_calls,
795
+ ROUND(SUM(COALESCE(total_cost_usd, 0)), 6) AS request_cost,
796
+ SUM(COALESCE(prompt_tokens, 0)) AS input_tokens,
797
+ SUM(COALESCE(completion_tokens, 0)) AS output_tokens,
798
+ ROUND(SUM(COALESCE(duration_ms, 0)), 0) AS total_latency_ms,
799
+ MIN(to_timestamp(start_ms / 1000.0)) AS started_at
800
+ FROM model_calls
801
+ ${where_clause}
802
+ GROUP BY request_id
803
+ ORDER BY MIN(start_ms);
804
+ "
805
+ return
806
+ fi
807
+
808
+ query_cost_per_request "${session_id}"
809
+ }
810
+
811
+ query_session_timeline_fast() {
812
+ local session_id="$1"
813
+ local escaped_session_id
814
+ escaped_session_id="$(escape_sql_literal "${session_id}")"
815
+
816
+ if has_indexed_db; then
817
+ echo "📊 Session Timeline: ${session_id}"
818
+ run_duckdb_sql "
819
+ SELECT
820
+ to_timestamp(start_ms / 1000.0) AS event_ts,
821
+ 'llm_call' AS event_type,
822
+ '' AS role,
823
+ COALESCE(model, '') AS model,
824
+ '' AS tool_name,
825
+ LEFT(COALESCE(request_id, ''), 220) AS detail,
826
+ ROUND(COALESCE(total_cost_usd, 0), 6) AS cost,
827
+ COALESCE(total_tokens, 0) AS total_tokens,
828
+ COALESCE(status, '') AS status
829
+ FROM model_calls
830
+ WHERE session_id = '${escaped_session_id}'
831
+
832
+ UNION ALL
833
+
834
+ SELECT
835
+ call_ts AS event_ts,
836
+ 'tool_call' AS event_type,
837
+ '' AS role,
838
+ '' AS model,
839
+ COALESCE(tool_name, '') AS tool_name,
840
+ LEFT(COALESCE(arguments_text, ''), 220) AS detail,
841
+ 0.0 AS cost,
842
+ 0 AS total_tokens,
843
+ COALESCE(status, '') AS status
844
+ FROM tool_calls
845
+ WHERE session_id = '${escaped_session_id}'
846
+
847
+ ORDER BY event_ts;
848
+ "
849
+ return
850
+ fi
851
+
852
+ query_session_timeline "${session_id}"
853
+ }
854
+
855
+ query_compare_versions_fast() {
856
+ local agent_name="$1"
857
+ local escaped_agent_name
858
+ escaped_agent_name="$(escape_sql_literal "${agent_name}")"
859
+
860
+ if has_indexed_db; then
861
+ echo "📊 Compare Versions: ${agent_name}"
862
+ echo "===================================="
863
+ run_duckdb_sql "
864
+ SELECT
865
+ agent_version AS version,
866
+ COUNT(DISTINCT session_id) AS sessions,
867
+ ROUND(AVG(session_cost), 4) AS avg_cost_per_session,
868
+ ROUND(AVG(session_tokens), 0) AS avg_tokens_per_session,
869
+ ROUND(AVG(session_latency), 0) AS avg_latency_ms,
870
+ ROUND(AVG(requests_per_session), 1) AS avg_llm_calls
871
+ FROM (
872
+ SELECT
873
+ agent_version,
874
+ session_id,
875
+ SUM(COALESCE(total_cost_usd, 0)) AS session_cost,
876
+ SUM(COALESCE(prompt_tokens, 0) + COALESCE(completion_tokens, 0)) AS session_tokens,
877
+ SUM(COALESCE(duration_ms, 0)) AS session_latency,
878
+ COUNT(*) AS requests_per_session
879
+ FROM model_calls
880
+ WHERE agent_id = '${escaped_agent_name}'
881
+ GROUP BY agent_version, session_id
882
+ )
883
+ GROUP BY agent_version
884
+ ORDER BY agent_version DESC;
885
+ "
886
+ return
887
+ fi
888
+
889
+ query_compare_versions "${agent_name}"
890
+ }
891
+
892
+ query_top_sessions_fast() {
893
+ local agent_name="$1"
894
+ local since_ms="${2:-86400000}"
895
+ local escaped_agent_name
896
+ escaped_agent_name="$(escape_sql_literal "${agent_name}")"
897
+
898
+ if has_indexed_db; then
899
+ echo "📊 Top Sessions: ${agent_name}"
900
+ echo "==============================="
901
+ run_duckdb_sql "
902
+ WITH recent AS (
903
+ SELECT *
904
+ FROM model_calls
905
+ WHERE agent_id = '${escaped_agent_name}'
906
+ AND start_ms > (EXTRACT(EPOCH FROM NOW()) * 1000 - ${since_ms})
907
+ ),
908
+ target AS (
909
+ SELECT * FROM recent
910
+ UNION ALL
911
+ SELECT *
912
+ FROM model_calls
913
+ WHERE agent_id = '${escaped_agent_name}'
914
+ AND NOT EXISTS (SELECT 1 FROM recent)
915
+ )
916
+ SELECT
917
+ session_id,
918
+ ROUND(SUM(COALESCE(total_cost_usd, 0)), 6) AS total_cost,
919
+ SUM(COALESCE(prompt_tokens, 0)) AS input_tokens,
920
+ SUM(COALESCE(completion_tokens, 0)) AS output_tokens,
921
+ COUNT(*) AS llm_calls,
922
+ COUNT(DISTINCT request_id) AS requests,
923
+ ROUND(AVG(COALESCE(duration_ms, 0)), 0) AS avg_latency_ms,
924
+ MIN(to_timestamp(start_ms / 1000.0)) AS started_at
925
+ FROM target
926
+ GROUP BY session_id
927
+ ORDER BY total_cost DESC
928
+ LIMIT 10;
929
+ "
930
+ return
931
+ fi
932
+
933
+ query_top_sessions "${agent_name}" "${since_ms}"
934
+ }
935
+
936
+ query_agent_kpis_daily() {
937
+ if has_indexed_db; then
938
+ echo "📊 Agent KPIs Daily"
939
+ run_duckdb_sql "
940
+ SELECT *
941
+ FROM agent_kpis_daily
942
+ ORDER BY day DESC, total_cost_usd DESC
943
+ LIMIT 50;
944
+ "
945
+ return
946
+ fi
947
+
948
+ echo "Indexed analytics DB not found; daily KPI view unavailable without index DB."
949
+ exit 1
950
+ }
951
+
952
+ query_training_examples() {
953
+ echo "training_examples view has been retired in this refactor."
954
+ echo "Use --session-last for message snapshots and --session-timeline for model/tool event chronology."
955
+ exit 1
956
+ }
957
+
958
+ query_recent_messages() {
959
+ local limit="${1:-20}"
960
+ if ! [[ "${limit}" =~ ^[0-9]+$ ]]; then
961
+ echo "Limit must be numeric"
962
+ exit 1
963
+ fi
964
+
965
+ if has_indexed_db; then
966
+ echo "📊 Recent Sessions (message snapshot source)"
967
+ echo "==========================================="
968
+ run_duckdb_sql "
969
+ SELECT
970
+ session_id,
971
+ to_timestamp(MAX(start_ms) / 1000.0) AS latest_call_ts,
972
+ COUNT(*) AS model_calls,
973
+ ROUND(SUM(COALESCE(total_cost_usd, 0)), 6) AS total_cost,
974
+ SUM(COALESCE(total_tokens, 0)) AS total_tokens
975
+ FROM model_calls
976
+ GROUP BY session_id
977
+ ORDER BY latest_call_ts DESC
978
+ LIMIT ${limit};
979
+ "
980
+ return
981
+ fi
982
+
983
+ echo "Indexed analytics DB not found; recent session query unavailable without index DB."
984
+ exit 1
985
+ }
986
+
987
+ query_last_session_messages() {
988
+ local limit="${1:-8}"
989
+ if ! [[ "${limit}" =~ ^[0-9]+$ ]]; then
990
+ echo "Limit must be numeric"
991
+ exit 1
992
+ fi
993
+
994
+ if has_indexed_db; then
995
+ local latest_session
996
+ latest_session="$(${DUCKDB} -readonly "${INDEX_DB_PATH}" -csv -c "
997
+ SELECT session_id
998
+ FROM sessions
999
+ ORDER BY ended_at DESC NULLS LAST, started_at DESC NULLS LAST
1000
+ LIMIT 1;
1001
+ " | tail -n 1)"
1002
+
1003
+ if [[ -z "${latest_session}" ]]; then
1004
+ echo "No sessions found in indexed DB."
1005
+ exit 0
1006
+ fi
1007
+
1008
+ query_session_last_messages "${latest_session}" "${limit}"
1009
+ return
1010
+ fi
1011
+
1012
+ local latest_session
1013
+ latest_session="$(${DUCKDB} -csv -c "
1014
+ ${BASE_QUERY},
1015
+ extracted AS (${EXTRACT_ATTRS})
1016
+ SELECT session_id
1017
+ FROM extracted
1018
+ WHERE session_id IS NOT NULL
1019
+ ORDER BY start_ms DESC
1020
+ LIMIT 1;
1021
+ " | tail -n 1)"
1022
+ if [[ -z "${latest_session}" ]]; then
1023
+ echo "No sessions found in traces."
1024
+ exit 0
1025
+ fi
1026
+ query_session_messages "${latest_session}" "${limit}"
1027
+ }
1028
+
1029
+ query_session_debug() {
1030
+ local session_id="$1"
1031
+ local escaped_session_id
1032
+ escaped_session_id="$(escape_sql_literal "${session_id}")"
1033
+
1034
+ echo "📊 Session Debug: ${session_id}"
1035
+ echo "=============================="
1036
+
1037
+ if has_indexed_db; then
1038
+ echo ""
1039
+ echo "Summary"
1040
+ echo "-------"
1041
+ run_duckdb_sql "
1042
+ SELECT
1043
+ session_id,
1044
+ COALESCE(agent_id, '') AS agent_id,
1045
+ COALESCE(agent_version, '') AS agent_version,
1046
+ COALESCE(engine, '') AS engine,
1047
+ COALESCE(status, '') AS status,
1048
+ started_at,
1049
+ ended_at,
1050
+ total_turns,
1051
+ total_requests,
1052
+ ROUND(COALESCE(total_cost_usd, 0), 6) AS total_cost_usd,
1053
+ COALESCE(total_tokens, 0) AS total_tokens
1054
+ FROM sessions
1055
+ WHERE session_id = '${escaped_session_id}'
1056
+ LIMIT 1;
1057
+ "
1058
+
1059
+ echo ""
1060
+ echo "Recent Requests"
1061
+ echo "---------------"
1062
+ run_duckdb_sql "
1063
+ SELECT
1064
+ COALESCE(request_id, '') AS request_id,
1065
+ COUNT(*) AS llm_calls,
1066
+ ROUND(SUM(COALESCE(total_cost_usd, 0)), 6) AS total_cost_usd,
1067
+ SUM(COALESCE(prompt_tokens, 0) + COALESCE(completion_tokens, 0)) AS total_tokens,
1068
+ SUM(CASE WHEN lower(COALESCE(status, '')) IN ('error', 'failed', 'failure') THEN 1 ELSE 0 END) AS error_calls,
1069
+ MAX(to_timestamp(start_ms / 1000.0)) AS latest_call_ts
1070
+ FROM model_calls
1071
+ WHERE session_id = '${escaped_session_id}'
1072
+ GROUP BY request_id
1073
+ ORDER BY latest_call_ts DESC
1074
+ LIMIT ${SESSION_DEBUG_REQUEST_LIMIT};
1075
+ "
1076
+
1077
+ if has_indexed_table "tool_calls"; then
1078
+ echo ""
1079
+ echo "Tool Signals"
1080
+ echo "------------"
1081
+ run_duckdb_sql "
1082
+ SELECT
1083
+ COALESCE(request_id, '') AS request_id,
1084
+ COALESCE(tool_name, '') AS tool_name,
1085
+ COUNT(*) AS calls,
1086
+ SUM(CASE WHEN lower(COALESCE(status, '')) IN ('error', 'failed', 'failure') THEN 1 ELSE 0 END) AS error_calls,
1087
+ ROUND(AVG(COALESCE(duration_ms, 0)), 2) AS avg_duration_ms,
1088
+ MAX(call_ts) AS latest_call_ts
1089
+ FROM tool_calls
1090
+ WHERE session_id = '${escaped_session_id}'
1091
+ GROUP BY request_id, tool_name
1092
+ ORDER BY error_calls DESC, calls DESC, latest_call_ts DESC
1093
+ LIMIT ${SESSION_DEBUG_TOOL_LIMIT};
1094
+ "
1095
+ fi
1096
+
1097
+ echo ""
1098
+ query_session_last_messages "${session_id}" 4
1099
+ echo ""
1100
+ echo "Next"
1101
+ echo "----"
1102
+ echo "Use --request <request-id> for one request, --session-tools ${session_id} for tool details, or --session-timeline ${session_id} for chronology."
1103
+ return
1104
+ fi
1105
+
1106
+ echo ""
1107
+ echo "Indexed analytics DB not found; using trace/session-file fallback."
1108
+ echo ""
1109
+ query_session "${session_id}"
1110
+ echo ""
1111
+ if [[ -f "${SESSIONS_DIR}/${session_id}.jsonl" ]]; then
1112
+ query_tool_loops "${session_id}"
1113
+ echo ""
1114
+ fi
1115
+ query_session_messages "${session_id}" 4
1116
+ echo ""
1117
+ echo "Next"
1118
+ echo "----"
1119
+ echo "Use --session ${session_id} for all LLM calls or --session-timeline ${session_id} for chronology."
1120
+ }
1121
+
1122
+ # Base query to parse OTEL JSONL format
1123
+ # Note: OTEL Collector file exporter writes one JSON object per line
1124
+ BASE_QUERY="
1125
+ WITH raw_spans AS (
1126
+ SELECT
1127
+ unnest(resourceSpans) as rs
1128
+ FROM read_json('${TRACES_DIR}/*.jsonl',
1129
+ format='newline_delimited',
1130
+ ignore_errors=true)
1131
+ ),
1132
+ spans AS (
1133
+ SELECT
1134
+ unnest(rs.scopeSpans) as ss
1135
+ FROM raw_spans
1136
+ ),
1137
+ flat_spans AS (
1138
+ SELECT
1139
+ unnest(ss.spans) as span
1140
+ FROM spans
1141
+ ),
1142
+ parsed AS (
1143
+ SELECT
1144
+ span.traceId as trace_id,
1145
+ span.spanId as span_id,
1146
+ span.name as span_name,
1147
+ span.startTimeUnixNano::BIGINT / 1000000 as start_ms,
1148
+ span.endTimeUnixNano::BIGINT / 1000000 as end_ms,
1149
+ (span.endTimeUnixNano::BIGINT - span.startTimeUnixNano::BIGINT) / 1000000 as duration_ms,
1150
+ json_extract(to_json(span), '$.attributes') as attrs_json
1151
+ FROM flat_spans
1152
+ ),
1153
+ span_filter AS (
1154
+ SELECT
1155
+ trace_id,
1156
+ span_name,
1157
+ duration_ms,
1158
+ start_ms,
1159
+ attrs_json
1160
+ FROM parsed
1161
+ WHERE span_name = 'Received Proxy Server Request'
1162
+ OR EXISTS (
1163
+ SELECT 1
1164
+ FROM json_each(coalesce(attrs_json, '[]'::JSON)) as attr
1165
+ WHERE json_extract_string(attr.value, '$.key') = 'gen_ai.cost.total_cost'
1166
+ )
1167
+ ),
1168
+ attrs AS (
1169
+ SELECT
1170
+ trace_id,
1171
+ span_name,
1172
+ duration_ms,
1173
+ start_ms,
1174
+ json_extract_string(value, '$.key') as attr_key,
1175
+ json_extract_string(value, '$.value.stringValue') as attr_string,
1176
+ json_extract_string(value, '$.value.intValue') as attr_int,
1177
+ json_extract(value, '$.value.doubleValue') as attr_double
1178
+ FROM span_filter,
1179
+ json_each(coalesce(attrs_json, '[]'::JSON))
1180
+ )
1181
+ "
1182
+
1183
+ # Extract LLM attributes from OTEL span attributes
1184
+ # OTEL attributes are stored as array of {key, value} objects
1185
+ # Canonical metadata source: metadata.requester_custom_headers (x-litellm-metadata-*)
1186
+ # Enriched with: session_id, user_id, project_id, agent_id, agent_version, source, engine, request_id
1187
+ EXTRACT_ATTRS="
1188
+ WITH span_rows AS (
1189
+ SELECT
1190
+ trace_id,
1191
+ span_name,
1192
+ duration_ms,
1193
+ start_ms,
1194
+ NULLIF(regexp_extract(
1195
+ max(CASE WHEN attr_key = 'metadata.requester_custom_headers' THEN attr_string END),
1196
+ 'x-litellm-metadata-session-id'': ''([^'']+)''', 1
1197
+ ), '') as session_id,
1198
+ NULLIF(regexp_extract(
1199
+ max(CASE WHEN attr_key = 'metadata.requester_custom_headers' THEN attr_string END),
1200
+ 'x-litellm-metadata-user-id'': ''([^'']+)''', 1
1201
+ ), '') as user_id,
1202
+ NULLIF(regexp_extract(
1203
+ max(CASE WHEN attr_key = 'metadata.requester_custom_headers' THEN attr_string END),
1204
+ 'x-litellm-metadata-project-id'': ''([^'']+)''', 1
1205
+ ), '') as project_id,
1206
+ max(CASE WHEN attr_key IN ('gen_ai.request.model', 'gen_ai.response.model', 'model') THEN attr_string END) as model,
1207
+ max(CASE WHEN attr_key = 'gen_ai.cost.total_cost' THEN CAST(attr_double AS DOUBLE) END) as cost,
1208
+ max(CASE WHEN attr_key = 'gen_ai.usage.input_tokens' THEN CAST(attr_int AS BIGINT) END) as input_tokens,
1209
+ max(CASE WHEN attr_key = 'gen_ai.usage.output_tokens' THEN CAST(attr_int AS BIGINT) END) as output_tokens,
1210
+ NULLIF(regexp_extract(
1211
+ max(CASE WHEN attr_key = 'metadata.requester_custom_headers' THEN attr_string END),
1212
+ 'x-litellm-metadata-agent-id'': ''([^'']+)''', 1
1213
+ ), '') as agent_id,
1214
+ NULLIF(regexp_extract(
1215
+ max(CASE WHEN attr_key = 'metadata.requester_custom_headers' THEN attr_string END),
1216
+ 'x-litellm-metadata-agent-version'': ''([^'']+)''', 1
1217
+ ), '') as agent_version,
1218
+ NULLIF(regexp_extract(
1219
+ max(CASE WHEN attr_key = 'metadata.requester_custom_headers' THEN attr_string END),
1220
+ 'x-litellm-metadata-source'': ''([^'']+)''', 1
1221
+ ), '') as source,
1222
+ NULLIF(regexp_extract(
1223
+ max(CASE WHEN attr_key = 'metadata.requester_custom_headers' THEN attr_string END),
1224
+ 'x-litellm-metadata-engine'': ''([^'']+)''', 1
1225
+ ), '') as engine,
1226
+ NULLIF(regexp_extract(
1227
+ max(CASE WHEN attr_key = 'metadata.requester_custom_headers' THEN attr_string END),
1228
+ 'x-litellm-metadata-request-id'': ''([^'']+)''', 1
1229
+ ), '') as request_id
1230
+ FROM attrs
1231
+ GROUP BY trace_id, span_name, duration_ms, start_ms
1232
+ ),
1233
+ deduped AS (
1234
+ SELECT DISTINCT ON (trace_id)
1235
+ trace_id,
1236
+ span_name,
1237
+ duration_ms,
1238
+ start_ms,
1239
+ session_id,
1240
+ user_id,
1241
+ project_id,
1242
+ model,
1243
+ cost,
1244
+ input_tokens,
1245
+ output_tokens,
1246
+ agent_id,
1247
+ agent_version,
1248
+ source,
1249
+ engine,
1250
+ request_id
1251
+ FROM span_rows
1252
+ ORDER BY
1253
+ trace_id,
1254
+ COALESCE(cost, 0) DESC,
1255
+ CASE
1256
+ WHEN span_name = 'raw_gen_ai_request' THEN 0
1257
+ WHEN span_name = 'litellm_request' THEN 1
1258
+ ELSE 2
1259
+ END,
1260
+ start_ms DESC
1261
+ )
1262
+ SELECT * FROM deduped
1263
+ "
1264
+
1265
+ # ============================================================================
1266
+ # Query functions
1267
+ # ============================================================================
1268
+
1269
+ query_total_cost() {
1270
+ check_traces
1271
+ echo "📊 Total Cost Summary"
1272
+ echo "===================="
1273
+ ${DUCKDB} -c "
1274
+ ${BASE_QUERY},
1275
+ extracted AS (${EXTRACT_ATTRS})
1276
+ SELECT
1277
+ COALESCE(SUM(cost), 0) as total_cost,
1278
+ COALESCE(SUM(input_tokens), 0) as total_input_tokens,
1279
+ COALESCE(SUM(output_tokens), 0) as total_output_tokens,
1280
+ COUNT(*) as total_requests,
1281
+ ROUND(AVG(duration_ms), 0) as avg_latency_ms
1282
+ FROM extracted;
1283
+ "
1284
+ }
1285
+
1286
+ query_cost_by_session() {
1287
+ check_traces
1288
+ echo "📊 Cost by Session"
1289
+ echo "=================="
1290
+ ${DUCKDB} -c "
1291
+ ${BASE_QUERY},
1292
+ extracted AS (${EXTRACT_ATTRS})
1293
+ SELECT
1294
+ COALESCE(session_id, '(no session)') as session_id,
1295
+ ROUND(COALESCE(SUM(cost), 0), 6) as total_cost,
1296
+ COALESCE(SUM(input_tokens), 0) as input_tokens,
1297
+ COALESCE(SUM(output_tokens), 0) as output_tokens,
1298
+ COUNT(*) as requests,
1299
+ ROUND(AVG(duration_ms), 0) as avg_latency_ms
1300
+ FROM extracted
1301
+ GROUP BY session_id
1302
+ ORDER BY total_cost DESC
1303
+ LIMIT 20;
1304
+ "
1305
+ }
1306
+
1307
+ query_cost_by_model() {
1308
+ check_traces
1309
+ echo "📊 Cost by Model"
1310
+ echo "================"
1311
+ ${DUCKDB} -c "
1312
+ ${BASE_QUERY},
1313
+ extracted AS (${EXTRACT_ATTRS})
1314
+ SELECT
1315
+ COALESCE(model, '(unknown)') as model,
1316
+ ROUND(COALESCE(SUM(cost), 0), 6) as total_cost,
1317
+ COALESCE(SUM(input_tokens), 0) as input_tokens,
1318
+ COALESCE(SUM(output_tokens), 0) as output_tokens,
1319
+ COUNT(*) as requests,
1320
+ ROUND(AVG(duration_ms), 0) as avg_latency_ms
1321
+ FROM extracted
1322
+ GROUP BY model
1323
+ ORDER BY total_cost DESC;
1324
+ "
1325
+ }
1326
+
1327
+ query_cost_by_pack() {
1328
+ check_traces
1329
+ echo "📊 Cost by Agent"
1330
+ echo "================"
1331
+ ${DUCKDB} -c "
1332
+ ${BASE_QUERY},
1333
+ extracted AS (${EXTRACT_ATTRS})
1334
+ SELECT
1335
+ COALESCE(agent_id, '(no agent)') as agent,
1336
+ COALESCE(agent_version, '?') as version,
1337
+ ROUND(COALESCE(SUM(cost), 0), 6) as total_cost,
1338
+ COALESCE(SUM(input_tokens), 0) as input_tokens,
1339
+ COALESCE(SUM(output_tokens), 0) as output_tokens,
1340
+ COUNT(*) as requests
1341
+ FROM extracted
1342
+ GROUP BY agent_id, agent_version
1343
+ ORDER BY total_cost DESC;
1344
+ "
1345
+ }
1346
+
1347
+ query_cost_by_engine() {
1348
+ check_traces
1349
+ echo "📊 Cost by Engine"
1350
+ echo "================="
1351
+ ${DUCKDB} -c "
1352
+ ${BASE_QUERY},
1353
+ extracted AS (${EXTRACT_ATTRS})
1354
+ SELECT
1355
+ COALESCE(engine, '(unknown)') as engine,
1356
+ model,
1357
+ ROUND(SUM(cost), 6) as total_cost,
1358
+ COUNT(*) as requests,
1359
+ ROUND(AVG(duration_ms), 0) as avg_latency_ms
1360
+ FROM extracted
1361
+ GROUP BY engine, model
1362
+ ORDER BY total_cost DESC;
1363
+ "
1364
+ }
1365
+
1366
+ query_cost_by_source() {
1367
+ check_traces
1368
+ echo "📊 Cost by Source Framework"
1369
+ echo "=========================="
1370
+ ${DUCKDB} -c "
1371
+ ${BASE_QUERY},
1372
+ extracted AS (${EXTRACT_ATTRS})
1373
+ SELECT
1374
+ COALESCE(source, '(unknown)') as framework,
1375
+ ROUND(SUM(cost), 6) as total_cost,
1376
+ COUNT(DISTINCT session_id) as sessions,
1377
+ COUNT(*) as requests,
1378
+ ROUND(AVG(cost), 6) as avg_cost_per_request
1379
+ FROM extracted
1380
+ GROUP BY source
1381
+ ORDER BY total_cost DESC;
1382
+ "
1383
+ }
1384
+
1385
+ query_cost_per_request() {
1386
+ local session_id="${1:-}"
1387
+ check_traces
1388
+ echo "📊 Cost per Request"
1389
+ echo "==================="
1390
+ local where_clause=""
1391
+ if [[ -n "${session_id}" ]]; then
1392
+ where_clause="WHERE session_id = '${session_id}' AND request_id IS NOT NULL"
1393
+ echo " Session: ${session_id}"
1394
+ else
1395
+ where_clause="WHERE request_id IS NOT NULL"
1396
+ fi
1397
+ ${DUCKDB} -c "
1398
+ ${BASE_QUERY},
1399
+ extracted AS (${EXTRACT_ATTRS})
1400
+ SELECT
1401
+ request_id,
1402
+ COUNT(*) as llm_calls,
1403
+ ROUND(COALESCE(SUM(cost), 0), 6) as request_cost,
1404
+ COALESCE(SUM(input_tokens), 0) as input_tokens,
1405
+ COALESCE(SUM(output_tokens), 0) as output_tokens,
1406
+ ROUND(COALESCE(SUM(duration_ms), 0), 0) as total_latency_ms,
1407
+ MIN(to_timestamp(start_ms / 1000)) as started_at
1408
+ FROM extracted
1409
+ ${where_clause}
1410
+ GROUP BY request_id
1411
+ ORDER BY MIN(start_ms);
1412
+ "
1413
+ }
1414
+
1415
+ query_session() {
1416
+ local session_id="$1"
1417
+ check_traces
1418
+ echo "📊 Session: ${session_id}"
1419
+ echo "========================="
1420
+ ${DUCKDB} -c "
1421
+ ${BASE_QUERY},
1422
+ extracted AS (${EXTRACT_ATTRS})
1423
+ SELECT
1424
+ model,
1425
+ ROUND(cost, 6) as cost,
1426
+ input_tokens,
1427
+ output_tokens,
1428
+ duration_ms as latency_ms,
1429
+ COALESCE(request_id, '') as request_id,
1430
+ to_timestamp(start_ms / 1000) as timestamp
1431
+ FROM extracted
1432
+ WHERE session_id = '${session_id}'
1433
+ ORDER BY start_ms;
1434
+ "
1435
+ }
1436
+
1437
+ query_request() {
1438
+ local request_id="$1"
1439
+ check_traces
1440
+ echo "📊 Request: ${request_id}"
1441
+ echo "========================="
1442
+ ${DUCKDB} -c "
1443
+ ${BASE_QUERY},
1444
+ extracted AS (${EXTRACT_ATTRS})
1445
+ SELECT
1446
+ model,
1447
+ ROUND(cost, 6) as cost,
1448
+ input_tokens,
1449
+ output_tokens,
1450
+ duration_ms as latency_ms,
1451
+ to_timestamp(start_ms / 1000) as timestamp
1452
+ FROM extracted
1453
+ WHERE request_id = '${request_id}'
1454
+ ORDER BY start_ms;
1455
+ "
1456
+ }
1457
+
1458
+ query_session_messages() {
1459
+ local session_id="$1"
1460
+ local limit="${2:-${TRACE_EVENT_PREVIEW_LIMIT}}"
1461
+ local artifact_path
1462
+ check_traces
1463
+ if ! [[ "${limit}" =~ ^[0-9]+$ ]]; then
1464
+ echo "Limit must be numeric"
1465
+ exit 1
1466
+ fi
1467
+ artifact_path="$(make_artifact_path "session-trace-$(sanitize_artifact_label "${session_id}")" "txt")"
1468
+ echo "📊 Session Messages (trace parse, safe preview): ${session_id}"
1469
+ echo "================================================"
1470
+ python3 - "${TRACES_DIR}" "${session_id}" "${limit}" "${MESSAGE_PREVIEW_CHARS}" "${MESSAGE_OUTPUT_BUDGET_CHARS}" "${artifact_path}" <<'PY'
1471
+ import hashlib
1472
+ import json
1473
+ import os
1474
+ import pathlib
1475
+ import re
1476
+ import sys
1477
+ import textwrap
1478
+ from datetime import datetime, timezone
1479
+
1480
+ traces_dir = pathlib.Path(sys.argv[1])
1481
+ target_session = sys.argv[2]
1482
+ limit = max(1, int(sys.argv[3]))
1483
+ preview_chars = max(80, int(sys.argv[4]))
1484
+ output_budget = max(1000, int(sys.argv[5]))
1485
+ artifact_path = sys.argv[6]
1486
+
1487
+ session_patterns = [
1488
+ re.compile(r"x-litellm-metadata-session-id'?:\s*'([^']+)'"),
1489
+ re.compile(r'"x-litellm-metadata-session-id"\s*:\s*"([^"]+)"'),
1490
+ ]
1491
+ request_patterns = [
1492
+ re.compile(r"x-litellm-metadata-request-id'?:\s*'([^']+)'"),
1493
+ re.compile(r'"x-litellm-metadata-request-id"\s*:\s*"([^"]+)"'),
1494
+ ]
1495
+
1496
+ input_keys = {
1497
+ "gen_ai.input.messages",
1498
+ "llm.None.messages",
1499
+ "llm.openai.messages",
1500
+ "llm.anthropic.messages",
1501
+ }
1502
+ output_keys = {
1503
+ "gen_ai.output.messages",
1504
+ "llm.openai.choices",
1505
+ "llm.anthropic.completion",
1506
+ }
1507
+
1508
+
1509
+ def attr_string_value(attr):
1510
+ value = (attr or {}).get("value", {})
1511
+ if "stringValue" in value:
1512
+ return value.get("stringValue")
1513
+ if "intValue" in value:
1514
+ return str(value.get("intValue"))
1515
+ if "doubleValue" in value:
1516
+ return str(value.get("doubleValue"))
1517
+ if "boolValue" in value:
1518
+ return "true" if value.get("boolValue") else "false"
1519
+ return ""
1520
+
1521
+
1522
+ def extract_header_value(raw, patterns):
1523
+ if not isinstance(raw, str):
1524
+ return ""
1525
+ for pattern in patterns:
1526
+ match = pattern.search(raw)
1527
+ if match:
1528
+ return match.group(1)
1529
+ return ""
1530
+
1531
+
1532
+ def extract_text_parts(msg):
1533
+ if isinstance(msg, str):
1534
+ return [msg]
1535
+ if not isinstance(msg, dict):
1536
+ return [str(msg)]
1537
+
1538
+ if "message" in msg:
1539
+ return extract_text_parts(msg["message"])
1540
+
1541
+ if "choices" in msg and isinstance(msg["choices"], list):
1542
+ collected = []
1543
+ for choice in msg["choices"]:
1544
+ collected.extend(extract_text_parts(choice))
1545
+ return collected
1546
+
1547
+ for key in ("parts", "content"):
1548
+ value = msg.get(key)
1549
+ if isinstance(value, str):
1550
+ return [value]
1551
+ if isinstance(value, list):
1552
+ collected = []
1553
+ for item in value:
1554
+ if isinstance(item, dict):
1555
+ item_type = item.get("type", "")
1556
+ if item_type in ("thinking", "signature", "tool_result"):
1557
+ continue
1558
+ text = item.get("text", item.get("content", ""))
1559
+ if text:
1560
+ collected.append(str(text))
1561
+ continue
1562
+ collected.extend(extract_text_parts(item))
1563
+ elif isinstance(item, str):
1564
+ collected.append(item)
1565
+ if collected:
1566
+ return collected
1567
+
1568
+ text = msg.get("text")
1569
+ if isinstance(text, str) and text:
1570
+ return [text]
1571
+
1572
+ content = msg.get("content")
1573
+ if isinstance(content, str) and content:
1574
+ return [content]
1575
+
1576
+ return [str(msg)]
1577
+
1578
+
1579
+ def build_part_tail_preview(parts):
1580
+ source_parts = [str(part) for part in parts if str(part)]
1581
+ if not source_parts:
1582
+ return "", "", 0, 0
1583
+ part_preview_chars = max(80, min(400, preview_chars))
1584
+ preview_parts = []
1585
+ truncated_part_count = 0
1586
+ for part in source_parts:
1587
+ if len(part) > part_preview_chars:
1588
+ preview_parts.append("..." + part[-part_preview_chars:])
1589
+ truncated_part_count += 1
1590
+ else:
1591
+ preview_parts.append(part)
1592
+ return "\n".join(source_parts), "\n...\n".join(preview_parts), len(source_parts), truncated_part_count
1593
+
1594
+
1595
+ def format_text(content, indent=4, width=100):
1596
+ if content is None:
1597
+ content = ""
1598
+ content = str(content)
1599
+ prefix = " " * indent
1600
+ return textwrap.fill(content, width=width, initial_indent=prefix, subsequent_indent=prefix)
1601
+
1602
+
1603
+ def iter_payload_messages(payload, kind):
1604
+ try:
1605
+ parsed = json.loads(payload)
1606
+ except Exception:
1607
+ parsed = None
1608
+
1609
+ if isinstance(parsed, list):
1610
+ for item in parsed:
1611
+ yield item
1612
+ return
1613
+ if isinstance(parsed, dict) and isinstance(parsed.get("choices"), list):
1614
+ for item in parsed["choices"]:
1615
+ yield item
1616
+ return
1617
+ if parsed is not None:
1618
+ yield parsed
1619
+ return
1620
+ yield payload
1621
+
1622
+
1623
+ messages = []
1624
+ seen_payloads = set()
1625
+ message_seq = 0
1626
+ for file_path in sorted(traces_dir.glob("*.jsonl")):
1627
+ try:
1628
+ with file_path.open("r", encoding="utf-8", errors="ignore") as handle:
1629
+ for line in handle:
1630
+ line = line.strip()
1631
+ if not line:
1632
+ continue
1633
+ try:
1634
+ root = json.loads(line)
1635
+ except Exception:
1636
+ continue
1637
+
1638
+ for resource_span in root.get("resourceSpans", []) or []:
1639
+ for scope_span in resource_span.get("scopeSpans", []) or []:
1640
+ for span in scope_span.get("spans", []) or []:
1641
+ attrs = span.get("attributes", []) or []
1642
+ headers = ""
1643
+ model = ""
1644
+ input_payloads = []
1645
+ output_payloads = []
1646
+
1647
+ for attr in attrs:
1648
+ key = attr.get("key")
1649
+ value = attr_string_value(attr)
1650
+ if key == "metadata.requester_custom_headers" and isinstance(value, str):
1651
+ headers = value
1652
+ elif key in ("gen_ai.request.model", "gen_ai.response.model", "model") and value:
1653
+ model = str(value)
1654
+ elif key in input_keys and value:
1655
+ input_payloads.append(str(value))
1656
+ elif key in output_keys and value:
1657
+ output_payloads.append(str(value))
1658
+
1659
+ session_id = extract_header_value(headers, session_patterns)
1660
+ if session_id != target_session:
1661
+ continue
1662
+
1663
+ request_id = extract_header_value(headers, request_patterns)
1664
+ try:
1665
+ start_ns = int(span.get("startTimeUnixNano") or 0)
1666
+ except Exception:
1667
+ start_ns = 0
1668
+
1669
+ trace_id = span.get("traceId", "")
1670
+ timestamp = datetime.fromtimestamp(start_ns / 1_000_000_000, tz=timezone.utc).isoformat() if start_ns else "(unknown)"
1671
+ for kind, payloads in (("input", input_payloads), ("output", output_payloads)):
1672
+ for payload in payloads:
1673
+ digest = hashlib.sha1(payload.encode("utf-8", errors="ignore")).hexdigest()
1674
+ request_key = request_id or trace_id
1675
+ dedupe_key = (kind, request_key, digest)
1676
+ if dedupe_key in seen_payloads:
1677
+ continue
1678
+ seen_payloads.add(dedupe_key)
1679
+ for raw_message in iter_payload_messages(payload, kind):
1680
+ role = "assistant" if kind == "output" else "user"
1681
+ if isinstance(raw_message, dict):
1682
+ role = str(
1683
+ raw_message.get("role")
1684
+ or raw_message.get("message", {}).get("role")
1685
+ or role
1686
+ ).lower()
1687
+ content_parts = extract_text_parts(raw_message)
1688
+ content, preview, preview_part_count, truncated_part_count = build_part_tail_preview(content_parts)
1689
+ if role not in ("user", "assistant") or not content:
1690
+ continue
1691
+ message_seq += 1
1692
+ messages.append(
1693
+ {
1694
+ "role": role,
1695
+ "ts": timestamp,
1696
+ "request_id": request_id,
1697
+ "model": model or "(unknown)",
1698
+ "start_ns": start_ns,
1699
+ "seq": message_seq,
1700
+ "content_text": content,
1701
+ "content_preview": preview,
1702
+ "content_chars": len(content),
1703
+ "is_truncated": truncated_part_count > 0,
1704
+ "preview_part_count": preview_part_count,
1705
+ "truncated_part_count": truncated_part_count,
1706
+ }
1707
+ )
1708
+ except Exception:
1709
+ continue
1710
+
1711
+ if not messages:
1712
+ print("(no trace messages found for this session)")
1713
+ raise SystemExit(0)
1714
+
1715
+ messages.sort(key=lambda row: (row["start_ns"], row["seq"]))
1716
+ selected = messages[-limit:]
1717
+
1718
+ def render_row(row, index, full=False):
1719
+ header = (
1720
+ f"\n[{index}] {row['ts']} | {row['role'].upper()} | model={row['model']}"
1721
+ + (f" | request_id={row['request_id']}" if row["request_id"] else "")
1722
+ )
1723
+ body = format_text(row["content_text"] if full else row["content_preview"])
1724
+ suffix = ""
1725
+ if row["is_truncated"] and not full:
1726
+ if row.get("preview_part_count", 1) > 1:
1727
+ suffix = (
1728
+ f"\n [per-part tail preview: {row['preview_part_count']} parts, "
1729
+ f"{row.get('truncated_part_count', 0)} truncated, {row['content_chars']} chars total]"
1730
+ )
1731
+ else:
1732
+ visible_chars = len(row["content_preview"]) - 3 if row["content_preview"].startswith("...") else len(row["content_preview"])
1733
+ suffix = f"\n [tail preview truncated: showing last {visible_chars} of {row['content_chars']} chars]"
1734
+ return f"{header}\n{body}{suffix}"
1735
+
1736
+ rendered_selected = [render_row(row, idx) for idx, row in enumerate(selected, start=1)]
1737
+ artifact_needed = (
1738
+ len(messages) > len(selected)
1739
+ or any(row["is_truncated"] for row in messages)
1740
+ )
1741
+ if artifact_needed:
1742
+ with open(artifact_path, "w", encoding="utf-8") as handle:
1743
+ handle.write("Spotlight Query trace message artifact\n")
1744
+ handle.write(f"messages={len(messages)} selected={len(selected)}\n")
1745
+ for idx, row in enumerate(messages, start=1):
1746
+ handle.write(render_row(row, idx, full=True))
1747
+ handle.write("\n")
1748
+
1749
+ printed = 0
1750
+ consumed = 0
1751
+ for block in rendered_selected:
1752
+ projected = consumed + len(block)
1753
+ if printed > 0 and projected > output_budget:
1754
+ break
1755
+ print(block)
1756
+ consumed = projected
1757
+ printed += 1
1758
+
1759
+ omitted_from_selection = len(rendered_selected) - printed
1760
+ if len(messages) > len(selected):
1761
+ print(f"\nShowing last {len(selected)} of {len(messages)} extracted user/assistant messages.")
1762
+ if omitted_from_selection > 0:
1763
+ print(f"Output clipped after {printed} rows to stay within {output_budget} chars.")
1764
+ if artifact_needed:
1765
+ print(f"Artifact: {artifact_path}")
1766
+ PY
1767
+ }
1768
+
1769
+ query_compare_versions() {
1770
+ local agent_name="$1"
1771
+ check_traces
1772
+ echo "📊 Compare Versions: ${agent_name}"
1773
+ echo "===================================="
1774
+ ${DUCKDB} -c "
1775
+ ${BASE_QUERY},
1776
+ extracted AS (${EXTRACT_ATTRS})
1777
+ SELECT
1778
+ agent_version as version,
1779
+ COUNT(DISTINCT session_id) as sessions,
1780
+ ROUND(AVG(session_cost), 4) as avg_cost_per_session,
1781
+ ROUND(AVG(session_tokens), 0) as avg_tokens_per_session,
1782
+ ROUND(AVG(session_latency), 0) as avg_latency_ms,
1783
+ ROUND(AVG(requests_per_session), 1) as avg_llm_calls
1784
+ FROM (
1785
+ SELECT
1786
+ agent_version,
1787
+ session_id,
1788
+ SUM(cost) as session_cost,
1789
+ SUM(COALESCE(input_tokens, 0) + COALESCE(output_tokens, 0)) as session_tokens,
1790
+ SUM(duration_ms) as session_latency,
1791
+ COUNT(*) as requests_per_session
1792
+ FROM extracted
1793
+ WHERE agent_id = '${agent_name}'
1794
+ GROUP BY agent_version, session_id
1795
+ )
1796
+ GROUP BY agent_version
1797
+ ORDER BY agent_version DESC;
1798
+ "
1799
+ }
1800
+
1801
+ query_top_sessions() {
1802
+ local agent_name="$1"
1803
+ local since_ms="${2:-86400000}" # default: 24h in ms
1804
+ check_traces
1805
+ echo "📊 Top Sessions: ${agent_name}"
1806
+ echo "==============================="
1807
+ ${DUCKDB} -c "
1808
+ ${BASE_QUERY},
1809
+ extracted AS (${EXTRACT_ATTRS})
1810
+ SELECT
1811
+ session_id,
1812
+ ROUND(SUM(cost), 6) as total_cost,
1813
+ COALESCE(SUM(input_tokens), 0) as input_tokens,
1814
+ COALESCE(SUM(output_tokens), 0) as output_tokens,
1815
+ COUNT(*) as llm_calls,
1816
+ COUNT(DISTINCT request_id) as requests,
1817
+ ROUND(AVG(duration_ms), 0) as avg_latency_ms,
1818
+ MIN(to_timestamp(start_ms / 1000)) as started_at
1819
+ FROM extracted
1820
+ WHERE agent_id = '${agent_name}'
1821
+ AND start_ms > (EXTRACT(EPOCH FROM NOW()) * 1000 - ${since_ms})
1822
+ GROUP BY session_id
1823
+ ORDER BY total_cost DESC
1824
+ LIMIT 10;
1825
+ "
1826
+ }
1827
+
1828
+ query_latency_percentiles() {
1829
+ check_traces
1830
+ echo "📊 Latency Percentiles (by model and agent)"
1831
+ echo "============================================"
1832
+ ${DUCKDB} -c "
1833
+ ${BASE_QUERY},
1834
+ extracted AS (${EXTRACT_ATTRS})
1835
+ SELECT
1836
+ COALESCE(agent_id, '(default)') as agent,
1837
+ model,
1838
+ COUNT(*) as requests,
1839
+ ROUND(PERCENTILE_CONT(0.50) WITHIN GROUP (ORDER BY duration_ms), 0) as p50_ms,
1840
+ ROUND(PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY duration_ms), 0) as p95_ms,
1841
+ ROUND(AVG(duration_ms), 0) as avg_ms,
1842
+ MAX(duration_ms) as max_ms
1843
+ FROM extracted
1844
+ GROUP BY agent_id, model
1845
+ ORDER BY p95_ms DESC;
1846
+ "
1847
+ }
1848
+
1849
+ query_errors() {
1850
+ check_traces
1851
+ echo "📊 Error Rate by Model"
1852
+ echo "======================"
1853
+ echo "(Note: error detection depends on LiteLLM OTEL span attributes)"
1854
+ ${DUCKDB} -c "
1855
+ ${BASE_QUERY},
1856
+ extracted AS (${EXTRACT_ATTRS}),
1857
+ error_traces AS (
1858
+ SELECT DISTINCT trace_id
1859
+ FROM attrs
1860
+ WHERE attr_key IN ('exception.type', 'exception.message')
1861
+ OR (attr_key = 'otel.status_code' AND attr_string = 'ERROR')
1862
+ OR (attr_key = 'http.status_code' AND CAST(attr_int AS INT) >= 400)
1863
+ )
1864
+ SELECT
1865
+ COALESCE(e.model, '(unknown)') as model,
1866
+ COUNT(*) as total_requests,
1867
+ COUNT(er.trace_id) as errors,
1868
+ ROUND(COUNT(er.trace_id)::FLOAT / COUNT(*) * 100, 1) as error_rate_pct
1869
+ FROM extracted e
1870
+ LEFT JOIN error_traces er ON e.trace_id = er.trace_id
1871
+ GROUP BY e.model
1872
+ ORDER BY error_rate_pct DESC;
1873
+ "
1874
+ }
1875
+
1876
+ query_largest_requests() {
1877
+ local limit="${1:-20}"
1878
+ check_traces
1879
+ echo "📊 Largest Requests by Input Tokens"
1880
+ echo "===================================="
1881
+ ${DUCKDB} -c "
1882
+ ${BASE_QUERY},
1883
+ extracted AS (${EXTRACT_ATTRS})
1884
+ SELECT
1885
+ COALESCE(session_id, '') as session_id,
1886
+ COALESCE(request_id, '') as request_id,
1887
+ COALESCE(agent_id, '') as agent,
1888
+ model,
1889
+ input_tokens,
1890
+ output_tokens,
1891
+ ROUND(cost, 6) as cost,
1892
+ duration_ms as latency_ms,
1893
+ to_timestamp(start_ms / 1000) as timestamp
1894
+ FROM extracted
1895
+ WHERE input_tokens IS NOT NULL
1896
+ ORDER BY input_tokens DESC
1897
+ LIMIT ${limit};
1898
+ "
1899
+ }
1900
+
1901
+ query_problems() {
1902
+ check_traces
1903
+ echo "📊 Problem Sessions (statistical outliers)"
1904
+ echo "==========================================="
1905
+ ${DUCKDB} -c "
1906
+ ${BASE_QUERY},
1907
+ extracted AS (${EXTRACT_ATTRS}),
1908
+ session_summary AS (
1909
+ SELECT
1910
+ session_id,
1911
+ COALESCE(agent_id, '(default)') as agent_id,
1912
+ ROUND(SUM(cost), 6) as total_cost,
1913
+ SUM(COALESCE(input_tokens, 0)) as total_input_tokens,
1914
+ COUNT(*) as llm_calls,
1915
+ COUNT(DISTINCT request_id) as requests,
1916
+ MAX(duration_ms) as max_latency_ms,
1917
+ MIN(to_timestamp(start_ms / 1000)) as started_at
1918
+ FROM extracted
1919
+ GROUP BY session_id, agent_id
1920
+ ),
1921
+ overall_stats AS (
1922
+ SELECT
1923
+ AVG(total_cost) as avg_cost,
1924
+ STDDEV(total_cost) as stddev_cost,
1925
+ AVG(total_input_tokens) as avg_tokens,
1926
+ STDDEV(total_input_tokens) as stddev_tokens
1927
+ FROM session_summary
1928
+ WHERE total_cost > 0
1929
+ )
1930
+ SELECT
1931
+ s.session_id,
1932
+ s.agent_id as agent,
1933
+ s.total_cost,
1934
+ s.total_input_tokens as input_tokens,
1935
+ s.llm_calls,
1936
+ s.requests,
1937
+ s.max_latency_ms,
1938
+ CASE
1939
+ WHEN o.stddev_cost > 0 AND s.total_cost > o.avg_cost + 2 * o.stddev_cost THEN 'COST OUTLIER'
1940
+ WHEN o.stddev_tokens > 0 AND s.total_input_tokens > o.avg_tokens + 2 * o.stddev_tokens THEN 'TOKEN OUTLIER'
1941
+ WHEN s.requests > 0 AND s.llm_calls > s.requests * 5 THEN 'EXCESSIVE LLM CALLS'
1942
+ WHEN s.max_latency_ms > 30000 THEN 'HIGH LATENCY'
1943
+ ELSE '?'
1944
+ END as problem_type,
1945
+ s.started_at
1946
+ FROM session_summary s, overall_stats o
1947
+ WHERE (o.stddev_cost > 0 AND s.total_cost > o.avg_cost + 2 * o.stddev_cost)
1948
+ OR (o.stddev_tokens > 0 AND s.total_input_tokens > o.avg_tokens + 2 * o.stddev_tokens)
1949
+ OR (s.requests > 0 AND s.llm_calls > s.requests * 5)
1950
+ OR s.max_latency_ms > 30000
1951
+ ORDER BY s.total_cost DESC
1952
+ LIMIT 20;
1953
+ "
1954
+ }
1955
+
1956
+ # ============================================================================
1957
+ # Tier 2: Session Event Queries (require Layer 2 session events)
1958
+ # Session events live in ${ANALYTICS_DIR}/sessions/<session_id>.jsonl
1959
+ # ============================================================================
1960
+ SESSIONS_DIR="${ANALYTICS_DIR}/sessions"
1961
+
1962
+ check_sessions() {
1963
+ if ! ls "${SESSIONS_DIR}"/*.jsonl &> /dev/null; then
1964
+ echo "No session event files found in ${SESSIONS_DIR}/"
1965
+ echo "Session events are captured when running agents with observability enabled."
1966
+ exit 0
1967
+ fi
1968
+ }
1969
+
1970
+ query_tool_loops() {
1971
+ local session_id="$1"
1972
+ check_sessions
1973
+ echo "📊 Tool Loop Detection: ${session_id}"
1974
+ echo "======================================"
1975
+ ${DUCKDB} -c "
1976
+ WITH tool_events AS (
1977
+ SELECT
1978
+ session_id,
1979
+ request_id,
1980
+ json_extract_string(to_json(payload), '$.toolName') as tool_name,
1981
+ json_extract_string(to_json(payload), '$.callId') as call_id,
1982
+ timestamp
1983
+ FROM read_json(
1984
+ '${SESSIONS_DIR}/${session_id}.jsonl',
1985
+ format='newline_delimited',
1986
+ ignore_errors=true
1987
+ )
1988
+ WHERE event_type = 'tool_start'
1989
+ ),
1990
+ tool_counts AS (
1991
+ SELECT
1992
+ request_id,
1993
+ tool_name,
1994
+ COUNT(*) as call_count,
1995
+ MIN(timestamp) as first_call,
1996
+ MAX(timestamp) as last_call
1997
+ FROM tool_events
1998
+ GROUP BY request_id, tool_name
1999
+ )
2000
+ SELECT
2001
+ COALESCE(request_id, '(unknown)') as request_id,
2002
+ tool_name,
2003
+ call_count,
2004
+ CASE WHEN call_count > 5 THEN 'POSSIBLE LOOP' ELSE 'ok' END as status,
2005
+ ROUND((last_call - first_call) / 1000.0, 1) as span_seconds
2006
+ FROM tool_counts
2007
+ ORDER BY call_count DESC;
2008
+ "
2009
+ }
2010
+
2011
+ query_tool_stats() {
2012
+ local session_filter="${1:-}"
2013
+ check_sessions
2014
+ echo "📊 Tool Usage Statistics"
2015
+ echo "========================"
2016
+ local glob_pattern
2017
+ if [[ -n "${session_filter}" ]]; then
2018
+ glob_pattern="${SESSIONS_DIR}/${session_filter}.jsonl"
2019
+ echo " Session: ${session_filter}"
2020
+ else
2021
+ glob_pattern="${SESSIONS_DIR}/*.jsonl"
2022
+ fi
2023
+ ${DUCKDB} -c "
2024
+ WITH starts AS (
2025
+ SELECT
2026
+ session_id,
2027
+ request_id,
2028
+ json_extract_string(to_json(payload), '$.toolName') as tool_name,
2029
+ json_extract_string(to_json(payload), '$.callId') as call_id,
2030
+ timestamp as start_ts
2031
+ FROM read_json('${glob_pattern}',
2032
+ format='newline_delimited', ignore_errors=true)
2033
+ WHERE event_type = 'tool_start'
2034
+ AND json_extract_string(to_json(payload), '$.callId') IS NOT NULL
2035
+ AND json_extract_string(to_json(payload), '$.callId') != ''
2036
+ ),
2037
+ ends AS (
2038
+ SELECT
2039
+ session_id,
2040
+ request_id,
2041
+ json_extract_string(to_json(payload), '$.toolName') as tool_name,
2042
+ json_extract_string(to_json(payload), '$.callId') as call_id,
2043
+ timestamp as end_ts,
2044
+ CASE WHEN json_extract_string(to_json(payload), '$.isError') = 'true' THEN 1 ELSE 0 END as is_error
2045
+ FROM read_json('${glob_pattern}',
2046
+ format='newline_delimited', ignore_errors=true)
2047
+ WHERE event_type = 'tool_end'
2048
+ AND json_extract_string(to_json(payload), '$.callId') IS NOT NULL
2049
+ AND json_extract_string(to_json(payload), '$.callId') != ''
2050
+ ),
2051
+ tool_pairs AS (
2052
+ SELECT
2053
+ s.tool_name,
2054
+ (e.end_ts - s.start_ts) as duration_ms,
2055
+ e.is_error
2056
+ FROM starts s
2057
+ JOIN ends e ON s.call_id = e.call_id AND s.session_id = e.session_id AND s.request_id = e.request_id
2058
+ )
2059
+ SELECT
2060
+ tool_name,
2061
+ COUNT(*) as total_calls,
2062
+ ROUND(AVG(duration_ms), 0) as avg_duration_ms,
2063
+ ROUND(SUM(is_error)::FLOAT / COUNT(*) * 100, 1) as error_rate_pct,
2064
+ MAX(duration_ms) as max_duration_ms
2065
+ FROM tool_pairs
2066
+ GROUP BY tool_name
2067
+ ORDER BY total_calls DESC;
2068
+ "
2069
+ }
2070
+
2071
+ query_session_timeline() {
2072
+ local session_id="$1"
2073
+ check_traces
2074
+ echo "📊 Session Timeline: ${session_id}"
2075
+ echo "==================================="
2076
+ echo "(unified LLM calls + tool events)"
2077
+ # Check if session events exist for this session
2078
+ local has_events=false
2079
+ if [[ -f "${SESSIONS_DIR}/${session_id}.jsonl" ]]; then
2080
+ has_events=true
2081
+ fi
2082
+
2083
+ if [[ "${has_events}" == "true" ]]; then
2084
+ ${DUCKDB} -c "
2085
+ ${BASE_QUERY},
2086
+ extracted AS (${EXTRACT_ATTRS}),
2087
+ llm_events AS (
2088
+ SELECT
2089
+ start_ms as timestamp,
2090
+ 'llm_call' as event_type,
2091
+ model as detail,
2092
+ ROUND(cost, 6) as cost,
2093
+ input_tokens,
2094
+ output_tokens,
2095
+ duration_ms
2096
+ FROM extracted
2097
+ WHERE session_id = '${session_id}'
2098
+ ),
2099
+ agent_events AS (
2100
+ SELECT
2101
+ timestamp,
2102
+ event_type,
2103
+ COALESCE(
2104
+ json_extract_string(to_json(payload), '$.toolName'),
2105
+ event_type
2106
+ ) as detail,
2107
+ NULL::DOUBLE as cost,
2108
+ NULL::BIGINT as input_tokens,
2109
+ NULL::BIGINT as output_tokens,
2110
+ NULL::BIGINT as duration_ms
2111
+ FROM read_json(
2112
+ '${SESSIONS_DIR}/${session_id}.jsonl',
2113
+ format='newline_delimited',
2114
+ ignore_errors=true
2115
+ )
2116
+ )
2117
+ SELECT * FROM (
2118
+ SELECT * FROM llm_events
2119
+ UNION ALL
2120
+ SELECT * FROM agent_events
2121
+ )
2122
+ ORDER BY timestamp;
2123
+ "
2124
+ else
2125
+ # No session events — just show LLM calls
2126
+ ${DUCKDB} -c "
2127
+ ${BASE_QUERY},
2128
+ extracted AS (${EXTRACT_ATTRS})
2129
+ SELECT
2130
+ start_ms as timestamp,
2131
+ 'llm_call' as event_type,
2132
+ model as detail,
2133
+ ROUND(cost, 6) as cost,
2134
+ input_tokens,
2135
+ output_tokens,
2136
+ duration_ms
2137
+ FROM extracted
2138
+ WHERE session_id = '${session_id}'
2139
+ ORDER BY start_ms;
2140
+ "
2141
+ fi
2142
+ }
2143
+
2144
+ query_latest_session() {
2145
+ check_traces
2146
+ echo "📊 Latest Session"
2147
+ echo "================"
2148
+ ${DUCKDB} -c "
2149
+ ${BASE_QUERY},
2150
+ extracted AS (${EXTRACT_ATTRS})
2151
+ SELECT
2152
+ COALESCE(session_id, '(no session)') as session_id,
2153
+ COALESCE(model, '(unknown)') as model,
2154
+ ROUND(COALESCE(cost, 0), 6) as cost,
2155
+ COALESCE(input_tokens, 0) as input_tokens,
2156
+ COALESCE(output_tokens, 0) as output_tokens,
2157
+ duration_ms as latency_ms,
2158
+ COALESCE(agent_id, '') as agent_id,
2159
+ COALESCE(request_id, '') as request_id,
2160
+ to_timestamp(start_ms / 1000) as timestamp
2161
+ FROM extracted
2162
+ ORDER BY start_ms DESC
2163
+ LIMIT 1;
2164
+ "
2165
+ }
2166
+
2167
+ query_custom() {
2168
+ local sql="$1"
2169
+ check_traces
2170
+
2171
+ # Catch common shell-expansion pitfall:
2172
+ # --sql "${BASE_QUERY}, extracted AS (${EXTRACT_ATTRS}) ..."
2173
+ # If BASE_QUERY/EXTRACT_ATTRS are not defined in the caller shell, this becomes invalid SQL.
2174
+ if [[ "${sql}" == *"extracted AS ()"* ]] || [[ "${sql}" == *"attrs AS ()"* ]]; then
2175
+ echo "Invalid SQL: CTE placeholders were expanded to empty by your shell."
2176
+ echo "Hint: wrap --sql in single quotes, e.g.:"
2177
+ echo " --sql '\${BASE_QUERY}, extracted AS (\${EXTRACT_ATTRS}) SELECT COUNT(*) FROM extracted'"
2178
+ exit 1
2179
+ fi
2180
+
2181
+ # Support callers that pass placeholders literally (e.g. Python subprocess):
2182
+ # ${BASE_QUERY}, extracted AS (${EXTRACT_ATTRS}) ...
2183
+ local expanded_sql="${sql//\$\{BASE_QUERY\}/${BASE_QUERY}}"
2184
+ expanded_sql="${expanded_sql//\$\{EXTRACT_ATTRS\}/${EXTRACT_ATTRS}}"
2185
+ expanded_sql="${expanded_sql//\$BASE_QUERY/${BASE_QUERY}}"
2186
+ expanded_sql="${expanded_sql//\$EXTRACT_ATTRS/${EXTRACT_ATTRS}}"
2187
+ run_duckdb_sql "${expanded_sql}"
2188
+ }
2189
+
2190
+ interactive_shell() {
2191
+ check_traces
2192
+ echo "🦆 DuckDB Interactive Shell"
2193
+ echo "==========================="
2194
+ echo ""
2195
+ if has_indexed_db; then
2196
+ echo "Indexed DB: ${INDEX_DB_PATH}"
2197
+ echo ""
2198
+ echo "Example queries:"
2199
+ echo " SELECT * FROM sessions ORDER BY ended_at DESC LIMIT 5;"
2200
+ echo " SELECT session_id, request_id, start_ms FROM model_calls ORDER BY start_ms DESC LIMIT 20;"
2201
+ echo ""
2202
+ ${DUCKDB} -readonly "${INDEX_DB_PATH}"
2203
+ else
2204
+ echo "Trace files: ${TRACES_DIR}/*.jsonl"
2205
+ echo ""
2206
+ echo "Example queries:"
2207
+ echo " SELECT * FROM read_json('${TRACES_DIR}/*.jsonl') LIMIT 5;"
2208
+ echo ""
2209
+ ${DUCKDB}
2210
+ fi
2211
+ }
2212
+
2213
+ dump_schema() {
2214
+ if ! has_indexed_db; then
2215
+ echo '{"error": "Indexed analytics DB not found. Schema discovery requires analytics.duckdb."}' | python3 -m json.tool
2216
+ exit 1
2217
+ fi
2218
+
2219
+ INDEX_DB_PATH="${INDEX_DB_PATH}" DUCKDB_BIN="${DUCKDB}" python3 << 'PYTHON_EOF'
2220
+ import json
2221
+ import subprocess
2222
+ import sys
2223
+ import os
2224
+
2225
+ db_path = os.environ.get('INDEX_DB_PATH', '')
2226
+ duckdb_bin = os.environ.get('DUCKDB_BIN', 'duckdb')
2227
+ if not db_path:
2228
+ print('{"error": "INDEX_DB_PATH not set"}', file=sys.stderr)
2229
+ sys.exit(1)
2230
+
2231
+ def run_query(sql):
2232
+ """Run DuckDB query and return JSON results."""
2233
+ result = subprocess.run(
2234
+ [duckdb_bin, '-readonly', db_path, '-json', '-c', sql],
2235
+ capture_output=True,
2236
+ text=True
2237
+ )
2238
+ if result.returncode != 0:
2239
+ print(f"Error: {result.stderr}", file=sys.stderr)
2240
+ return []
2241
+ try:
2242
+ return json.loads(result.stdout)
2243
+ except json.JSONDecodeError:
2244
+ return []
2245
+
2246
+ # Fetch all tables
2247
+ tables = run_query("""
2248
+ SELECT table_name
2249
+ FROM information_schema.tables
2250
+ WHERE table_schema = 'main'
2251
+ ORDER BY table_name
2252
+ """)
2253
+
2254
+ # Fetch all columns with types and indices
2255
+ columns = run_query("""
2256
+ SELECT table_name, column_name, data_type
2257
+ FROM information_schema.columns
2258
+ WHERE table_schema = 'main'
2259
+ ORDER BY table_name, ordinal_position
2260
+ """)
2261
+
2262
+ # Fetch primary keys
2263
+ pks = run_query("""
2264
+ SELECT table_name, column_name
2265
+ FROM information_schema.key_column_usage
2266
+ WHERE table_schema = 'main' AND constraint_type = 'PRIMARY KEY'
2267
+ ORDER BY table_name, ordinal_position
2268
+ """)
2269
+
2270
+ # Fetch foreign keys
2271
+ fks = run_query("""
2272
+ SELECT
2273
+ table_name,
2274
+ column_name,
2275
+ constraint_name
2276
+ FROM information_schema.key_column_usage
2277
+ WHERE table_schema = 'main' AND constraint_type = 'FOREIGN KEY'
2278
+ ORDER BY table_name
2279
+ """)
2280
+
2281
+ # Fetch index info
2282
+ indices = run_query("""
2283
+ SELECT table_name, index_name, expressions
2284
+ FROM duckdb_indexes()
2285
+ ORDER BY table_name
2286
+ """)
2287
+
2288
+ # Build output structure
2289
+ pk_set = set((row['table_name'], row['column_name']) for row in pks)
2290
+ col_map = {}
2291
+ for row in columns:
2292
+ tbl = row['table_name']
2293
+ if tbl not in col_map:
2294
+ col_map[tbl] = []
2295
+ col_map[tbl].append({
2296
+ 'name': row['column_name'],
2297
+ 'type': row['data_type'],
2298
+ 'is_primary_key': (tbl, row['column_name']) in pk_set
2299
+ })
2300
+
2301
+ idx_map = {}
2302
+ for row in indices:
2303
+ tbl = row['table_name']
2304
+ if tbl not in idx_map:
2305
+ idx_map[tbl] = []
2306
+ idx_map[tbl].append({
2307
+ 'name': row['index_name'],
2308
+ 'expressions': row['expressions']
2309
+ })
2310
+
2311
+ schema = {
2312
+ 'tables': [
2313
+ {
2314
+ 'name': t['table_name'],
2315
+ 'columns': col_map.get(t['table_name'], []),
2316
+ 'indices': idx_map.get(t['table_name'], [])
2317
+ }
2318
+ for t in tables
2319
+ ],
2320
+ 'relationships': [
2321
+ {
2322
+ 'table': fk['table_name'],
2323
+ 'column': fk['column_name'],
2324
+ 'constraint': fk['constraint_name']
2325
+ }
2326
+ for fk in fks
2327
+ ]
2328
+ }
2329
+
2330
+ print(json.dumps(schema, indent=2))
2331
+ PYTHON_EOF
2332
+ }
2333
+
2334
+ # Parse --since duration to milliseconds (e.g., 24h, 7d, 1h)
2335
+ parse_since() {
2336
+ local since="$1"
2337
+ local num="${since%[hdm]}"
2338
+ local unit="${since: -1}"
2339
+ case "${unit}" in
2340
+ h) echo $(( num * 3600000 )) ;;
2341
+ d) echo $(( num * 86400000 )) ;;
2342
+ m) echo $(( num * 60000 )) ;;
2343
+ *) echo "86400000" ;; # default 24h
2344
+ esac
2345
+ }
2346
+
2347
+ # Main
2348
+ case "${1:-}" in
2349
+ --cost)
2350
+ query_total_cost_fast
2351
+ ;;
2352
+ --cost-by-session|--sessions)
2353
+ query_cost_by_session_fast
2354
+ ;;
2355
+ --cost-by-model|--models)
2356
+ query_cost_by_model_fast
2357
+ ;;
2358
+ --cost-by-pack|--agents)
2359
+ query_cost_by_pack_fast
2360
+ ;;
2361
+ --cost-by-engine)
2362
+ query_cost_by_engine_fast
2363
+ ;;
2364
+ --cost-by-source)
2365
+ query_cost_by_source_fast
2366
+ ;;
2367
+ --cost-per-request)
2368
+ query_cost_per_request_fast "${2:-}"
2369
+ ;;
2370
+ --session)
2371
+ if [[ -z "${2:-}" ]]; then
2372
+ echo "Usage: $0 --session <session-id>"
2373
+ exit 1
2374
+ fi
2375
+ query_session_fast "$2"
2376
+ ;;
2377
+ --session-summary)
2378
+ if [[ -z "${2:-}" ]]; then
2379
+ echo "Usage: $0 --session-summary <session-id>"
2380
+ exit 1
2381
+ fi
2382
+ query_session_summary_fast "$2"
2383
+ ;;
2384
+ --session-debug)
2385
+ if [[ -z "${2:-}" ]]; then
2386
+ echo "Usage: $0 --session-debug <session-id>"
2387
+ exit 1
2388
+ fi
2389
+ query_session_debug "$2"
2390
+ ;;
2391
+ --session-last)
2392
+ if [[ -z "${2:-}" ]]; then
2393
+ echo "Usage: $0 --session-last <session-id> [limit]"
2394
+ exit 1
2395
+ fi
2396
+ query_session_last_messages "$2" "${3:-8}"
2397
+ ;;
2398
+ --session-messages|--messages)
2399
+ if [[ -z "${2:-}" ]]; then
2400
+ echo "Usage: $0 --session-messages <session-id>"
2401
+ exit 1
2402
+ fi
2403
+ query_session_last_messages "$2" "${3:-8}"
2404
+ ;;
2405
+ --session-tools)
2406
+ if [[ -z "${2:-}" ]]; then
2407
+ echo "Usage: $0 --session-tools <session-id>"
2408
+ exit 1
2409
+ fi
2410
+ query_session_tools "$2"
2411
+ ;;
2412
+ --recent-messages)
2413
+ query_recent_messages "${2:-20}"
2414
+ ;;
2415
+ --last-session-messages)
2416
+ query_last_session_messages "${2:-8}"
2417
+ ;;
2418
+ --request)
2419
+ if [[ -z "${2:-}" ]]; then
2420
+ echo "Usage: $0 --request <request-id>"
2421
+ exit 1
2422
+ fi
2423
+ query_request_fast "$2"
2424
+ ;;
2425
+ --latest-session)
2426
+ query_latest_session_fast
2427
+ ;;
2428
+ --kpis-daily)
2429
+ query_agent_kpis_daily
2430
+ ;;
2431
+ --training-examples)
2432
+ query_training_examples "${2:-20}"
2433
+ ;;
2434
+ --session-timeline)
2435
+ if [[ -z "${2:-}" ]]; then
2436
+ echo "Usage: $0 --session-timeline <session-id>"
2437
+ exit 1
2438
+ fi
2439
+ query_session_timeline_fast "$2"
2440
+ ;;
2441
+ --compare-versions)
2442
+ if [[ -z "${2:-}" ]]; then
2443
+ echo "Usage: $0 --compare-versions <agent-name>"
2444
+ exit 1
2445
+ fi
2446
+ query_compare_versions_fast "$2"
2447
+ ;;
2448
+ --top-sessions)
2449
+ if [[ -z "${2:-}" ]]; then
2450
+ echo "Usage: $0 --top-sessions <agent-name> [--since <duration>]"
2451
+ exit 1
2452
+ fi
2453
+ local_since="86400000"
2454
+ if [[ "${3:-}" == "--since" && -n "${4:-}" ]]; then
2455
+ local_since=$(parse_since "$4")
2456
+ fi
2457
+ query_top_sessions_fast "$2" "${local_since}"
2458
+ ;;
2459
+ --latency-percentiles|--latency)
2460
+ query_latency_percentiles
2461
+ ;;
2462
+ --errors)
2463
+ query_errors
2464
+ ;;
2465
+ --largest-requests)
2466
+ query_largest_requests "${2:-20}"
2467
+ ;;
2468
+ --problems)
2469
+ query_problems
2470
+ ;;
2471
+ --tool-loops)
2472
+ if [[ -z "${2:-}" ]]; then
2473
+ echo "Usage: $0 --tool-loops <session-id>"
2474
+ exit 1
2475
+ fi
2476
+ query_tool_loops "$2"
2477
+ ;;
2478
+ --tool-stats)
2479
+ query_tool_stats "${2:-}"
2480
+ ;;
2481
+ --sql)
2482
+ if [[ -z "${2:-}" ]]; then
2483
+ echo "Usage: $0 --sql \"SELECT ...\""
2484
+ exit 1
2485
+ fi
2486
+ query_custom "$2"
2487
+ ;;
2488
+ --schema)
2489
+ dump_schema
2490
+ ;;
2491
+ --help|-h)
2492
+ echo "Usage: $0 [flag] [args]"
2493
+ echo ""
2494
+ echo "COST ANALYSIS — \"Where is money going?\""
2495
+ echo " --cost Total cost, tokens, request count"
2496
+ echo " --cost-by-session Cost per session — find expensive conversations"
2497
+ echo " --cost-by-model Cost per model — check tier usage"
2498
+ echo " --cost-by-pack Cost per agent — compare agent efficiency"
2499
+ echo " --cost-by-engine Cost per engine (claude/codex/etc)"
2500
+ echo " --cost-by-source Cost per framework (vscode/cli/api)"
2501
+ echo " --cost-per-request [id] Cost per user message — find bloated requests"
2502
+ echo ""
2503
+ echo "DEBUGGING — \"What happened in this session/request?\""
2504
+ echo " --session <id> All LLM calls in a session"
2505
+ echo " --session-summary <id> Session-level totals and metadata"
2506
+ echo " --session-debug <id> Compact agent-safe bundle: summary, requests, tools, latest messages"
2507
+ echo " --session-last <id> [N] Last N conversational user/assistant previews from latest call snapshot"
2508
+ echo " --session-messages <id> Alias of --session-last"
2509
+ echo " --session-tools <id> Tool calls and outcomes in session"
2510
+ echo " --request <id> All LLM calls for one user message"
2511
+ echo " --latest-session Most recent session summary"
2512
+ echo " --last-session-messages [N] Last N messages for latest session"
2513
+ echo " --recent-messages [N] Most recent sessions by latest call"
2514
+ echo " --session-timeline <id> Interleaved model/tool timeline"
2515
+ echo " --kpis-daily Daily KPI rollup from indexed DB"
2516
+ echo " --training-examples [N] Retired (prints guidance)"
2517
+ echo ""
2518
+ echo "REGRESSION — \"Did performance change?\""
2519
+ echo " --compare-versions <agent> Avg cost/tokens/latency by agent version"
2520
+ echo " --top-sessions <agent> Expensive sessions for an agent (last 24h)"
2521
+ echo ""
2522
+ echo "HEALTH — \"What's broken?\""
2523
+ echo " --problems Statistical outliers (cost/token/latency spikes)"
2524
+ echo " --errors Error rate by model"
2525
+ echo " --latency-percentiles p50/p95 latency by model and agent"
2526
+ echo " --largest-requests [N] Biggest input token payloads (prompt bloat)"
2527
+ echo " --tool-loops <id> Detect repeated tool calls (possible loops)"
2528
+ echo " --tool-stats [id] Tool usage frequency and error rates"
2529
+ echo ""
2530
+ echo "DISCOVERY — \"Agent self-service schema learning\""
2531
+ echo " --schema All tables, columns, types, and relationships (JSON for agents)"
2532
+ echo " (no args) Interactive DuckDB shell"
2533
+ echo ""
2534
+ echo "CUSTOM — \"I need something specific\""
2535
+ echo " --sql \"SELECT ...\" Run custom DuckDB SQL"
2536
+ echo ""
2537
+ echo "The --sql flag can use built-in CTEs for complex queries:"
2538
+ echo " \${BASE_QUERY} — flattens OTEL JSONL into spans"
2539
+ echo " \${EXTRACT_ATTRS} — extracts common attributes (cost, model, session, etc)"
2540
+ echo ""
2541
+ echo "Example: scripts/query-analytics.sh --sql \"\${BASE_QUERY}, attrs AS (\${EXTRACT_ATTRS}) SELECT model, SUM(cost) FROM attrs GROUP BY model\""
2542
+ echo ""
2543
+ echo "Options:"
2544
+ echo " --since <dur> Time filter for --top-sessions (24h, 7d, 30m)"
2545
+ echo ""
2546
+ echo "Message previews are bounded by default and may spill a fuller preview artifact to ${TMP_DIR}."
2547
+ ;;
2548
+ *)
2549
+ interactive_shell
2550
+ ;;
2551
+ esac