@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.
- package/LICENSE +21 -0
- package/bin/agenthippo.js +2 -0
- package/out/agentpack/github.js +547 -0
- package/out/agentpack/index.js +66 -0
- package/out/agentpack/install.js +297 -0
- package/out/agentpack/manager.js +441 -0
- package/out/agentpack/manifest.js +135 -0
- package/out/agentpack/paths.js +475 -0
- package/out/agentpack/publish.js +248 -0
- package/out/agentpack/stores.js +554 -0
- package/out/agentpack/types.js +5 -0
- package/out/analytics/dashboard/artifactManager.js +439 -0
- package/out/analytics/dashboard/fleetDisplayUtils.js +159 -0
- package/out/analytics/dashboard/fleetOrchestrator.js +142 -0
- package/out/analytics/dashboard/fleetRegistry.js +79 -0
- package/out/analytics/dashboard/fleetRowDefaults.js +87 -0
- package/out/analytics/dashboard/fleetScriptAdapter.js +146 -0
- package/out/analytics/dashboard/fleetStatusUtils.js +30 -0
- package/out/analytics/dashboard/fleetTitleUtils.js +68 -0
- package/out/analytics/dashboard/fleetTypes.js +29 -0
- package/out/analytics/dashboard/index.js +17 -0
- package/out/analytics/dashboard/indexingService.js +422 -0
- package/out/analytics/dashboard/localFleetAdapter.js +222 -0
- package/out/analytics/dashboard/performanceMonitor.js +76 -0
- package/out/analytics/dashboard/processManager.js +209 -0
- package/out/analytics/dashboard/provider.js +3885 -0
- package/out/analytics/dashboard/queryRunner.js +994 -0
- package/out/analytics/dashboard/refreshPolicy.js +16 -0
- package/out/analytics/dashboard/scheduledFleetFingerprint.js +22 -0
- package/out/analytics/dashboard/scheduledFleetStorage.js +241 -0
- package/out/analytics/dashboard/scheduledFleetUtils.js +22 -0
- package/out/analytics/dashboard/sessionEventFleetStats.js +118 -0
- package/out/analytics/dashboard/sessionJsonlFleet.js +204 -0
- package/out/analytics/dashboard/teamViewDiscovery.js +106 -0
- package/out/analytics/dashboard/viewsConfigMerge.js +221 -0
- package/out/analytics/dashboard/webview/analytics-panel.html +155 -0
- package/out/analytics/dashboard/webview/codicon.ttf +0 -0
- package/out/analytics/dashboard/webview/index.html +78 -0
- package/out/analytics/dashboard/webview/panel.js +2295 -0
- package/out/analytics/dashboard/webview/runtime-bridge.js +82 -0
- package/out/analytics/dashboard/webview/styles.css +1009 -0
- package/out/analytics/lifecycle.js +43 -0
- package/out/analytics/service.js +1279 -0
- package/out/analytics/sessionEvents.js +231 -0
- package/out/auth/bootstrapApply.js +72 -0
- package/out/auth/brokerClient.js +126 -0
- package/out/auth/callbackUri.js +59 -0
- package/out/auth/callbackUriParse.js +59 -0
- package/out/auth/githubStoreGate.js +70 -0
- package/out/auth/loopbackAuthServer.js +165 -0
- package/out/auth/pkce.js +9 -0
- package/out/auth/provider.js +239 -0
- package/out/auth/sessionStore.js +193 -0
- package/out/auth/signInFlow.js +182 -0
- package/out/auth/types.js +12 -0
- package/out/auth-broker/config.js +62 -0
- package/out/auth-broker/jwt.js +105 -0
- package/out/auth-broker/pkce.js +57 -0
- package/out/auth-broker/server.js +539 -0
- package/out/auth-broker/store.js +273 -0
- package/out/base-prompts/gateway/openclaw-starter.json5 +144 -0
- package/out/base-prompts/gateway/slack-socket-mode-minimal.manifest.json +44 -0
- package/out/base-prompts/mcp-servers/.mcp.json +13 -0
- package/out/base-prompts/mcp-servers/langfuse-analytics/README.md +142 -0
- package/out/base-prompts/mcp-servers/langfuse-analytics/mcp.json +13 -0
- package/out/base-prompts/mcp-servers/langfuse-analytics/package-lock.json +1192 -0
- package/out/base-prompts/mcp-servers/langfuse-analytics/package.json +23 -0
- package/out/base-prompts/mcp-servers/langfuse-analytics/scripts/run-with-env.sh +12 -0
- package/out/base-prompts/mcp-servers/langfuse-analytics/scripts/sdk-test.js +102 -0
- package/out/base-prompts/mcp-servers/langfuse-analytics/scripts/simple-test.js +137 -0
- package/out/base-prompts/mcp-servers/langfuse-analytics/scripts/test-client.js +118 -0
- package/out/base-prompts/mcp-servers/langfuse-analytics/scripts/test-server.js +209 -0
- package/out/base-prompts/mcp-servers/langfuse-analytics/src/client.ts +431 -0
- package/out/base-prompts/mcp-servers/langfuse-analytics/src/index.ts +270 -0
- package/out/base-prompts/mcp-servers/langfuse-analytics/tsconfig.json +18 -0
- package/out/base-prompts/rules/AGENTS.md +204 -0
- package/out/base-prompts/skills/agent-browser/SKILL.md +102 -0
- package/out/base-prompts/skills/agent-builder/SKILL.md +73 -0
- package/out/base-prompts/skills/agent-builder/references/agent-pack.md +63 -0
- package/out/base-prompts/skills/agent-builder/references/crewai.md +66 -0
- package/out/base-prompts/skills/agent-builder/references/langchain.md +60 -0
- package/out/base-prompts/skills/agent-builder/references/raw-sdk.md +73 -0
- package/out/base-prompts/skills/agent-engine-creator/SKILL.md +586 -0
- package/out/base-prompts/skills/agent-engine-installer/SKILL.md +106 -0
- package/out/base-prompts/skills/agent-engine-installer/scripts/install-engine.mjs +259 -0
- package/out/base-prompts/skills/agent-engine-installer/scripts/install-engine.ps1 +189 -0
- package/out/base-prompts/skills/agenthippo-cli/SKILL.md +69 -0
- package/out/base-prompts/skills/agenthippo-cli/scripts/install.sh +63 -0
- package/out/base-prompts/skills/agentpack-creator/SKILL.md +375 -0
- package/out/base-prompts/skills/agentpack-creator/references/agent-templates.md +261 -0
- package/out/base-prompts/skills/agentpack-creator/references/agents-md-guide.md +195 -0
- package/out/base-prompts/skills/agentpack-creator/scripts/init_pack.py +391 -0
- package/out/base-prompts/skills/agentpack-creator/scripts/init_pack_test.py +294 -0
- package/out/base-prompts/skills/agentpack-testbench/SKILL.md +135 -0
- package/out/base-prompts/skills/algorithmic-art/LICENSE.txt +202 -0
- package/out/base-prompts/skills/algorithmic-art/SKILL.md +405 -0
- package/out/base-prompts/skills/algorithmic-art/templates/generator_template.js +223 -0
- package/out/base-prompts/skills/algorithmic-art/templates/viewer.html +599 -0
- package/out/base-prompts/skills/debug-agent/SKILL.md +83 -0
- package/out/base-prompts/skills/desktop-automation/SKILL.md +166 -0
- package/out/base-prompts/skills/desktop-automation/references/agenthippo-ui-recipes.md +441 -0
- package/out/base-prompts/skills/desktop-automation/scripts/windows-desktop-uia.psm1 +280 -0
- package/out/base-prompts/skills/docx/LICENSE.txt +30 -0
- package/out/base-prompts/skills/docx/SKILL.md +197 -0
- package/out/base-prompts/skills/docx/docx-js.md +350 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/mce/mc.xsd +75 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/microsoft/wml-2010.xsd +560 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/microsoft/wml-2012.xsd +67 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/microsoft/wml-2018.xsd +14 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/microsoft/wml-cex-2018.xsd +20 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/microsoft/wml-cid-2016.xsd +13 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/microsoft/wml-symex-2015.xsd +8 -0
- package/out/base-prompts/skills/docx/ooxml/scripts/pack.py +159 -0
- package/out/base-prompts/skills/docx/ooxml/scripts/unpack.py +29 -0
- package/out/base-prompts/skills/docx/ooxml/scripts/validate.py +69 -0
- package/out/base-prompts/skills/docx/ooxml/scripts/validation/__init__.py +15 -0
- package/out/base-prompts/skills/docx/ooxml/scripts/validation/base.py +951 -0
- package/out/base-prompts/skills/docx/ooxml/scripts/validation/docx.py +274 -0
- package/out/base-prompts/skills/docx/ooxml/scripts/validation/pptx.py +315 -0
- package/out/base-prompts/skills/docx/ooxml/scripts/validation/redlining.py +279 -0
- package/out/base-prompts/skills/docx/ooxml.md +610 -0
- package/out/base-prompts/skills/docx/scripts/__init__.py +1 -0
- package/out/base-prompts/skills/docx/scripts/document.py +1276 -0
- package/out/base-prompts/skills/docx/scripts/templates/comments.xml +3 -0
- package/out/base-prompts/skills/docx/scripts/templates/commentsExtended.xml +3 -0
- package/out/base-prompts/skills/docx/scripts/templates/commentsExtensible.xml +3 -0
- package/out/base-prompts/skills/docx/scripts/templates/commentsIds.xml +3 -0
- package/out/base-prompts/skills/docx/scripts/templates/people.xml +3 -0
- package/out/base-prompts/skills/docx/scripts/utilities.py +374 -0
- package/out/base-prompts/skills/gateway-outbound/SKILL.md +136 -0
- package/out/base-prompts/skills/mcp-builder/LICENSE.txt +202 -0
- package/out/base-prompts/skills/mcp-builder/SKILL.md +328 -0
- package/out/base-prompts/skills/mcp-builder/reference/evaluation.md +602 -0
- package/out/base-prompts/skills/mcp-builder/reference/mcp_best_practices.md +249 -0
- package/out/base-prompts/skills/mcp-builder/reference/node_mcp_server.md +970 -0
- package/out/base-prompts/skills/mcp-builder/reference/python_mcp_server.md +719 -0
- package/out/base-prompts/skills/mcp-builder/scripts/connections.py +151 -0
- package/out/base-prompts/skills/mcp-builder/scripts/evaluation.py +373 -0
- package/out/base-prompts/skills/mcp-builder/scripts/example_evaluation.xml +22 -0
- package/out/base-prompts/skills/mcp-builder/scripts/requirements.txt +2 -0
- package/out/base-prompts/skills/openclaw/SKILL.md +406 -0
- package/out/base-prompts/skills/openclaw/scripts/run-smoke.sh +61 -0
- package/out/base-prompts/skills/pdf/LICENSE.txt +30 -0
- package/out/base-prompts/skills/pdf/SKILL.md +294 -0
- package/out/base-prompts/skills/pdf/forms.md +205 -0
- package/out/base-prompts/skills/pdf/reference.md +612 -0
- package/out/base-prompts/skills/pdf/scripts/check_bounding_boxes.py +70 -0
- package/out/base-prompts/skills/pdf/scripts/check_bounding_boxes_test.py +226 -0
- package/out/base-prompts/skills/pdf/scripts/check_fillable_fields.py +12 -0
- package/out/base-prompts/skills/pdf/scripts/convert_pdf_to_images.py +35 -0
- package/out/base-prompts/skills/pdf/scripts/create_validation_image.py +41 -0
- package/out/base-prompts/skills/pdf/scripts/extract_form_field_info.py +152 -0
- package/out/base-prompts/skills/pdf/scripts/fill_fillable_fields.py +114 -0
- package/out/base-prompts/skills/pdf/scripts/fill_pdf_form_with_annotations.py +108 -0
- package/out/base-prompts/skills/pptx/LICENSE.txt +30 -0
- package/out/base-prompts/skills/pptx/SKILL.md +484 -0
- package/out/base-prompts/skills/pptx/html2pptx.md +625 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/mce/mc.xsd +75 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/microsoft/wml-2010.xsd +560 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/microsoft/wml-2012.xsd +67 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/microsoft/wml-2018.xsd +14 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/microsoft/wml-cex-2018.xsd +20 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/microsoft/wml-cid-2016.xsd +13 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/microsoft/wml-symex-2015.xsd +8 -0
- package/out/base-prompts/skills/pptx/ooxml/scripts/pack.py +159 -0
- package/out/base-prompts/skills/pptx/ooxml/scripts/unpack.py +29 -0
- package/out/base-prompts/skills/pptx/ooxml/scripts/validate.py +69 -0
- package/out/base-prompts/skills/pptx/ooxml/scripts/validation/__init__.py +15 -0
- package/out/base-prompts/skills/pptx/ooxml/scripts/validation/base.py +951 -0
- package/out/base-prompts/skills/pptx/ooxml/scripts/validation/docx.py +274 -0
- package/out/base-prompts/skills/pptx/ooxml/scripts/validation/pptx.py +315 -0
- package/out/base-prompts/skills/pptx/ooxml/scripts/validation/redlining.py +279 -0
- package/out/base-prompts/skills/pptx/ooxml.md +427 -0
- package/out/base-prompts/skills/pptx/scripts/html2pptx.js +984 -0
- package/out/base-prompts/skills/pptx/scripts/inventory.py +1020 -0
- package/out/base-prompts/skills/pptx/scripts/rearrange.py +231 -0
- package/out/base-prompts/skills/pptx/scripts/replace.py +385 -0
- package/out/base-prompts/skills/pptx/scripts/thumbnail.py +450 -0
- package/out/base-prompts/skills/prompt-lab/SKILL.md +149 -0
- package/out/base-prompts/skills/prompt-lab/scripts/query-analytics.sh +2551 -0
- package/out/base-prompts/skills/skill-creator/LICENSE.txt +202 -0
- package/out/base-prompts/skills/skill-creator/SKILL.md +371 -0
- package/out/base-prompts/skills/skill-creator/references/output-patterns.md +82 -0
- package/out/base-prompts/skills/skill-creator/references/workflows.md +28 -0
- package/out/base-prompts/skills/skill-creator/scripts/init_skill.py +303 -0
- package/out/base-prompts/skills/skill-creator/scripts/package_skill.py +110 -0
- package/out/base-prompts/skills/skill-creator/scripts/quick_validate.py +95 -0
- package/out/base-prompts/skills/skill-installer/SKILL.md +251 -0
- package/out/base-prompts/skills/skill-installer/references/store-api.md +219 -0
- package/out/base-prompts/skills/spotlight-dashboard/SKILL.md +83 -0
- package/out/base-prompts/skills/spotlight-insights/SKILL.md +101 -0
- package/out/base-prompts/skills/spotlight-optimize/SKILL.md +93 -0
- package/out/base-prompts/skills/spotlight-optimize/scripts/query-analytics.sh +2551 -0
- package/out/base-prompts/skills/spotlight-query/SKILL.md +314 -0
- package/out/base-prompts/skills/spotlight-query/references/otel-schema.md +163 -0
- package/out/base-prompts/skills/spotlight-query/scripts/query-analytics.sh +2551 -0
- package/out/base-prompts/skills/spotlight-query/scripts/query-analytics.test.sh +744 -0
- package/out/base-prompts/skills/spotlight-query/scripts/test-fixtures/analytics/sessions/sess-test-1.jsonl +14 -0
- package/out/base-prompts/skills/spotlight-query/scripts/test-fixtures/analytics/traces/.duckdb-index.json +7 -0
- package/out/base-prompts/skills/spotlight-query/scripts/test-fixtures/analytics/traces/sample.jsonl +5 -0
- package/out/base-prompts/skills/spotlight-report/SKILL.md +86 -0
- package/out/base-prompts/skills/spotlight-visualize/SKILL.md +92 -0
- package/out/base-prompts/skills/spotlight-visualize/scripts/query-analytics.sh +2551 -0
- package/out/base-prompts/skills/xlsx/LICENSE.txt +30 -0
- package/out/base-prompts/skills/xlsx/SKILL.md +289 -0
- package/out/base-prompts/skills/xlsx/recalc.py +178 -0
- package/out/branding.js +23 -0
- package/out/cli/commands/anywhere.js +66 -0
- package/out/cli/commands/ask.js +243 -0
- package/out/cli/commands/auth-broker.js +24 -0
- package/out/cli/commands/auth.js +219 -0
- package/out/cli/commands/chat.js +354 -0
- package/out/cli/commands/deploy.js +223 -0
- package/out/cli/commands/gateway.js +160 -0
- package/out/cli/commands/pack.js +220 -0
- package/out/cli/commands/probe-custom-engine.js +82 -0
- package/out/cli/commands/serve.js +583 -0
- package/out/cli/commands/store.js +919 -0
- package/out/cli/credential-context.js +113 -0
- package/out/cli/deploy-init.js +2087 -0
- package/out/cli/deps.js +238 -0
- package/out/cli/index.js +390 -0
- package/out/cli/pack-loader.js +376 -0
- package/out/cli/run-custom-engine-runner.js +318 -0
- package/out/cli/runtime.js +409 -0
- package/out/cli/serve-hardening.js +325 -0
- package/out/cli/state-sync.js +135 -0
- package/out/cli/utils.js +220 -0
- package/out/cli/vscode-shim.js +86 -0
- package/out/engines/base.js +159 -0
- package/out/engines/claude.js +1215 -0
- package/out/engines/codex.js +759 -0
- package/out/engines/custom.js +632 -0
- package/out/engines/customEngineIpc.js +95 -0
- package/out/engines/customEngineRunnerEmbed.js +107 -0
- package/out/engines/customRegistry.js +274 -0
- package/out/engines/embeddedCustomEngineRunner.generated.js +9 -0
- package/out/engines/engineApiKey.js +138 -0
- package/out/engines/factory.js +45 -0
- package/out/engines/install.js +372 -0
- package/out/engines/persistentEngineWorker.js +320 -0
- package/out/engines/publish.js +341 -0
- package/out/engines/types.js +17 -0
- package/out/examples/run-headless-repl.js +291 -0
- package/out/examples/run-headless.js +187 -0
- package/out/extension.js +4472 -0
- package/out/gateway/bridge.js +805 -0
- package/out/gateway/cli-agent-id.js +36 -0
- package/out/gateway/client.js +303 -0
- package/out/gateway/client.test.js +395 -0
- package/out/gateway/gateway-scope-errors.js +14 -0
- package/out/gateway/gatewayBackup.js +76 -0
- package/out/gateway/gatewayRouteIndex.js +371 -0
- package/out/gateway/gatewaySessionStore.js +243 -0
- package/out/gateway/index.js +31 -0
- package/out/gateway/lifecycle.js +3056 -0
- package/out/gateway/openclaw-channel-routing.js +317 -0
- package/out/gateway/outboundHandles.js +373 -0
- package/out/gateway/outboundPaths.js +72 -0
- package/out/gateway/outboundSend.js +254 -0
- package/out/gateway/protocol.js +48 -0
- package/out/gateway/resetMappings.js +52 -0
- package/out/gateway/service.js +1244 -0
- package/out/gateway/sessionRouting.js +77 -0
- package/out/headless/index.js +505 -0
- package/out/headless/sync.js +473 -0
- package/out/mcp/publish.js +143 -0
- package/out/mcp.js +597 -0
- package/out/permissions.js +108 -0
- package/out/permissions.mapping.yaml +68 -0
- package/out/registry/index.js +332 -0
- package/out/remote/handleRemoteChat.js +122 -0
- package/out/remote/remotePackStatusBar.js +103 -0
- package/out/remote/serveClient.js +216 -0
- package/out/remoteSkills.js +191 -0
- package/out/runtime/index.js +32 -0
- package/out/runtime/interfaces/config-provider.js +32 -0
- package/out/runtime/interfaces/edit-tracker.js +22 -0
- package/out/runtime/interfaces/index.js +29 -0
- package/out/runtime/interfaces/logger.js +40 -0
- package/out/runtime/interfaces/permission-handler.js +66 -0
- package/out/runtime/interfaces/session-store.js +342 -0
- package/out/runtime/vscode/index.js +20 -0
- package/out/runtime/vscode/vscode-config-provider.js +77 -0
- package/out/runtime/vscode/vscode-edit-tracker.js +120 -0
- package/out/runtime/vscode/vscode-logger.js +30 -0
- package/out/runtime/vscode/vscode-permission-handler.js +72 -0
- package/out/runtime/vscode/vscode-session-store.js +22 -0
- package/out/scripts/agentide-litellm.ps1 +501 -0
- package/out/scripts/agentide-litellm.sh +700 -0
- package/out/scripts/agentide_litellm_hooks_v1.py +604 -0
- package/out/scripts/analytics/agent-fleet-widget.js +954 -0
- package/out/scripts/analytics/agentide-litellm-otel.config.yaml +160 -0
- package/out/scripts/analytics/default-seed-scripts.json +12 -0
- package/out/scripts/analytics/duckdbLocator.js +29 -0
- package/out/scripts/analytics/fleet-local-adapter.js +246 -0
- package/out/scripts/analytics/fleet-workflow.sh +263 -0
- package/out/scripts/analytics/generate-default-charts.py +685 -0
- package/out/scripts/analytics/hotspot-analysis-widget.js +909 -0
- package/out/scripts/analytics/index-traces.py +1394 -0
- package/out/scripts/analytics/install-analytics.sh +286 -0
- package/out/scripts/analytics/live-fleet-4-collector.cjs +315 -0
- package/out/scripts/analytics/live-fleet-4-daemon.mjs +134 -0
- package/out/scripts/analytics/live-fleet-4-widget.cjs +433 -0
- package/out/scripts/analytics/otel-collector.yaml +55 -0
- package/out/scripts/analytics/query-analytics.ps1 +396 -0
- package/out/scripts/analytics/query-analytics.sh +2551 -0
- package/out/scripts/analytics/session-comparison-widget.js +1468 -0
- package/out/scripts/analytics/session-timeline-widget.js +2137 -0
- package/out/scripts/analytics/setup-analytics.ps1 +432 -0
- package/out/scripts/analytics/setup-analytics.sh +854 -0
- package/out/scripts/analytics/start-analytics.ps1 +169 -0
- package/out/scripts/analytics/start-analytics.sh +323 -0
- package/out/scripts/analytics/token-cost-flamegraph-widget.js +954 -0
- package/out/scripts/analytics/token-cost-trend-widget.js +1062 -0
- package/out/scripts/analytics/workflows-collector.cjs +315 -0
- package/out/scripts/analytics/workflows-daemon.mjs +186 -0
- package/out/scripts/analytics/workflows-widget.cjs +433 -0
- package/out/scripts/analytics/wow-model-widget.js +191 -0
- package/out/scripts/gateway/Dockerfile.serve +45 -0
- package/out/scripts/gateway/docker-serve.sh +248 -0
- package/out/scripts/gateway/setup-gateway-server.ps1 +953 -0
- package/out/scripts/gateway/setup-gateway-server.sh +935 -0
- package/out/scripts/gateway/setup-gateway.ps1 +1813 -0
- package/out/scripts/gateway/setup-gateway.sh +1541 -0
- package/out/scripts/setup-cli.ps1 +86 -0
- package/out/scripts/setup-cli.sh +117 -0
- package/out/scripts/tools-manifest.json +83 -0
- package/out/scripts/vendor-tools.mjs +234 -0
- package/out/session.js +272 -0
- package/out/shared/agentEngines.js +170 -0
- package/out/shared/index.js +109 -0
- package/out/shared/models.js +235 -0
- package/out/shared/providers.js +202 -0
- package/out/skills/publish.js +178 -0
- package/out/spotlight/install.js +339 -0
- package/out/spotlight/publish.js +544 -0
- package/out/spotlight/yaml.js +31 -0
- package/out/store/github/setup.js +235 -0
- package/out/store/github/templates/build-index.ts +339 -0
- package/out/store/github/templates/build-index.yml +97 -0
- package/out/store/github/templates/package.json +11 -0
- package/out/store/index.js +755 -0
- package/out/store/oramaIndex.js +884 -0
- package/out/store/shared.js +241 -0
- package/out/sync.js +1827 -0
- package/out/terminalLogger.js +358 -0
- package/out/test/agentEnvFile.test.js +97 -0
- package/out/test/agentYaml.test.js +373 -0
- package/out/test/agentpack.test.js +639 -0
- package/out/test/analytics-lifecycle.test.js +174 -0
- package/out/test/analytics.test.js +524 -0
- package/out/test/auth-bootstrapApply.test.js +102 -0
- package/out/test/auth-broker.test.js +434 -0
- package/out/test/auth-callback.test.js +62 -0
- package/out/test/branding.test.js +229 -0
- package/out/test/cli-agent-id.test.js +55 -0
- package/out/test/credential-context.test.js +105 -0
- package/out/test/customEngineInstall.test.js +100 -0
- package/out/test/customRegistry.test.js +122 -0
- package/out/test/deploy-init.test.js +532 -0
- package/out/test/deps-auto-install.test.js +81 -0
- package/out/test/engine-integration.test.js +297 -0
- package/out/test/enginePaths.test.js +195 -0
- package/out/test/enginePublish.test.js +82 -0
- package/out/test/fleetDisplayUtils.test.js +96 -0
- package/out/test/gateway-client.test.js +381 -0
- package/out/test/gateway-lifecycle.test.js +608 -0
- package/out/test/gateway-outbound.test.js +531 -0
- package/out/test/gateway-reset-mappings.test.js +221 -0
- package/out/test/gateway-route-index.test.js +235 -0
- package/out/test/gateway-session-routing.test.js +47 -0
- package/out/test/gateway-session-store.test.js +216 -0
- package/out/test/github.test.js +232 -0
- package/out/test/headless-sync.test.js +279 -0
- package/out/test/helpers/manifest-fixtures.js +27 -0
- package/out/test/mcp-obo.test.js +82 -0
- package/out/test/mcp.test.js +307 -0
- package/out/test/openclaw-channel-routing.test.js +191 -0
- package/out/test/pathSafety.test.js +63 -0
- package/out/test/permissions.test.js +175 -0
- package/out/test/persistent-engine-worker.test.js +220 -0
- package/out/test/refreshPolicy.test.js +60 -0
- package/out/test/registry.test.js +351 -0
- package/out/test/runtime.test.js +349 -0
- package/out/test/scheduledFleetStorage.test.js +72 -0
- package/out/test/serve-hardening.test.js +192 -0
- package/out/test/serveClient.test.js +173 -0
- package/out/test/sessionJsonlFleet.test.js +78 -0
- package/out/test/shared.test.js +173 -0
- package/out/test/skills-mcp.test.js +299 -0
- package/out/test/spotlightPublish.test.js +299 -0
- package/out/test/spotlightYaml.test.js +58 -0
- package/out/test/state-sync-peer.test.js +74 -0
- package/out/test/store.test.js +331 -0
- package/out/test/stores.test.js +504 -0
- package/out/test/sync-switch.test.js +251 -0
- package/out/test/teamViewDiscovery.test.js +90 -0
- package/out/test/toolPath.test.js +120 -0
- package/out/test/utils.test.js +308 -0
- package/out/test/viewsConfigMerge.test.js +180 -0
- package/out/test/windows-command-line.test.js +75 -0
- package/out/utils/agentEnvFile.js +131 -0
- package/out/utils/binary.js +175 -0
- package/out/utils/index.js +37 -0
- package/out/utils/network.js +74 -0
- package/out/utils/pathSafety.js +60 -0
- package/out/utils/paths.js +116 -0
- package/out/utils/pid.js +180 -0
- package/out/utils/pythonRunner.js +258 -0
- package/out/utils/resolveNodeBinary.js +168 -0
- package/out/utils/toolPath.js +83 -0
- package/out/utils/windowsCommandLine.js +29 -0
- package/out/utils.js +915 -0
- package/out/workspaceFolders.js +59 -0
- 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
|