oh-my-knowledge 0.48.0 → 0.49.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -18
- package/README.zh.md +55 -23
- package/dist/analysis/coverage-analyzer.d.ts +1 -0
- package/dist/analysis/coverage-analyzer.js +125 -62
- package/dist/analysis/failure-clusterer.js +2 -1
- package/dist/analysis/gap-analyzer.d.ts +2 -2
- package/dist/analysis/gap-analyzer.js +13 -3
- package/dist/analysis/hedging-classifier.d.ts +2 -2
- package/dist/analysis/hedging-classifier.js +3 -4
- package/dist/analysis/report-diagnostics.js +9 -7
- package/dist/analysis/sample-diagnostics.js +6 -6
- package/dist/artifact-graph/doctor.js +15 -7
- package/dist/assets/agent-skills/omk/SKILL.md +27 -7
- package/dist/assets/agent-skills/omk/references/commands.md +18 -17
- package/dist/authoring/evolver.d.ts +10 -6
- package/dist/authoring/evolver.js +496 -83
- package/dist/authoring/generator.d.ts +3 -3
- package/dist/authoring/generator.js +5 -10
- package/dist/authoring/sample-fixer.d.ts +8 -6
- package/dist/authoring/sample-fixer.js +76 -5
- package/dist/cli/commands/doctor.js +31 -14
- package/dist/cli/commands/eval/index.d.ts +3 -0
- package/dist/cli/commands/eval/index.js +163 -17
- package/dist/cli/commands/evolve.d.ts +4 -4
- package/dist/cli/commands/evolve.js +27 -13
- package/dist/cli/commands/init.js +16 -3
- package/dist/cli/commands/observe/inbox.js +28 -21
- package/dist/cli/commands/observe/index.js +20 -11
- package/dist/cli/commands/observe/ingest.d.ts +3 -0
- package/dist/cli/commands/observe/ingest.js +30 -2
- package/dist/cli/commands/sample.d.ts +6 -3
- package/dist/cli/commands/sample.js +72 -68
- package/dist/cli/lib/codex-model-hint.d.ts +9 -0
- package/dist/cli/lib/codex-model-hint.js +45 -0
- package/dist/cli/lib/generation-failure-hint.d.ts +2 -0
- package/dist/cli/lib/generation-failure-hint.js +61 -0
- package/dist/cli/lib/i18n-dict/common.d.ts +1 -1
- package/dist/cli/lib/i18n-dict/common.js +4 -0
- package/dist/cli/lib/i18n-dict/gen.d.ts +1 -1
- package/dist/cli/lib/i18n-dict/gen.js +38 -6
- package/dist/cli/lib/i18n-dict/help.js +6 -6
- package/dist/cli/lib/i18n-dict/init.d.ts +1 -1
- package/dist/cli/lib/i18n-dict/init.js +13 -9
- package/dist/cli/lib/i18n-dict/run.d.ts +1 -1
- package/dist/cli/lib/i18n-dict/run.js +34 -2
- package/dist/cli/lib/llm-failure-classifier.d.ts +2 -0
- package/dist/cli/lib/llm-failure-classifier.js +8 -0
- package/dist/cli/lib/parse-run-config.d.ts +6 -5
- package/dist/cli/lib/parse-run-config.js +16 -9
- package/dist/cli/lib/runtime-defaults.d.ts +21 -0
- package/dist/cli/lib/runtime-defaults.js +79 -0
- package/dist/diagnosis/observe-mapper.js +14 -15
- package/dist/diagnosis/observe-producer.js +3 -1
- package/dist/diagnosis/studio-projection.js +14 -7
- package/dist/diagnosis/types.d.ts +2 -0
- package/dist/diagnosis/types.js +12 -0
- package/dist/doctor/endpoint-rule.js +2 -1
- package/dist/eval-core/artifact-file-names.js +18 -1
- package/dist/eval-core/artifact-index.d.ts +7 -11
- package/dist/eval-core/artifact-index.js +139 -80
- package/dist/eval-core/cache.d.ts +12 -3
- package/dist/eval-core/cache.js +89 -29
- package/dist/eval-core/comparability.js +10 -6
- package/dist/eval-core/evaluation-execution.d.ts +2 -1
- package/dist/eval-core/evaluation-execution.js +122 -37
- package/dist/eval-core/evaluation-job.d.ts +4 -1
- package/dist/eval-core/evaluation-job.js +4 -1
- package/dist/eval-core/evaluation-reporting.d.ts +15 -13
- package/dist/eval-core/evaluation-reporting.js +54 -52
- package/dist/eval-core/execution-strategy.d.ts +2 -0
- package/dist/eval-core/execution-strategy.js +11 -9
- package/dist/eval-core/fact-checker.js +15 -7
- package/dist/eval-core/holdout.js +3 -2
- package/dist/eval-core/judge-independence.d.ts +2 -2
- package/dist/eval-core/mock-hook.cjs +23 -6
- package/dist/eval-core/mocks-runtime.js +30 -8
- package/dist/eval-core/report-document.d.ts +12 -0
- package/dist/eval-core/report-document.js +1151 -0
- package/dist/eval-core/report-extensions.d.ts +4 -0
- package/dist/eval-core/report-extensions.js +500 -0
- package/dist/eval-core/report-file-migration.js +7 -2
- package/dist/eval-core/resume-compatibility.d.ts +31 -0
- package/dist/eval-core/resume-compatibility.js +141 -0
- package/dist/eval-core/sample-fingerprint.d.ts +12 -0
- package/dist/eval-core/sample-fingerprint.js +193 -0
- package/dist/eval-core/schema.js +86 -31
- package/dist/eval-core/verdict.d.ts +8 -4
- package/dist/eval-core/verdict.js +24 -10
- package/dist/eval-workflows/batch-evaluation-workflow.d.ts +2 -1
- package/dist/eval-workflows/batch-evaluation-workflow.js +25 -12
- package/dist/eval-workflows/evaluation-pipeline/preflight-warnings.d.ts +10 -5
- package/dist/eval-workflows/evaluation-pipeline/preflight-warnings.js +58 -21
- package/dist/eval-workflows/evaluation-pipeline/report-finalize.js +3 -1
- package/dist/eval-workflows/evaluation-pipeline/run-state.d.ts +4 -1
- package/dist/eval-workflows/evaluation-pipeline/run-state.js +4 -1
- package/dist/eval-workflows/evaluation-pipeline/test-set-hash.d.ts +6 -5
- package/dist/eval-workflows/evaluation-pipeline/test-set-hash.js +17 -10
- package/dist/eval-workflows/evaluation-pipeline.js +12 -7
- package/dist/eval-workflows/run-evaluation.d.ts +9 -7
- package/dist/eval-workflows/run-evaluation.js +79 -51
- package/dist/executors/anthropic-api.js +65 -9
- package/dist/executors/claude-cli.js +16 -79
- package/dist/executors/claude-protocol.d.ts +28 -0
- package/dist/executors/claude-protocol.js +180 -0
- package/dist/executors/claude-sdk-trace.js +56 -28
- package/dist/executors/claude-sdk.d.ts +1 -0
- package/dist/executors/claude-sdk.js +39 -93
- package/dist/executors/codex-cli-trace.js +166 -31
- package/dist/executors/codex-cli.d.ts +6 -8
- package/dist/executors/codex-cli.js +49 -151
- package/dist/executors/codex-protocol.d.ts +24 -0
- package/dist/executors/codex-protocol.js +234 -0
- package/dist/executors/codex-sdk.js +68 -120
- package/dist/executors/gemini.js +88 -13
- package/dist/executors/index.d.ts +2 -3
- package/dist/executors/index.js +5 -3
- package/dist/executors/openai-api.js +70 -9
- package/dist/executors/runtime-fingerprint.js +88 -11
- package/dist/executors/script-command.d.ts +8 -0
- package/dist/executors/script-command.js +87 -0
- package/dist/executors/script.js +202 -29
- package/dist/executors/shared.d.ts +35 -3
- package/dist/executors/shared.js +113 -15
- package/dist/grading/assertions.d.ts +1 -1
- package/dist/grading/assertions.js +19 -9
- package/dist/grading/diagnostic.d.ts +9 -2
- package/dist/grading/diagnostic.js +25 -2
- package/dist/grading/index.js +10 -4
- package/dist/grading/judge.js +19 -6
- package/dist/grading/layered-scores.d.ts +2 -3
- package/dist/grading/layered-scores.js +2 -3
- package/dist/inputs/load-samples.d.ts +1 -2
- package/dist/inputs/load-samples.js +23 -1
- package/dist/inputs/mcp-resolver.js +6 -3
- package/dist/inputs/sample-document.d.ts +11 -0
- package/dist/inputs/sample-document.js +96 -0
- package/dist/managed/evidence.d.ts +1 -0
- package/dist/managed/evidence.js +1 -1
- package/dist/managed/store.js +200 -91
- package/dist/observability/codex-trace-adapter.d.ts +5 -0
- package/dist/observability/codex-trace-adapter.js +850 -0
- package/dist/observability/experience.d.ts +32 -6
- package/dist/observability/experience.js +2695 -459
- package/dist/observability/feedback-matchers.js +16 -1
- package/dist/observability/inbox-view-model.d.ts +1 -1
- package/dist/observability/inbox-view-model.js +19 -14
- package/dist/observability/inbox.d.ts +7 -1
- package/dist/observability/inbox.js +632 -124
- package/dist/observability/problem-patterns.js +2 -0
- package/dist/observability/review-state.d.ts +6 -0
- package/dist/observability/review-state.js +235 -63
- package/dist/observability/skill-chain-advisories.js +1 -1
- package/dist/observability/skill-chain.js +17 -4
- package/dist/observability/skill-health-analyzer.d.ts +32 -7
- package/dist/observability/skill-health-analyzer.js +194 -121
- package/dist/observability/skill-health-report.d.ts +10 -0
- package/dist/observability/skill-health-report.js +620 -0
- package/dist/observability/soft-standards/constants.d.ts +0 -1
- package/dist/observability/soft-standards/constants.js +0 -1
- package/dist/observability/soft-standards/index.d.ts +1 -1
- package/dist/observability/soft-standards/index.js +1 -1
- package/dist/observability/soft-standards/llm-extractor.js +8 -10
- package/dist/observability/soft-standards/skill-standards-store.d.ts +2 -1
- package/dist/observability/soft-standards/skill-standards-store.js +59 -18
- package/dist/observability/soft-standards/types.d.ts +2 -2
- package/dist/observability/trace-adapter.d.ts +12 -7
- package/dist/observability/trace-adapter.js +11 -9
- package/dist/observability/trace-attribution.d.ts +13 -5
- package/dist/observability/trace-attribution.js +315 -21
- package/dist/observability/trace-ingestion.d.ts +9 -0
- package/dist/observability/trace-ingestion.js +80 -0
- package/dist/observability/trace-ir.d.ts +113 -0
- package/dist/observability/trace-ir.js +87 -0
- package/dist/observability/trace-segmenter.d.ts +19 -6
- package/dist/observability/trace-segmenter.js +377 -196
- package/dist/observability/trace-session-index.d.ts +19 -0
- package/dist/observability/trace-session-index.js +68 -0
- package/dist/observability/trace-source.d.ts +12 -4
- package/dist/observability/trace-source.js +939 -215
- package/dist/renderer/html-renderer.js +37 -6
- package/dist/renderer/icons.js +3 -0
- package/dist/renderer/observation-inbox-renderer.js +208 -90
- package/dist/renderer/skill-detail-renderer.js +452 -109
- package/dist/renderer/skill-health-renderer.js +69 -12
- package/dist/renderer/summary.js +28 -7
- package/dist/renderer/table.js +21 -4
- package/dist/renderer/test-view.d.ts +1 -0
- package/dist/renderer/test-view.js +44 -9
- package/dist/server/indexed-report-store.js +14 -18
- package/dist/server/job-store.js +64 -26
- package/dist/server/report-server.js +190 -78
- package/dist/server/report-store.js +57 -80
- package/dist/server/skill-index.js +143 -49
- package/dist/server/skill-insights.js +44 -5
- package/dist/shared/artifact-graph.d.ts +3 -0
- package/dist/shared/artifact-graph.js +224 -0
- package/dist/shared/assertion-types.d.ts +8 -0
- package/dist/shared/assertion-types.js +46 -0
- package/dist/shared/atomic-json.d.ts +8 -0
- package/dist/shared/atomic-json.js +33 -0
- package/dist/shared/diagnosis-schema.d.ts +9 -0
- package/dist/shared/diagnosis-schema.js +181 -0
- package/dist/shared/doctor-report.d.ts +3 -0
- package/dist/shared/doctor-report.js +103 -0
- package/dist/shared/evaluation-job.d.ts +6 -0
- package/dist/shared/evaluation-job.js +217 -0
- package/dist/shared/executor-result.d.ts +17 -0
- package/dist/shared/executor-result.js +221 -0
- package/dist/shared/file-lock.d.ts +12 -0
- package/dist/shared/file-lock.js +129 -0
- package/dist/shared/json-value.d.ts +5 -0
- package/dist/shared/json-value.js +36 -0
- package/dist/shared/keyed-mutex.d.ts +7 -0
- package/dist/shared/keyed-mutex.js +24 -0
- package/dist/shared/record-count.d.ts +8 -0
- package/dist/shared/record-count.js +43 -0
- package/dist/shared/sample-contract.d.ts +3 -0
- package/dist/shared/sample-contract.js +332 -0
- package/dist/shared/timestamp.d.ts +6 -0
- package/dist/shared/timestamp.js +64 -0
- package/dist/shared/token-usage.d.ts +19 -0
- package/dist/shared/token-usage.js +50 -0
- package/dist/shared/tool-call-status.d.ts +8 -0
- package/dist/shared/tool-call-status.js +28 -0
- package/dist/shared/tool-identity.d.ts +21 -0
- package/dist/shared/tool-identity.js +84 -0
- package/dist/shared/tool-search.js +73 -16
- package/dist/shared/trace-projection.d.ts +5 -0
- package/dist/shared/trace-projection.js +20 -0
- package/dist/shared/trace-source-kind.d.ts +3 -0
- package/dist/shared/trace-source-kind.js +12 -0
- package/dist/types/diagnosis.d.ts +2 -0
- package/dist/types/eval.d.ts +4 -0
- package/dist/types/executor.d.ts +32 -5
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/judge.d.ts +2 -0
- package/dist/types/observability.d.ts +116 -9
- package/dist/types/report.d.ts +58 -6
- package/dist/types/skill-index.d.ts +7 -0
- package/dist/types/trace.d.ts +2 -0
- package/dist/types/trace.js +1 -0
- package/package.json +9 -5
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
+
import { basename } from 'node:path';
|
|
3
|
+
import { isTraceSourceKind as isObservationSourceKind } from '../shared/trace-source-kind.js';
|
|
2
4
|
import { buildExperienceProblemPatterns, mergeExperienceProblemPatterns, } from './problem-patterns.js';
|
|
3
5
|
import { observationMetricAnnotationVerdict, observationReviewStateKey } from './review-state.js';
|
|
6
|
+
import { correlateTraceToolEvents, normalizeTraceTimestamp, } from './trace-ir.js';
|
|
7
|
+
import { skillSegmentTimestampObserved, UNOBSERVED_TRACE_TIMESTAMP, } from './trace-segmenter.js';
|
|
8
|
+
import { createTraceSessionIndex, traceSessionRefIdentity } from './trace-session-index.js';
|
|
4
9
|
import { extractCommandEnvelopeText, stripCommandEnvelopeText } from './trace-attribution.js';
|
|
5
|
-
import { hasAssistantDeliverableArtifactText, hasAssistantDeliverySignalText, hasUserHardRuleText, isAssistantProgressUpdateText, isAssistantProtocolReplyText,
|
|
10
|
+
import { hasAssistantDeliverableArtifactText, hasAssistantDeliverySignalText, hasUserHardRuleText, isAssistantProgressUpdateText, isAssistantProtocolReplyText, isSyntheticUserMessageText, isToolResultFailureText, isUserInteractionMetricText } from './text-signals.js';
|
|
6
11
|
import { durationMsBetween } from '../shared/time.js';
|
|
12
|
+
import { incrementRecordCount, ownRecordValue, sumRecordCounts as sumSafeCounts, } from '../shared/record-count.js';
|
|
13
|
+
import { checkedSumTokenCounts } from '../shared/token-usage.js';
|
|
7
14
|
import { loadExpectedToolsForSkill, loadFrontmatterSkillType, loadSkillDeclarationCheck, } from './experience-frontmatter.js';
|
|
8
15
|
import { findNegativeFeedbackMatches, findPositiveFeedbackMatches, findUserCorrectionMatches, findUserGoalShiftMatches, hasNegativeFeedbackSignal, hasPositiveFeedbackSignal, hasUserCorrectionSignal, hasUserGoalShiftSignal, } from './feedback-matchers.js';
|
|
9
16
|
export { findNegativeFeedbackMatches, findPositiveFeedbackMatches, findUserCorrectionMatches, findUserGoalShiftMatches, hasNegativeFeedbackSignal, hasPositiveFeedbackSignal, hasUserCorrectionSignal, hasUserGoalShiftSignal, } from './feedback-matchers.js';
|
|
10
|
-
export const OBSERVATION_EXPERIENCE_SCHEMA_VERSION =
|
|
17
|
+
export const OBSERVATION_EXPERIENCE_SCHEMA_VERSION = 3;
|
|
18
|
+
const LEGACY_OBSERVATION_EXPERIENCE_SCHEMA_VERSION = 2;
|
|
11
19
|
export function aggregateExperienceChecklistItemStatus(statuses) {
|
|
12
20
|
if (statuses.includes('degraded'))
|
|
13
21
|
return 'degraded';
|
|
@@ -41,53 +49,63 @@ const ZERO_INDICATORS = {
|
|
|
41
49
|
repeatedExecutionCount: 0,
|
|
42
50
|
toolCallCount: 0,
|
|
43
51
|
toolFailureCount: 0,
|
|
52
|
+
toolCancelledCount: 0,
|
|
53
|
+
toolUnknownCount: 0,
|
|
44
54
|
highObservationCount: 0,
|
|
45
55
|
mediumObservationCount: 0,
|
|
46
56
|
hedgingCount: 0,
|
|
47
57
|
explicitMarkerCount: 0,
|
|
48
58
|
};
|
|
49
59
|
export function buildObservationExperienceReport(input) {
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
60
|
+
const traceSessions = input.sessions.map((session) => ({
|
|
61
|
+
...session,
|
|
62
|
+
events: correlateTraceToolEvents(session.events),
|
|
63
|
+
}));
|
|
64
|
+
const sessionIndex = createTraceSessionIndex(traceSessions);
|
|
65
|
+
const sessionGroupsByKey = groupSessionsByExperienceKey(traceSessions);
|
|
53
66
|
const goalSlices = [];
|
|
54
67
|
const invocations = [];
|
|
55
68
|
for (const segment of input.segments) {
|
|
56
69
|
if (segment.skillName === 'general')
|
|
57
70
|
continue;
|
|
58
|
-
const session =
|
|
71
|
+
const session = sessionIndex.resolve(segment);
|
|
59
72
|
const sourceTrace = segment.sourceTrace ?? session?.sourcePath ?? '';
|
|
60
73
|
const sessionGroupKey = session ? experienceSessionGroupKey(session) : `trace:${sourceTrace || segment.sessionId}`;
|
|
61
74
|
const relatedItems = relatedObservationItems(segment, input.items);
|
|
62
75
|
const bounds = session ? segmentRecordBounds(session, segment) : { start: 0, end: 0 };
|
|
63
|
-
const timeline = session ?
|
|
76
|
+
const timeline = session ? buildInvocationTimeline(session, bounds.start, bounds.end, segment) : [];
|
|
77
|
+
const metricTimeline = metricTimelineForSegment(timeline, segment);
|
|
64
78
|
const metricScopeId = hashParts('session', segment.skillName, sessionGroupKey);
|
|
65
79
|
const userRefs = timeline.filter((event) => event.kind === 'user_message');
|
|
66
|
-
const indicators = indicatorsForSegment(segment, relatedItems,
|
|
80
|
+
const indicators = indicatorsForSegment(segment, relatedItems, metricTimeline, metricScopeId, input.reviewState);
|
|
67
81
|
const observationRefs = relatedItems.map(observationEvidenceRef);
|
|
68
|
-
const evidenceChain = evidenceChainForTimeline(
|
|
69
|
-
const ruleFindings = ruleFindingsForEvidence(indicators,
|
|
82
|
+
const evidenceChain = evidenceChainForTimeline(metricTimeline, observationRefs);
|
|
83
|
+
const ruleFindings = ruleFindingsForEvidence(indicators, metricTimeline, observationRefs, evidenceChain, metricScopeId, input.reviewState);
|
|
70
84
|
const assistiveInference = assistiveInferenceForEvidence(indicators, evidenceChain, ruleFindings);
|
|
71
85
|
const problemPatterns = buildExperienceProblemPatterns({
|
|
72
86
|
skillName: segment.skillName,
|
|
73
|
-
sessionId:
|
|
74
|
-
timeline,
|
|
87
|
+
sessionId: sessionGroupKey,
|
|
88
|
+
timeline: metricTimeline,
|
|
75
89
|
metricScopeId,
|
|
76
90
|
reviewState: input.reviewState,
|
|
77
91
|
});
|
|
78
92
|
const hasGoalShift = userRefs.some((ref) => hasUserGoalShiftSignal(ref.snippet ?? ''));
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
const
|
|
82
|
-
const
|
|
93
|
+
// traceId 是物理证据流身份。sourcePath 不是身份:一个 Markdown 文件可包含多个
|
|
94
|
+
// session,且不同 block 可能复用同一来源 sessionId。
|
|
95
|
+
const evidenceStreamId = traceSessionRefIdentity(segment);
|
|
96
|
+
const traceId = segment.traceId ?? session?.traceId ?? evidenceStreamId;
|
|
97
|
+
const goalSliceId = hashParts('goal', evidenceStreamId, segment.skillName, String(segment.segmentIndex));
|
|
98
|
+
const invocationId = hashParts('invocation', evidenceStreamId, segment.skillName, String(segment.segmentIndex));
|
|
83
99
|
goalSlices.push({
|
|
84
100
|
id: goalSliceId,
|
|
85
101
|
skillName: segment.skillName,
|
|
86
102
|
sessionId: segment.sessionId,
|
|
103
|
+
traceId,
|
|
87
104
|
sourceTrace,
|
|
88
105
|
cwd: segment.cwd,
|
|
89
106
|
startTimestamp: segment.startTimestamp,
|
|
90
107
|
endTimestamp: segment.endTimestamp,
|
|
108
|
+
timestampObserved: skillSegmentTimestampObserved(segment),
|
|
91
109
|
sliceReasonCode: hasGoalShift ? 'explicit_user_goal_shift' : 'skill_segment_boundary',
|
|
92
110
|
sliceConfidence: hasGoalShift ? 'medium' : 'high',
|
|
93
111
|
inferredUserGoal: inferUserGoal(userRefs),
|
|
@@ -98,15 +116,17 @@ export function buildObservationExperienceReport(input) {
|
|
|
98
116
|
skillName: segment.skillName,
|
|
99
117
|
sessionId: segment.sessionId,
|
|
100
118
|
sessionGroupKey,
|
|
119
|
+
traceId,
|
|
101
120
|
sourceTrace,
|
|
102
|
-
sourceKind: segment.sourceKind ??
|
|
103
|
-
entrypoint: session
|
|
104
|
-
sourceMetadata: session?.sourceMetadata,
|
|
121
|
+
sourceKind: segment.sourceKind ?? session?.sourceKind ?? 'unknown',
|
|
122
|
+
entrypoint: session?.entrypoint,
|
|
123
|
+
sourceMetadata: segment.sourceMetadata ?? session?.sourceMetadata,
|
|
105
124
|
cwd: segment.cwd,
|
|
106
125
|
segmentIndex: segment.segmentIndex,
|
|
107
126
|
goalSliceId,
|
|
108
127
|
startTimestamp: segment.startTimestamp,
|
|
109
128
|
endTimestamp: segment.endTimestamp,
|
|
129
|
+
timestampObserved: skillSegmentTimestampObserved(segment),
|
|
110
130
|
attribution: {
|
|
111
131
|
source: segment.attribution?.source ?? 'unknown',
|
|
112
132
|
confidence: segment.attribution?.confidence ?? 0.3,
|
|
@@ -126,11 +146,15 @@ export function buildObservationExperienceReport(input) {
|
|
|
126
146
|
...observationRefs,
|
|
127
147
|
...userRefs.slice(0, 5).map(evidenceRefFromTimeline),
|
|
128
148
|
],
|
|
149
|
+
timelineRef: timelineRefForSessionGroup(sessionGroupKey),
|
|
150
|
+
timelineEventIds: timeline.map((event) => event.id),
|
|
129
151
|
timeline,
|
|
130
152
|
});
|
|
131
153
|
}
|
|
132
154
|
const sessions = summarizeExperienceSessions(invocations, sessionGroupsByKey, input.generatedAt, input.reviewState);
|
|
133
155
|
const skills = summarizeExperienceSkills(sessions, invocations);
|
|
156
|
+
const traceTimelines = traceTimelinesFromSessions(sessions, invocations);
|
|
157
|
+
const storyContexts = storyContextsFromSessions(sessions, invocations);
|
|
134
158
|
return {
|
|
135
159
|
kind: 'observe-experience',
|
|
136
160
|
schemaVersion: OBSERVATION_EXPERIENCE_SCHEMA_VERSION,
|
|
@@ -144,6 +168,8 @@ export function buildObservationExperienceReport(input) {
|
|
|
144
168
|
noteCodes: ['no_llm_judge', 'no_auto_verdict', 'default_goal_slice_is_allowed', 'deterministic_assistive_inference'],
|
|
145
169
|
},
|
|
146
170
|
goalSlices,
|
|
171
|
+
traceTimelines,
|
|
172
|
+
storyContexts,
|
|
147
173
|
invocations,
|
|
148
174
|
sessions,
|
|
149
175
|
skills,
|
|
@@ -156,54 +182,2043 @@ export function normalizeObservationExperienceReport(value) {
|
|
|
156
182
|
const kind = report.kind === 'observe-experience' ? report.kind : null;
|
|
157
183
|
if (!kind)
|
|
158
184
|
return null;
|
|
159
|
-
if (report.schemaVersion !== OBSERVATION_EXPERIENCE_SCHEMA_VERSION
|
|
185
|
+
if (report.schemaVersion !== OBSERVATION_EXPERIENCE_SCHEMA_VERSION
|
|
186
|
+
&& report.schemaVersion !== LEGACY_OBSERVATION_EXPERIENCE_SCHEMA_VERSION)
|
|
160
187
|
return null;
|
|
188
|
+
const isLegacyReport = report.schemaVersion === LEGACY_OBSERVATION_EXPERIENCE_SCHEMA_VERSION;
|
|
161
189
|
if (report.scope !== 'evidence-only')
|
|
162
190
|
return null;
|
|
163
|
-
if (
|
|
191
|
+
if (!isTimestamp(report.generatedAt)
|
|
192
|
+
|| !report.meta
|
|
193
|
+
|| typeof report.meta !== 'object'
|
|
194
|
+
|| !isExperienceMeta(report.meta))
|
|
164
195
|
return null;
|
|
165
196
|
if (!Array.isArray(report.goalSlices) || !Array.isArray(report.invocations) || !Array.isArray(report.sessions) || !Array.isArray(report.skills)) {
|
|
166
197
|
return null;
|
|
167
198
|
}
|
|
168
|
-
|
|
199
|
+
const invocations = normalizeExperienceInvocationShells(report.invocations, !isLegacyReport);
|
|
200
|
+
const sessions = normalizeExperienceSessionShells(report.sessions, !isLegacyReport);
|
|
201
|
+
if (!invocations || !sessions)
|
|
202
|
+
return null;
|
|
203
|
+
if (!isLegacyReport
|
|
204
|
+
&& (!Array.isArray(report.traceTimelines) || !Array.isArray(report.storyContexts)))
|
|
205
|
+
return null;
|
|
206
|
+
const traceTimelines = Array.isArray(report.traceTimelines)
|
|
207
|
+
? normalizeTraceTimelines(report.traceTimelines)
|
|
208
|
+
: traceTimelinesFromSessions(sessions, invocations);
|
|
209
|
+
const storyContexts = Array.isArray(report.storyContexts)
|
|
210
|
+
? normalizeStoryContexts(report.storyContexts)
|
|
211
|
+
: storyContextsFromSessions(sessions, invocations);
|
|
212
|
+
if (!traceTimelines || !storyContexts)
|
|
213
|
+
return null;
|
|
214
|
+
const normalized = {
|
|
169
215
|
kind: 'observe-experience',
|
|
170
216
|
schemaVersion: OBSERVATION_EXPERIENCE_SCHEMA_VERSION,
|
|
171
217
|
scope: 'evidence-only',
|
|
172
218
|
generatedAt: report.generatedAt,
|
|
173
219
|
meta: report.meta,
|
|
174
220
|
goalSlices: report.goalSlices,
|
|
175
|
-
|
|
176
|
-
|
|
221
|
+
traceTimelines,
|
|
222
|
+
storyContexts,
|
|
223
|
+
invocations,
|
|
224
|
+
sessions,
|
|
177
225
|
skills: report.skills,
|
|
178
226
|
};
|
|
227
|
+
const hydrated = hydrateExperienceTimelines(normalized);
|
|
228
|
+
try {
|
|
229
|
+
if (!validateExperienceReferences(hydrated, !isLegacyReport))
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
return hydrated;
|
|
236
|
+
}
|
|
237
|
+
function normalizeExperienceInvocationShells(values, strict) {
|
|
238
|
+
const records = values.filter(isObjectRecord);
|
|
239
|
+
if (records.length !== values.length)
|
|
240
|
+
return null;
|
|
241
|
+
if (records.some((value) => typeof value.id !== 'string'
|
|
242
|
+
|| typeof value.skillName !== 'string'
|
|
243
|
+
|| typeof value.sessionId !== 'string'
|
|
244
|
+
|| typeof value.sessionGroupKey !== 'string'
|
|
245
|
+
|| (strict ? typeof value.traceId !== 'string' : !isOptionalString(value.traceId))
|
|
246
|
+
|| typeof value.sourceTrace !== 'string'
|
|
247
|
+
|| !isObservationSourceKind(value.sourceKind)
|
|
248
|
+
|| !isOptionalString(value.entrypoint)
|
|
249
|
+
|| !isOptionalTraceSourceMetadata(value.sourceMetadata)
|
|
250
|
+
|| !isOptionalString(value.cwd)
|
|
251
|
+
|| !isNonNegativeInteger(value.segmentIndex)
|
|
252
|
+
|| typeof value.goalSliceId !== 'string'
|
|
253
|
+
|| !isTimestampRange(value.startTimestamp, value.endTimestamp)
|
|
254
|
+
|| (value.timestampObserved !== undefined && typeof value.timestampObserved !== 'boolean')
|
|
255
|
+
|| (strict && typeof value.timestampObserved !== 'boolean')
|
|
256
|
+
|| !isExperienceAttribution(value.attribution)
|
|
257
|
+
|| !isExperienceInvocationMetrics(value.metrics, strict)
|
|
258
|
+
|| !isCountRecord(value.toolCounts)
|
|
259
|
+
|| !isExperienceIndicators(value.indicators, strict)
|
|
260
|
+
|| !isExperienceEvidenceChain(value.evidenceChain)
|
|
261
|
+
|| !isExperienceRuleFindingArray(value.ruleFindings)
|
|
262
|
+
|| !isExperienceAssistiveInference(value.assistiveInference)
|
|
263
|
+
|| !isExperienceProblemPatternArray(value.problemPatterns)
|
|
264
|
+
|| !isStringArray(value.relatedObservationIds)
|
|
265
|
+
|| !isExperienceEvidenceRefArray(value.evidenceRefs)
|
|
266
|
+
|| (strict
|
|
267
|
+
? typeof value.timelineRef !== 'string'
|
|
268
|
+
|| !isStringArray(value.timelineEventIds)
|
|
269
|
+
|| (value.timeline !== undefined && !isTimelineEventArray(value.timeline))
|
|
270
|
+
: !isOptionalString(value.timelineRef)
|
|
271
|
+
|| (value.timelineEventIds !== undefined && !isStringArray(value.timelineEventIds))
|
|
272
|
+
|| !isTimelineEventArray(value.timeline))))
|
|
273
|
+
return null;
|
|
274
|
+
return records.map((value) => ({
|
|
275
|
+
...value,
|
|
276
|
+
metrics: {
|
|
277
|
+
...value.metrics,
|
|
278
|
+
tokenUsageObserved: strict
|
|
279
|
+
? value.metrics.tokenUsageObserved
|
|
280
|
+
: false,
|
|
281
|
+
},
|
|
282
|
+
timeline: isTimelineEventArray(value.timeline)
|
|
283
|
+
? value.timeline
|
|
284
|
+
: [],
|
|
285
|
+
}));
|
|
286
|
+
}
|
|
287
|
+
function normalizeExperienceSessionShells(values, strict) {
|
|
288
|
+
const records = values.filter(isObjectRecord);
|
|
289
|
+
if (records.length !== values.length)
|
|
290
|
+
return null;
|
|
291
|
+
if (records.some((value) => typeof value.id !== 'string'
|
|
292
|
+
|| typeof value.skillName !== 'string'
|
|
293
|
+
|| typeof value.sessionId !== 'string'
|
|
294
|
+
|| typeof value.sourceTrace !== 'string'
|
|
295
|
+
|| !isObservationSourceKind(value.sourceKind)
|
|
296
|
+
|| !isOptionalString(value.entrypoint)
|
|
297
|
+
|| !isOptionalTraceSourceMetadata(value.sourceMetadata)
|
|
298
|
+
|| !isOptionalString(value.cwd)
|
|
299
|
+
|| !isConsistentSourceSessionTime(value)
|
|
300
|
+
|| !isTimestampRange(value.startTimestamp, value.endTimestamp)
|
|
301
|
+
|| !isStringArray(value.invocationIds)
|
|
302
|
+
|| !isOptionalTimestampCoverage(value.timestampedInvocationCount, value.timestampCoverage, Array.isArray(value.invocationIds) ? value.invocationIds.length : -1)
|
|
303
|
+
|| (strict
|
|
304
|
+
&& (!isNonNegativeInteger(value.timestampedInvocationCount)
|
|
305
|
+
|| !isRate(value.timestampCoverage)))
|
|
306
|
+
|| !isStringArray(value.goalSliceIds)
|
|
307
|
+
|| !isExperienceReviewPriority(value.reviewPriority)
|
|
308
|
+
|| typeof value.reviewPriorityScore !== 'number'
|
|
309
|
+
|| !Number.isFinite(value.reviewPriorityScore)
|
|
310
|
+
|| value.reviewPriorityScore < 0
|
|
311
|
+
|| !isEnumArray(value.reviewBasisCodes, [
|
|
312
|
+
'has_high_observation',
|
|
313
|
+
'has_medium_observation',
|
|
314
|
+
'user_correction',
|
|
315
|
+
'user_interruption',
|
|
316
|
+
'session_interrupted',
|
|
317
|
+
'negative_feedback',
|
|
318
|
+
'hard_rule_text_hit',
|
|
319
|
+
'tool_failure',
|
|
320
|
+
'hedging_signal',
|
|
321
|
+
'explicit_marker',
|
|
322
|
+
])
|
|
323
|
+
|| !isExperienceIndicators(value.indicators, strict)
|
|
324
|
+
|| !isExperienceEvidenceChain(value.evidenceChain)
|
|
325
|
+
|| !isExperienceRuleFindingArray(value.ruleFindings)
|
|
326
|
+
|| !isExperienceAssistiveInference(value.assistiveInference)
|
|
327
|
+
|| !isExperienceProblemPatternArray(value.problemPatterns)
|
|
328
|
+
|| !isStringArray(value.relatedObservationIds)
|
|
329
|
+
|| !(strict
|
|
330
|
+
? isExperienceTimelineScope(value.timelineScope)
|
|
331
|
+
: isLegacyExperienceTimelineScope(value.timelineScope))
|
|
332
|
+
|| !isStringArray(value.attributionSources)
|
|
333
|
+
|| !isStringArray(value.pluginNames)
|
|
334
|
+
|| !isStringArray(value.rawSkillRefs)
|
|
335
|
+
|| !isStringArray(value.commandNames)
|
|
336
|
+
|| (strict
|
|
337
|
+
? typeof value.timelineRef !== 'string'
|
|
338
|
+
|| !isStringArray(value.timelinePreviewEventIds)
|
|
339
|
+
|| (value.timelinePreview !== undefined && !isTimelineEventArray(value.timelinePreview))
|
|
340
|
+
|| (value.fullSessionTimeline !== undefined && !isTimelineEventArray(value.fullSessionTimeline))
|
|
341
|
+
: !isOptionalString(value.timelineRef)
|
|
342
|
+
|| (value.timelinePreviewEventIds !== undefined
|
|
343
|
+
&& !isStringArray(value.timelinePreviewEventIds))
|
|
344
|
+
|| !isTimelineEventArray(value.timelinePreview)
|
|
345
|
+
|| !isTimelineEventArray(value.fullSessionTimeline))
|
|
346
|
+
|| (value.timelineTree !== undefined && !isTimelineTree(value.timelineTree))
|
|
347
|
+
|| (value.sessionStory !== undefined
|
|
348
|
+
&& !isExperienceSessionStory(value.sessionStory, strict))
|
|
349
|
+
|| (value.reviewerReport !== undefined
|
|
350
|
+
&& !isExperienceReviewerReport(value.reviewerReport, strict))))
|
|
351
|
+
return null;
|
|
352
|
+
return records.map((value) => {
|
|
353
|
+
const sessionStory = isObjectRecord(value.sessionStory)
|
|
354
|
+
? {
|
|
355
|
+
...value.sessionStory,
|
|
356
|
+
goalSlices: Array.isArray(value.sessionStory.goalSlices) ? value.sessionStory.goalSlices : [],
|
|
357
|
+
subagentDispatches: Array.isArray(value.sessionStory.subagentDispatches)
|
|
358
|
+
? value.sessionStory.subagentDispatches
|
|
359
|
+
: [],
|
|
360
|
+
episodes: Array.isArray(value.sessionStory.episodes) ? value.sessionStory.episodes : [],
|
|
361
|
+
}
|
|
362
|
+
: undefined;
|
|
363
|
+
return {
|
|
364
|
+
...value,
|
|
365
|
+
invocationIds: Array.isArray(value.invocationIds) ? value.invocationIds : [],
|
|
366
|
+
timelinePreview: Array.isArray(value.timelinePreview) ? value.timelinePreview : [],
|
|
367
|
+
fullSessionTimeline: Array.isArray(value.fullSessionTimeline) ? value.fullSessionTimeline : [],
|
|
368
|
+
sessionStory,
|
|
369
|
+
};
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
function normalizeTraceTimelines(values) {
|
|
373
|
+
if (values.some((value) => {
|
|
374
|
+
if (!isObjectRecord(value)
|
|
375
|
+
|| typeof value.id !== 'string'
|
|
376
|
+
|| typeof value.sessionGroupKey !== 'string'
|
|
377
|
+
|| typeof value.sessionId !== 'string'
|
|
378
|
+
|| !isNonNegativeInteger(value.eventCount))
|
|
379
|
+
return true;
|
|
380
|
+
return !isTimelineTree(value.tree);
|
|
381
|
+
}))
|
|
382
|
+
return null;
|
|
383
|
+
return values;
|
|
384
|
+
}
|
|
385
|
+
function normalizeStoryContexts(values) {
|
|
386
|
+
if (values.some((value) => !isExperienceStoryContext(value)))
|
|
387
|
+
return null;
|
|
388
|
+
return values;
|
|
389
|
+
}
|
|
390
|
+
function isObjectRecord(value) {
|
|
391
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
392
|
+
}
|
|
393
|
+
function isNonNegativeInteger(value) {
|
|
394
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
|
|
395
|
+
}
|
|
396
|
+
function isRate(value) {
|
|
397
|
+
return typeof value === 'number' && Number.isFinite(value) && value >= 0 && value <= 1;
|
|
398
|
+
}
|
|
399
|
+
function isOptionalTimestampCoverage(count, coverage, total) {
|
|
400
|
+
if (count === undefined && coverage === undefined)
|
|
401
|
+
return true;
|
|
402
|
+
if (total < 0
|
|
403
|
+
|| !isNonNegativeInteger(count)
|
|
404
|
+
|| count > total
|
|
405
|
+
|| !isRate(coverage))
|
|
406
|
+
return false;
|
|
407
|
+
return coverage === (total > 0 ? count / total : 0);
|
|
408
|
+
}
|
|
409
|
+
function isStringArray(value) {
|
|
410
|
+
return Array.isArray(value) && value.every((item) => typeof item === 'string');
|
|
411
|
+
}
|
|
412
|
+
function isOptionalString(value) {
|
|
413
|
+
return value === undefined || typeof value === 'string';
|
|
414
|
+
}
|
|
415
|
+
function isTimestamp(value) {
|
|
416
|
+
return typeof value === 'string' && normalizeTraceTimestamp(value) !== undefined;
|
|
417
|
+
}
|
|
418
|
+
function isOptionalTimestamp(value) {
|
|
419
|
+
return value === undefined || isTimestamp(value);
|
|
420
|
+
}
|
|
421
|
+
function isTimestampRange(start, end) {
|
|
422
|
+
if (!isTimestamp(start) || !isTimestamp(end))
|
|
423
|
+
return false;
|
|
424
|
+
return Date.parse(start) <= Date.parse(end);
|
|
425
|
+
}
|
|
426
|
+
function invocationTimestampObserved(invocation) {
|
|
427
|
+
return invocation.timestampObserved
|
|
428
|
+
?? invocation.startTimestamp !== UNOBSERVED_TRACE_TIMESTAMP;
|
|
429
|
+
}
|
|
430
|
+
function sessionTimestampedInvocationCount(session) {
|
|
431
|
+
return session.timestampedInvocationCount
|
|
432
|
+
?? (session.startTimestamp === UNOBSERVED_TRACE_TIMESTAMP
|
|
433
|
+
? 0
|
|
434
|
+
: session.invocationIds.length);
|
|
435
|
+
}
|
|
436
|
+
function isConsistentSourceSessionTime(value) {
|
|
437
|
+
const start = value.sourceSessionStartTimestamp;
|
|
438
|
+
const end = value.sourceSessionEndTimestamp;
|
|
439
|
+
const duration = value.sourceSessionDurationMs;
|
|
440
|
+
if (!isOptionalTimestamp(start) || !isOptionalTimestamp(end))
|
|
441
|
+
return false;
|
|
442
|
+
if (duration !== undefined && !isNonNegativeInteger(duration))
|
|
443
|
+
return false;
|
|
444
|
+
if (start === undefined || end === undefined)
|
|
445
|
+
return duration === undefined;
|
|
446
|
+
return typeof start === 'string'
|
|
447
|
+
&& typeof end === 'string'
|
|
448
|
+
&& isTimestampRange(start, end)
|
|
449
|
+
&& duration === durationMsBetween(start, end);
|
|
450
|
+
}
|
|
451
|
+
function isOptionalNonNegativeInteger(value) {
|
|
452
|
+
return value === undefined || isNonNegativeInteger(value);
|
|
453
|
+
}
|
|
454
|
+
function isEnumValue(value, values) {
|
|
455
|
+
return typeof value === 'string' && values.includes(value);
|
|
456
|
+
}
|
|
457
|
+
function isEnumArray(value, values) {
|
|
458
|
+
return Array.isArray(value) && value.every((item) => isEnumValue(item, values));
|
|
459
|
+
}
|
|
460
|
+
function isExperienceMeta(value) {
|
|
461
|
+
if (!isObjectRecord(value)
|
|
462
|
+
|| !isNonNegativeInteger(value.sessionCount)
|
|
463
|
+
|| !isNonNegativeInteger(value.skillCount)
|
|
464
|
+
|| !isNonNegativeInteger(value.invocationCount)
|
|
465
|
+
|| !isNonNegativeInteger(value.goalSliceCount))
|
|
466
|
+
return false;
|
|
467
|
+
return isEnumArray(value.noteCodes, [
|
|
468
|
+
'no_llm_judge',
|
|
469
|
+
'no_auto_verdict',
|
|
470
|
+
'default_goal_slice_is_allowed',
|
|
471
|
+
'deterministic_assistive_inference',
|
|
472
|
+
]);
|
|
473
|
+
}
|
|
474
|
+
function isOptionalTraceSourceMetadata(value) {
|
|
475
|
+
if (value === undefined)
|
|
476
|
+
return true;
|
|
477
|
+
return isObjectRecord(value)
|
|
478
|
+
&& isOptionalString(value.channel)
|
|
479
|
+
&& isOptionalString(value.sender)
|
|
480
|
+
&& isOptionalString(value.senderId)
|
|
481
|
+
&& isOptionalString(value.provider)
|
|
482
|
+
&& isOptionalString(value.model)
|
|
483
|
+
&& isOptionalString(value.modelApi)
|
|
484
|
+
&& (value.businessActions === undefined || isStringArray(value.businessActions));
|
|
485
|
+
}
|
|
486
|
+
function isExperienceReviewPriority(value) {
|
|
487
|
+
return value === 'review_first' || value === 'sample_review' || value === 'routine_sample';
|
|
488
|
+
}
|
|
489
|
+
function isExperienceEvidenceKind(value) {
|
|
490
|
+
return isEnumValue(value, [
|
|
491
|
+
'user_message',
|
|
492
|
+
'synthetic_user_event',
|
|
493
|
+
'assistant_message',
|
|
494
|
+
'tool_use',
|
|
495
|
+
'tool_result',
|
|
496
|
+
'skill_context',
|
|
497
|
+
'runtime_context',
|
|
498
|
+
'observation',
|
|
499
|
+
]);
|
|
500
|
+
}
|
|
501
|
+
function isExperienceEvidenceRef(value) {
|
|
502
|
+
if (!isObjectRecord(value)
|
|
503
|
+
|| typeof value.id !== 'string'
|
|
504
|
+
|| !isExperienceEvidenceKind(value.kind)
|
|
505
|
+
|| typeof value.sourceTrace !== 'string'
|
|
506
|
+
|| typeof value.sessionId !== 'string'
|
|
507
|
+
|| !isOptionalNonNegativeInteger(value.messageIndex)
|
|
508
|
+
|| !isOptionalNonNegativeInteger(value.logicalMessageIndex)
|
|
509
|
+
|| !isOptionalNonNegativeInteger(value.sourceLineIndex))
|
|
510
|
+
return false;
|
|
511
|
+
if (value.traceRole !== undefined
|
|
512
|
+
&& !isEnumValue(value.traceRole, ['standalone', 'main', 'subagent']))
|
|
513
|
+
return false;
|
|
514
|
+
if (value.role !== undefined
|
|
515
|
+
&& !isEnumValue(value.role, ['user', 'assistant', 'tool', 'other']))
|
|
516
|
+
return false;
|
|
517
|
+
return [
|
|
518
|
+
value.traceLabel,
|
|
519
|
+
value.traceId,
|
|
520
|
+
value.messageUuid,
|
|
521
|
+
value.callInstanceId,
|
|
522
|
+
value.toolUseId,
|
|
523
|
+
value.label,
|
|
524
|
+
value.snippet,
|
|
525
|
+
].every(isOptionalString)
|
|
526
|
+
&& isOptionalTimestamp(value.timestamp);
|
|
527
|
+
}
|
|
528
|
+
function isExperienceEvidenceRefArray(value) {
|
|
529
|
+
return Array.isArray(value) && value.every(isExperienceEvidenceRef);
|
|
530
|
+
}
|
|
531
|
+
function isExperienceGoalSlice(value) {
|
|
532
|
+
return isObjectRecord(value)
|
|
533
|
+
&& typeof value.id === 'string'
|
|
534
|
+
&& typeof value.skillName === 'string'
|
|
535
|
+
&& typeof value.sessionId === 'string'
|
|
536
|
+
&& isOptionalString(value.traceId)
|
|
537
|
+
&& typeof value.sourceTrace === 'string'
|
|
538
|
+
&& isOptionalString(value.cwd)
|
|
539
|
+
&& isTimestampRange(value.startTimestamp, value.endTimestamp)
|
|
540
|
+
&& (value.timestampObserved === undefined || typeof value.timestampObserved === 'boolean')
|
|
541
|
+
&& isEnumValue(value.sliceReasonCode, [
|
|
542
|
+
'skill_segment_boundary',
|
|
543
|
+
'explicit_user_goal_shift',
|
|
544
|
+
'default_session_slice',
|
|
545
|
+
])
|
|
546
|
+
&& isEnumValue(value.sliceConfidence, ['low', 'medium', 'high'])
|
|
547
|
+
&& isOptionalString(value.inferredUserGoal)
|
|
548
|
+
&& isExperienceEvidenceRefArray(value.userMessageRefs);
|
|
549
|
+
}
|
|
550
|
+
function isExperienceEvidenceChain(value) {
|
|
551
|
+
if (!isObjectRecord(value))
|
|
552
|
+
return false;
|
|
553
|
+
const countKeys = [
|
|
554
|
+
'userMessageCount',
|
|
555
|
+
'runtimeContextCount',
|
|
556
|
+
'skillContextCount',
|
|
557
|
+
'assistantMessageCount',
|
|
558
|
+
'toolUseCount',
|
|
559
|
+
'toolResultCount',
|
|
560
|
+
'toolFailureResultCount',
|
|
561
|
+
'observationCount',
|
|
562
|
+
];
|
|
563
|
+
if (!countKeys.every((key) => isNonNegativeInteger(value[key])))
|
|
564
|
+
return false;
|
|
565
|
+
const optionalRefs = [
|
|
566
|
+
value.firstUserMessage,
|
|
567
|
+
value.firstRuntimeContext,
|
|
568
|
+
value.firstSkillContext,
|
|
569
|
+
value.firstToolUse,
|
|
570
|
+
value.firstToolFailure,
|
|
571
|
+
value.lastAssistantMessage,
|
|
572
|
+
];
|
|
573
|
+
return optionalRefs.every((ref) => ref === undefined || isExperienceEvidenceRef(ref));
|
|
574
|
+
}
|
|
575
|
+
function isExperienceRuleFinding(value) {
|
|
576
|
+
return isObjectRecord(value)
|
|
577
|
+
&& isEnumValue(value.code, [
|
|
578
|
+
'high_observation_seen',
|
|
579
|
+
'medium_observation_seen',
|
|
580
|
+
'user_correction_seen',
|
|
581
|
+
'user_interruption_seen',
|
|
582
|
+
'session_interrupted_seen',
|
|
583
|
+
'negative_feedback_seen',
|
|
584
|
+
'positive_feedback_seen',
|
|
585
|
+
'user_goal_shift_seen',
|
|
586
|
+
'hard_rule_seen',
|
|
587
|
+
'tool_failure_seen',
|
|
588
|
+
'hedging_seen',
|
|
589
|
+
'explicit_marker_seen',
|
|
590
|
+
'runtime_context_excluded',
|
|
591
|
+
'skill_context_excluded',
|
|
592
|
+
'no_priority_signal',
|
|
593
|
+
])
|
|
594
|
+
&& isEnumValue(value.level, ['attention', 'sample', 'normal'])
|
|
595
|
+
&& isNonNegativeInteger(value.count)
|
|
596
|
+
&& isExperienceEvidenceRefArray(value.evidenceRefs);
|
|
597
|
+
}
|
|
598
|
+
function isExperienceRuleFindingArray(value) {
|
|
599
|
+
return Array.isArray(value) && value.every(isExperienceRuleFinding);
|
|
600
|
+
}
|
|
601
|
+
function isExperienceAssistiveInference(value) {
|
|
602
|
+
return isObjectRecord(value)
|
|
603
|
+
&& value.mode === 'deterministic_rules_only'
|
|
604
|
+
&& isEnumValue(value.code, [
|
|
605
|
+
'review_recommended',
|
|
606
|
+
'sample_recommended',
|
|
607
|
+
'positive_signal_observed',
|
|
608
|
+
'user_switched_topic_neutral',
|
|
609
|
+
'no_obvious_issue_from_rules',
|
|
610
|
+
'insufficient_human_context',
|
|
611
|
+
])
|
|
612
|
+
&& isEnumValue(value.confidence, ['low', 'medium', 'high'])
|
|
613
|
+
&& isEnumArray(value.basisRuleCodes, [
|
|
614
|
+
'high_observation_seen',
|
|
615
|
+
'medium_observation_seen',
|
|
616
|
+
'user_correction_seen',
|
|
617
|
+
'user_interruption_seen',
|
|
618
|
+
'session_interrupted_seen',
|
|
619
|
+
'negative_feedback_seen',
|
|
620
|
+
'positive_feedback_seen',
|
|
621
|
+
'user_goal_shift_seen',
|
|
622
|
+
'hard_rule_seen',
|
|
623
|
+
'tool_failure_seen',
|
|
624
|
+
'hedging_seen',
|
|
625
|
+
'explicit_marker_seen',
|
|
626
|
+
'runtime_context_excluded',
|
|
627
|
+
'skill_context_excluded',
|
|
628
|
+
'no_priority_signal',
|
|
629
|
+
])
|
|
630
|
+
&& isEnumArray(value.cautionCodes, [
|
|
631
|
+
'no_llm_judge',
|
|
632
|
+
'rule_only',
|
|
633
|
+
'runtime_context_excluded',
|
|
634
|
+
'skill_context_excluded',
|
|
635
|
+
'no_human_user_message',
|
|
636
|
+
'limited_timeline_window',
|
|
637
|
+
])
|
|
638
|
+
&& isExperienceEvidenceRefArray(value.evidenceRefs);
|
|
639
|
+
}
|
|
640
|
+
function isExperienceProblemEvidenceRef(value) {
|
|
641
|
+
if (!isObjectRecord(value)
|
|
642
|
+
|| typeof value.id !== 'string'
|
|
643
|
+
|| typeof value.kind !== 'string'
|
|
644
|
+
|| typeof value.sourceTrace !== 'string'
|
|
645
|
+
|| typeof value.sessionId !== 'string'
|
|
646
|
+
|| !isOptionalNonNegativeInteger(value.messageIndex)
|
|
647
|
+
|| !isOptionalNonNegativeInteger(value.logicalMessageIndex)
|
|
648
|
+
|| !isOptionalNonNegativeInteger(value.sourceLineIndex))
|
|
649
|
+
return false;
|
|
650
|
+
if (value.role !== undefined
|
|
651
|
+
&& !isEnumValue(value.role, ['user', 'assistant', 'tool', 'other']))
|
|
652
|
+
return false;
|
|
653
|
+
return [
|
|
654
|
+
value.messageUuid,
|
|
655
|
+
value.traceId,
|
|
656
|
+
value.callInstanceId,
|
|
657
|
+
value.toolUseId,
|
|
658
|
+
value.label,
|
|
659
|
+
value.snippet,
|
|
660
|
+
].every(isOptionalString)
|
|
661
|
+
&& isOptionalTimestamp(value.timestamp);
|
|
662
|
+
}
|
|
663
|
+
function isExperienceProblemPattern(value) {
|
|
664
|
+
return isObjectRecord(value)
|
|
665
|
+
&& typeof value.id === 'string'
|
|
666
|
+
&& isEnumValue(value.bucket, [
|
|
667
|
+
'output_format',
|
|
668
|
+
'content_accuracy',
|
|
669
|
+
'missing_context',
|
|
670
|
+
'rule_violation',
|
|
671
|
+
'workflow_mismatch',
|
|
672
|
+
'tool_runtime',
|
|
673
|
+
'goal_shift',
|
|
674
|
+
'unclear',
|
|
675
|
+
])
|
|
676
|
+
&& typeof value.patternKey === 'string'
|
|
677
|
+
&& isNonNegativeInteger(value.count)
|
|
678
|
+
&& isNonNegativeInteger(value.sessionCount)
|
|
679
|
+
&& isStringArray(value.recentSessionIds)
|
|
680
|
+
&& isEnumArray(value.signalTypes, [
|
|
681
|
+
'user_correction',
|
|
682
|
+
'negative_feedback',
|
|
683
|
+
'user_interruption',
|
|
684
|
+
'hard_rule',
|
|
685
|
+
'user_goal_shift',
|
|
686
|
+
'tool_failure',
|
|
687
|
+
'workflow_mismatch',
|
|
688
|
+
'artifact_missing',
|
|
689
|
+
'observer_lifecycle_failed',
|
|
690
|
+
'orchestration_boundary_violation',
|
|
691
|
+
])
|
|
692
|
+
&& Array.isArray(value.evidenceRefs)
|
|
693
|
+
&& value.evidenceRefs.every(isExperienceProblemEvidenceRef)
|
|
694
|
+
&& isOptionalTimestamp(value.lastSeen);
|
|
695
|
+
}
|
|
696
|
+
function isExperienceProblemPatternArray(value) {
|
|
697
|
+
return Array.isArray(value) && value.every(isExperienceProblemPattern);
|
|
698
|
+
}
|
|
699
|
+
function isExperienceAttribution(value) {
|
|
700
|
+
return isObjectRecord(value)
|
|
701
|
+
&& typeof value.source === 'string'
|
|
702
|
+
&& isRate(value.confidence)
|
|
703
|
+
&& isOptionalString(value.rawSkillRef)
|
|
704
|
+
&& isOptionalString(value.pluginName)
|
|
705
|
+
&& isOptionalString(value.commandName);
|
|
706
|
+
}
|
|
707
|
+
function isExperienceInvocationMetrics(value, requireSourceNeutralOutcomes = false) {
|
|
708
|
+
if (!isObjectRecord(value))
|
|
709
|
+
return false;
|
|
710
|
+
const fields = [
|
|
711
|
+
value.durationMs,
|
|
712
|
+
value.inputTokens,
|
|
713
|
+
value.outputTokens,
|
|
714
|
+
value.cacheReadTokens,
|
|
715
|
+
value.cacheCreationTokens,
|
|
716
|
+
value.numTurns,
|
|
717
|
+
value.numToolCalls,
|
|
718
|
+
value.numToolFailures,
|
|
719
|
+
];
|
|
720
|
+
if (!fields.every(isNonNegativeInteger))
|
|
721
|
+
return false;
|
|
722
|
+
if (requireSourceNeutralOutcomes
|
|
723
|
+
&& (typeof value.tokenUsageObserved !== 'boolean'
|
|
724
|
+
||
|
|
725
|
+
!isNonNegativeInteger(value.numToolCancelled)
|
|
726
|
+
|| !isNonNegativeInteger(value.numToolUnknown)))
|
|
727
|
+
return false;
|
|
728
|
+
if (value.tokenUsageObserved !== undefined
|
|
729
|
+
&& typeof value.tokenUsageObserved !== 'boolean')
|
|
730
|
+
return false;
|
|
731
|
+
if (value.numToolCancelled !== undefined && !isNonNegativeInteger(value.numToolCancelled))
|
|
732
|
+
return false;
|
|
733
|
+
if (value.numToolUnknown !== undefined && !isNonNegativeInteger(value.numToolUnknown))
|
|
734
|
+
return false;
|
|
735
|
+
const cancelled = typeof value.numToolCancelled === 'number' ? value.numToolCancelled : 0;
|
|
736
|
+
const unknown = typeof value.numToolUnknown === 'number' ? value.numToolUnknown : 0;
|
|
737
|
+
return value.numToolFailures + cancelled + unknown <= value.numToolCalls;
|
|
738
|
+
}
|
|
739
|
+
const EXPERIENCE_INDICATOR_KEYS = [
|
|
740
|
+
'userMessageCount',
|
|
741
|
+
'userFollowUpCount',
|
|
742
|
+
'userCorrectionCount',
|
|
743
|
+
'userInterruptionCount',
|
|
744
|
+
'sessionInterruptedCount',
|
|
745
|
+
'negativeFeedbackCount',
|
|
746
|
+
'positiveFeedbackCount',
|
|
747
|
+
'userGoalShiftCount',
|
|
748
|
+
'hardRuleTextHitCount',
|
|
749
|
+
'assistantDeliverySignalCount',
|
|
750
|
+
'deliverableArtifactSignalCount',
|
|
751
|
+
'routerDownstreamCompleted',
|
|
752
|
+
'routerDownstreamFailed',
|
|
753
|
+
'selfCorrectionCount',
|
|
754
|
+
'repeatedExecutionCount',
|
|
755
|
+
'toolCallCount',
|
|
756
|
+
'toolFailureCount',
|
|
757
|
+
'highObservationCount',
|
|
758
|
+
'mediumObservationCount',
|
|
759
|
+
'hedgingCount',
|
|
760
|
+
'explicitMarkerCount',
|
|
761
|
+
];
|
|
762
|
+
function isExperienceIndicators(value, requireSourceNeutralOutcomes = false) {
|
|
763
|
+
return isObjectRecord(value)
|
|
764
|
+
&& EXPERIENCE_INDICATOR_KEYS.every((key) => isNonNegativeInteger(value[key]))
|
|
765
|
+
&& (!requireSourceNeutralOutcomes
|
|
766
|
+
|| (isNonNegativeInteger(value.toolCancelledCount)
|
|
767
|
+
&& isNonNegativeInteger(value.toolUnknownCount)))
|
|
768
|
+
&& (value.toolCancelledCount === undefined || isNonNegativeInteger(value.toolCancelledCount))
|
|
769
|
+
&& (value.toolUnknownCount === undefined || isNonNegativeInteger(value.toolUnknownCount))
|
|
770
|
+
&& value.toolFailureCount
|
|
771
|
+
+ (typeof value.toolCancelledCount === 'number' ? value.toolCancelledCount : 0)
|
|
772
|
+
+ (typeof value.toolUnknownCount === 'number' ? value.toolUnknownCount : 0)
|
|
773
|
+
<= value.toolCallCount;
|
|
774
|
+
}
|
|
775
|
+
function isCountRecord(value) {
|
|
776
|
+
return isObjectRecord(value)
|
|
777
|
+
&& Object.values(value).every(isNonNegativeInteger);
|
|
778
|
+
}
|
|
779
|
+
function isExperienceTimelineScope(value) {
|
|
780
|
+
if (!isObjectRecord(value)
|
|
781
|
+
|| value.mode !== 'skill_segment_window'
|
|
782
|
+
|| !isNonNegativeInteger(value.segmentEventCount)
|
|
783
|
+
|| !isNonNegativeInteger(value.previewEventCount)
|
|
784
|
+
|| !isNonNegativeInteger(value.fullSessionEventCount)
|
|
785
|
+
|| !isExperienceTraceRecordRangeArray(value.segmentRecordRanges)
|
|
786
|
+
|| !isExperienceTraceRecordRangeArray(value.previewRecordRanges)
|
|
787
|
+
|| !isExperienceTraceRecordRangeArray(value.sessionRecordRanges)
|
|
788
|
+
|| typeof value.truncated !== 'boolean'
|
|
789
|
+
|| !isNonNegativeInteger(value.omittedBeforeCount)
|
|
790
|
+
|| !isNonNegativeInteger(value.omittedAfterCount))
|
|
791
|
+
return false;
|
|
792
|
+
return value.previewEventCount <= value.segmentEventCount
|
|
793
|
+
&& value.segmentEventCount <= value.fullSessionEventCount
|
|
794
|
+
&& value.omittedBeforeCount + value.omittedAfterCount <= value.fullSessionEventCount;
|
|
795
|
+
}
|
|
796
|
+
function isLegacyExperienceTimelineScope(value) {
|
|
797
|
+
if (!isObjectRecord(value)
|
|
798
|
+
|| value.mode !== 'skill_segment_window'
|
|
799
|
+
|| !isOptionalNonNegativeInteger(value.segmentStartRecordIndex)
|
|
800
|
+
|| !isOptionalNonNegativeInteger(value.segmentEndRecordIndex)
|
|
801
|
+
|| !isOptionalNonNegativeInteger(value.previewStartRecordIndex)
|
|
802
|
+
|| !isOptionalNonNegativeInteger(value.previewEndRecordIndex)
|
|
803
|
+
|| !isNonNegativeInteger(value.sessionStartRecordIndex)
|
|
804
|
+
|| !isNonNegativeInteger(value.sessionEndRecordIndex)
|
|
805
|
+
|| !isNonNegativeInteger(value.previewEventCount)
|
|
806
|
+
|| !isNonNegativeInteger(value.fullSessionEventCount)
|
|
807
|
+
|| typeof value.truncated !== 'boolean'
|
|
808
|
+
|| !isNonNegativeInteger(value.omittedBeforeCount)
|
|
809
|
+
|| !isNonNegativeInteger(value.omittedAfterCount))
|
|
810
|
+
return false;
|
|
811
|
+
const optionalRanges = [
|
|
812
|
+
[value.segmentStartRecordIndex, value.segmentEndRecordIndex],
|
|
813
|
+
[value.previewStartRecordIndex, value.previewEndRecordIndex],
|
|
814
|
+
];
|
|
815
|
+
if (optionalRanges.some(([start, end]) => (start === undefined) !== (end === undefined)
|
|
816
|
+
|| (typeof start === 'number'
|
|
817
|
+
&& typeof end === 'number'
|
|
818
|
+
&& start > end)))
|
|
819
|
+
return false;
|
|
820
|
+
return value.sessionStartRecordIndex <= value.sessionEndRecordIndex
|
|
821
|
+
&& value.previewEventCount <= value.fullSessionEventCount
|
|
822
|
+
&& value.omittedBeforeCount + value.omittedAfterCount <= value.fullSessionEventCount;
|
|
823
|
+
}
|
|
824
|
+
function isExperienceTraceRecordRangeArray(value) {
|
|
825
|
+
if (!Array.isArray(value))
|
|
826
|
+
return false;
|
|
827
|
+
const identities = new Set();
|
|
828
|
+
for (const range of value) {
|
|
829
|
+
if (!isObjectRecord(range)
|
|
830
|
+
|| typeof range.traceId !== 'string'
|
|
831
|
+
|| typeof range.sourceTrace !== 'string'
|
|
832
|
+
|| !isNonNegativeInteger(range.startRecordIndex)
|
|
833
|
+
|| !isNonNegativeInteger(range.endRecordIndex)
|
|
834
|
+
|| range.startRecordIndex > range.endRecordIndex
|
|
835
|
+
|| !isNonNegativeInteger(range.eventCount)
|
|
836
|
+
|| range.eventCount === 0
|
|
837
|
+
|| identities.has(range.traceId))
|
|
838
|
+
return false;
|
|
839
|
+
identities.add(range.traceId);
|
|
840
|
+
}
|
|
841
|
+
return true;
|
|
842
|
+
}
|
|
843
|
+
function isExperienceSkillSummary(value, requireToolUnknown = false) {
|
|
844
|
+
if (!isObjectRecord(value)
|
|
845
|
+
|| typeof value.skillName !== 'string'
|
|
846
|
+
|| !isNonNegativeInteger(value.invocationCount)
|
|
847
|
+
|| !isNonNegativeInteger(value.sessionCount)
|
|
848
|
+
|| !Array.isArray(value.sourceKinds)
|
|
849
|
+
|| !value.sourceKinds.every(isObservationSourceKind)
|
|
850
|
+
|| !isStringArray(value.entrypoints)
|
|
851
|
+
|| !isCountRecord(value.entrypointCounts)
|
|
852
|
+
|| !isCountRecord(value.attributionCounts)
|
|
853
|
+
|| !isStringArray(value.pluginNames)
|
|
854
|
+
|| !isStringArray(value.rawSkillRefs)
|
|
855
|
+
|| !isStringArray(value.commandNames)
|
|
856
|
+
|| !isCountRecord(value.toolCounts)
|
|
857
|
+
|| !isTimestampRange(value.firstSeen, value.lastSeen)
|
|
858
|
+
|| !isOptionalTimestampCoverage(value.timestampedInvocationCount, value.timestampCoverage, value.invocationCount)
|
|
859
|
+
|| (requireToolUnknown
|
|
860
|
+
&& (!isNonNegativeInteger(value.timestampedInvocationCount)
|
|
861
|
+
|| !isRate(value.timestampCoverage)))
|
|
862
|
+
|| !isNonNegativeInteger(value.reviewFirstSessionCount)
|
|
863
|
+
|| !isNonNegativeInteger(value.sampleReviewSessionCount)
|
|
864
|
+
|| !isExperienceIndicators(value.indicators, requireToolUnknown)
|
|
865
|
+
|| !isExperienceEvidenceChain(value.evidenceChain)
|
|
866
|
+
|| !isExperienceRuleFindingArray(value.ruleFindings)
|
|
867
|
+
|| !isExperienceAssistiveInference(value.assistiveInference)
|
|
868
|
+
|| !isExperienceProblemPatternArray(value.problemPatterns)
|
|
869
|
+
|| !isStringArray(value.relatedObservationIds)
|
|
870
|
+
|| !isObjectRecord(value.sourceMetadataCounts))
|
|
871
|
+
return false;
|
|
872
|
+
const metadataCounts = value.sourceMetadataCounts;
|
|
873
|
+
return ['channels', 'senders', 'businessActions', 'providers', 'models']
|
|
874
|
+
.every((key) => isCountRecord(metadataCounts[key]));
|
|
875
|
+
}
|
|
876
|
+
function isTimelineEvent(value) {
|
|
877
|
+
if (!isObjectRecord(value)
|
|
878
|
+
|| typeof value.id !== 'string'
|
|
879
|
+
|| ![
|
|
880
|
+
'user_message',
|
|
881
|
+
'synthetic_user_event',
|
|
882
|
+
'assistant_message',
|
|
883
|
+
'tool_use',
|
|
884
|
+
'tool_result',
|
|
885
|
+
'skill_context',
|
|
886
|
+
'runtime_context',
|
|
887
|
+
'observation',
|
|
888
|
+
].includes(String(value.kind))
|
|
889
|
+
|| typeof value.sourceTrace !== 'string'
|
|
890
|
+
|| typeof value.sessionId !== 'string'
|
|
891
|
+
|| !isNonNegativeInteger(value.order))
|
|
892
|
+
return false;
|
|
893
|
+
const optionalIndexes = [
|
|
894
|
+
value.messageIndex,
|
|
895
|
+
value.logicalMessageIndex,
|
|
896
|
+
value.sourceLineIndex,
|
|
897
|
+
];
|
|
898
|
+
if (!optionalIndexes.every((index) => index === undefined || isNonNegativeInteger(index)))
|
|
899
|
+
return false;
|
|
900
|
+
if (value.traceRole !== undefined
|
|
901
|
+
&& value.traceRole !== 'standalone'
|
|
902
|
+
&& value.traceRole !== 'main'
|
|
903
|
+
&& value.traceRole !== 'subagent')
|
|
904
|
+
return false;
|
|
905
|
+
if (value.role !== undefined
|
|
906
|
+
&& value.role !== 'user'
|
|
907
|
+
&& value.role !== 'assistant'
|
|
908
|
+
&& value.role !== 'tool'
|
|
909
|
+
&& value.role !== 'other')
|
|
910
|
+
return false;
|
|
911
|
+
if (value.toolStatus !== undefined
|
|
912
|
+
&& value.toolStatus !== 'success'
|
|
913
|
+
&& value.toolStatus !== 'failure'
|
|
914
|
+
&& value.toolStatus !== 'cancelled'
|
|
915
|
+
&& value.toolStatus !== 'unknown')
|
|
916
|
+
return false;
|
|
917
|
+
const optionalStrings = [
|
|
918
|
+
value.traceId,
|
|
919
|
+
value.traceLabel,
|
|
920
|
+
value.messageUuid,
|
|
921
|
+
value.callInstanceId,
|
|
922
|
+
value.toolUseId,
|
|
923
|
+
value.label,
|
|
924
|
+
value.snippet,
|
|
925
|
+
value.toolName,
|
|
926
|
+
value.fullText,
|
|
927
|
+
];
|
|
928
|
+
if (!optionalStrings.every((field) => field === undefined || typeof field === 'string')
|
|
929
|
+
|| !isOptionalTimestamp(value.timestamp)
|
|
930
|
+
|| (value.isError !== undefined && typeof value.isError !== 'boolean'))
|
|
931
|
+
return false;
|
|
932
|
+
return value.kind !== 'tool_result'
|
|
933
|
+
|| value.toolStatus === undefined
|
|
934
|
+
|| value.isError === undefined
|
|
935
|
+
|| value.isError === (value.toolStatus === 'failure');
|
|
936
|
+
}
|
|
937
|
+
function isTimelineEventArray(value) {
|
|
938
|
+
return Array.isArray(value) && value.every(isTimelineEvent);
|
|
939
|
+
}
|
|
940
|
+
function isTimelineTree(value) {
|
|
941
|
+
if (!isObjectRecord(value)
|
|
942
|
+
|| typeof value.sessionId !== 'string'
|
|
943
|
+
|| !isTimelineEventArray(value.main)
|
|
944
|
+
|| !Array.isArray(value.branches))
|
|
945
|
+
return false;
|
|
946
|
+
return value.branches.every((branch) => {
|
|
947
|
+
if (!isObjectRecord(branch)
|
|
948
|
+
|| typeof branch.id !== 'string'
|
|
949
|
+
|| typeof branch.label !== 'string'
|
|
950
|
+
|| typeof branch.sessionId !== 'string'
|
|
951
|
+
|| !isOptionalString(branch.traceId)
|
|
952
|
+
|| typeof branch.sourceTrace !== 'string'
|
|
953
|
+
|| (branch.traceRole !== 'main'
|
|
954
|
+
&& branch.traceRole !== 'subagent'
|
|
955
|
+
&& branch.traceRole !== 'standalone')
|
|
956
|
+
|| !isTimelineEventArray(branch.events))
|
|
957
|
+
return false;
|
|
958
|
+
if (branch.attachTo === undefined)
|
|
959
|
+
return true;
|
|
960
|
+
if (!isObjectRecord(branch.attachTo)
|
|
961
|
+
|| !isOptionalString(branch.attachTo.traceId)
|
|
962
|
+
|| typeof branch.attachTo.sourceTrace !== 'string')
|
|
963
|
+
return false;
|
|
964
|
+
return (branch.attachTo.messageIndex === undefined || isNonNegativeInteger(branch.attachTo.messageIndex))
|
|
965
|
+
&& (branch.attachTo.callInstanceId === undefined || typeof branch.attachTo.callInstanceId === 'string')
|
|
966
|
+
&& (branch.attachTo.toolUseId === undefined || typeof branch.attachTo.toolUseId === 'string')
|
|
967
|
+
&& (branch.attachTo.label === undefined || typeof branch.attachTo.label === 'string');
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
function isExperienceChecklistItem(value) {
|
|
971
|
+
return isObjectRecord(value)
|
|
972
|
+
&& typeof value.key === 'string'
|
|
973
|
+
&& typeof value.label === 'string'
|
|
974
|
+
&& isEnumValue(value.status, [
|
|
975
|
+
'passed',
|
|
976
|
+
'failed',
|
|
977
|
+
'unknown',
|
|
978
|
+
'not_declared',
|
|
979
|
+
'not_applicable',
|
|
980
|
+
'degraded',
|
|
981
|
+
])
|
|
982
|
+
&& isEnumValue(value.contribution, [
|
|
983
|
+
'blocking',
|
|
984
|
+
'attention',
|
|
985
|
+
'informational',
|
|
986
|
+
'positive',
|
|
987
|
+
'neutral',
|
|
988
|
+
])
|
|
989
|
+
&& typeof value.reason === 'string'
|
|
990
|
+
&& isExperienceEvidenceRefArray(value.evidenceRefs)
|
|
991
|
+
&& isEnumValue(value.source, ['deterministic_rule', 'llm_soft', 'manual'])
|
|
992
|
+
&& isOptionalString(value.suggestionKey);
|
|
993
|
+
}
|
|
994
|
+
function isExperienceSessionStoryGoalSlice(value) {
|
|
995
|
+
return isObjectRecord(value)
|
|
996
|
+
&& typeof value.id === 'string'
|
|
997
|
+
&& isNonNegativeInteger(value.order)
|
|
998
|
+
&& isStringArray(value.skillNames)
|
|
999
|
+
&& isTimestampRange(value.startTimestamp, value.endTimestamp)
|
|
1000
|
+
&& isEnumValue(value.reasonCode, [
|
|
1001
|
+
'skill_segment_boundary',
|
|
1002
|
+
'explicit_user_goal_shift',
|
|
1003
|
+
'default_session_slice',
|
|
1004
|
+
])
|
|
1005
|
+
&& isOptionalString(value.inferredUserGoal)
|
|
1006
|
+
&& isExperienceEvidenceRefArray(value.evidenceRefs);
|
|
1007
|
+
}
|
|
1008
|
+
function isExperienceSessionStorySubagentDispatch(value) {
|
|
1009
|
+
if (!isObjectRecord(value)
|
|
1010
|
+
|| typeof value.id !== 'string'
|
|
1011
|
+
|| !isNonNegativeInteger(value.order)
|
|
1012
|
+
|| typeof value.branchId !== 'string'
|
|
1013
|
+
|| typeof value.childSessionId !== 'string'
|
|
1014
|
+
|| typeof value.traceId !== 'string'
|
|
1015
|
+
|| typeof value.label !== 'string'
|
|
1016
|
+
|| typeof value.sourceTrace !== 'string'
|
|
1017
|
+
|| !isNonNegativeInteger(value.eventCount)
|
|
1018
|
+
|| !isExperienceEvidenceRefArray(value.evidenceRefs))
|
|
1019
|
+
return false;
|
|
1020
|
+
if (value.attachTo === undefined)
|
|
1021
|
+
return true;
|
|
1022
|
+
return isObjectRecord(value.attachTo)
|
|
1023
|
+
&& isOptionalNonNegativeInteger(value.attachTo.messageIndex)
|
|
1024
|
+
&& isOptionalString(value.attachTo.callInstanceId)
|
|
1025
|
+
&& isOptionalString(value.attachTo.toolUseId)
|
|
1026
|
+
&& isOptionalString(value.attachTo.label);
|
|
1027
|
+
}
|
|
1028
|
+
function isExperienceSessionStorySkillLink(value) {
|
|
1029
|
+
return isObjectRecord(value)
|
|
1030
|
+
&& typeof value.id === 'string'
|
|
1031
|
+
&& isNonNegativeInteger(value.order)
|
|
1032
|
+
&& typeof value.skillName === 'string'
|
|
1033
|
+
&& isEnumValue(value.role, ['router', 'executor', 'mixed', 'unknown'])
|
|
1034
|
+
&& isStringArray(value.invocationIds)
|
|
1035
|
+
&& isStringArray(value.goalSliceIds)
|
|
1036
|
+
&& isExperienceEvidenceRefArray(value.evidenceRefs);
|
|
1037
|
+
}
|
|
1038
|
+
function isExperienceSessionStoryGraphNode(value) {
|
|
1039
|
+
return isObjectRecord(value)
|
|
1040
|
+
&& typeof value.id === 'string'
|
|
1041
|
+
&& typeof value.label === 'string'
|
|
1042
|
+
&& isEnumValue(value.kind, [
|
|
1043
|
+
'user_goal',
|
|
1044
|
+
'skill_invocation',
|
|
1045
|
+
'subagent_branch',
|
|
1046
|
+
'tool_execution',
|
|
1047
|
+
'delivery',
|
|
1048
|
+
'user_feedback',
|
|
1049
|
+
'goal_shift',
|
|
1050
|
+
])
|
|
1051
|
+
&& isEnumValue(value.status, ['ok', 'attention', 'unknown', 'degraded', 'not_applicable'])
|
|
1052
|
+
&& (value.role === undefined || isEnumValue(value.role, ['router', 'executor', 'mixed', 'unknown']))
|
|
1053
|
+
&& isOptionalString(value.detailNodeId);
|
|
1054
|
+
}
|
|
1055
|
+
function isExperienceSessionStoryGraphEdge(value) {
|
|
1056
|
+
return isObjectRecord(value)
|
|
1057
|
+
&& typeof value.fromId === 'string'
|
|
1058
|
+
&& typeof value.toId === 'string'
|
|
1059
|
+
&& typeof value.label === 'string';
|
|
1060
|
+
}
|
|
1061
|
+
function isExperienceSessionStoryNode(value) {
|
|
1062
|
+
return isObjectRecord(value)
|
|
1063
|
+
&& typeof value.id === 'string'
|
|
1064
|
+
&& isNonNegativeInteger(value.order)
|
|
1065
|
+
&& isEnumValue(value.kind, [
|
|
1066
|
+
'user_goal',
|
|
1067
|
+
'skill_invocation',
|
|
1068
|
+
'subagent_branch',
|
|
1069
|
+
'tool_execution',
|
|
1070
|
+
'delivery',
|
|
1071
|
+
'user_feedback',
|
|
1072
|
+
'goal_shift',
|
|
1073
|
+
])
|
|
1074
|
+
&& typeof value.label === 'string'
|
|
1075
|
+
&& isEnumValue(value.status, ['ok', 'attention', 'unknown', 'degraded', 'not_applicable'])
|
|
1076
|
+
&& typeof value.text === 'string'
|
|
1077
|
+
&& isExperienceEvidenceRefArray(value.evidenceRefs);
|
|
1078
|
+
}
|
|
1079
|
+
function isExperienceSessionStoryAnswer(value) {
|
|
1080
|
+
return isObjectRecord(value)
|
|
1081
|
+
&& isEnumValue(value.key, ['goal_satisfaction', 'declared_behavior_fit', 'user_feeling'])
|
|
1082
|
+
&& typeof value.label === 'string'
|
|
1083
|
+
&& isEnumValue(value.status, ['ok', 'attention', 'unknown', 'degraded', 'not_applicable'])
|
|
1084
|
+
&& isEnumValue(value.reason, [
|
|
1085
|
+
'data_degraded',
|
|
1086
|
+
'blocking_failed',
|
|
1087
|
+
'attention_accumulated',
|
|
1088
|
+
'unknown_dominant',
|
|
1089
|
+
'all_passed',
|
|
1090
|
+
'not_applicable',
|
|
1091
|
+
])
|
|
1092
|
+
&& isStringArray(value.sourceItemKeys)
|
|
1093
|
+
&& typeof value.text === 'string'
|
|
1094
|
+
&& isExperienceEvidenceRefArray(value.evidenceRefs)
|
|
1095
|
+
&& Array.isArray(value.checklistItems)
|
|
1096
|
+
&& value.checklistItems.every(isExperienceChecklistItem);
|
|
1097
|
+
}
|
|
1098
|
+
function isExperienceGoalEvidenceRef(value) {
|
|
1099
|
+
return isObjectRecord(value)
|
|
1100
|
+
&& isEnumValue(value.kind, ['user_message', 'goal_slice', 'llm_goal'])
|
|
1101
|
+
&& isOptionalString(value.goalSliceId)
|
|
1102
|
+
&& (value.evidenceRef === undefined || isExperienceEvidenceRef(value.evidenceRef))
|
|
1103
|
+
&& isOptionalString(value.label);
|
|
1104
|
+
}
|
|
1105
|
+
function isExperienceSkillSegment(value) {
|
|
1106
|
+
if (!isObjectRecord(value)
|
|
1107
|
+
|| typeof value.id !== 'string'
|
|
1108
|
+
|| !isNonNegativeInteger(value.order)
|
|
1109
|
+
|| typeof value.skillName !== 'string'
|
|
1110
|
+
|| !isEnumValue(value.skillType, [
|
|
1111
|
+
'router',
|
|
1112
|
+
'delegation',
|
|
1113
|
+
'executor',
|
|
1114
|
+
'advisory',
|
|
1115
|
+
'workflow_owner',
|
|
1116
|
+
'unknown',
|
|
1117
|
+
])
|
|
1118
|
+
|| (value.skillTypeSource !== undefined
|
|
1119
|
+
&& !isEnumValue(value.skillTypeSource, ['frontmatter', 'trace', 'unknown']))
|
|
1120
|
+
|| (value.declaredSkillType !== undefined
|
|
1121
|
+
&& !isEnumValue(value.declaredSkillType, [
|
|
1122
|
+
'router',
|
|
1123
|
+
'delegation',
|
|
1124
|
+
'executor',
|
|
1125
|
+
'advisory',
|
|
1126
|
+
'workflow_owner',
|
|
1127
|
+
'unknown',
|
|
1128
|
+
]))
|
|
1129
|
+
|| (value.traceInferredSkillType !== undefined
|
|
1130
|
+
&& !isEnumValue(value.traceInferredSkillType, [
|
|
1131
|
+
'router',
|
|
1132
|
+
'delegation',
|
|
1133
|
+
'executor',
|
|
1134
|
+
'advisory',
|
|
1135
|
+
'workflow_owner',
|
|
1136
|
+
'unknown',
|
|
1137
|
+
]))
|
|
1138
|
+
|| !isEnumValue(value.episodeRole, [
|
|
1139
|
+
'main_executor',
|
|
1140
|
+
'router',
|
|
1141
|
+
'delegator',
|
|
1142
|
+
'supporting',
|
|
1143
|
+
'observer',
|
|
1144
|
+
])
|
|
1145
|
+
|| !isStringArray(value.skillInvocationIds)
|
|
1146
|
+
|| !isOptionalNonNegativeInteger(value.startMessageIndex)
|
|
1147
|
+
|| !isOptionalNonNegativeInteger(value.endMessageIndex)
|
|
1148
|
+
|| !isTimestampRange(value.startTimestamp, value.endTimestamp)
|
|
1149
|
+
|| !Array.isArray(value.typeSpecificChecklist)
|
|
1150
|
+
|| !value.typeSpecificChecklist.every(isExperienceChecklistItem)
|
|
1151
|
+
|| !isExperienceEvidenceRefArray(value.evidenceRefs))
|
|
1152
|
+
return false;
|
|
1153
|
+
if (value.messageRanges !== undefined
|
|
1154
|
+
&& (!Array.isArray(value.messageRanges)
|
|
1155
|
+
|| value.messageRanges.some((range) => !isObjectRecord(range)
|
|
1156
|
+
|| !isNonNegativeInteger(range.startMessageIndex)
|
|
1157
|
+
|| !isNonNegativeInteger(range.endMessageIndex)
|
|
1158
|
+
|| range.startMessageIndex > range.endMessageIndex
|
|
1159
|
+
|| !isOptionalString(range.traceId)
|
|
1160
|
+
|| !isOptionalString(range.sourceTrace)
|
|
1161
|
+
|| !isOptionalString(range.sessionId))))
|
|
1162
|
+
return false;
|
|
1163
|
+
if (typeof value.startMessageIndex === 'number'
|
|
1164
|
+
&& typeof value.endMessageIndex === 'number'
|
|
1165
|
+
&& value.startMessageIndex > value.endMessageIndex)
|
|
1166
|
+
return false;
|
|
1167
|
+
if (value.runtimeAssessment === undefined)
|
|
1168
|
+
return true;
|
|
1169
|
+
return isObjectRecord(value.runtimeAssessment)
|
|
1170
|
+
&& isOptionalString(value.runtimeAssessment.goalSatisfaction)
|
|
1171
|
+
&& isOptionalString(value.runtimeAssessment.declaredBehaviorFit)
|
|
1172
|
+
&& isOptionalString(value.runtimeAssessment.artifactGoalMatch)
|
|
1173
|
+
&& isOptionalString(value.runtimeAssessment.userFeeling);
|
|
1174
|
+
}
|
|
1175
|
+
function isExperienceOrchestrationEdge(value) {
|
|
1176
|
+
return isObjectRecord(value)
|
|
1177
|
+
&& typeof value.id === 'string'
|
|
1178
|
+
&& typeof value.episodeId === 'string'
|
|
1179
|
+
&& isEnumValue(value.edgeKind, ['internal_skill', 'external_child_session'])
|
|
1180
|
+
&& isOptionalString(value.parentSkillSegmentId)
|
|
1181
|
+
&& isOptionalString(value.executorSkillSegmentId)
|
|
1182
|
+
&& isOptionalString(value.childSessionId)
|
|
1183
|
+
&& (value.runnerStartedRef === undefined || isExperienceEvidenceRef(value.runnerStartedRef))
|
|
1184
|
+
&& (value.runnerCompletedRef === undefined || isExperienceEvidenceRef(value.runnerCompletedRef))
|
|
1185
|
+
&& (value.notificationRef === undefined || isExperienceEvidenceRef(value.notificationRef))
|
|
1186
|
+
&& isEnumValue(value.status, ['started', 'completed', 'failed', 'unknown'])
|
|
1187
|
+
&& isExperienceEvidenceRefArray(value.evidenceRefs);
|
|
1188
|
+
}
|
|
1189
|
+
function isExperienceFeedbackAttribution(value) {
|
|
1190
|
+
return isObjectRecord(value)
|
|
1191
|
+
&& isOptionalString(value.skillName)
|
|
1192
|
+
&& isOptionalString(value.skillSegmentId)
|
|
1193
|
+
&& isEnumValue(value.attributionRole, ['primary_fault', 'downstream_related', 'context_only'])
|
|
1194
|
+
&& isEnumValue(value.reasonCode, [
|
|
1195
|
+
'object_match',
|
|
1196
|
+
'promise_match',
|
|
1197
|
+
'action_match',
|
|
1198
|
+
'orchestration_edge',
|
|
1199
|
+
'episode_context',
|
|
1200
|
+
])
|
|
1201
|
+
&& isExperienceEvidenceRefArray(value.evidenceRefs);
|
|
1202
|
+
}
|
|
1203
|
+
function isExperienceFeedbackSignal(value) {
|
|
1204
|
+
return isObjectRecord(value)
|
|
1205
|
+
&& typeof value.id === 'string'
|
|
1206
|
+
&& isNonNegativeInteger(value.order)
|
|
1207
|
+
&& isEnumValue(value.type, [
|
|
1208
|
+
'correction',
|
|
1209
|
+
'follow_up',
|
|
1210
|
+
'frustration',
|
|
1211
|
+
'interruption',
|
|
1212
|
+
'positive',
|
|
1213
|
+
'unknown',
|
|
1214
|
+
])
|
|
1215
|
+
&& typeof value.text === 'string'
|
|
1216
|
+
&& isOptionalString(value.targetObject)
|
|
1217
|
+
&& isEnumValue(value.sourceWindow, [
|
|
1218
|
+
'session',
|
|
1219
|
+
'episode',
|
|
1220
|
+
'skill_invocation',
|
|
1221
|
+
'downstream_child',
|
|
1222
|
+
])
|
|
1223
|
+
&& isExperienceEvidenceRef(value.evidenceRef)
|
|
1224
|
+
&& (value.canonicalAttributions === undefined
|
|
1225
|
+
|| (Array.isArray(value.canonicalAttributions)
|
|
1226
|
+
&& value.canonicalAttributions.every(isExperienceFeedbackAttribution)))
|
|
1227
|
+
&& Array.isArray(value.attributions)
|
|
1228
|
+
&& value.attributions.every(isExperienceFeedbackAttribution);
|
|
1229
|
+
}
|
|
1230
|
+
function isExperienceEpisodeArtifact(value) {
|
|
1231
|
+
return isObjectRecord(value)
|
|
1232
|
+
&& isEnumValue(value.kind, [
|
|
1233
|
+
'path',
|
|
1234
|
+
'url',
|
|
1235
|
+
'document',
|
|
1236
|
+
'code',
|
|
1237
|
+
'execution_window',
|
|
1238
|
+
'unknown',
|
|
1239
|
+
])
|
|
1240
|
+
&& typeof value.label === 'string'
|
|
1241
|
+
&& isOptionalString(value.pathOrUrl)
|
|
1242
|
+
&& isEnumValue(value.artifactGoalMatch, ['passed', 'failed', 'unknown'])
|
|
1243
|
+
&& isExperienceEvidenceRef(value.evidenceRef);
|
|
1244
|
+
}
|
|
1245
|
+
function isExperienceEpisodeOutcome(value) {
|
|
1246
|
+
return isObjectRecord(value)
|
|
1247
|
+
&& isEnumValue(value.closure, ['closed', 'unresolved', 'abandoned', 'unknown'])
|
|
1248
|
+
&& Array.isArray(value.artifacts)
|
|
1249
|
+
&& value.artifacts.every(isExperienceEpisodeArtifact)
|
|
1250
|
+
&& isExperienceReviewPriority(value.verdict)
|
|
1251
|
+
&& isOptionalString(value.acceptanceCriteria);
|
|
1252
|
+
}
|
|
1253
|
+
function isExperienceEpisode(value) {
|
|
1254
|
+
return isObjectRecord(value)
|
|
1255
|
+
&& typeof value.id === 'string'
|
|
1256
|
+
&& isNonNegativeInteger(value.order)
|
|
1257
|
+
&& typeof value.sessionId === 'string'
|
|
1258
|
+
&& isOptionalString(value.primaryGoal)
|
|
1259
|
+
&& Array.isArray(value.goalEvidenceRefs)
|
|
1260
|
+
&& value.goalEvidenceRefs.every(isExperienceGoalEvidenceRef)
|
|
1261
|
+
&& isTimestampRange(value.startTimestamp, value.endTimestamp)
|
|
1262
|
+
&& (value.startRef === undefined || isExperienceEvidenceRef(value.startRef))
|
|
1263
|
+
&& (value.endRef === undefined || isExperienceEvidenceRef(value.endRef))
|
|
1264
|
+
&& isEnumValue(value.boundaryReason, [
|
|
1265
|
+
'goal_shift',
|
|
1266
|
+
'checkpoint_or_subagent',
|
|
1267
|
+
'downstream_closed',
|
|
1268
|
+
'session_end',
|
|
1269
|
+
])
|
|
1270
|
+
&& Array.isArray(value.skillSegments)
|
|
1271
|
+
&& value.skillSegments.every(isExperienceSkillSegment)
|
|
1272
|
+
&& Array.isArray(value.orchestrationEdges)
|
|
1273
|
+
&& value.orchestrationEdges.every(isExperienceOrchestrationEdge)
|
|
1274
|
+
&& Array.isArray(value.feedbackSignals)
|
|
1275
|
+
&& value.feedbackSignals.every(isExperienceFeedbackSignal)
|
|
1276
|
+
&& isExperienceEpisodeOutcome(value.outcome);
|
|
1277
|
+
}
|
|
1278
|
+
function isExperienceStoryContext(value) {
|
|
1279
|
+
return isObjectRecord(value)
|
|
1280
|
+
&& typeof value.id === 'string'
|
|
1281
|
+
&& typeof value.sessionGroupKey === 'string'
|
|
1282
|
+
&& Array.isArray(value.goalSlices)
|
|
1283
|
+
&& value.goalSlices.every(isExperienceSessionStoryGoalSlice)
|
|
1284
|
+
&& Array.isArray(value.subagentDispatches)
|
|
1285
|
+
&& value.subagentDispatches.every(isExperienceSessionStorySubagentDispatch)
|
|
1286
|
+
&& Array.isArray(value.episodes)
|
|
1287
|
+
&& value.episodes.every(isExperienceEpisode);
|
|
1288
|
+
}
|
|
1289
|
+
function isExperienceSessionStory(value, compact) {
|
|
1290
|
+
if (!isObjectRecord(value)
|
|
1291
|
+
|| value.schemaVersion !== 1
|
|
1292
|
+
|| (compact ? typeof value.contextRef !== 'string' : !isOptionalString(value.contextRef))
|
|
1293
|
+
|| typeof value.summary !== 'string'
|
|
1294
|
+
|| !isNonNegativeInteger(value.invocationCount)
|
|
1295
|
+
|| !isNonNegativeInteger(value.goalSliceCount)
|
|
1296
|
+
|| !isNonNegativeInteger(value.branchCount)
|
|
1297
|
+
|| !isNonNegativeInteger(value.progressUpdateCount)
|
|
1298
|
+
|| !isNonNegativeInteger(value.finalDeliverySignalCount)
|
|
1299
|
+
|| !isStringArray(value.mainlineNodeIds)
|
|
1300
|
+
|| !Array.isArray(value.skillLinks)
|
|
1301
|
+
|| !value.skillLinks.every(isExperienceSessionStorySkillLink)
|
|
1302
|
+
|| !isObjectRecord(value.graph)
|
|
1303
|
+
|| !Array.isArray(value.graph.nodes)
|
|
1304
|
+
|| !value.graph.nodes.every(isExperienceSessionStoryGraphNode)
|
|
1305
|
+
|| !Array.isArray(value.graph.edges)
|
|
1306
|
+
|| !value.graph.edges.every(isExperienceSessionStoryGraphEdge)
|
|
1307
|
+
|| !Array.isArray(value.nodes)
|
|
1308
|
+
|| !value.nodes.every(isExperienceSessionStoryNode)
|
|
1309
|
+
|| !Array.isArray(value.answers)
|
|
1310
|
+
|| !value.answers.every(isExperienceSessionStoryAnswer))
|
|
1311
|
+
return false;
|
|
1312
|
+
if (compact) {
|
|
1313
|
+
return value.goalSlices === undefined
|
|
1314
|
+
&& value.subagentDispatches === undefined
|
|
1315
|
+
&& value.episodes === undefined;
|
|
1316
|
+
}
|
|
1317
|
+
return Array.isArray(value.goalSlices)
|
|
1318
|
+
&& value.goalSlices.every(isExperienceSessionStoryGoalSlice)
|
|
1319
|
+
&& Array.isArray(value.subagentDispatches)
|
|
1320
|
+
&& value.subagentDispatches.every(isExperienceSessionStorySubagentDispatch)
|
|
1321
|
+
&& (value.episodes === undefined
|
|
1322
|
+
|| (Array.isArray(value.episodes) && value.episodes.every(isExperienceEpisode)));
|
|
1323
|
+
}
|
|
1324
|
+
function isExperienceReviewerReportStep(value) {
|
|
1325
|
+
return isObjectRecord(value)
|
|
1326
|
+
&& isNonNegativeInteger(value.order)
|
|
1327
|
+
&& typeof value.label === 'string'
|
|
1328
|
+
&& isEnumValue(value.status, ['ok', 'attention', 'unknown', 'degraded', 'not_applicable'])
|
|
1329
|
+
&& typeof value.text === 'string'
|
|
1330
|
+
&& isExperienceEvidenceRefArray(value.evidenceRefs);
|
|
1331
|
+
}
|
|
1332
|
+
function isExperienceReviewerReportFinding(value) {
|
|
1333
|
+
if (!isObjectRecord(value)
|
|
1334
|
+
|| typeof value.id !== 'string'
|
|
1335
|
+
|| typeof value.judgmentId !== 'string'
|
|
1336
|
+
|| !isEnumValue(value.source, ['deterministic_rule', 'llm_soft', 'manual'])
|
|
1337
|
+
|| !isEnumValue(value.level, ['attention', 'possible_false_positive', 'note'])
|
|
1338
|
+
|| typeof value.title !== 'string'
|
|
1339
|
+
|| typeof value.body !== 'string'
|
|
1340
|
+
|| typeof value.ruleSource !== 'string'
|
|
1341
|
+
|| typeof value.ruleVersion !== 'string'
|
|
1342
|
+
|| !isExperienceEvidenceRefArray(value.evidenceRefs)
|
|
1343
|
+
|| !isObjectRecord(value.reviewStateRef)
|
|
1344
|
+
|| value.reviewStateRef.targetType !== 'reviewer_judgment'
|
|
1345
|
+
|| typeof value.reviewStateRef.targetId !== 'string')
|
|
1346
|
+
return false;
|
|
1347
|
+
return [
|
|
1348
|
+
value.reviewStateRef.verdict,
|
|
1349
|
+
value.reviewStateRef.reason,
|
|
1350
|
+
value.reviewStateRef.note,
|
|
1351
|
+
].every(isOptionalString)
|
|
1352
|
+
&& isOptionalTimestamp(value.reviewStateRef.reviewedAt);
|
|
1353
|
+
}
|
|
1354
|
+
function isExperienceReviewerMetrics(value, requireSourceNeutralOutcomes) {
|
|
1355
|
+
if (!isObjectRecord(value))
|
|
1356
|
+
return false;
|
|
1357
|
+
const countKeys = [
|
|
1358
|
+
'toolCallCount',
|
|
1359
|
+
'toolFailureCount',
|
|
1360
|
+
'userMessageCount',
|
|
1361
|
+
'userFollowUpCount',
|
|
1362
|
+
'assistantDeliverySignalCount',
|
|
1363
|
+
'deliverableArtifactSignalCount',
|
|
1364
|
+
'routerDownstreamCompleted',
|
|
1365
|
+
'routerDownstreamFailed',
|
|
1366
|
+
'assistantProgressUpdateCount',
|
|
1367
|
+
'selfCorrectionCount',
|
|
1368
|
+
'repeatedExecutionCount',
|
|
1369
|
+
'finalDeliverySignalCount',
|
|
1370
|
+
'traceEventCount',
|
|
1371
|
+
];
|
|
1372
|
+
if (!countKeys.every((key) => isNonNegativeInteger(value[key]))
|
|
1373
|
+
|| (requireSourceNeutralOutcomes
|
|
1374
|
+
&& (!isNonNegativeInteger(value.toolCancelledCount)
|
|
1375
|
+
|| !isNonNegativeInteger(value.toolUnknownCount)))
|
|
1376
|
+
|| (value.toolCancelledCount !== undefined
|
|
1377
|
+
&& !isNonNegativeInteger(value.toolCancelledCount))
|
|
1378
|
+
|| (value.toolUnknownCount !== undefined
|
|
1379
|
+
&& !isNonNegativeInteger(value.toolUnknownCount))
|
|
1380
|
+
|| !isObjectRecord(value.tokenUsage))
|
|
1381
|
+
return false;
|
|
1382
|
+
const tokenKeys = [
|
|
1383
|
+
'inputTokens',
|
|
1384
|
+
'outputTokens',
|
|
1385
|
+
'cacheReadTokens',
|
|
1386
|
+
'cacheCreationTokens',
|
|
1387
|
+
];
|
|
1388
|
+
const tokenUsage = value.tokenUsage;
|
|
1389
|
+
const observedInvocationCount = tokenUsage.observedInvocationCount;
|
|
1390
|
+
const invocationCount = tokenUsage.invocationCount;
|
|
1391
|
+
const coverage = tokenUsage.coverage;
|
|
1392
|
+
const hasCoverage = observedInvocationCount !== undefined
|
|
1393
|
+
|| invocationCount !== undefined
|
|
1394
|
+
|| coverage !== undefined;
|
|
1395
|
+
return tokenKeys.every((key) => isNonNegativeInteger(tokenUsage[key]))
|
|
1396
|
+
&& tokenUsage.attribution === 'skill_segment'
|
|
1397
|
+
&& (!hasCoverage
|
|
1398
|
+
|| (isNonNegativeInteger(observedInvocationCount)
|
|
1399
|
+
&& isNonNegativeInteger(invocationCount)
|
|
1400
|
+
&& isRate(coverage)
|
|
1401
|
+
&& observedInvocationCount <= invocationCount
|
|
1402
|
+
&& Math.abs(coverage
|
|
1403
|
+
- (invocationCount > 0
|
|
1404
|
+
? observedInvocationCount / invocationCount
|
|
1405
|
+
: 1)) <= 0.0001))
|
|
1406
|
+
&& (!requireSourceNeutralOutcomes || hasCoverage)
|
|
1407
|
+
&& value.toolFailureCount
|
|
1408
|
+
+ (typeof value.toolCancelledCount === 'number' ? value.toolCancelledCount : 0)
|
|
1409
|
+
+ (typeof value.toolUnknownCount === 'number' ? value.toolUnknownCount : 0)
|
|
1410
|
+
<= value.toolCallCount;
|
|
1411
|
+
}
|
|
1412
|
+
function isExperienceReviewerReport(value, compact) {
|
|
1413
|
+
if (!isObjectRecord(value)
|
|
1414
|
+
|| value.schemaVersion !== 1
|
|
1415
|
+
|| !isEnumValue(value.mode, ['deterministic_milestone_1', 'deterministic_session_story'])
|
|
1416
|
+
|| !isTimestamp(value.generatedAt)
|
|
1417
|
+
|| typeof value.title !== 'string'
|
|
1418
|
+
|| typeof value.summary !== 'string'
|
|
1419
|
+
|| !isObjectRecord(value.scope)
|
|
1420
|
+
|| !isEnumValue(value.scope.kind, ['single_skill_single_goal', 'degraded_complex'])
|
|
1421
|
+
|| !isStringArray(value.scope.reasonCodes)
|
|
1422
|
+
|| !Array.isArray(value.chainSteps)
|
|
1423
|
+
|| !value.chainSteps.every(isExperienceReviewerReportStep)
|
|
1424
|
+
|| !Array.isArray(value.findings)
|
|
1425
|
+
|| !value.findings.every(isExperienceReviewerReportFinding)
|
|
1426
|
+
|| !isExperienceReviewerMetrics(value.oneLookMetrics, compact)
|
|
1427
|
+
|| !isStringArray(value.authorSuggestions)
|
|
1428
|
+
|| !isExperienceEvidenceRefArray(value.traceLinks))
|
|
1429
|
+
return false;
|
|
1430
|
+
if (compact) {
|
|
1431
|
+
return value.sessionStory === undefined && value.sessionStoryRef === 'session';
|
|
1432
|
+
}
|
|
1433
|
+
return isExperienceSessionStory(value.sessionStory, false)
|
|
1434
|
+
&& (value.sessionStoryRef === undefined || value.sessionStoryRef === 'session');
|
|
1435
|
+
}
|
|
1436
|
+
/**
|
|
1437
|
+
* Converts a hydrated report into the wire format used by inbox JSON.
|
|
1438
|
+
* Timeline evidence is stored once per logical trace; consumers hydrate the
|
|
1439
|
+
* invocation/session views through normalizeObservationExperienceReport().
|
|
1440
|
+
*/
|
|
1441
|
+
export function compactObservationExperienceReport(report) {
|
|
1442
|
+
const hydrated = hydrateExperienceTimelines(report);
|
|
1443
|
+
return {
|
|
1444
|
+
...hydrated,
|
|
1445
|
+
invocations: hydrated.invocations.map((invocation) => {
|
|
1446
|
+
const { timeline, ...persisted } = invocation;
|
|
1447
|
+
return {
|
|
1448
|
+
...persisted,
|
|
1449
|
+
timelineRef: invocation.timelineRef
|
|
1450
|
+
?? timelineRefForSessionGroup(invocation.sessionGroupKey),
|
|
1451
|
+
timelineEventIds: invocation.timelineEventIds
|
|
1452
|
+
?? timeline.map((event) => event.id),
|
|
1453
|
+
};
|
|
1454
|
+
}),
|
|
1455
|
+
sessions: hydrated.sessions.map((session) => {
|
|
1456
|
+
const { timelinePreview, sessionStory, reviewerReport } = session;
|
|
1457
|
+
const persisted = omitProperties(session, [
|
|
1458
|
+
'timelinePreview',
|
|
1459
|
+
'fullSessionTimeline',
|
|
1460
|
+
'timelineTree',
|
|
1461
|
+
'sessionStory',
|
|
1462
|
+
'reviewerReport',
|
|
1463
|
+
]);
|
|
1464
|
+
const contextRef = sessionStory?.contextRef;
|
|
1465
|
+
return {
|
|
1466
|
+
...persisted,
|
|
1467
|
+
timelineRef: session.timelineRef
|
|
1468
|
+
?? timelineRefForSessionGroup(session.sessionId),
|
|
1469
|
+
timelinePreviewEventIds: session.timelinePreviewEventIds
|
|
1470
|
+
?? timelinePreview.map((event) => event.id),
|
|
1471
|
+
sessionStory: sessionStory && contextRef
|
|
1472
|
+
? compactSessionStory(sessionStory, contextRef)
|
|
1473
|
+
: undefined,
|
|
1474
|
+
reviewerReport: reviewerReport && sessionStory && contextRef
|
|
1475
|
+
? {
|
|
1476
|
+
...omitSessionStory(reviewerReport),
|
|
1477
|
+
sessionStoryRef: 'session',
|
|
1478
|
+
}
|
|
1479
|
+
: undefined,
|
|
1480
|
+
};
|
|
1481
|
+
}),
|
|
1482
|
+
};
|
|
1483
|
+
}
|
|
1484
|
+
function hydrateExperienceTimelines(report) {
|
|
1485
|
+
const timelineById = new Map(report.traceTimelines.map((timeline) => [timeline.id, timeline]));
|
|
1486
|
+
const storyContextById = new Map(report.storyContexts.map((context) => [context.id, context]));
|
|
1487
|
+
const flattenedById = new Map();
|
|
1488
|
+
const eventByTimelineId = new Map();
|
|
1489
|
+
const flattenedTimeline = (ref) => {
|
|
1490
|
+
const cached = flattenedById.get(ref);
|
|
1491
|
+
if (cached)
|
|
1492
|
+
return cached;
|
|
1493
|
+
const timeline = timelineById.get(ref);
|
|
1494
|
+
if (!timeline)
|
|
1495
|
+
return [];
|
|
1496
|
+
const flattened = flattenTimelineTree(timeline.tree);
|
|
1497
|
+
flattenedById.set(ref, flattened);
|
|
1498
|
+
eventByTimelineId.set(ref, new Map(flattened.map((event) => [event.id, event])));
|
|
1499
|
+
return flattened;
|
|
1500
|
+
};
|
|
1501
|
+
const selectEvents = (ref, ids, fallback) => {
|
|
1502
|
+
if (!timelineById.has(ref))
|
|
1503
|
+
return fallback;
|
|
1504
|
+
if (!ids)
|
|
1505
|
+
return fallback.length > 0 ? fallback : flattenedTimeline(ref);
|
|
1506
|
+
const byId = eventByTimelineId.get(ref)
|
|
1507
|
+
?? new Map(flattenedTimeline(ref).map((event) => [event.id, event]));
|
|
1508
|
+
return ids.flatMap((id) => {
|
|
1509
|
+
const event = byId.get(id);
|
|
1510
|
+
return event ? [event] : [];
|
|
1511
|
+
});
|
|
1512
|
+
};
|
|
1513
|
+
const invocations = report.invocations.map((invocation) => {
|
|
1514
|
+
const timelineRef = invocation.timelineRef ?? timelineRefForSessionGroup(invocation.sessionGroupKey);
|
|
1515
|
+
const timelineEventIds = invocation.timelineEventIds
|
|
1516
|
+
?? invocation.timeline.map((event) => event.id);
|
|
1517
|
+
return {
|
|
1518
|
+
...invocation,
|
|
1519
|
+
timelineRef,
|
|
1520
|
+
timelineEventIds,
|
|
1521
|
+
timeline: selectEvents(timelineRef, timelineEventIds, invocation.timeline),
|
|
1522
|
+
};
|
|
1523
|
+
});
|
|
1524
|
+
const invocationById = new Map(invocations.map((invocation) => [invocation.id, invocation]));
|
|
1525
|
+
const sessions = report.sessions.map((session) => {
|
|
1526
|
+
const firstInvocation = session.invocationIds
|
|
1527
|
+
.map((id) => invocationById.get(id))
|
|
1528
|
+
.find((invocation) => Boolean(invocation));
|
|
1529
|
+
const timelineRef = session.timelineRef
|
|
1530
|
+
?? firstInvocation?.timelineRef
|
|
1531
|
+
?? timelineRefForSessionGroup(firstInvocation?.sessionGroupKey ?? session.sessionId);
|
|
1532
|
+
const storedTimeline = timelineById.get(timelineRef);
|
|
1533
|
+
const fullSessionTimeline = storedTimeline
|
|
1534
|
+
? flattenedTimeline(timelineRef)
|
|
1535
|
+
: session.fullSessionTimeline;
|
|
1536
|
+
const timelinePreviewEventIds = session.timelinePreviewEventIds
|
|
1537
|
+
?? session.timelinePreview.map((event) => event.id);
|
|
1538
|
+
const timelinePreview = timelinePreviewEventIds.length > 0
|
|
1539
|
+
? selectEvents(timelineRef, timelinePreviewEventIds, session.timelinePreview)
|
|
1540
|
+
: fullSessionTimeline.slice(0, TIMELINE_PREVIEW_EVENT_LIMIT);
|
|
1541
|
+
const contextRef = session.sessionStory?.contextRef
|
|
1542
|
+
?? storyContextRefForSessionGroup(firstInvocation?.sessionGroupKey ?? session.sessionId);
|
|
1543
|
+
const storyContext = storyContextById.get(contextRef);
|
|
1544
|
+
const sessionStory = session.sessionStory
|
|
1545
|
+
? {
|
|
1546
|
+
...session.sessionStory,
|
|
1547
|
+
contextRef,
|
|
1548
|
+
goalSlices: storyContext?.goalSlices ?? session.sessionStory.goalSlices,
|
|
1549
|
+
subagentDispatches: storyContext?.subagentDispatches
|
|
1550
|
+
?? session.sessionStory.subagentDispatches,
|
|
1551
|
+
episodes: storyContext?.episodes ?? session.sessionStory.episodes ?? [],
|
|
1552
|
+
}
|
|
1553
|
+
: undefined;
|
|
1554
|
+
return {
|
|
1555
|
+
...session,
|
|
1556
|
+
timelineRef,
|
|
1557
|
+
timelinePreviewEventIds: timelinePreview.map((event) => event.id),
|
|
1558
|
+
timelinePreview,
|
|
1559
|
+
fullSessionTimeline,
|
|
1560
|
+
timelineTree: storedTimeline?.tree ?? session.timelineTree,
|
|
1561
|
+
sessionStory,
|
|
1562
|
+
reviewerReport: session.reviewerReport && sessionStory
|
|
1563
|
+
? {
|
|
1564
|
+
...session.reviewerReport,
|
|
1565
|
+
sessionStory: session.reviewerReport.sessionStoryRef === 'session'
|
|
1566
|
+
? sessionStory
|
|
1567
|
+
: session.reviewerReport.sessionStory,
|
|
1568
|
+
}
|
|
1569
|
+
: session.reviewerReport,
|
|
1570
|
+
};
|
|
1571
|
+
});
|
|
1572
|
+
return {
|
|
1573
|
+
...report,
|
|
1574
|
+
schemaVersion: OBSERVATION_EXPERIENCE_SCHEMA_VERSION,
|
|
1575
|
+
invocations,
|
|
1576
|
+
sessions,
|
|
1577
|
+
};
|
|
1578
|
+
}
|
|
1579
|
+
function traceTimelinesFromSessions(sessions, invocations) {
|
|
1580
|
+
const invocationById = new Map(invocations.map((invocation) => [invocation.id, invocation]));
|
|
1581
|
+
const timelines = new Map();
|
|
1582
|
+
for (const session of sessions) {
|
|
1583
|
+
const invocation = session.invocationIds
|
|
1584
|
+
.map((id) => invocationById.get(id))
|
|
1585
|
+
.find((value) => Boolean(value));
|
|
1586
|
+
const sessionGroupKey = invocation?.sessionGroupKey ?? session.sessionId;
|
|
1587
|
+
const id = session.timelineRef
|
|
1588
|
+
?? invocation?.timelineRef
|
|
1589
|
+
?? timelineRefForSessionGroup(sessionGroupKey);
|
|
1590
|
+
if (timelines.has(id))
|
|
1591
|
+
continue;
|
|
1592
|
+
const fallbackEvents = session.fullSessionTimeline.length > 0
|
|
1593
|
+
? session.fullSessionTimeline
|
|
1594
|
+
: session.timelinePreview;
|
|
1595
|
+
const tree = session.timelineTree ?? {
|
|
1596
|
+
sessionId: session.sessionId,
|
|
1597
|
+
main: fallbackEvents,
|
|
1598
|
+
branches: [],
|
|
1599
|
+
};
|
|
1600
|
+
timelines.set(id, {
|
|
1601
|
+
id,
|
|
1602
|
+
sessionGroupKey,
|
|
1603
|
+
sessionId: session.sessionId,
|
|
1604
|
+
eventCount: flattenTimelineTree(tree).length,
|
|
1605
|
+
tree,
|
|
1606
|
+
});
|
|
1607
|
+
}
|
|
1608
|
+
return Array.from(timelines.values());
|
|
1609
|
+
}
|
|
1610
|
+
function validateExperienceReferences(report, strict) {
|
|
1611
|
+
if (!isObjectRecord(report.meta)
|
|
1612
|
+
|| report.meta.sessionCount !== report.sessions.length
|
|
1613
|
+
|| report.meta.skillCount !== report.skills.length
|
|
1614
|
+
|| report.meta.invocationCount !== report.invocations.length
|
|
1615
|
+
|| report.meta.goalSliceCount !== report.goalSlices.length)
|
|
1616
|
+
return false;
|
|
1617
|
+
const timelineEventsByRef = new Map();
|
|
1618
|
+
const timelineByRef = new Map();
|
|
1619
|
+
const timelineSessionGroupKeys = new Set();
|
|
1620
|
+
for (const timeline of report.traceTimelines) {
|
|
1621
|
+
if (timelineEventsByRef.has(timeline.id)
|
|
1622
|
+
|| timelineSessionGroupKeys.has(timeline.sessionGroupKey))
|
|
1623
|
+
return false;
|
|
1624
|
+
if (timeline.tree.sessionId !== timeline.sessionId
|
|
1625
|
+
|| (strict && !traceTimelineStructureIsConsistent(timeline)))
|
|
1626
|
+
return false;
|
|
1627
|
+
const rawEvents = [
|
|
1628
|
+
...timeline.tree.main,
|
|
1629
|
+
...timeline.tree.branches.flatMap((branch) => branch.events),
|
|
1630
|
+
];
|
|
1631
|
+
if (strict
|
|
1632
|
+
&& (rawEvents.some((event) => typeof event.traceId !== 'string')
|
|
1633
|
+
|| rawEvents.some((event) => (event.kind === 'tool_use' || event.kind === 'tool_result')
|
|
1634
|
+
&& typeof event.callInstanceId !== 'string')
|
|
1635
|
+
|| timeline.tree.branches.some((branch) => typeof branch.traceId !== 'string'
|
|
1636
|
+
|| (branch.attachTo !== undefined && typeof branch.attachTo.traceId !== 'string'))))
|
|
1637
|
+
return false;
|
|
1638
|
+
if (new Set(rawEvents.map((event) => event.id)).size !== rawEvents.length)
|
|
1639
|
+
return false;
|
|
1640
|
+
const events = flattenTimelineTree(timeline.tree);
|
|
1641
|
+
if (timeline.eventCount !== events.length)
|
|
1642
|
+
return false;
|
|
1643
|
+
timelineEventsByRef.set(timeline.id, new Set(events.map((event) => event.id)));
|
|
1644
|
+
timelineByRef.set(timeline.id, timeline);
|
|
1645
|
+
timelineSessionGroupKeys.add(timeline.sessionGroupKey);
|
|
1646
|
+
}
|
|
1647
|
+
const goalSliceIds = new Set();
|
|
1648
|
+
const goalSliceById = new Map();
|
|
1649
|
+
for (const goalSlice of report.goalSlices) {
|
|
1650
|
+
if (!isExperienceGoalSlice(goalSlice)
|
|
1651
|
+
|| (strict
|
|
1652
|
+
&& (typeof goalSlice.traceId !== 'string'
|
|
1653
|
+
|| typeof goalSlice.timestampObserved !== 'boolean'))
|
|
1654
|
+
|| goalSliceIds.has(goalSlice.id))
|
|
1655
|
+
return false;
|
|
1656
|
+
goalSliceIds.add(goalSlice.id);
|
|
1657
|
+
goalSliceById.set(goalSlice.id, goalSlice);
|
|
1658
|
+
}
|
|
1659
|
+
const invocationById = new Map();
|
|
1660
|
+
const referencedGoalSliceIds = new Set();
|
|
1661
|
+
for (const invocation of report.invocations) {
|
|
1662
|
+
if (invocationById.has(invocation.id))
|
|
1663
|
+
return false;
|
|
1664
|
+
invocationById.set(invocation.id, invocation);
|
|
1665
|
+
if (strict
|
|
1666
|
+
&& (countRecordTotal(invocation.toolCounts) !== invocation.metrics.numToolCalls
|
|
1667
|
+
|| !toolOutcomeCountsMatch(invocation.metrics, invocation.indicators)))
|
|
1668
|
+
return false;
|
|
1669
|
+
if (!goalSliceIds.has(invocation.goalSliceId))
|
|
1670
|
+
return false;
|
|
1671
|
+
const goalSlice = goalSliceById.get(invocation.goalSliceId);
|
|
1672
|
+
if (goalSlice
|
|
1673
|
+
&& (goalSlice.skillName !== invocation.skillName
|
|
1674
|
+
|| goalSlice.sessionId !== invocation.sessionId
|
|
1675
|
+
|| goalSlice.sourceTrace !== invocation.sourceTrace
|
|
1676
|
+
|| goalSlice.startTimestamp !== invocation.startTimestamp
|
|
1677
|
+
|| goalSlice.endTimestamp !== invocation.endTimestamp
|
|
1678
|
+
|| (strict
|
|
1679
|
+
&& (goalSlice.traceId !== invocation.traceId
|
|
1680
|
+
|| goalSlice.timestampObserved !== invocation.timestampObserved))))
|
|
1681
|
+
return false;
|
|
1682
|
+
referencedGoalSliceIds.add(invocation.goalSliceId);
|
|
1683
|
+
if (strict && (!invocation.timelineRef || !invocation.timelineEventIds))
|
|
1684
|
+
return false;
|
|
1685
|
+
if (!invocation.timelineRef) {
|
|
1686
|
+
if (invocation.timeline.length === 0 && (invocation.timelineEventIds?.length ?? 0) > 0)
|
|
1687
|
+
return false;
|
|
1688
|
+
continue;
|
|
1689
|
+
}
|
|
1690
|
+
const eventIds = timelineEventsByRef.get(invocation.timelineRef);
|
|
1691
|
+
if (!eventIds)
|
|
1692
|
+
return false;
|
|
1693
|
+
if (timelineByRef.get(invocation.timelineRef)?.sessionGroupKey !== invocation.sessionGroupKey)
|
|
1694
|
+
return false;
|
|
1695
|
+
if (invocation.timelineEventIds
|
|
1696
|
+
&& new Set(invocation.timelineEventIds).size !== invocation.timelineEventIds.length)
|
|
1697
|
+
return false;
|
|
1698
|
+
if (invocation.timelineEventIds?.some((id) => !eventIds.has(id)))
|
|
1699
|
+
return false;
|
|
1700
|
+
}
|
|
1701
|
+
if (referencedGoalSliceIds.size !== goalSliceIds.size)
|
|
1702
|
+
return false;
|
|
1703
|
+
const sessionIds = new Set();
|
|
1704
|
+
const invocationReferenceCounts = new Map(report.invocations.map((invocation) => [invocation.id, 0]));
|
|
1705
|
+
const referencedTimelineIds = new Set();
|
|
1706
|
+
const referencedStoryContextIds = new Set();
|
|
1707
|
+
for (const session of report.sessions) {
|
|
1708
|
+
if (sessionIds.has(session.id))
|
|
1709
|
+
return false;
|
|
1710
|
+
sessionIds.add(session.id);
|
|
1711
|
+
if (session.invocationIds.length === 0
|
|
1712
|
+
|| new Set(session.invocationIds).size !== session.invocationIds.length
|
|
1713
|
+
|| session.invocationIds.some((id) => !invocationById.has(id)))
|
|
1714
|
+
return false;
|
|
1715
|
+
if (session.goalSliceIds.some((id) => !goalSliceIds.has(id)))
|
|
1716
|
+
return false;
|
|
1717
|
+
const sessionInvocations = session.invocationIds
|
|
1718
|
+
.map((id) => invocationById.get(id))
|
|
1719
|
+
.filter((value) => Boolean(value));
|
|
1720
|
+
const sessionGroupKeys = new Set(sessionInvocations.map((invocation) => invocation.sessionGroupKey));
|
|
1721
|
+
const invocationGoalSliceIds = new Set(sessionInvocations.map((invocation) => invocation.goalSliceId));
|
|
1722
|
+
if (sessionInvocations.some((invocation) => invocation.skillName !== session.skillName)
|
|
1723
|
+
|| sessionGroupKeys.size !== 1
|
|
1724
|
+
|| session.goalSliceIds.length !== invocationGoalSliceIds.size
|
|
1725
|
+
|| session.goalSliceIds.some((id) => !invocationGoalSliceIds.has(id)))
|
|
1726
|
+
return false;
|
|
1727
|
+
for (const invocation of sessionInvocations) {
|
|
1728
|
+
invocationReferenceCounts.set(invocation.id, (invocationReferenceCounts.get(invocation.id) ?? 0) + 1);
|
|
1729
|
+
}
|
|
1730
|
+
if (strict) {
|
|
1731
|
+
const timestampedInvocations = sessionInvocations.filter(invocationTimestampObserved);
|
|
1732
|
+
const expectedStartTimestamp = minString(timestampedInvocations.map((invocation) => invocation.startTimestamp)) ?? sessionInvocations[0]?.startTimestamp;
|
|
1733
|
+
const expectedEndTimestamp = maxString(timestampedInvocations.map((invocation) => invocation.endTimestamp)) ?? sessionInvocations[0]?.endTimestamp;
|
|
1734
|
+
if (sessionInvocations.some((invocation) => invocation.timelineRef !== session.timelineRef))
|
|
1735
|
+
return false;
|
|
1736
|
+
const invocationIndicators = sumIndicators(sessionInvocations.map((invocation) => invocation.indicators));
|
|
1737
|
+
const expectedIndicators = enrichRouterDownstreamIndicators({
|
|
1738
|
+
...session,
|
|
1739
|
+
indicators: invocationIndicators,
|
|
1740
|
+
});
|
|
1741
|
+
const expectedReviewPriorityScore = scoreForIndicators(expectedIndicators);
|
|
1742
|
+
const expectedReviewPriority = session.reviewerReport
|
|
1743
|
+
? priorityForReviewerFindings({
|
|
1744
|
+
...session,
|
|
1745
|
+
indicators: expectedIndicators,
|
|
1746
|
+
reviewPriorityScore: expectedReviewPriorityScore,
|
|
1747
|
+
}, session.reviewerReport.findings)
|
|
1748
|
+
: priorityForScore(expectedReviewPriorityScore);
|
|
1749
|
+
if (!indicatorRecordsEqual(session.indicators, expectedIndicators)
|
|
1750
|
+
|| session.reviewPriorityScore !== expectedReviewPriorityScore
|
|
1751
|
+
|| session.reviewPriority !== expectedReviewPriority
|
|
1752
|
+
|| !sameStringArray(session.reviewBasisCodes, basisCodesForIndicators(expectedIndicators))
|
|
1753
|
+
|| session.startTimestamp !== expectedStartTimestamp
|
|
1754
|
+
|| session.endTimestamp !== expectedEndTimestamp
|
|
1755
|
+
|| session.timestampedInvocationCount !== timestampedInvocations.length
|
|
1756
|
+
|| session.timestampCoverage !== timestampedInvocations.length / sessionInvocations.length)
|
|
1757
|
+
return false;
|
|
1758
|
+
const sessionGroupKey = sessionInvocations[0]?.sessionGroupKey;
|
|
1759
|
+
if (sessionGroupKey
|
|
1760
|
+
&& timelineByRef.get(session.timelineRef ?? '')?.sessionGroupKey !== sessionGroupKey)
|
|
1761
|
+
return false;
|
|
1762
|
+
}
|
|
1763
|
+
if (strict && (!session.timelineRef || !session.timelinePreviewEventIds))
|
|
1764
|
+
return false;
|
|
1765
|
+
if (session.timelineRef) {
|
|
1766
|
+
referencedTimelineIds.add(session.timelineRef);
|
|
1767
|
+
const eventIds = timelineEventsByRef.get(session.timelineRef);
|
|
1768
|
+
const timeline = timelineByRef.get(session.timelineRef);
|
|
1769
|
+
if (!eventIds)
|
|
1770
|
+
return false;
|
|
1771
|
+
if (session.timelinePreviewEventIds
|
|
1772
|
+
&& new Set(session.timelinePreviewEventIds).size !== session.timelinePreviewEventIds.length)
|
|
1773
|
+
return false;
|
|
1774
|
+
if (session.timelinePreviewEventIds?.some((id) => !eventIds.has(id)))
|
|
1775
|
+
return false;
|
|
1776
|
+
if (strict
|
|
1777
|
+
&& (timeline?.sessionId !== session.sessionId
|
|
1778
|
+
|| !timelineScopeMatches(session, sessionInvocations, timeline)))
|
|
1779
|
+
return false;
|
|
1780
|
+
}
|
|
1781
|
+
else if (session.fullSessionTimeline.length === 0
|
|
1782
|
+
&& session.timelinePreview.length === 0
|
|
1783
|
+
&& (session.timelinePreviewEventIds?.length ?? 0) > 0) {
|
|
1784
|
+
return false;
|
|
1785
|
+
}
|
|
1786
|
+
if (session.sessionStory?.contextRef) {
|
|
1787
|
+
referencedStoryContextIds.add(session.sessionStory.contextRef);
|
|
1788
|
+
}
|
|
1789
|
+
if (strict
|
|
1790
|
+
&& session.reviewerReport
|
|
1791
|
+
&& !reviewerMetricsMatch(session.reviewerReport, session, sessionInvocations, timelineByRef.get(session.timelineRef ?? '')))
|
|
1792
|
+
return false;
|
|
1793
|
+
}
|
|
1794
|
+
if (Array.from(invocationReferenceCounts.values()).some((count) => count !== 1)
|
|
1795
|
+
|| (strict && referencedTimelineIds.size !== timelineEventsByRef.size))
|
|
1796
|
+
return false;
|
|
1797
|
+
const storyContextIds = new Set();
|
|
1798
|
+
const storyContextById = new Map();
|
|
1799
|
+
const storyContextSessionGroupKeys = new Set();
|
|
1800
|
+
for (const context of report.storyContexts) {
|
|
1801
|
+
if (storyContextIds.has(context.id)
|
|
1802
|
+
|| storyContextSessionGroupKeys.has(context.sessionGroupKey))
|
|
1803
|
+
return false;
|
|
1804
|
+
storyContextIds.add(context.id);
|
|
1805
|
+
storyContextById.set(context.id, context);
|
|
1806
|
+
storyContextSessionGroupKeys.add(context.sessionGroupKey);
|
|
1807
|
+
}
|
|
1808
|
+
const skillNames = new Set();
|
|
1809
|
+
for (const skill of report.skills) {
|
|
1810
|
+
if (!isExperienceSkillSummary(skill, strict)
|
|
1811
|
+
|| skillNames.has(skill.skillName))
|
|
1812
|
+
return false;
|
|
1813
|
+
if (strict) {
|
|
1814
|
+
const skillInvocations = report.invocations.filter((invocation) => invocation.skillName === skill.skillName);
|
|
1815
|
+
const skillSessions = report.sessions.filter((session) => session.skillName === skill.skillName);
|
|
1816
|
+
const timestampedInvocations = skillInvocations.filter(invocationTimestampObserved);
|
|
1817
|
+
const expectedFirstSeen = minString(timestampedInvocations.map((invocation) => invocation.startTimestamp)) ?? skillSessions[0]?.startTimestamp;
|
|
1818
|
+
const expectedLastSeen = maxString(timestampedInvocations.map((invocation) => invocation.endTimestamp)) ?? skillSessions[0]?.endTimestamp;
|
|
1819
|
+
if (skill.invocationCount !== skillInvocations.length
|
|
1820
|
+
|| skill.sessionCount !== skillSessions.length
|
|
1821
|
+
|| !indicatorRecordsEqual(skill.indicators, sumIndicators(skillSessions.map((session) => session.indicators)))
|
|
1822
|
+
|| !countRecordsEqual(skill.toolCounts, sumRecordCounts(skillInvocations.map((invocation) => invocation.toolCounts)))
|
|
1823
|
+
|| skill.firstSeen !== expectedFirstSeen
|
|
1824
|
+
|| skill.lastSeen !== expectedLastSeen
|
|
1825
|
+
|| skill.timestampedInvocationCount !== timestampedInvocations.length
|
|
1826
|
+
|| skill.timestampCoverage !== (skillInvocations.length > 0
|
|
1827
|
+
? timestampedInvocations.length / skillInvocations.length
|
|
1828
|
+
: 0)
|
|
1829
|
+
|| skill.reviewFirstSessionCount !== skillSessions.filter((session) => session.reviewPriority === 'review_first').length
|
|
1830
|
+
|| skill.sampleReviewSessionCount !== skillSessions.filter((session) => session.reviewPriority === 'sample_review').length)
|
|
1831
|
+
return false;
|
|
1832
|
+
}
|
|
1833
|
+
skillNames.add(skill.skillName);
|
|
1834
|
+
}
|
|
1835
|
+
if (report.sessions.some((session) => !skillNames.has(session.skillName))
|
|
1836
|
+
|| report.invocations.some((invocation) => !skillNames.has(invocation.skillName))
|
|
1837
|
+
|| (strict && referencedStoryContextIds.size !== storyContextIds.size))
|
|
1838
|
+
return false;
|
|
1839
|
+
return report.sessions.every((session) => {
|
|
1840
|
+
const contextRef = session.sessionStory?.contextRef;
|
|
1841
|
+
if (strict && session.sessionStory && !contextRef)
|
|
1842
|
+
return false;
|
|
1843
|
+
if (contextRef && !storyContextIds.has(contextRef))
|
|
1844
|
+
return false;
|
|
1845
|
+
if (strict && contextRef) {
|
|
1846
|
+
const firstInvocation = session.invocationIds
|
|
1847
|
+
.map((id) => invocationById.get(id))
|
|
1848
|
+
.find((value) => Boolean(value));
|
|
1849
|
+
const storyInvocations = firstInvocation
|
|
1850
|
+
? report.invocations.filter((invocation) => invocation.sessionGroupKey === firstInvocation.sessionGroupKey)
|
|
1851
|
+
: [];
|
|
1852
|
+
const context = storyContextById.get(contextRef);
|
|
1853
|
+
const timeline = timelineByRef.get(session.timelineRef ?? '');
|
|
1854
|
+
if (firstInvocation
|
|
1855
|
+
&& context?.sessionGroupKey !== firstInvocation.sessionGroupKey)
|
|
1856
|
+
return false;
|
|
1857
|
+
if (session.sessionStory
|
|
1858
|
+
&& context
|
|
1859
|
+
&& timeline
|
|
1860
|
+
&& !sessionStoryStructureIsConsistent(session.sessionStory, context, storyInvocations, timeline))
|
|
1861
|
+
return false;
|
|
1862
|
+
}
|
|
1863
|
+
if (strict
|
|
1864
|
+
&& session.reviewerReport
|
|
1865
|
+
&& session.reviewerReport.sessionStoryRef !== 'session'
|
|
1866
|
+
&& !isObjectRecord(session.reviewerReport.sessionStory))
|
|
1867
|
+
return false;
|
|
1868
|
+
return session.reviewerReport?.sessionStoryRef !== 'session' || Boolean(session.sessionStory);
|
|
1869
|
+
});
|
|
1870
|
+
}
|
|
1871
|
+
function sessionStoryStructureIsConsistent(story, context, invocations, timeline) {
|
|
1872
|
+
const timelineEvents = flattenTimelineTree(timeline.tree);
|
|
1873
|
+
const expectedGoalSliceIds = new Set(invocations.map((invocation) => invocation.goalSliceId));
|
|
1874
|
+
const contextGoalSliceIds = context.goalSlices.map((goalSlice) => goalSlice.id);
|
|
1875
|
+
if (story.invocationCount !== invocations.length
|
|
1876
|
+
|| story.goalSliceCount !== expectedGoalSliceIds.size
|
|
1877
|
+
|| contextGoalSliceIds.length !== expectedGoalSliceIds.size
|
|
1878
|
+
|| new Set(contextGoalSliceIds).size !== contextGoalSliceIds.length
|
|
1879
|
+
|| contextGoalSliceIds.some((id) => !expectedGoalSliceIds.has(id))
|
|
1880
|
+
|| story.branchCount !== timeline.tree.branches.length
|
|
1881
|
+
|| story.progressUpdateCount !== timelineEvents.filter(isAssistantProgressUpdateEvent).length
|
|
1882
|
+
|| story.finalDeliverySignalCount !== timelineEvents.filter(isAssistantDeliveryEvent).length)
|
|
1883
|
+
return false;
|
|
1884
|
+
if (!storyContextEpisodesAreConsistent(context, invocations, timeline.sessionId))
|
|
1885
|
+
return false;
|
|
1886
|
+
const branchById = new Map(timeline.tree.branches.map((branch) => [branch.id, branch]));
|
|
1887
|
+
if (context.subagentDispatches.length !== branchById.size
|
|
1888
|
+
|| new Set(context.subagentDispatches.map((dispatch) => dispatch.id)).size
|
|
1889
|
+
!== context.subagentDispatches.length
|
|
1890
|
+
|| new Set(context.subagentDispatches.map((dispatch) => dispatch.branchId)).size
|
|
1891
|
+
!== context.subagentDispatches.length
|
|
1892
|
+
|| context.subagentDispatches.some((dispatch) => {
|
|
1893
|
+
const branch = branchById.get(dispatch.branchId);
|
|
1894
|
+
return !branch
|
|
1895
|
+
|| dispatch.childSessionId !== branch.sessionId
|
|
1896
|
+
|| dispatch.traceId !== (branch.traceId ?? branch.sourceTrace)
|
|
1897
|
+
|| dispatch.sourceTrace !== branch.sourceTrace
|
|
1898
|
+
|| dispatch.eventCount !== branch.events.length;
|
|
1899
|
+
}))
|
|
1900
|
+
return false;
|
|
1901
|
+
const invocationById = new Map(invocations.map((invocation) => [invocation.id, invocation]));
|
|
1902
|
+
const invocationReferenceCounts = new Map(invocations.map((invocation) => [invocation.id, 0]));
|
|
1903
|
+
const skillLinkIds = new Set();
|
|
1904
|
+
for (const link of story.skillLinks) {
|
|
1905
|
+
if (skillLinkIds.has(link.id)
|
|
1906
|
+
|| new Set(link.invocationIds).size !== link.invocationIds.length
|
|
1907
|
+
|| link.invocationIds.some((id) => !invocationById.has(id)))
|
|
1908
|
+
return false;
|
|
1909
|
+
skillLinkIds.add(link.id);
|
|
1910
|
+
const linkedInvocations = link.invocationIds
|
|
1911
|
+
.map((id) => invocationById.get(id))
|
|
1912
|
+
.filter((value) => Boolean(value));
|
|
1913
|
+
const linkedGoalSliceIds = new Set(linkedInvocations.map((invocation) => invocation.goalSliceId));
|
|
1914
|
+
if (linkedInvocations.some((invocation) => invocation.skillName !== link.skillName)
|
|
1915
|
+
|| link.goalSliceIds.length !== linkedGoalSliceIds.size
|
|
1916
|
+
|| link.goalSliceIds.some((id) => !linkedGoalSliceIds.has(id)))
|
|
1917
|
+
return false;
|
|
1918
|
+
for (const invocation of linkedInvocations) {
|
|
1919
|
+
invocationReferenceCounts.set(invocation.id, (invocationReferenceCounts.get(invocation.id) ?? 0) + 1);
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
if ([...invocationReferenceCounts.values()].some((count) => count !== 1))
|
|
1923
|
+
return false;
|
|
1924
|
+
const nodeIds = story.nodes.map((node) => node.id);
|
|
1925
|
+
const nodeIdSet = new Set(nodeIds);
|
|
1926
|
+
const graphNodeIds = story.graph.nodes.map((node) => node.id);
|
|
1927
|
+
const expectedGraphNodeIds = new Set([...nodeIds, ...skillLinkIds]);
|
|
1928
|
+
if (nodeIdSet.size !== nodeIds.length
|
|
1929
|
+
|| new Set(story.mainlineNodeIds).size !== story.mainlineNodeIds.length
|
|
1930
|
+
|| story.mainlineNodeIds.some((id) => !nodeIdSet.has(id))
|
|
1931
|
+
|| new Set(graphNodeIds).size !== graphNodeIds.length
|
|
1932
|
+
|| graphNodeIds.length !== expectedGraphNodeIds.size
|
|
1933
|
+
|| graphNodeIds.some((id) => !expectedGraphNodeIds.has(id))
|
|
1934
|
+
|| story.graph.nodes.some((node) => node.detailNodeId !== undefined && !nodeIdSet.has(node.detailNodeId))
|
|
1935
|
+
|| story.graph.edges.some((edge) => !expectedGraphNodeIds.has(edge.fromId) || !expectedGraphNodeIds.has(edge.toId)))
|
|
1936
|
+
return false;
|
|
1937
|
+
return true;
|
|
1938
|
+
}
|
|
1939
|
+
function timelineScopeMatches(session, invocations, timeline) {
|
|
1940
|
+
if (!timeline)
|
|
1941
|
+
return false;
|
|
1942
|
+
const events = flattenTimelineTree(timeline.tree);
|
|
1943
|
+
const eventById = new Map(events.map((event) => [event.id, event]));
|
|
1944
|
+
const invocationEventIds = new Set(invocations.flatMap((invocation) => invocation.timelineEventIds ?? []));
|
|
1945
|
+
const previewEventIds = session.timelinePreviewEventIds ?? [];
|
|
1946
|
+
if (previewEventIds.some((id) => !invocationEventIds.has(id)))
|
|
1947
|
+
return false;
|
|
1948
|
+
const invocationEvents = [...invocationEventIds]
|
|
1949
|
+
.map((id) => eventById.get(id))
|
|
1950
|
+
.filter((event) => Boolean(event));
|
|
1951
|
+
if (invocationEvents.length !== invocationEventIds.size)
|
|
1952
|
+
return false;
|
|
1953
|
+
const previewEvents = previewEventIds
|
|
1954
|
+
.map((id) => eventById.get(id))
|
|
1955
|
+
.filter((event) => Boolean(event));
|
|
1956
|
+
const eventPositionById = new Map(events.map((event, index) => [event.id, index]));
|
|
1957
|
+
const previewPositions = previewEventIds
|
|
1958
|
+
.map((id) => eventPositionById.get(id))
|
|
1959
|
+
.filter((index) => typeof index === 'number');
|
|
1960
|
+
const omittedBeforeCount = previewPositions.length > 0 ? Math.min(...previewPositions) : 0;
|
|
1961
|
+
const omittedAfterCount = previewPositions.length > 0
|
|
1962
|
+
? events.length - 1 - Math.max(...previewPositions)
|
|
1963
|
+
: 0;
|
|
1964
|
+
const scope = session.timelineScope;
|
|
1965
|
+
return scope.segmentEventCount === invocationEventIds.size
|
|
1966
|
+
&& scope.previewEventCount === previewEventIds.length
|
|
1967
|
+
&& scope.fullSessionEventCount === events.length
|
|
1968
|
+
&& traceRecordRangesEqual(scope.segmentRecordRanges, traceRecordRanges(invocationEvents))
|
|
1969
|
+
&& traceRecordRangesEqual(scope.previewRecordRanges, traceRecordRanges(previewEvents))
|
|
1970
|
+
&& traceRecordRangesEqual(scope.sessionRecordRanges, traceRecordRanges(events))
|
|
1971
|
+
&& scope.omittedBeforeCount === omittedBeforeCount
|
|
1972
|
+
&& scope.omittedAfterCount === omittedAfterCount
|
|
1973
|
+
&& scope.truncated === (invocationEventIds.size > previewEventIds.length
|
|
1974
|
+
|| omittedBeforeCount > 0
|
|
1975
|
+
|| omittedAfterCount > 0);
|
|
1976
|
+
}
|
|
1977
|
+
function traceRecordRangesEqual(left, right) {
|
|
1978
|
+
return left.length === right.length
|
|
1979
|
+
&& left.every((range, index) => range.traceId === right[index].traceId
|
|
1980
|
+
&& range.sourceTrace === right[index].sourceTrace
|
|
1981
|
+
&& range.startRecordIndex === right[index].startRecordIndex
|
|
1982
|
+
&& range.endRecordIndex === right[index].endRecordIndex
|
|
1983
|
+
&& range.eventCount === right[index].eventCount);
|
|
1984
|
+
}
|
|
1985
|
+
function storyContextEpisodesAreConsistent(context, invocations, sessionId) {
|
|
1986
|
+
const goalSliceIds = new Set(context.goalSlices.map((goalSlice) => goalSlice.id));
|
|
1987
|
+
const invocationById = new Map(invocations.map((invocation) => [invocation.id, invocation]));
|
|
1988
|
+
const episodeIds = new Set();
|
|
1989
|
+
for (const episode of context.episodes) {
|
|
1990
|
+
if (episodeIds.has(episode.id) || episode.sessionId !== sessionId)
|
|
1991
|
+
return false;
|
|
1992
|
+
episodeIds.add(episode.id);
|
|
1993
|
+
const skillSegmentById = new Map();
|
|
1994
|
+
for (const segment of episode.skillSegments) {
|
|
1995
|
+
if (skillSegmentById.has(segment.id)
|
|
1996
|
+
|| new Set(segment.skillInvocationIds).size !== segment.skillInvocationIds.length
|
|
1997
|
+
|| segment.skillInvocationIds.some((id) => invocationById.get(id)?.skillName !== segment.skillName)
|
|
1998
|
+
|| Date.parse(segment.startTimestamp) < Date.parse(episode.startTimestamp)
|
|
1999
|
+
|| Date.parse(segment.endTimestamp) > Date.parse(episode.endTimestamp))
|
|
2000
|
+
return false;
|
|
2001
|
+
skillSegmentById.set(segment.id, segment);
|
|
2002
|
+
}
|
|
2003
|
+
const edgeIds = new Set();
|
|
2004
|
+
for (const edge of episode.orchestrationEdges) {
|
|
2005
|
+
const parent = edge.parentSkillSegmentId
|
|
2006
|
+
? skillSegmentById.get(edge.parentSkillSegmentId)
|
|
2007
|
+
: undefined;
|
|
2008
|
+
const executor = edge.executorSkillSegmentId
|
|
2009
|
+
? skillSegmentById.get(edge.executorSkillSegmentId)
|
|
2010
|
+
: undefined;
|
|
2011
|
+
if (edgeIds.has(edge.id)
|
|
2012
|
+
|| edge.episodeId !== episode.id
|
|
2013
|
+
|| (edge.parentSkillSegmentId !== undefined && !parent)
|
|
2014
|
+
|| (edge.executorSkillSegmentId !== undefined && !executor)
|
|
2015
|
+
|| (edge.edgeKind === 'internal_skill'
|
|
2016
|
+
&& (!parent || !executor || parent.id === executor.id)))
|
|
2017
|
+
return false;
|
|
2018
|
+
edgeIds.add(edge.id);
|
|
2019
|
+
}
|
|
2020
|
+
const signalIds = new Set();
|
|
2021
|
+
for (const signal of episode.feedbackSignals) {
|
|
2022
|
+
if (signalIds.has(signal.id))
|
|
2023
|
+
return false;
|
|
2024
|
+
signalIds.add(signal.id);
|
|
2025
|
+
const attributionGroups = [
|
|
2026
|
+
signal.attributions,
|
|
2027
|
+
signal.canonicalAttributions ?? [],
|
|
2028
|
+
];
|
|
2029
|
+
for (const attribution of attributionGroups.flat()) {
|
|
2030
|
+
if (!attribution.skillSegmentId)
|
|
2031
|
+
continue;
|
|
2032
|
+
const segment = skillSegmentById.get(attribution.skillSegmentId);
|
|
2033
|
+
if (!segment || (attribution.skillName && attribution.skillName !== segment.skillName)) {
|
|
2034
|
+
return false;
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
if (episode.goalEvidenceRefs.some((ref) => ref.kind === 'goal_slice'
|
|
2039
|
+
? !ref.goalSliceId || !goalSliceIds.has(ref.goalSliceId)
|
|
2040
|
+
: ref.goalSliceId !== undefined && !goalSliceIds.has(ref.goalSliceId)))
|
|
2041
|
+
return false;
|
|
2042
|
+
}
|
|
2043
|
+
return true;
|
|
2044
|
+
}
|
|
2045
|
+
function traceTimelineStructureIsConsistent(timeline) {
|
|
2046
|
+
const branches = timeline.tree.branches;
|
|
2047
|
+
if (new Set(branches.map((branch) => branch.id)).size !== branches.length)
|
|
2048
|
+
return false;
|
|
2049
|
+
if (timeline.tree.main.some((event) => event.sessionId !== timeline.sessionId))
|
|
2050
|
+
return false;
|
|
2051
|
+
const mainTraceIds = new Set(timeline.tree.main
|
|
2052
|
+
.map((event) => event.traceId)
|
|
2053
|
+
.filter((value) => Boolean(value)));
|
|
2054
|
+
const mainSourceTraces = new Set(timeline.tree.main.map((event) => event.sourceTrace));
|
|
2055
|
+
if (mainTraceIds.size > 1 || mainSourceTraces.size > 1)
|
|
2056
|
+
return false;
|
|
2057
|
+
for (const branch of branches) {
|
|
2058
|
+
if (branch.events.some((event) => event.sessionId !== timeline.sessionId
|
|
2059
|
+
|| (branch.traceId !== undefined && event.traceId !== branch.traceId)
|
|
2060
|
+
|| event.sourceTrace !== branch.sourceTrace
|
|
2061
|
+
|| event.traceRole !== branch.traceRole))
|
|
2062
|
+
return false;
|
|
2063
|
+
if (branch.attachTo
|
|
2064
|
+
&& !timeline.tree.main.some((event) => (branch.attachTo?.traceId !== undefined
|
|
2065
|
+
? event.traceId === branch.attachTo.traceId
|
|
2066
|
+
: event.sourceTrace === branch.attachTo?.sourceTrace)
|
|
2067
|
+
&& event.sourceTrace === branch.attachTo?.sourceTrace
|
|
2068
|
+
&& (branch.attachTo.messageIndex === undefined
|
|
2069
|
+
|| event.messageIndex === branch.attachTo.messageIndex)
|
|
2070
|
+
&& (branch.attachTo.callInstanceId === undefined
|
|
2071
|
+
|| event.callInstanceId === branch.attachTo.callInstanceId)
|
|
2072
|
+
&& (branch.attachTo.toolUseId === undefined
|
|
2073
|
+
|| event.toolUseId === branch.attachTo.toolUseId)
|
|
2074
|
+
&& (branch.attachTo.label === undefined
|
|
2075
|
+
|| event.label === branch.attachTo.label
|
|
2076
|
+
|| event.toolName === branch.attachTo.label)))
|
|
2077
|
+
return false;
|
|
2078
|
+
}
|
|
2079
|
+
return true;
|
|
2080
|
+
}
|
|
2081
|
+
function toolOutcomeCountsMatch(left, right) {
|
|
2082
|
+
const normalized = 'numToolCalls' in left
|
|
2083
|
+
? {
|
|
2084
|
+
toolCallCount: left.numToolCalls,
|
|
2085
|
+
toolFailureCount: left.numToolFailures,
|
|
2086
|
+
toolCancelledCount: left.numToolCancelled,
|
|
2087
|
+
toolUnknownCount: left.numToolUnknown,
|
|
2088
|
+
}
|
|
2089
|
+
: left;
|
|
2090
|
+
return normalized.toolCallCount === right.toolCallCount
|
|
2091
|
+
&& normalized.toolFailureCount === right.toolFailureCount
|
|
2092
|
+
&& (normalized.toolCancelledCount ?? 0) === (right.toolCancelledCount ?? 0)
|
|
2093
|
+
&& (normalized.toolUnknownCount ?? 0) === (right.toolUnknownCount ?? 0);
|
|
2094
|
+
}
|
|
2095
|
+
function indicatorRecordsEqual(left, right) {
|
|
2096
|
+
const keys = [
|
|
2097
|
+
...EXPERIENCE_INDICATOR_KEYS,
|
|
2098
|
+
'toolCancelledCount',
|
|
2099
|
+
'toolUnknownCount',
|
|
2100
|
+
];
|
|
2101
|
+
return keys.every((key) => (left[key] ?? 0) === (right[key] ?? 0));
|
|
2102
|
+
}
|
|
2103
|
+
function reviewerMetricsMatch(reviewer, session, invocations, timeline) {
|
|
2104
|
+
const metrics = reviewer.oneLookMetrics;
|
|
2105
|
+
const expectedUsage = sumTokenUsage(invocations);
|
|
2106
|
+
const timelineEvents = timeline ? flattenTimelineTree(timeline.tree) : [];
|
|
2107
|
+
const hydratedSession = {
|
|
2108
|
+
...session,
|
|
2109
|
+
fullSessionTimeline: timelineEvents,
|
|
2110
|
+
timelineTree: timeline?.tree,
|
|
2111
|
+
};
|
|
2112
|
+
return toolOutcomeCountsMatch(metrics, session.indicators)
|
|
2113
|
+
&& metrics.userMessageCount === session.indicators.userMessageCount
|
|
2114
|
+
&& metrics.assistantDeliverySignalCount === session.indicators.assistantDeliverySignalCount
|
|
2115
|
+
&& metrics.deliverableArtifactSignalCount === session.indicators.deliverableArtifactSignalCount
|
|
2116
|
+
&& metrics.routerDownstreamCompleted === session.indicators.routerDownstreamCompleted
|
|
2117
|
+
&& metrics.routerDownstreamFailed === session.indicators.routerDownstreamFailed
|
|
2118
|
+
&& metrics.assistantProgressUpdateCount === assistantProgressUpdateEvents(hydratedSession).length
|
|
2119
|
+
&& metrics.selfCorrectionCount === session.indicators.selfCorrectionCount
|
|
2120
|
+
&& metrics.repeatedExecutionCount === session.indicators.repeatedExecutionCount
|
|
2121
|
+
&& metrics.finalDeliverySignalCount === assistantFinalDeliveryEvents(hydratedSession).length
|
|
2122
|
+
&& metrics.traceEventCount === timelineEvents.length
|
|
2123
|
+
&& metrics.tokenUsage.inputTokens === expectedUsage.inputTokens
|
|
2124
|
+
&& metrics.tokenUsage.outputTokens === expectedUsage.outputTokens
|
|
2125
|
+
&& metrics.tokenUsage.cacheReadTokens === expectedUsage.cacheReadTokens
|
|
2126
|
+
&& metrics.tokenUsage.cacheCreationTokens === expectedUsage.cacheCreationTokens
|
|
2127
|
+
&& metrics.tokenUsage.observedInvocationCount === expectedUsage.observedInvocationCount
|
|
2128
|
+
&& metrics.tokenUsage.invocationCount === expectedUsage.invocationCount
|
|
2129
|
+
&& metrics.tokenUsage.coverage === expectedUsage.coverage;
|
|
2130
|
+
}
|
|
2131
|
+
function countRecordTotal(value) {
|
|
2132
|
+
return sumSafeCounts(...Object.values(value));
|
|
2133
|
+
}
|
|
2134
|
+
function countRecordsEqual(left, right) {
|
|
2135
|
+
const keys = new Set([...Object.keys(left), ...Object.keys(right)]);
|
|
2136
|
+
return [...keys].every((key) => (left[key] ?? 0) === (right[key] ?? 0));
|
|
2137
|
+
}
|
|
2138
|
+
function sameStringArray(left, right) {
|
|
2139
|
+
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
2140
|
+
}
|
|
2141
|
+
function storyContextsFromSessions(sessions, invocations) {
|
|
2142
|
+
const invocationById = new Map(invocations.map((invocation) => [invocation.id, invocation]));
|
|
2143
|
+
const contexts = new Map();
|
|
2144
|
+
for (const session of sessions) {
|
|
2145
|
+
const story = session.sessionStory;
|
|
2146
|
+
if (!story)
|
|
2147
|
+
continue;
|
|
2148
|
+
const invocation = session.invocationIds
|
|
2149
|
+
.map((id) => invocationById.get(id))
|
|
2150
|
+
.find((value) => Boolean(value));
|
|
2151
|
+
const sessionGroupKey = invocation?.sessionGroupKey ?? session.sessionId;
|
|
2152
|
+
const id = story.contextRef ?? storyContextRefForSessionGroup(sessionGroupKey);
|
|
2153
|
+
if (contexts.has(id))
|
|
2154
|
+
continue;
|
|
2155
|
+
contexts.set(id, {
|
|
2156
|
+
id,
|
|
2157
|
+
sessionGroupKey,
|
|
2158
|
+
goalSlices: story.goalSlices,
|
|
2159
|
+
subagentDispatches: story.subagentDispatches,
|
|
2160
|
+
episodes: story.episodes ?? [],
|
|
2161
|
+
});
|
|
2162
|
+
}
|
|
2163
|
+
return Array.from(contexts.values());
|
|
2164
|
+
}
|
|
2165
|
+
function timelineRefForSessionGroup(sessionGroupKey) {
|
|
2166
|
+
return hashParts('trace-timeline', sessionGroupKey);
|
|
2167
|
+
}
|
|
2168
|
+
function storyContextRefForSessionGroup(sessionGroupKey) {
|
|
2169
|
+
return hashParts('story-context', sessionGroupKey);
|
|
2170
|
+
}
|
|
2171
|
+
function flattenTimelineTree(tree) {
|
|
2172
|
+
return uniqueTimelineEvents([
|
|
2173
|
+
...tree.main,
|
|
2174
|
+
...tree.branches.flatMap((branch) => branch.events),
|
|
2175
|
+
]).sort(compareTimelineEvents);
|
|
2176
|
+
}
|
|
2177
|
+
function compactSessionStory(story, contextRef) {
|
|
2178
|
+
const persisted = omitProperties(story, [
|
|
2179
|
+
'goalSlices',
|
|
2180
|
+
'subagentDispatches',
|
|
2181
|
+
'episodes',
|
|
2182
|
+
]);
|
|
2183
|
+
return { ...persisted, contextRef };
|
|
2184
|
+
}
|
|
2185
|
+
function omitSessionStory(report) {
|
|
2186
|
+
return omitProperties(report, ['sessionStory', 'sessionStoryRef']);
|
|
2187
|
+
}
|
|
2188
|
+
function omitProperties(value, keys) {
|
|
2189
|
+
const copy = { ...value };
|
|
2190
|
+
for (const key of keys)
|
|
2191
|
+
delete copy[key];
|
|
2192
|
+
return copy;
|
|
179
2193
|
}
|
|
180
2194
|
function relatedObservationItems(segment, items) {
|
|
181
2195
|
return items.filter((item) => item.skillName === segment.skillName
|
|
2196
|
+
&& (item.traceId && segment.traceId
|
|
2197
|
+
? item.traceId === segment.traceId
|
|
2198
|
+
: true)
|
|
2199
|
+
// main/subagent 可以共享逻辑 sessionId,且调用同一个 skill。新数据都有
|
|
2200
|
+
// sourceTrace,必须严格按物理 trace 归因;空字符串仅用于兼容旧聚合数据。
|
|
2201
|
+
&& (!item.sourceTrace || !segment.sourceTrace || item.sourceTrace === segment.sourceTrace)
|
|
182
2202
|
&& (item.sessionId === segment.sessionId || item.recentSessionIds.includes(segment.sessionId))
|
|
183
2203
|
&& timestampsOverlap(item.firstSeen, item.lastSeen, segment.startTimestamp, segment.endTimestamp));
|
|
184
2204
|
}
|
|
2205
|
+
function metricTimelineForSegment(timeline, segment) {
|
|
2206
|
+
const callInstanceIds = new Set(segment.toolCalls.flatMap((call) => call.callInstanceId ? [call.callInstanceId] : []));
|
|
2207
|
+
const legacyToolUseIds = new Set(segment.toolCalls.flatMap((call) => !call.callInstanceId && call.toolUseId ? [call.toolUseId] : []));
|
|
2208
|
+
return timeline.filter((event) => (event.kind !== 'tool_use' && event.kind !== 'tool_result')
|
|
2209
|
+
|| (event.callInstanceId
|
|
2210
|
+
? callInstanceIds.has(event.callInstanceId)
|
|
2211
|
+
: Boolean(event.toolUseId) && legacyToolUseIds.has(event.toolUseId)));
|
|
2212
|
+
}
|
|
185
2213
|
function logicalSessionId(session) {
|
|
186
|
-
return session.
|
|
2214
|
+
return session.rootRunId;
|
|
187
2215
|
}
|
|
188
2216
|
function experienceSessionGroupKey(session) {
|
|
189
2217
|
const logicalId = logicalSessionId(session);
|
|
190
|
-
if (session.
|
|
191
|
-
return `group:${session.
|
|
192
|
-
}
|
|
193
|
-
return `trace:${session.sourcePath}`;
|
|
194
|
-
}
|
|
195
|
-
function groupSessionsByLogicalId(sessions) {
|
|
196
|
-
const groups = new Map();
|
|
197
|
-
for (const session of sessions) {
|
|
198
|
-
const key = logicalSessionId(session);
|
|
199
|
-
const group = groups.get(key) ?? [];
|
|
200
|
-
group.push(session);
|
|
201
|
-
groups.set(key, group);
|
|
2218
|
+
if (session.role !== 'standalone' && session.groupPath) {
|
|
2219
|
+
return `group:${session.groupPath}\u0000${logicalId}`;
|
|
202
2220
|
}
|
|
203
|
-
|
|
204
|
-
groups.set(key, group.sort(compareSessionsForTimeline));
|
|
205
|
-
}
|
|
206
|
-
return groups;
|
|
2221
|
+
return `trace:${session.traceId}`;
|
|
207
2222
|
}
|
|
208
2223
|
function groupSessionsByExperienceKey(sessions) {
|
|
209
2224
|
const groups = new Map();
|
|
@@ -219,14 +2234,15 @@ function groupSessionsByExperienceKey(sessions) {
|
|
|
219
2234
|
return groups;
|
|
220
2235
|
}
|
|
221
2236
|
function compareSessionsForTimeline(a, b) {
|
|
222
|
-
const roleRank = (session) => session.
|
|
2237
|
+
const roleRank = (session) => session.role === 'main' ? 0 : session.role === 'standalone' ? 1 : 2;
|
|
223
2238
|
const rank = roleRank(a) - roleRank(b);
|
|
224
2239
|
if (rank !== 0)
|
|
225
2240
|
return rank;
|
|
226
2241
|
const time = (a.startTimestamp ?? '').localeCompare(b.startTimestamp ?? '');
|
|
227
2242
|
if (time !== 0)
|
|
228
2243
|
return time;
|
|
229
|
-
return a.sourcePath.localeCompare(b.sourcePath)
|
|
2244
|
+
return a.sourcePath.localeCompare(b.sourcePath)
|
|
2245
|
+
|| a.traceId.localeCompare(b.traceId);
|
|
230
2246
|
}
|
|
231
2247
|
function timestampsOverlap(aStart, aEnd, bStart, bEnd) {
|
|
232
2248
|
if (!aStart || !aEnd || !bStart || !bEnd)
|
|
@@ -235,45 +2251,58 @@ function timestampsOverlap(aStart, aEnd, bStart, bEnd) {
|
|
|
235
2251
|
}
|
|
236
2252
|
function segmentRecordBounds(session, segment) {
|
|
237
2253
|
if (typeof segment.startRecordIndex === 'number' && typeof segment.endRecordIndex === 'number') {
|
|
238
|
-
const rawStart =
|
|
239
|
-
const rawEnd =
|
|
2254
|
+
const rawStart = eventIndexForSourceIndex(session, segment.startRecordIndex, 'first');
|
|
2255
|
+
const rawEnd = eventIndexForSourceIndex(session, Math.max(segment.startRecordIndex, segment.endRecordIndex), 'last');
|
|
240
2256
|
const humanStart = Math.min(rawStart, previousHumanUserRecordIndex(session, rawStart) ?? rawStart);
|
|
241
2257
|
return {
|
|
242
2258
|
start: includeLeadingRuntimeContext(session, humanStart),
|
|
243
|
-
end: includeTrailingDeliveryContext(session, Math.min(session.
|
|
2259
|
+
end: includeTrailingDeliveryContext(session, Math.min(session.events.length, rawEnd + 1)),
|
|
244
2260
|
};
|
|
245
2261
|
}
|
|
246
2262
|
const indexes = segment.toolCalls
|
|
247
2263
|
.map((toolCall) => toolCall.messageIndex)
|
|
248
2264
|
.filter((index) => typeof index === 'number' && index >= 0);
|
|
249
2265
|
if (indexes.length > 0) {
|
|
250
|
-
const
|
|
2266
|
+
const firstEventIndex = eventIndexForSourceIndex(session, Math.min(...indexes), 'first');
|
|
2267
|
+
const lastEventIndex = eventIndexForSourceIndex(session, Math.max(...indexes), 'last');
|
|
2268
|
+
const start = Math.max(0, firstEventIndex - 3);
|
|
251
2269
|
return {
|
|
252
2270
|
start: Math.min(start, previousHumanUserRecordIndex(session, start) ?? start),
|
|
253
|
-
end: includeTrailingDeliveryContext(session, Math.min(session.
|
|
2271
|
+
end: includeTrailingDeliveryContext(session, Math.min(session.events.length, lastEventIndex + 5)),
|
|
254
2272
|
};
|
|
255
2273
|
}
|
|
256
2274
|
const timestampIndexes = [];
|
|
257
|
-
session.
|
|
258
|
-
const ts =
|
|
2275
|
+
session.events.forEach((event, index) => {
|
|
2276
|
+
const ts = event.timestamp;
|
|
259
2277
|
if (ts && ts >= segment.startTimestamp && ts <= segment.endTimestamp)
|
|
260
2278
|
timestampIndexes.push(index);
|
|
261
2279
|
});
|
|
262
2280
|
if (timestampIndexes.length === 0)
|
|
263
|
-
return { start: 0, end: Math.min(session.
|
|
2281
|
+
return { start: 0, end: Math.min(session.events.length, 12) };
|
|
264
2282
|
const start = Math.max(0, Math.min(...timestampIndexes) - 2);
|
|
265
2283
|
return {
|
|
266
2284
|
start: Math.min(start, previousHumanUserRecordIndex(session, start) ?? start),
|
|
267
|
-
end: includeTrailingDeliveryContext(session, Math.min(session.
|
|
2285
|
+
end: includeTrailingDeliveryContext(session, Math.min(session.events.length, Math.max(...timestampIndexes) + 3)),
|
|
268
2286
|
};
|
|
269
2287
|
}
|
|
270
2288
|
function clampRecordIndex(session, index) {
|
|
271
|
-
return Math.max(0, Math.min(session.
|
|
2289
|
+
return Math.max(0, Math.min(session.events.length - 1, index));
|
|
2290
|
+
}
|
|
2291
|
+
function eventIndexForSourceIndex(session, sourceIndex, edge) {
|
|
2292
|
+
if (edge === 'first') {
|
|
2293
|
+
const index = session.events.findIndex((event) => event.sourceIndex >= sourceIndex);
|
|
2294
|
+
return index < 0 ? clampRecordIndex(session, sourceIndex) : index;
|
|
2295
|
+
}
|
|
2296
|
+
for (let index = session.events.length - 1; index >= 0; index -= 1) {
|
|
2297
|
+
if (session.events[index].sourceIndex <= sourceIndex)
|
|
2298
|
+
return index;
|
|
2299
|
+
}
|
|
2300
|
+
return 0;
|
|
272
2301
|
}
|
|
273
2302
|
function includeLeadingRuntimeContext(session, start) {
|
|
274
2303
|
let nextStart = start;
|
|
275
2304
|
for (let index = start - 1; index >= 0; index -= 1) {
|
|
276
|
-
const events =
|
|
2305
|
+
const events = timelineEventsFromTraceEvent(session, session.events[index], index);
|
|
277
2306
|
if (events.length === 0)
|
|
278
2307
|
continue;
|
|
279
2308
|
if (events.some((event) => event.kind === 'runtime_context')) {
|
|
@@ -285,10 +2314,10 @@ function includeLeadingRuntimeContext(session, start) {
|
|
|
285
2314
|
return nextStart;
|
|
286
2315
|
}
|
|
287
2316
|
function includeTrailingDeliveryContext(session, end) {
|
|
288
|
-
const safeEnd = Math.min(session.
|
|
289
|
-
const lookaheadEnd = Math.min(session.
|
|
2317
|
+
const safeEnd = Math.min(session.events.length, Math.max(0, end));
|
|
2318
|
+
const lookaheadEnd = Math.min(session.events.length, safeEnd + 8);
|
|
290
2319
|
for (let index = safeEnd; index < lookaheadEnd; index += 1) {
|
|
291
|
-
const events =
|
|
2320
|
+
const events = timelineEventsFromTraceEvent(session, session.events[index], index);
|
|
292
2321
|
if (events.some((event) => event.kind === 'user_message'))
|
|
293
2322
|
break;
|
|
294
2323
|
if (events.some(isAssistantDeliveryEvent))
|
|
@@ -297,8 +2326,8 @@ function includeTrailingDeliveryContext(session, end) {
|
|
|
297
2326
|
return safeEnd;
|
|
298
2327
|
}
|
|
299
2328
|
function previousHumanUserRecordIndex(session, start) {
|
|
300
|
-
for (let index = Math.min(start, session.
|
|
301
|
-
const events =
|
|
2329
|
+
for (let index = Math.min(start, session.events.length - 1); index >= 0; index -= 1) {
|
|
2330
|
+
const events = timelineEventsFromTraceEvent(session, session.events[index], index);
|
|
302
2331
|
if (events.some((event) => event.kind === 'user_message'))
|
|
303
2332
|
return index;
|
|
304
2333
|
}
|
|
@@ -334,195 +2363,183 @@ function hasRepeatedExecutionSignal(event) {
|
|
|
334
2363
|
const text = `${event.label ?? ''} ${event.toolName ?? ''} ${event.fullText ?? event.snippet ?? ''}`;
|
|
335
2364
|
return /重复(?:执行|尝试|读取|搜索|调用)|再次(?:执行|读取|搜索|调用)|重新(?:执行|读取|搜索|调用|跑|运行)|再(?:执行|读取|搜索|调用|跑)一遍|重试|\b(?:retry|rerun)\b/i.test(text);
|
|
336
2365
|
}
|
|
337
|
-
function
|
|
338
|
-
|
|
2366
|
+
function buildInvocationTimeline(session, start, end, segment) {
|
|
2367
|
+
const window = buildTimelineWindow(session, start, end);
|
|
2368
|
+
const callInstanceIds = new Set(segment.toolCalls.flatMap((toolCall) => toolCall.callInstanceId ? [toolCall.callInstanceId] : []));
|
|
2369
|
+
const legacyCallIds = new Set(segment.toolCalls.flatMap((toolCall) => !toolCall.callInstanceId && toolCall.toolUseId ? [toolCall.toolUseId] : []));
|
|
2370
|
+
if (callInstanceIds.size === 0 && legacyCallIds.size === 0)
|
|
2371
|
+
return window;
|
|
2372
|
+
const linkedResults = session.events.flatMap((event, eventIndex) => event.eventKind === 'tool_result'
|
|
2373
|
+
&& (event.callInstanceId
|
|
2374
|
+
? callInstanceIds.has(event.callInstanceId)
|
|
2375
|
+
: legacyCallIds.has(event.callId))
|
|
2376
|
+
&& (eventIndex < start || eventIndex >= end)
|
|
2377
|
+
? timelineEventsFromTraceEvent(session, event, eventIndex)
|
|
2378
|
+
: []);
|
|
2379
|
+
return uniqueTimelineEvents([...window, ...linkedResults]).sort(compareTimelineEvents);
|
|
339
2380
|
}
|
|
340
2381
|
function buildTimelineWindow(session, start, end) {
|
|
341
2382
|
const events = [];
|
|
342
|
-
const safeEnd = Math.min(session.
|
|
2383
|
+
const safeEnd = Math.min(session.events.length, Math.max(start, end));
|
|
343
2384
|
for (let index = start; index < safeEnd; index += 1) {
|
|
344
|
-
events.push(...
|
|
2385
|
+
events.push(...timelineEventsFromTraceEvent(session, session.events[index], index));
|
|
345
2386
|
}
|
|
346
|
-
return events
|
|
347
|
-
.sort((a, b) => a.order - b.order);
|
|
2387
|
+
return events.sort((a, b) => a.order - b.order);
|
|
348
2388
|
}
|
|
349
|
-
function
|
|
350
|
-
|
|
351
|
-
return [];
|
|
352
|
-
const rec = record;
|
|
353
|
-
const uuid = typeof rec.uuid === 'string' ? rec.uuid : undefined;
|
|
354
|
-
const timestamp = timestampOf(record);
|
|
2389
|
+
function timelineEventsFromTraceEvent(session, event, eventIndex) {
|
|
2390
|
+
const messageIndex = event.sourceIndex;
|
|
355
2391
|
const base = {
|
|
2392
|
+
traceId: session.traceId,
|
|
356
2393
|
sourceTrace: session.sourcePath,
|
|
357
2394
|
sessionId: logicalSessionId(session),
|
|
358
|
-
traceRole: session.
|
|
359
|
-
traceLabel: session.
|
|
2395
|
+
traceRole: session.role,
|
|
2396
|
+
traceLabel: session.label,
|
|
360
2397
|
messageIndex,
|
|
361
2398
|
logicalMessageIndex: messageIndex,
|
|
362
2399
|
sourceLineIndex: messageIndex,
|
|
363
|
-
messageUuid:
|
|
364
|
-
timestamp,
|
|
2400
|
+
messageUuid: event.sourceEventId ?? event.eventId,
|
|
2401
|
+
timestamp: event.timestamp,
|
|
365
2402
|
};
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
return
|
|
2403
|
+
const order = eventIndex * 10;
|
|
2404
|
+
if (event.eventKind === 'message' && event.role === 'user') {
|
|
2405
|
+
return userTimelineEvents(event, base, order);
|
|
369
2406
|
}
|
|
370
|
-
if (
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
return [];
|
|
381
|
-
return [timelineEvent({
|
|
382
|
-
...base,
|
|
383
|
-
kind: 'runtime_context',
|
|
384
|
-
role: 'other',
|
|
385
|
-
order: messageIndex * 10,
|
|
386
|
-
snippet: snippet(rawText || type, 700),
|
|
387
|
-
fullText: fullText(rawText || type),
|
|
388
|
-
label: type || 'session event',
|
|
389
|
-
})];
|
|
390
|
-
}
|
|
391
|
-
function isSessionBoundaryType(type) {
|
|
392
|
-
return /^session[._-](?:started|ended)$/i.test(type);
|
|
393
|
-
}
|
|
394
|
-
function safeRecordText(record) {
|
|
395
|
-
try {
|
|
396
|
-
return JSON.stringify(record);
|
|
2407
|
+
if (event.eventKind === 'message' && event.role === 'system') {
|
|
2408
|
+
return [timelineEvent({
|
|
2409
|
+
...base,
|
|
2410
|
+
kind: 'runtime_context',
|
|
2411
|
+
role: 'tool',
|
|
2412
|
+
order,
|
|
2413
|
+
snippet: snippet(event.text, 700),
|
|
2414
|
+
fullText: fullText(event.text),
|
|
2415
|
+
label: 'system context',
|
|
2416
|
+
})];
|
|
397
2417
|
}
|
|
398
|
-
|
|
399
|
-
|
|
2418
|
+
if (event.eventKind === 'message' && event.role === 'assistant') {
|
|
2419
|
+
const protocolReply = isAssistantProtocolReplyText(event.text);
|
|
2420
|
+
return [timelineEvent({
|
|
2421
|
+
...base,
|
|
2422
|
+
kind: protocolReply ? 'runtime_context' : 'assistant_message',
|
|
2423
|
+
role: 'assistant',
|
|
2424
|
+
order,
|
|
2425
|
+
snippet: snippet(event.text, 700),
|
|
2426
|
+
fullText: fullText(event.text),
|
|
2427
|
+
label: protocolReply ? 'assistant protocol reply' : 'assistant message',
|
|
2428
|
+
})];
|
|
400
2429
|
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
2430
|
+
if (event.eventKind === 'tool_call') {
|
|
2431
|
+
const inputText = JSON.stringify(event.input);
|
|
2432
|
+
return [timelineEvent({
|
|
2433
|
+
...base,
|
|
2434
|
+
kind: 'tool_use',
|
|
2435
|
+
role: 'assistant',
|
|
2436
|
+
order,
|
|
2437
|
+
callInstanceId: event.callInstanceId,
|
|
2438
|
+
toolUseId: event.callId,
|
|
2439
|
+
toolName: event.tool.displayName ?? event.tool.name,
|
|
2440
|
+
snippet: snippet(inputText, 900),
|
|
2441
|
+
fullText: fullText(inputText),
|
|
2442
|
+
label: `tool_use ${event.tool.displayName ?? event.tool.name}`,
|
|
2443
|
+
})];
|
|
411
2444
|
}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
if (part.type === 'text') {
|
|
416
|
-
const nextEvents = userTextEventsFromText(record, part.text, base, messageIndex, textIndex);
|
|
417
|
-
events.push(...nextEvents);
|
|
418
|
-
if (nextEvents.length > 0)
|
|
419
|
-
textIndex += nextEvents.length;
|
|
420
|
-
}
|
|
421
|
-
else if (part.type === 'tool_result') {
|
|
422
|
-
const full = fullText(part.content);
|
|
423
|
-
const text = snippet(part.content, 900);
|
|
424
|
-
const failed = part.is_error === true || isToolResultFailureText(part.content);
|
|
425
|
-
events.push(timelineEvent({
|
|
2445
|
+
if (event.eventKind === 'tool_result') {
|
|
2446
|
+
const failed = event.status === 'failure';
|
|
2447
|
+
return [timelineEvent({
|
|
426
2448
|
...base,
|
|
427
2449
|
kind: 'tool_result',
|
|
428
2450
|
role: 'tool',
|
|
429
|
-
order
|
|
430
|
-
|
|
2451
|
+
order,
|
|
2452
|
+
callInstanceId: event.callInstanceId,
|
|
2453
|
+
toolUseId: event.callId,
|
|
2454
|
+
toolStatus: event.status,
|
|
431
2455
|
isError: failed,
|
|
432
|
-
snippet:
|
|
433
|
-
fullText:
|
|
434
|
-
label: failed
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
2456
|
+
snippet: snippet(event.output, 900),
|
|
2457
|
+
fullText: fullText(event.output),
|
|
2458
|
+
label: failed
|
|
2459
|
+
? 'tool result error'
|
|
2460
|
+
: event.status === 'cancelled' ? 'tool result cancelled'
|
|
2461
|
+
: event.status === 'unknown' ? 'tool result status unknown' : 'tool result',
|
|
2462
|
+
})];
|
|
438
2463
|
}
|
|
439
|
-
|
|
2464
|
+
if (event.eventKind === 'lifecycle') {
|
|
2465
|
+
return [timelineEvent({
|
|
2466
|
+
...base,
|
|
2467
|
+
kind: 'runtime_context',
|
|
2468
|
+
role: 'other',
|
|
2469
|
+
order,
|
|
2470
|
+
snippet: snippet(`${event.phase}${event.reason ? `: ${event.reason}` : ''}`, 700),
|
|
2471
|
+
fullText: fullText(JSON.stringify(event)),
|
|
2472
|
+
label: event.phase,
|
|
2473
|
+
})];
|
|
2474
|
+
}
|
|
2475
|
+
if (event.eventKind === 'unknown') {
|
|
2476
|
+
const rawText = safeRecordText(event.raw);
|
|
2477
|
+
if (!hasAssistantTurnFailedText(rawText))
|
|
2478
|
+
return [];
|
|
2479
|
+
return [timelineEvent({
|
|
2480
|
+
...base,
|
|
2481
|
+
kind: 'runtime_context',
|
|
2482
|
+
role: 'other',
|
|
2483
|
+
order,
|
|
2484
|
+
snippet: snippet(rawText, 700),
|
|
2485
|
+
fullText: fullText(rawText),
|
|
2486
|
+
label: event.sourceType,
|
|
2487
|
+
})];
|
|
2488
|
+
}
|
|
2489
|
+
return [];
|
|
440
2490
|
}
|
|
441
|
-
function
|
|
2491
|
+
function userTimelineEvents(event, base, order) {
|
|
442
2492
|
const events = [];
|
|
443
|
-
const commandEnvelope = extractCommandEnvelopeText(
|
|
2493
|
+
const commandEnvelope = extractCommandEnvelopeText(event.text);
|
|
444
2494
|
if (commandEnvelope) {
|
|
445
2495
|
events.push(timelineEvent({
|
|
446
2496
|
...base,
|
|
447
2497
|
kind: 'runtime_context',
|
|
448
2498
|
role: 'tool',
|
|
449
|
-
order
|
|
2499
|
+
order,
|
|
450
2500
|
snippet: snippet(commandEnvelope, 700),
|
|
451
2501
|
fullText: fullText(commandEnvelope),
|
|
452
2502
|
label: 'command envelope',
|
|
453
2503
|
}));
|
|
454
2504
|
}
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
2505
|
+
const text = commandEnvelope ? stripCommandEnvelopeText(event.text) : event.text;
|
|
2506
|
+
if (!text.trim())
|
|
2507
|
+
return events;
|
|
2508
|
+
const kind = event.origin === 'human'
|
|
2509
|
+
? 'user_message'
|
|
2510
|
+
: event.origin === 'skill-context'
|
|
2511
|
+
? 'skill_context'
|
|
2512
|
+
: event.origin === 'synthetic'
|
|
2513
|
+
? 'synthetic_user_event'
|
|
2514
|
+
: 'runtime_context';
|
|
2515
|
+
events.push(timelineEvent({
|
|
2516
|
+
...base,
|
|
2517
|
+
kind,
|
|
2518
|
+
role: kind === 'user_message' ? 'user' : kind === 'synthetic_user_event' ? 'other' : 'tool',
|
|
2519
|
+
order: order + (commandEnvelope ? 1 : 0),
|
|
2520
|
+
snippet: snippet(text, 700),
|
|
2521
|
+
fullText: fullText(text),
|
|
2522
|
+
label: userTextEventLabel(kind),
|
|
2523
|
+
}));
|
|
470
2524
|
return events;
|
|
471
2525
|
}
|
|
472
|
-
function
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
let index = 0;
|
|
476
|
-
for (const part of record.message.content) {
|
|
477
|
-
if (part.type === 'text' && part.text)
|
|
478
|
-
textParts.push(part.text);
|
|
479
|
-
if (part.type === 'tool_use' && part.id && part.name) {
|
|
480
|
-
const inputText = JSON.stringify(part.input ?? {});
|
|
481
|
-
events.push(timelineEvent({
|
|
482
|
-
...base,
|
|
483
|
-
kind: 'tool_use',
|
|
484
|
-
role: 'assistant',
|
|
485
|
-
order: messageIndex * 10 + 5 + index,
|
|
486
|
-
toolUseId: part.id,
|
|
487
|
-
toolName: part.name,
|
|
488
|
-
snippet: snippet(inputText, 900),
|
|
489
|
-
fullText: fullText(inputText),
|
|
490
|
-
label: `tool_use ${part.name}`,
|
|
491
|
-
}));
|
|
492
|
-
index += 1;
|
|
493
|
-
}
|
|
2526
|
+
function safeRecordText(record) {
|
|
2527
|
+
try {
|
|
2528
|
+
return JSON.stringify(record);
|
|
494
2529
|
}
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
if (text) {
|
|
498
|
-
const protocolReply = isAssistantProtocolReplyText(rawText);
|
|
499
|
-
events.unshift(timelineEvent({
|
|
500
|
-
...base,
|
|
501
|
-
kind: protocolReply ? 'runtime_context' : 'assistant_message',
|
|
502
|
-
role: 'assistant',
|
|
503
|
-
order: messageIndex * 10,
|
|
504
|
-
snippet: text,
|
|
505
|
-
fullText: fullText(rawText),
|
|
506
|
-
label: protocolReply ? 'assistant protocol reply' : 'assistant message',
|
|
507
|
-
}));
|
|
2530
|
+
catch {
|
|
2531
|
+
return String(record ?? '');
|
|
508
2532
|
}
|
|
509
|
-
|
|
2533
|
+
}
|
|
2534
|
+
function hasAssistantTurnFailedText(value) {
|
|
2535
|
+
return /\[assistant turn failed\]|assistant turn failed|assistant_turn_failed|turn failed/i.test(value);
|
|
510
2536
|
}
|
|
511
2537
|
function timelineEvent(input) {
|
|
512
2538
|
return {
|
|
513
2539
|
...input,
|
|
514
|
-
id: hashParts(input.sourceTrace, input.sessionId, String(input.messageIndex ?? ''), input.kind, input.toolUseId ?? '', input.snippet ?? ''),
|
|
2540
|
+
id: hashParts(input.traceId ?? '', input.sourceTrace, input.sessionId, input.messageUuid ?? '', String(input.messageIndex ?? ''), input.kind, input.callInstanceId ?? '', input.toolUseId ?? '', input.snippet ?? ''),
|
|
515
2541
|
};
|
|
516
2542
|
}
|
|
517
|
-
function userTextEventKind(record, text) {
|
|
518
|
-
if (isSkillContextRecord(record, text))
|
|
519
|
-
return 'skill_context';
|
|
520
|
-
if (isRuntimeContextRecord(record, text))
|
|
521
|
-
return 'runtime_context';
|
|
522
|
-
if (isSyntheticUserMessageText(text))
|
|
523
|
-
return 'synthetic_user_event';
|
|
524
|
-
return 'user_message';
|
|
525
|
-
}
|
|
526
2543
|
function userTextEventLabel(kind) {
|
|
527
2544
|
if (kind === 'skill_context')
|
|
528
2545
|
return 'skill context';
|
|
@@ -536,12 +2553,14 @@ function observationEvidenceRef(item) {
|
|
|
536
2553
|
return {
|
|
537
2554
|
id: hashParts('observation', item.id),
|
|
538
2555
|
kind: 'observation',
|
|
2556
|
+
traceId: item.traceId ?? item.evidence.traceId,
|
|
539
2557
|
sourceTrace: item.sourceTrace,
|
|
540
2558
|
sessionId: item.sessionId,
|
|
541
2559
|
messageIndex: item.evidence.messageIndex,
|
|
542
2560
|
logicalMessageIndex: item.evidence.messageIndex,
|
|
543
2561
|
sourceLineIndex: item.evidence.messageIndex,
|
|
544
2562
|
messageUuid: item.evidence.messageUuid,
|
|
2563
|
+
callInstanceId: item.evidence.callInstanceId,
|
|
545
2564
|
toolUseId: item.evidence.toolUseId,
|
|
546
2565
|
timestamp: item.evidence.segmentTimestamp,
|
|
547
2566
|
role: 'other',
|
|
@@ -553,6 +2572,7 @@ function evidenceRefFromTimeline(event) {
|
|
|
553
2572
|
return {
|
|
554
2573
|
id: event.id,
|
|
555
2574
|
kind: event.kind,
|
|
2575
|
+
traceId: event.traceId,
|
|
556
2576
|
sourceTrace: event.sourceTrace,
|
|
557
2577
|
sessionId: event.sessionId,
|
|
558
2578
|
traceRole: event.traceRole,
|
|
@@ -561,6 +2581,7 @@ function evidenceRefFromTimeline(event) {
|
|
|
561
2581
|
logicalMessageIndex: event.logicalMessageIndex ?? event.messageIndex,
|
|
562
2582
|
sourceLineIndex: event.sourceLineIndex ?? event.messageIndex,
|
|
563
2583
|
messageUuid: event.messageUuid,
|
|
2584
|
+
callInstanceId: event.callInstanceId,
|
|
564
2585
|
toolUseId: event.toolUseId,
|
|
565
2586
|
timestamp: event.timestamp,
|
|
566
2587
|
role: event.role,
|
|
@@ -709,14 +2730,27 @@ function indicatorsForSegment(segment, relatedItems, timeline, metricScopeId, re
|
|
|
709
2730
|
repeatedExecutionCount: timeline.reduce((sum, ref) => sum + (metricIsActive(ref, 'repeated_execution', hasRepeatedExecutionSignal(ref), reviewState) ? 1 : 0), 0),
|
|
710
2731
|
toolCallCount: segment.metrics.numToolCalls,
|
|
711
2732
|
toolFailureCount: Math.max(segment.metrics.numToolFailures, timeline.filter(isToolFailureEvent).length),
|
|
2733
|
+
toolCancelledCount: segment.metrics.numToolCancelled,
|
|
2734
|
+
toolUnknownCount: segment.metrics.numToolUnknown,
|
|
712
2735
|
highObservationCount: relatedItems.filter((item) => item.severity === 'high').length,
|
|
713
2736
|
mediumObservationCount: relatedItems.filter((item) => item.severity === 'medium').length,
|
|
714
|
-
hedgingCount: relatedItems
|
|
715
|
-
|
|
2737
|
+
hedgingCount: sumSafeCounts(...relatedItems
|
|
2738
|
+
.filter((item) => item.signalType === 'hedging')
|
|
2739
|
+
.map((item) => item.occurrences)),
|
|
2740
|
+
explicitMarkerCount: sumSafeCounts(...relatedItems
|
|
2741
|
+
.filter((item) => item.signalType === 'explicit_marker')
|
|
2742
|
+
.map((item) => item.occurrences)),
|
|
716
2743
|
};
|
|
717
2744
|
}
|
|
718
2745
|
function isToolFailureEvent(event) {
|
|
719
|
-
|
|
2746
|
+
if (event.kind !== 'tool_result')
|
|
2747
|
+
return false;
|
|
2748
|
+
if (event.toolStatus !== undefined)
|
|
2749
|
+
return event.toolStatus === 'failure';
|
|
2750
|
+
return event.isError === true || isToolResultFailureText(event.fullText ?? event.snippet ?? '');
|
|
2751
|
+
}
|
|
2752
|
+
function isSessionInterruptedType(type) {
|
|
2753
|
+
return /^turn[._-](?:aborted|interrupted)$/i.test(type);
|
|
720
2754
|
}
|
|
721
2755
|
function sessionInterruptedEvents(timeline) {
|
|
722
2756
|
const events = uniqueTimelineEvents(timeline).sort(compareTimelineEvents);
|
|
@@ -724,7 +2758,7 @@ function sessionInterruptedEvents(timeline) {
|
|
|
724
2758
|
for (let index = 0; index < events.length; index += 1) {
|
|
725
2759
|
const event = events[index];
|
|
726
2760
|
const text = `${event.label ?? ''}\n${event.snippet ?? ''}\n${event.fullText ?? ''}`;
|
|
727
|
-
if (hasAssistantTurnFailedText(text)) {
|
|
2761
|
+
if (isSessionInterruptedType(event.label ?? '') || hasAssistantTurnFailedText(text)) {
|
|
728
2762
|
interrupted.push(event);
|
|
729
2763
|
continue;
|
|
730
2764
|
}
|
|
@@ -755,6 +2789,7 @@ function metricCount(ref, metricKey, ruleCount, reviewState, metricScopeId) {
|
|
|
755
2789
|
function summarizeExperienceSessions(invocations, sessionGroupsByKey, generatedAt, reviewState) {
|
|
756
2790
|
const byKey = new Map();
|
|
757
2791
|
const canonicalEpisodesBySessionGroup = new Map();
|
|
2792
|
+
const timelineBySessionGroup = new Map();
|
|
758
2793
|
for (const invocation of invocations) {
|
|
759
2794
|
const key = `${invocation.skillName}\u0000${invocation.sessionGroupKey}`;
|
|
760
2795
|
const group = byKey.get(key) ?? [];
|
|
@@ -771,40 +2806,44 @@ function summarizeExperienceSessions(invocations, sessionGroupsByKey, generatedA
|
|
|
771
2806
|
const sessionGroup = sessionGroupsByKey.get(first.sessionGroupKey) ?? [];
|
|
772
2807
|
const sourceSessionStartTimestamp = minString(sessionGroup.map((session) => session.startTimestamp));
|
|
773
2808
|
const sourceSessionEndTimestamp = maxString(sessionGroup.map((session) => session.endTimestamp));
|
|
774
|
-
const
|
|
775
|
-
const
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
2809
|
+
const timestampedInvocations = group.filter(invocationTimestampObserved);
|
|
2810
|
+
const timestampedInvocationCount = timestampedInvocations.length;
|
|
2811
|
+
const invocationStartTimestamp = minString(timestampedInvocations.map((invocation) => invocation.startTimestamp)) ?? first.startTimestamp;
|
|
2812
|
+
const invocationEndTimestamp = maxString(timestampedInvocations.map((invocation) => invocation.endTimestamp)) ?? first.endTimestamp;
|
|
2813
|
+
let cachedTimeline = timelineBySessionGroup.get(first.sessionGroupKey);
|
|
2814
|
+
if (!cachedTimeline) {
|
|
2815
|
+
const tree = sessionGroup.length > 0
|
|
2816
|
+
? buildSessionTimelineTree(first.sessionId, sessionGroup)
|
|
2817
|
+
: undefined;
|
|
2818
|
+
cachedTimeline = {
|
|
2819
|
+
tree,
|
|
2820
|
+
flattened: tree ? flattenTimelineTree(tree) : timeline,
|
|
2821
|
+
};
|
|
2822
|
+
timelineBySessionGroup.set(first.sessionGroupKey, cachedTimeline);
|
|
2823
|
+
}
|
|
2824
|
+
const timelineTree = cachedTimeline.tree;
|
|
2825
|
+
const fullSessionTimeline = cachedTimeline.flattened;
|
|
781
2826
|
const fullSessionEventCount = fullSessionTimeline.length;
|
|
782
2827
|
const previewEvents = timeline.slice(0, TIMELINE_PREVIEW_EVENT_LIMIT);
|
|
783
|
-
const
|
|
784
|
-
|
|
2828
|
+
const fullSessionPositionById = new Map(fullSessionTimeline.map((event, index) => [event.id, index]));
|
|
2829
|
+
const previewPositions = previewEvents
|
|
2830
|
+
.map((event) => fullSessionPositionById.get(event.id))
|
|
785
2831
|
.filter((index) => typeof index === 'number');
|
|
786
|
-
const
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
const
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
.filter((index) => typeof index === 'number');
|
|
793
|
-
const sessionStartRecordIndex = minDefined(fullSessionIndexes) ?? 0;
|
|
794
|
-
const sessionEndRecordIndex = maxDefined(fullSessionIndexes) ?? Math.max(0, (sessionGroup[0]?.records.length ?? 1) - 1);
|
|
795
|
-
const omittedBeforeCount = previewStartRecordIndex === undefined
|
|
796
|
-
? 0
|
|
797
|
-
: fullSessionTimeline.filter((event) => typeof event.messageIndex === 'number' && event.messageIndex < previewStartRecordIndex).length;
|
|
798
|
-
const omittedAfterCount = previewEndRecordIndex === undefined
|
|
799
|
-
? 0
|
|
800
|
-
: fullSessionTimeline.filter((event) => typeof event.messageIndex === 'number' && event.messageIndex > previewEndRecordIndex).length;
|
|
2832
|
+
const omittedBeforeCount = previewPositions.length > 0
|
|
2833
|
+
? Math.min(...previewPositions)
|
|
2834
|
+
: 0;
|
|
2835
|
+
const omittedAfterCount = previewPositions.length > 0
|
|
2836
|
+
? fullSessionTimeline.length - 1 - Math.max(...previewPositions)
|
|
2837
|
+
: 0;
|
|
801
2838
|
const observationRefs = uniqueEvidenceRefs(group.flatMap((invocation) => invocation.evidenceRefs.filter((ref) => ref.kind === 'observation')));
|
|
802
2839
|
const evidenceChain = evidenceChainForTimeline(timeline, observationRefs);
|
|
803
2840
|
const metricScopeId = hashParts('session', first.skillName, first.sessionGroupKey);
|
|
804
2841
|
const ruleFindings = ruleFindingsForEvidence(indicators, timeline, observationRefs, evidenceChain, metricScopeId);
|
|
805
2842
|
const assistiveInference = assistiveInferenceForEvidence(indicators, evidenceChain, ruleFindings);
|
|
806
2843
|
const problemPatterns = mergeExperienceProblemPatterns(group.flatMap((invocation) => invocation.problemPatterns));
|
|
807
|
-
const storyInvocations = invocations
|
|
2844
|
+
const storyInvocations = invocations
|
|
2845
|
+
.filter((invocation) => invocation.sessionGroupKey === first.sessionGroupKey)
|
|
2846
|
+
.sort(compareStoryInvocations);
|
|
808
2847
|
const baseSession = {
|
|
809
2848
|
id: metricScopeId,
|
|
810
2849
|
skillName: first.skillName,
|
|
@@ -817,8 +2856,10 @@ function summarizeExperienceSessions(invocations, sessionGroupsByKey, generatedA
|
|
|
817
2856
|
sourceSessionStartTimestamp,
|
|
818
2857
|
sourceSessionEndTimestamp,
|
|
819
2858
|
sourceSessionDurationMs: durationMsBetween(sourceSessionStartTimestamp, sourceSessionEndTimestamp),
|
|
820
|
-
startTimestamp:
|
|
821
|
-
endTimestamp:
|
|
2859
|
+
startTimestamp: invocationStartTimestamp,
|
|
2860
|
+
endTimestamp: invocationEndTimestamp,
|
|
2861
|
+
timestampedInvocationCount,
|
|
2862
|
+
timestampCoverage: group.length > 0 ? timestampedInvocationCount / group.length : 0,
|
|
822
2863
|
invocationIds: group.map((invocation) => invocation.id),
|
|
823
2864
|
goalSliceIds: unique(group.map((invocation) => invocation.goalSliceId)),
|
|
824
2865
|
reviewPriority: priorityForScore(reviewPriorityScore),
|
|
@@ -830,19 +2871,19 @@ function summarizeExperienceSessions(invocations, sessionGroupsByKey, generatedA
|
|
|
830
2871
|
assistiveInference,
|
|
831
2872
|
problemPatterns,
|
|
832
2873
|
relatedObservationIds,
|
|
2874
|
+
timelineRef: timelineRefForSessionGroup(first.sessionGroupKey),
|
|
2875
|
+
timelinePreviewEventIds: previewEvents.map((event) => event.id),
|
|
833
2876
|
timelinePreview: previewEvents,
|
|
834
2877
|
fullSessionTimeline,
|
|
835
2878
|
timelineTree,
|
|
836
2879
|
timelineScope: {
|
|
837
2880
|
mode: 'skill_segment_window',
|
|
838
|
-
|
|
839
|
-
segmentEndRecordIndex: groupEndRecordIndex,
|
|
840
|
-
previewStartRecordIndex,
|
|
841
|
-
previewEndRecordIndex,
|
|
842
|
-
sessionStartRecordIndex,
|
|
843
|
-
sessionEndRecordIndex,
|
|
2881
|
+
segmentEventCount: timeline.length,
|
|
844
2882
|
previewEventCount: previewEvents.length,
|
|
845
2883
|
fullSessionEventCount,
|
|
2884
|
+
segmentRecordRanges: traceRecordRanges(timeline),
|
|
2885
|
+
previewRecordRanges: traceRecordRanges(previewEvents),
|
|
2886
|
+
sessionRecordRanges: traceRecordRanges(fullSessionTimeline),
|
|
846
2887
|
truncated: timeline.length > previewEvents.length || omittedBeforeCount > 0 || omittedAfterCount > 0,
|
|
847
2888
|
omittedBeforeCount,
|
|
848
2889
|
omittedAfterCount,
|
|
@@ -852,17 +2893,28 @@ function summarizeExperienceSessions(invocations, sessionGroupsByKey, generatedA
|
|
|
852
2893
|
rawSkillRefs: unique(group.map((invocation) => invocation.attribution.rawSkillRef).filter((value) => Boolean(value))).sort(),
|
|
853
2894
|
commandNames: unique(group.map((invocation) => invocation.attribution.commandName).filter((value) => Boolean(value))).sort(),
|
|
854
2895
|
};
|
|
855
|
-
|
|
856
|
-
if (!
|
|
857
|
-
|
|
2896
|
+
let canonicalEpisodes = canonicalEpisodesBySessionGroup.get(first.sessionGroupKey);
|
|
2897
|
+
if (!canonicalEpisodes) {
|
|
2898
|
+
const canonicalSession = canonicalStorySession(baseSession, storyInvocations);
|
|
2899
|
+
canonicalEpisodes = buildSessionStory(canonicalSession, storyInvocations, undefined, reviewState).episodes ?? [];
|
|
2900
|
+
canonicalEpisodesBySessionGroup.set(first.sessionGroupKey, canonicalEpisodes);
|
|
858
2901
|
}
|
|
2902
|
+
const sessionStory = {
|
|
2903
|
+
...buildSessionStory(baseSession, storyInvocations, canonicalEpisodes, reviewState),
|
|
2904
|
+
contextRef: storyContextRefForSessionGroup(first.sessionGroupKey),
|
|
2905
|
+
};
|
|
859
2906
|
const sessionWithStoryBase = {
|
|
860
2907
|
...baseSession,
|
|
861
2908
|
sessionStory,
|
|
862
2909
|
};
|
|
2910
|
+
const enrichedIndicators = enrichRouterDownstreamIndicators(sessionWithStoryBase);
|
|
2911
|
+
const enrichedReviewPriorityScore = scoreForIndicators(enrichedIndicators);
|
|
863
2912
|
const sessionWithStory = {
|
|
864
2913
|
...sessionWithStoryBase,
|
|
865
|
-
indicators:
|
|
2914
|
+
indicators: enrichedIndicators,
|
|
2915
|
+
reviewPriority: priorityForScore(enrichedReviewPriorityScore),
|
|
2916
|
+
reviewPriorityScore: enrichedReviewPriorityScore,
|
|
2917
|
+
reviewBasisCodes: basisCodesForIndicators(enrichedIndicators),
|
|
866
2918
|
};
|
|
867
2919
|
const reviewerReport = buildReviewerReport(sessionWithStory, group, generatedAt, reviewState, storyInvocations, sessionStory);
|
|
868
2920
|
return {
|
|
@@ -876,9 +2928,67 @@ function summarizeExperienceSessions(invocations, sessionGroupsByKey, generatedA
|
|
|
876
2928
|
return priorityDiff;
|
|
877
2929
|
if (b.reviewPriorityScore !== a.reviewPriorityScore)
|
|
878
2930
|
return b.reviewPriorityScore - a.reviewPriorityScore;
|
|
2931
|
+
const timestampCoverageDiff = sessionTimestampedInvocationCount(b) - sessionTimestampedInvocationCount(a);
|
|
2932
|
+
if (timestampCoverageDiff !== 0)
|
|
2933
|
+
return timestampCoverageDiff;
|
|
879
2934
|
return b.endTimestamp.localeCompare(a.endTimestamp);
|
|
880
2935
|
});
|
|
881
2936
|
}
|
|
2937
|
+
function compareStoryInvocations(left, right) {
|
|
2938
|
+
return left.startTimestamp.localeCompare(right.startTimestamp)
|
|
2939
|
+
|| left.sourceTrace.localeCompare(right.sourceTrace)
|
|
2940
|
+
|| left.segmentIndex - right.segmentIndex
|
|
2941
|
+
|| left.skillName.localeCompare(right.skillName)
|
|
2942
|
+
|| left.id.localeCompare(right.id);
|
|
2943
|
+
}
|
|
2944
|
+
function canonicalStorySession(base, invocations) {
|
|
2945
|
+
const first = invocations[0];
|
|
2946
|
+
const indicators = sumIndicators(invocations.map((invocation) => invocation.indicators));
|
|
2947
|
+
const relatedObservationIds = unique(invocations.flatMap((invocation) => invocation.relatedObservationIds));
|
|
2948
|
+
const observationRefs = uniqueEvidenceRefs(invocations.flatMap((invocation) => invocation.evidenceRefs.filter((ref) => ref.kind === 'observation')));
|
|
2949
|
+
const evidenceChain = evidenceChainForTimeline(base.fullSessionTimeline, observationRefs);
|
|
2950
|
+
const reviewPriorityScore = scoreForIndicators(indicators);
|
|
2951
|
+
const id = hashParts('session-story', first?.sessionGroupKey ?? base.sessionId);
|
|
2952
|
+
const timestampedInvocations = invocations.filter(invocationTimestampObserved);
|
|
2953
|
+
const ruleFindings = ruleFindingsForEvidence(indicators, base.fullSessionTimeline, observationRefs, evidenceChain, id);
|
|
2954
|
+
return {
|
|
2955
|
+
...base,
|
|
2956
|
+
id,
|
|
2957
|
+
skillName: first?.skillName ?? base.skillName,
|
|
2958
|
+
sourceTrace: base.fullSessionTimeline.find((event) => event.traceRole === 'main' || event.traceRole === 'standalone')?.sourceTrace ?? first?.sourceTrace ?? base.sourceTrace,
|
|
2959
|
+
sourceKind: first?.sourceKind ?? base.sourceKind,
|
|
2960
|
+
entrypoint: first?.entrypoint ?? base.entrypoint,
|
|
2961
|
+
sourceMetadata: mergeSourceMetadata(invocations.map((invocation) => invocation.sourceMetadata)),
|
|
2962
|
+
cwd: first?.cwd ?? base.cwd,
|
|
2963
|
+
startTimestamp: minString(timestampedInvocations.map((invocation) => invocation.startTimestamp)) ?? base.startTimestamp,
|
|
2964
|
+
endTimestamp: maxString(timestampedInvocations.map((invocation) => invocation.endTimestamp)) ?? base.endTimestamp,
|
|
2965
|
+
timestampedInvocationCount: timestampedInvocations.length,
|
|
2966
|
+
timestampCoverage: invocations.length > 0
|
|
2967
|
+
? timestampedInvocations.length / invocations.length
|
|
2968
|
+
: 0,
|
|
2969
|
+
invocationIds: invocations.map((invocation) => invocation.id),
|
|
2970
|
+
goalSliceIds: unique(invocations.map((invocation) => invocation.goalSliceId)),
|
|
2971
|
+
reviewPriority: priorityForScore(reviewPriorityScore),
|
|
2972
|
+
reviewPriorityScore,
|
|
2973
|
+
reviewBasisCodes: basisCodesForIndicators(indicators),
|
|
2974
|
+
indicators,
|
|
2975
|
+
evidenceChain,
|
|
2976
|
+
ruleFindings,
|
|
2977
|
+
assistiveInference: assistiveInferenceForEvidence(indicators, evidenceChain, ruleFindings),
|
|
2978
|
+
problemPatterns: mergeExperienceProblemPatterns(invocations.flatMap((invocation) => invocation.problemPatterns)),
|
|
2979
|
+
relatedObservationIds,
|
|
2980
|
+
attributionSources: unique(invocations.map((invocation) => invocation.attribution.source).filter(Boolean)).sort(),
|
|
2981
|
+
pluginNames: unique(invocations
|
|
2982
|
+
.map((invocation) => invocation.attribution.pluginName)
|
|
2983
|
+
.filter((value) => Boolean(value))).sort(),
|
|
2984
|
+
rawSkillRefs: unique(invocations
|
|
2985
|
+
.map((invocation) => invocation.attribution.rawSkillRef)
|
|
2986
|
+
.filter((value) => Boolean(value))).sort(),
|
|
2987
|
+
commandNames: unique(invocations
|
|
2988
|
+
.map((invocation) => invocation.attribution.commandName)
|
|
2989
|
+
.filter((value) => Boolean(value))).sort(),
|
|
2990
|
+
};
|
|
2991
|
+
}
|
|
882
2992
|
function experiencePriorityRank(priority) {
|
|
883
2993
|
if (priority === 'review_first')
|
|
884
2994
|
return 2;
|
|
@@ -926,6 +3036,8 @@ function buildReviewerReport(session, invocations, generatedAt, reviewState, sto
|
|
|
926
3036
|
oneLookMetrics: {
|
|
927
3037
|
toolCallCount: indicators.toolCallCount,
|
|
928
3038
|
toolFailureCount: indicators.toolFailureCount,
|
|
3039
|
+
toolCancelledCount: indicators.toolCancelledCount ?? 0,
|
|
3040
|
+
toolUnknownCount: indicators.toolUnknownCount ?? 0,
|
|
929
3041
|
userMessageCount: indicators.userMessageCount,
|
|
930
3042
|
userFollowUpCount: feedbackCounts.userFollowUpCount,
|
|
931
3043
|
assistantDeliverySignalCount: indicators.assistantDeliverySignalCount,
|
|
@@ -982,8 +3094,13 @@ function buildSessionStory(session, invocations, canonicalEpisodes, reviewState)
|
|
|
982
3094
|
if (subagentDispatches.length > 0) {
|
|
983
3095
|
subagentNode = push('subagent_branch', '分支 / 子任务', 'unknown', `检测到 ${subagentDispatches.length} 条分支或子任务执行线;主线和分支已单独列出,仍需要结合原文确认真实委派关系。`, subagentDispatches.flatMap((dispatch) => dispatch.evidenceRefs.slice(0, 1)));
|
|
984
3096
|
}
|
|
985
|
-
const executionNode = push('tool_execution', '执行过程', session.indicators.toolFailureCount > 0
|
|
986
|
-
?
|
|
3097
|
+
const executionNode = push('tool_execution', '执行过程', session.indicators.toolFailureCount > 0
|
|
3098
|
+
? 'attention'
|
|
3099
|
+
: (session.indicators.toolCancelledCount ?? 0) > 0
|
|
3100
|
+
|| (session.indicators.toolUnknownCount ?? 0) > 0
|
|
3101
|
+
? 'unknown'
|
|
3102
|
+
: session.indicators.toolCallCount > 0 ? 'ok' : 'unknown', session.indicators.toolCallCount > 0
|
|
3103
|
+
? executionOutcomeText(session.indicators)
|
|
987
3104
|
: '没有看到明确工具调用;只能根据消息上下文复盘。', uniqueEvidenceRefs([
|
|
988
3105
|
session.evidenceChain.firstToolUse,
|
|
989
3106
|
session.evidenceChain.firstToolFailure,
|
|
@@ -1025,7 +3142,7 @@ function buildSessionStory(session, invocations, canonicalEpisodes, reviewState)
|
|
|
1025
3142
|
schemaVersion: 1,
|
|
1026
3143
|
summary,
|
|
1027
3144
|
invocationCount: invocations.length,
|
|
1028
|
-
goalSliceCount:
|
|
3145
|
+
goalSliceCount: goalSlices.length,
|
|
1029
3146
|
branchCount: subagentDispatches.length,
|
|
1030
3147
|
progressUpdateCount: progressUpdates.length,
|
|
1031
3148
|
finalDeliverySignalCount: finalDeliveries.length,
|
|
@@ -1058,17 +3175,23 @@ function sessionStoryEpisodes(session, invocations, goalSlices, subagentDispatch
|
|
|
1058
3175
|
const ranges = sessionStoryEpisodeRanges(session, timeline);
|
|
1059
3176
|
return ranges.map((range, index) => {
|
|
1060
3177
|
const episodeId = hashParts('session-story-episode', session.id, String(index));
|
|
1061
|
-
const
|
|
1062
|
-
const
|
|
3178
|
+
const rangedSkillSegments = skillSegments.filter((segment) => (segment.messageRanges ?? []).some((messageRange) => messageRangeOverlapsEpisodeRange(messageRange, range)));
|
|
3179
|
+
const rangedSegmentIds = new Set(rangedSkillSegments.map((segment) => segment.id));
|
|
1063
3180
|
const episodeEdges = orchestrationEdges
|
|
1064
3181
|
.filter((edge) => {
|
|
1065
3182
|
if (edge.edgeKind === 'internal_skill' && edge.parentSkillSegmentId && edge.executorSkillSegmentId) {
|
|
1066
|
-
return
|
|
3183
|
+
return rangedSegmentIds.has(edge.parentSkillSegmentId) && rangedSegmentIds.has(edge.executorSkillSegmentId);
|
|
1067
3184
|
}
|
|
1068
|
-
return (edge.parentSkillSegmentId &&
|
|
3185
|
+
return (edge.parentSkillSegmentId && rangedSegmentIds.has(edge.parentSkillSegmentId))
|
|
1069
3186
|
|| edge.evidenceRefs.some((ref) => episodeRangeContainsRef(range, ref));
|
|
1070
3187
|
})
|
|
1071
3188
|
.map((edge) => ({ ...edge, episodeId }));
|
|
3189
|
+
const linkedSegmentIds = new Set(episodeEdges.flatMap((edge) => [
|
|
3190
|
+
edge.parentSkillSegmentId,
|
|
3191
|
+
edge.executorSkillSegmentId,
|
|
3192
|
+
].filter((id) => Boolean(id))));
|
|
3193
|
+
const episodeSkillSegments = skillSegments.filter((segment) => rangedSegmentIds.has(segment.id) || linkedSegmentIds.has(segment.id));
|
|
3194
|
+
const segmentIds = new Set(episodeSkillSegments.map((segment) => segment.id));
|
|
1072
3195
|
const episodeFeedbackSignals = feedbackSignals
|
|
1073
3196
|
.filter((signal) => episodeRangeContainsRef(range, signal.evidenceRef))
|
|
1074
3197
|
.map((signal) => {
|
|
@@ -1162,17 +3285,31 @@ function skillSegmentsWithOrchestrationRoles(skillSegments, orchestrationEdges)
|
|
|
1162
3285
|
}
|
|
1163
3286
|
function sessionStoryEpisodeRanges(session, timeline) {
|
|
1164
3287
|
const primarySourceTrace = primarySourceTraceForSession(session);
|
|
1165
|
-
const
|
|
3288
|
+
const primaryTraceId = primaryTraceIdForSession(session, primarySourceTrace);
|
|
3289
|
+
const rangeTimeline = timeline.filter((event) => primaryTraceId
|
|
3290
|
+
? !event.traceId || event.traceId === primaryTraceId
|
|
3291
|
+
: !primarySourceTrace || !event.sourceTrace || event.sourceTrace === primarySourceTrace);
|
|
1166
3292
|
const indexes = rangeTimeline
|
|
1167
3293
|
.map((event) => event.messageIndex)
|
|
1168
3294
|
.filter((value) => typeof value === 'number');
|
|
1169
|
-
const
|
|
1170
|
-
|
|
3295
|
+
const persistedRange = session.timelineScope.sessionRecordRanges?.find((range) => primaryTraceId
|
|
3296
|
+
? range.traceId === primaryTraceId
|
|
3297
|
+
: range.sourceTrace === primarySourceTrace);
|
|
3298
|
+
const sessionStart = minDefined(indexes)
|
|
3299
|
+
?? persistedRange?.startRecordIndex
|
|
3300
|
+
?? session.timelineScope.sessionStartRecordIndex
|
|
3301
|
+
?? 0;
|
|
3302
|
+
const sessionEnd = maxDefined(indexes)
|
|
3303
|
+
?? persistedRange?.endRecordIndex
|
|
3304
|
+
?? session.timelineScope.sessionEndRecordIndex
|
|
3305
|
+
?? sessionStart;
|
|
1171
3306
|
const goalShiftStarts = unique(timeline
|
|
1172
3307
|
.filter((event) => event.kind === 'user_message'
|
|
1173
3308
|
&& typeof event.messageIndex === 'number'
|
|
1174
3309
|
&& event.messageIndex > sessionStart
|
|
1175
|
-
&& (
|
|
3310
|
+
&& (primaryTraceId
|
|
3311
|
+
? !event.traceId || event.traceId === primaryTraceId
|
|
3312
|
+
: !primarySourceTrace || !event.sourceTrace || event.sourceTrace === primarySourceTrace))
|
|
1176
3313
|
.filter((event) => hasUserGoalShiftSignal(event.snippet ?? event.fullText ?? ''))
|
|
1177
3314
|
.map((event) => event.messageIndex))
|
|
1178
3315
|
.sort((a, b) => a - b);
|
|
@@ -1180,11 +3317,20 @@ function sessionStoryEpisodeRanges(session, timeline) {
|
|
|
1180
3317
|
return starts.map((start, index) => ({
|
|
1181
3318
|
startMessageIndex: start,
|
|
1182
3319
|
endMessageIndex: (starts[index + 1] ?? (sessionEnd + 1)) - 1,
|
|
3320
|
+
traceId: primaryTraceId,
|
|
1183
3321
|
sourceTrace: primarySourceTrace,
|
|
1184
3322
|
sessionId: session.sessionId,
|
|
1185
3323
|
boundaryReason: index < starts.length - 1 ? 'goal_shift' : undefined,
|
|
1186
3324
|
}));
|
|
1187
3325
|
}
|
|
3326
|
+
function primaryTraceIdForSession(session, primarySourceTrace = primarySourceTraceForSession(session)) {
|
|
3327
|
+
const timeline = session.fullSessionTimeline.length > 0
|
|
3328
|
+
? session.fullSessionTimeline
|
|
3329
|
+
: session.timelinePreview;
|
|
3330
|
+
return timeline.find((event) => event.traceRole === 'main'
|
|
3331
|
+
&& (!primarySourceTrace || event.sourceTrace === primarySourceTrace))?.traceId
|
|
3332
|
+
?? timeline.find((event) => !primarySourceTrace || event.sourceTrace === primarySourceTrace)?.traceId;
|
|
3333
|
+
}
|
|
1188
3334
|
function primarySourceTraceForSession(session) {
|
|
1189
3335
|
return session.sourceTrace
|
|
1190
3336
|
?? session.evidenceChain.firstUserMessage?.sourceTrace
|
|
@@ -1195,8 +3341,8 @@ function primarySourceTraceForSession(session) {
|
|
|
1195
3341
|
function episodeRangeContainsRef(range, ref) {
|
|
1196
3342
|
if (!ref || typeof ref.messageIndex !== 'number')
|
|
1197
3343
|
return false;
|
|
1198
|
-
|
|
1199
|
-
|
|
3344
|
+
if (range.traceId && ref.traceId && range.traceId !== ref.traceId)
|
|
3345
|
+
return false;
|
|
1200
3346
|
if (range.sourceTrace && ref.sourceTrace && range.sourceTrace !== ref.sourceTrace)
|
|
1201
3347
|
return false;
|
|
1202
3348
|
if (range.sessionId && ref.sessionId && range.sessionId !== ref.sessionId)
|
|
@@ -1204,6 +3350,8 @@ function episodeRangeContainsRef(range, ref) {
|
|
|
1204
3350
|
return ref.messageIndex >= range.startMessageIndex && ref.messageIndex <= range.endMessageIndex;
|
|
1205
3351
|
}
|
|
1206
3352
|
function messageRangeOverlapsEpisodeRange(messageRange, range) {
|
|
3353
|
+
if (range.traceId && messageRange.traceId && range.traceId !== messageRange.traceId)
|
|
3354
|
+
return false;
|
|
1207
3355
|
if (range.sourceTrace && messageRange.sourceTrace && range.sourceTrace !== messageRange.sourceTrace)
|
|
1208
3356
|
return false;
|
|
1209
3357
|
if (range.sessionId && messageRange.sessionId && range.sessionId !== messageRange.sessionId)
|
|
@@ -1239,10 +3387,11 @@ function sessionStorySkillSegments(session, invocations, skillLinks) {
|
|
|
1239
3387
|
.filter((value) => typeof value === 'number');
|
|
1240
3388
|
const startMessageIndex = minDefined(indexes);
|
|
1241
3389
|
const endMessageIndex = maxDefined(indexes);
|
|
3390
|
+
const traceId = invocation.traceId ?? timelineWithIndex[0]?.traceId;
|
|
1242
3391
|
const sourceTrace = invocation.sourceTrace ?? timelineWithIndex[0]?.sourceTrace;
|
|
1243
3392
|
const sessionId = invocation.sessionId ?? timelineWithIndex[0]?.sessionId;
|
|
1244
3393
|
return typeof startMessageIndex === 'number' && typeof endMessageIndex === 'number'
|
|
1245
|
-
? { startMessageIndex, endMessageIndex, sourceTrace, sessionId }
|
|
3394
|
+
? { startMessageIndex, endMessageIndex, traceId, sourceTrace, sessionId }
|
|
1246
3395
|
: undefined;
|
|
1247
3396
|
})
|
|
1248
3397
|
.filter((value) => Boolean(value));
|
|
@@ -1305,16 +3454,8 @@ function sessionStoryOrchestrationEdges(episodeId, skillSegments, subagentDispat
|
|
|
1305
3454
|
const mentionedUpstream = runnerOwner
|
|
1306
3455
|
? mentionedUpstreamSkillSegmentForRuntime(runnerOwner, runnerEvent, timeline, skillSegments)
|
|
1307
3456
|
: undefined;
|
|
1308
|
-
const
|
|
1309
|
-
|
|
1310
|
-
? (mentionedUpstream && runnerOwner && runnerOwner.id !== mentionedUpstream.id
|
|
1311
|
-
? runnerOwner
|
|
1312
|
-
: skillSegments.find((segment) => {
|
|
1313
|
-
if (segment.id === runnerOwner?.id || segment.id === mentionedUpstream?.id)
|
|
1314
|
-
return false;
|
|
1315
|
-
const name = segment.skillName.toLowerCase();
|
|
1316
|
-
return name.includes('apply-cc') && /apply-cc|runner\.js|send-input\.js/i.test(runnerText);
|
|
1317
|
-
}))
|
|
3457
|
+
const runnerExecutor = mentionedUpstream && runnerOwner && runnerOwner.id !== mentionedUpstream.id
|
|
3458
|
+
? runnerOwner
|
|
1318
3459
|
: undefined;
|
|
1319
3460
|
const priorUpstreamCandidate = runnerOwner
|
|
1320
3461
|
? bestPriorUpstreamSkillSegmentForRuntime(runnerOwner, runnerEvent, timeline, skillSegments)
|
|
@@ -1330,25 +3471,30 @@ function sessionStoryOrchestrationEdges(episodeId, skillSegments, subagentDispat
|
|
|
1330
3471
|
&& segment.id !== router?.id
|
|
1331
3472
|
&& segment.episodeRole === 'main_executor') : undefined);
|
|
1332
3473
|
if (parentSegment && (executor || runnerEvent) && parentSegment.id !== executor?.id) {
|
|
1333
|
-
const edgeKind = executor &&
|
|
3474
|
+
const edgeKind = executor && skillSegmentsSharePhysicalTrace(parentSegment, executor)
|
|
1334
3475
|
? 'internal_skill'
|
|
1335
3476
|
: 'external_child_session';
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
3477
|
+
// A concrete branch below is the authoritative external-child edge.
|
|
3478
|
+
// Keep this inferred edge only for an internal handoff or when no branch
|
|
3479
|
+
// trace was captured, otherwise the same launch appears twice.
|
|
3480
|
+
if (edgeKind === 'internal_skill' || subagentDispatches.length === 0) {
|
|
3481
|
+
edges.push({
|
|
3482
|
+
id: hashParts('session-story-edge', episodeId, parentSegment.id, executor?.id ?? 'downstream', '0'),
|
|
3483
|
+
episodeId,
|
|
3484
|
+
edgeKind,
|
|
3485
|
+
parentSkillSegmentId: parentSegment.id,
|
|
3486
|
+
executorSkillSegmentId: executor?.id,
|
|
3487
|
+
childSessionId: sessionStoryChildSessionId(runnerEvent),
|
|
3488
|
+
runnerStartedRef: runnerRef,
|
|
3489
|
+
status: runnerEvent || subagentDispatches.length > 0 ? 'started' : 'unknown',
|
|
3490
|
+
evidenceRefs: uniqueEvidenceRefs([
|
|
3491
|
+
...parentSegment.evidenceRefs.slice(0, 2),
|
|
3492
|
+
...(executor?.evidenceRefs.slice(0, 2) ?? []),
|
|
3493
|
+
...(runnerRef ? [runnerRef] : []),
|
|
3494
|
+
...subagentDispatches.flatMap((dispatch) => dispatch.evidenceRefs.slice(0, 1)),
|
|
3495
|
+
]).slice(0, 6),
|
|
3496
|
+
});
|
|
3497
|
+
}
|
|
1352
3498
|
}
|
|
1353
3499
|
if (edges.length === 0) {
|
|
1354
3500
|
const fallbackEdge = fallbackOrchestrationEdgeFromRuntime(episodeId, skillSegments, invocations);
|
|
@@ -1356,17 +3502,30 @@ function sessionStoryOrchestrationEdges(episodeId, skillSegments, subagentDispat
|
|
|
1356
3502
|
edges.push(fallbackEdge);
|
|
1357
3503
|
}
|
|
1358
3504
|
for (const dispatch of subagentDispatches) {
|
|
1359
|
-
const parentSegment = delegator ?? router;
|
|
1360
|
-
const executor = skillSegments.find((segment) => segment.id !== parentSegment?.id
|
|
3505
|
+
const parentSegment = delegator ?? router ?? runnerOwner;
|
|
3506
|
+
const executor = skillSegments.find((segment) => segment.id !== parentSegment?.id
|
|
3507
|
+
&& segment.evidenceRefs.some((ref) => ref.traceId === dispatch.traceId)) ?? skillSegments.find((segment) => segment.id !== parentSegment?.id && segment.episodeRole === 'main_executor');
|
|
3508
|
+
const dispatchRunnerRef = runnerRef && (!dispatch.attachTo?.callInstanceId
|
|
3509
|
+
? !dispatch.attachTo?.toolUseId
|
|
3510
|
+
|| dispatch.attachTo.toolUseId === runnerRef.toolUseId
|
|
3511
|
+
: dispatch.attachTo.callInstanceId === runnerRef.callInstanceId)
|
|
3512
|
+
? runnerRef
|
|
3513
|
+
: undefined;
|
|
1361
3514
|
edges.push({
|
|
1362
3515
|
id: hashParts('session-story-edge', episodeId, dispatch.id),
|
|
1363
3516
|
episodeId,
|
|
1364
|
-
edgeKind:
|
|
3517
|
+
edgeKind: 'external_child_session',
|
|
1365
3518
|
parentSkillSegmentId: parentSegment?.id,
|
|
1366
3519
|
executorSkillSegmentId: executor?.id,
|
|
1367
|
-
childSessionId: dispatch.
|
|
3520
|
+
childSessionId: dispatch.childSessionId,
|
|
3521
|
+
runnerStartedRef: dispatchRunnerRef,
|
|
1368
3522
|
status: 'started',
|
|
1369
|
-
evidenceRefs:
|
|
3523
|
+
evidenceRefs: uniqueEvidenceRefs([
|
|
3524
|
+
...(parentSegment?.evidenceRefs.slice(0, 2) ?? []),
|
|
3525
|
+
...(executor?.evidenceRefs.slice(0, 2) ?? []),
|
|
3526
|
+
...(dispatchRunnerRef ? [dispatchRunnerRef] : []),
|
|
3527
|
+
...dispatch.evidenceRefs,
|
|
3528
|
+
]).slice(0, 6),
|
|
1370
3529
|
});
|
|
1371
3530
|
}
|
|
1372
3531
|
return edges;
|
|
@@ -1418,7 +3577,7 @@ function mentionedUpstreamSkillSegmentForRuntime(runnerOwner, runnerEvent, timel
|
|
|
1418
3577
|
const compactMentioned = compactName.length >= 6 && compactText.includes(compactName);
|
|
1419
3578
|
if (!explicitlyMentioned && !compactMentioned)
|
|
1420
3579
|
return false;
|
|
1421
|
-
return /skill
|
|
3580
|
+
return /skill|技能|流程|工作流|按|根据|启动|触发|委派|分发|delegate|dispatch|route/i.test(localContext);
|
|
1422
3581
|
})
|
|
1423
3582
|
.sort((a, b) => upstreamParentScore(b, runnerOwner, nearbyText) - upstreamParentScore(a, runnerOwner, nearbyText)
|
|
1424
3583
|
|| Math.abs(a.order - runnerOwner.order) - Math.abs(b.order - runnerOwner.order))[0];
|
|
@@ -1433,10 +3592,6 @@ function upstreamParentScore(segment, runnerOwner, contextText) {
|
|
|
1433
3592
|
score += 70;
|
|
1434
3593
|
if (segment.episodeRole === 'observer' || segment.skillType === 'advisory')
|
|
1435
3594
|
score += 45;
|
|
1436
|
-
if (/dev[-_\s]*lifecycle|aiprd[-_\s]*task[-_\s]*runner|omk[-_\s]*reviewer/i.test(lowerName))
|
|
1437
|
-
score += 35;
|
|
1438
|
-
if (/yuque|web[-_\s]*fetch|fetch|search/i.test(lowerName))
|
|
1439
|
-
score -= 35;
|
|
1440
3595
|
const nameIndex = lowerText.indexOf(lowerName);
|
|
1441
3596
|
if (nameIndex >= 0) {
|
|
1442
3597
|
const localContext = lowerText.slice(Math.max(0, nameIndex - 50), nameIndex + lowerName.length + 120);
|
|
@@ -1451,10 +3606,7 @@ function upstreamParentScore(segment, runnerOwner, contextText) {
|
|
|
1451
3606
|
}
|
|
1452
3607
|
function fallbackOrchestrationEdgeFromRuntime(episodeId, skillSegments, invocations) {
|
|
1453
3608
|
const timeline = uniqueTimelineEvents(invocations.flatMap((invocation) => invocation.timeline)).sort(compareTimelineEvents);
|
|
1454
|
-
const runtimeEvent = timeline.find(
|
|
1455
|
-
const text = `${event.toolName ?? ''} ${event.snippet ?? ''} ${event.fullText ?? ''}`;
|
|
1456
|
-
return /skills\/[\w.-]+\/scripts\/|runner\.js|send-input\.js|check-session\.js|session:\s*claude-|ttydUrl/i.test(text);
|
|
1457
|
-
});
|
|
3609
|
+
const runtimeEvent = timeline.find(isOrchestrationRuntimeEvent);
|
|
1458
3610
|
if (!runtimeEvent)
|
|
1459
3611
|
return undefined;
|
|
1460
3612
|
const runtimeRef = evidenceRefFromTimeline(runtimeEvent);
|
|
@@ -1466,7 +3618,7 @@ function fallbackOrchestrationEdgeFromRuntime(episodeId, skillSegments, invocati
|
|
|
1466
3618
|
.sort((a, b) => b.order - a.order)[0];
|
|
1467
3619
|
if (!parent)
|
|
1468
3620
|
return undefined;
|
|
1469
|
-
const edgeKind =
|
|
3621
|
+
const edgeKind = skillSegmentsSharePhysicalTrace(parent, executor) ? 'internal_skill' : 'external_child_session';
|
|
1470
3622
|
return {
|
|
1471
3623
|
id: hashParts('session-story-edge', episodeId, parent.id, executor.id, 'fallback'),
|
|
1472
3624
|
episodeId,
|
|
@@ -1486,22 +3638,28 @@ function fallbackOrchestrationEdgeFromRuntime(episodeId, skillSegments, invocati
|
|
|
1486
3638
|
function isOrchestrationRuntimeEvent(event) {
|
|
1487
3639
|
const text = `${event.toolName ?? ''} ${event.snippet ?? ''} ${event.fullText ?? ''}`;
|
|
1488
3640
|
if (event.kind === 'tool_use') {
|
|
1489
|
-
return
|
|
3641
|
+
return /^(?:Task|Agent)$/i.test(event.toolName ?? '')
|
|
3642
|
+
|| /runner\.js|send-input\.js|check-session\.js/i.test(text);
|
|
1490
3643
|
}
|
|
1491
3644
|
if (event.kind === 'tool_result') {
|
|
1492
3645
|
const trimmed = text.trim();
|
|
1493
3646
|
if (/^---\s*\n?\s*name:/i.test(trimmed) || /^---\s+name:/i.test(trimmed))
|
|
1494
3647
|
return false;
|
|
1495
|
-
return /"event"\s*:\s*"started"|Command still running \(session|Process exited with code|Process exited with signal
|
|
3648
|
+
return /"event"\s*:\s*"started"|Command still running \(session|Process exited with code|Process exited with signal/i.test(text)
|
|
3649
|
+
|| /["']?(?:child_?session_?id|agent_?id|thread_?id|session_?id)["']?\s*[:=]/i.test(text)
|
|
3650
|
+
|| /\b(?:session|thread|agent)(?:\s+id)?\s*[:=]\s*[a-z0-9][a-z0-9._-]*/i.test(text);
|
|
1496
3651
|
}
|
|
1497
3652
|
if (event.kind === 'assistant_message') {
|
|
1498
|
-
return
|
|
3653
|
+
return /ttyd|(?:已启动|启动了|spawned|started).*(?:subagent|sub-agent|child agent|子\s*(?:agent|代理|智能体|Claude|Codex))|(?:subagent|sub-agent|child agent|子\s*(?:agent|代理|智能体|Claude|Codex)).*(?:执行|运行|分析|started|running)/i.test(text)
|
|
3654
|
+
|| /\b(?:session|thread|agent)(?:\s+id)?\s*[:=]\s*[a-z0-9][a-z0-9._-]*/i.test(text);
|
|
1499
3655
|
}
|
|
1500
3656
|
return false;
|
|
1501
3657
|
}
|
|
1502
3658
|
function sessionStoryChildSessionId(event) {
|
|
1503
3659
|
const text = `${event?.snippet ?? ''} ${event?.fullText ?? ''}`;
|
|
1504
|
-
return text.match(
|
|
3660
|
+
return text.match(/["']?(?:child_?session_?id|agent_?id|thread_?id|session_?id)["']?\s*[:=]\s*["']?([a-z0-9][a-z0-9._-]*)/i)?.[1]
|
|
3661
|
+
?? text.match(/\b(?:session|thread|agent)(?:\s+id)?\s*[:=]\s*([a-z0-9][a-z0-9._-]*)/i)?.[1]
|
|
3662
|
+
?? text.match(/\b(?:claude|codex|agent|subagent)-[a-z0-9_-]+\b/i)?.[0];
|
|
1505
3663
|
}
|
|
1506
3664
|
function sessionStoryFeedbackSignals(session, invocations, skillSegments, orchestrationEdges) {
|
|
1507
3665
|
const timeline = uniqueTimelineEvents(invocations.flatMap((invocation) => invocation.timeline))
|
|
@@ -1579,7 +3737,7 @@ function feedbackAttributionsForText(text, evidenceRef, skillSegments, orchestra
|
|
|
1579
3737
|
: targetOwner
|
|
1580
3738
|
? { segment: targetOwner, reason: 'object_match' }
|
|
1581
3739
|
: actionOwner
|
|
1582
|
-
?
|
|
3740
|
+
? actionOwner
|
|
1583
3741
|
: windowOwner
|
|
1584
3742
|
? { segment: windowOwner, reason: feedbackAttributionReasonForText(lower) }
|
|
1585
3743
|
: undefined;
|
|
@@ -1652,8 +3810,6 @@ function upstreamPromiseOwnerScore(segment) {
|
|
|
1652
3810
|
score += 80;
|
|
1653
3811
|
if (segment.episodeRole === 'delegator' || segment.skillType === 'delegation')
|
|
1654
3812
|
score += 70;
|
|
1655
|
-
if (/aiprd[-_\s]*task[-_\s]*runner|dev[-_\s]*lifecycle|omk[-_\s]*reviewer/i.test(segment.skillName))
|
|
1656
|
-
score += 30;
|
|
1657
3813
|
return score;
|
|
1658
3814
|
}
|
|
1659
3815
|
function downstreamRelatedParentsForPrimary(primary, skillSegments, orchestrationEdges) {
|
|
@@ -1718,21 +3874,19 @@ function targetObjectSkillOwner(text, skillSegments) {
|
|
|
1718
3874
|
});
|
|
1719
3875
|
if (explicitSkill)
|
|
1720
3876
|
return explicitSkill;
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
3877
|
+
if (/runner|child|subagent|sub-agent|子\s*(?:claude|codex|agent|代理|智能体)|(?:claude|codex|agent)\s*session|ttyd|执行窗口/.test(lower)) {
|
|
3878
|
+
return skillSegments
|
|
3879
|
+
.filter((segment) => segment.episodeRole === 'delegator'
|
|
3880
|
+
|| segment.episodeRole === 'router'
|
|
3881
|
+
|| segment.skillType === 'delegation'
|
|
3882
|
+
|| segment.skillType === 'router'
|
|
3883
|
+
|| segment.skillType === 'workflow_owner')
|
|
3884
|
+
.sort((a, b) => upstreamPromiseOwnerScore(b) - upstreamPromiseOwnerScore(a) || a.order - b.order)[0];
|
|
1727
3885
|
}
|
|
1728
|
-
if (/
|
|
1729
|
-
return
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
return byName([/dev[-_\s]*lifecycle/, /antcode/, /git/]);
|
|
1733
|
-
}
|
|
1734
|
-
if (/日报|每天早上|每天晚上|复盘报告/.test(lower)) {
|
|
1735
|
-
return byName([/damai[-_\s]*daily/, /daily/]);
|
|
3886
|
+
if (/skill\s*extract|soft[-_\s]*standard|llm[-_\s]*enhanced|skill\.md|这个skill|执行流程|依赖.*脚本|删除.*脚本|review/.test(lower)) {
|
|
3887
|
+
return skillSegments
|
|
3888
|
+
.filter((segment) => segment.episodeRole === 'observer' || segment.skillType === 'advisory')
|
|
3889
|
+
.sort((a, b) => a.order - b.order)[0];
|
|
1736
3890
|
}
|
|
1737
3891
|
return undefined;
|
|
1738
3892
|
}
|
|
@@ -1756,6 +3910,21 @@ function actionOwnerForFeedback(evidenceRef, text, timeline, skillSegments) {
|
|
|
1756
3910
|
if (!category || typeof evidenceRef.messageIndex !== 'number')
|
|
1757
3911
|
return undefined;
|
|
1758
3912
|
const feedbackMessageIndex = evidenceRef.messageIndex;
|
|
3913
|
+
const commandEnvelopeText = timeline
|
|
3914
|
+
.filter((event) => event.kind === 'runtime_context'
|
|
3915
|
+
&& event.messageIndex === feedbackMessageIndex
|
|
3916
|
+
&& evidenceRefsShareTraceScope(event, evidenceRef))
|
|
3917
|
+
.map((event) => `${event.snippet ?? ''} ${event.fullText ?? ''}`)
|
|
3918
|
+
.join('\n')
|
|
3919
|
+
.toLowerCase();
|
|
3920
|
+
const commandOwner = skillSegments.find((segment) => {
|
|
3921
|
+
const name = segment.skillName.toLowerCase();
|
|
3922
|
+
const compactName = compactObjectText(name);
|
|
3923
|
+
return commandEnvelopeText.includes(name)
|
|
3924
|
+
|| (compactName.length >= 4 && compactObjectText(commandEnvelopeText).includes(compactName));
|
|
3925
|
+
});
|
|
3926
|
+
if (commandOwner)
|
|
3927
|
+
return { segment: commandOwner, reason: 'object_match' };
|
|
1759
3928
|
const nextRuntimeEvent = timeline.find((event) => {
|
|
1760
3929
|
if (typeof event.messageIndex !== 'number')
|
|
1761
3930
|
return false;
|
|
@@ -1768,7 +3937,10 @@ function actionOwnerForFeedback(evidenceRef, text, timeline, skillSegments) {
|
|
|
1768
3937
|
const eventText = `${event.toolName ?? ''} ${event.snippet ?? ''} ${event.fullText ?? ''}`;
|
|
1769
3938
|
return actionCategoryMatchesRuntimeEvent(category, eventText);
|
|
1770
3939
|
});
|
|
1771
|
-
|
|
3940
|
+
const runtimeOwner = nextRuntimeEvent
|
|
3941
|
+
? skillSegmentForEvidenceRef(evidenceRefFromTimeline(nextRuntimeEvent), skillSegments)
|
|
3942
|
+
: undefined;
|
|
3943
|
+
return runtimeOwner ? { segment: runtimeOwner, reason: 'action_match' } : undefined;
|
|
1772
3944
|
}
|
|
1773
3945
|
function feedbackActionCategory(text) {
|
|
1774
3946
|
if (/删除|删掉|remove|delete|rm\s/.test(text))
|
|
@@ -1802,7 +3974,7 @@ function actionCategoryMatchesRuntimeEvent(category, eventText) {
|
|
|
1802
3974
|
function shouldUseWindowFeedbackOwner(segment, text) {
|
|
1803
3975
|
if (segment.skillType !== 'delegation' && segment.episodeRole !== 'delegator')
|
|
1804
3976
|
return true;
|
|
1805
|
-
return /子\s*claude|child|runner|ttyd|session|
|
|
3977
|
+
return /子\s*(?:claude|codex|agent|代理|智能体)|subagent|sub-agent|child|runner|ttyd|session|thread|agent|有结论|进度|怎么样了|没返回|通知|同步|跑完|完成了吗/i.test(text);
|
|
1806
3978
|
}
|
|
1807
3979
|
function dedupeFeedbackAttributions(attributions) {
|
|
1808
3980
|
const seen = new Set();
|
|
@@ -1817,7 +3989,7 @@ function dedupeFeedbackAttributions(attributions) {
|
|
|
1817
3989
|
return out;
|
|
1818
3990
|
}
|
|
1819
3991
|
function feedbackAttributionReasonForText(lowerText) {
|
|
1820
|
-
if (/pr|pull request|master|分支|组件|产物|文档|报告|demo|链接|地址|文件|项目|skill|agent|claude/.test(lowerText)) {
|
|
3992
|
+
if (/pr|pull request|master|分支|组件|产物|文档|报告|demo|链接|地址|文件|项目|skill|agent|subagent|claude|codex/.test(lowerText)) {
|
|
1821
3993
|
return 'object_match';
|
|
1822
3994
|
}
|
|
1823
3995
|
if (/有结论|进度|没返回|通知|返回|同步|发给|给我|怎么样了|还在线|完成了吗|跑完/.test(lowerText)) {
|
|
@@ -1859,6 +4031,8 @@ function skillSegmentForEvidenceRef(evidenceRef, skillSegments, preferredSkillNa
|
|
|
1859
4031
|
function messageRangeContainsEvidenceRef(range, ref) {
|
|
1860
4032
|
if (typeof ref.messageIndex !== 'number')
|
|
1861
4033
|
return false;
|
|
4034
|
+
if (range.traceId && ref.traceId && range.traceId !== ref.traceId)
|
|
4035
|
+
return false;
|
|
1862
4036
|
if (range.sourceTrace && ref.sourceTrace && range.sourceTrace !== ref.sourceTrace)
|
|
1863
4037
|
return false;
|
|
1864
4038
|
if (range.sessionId && ref.sessionId && range.sessionId !== ref.sessionId)
|
|
@@ -1866,12 +4040,15 @@ function messageRangeContainsEvidenceRef(range, ref) {
|
|
|
1866
4040
|
return ref.messageIndex >= range.startMessageIndex && ref.messageIndex <= range.endMessageIndex;
|
|
1867
4041
|
}
|
|
1868
4042
|
function messageRangeTraceSpecificity(range, ref) {
|
|
1869
|
-
return (range.
|
|
4043
|
+
return (range.traceId && ref.traceId && range.traceId === ref.traceId ? 4 : 0)
|
|
4044
|
+
+ (range.sourceTrace && ref.sourceTrace && range.sourceTrace === ref.sourceTrace ? 2 : 0)
|
|
1870
4045
|
+ (range.sessionId && ref.sessionId && range.sessionId === ref.sessionId ? 1 : 0);
|
|
1871
4046
|
}
|
|
1872
4047
|
function evidenceRefsShareTraceScope(a, b) {
|
|
1873
4048
|
if (!a || !b)
|
|
1874
4049
|
return true;
|
|
4050
|
+
if (a.traceId && b.traceId && a.traceId !== b.traceId)
|
|
4051
|
+
return false;
|
|
1875
4052
|
if (a.sourceTrace && b.sourceTrace && a.sourceTrace !== b.sourceTrace)
|
|
1876
4053
|
return false;
|
|
1877
4054
|
if (a.sessionId && b.sessionId && a.sessionId !== b.sessionId)
|
|
@@ -1881,7 +4058,12 @@ function evidenceRefsShareTraceScope(a, b) {
|
|
|
1881
4058
|
function timelineEventSharesTraceScope(event, anchor) {
|
|
1882
4059
|
return evidenceRefsShareTraceScope(event, anchor);
|
|
1883
4060
|
}
|
|
1884
|
-
function
|
|
4061
|
+
function skillSegmentsSharePhysicalTrace(a, b) {
|
|
4062
|
+
const aTraceIds = new Set((a.messageRanges ?? []).map((range) => range.traceId).filter((value) => Boolean(value)));
|
|
4063
|
+
const bTraceIds = new Set((b.messageRanges ?? []).map((range) => range.traceId).filter((value) => Boolean(value)));
|
|
4064
|
+
if (aTraceIds.size > 0 && bTraceIds.size > 0) {
|
|
4065
|
+
return Array.from(aTraceIds).some((traceId) => bTraceIds.has(traceId));
|
|
4066
|
+
}
|
|
1885
4067
|
const aTraces = new Set((a.messageRanges ?? []).map((range) => range.sourceTrace).filter((value) => Boolean(value)));
|
|
1886
4068
|
const bTraces = new Set((b.messageRanges ?? []).map((range) => range.sourceTrace).filter((value) => Boolean(value)));
|
|
1887
4069
|
if (aTraces.size === 0 || bTraces.size === 0)
|
|
@@ -1943,8 +4125,9 @@ function sessionStoryGoalSlices(session, invocations) {
|
|
|
1943
4125
|
return Array.from(byId.entries()).map(([id, group], index) => {
|
|
1944
4126
|
const timeline = uniqueTimelineEvents(group.flatMap((invocation) => invocation.timeline)).sort(compareTimelineEvents);
|
|
1945
4127
|
const userEvents = timeline.filter((event) => event.kind === 'user_message');
|
|
1946
|
-
const
|
|
1947
|
-
const
|
|
4128
|
+
const timestampedInvocations = group.filter(invocationTimestampObserved);
|
|
4129
|
+
const startTimestamp = minString(timestampedInvocations.map((invocation) => invocation.startTimestamp)) ?? session.startTimestamp;
|
|
4130
|
+
const endTimestamp = maxString(timestampedInvocations.map((invocation) => invocation.endTimestamp)) ?? session.endTimestamp;
|
|
1948
4131
|
const hasGoalShift = userEvents.some((event) => hasUserGoalShiftSignal(event.snippet ?? ''));
|
|
1949
4132
|
const reasonCode = hasGoalShift
|
|
1950
4133
|
? 'explicit_user_goal_shift'
|
|
@@ -1959,7 +4142,12 @@ function sessionStoryGoalSlices(session, invocations) {
|
|
|
1959
4142
|
inferredUserGoal: inferUserGoal(userEvents),
|
|
1960
4143
|
evidenceRefs: userEvents.slice(0, 3).map(evidenceRefFromTimeline),
|
|
1961
4144
|
};
|
|
1962
|
-
}).sort((a, b) =>
|
|
4145
|
+
}).sort((a, b) => {
|
|
4146
|
+
const aObserved = a.startTimestamp !== UNOBSERVED_TRACE_TIMESTAMP;
|
|
4147
|
+
const bObserved = b.startTimestamp !== UNOBSERVED_TRACE_TIMESTAMP;
|
|
4148
|
+
return Number(bObserved) - Number(aObserved)
|
|
4149
|
+
|| a.startTimestamp.localeCompare(b.startTimestamp);
|
|
4150
|
+
});
|
|
1963
4151
|
}
|
|
1964
4152
|
function sessionStorySubagentDispatches(session) {
|
|
1965
4153
|
const branches = session.timelineTree?.branches ?? [];
|
|
@@ -1967,10 +4155,13 @@ function sessionStorySubagentDispatches(session) {
|
|
|
1967
4155
|
id: hashParts('session-story-dispatch', session.id, branch.id),
|
|
1968
4156
|
order: index + 1,
|
|
1969
4157
|
branchId: branch.id,
|
|
4158
|
+
childSessionId: branch.sessionId,
|
|
4159
|
+
traceId: branch.traceId ?? branch.sourceTrace,
|
|
1970
4160
|
label: branch.label,
|
|
1971
4161
|
sourceTrace: branch.sourceTrace,
|
|
1972
4162
|
attachTo: branch.attachTo ? {
|
|
1973
4163
|
messageIndex: branch.attachTo.messageIndex,
|
|
4164
|
+
callInstanceId: branch.attachTo.callInstanceId,
|
|
1974
4165
|
toolUseId: branch.attachTo.toolUseId,
|
|
1975
4166
|
label: branch.attachTo.label,
|
|
1976
4167
|
} : undefined,
|
|
@@ -2004,8 +4195,11 @@ function sessionStorySkillLinks(session, invocations) {
|
|
|
2004
4195
|
}
|
|
2005
4196
|
function inferSkillRole(group, allInvocations, session) {
|
|
2006
4197
|
const hasRoutingSignal = group.some((invocation) => routingEvidenceEvents(invocation).length > 0);
|
|
2007
|
-
const hasBranchDispatch = (session.timelineTree?.branches.length ?? 0) > 0 && group.some((invocation) => invocation.timeline.some((event) =>
|
|
2008
|
-
|
|
4198
|
+
const hasBranchDispatch = (session.timelineTree?.branches.length ?? 0) > 0 && group.some((invocation) => invocation.timeline.some((event) => isOrchestrationRuntimeEvent(event)
|
|
4199
|
+
|| isDifferentSkillInvocationEvent(event, invocation.skillName)));
|
|
4200
|
+
const hasExecutionSignal = group.some((invocation) => invocation.timeline.some((event) => event.kind === 'tool_use'
|
|
4201
|
+
&& !isOrchestrationRuntimeEvent(event)
|
|
4202
|
+
&& !isDifferentSkillInvocationEvent(event, invocation.skillName)));
|
|
2009
4203
|
if ((hasRoutingSignal || hasBranchDispatch) && allInvocations.length > group.length)
|
|
2010
4204
|
return hasExecutionSignal ? 'mixed' : 'router';
|
|
2011
4205
|
if (hasRoutingSignal || hasBranchDispatch)
|
|
@@ -2018,10 +4212,35 @@ function routingEvidenceEvents(invocation) {
|
|
|
2018
4212
|
return invocation.timeline.filter((event) => {
|
|
2019
4213
|
if (event.kind !== 'assistant_message' && event.kind !== 'tool_use')
|
|
2020
4214
|
return false;
|
|
4215
|
+
if (isOrchestrationRuntimeEvent(event))
|
|
4216
|
+
return true;
|
|
4217
|
+
if (isDifferentSkillInvocationEvent(event, invocation.skillName))
|
|
4218
|
+
return true;
|
|
2021
4219
|
const text = event.fullText ?? event.snippet ?? '';
|
|
2022
|
-
return
|
|
4220
|
+
return /subagent|sub-agent|child agent|子\s*(?:agent|代理|智能体|Claude|Codex)|子任务|分发|委派|路由|delegate|dispatch|route|调用.+skill|走\s*`?[\w-]+`?\s*skill/i.test(text);
|
|
2023
4221
|
}).slice(0, 3);
|
|
2024
4222
|
}
|
|
4223
|
+
function isDifferentSkillInvocationEvent(event, currentSkillName) {
|
|
4224
|
+
if (event.kind !== 'tool_use' || !/^Skill$/i.test(event.toolName ?? ''))
|
|
4225
|
+
return false;
|
|
4226
|
+
const text = event.fullText ?? event.snippet ?? '';
|
|
4227
|
+
let targetSkill;
|
|
4228
|
+
try {
|
|
4229
|
+
const input = JSON.parse(text);
|
|
4230
|
+
if (isObjectRecord(input)) {
|
|
4231
|
+
targetSkill = typeof input.skill === 'string'
|
|
4232
|
+
? input.skill
|
|
4233
|
+
: typeof input.name === 'string'
|
|
4234
|
+
? input.name
|
|
4235
|
+
: undefined;
|
|
4236
|
+
}
|
|
4237
|
+
}
|
|
4238
|
+
catch {
|
|
4239
|
+
targetSkill = text.match(/["']?(?:skill|name)["']?\s*[:=]\s*["']?([a-z0-9][\w.-]*)/i)?.[1];
|
|
4240
|
+
}
|
|
4241
|
+
return Boolean(targetSkill
|
|
4242
|
+
&& targetSkill.trim().toLowerCase() !== currentSkillName.trim().toLowerCase());
|
|
4243
|
+
}
|
|
2025
4244
|
function skillRoleLabel(role) {
|
|
2026
4245
|
if (role === 'router')
|
|
2027
4246
|
return '路由';
|
|
@@ -2271,7 +4490,7 @@ function declaredBehaviorChecklistItems(session, episodes) {
|
|
|
2271
4490
|
contribution: 'informational',
|
|
2272
4491
|
reason: hasSkillRead
|
|
2273
4492
|
? `看到 ${session.evidenceChain.skillContextCount} 次 SKILL.md 加载事件,可作为能力归因证据。`
|
|
2274
|
-
: `日志里没看到 LLM
|
|
4493
|
+
: `日志里没看到 LLM 主动读取 SKILL.md。这次 skill 归因来自:${attributionLabel}。脚本、Skill 工具或命令触发时可能没有显式读取事件,不代表 skill 没用上。`,
|
|
2275
4494
|
evidenceRefs: [session.evidenceChain.firstSkillContext, session.evidenceChain.firstToolUse],
|
|
2276
4495
|
}),
|
|
2277
4496
|
checklistItem({
|
|
@@ -2687,18 +4906,28 @@ function skillSelectionStepText(session) {
|
|
|
2687
4906
|
function executionStepStatus(session, expectedToolCheck) {
|
|
2688
4907
|
if (session.indicators.toolFailureCount > 0 || expectedToolCheck.declared && expectedToolCheck.matchedTools.length === 0)
|
|
2689
4908
|
return 'attention';
|
|
4909
|
+
if ((session.indicators.toolCancelledCount ?? 0) > 0
|
|
4910
|
+
|| (session.indicators.toolUnknownCount ?? 0) > 0)
|
|
4911
|
+
return 'unknown';
|
|
2690
4912
|
if (session.indicators.toolCallCount > 0)
|
|
2691
4913
|
return 'ok';
|
|
2692
4914
|
return 'unknown';
|
|
2693
4915
|
}
|
|
2694
4916
|
function executionStepText(session, expectedToolCheck = expectedToolCheckForSession(session)) {
|
|
2695
|
-
const failures = session.indicators.toolFailureCount > 0 ? `,其中失败 ${session.indicators.toolFailureCount} 次` : '';
|
|
2696
4917
|
const expected = expectedToolCheck.declared
|
|
2697
4918
|
? expectedToolCheck.matchedTools.length > 0
|
|
2698
4919
|
? `命中声明的核心工具:${expectedToolCheck.matchedTools.join('、')}。`
|
|
2699
4920
|
: `但没有命中能力声明的核心工具:${expectedToolCheck.expectedTools.join('、')}。`
|
|
2700
4921
|
: '';
|
|
2701
|
-
return
|
|
4922
|
+
return `${executionOutcomeText(session.indicators)}${expected}`;
|
|
4923
|
+
}
|
|
4924
|
+
function executionOutcomeText(indicators) {
|
|
4925
|
+
const details = [
|
|
4926
|
+
indicators.toolFailureCount > 0 ? `失败 ${indicators.toolFailureCount} 次` : '',
|
|
4927
|
+
(indicators.toolCancelledCount ?? 0) > 0 ? `取消 ${indicators.toolCancelledCount ?? 0} 次` : '',
|
|
4928
|
+
(indicators.toolUnknownCount ?? 0) > 0 ? `状态未知 ${indicators.toolUnknownCount ?? 0} 次` : '',
|
|
4929
|
+
].filter(Boolean);
|
|
4930
|
+
return `执行中看到 ${indicators.toolCallCount} 次工具调用${details.length > 0 ? `,其中${details.join(',')}` : ''}。`;
|
|
2702
4931
|
}
|
|
2703
4932
|
function deliveryStepText(session) {
|
|
2704
4933
|
const closure = userFacingClosureForSession(session);
|
|
@@ -3030,17 +5259,30 @@ function suggestionTextForChecklistItem(key) {
|
|
|
3030
5259
|
return suggestions[key];
|
|
3031
5260
|
}
|
|
3032
5261
|
function sumTokenUsage(invocations) {
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
inputTokens
|
|
3040
|
-
outputTokens
|
|
3041
|
-
cacheReadTokens
|
|
3042
|
-
cacheCreationTokens
|
|
3043
|
-
|
|
5262
|
+
const observed = invocations.filter((invocation) => invocation.metrics.tokenUsageObserved);
|
|
5263
|
+
const inputTokens = checkedSumTokenCounts(...observed.map((invocation) => invocation.metrics.inputTokens));
|
|
5264
|
+
const outputTokens = checkedSumTokenCounts(...observed.map((invocation) => invocation.metrics.outputTokens));
|
|
5265
|
+
const cacheReadTokens = checkedSumTokenCounts(...observed.map((invocation) => invocation.metrics.cacheReadTokens));
|
|
5266
|
+
const cacheCreationTokens = checkedSumTokenCounts(...observed.map((invocation) => invocation.metrics.cacheCreationTokens));
|
|
5267
|
+
const aggregateValid = [
|
|
5268
|
+
inputTokens,
|
|
5269
|
+
outputTokens,
|
|
5270
|
+
cacheReadTokens,
|
|
5271
|
+
cacheCreationTokens,
|
|
5272
|
+
].every((value) => value !== undefined)
|
|
5273
|
+
&& checkedSumTokenCounts(inputTokens, outputTokens, cacheReadTokens, cacheCreationTokens) !== undefined;
|
|
5274
|
+
const observedInvocationCount = aggregateValid ? observed.length : 0;
|
|
5275
|
+
return {
|
|
5276
|
+
inputTokens: inputTokens ?? 0,
|
|
5277
|
+
outputTokens: outputTokens ?? 0,
|
|
5278
|
+
cacheReadTokens: cacheReadTokens ?? 0,
|
|
5279
|
+
cacheCreationTokens: cacheCreationTokens ?? 0,
|
|
5280
|
+
observedInvocationCount,
|
|
5281
|
+
invocationCount: invocations.length,
|
|
5282
|
+
coverage: invocations.length > 0
|
|
5283
|
+
? Number((observedInvocationCount / invocations.length).toFixed(4))
|
|
5284
|
+
: 1,
|
|
5285
|
+
};
|
|
3044
5286
|
}
|
|
3045
5287
|
function summarizeExperienceSkills(sessions, invocations) {
|
|
3046
5288
|
const bySkill = new Map();
|
|
@@ -3050,7 +5292,7 @@ function summarizeExperienceSkills(sessions, invocations) {
|
|
|
3050
5292
|
bySkill.set(session.skillName, group);
|
|
3051
5293
|
}
|
|
3052
5294
|
const invocationCountBySkill = invocations.reduce((acc, invocation) => {
|
|
3053
|
-
acc
|
|
5295
|
+
incrementRecordCount(acc, invocation.skillName);
|
|
3054
5296
|
return acc;
|
|
3055
5297
|
}, {});
|
|
3056
5298
|
const invocationGroupBySkill = invocations.reduce((acc, invocation) => {
|
|
@@ -3066,9 +5308,15 @@ function summarizeExperienceSkills(sessions, invocations) {
|
|
|
3066
5308
|
const evidenceChain = sumEvidenceChains(group.map((session) => session.evidenceChain));
|
|
3067
5309
|
const ruleFindings = mergeRuleFindings(group.flatMap((session) => session.ruleFindings));
|
|
3068
5310
|
const problemPatterns = mergeExperienceProblemPatterns(group.flatMap((session) => session.problemPatterns));
|
|
5311
|
+
const timestampedInvocations = skillInvocations.filter(invocationTimestampObserved);
|
|
5312
|
+
const timestampedInvocationCount = timestampedInvocations.length;
|
|
5313
|
+
const firstSeen = minString(timestampedInvocations.map((invocation) => invocation.startTimestamp))
|
|
5314
|
+
?? first.startTimestamp;
|
|
5315
|
+
const lastSeen = maxString(timestampedInvocations.map((invocation) => invocation.endTimestamp))
|
|
5316
|
+
?? first.endTimestamp;
|
|
3069
5317
|
return {
|
|
3070
5318
|
skillName,
|
|
3071
|
-
invocationCount: invocationCountBySkill
|
|
5319
|
+
invocationCount: ownRecordValue(invocationCountBySkill, skillName) ?? 0,
|
|
3072
5320
|
sessionCount: group.length,
|
|
3073
5321
|
sourceKinds: unique(group.map((session) => session.sourceKind)).sort(),
|
|
3074
5322
|
entrypoints: unique(group.map((session) => session.entrypoint).filter((value) => Boolean(value))).sort(),
|
|
@@ -3079,8 +5327,12 @@ function summarizeExperienceSkills(sessions, invocations) {
|
|
|
3079
5327
|
rawSkillRefs: unique(skillInvocations.map((invocation) => invocation.attribution.rawSkillRef).filter((value) => Boolean(value))).sort(),
|
|
3080
5328
|
commandNames: unique(skillInvocations.map((invocation) => invocation.attribution.commandName).filter((value) => Boolean(value))).sort(),
|
|
3081
5329
|
toolCounts: sumRecordCounts(skillInvocations.map((invocation) => invocation.toolCounts)),
|
|
3082
|
-
firstSeen
|
|
3083
|
-
lastSeen
|
|
5330
|
+
firstSeen,
|
|
5331
|
+
lastSeen,
|
|
5332
|
+
timestampedInvocationCount,
|
|
5333
|
+
timestampCoverage: skillInvocations.length > 0
|
|
5334
|
+
? timestampedInvocationCount / skillInvocations.length
|
|
5335
|
+
: 0,
|
|
3084
5336
|
reviewFirstSessionCount: group.filter((session) => session.reviewPriority === 'review_first').length,
|
|
3085
5337
|
sampleReviewSessionCount: group.filter((session) => session.reviewPriority === 'sample_review').length,
|
|
3086
5338
|
indicators,
|
|
@@ -3152,17 +5404,14 @@ function sourceSenderLabel(value) {
|
|
|
3152
5404
|
return value.sender ?? value.senderId;
|
|
3153
5405
|
}
|
|
3154
5406
|
function scoreForIndicators(indicators) {
|
|
3155
|
-
return indicators.highObservationCount
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
+ indicators.routerDownstreamFailed * 2
|
|
3164
|
-
+ indicators.hedgingCount
|
|
3165
|
-
+ indicators.explicitMarkerCount * 2;
|
|
5407
|
+
return sumSafeCounts(weightedCount(indicators.highObservationCount, 3), indicators.mediumObservationCount, weightedCount(indicators.userCorrectionCount, 2), weightedCount(indicators.userInterruptionCount, 2), weightedCount(indicators.sessionInterruptedCount, 2), weightedCount(indicators.negativeFeedbackCount, 2), indicators.hardRuleTextHitCount, indicators.toolFailureCount, weightedCount(indicators.routerDownstreamFailed, 2), indicators.hedgingCount, weightedCount(indicators.explicitMarkerCount, 2));
|
|
5408
|
+
}
|
|
5409
|
+
function weightedCount(count, weight) {
|
|
5410
|
+
const weighted = count * weight;
|
|
5411
|
+
if (!Number.isSafeInteger(weighted) || weighted < 0) {
|
|
5412
|
+
throw new RangeError('Weighted observation count exceeds Number.MAX_SAFE_INTEGER');
|
|
5413
|
+
}
|
|
5414
|
+
return weighted;
|
|
3166
5415
|
}
|
|
3167
5416
|
function priorityForScore(score) {
|
|
3168
5417
|
if (score >= 3)
|
|
@@ -3213,14 +5462,14 @@ function basisCodesForIndicators(indicators) {
|
|
|
3213
5462
|
function countTools(segment) {
|
|
3214
5463
|
const counts = {};
|
|
3215
5464
|
for (const toolCall of segment.toolCalls) {
|
|
3216
|
-
counts
|
|
5465
|
+
incrementRecordCount(counts, toolCall.tool);
|
|
3217
5466
|
}
|
|
3218
5467
|
return counts;
|
|
3219
5468
|
}
|
|
3220
5469
|
function countBy(values) {
|
|
3221
5470
|
const counts = {};
|
|
3222
5471
|
for (const value of values) {
|
|
3223
|
-
counts
|
|
5472
|
+
incrementRecordCount(counts, value);
|
|
3224
5473
|
}
|
|
3225
5474
|
return counts;
|
|
3226
5475
|
}
|
|
@@ -3228,46 +5477,48 @@ function sumRecordCounts(values) {
|
|
|
3228
5477
|
const counts = {};
|
|
3229
5478
|
for (const value of values) {
|
|
3230
5479
|
for (const [key, count] of Object.entries(value)) {
|
|
3231
|
-
counts
|
|
5480
|
+
incrementRecordCount(counts, key, count);
|
|
3232
5481
|
}
|
|
3233
5482
|
}
|
|
3234
5483
|
return counts;
|
|
3235
5484
|
}
|
|
3236
5485
|
function sumIndicators(values) {
|
|
3237
5486
|
return values.reduce((acc, value) => ({
|
|
3238
|
-
userMessageCount: acc.userMessageCount
|
|
3239
|
-
userFollowUpCount: acc.userFollowUpCount
|
|
3240
|
-
userCorrectionCount: acc.userCorrectionCount
|
|
3241
|
-
userInterruptionCount: acc.userInterruptionCount
|
|
3242
|
-
sessionInterruptedCount: acc.sessionInterruptedCount
|
|
3243
|
-
negativeFeedbackCount: acc.negativeFeedbackCount
|
|
3244
|
-
positiveFeedbackCount: acc.positiveFeedbackCount
|
|
3245
|
-
userGoalShiftCount: acc.userGoalShiftCount
|
|
3246
|
-
hardRuleTextHitCount: acc.hardRuleTextHitCount
|
|
3247
|
-
assistantDeliverySignalCount: acc.assistantDeliverySignalCount
|
|
3248
|
-
deliverableArtifactSignalCount: acc.deliverableArtifactSignalCount
|
|
3249
|
-
routerDownstreamCompleted: acc.routerDownstreamCompleted
|
|
3250
|
-
routerDownstreamFailed: acc.routerDownstreamFailed
|
|
3251
|
-
selfCorrectionCount: acc.selfCorrectionCount
|
|
3252
|
-
repeatedExecutionCount: acc.repeatedExecutionCount
|
|
3253
|
-
toolCallCount: acc.toolCallCount
|
|
3254
|
-
toolFailureCount: acc.toolFailureCount
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
5487
|
+
userMessageCount: sumSafeCounts(acc.userMessageCount, value.userMessageCount),
|
|
5488
|
+
userFollowUpCount: sumSafeCounts(acc.userFollowUpCount, value.userFollowUpCount),
|
|
5489
|
+
userCorrectionCount: sumSafeCounts(acc.userCorrectionCount, value.userCorrectionCount),
|
|
5490
|
+
userInterruptionCount: sumSafeCounts(acc.userInterruptionCount, value.userInterruptionCount),
|
|
5491
|
+
sessionInterruptedCount: sumSafeCounts(acc.sessionInterruptedCount, value.sessionInterruptedCount ?? 0),
|
|
5492
|
+
negativeFeedbackCount: sumSafeCounts(acc.negativeFeedbackCount, value.negativeFeedbackCount ?? 0),
|
|
5493
|
+
positiveFeedbackCount: sumSafeCounts(acc.positiveFeedbackCount, value.positiveFeedbackCount ?? 0),
|
|
5494
|
+
userGoalShiftCount: sumSafeCounts(acc.userGoalShiftCount, value.userGoalShiftCount ?? 0),
|
|
5495
|
+
hardRuleTextHitCount: sumSafeCounts(acc.hardRuleTextHitCount, value.hardRuleTextHitCount),
|
|
5496
|
+
assistantDeliverySignalCount: sumSafeCounts(acc.assistantDeliverySignalCount, value.assistantDeliverySignalCount ?? 0),
|
|
5497
|
+
deliverableArtifactSignalCount: sumSafeCounts(acc.deliverableArtifactSignalCount, value.deliverableArtifactSignalCount ?? 0),
|
|
5498
|
+
routerDownstreamCompleted: sumSafeCounts(acc.routerDownstreamCompleted, value.routerDownstreamCompleted ?? 0),
|
|
5499
|
+
routerDownstreamFailed: sumSafeCounts(acc.routerDownstreamFailed, value.routerDownstreamFailed ?? 0),
|
|
5500
|
+
selfCorrectionCount: sumSafeCounts(acc.selfCorrectionCount, value.selfCorrectionCount ?? 0),
|
|
5501
|
+
repeatedExecutionCount: sumSafeCounts(acc.repeatedExecutionCount, value.repeatedExecutionCount ?? 0),
|
|
5502
|
+
toolCallCount: sumSafeCounts(acc.toolCallCount, value.toolCallCount),
|
|
5503
|
+
toolFailureCount: sumSafeCounts(acc.toolFailureCount, value.toolFailureCount),
|
|
5504
|
+
toolCancelledCount: sumSafeCounts(acc.toolCancelledCount ?? 0, value.toolCancelledCount ?? 0),
|
|
5505
|
+
toolUnknownCount: sumSafeCounts(acc.toolUnknownCount ?? 0, value.toolUnknownCount ?? 0),
|
|
5506
|
+
highObservationCount: sumSafeCounts(acc.highObservationCount, value.highObservationCount),
|
|
5507
|
+
mediumObservationCount: sumSafeCounts(acc.mediumObservationCount, value.mediumObservationCount),
|
|
5508
|
+
hedgingCount: sumSafeCounts(acc.hedgingCount, value.hedgingCount),
|
|
5509
|
+
explicitMarkerCount: sumSafeCounts(acc.explicitMarkerCount, value.explicitMarkerCount),
|
|
3259
5510
|
}), { ...ZERO_INDICATORS });
|
|
3260
5511
|
}
|
|
3261
5512
|
function sumEvidenceChains(values) {
|
|
3262
5513
|
return values.reduce((acc, value) => ({
|
|
3263
|
-
userMessageCount: acc.userMessageCount
|
|
3264
|
-
runtimeContextCount: acc.runtimeContextCount
|
|
3265
|
-
skillContextCount: acc.skillContextCount
|
|
3266
|
-
assistantMessageCount: acc.assistantMessageCount
|
|
3267
|
-
toolUseCount: acc.toolUseCount
|
|
3268
|
-
toolResultCount: acc.toolResultCount
|
|
3269
|
-
toolFailureResultCount: acc.toolFailureResultCount
|
|
3270
|
-
observationCount: acc.observationCount
|
|
5514
|
+
userMessageCount: sumSafeCounts(acc.userMessageCount, value?.userMessageCount ?? 0),
|
|
5515
|
+
runtimeContextCount: sumSafeCounts(acc.runtimeContextCount, value?.runtimeContextCount ?? 0),
|
|
5516
|
+
skillContextCount: sumSafeCounts(acc.skillContextCount, value?.skillContextCount ?? 0),
|
|
5517
|
+
assistantMessageCount: sumSafeCounts(acc.assistantMessageCount, value?.assistantMessageCount ?? 0),
|
|
5518
|
+
toolUseCount: sumSafeCounts(acc.toolUseCount, value?.toolUseCount ?? 0),
|
|
5519
|
+
toolResultCount: sumSafeCounts(acc.toolResultCount, value?.toolResultCount ?? 0),
|
|
5520
|
+
toolFailureResultCount: sumSafeCounts(acc.toolFailureResultCount, value?.toolFailureResultCount ?? 0),
|
|
5521
|
+
observationCount: sumSafeCounts(acc.observationCount, value?.observationCount ?? 0),
|
|
3271
5522
|
firstUserMessage: acc.firstUserMessage ?? value?.firstUserMessage,
|
|
3272
5523
|
firstRuntimeContext: acc.firstRuntimeContext ?? value?.firstRuntimeContext,
|
|
3273
5524
|
firstSkillContext: acc.firstSkillContext ?? value?.firstSkillContext,
|
|
@@ -3290,7 +5541,7 @@ function mergeRuleFindings(values) {
|
|
|
3290
5541
|
for (const value of values) {
|
|
3291
5542
|
const existing = byCode.get(value.code);
|
|
3292
5543
|
if (existing) {
|
|
3293
|
-
existing.count
|
|
5544
|
+
existing.count = sumSafeCounts(existing.count, value.count);
|
|
3294
5545
|
existing.evidenceRefs = uniqueEvidenceRefs([...existing.evidenceRefs, ...value.evidenceRefs]).slice(0, 5);
|
|
3295
5546
|
}
|
|
3296
5547
|
else {
|
|
@@ -3318,8 +5569,11 @@ function compareTimelineEvents(a, b) {
|
|
|
3318
5569
|
if (ta && tb && ta !== tb) {
|
|
3319
5570
|
return ta.localeCompare(tb);
|
|
3320
5571
|
}
|
|
3321
|
-
//
|
|
3322
|
-
|
|
5572
|
+
// 同一条物理 trace 内 → 按 messageIndex 派生的 order(跨 trace 比 order 没意义)。
|
|
5573
|
+
const samePhysicalTrace = a.traceId && b.traceId
|
|
5574
|
+
? a.traceId === b.traceId
|
|
5575
|
+
: a.sourceTrace === b.sourceTrace;
|
|
5576
|
+
if (samePhysicalTrace) {
|
|
3323
5577
|
return a.order - b.order;
|
|
3324
5578
|
}
|
|
3325
5579
|
// 跨 trace 且 timestamp 不可比 → 主线优先(避免缺 timestamp 时 subagent 顶到最前)
|
|
@@ -3327,23 +5581,50 @@ function compareTimelineEvents(a, b) {
|
|
|
3327
5581
|
const rankDiff = roleRank(a) - roleRank(b);
|
|
3328
5582
|
if (rankDiff !== 0)
|
|
3329
5583
|
return rankDiff;
|
|
3330
|
-
// 同 traceRole
|
|
3331
|
-
return a.sourceTrace.localeCompare(b.sourceTrace)
|
|
5584
|
+
// 同 traceRole 且 timestamp 缺失时,用物理 trace 身份提供稳定顺序。
|
|
5585
|
+
return (a.traceId ?? a.sourceTrace).localeCompare(b.traceId ?? b.sourceTrace)
|
|
5586
|
+
|| a.sourceTrace.localeCompare(b.sourceTrace)
|
|
5587
|
+
|| a.order - b.order;
|
|
5588
|
+
}
|
|
5589
|
+
function traceRecordRanges(events) {
|
|
5590
|
+
const byTrace = new Map();
|
|
5591
|
+
for (const event of events) {
|
|
5592
|
+
if (typeof event.messageIndex !== 'number')
|
|
5593
|
+
continue;
|
|
5594
|
+
const traceId = event.traceId ?? event.sourceTrace;
|
|
5595
|
+
const group = byTrace.get(traceId) ?? [];
|
|
5596
|
+
group.push(event);
|
|
5597
|
+
byTrace.set(traceId, group);
|
|
5598
|
+
}
|
|
5599
|
+
return Array.from(byTrace.entries())
|
|
5600
|
+
.map(([traceId, group]) => {
|
|
5601
|
+
const indexes = group.map((event) => event.messageIndex);
|
|
5602
|
+
return {
|
|
5603
|
+
traceId,
|
|
5604
|
+
sourceTrace: group[0].sourceTrace,
|
|
5605
|
+
startRecordIndex: Math.min(...indexes),
|
|
5606
|
+
endRecordIndex: Math.max(...indexes),
|
|
5607
|
+
eventCount: group.length,
|
|
5608
|
+
};
|
|
5609
|
+
})
|
|
5610
|
+
.sort((a, b) => a.traceId.localeCompare(b.traceId));
|
|
3332
5611
|
}
|
|
3333
5612
|
function buildSessionTimelineTree(sessionId, sessions) {
|
|
3334
|
-
const mainSession = sessions.find((session) => session.
|
|
3335
|
-
?? sessions.find((session) => session.
|
|
3336
|
-
const main = mainSession ? buildTimelineWindow(mainSession, 0, mainSession.
|
|
5613
|
+
const mainSession = sessions.find((session) => session.role === 'main')
|
|
5614
|
+
?? sessions.find((session) => session.role === 'standalone');
|
|
5615
|
+
const main = mainSession ? buildTimelineWindow(mainSession, 0, mainSession.events.length) : [];
|
|
3337
5616
|
const branches = sessions
|
|
3338
5617
|
.filter((session) => !mainSession || session !== mainSession)
|
|
3339
5618
|
.map((session) => {
|
|
3340
|
-
const events = buildTimelineWindow(session, 0, session.
|
|
5619
|
+
const events = buildTimelineWindow(session, 0, session.events.length);
|
|
3341
5620
|
const attachTo = inferSubagentAttachment(main, session);
|
|
3342
5621
|
return {
|
|
3343
|
-
id: hashParts('timeline-branch', session.
|
|
3344
|
-
label: session.
|
|
5622
|
+
id: hashParts('timeline-branch', session.traceId),
|
|
5623
|
+
label: (session.label ?? basename(session.sourcePath)) || 'subagent',
|
|
5624
|
+
sessionId: session.runId,
|
|
5625
|
+
traceId: session.traceId,
|
|
3345
5626
|
sourceTrace: session.sourcePath,
|
|
3346
|
-
traceRole: session.
|
|
5627
|
+
traceRole: session.role,
|
|
3347
5628
|
attachTo,
|
|
3348
5629
|
events,
|
|
3349
5630
|
};
|
|
@@ -3364,8 +5645,10 @@ function inferSubagentAttachment(mainEvents, branchSession) {
|
|
|
3364
5645
|
if (!event)
|
|
3365
5646
|
return undefined;
|
|
3366
5647
|
return {
|
|
5648
|
+
traceId: event.traceId,
|
|
3367
5649
|
sourceTrace: event.sourceTrace,
|
|
3368
5650
|
messageIndex: event.messageIndex,
|
|
5651
|
+
callInstanceId: event.callInstanceId,
|
|
3369
5652
|
toolUseId: event.toolUseId,
|
|
3370
5653
|
label: event.toolName,
|
|
3371
5654
|
};
|
|
@@ -3380,11 +5663,15 @@ function maxDefined(values) {
|
|
|
3380
5663
|
}
|
|
3381
5664
|
function minString(values) {
|
|
3382
5665
|
const filtered = values.filter((value) => Boolean(value));
|
|
3383
|
-
return filtered.length > 0
|
|
5666
|
+
return filtered.length > 0
|
|
5667
|
+
? filtered.reduce((min, value) => Date.parse(value) < Date.parse(min) ? value : min, filtered[0])
|
|
5668
|
+
: undefined;
|
|
3384
5669
|
}
|
|
3385
5670
|
function maxString(values) {
|
|
3386
5671
|
const filtered = values.filter((value) => Boolean(value));
|
|
3387
|
-
return filtered.length > 0
|
|
5672
|
+
return filtered.length > 0
|
|
5673
|
+
? filtered.reduce((max, value) => Date.parse(value) > Date.parse(max) ? value : max, filtered[0])
|
|
5674
|
+
: undefined;
|
|
3388
5675
|
}
|
|
3389
5676
|
function uniqueEvidenceRefs(refs) {
|
|
3390
5677
|
const byId = new Map();
|
|
@@ -3397,57 +5684,6 @@ function inferUserGoal(userRefs) {
|
|
|
3397
5684
|
const first = userRefs.find((ref) => ref.snippet && !ref.snippet.includes('tool_result'));
|
|
3398
5685
|
return snippet(first?.snippet, 180);
|
|
3399
5686
|
}
|
|
3400
|
-
function isSkillContextRecord(record, text) {
|
|
3401
|
-
const meta = record;
|
|
3402
|
-
if (meta.isMeta === true && typeof meta.sourceToolUseID === 'string')
|
|
3403
|
-
return true;
|
|
3404
|
-
return /^Base directory for this skill:\s+.+(?:\n| )#\s+[a-z0-9][\w.-]*/i.test(text);
|
|
3405
|
-
}
|
|
3406
|
-
function isRuntimeContextRecord(record, text) {
|
|
3407
|
-
const meta = record;
|
|
3408
|
-
if (isRuntimeProtocolPromptText(text))
|
|
3409
|
-
return true;
|
|
3410
|
-
if (/^Conversation info \(untrusted metadata\):\s*```json/i.test(text))
|
|
3411
|
-
return true;
|
|
3412
|
-
if (meta.entrypoint !== 'sdk-ts' || typeof meta.promptId !== 'string')
|
|
3413
|
-
return false;
|
|
3414
|
-
return /^进入.+流程。当前页面已经完成本地工作区恢复/.test(text)
|
|
3415
|
-
|| /gui-workflow route/.test(text)
|
|
3416
|
-
|| /当前页面已经完成本地工作区恢复/.test(text);
|
|
3417
|
-
}
|
|
3418
|
-
function timestampOf(record) {
|
|
3419
|
-
if (!record || typeof record !== 'object')
|
|
3420
|
-
return undefined;
|
|
3421
|
-
const value = record.timestamp;
|
|
3422
|
-
return typeof value === 'string' ? value : undefined;
|
|
3423
|
-
}
|
|
3424
|
-
function sourceKindForPath(path) {
|
|
3425
|
-
if (path.includes('/openclaw') || path.includes('/.openclaw/'))
|
|
3426
|
-
return 'openclaw';
|
|
3427
|
-
if (path.endsWith('.jsonl'))
|
|
3428
|
-
return 'claude';
|
|
3429
|
-
if (path.endsWith('.log'))
|
|
3430
|
-
return 'markdown_log';
|
|
3431
|
-
return 'unknown';
|
|
3432
|
-
}
|
|
3433
|
-
function inferEntrypointFromRecords(session) {
|
|
3434
|
-
for (const record of session.records) {
|
|
3435
|
-
if (!record || typeof record !== 'object')
|
|
3436
|
-
continue;
|
|
3437
|
-
const entrypoint = record.entrypoint;
|
|
3438
|
-
if (typeof entrypoint === 'string' && entrypoint.trim())
|
|
3439
|
-
return entrypoint;
|
|
3440
|
-
}
|
|
3441
|
-
if (session.entrypoint)
|
|
3442
|
-
return session.entrypoint;
|
|
3443
|
-
if (session.sourceKind === 'openclaw')
|
|
3444
|
-
return 'openclaw';
|
|
3445
|
-
if (session.sourceKind === 'markdown_log')
|
|
3446
|
-
return 'markdown_log';
|
|
3447
|
-
if (session.sourcePath.endsWith('.log'))
|
|
3448
|
-
return 'markdown_log';
|
|
3449
|
-
return undefined;
|
|
3450
|
-
}
|
|
3451
5687
|
function snippet(value, max = 240) {
|
|
3452
5688
|
const text = typeof value === 'string' ? value : String(value ?? '');
|
|
3453
5689
|
const normalized = text.replace(/\s+/g, ' ').trim();
|