@obsunified/collector 2.0.1
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/README.md +60 -0
- package/dist/auth.d.ts +2 -0
- package/dist/auth.js +9 -0
- package/dist/auth.js.map +1 -0
- package/dist/chunk-UGWDOGGH.js +319 -0
- package/dist/chunk-UGWDOGGH.js.map +1 -0
- package/dist/chunk-XKQTVIRE.js +671 -0
- package/dist/chunk-XKQTVIRE.js.map +1 -0
- package/dist/index.d.ts +1197 -0
- package/dist/index.js +17707 -0
- package/dist/index.js.map +1 -0
- package/dist/ingest-auth-HFVO1zIp.d.ts +165 -0
- package/dist/openai-FJYOQ7WO.js +384 -0
- package/dist/openai-FJYOQ7WO.js.map +1 -0
- package/package.json +73 -0
- package/src/ai-parity/ai-parity.acceptance.test.ts +449 -0
- package/src/analyses/derive.ts +642 -0
- package/src/analyses/index.ts +50 -0
- package/src/analyses/investigations.ts +277 -0
- package/src/analyses/tier0.ts +568 -0
- package/src/auth/dashboard-auth.ts +269 -0
- package/src/auth/ingest-auth.ts +147 -0
- package/src/durable-objects/tail-hub.test.ts +32 -0
- package/src/durable-objects/tail-hub.ts +158 -0
- package/src/framework/collector.ts +467 -0
- package/src/framework/env.ts +90 -0
- package/src/framework/logger.ts +81 -0
- package/src/index.ts +211 -0
- package/src/lib/action-aggregates.ts +1031 -0
- package/src/lib/action-identity.ts +106 -0
- package/src/lib/actions-store.ts +397 -0
- package/src/lib/ai-pricing.ts +80 -0
- package/src/lib/ai-store/calls.ts +148 -0
- package/src/lib/ai-store/evaluations.test.ts +47 -0
- package/src/lib/ai-store/evaluations.ts +125 -0
- package/src/lib/ai-store/helpers.ts +37 -0
- package/src/lib/ai-store/retention.ts +17 -0
- package/src/lib/ai-store/sessions.ts +277 -0
- package/src/lib/ai-store/spans.ts +109 -0
- package/src/lib/ai-store/types.ts +112 -0
- package/src/lib/ai-store.ts +76 -0
- package/src/lib/alerts-store.ts +458 -0
- package/src/lib/analyses-runner.ts +439 -0
- package/src/lib/analyses-store.test.ts +243 -0
- package/src/lib/analyses-store.ts +454 -0
- package/src/lib/ask.test.ts +302 -0
- package/src/lib/ask.ts +390 -0
- package/src/lib/blob-store-s3.ts +213 -0
- package/src/lib/blob-store.ts +366 -0
- package/src/lib/eval-cases-store.test.ts +171 -0
- package/src/lib/eval-cases-store.ts +1033 -0
- package/src/lib/evidence-references.test.ts +10 -0
- package/src/lib/evidence-references.ts +684 -0
- package/src/lib/hash.ts +26 -0
- package/src/lib/identity-index/action-lookups.ts +805 -0
- package/src/lib/identity-index/constants.ts +1 -0
- package/src/lib/identity-index/key-lookups.ts +199 -0
- package/src/lib/identity-index/mappers.ts +349 -0
- package/src/lib/identity-index/types.ts +196 -0
- package/src/lib/identity-index/user-lookup.ts +137 -0
- package/src/lib/identity-index.test.ts +624 -0
- package/src/lib/identity-index.ts +85 -0
- package/src/lib/json.ts +39 -0
- package/src/lib/llm.ts +263 -0
- package/src/lib/logs-store.ts +188 -0
- package/src/lib/metrics-store.test.ts +189 -0
- package/src/lib/metrics-store.ts +312 -0
- package/src/lib/narrate-gate.test.ts +260 -0
- package/src/lib/narrate-gate.ts +253 -0
- package/src/lib/openai.test.ts +300 -0
- package/src/lib/openai.ts +512 -0
- package/src/lib/otlp.ts +212 -0
- package/src/lib/parse-pprof.test.ts +64 -0
- package/src/lib/parse-pprof.ts +184 -0
- package/src/lib/payload-capture.ts +22 -0
- package/src/lib/projects-store.ts +207 -0
- package/src/lib/propagation-metric.test.ts +196 -0
- package/src/lib/propagation-metric.ts +175 -0
- package/src/lib/sql-db-dialect.test.ts +22 -0
- package/src/lib/sql-db-postgres.ts +229 -0
- package/src/lib/sql-db.ts +166 -0
- package/src/lib/stage6.test.ts +216 -0
- package/src/lib/store/export.ts +65 -0
- package/src/lib/store/helpers.ts +367 -0
- package/src/lib/store/ingest.ts +68 -0
- package/src/lib/store/issues.ts +158 -0
- package/src/lib/store/overview.ts +127 -0
- package/src/lib/store/retention.ts +10 -0
- package/src/lib/store/service-map.ts +384 -0
- package/src/lib/store/trace-detail.bench.ts +76 -0
- package/src/lib/store/trace-detail.test.ts +410 -0
- package/src/lib/store/trace-detail.ts +427 -0
- package/src/lib/store/trace-queries.test.ts +29 -0
- package/src/lib/store/trace-queries.ts +89 -0
- package/src/lib/store-overview.test.ts +77 -0
- package/src/lib/store.ts +96 -0
- package/src/lib/tail-publisher.ts +64 -0
- package/src/lib/test-utils/mem-sql-db.test.ts +93 -0
- package/src/lib/test-utils/mem-sql-db.ts +86 -0
- package/src/lib/ua-parser.ts +61 -0
- package/src/lib/usage-store.ts +567 -0
- package/src/migrations/001_telemetry_spans.sql +28 -0
- package/src/migrations/002_usage_events.sql +24 -0
- package/src/migrations/003_usage_analytics_columns.sql +13 -0
- package/src/migrations/004_logs.sql +19 -0
- package/src/migrations/005_ai_calls.sql +25 -0
- package/src/migrations/006_user_profiles.sql +11 -0
- package/src/migrations/007_session_replay_metadata.sql +10 -0
- package/src/migrations/008_session_replay_storage_bytes.sql +1 -0
- package/src/migrations/009_projects_and_keys.sql +22 -0
- package/src/migrations/010_signal_project_id.sql +15 -0
- package/src/migrations/011_default_project_seed.sql +3 -0
- package/src/migrations/012_alert_rules.sql +17 -0
- package/src/migrations/013_alert_evaluations.sql +13 -0
- package/src/migrations/014_alert_state.sql +9 -0
- package/src/migrations/015_metrics.sql +58 -0
- package/src/migrations/016_metrics_exp_histogram_summary.sql +5 -0
- package/src/migrations/017_span_dropped_counts.sql +11 -0
- package/src/migrations/018_log_flags.sql +8 -0
- package/src/migrations/019_ai_span_payloads.sql +30 -0
- package/src/migrations/020_ai_span_evaluations.sql +33 -0
- package/src/migrations/021_ai_span_session.sql +13 -0
- package/src/migrations/022_span_log_session_id.sql +13 -0
- package/src/migrations/023_service_map_indexes.sql +32 -0
- package/src/migrations/024_analysis_results.sql +49 -0
- package/src/migrations/025_stage6_autopinning_alerts.sql +38 -0
- package/src/migrations/026_obs_dashboard_project_seed.sql +18 -0
- package/src/migrations/027_identity_propagation.sql +50 -0
- package/src/migrations/028_profile_blobs.sql +57 -0
- package/src/migrations/029_telemetry_sdk_name.sql +20 -0
- package/src/migrations/030_analysis_claim.sql +18 -0
- package/src/migrations/031_agent_action_graph.sql +154 -0
- package/src/migrations/032_payload_capture_flags.sql +8 -0
- package/src/migrations/033_eval_cases.sql +52 -0
- package/src/migrations/034_eval_case_results.sql +23 -0
- package/src/migrations/035_metric_exemplars.sql +33 -0
- package/src/migrations/036_trace_instrumentation_gaps.sql +19 -0
- package/src/migrations/037_eval_runs.sql +37 -0
- package/src/migrations/038_tool_call_evidence.sql +11 -0
- package/src/migrations/039_drop_trace_instrumentation_gaps.sql +11 -0
- package/src/migrations/040_evidence_retrieval_refs.sql +50 -0
- package/src/migrations-postgres/001_telemetry_spans.sql +33 -0
- package/src/migrations-postgres/002_usage_events.sql +24 -0
- package/src/migrations-postgres/003_usage_analytics_columns.sql +13 -0
- package/src/migrations-postgres/004_logs.sql +19 -0
- package/src/migrations-postgres/005_ai_calls.sql +25 -0
- package/src/migrations-postgres/006_user_profiles.sql +11 -0
- package/src/migrations-postgres/007_session_replay_metadata.sql +10 -0
- package/src/migrations-postgres/008_session_replay_storage_bytes.sql +2 -0
- package/src/migrations-postgres/009_projects_and_keys.sql +25 -0
- package/src/migrations-postgres/010_signal_project_id.sql +18 -0
- package/src/migrations-postgres/011_default_project_seed.sql +5 -0
- package/src/migrations-postgres/012_alert_rules.sql +17 -0
- package/src/migrations-postgres/013_alert_evaluations.sql +13 -0
- package/src/migrations-postgres/014_alert_state.sql +9 -0
- package/src/migrations-postgres/015_metrics.sql +47 -0
- package/src/migrations-postgres/016_metrics_exp_histogram_summary.sql +1 -0
- package/src/migrations-postgres/017_span_dropped_counts.sql +4 -0
- package/src/migrations-postgres/018_log_flags.sql +2 -0
- package/src/migrations-postgres/019_ai_span_payloads.sql +18 -0
- package/src/migrations-postgres/020_ai_span_evaluations.sql +21 -0
- package/src/migrations-postgres/021_ai_span_session.sql +7 -0
- package/src/migrations-postgres/022_span_log_session_id.sql +7 -0
- package/src/migrations-postgres/023_service_map_indexes.sql +9 -0
- package/src/migrations-postgres/024_analysis_results.sql +41 -0
- package/src/migrations-postgres/025_stage6_autopinning_alerts.sql +17 -0
- package/src/migrations-postgres/026_obs_dashboard_project_seed.sql +14 -0
- package/src/migrations-postgres/027_identity_propagation.sql +27 -0
- package/src/migrations-postgres/028_profile_blobs.sql +48 -0
- package/src/migrations-postgres/029_telemetry_sdk_name.sql +5 -0
- package/src/migrations-postgres/030_analysis_claim.sql +5 -0
- package/src/migrations-postgres/031_agent_action_graph.sql +190 -0
- package/src/migrations-postgres/032_payload_capture_flags.sql +8 -0
- package/src/migrations-postgres/033_eval_cases.sql +55 -0
- package/src/migrations-postgres/034_eval_case_results.sql +26 -0
- package/src/migrations-postgres/035_metric_exemplars.sql +31 -0
- package/src/migrations-postgres/036_trace_instrumentation_gaps.sql +19 -0
- package/src/migrations-postgres/037_eval_runs.sql +40 -0
- package/src/migrations-postgres/038_tool_call_evidence.sql +11 -0
- package/src/migrations-postgres/039_drop_trace_instrumentation_gaps.sql +11 -0
- package/src/migrations-postgres/040_evidence_retrieval_refs.sql +47 -0
- package/src/migrations-postgres/README.md +40 -0
- package/src/otlp/acceptance.test.ts +373 -0
- package/src/otlp/decode/body.ts +127 -0
- package/src/otlp/decode/logs.ts +96 -0
- package/src/otlp/decode/metrics.ts +402 -0
- package/src/otlp/decode/traces.ts +92 -0
- package/src/otlp/decode/values.ts +150 -0
- package/src/otlp/decode.test.ts +800 -0
- package/src/otlp/decode.ts +11 -0
- package/src/otlp/gen/opentelemetry/proto/collector/logs/v1/logs_service_pb.ts +132 -0
- package/src/otlp/gen/opentelemetry/proto/collector/metrics/v1/metrics_service_pb.ts +158 -0
- package/src/otlp/gen/opentelemetry/proto/collector/trace/v1/trace_service_pb.ts +158 -0
- package/src/otlp/gen/opentelemetry/proto/common/v1/common_pb.ts +289 -0
- package/src/otlp/gen/opentelemetry/proto/logs/v1/logs_pb.ts +397 -0
- package/src/otlp/gen/opentelemetry/proto/metrics/v1/metrics_pb.ts +1329 -0
- package/src/otlp/gen/opentelemetry/proto/resource/v1/resource_pb.ts +86 -0
- package/src/otlp/gen/opentelemetry/proto/trace/v1/trace_pb.ts +710 -0
- package/src/otlp/response.test.ts +158 -0
- package/src/otlp/response.ts +129 -0
- package/src/plugins/_context.ts +15 -0
- package/src/plugins/action-graph-processor/enrichers.ts +45 -0
- package/src/plugins/action-graph-processor/redaction.ts +84 -0
- package/src/plugins/action-graph-processor.test.ts +1023 -0
- package/src/plugins/action-graph-processor.ts +1172 -0
- package/src/plugins/action-routes.test.ts +716 -0
- package/src/plugins/action-routes.ts +343 -0
- package/src/plugins/ai-receiver.ts +254 -0
- package/src/plugins/ai-span-payloads-processor.test.ts +90 -0
- package/src/plugins/ai-span-payloads-processor.ts +180 -0
- package/src/plugins/alerts-evaluator.test.ts +91 -0
- package/src/plugins/alerts-evaluator.ts +328 -0
- package/src/plugins/alerts-routes.ts +101 -0
- package/src/plugins/analyses-routes.ts +138 -0
- package/src/plugins/ask-routes.ts +182 -0
- package/src/plugins/bot-filter.ts +19 -0
- package/src/plugins/connected-routes/manifest.ts +531 -0
- package/src/plugins/connected-routes.test.ts +1791 -0
- package/src/plugins/connected-routes.ts +1103 -0
- package/src/plugins/dashboard-routes.ts +41 -0
- package/src/plugins/default-span-enrichment.ts +92 -0
- package/src/plugins/eval-cases-routes.test.ts +391 -0
- package/src/plugins/eval-cases-routes.ts +529 -0
- package/src/plugins/evidence-retrieval-routes.benchmark.test.ts +245 -0
- package/src/plugins/evidence-retrieval-routes.test.ts +932 -0
- package/src/plugins/evidence-retrieval-routes.ts +2179 -0
- package/src/plugins/gen-ai-normalizer.test.ts +680 -0
- package/src/plugins/gen-ai-normalizer.ts +302 -0
- package/src/plugins/identity-receiver.ts +96 -0
- package/src/plugins/issue-enrichment.ts +132 -0
- package/src/plugins/issue-insights.ts +81 -0
- package/src/plugins/logs-receiver.ts +160 -0
- package/src/plugins/metrics-receiver.ts +91 -0
- package/src/plugins/onboarding-routes.ts +54 -0
- package/src/plugins/otlp-receiver.ts +124 -0
- package/src/plugins/platform-routes.ts +165 -0
- package/src/plugins/profile-routes.ts +468 -0
- package/src/plugins/projects-routes.ts +92 -0
- package/src/plugins/query-routes.ts +220 -0
- package/src/plugins/redaction-processor.ts +132 -0
- package/src/plugins/replay-query-routes.test.ts +38 -0
- package/src/plugins/replay-query-routes.ts +199 -0
- package/src/plugins/replay-receiver.ts +81 -0
- package/src/plugins/tail-routes.test.ts +47 -0
- package/src/plugins/tail-routes.ts +42 -0
- package/src/plugins/timeline-routes.ts +244 -0
- package/src/plugins/ua-enrichment.ts +27 -0
- package/src/plugins/usage-normalization.ts +46 -0
- package/src/plugins/usage-privacy.ts +55 -0
- package/src/plugins/usage-query-routes.ts +102 -0
- package/src/plugins/usage-receiver.ts +129 -0
- package/src/plugins/users-query-routes.ts +78 -0
- package/src/plugins/utm-enrichment.ts +36 -0
|
@@ -0,0 +1,1031 @@
|
|
|
1
|
+
import type { SqlDb } from "./sql-db";
|
|
2
|
+
import { dialectFor } from "./sql-db";
|
|
3
|
+
|
|
4
|
+
const TOP_CAUSERS_LIMIT = 5;
|
|
5
|
+
const EXEMPLAR_LIMIT = 3;
|
|
6
|
+
|
|
7
|
+
export interface ActionAggregateExemplar {
|
|
8
|
+
actionId: string;
|
|
9
|
+
agentRunId: string | null;
|
|
10
|
+
traceId: string | null;
|
|
11
|
+
toolCallId: string | null;
|
|
12
|
+
evalId: string | null;
|
|
13
|
+
label: string | null;
|
|
14
|
+
status: string | null;
|
|
15
|
+
occurredAt: string | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ToolReliabilityCauser {
|
|
19
|
+
id: string;
|
|
20
|
+
label: string | null;
|
|
21
|
+
count: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ToolReliabilityAggregate {
|
|
25
|
+
toolName: string;
|
|
26
|
+
callCount: number;
|
|
27
|
+
p50LatencyMs: number | null;
|
|
28
|
+
p95LatencyMs: number | null;
|
|
29
|
+
errorCount: number;
|
|
30
|
+
errorRate: number;
|
|
31
|
+
timeoutCount: number;
|
|
32
|
+
timeoutRate: number;
|
|
33
|
+
retryCount: number;
|
|
34
|
+
malformedArgumentCount: number;
|
|
35
|
+
sideEffectCount: number;
|
|
36
|
+
topCausingAgents: ToolReliabilityCauser[];
|
|
37
|
+
topCausingWorkflows: ToolReliabilityCauser[];
|
|
38
|
+
exemplars: ActionAggregateExemplar[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ToolReliabilityResult {
|
|
42
|
+
projectId: string;
|
|
43
|
+
windowHours: number;
|
|
44
|
+
tools: ToolReliabilityAggregate[];
|
|
45
|
+
generatedAt: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface CostAttributionRow {
|
|
49
|
+
dimension: string;
|
|
50
|
+
key: string | null;
|
|
51
|
+
label: string | null;
|
|
52
|
+
totalCostUsd: number;
|
|
53
|
+
actionCount: number;
|
|
54
|
+
agentRunCount: number;
|
|
55
|
+
toolCallCount: number;
|
|
56
|
+
exemplars: ActionAggregateExemplar[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface CostAttributionResult {
|
|
60
|
+
projectId: string;
|
|
61
|
+
windowHours: number;
|
|
62
|
+
byAgent: CostAttributionRow[];
|
|
63
|
+
byRun: CostAttributionRow[];
|
|
64
|
+
byModel: CostAttributionRow[];
|
|
65
|
+
byProvider: CostAttributionRow[];
|
|
66
|
+
byPromptVersion: CostAttributionRow[];
|
|
67
|
+
byTool: CostAttributionRow[];
|
|
68
|
+
byUser: CostAttributionRow[];
|
|
69
|
+
byTenant: CostAttributionRow[];
|
|
70
|
+
byWorkflow: CostAttributionRow[];
|
|
71
|
+
generatedAt: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface AutonomousReviewRow {
|
|
75
|
+
id: string;
|
|
76
|
+
toolName: string;
|
|
77
|
+
actionId: string;
|
|
78
|
+
actionName: string;
|
|
79
|
+
agentRunId: string;
|
|
80
|
+
agentName: string;
|
|
81
|
+
agentVersion: string;
|
|
82
|
+
autonomyLevel: string;
|
|
83
|
+
sideEffect: boolean;
|
|
84
|
+
approvalState: string;
|
|
85
|
+
status: "ok" | "error";
|
|
86
|
+
errorSnippet: string | null;
|
|
87
|
+
traceId: string;
|
|
88
|
+
occurredAt: string;
|
|
89
|
+
mutationEvidence: boolean;
|
|
90
|
+
mutationArtifactId: string | null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface AutonomousReviewResult {
|
|
94
|
+
projectId: string;
|
|
95
|
+
windowHours: number;
|
|
96
|
+
rows: AutonomousReviewRow[];
|
|
97
|
+
timestamp: string;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface VersionDiffMetric {
|
|
101
|
+
label: string;
|
|
102
|
+
baselineValue: string | number;
|
|
103
|
+
targetValue: string | number;
|
|
104
|
+
deltaValue: string | number | null;
|
|
105
|
+
deltaDirection: "positive" | "negative" | "neutral";
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface VersionComparisonResult {
|
|
109
|
+
projectId: string;
|
|
110
|
+
baselineVersion: string;
|
|
111
|
+
targetVersion: string;
|
|
112
|
+
metrics: VersionDiffMetric[];
|
|
113
|
+
baselineExemplars: ActionAggregateExemplar[];
|
|
114
|
+
targetExemplars: ActionAggregateExemplar[];
|
|
115
|
+
timestamp: string;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
interface ToolAggregateRow {
|
|
119
|
+
tool_name: string;
|
|
120
|
+
call_count: number;
|
|
121
|
+
error_count: number | null;
|
|
122
|
+
timeout_count: number | null;
|
|
123
|
+
malformed_argument_count: number | null;
|
|
124
|
+
side_effect_count: number | null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
interface ToolDetailRow {
|
|
128
|
+
tool_name: string;
|
|
129
|
+
duration_ms: number | null;
|
|
130
|
+
agent_id: string | null;
|
|
131
|
+
agent_label: string | null;
|
|
132
|
+
workflow_id: string | null;
|
|
133
|
+
workflow_label: string | null;
|
|
134
|
+
action_id: string | null;
|
|
135
|
+
agent_run_id: string | null;
|
|
136
|
+
trace_id: string | null;
|
|
137
|
+
tool_call_id: string | null;
|
|
138
|
+
eval_id: string | null;
|
|
139
|
+
action_label: string | null;
|
|
140
|
+
status: string | null;
|
|
141
|
+
occurred_at: string | null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
interface CostRow {
|
|
145
|
+
key: string | null;
|
|
146
|
+
label: string | null;
|
|
147
|
+
total_cost_usd: number | null;
|
|
148
|
+
action_count: number | null;
|
|
149
|
+
agent_run_count: number | null;
|
|
150
|
+
tool_call_count: number | null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
interface ExemplarSqlRow {
|
|
154
|
+
dimension_key?: string | null;
|
|
155
|
+
action_id: string | null;
|
|
156
|
+
agent_run_id: string | null;
|
|
157
|
+
trace_id: string | null;
|
|
158
|
+
tool_call_id: string | null;
|
|
159
|
+
eval_id: string | null;
|
|
160
|
+
label: string | null;
|
|
161
|
+
status: string | null;
|
|
162
|
+
occurred_at: string | null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
interface AutonomousReviewSqlRow {
|
|
166
|
+
id: string;
|
|
167
|
+
tool_name: string;
|
|
168
|
+
action_id: string;
|
|
169
|
+
action_name: string | null;
|
|
170
|
+
agent_run_id: string | null;
|
|
171
|
+
agent_name: string | null;
|
|
172
|
+
agent_version: string | null;
|
|
173
|
+
autonomy_level: string | null;
|
|
174
|
+
side_effect: number | null;
|
|
175
|
+
approval_state: string | null;
|
|
176
|
+
status: string | null;
|
|
177
|
+
error_snippet: string | null;
|
|
178
|
+
trace_id: string | null;
|
|
179
|
+
occurred_at: string | null;
|
|
180
|
+
mutation_evidence: number | null;
|
|
181
|
+
mutation_artifact_id: string | null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
interface VersionStatsRow {
|
|
185
|
+
version: string;
|
|
186
|
+
run_count: number | null;
|
|
187
|
+
success_count: number | null;
|
|
188
|
+
avg_duration_ms: number | null;
|
|
189
|
+
avg_cost_usd: number | null;
|
|
190
|
+
eval_count: number | null;
|
|
191
|
+
passed_eval_count: number | null;
|
|
192
|
+
avg_eval_score: number | null;
|
|
193
|
+
tool_count: number | null;
|
|
194
|
+
tool_error_count: number | null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const toNumber = (value: unknown): number => {
|
|
198
|
+
const n = typeof value === "number" ? value : Number(value);
|
|
199
|
+
return Number.isFinite(n) ? n : 0;
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const percentile = (values: number[], p: number): number | null => {
|
|
203
|
+
const sorted = values
|
|
204
|
+
.filter((v) => Number.isFinite(v) && v >= 0)
|
|
205
|
+
.sort((a, b) => a - b);
|
|
206
|
+
if (sorted.length === 0) return null;
|
|
207
|
+
const index = Math.ceil((p / 100) * sorted.length) - 1;
|
|
208
|
+
return sorted[Math.min(Math.max(index, 0), sorted.length - 1)] ?? null;
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const topCausers = (
|
|
212
|
+
rows: ToolDetailRow[],
|
|
213
|
+
idKey: "agent_id" | "workflow_id",
|
|
214
|
+
labelKey: "agent_label" | "workflow_label",
|
|
215
|
+
): ToolReliabilityCauser[] => {
|
|
216
|
+
const counts = new Map<string, { label: string | null; count: number }>();
|
|
217
|
+
for (const row of rows) {
|
|
218
|
+
const id = row[idKey];
|
|
219
|
+
if (!id) continue;
|
|
220
|
+
const current = counts.get(id) ?? { label: row[labelKey], count: 0 };
|
|
221
|
+
current.count += 1;
|
|
222
|
+
if (!current.label && row[labelKey]) current.label = row[labelKey];
|
|
223
|
+
counts.set(id, current);
|
|
224
|
+
}
|
|
225
|
+
return [...counts.entries()]
|
|
226
|
+
.map(([id, value]) => ({ id, label: value.label, count: value.count }))
|
|
227
|
+
.sort((a, b) => b.count - a.count || a.id.localeCompare(b.id))
|
|
228
|
+
.slice(0, TOP_CAUSERS_LIMIT);
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const exemplarFromRow = (row: ExemplarSqlRow): ActionAggregateExemplar => ({
|
|
232
|
+
actionId: row.action_id ?? "",
|
|
233
|
+
agentRunId: row.agent_run_id ?? null,
|
|
234
|
+
traceId: row.trace_id ?? null,
|
|
235
|
+
toolCallId: row.tool_call_id ?? null,
|
|
236
|
+
evalId: row.eval_id ?? null,
|
|
237
|
+
label: row.label ?? null,
|
|
238
|
+
status: row.status ?? null,
|
|
239
|
+
occurredAt: row.occurred_at ?? null,
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
const exemplarsFromRows = (rows: ExemplarSqlRow[]): ActionAggregateExemplar[] =>
|
|
243
|
+
rows
|
|
244
|
+
.filter((row) => row.action_id)
|
|
245
|
+
.map(exemplarFromRow)
|
|
246
|
+
.slice(0, EXEMPLAR_LIMIT);
|
|
247
|
+
|
|
248
|
+
const appendExemplar = (
|
|
249
|
+
map: Map<string, ActionAggregateExemplar[]>,
|
|
250
|
+
key: string | null,
|
|
251
|
+
row: ExemplarSqlRow,
|
|
252
|
+
) => {
|
|
253
|
+
if (key === null || key === undefined || !row.action_id) return;
|
|
254
|
+
const current = map.get(key) ?? [];
|
|
255
|
+
if (current.length >= EXEMPLAR_LIMIT) return;
|
|
256
|
+
current.push(exemplarFromRow(row));
|
|
257
|
+
map.set(key, current);
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const costRows = (
|
|
261
|
+
dimension: string,
|
|
262
|
+
rows: CostRow[],
|
|
263
|
+
exemplars = new Map<string, ActionAggregateExemplar[]>(),
|
|
264
|
+
): CostAttributionRow[] =>
|
|
265
|
+
rows.map((row) => ({
|
|
266
|
+
dimension,
|
|
267
|
+
key: row.key,
|
|
268
|
+
label: row.label,
|
|
269
|
+
totalCostUsd: toNumber(row.total_cost_usd),
|
|
270
|
+
actionCount: toNumber(row.action_count),
|
|
271
|
+
agentRunCount: toNumber(row.agent_run_count),
|
|
272
|
+
toolCallCount: toNumber(row.tool_call_count),
|
|
273
|
+
exemplars: row.key ? (exemplars.get(row.key) ?? []) : [],
|
|
274
|
+
}));
|
|
275
|
+
|
|
276
|
+
const inClause = (values: string[]): string => values.map(() => "?").join(", ");
|
|
277
|
+
|
|
278
|
+
const actionTenantExpr = (db: SqlDb, alias = "a"): string => {
|
|
279
|
+
const dialect = dialectFor(db);
|
|
280
|
+
return `COALESCE(
|
|
281
|
+
${dialect.jsonText(`${alias}.attrs_json`, '$."obs.tenant.id"')},
|
|
282
|
+
${dialect.jsonText(`${alias}.attrs_json`, '$."tenant.id"')},
|
|
283
|
+
${dialect.jsonText(`${alias}.attrs_json`, "$.tenant_id")},
|
|
284
|
+
${dialect.jsonText(`${alias}.attrs_json`, "$.target_tenant")}
|
|
285
|
+
)`;
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
const actionWorkflowExpr = (db: SqlDb, alias = "a"): string => {
|
|
289
|
+
const dialect = dialectFor(db);
|
|
290
|
+
return `COALESCE(
|
|
291
|
+
CASE WHEN ${alias}.actor_type = 'workflow' THEN ${alias}.actor_id ELSE NULL END,
|
|
292
|
+
${dialect.jsonText(`${alias}.attrs_json`, '$."obs.workflow.id"')},
|
|
293
|
+
${dialect.jsonText(`${alias}.attrs_json`, '$."workflow.id"')},
|
|
294
|
+
${dialect.jsonText(`${alias}.attrs_json`, "$.workflow_id")},
|
|
295
|
+
${dialect.jsonText(`${alias}.attrs_json`, "$.task_id")}
|
|
296
|
+
)`;
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
export const getToolReliabilityAggregates = async (
|
|
300
|
+
db: SqlDb,
|
|
301
|
+
projectId: string,
|
|
302
|
+
windowHours: number,
|
|
303
|
+
limit: number,
|
|
304
|
+
): Promise<ToolReliabilityResult> => {
|
|
305
|
+
const dialect = dialectFor(db);
|
|
306
|
+
const since = dialect.sinceHours("?");
|
|
307
|
+
|
|
308
|
+
const aggregateRows = await db
|
|
309
|
+
.prepare(`
|
|
310
|
+
SELECT
|
|
311
|
+
t.tool_name,
|
|
312
|
+
COUNT(*) AS call_count,
|
|
313
|
+
SUM(CASE WHEN t.error_type IS NOT NULL AND t.error_type <> '' THEN 1 ELSE 0 END) AS error_count,
|
|
314
|
+
SUM(CASE
|
|
315
|
+
WHEN LOWER(COALESCE(t.error_type, '')) LIKE '%timeout%'
|
|
316
|
+
OR LOWER(COALESCE(t.error_type, '')) = 'deadline_exceeded'
|
|
317
|
+
THEN 1 ELSE 0
|
|
318
|
+
END) AS timeout_count,
|
|
319
|
+
SUM(CASE
|
|
320
|
+
WHEN LOWER(COALESCE(t.error_type, '')) LIKE '%malformed%'
|
|
321
|
+
OR LOWER(COALESCE(t.error_type, '')) LIKE '%invalid_argument%'
|
|
322
|
+
OR LOWER(COALESCE(t.error_type, '')) LIKE '%invalid_arguments%'
|
|
323
|
+
OR LOWER(COALESCE(t.error_type, '')) LIKE '%schema_validation%'
|
|
324
|
+
OR LOWER(COALESCE(t.error_type, '')) = 'bad_request'
|
|
325
|
+
THEN 1 ELSE 0
|
|
326
|
+
END) AS malformed_argument_count,
|
|
327
|
+
SUM(CASE WHEN t.side_effect <> 0 THEN 1 ELSE 0 END) AS side_effect_count
|
|
328
|
+
FROM tool_calls t
|
|
329
|
+
LEFT JOIN actions a ON a.project_id = t.project_id AND a.id = t.action_id
|
|
330
|
+
WHERE t.project_id = ? AND (a.started_at IS NULL OR a.started_at >= ${since})
|
|
331
|
+
GROUP BY t.tool_name
|
|
332
|
+
ORDER BY call_count DESC, t.tool_name ASC
|
|
333
|
+
LIMIT ?
|
|
334
|
+
`)
|
|
335
|
+
.bind(projectId, windowHours, limit)
|
|
336
|
+
.all<ToolAggregateRow>();
|
|
337
|
+
|
|
338
|
+
const toolNames = aggregateRows.results.map((row) => row.tool_name);
|
|
339
|
+
if (toolNames.length === 0) {
|
|
340
|
+
return {
|
|
341
|
+
projectId,
|
|
342
|
+
windowHours,
|
|
343
|
+
tools: [],
|
|
344
|
+
generatedAt: new Date().toISOString(),
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const workflowIdExpr = `COALESCE(
|
|
349
|
+
${actionWorkflowExpr(db, "a")},
|
|
350
|
+
${actionWorkflowExpr(db, "root")}
|
|
351
|
+
)`;
|
|
352
|
+
|
|
353
|
+
const detailRows = await db
|
|
354
|
+
.prepare(`
|
|
355
|
+
SELECT
|
|
356
|
+
t.tool_name,
|
|
357
|
+
a.duration_ms,
|
|
358
|
+
COALESCE(
|
|
359
|
+
ar.agent_id,
|
|
360
|
+
CASE WHEN a.actor_type = 'agent' THEN a.actor_id ELSE NULL END,
|
|
361
|
+
CASE WHEN root.actor_type = 'agent' THEN root.actor_id ELSE NULL END
|
|
362
|
+
) AS agent_id,
|
|
363
|
+
COALESCE(
|
|
364
|
+
ar.agent_name,
|
|
365
|
+
CASE WHEN a.actor_type = 'agent' THEN a.name ELSE NULL END,
|
|
366
|
+
CASE WHEN root.actor_type = 'agent' THEN root.name ELSE NULL END
|
|
367
|
+
) AS agent_label,
|
|
368
|
+
${workflowIdExpr} AS workflow_id,
|
|
369
|
+
${workflowIdExpr} AS workflow_label,
|
|
370
|
+
a.id AS action_id,
|
|
371
|
+
COALESCE(a.agent_run_id, a.root_action_id) AS agent_run_id,
|
|
372
|
+
COALESCE(a.trace_id, root.trace_id) AS trace_id,
|
|
373
|
+
t.id AS tool_call_id,
|
|
374
|
+
er.id AS eval_id,
|
|
375
|
+
COALESCE(a.name, t.tool_name) AS action_label,
|
|
376
|
+
a.status,
|
|
377
|
+
a.started_at AS occurred_at
|
|
378
|
+
FROM tool_calls t
|
|
379
|
+
LEFT JOIN actions a ON a.project_id = t.project_id AND a.id = t.action_id
|
|
380
|
+
LEFT JOIN actions root ON root.project_id = t.project_id AND root.id = a.root_action_id
|
|
381
|
+
LEFT JOIN agent_runs ar ON ar.project_id = t.project_id AND ar.id = COALESCE(a.agent_run_id, a.root_action_id)
|
|
382
|
+
LEFT JOIN eval_results er ON er.project_id = t.project_id AND er.action_id = a.id
|
|
383
|
+
WHERE t.project_id = ?
|
|
384
|
+
AND (a.started_at IS NULL OR a.started_at >= ${since})
|
|
385
|
+
AND t.tool_name IN (${inClause(toolNames)})
|
|
386
|
+
ORDER BY
|
|
387
|
+
CASE WHEN a.status = 'error' OR t.error_type IS NOT NULL THEN 0 ELSE 1 END,
|
|
388
|
+
a.started_at DESC
|
|
389
|
+
`)
|
|
390
|
+
.bind(projectId, windowHours, ...toolNames)
|
|
391
|
+
.all<ToolDetailRow>();
|
|
392
|
+
|
|
393
|
+
const detailsByTool = new Map<string, ToolDetailRow[]>();
|
|
394
|
+
for (const row of detailRows.results) {
|
|
395
|
+
const rows = detailsByTool.get(row.tool_name) ?? [];
|
|
396
|
+
rows.push(row);
|
|
397
|
+
detailsByTool.set(row.tool_name, rows);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return {
|
|
401
|
+
projectId,
|
|
402
|
+
windowHours,
|
|
403
|
+
tools: aggregateRows.results.map((row) => {
|
|
404
|
+
const callCount = toNumber(row.call_count);
|
|
405
|
+
const errorCount = toNumber(row.error_count);
|
|
406
|
+
const timeoutCount = toNumber(row.timeout_count);
|
|
407
|
+
const details = detailsByTool.get(row.tool_name) ?? [];
|
|
408
|
+
const durations = details
|
|
409
|
+
.map((detail) => detail.duration_ms)
|
|
410
|
+
.filter((duration): duration is number => duration !== null);
|
|
411
|
+
return {
|
|
412
|
+
toolName: row.tool_name,
|
|
413
|
+
callCount,
|
|
414
|
+
p50LatencyMs: percentile(durations, 50),
|
|
415
|
+
p95LatencyMs: percentile(durations, 95),
|
|
416
|
+
errorCount,
|
|
417
|
+
errorRate: callCount > 0 ? errorCount / callCount : 0,
|
|
418
|
+
timeoutCount,
|
|
419
|
+
timeoutRate: callCount > 0 ? timeoutCount / callCount : 0,
|
|
420
|
+
retryCount: 0,
|
|
421
|
+
malformedArgumentCount: toNumber(row.malformed_argument_count),
|
|
422
|
+
sideEffectCount: toNumber(row.side_effect_count),
|
|
423
|
+
topCausingAgents: topCausers(details, "agent_id", "agent_label"),
|
|
424
|
+
topCausingWorkflows: topCausers(
|
|
425
|
+
details,
|
|
426
|
+
"workflow_id",
|
|
427
|
+
"workflow_label",
|
|
428
|
+
),
|
|
429
|
+
exemplars: exemplarsFromRows(
|
|
430
|
+
details.map((detail) => ({
|
|
431
|
+
action_id: detail.action_id,
|
|
432
|
+
agent_run_id: detail.agent_run_id,
|
|
433
|
+
trace_id: detail.trace_id,
|
|
434
|
+
tool_call_id: detail.tool_call_id,
|
|
435
|
+
eval_id: detail.eval_id,
|
|
436
|
+
label: detail.action_label,
|
|
437
|
+
status: detail.status,
|
|
438
|
+
occurred_at: detail.occurred_at,
|
|
439
|
+
})),
|
|
440
|
+
),
|
|
441
|
+
};
|
|
442
|
+
}),
|
|
443
|
+
generatedAt: new Date().toISOString(),
|
|
444
|
+
};
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
const groupByActionDimension = async (
|
|
448
|
+
db: SqlDb,
|
|
449
|
+
projectId: string,
|
|
450
|
+
windowHours: number,
|
|
451
|
+
limit: number,
|
|
452
|
+
dimension: string,
|
|
453
|
+
keyExpr: string,
|
|
454
|
+
labelExpr = keyExpr,
|
|
455
|
+
): Promise<CostAttributionRow[]> => {
|
|
456
|
+
const dialect = dialectFor(db);
|
|
457
|
+
const since = dialect.sinceHours("?");
|
|
458
|
+
const rows = await db
|
|
459
|
+
.prepare(`
|
|
460
|
+
SELECT
|
|
461
|
+
${keyExpr} AS key,
|
|
462
|
+
${labelExpr} AS label,
|
|
463
|
+
COALESCE(SUM(COALESCE(a.total_cost_usd, 0)), 0) AS total_cost_usd,
|
|
464
|
+
COUNT(*) AS action_count,
|
|
465
|
+
COUNT(DISTINCT a.agent_run_id) AS agent_run_count,
|
|
466
|
+
0 AS tool_call_count
|
|
467
|
+
FROM actions a
|
|
468
|
+
WHERE a.project_id = ? AND a.started_at >= ${since}
|
|
469
|
+
GROUP BY 1, 2
|
|
470
|
+
ORDER BY total_cost_usd DESC, action_count DESC
|
|
471
|
+
LIMIT ?
|
|
472
|
+
`)
|
|
473
|
+
.bind(projectId, windowHours, limit)
|
|
474
|
+
.all<CostRow>();
|
|
475
|
+
const keys = rows.results
|
|
476
|
+
.map((row) => row.key)
|
|
477
|
+
.filter((key): key is string => !!key);
|
|
478
|
+
if (keys.length === 0) return costRows(dimension, rows.results);
|
|
479
|
+
|
|
480
|
+
const exemplarRows = await db
|
|
481
|
+
.prepare(`
|
|
482
|
+
SELECT
|
|
483
|
+
${keyExpr} AS dimension_key,
|
|
484
|
+
a.id AS action_id,
|
|
485
|
+
COALESCE(a.agent_run_id, a.root_action_id) AS agent_run_id,
|
|
486
|
+
a.trace_id,
|
|
487
|
+
a.tool_call_id,
|
|
488
|
+
er.id AS eval_id,
|
|
489
|
+
a.name AS label,
|
|
490
|
+
a.status,
|
|
491
|
+
a.started_at AS occurred_at
|
|
492
|
+
FROM actions a
|
|
493
|
+
LEFT JOIN eval_results er ON er.project_id = a.project_id AND er.action_id = a.id
|
|
494
|
+
WHERE a.project_id = ?
|
|
495
|
+
AND a.started_at >= ${since}
|
|
496
|
+
AND ${keyExpr} IN (${inClause(keys)})
|
|
497
|
+
ORDER BY COALESCE(a.total_cost_usd, 0) DESC, a.started_at DESC
|
|
498
|
+
`)
|
|
499
|
+
.bind(projectId, windowHours, ...keys)
|
|
500
|
+
.all<ExemplarSqlRow>();
|
|
501
|
+
const exemplars = new Map<string, ActionAggregateExemplar[]>();
|
|
502
|
+
for (const row of exemplarRows.results) {
|
|
503
|
+
appendExemplar(exemplars, row.dimension_key ?? null, row);
|
|
504
|
+
}
|
|
505
|
+
return costRows(dimension, rows.results, exemplars);
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
const getAgentCostExemplars = async (
|
|
509
|
+
db: SqlDb,
|
|
510
|
+
projectId: string,
|
|
511
|
+
windowHours: number,
|
|
512
|
+
keys: string[],
|
|
513
|
+
): Promise<Map<string, ActionAggregateExemplar[]>> => {
|
|
514
|
+
if (keys.length === 0) return new Map();
|
|
515
|
+
const dialect = dialectFor(db);
|
|
516
|
+
const since = dialect.sinceHours("?");
|
|
517
|
+
const rows = await db
|
|
518
|
+
.prepare(`
|
|
519
|
+
SELECT
|
|
520
|
+
ar.agent_id AS dimension_key,
|
|
521
|
+
root.id AS action_id,
|
|
522
|
+
ar.id AS agent_run_id,
|
|
523
|
+
root.trace_id,
|
|
524
|
+
root.tool_call_id,
|
|
525
|
+
er.id AS eval_id,
|
|
526
|
+
ar.agent_name AS label,
|
|
527
|
+
ar.status,
|
|
528
|
+
root.started_at AS occurred_at
|
|
529
|
+
FROM agent_runs ar
|
|
530
|
+
LEFT JOIN actions root ON root.project_id = ar.project_id AND root.id = ar.id
|
|
531
|
+
LEFT JOIN eval_results er ON er.project_id = ar.project_id AND er.action_id = root.id
|
|
532
|
+
WHERE ar.project_id = ?
|
|
533
|
+
AND (root.started_at IS NULL OR root.started_at >= ${since})
|
|
534
|
+
AND ar.agent_id IN (${inClause(keys)})
|
|
535
|
+
ORDER BY COALESCE(ar.total_cost_usd, 0) DESC, root.started_at DESC
|
|
536
|
+
`)
|
|
537
|
+
.bind(projectId, windowHours, ...keys)
|
|
538
|
+
.all<ExemplarSqlRow>();
|
|
539
|
+
const exemplars = new Map<string, ActionAggregateExemplar[]>();
|
|
540
|
+
for (const row of rows.results) {
|
|
541
|
+
appendExemplar(exemplars, row.dimension_key ?? null, row);
|
|
542
|
+
}
|
|
543
|
+
return exemplars;
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
const getRunCostExemplars = async (
|
|
547
|
+
db: SqlDb,
|
|
548
|
+
projectId: string,
|
|
549
|
+
windowHours: number,
|
|
550
|
+
keys: string[],
|
|
551
|
+
): Promise<Map<string, ActionAggregateExemplar[]>> => {
|
|
552
|
+
if (keys.length === 0) return new Map();
|
|
553
|
+
const dialect = dialectFor(db);
|
|
554
|
+
const since = dialect.sinceHours("?");
|
|
555
|
+
const rows = await db
|
|
556
|
+
.prepare(`
|
|
557
|
+
SELECT
|
|
558
|
+
ar.id AS dimension_key,
|
|
559
|
+
root.id AS action_id,
|
|
560
|
+
ar.id AS agent_run_id,
|
|
561
|
+
root.trace_id,
|
|
562
|
+
root.tool_call_id,
|
|
563
|
+
er.id AS eval_id,
|
|
564
|
+
ar.agent_name AS label,
|
|
565
|
+
ar.status,
|
|
566
|
+
root.started_at AS occurred_at
|
|
567
|
+
FROM agent_runs ar
|
|
568
|
+
LEFT JOIN actions root ON root.project_id = ar.project_id AND root.id = ar.id
|
|
569
|
+
LEFT JOIN eval_results er ON er.project_id = ar.project_id AND er.action_id = root.id
|
|
570
|
+
WHERE ar.project_id = ?
|
|
571
|
+
AND (root.started_at IS NULL OR root.started_at >= ${since})
|
|
572
|
+
AND ar.id IN (${inClause(keys)})
|
|
573
|
+
ORDER BY COALESCE(ar.total_cost_usd, 0) DESC, root.started_at DESC
|
|
574
|
+
`)
|
|
575
|
+
.bind(projectId, windowHours, ...keys)
|
|
576
|
+
.all<ExemplarSqlRow>();
|
|
577
|
+
const exemplars = new Map<string, ActionAggregateExemplar[]>();
|
|
578
|
+
for (const row of rows.results) {
|
|
579
|
+
appendExemplar(exemplars, row.dimension_key ?? null, row);
|
|
580
|
+
}
|
|
581
|
+
return exemplars;
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
const getToolCostExemplars = async (
|
|
585
|
+
db: SqlDb,
|
|
586
|
+
projectId: string,
|
|
587
|
+
windowHours: number,
|
|
588
|
+
keys: string[],
|
|
589
|
+
): Promise<Map<string, ActionAggregateExemplar[]>> => {
|
|
590
|
+
if (keys.length === 0) return new Map();
|
|
591
|
+
const dialect = dialectFor(db);
|
|
592
|
+
const since = dialect.sinceHours("?");
|
|
593
|
+
const rows = await db
|
|
594
|
+
.prepare(`
|
|
595
|
+
SELECT
|
|
596
|
+
t.tool_name AS dimension_key,
|
|
597
|
+
a.id AS action_id,
|
|
598
|
+
COALESCE(a.agent_run_id, a.root_action_id) AS agent_run_id,
|
|
599
|
+
COALESCE(a.trace_id, root.trace_id) AS trace_id,
|
|
600
|
+
t.id AS tool_call_id,
|
|
601
|
+
er.id AS eval_id,
|
|
602
|
+
COALESCE(a.name, t.tool_name) AS label,
|
|
603
|
+
a.status,
|
|
604
|
+
a.started_at AS occurred_at
|
|
605
|
+
FROM tool_calls t
|
|
606
|
+
LEFT JOIN actions a ON a.project_id = t.project_id AND a.id = t.action_id
|
|
607
|
+
LEFT JOIN actions root ON root.project_id = t.project_id AND root.id = a.root_action_id
|
|
608
|
+
LEFT JOIN eval_results er ON er.project_id = t.project_id AND er.action_id = a.id
|
|
609
|
+
WHERE t.project_id = ?
|
|
610
|
+
AND (a.started_at IS NULL OR a.started_at >= ${since})
|
|
611
|
+
AND t.tool_name IN (${inClause(keys)})
|
|
612
|
+
ORDER BY COALESCE(a.total_cost_usd, 0) DESC, a.started_at DESC
|
|
613
|
+
`)
|
|
614
|
+
.bind(projectId, windowHours, ...keys)
|
|
615
|
+
.all<ExemplarSqlRow>();
|
|
616
|
+
const exemplars = new Map<string, ActionAggregateExemplar[]>();
|
|
617
|
+
for (const row of rows.results) {
|
|
618
|
+
appendExemplar(exemplars, row.dimension_key ?? null, row);
|
|
619
|
+
}
|
|
620
|
+
return exemplars;
|
|
621
|
+
};
|
|
622
|
+
|
|
623
|
+
export const getCostAttributionAggregates = async (
|
|
624
|
+
db: SqlDb,
|
|
625
|
+
projectId: string,
|
|
626
|
+
windowHours: number,
|
|
627
|
+
limit: number,
|
|
628
|
+
): Promise<CostAttributionResult> => {
|
|
629
|
+
const dialect = dialectFor(db);
|
|
630
|
+
const since = dialect.sinceHours("?");
|
|
631
|
+
const tenantExpr = actionTenantExpr(db, "a");
|
|
632
|
+
const workflowExpr = actionWorkflowExpr(db, "a");
|
|
633
|
+
|
|
634
|
+
const [
|
|
635
|
+
agentRows,
|
|
636
|
+
runRows,
|
|
637
|
+
byModel,
|
|
638
|
+
byProvider,
|
|
639
|
+
byPromptVersion,
|
|
640
|
+
toolRows,
|
|
641
|
+
byUser,
|
|
642
|
+
byTenant,
|
|
643
|
+
byWorkflow,
|
|
644
|
+
] = await Promise.all([
|
|
645
|
+
db
|
|
646
|
+
.prepare(`
|
|
647
|
+
SELECT
|
|
648
|
+
ar.agent_id AS key,
|
|
649
|
+
ar.agent_name AS label,
|
|
650
|
+
COALESCE(SUM(COALESCE(ar.total_cost_usd, 0)), 0) AS total_cost_usd,
|
|
651
|
+
0 AS action_count,
|
|
652
|
+
COUNT(*) AS agent_run_count,
|
|
653
|
+
0 AS tool_call_count
|
|
654
|
+
FROM agent_runs ar
|
|
655
|
+
LEFT JOIN actions root ON root.project_id = ar.project_id AND root.id = ar.id
|
|
656
|
+
WHERE ar.project_id = ? AND (root.started_at IS NULL OR root.started_at >= ${since})
|
|
657
|
+
GROUP BY ar.agent_id, ar.agent_name
|
|
658
|
+
ORDER BY total_cost_usd DESC, agent_run_count DESC
|
|
659
|
+
LIMIT ?
|
|
660
|
+
`)
|
|
661
|
+
.bind(projectId, windowHours, limit)
|
|
662
|
+
.all<CostRow>(),
|
|
663
|
+
db
|
|
664
|
+
.prepare(`
|
|
665
|
+
SELECT
|
|
666
|
+
ar.id AS key,
|
|
667
|
+
ar.agent_name AS label,
|
|
668
|
+
COALESCE(ar.total_cost_usd, 0) AS total_cost_usd,
|
|
669
|
+
0 AS action_count,
|
|
670
|
+
1 AS agent_run_count,
|
|
671
|
+
0 AS tool_call_count
|
|
672
|
+
FROM agent_runs ar
|
|
673
|
+
LEFT JOIN actions root ON root.project_id = ar.project_id AND root.id = ar.id
|
|
674
|
+
WHERE ar.project_id = ? AND (root.started_at IS NULL OR root.started_at >= ${since})
|
|
675
|
+
ORDER BY total_cost_usd DESC
|
|
676
|
+
LIMIT ?
|
|
677
|
+
`)
|
|
678
|
+
.bind(projectId, windowHours, limit)
|
|
679
|
+
.all<CostRow>(),
|
|
680
|
+
groupByActionDimension(
|
|
681
|
+
db,
|
|
682
|
+
projectId,
|
|
683
|
+
windowHours,
|
|
684
|
+
limit,
|
|
685
|
+
"model",
|
|
686
|
+
"a.model_name",
|
|
687
|
+
),
|
|
688
|
+
groupByActionDimension(
|
|
689
|
+
db,
|
|
690
|
+
projectId,
|
|
691
|
+
windowHours,
|
|
692
|
+
limit,
|
|
693
|
+
"provider",
|
|
694
|
+
"a.provider",
|
|
695
|
+
),
|
|
696
|
+
groupByActionDimension(
|
|
697
|
+
db,
|
|
698
|
+
projectId,
|
|
699
|
+
windowHours,
|
|
700
|
+
limit,
|
|
701
|
+
"prompt_version",
|
|
702
|
+
"a.prompt_version",
|
|
703
|
+
),
|
|
704
|
+
db
|
|
705
|
+
.prepare(`
|
|
706
|
+
SELECT
|
|
707
|
+
t.tool_name AS key,
|
|
708
|
+
t.tool_name AS label,
|
|
709
|
+
COALESCE(SUM(COALESCE(a.total_cost_usd, 0)), 0) AS total_cost_usd,
|
|
710
|
+
COUNT(DISTINCT a.id) AS action_count,
|
|
711
|
+
COUNT(DISTINCT a.agent_run_id) AS agent_run_count,
|
|
712
|
+
COUNT(*) AS tool_call_count
|
|
713
|
+
FROM tool_calls t
|
|
714
|
+
LEFT JOIN actions a ON a.project_id = t.project_id AND a.id = t.action_id
|
|
715
|
+
WHERE t.project_id = ? AND (a.started_at IS NULL OR a.started_at >= ${since})
|
|
716
|
+
GROUP BY t.tool_name
|
|
717
|
+
ORDER BY total_cost_usd DESC, tool_call_count DESC
|
|
718
|
+
LIMIT ?
|
|
719
|
+
`)
|
|
720
|
+
.bind(projectId, windowHours, limit)
|
|
721
|
+
.all<CostRow>(),
|
|
722
|
+
groupByActionDimension(
|
|
723
|
+
db,
|
|
724
|
+
projectId,
|
|
725
|
+
windowHours,
|
|
726
|
+
limit,
|
|
727
|
+
"user",
|
|
728
|
+
"a.user_id",
|
|
729
|
+
),
|
|
730
|
+
groupByActionDimension(
|
|
731
|
+
db,
|
|
732
|
+
projectId,
|
|
733
|
+
windowHours,
|
|
734
|
+
limit,
|
|
735
|
+
"tenant",
|
|
736
|
+
tenantExpr,
|
|
737
|
+
),
|
|
738
|
+
groupByActionDimension(
|
|
739
|
+
db,
|
|
740
|
+
projectId,
|
|
741
|
+
windowHours,
|
|
742
|
+
limit,
|
|
743
|
+
"workflow",
|
|
744
|
+
workflowExpr,
|
|
745
|
+
),
|
|
746
|
+
]);
|
|
747
|
+
const agentKeys = agentRows.results
|
|
748
|
+
.map((row) => row.key)
|
|
749
|
+
.filter((key): key is string => !!key);
|
|
750
|
+
const runKeys = runRows.results
|
|
751
|
+
.map((row) => row.key)
|
|
752
|
+
.filter((key): key is string => !!key);
|
|
753
|
+
const toolKeys = toolRows.results
|
|
754
|
+
.map((row) => row.key)
|
|
755
|
+
.filter((key): key is string => !!key);
|
|
756
|
+
const [agentExemplars, runExemplars, toolExemplars] = await Promise.all([
|
|
757
|
+
getAgentCostExemplars(db, projectId, windowHours, agentKeys),
|
|
758
|
+
getRunCostExemplars(db, projectId, windowHours, runKeys),
|
|
759
|
+
getToolCostExemplars(db, projectId, windowHours, toolKeys),
|
|
760
|
+
]);
|
|
761
|
+
|
|
762
|
+
return {
|
|
763
|
+
projectId,
|
|
764
|
+
windowHours,
|
|
765
|
+
byAgent: costRows("agent", agentRows.results, agentExemplars),
|
|
766
|
+
byRun: costRows("run", runRows.results, runExemplars),
|
|
767
|
+
byModel,
|
|
768
|
+
byProvider,
|
|
769
|
+
byPromptVersion,
|
|
770
|
+
byTool: costRows("tool", toolRows.results, toolExemplars),
|
|
771
|
+
byUser,
|
|
772
|
+
byTenant,
|
|
773
|
+
byWorkflow,
|
|
774
|
+
generatedAt: new Date().toISOString(),
|
|
775
|
+
};
|
|
776
|
+
};
|
|
777
|
+
|
|
778
|
+
export const getAutonomousReviewAggregates = async (
|
|
779
|
+
db: SqlDb,
|
|
780
|
+
projectId: string,
|
|
781
|
+
windowHours: number,
|
|
782
|
+
limit: number,
|
|
783
|
+
): Promise<AutonomousReviewResult> => {
|
|
784
|
+
const dialect = dialectFor(db);
|
|
785
|
+
const since = dialect.sinceHours("?");
|
|
786
|
+
const rows = await db
|
|
787
|
+
.prepare(`
|
|
788
|
+
SELECT
|
|
789
|
+
t.id,
|
|
790
|
+
t.tool_name,
|
|
791
|
+
t.action_id,
|
|
792
|
+
a.name AS action_name,
|
|
793
|
+
COALESCE(a.agent_run_id, a.root_action_id) AS agent_run_id,
|
|
794
|
+
ar.agent_name,
|
|
795
|
+
ar.agent_version,
|
|
796
|
+
COALESCE(ar.autonomy_level, 'autonomous_write') AS autonomy_level,
|
|
797
|
+
t.side_effect,
|
|
798
|
+
COALESCE(t.approval_state, 'suggested') AS approval_state,
|
|
799
|
+
COALESCE(a.status, 'ok') AS status,
|
|
800
|
+
t.error_type AS error_snippet,
|
|
801
|
+
COALESCE(a.trace_id, root.trace_id) AS trace_id,
|
|
802
|
+
COALESCE(a.started_at, root.started_at) AS occurred_at,
|
|
803
|
+
CASE
|
|
804
|
+
WHEN t.mutation_before_json IS NOT NULL
|
|
805
|
+
OR t.mutation_after_json IS NOT NULL
|
|
806
|
+
OR t.mutation_diff_json IS NOT NULL
|
|
807
|
+
OR t.mutation_artifact_id IS NOT NULL
|
|
808
|
+
THEN 1 ELSE 0
|
|
809
|
+
END AS mutation_evidence,
|
|
810
|
+
t.mutation_artifact_id
|
|
811
|
+
FROM tool_calls t
|
|
812
|
+
LEFT JOIN actions a ON a.project_id = t.project_id AND a.id = t.action_id
|
|
813
|
+
LEFT JOIN actions root ON root.project_id = t.project_id AND root.id = a.root_action_id
|
|
814
|
+
LEFT JOIN agent_runs ar ON ar.project_id = t.project_id AND ar.id = COALESCE(a.agent_run_id, a.root_action_id)
|
|
815
|
+
WHERE t.project_id = ?
|
|
816
|
+
AND t.side_effect <> 0
|
|
817
|
+
AND COALESCE(ar.autonomy_level, '') = 'autonomous_write'
|
|
818
|
+
AND (a.started_at IS NULL OR a.started_at >= ${since})
|
|
819
|
+
ORDER BY occurred_at DESC
|
|
820
|
+
LIMIT ?
|
|
821
|
+
`)
|
|
822
|
+
.bind(projectId, windowHours, limit)
|
|
823
|
+
.all<AutonomousReviewSqlRow>();
|
|
824
|
+
|
|
825
|
+
return {
|
|
826
|
+
projectId,
|
|
827
|
+
windowHours,
|
|
828
|
+
rows: rows.results.map((row) => ({
|
|
829
|
+
id: row.id,
|
|
830
|
+
toolName: row.tool_name,
|
|
831
|
+
actionId: row.action_id,
|
|
832
|
+
actionName: row.action_name ?? row.tool_name,
|
|
833
|
+
agentRunId: row.agent_run_id ?? "",
|
|
834
|
+
agentName: row.agent_name ?? "Unknown agent",
|
|
835
|
+
agentVersion: row.agent_version ?? "unknown",
|
|
836
|
+
autonomyLevel: row.autonomy_level ?? "autonomous_write",
|
|
837
|
+
sideEffect: row.side_effect !== 0,
|
|
838
|
+
approvalState: row.approval_state ?? "suggested",
|
|
839
|
+
status: row.status === "error" ? "error" : "ok",
|
|
840
|
+
errorSnippet: row.error_snippet,
|
|
841
|
+
traceId: row.trace_id ?? "",
|
|
842
|
+
occurredAt: row.occurred_at ?? new Date(0).toISOString(),
|
|
843
|
+
mutationEvidence: row.mutation_evidence !== 0,
|
|
844
|
+
mutationArtifactId: row.mutation_artifact_id ?? null,
|
|
845
|
+
})),
|
|
846
|
+
timestamp: new Date().toISOString(),
|
|
847
|
+
};
|
|
848
|
+
};
|
|
849
|
+
|
|
850
|
+
const formatPercent = (value: number): string => `${(value * 100).toFixed(1)}%`;
|
|
851
|
+
const directionForHigherBetter = (
|
|
852
|
+
baseline: number,
|
|
853
|
+
target: number,
|
|
854
|
+
): "positive" | "negative" | "neutral" =>
|
|
855
|
+
target === baseline ? "neutral" : target > baseline ? "positive" : "negative";
|
|
856
|
+
const directionForLowerBetter = (
|
|
857
|
+
baseline: number,
|
|
858
|
+
target: number,
|
|
859
|
+
): "positive" | "negative" | "neutral" =>
|
|
860
|
+
target === baseline ? "neutral" : target < baseline ? "positive" : "negative";
|
|
861
|
+
|
|
862
|
+
const getVersionExemplars = async (
|
|
863
|
+
db: SqlDb,
|
|
864
|
+
projectId: string,
|
|
865
|
+
version: string,
|
|
866
|
+
): Promise<ActionAggregateExemplar[]> => {
|
|
867
|
+
if (!version || version === "unknown") return [];
|
|
868
|
+
const rows = await db
|
|
869
|
+
.prepare(`
|
|
870
|
+
SELECT
|
|
871
|
+
root.id AS action_id,
|
|
872
|
+
ar.id AS agent_run_id,
|
|
873
|
+
root.trace_id,
|
|
874
|
+
root.tool_call_id,
|
|
875
|
+
er.id AS eval_id,
|
|
876
|
+
ar.agent_name AS label,
|
|
877
|
+
ar.status,
|
|
878
|
+
root.started_at AS occurred_at
|
|
879
|
+
FROM agent_runs ar
|
|
880
|
+
LEFT JOIN actions root ON root.project_id = ar.project_id AND root.id = ar.id
|
|
881
|
+
LEFT JOIN eval_results er ON er.project_id = ar.project_id AND er.action_id = root.id
|
|
882
|
+
WHERE ar.project_id = ? AND ar.agent_version = ?
|
|
883
|
+
ORDER BY
|
|
884
|
+
CASE WHEN ar.status <> 'success' THEN 0 ELSE 1 END,
|
|
885
|
+
root.started_at DESC
|
|
886
|
+
LIMIT ?
|
|
887
|
+
`)
|
|
888
|
+
.bind(projectId, version, EXEMPLAR_LIMIT)
|
|
889
|
+
.all<ExemplarSqlRow>();
|
|
890
|
+
return exemplarsFromRows(rows.results);
|
|
891
|
+
};
|
|
892
|
+
|
|
893
|
+
export const getVersionDiffAggregates = async (
|
|
894
|
+
db: SqlDb,
|
|
895
|
+
projectId: string,
|
|
896
|
+
baselineVersion?: string,
|
|
897
|
+
targetVersion?: string,
|
|
898
|
+
): Promise<VersionComparisonResult> => {
|
|
899
|
+
const rows = await db
|
|
900
|
+
.prepare(`
|
|
901
|
+
SELECT
|
|
902
|
+
ar.agent_version AS version,
|
|
903
|
+
COUNT(DISTINCT ar.id) AS run_count,
|
|
904
|
+
SUM(CASE WHEN ar.status = 'success' THEN 1 ELSE 0 END) AS success_count,
|
|
905
|
+
AVG(ar.total_duration_ms) AS avg_duration_ms,
|
|
906
|
+
AVG(ar.total_cost_usd) AS avg_cost_usd,
|
|
907
|
+
COUNT(er.id) AS eval_count,
|
|
908
|
+
SUM(CASE WHEN er.passed <> 0 THEN 1 ELSE 0 END) AS passed_eval_count,
|
|
909
|
+
AVG(er.score) AS avg_eval_score,
|
|
910
|
+
COUNT(t.id) AS tool_count,
|
|
911
|
+
SUM(CASE WHEN t.error_type IS NOT NULL AND t.error_type <> '' THEN 1 ELSE 0 END) AS tool_error_count
|
|
912
|
+
FROM agent_runs ar
|
|
913
|
+
LEFT JOIN actions a ON a.project_id = ar.project_id AND a.agent_run_id = ar.id
|
|
914
|
+
LEFT JOIN eval_results er ON er.project_id = ar.project_id AND er.action_id = a.id
|
|
915
|
+
LEFT JOIN tool_calls t ON t.project_id = ar.project_id AND t.action_id = a.id
|
|
916
|
+
WHERE ar.project_id = ?
|
|
917
|
+
GROUP BY ar.agent_version
|
|
918
|
+
ORDER BY MAX(COALESCE(a.started_at, '')) DESC, ar.agent_version DESC
|
|
919
|
+
LIMIT 20
|
|
920
|
+
`)
|
|
921
|
+
.bind(projectId)
|
|
922
|
+
.all<VersionStatsRow>();
|
|
923
|
+
|
|
924
|
+
const versions = rows.results.filter((row) => row.version);
|
|
925
|
+
const baseline =
|
|
926
|
+
versions.find((row) => row.version === baselineVersion) ??
|
|
927
|
+
versions[1] ??
|
|
928
|
+
versions[0];
|
|
929
|
+
const target =
|
|
930
|
+
versions.find((row) => row.version === targetVersion) ??
|
|
931
|
+
versions[0] ??
|
|
932
|
+
baseline;
|
|
933
|
+
|
|
934
|
+
const metric = (
|
|
935
|
+
label: string,
|
|
936
|
+
baselineValue: number,
|
|
937
|
+
targetValue: number,
|
|
938
|
+
format: (value: number) => string | number,
|
|
939
|
+
direction: "higher" | "lower",
|
|
940
|
+
): VersionDiffMetric => ({
|
|
941
|
+
label,
|
|
942
|
+
baselineValue: format(baselineValue),
|
|
943
|
+
targetValue: format(targetValue),
|
|
944
|
+
deltaValue:
|
|
945
|
+
typeof format(targetValue - baselineValue) === "number"
|
|
946
|
+
? format(targetValue - baselineValue)
|
|
947
|
+
: `${targetValue - baselineValue >= 0 ? "+" : ""}${format(targetValue - baselineValue)}`,
|
|
948
|
+
deltaDirection:
|
|
949
|
+
direction === "higher"
|
|
950
|
+
? directionForHigherBetter(baselineValue, targetValue)
|
|
951
|
+
: directionForLowerBetter(baselineValue, targetValue),
|
|
952
|
+
});
|
|
953
|
+
|
|
954
|
+
const empty = {
|
|
955
|
+
version: "unknown",
|
|
956
|
+
run_count: 0,
|
|
957
|
+
success_count: 0,
|
|
958
|
+
avg_duration_ms: 0,
|
|
959
|
+
avg_cost_usd: 0,
|
|
960
|
+
eval_count: 0,
|
|
961
|
+
passed_eval_count: 0,
|
|
962
|
+
avg_eval_score: 0,
|
|
963
|
+
tool_count: 0,
|
|
964
|
+
tool_error_count: 0,
|
|
965
|
+
} satisfies VersionStatsRow;
|
|
966
|
+
const b = baseline ?? empty;
|
|
967
|
+
const t = target ?? empty;
|
|
968
|
+
const bRuns = toNumber(b.run_count);
|
|
969
|
+
const tRuns = toNumber(t.run_count);
|
|
970
|
+
const bTools = toNumber(b.tool_count);
|
|
971
|
+
const tTools = toNumber(t.tool_count);
|
|
972
|
+
const bEvals = toNumber(b.eval_count);
|
|
973
|
+
const tEvals = toNumber(t.eval_count);
|
|
974
|
+
const [baselineExemplars, targetExemplars] = await Promise.all([
|
|
975
|
+
getVersionExemplars(db, projectId, b.version),
|
|
976
|
+
getVersionExemplars(db, projectId, t.version),
|
|
977
|
+
]);
|
|
978
|
+
|
|
979
|
+
return {
|
|
980
|
+
projectId,
|
|
981
|
+
baselineVersion: b.version,
|
|
982
|
+
targetVersion: t.version,
|
|
983
|
+
metrics: [
|
|
984
|
+
metric(
|
|
985
|
+
"Success Rate",
|
|
986
|
+
bRuns > 0 ? toNumber(b.success_count) / bRuns : 0,
|
|
987
|
+
tRuns > 0 ? toNumber(t.success_count) / tRuns : 0,
|
|
988
|
+
formatPercent,
|
|
989
|
+
"higher",
|
|
990
|
+
),
|
|
991
|
+
metric(
|
|
992
|
+
"Evaluation Score",
|
|
993
|
+
toNumber(b.avg_eval_score),
|
|
994
|
+
toNumber(t.avg_eval_score),
|
|
995
|
+
(value) => value.toFixed(2),
|
|
996
|
+
"higher",
|
|
997
|
+
),
|
|
998
|
+
metric(
|
|
999
|
+
"Evaluation Pass Rate",
|
|
1000
|
+
bEvals > 0 ? toNumber(b.passed_eval_count) / bEvals : 0,
|
|
1001
|
+
tEvals > 0 ? toNumber(t.passed_eval_count) / tEvals : 0,
|
|
1002
|
+
formatPercent,
|
|
1003
|
+
"higher",
|
|
1004
|
+
),
|
|
1005
|
+
metric(
|
|
1006
|
+
"Average Run Latency",
|
|
1007
|
+
toNumber(b.avg_duration_ms),
|
|
1008
|
+
toNumber(t.avg_duration_ms),
|
|
1009
|
+
(value) => `${Math.round(value)}ms`,
|
|
1010
|
+
"lower",
|
|
1011
|
+
),
|
|
1012
|
+
metric(
|
|
1013
|
+
"Average Run Cost",
|
|
1014
|
+
toNumber(b.avg_cost_usd),
|
|
1015
|
+
toNumber(t.avg_cost_usd),
|
|
1016
|
+
(value) => `$${value.toFixed(4)}`,
|
|
1017
|
+
"lower",
|
|
1018
|
+
),
|
|
1019
|
+
metric(
|
|
1020
|
+
"Tool Error Rate",
|
|
1021
|
+
bTools > 0 ? toNumber(b.tool_error_count) / bTools : 0,
|
|
1022
|
+
tTools > 0 ? toNumber(t.tool_error_count) / tTools : 0,
|
|
1023
|
+
formatPercent,
|
|
1024
|
+
"lower",
|
|
1025
|
+
),
|
|
1026
|
+
],
|
|
1027
|
+
baselineExemplars,
|
|
1028
|
+
targetExemplars,
|
|
1029
|
+
timestamp: new Date().toISOString(),
|
|
1030
|
+
};
|
|
1031
|
+
};
|