oh-my-knowledge 0.52.0 → 0.52.2
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/dist/observability/codex-conversation-index.d.ts +5 -1
- package/dist/observability/codex-conversation-index.js +39 -5
- package/dist/observability/conversation-catalog.js +3 -1
- package/dist/observability/feedback-matchers.d.ts +5 -0
- package/dist/observability/feedback-matchers.js +15 -0
- package/dist/observability/knowledge-debugger.js +52 -19
- package/dist/observability/task-semantic-projection.d.ts +11 -0
- package/dist/observability/task-semantic-projection.js +172 -0
- package/dist/observability/task-window.d.ts +1 -0
- package/dist/observability/task-window.js +28 -7
- package/dist/renderer/knowledge-debugger-renderer.js +123 -30
- package/dist/renderer/trajectory-evidence.d.ts +8 -0
- package/dist/renderer/trajectory-evidence.js +12 -0
- package/package.json +1 -1
|
@@ -51,6 +51,10 @@ export interface CodexJsonlLine {
|
|
|
51
51
|
startOffset: number;
|
|
52
52
|
endOffset: number;
|
|
53
53
|
}
|
|
54
|
+
export interface CodexTaskRecordReadOptions {
|
|
55
|
+
/** Include only the next task prefix through its first human user message. */
|
|
56
|
+
includeNextHumanMessage?: boolean;
|
|
57
|
+
}
|
|
54
58
|
/**
|
|
55
59
|
* Build a compact byte-range index without retaining the rollout. Only records
|
|
56
60
|
* needed for turn boundaries and list metrics are parsed.
|
|
@@ -80,7 +84,7 @@ export declare function isCurrentCodexRolloutIndex(value: unknown, sourcePath: s
|
|
|
80
84
|
* index remains internally consistent even after newer records are appended.
|
|
81
85
|
*/
|
|
82
86
|
export declare function isReusableCodexRolloutIndex(value: unknown, sourcePath: string): value is CodexRolloutIndex;
|
|
83
|
-
export declare function readCodexTaskRecords(index: CodexRolloutIndex, task: CodexIndexedTask): {
|
|
87
|
+
export declare function readCodexTaskRecords(index: CodexRolloutIndex, task: CodexIndexedTask, options?: CodexTaskRecordReadOptions): {
|
|
84
88
|
records: Array<unknown | undefined>;
|
|
85
89
|
lines: CodexJsonlLine[];
|
|
86
90
|
malformedRecordCount: number;
|
|
@@ -327,7 +327,7 @@ export function isReusableCodexRolloutIndex(value, sourcePath) {
|
|
|
327
327
|
return false;
|
|
328
328
|
}
|
|
329
329
|
}
|
|
330
|
-
export function readCodexTaskRecords(index, task) {
|
|
330
|
+
export function readCodexTaskRecords(index, task, options = {}) {
|
|
331
331
|
const records = [];
|
|
332
332
|
if (index.sessionMeta !== undefined && index.sessionMetaLine !== undefined) {
|
|
333
333
|
records[index.sessionMetaLine] = index.sessionMeta;
|
|
@@ -345,8 +345,40 @@ export function readCodexTaskRecords(index, task) {
|
|
|
345
345
|
malformedRecordCount += 1;
|
|
346
346
|
}
|
|
347
347
|
}, task.startOffset, task.endOffset, task.startLine);
|
|
348
|
+
if (options.includeNextHumanMessage) {
|
|
349
|
+
const taskIndex = index.tasks.findIndex((candidate) => candidate.turnId === task.turnId);
|
|
350
|
+
const nextTask = taskIndex >= 0 ? index.tasks[taskIndex + 1] : undefined;
|
|
351
|
+
if (nextTask) {
|
|
352
|
+
forEachJsonlLine(index.sourcePath, (record) => {
|
|
353
|
+
lines.push(record);
|
|
354
|
+
try {
|
|
355
|
+
const parsed = JSON.parse(record.text);
|
|
356
|
+
records[record.line] = parsed;
|
|
357
|
+
return isHumanUserPrompt(parsed) ? false : undefined;
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
malformedRecordCount += 1;
|
|
361
|
+
return undefined;
|
|
362
|
+
}
|
|
363
|
+
}, nextTask.startOffset, nextTask.endOffset, nextTask.startLine);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
348
366
|
return { records, lines, malformedRecordCount };
|
|
349
367
|
}
|
|
368
|
+
function isHumanUserPrompt(value) {
|
|
369
|
+
const raw = objectValue(value);
|
|
370
|
+
if (!raw)
|
|
371
|
+
return false;
|
|
372
|
+
const recordType = stringValue(raw.type);
|
|
373
|
+
const payload = objectValue(raw.payload) ?? {};
|
|
374
|
+
const payloadType = stringValue(payload.type);
|
|
375
|
+
if (!isUserPromptRecord(recordType, payloadType, payload))
|
|
376
|
+
return false;
|
|
377
|
+
const message = userPromptText(payload)?.trim();
|
|
378
|
+
return Boolean(message
|
|
379
|
+
&& codexUserMessageOrigin(message) === 'human'
|
|
380
|
+
&& codexUserDisplayText(message)?.trim());
|
|
381
|
+
}
|
|
350
382
|
function finalizeSupersededTask(task, endOffset, endLine, endTimestamp) {
|
|
351
383
|
return stripMutable({
|
|
352
384
|
...task,
|
|
@@ -501,7 +533,7 @@ function forEachJsonlLine(filePath, visit, startOffset = 0, endOffset = Number.P
|
|
|
501
533
|
let indexedEndsWithNewline = true;
|
|
502
534
|
let fragments = [];
|
|
503
535
|
try {
|
|
504
|
-
while (absoluteOffset < endOffset) {
|
|
536
|
+
readLoop: while (absoluteOffset < endOffset) {
|
|
505
537
|
const requestBytes = Math.min(buffer.length, endOffset - absoluteOffset);
|
|
506
538
|
const bytesRead = readSync(fd, buffer, 0, requestBytes, absoluteOffset);
|
|
507
539
|
if (bytesRead === 0)
|
|
@@ -515,7 +547,7 @@ function forEachJsonlLine(filePath, visit, startOffset = 0, endOffset = Number.P
|
|
|
515
547
|
? Buffer.concat([...fragments, tail])
|
|
516
548
|
: tail;
|
|
517
549
|
const nextOffset = absoluteOffset + index + 1;
|
|
518
|
-
emitLine(lineBuffer, lineNumber, lineStartOffset, nextOffset, visit);
|
|
550
|
+
const shouldContinue = emitLine(lineBuffer, lineNumber, lineStartOffset, nextOffset, visit);
|
|
519
551
|
indexedSize = nextOffset;
|
|
520
552
|
indexedLineCount = lineNumber + 1;
|
|
521
553
|
indexedEndsWithNewline = true;
|
|
@@ -523,6 +555,8 @@ function forEachJsonlLine(filePath, visit, startOffset = 0, endOffset = Number.P
|
|
|
523
555
|
cursor = index + 1;
|
|
524
556
|
lineStartOffset = nextOffset;
|
|
525
557
|
lineNumber += 1;
|
|
558
|
+
if (!shouldContinue)
|
|
559
|
+
break readLoop;
|
|
526
560
|
}
|
|
527
561
|
if (cursor < bytesRead)
|
|
528
562
|
fragments.push(Buffer.from(buffer.subarray(cursor, bytesRead)));
|
|
@@ -561,6 +595,6 @@ function isCompleteJsonRecord(buffer) {
|
|
|
561
595
|
function emitLine(buffer, line, startOffset, endOffset, visit) {
|
|
562
596
|
const text = buffer.toString('utf8').trim();
|
|
563
597
|
if (!text)
|
|
564
|
-
return;
|
|
565
|
-
visit({ text, line, startOffset, endOffset });
|
|
598
|
+
return true;
|
|
599
|
+
return visit({ text, line, startOffset, endOffset }) !== false;
|
|
566
600
|
}
|
|
@@ -106,7 +106,9 @@ class CodexConversationCatalog {
|
|
|
106
106
|
trajectoryFromIndex(row, index, indexedTask) {
|
|
107
107
|
const threadId = row.id;
|
|
108
108
|
const turnId = indexedTask.turnId;
|
|
109
|
-
const selected = readCodexTaskRecords(index, indexedTask
|
|
109
|
+
const selected = readCodexTaskRecords(index, indexedTask, {
|
|
110
|
+
includeNextHumanMessage: true,
|
|
111
|
+
});
|
|
110
112
|
const traceSession = parseCodexSessionFile(row.rolloutPath, selected.records);
|
|
111
113
|
const fullSessionTimeline = projectTraceSessionTimeline(traceSession);
|
|
112
114
|
const turns = reconstructExperienceTurns(fullSessionTimeline);
|
|
@@ -28,6 +28,11 @@ export interface TextMatchRange {
|
|
|
28
28
|
}
|
|
29
29
|
export declare function findUserCorrectionMatches(value: string): TextMatchRange[];
|
|
30
30
|
export declare function hasUserCorrectionSignal(value: string): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* 用严格纠正信号把下一轮用户消息关联回上一任务。统计反馈匹配仍保持更宽,
|
|
33
|
+
* 因为「改成」等表达可用于指标统计,但不足以单独证明任务连续性。
|
|
34
|
+
*/
|
|
35
|
+
export declare function hasExplicitFollowUpCorrectionSignal(value: string): boolean;
|
|
31
36
|
export declare function findUserGoalShiftMatches(value: string): TextMatchRange[];
|
|
32
37
|
export declare function hasUserGoalShiftSignal(value: string): boolean;
|
|
33
38
|
export declare function findNegativeFeedbackMatches(value: string): TextMatchRange[];
|
|
@@ -40,6 +40,14 @@ const PHRASE_USER_CORRECTION_TERMS = [
|
|
|
40
40
|
'改成',
|
|
41
41
|
'重来',
|
|
42
42
|
];
|
|
43
|
+
const EXPLICIT_FOLLOW_UP_CORRECTION_PATTERNS = [
|
|
44
|
+
/(?:^|[\s,,;;.。!!??::])(?:不对|错了)(?:啊|呀|吧|呢)?(?=$|[\s,,;;.。!!??::])/iu,
|
|
45
|
+
/(?:这|这个|这样|你的|你.{0,8})(?:不对|错了)/iu,
|
|
46
|
+
/不是这个|不是我要的|不要这样|理解错|看错|你没懂|重来|重新来|重做|重新做/iu,
|
|
47
|
+
/\b(?:that(?:'s| is)|this is|you(?:'re| are)) wrong\b/iu,
|
|
48
|
+
/\b(?:this|that) is (?:incorrect|not right)\b/iu,
|
|
49
|
+
/\byou (?:misunderstood|misread)\b|\b(?:start|do)(?: it)? over\b/iu,
|
|
50
|
+
];
|
|
43
51
|
const USER_GOAL_SHIFT_TERMS = [
|
|
44
52
|
'换个方向',
|
|
45
53
|
'重新来',
|
|
@@ -165,6 +173,13 @@ export function findUserCorrectionMatches(value) {
|
|
|
165
173
|
export function hasUserCorrectionSignal(value) {
|
|
166
174
|
return findUserCorrectionMatches(value).length > 0;
|
|
167
175
|
}
|
|
176
|
+
/**
|
|
177
|
+
* 用严格纠正信号把下一轮用户消息关联回上一任务。统计反馈匹配仍保持更宽,
|
|
178
|
+
* 因为「改成」等表达可用于指标统计,但不足以单独证明任务连续性。
|
|
179
|
+
*/
|
|
180
|
+
export function hasExplicitFollowUpCorrectionSignal(value) {
|
|
181
|
+
return EXPLICIT_FOLLOW_UP_CORRECTION_PATTERNS.some((pattern) => pattern.test(value));
|
|
182
|
+
}
|
|
168
183
|
export function findUserGoalShiftMatches(value) {
|
|
169
184
|
const ranges = [];
|
|
170
185
|
for (const term of USER_GOAL_SHIFT_TERMS) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
+
import { hasUserCorrectionSignal } from './feedback-matchers.js';
|
|
2
3
|
import { resolveTaskWindow } from './task-window.js';
|
|
3
4
|
const AGENTS_CONTEXT_RE = /^# AGENTS\.md instructions for ([^\n]+)\n/gim;
|
|
4
5
|
const SKILL_PATH_RE = /((?:~|\.{0,2}|\/)?[^\s"'`]*\/skills\/(?:\.system\/)?([^/\s"'`]+)\/SKILL\.md)\b/i;
|
|
@@ -16,13 +17,16 @@ export function buildKnowledgeDebuggerViewModel(session, targetTurnId, ingestion
|
|
|
16
17
|
}) {
|
|
17
18
|
const taskWindow = resolveTaskWindow(session, targetTurnId);
|
|
18
19
|
const normalizedEvents = taskWindow.events;
|
|
20
|
+
const normalizedTimeline = normalizedEvents.filter((event) => event.runtimeKind !== 'usage');
|
|
19
21
|
const timeline = taskWindow.semanticEvents.filter((event) => event.runtimeKind !== 'usage');
|
|
22
|
+
const replayTimeline = [...timeline, ...taskWindow.relatedEvents]
|
|
23
|
+
.sort((left, right) => left.order - right.order);
|
|
20
24
|
const knowledgeEvidence = projectKnowledgeEvidence(timeline);
|
|
21
|
-
const steps = buildTaskReplaySteps(session,
|
|
22
|
-
const notices = buildIntegrityNotices(taskWindow.scope,
|
|
23
|
-
const userEvents =
|
|
24
|
-
const assistantEvents =
|
|
25
|
-
const observedModels =
|
|
25
|
+
const steps = buildTaskReplaySteps(session, replayTimeline, knowledgeEvidence);
|
|
26
|
+
const notices = buildIntegrityNotices(taskWindow.scope, normalizedEvents, ingestion);
|
|
27
|
+
const userEvents = normalizedTimeline.filter((event) => event.kind === 'user_message');
|
|
28
|
+
const assistantEvents = normalizedTimeline.filter((event) => event.kind === 'assistant_message');
|
|
29
|
+
const observedModels = normalizedTimeline.reduce((models, event) => {
|
|
26
30
|
const eventModel = event.model?.trim();
|
|
27
31
|
if (eventModel && !models.includes(eventModel))
|
|
28
32
|
models.push(eventModel);
|
|
@@ -34,11 +38,11 @@ export function buildKnowledgeDebuggerViewModel(session, targetTurnId, ingestion
|
|
|
34
38
|
summary: {
|
|
35
39
|
userGoal: eventText(userEvents[0]),
|
|
36
40
|
finalResponse: eventText(assistantEvents.at(-1)),
|
|
37
|
-
observedStartTimestamp:
|
|
38
|
-
??
|
|
39
|
-
observedEndTimestamp: [...
|
|
40
|
-
toolCallCount:
|
|
41
|
-
toolFailureCount:
|
|
41
|
+
observedStartTimestamp: normalizedTimeline.find((event) => (event.timestamp && event.runtimeKind !== 'session_context'))?.timestamp
|
|
42
|
+
?? normalizedTimeline.find((event) => event.timestamp)?.timestamp,
|
|
43
|
+
observedEndTimestamp: [...normalizedTimeline].reverse().find((event) => event.timestamp)?.timestamp,
|
|
44
|
+
toolCallCount: normalizedEvents.filter((event) => event.kind === 'tool_use').length,
|
|
45
|
+
toolFailureCount: normalizedEvents.filter((event) => (event.kind === 'tool_result' && (event.toolStatus === 'failure' || event.isError))).length,
|
|
42
46
|
hasUserCorrection: steps.some((step) => step.stepKind === 'user_correction'),
|
|
43
47
|
observedModels,
|
|
44
48
|
},
|
|
@@ -199,7 +203,7 @@ function isKnowledgeReturningCall(toolName, callText) {
|
|
|
199
203
|
const command = sourceLocator(callText);
|
|
200
204
|
return command ? READ_ONLY_SHELL_RE.test(command) : false;
|
|
201
205
|
}
|
|
202
|
-
function buildIntegrityNotices(taskScope,
|
|
206
|
+
function buildIntegrityNotices(taskScope, normalizedEvents, ingestion) {
|
|
203
207
|
const notices = [];
|
|
204
208
|
if (taskScope.basis === 'unresolved') {
|
|
205
209
|
notices.push({ code: 'task_boundary_unavailable', count: 1 });
|
|
@@ -214,17 +218,40 @@ function buildIntegrityNotices(taskScope, timeline, steps, ingestion) {
|
|
|
214
218
|
notices.push({ code: 'ignored_values', count: ingestion.ignoredValueCount });
|
|
215
219
|
if (ingestion?.unknownEventCount)
|
|
216
220
|
notices.push({ code: 'unknown_events', count: ingestion.unknownEventCount });
|
|
217
|
-
const
|
|
218
|
-
if (
|
|
219
|
-
notices.push({ code: 'unmatched_tool_calls', count:
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
const missingTimestamps = timeline.filter((event) => !event.timestamp).length;
|
|
221
|
+
const unmatched = unmatchedToolEventCounts(normalizedEvents);
|
|
222
|
+
if (unmatched.calls > 0)
|
|
223
|
+
notices.push({ code: 'unmatched_tool_calls', count: unmatched.calls });
|
|
224
|
+
if (unmatched.results > 0)
|
|
225
|
+
notices.push({ code: 'unmatched_tool_results', count: unmatched.results });
|
|
226
|
+
const missingTimestamps = normalizedEvents.filter((event) => !event.timestamp).length;
|
|
224
227
|
if (missingTimestamps > 0)
|
|
225
228
|
notices.push({ code: 'missing_timestamps', count: missingTimestamps });
|
|
226
229
|
return notices;
|
|
227
230
|
}
|
|
231
|
+
function unmatchedToolEventCounts(events) {
|
|
232
|
+
const availableResults = new Map();
|
|
233
|
+
for (const event of events) {
|
|
234
|
+
if (event.kind !== 'tool_result')
|
|
235
|
+
continue;
|
|
236
|
+
const key = toolCorrelationKey(event);
|
|
237
|
+
availableResults.set(key, (availableResults.get(key) ?? 0) + 1);
|
|
238
|
+
}
|
|
239
|
+
let unmatchedCalls = 0;
|
|
240
|
+
for (const event of events) {
|
|
241
|
+
if (event.kind !== 'tool_use')
|
|
242
|
+
continue;
|
|
243
|
+
const key = toolCorrelationKey(event);
|
|
244
|
+
const remaining = availableResults.get(key) ?? 0;
|
|
245
|
+
if (remaining === 0) {
|
|
246
|
+
unmatchedCalls += 1;
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
availableResults.set(key, remaining - 1);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
const unmatchedResults = [...availableResults.values()].reduce((sum, count) => sum + count, 0);
|
|
253
|
+
return { calls: unmatchedCalls, results: unmatchedResults };
|
|
254
|
+
}
|
|
228
255
|
function correctionEventIds(session, timeline) {
|
|
229
256
|
const ids = new Set();
|
|
230
257
|
for (const episode of session.sessionStory?.episodes ?? []) {
|
|
@@ -235,9 +262,15 @@ function correctionEventIds(session, timeline) {
|
|
|
235
262
|
}
|
|
236
263
|
}
|
|
237
264
|
const userCorrectionCount = session.indicators?.userCorrectionCount ?? 0;
|
|
265
|
+
const lastAssistantOrder = Math.max(-1, ...timeline.filter((event) => event.kind === 'assistant_message').map((event) => event.order));
|
|
266
|
+
const explicitCandidates = timeline
|
|
267
|
+
.filter((event) => (event.kind === 'user_message'
|
|
268
|
+
&& event.order > lastAssistantOrder
|
|
269
|
+
&& hasUserCorrectionSignal(event.fullText ?? event.snippet ?? '')));
|
|
270
|
+
for (const event of explicitCandidates)
|
|
271
|
+
ids.add(event.id);
|
|
238
272
|
if (ids.size > 0 || userCorrectionCount === 0)
|
|
239
273
|
return ids;
|
|
240
|
-
const lastAssistantOrder = Math.max(-1, ...timeline.filter((event) => event.kind === 'assistant_message').map((event) => event.order));
|
|
241
274
|
const correctionCandidates = timeline
|
|
242
275
|
.filter((event) => event.kind === 'user_message' && event.order > lastAssistantOrder)
|
|
243
276
|
.slice(-userCorrectionCount);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ExperienceTimelineEvent } from '../types/index.js';
|
|
2
|
+
export interface TaskSemanticProjectionOptions {
|
|
3
|
+
preservePendingToolCalls?: boolean;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Bound one task's semantic projection without separating tool exchanges or
|
|
7
|
+
* blindly discarding the middle of a long task. Failures, task boundaries and
|
|
8
|
+
* the user/assistant endpoints are retained first; remaining capacity is
|
|
9
|
+
* distributed across the task so the preview still explains its progression.
|
|
10
|
+
*/
|
|
11
|
+
export declare function projectTaskSemanticEvents(events: ExperienceTimelineEvent[], limit: number, options?: TaskSemanticProjectionOptions): ExperienceTimelineEvent[];
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
const PRIORITY_BANDS = [100, 90, 80, 70, 60, 40, 20, 0];
|
|
2
|
+
/**
|
|
3
|
+
* Bound one task's semantic projection without separating tool exchanges or
|
|
4
|
+
* blindly discarding the middle of a long task. Failures, task boundaries and
|
|
5
|
+
* the user/assistant endpoints are retained first; remaining capacity is
|
|
6
|
+
* distributed across the task so the preview still explains its progression.
|
|
7
|
+
*/
|
|
8
|
+
export function projectTaskSemanticEvents(events, limit, options = {}) {
|
|
9
|
+
if (limit <= 0 || events.length === 0)
|
|
10
|
+
return [];
|
|
11
|
+
if (events.length <= limit)
|
|
12
|
+
return events;
|
|
13
|
+
const units = semanticUnits(events, options);
|
|
14
|
+
const selected = new Map();
|
|
15
|
+
let remaining = limit;
|
|
16
|
+
const required = units
|
|
17
|
+
.filter((unit) => unit.required)
|
|
18
|
+
.sort((left, right) => right.priority - left.priority || left.retentionOrder - right.retentionOrder);
|
|
19
|
+
for (const unit of required) {
|
|
20
|
+
if (unit.events.length > remaining)
|
|
21
|
+
continue;
|
|
22
|
+
selected.set(unit.id, unit);
|
|
23
|
+
remaining -= unit.events.length;
|
|
24
|
+
}
|
|
25
|
+
for (const priority of PRIORITY_BANDS) {
|
|
26
|
+
if (remaining <= 0)
|
|
27
|
+
break;
|
|
28
|
+
const candidates = units.filter((unit) => (!selected.has(unit.id) && unit.priority === priority));
|
|
29
|
+
if (candidates.length === 0)
|
|
30
|
+
continue;
|
|
31
|
+
const totalSize = candidates.reduce((sum, unit) => sum + unit.events.length, 0);
|
|
32
|
+
if (totalSize <= remaining) {
|
|
33
|
+
for (const unit of candidates)
|
|
34
|
+
selected.set(unit.id, unit);
|
|
35
|
+
remaining -= totalSize;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
remaining = selectSpreadCandidates(candidates, selected, units, remaining);
|
|
39
|
+
}
|
|
40
|
+
return [...selected.values()]
|
|
41
|
+
.flatMap((unit) => unit.events)
|
|
42
|
+
.sort((left, right) => left.order - right.order);
|
|
43
|
+
}
|
|
44
|
+
function semanticUnits(events, options) {
|
|
45
|
+
const resultByCall = new Map();
|
|
46
|
+
for (const event of events) {
|
|
47
|
+
if (event.kind !== 'tool_result')
|
|
48
|
+
continue;
|
|
49
|
+
const key = toolCorrelationKey(event);
|
|
50
|
+
resultByCall.set(key, [...(resultByCall.get(key) ?? []), event]);
|
|
51
|
+
}
|
|
52
|
+
const firstUserId = events.find((event) => event.kind === 'user_message')?.id;
|
|
53
|
+
const finalAssistantId = [...events].reverse()
|
|
54
|
+
.find((event) => event.kind === 'assistant_message')?.id;
|
|
55
|
+
const consumedResults = new Set();
|
|
56
|
+
const units = [];
|
|
57
|
+
for (const event of events) {
|
|
58
|
+
if (event.kind === 'tool_result' && consumedResults.has(event.id))
|
|
59
|
+
continue;
|
|
60
|
+
if (event.kind === 'tool_use') {
|
|
61
|
+
const result = resultByCall.get(toolCorrelationKey(event))
|
|
62
|
+
?.find((candidate) => !consumedResults.has(candidate.id));
|
|
63
|
+
if (result)
|
|
64
|
+
consumedResults.add(result.id);
|
|
65
|
+
const exchange = result ? [event, result] : [event];
|
|
66
|
+
const failed = exchange.some((candidate) => candidate.isError || candidate.toolStatus === 'failure');
|
|
67
|
+
const pending = !result && options.preservePendingToolCalls === true;
|
|
68
|
+
units.push({
|
|
69
|
+
id: `tool:${event.id}`,
|
|
70
|
+
events: exchange,
|
|
71
|
+
order: event.order,
|
|
72
|
+
priority: failed ? 135 : pending ? 140 : 70,
|
|
73
|
+
required: failed || pending,
|
|
74
|
+
retentionOrder: pending ? -event.order : event.order,
|
|
75
|
+
});
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const isFirstUser = event.id === firstUserId;
|
|
79
|
+
const isFinalAssistant = event.id === finalAssistantId;
|
|
80
|
+
const isBoundary = event.kind === 'lifecycle' && isBoundaryLifecycle(event);
|
|
81
|
+
const isFailedResult = event.kind === 'tool_result'
|
|
82
|
+
&& (event.isError || event.toolStatus === 'failure');
|
|
83
|
+
const required = isFirstUser
|
|
84
|
+
|| event.id === finalAssistantId
|
|
85
|
+
|| isFailedResult
|
|
86
|
+
|| isBoundary;
|
|
87
|
+
units.push({
|
|
88
|
+
id: `event:${event.id}`,
|
|
89
|
+
events: [event],
|
|
90
|
+
order: event.order,
|
|
91
|
+
priority: requiredPriority(event, {
|
|
92
|
+
isFirstUser,
|
|
93
|
+
isFinalAssistant,
|
|
94
|
+
isBoundary,
|
|
95
|
+
isFailedResult,
|
|
96
|
+
}),
|
|
97
|
+
required,
|
|
98
|
+
retentionOrder: event.order,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return units;
|
|
102
|
+
}
|
|
103
|
+
function requiredPriority(event, flags) {
|
|
104
|
+
if (flags.isFirstUser)
|
|
105
|
+
return 160;
|
|
106
|
+
if (flags.isFinalAssistant)
|
|
107
|
+
return 150;
|
|
108
|
+
if (flags.isBoundary && event.label === 'turn_started')
|
|
109
|
+
return 145;
|
|
110
|
+
if (flags.isFailedResult)
|
|
111
|
+
return 135;
|
|
112
|
+
if (flags.isBoundary)
|
|
113
|
+
return 130;
|
|
114
|
+
return semanticPriority(event);
|
|
115
|
+
}
|
|
116
|
+
function semanticPriority(event) {
|
|
117
|
+
if (event.kind === 'user_message' || event.kind === 'assistant_message')
|
|
118
|
+
return 90;
|
|
119
|
+
if (event.kind === 'skill_context')
|
|
120
|
+
return 80;
|
|
121
|
+
if (event.kind === 'runtime_context')
|
|
122
|
+
return event.runtimeKind === 'usage' ? 0 : 80;
|
|
123
|
+
if (event.kind === 'lifecycle')
|
|
124
|
+
return 70;
|
|
125
|
+
if (event.kind === 'tool_result')
|
|
126
|
+
return event.isError || event.toolStatus === 'failure' ? 100 : 70;
|
|
127
|
+
if (event.kind === 'observation' || event.kind === 'agent_activity')
|
|
128
|
+
return 60;
|
|
129
|
+
if (event.kind === 'model_activity')
|
|
130
|
+
return event.contentVisibility === 'plaintext' ? 40 : 20;
|
|
131
|
+
return 0;
|
|
132
|
+
}
|
|
133
|
+
function isBoundaryLifecycle(event) {
|
|
134
|
+
const label = `${event.label ?? ''} ${event.sourceType ?? ''}`.toLowerCase();
|
|
135
|
+
return /(?:turn|task)[_-]?(?:start|complete|abort|interrupt|end)/.test(label);
|
|
136
|
+
}
|
|
137
|
+
function selectSpreadCandidates(candidates, selected, allUnits, capacity) {
|
|
138
|
+
const selectedOrders = [...selected.values()].map((unit) => unit.order);
|
|
139
|
+
const firstOrder = allUnits[0]?.order ?? 0;
|
|
140
|
+
const lastOrder = allUnits.at(-1)?.order ?? firstOrder;
|
|
141
|
+
const distances = new Map(candidates.map((candidate) => [
|
|
142
|
+
candidate.id,
|
|
143
|
+
selectedOrders.length > 0
|
|
144
|
+
? Math.min(...selectedOrders.map((order) => Math.abs(candidate.order - order)))
|
|
145
|
+
: Math.min(Math.abs(candidate.order - firstOrder), Math.abs(lastOrder - candidate.order)),
|
|
146
|
+
]));
|
|
147
|
+
let remaining = capacity;
|
|
148
|
+
while (remaining > 0) {
|
|
149
|
+
const fitting = candidates.filter((unit) => (!selected.has(unit.id) && unit.events.length <= remaining));
|
|
150
|
+
if (fitting.length === 0)
|
|
151
|
+
break;
|
|
152
|
+
const next = fitting.reduce((best, candidate) => {
|
|
153
|
+
const candidateDistance = distances.get(candidate.id) ?? 0;
|
|
154
|
+
const bestDistance = distances.get(best.id) ?? 0;
|
|
155
|
+
return candidateDistance > bestDistance
|
|
156
|
+
|| (candidateDistance === bestDistance && candidate.order < best.order)
|
|
157
|
+
? candidate
|
|
158
|
+
: best;
|
|
159
|
+
});
|
|
160
|
+
selected.set(next.id, next);
|
|
161
|
+
remaining -= next.events.length;
|
|
162
|
+
for (const candidate of candidates) {
|
|
163
|
+
if (selected.has(candidate.id))
|
|
164
|
+
continue;
|
|
165
|
+
distances.set(candidate.id, Math.min(distances.get(candidate.id) ?? Number.POSITIVE_INFINITY, Math.abs(candidate.order - next.order)));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return remaining;
|
|
169
|
+
}
|
|
170
|
+
function toolCorrelationKey(event) {
|
|
171
|
+
return event.callInstanceId ?? event.toolUseId ?? event.id;
|
|
172
|
+
}
|
|
@@ -3,6 +3,7 @@ export declare const TASK_SEMANTIC_EVENT_LIMIT = 240;
|
|
|
3
3
|
export interface ResolvedTaskWindow {
|
|
4
4
|
events: ExperienceTimelineEvent[];
|
|
5
5
|
semanticEvents: ExperienceTimelineEvent[];
|
|
6
|
+
relatedEvents: ExperienceTimelineEvent[];
|
|
6
7
|
scope: TaskWindowScope;
|
|
7
8
|
}
|
|
8
9
|
export declare function resolveTaskWindow(session: TaskTrajectorySession, targetTurnId: string, semanticLimit?: number): ResolvedTaskWindow;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { hasExplicitFollowUpCorrectionSignal } from './feedback-matchers.js';
|
|
2
|
+
import { projectTaskSemanticEvents } from './task-semantic-projection.js';
|
|
1
3
|
export const TASK_SEMANTIC_EVENT_LIMIT = 240;
|
|
2
4
|
export function resolveTaskWindow(session, targetTurnId, semanticLimit = TASK_SEMANTIC_EVENT_LIMIT) {
|
|
3
5
|
const events = session.fullSessionTimeline;
|
|
@@ -11,11 +13,17 @@ export function resolveTaskWindow(session, targetTurnId, semanticLimit = TASK_SE
|
|
|
11
13
|
return event ? [event] : [];
|
|
12
14
|
})
|
|
13
15
|
: [];
|
|
14
|
-
const semanticEvents =
|
|
16
|
+
const semanticEvents = projectTaskSemanticEvents(taskEvents, semanticLimit, {
|
|
17
|
+
preservePendingToolCalls: selected?.status === 'open',
|
|
18
|
+
});
|
|
19
|
+
const relatedEvents = selected
|
|
20
|
+
? relatedCorrectionEvents(session, selected.turnId, eventById)
|
|
21
|
+
: [];
|
|
15
22
|
const matchedAttributedEventCount = taskEvents.filter((event) => attributedIds.has(event.id)).length;
|
|
16
23
|
return {
|
|
17
24
|
events: taskEvents,
|
|
18
25
|
semanticEvents,
|
|
26
|
+
relatedEvents,
|
|
19
27
|
scope: {
|
|
20
28
|
basis: selected?.boundaryBasis ?? 'unresolved',
|
|
21
29
|
turnId: selected?.turnId,
|
|
@@ -27,10 +35,23 @@ export function resolveTaskWindow(session, targetTurnId, semanticLimit = TASK_SE
|
|
|
27
35
|
},
|
|
28
36
|
};
|
|
29
37
|
}
|
|
30
|
-
function
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
38
|
+
function relatedCorrectionEvents(session, selectedTurnId, eventById) {
|
|
39
|
+
const selectedIndex = session.turns.findIndex((turn) => turn.turnId === selectedTurnId);
|
|
40
|
+
const selectedTurn = selectedIndex >= 0 ? session.turns[selectedIndex] : undefined;
|
|
41
|
+
if (!selectedTurn)
|
|
42
|
+
return [];
|
|
43
|
+
const selectedTraceKey = turnTraceKey(selectedTurn);
|
|
44
|
+
const nextTurn = session.turns.slice(selectedIndex + 1).find((turn) => (turnTraceKey(turn) === selectedTraceKey));
|
|
45
|
+
if (!nextTurn)
|
|
46
|
+
return [];
|
|
47
|
+
const firstHumanMessage = nextTurn.eventIds
|
|
48
|
+
.map((id) => eventById.get(id))
|
|
49
|
+
.find((event) => event?.kind === 'user_message' && event.role === 'user');
|
|
50
|
+
if (!firstHumanMessage)
|
|
51
|
+
return [];
|
|
52
|
+
const text = firstHumanMessage.fullText ?? firstHumanMessage.snippet ?? '';
|
|
53
|
+
return hasExplicitFollowUpCorrectionSignal(text) ? [firstHumanMessage] : [];
|
|
54
|
+
}
|
|
55
|
+
function turnTraceKey(turn) {
|
|
56
|
+
return turn.traceId?.trim() || turn.sourceTrace;
|
|
36
57
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { inlineMarkdownText, renderSafeInlineMarkdown } from './inline-markdown.js';
|
|
2
2
|
import { icon } from './icons.js';
|
|
3
3
|
import { DEFAULT_LANG, e, layout } from './layout.js';
|
|
4
|
+
import { primaryTrajectoryEvidenceRef, trajectoryEvidenceRef, } from './trajectory-evidence.js';
|
|
4
5
|
import { renderTrajectoryLiveClientSource } from './trajectory-live.js';
|
|
5
6
|
import { renderTrajectoryRoutingClientSource } from './trajectory-routing.js';
|
|
6
7
|
const ACCESS_LABELS = {
|
|
@@ -61,6 +62,16 @@ const LANE_LABELS = {
|
|
|
61
62
|
en: { title: 'Results', detail: 'Tool returns and call status', empty: 'No tool results observed' },
|
|
62
63
|
},
|
|
63
64
|
};
|
|
65
|
+
const SOURCE_RECORD_PARTIAL_LABELS = {
|
|
66
|
+
zh: {
|
|
67
|
+
records: '原始日志归档不完整:已保留 {retained} 条,省略 {omitted} 条。',
|
|
68
|
+
content: '部分原始日志内容已按归档上限截断。',
|
|
69
|
+
},
|
|
70
|
+
en: {
|
|
71
|
+
records: 'The raw-log archive is partial: {retained} retained, {omitted} omitted.',
|
|
72
|
+
content: 'Some raw-log content was truncated by the archive limit.',
|
|
73
|
+
},
|
|
74
|
+
};
|
|
64
75
|
export function renderKnowledgeDebuggerPage(model, lang = DEFAULT_LANG, options = {}) {
|
|
65
76
|
const zh = lang === 'zh';
|
|
66
77
|
const projection = projectReplay(model, lang, { pendingToolResults: Boolean(options.live) });
|
|
@@ -87,8 +98,8 @@ export function renderKnowledgeDebuggerPage(model, lang = DEFAULT_LANG, options
|
|
|
87
98
|
.app-main{width:100%;height:100%;min-height:0;max-width:none;margin:0;padding:10px 18px 14px;overflow:hidden}
|
|
88
99
|
.footer{display:none!important}
|
|
89
100
|
.trajectory-shell{height:100%;min-height:0;display:flex;flex-direction:column;margin:0;padding:0;letter-spacing:0}
|
|
90
|
-
.trajectory-mode{display:inline-flex;padding:2px;border:1px solid var(--border);border-radius:7px;background:var(--bg-elevated)}
|
|
91
|
-
.trajectory-mode button{height:28px;padding:3px 11px;border:0;border-radius:5px;background:transparent;color:var(--text-secondary);font:500 12px/1.4 inherit;letter-spacing:0;cursor:pointer}
|
|
101
|
+
.trajectory-mode{display:inline-flex;flex:none;padding:2px;border:1px solid var(--border);border-radius:7px;background:var(--bg-elevated)}
|
|
102
|
+
.trajectory-mode button{height:28px;padding:3px 11px;border:0;border-radius:5px;background:transparent;color:var(--text-secondary);font:500 12px/1.4 inherit;letter-spacing:0;white-space:nowrap;cursor:pointer}
|
|
92
103
|
.trajectory-mode button[aria-pressed="true"]{background:var(--text-primary);color:var(--bg-surface);box-shadow:0 1px 2px rgba(24,32,51,.14)}
|
|
93
104
|
.trajectory-mode button:focus-visible,.trajectory-event:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
|
|
94
105
|
.trajectory-heading{flex:none;display:grid;grid-template-columns:minmax(0,1fr) auto;gap:24px;align-items:center;min-height:32px;margin:0 0 8px}
|
|
@@ -103,7 +114,7 @@ export function renderKnowledgeDebuggerPage(model, lang = DEFAULT_LANG, options
|
|
|
103
114
|
.trajectory-warning strong{color:var(--text-primary)}
|
|
104
115
|
.trajectory-frame{flex:1;min-height:0;overflow:hidden;display:grid;grid-template-rows:40px minmax(0,1fr);border:1px solid var(--border);border-radius:8px;background:var(--bg-surface);box-shadow:var(--shadow-sm)}
|
|
105
116
|
.trajectory-frame-head{display:flex;align-items:center;min-height:0;padding:0 10px 0 14px;border-bottom:1px solid var(--border)}
|
|
106
|
-
.trajectory-frame-head h2{margin:0;font-size:13px;font-weight:600;letter-spacing:0}
|
|
117
|
+
.trajectory-frame-head h2{flex:none;margin:0;font-size:13px;font-weight:600;letter-spacing:0;white-space:nowrap}
|
|
107
118
|
.trajectory-live-controls{display:inline-flex;align-items:center;gap:3px;margin-left:8px}
|
|
108
119
|
.trajectory-live-state{display:inline-flex;align-items:center;gap:5px;color:var(--text-muted);font-size:9px;white-space:nowrap}
|
|
109
120
|
.trajectory-live-state:before{content:"";width:5px;height:5px;border-radius:50%;background:var(--green);box-shadow:0 0 0 3px rgba(31,157,99,.1)}
|
|
@@ -239,6 +250,11 @@ export function renderKnowledgeDebuggerPage(model, lang = DEFAULT_LANG, options
|
|
|
239
250
|
.trajectory-operation-copy p{display:-webkit-box;margin:0;overflow:hidden;color:var(--text-secondary);font-size:11px;line-height:1.45;-webkit-box-orient:vertical;-webkit-line-clamp:2}
|
|
240
251
|
.trajectory-operation-actions{display:flex;align-items:center;gap:6px;padding-top:0}
|
|
241
252
|
.trajectory-evidence-count{color:var(--text-secondary);font-size:10px;white-space:nowrap}
|
|
253
|
+
.trajectory-evidence-jump,.trajectory-field-evidence{border:0;border-radius:4px;background:transparent;color:var(--accent);font-size:9px;font-weight:600;white-space:nowrap;cursor:pointer}
|
|
254
|
+
.trajectory-evidence-jump{padding:4px 6px}
|
|
255
|
+
.trajectory-field-evidence{padding:3px 5px}
|
|
256
|
+
.trajectory-evidence-jump:hover,.trajectory-field-evidence:hover{background:var(--bg-elevated);color:var(--text-primary)}
|
|
257
|
+
.trajectory-evidence-jump:focus-visible,.trajectory-field-evidence:focus-visible{outline:2px solid var(--accent);outline-offset:1px}
|
|
242
258
|
.trajectory-inspector-close{display:grid;width:24px;height:24px;place-items:center;border:0;border-radius:4px;background:transparent;color:var(--text-muted);cursor:pointer}
|
|
243
259
|
.trajectory-inspector-close:hover{background:var(--bg-elevated);color:var(--text-primary)}
|
|
244
260
|
.trajectory-inspector-close:focus-visible{outline:2px solid var(--accent);outline-offset:1px}
|
|
@@ -274,6 +290,7 @@ export function renderKnowledgeDebuggerPage(model, lang = DEFAULT_LANG, options
|
|
|
274
290
|
.trajectory-raw-head,.trajectory-raw-row summary{display:grid;grid-template-columns:76px 126px minmax(240px,1fr) 138px;gap:12px;align-items:center}
|
|
275
291
|
.trajectory-raw-head{position:sticky;top:0;z-index:3;min-height:32px;padding:0 14px;border-bottom:1px solid var(--border);background:var(--bg-elevated);color:var(--text-muted);font-size:9px;font-weight:600}
|
|
276
292
|
.trajectory-raw-row{border-bottom:1px solid var(--border)}
|
|
293
|
+
.trajectory-raw-row.is-evidence-target{background:rgba(79,70,229,.07);box-shadow:inset 3px 0 0 var(--accent)}
|
|
277
294
|
.trajectory-raw-row[hidden]{display:none}
|
|
278
295
|
.trajectory-raw-row summary{min-height:46px;padding:7px 14px;color:var(--text-primary);cursor:pointer;list-style:none}
|
|
279
296
|
.trajectory-raw-row summary::-webkit-details-marker{display:none}
|
|
@@ -291,7 +308,7 @@ export function renderKnowledgeDebuggerPage(model, lang = DEFAULT_LANG, options
|
|
|
291
308
|
@media(max-width:1100px){.trajectory-shell[data-inspector-open="true"] .trajectory-body{grid-template-columns:minmax(0,1fr) 320px}.trajectory-canvas{--lane-label-width:96px;--event-width:148px}.trajectory-lane-name{padding-inline:10px}.trajectory-lane-name span{font-size:9px}}
|
|
292
309
|
@media(max-width:1080px){.trajectory-meta-time{display:none!important}}
|
|
293
310
|
@media(max-width:860px){.app-main{padding-inline:10px}.trajectory-heading{grid-template-columns:1fr;gap:0}.trajectory-meta{display:none}.trajectory-shell[data-inspector-open="true"] .trajectory-body{grid-template-columns:1fr;grid-template-rows:minmax(0,3fr) minmax(160px,2fr)}.trajectory-inspector{border-top:1px solid var(--border);border-left:0}.trajectory-operation-head{padding-block:9px}}
|
|
294
|
-
@media(max-width:600px){.app-bar{padding-inline:10px}.app-brand-tag{display:none}.trajectory-heading h1{font-size:17px}.trajectory-range{display:none}.trajectory-frame-head .trajectory-mode{margin-left:
|
|
311
|
+
@media(max-width:600px){.app-bar{padding-inline:10px}.app-brand-tag{display:none}.trajectory-heading h1{font-size:17px}.trajectory-frame-head{gap:2px;padding-inline:8px}.trajectory-frame-space{min-width:0}.trajectory-range{display:none}.trajectory-live-controls{margin-left:3px}.trajectory-live-state{gap:0;font-size:0}.trajectory-live-follow{width:22px;padding:0;justify-content:center}.trajectory-live-follow span{display:none}.trajectory-boundary-info{margin-left:1px}.trajectory-focus{margin-left:2px}.trajectory-frame-head .trajectory-mode{margin-left:2px}.trajectory-mode button{min-width:37px;padding-inline:6px;font-size:0}.trajectory-mode button:after{content:attr(data-short-label);font-size:10px}.trajectory-canvas{--lane-label-width:76px;--event-width:126px}.trajectory-lane-name{padding-inline:8px}.trajectory-lane-name span{display:none}.trajectory-event-time{display:none}.trajectory-raw-head,.trajectory-raw-row summary{grid-template-columns:66px 94px minmax(160px,1fr)}.trajectory-raw-id{display:none}.trajectory-raw-row pre{padding-left:14px}}
|
|
295
312
|
@media(prefers-reduced-motion:reduce){.trajectory-body{transition:none}}
|
|
296
313
|
</style>
|
|
297
314
|
<main class="trajectory-shell" data-mode="semantic"${options.live ? ` data-live-endpoint="${e(options.live.endpoint)}" data-live-revision="${e(options.live.revision)}"` : ''}>
|
|
@@ -323,9 +340,9 @@ export function renderKnowledgeDebuggerPage(model, lang = DEFAULT_LANG, options
|
|
|
323
340
|
<span class="trajectory-range">${e(formatRelativeTime(0))} — ${e(formatRelativeTime(projection.durationMs))}</span>
|
|
324
341
|
${projection.facets.length > 0 ? renderFacetFocus(projection.facets, lang) : ''}
|
|
325
342
|
<div class="trajectory-mode" aria-label="${zh ? '查看模式' : 'View mode'}">
|
|
326
|
-
<button type="button" data-trajectory-mode="semantic" aria-pressed="true">${zh ? '语义轨迹' : 'Semantic trajectory'}</button>
|
|
327
|
-
<button type="button" data-trajectory-mode="normalized" aria-pressed="false">${zh ? '规范化事件' : 'Normalized events'}</button>
|
|
328
|
-
<button type="button" data-trajectory-mode="source" aria-pressed="false">${zh ? '原始日志' : 'Raw logs'}</button>
|
|
343
|
+
<button type="button" data-trajectory-mode="semantic" data-short-label="${zh ? '轨迹' : 'Track'}" aria-pressed="true">${zh ? '语义轨迹' : 'Semantic trajectory'}</button>
|
|
344
|
+
<button type="button" data-trajectory-mode="normalized" data-short-label="${zh ? '事件' : 'Events'}" aria-pressed="false">${zh ? '规范化事件' : 'Normalized events'}</button>
|
|
345
|
+
<button type="button" data-trajectory-mode="source" data-short-label="${zh ? '日志' : 'Logs'}" aria-pressed="false">${zh ? '原始日志' : 'Raw logs'}</button>
|
|
329
346
|
</div>
|
|
330
347
|
</header>
|
|
331
348
|
|
|
@@ -348,7 +365,7 @@ export function renderKnowledgeDebuggerPage(model, lang = DEFAULT_LANG, options
|
|
|
348
365
|
<header class="trajectory-operation-head">
|
|
349
366
|
<div class="trajectory-operation-type" id="trajectory-operation-type"></div>
|
|
350
367
|
<div class="trajectory-operation-copy"><h3 id="trajectory-operation-title"></h3><p id="trajectory-operation-summary"></p></div>
|
|
351
|
-
<div class="trajectory-operation-actions"><div class="trajectory-evidence-count" id="trajectory-evidence-count"></div><button class="trajectory-inspector-close" type="button" data-inspector-close aria-label="${zh ? '关闭详情' : 'Close details'}" title="${zh ? '关闭详情' : 'Close details'}"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 6l12 12"></path><path d="M18 6L6 18"></path></svg></button></div>
|
|
368
|
+
<div class="trajectory-operation-actions"><div class="trajectory-evidence-count" id="trajectory-evidence-count"></div><button class="trajectory-evidence-jump" type="button" data-operation-evidence hidden></button><button class="trajectory-inspector-close" type="button" data-inspector-close aria-label="${zh ? '关闭详情' : 'Close details'}" title="${zh ? '关闭详情' : 'Close details'}"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 6l12 12"></path><path d="M18 6L6 18"></path></svg></button></div>
|
|
352
369
|
</header>
|
|
353
370
|
<div class="trajectory-semantic-panels">${projection.operations.map((operation) => renderSemanticPanel(operation, false, lang)).join('')}</div>
|
|
354
371
|
</aside>
|
|
@@ -367,7 +384,7 @@ export function renderKnowledgeDebuggerPage(model, lang = DEFAULT_LANG, options
|
|
|
367
384
|
const cards = Array.from(document.querySelectorAll('[data-trajectory-operation]'));
|
|
368
385
|
const normalizedRows = Array.from(document.querySelectorAll('[data-trajectory-normalized-event]'));
|
|
369
386
|
const normalizedEmpty = document.querySelector('[data-trajectory-normalized-empty]');
|
|
370
|
-
const sourceRecordList = document.querySelector('[data-source
|
|
387
|
+
const sourceRecordList = document.querySelector('[data-event-view="source"]');
|
|
371
388
|
const semanticPanels = Array.from(document.querySelectorAll('[data-trajectory-semantic-panel]'));
|
|
372
389
|
const semanticPanelContainer = document.querySelector('.trajectory-semantic-panels');
|
|
373
390
|
const detailBlocks = Array.from(document.querySelectorAll('[data-field-detail-block]'));
|
|
@@ -375,6 +392,7 @@ export function renderKnowledgeDebuggerPage(model, lang = DEFAULT_LANG, options
|
|
|
375
392
|
const operationTitle = document.querySelector('#trajectory-operation-title');
|
|
376
393
|
const operationSummary = document.querySelector('#trajectory-operation-summary');
|
|
377
394
|
const evidenceCount = document.querySelector('#trajectory-evidence-count');
|
|
395
|
+
const operationEvidence = document.querySelector('[data-operation-evidence]');
|
|
378
396
|
const lanes = document.querySelector('.trajectory-lanes');
|
|
379
397
|
const operationLinks = document.querySelector('.trajectory-links');
|
|
380
398
|
const facetFocus = document.querySelector('.trajectory-focus');
|
|
@@ -398,7 +416,10 @@ export function renderKnowledgeDebuggerPage(model, lang = DEFAULT_LANG, options
|
|
|
398
416
|
redacted: zh ? '【不透明加密载荷已省略】' : '[Opaque encrypted payload omitted]',
|
|
399
417
|
truncated: zh ? '【该记录已按归档上限截断】' : '[Record truncated by archive limit]',
|
|
400
418
|
loadFailed: zh ? '原始日志读取失败。可以切换视图后重试。' : 'Raw logs could not be loaded. Switch views and retry.',
|
|
401
|
-
|
|
419
|
+
partialRecords: SOURCE_RECORD_PARTIAL_LABELS[lang].records,
|
|
420
|
+
partialContent: SOURCE_RECORD_PARTIAL_LABELS[lang].content,
|
|
421
|
+
viewSource: zh ? '查看原始日志' : 'View raw log',
|
|
422
|
+
viewNormalized: zh ? '查看规范化事件' : 'View normalized event',
|
|
402
423
|
})};
|
|
403
424
|
const cardLane = (card) => card.closest('.trajectory-lane')?.dataset.lane || '';
|
|
404
425
|
const operationIds = semanticPanels.map((panel) => panel.dataset.trajectorySemanticPanel).filter(Boolean);
|
|
@@ -804,6 +825,16 @@ export function renderKnowledgeDebuggerPage(model, lang = DEFAULT_LANG, options
|
|
|
804
825
|
if (operationTitle) operationTitle.textContent = panel.dataset.title || '—';
|
|
805
826
|
if (operationSummary) operationSummary.textContent = panel.dataset.summary || '';
|
|
806
827
|
if (evidenceCount) evidenceCount.textContent = panel.dataset.evidenceLabel || '';
|
|
828
|
+
if (operationEvidence) {
|
|
829
|
+
const sourceEventId = panel.dataset.evidenceSourceEventId || '';
|
|
830
|
+
operationEvidence.hidden = !sourceEventId;
|
|
831
|
+
operationEvidence.dataset.evidenceSourceEventId = sourceEventId;
|
|
832
|
+
operationEvidence.dataset.evidenceSourceLineIndex = panel.dataset.evidenceSourceLineIndex || '';
|
|
833
|
+
operationEvidence.dataset.evidenceTraceId = panel.dataset.evidenceTraceId || '';
|
|
834
|
+
operationEvidence.textContent = panel.dataset.evidenceSourceLineIndex
|
|
835
|
+
? sourceRecordLabels.viewSource
|
|
836
|
+
: sourceRecordLabels.viewNormalized;
|
|
837
|
+
}
|
|
807
838
|
currentOperationId = id;
|
|
808
839
|
if (animateLayout && shell.dataset.inspectorOpen !== 'true') beginLayoutTransition();
|
|
809
840
|
shell.dataset.inspectorOpen = 'true';
|
|
@@ -820,6 +851,7 @@ export function renderKnowledgeDebuggerPage(model, lang = DEFAULT_LANG, options
|
|
|
820
851
|
});
|
|
821
852
|
semanticPanels.forEach((item) => { item.hidden = true; });
|
|
822
853
|
currentOperationId = '';
|
|
854
|
+
if (operationEvidence) operationEvidence.hidden = true;
|
|
823
855
|
if (inspectorWasOpen) beginLayoutTransition();
|
|
824
856
|
delete shell.dataset.inspectorOpen;
|
|
825
857
|
if (inspector) inspector.hidden = true;
|
|
@@ -885,6 +917,8 @@ export function renderKnowledgeDebuggerPage(model, lang = DEFAULT_LANG, options
|
|
|
885
917
|
if (!sourceRecordList) return;
|
|
886
918
|
const details = document.createElement('details');
|
|
887
919
|
details.className = 'trajectory-raw-row';
|
|
920
|
+
details.dataset.sourceLineIndex = String(record.sourceIndex ?? '');
|
|
921
|
+
details.dataset.sourceTraceId = String(record.traceId || '');
|
|
888
922
|
const summary = document.createElement('summary');
|
|
889
923
|
const time = document.createElement('time');
|
|
890
924
|
time.textContent = relativeSourceTime(record.timestamp);
|
|
@@ -919,9 +953,12 @@ export function renderKnowledgeDebuggerPage(model, lang = DEFAULT_LANG, options
|
|
|
919
953
|
if (archive.status === 'partial') {
|
|
920
954
|
const notice = document.createElement('div');
|
|
921
955
|
notice.className = 'trajectory-record-notice';
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
956
|
+
const omittedRecordCount = Number(archive.omittedRecordCount ?? 0);
|
|
957
|
+
notice.textContent = omittedRecordCount > 0
|
|
958
|
+
? sourceRecordLabels.partialRecords
|
|
959
|
+
.replace('{retained}', String(archive.recordCount ?? archive.records?.length ?? 0))
|
|
960
|
+
.replace('{omitted}', String(omittedRecordCount))
|
|
961
|
+
: sourceRecordLabels.partialContent;
|
|
925
962
|
sourceRecordList.append(notice);
|
|
926
963
|
}
|
|
927
964
|
(Array.isArray(archive.records) ? archive.records : []).forEach(appendSourceRecord);
|
|
@@ -941,6 +978,40 @@ export function renderKnowledgeDebuggerPage(model, lang = DEFAULT_LANG, options
|
|
|
941
978
|
if (mode === 'source') void loadSourceRecords();
|
|
942
979
|
document.querySelectorAll('[data-trajectory-mode]').forEach((item) => item.setAttribute('aria-pressed', String(item.dataset.trajectoryMode === mode)));
|
|
943
980
|
};
|
|
981
|
+
const clearEvidenceTarget = () => {
|
|
982
|
+
document.querySelectorAll('.trajectory-raw-row.is-evidence-target').forEach((row) => row.classList.remove('is-evidence-target'));
|
|
983
|
+
};
|
|
984
|
+
const revealEvidenceRow = (row) => {
|
|
985
|
+
if (!row) return false;
|
|
986
|
+
clearEvidenceTarget();
|
|
987
|
+
row.open = true;
|
|
988
|
+
row.classList.add('is-evidence-target');
|
|
989
|
+
requestAnimationFrame(() => row.scrollIntoView({
|
|
990
|
+
behavior: window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth',
|
|
991
|
+
block: 'center',
|
|
992
|
+
}));
|
|
993
|
+
return true;
|
|
994
|
+
};
|
|
995
|
+
const matchingSourceRow = (lineIndex, traceId) => Array.from(sourceRecordList?.querySelectorAll('[data-source-line-index]') || [])
|
|
996
|
+
.find((row) => row.dataset.sourceLineIndex === lineIndex
|
|
997
|
+
&& (!traceId || row.dataset.sourceTraceId === traceId));
|
|
998
|
+
const revealEvidence = async (target) => {
|
|
999
|
+
const sourceEventId = target.dataset.evidenceSourceEventId || target.dataset.detailSourceEventId || '';
|
|
1000
|
+
const sourceLineIndex = target.dataset.evidenceSourceLineIndex || target.dataset.detailSourceLineIndex || '';
|
|
1001
|
+
const traceId = target.dataset.evidenceTraceId || target.dataset.detailTraceId || '';
|
|
1002
|
+
if (sourceLineIndex && sourceRecordList) {
|
|
1003
|
+
setTrajectoryMode('source');
|
|
1004
|
+
await loadSourceRecords();
|
|
1005
|
+
if (revealEvidenceRow(matchingSourceRow(sourceLineIndex, traceId))) return;
|
|
1006
|
+
}
|
|
1007
|
+
if (!sourceEventId) return;
|
|
1008
|
+
setTrajectoryMode('normalized');
|
|
1009
|
+
revealEvidenceRow(normalizedRows.find((row) => row.dataset.trajectoryNormalizedEvent === sourceEventId));
|
|
1010
|
+
};
|
|
1011
|
+
operationEvidence?.addEventListener('click', () => void revealEvidence(operationEvidence), { signal: pageLifecycle.signal });
|
|
1012
|
+
document.querySelectorAll('[data-view-evidence]').forEach((button) => {
|
|
1013
|
+
button.addEventListener('click', () => void revealEvidence(button.closest('[data-field-detail-block]') || button), { signal: pageLifecycle.signal });
|
|
1014
|
+
});
|
|
944
1015
|
document.querySelectorAll('[data-trajectory-mode]').forEach((button) => {
|
|
945
1016
|
button.addEventListener('click', () => {
|
|
946
1017
|
const mode = button.dataset.trajectoryMode || 'semantic';
|
|
@@ -1194,8 +1265,8 @@ function projectReplay(model, lang, options) {
|
|
|
1194
1265
|
: (zh ? '已观测到工具调用,但当前 trace 中没有匹配到返回结果。' : 'A tool call was observed, but no matching result appears in the trace.'),
|
|
1195
1266
|
evidenceLabel: zh ? `${step.events.length + evidence.length} 条关联证据` : `${step.events.length + evidence.length} related records`,
|
|
1196
1267
|
fields: [
|
|
1197
|
-
{ label: zh ? '执行' : 'Action', value: `${call?.toolName ?? step.title} · ${inferToolActionLabel(evidence, lang)}`, detail: compactText(input || (zh ? '未记录输入' : 'Input not recorded'), 520), detailKind: 'content' },
|
|
1198
|
-
{ label: zh ? '结果' : 'Result', value: `${toolStatusLabel(resultState, lang)}${duration ? ` · ${duration}` : ''}`, detail: result ? eventPreview(result, zh ? '工具没有返回内容。' : 'The tool returned no content.') : resultState === 'pending' ? (zh ? '正在等待工具结果写入 trace。' : 'Waiting for the tool result to appear in the trace.') : (zh ? '当前 trace 中没有匹配到工具结果。' : 'No matching tool result in this trace.'),
|
|
1268
|
+
{ label: zh ? '执行' : 'Action', value: `${call?.toolName ?? step.title} · ${inferToolActionLabel(evidence, lang)}`, detail: compactText(input || (zh ? '未记录输入' : 'Input not recorded'), 520), evidence: trajectoryEvidenceRef(call), detailKind: 'content' },
|
|
1269
|
+
{ label: zh ? '结果' : 'Result', value: `${toolStatusLabel(resultState, lang)}${duration ? ` · ${duration}` : ''}`, detail: result ? eventPreview(result, zh ? '工具没有返回内容。' : 'The tool returned no content.') : resultState === 'pending' ? (zh ? '正在等待工具结果写入 trace。' : 'Waiting for the tool result to appear in the trace.') : (zh ? '当前 trace 中没有匹配到工具结果。' : 'No matching tool result in this trace.'), evidence: trajectoryEvidenceRef(result), detailKind: 'result' },
|
|
1199
1270
|
{ label: 'Knowledge', value: evidence.length > 0 ? evidence.map((item) => `${knowledgeKindLabel(item, lang)} · ${item.label}`).join('、') : (zh ? '未关联' : 'Not associated'), detail: evidence.length > 0 ? evidence.map((item) => `${item.accessKind} · ${shortHash(item.contentHash)}`).join('\n') : (zh ? '未从本次工具交换投影出 Knowledge' : 'No Knowledge projected from this tool exchange') },
|
|
1200
1271
|
],
|
|
1201
1272
|
events: step.events,
|
|
@@ -1242,7 +1313,7 @@ function projectReplay(model, lang, options) {
|
|
|
1242
1313
|
value: contextContent ? (zh ? '源日志已记录可见内容' : 'Observable content recorded') : (zh ? '源日志未记录上下文内容' : 'Context content not recorded by the source log'),
|
|
1243
1314
|
detail: contextContent ?? (zh ? '当前规范化事件只保留了上下文类型与元数据。' : 'The normalized event only retains the context type and metadata.'),
|
|
1244
1315
|
copyable: Boolean(contextContent),
|
|
1245
|
-
|
|
1316
|
+
evidence: trajectoryEvidenceRef(contextEvent),
|
|
1246
1317
|
detailKind: 'content',
|
|
1247
1318
|
},
|
|
1248
1319
|
{ label: zh ? '内容身份' : 'Content identity', value: first?.contentHash ? `sha256:${shortHash(first.contentHash)}` : (zh ? '未记录哈希' : 'Hash not recorded'), detail: zh ? '只标识观测到的内容,不推断是否被模型采用' : 'Identifies observed content without inferring model use' },
|
|
@@ -1315,6 +1386,7 @@ function projectReplay(model, lang, options) {
|
|
|
1315
1386
|
value: text,
|
|
1316
1387
|
detail: reasoningContentSourceLabel(event?.contentSource, lang),
|
|
1317
1388
|
presentation: 'content',
|
|
1389
|
+
evidence: trajectoryEvidenceRef(event),
|
|
1318
1390
|
}]),
|
|
1319
1391
|
]
|
|
1320
1392
|
: [
|
|
@@ -1326,6 +1398,7 @@ function projectReplay(model, lang, options) {
|
|
|
1326
1398
|
value: text,
|
|
1327
1399
|
detail: zh ? '未做隐藏意图或原因推断' : 'No hidden-intent or causal inference',
|
|
1328
1400
|
presentation: 'content',
|
|
1401
|
+
evidence: trajectoryEvidenceRef(event),
|
|
1329
1402
|
},
|
|
1330
1403
|
],
|
|
1331
1404
|
events: step.events,
|
|
@@ -1527,9 +1600,10 @@ function buildOperationLayout(steps, startTimestamp, lang, pendingToolResults) {
|
|
|
1527
1600
|
previousEndMs = eventTimes.length > 0 ? Math.max(...eventTimes) : startMs ?? previousEndMs;
|
|
1528
1601
|
});
|
|
1529
1602
|
const tickStride = Math.max(1, Math.ceil(steps.length / 9));
|
|
1530
|
-
const
|
|
1603
|
+
const axisTickCandidates = steps.flatMap((step, index) => (index === 0 || index === steps.length - 1 || index % tickStride === 0
|
|
1531
1604
|
? [{ position: positions[index] ?? TRACK_START_PADDING, label: formatRelativeTimestamp(step.timestamp, startTimestamp) }]
|
|
1532
1605
|
: []));
|
|
1606
|
+
const axisTicks = axisTickCandidates.filter((tick, index) => index === 0 || tick.label !== axisTickCandidates[index - 1]?.label);
|
|
1533
1607
|
const lastPosition = positions.at(-1) ?? TRACK_START_PADDING;
|
|
1534
1608
|
const lastWidth = steps.length > 0 ? replayCardWidth(steps[steps.length - 1], lang, pendingToolResults) : REPLAY_CARD_WIDTH;
|
|
1535
1609
|
const occupiedRight = Math.max(lastPosition + lastWidth, ...rightEdgeByTrack.values());
|
|
@@ -1587,7 +1661,8 @@ function renderFacetFocus(facets, lang) {
|
|
|
1587
1661
|
return `<details class="trajectory-focus"><summary aria-label="${zh ? '类型筛选' : 'Filter by type'}" title="${zh ? '类型筛选' : 'Filter by type'}"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3 6h18"></path><path d="M7 12h10"></path><path d="M10 18h4"></path></svg></summary><div class="trajectory-focus-menu" role="group" aria-label="${zh ? '类型筛选' : 'Type filter'}"><span class="trajectory-focus-title">${zh ? '类型筛选' : 'Type filter'}</span><button class="trajectory-focus-option" type="button" data-trajectory-facet="" aria-pressed="true"><span class="trajectory-focus-swatch" aria-hidden="true"></span><span>${zh ? '全部类型' : 'All types'}</span><small>${zh ? '清除' : 'Clear'}</small></button>${options}</div></details>`;
|
|
1588
1662
|
}
|
|
1589
1663
|
function renderSemanticPanel(operation, selected, lang) {
|
|
1590
|
-
|
|
1664
|
+
const evidence = primaryTrajectoryEvidenceRef(operation.events);
|
|
1665
|
+
return `<div class="trajectory-operation-panel trajectory-fields" data-trajectory-semantic-panel="${e(operation.id)}" data-selection-label="${e(operation.selectionLabel)}" data-type-label="${e(operation.typeLabel)}" data-title="${e(operation.title)}" data-summary="${e(operation.summary)}" data-evidence-label="${e(operation.evidenceLabel)}"${renderEvidenceDataAttributes(evidence)}${selected ? '' : ' hidden'}>${operation.fields.map((field) => {
|
|
1591
1666
|
const content = field.presentation === 'content'
|
|
1592
1667
|
? `<div class="trajectory-field-content">${renderSafeInlineMarkdown(field.value)}</div>`
|
|
1593
1668
|
: `<strong class="trajectory-field-value">${e(field.value)}</strong>`;
|
|
@@ -1596,7 +1671,7 @@ function renderSemanticPanel(operation, selected, lang) {
|
|
|
1596
1671
|
}
|
|
1597
1672
|
function renderFieldDetail(field, lang) {
|
|
1598
1673
|
const preview = fieldDetailPreview(field.detail);
|
|
1599
|
-
if ((!preview.truncated || !field.
|
|
1674
|
+
if ((!preview.truncated || !field.evidence?.normalizedEventId) && !field.copyable && !field.evidence) {
|
|
1600
1675
|
return `<code class="trajectory-field-detail">${e(field.detail)}</code>`;
|
|
1601
1676
|
}
|
|
1602
1677
|
const zh = lang === 'zh';
|
|
@@ -1609,18 +1684,30 @@ function renderFieldDetail(field, lang) {
|
|
|
1609
1684
|
const collapseLabel = zh ? (result ? '收起结果' : '收起内容') : (result ? 'Collapse result' : 'Collapse content');
|
|
1610
1685
|
const copyLabel = zh ? (result ? '复制结果' : '复制内容') : (result ? 'Copy result' : 'Copy content');
|
|
1611
1686
|
const copiedLabel = zh ? '已复制' : 'Copied';
|
|
1612
|
-
const canExpand = preview.truncated && Boolean(field.
|
|
1687
|
+
const canExpand = preview.truncated && Boolean(field.evidence?.normalizedEventId);
|
|
1613
1688
|
const displayedDetail = canExpand ? preview.text : field.detail;
|
|
1614
|
-
const
|
|
1615
|
-
? ` data-detail-source-event-id="${e(field.detailSourceEventId)}"`
|
|
1616
|
-
: '';
|
|
1689
|
+
const evidenceAttributes = renderEvidenceDataAttributes(field.evidence, 'detail');
|
|
1617
1690
|
const status = canExpand
|
|
1618
1691
|
? `<span class="trajectory-field-detail-status" data-field-detail-status>${e(previewStatus)}</span>`
|
|
1619
1692
|
: '<span></span>';
|
|
1620
1693
|
const toggle = canExpand
|
|
1621
1694
|
? `<button class="trajectory-field-detail-toggle" type="button" data-field-detail-toggle aria-expanded="false">${e(expandLabel)}</button>`
|
|
1622
1695
|
: '';
|
|
1623
|
-
|
|
1696
|
+
const evidenceButton = field.evidence
|
|
1697
|
+
? `<button class="trajectory-field-evidence" type="button" data-view-evidence>${lang === 'zh' ? (field.evidence.sourceLineIndex !== undefined ? '查看原始日志' : '查看规范化事件') : (field.evidence.sourceLineIndex !== undefined ? 'View raw log' : 'View normalized event')}</button>`
|
|
1698
|
+
: '';
|
|
1699
|
+
return `<div class="trajectory-field-detail-block" data-field-detail-block data-expanded="false" data-expandable="${String(canExpand)}"${evidenceAttributes} data-preview-status="${e(previewStatus)}" data-full-status="${e(fullStatus)}" data-expand-label="${e(expandLabel)}" data-collapse-label="${e(collapseLabel)}" data-copy-label="${e(copyLabel)}" data-copied-label="${e(copiedLabel)}"><code class="trajectory-field-detail" data-field-detail>${e(displayedDetail)}</code><div class="trajectory-field-detail-actions">${status}<span class="trajectory-field-detail-controls">${evidenceButton}${toggle}<button class="trajectory-field-detail-copy" type="button" data-field-detail-copy aria-label="${e(copyLabel)}" title="${e(copyLabel)}">${icon('copy', { size: 13 })}</button></span></div></div>`;
|
|
1700
|
+
}
|
|
1701
|
+
function renderEvidenceDataAttributes(evidence, prefix = 'evidence') {
|
|
1702
|
+
if (!evidence)
|
|
1703
|
+
return '';
|
|
1704
|
+
const attributes = [` data-${prefix}-source-event-id="${e(evidence.normalizedEventId)}"`];
|
|
1705
|
+
if (evidence.sourceLineIndex !== undefined) {
|
|
1706
|
+
attributes.push(` data-${prefix}-source-line-index="${evidence.sourceLineIndex}"`);
|
|
1707
|
+
}
|
|
1708
|
+
if (evidence.traceId)
|
|
1709
|
+
attributes.push(` data-${prefix}-trace-id="${e(evidence.traceId)}"`);
|
|
1710
|
+
return attributes.join('');
|
|
1624
1711
|
}
|
|
1625
1712
|
function fieldDetailPreview(detail) {
|
|
1626
1713
|
const lines = detail.replace(/\r\n/g, '\n').split('\n');
|
|
@@ -1654,7 +1741,8 @@ function renderNormalizedEventList(events, operations, startTimestamp, lang) {
|
|
|
1654
1741
|
const source = `${event.sourceLineIndex !== undefined ? `#${event.sourceLineIndex} · ` : ''}${shortHash(event.traceId ?? event.id)}`;
|
|
1655
1742
|
const facetIds = [...(facetsByEventId.get(event.id) ?? [])];
|
|
1656
1743
|
const kind = event.sourceType ? `${event.kind} · ${event.sourceType}` : event.kind;
|
|
1657
|
-
|
|
1744
|
+
const sourceAttributes = `${event.sourceLineIndex !== undefined ? ` data-source-line-index="${event.sourceLineIndex}"` : ''}${event.traceId ? ` data-source-trace-id="${e(event.traceId)}"` : ''}`;
|
|
1745
|
+
return `<details class="trajectory-raw-row" data-trajectory-normalized-event="${e(event.id)}"${sourceAttributes} data-trajectory-facets="${e(facetIds.join(' '))}"><summary><time>${e(formatRelativeTimestamp(event.timestamp, startTimestamp))}</time><span class="trajectory-raw-kind">${e(kind)}</span><strong>${e(preview)}</strong><code class="trajectory-raw-id">${e(source)}</code></summary><pre>${e(content || event.label || '')}</pre></details>`;
|
|
1658
1746
|
}).join('');
|
|
1659
1747
|
return `<section class="trajectory-raw-list" data-event-view="normalized" aria-label="${zh ? '按来源顺序排列的规范化事件' : 'Normalized events in source order'}"><header class="trajectory-raw-head"><span>${zh ? '时间' : 'Time'}</span><span>${zh ? '规范化 / 来源类型' : 'Normalized / source type'}</span><span>${zh ? '内容' : 'Content'}</span><span>${zh ? '来源位置' : 'Source position'}</span></header>${rows}<div class="trajectory-raw-empty" data-trajectory-normalized-empty hidden>${zh ? '没有符合当前类型筛选的规范化事件' : 'No normalized events match the current type filter'}</div></section>`;
|
|
1660
1748
|
}
|
|
@@ -1696,10 +1784,8 @@ function renderSourceRecordList(model, startTimestamp, lang, sourceRecordsEndpoi
|
|
|
1696
1784
|
const zh = lang === 'zh';
|
|
1697
1785
|
const source = model.sourceRecords;
|
|
1698
1786
|
const lazy = Boolean(sourceRecordsEndpoint && source.status !== 'unavailable' && source.recordCount > 0 && source.records.length === 0);
|
|
1699
|
-
const statusNotice = source.status === 'partial'
|
|
1700
|
-
? `<div class="trajectory-record-notice">${
|
|
1701
|
-
? `原始日志归档不完整:已保留 ${source.recordCount} 条,省略 ${source.omittedRecordCount} 条。`
|
|
1702
|
-
: `The raw-log archive is partial: ${source.recordCount} retained, ${source.omittedRecordCount} omitted.`}</div>`
|
|
1787
|
+
const statusNotice = !lazy && source.status === 'partial'
|
|
1788
|
+
? `<div class="trajectory-record-notice">${e(sourceRecordPartialNotice(source.recordCount, source.omittedRecordCount, lang))}</div>`
|
|
1703
1789
|
: '';
|
|
1704
1790
|
const rows = source.records.map((record) => {
|
|
1705
1791
|
const preview = compactText(record.raw || (zh ? '空记录' : 'Empty record'), 180);
|
|
@@ -1708,7 +1794,7 @@ function renderSourceRecordList(model, startTimestamp, lang, sourceRecordsEndpoi
|
|
|
1708
1794
|
record.redacted ? (zh ? '【不透明加密载荷已省略】' : '[Opaque encrypted payload omitted]') : '',
|
|
1709
1795
|
record.truncated ? (zh ? '【该记录已按归档上限截断】' : '[Record truncated by archive limit]') : '',
|
|
1710
1796
|
].filter(Boolean).join('\n');
|
|
1711
|
-
return `<details class="trajectory-raw-row"><summary><time>${e(formatRelativeTimestamp(record.timestamp, startTimestamp))}</time><span class="trajectory-raw-kind">${e(record.sourceType)}</span><strong>${e(preview)}</strong><code class="trajectory-raw-id">${e(locator)}</code></summary><pre>${e(record.raw)}${notices ? `\n\n${e(notices)}` : ''}</pre></details>`;
|
|
1797
|
+
return `<details class="trajectory-raw-row" data-source-line-index="${record.sourceIndex}" data-source-trace-id="${e(record.traceId)}"><summary><time>${e(formatRelativeTimestamp(record.timestamp, startTimestamp))}</time><span class="trajectory-raw-kind">${e(record.sourceType)}</span><strong>${e(preview)}</strong><code class="trajectory-raw-id">${e(locator)}</code></summary><pre>${e(record.raw)}${notices ? `\n\n${e(notices)}` : ''}</pre></details>`;
|
|
1712
1798
|
}).join('');
|
|
1713
1799
|
const unavailable = source.status === 'unavailable'
|
|
1714
1800
|
? `<div class="trajectory-raw-empty">${e(sourceRecordUnavailableLabel(source.reason, lang))}</div>`
|
|
@@ -1720,6 +1806,13 @@ function renderSourceRecordList(model, startTimestamp, lang, sourceRecordsEndpoi
|
|
|
1720
1806
|
const start = startTimestamp ? ` data-source-records-start="${e(startTimestamp)}"` : '';
|
|
1721
1807
|
return `<section class="trajectory-raw-list" data-event-view="source"${endpoint}${start} data-source-records-loaded="${lazy ? 'false' : 'true'}" aria-label="${zh ? '按来源顺序排列的原始日志' : 'Raw logs in source order'}"><header class="trajectory-raw-head"><span>${zh ? '时间' : 'Time'}</span><span>${zh ? '来源类型' : 'Source type'}</span><span>${zh ? '原始 JSONL' : 'Raw JSONL'}</span><span>${zh ? '来源位置' : 'Source position'}</span></header>${statusNotice}${rows}${unavailable}${loading}</section>`;
|
|
1722
1808
|
}
|
|
1809
|
+
function sourceRecordPartialNotice(recordCount, omittedRecordCount, lang) {
|
|
1810
|
+
if (omittedRecordCount === 0)
|
|
1811
|
+
return SOURCE_RECORD_PARTIAL_LABELS[lang].content;
|
|
1812
|
+
return SOURCE_RECORD_PARTIAL_LABELS[lang].records
|
|
1813
|
+
.replace('{retained}', String(recordCount))
|
|
1814
|
+
.replace('{omitted}', String(omittedRecordCount));
|
|
1815
|
+
}
|
|
1723
1816
|
function sourceRecordUnavailableLabel(reason, lang) {
|
|
1724
1817
|
const zh = lang === 'zh';
|
|
1725
1818
|
if (reason === 'source_missing')
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ExperienceTimelineEvent } from '../types/index.js';
|
|
2
|
+
export interface TrajectoryEvidenceRef {
|
|
3
|
+
normalizedEventId: string;
|
|
4
|
+
sourceLineIndex?: number;
|
|
5
|
+
traceId?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function trajectoryEvidenceRef(event: ExperienceTimelineEvent | undefined): TrajectoryEvidenceRef | undefined;
|
|
8
|
+
export declare function primaryTrajectoryEvidenceRef(events: ExperienceTimelineEvent[]): TrajectoryEvidenceRef | undefined;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function trajectoryEvidenceRef(event) {
|
|
2
|
+
if (!event)
|
|
3
|
+
return undefined;
|
|
4
|
+
return {
|
|
5
|
+
normalizedEventId: event.id,
|
|
6
|
+
...(event.sourceLineIndex !== undefined ? { sourceLineIndex: event.sourceLineIndex } : {}),
|
|
7
|
+
...(event.traceId ? { traceId: event.traceId } : {}),
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export function primaryTrajectoryEvidenceRef(events) {
|
|
11
|
+
return trajectoryEvidenceRef(events.find((event) => event.sourceLineIndex !== undefined) ?? events[0]);
|
|
12
|
+
}
|