@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,2179 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
EvidenceBundle,
|
|
3
|
+
EvidenceBundleDetailLevel,
|
|
4
|
+
EvidenceBundleIntent,
|
|
5
|
+
EvidenceCompaction,
|
|
6
|
+
EvidenceEntityKind,
|
|
7
|
+
EvidenceNextPivot,
|
|
8
|
+
EvidenceReference,
|
|
9
|
+
EvidenceRetrievalRef,
|
|
10
|
+
LogRecord,
|
|
11
|
+
TelemetrySpanDetail,
|
|
12
|
+
TelemetryTraceDetailResponse,
|
|
13
|
+
} from "@obsunified/types";
|
|
14
|
+
import type { CollectorPlugin } from "../framework/collector";
|
|
15
|
+
import type {
|
|
16
|
+
ActionRef,
|
|
17
|
+
EntityManifestExtended,
|
|
18
|
+
ToolCallRef,
|
|
19
|
+
} from "../lib/identity-index";
|
|
20
|
+
import { IdentityIndex } from "../lib/identity-index";
|
|
21
|
+
import { LogsStore } from "../lib/logs-store";
|
|
22
|
+
import { decodePprofBlob, summarizeProfileFrames } from "../lib/parse-pprof";
|
|
23
|
+
import type { SqlDb } from "../lib/sql-db";
|
|
24
|
+
import { getProjectId } from "./_context";
|
|
25
|
+
import { fetchReplayChunks } from "./replay-query-routes";
|
|
26
|
+
|
|
27
|
+
interface EvidenceRefPayload {
|
|
28
|
+
kind:
|
|
29
|
+
| "logs"
|
|
30
|
+
| "trace"
|
|
31
|
+
| "action_graph"
|
|
32
|
+
| "profile"
|
|
33
|
+
| "replay"
|
|
34
|
+
| "ai_call"
|
|
35
|
+
| "tool_call";
|
|
36
|
+
projectId: string;
|
|
37
|
+
traceId?: string;
|
|
38
|
+
anchorKind?: "action" | "agent_run" | "tool_call";
|
|
39
|
+
anchorId?: string;
|
|
40
|
+
profileId?: string;
|
|
41
|
+
profileMode?: "metadata" | "frames";
|
|
42
|
+
sessionId?: string;
|
|
43
|
+
replayMode?: "metadata" | "events";
|
|
44
|
+
chunkOffset?: number;
|
|
45
|
+
callId?: string;
|
|
46
|
+
toolCallId?: string;
|
|
47
|
+
hours?: number;
|
|
48
|
+
limit?: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface ProfileEvidenceRow {
|
|
52
|
+
id: string;
|
|
53
|
+
service_name: string | null;
|
|
54
|
+
profile_type: string;
|
|
55
|
+
start_ts: string;
|
|
56
|
+
end_ts: string;
|
|
57
|
+
duration_ms: number;
|
|
58
|
+
blob_size_bytes: number;
|
|
59
|
+
blob_url?: string;
|
|
60
|
+
sample_count: number | null;
|
|
61
|
+
agent: string | null;
|
|
62
|
+
received_at?: string;
|
|
63
|
+
expires_at?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface ReplayEvidenceRow {
|
|
67
|
+
session_id: string;
|
|
68
|
+
visitor_id?: string;
|
|
69
|
+
first_chunk_at: string;
|
|
70
|
+
last_chunk_at: string;
|
|
71
|
+
chunk_count: number;
|
|
72
|
+
events_count: number;
|
|
73
|
+
storage_bytes?: number;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
interface AICallEvidenceRow {
|
|
77
|
+
call_id: string;
|
|
78
|
+
trace_id: string | null;
|
|
79
|
+
span_id?: string | null;
|
|
80
|
+
service_name?: string | null;
|
|
81
|
+
model_name: string;
|
|
82
|
+
provider: string;
|
|
83
|
+
call_type?: string;
|
|
84
|
+
prompt_tokens?: number | null;
|
|
85
|
+
completion_tokens?: number | null;
|
|
86
|
+
total_cost_usd: number | null;
|
|
87
|
+
latency_ms?: number | null;
|
|
88
|
+
is_error?: number | null;
|
|
89
|
+
error_message?: string | null;
|
|
90
|
+
occurred_at: string;
|
|
91
|
+
received_at?: string;
|
|
92
|
+
expires_at?: string;
|
|
93
|
+
interaction_id?: string | null;
|
|
94
|
+
session_id?: string | null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const BUNDLE_SCHEMA_VERSION = "obs-unified.evidence-bundle.v1" as const;
|
|
98
|
+
const DEFAULT_TARGET_TOKENS = 4000;
|
|
99
|
+
const DEFAULT_HOURS = 24;
|
|
100
|
+
const MAX_LOGS_FOR_BUNDLE = 500;
|
|
101
|
+
const MAX_LOGS_FOR_RETRIEVAL = 1000;
|
|
102
|
+
const DEFAULT_REPLAY_EVENT_CHUNKS = 5;
|
|
103
|
+
const MAX_REPLAY_EVENT_CHUNKS = 25;
|
|
104
|
+
const DEFAULT_PROFILE_FRAME_LIMIT = 50;
|
|
105
|
+
const MAX_PROFILE_FRAME_LIMIT = 200;
|
|
106
|
+
const EVIDENCE_INTENTS = new Set<EvidenceBundleIntent>([
|
|
107
|
+
"debug_failure",
|
|
108
|
+
"explain_latency",
|
|
109
|
+
"explain_cost",
|
|
110
|
+
"inspect_agent_run",
|
|
111
|
+
"inspect_tool_call",
|
|
112
|
+
"find_instrumentation_gap",
|
|
113
|
+
"general",
|
|
114
|
+
]);
|
|
115
|
+
const LOG_SEVERITIES = new Set<LogRecord["severity"]>([
|
|
116
|
+
"DEBUG",
|
|
117
|
+
"INFO",
|
|
118
|
+
"WARN",
|
|
119
|
+
"ERROR",
|
|
120
|
+
"FATAL",
|
|
121
|
+
]);
|
|
122
|
+
|
|
123
|
+
const clampInt = (
|
|
124
|
+
value: unknown,
|
|
125
|
+
min: number,
|
|
126
|
+
max: number,
|
|
127
|
+
fallback: number,
|
|
128
|
+
): number => {
|
|
129
|
+
const parsed =
|
|
130
|
+
typeof value === "number"
|
|
131
|
+
? value
|
|
132
|
+
: Number.parseInt(typeof value === "string" ? value : "", 10);
|
|
133
|
+
if (!Number.isFinite(parsed)) return fallback;
|
|
134
|
+
return Math.max(min, Math.min(max, parsed));
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const estimateTokens = (value: unknown): number =>
|
|
138
|
+
Math.ceil(JSON.stringify(value).length / 4);
|
|
139
|
+
|
|
140
|
+
const traceRoute = (traceId: string) =>
|
|
141
|
+
`#/traces/${encodeURIComponent(traceId)}`;
|
|
142
|
+
const spanRoute = (traceId: string, spanId: string) =>
|
|
143
|
+
`${traceRoute(traceId)}#span=${encodeURIComponent(spanId)}`;
|
|
144
|
+
const logRoute = (logId: string) => `#/logs?id=${encodeURIComponent(logId)}`;
|
|
145
|
+
const actionRoute = (actionId: string) =>
|
|
146
|
+
`#/actions/${encodeURIComponent(actionId)}`;
|
|
147
|
+
const agentRunRoute = (agentRunId: string) =>
|
|
148
|
+
`#/agent-runs/${encodeURIComponent(agentRunId)}`;
|
|
149
|
+
const toolCallRoute = (toolCallId: string) =>
|
|
150
|
+
`#/tool-calls/${encodeURIComponent(toolCallId)}`;
|
|
151
|
+
const replayRoute = (sessionId: string) =>
|
|
152
|
+
`#/replay?session=${encodeURIComponent(sessionId)}`;
|
|
153
|
+
const profileRoute = (profileId: string, traceId?: string) =>
|
|
154
|
+
`#/profiles/${encodeURIComponent(profileId)}${
|
|
155
|
+
traceId ? `?trace_id=${encodeURIComponent(traceId)}` : ""
|
|
156
|
+
}`;
|
|
157
|
+
const aiCallRoute = (callId: string) =>
|
|
158
|
+
`#/ai/calls/${encodeURIComponent(callId)}`;
|
|
159
|
+
|
|
160
|
+
const evidenceRef = (
|
|
161
|
+
evidenceId: string,
|
|
162
|
+
entityKind: EvidenceEntityKind,
|
|
163
|
+
entityId: string,
|
|
164
|
+
route: string,
|
|
165
|
+
source: string,
|
|
166
|
+
confidence: number,
|
|
167
|
+
reason: string,
|
|
168
|
+
citations: EvidenceReference["citations"] = [],
|
|
169
|
+
suggestedNextPivots: EvidenceNextPivot[] = [],
|
|
170
|
+
): EvidenceReference => ({
|
|
171
|
+
evidenceId,
|
|
172
|
+
entityKind,
|
|
173
|
+
entityId,
|
|
174
|
+
route,
|
|
175
|
+
source,
|
|
176
|
+
confidence,
|
|
177
|
+
reason,
|
|
178
|
+
citations,
|
|
179
|
+
suggestedNextPivots,
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const encodeRef = (payload: EvidenceRefPayload): string => {
|
|
183
|
+
const json = JSON.stringify(payload);
|
|
184
|
+
const bytes = new TextEncoder().encode(json);
|
|
185
|
+
let binary = "";
|
|
186
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
187
|
+
return `eref_${globalThis
|
|
188
|
+
.btoa(binary)
|
|
189
|
+
.replace(/\+/g, "-")
|
|
190
|
+
.replace(/\//g, "_")
|
|
191
|
+
.replace(/=+$/, "")}`;
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const decodeRef = (refId: string): EvidenceRefPayload | null => {
|
|
195
|
+
if (!refId.startsWith("eref_")) return null;
|
|
196
|
+
try {
|
|
197
|
+
const encoded = refId
|
|
198
|
+
.slice("eref_".length)
|
|
199
|
+
.replace(/-/g, "+")
|
|
200
|
+
.replace(/_/g, "/");
|
|
201
|
+
const padded = encoded.padEnd(
|
|
202
|
+
encoded.length + ((4 - (encoded.length % 4)) % 4),
|
|
203
|
+
"=",
|
|
204
|
+
);
|
|
205
|
+
const binary = globalThis.atob(padded);
|
|
206
|
+
const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
|
|
207
|
+
return JSON.parse(new TextDecoder().decode(bytes)) as EvidenceRefPayload;
|
|
208
|
+
} catch {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const isProjectScopedRefPayload = (
|
|
214
|
+
payload: EvidenceRefPayload | null,
|
|
215
|
+
): payload is EvidenceRefPayload =>
|
|
216
|
+
Boolean(
|
|
217
|
+
payload &&
|
|
218
|
+
typeof payload.projectId === "string" &&
|
|
219
|
+
payload.projectId.length > 0,
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
const hasTraceId = (
|
|
223
|
+
payload: EvidenceRefPayload,
|
|
224
|
+
): payload is EvidenceRefPayload & { traceId: string } =>
|
|
225
|
+
typeof payload.traceId === "string" && payload.traceId.length > 0;
|
|
226
|
+
|
|
227
|
+
const expansionId = (): string =>
|
|
228
|
+
`exp_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
|
229
|
+
|
|
230
|
+
const jsonOrNull = (value: unknown): string | null =>
|
|
231
|
+
value === undefined ? null : JSON.stringify(value);
|
|
232
|
+
|
|
233
|
+
const materializeRetrievalRefs = async (
|
|
234
|
+
db: SqlDb,
|
|
235
|
+
projectId: string,
|
|
236
|
+
refs: EvidenceRetrievalRef[],
|
|
237
|
+
) => {
|
|
238
|
+
if (refs.length === 0) return;
|
|
239
|
+
const stmt = db.prepare(
|
|
240
|
+
`INSERT INTO evidence_retrieval_refs (
|
|
241
|
+
ref_id, project_id, kind, anchor_kind, anchor_id, source, query_json,
|
|
242
|
+
compacted_from_json, returned_json, last_seen_at, expires_at
|
|
243
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, ?)
|
|
244
|
+
ON CONFLICT(ref_id) DO UPDATE SET
|
|
245
|
+
last_seen_at = CURRENT_TIMESTAMP,
|
|
246
|
+
kind = excluded.kind,
|
|
247
|
+
anchor_kind = excluded.anchor_kind,
|
|
248
|
+
anchor_id = excluded.anchor_id,
|
|
249
|
+
source = excluded.source,
|
|
250
|
+
query_json = excluded.query_json,
|
|
251
|
+
compacted_from_json = excluded.compacted_from_json,
|
|
252
|
+
returned_json = excluded.returned_json,
|
|
253
|
+
expires_at = excluded.expires_at`,
|
|
254
|
+
);
|
|
255
|
+
try {
|
|
256
|
+
await db.batch(
|
|
257
|
+
refs.map((ref) =>
|
|
258
|
+
stmt.bind(
|
|
259
|
+
ref.refId,
|
|
260
|
+
projectId,
|
|
261
|
+
ref.kind,
|
|
262
|
+
ref.anchor.entityKind,
|
|
263
|
+
ref.anchor.entityId,
|
|
264
|
+
ref.source,
|
|
265
|
+
jsonOrNull(ref.query),
|
|
266
|
+
jsonOrNull(ref.compactedFrom),
|
|
267
|
+
jsonOrNull(ref.returned),
|
|
268
|
+
ref.expiresAt ?? null,
|
|
269
|
+
),
|
|
270
|
+
),
|
|
271
|
+
);
|
|
272
|
+
} catch {
|
|
273
|
+
// Best-effort: encoded refs remain valid even before the migration lands.
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
const recordRefExpansion = async (
|
|
278
|
+
db: SqlDb,
|
|
279
|
+
args: {
|
|
280
|
+
projectId: string;
|
|
281
|
+
refId: string;
|
|
282
|
+
kind: EvidenceRefPayload["kind"];
|
|
283
|
+
operation: "retrieve" | "search";
|
|
284
|
+
resultStatus: "ok" | "error";
|
|
285
|
+
limit?: number;
|
|
286
|
+
query?: string;
|
|
287
|
+
},
|
|
288
|
+
) => {
|
|
289
|
+
try {
|
|
290
|
+
const source = await db
|
|
291
|
+
.prepare(
|
|
292
|
+
`SELECT source FROM evidence_retrieval_refs
|
|
293
|
+
WHERE project_id = ? AND ref_id = ?
|
|
294
|
+
LIMIT 1`,
|
|
295
|
+
)
|
|
296
|
+
.bind(args.projectId, args.refId)
|
|
297
|
+
.first<{ source: string }>();
|
|
298
|
+
await db
|
|
299
|
+
.prepare(
|
|
300
|
+
`INSERT INTO evidence_ref_expansions (
|
|
301
|
+
id, ref_id, project_id, kind, source, operation, result_status,
|
|
302
|
+
limit_value, query_text, expanded_at
|
|
303
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`,
|
|
304
|
+
)
|
|
305
|
+
.bind(
|
|
306
|
+
expansionId(),
|
|
307
|
+
args.refId,
|
|
308
|
+
args.projectId,
|
|
309
|
+
args.kind,
|
|
310
|
+
source?.source ?? null,
|
|
311
|
+
args.operation,
|
|
312
|
+
args.resultStatus,
|
|
313
|
+
args.limit ?? null,
|
|
314
|
+
args.query ?? null,
|
|
315
|
+
)
|
|
316
|
+
.run();
|
|
317
|
+
} catch {
|
|
318
|
+
// Best-effort telemetry must not break evidence expansion.
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
const materializeBundle = async (
|
|
323
|
+
db: SqlDb,
|
|
324
|
+
projectId: string,
|
|
325
|
+
bundle: EvidenceBundle,
|
|
326
|
+
): Promise<EvidenceBundle> => {
|
|
327
|
+
await materializeRetrievalRefs(db, projectId, bundle.retrievalRefs);
|
|
328
|
+
return bundle;
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
const spanLabel = (span: TelemetrySpanDetail): string =>
|
|
332
|
+
`${span.serviceName || "unknown"}:${span.spanName}`;
|
|
333
|
+
|
|
334
|
+
const failedSpans = (spans: TelemetrySpanDetail[]): TelemetrySpanDetail[] =>
|
|
335
|
+
spans.filter((span) => span.statusCode > 1 || Boolean(span.statusMessage));
|
|
336
|
+
|
|
337
|
+
const criticalPath = (spans: TelemetrySpanDetail[]): TelemetrySpanDetail[] =>
|
|
338
|
+
[...spans].sort((a, b) => b.durationMs - a.durationMs).slice(0, 5);
|
|
339
|
+
|
|
340
|
+
const normalizeLogSignature = (message: string): string =>
|
|
341
|
+
message
|
|
342
|
+
.toLowerCase()
|
|
343
|
+
.replace(/[0-9a-f]{16,}/g, "<hex>")
|
|
344
|
+
.replace(/\b\d+\b/g, "<num>")
|
|
345
|
+
.replace(/\s+/g, " ")
|
|
346
|
+
.trim();
|
|
347
|
+
|
|
348
|
+
const clusterLogs = (
|
|
349
|
+
traceId: string,
|
|
350
|
+
logs: LogRecord[],
|
|
351
|
+
logsRefId: string,
|
|
352
|
+
): { compactions: EvidenceCompaction[]; exemplars: LogRecord[] } => {
|
|
353
|
+
const bySignature = new Map<string, LogRecord[]>();
|
|
354
|
+
for (const log of logs) {
|
|
355
|
+
const signature = normalizeLogSignature(log.message);
|
|
356
|
+
const group = bySignature.get(signature) ?? [];
|
|
357
|
+
group.push(log);
|
|
358
|
+
bySignature.set(signature, group);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const exemplars: LogRecord[] = [];
|
|
362
|
+
const compactions: EvidenceCompaction[] = [];
|
|
363
|
+
for (const [signature, group] of bySignature) {
|
|
364
|
+
const severitySorted = [...group].sort((a, b) => {
|
|
365
|
+
const severityOrder = { FATAL: 5, ERROR: 4, WARN: 3, INFO: 2, DEBUG: 1 };
|
|
366
|
+
return severityOrder[b.severity] - severityOrder[a.severity];
|
|
367
|
+
});
|
|
368
|
+
const selected = severitySorted.slice(
|
|
369
|
+
0,
|
|
370
|
+
Math.min(3, severitySorted.length),
|
|
371
|
+
);
|
|
372
|
+
exemplars.push(...selected);
|
|
373
|
+
if (group.length > selected.length) {
|
|
374
|
+
compactions.push({
|
|
375
|
+
compactionId: `cmp_logs_${traceId}_${compactions.length + 1}`,
|
|
376
|
+
kind: "logs",
|
|
377
|
+
strategy: "signature_cluster",
|
|
378
|
+
inputCount: group.length,
|
|
379
|
+
outputCount: selected.length,
|
|
380
|
+
reason: `Collapsed ${group.length} logs matching signature "${signature}" into ${selected.length} exemplar(s).`,
|
|
381
|
+
exemplarEntityIds: selected.map((log) => log.logId),
|
|
382
|
+
retrievalRefIds: [logsRefId],
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return { compactions, exemplars: exemplars.slice(0, 20) };
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
const profilesForTrace = async (
|
|
391
|
+
db: SqlDb,
|
|
392
|
+
projectId: string,
|
|
393
|
+
traceId: string,
|
|
394
|
+
): Promise<ProfileEvidenceRow[]> => {
|
|
395
|
+
const rows = await db
|
|
396
|
+
.prepare(
|
|
397
|
+
`SELECT b.id, b.service_name, b.profile_type, b.start_ts, b.end_ts,
|
|
398
|
+
b.duration_ms, b.blob_size_bytes, b.sample_count, b.agent,
|
|
399
|
+
b.received_at, b.expires_at
|
|
400
|
+
FROM profile_trace_index i
|
|
401
|
+
JOIN profile_blobs b ON b.id = i.profile_id
|
|
402
|
+
WHERE i.project_id = ? AND i.trace_id = ?
|
|
403
|
+
ORDER BY b.end_ts DESC
|
|
404
|
+
LIMIT 10`,
|
|
405
|
+
)
|
|
406
|
+
.bind(projectId, traceId)
|
|
407
|
+
.all<ProfileEvidenceRow>();
|
|
408
|
+
return rows.results;
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
const profileTraceIds = async (
|
|
412
|
+
db: SqlDb,
|
|
413
|
+
projectId: string,
|
|
414
|
+
profileId: string,
|
|
415
|
+
): Promise<string[]> => {
|
|
416
|
+
const rows = await db
|
|
417
|
+
.prepare(
|
|
418
|
+
`SELECT trace_id FROM profile_trace_index
|
|
419
|
+
WHERE profile_id = ? AND project_id = ?
|
|
420
|
+
LIMIT 1000`,
|
|
421
|
+
)
|
|
422
|
+
.bind(profileId, projectId)
|
|
423
|
+
.all<{ trace_id: string }>();
|
|
424
|
+
return rows.results.map((row) => row.trace_id);
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
const replayBySession = async (
|
|
428
|
+
db: SqlDb,
|
|
429
|
+
projectId: string,
|
|
430
|
+
sessionId: string,
|
|
431
|
+
): Promise<ReplayEvidenceRow | null> =>
|
|
432
|
+
db
|
|
433
|
+
.prepare(
|
|
434
|
+
`SELECT session_id, visitor_id, first_chunk_at, last_chunk_at,
|
|
435
|
+
chunk_count, events_count, storage_bytes
|
|
436
|
+
FROM session_replay_metadata
|
|
437
|
+
WHERE project_id = ? AND session_id = ?
|
|
438
|
+
LIMIT 1`,
|
|
439
|
+
)
|
|
440
|
+
.bind(projectId, sessionId)
|
|
441
|
+
.first<ReplayEvidenceRow>();
|
|
442
|
+
|
|
443
|
+
const replayEventWindow = async (
|
|
444
|
+
bucket: R2Bucket,
|
|
445
|
+
projectId: string,
|
|
446
|
+
sessionId: string,
|
|
447
|
+
chunkOffset: number,
|
|
448
|
+
chunkLimit: number,
|
|
449
|
+
): Promise<{
|
|
450
|
+
events: Record<string, unknown>[];
|
|
451
|
+
chunks: {
|
|
452
|
+
offset: number;
|
|
453
|
+
limit: number;
|
|
454
|
+
returned: number;
|
|
455
|
+
total: number;
|
|
456
|
+
nextChunkOffset: number | null;
|
|
457
|
+
};
|
|
458
|
+
}> => {
|
|
459
|
+
const prefix = `replays/${projectId}/${sessionId}/`;
|
|
460
|
+
const objects: Array<{ key: string }> = [];
|
|
461
|
+
let cursor: string | undefined;
|
|
462
|
+
do {
|
|
463
|
+
const page = await bucket.list({ prefix, cursor });
|
|
464
|
+
objects.push(...page.objects);
|
|
465
|
+
cursor = page.truncated ? page.cursor : undefined;
|
|
466
|
+
} while (cursor);
|
|
467
|
+
|
|
468
|
+
objects.sort((a, b) => a.key.localeCompare(b.key));
|
|
469
|
+
const offset = Math.max(0, chunkOffset);
|
|
470
|
+
const limit = Math.max(1, Math.min(MAX_REPLAY_EVENT_CHUNKS, chunkLimit));
|
|
471
|
+
const selectedObjects = objects.slice(offset, offset + limit);
|
|
472
|
+
return {
|
|
473
|
+
events: await fetchReplayChunks(bucket, selectedObjects),
|
|
474
|
+
chunks: {
|
|
475
|
+
offset,
|
|
476
|
+
limit,
|
|
477
|
+
returned: selectedObjects.length,
|
|
478
|
+
total: objects.length,
|
|
479
|
+
nextChunkOffset: offset + limit < objects.length ? offset + limit : null,
|
|
480
|
+
},
|
|
481
|
+
};
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
const aiCallById = async (
|
|
485
|
+
db: SqlDb,
|
|
486
|
+
projectId: string,
|
|
487
|
+
callId: string,
|
|
488
|
+
): Promise<AICallEvidenceRow | null> =>
|
|
489
|
+
db
|
|
490
|
+
.prepare(
|
|
491
|
+
`SELECT call_id, trace_id, span_id, service_name, model_name, provider,
|
|
492
|
+
call_type, prompt_tokens, completion_tokens, total_cost_usd,
|
|
493
|
+
latency_ms, is_error, error_message, occurred_at, received_at,
|
|
494
|
+
expires_at, interaction_id, session_id
|
|
495
|
+
FROM ai_calls
|
|
496
|
+
WHERE project_id = ? AND call_id = ?
|
|
497
|
+
LIMIT 1`,
|
|
498
|
+
)
|
|
499
|
+
.bind(projectId, callId)
|
|
500
|
+
.first<AICallEvidenceRow>();
|
|
501
|
+
|
|
502
|
+
const toolCallById = async (
|
|
503
|
+
db: SqlDb,
|
|
504
|
+
projectId: string,
|
|
505
|
+
toolCallId: string,
|
|
506
|
+
): Promise<ToolCallRef | null> => {
|
|
507
|
+
const row = await db
|
|
508
|
+
.prepare(
|
|
509
|
+
`SELECT * FROM tool_calls
|
|
510
|
+
WHERE project_id = ? AND id = ?
|
|
511
|
+
LIMIT 1`,
|
|
512
|
+
)
|
|
513
|
+
.bind(projectId, toolCallId)
|
|
514
|
+
.first<{
|
|
515
|
+
id: string;
|
|
516
|
+
action_id: string;
|
|
517
|
+
project_id: string;
|
|
518
|
+
tool_name: string;
|
|
519
|
+
args_hash: string;
|
|
520
|
+
result_hash: string;
|
|
521
|
+
error_type: string | null;
|
|
522
|
+
side_effect: number;
|
|
523
|
+
approval_state: string | null;
|
|
524
|
+
args_redacted: string | null;
|
|
525
|
+
result_redacted: string | null;
|
|
526
|
+
mcp_audit_json: string | null;
|
|
527
|
+
mutation_before_json: string | null;
|
|
528
|
+
mutation_after_json: string | null;
|
|
529
|
+
mutation_diff_json: string | null;
|
|
530
|
+
mutation_artifact_id: string | null;
|
|
531
|
+
}>();
|
|
532
|
+
if (!row) return null;
|
|
533
|
+
return {
|
|
534
|
+
id: row.id,
|
|
535
|
+
actionId: row.action_id,
|
|
536
|
+
projectId: row.project_id,
|
|
537
|
+
toolName: row.tool_name,
|
|
538
|
+
argsHash: row.args_hash,
|
|
539
|
+
resultHash: row.result_hash,
|
|
540
|
+
errorType: row.error_type,
|
|
541
|
+
sideEffect: row.side_effect,
|
|
542
|
+
approvalState: row.approval_state,
|
|
543
|
+
argsRedacted: row.args_redacted,
|
|
544
|
+
resultRedacted: row.result_redacted,
|
|
545
|
+
mcpAuditJson: row.mcp_audit_json,
|
|
546
|
+
mutationBeforeJson: row.mutation_before_json,
|
|
547
|
+
mutationAfterJson: row.mutation_after_json,
|
|
548
|
+
mutationDiffJson: row.mutation_diff_json,
|
|
549
|
+
mutationArtifactId: row.mutation_artifact_id,
|
|
550
|
+
};
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
const traceEvidenceReferences = (
|
|
554
|
+
detail: TelemetryTraceDetailResponse,
|
|
555
|
+
logExemplars: LogRecord[],
|
|
556
|
+
profiles: ProfileEvidenceRow[],
|
|
557
|
+
): EvidenceReference[] => {
|
|
558
|
+
const traceId = detail.trace.traceId;
|
|
559
|
+
const refs: EvidenceReference[] = [
|
|
560
|
+
evidenceRef(
|
|
561
|
+
`trace:${traceId}`,
|
|
562
|
+
"trace",
|
|
563
|
+
traceId,
|
|
564
|
+
traceRoute(traceId),
|
|
565
|
+
"evidence.bundle.trace",
|
|
566
|
+
1,
|
|
567
|
+
`Trace ${traceId} contains ${detail.spans.length} span(s), with ${detail.trace.errorSpanCount} error span(s).`,
|
|
568
|
+
[],
|
|
569
|
+
[
|
|
570
|
+
{
|
|
571
|
+
label: "Open trace",
|
|
572
|
+
entityKind: "trace",
|
|
573
|
+
entityId: traceId,
|
|
574
|
+
route: traceRoute(traceId),
|
|
575
|
+
reason: "Inspect the full span tree.",
|
|
576
|
+
},
|
|
577
|
+
],
|
|
578
|
+
),
|
|
579
|
+
];
|
|
580
|
+
|
|
581
|
+
for (const span of failedSpans(detail.spans).slice(0, 5)) {
|
|
582
|
+
refs.push(
|
|
583
|
+
evidenceRef(
|
|
584
|
+
`span:${traceId}:${span.spanId}`,
|
|
585
|
+
"span",
|
|
586
|
+
`${traceId}:${span.spanId}`,
|
|
587
|
+
spanRoute(traceId, span.spanId),
|
|
588
|
+
"evidence.bundle.failed_spans",
|
|
589
|
+
0.95,
|
|
590
|
+
`Span ${spanLabel(span)} is marked as failed or has a status message.`,
|
|
591
|
+
[
|
|
592
|
+
{
|
|
593
|
+
label: `trace ${traceId}`,
|
|
594
|
+
entityKind: "trace",
|
|
595
|
+
entityId: traceId,
|
|
596
|
+
route: traceRoute(traceId),
|
|
597
|
+
},
|
|
598
|
+
],
|
|
599
|
+
),
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
for (const log of logExemplars.slice(0, 5)) {
|
|
604
|
+
refs.push(
|
|
605
|
+
evidenceRef(
|
|
606
|
+
`log:${log.logId}`,
|
|
607
|
+
"log",
|
|
608
|
+
log.logId,
|
|
609
|
+
logRoute(log.logId),
|
|
610
|
+
"evidence.bundle.log_exemplars",
|
|
611
|
+
log.severity === "ERROR" || log.severity === "FATAL" ? 0.9 : 0.7,
|
|
612
|
+
`${log.severity} log exemplar: ${log.message}`,
|
|
613
|
+
[
|
|
614
|
+
{
|
|
615
|
+
label: `trace ${traceId}`,
|
|
616
|
+
entityKind: "trace",
|
|
617
|
+
entityId: traceId,
|
|
618
|
+
route: traceRoute(traceId),
|
|
619
|
+
},
|
|
620
|
+
],
|
|
621
|
+
),
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
for (const profile of profiles.slice(0, 5)) {
|
|
626
|
+
refs.push(
|
|
627
|
+
evidenceRef(
|
|
628
|
+
`profile:${profile.id}`,
|
|
629
|
+
"profile",
|
|
630
|
+
profile.id,
|
|
631
|
+
profileRoute(profile.id, traceId),
|
|
632
|
+
"evidence.bundle.connected_profiles",
|
|
633
|
+
0.85,
|
|
634
|
+
`${profile.profile_type} profile covers trace ${traceId} for ${profile.duration_ms}ms.`,
|
|
635
|
+
[
|
|
636
|
+
{
|
|
637
|
+
label: `trace ${traceId}`,
|
|
638
|
+
entityKind: "trace",
|
|
639
|
+
entityId: traceId,
|
|
640
|
+
route: traceRoute(traceId),
|
|
641
|
+
},
|
|
642
|
+
],
|
|
643
|
+
),
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
return refs;
|
|
648
|
+
};
|
|
649
|
+
|
|
650
|
+
const buildTraceBundle = async (
|
|
651
|
+
projectId: string,
|
|
652
|
+
detail: TelemetryTraceDetailResponse,
|
|
653
|
+
logs: LogRecord[],
|
|
654
|
+
profiles: ProfileEvidenceRow[],
|
|
655
|
+
intent: EvidenceBundleIntent,
|
|
656
|
+
targetTokens: number,
|
|
657
|
+
detailLevel: EvidenceBundleDetailLevel,
|
|
658
|
+
hours: number,
|
|
659
|
+
): Promise<EvidenceBundle> => {
|
|
660
|
+
const traceId = detail.trace.traceId;
|
|
661
|
+
const traceRefId = encodeRef({ kind: "trace", projectId, traceId });
|
|
662
|
+
const logsRefId = encodeRef({
|
|
663
|
+
kind: "logs",
|
|
664
|
+
projectId,
|
|
665
|
+
traceId,
|
|
666
|
+
hours,
|
|
667
|
+
limit: MAX_LOGS_FOR_RETRIEVAL,
|
|
668
|
+
});
|
|
669
|
+
const { compactions, exemplars } = clusterLogs(traceId, logs, logsRefId);
|
|
670
|
+
const failed = failedSpans(detail.spans);
|
|
671
|
+
const critical = criticalPath(detail.spans);
|
|
672
|
+
const evidenceReferences = traceEvidenceReferences(
|
|
673
|
+
detail,
|
|
674
|
+
exemplars,
|
|
675
|
+
profiles,
|
|
676
|
+
);
|
|
677
|
+
const retrievalRefs: EvidenceRetrievalRef[] = [
|
|
678
|
+
{
|
|
679
|
+
refId: traceRefId,
|
|
680
|
+
kind: "trace",
|
|
681
|
+
anchor: { entityKind: "trace", entityId: traceId },
|
|
682
|
+
source: "evidence.bundle.trace_detail",
|
|
683
|
+
query: { traceId },
|
|
684
|
+
compactedFrom: {
|
|
685
|
+
recordCount: detail.spans.length,
|
|
686
|
+
tokenEstimate: estimateTokens(detail),
|
|
687
|
+
},
|
|
688
|
+
returned: {
|
|
689
|
+
recordCount: critical.length,
|
|
690
|
+
tokenEstimate: estimateTokens(critical),
|
|
691
|
+
},
|
|
692
|
+
},
|
|
693
|
+
{
|
|
694
|
+
refId: logsRefId,
|
|
695
|
+
kind: "logs",
|
|
696
|
+
anchor: { entityKind: "trace", entityId: traceId },
|
|
697
|
+
source: "evidence.bundle.correlated_logs",
|
|
698
|
+
query: { traceId, hours, limit: MAX_LOGS_FOR_RETRIEVAL },
|
|
699
|
+
compactedFrom: {
|
|
700
|
+
recordCount: logs.length,
|
|
701
|
+
tokenEstimate: estimateTokens(logs),
|
|
702
|
+
},
|
|
703
|
+
returned: {
|
|
704
|
+
recordCount: exemplars.length,
|
|
705
|
+
tokenEstimate: estimateTokens(exemplars),
|
|
706
|
+
},
|
|
707
|
+
},
|
|
708
|
+
...profiles.map(
|
|
709
|
+
(profile): EvidenceRetrievalRef => ({
|
|
710
|
+
refId: encodeRef({
|
|
711
|
+
kind: "profile",
|
|
712
|
+
projectId,
|
|
713
|
+
traceId,
|
|
714
|
+
profileId: profile.id,
|
|
715
|
+
profileMode: "metadata",
|
|
716
|
+
}),
|
|
717
|
+
kind: "profile",
|
|
718
|
+
anchor: { entityKind: "trace", entityId: traceId },
|
|
719
|
+
source: "evidence.bundle.connected_profiles",
|
|
720
|
+
query: { profileId: profile.id, traceId },
|
|
721
|
+
compactedFrom: {
|
|
722
|
+
recordCount: 1,
|
|
723
|
+
byteEstimate: profile.blob_size_bytes,
|
|
724
|
+
},
|
|
725
|
+
returned: {
|
|
726
|
+
recordCount: 1,
|
|
727
|
+
tokenEstimate: estimateTokens(profile),
|
|
728
|
+
},
|
|
729
|
+
}),
|
|
730
|
+
),
|
|
731
|
+
...profiles.map(
|
|
732
|
+
(profile): EvidenceRetrievalRef => ({
|
|
733
|
+
refId: encodeRef({
|
|
734
|
+
kind: "profile",
|
|
735
|
+
projectId,
|
|
736
|
+
traceId,
|
|
737
|
+
profileId: profile.id,
|
|
738
|
+
profileMode: "frames",
|
|
739
|
+
limit: DEFAULT_PROFILE_FRAME_LIMIT,
|
|
740
|
+
}),
|
|
741
|
+
kind: "profile",
|
|
742
|
+
anchor: { entityKind: "trace", entityId: traceId },
|
|
743
|
+
source: "evidence.bundle.profile_frames",
|
|
744
|
+
query: {
|
|
745
|
+
profileId: profile.id,
|
|
746
|
+
traceId,
|
|
747
|
+
frameLimit: DEFAULT_PROFILE_FRAME_LIMIT,
|
|
748
|
+
},
|
|
749
|
+
compactedFrom: {
|
|
750
|
+
recordCount: profile.sample_count ?? undefined,
|
|
751
|
+
byteEstimate: profile.blob_size_bytes,
|
|
752
|
+
},
|
|
753
|
+
}),
|
|
754
|
+
),
|
|
755
|
+
];
|
|
756
|
+
|
|
757
|
+
const suggestedNextPivots: EvidenceNextPivot[] = [
|
|
758
|
+
{
|
|
759
|
+
label: "Inspect full trace",
|
|
760
|
+
entityKind: "trace",
|
|
761
|
+
entityId: traceId,
|
|
762
|
+
route: traceRoute(traceId),
|
|
763
|
+
reason:
|
|
764
|
+
"Retrieve the complete span tree if the critical path is insufficient.",
|
|
765
|
+
},
|
|
766
|
+
];
|
|
767
|
+
|
|
768
|
+
if (failed[0]) {
|
|
769
|
+
suggestedNextPivots.push({
|
|
770
|
+
label: "Inspect failed span",
|
|
771
|
+
entityKind: "span",
|
|
772
|
+
entityId: `${traceId}:${failed[0].spanId}`,
|
|
773
|
+
route: spanRoute(traceId, failed[0].spanId),
|
|
774
|
+
reason: "Start with the failed span before expanding unrelated spans.",
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
if (profiles[0]) {
|
|
779
|
+
suggestedNextPivots.push({
|
|
780
|
+
label: "Inspect connected profile",
|
|
781
|
+
entityKind: "profile",
|
|
782
|
+
entityId: profiles[0].id,
|
|
783
|
+
route: profileRoute(profiles[0].id, traceId),
|
|
784
|
+
reason:
|
|
785
|
+
"Use the sampled profile to inspect code-level CPU or runtime hotspots.",
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
const derivedSummaries = [
|
|
790
|
+
{
|
|
791
|
+
title: "Trace critical path",
|
|
792
|
+
reason:
|
|
793
|
+
critical.length > 0
|
|
794
|
+
? `Top span by duration is ${spanLabel(critical[0])} at ${critical[0].durationMs}ms.`
|
|
795
|
+
: "Trace has no spans available for critical path computation.",
|
|
796
|
+
confidence: critical.length > 0 ? 0.9 : 0.2,
|
|
797
|
+
evidenceIds: critical.map((span) => `span:${traceId}:${span.spanId}`),
|
|
798
|
+
retrievalRefIds: [traceRefId],
|
|
799
|
+
},
|
|
800
|
+
{
|
|
801
|
+
title: "Correlated logs",
|
|
802
|
+
reason:
|
|
803
|
+
logs.length > exemplars.length
|
|
804
|
+
? `${logs.length} correlated log(s) were reduced to ${exemplars.length} exemplar(s).`
|
|
805
|
+
: `${logs.length} correlated log(s) were included without lossy compaction.`,
|
|
806
|
+
confidence: 0.85,
|
|
807
|
+
evidenceIds: exemplars.map((log) => `log:${log.logId}`),
|
|
808
|
+
retrievalRefIds: [logsRefId],
|
|
809
|
+
},
|
|
810
|
+
{
|
|
811
|
+
title: "Connected profiles",
|
|
812
|
+
reason:
|
|
813
|
+
profiles.length > 0
|
|
814
|
+
? `${profiles.length} profile(s) are indexed to this trace.`
|
|
815
|
+
: "No profile metadata is indexed to this trace.",
|
|
816
|
+
confidence: profiles.length > 0 ? 0.85 : 0.5,
|
|
817
|
+
evidenceIds: profiles.map((profile) => `profile:${profile.id}`),
|
|
818
|
+
retrievalRefIds: retrievalRefs
|
|
819
|
+
.filter((ref) => ref.kind === "profile")
|
|
820
|
+
.map((ref) => ref.refId),
|
|
821
|
+
},
|
|
822
|
+
];
|
|
823
|
+
|
|
824
|
+
const findings =
|
|
825
|
+
failed.length > 0
|
|
826
|
+
? [
|
|
827
|
+
{
|
|
828
|
+
title: "Failed span present",
|
|
829
|
+
reason: `Trace contains ${failed.length} failed or status-message span(s).`,
|
|
830
|
+
confidence: 0.9,
|
|
831
|
+
evidenceIds: failed
|
|
832
|
+
.slice(0, 5)
|
|
833
|
+
.map((span) => `span:${traceId}:${span.spanId}`),
|
|
834
|
+
retrievalRefIds: [traceRefId],
|
|
835
|
+
},
|
|
836
|
+
]
|
|
837
|
+
: [];
|
|
838
|
+
|
|
839
|
+
const bundle: EvidenceBundle = {
|
|
840
|
+
schemaVersion: BUNDLE_SCHEMA_VERSION,
|
|
841
|
+
intent,
|
|
842
|
+
anchor: { entityKind: "trace", entityId: traceId },
|
|
843
|
+
budget: {
|
|
844
|
+
targetTokens,
|
|
845
|
+
detailLevel,
|
|
846
|
+
},
|
|
847
|
+
summary:
|
|
848
|
+
failed.length > 0
|
|
849
|
+
? `Trace ${traceId} has ${failed.length} failed/status span(s), ${detail.spans.length} total span(s), ${logs.length} correlated log(s), and ${profiles.length} connected profile(s).`
|
|
850
|
+
: `Trace ${traceId} has ${detail.spans.length} span(s), ${logs.length} correlated log(s), and ${profiles.length} connected profile(s); no failed spans were detected.`,
|
|
851
|
+
derivedSummaries,
|
|
852
|
+
findings,
|
|
853
|
+
compactions,
|
|
854
|
+
evidenceReferences,
|
|
855
|
+
retrievalRefs,
|
|
856
|
+
suggestedNextPivots,
|
|
857
|
+
};
|
|
858
|
+
bundle.budget = {
|
|
859
|
+
targetTokens,
|
|
860
|
+
detailLevel,
|
|
861
|
+
estimatedTokens: estimateTokens(bundle),
|
|
862
|
+
};
|
|
863
|
+
return bundle;
|
|
864
|
+
};
|
|
865
|
+
|
|
866
|
+
const actionGraphRouteFor = (
|
|
867
|
+
kind: "action" | "agent_run" | "tool_call",
|
|
868
|
+
id: string,
|
|
869
|
+
) => {
|
|
870
|
+
switch (kind) {
|
|
871
|
+
case "action":
|
|
872
|
+
return actionRoute(id);
|
|
873
|
+
case "agent_run":
|
|
874
|
+
return agentRunRoute(id);
|
|
875
|
+
case "tool_call":
|
|
876
|
+
return toolCallRoute(id);
|
|
877
|
+
}
|
|
878
|
+
};
|
|
879
|
+
|
|
880
|
+
const loadManifestForAnchor = async (
|
|
881
|
+
db: SqlDb,
|
|
882
|
+
projectId: string,
|
|
883
|
+
kind: "action" | "agent_run" | "tool_call",
|
|
884
|
+
id: string,
|
|
885
|
+
): Promise<{ manifest: EntityManifestExtended; found: boolean }> => {
|
|
886
|
+
const index = new IdentityIndex(db);
|
|
887
|
+
if (kind === "action") {
|
|
888
|
+
const manifest = await index.byAction(projectId, id);
|
|
889
|
+
return {
|
|
890
|
+
manifest,
|
|
891
|
+
found: manifest.actions.some((action) => action.id === id),
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
if (kind === "agent_run") {
|
|
895
|
+
const manifest = await index.byAgentRun(projectId, id);
|
|
896
|
+
return {
|
|
897
|
+
manifest,
|
|
898
|
+
found: manifest.agentRuns.some((run) => run.id === id),
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
const toolCallRow = await db
|
|
903
|
+
.prepare(
|
|
904
|
+
`SELECT action_id FROM tool_calls
|
|
905
|
+
WHERE project_id = ? AND id = ? LIMIT 1`,
|
|
906
|
+
)
|
|
907
|
+
.bind(projectId, id)
|
|
908
|
+
.first<{ action_id: string }>();
|
|
909
|
+
if (!toolCallRow) {
|
|
910
|
+
return {
|
|
911
|
+
manifest: await index.byAction(projectId, "__missing__"),
|
|
912
|
+
found: false,
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
const manifest = await index.byAction(projectId, toolCallRow.action_id);
|
|
916
|
+
return {
|
|
917
|
+
manifest,
|
|
918
|
+
found: manifest.toolCalls.some((tool) => tool.id === id),
|
|
919
|
+
};
|
|
920
|
+
};
|
|
921
|
+
|
|
922
|
+
const causalPath = (manifest: EntityManifestExtended): ActionRef[] => {
|
|
923
|
+
const byId = new Map(manifest.actions.map((action) => [action.id, action]));
|
|
924
|
+
const roots = manifest.actions.filter(
|
|
925
|
+
(action) => !action.causedByActionId || !byId.has(action.causedByActionId),
|
|
926
|
+
);
|
|
927
|
+
const root = roots[0] ?? manifest.actions[0];
|
|
928
|
+
if (!root) return [];
|
|
929
|
+
|
|
930
|
+
const children = new Map<string, ActionRef[]>();
|
|
931
|
+
for (const action of manifest.actions) {
|
|
932
|
+
if (!action.causedByActionId) continue;
|
|
933
|
+
const list = children.get(action.causedByActionId) ?? [];
|
|
934
|
+
list.push(action);
|
|
935
|
+
children.set(action.causedByActionId, list);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
const path: ActionRef[] = [];
|
|
939
|
+
let current: ActionRef | undefined = root;
|
|
940
|
+
while (current) {
|
|
941
|
+
path.push(current);
|
|
942
|
+
const nextChildren: ActionRef[] | undefined = children
|
|
943
|
+
.get(current.id)
|
|
944
|
+
?.sort((a, b) => {
|
|
945
|
+
const status = Number(b.status !== "ok") - Number(a.status !== "ok");
|
|
946
|
+
if (status !== 0) return status;
|
|
947
|
+
return (b.durationMs ?? 0) - (a.durationMs ?? 0);
|
|
948
|
+
});
|
|
949
|
+
current = nextChildren?.[0];
|
|
950
|
+
}
|
|
951
|
+
return path;
|
|
952
|
+
};
|
|
953
|
+
|
|
954
|
+
const actionEvidenceReferences = (
|
|
955
|
+
anchorKind: "action" | "agent_run" | "tool_call",
|
|
956
|
+
anchorId: string,
|
|
957
|
+
manifest: EntityManifestExtended,
|
|
958
|
+
profiles: ProfileEvidenceRow[],
|
|
959
|
+
): EvidenceReference[] => {
|
|
960
|
+
const refs: EvidenceReference[] = [
|
|
961
|
+
evidenceRef(
|
|
962
|
+
`${anchorKind}:${anchorId}`,
|
|
963
|
+
anchorKind,
|
|
964
|
+
anchorId,
|
|
965
|
+
actionGraphRouteFor(anchorKind, anchorId),
|
|
966
|
+
"evidence.bundle.action_graph",
|
|
967
|
+
1,
|
|
968
|
+
`Action graph bundle for ${anchorKind} ${anchorId} includes ${manifest.actions.length} action(s), ${manifest.toolCalls.length} tool call(s), and ${manifest.evalResults.length} eval result(s).`,
|
|
969
|
+
),
|
|
970
|
+
];
|
|
971
|
+
|
|
972
|
+
for (const tool of manifest.toolCalls.slice(0, 5)) {
|
|
973
|
+
refs.push(
|
|
974
|
+
evidenceRef(
|
|
975
|
+
`tool_call:${tool.id}`,
|
|
976
|
+
"tool_call",
|
|
977
|
+
tool.id,
|
|
978
|
+
toolCallRoute(tool.id),
|
|
979
|
+
"evidence.bundle.tool_calls",
|
|
980
|
+
tool.sideEffect ? 0.95 : 0.8,
|
|
981
|
+
`Tool ${tool.toolName} sideEffect=${Boolean(tool.sideEffect)} approvalState=${tool.approvalState ?? "unknown"}.`,
|
|
982
|
+
[
|
|
983
|
+
{
|
|
984
|
+
label: `action ${tool.actionId}`,
|
|
985
|
+
entityKind: "action",
|
|
986
|
+
entityId: tool.actionId,
|
|
987
|
+
route: actionRoute(tool.actionId),
|
|
988
|
+
},
|
|
989
|
+
],
|
|
990
|
+
),
|
|
991
|
+
);
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
for (const aiCall of manifest.aiCalls.slice(0, 5)) {
|
|
995
|
+
refs.push(
|
|
996
|
+
evidenceRef(
|
|
997
|
+
`ai_call:${aiCall.callId}`,
|
|
998
|
+
"ai_call",
|
|
999
|
+
aiCall.callId,
|
|
1000
|
+
aiCallRoute(aiCall.callId),
|
|
1001
|
+
"evidence.bundle.ai_calls",
|
|
1002
|
+
0.85,
|
|
1003
|
+
`AI call ${aiCall.provider}/${aiCall.modelName} cost=${(aiCall.totalCostUsd ?? 0).toFixed(6)} USD.`,
|
|
1004
|
+
aiCall.traceId
|
|
1005
|
+
? [
|
|
1006
|
+
{
|
|
1007
|
+
label: `trace ${aiCall.traceId}`,
|
|
1008
|
+
entityKind: "trace",
|
|
1009
|
+
entityId: aiCall.traceId,
|
|
1010
|
+
route: traceRoute(aiCall.traceId),
|
|
1011
|
+
},
|
|
1012
|
+
]
|
|
1013
|
+
: [],
|
|
1014
|
+
),
|
|
1015
|
+
);
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
if (manifest.replay) {
|
|
1019
|
+
refs.push(
|
|
1020
|
+
evidenceRef(
|
|
1021
|
+
`replay:${manifest.replay.sessionId}`,
|
|
1022
|
+
"replay",
|
|
1023
|
+
manifest.replay.sessionId,
|
|
1024
|
+
replayRoute(manifest.replay.sessionId),
|
|
1025
|
+
"evidence.bundle.session_replay",
|
|
1026
|
+
0.8,
|
|
1027
|
+
`Session replay has ${manifest.replay.eventsCount} event(s) across ${manifest.replay.chunkCount} chunk(s).`,
|
|
1028
|
+
),
|
|
1029
|
+
);
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
for (const profile of profiles.slice(0, 5)) {
|
|
1033
|
+
refs.push(
|
|
1034
|
+
evidenceRef(
|
|
1035
|
+
`profile:${profile.id}`,
|
|
1036
|
+
"profile",
|
|
1037
|
+
profile.id,
|
|
1038
|
+
profileRoute(profile.id),
|
|
1039
|
+
"evidence.bundle.connected_profiles",
|
|
1040
|
+
0.8,
|
|
1041
|
+
`${profile.profile_type} profile is connected through this action graph's traces.`,
|
|
1042
|
+
),
|
|
1043
|
+
);
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
for (const evalResult of manifest.evalResults.slice(0, 5)) {
|
|
1047
|
+
refs.push(
|
|
1048
|
+
evidenceRef(
|
|
1049
|
+
`eval:${evalResult.id}`,
|
|
1050
|
+
"eval",
|
|
1051
|
+
evalResult.id,
|
|
1052
|
+
`#/evals/${encodeURIComponent(evalResult.id)}`,
|
|
1053
|
+
"evidence.bundle.eval_results",
|
|
1054
|
+
evalResult.passed ? 0.75 : 0.9,
|
|
1055
|
+
`Eval ${evalResult.evaluatorName} ${evalResult.passed ? "passed" : "failed"}${evalResult.score === null ? "" : ` with score ${evalResult.score}`}.`,
|
|
1056
|
+
[
|
|
1057
|
+
{
|
|
1058
|
+
label: `action ${evalResult.actionId}`,
|
|
1059
|
+
entityKind: "action",
|
|
1060
|
+
entityId: evalResult.actionId,
|
|
1061
|
+
route: actionRoute(evalResult.actionId),
|
|
1062
|
+
},
|
|
1063
|
+
],
|
|
1064
|
+
),
|
|
1065
|
+
);
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
for (const span of manifest.spans.slice(0, 5)) {
|
|
1069
|
+
refs.push(
|
|
1070
|
+
evidenceRef(
|
|
1071
|
+
`span:${span.traceId}:${span.spanId}`,
|
|
1072
|
+
"span",
|
|
1073
|
+
`${span.traceId}:${span.spanId}`,
|
|
1074
|
+
spanRoute(span.traceId, span.spanId),
|
|
1075
|
+
"evidence.bundle.connected_spans",
|
|
1076
|
+
span.statusCode > 1 ? 0.9 : 0.7,
|
|
1077
|
+
`Connected span ${span.serviceName ?? "unknown"}:${span.spanName} status=${span.statusCode}.`,
|
|
1078
|
+
),
|
|
1079
|
+
);
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
return refs;
|
|
1083
|
+
};
|
|
1084
|
+
|
|
1085
|
+
const buildActionGraphBundle = (
|
|
1086
|
+
projectId: string,
|
|
1087
|
+
anchorKind: "action" | "agent_run" | "tool_call",
|
|
1088
|
+
anchorId: string,
|
|
1089
|
+
manifest: EntityManifestExtended,
|
|
1090
|
+
profiles: ProfileEvidenceRow[],
|
|
1091
|
+
intent: EvidenceBundleIntent,
|
|
1092
|
+
targetTokens: number,
|
|
1093
|
+
detailLevel: EvidenceBundleDetailLevel,
|
|
1094
|
+
): EvidenceBundle => {
|
|
1095
|
+
const graphRefId = encodeRef({
|
|
1096
|
+
kind: "action_graph",
|
|
1097
|
+
projectId,
|
|
1098
|
+
traceId: manifest.spans[0]?.traceId,
|
|
1099
|
+
anchorKind,
|
|
1100
|
+
anchorId,
|
|
1101
|
+
});
|
|
1102
|
+
const traceIds = Array.from(
|
|
1103
|
+
new Set(
|
|
1104
|
+
[
|
|
1105
|
+
...manifest.spans.map((span) => span.traceId),
|
|
1106
|
+
...manifest.logs.map((log) => log.traceId),
|
|
1107
|
+
...manifest.actions.map((action) => action.traceId),
|
|
1108
|
+
].filter((id): id is string => Boolean(id)),
|
|
1109
|
+
),
|
|
1110
|
+
).slice(0, 5);
|
|
1111
|
+
const retrievalRefs: EvidenceRetrievalRef[] = [
|
|
1112
|
+
{
|
|
1113
|
+
refId: graphRefId,
|
|
1114
|
+
kind: anchorKind,
|
|
1115
|
+
anchor: { entityKind: anchorKind, entityId: anchorId },
|
|
1116
|
+
source: "evidence.bundle.action_graph_manifest",
|
|
1117
|
+
query: { anchorKind, anchorId },
|
|
1118
|
+
compactedFrom: {
|
|
1119
|
+
recordCount:
|
|
1120
|
+
manifest.actions.length +
|
|
1121
|
+
manifest.toolCalls.length +
|
|
1122
|
+
manifest.evalResults.length +
|
|
1123
|
+
manifest.logs.length +
|
|
1124
|
+
manifest.spans.length +
|
|
1125
|
+
manifest.aiCalls.length +
|
|
1126
|
+
(manifest.replay ? 1 : 0) +
|
|
1127
|
+
profiles.length,
|
|
1128
|
+
tokenEstimate: estimateTokens(manifest),
|
|
1129
|
+
},
|
|
1130
|
+
returned: {
|
|
1131
|
+
recordCount: causalPath(manifest).length,
|
|
1132
|
+
tokenEstimate: estimateTokens(causalPath(manifest)),
|
|
1133
|
+
},
|
|
1134
|
+
},
|
|
1135
|
+
...traceIds.map(
|
|
1136
|
+
(traceId): EvidenceRetrievalRef => ({
|
|
1137
|
+
refId: encodeRef({ kind: "trace", projectId, traceId }),
|
|
1138
|
+
kind: "trace",
|
|
1139
|
+
anchor: { entityKind: anchorKind, entityId: anchorId },
|
|
1140
|
+
source: "evidence.bundle.connected_traces",
|
|
1141
|
+
query: { traceId },
|
|
1142
|
+
}),
|
|
1143
|
+
),
|
|
1144
|
+
...traceIds.map(
|
|
1145
|
+
(traceId): EvidenceRetrievalRef => ({
|
|
1146
|
+
refId: encodeRef({
|
|
1147
|
+
kind: "logs",
|
|
1148
|
+
projectId,
|
|
1149
|
+
traceId,
|
|
1150
|
+
hours: DEFAULT_HOURS,
|
|
1151
|
+
limit: MAX_LOGS_FOR_RETRIEVAL,
|
|
1152
|
+
}),
|
|
1153
|
+
kind: "logs",
|
|
1154
|
+
anchor: { entityKind: anchorKind, entityId: anchorId },
|
|
1155
|
+
source: "evidence.bundle.connected_logs",
|
|
1156
|
+
query: { traceId, hours: DEFAULT_HOURS, limit: MAX_LOGS_FOR_RETRIEVAL },
|
|
1157
|
+
}),
|
|
1158
|
+
),
|
|
1159
|
+
...manifest.toolCalls.map(
|
|
1160
|
+
(tool): EvidenceRetrievalRef => ({
|
|
1161
|
+
refId: encodeRef({ kind: "tool_call", projectId, toolCallId: tool.id }),
|
|
1162
|
+
kind: "tool_call",
|
|
1163
|
+
anchor: { entityKind: anchorKind, entityId: anchorId },
|
|
1164
|
+
source: "evidence.bundle.tool_call_payloads",
|
|
1165
|
+
query: { toolCallId: tool.id },
|
|
1166
|
+
compactedFrom: {
|
|
1167
|
+
recordCount: 1,
|
|
1168
|
+
tokenEstimate: estimateTokens(tool),
|
|
1169
|
+
},
|
|
1170
|
+
returned: {
|
|
1171
|
+
recordCount: 1,
|
|
1172
|
+
tokenEstimate: estimateTokens({
|
|
1173
|
+
id: tool.id,
|
|
1174
|
+
toolName: tool.toolName,
|
|
1175
|
+
argsHash: tool.argsHash,
|
|
1176
|
+
resultHash: tool.resultHash,
|
|
1177
|
+
argsRedacted: tool.argsRedacted,
|
|
1178
|
+
resultRedacted: tool.resultRedacted,
|
|
1179
|
+
}),
|
|
1180
|
+
},
|
|
1181
|
+
}),
|
|
1182
|
+
),
|
|
1183
|
+
...manifest.aiCalls.map(
|
|
1184
|
+
(aiCall): EvidenceRetrievalRef => ({
|
|
1185
|
+
refId: encodeRef({ kind: "ai_call", projectId, callId: aiCall.callId }),
|
|
1186
|
+
kind: "ai_call",
|
|
1187
|
+
anchor: { entityKind: anchorKind, entityId: anchorId },
|
|
1188
|
+
source: "evidence.bundle.ai_calls",
|
|
1189
|
+
query: { callId: aiCall.callId },
|
|
1190
|
+
compactedFrom: {
|
|
1191
|
+
recordCount: 1,
|
|
1192
|
+
tokenEstimate: estimateTokens(aiCall),
|
|
1193
|
+
},
|
|
1194
|
+
returned: {
|
|
1195
|
+
recordCount: 1,
|
|
1196
|
+
tokenEstimate: estimateTokens(aiCall),
|
|
1197
|
+
},
|
|
1198
|
+
}),
|
|
1199
|
+
),
|
|
1200
|
+
...(manifest.replay
|
|
1201
|
+
? [
|
|
1202
|
+
{
|
|
1203
|
+
refId: encodeRef({
|
|
1204
|
+
kind: "replay" as const,
|
|
1205
|
+
projectId,
|
|
1206
|
+
sessionId: manifest.replay.sessionId,
|
|
1207
|
+
replayMode: "metadata" as const,
|
|
1208
|
+
}),
|
|
1209
|
+
kind: "replay" as const,
|
|
1210
|
+
anchor: { entityKind: anchorKind, entityId: anchorId },
|
|
1211
|
+
source: "evidence.bundle.session_replay",
|
|
1212
|
+
query: { sessionId: manifest.replay.sessionId },
|
|
1213
|
+
compactedFrom: {
|
|
1214
|
+
recordCount: manifest.replay.eventsCount,
|
|
1215
|
+
},
|
|
1216
|
+
returned: {
|
|
1217
|
+
recordCount: 1,
|
|
1218
|
+
tokenEstimate: estimateTokens(manifest.replay),
|
|
1219
|
+
},
|
|
1220
|
+
},
|
|
1221
|
+
]
|
|
1222
|
+
: []),
|
|
1223
|
+
...(manifest.replay
|
|
1224
|
+
? [
|
|
1225
|
+
{
|
|
1226
|
+
refId: encodeRef({
|
|
1227
|
+
kind: "replay" as const,
|
|
1228
|
+
projectId,
|
|
1229
|
+
sessionId: manifest.replay.sessionId,
|
|
1230
|
+
replayMode: "events" as const,
|
|
1231
|
+
chunkOffset: 0,
|
|
1232
|
+
limit: DEFAULT_REPLAY_EVENT_CHUNKS,
|
|
1233
|
+
}),
|
|
1234
|
+
kind: "replay" as const,
|
|
1235
|
+
anchor: { entityKind: anchorKind, entityId: anchorId },
|
|
1236
|
+
source: "evidence.bundle.replay_event_window",
|
|
1237
|
+
query: {
|
|
1238
|
+
sessionId: manifest.replay.sessionId,
|
|
1239
|
+
chunkOffset: 0,
|
|
1240
|
+
chunkLimit: DEFAULT_REPLAY_EVENT_CHUNKS,
|
|
1241
|
+
},
|
|
1242
|
+
compactedFrom: {
|
|
1243
|
+
recordCount: manifest.replay.eventsCount,
|
|
1244
|
+
},
|
|
1245
|
+
},
|
|
1246
|
+
]
|
|
1247
|
+
: []),
|
|
1248
|
+
...profiles.map(
|
|
1249
|
+
(profile): EvidenceRetrievalRef => ({
|
|
1250
|
+
refId: encodeRef({
|
|
1251
|
+
kind: "profile",
|
|
1252
|
+
projectId,
|
|
1253
|
+
profileId: profile.id,
|
|
1254
|
+
profileMode: "metadata",
|
|
1255
|
+
}),
|
|
1256
|
+
kind: "profile",
|
|
1257
|
+
anchor: { entityKind: anchorKind, entityId: anchorId },
|
|
1258
|
+
source: "evidence.bundle.connected_profiles",
|
|
1259
|
+
query: { profileId: profile.id },
|
|
1260
|
+
compactedFrom: {
|
|
1261
|
+
recordCount: 1,
|
|
1262
|
+
byteEstimate: profile.blob_size_bytes,
|
|
1263
|
+
},
|
|
1264
|
+
returned: {
|
|
1265
|
+
recordCount: 1,
|
|
1266
|
+
tokenEstimate: estimateTokens(profile),
|
|
1267
|
+
},
|
|
1268
|
+
}),
|
|
1269
|
+
),
|
|
1270
|
+
...profiles.map(
|
|
1271
|
+
(profile): EvidenceRetrievalRef => ({
|
|
1272
|
+
refId: encodeRef({
|
|
1273
|
+
kind: "profile",
|
|
1274
|
+
projectId,
|
|
1275
|
+
profileId: profile.id,
|
|
1276
|
+
profileMode: "frames",
|
|
1277
|
+
limit: DEFAULT_PROFILE_FRAME_LIMIT,
|
|
1278
|
+
}),
|
|
1279
|
+
kind: "profile",
|
|
1280
|
+
anchor: { entityKind: anchorKind, entityId: anchorId },
|
|
1281
|
+
source: "evidence.bundle.profile_frames",
|
|
1282
|
+
query: {
|
|
1283
|
+
profileId: profile.id,
|
|
1284
|
+
frameLimit: DEFAULT_PROFILE_FRAME_LIMIT,
|
|
1285
|
+
},
|
|
1286
|
+
compactedFrom: {
|
|
1287
|
+
recordCount: profile.sample_count ?? undefined,
|
|
1288
|
+
byteEstimate: profile.blob_size_bytes,
|
|
1289
|
+
},
|
|
1290
|
+
}),
|
|
1291
|
+
),
|
|
1292
|
+
];
|
|
1293
|
+
|
|
1294
|
+
const path = causalPath(manifest);
|
|
1295
|
+
const sideEffectTools = manifest.toolCalls.filter((tool) => tool.sideEffect);
|
|
1296
|
+
const failedEvals = manifest.evalResults.filter(
|
|
1297
|
+
(evalResult) => !evalResult.passed,
|
|
1298
|
+
);
|
|
1299
|
+
const failedActions = manifest.actions.filter(
|
|
1300
|
+
(action) => action.status !== "ok",
|
|
1301
|
+
);
|
|
1302
|
+
const totalCost = manifest.aiCalls.reduce(
|
|
1303
|
+
(sum, call) => sum + (call.totalCostUsd ?? 0),
|
|
1304
|
+
manifest.agentRuns.reduce(
|
|
1305
|
+
(sum, run) => sum + (run.totalCostUsd ?? 0),
|
|
1306
|
+
manifest.actions.reduce(
|
|
1307
|
+
(sum, action) => sum + (action.totalCostUsd ?? 0),
|
|
1308
|
+
0,
|
|
1309
|
+
),
|
|
1310
|
+
),
|
|
1311
|
+
);
|
|
1312
|
+
const evidenceReferences = actionEvidenceReferences(
|
|
1313
|
+
anchorKind,
|
|
1314
|
+
anchorId,
|
|
1315
|
+
manifest,
|
|
1316
|
+
profiles,
|
|
1317
|
+
);
|
|
1318
|
+
const suggestedNextPivots: EvidenceNextPivot[] = [
|
|
1319
|
+
{
|
|
1320
|
+
label: "Inspect action graph",
|
|
1321
|
+
entityKind: anchorKind,
|
|
1322
|
+
entityId: anchorId,
|
|
1323
|
+
route: actionGraphRouteFor(anchorKind, anchorId),
|
|
1324
|
+
reason: "Expand the full causal action graph and connected signals.",
|
|
1325
|
+
},
|
|
1326
|
+
...traceIds.slice(0, 3).map(
|
|
1327
|
+
(traceId): EvidenceNextPivot => ({
|
|
1328
|
+
label: "Inspect connected trace",
|
|
1329
|
+
entityKind: "trace",
|
|
1330
|
+
entityId: traceId,
|
|
1331
|
+
route: traceRoute(traceId),
|
|
1332
|
+
reason:
|
|
1333
|
+
"Inspect backend spans and logs connected to this action graph.",
|
|
1334
|
+
}),
|
|
1335
|
+
),
|
|
1336
|
+
];
|
|
1337
|
+
|
|
1338
|
+
if (manifest.replay) {
|
|
1339
|
+
suggestedNextPivots.push({
|
|
1340
|
+
label: "Inspect session replay",
|
|
1341
|
+
entityKind: "replay",
|
|
1342
|
+
entityId: manifest.replay.sessionId,
|
|
1343
|
+
route: replayRoute(manifest.replay.sessionId),
|
|
1344
|
+
reason: "Inspect the user-visible path that led into the action graph.",
|
|
1345
|
+
});
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
if (profiles[0]) {
|
|
1349
|
+
suggestedNextPivots.push({
|
|
1350
|
+
label: "Inspect connected profile",
|
|
1351
|
+
entityKind: "profile",
|
|
1352
|
+
entityId: profiles[0].id,
|
|
1353
|
+
route: profileRoute(profiles[0].id),
|
|
1354
|
+
reason: "Inspect runtime hotspots connected through the graph traces.",
|
|
1355
|
+
});
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
const derivedSummaries = [
|
|
1359
|
+
{
|
|
1360
|
+
title: "Causal action path",
|
|
1361
|
+
reason:
|
|
1362
|
+
path.length > 0
|
|
1363
|
+
? `Representative causal path has ${path.length} action(s): ${path.map((action) => action.actionKind).join(" -> ")}.`
|
|
1364
|
+
: "No actions were available for causal path extraction.",
|
|
1365
|
+
confidence: path.length > 0 ? 0.9 : 0.2,
|
|
1366
|
+
evidenceIds: path.map((action) => `action:${action.id}`),
|
|
1367
|
+
retrievalRefIds: [graphRefId],
|
|
1368
|
+
},
|
|
1369
|
+
{
|
|
1370
|
+
title: "Tool side effects",
|
|
1371
|
+
reason:
|
|
1372
|
+
sideEffectTools.length > 0
|
|
1373
|
+
? `${sideEffectTools.length} side-effecting tool call(s) are connected to this graph.`
|
|
1374
|
+
: "No side-effecting tool calls are connected to this graph.",
|
|
1375
|
+
confidence: manifest.toolCalls.length > 0 ? 0.9 : 0.6,
|
|
1376
|
+
evidenceIds: sideEffectTools.map((tool) => `tool_call:${tool.id}`),
|
|
1377
|
+
retrievalRefIds: [graphRefId],
|
|
1378
|
+
},
|
|
1379
|
+
{
|
|
1380
|
+
title: "Cost and model context",
|
|
1381
|
+
reason: `Connected agent/action/AI records report total cost ${totalCost.toFixed(6)} USD across ${manifest.agentRuns.length} agent run(s) and ${manifest.aiCalls.length} AI call(s).`,
|
|
1382
|
+
confidence:
|
|
1383
|
+
manifest.agentRuns.length > 0 || manifest.actions.length > 0
|
|
1384
|
+
? 0.8
|
|
1385
|
+
: 0.3,
|
|
1386
|
+
evidenceIds: [
|
|
1387
|
+
...manifest.agentRuns.map((run) => `agent_run:${run.id}`),
|
|
1388
|
+
...manifest.aiCalls.map((call) => `ai_call:${call.callId}`),
|
|
1389
|
+
],
|
|
1390
|
+
retrievalRefIds: [graphRefId],
|
|
1391
|
+
},
|
|
1392
|
+
{
|
|
1393
|
+
title: "Replay and profile pivots",
|
|
1394
|
+
reason: `${manifest.replay ? "One session replay" : "No session replay"} and ${profiles.length} connected profile(s) are available for this graph.`,
|
|
1395
|
+
confidence: manifest.replay || profiles.length > 0 ? 0.8 : 0.45,
|
|
1396
|
+
evidenceIds: [
|
|
1397
|
+
...(manifest.replay ? [`replay:${manifest.replay.sessionId}`] : []),
|
|
1398
|
+
...profiles.map((profile) => `profile:${profile.id}`),
|
|
1399
|
+
],
|
|
1400
|
+
retrievalRefIds: retrievalRefs
|
|
1401
|
+
.filter((ref) => ref.kind === "replay" || ref.kind === "profile")
|
|
1402
|
+
.map((ref) => ref.refId),
|
|
1403
|
+
},
|
|
1404
|
+
];
|
|
1405
|
+
|
|
1406
|
+
const findings = [
|
|
1407
|
+
...sideEffectTools.map((tool) => ({
|
|
1408
|
+
title: "Side-effecting tool call present",
|
|
1409
|
+
reason: `Tool ${tool.toolName} can mutate state and has approvalState=${tool.approvalState ?? "unknown"}.`,
|
|
1410
|
+
confidence: 0.9,
|
|
1411
|
+
evidenceIds: [`tool_call:${tool.id}`],
|
|
1412
|
+
retrievalRefIds: [graphRefId],
|
|
1413
|
+
})),
|
|
1414
|
+
...failedEvals.map((evalResult) => ({
|
|
1415
|
+
title: "Failed eval present",
|
|
1416
|
+
reason: `Eval ${evalResult.evaluatorName} failed${evalResult.reasoning ? `: ${evalResult.reasoning}` : "."}`,
|
|
1417
|
+
confidence: 0.9,
|
|
1418
|
+
evidenceIds: [`eval:${evalResult.id}`],
|
|
1419
|
+
retrievalRefIds: [graphRefId],
|
|
1420
|
+
})),
|
|
1421
|
+
...failedActions.slice(0, 3).map((action) => ({
|
|
1422
|
+
title: "Non-ok action present",
|
|
1423
|
+
reason: `Action ${action.id} has status ${action.status}.`,
|
|
1424
|
+
confidence: 0.85,
|
|
1425
|
+
evidenceIds: [`action:${action.id}`],
|
|
1426
|
+
retrievalRefIds: [graphRefId],
|
|
1427
|
+
})),
|
|
1428
|
+
...manifest.aiCalls
|
|
1429
|
+
.filter((call) => (call.totalCostUsd ?? 0) > 0)
|
|
1430
|
+
.slice(0, 3)
|
|
1431
|
+
.map((call) => ({
|
|
1432
|
+
title: "AI call cost present",
|
|
1433
|
+
reason: `AI call ${call.callId} reports cost ${(call.totalCostUsd ?? 0).toFixed(6)} USD.`,
|
|
1434
|
+
confidence: 0.75,
|
|
1435
|
+
evidenceIds: [`ai_call:${call.callId}`],
|
|
1436
|
+
retrievalRefIds: retrievalRefs
|
|
1437
|
+
.filter((ref) => ref.kind === "ai_call")
|
|
1438
|
+
.map((ref) => ref.refId),
|
|
1439
|
+
})),
|
|
1440
|
+
];
|
|
1441
|
+
|
|
1442
|
+
const bundle: EvidenceBundle = {
|
|
1443
|
+
schemaVersion: BUNDLE_SCHEMA_VERSION,
|
|
1444
|
+
intent,
|
|
1445
|
+
anchor: { entityKind: anchorKind, entityId: anchorId },
|
|
1446
|
+
budget: { targetTokens, detailLevel },
|
|
1447
|
+
summary: `${anchorKind} ${anchorId} bundle includes ${manifest.actions.length} action(s), ${manifest.toolCalls.length} tool call(s), ${manifest.evalResults.length} eval result(s), ${manifest.spans.length} span(s), ${manifest.logs.length} log(s), ${manifest.aiCalls.length} AI call(s), ${profiles.length} profile(s), and ${manifest.replay ? 1 : 0} replay(s).`,
|
|
1448
|
+
derivedSummaries,
|
|
1449
|
+
findings,
|
|
1450
|
+
compactions: [],
|
|
1451
|
+
evidenceReferences,
|
|
1452
|
+
retrievalRefs,
|
|
1453
|
+
suggestedNextPivots,
|
|
1454
|
+
};
|
|
1455
|
+
bundle.budget = {
|
|
1456
|
+
targetTokens,
|
|
1457
|
+
detailLevel,
|
|
1458
|
+
estimatedTokens: estimateTokens(bundle),
|
|
1459
|
+
};
|
|
1460
|
+
return bundle;
|
|
1461
|
+
};
|
|
1462
|
+
|
|
1463
|
+
const parseBundleRequest = async (request: Request) => {
|
|
1464
|
+
try {
|
|
1465
|
+
const body = await request.json();
|
|
1466
|
+
return body && typeof body === "object"
|
|
1467
|
+
? (body as Record<string, unknown>)
|
|
1468
|
+
: {};
|
|
1469
|
+
} catch {
|
|
1470
|
+
return {};
|
|
1471
|
+
}
|
|
1472
|
+
};
|
|
1473
|
+
|
|
1474
|
+
export const evidenceRetrievalRoutesPlugin: CollectorPlugin = {
|
|
1475
|
+
name: "evidence-retrieval-routes",
|
|
1476
|
+
register(app, runtime) {
|
|
1477
|
+
app.post("/internal/evidence/bundle", async (c) => {
|
|
1478
|
+
const projectId = getProjectId(c);
|
|
1479
|
+
const body = await parseBundleRequest(c.req.raw);
|
|
1480
|
+
const anchor = body.anchor as
|
|
1481
|
+
| { entityKind?: unknown; entityId?: unknown }
|
|
1482
|
+
| undefined;
|
|
1483
|
+
if (!anchor || typeof anchor.entityId !== "string" || !anchor.entityId) {
|
|
1484
|
+
return c.json(
|
|
1485
|
+
{
|
|
1486
|
+
error: "Bad Request",
|
|
1487
|
+
message: "anchor.entityKind and anchor.entityId are required.",
|
|
1488
|
+
},
|
|
1489
|
+
400,
|
|
1490
|
+
);
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
const intent =
|
|
1494
|
+
typeof body.intent === "string" &&
|
|
1495
|
+
EVIDENCE_INTENTS.has(body.intent as EvidenceBundleIntent)
|
|
1496
|
+
? (body.intent as EvidenceBundleIntent)
|
|
1497
|
+
: "debug_failure";
|
|
1498
|
+
const budget = body.budget as Record<string, unknown> | undefined;
|
|
1499
|
+
const targetTokens = clampInt(
|
|
1500
|
+
budget?.targetTokens,
|
|
1501
|
+
500,
|
|
1502
|
+
30000,
|
|
1503
|
+
DEFAULT_TARGET_TOKENS,
|
|
1504
|
+
);
|
|
1505
|
+
const detailLevel =
|
|
1506
|
+
budget?.detailLevel === "brief" ||
|
|
1507
|
+
budget?.detailLevel === "standard" ||
|
|
1508
|
+
budget?.detailLevel === "deep"
|
|
1509
|
+
? budget.detailLevel
|
|
1510
|
+
: "standard";
|
|
1511
|
+
const hours = clampInt(body.hours, 1, 720, DEFAULT_HOURS);
|
|
1512
|
+
const db = runtime.getSqlDb(c.env);
|
|
1513
|
+
if (
|
|
1514
|
+
anchor.entityKind === "action" ||
|
|
1515
|
+
anchor.entityKind === "agent_run" ||
|
|
1516
|
+
anchor.entityKind === "tool_call"
|
|
1517
|
+
) {
|
|
1518
|
+
const { manifest, found } = await loadManifestForAnchor(
|
|
1519
|
+
db,
|
|
1520
|
+
projectId,
|
|
1521
|
+
anchor.entityKind,
|
|
1522
|
+
anchor.entityId,
|
|
1523
|
+
);
|
|
1524
|
+
if (!found) {
|
|
1525
|
+
return c.json(
|
|
1526
|
+
{
|
|
1527
|
+
error: "Not Found",
|
|
1528
|
+
message: `${anchor.entityKind} not found`,
|
|
1529
|
+
},
|
|
1530
|
+
404,
|
|
1531
|
+
);
|
|
1532
|
+
}
|
|
1533
|
+
const traceIds = Array.from(
|
|
1534
|
+
new Set(
|
|
1535
|
+
[
|
|
1536
|
+
...manifest.spans.map((span) => span.traceId),
|
|
1537
|
+
...manifest.logs.map((log) => log.traceId),
|
|
1538
|
+
...manifest.actions.map((action) => action.traceId),
|
|
1539
|
+
].filter((id): id is string => Boolean(id)),
|
|
1540
|
+
),
|
|
1541
|
+
).slice(0, 5);
|
|
1542
|
+
const profiles = (
|
|
1543
|
+
await Promise.all(
|
|
1544
|
+
traceIds.map((traceId) => profilesForTrace(db, projectId, traceId)),
|
|
1545
|
+
)
|
|
1546
|
+
)
|
|
1547
|
+
.flat()
|
|
1548
|
+
.filter(
|
|
1549
|
+
(profile, index, all) =>
|
|
1550
|
+
all.findIndex((candidate) => candidate.id === profile.id) ===
|
|
1551
|
+
index,
|
|
1552
|
+
);
|
|
1553
|
+
const bundle = buildActionGraphBundle(
|
|
1554
|
+
projectId,
|
|
1555
|
+
anchor.entityKind,
|
|
1556
|
+
anchor.entityId,
|
|
1557
|
+
manifest,
|
|
1558
|
+
profiles,
|
|
1559
|
+
intent,
|
|
1560
|
+
targetTokens,
|
|
1561
|
+
detailLevel,
|
|
1562
|
+
);
|
|
1563
|
+
return c.json(await materializeBundle(db, projectId, bundle));
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
if (anchor.entityKind !== "trace") {
|
|
1567
|
+
return c.json(
|
|
1568
|
+
{
|
|
1569
|
+
error: "Bad Request",
|
|
1570
|
+
message:
|
|
1571
|
+
"Supported evidence bundle anchors are trace, action, agent_run, and tool_call.",
|
|
1572
|
+
},
|
|
1573
|
+
400,
|
|
1574
|
+
);
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
const traceId = anchor.entityId;
|
|
1578
|
+
const store = runtime.createStore(c.env);
|
|
1579
|
+
const detail = await store.getTraceDetail(traceId, projectId);
|
|
1580
|
+
if (!detail) {
|
|
1581
|
+
return c.json({ error: "Not Found", message: "Trace not found" }, 404);
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
const logs = await new LogsStore(db).getLogs({
|
|
1585
|
+
projectId,
|
|
1586
|
+
traceId,
|
|
1587
|
+
hours,
|
|
1588
|
+
limit: MAX_LOGS_FOR_BUNDLE,
|
|
1589
|
+
});
|
|
1590
|
+
const profiles = await profilesForTrace(db, projectId, traceId);
|
|
1591
|
+
const bundle = await buildTraceBundle(
|
|
1592
|
+
projectId,
|
|
1593
|
+
detail,
|
|
1594
|
+
logs.logs,
|
|
1595
|
+
profiles,
|
|
1596
|
+
intent,
|
|
1597
|
+
targetTokens,
|
|
1598
|
+
detailLevel,
|
|
1599
|
+
hours,
|
|
1600
|
+
);
|
|
1601
|
+
return c.json(await materializeBundle(db, projectId, bundle));
|
|
1602
|
+
});
|
|
1603
|
+
|
|
1604
|
+
app.get("/internal/evidence/stats", async (c) => {
|
|
1605
|
+
const projectId = getProjectId(c);
|
|
1606
|
+
const db = runtime.getSqlDb(c.env);
|
|
1607
|
+
const limit = clampInt(c.req.query("limit"), 1, 100, 20);
|
|
1608
|
+
try {
|
|
1609
|
+
const [byKind, bySource, recentRefs, recentExpansions] =
|
|
1610
|
+
await Promise.all([
|
|
1611
|
+
db
|
|
1612
|
+
.prepare(
|
|
1613
|
+
`SELECT
|
|
1614
|
+
r.kind AS kind,
|
|
1615
|
+
COUNT(DISTINCT r.ref_id) AS issuedCount,
|
|
1616
|
+
COUNT(e.id) AS expansionCount
|
|
1617
|
+
FROM evidence_retrieval_refs r
|
|
1618
|
+
LEFT JOIN evidence_ref_expansions e
|
|
1619
|
+
ON e.project_id = r.project_id AND e.ref_id = r.ref_id
|
|
1620
|
+
WHERE r.project_id = ?
|
|
1621
|
+
GROUP BY r.kind
|
|
1622
|
+
ORDER BY expansionCount DESC, issuedCount DESC`,
|
|
1623
|
+
)
|
|
1624
|
+
.bind(projectId)
|
|
1625
|
+
.all<{
|
|
1626
|
+
kind: string;
|
|
1627
|
+
issuedCount: number;
|
|
1628
|
+
expansionCount: number;
|
|
1629
|
+
}>(),
|
|
1630
|
+
db
|
|
1631
|
+
.prepare(
|
|
1632
|
+
`SELECT
|
|
1633
|
+
r.source AS source,
|
|
1634
|
+
r.kind AS kind,
|
|
1635
|
+
COUNT(DISTINCT r.ref_id) AS issuedCount,
|
|
1636
|
+
COUNT(e.id) AS expansionCount
|
|
1637
|
+
FROM evidence_retrieval_refs r
|
|
1638
|
+
LEFT JOIN evidence_ref_expansions e
|
|
1639
|
+
ON e.project_id = r.project_id AND e.ref_id = r.ref_id
|
|
1640
|
+
WHERE r.project_id = ?
|
|
1641
|
+
GROUP BY r.source, r.kind
|
|
1642
|
+
ORDER BY expansionCount DESC, issuedCount DESC
|
|
1643
|
+
LIMIT ?`,
|
|
1644
|
+
)
|
|
1645
|
+
.bind(projectId, limit)
|
|
1646
|
+
.all<{
|
|
1647
|
+
source: string;
|
|
1648
|
+
kind: string;
|
|
1649
|
+
issuedCount: number;
|
|
1650
|
+
expansionCount: number;
|
|
1651
|
+
}>(),
|
|
1652
|
+
db
|
|
1653
|
+
.prepare(
|
|
1654
|
+
`SELECT ref_id, kind, anchor_kind, anchor_id, source,
|
|
1655
|
+
last_seen_at, issued_at
|
|
1656
|
+
FROM evidence_retrieval_refs
|
|
1657
|
+
WHERE project_id = ?
|
|
1658
|
+
ORDER BY last_seen_at DESC
|
|
1659
|
+
LIMIT ?`,
|
|
1660
|
+
)
|
|
1661
|
+
.bind(projectId, limit)
|
|
1662
|
+
.all<{
|
|
1663
|
+
ref_id: string;
|
|
1664
|
+
kind: string;
|
|
1665
|
+
anchor_kind: string;
|
|
1666
|
+
anchor_id: string;
|
|
1667
|
+
source: string;
|
|
1668
|
+
last_seen_at: string;
|
|
1669
|
+
issued_at: string;
|
|
1670
|
+
}>(),
|
|
1671
|
+
db
|
|
1672
|
+
.prepare(
|
|
1673
|
+
`SELECT id, ref_id, kind, source, operation, result_status,
|
|
1674
|
+
limit_value, query_text, expanded_at
|
|
1675
|
+
FROM evidence_ref_expansions
|
|
1676
|
+
WHERE project_id = ?
|
|
1677
|
+
ORDER BY expanded_at DESC
|
|
1678
|
+
LIMIT ?`,
|
|
1679
|
+
)
|
|
1680
|
+
.bind(projectId, limit)
|
|
1681
|
+
.all<{
|
|
1682
|
+
id: string;
|
|
1683
|
+
ref_id: string;
|
|
1684
|
+
kind: string;
|
|
1685
|
+
source: string | null;
|
|
1686
|
+
operation: string;
|
|
1687
|
+
result_status: string;
|
|
1688
|
+
limit_value: number | null;
|
|
1689
|
+
query_text: string | null;
|
|
1690
|
+
expanded_at: string;
|
|
1691
|
+
}>(),
|
|
1692
|
+
]);
|
|
1693
|
+
return c.json({
|
|
1694
|
+
projectId,
|
|
1695
|
+
generatedAt: new Date().toISOString(),
|
|
1696
|
+
byKind: byKind.results.map((row) => ({
|
|
1697
|
+
kind: row.kind,
|
|
1698
|
+
issuedCount: row.issuedCount,
|
|
1699
|
+
expansionCount: row.expansionCount,
|
|
1700
|
+
})),
|
|
1701
|
+
bySource: bySource.results.map((row) => ({
|
|
1702
|
+
source: row.source,
|
|
1703
|
+
kind: row.kind,
|
|
1704
|
+
issuedCount: row.issuedCount,
|
|
1705
|
+
expansionCount: row.expansionCount,
|
|
1706
|
+
})),
|
|
1707
|
+
recentRefs: recentRefs.results.map((row) => ({
|
|
1708
|
+
refId: row.ref_id,
|
|
1709
|
+
kind: row.kind,
|
|
1710
|
+
anchor: {
|
|
1711
|
+
entityKind: row.anchor_kind,
|
|
1712
|
+
entityId: row.anchor_id,
|
|
1713
|
+
},
|
|
1714
|
+
source: row.source,
|
|
1715
|
+
issuedAt: row.issued_at,
|
|
1716
|
+
lastSeenAt: row.last_seen_at,
|
|
1717
|
+
})),
|
|
1718
|
+
recentExpansions: recentExpansions.results.map((row) => ({
|
|
1719
|
+
id: row.id,
|
|
1720
|
+
refId: row.ref_id,
|
|
1721
|
+
kind: row.kind,
|
|
1722
|
+
source: row.source,
|
|
1723
|
+
operation: row.operation,
|
|
1724
|
+
resultStatus: row.result_status,
|
|
1725
|
+
limit: row.limit_value,
|
|
1726
|
+
query: row.query_text,
|
|
1727
|
+
expandedAt: row.expanded_at,
|
|
1728
|
+
})),
|
|
1729
|
+
});
|
|
1730
|
+
} catch {
|
|
1731
|
+
return c.json({
|
|
1732
|
+
projectId,
|
|
1733
|
+
generatedAt: new Date().toISOString(),
|
|
1734
|
+
byKind: [],
|
|
1735
|
+
bySource: [],
|
|
1736
|
+
recentRefs: [],
|
|
1737
|
+
recentExpansions: [],
|
|
1738
|
+
warning: "Evidence materialization tables are not available.",
|
|
1739
|
+
});
|
|
1740
|
+
}
|
|
1741
|
+
});
|
|
1742
|
+
|
|
1743
|
+
app.get("/internal/evidence/refs/:refId", async (c) => {
|
|
1744
|
+
const projectId = getProjectId(c);
|
|
1745
|
+
const refId = c.req.param("refId");
|
|
1746
|
+
const db = runtime.getSqlDb(c.env);
|
|
1747
|
+
const payload = decodeRef(refId);
|
|
1748
|
+
if (
|
|
1749
|
+
!isProjectScopedRefPayload(payload) ||
|
|
1750
|
+
payload.projectId !== projectId
|
|
1751
|
+
) {
|
|
1752
|
+
return c.json(
|
|
1753
|
+
{ error: "Not Found", message: "Evidence ref not found" },
|
|
1754
|
+
404,
|
|
1755
|
+
);
|
|
1756
|
+
}
|
|
1757
|
+
const recordOk = (limit?: number) =>
|
|
1758
|
+
recordRefExpansion(db, {
|
|
1759
|
+
projectId,
|
|
1760
|
+
refId,
|
|
1761
|
+
kind: payload.kind,
|
|
1762
|
+
operation: "retrieve",
|
|
1763
|
+
resultStatus: "ok",
|
|
1764
|
+
limit,
|
|
1765
|
+
});
|
|
1766
|
+
if (payload.kind === "action_graph") {
|
|
1767
|
+
if (!payload.anchorKind || !payload.anchorId) {
|
|
1768
|
+
return c.json(
|
|
1769
|
+
{ error: "Bad Request", message: "Action graph ref is malformed" },
|
|
1770
|
+
400,
|
|
1771
|
+
);
|
|
1772
|
+
}
|
|
1773
|
+
const { manifest, found } = await loadManifestForAnchor(
|
|
1774
|
+
runtime.getSqlDb(c.env),
|
|
1775
|
+
projectId,
|
|
1776
|
+
payload.anchorKind,
|
|
1777
|
+
payload.anchorId,
|
|
1778
|
+
);
|
|
1779
|
+
if (!found) {
|
|
1780
|
+
return c.json(
|
|
1781
|
+
{ error: "Not Found", message: "Action graph not found" },
|
|
1782
|
+
404,
|
|
1783
|
+
);
|
|
1784
|
+
}
|
|
1785
|
+
await recordOk();
|
|
1786
|
+
return c.json({
|
|
1787
|
+
refId,
|
|
1788
|
+
kind: payload.anchorKind,
|
|
1789
|
+
data: manifest,
|
|
1790
|
+
});
|
|
1791
|
+
}
|
|
1792
|
+
if (payload.kind === "trace") {
|
|
1793
|
+
if (!hasTraceId(payload)) {
|
|
1794
|
+
return c.json(
|
|
1795
|
+
{ error: "Not Found", message: "Evidence ref not found" },
|
|
1796
|
+
404,
|
|
1797
|
+
);
|
|
1798
|
+
}
|
|
1799
|
+
const detail = await runtime
|
|
1800
|
+
.createStore(c.env)
|
|
1801
|
+
.getTraceDetail(payload.traceId, projectId);
|
|
1802
|
+
if (!detail) {
|
|
1803
|
+
return c.json(
|
|
1804
|
+
{ error: "Not Found", message: "Trace not found" },
|
|
1805
|
+
404,
|
|
1806
|
+
);
|
|
1807
|
+
}
|
|
1808
|
+
await recordOk();
|
|
1809
|
+
return c.json({ refId, kind: "trace", data: detail });
|
|
1810
|
+
}
|
|
1811
|
+
if (payload.kind === "profile") {
|
|
1812
|
+
if (!payload.profileId) {
|
|
1813
|
+
return c.json(
|
|
1814
|
+
{ error: "Bad Request", message: "Profile ref is malformed" },
|
|
1815
|
+
400,
|
|
1816
|
+
);
|
|
1817
|
+
}
|
|
1818
|
+
const profile = await db
|
|
1819
|
+
.prepare(
|
|
1820
|
+
`SELECT id, service_name, profile_type, start_ts, end_ts,
|
|
1821
|
+
duration_ms, blob_size_bytes, blob_url, sample_count,
|
|
1822
|
+
agent, received_at, expires_at
|
|
1823
|
+
FROM profile_blobs
|
|
1824
|
+
WHERE project_id = ? AND id = ?
|
|
1825
|
+
LIMIT 1`,
|
|
1826
|
+
)
|
|
1827
|
+
.bind(projectId, payload.profileId)
|
|
1828
|
+
.first<ProfileEvidenceRow>();
|
|
1829
|
+
if (!profile) {
|
|
1830
|
+
return c.json(
|
|
1831
|
+
{ error: "Not Found", message: "Profile not found" },
|
|
1832
|
+
404,
|
|
1833
|
+
);
|
|
1834
|
+
}
|
|
1835
|
+
if (payload.profileMode === "frames") {
|
|
1836
|
+
if (!c.env.PROFILES_BUCKET) {
|
|
1837
|
+
return c.json({ error: "Profile storage not configured" }, 500);
|
|
1838
|
+
}
|
|
1839
|
+
if (!profile.blob_url) {
|
|
1840
|
+
return c.json(
|
|
1841
|
+
{ error: "Not Found", message: "Profile blob not found" },
|
|
1842
|
+
404,
|
|
1843
|
+
);
|
|
1844
|
+
}
|
|
1845
|
+
const obj = await c.env.PROFILES_BUCKET.get(profile.blob_url);
|
|
1846
|
+
if (!obj) {
|
|
1847
|
+
return c.json(
|
|
1848
|
+
{ error: "Not Found", message: "Profile blob not found" },
|
|
1849
|
+
404,
|
|
1850
|
+
);
|
|
1851
|
+
}
|
|
1852
|
+
try {
|
|
1853
|
+
const raw = new Uint8Array(await obj.arrayBuffer());
|
|
1854
|
+
const decoded = await decodePprofBlob(raw);
|
|
1855
|
+
const frameLimit = clampInt(
|
|
1856
|
+
c.req.query("limit") ?? payload.limit,
|
|
1857
|
+
1,
|
|
1858
|
+
MAX_PROFILE_FRAME_LIMIT,
|
|
1859
|
+
DEFAULT_PROFILE_FRAME_LIMIT,
|
|
1860
|
+
);
|
|
1861
|
+
await recordOk(frameLimit);
|
|
1862
|
+
return c.json({
|
|
1863
|
+
refId,
|
|
1864
|
+
kind: "profile",
|
|
1865
|
+
data: {
|
|
1866
|
+
profile: {
|
|
1867
|
+
id: profile.id,
|
|
1868
|
+
serviceName: profile.service_name,
|
|
1869
|
+
profileType: profile.profile_type,
|
|
1870
|
+
durationMs: profile.duration_ms,
|
|
1871
|
+
blobSizeBytes: profile.blob_size_bytes,
|
|
1872
|
+
sampleCount: profile.sample_count,
|
|
1873
|
+
},
|
|
1874
|
+
traceIdRequested: payload.traceId ?? null,
|
|
1875
|
+
frames: summarizeProfileFrames(decoded, {
|
|
1876
|
+
limit: frameLimit,
|
|
1877
|
+
traceIdFilter: payload.traceId ?? null,
|
|
1878
|
+
}),
|
|
1879
|
+
frameLimit,
|
|
1880
|
+
},
|
|
1881
|
+
});
|
|
1882
|
+
} catch (err) {
|
|
1883
|
+
runtime.logger.warn(
|
|
1884
|
+
"[evidence-retrieval] profile frame decode failed",
|
|
1885
|
+
{
|
|
1886
|
+
profile_id: profile.id,
|
|
1887
|
+
trace_id: payload.traceId ?? null,
|
|
1888
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1889
|
+
},
|
|
1890
|
+
);
|
|
1891
|
+
return c.json(
|
|
1892
|
+
{
|
|
1893
|
+
error: "Profile decode failed",
|
|
1894
|
+
message: err instanceof Error ? err.message : String(err),
|
|
1895
|
+
},
|
|
1896
|
+
422,
|
|
1897
|
+
);
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
await recordOk();
|
|
1901
|
+
return c.json({
|
|
1902
|
+
refId,
|
|
1903
|
+
kind: "profile",
|
|
1904
|
+
data: {
|
|
1905
|
+
profile: {
|
|
1906
|
+
id: profile.id,
|
|
1907
|
+
serviceName: profile.service_name,
|
|
1908
|
+
profileType: profile.profile_type,
|
|
1909
|
+
startTs: profile.start_ts,
|
|
1910
|
+
endTs: profile.end_ts,
|
|
1911
|
+
durationMs: profile.duration_ms,
|
|
1912
|
+
blobSizeBytes: profile.blob_size_bytes,
|
|
1913
|
+
sampleCount: profile.sample_count,
|
|
1914
|
+
agent: profile.agent,
|
|
1915
|
+
receivedAt: profile.received_at,
|
|
1916
|
+
expiresAt: profile.expires_at,
|
|
1917
|
+
},
|
|
1918
|
+
traceIds: await profileTraceIds(db, projectId, profile.id),
|
|
1919
|
+
traceIdRequested: payload.traceId ?? null,
|
|
1920
|
+
},
|
|
1921
|
+
});
|
|
1922
|
+
}
|
|
1923
|
+
if (payload.kind === "replay") {
|
|
1924
|
+
if (!payload.sessionId) {
|
|
1925
|
+
return c.json(
|
|
1926
|
+
{ error: "Bad Request", message: "Replay ref is malformed" },
|
|
1927
|
+
400,
|
|
1928
|
+
);
|
|
1929
|
+
}
|
|
1930
|
+
const replay = await replayBySession(db, projectId, payload.sessionId);
|
|
1931
|
+
if (!replay) {
|
|
1932
|
+
return c.json(
|
|
1933
|
+
{ error: "Not Found", message: "Replay not found" },
|
|
1934
|
+
404,
|
|
1935
|
+
);
|
|
1936
|
+
}
|
|
1937
|
+
if (payload.replayMode === "events") {
|
|
1938
|
+
if (!c.env.REPLAYS_BUCKET) {
|
|
1939
|
+
return c.json({ error: "Replay storage not configured" }, 500);
|
|
1940
|
+
}
|
|
1941
|
+
const chunkOffset = clampInt(
|
|
1942
|
+
c.req.query("chunkOffset") ?? payload.chunkOffset,
|
|
1943
|
+
0,
|
|
1944
|
+
Number.MAX_SAFE_INTEGER,
|
|
1945
|
+
0,
|
|
1946
|
+
);
|
|
1947
|
+
const chunkLimit = clampInt(
|
|
1948
|
+
c.req.query("limit") ?? payload.limit,
|
|
1949
|
+
1,
|
|
1950
|
+
MAX_REPLAY_EVENT_CHUNKS,
|
|
1951
|
+
DEFAULT_REPLAY_EVENT_CHUNKS,
|
|
1952
|
+
);
|
|
1953
|
+
const window = await replayEventWindow(
|
|
1954
|
+
c.env.REPLAYS_BUCKET,
|
|
1955
|
+
projectId,
|
|
1956
|
+
payload.sessionId,
|
|
1957
|
+
chunkOffset,
|
|
1958
|
+
chunkLimit,
|
|
1959
|
+
);
|
|
1960
|
+
if (window.chunks.total === 0) {
|
|
1961
|
+
return c.json(
|
|
1962
|
+
{ error: "Not Found", message: "Replay chunks missing" },
|
|
1963
|
+
404,
|
|
1964
|
+
);
|
|
1965
|
+
}
|
|
1966
|
+
await recordOk(chunkLimit);
|
|
1967
|
+
return c.json({
|
|
1968
|
+
refId,
|
|
1969
|
+
kind: "replay",
|
|
1970
|
+
data: {
|
|
1971
|
+
metadata: {
|
|
1972
|
+
sessionId: replay.session_id,
|
|
1973
|
+
visitorId: replay.visitor_id,
|
|
1974
|
+
firstChunkAt: replay.first_chunk_at,
|
|
1975
|
+
lastChunkAt: replay.last_chunk_at,
|
|
1976
|
+
chunkCount: replay.chunk_count,
|
|
1977
|
+
eventsCount: replay.events_count,
|
|
1978
|
+
storageBytes: replay.storage_bytes,
|
|
1979
|
+
},
|
|
1980
|
+
events: window.events,
|
|
1981
|
+
chunks: window.chunks,
|
|
1982
|
+
},
|
|
1983
|
+
});
|
|
1984
|
+
}
|
|
1985
|
+
await recordOk();
|
|
1986
|
+
return c.json({
|
|
1987
|
+
refId,
|
|
1988
|
+
kind: "replay",
|
|
1989
|
+
data: {
|
|
1990
|
+
sessionId: replay.session_id,
|
|
1991
|
+
visitorId: replay.visitor_id,
|
|
1992
|
+
firstChunkAt: replay.first_chunk_at,
|
|
1993
|
+
lastChunkAt: replay.last_chunk_at,
|
|
1994
|
+
chunkCount: replay.chunk_count,
|
|
1995
|
+
eventsCount: replay.events_count,
|
|
1996
|
+
storageBytes: replay.storage_bytes,
|
|
1997
|
+
},
|
|
1998
|
+
});
|
|
1999
|
+
}
|
|
2000
|
+
if (payload.kind === "ai_call") {
|
|
2001
|
+
if (!payload.callId) {
|
|
2002
|
+
return c.json(
|
|
2003
|
+
{ error: "Bad Request", message: "AI call ref is malformed" },
|
|
2004
|
+
400,
|
|
2005
|
+
);
|
|
2006
|
+
}
|
|
2007
|
+
const aiCall = await aiCallById(db, projectId, payload.callId);
|
|
2008
|
+
if (!aiCall) {
|
|
2009
|
+
return c.json(
|
|
2010
|
+
{ error: "Not Found", message: "AI call not found" },
|
|
2011
|
+
404,
|
|
2012
|
+
);
|
|
2013
|
+
}
|
|
2014
|
+
await recordOk();
|
|
2015
|
+
return c.json({
|
|
2016
|
+
refId,
|
|
2017
|
+
kind: "ai_call",
|
|
2018
|
+
data: {
|
|
2019
|
+
callId: aiCall.call_id,
|
|
2020
|
+
traceId: aiCall.trace_id,
|
|
2021
|
+
spanId: aiCall.span_id,
|
|
2022
|
+
serviceName: aiCall.service_name,
|
|
2023
|
+
modelName: aiCall.model_name,
|
|
2024
|
+
provider: aiCall.provider,
|
|
2025
|
+
callType: aiCall.call_type,
|
|
2026
|
+
promptTokens: aiCall.prompt_tokens,
|
|
2027
|
+
completionTokens: aiCall.completion_tokens,
|
|
2028
|
+
totalCostUsd: aiCall.total_cost_usd,
|
|
2029
|
+
latencyMs: aiCall.latency_ms,
|
|
2030
|
+
isError: Boolean(aiCall.is_error),
|
|
2031
|
+
errorMessage: aiCall.error_message,
|
|
2032
|
+
occurredAt: aiCall.occurred_at,
|
|
2033
|
+
receivedAt: aiCall.received_at,
|
|
2034
|
+
expiresAt: aiCall.expires_at,
|
|
2035
|
+
interactionId: aiCall.interaction_id,
|
|
2036
|
+
sessionId: aiCall.session_id,
|
|
2037
|
+
requestJson: "[redacted]",
|
|
2038
|
+
responseJson: "[redacted]",
|
|
2039
|
+
},
|
|
2040
|
+
});
|
|
2041
|
+
}
|
|
2042
|
+
if (payload.kind === "tool_call") {
|
|
2043
|
+
if (!payload.toolCallId) {
|
|
2044
|
+
return c.json(
|
|
2045
|
+
{ error: "Bad Request", message: "Tool call ref is malformed" },
|
|
2046
|
+
400,
|
|
2047
|
+
);
|
|
2048
|
+
}
|
|
2049
|
+
const tool = await toolCallById(db, projectId, payload.toolCallId);
|
|
2050
|
+
if (!tool) {
|
|
2051
|
+
return c.json(
|
|
2052
|
+
{ error: "Not Found", message: "Tool call not found" },
|
|
2053
|
+
404,
|
|
2054
|
+
);
|
|
2055
|
+
}
|
|
2056
|
+
await recordOk();
|
|
2057
|
+
return c.json({
|
|
2058
|
+
refId,
|
|
2059
|
+
kind: "tool_call",
|
|
2060
|
+
data: {
|
|
2061
|
+
id: tool.id,
|
|
2062
|
+
actionId: tool.actionId,
|
|
2063
|
+
toolName: tool.toolName,
|
|
2064
|
+
argsHash: tool.argsHash,
|
|
2065
|
+
resultHash: tool.resultHash,
|
|
2066
|
+
errorType: tool.errorType,
|
|
2067
|
+
sideEffect: Boolean(tool.sideEffect),
|
|
2068
|
+
approvalState: tool.approvalState,
|
|
2069
|
+
argsRedacted: tool.argsRedacted,
|
|
2070
|
+
resultRedacted: tool.resultRedacted,
|
|
2071
|
+
mcpAuditJson: tool.mcpAuditJson,
|
|
2072
|
+
mutationBeforeJson: tool.mutationBeforeJson,
|
|
2073
|
+
mutationAfterJson: tool.mutationAfterJson,
|
|
2074
|
+
mutationDiffJson: tool.mutationDiffJson,
|
|
2075
|
+
mutationArtifactId: tool.mutationArtifactId,
|
|
2076
|
+
},
|
|
2077
|
+
});
|
|
2078
|
+
}
|
|
2079
|
+
if (payload.kind === "logs" && !hasTraceId(payload)) {
|
|
2080
|
+
return c.json(
|
|
2081
|
+
{ error: "Not Found", message: "Evidence ref not found" },
|
|
2082
|
+
404,
|
|
2083
|
+
);
|
|
2084
|
+
}
|
|
2085
|
+
if (payload.kind !== "logs") {
|
|
2086
|
+
return c.json(
|
|
2087
|
+
{ error: "Bad Request", message: "Evidence ref kind is unsupported" },
|
|
2088
|
+
400,
|
|
2089
|
+
);
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
const limit = clampInt(
|
|
2093
|
+
c.req.query("limit"),
|
|
2094
|
+
1,
|
|
2095
|
+
MAX_LOGS_FOR_RETRIEVAL,
|
|
2096
|
+
payload.limit ?? 100,
|
|
2097
|
+
);
|
|
2098
|
+
const severity = c.req.query("severity");
|
|
2099
|
+
if (severity && !LOG_SEVERITIES.has(severity as LogRecord["severity"])) {
|
|
2100
|
+
return c.json(
|
|
2101
|
+
{ error: "Bad Request", message: "severity is invalid" },
|
|
2102
|
+
400,
|
|
2103
|
+
);
|
|
2104
|
+
}
|
|
2105
|
+
const logs = await new LogsStore(db).getLogs({
|
|
2106
|
+
projectId,
|
|
2107
|
+
traceId: payload.traceId,
|
|
2108
|
+
hours: payload.hours ?? DEFAULT_HOURS,
|
|
2109
|
+
limit,
|
|
2110
|
+
severity: severity as LogRecord["severity"] | undefined,
|
|
2111
|
+
search: c.req.query("search"),
|
|
2112
|
+
});
|
|
2113
|
+
await recordOk(limit);
|
|
2114
|
+
return c.json({ refId, kind: "logs", data: logs });
|
|
2115
|
+
});
|
|
2116
|
+
|
|
2117
|
+
app.post("/internal/evidence/refs/:refId/search", async (c) => {
|
|
2118
|
+
const projectId = getProjectId(c);
|
|
2119
|
+
const refId = c.req.param("refId");
|
|
2120
|
+
const db = runtime.getSqlDb(c.env);
|
|
2121
|
+
const payload = decodeRef(refId);
|
|
2122
|
+
if (
|
|
2123
|
+
!isProjectScopedRefPayload(payload) ||
|
|
2124
|
+
payload.projectId !== projectId
|
|
2125
|
+
) {
|
|
2126
|
+
return c.json(
|
|
2127
|
+
{ error: "Not Found", message: "Evidence ref not found" },
|
|
2128
|
+
404,
|
|
2129
|
+
);
|
|
2130
|
+
}
|
|
2131
|
+
if (payload.kind !== "logs") {
|
|
2132
|
+
return c.json(
|
|
2133
|
+
{
|
|
2134
|
+
error: "Bad Request",
|
|
2135
|
+
message: "Search is currently supported for log refs only.",
|
|
2136
|
+
},
|
|
2137
|
+
400,
|
|
2138
|
+
);
|
|
2139
|
+
}
|
|
2140
|
+
if (!hasTraceId(payload)) {
|
|
2141
|
+
return c.json(
|
|
2142
|
+
{ error: "Not Found", message: "Evidence ref not found" },
|
|
2143
|
+
404,
|
|
2144
|
+
);
|
|
2145
|
+
}
|
|
2146
|
+
const body = await parseBundleRequest(c.req.raw);
|
|
2147
|
+
const query = typeof body.query === "string" ? body.query.trim() : "";
|
|
2148
|
+
if (!query) {
|
|
2149
|
+
return c.json(
|
|
2150
|
+
{ error: "Bad Request", message: "query is required" },
|
|
2151
|
+
400,
|
|
2152
|
+
);
|
|
2153
|
+
}
|
|
2154
|
+
const limit = clampInt(body.limit, 1, 100, 20);
|
|
2155
|
+
const logs = await new LogsStore(db).getLogs({
|
|
2156
|
+
projectId,
|
|
2157
|
+
traceId: payload.traceId,
|
|
2158
|
+
hours: payload.hours ?? DEFAULT_HOURS,
|
|
2159
|
+
limit,
|
|
2160
|
+
search: query,
|
|
2161
|
+
});
|
|
2162
|
+
await recordRefExpansion(db, {
|
|
2163
|
+
projectId,
|
|
2164
|
+
refId,
|
|
2165
|
+
kind: payload.kind,
|
|
2166
|
+
operation: "search",
|
|
2167
|
+
resultStatus: "ok",
|
|
2168
|
+
limit,
|
|
2169
|
+
query,
|
|
2170
|
+
});
|
|
2171
|
+
return c.json({
|
|
2172
|
+
refId,
|
|
2173
|
+
kind: "logs",
|
|
2174
|
+
query,
|
|
2175
|
+
data: logs,
|
|
2176
|
+
});
|
|
2177
|
+
});
|
|
2178
|
+
},
|
|
2179
|
+
};
|