@x-otto/trace-view 0.0.1-alpha.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 +99 -0
- package/dist/index.d.ts +337 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/package.json +31 -0
package/README.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# @x-otto/trace-view
|
|
2
|
+
|
|
3
|
+
Structured trace event view model — transforms raw `TraceEvent` streams into timeline/waterfall/causal-graph views. Shared between Web and TUI consumers. Extracted from `@x-otto/agent` when `web-ui`/`web-trace` were deleted (2026-07-17).
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @x-otto/trace-view
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { dispatchEventView, traceEventsToTimeline } from '@x-otto/trace-view'
|
|
15
|
+
|
|
16
|
+
// Single event dispatch
|
|
17
|
+
const event: TraceEvent = { seq: 1, ts: Date.now(), kind: 'lifecycle', node: 'turn.start', payload: {} }
|
|
18
|
+
const view = dispatchEventView(1, event)
|
|
19
|
+
// → { viewKind: 'lifecycle:turn.start', seq: 1, ts: ..., turn: 0 }
|
|
20
|
+
|
|
21
|
+
// Full timeline from trace records
|
|
22
|
+
const timeline = await traceEventsToTimeline(records)
|
|
23
|
+
// → { sessionId, turns: TraceTurnView[], orphanEvents, causalEdges, ... }
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## dispatchEventView
|
|
27
|
+
|
|
28
|
+
Deterministic single-event dispatch. Register-based pattern:
|
|
29
|
+
|
|
30
|
+
- `lifecycle` kind: node → builder Map (`prompt.before`, `context.build`, `turn.start/end`, `truncated`, `compaction.reactive`, `stream.retry/accelerated/auth-failed`, `agent.error`, `memory.prune/archive`, `max.turns`)
|
|
31
|
+
- `tool` kind: node → builder Map (`tool.call.start`, `tool.call.end`)
|
|
32
|
+
- `stream` kind: `payload.type` inline dispatch (`text_delta`/`thinking_delta` → `StreamDeltaView`, `done`/`message_stop` → `StreamDoneView`)
|
|
33
|
+
- All unmatched → `GenericEventView` (never throws)
|
|
34
|
+
|
|
35
|
+
## 18 View Types
|
|
36
|
+
|
|
37
|
+
| ViewKind | Source Event |
|
|
38
|
+
|----------|-------------|
|
|
39
|
+
| `lifecycle:prompt.before` | lifecycle + prompt.before |
|
|
40
|
+
| `lifecycle:context.build` | lifecycle + context.build |
|
|
41
|
+
| `lifecycle:turn.start` | lifecycle + turn.start |
|
|
42
|
+
| `lifecycle:turn.end` | lifecycle + turn.end (model, provider, usage) |
|
|
43
|
+
| `stream:delta` | stream + stream.event (text_delta/thinking_delta) |
|
|
44
|
+
| `stream:done` | stream + stream.event (done/message_stop) |
|
|
45
|
+
| `tool:call.start` | tool + tool.call.start |
|
|
46
|
+
| `tool:call.end` | tool + tool.call.end (ToolResultView) |
|
|
47
|
+
| `lifecycle:turn.truncated` | lifecycle + turn.truncated |
|
|
48
|
+
| `lifecycle:compaction.reactive` | lifecycle + compaction.reactive |
|
|
49
|
+
| `lifecycle:stream.retry` | lifecycle + stream.retry |
|
|
50
|
+
| `lifecycle:stream.retry.accelerated` | lifecycle + stream.retry.accelerated |
|
|
51
|
+
| `lifecycle:stream.auth-failed` | lifecycle + stream.auth-failed |
|
|
52
|
+
| `lifecycle:agent.error` | lifecycle + agent.error |
|
|
53
|
+
| `lifecycle:memory.prune` | lifecycle + memory.prune |
|
|
54
|
+
| `lifecycle:memory.archive` | lifecycle + memory.archive |
|
|
55
|
+
| `lifecycle:max.turns` | lifecycle + max.turns |
|
|
56
|
+
| `generic` | All unmatched |
|
|
57
|
+
|
|
58
|
+
## traceEventsToTimeline
|
|
59
|
+
|
|
60
|
+
Full materialization + turn aggregation. Processes up to `MAX_TIMELINE_EVENTS` (5000), returns `truncated: true` if exceeded.
|
|
61
|
+
|
|
62
|
+
Produces:
|
|
63
|
+
|
|
64
|
+
- **TraceTurnView[]**: events grouped by turn index with computed `TurnStats` (eventCount, toolCallCount, input/output tokens, duration, errorCount, retryCount, authFailureCount)
|
|
65
|
+
- **orphanEvents**: events without turn assignment
|
|
66
|
+
- **causalEdges**: from `causalDag()` (`@x-otto/agent`) — `{fromSeq, toSeq}` pairs
|
|
67
|
+
- **schemaVersion**: `TRACE_VIEW_SCHEMA_VERSION` — increment on field changes
|
|
68
|
+
|
|
69
|
+
### Schema Versions
|
|
70
|
+
|
|
71
|
+
| Version | Change |
|
|
72
|
+
|---------|--------|
|
|
73
|
+
| 1 | Initial |
|
|
74
|
+
| 2 | TurnStats.authFailureCount added |
|
|
75
|
+
| 3 | StreamAuthFailedView.errorType added (RFC-139 D3) |
|
|
76
|
+
|
|
77
|
+
Current: **v3**.
|
|
78
|
+
|
|
79
|
+
## Key Files
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
src/
|
|
83
|
+
trace-event-view.ts # All 18 view types + dispatchEventView + traceEventsToTimeline + TurnStats
|
|
84
|
+
index.ts # Barrel exports
|
|
85
|
+
tests/ # Test files
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Dependencies
|
|
89
|
+
|
|
90
|
+
- Internal: `@x-otto/agent` (TraceEvent, TraceRecord, causalDag)
|
|
91
|
+
- Consumers: Web UI, TUI, `otto observe --json`
|
|
92
|
+
|
|
93
|
+
## Testing
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
pnpm --filter @x-otto/trace-view typecheck
|
|
97
|
+
pnpm --filter @x-otto/trace-view build
|
|
98
|
+
pnpm vitest run packages/trace-view/tests/
|
|
99
|
+
```
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import { TraceEvent, TraceRecord } from "@x-otto/agent";
|
|
2
|
+
|
|
3
|
+
//#region src/trace-event-view.d.ts
|
|
4
|
+
/** 视图模型版本号,初始 = 1。字段变更时递增。 */
|
|
5
|
+
declare const TRACE_VIEW_SCHEMA_VERSION = 3;
|
|
6
|
+
/** traceEventsToTimeline() 最大处理事件数。超限返回 truncated: true。 */
|
|
7
|
+
declare const MAX_TIMELINE_EVENTS = 5000;
|
|
8
|
+
interface ContentPreviewItem {
|
|
9
|
+
type: string;
|
|
10
|
+
text?: string;
|
|
11
|
+
preview?: string;
|
|
12
|
+
totalLength?: number;
|
|
13
|
+
}
|
|
14
|
+
interface ToolResultView {
|
|
15
|
+
isError: boolean;
|
|
16
|
+
contentCount: number;
|
|
17
|
+
contentPreview: ContentPreviewItem[];
|
|
18
|
+
details?: Record<string, unknown>;
|
|
19
|
+
}
|
|
20
|
+
/** 源: kind='lifecycle' node='prompt.before'
|
|
21
|
+
* payload → { sessionId, isFirstMessage, text } */
|
|
22
|
+
interface PromptBeforeView {
|
|
23
|
+
viewKind: 'lifecycle:prompt.before';
|
|
24
|
+
seq: number;
|
|
25
|
+
ts: number;
|
|
26
|
+
turn?: number;
|
|
27
|
+
text: string;
|
|
28
|
+
isFirstMessage: boolean;
|
|
29
|
+
}
|
|
30
|
+
/** 源: kind='lifecycle' node='context.build'
|
|
31
|
+
* payload → { sessionId, hasSummary, messages }(messages 为数量) */
|
|
32
|
+
interface ContextBuildView {
|
|
33
|
+
viewKind: 'lifecycle:context.build';
|
|
34
|
+
seq: number;
|
|
35
|
+
ts: number;
|
|
36
|
+
turn?: number;
|
|
37
|
+
hasSummary: boolean;
|
|
38
|
+
messageCount: number;
|
|
39
|
+
}
|
|
40
|
+
/** 源: kind='lifecycle' node='turn.start'
|
|
41
|
+
* payload → {}(无载荷) */
|
|
42
|
+
interface TurnStartView {
|
|
43
|
+
viewKind: 'lifecycle:turn.start';
|
|
44
|
+
seq: number;
|
|
45
|
+
ts: number;
|
|
46
|
+
turn: number;
|
|
47
|
+
}
|
|
48
|
+
/** 源: kind='lifecycle' node='turn.end'
|
|
49
|
+
* payload → { stopReason, usage: { inputTokens, outputTokens }, model, provider }
|
|
50
|
+
* model/provider 自 2026-07-30 起随 payload 落盘(同一会话可跨回合切模型,回放需按
|
|
51
|
+
* model 归因单个回合);旧 trace 无此字段 → undefined(白名单拾取,不抛)。 */
|
|
52
|
+
interface TurnEndView {
|
|
53
|
+
viewKind: 'lifecycle:turn.end';
|
|
54
|
+
seq: number;
|
|
55
|
+
ts: number;
|
|
56
|
+
turn: number;
|
|
57
|
+
stopReason?: string;
|
|
58
|
+
inputTokens?: number;
|
|
59
|
+
outputTokens?: number;
|
|
60
|
+
/** 本回合实际执行的模型 id;旧 trace(无该字段)为 undefined。 */
|
|
61
|
+
model?: string;
|
|
62
|
+
/** 本回合实际执行的 provider;旧 trace 为 undefined。 */
|
|
63
|
+
provider?: string;
|
|
64
|
+
}
|
|
65
|
+
/** 源: kind='stream' node='stream.event'
|
|
66
|
+
* payload.type in ('text_delta','thinking_delta') → StreamDeltaView
|
|
67
|
+
* payload.delta → text */
|
|
68
|
+
interface StreamDeltaView {
|
|
69
|
+
viewKind: 'stream:delta';
|
|
70
|
+
seq: number;
|
|
71
|
+
ts: number;
|
|
72
|
+
turn?: number;
|
|
73
|
+
deltaType: 'text_delta' | 'thinking_delta';
|
|
74
|
+
text: string;
|
|
75
|
+
}
|
|
76
|
+
/** 源: kind='stream' node='stream.event'
|
|
77
|
+
* payload.type in ('done','message_stop') → StreamDoneView
|
|
78
|
+
* payload.reason → reason */
|
|
79
|
+
interface StreamDoneView {
|
|
80
|
+
viewKind: 'stream:done';
|
|
81
|
+
seq: number;
|
|
82
|
+
ts: number;
|
|
83
|
+
turn?: number;
|
|
84
|
+
reason?: string;
|
|
85
|
+
}
|
|
86
|
+
/** 源: kind='tool' node='tool.call.start'
|
|
87
|
+
* payload → { id, name, arguments, turnIndex, toolCallIndex } */
|
|
88
|
+
interface ToolCallStartView {
|
|
89
|
+
viewKind: 'tool:call.start';
|
|
90
|
+
seq: number;
|
|
91
|
+
ts: number;
|
|
92
|
+
turn: number;
|
|
93
|
+
id: string;
|
|
94
|
+
name: string;
|
|
95
|
+
arguments: string;
|
|
96
|
+
toolCallIndex: number;
|
|
97
|
+
}
|
|
98
|
+
/** 源: kind='tool' node='tool.call.end'
|
|
99
|
+
* payload → { id, name, resultSummary, durationMs, errorCategory, toolCallIndex } */
|
|
100
|
+
interface ToolCallEndView {
|
|
101
|
+
viewKind: 'tool:call.end';
|
|
102
|
+
seq: number;
|
|
103
|
+
ts: number;
|
|
104
|
+
turn: number;
|
|
105
|
+
id: string;
|
|
106
|
+
name: string;
|
|
107
|
+
durationMs?: number;
|
|
108
|
+
result: ToolResultView;
|
|
109
|
+
errorCategory?: string;
|
|
110
|
+
toolCallIndex: number;
|
|
111
|
+
}
|
|
112
|
+
/** 源: kind='lifecycle' node='turn.truncated'
|
|
113
|
+
* payload → { action, maxTokens?, continuation? } */
|
|
114
|
+
interface TruncatedView {
|
|
115
|
+
viewKind: 'lifecycle:turn.truncated';
|
|
116
|
+
seq: number;
|
|
117
|
+
ts: number;
|
|
118
|
+
turn?: number;
|
|
119
|
+
action: 'escalate' | 'continue' | 'refusal';
|
|
120
|
+
maxTokens?: number;
|
|
121
|
+
continuation?: string;
|
|
122
|
+
}
|
|
123
|
+
/** 源: kind='lifecycle' node='compaction.reactive'
|
|
124
|
+
* payload → { messagesBefore, messagesAfter } */
|
|
125
|
+
interface CompactionView {
|
|
126
|
+
viewKind: 'lifecycle:compaction.reactive';
|
|
127
|
+
seq: number;
|
|
128
|
+
ts: number;
|
|
129
|
+
turn?: number;
|
|
130
|
+
messagesBefore: number;
|
|
131
|
+
messagesAfter: number;
|
|
132
|
+
}
|
|
133
|
+
/** 源: kind='lifecycle' node='stream.retry'
|
|
134
|
+
* payload → { attempt, maxRetries, delayMs, reason } */
|
|
135
|
+
interface StreamRetryView {
|
|
136
|
+
viewKind: 'lifecycle:stream.retry';
|
|
137
|
+
seq: number;
|
|
138
|
+
ts: number;
|
|
139
|
+
turn?: number;
|
|
140
|
+
attempt: number;
|
|
141
|
+
maxRetries: number;
|
|
142
|
+
delayMs: number;
|
|
143
|
+
reason: string;
|
|
144
|
+
}
|
|
145
|
+
/** 源: kind='lifecycle' node='stream.retry.accelerated'
|
|
146
|
+
* payload → { attempt, maxRetries, elapsedMs } */
|
|
147
|
+
interface StreamRetryAccelView {
|
|
148
|
+
viewKind: 'lifecycle:stream.retry.accelerated';
|
|
149
|
+
seq: number;
|
|
150
|
+
ts: number;
|
|
151
|
+
turn?: number;
|
|
152
|
+
attempt: number;
|
|
153
|
+
maxRetries: number;
|
|
154
|
+
elapsedMs: number;
|
|
155
|
+
}
|
|
156
|
+
/** 源: kind='lifecycle' node='stream.auth-failed'
|
|
157
|
+
* payload → { provider, modelId, reason, errorType }
|
|
158
|
+
* errorType(RFC-139 D3,schema v3):credential-missing / refresh-failed / server-rejected;
|
|
159
|
+
* 旧 trace 无此字段时回退 'server-rejected'(与分类器的无 code 回退一致)。 */
|
|
160
|
+
interface StreamAuthFailedView {
|
|
161
|
+
viewKind: 'lifecycle:stream.auth-failed';
|
|
162
|
+
seq: number;
|
|
163
|
+
ts: number;
|
|
164
|
+
turn?: number;
|
|
165
|
+
provider: string;
|
|
166
|
+
modelId: string;
|
|
167
|
+
reason: string;
|
|
168
|
+
errorType: string;
|
|
169
|
+
}
|
|
170
|
+
/** 源: kind='lifecycle' node='agent.error'
|
|
171
|
+
* payload → { message } */
|
|
172
|
+
interface AgentErrorView {
|
|
173
|
+
viewKind: 'lifecycle:agent.error';
|
|
174
|
+
seq: number;
|
|
175
|
+
ts: number;
|
|
176
|
+
turn?: number;
|
|
177
|
+
message: string;
|
|
178
|
+
}
|
|
179
|
+
/** 源: kind='lifecycle' node='memory.prune'
|
|
180
|
+
* payload → { trigger, prunedCount?, tokensSaved?, messagesBefore?, messagesAfter? } */
|
|
181
|
+
interface MemoryPruneView {
|
|
182
|
+
viewKind: 'lifecycle:memory.prune';
|
|
183
|
+
seq: number;
|
|
184
|
+
ts: number;
|
|
185
|
+
turn?: number;
|
|
186
|
+
trigger: string;
|
|
187
|
+
prunedCount?: number;
|
|
188
|
+
tokensSaved?: number;
|
|
189
|
+
messagesBefore?: number;
|
|
190
|
+
messagesAfter?: number;
|
|
191
|
+
}
|
|
192
|
+
/** 源: kind='lifecycle' node='memory.archive'
|
|
193
|
+
* payload → { trigger, archivePath?, archivedCount? } */
|
|
194
|
+
interface MemoryArchiveView {
|
|
195
|
+
viewKind: 'lifecycle:memory.archive';
|
|
196
|
+
seq: number;
|
|
197
|
+
ts: number;
|
|
198
|
+
turn?: number;
|
|
199
|
+
trigger: string;
|
|
200
|
+
archivePath?: string;
|
|
201
|
+
archivedCount?: number;
|
|
202
|
+
}
|
|
203
|
+
/** 源: kind='lifecycle' node='max.turns'
|
|
204
|
+
* payload → { maxTurns, extended, steered, extension?, maxExtensions? }
|
|
205
|
+
* extension 和 maxExtensions 为「派生字段」:dispatchEventView 从 payload 取值,
|
|
206
|
+
* traceEventsToTimeline 可跨 turn 补全。 */
|
|
207
|
+
interface MaxTurnsView {
|
|
208
|
+
viewKind: 'lifecycle:max.turns';
|
|
209
|
+
seq: number;
|
|
210
|
+
ts: number;
|
|
211
|
+
turn?: number;
|
|
212
|
+
maxTurns: number;
|
|
213
|
+
extended?: boolean;
|
|
214
|
+
steered?: boolean;
|
|
215
|
+
/** RFC-104 D1:窗口无进展被闸门拒绝(区别于额度耗尽)。 */
|
|
216
|
+
progressDenied?: boolean;
|
|
217
|
+
extension?: number;
|
|
218
|
+
maxExtensions?: number;
|
|
219
|
+
}
|
|
220
|
+
/** 源: kind='lifecycle' node='model.switch'
|
|
221
|
+
* payload → { from, to, label }——CLI 侧 applyModel 三层记录(model-effort-record.ts)。 */
|
|
222
|
+
interface ModelSwitchView {
|
|
223
|
+
viewKind: 'lifecycle:model.switch';
|
|
224
|
+
seq: number;
|
|
225
|
+
ts: number;
|
|
226
|
+
turn?: number;
|
|
227
|
+
/** 旧模型 id(首次切换/未命中时可为 null)。 */
|
|
228
|
+
from: string | null;
|
|
229
|
+
/** 新模型 id。 */
|
|
230
|
+
to: string;
|
|
231
|
+
/** 格式化显示串(如 "claude-sonnet-5 (200K)")。 */
|
|
232
|
+
label: string;
|
|
233
|
+
}
|
|
234
|
+
/** 源: kind='lifecycle' node='effort.change'
|
|
235
|
+
* payload → { from, to, label }——CLI 侧 applyEffort 三层记录(model-effort-record.ts)。 */
|
|
236
|
+
interface EffortChangeView {
|
|
237
|
+
viewKind: 'lifecycle:effort.change';
|
|
238
|
+
seq: number;
|
|
239
|
+
ts: number;
|
|
240
|
+
turn?: number;
|
|
241
|
+
/** 旧档位(首次切换可为 null)。 */
|
|
242
|
+
from: string | null;
|
|
243
|
+
/** 新档位。 */
|
|
244
|
+
to: string;
|
|
245
|
+
/** 格式化显示串(如 "High")。 */
|
|
246
|
+
label: string;
|
|
247
|
+
}
|
|
248
|
+
/** 源: kind='lifecycle' node='retry.queued|cancelled|reset'——CLI 侧回合级重试卡动作
|
|
249
|
+
* (ErrorRetryCard,model-effort-record.ts)。 */
|
|
250
|
+
interface RetryActionView {
|
|
251
|
+
viewKind: 'lifecycle:retry.queued' | 'lifecycle:retry.cancelled' | 'lifecycle:retry.reset';
|
|
252
|
+
seq: number;
|
|
253
|
+
ts: number;
|
|
254
|
+
turn?: number;
|
|
255
|
+
/** retry.queued:本次为第几次(1-based);retry.cancelled:取消时的已重试次数。 */
|
|
256
|
+
attempt?: number;
|
|
257
|
+
/** retry.queued:per-prompt 总配额。 */
|
|
258
|
+
max?: number;
|
|
259
|
+
/** retry.queued:重试模式(auto=30s×N 自动 / resetsAt=官方倒计时 / manual=手动)。 */
|
|
260
|
+
mode?: string;
|
|
261
|
+
/** retry.queued:触发错误原因(截断)。 */
|
|
262
|
+
reason?: string;
|
|
263
|
+
}
|
|
264
|
+
/** 源: kind='lifecycle' node='pause.requested|paused|cancelled|resumed'——流程暂停四态
|
|
265
|
+
* (PauseDialog / turn 边界暂停,interactive-events.ts 事件驱动记录)。 */
|
|
266
|
+
interface PauseStateView {
|
|
267
|
+
viewKind: 'lifecycle:pause.requested' | 'lifecycle:pause.paused' | 'lifecycle:pause.cancelled' | 'lifecycle:pause.resumed';
|
|
268
|
+
seq: number;
|
|
269
|
+
ts: number;
|
|
270
|
+
turn?: number;
|
|
271
|
+
/** 触发入口(如 'session-event')。 */
|
|
272
|
+
source?: string;
|
|
273
|
+
}
|
|
274
|
+
/** 兜底:不匹配上述任何 kind×node 组合的事件。
|
|
275
|
+
* 包含: loop.start/end, prompt.after, agent.done/aborted, messages.persist,
|
|
276
|
+
* rate.limit.exhausted, 未知节点等。 */
|
|
277
|
+
interface GenericEventView {
|
|
278
|
+
viewKind: 'generic';
|
|
279
|
+
seq: number;
|
|
280
|
+
ts: number;
|
|
281
|
+
turn?: number;
|
|
282
|
+
eventKind: string;
|
|
283
|
+
node?: string;
|
|
284
|
+
payload: Record<string, unknown>;
|
|
285
|
+
}
|
|
286
|
+
type TraceEventView = PromptBeforeView | ContextBuildView | TurnStartView | TurnEndView | StreamDeltaView | StreamDoneView | ToolCallStartView | ToolCallEndView | TruncatedView | CompactionView | StreamRetryView | StreamRetryAccelView | StreamAuthFailedView | AgentErrorView | MemoryPruneView | MemoryArchiveView | MaxTurnsView | ModelSwitchView | EffortChangeView | RetryActionView | PauseStateView | GenericEventView;
|
|
287
|
+
interface TurnStats {
|
|
288
|
+
eventCount: number;
|
|
289
|
+
toolCallCount: number;
|
|
290
|
+
inputTokens?: number;
|
|
291
|
+
outputTokens?: number;
|
|
292
|
+
durationMs?: number;
|
|
293
|
+
stopReason?: string;
|
|
294
|
+
errorCount: number;
|
|
295
|
+
retryCount: number;
|
|
296
|
+
/** 本轮内 stream.auth-failed 事件次数(对抗审核 finding B:与 retryCount 同粒度统计,
|
|
297
|
+
* 否则 `otto observe --json` 结构化输出路径只能看展示层文本摘要,看不到统计维度)。 */
|
|
298
|
+
authFailureCount: number;
|
|
299
|
+
}
|
|
300
|
+
interface CausalEdge {
|
|
301
|
+
fromSeq: number;
|
|
302
|
+
toSeq: number;
|
|
303
|
+
}
|
|
304
|
+
interface TraceTurnView {
|
|
305
|
+
turnIndex: number;
|
|
306
|
+
startTs: number;
|
|
307
|
+
endTs?: number;
|
|
308
|
+
events: TraceEventView[];
|
|
309
|
+
stats: TurnStats;
|
|
310
|
+
}
|
|
311
|
+
interface TraceTimelineView {
|
|
312
|
+
/** 视图模型版本号,初始 = 1。字段变更时递增 */
|
|
313
|
+
schemaVersion: number;
|
|
314
|
+
sessionId: string;
|
|
315
|
+
totalDurationMs?: number;
|
|
316
|
+
turns: TraceTurnView[];
|
|
317
|
+
/** turn 归属不明的事件(TurnStartView 固定放每轮首条,不在此列) */
|
|
318
|
+
orphanEvents: TraceEventView[];
|
|
319
|
+
causalEdges: CausalEdge[];
|
|
320
|
+
/** 超 5000 事件截断标志 */
|
|
321
|
+
truncated?: boolean;
|
|
322
|
+
/** 截断时记录总事件数 */
|
|
323
|
+
totalEventCount?: number;
|
|
324
|
+
}
|
|
325
|
+
declare function dispatchEventView(seq: number, event: TraceEvent): TraceEventView;
|
|
326
|
+
/**
|
|
327
|
+
* TraceRecord 迭代器 → TraceTimelineView(全量转换 + 按 turn 聚合)。
|
|
328
|
+
*
|
|
329
|
+
* 全量物化——超大会话(>5000 事件)截断。
|
|
330
|
+
* 空 trace → turns:[], orphanEvents:[], causalEdges:[], totalDurationMs: undefined。
|
|
331
|
+
* causalEdges 调用 causalDag()(从 trace-views.ts 导入复用)。
|
|
332
|
+
* TurnStats 按计算公式表填充。
|
|
333
|
+
*/
|
|
334
|
+
declare function traceEventsToTimeline(records: AsyncIterable<TraceRecord> | Iterable<TraceRecord>): Promise<TraceTimelineView>;
|
|
335
|
+
//#endregion
|
|
336
|
+
export { type AgentErrorView, type CausalEdge, type CompactionView, type ContentPreviewItem, type ContextBuildView, type EffortChangeView, type GenericEventView, MAX_TIMELINE_EVENTS, type MaxTurnsView, type MemoryArchiveView, type MemoryPruneView, type ModelSwitchView, type PauseStateView, type PromptBeforeView, type RetryActionView, type StreamAuthFailedView, type StreamDeltaView, type StreamDoneView, type StreamRetryAccelView, type StreamRetryView, TRACE_VIEW_SCHEMA_VERSION, type ToolCallEndView, type ToolCallStartView, type ToolResultView, type TraceEventView, type TraceTimelineView, type TraceTurnView, type TruncatedView, type TurnEndView, type TurnStartView, type TurnStats, dispatchEventView, traceEventsToTimeline };
|
|
337
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/trace-event-view.ts"],"mappings":"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"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{causalDag as e}from"@x-otto/agent";const t=3,n=5e3,r={"prompt.before":({seq:e,ts:t,turn:n,p:r})=>({viewKind:`lifecycle:prompt.before`,seq:e,ts:t,turn:n,text:typeof r.text==`string`?r.text:``,isFirstMessage:!!r.isFirstMessage}),"context.build":({seq:e,ts:t,turn:n,p:r})=>({viewKind:`lifecycle:context.build`,seq:e,ts:t,turn:n,hasSummary:!!r.hasSummary,messageCount:typeof r.messages==`number`?r.messages:0}),"turn.start":({seq:e,ts:t,turn:n})=>({viewKind:`lifecycle:turn.start`,seq:e,ts:t,turn:n??0}),"turn.end":({seq:e,ts:t,turn:n,p:r})=>{let i=typeof r.usage==`object`&&r.usage!==null?r.usage:void 0;return{viewKind:`lifecycle:turn.end`,seq:e,ts:t,turn:n??0,stopReason:typeof r.stopReason==`string`?r.stopReason:void 0,inputTokens:i&&typeof i.inputTokens==`number`?i.inputTokens:void 0,outputTokens:i&&typeof i.outputTokens==`number`?i.outputTokens:void 0,model:typeof r.model==`string`?r.model:void 0,provider:typeof r.provider==`string`?r.provider:void 0}},"turn.truncated":({seq:e,ts:t,turn:n,p:r})=>({viewKind:`lifecycle:turn.truncated`,seq:e,ts:t,turn:n,action:c(r.action)?r.action:`continue`,maxTokens:typeof r.maxTokens==`number`?r.maxTokens:void 0,continuation:typeof r.continuation==`number`?String(r.continuation):void 0}),"compaction.reactive":({seq:e,ts:t,turn:n,p:r})=>({viewKind:`lifecycle:compaction.reactive`,seq:e,ts:t,turn:n,messagesBefore:typeof r.messagesBefore==`number`?r.messagesBefore:0,messagesAfter:typeof r.messagesAfter==`number`?r.messagesAfter:0}),"stream.retry":({seq:e,ts:t,turn:n,p:r})=>({viewKind:`lifecycle:stream.retry`,seq:e,ts:t,turn:n,attempt:typeof r.attempt==`number`?r.attempt:0,maxRetries:typeof r.maxRetries==`number`?r.maxRetries:0,delayMs:typeof r.delayMs==`number`?r.delayMs:0,reason:typeof r.reason==`string`?r.reason:``}),"stream.retry.accelerated":({seq:e,ts:t,turn:n,p:r})=>({viewKind:`lifecycle:stream.retry.accelerated`,seq:e,ts:t,turn:n,attempt:typeof r.attempt==`number`?r.attempt:0,maxRetries:typeof r.maxRetries==`number`?r.maxRetries:0,elapsedMs:typeof r.elapsedMs==`number`?r.elapsedMs:0}),"stream.auth-failed":({seq:e,ts:t,turn:n,p:r})=>({viewKind:`lifecycle:stream.auth-failed`,seq:e,ts:t,turn:n,provider:typeof r.provider==`string`?r.provider:``,modelId:typeof r.modelId==`string`?r.modelId:``,reason:typeof r.reason==`string`?r.reason:``,errorType:typeof r.errorType==`string`?r.errorType:`server-rejected`}),"agent.error":({seq:e,ts:t,turn:n,p:r})=>({viewKind:`lifecycle:agent.error`,seq:e,ts:t,turn:n,message:typeof r.message==`string`?r.message:``}),"memory.prune":({seq:e,ts:t,turn:n,p:r})=>({viewKind:`lifecycle:memory.prune`,seq:e,ts:t,turn:n,trigger:typeof r.trigger==`string`?r.trigger:``,prunedCount:typeof r.prunedCount==`number`?r.prunedCount:void 0,tokensSaved:typeof r.tokensSaved==`number`?r.tokensSaved:void 0,messagesBefore:typeof r.messagesBefore==`number`?r.messagesBefore:void 0,messagesAfter:typeof r.messagesAfter==`number`?r.messagesAfter:void 0}),"memory.archive":({seq:e,ts:t,turn:n,p:r})=>({viewKind:`lifecycle:memory.archive`,seq:e,ts:t,turn:n,trigger:typeof r.trigger==`string`?r.trigger:``,archivePath:typeof r.archivePath==`string`?r.archivePath:void 0,archivedCount:typeof r.archivedCount==`number`?r.archivedCount:void 0}),"max.turns":({seq:e,ts:t,turn:n,p:r})=>({viewKind:`lifecycle:max.turns`,seq:e,ts:t,turn:n,maxTurns:typeof r.maxTurns==`number`?r.maxTurns:0,extended:typeof r.extended==`boolean`?r.extended:void 0,steered:typeof r.steered==`boolean`?r.steered:void 0,progressDenied:typeof r.progressDenied==`boolean`?r.progressDenied:void 0,extension:typeof r.extension==`number`?r.extension:void 0,maxExtensions:typeof r.maxExtensions==`number`?r.maxExtensions:void 0}),"model.switch":({seq:e,ts:t,turn:n,p:r})=>({viewKind:`lifecycle:model.switch`,seq:e,ts:t,turn:n,from:typeof r.from==`string`?r.from:null,to:typeof r.to==`string`?r.to:``,label:typeof r.label==`string`?r.label:``}),"effort.change":({seq:e,ts:t,turn:n,p:r})=>({viewKind:`lifecycle:effort.change`,seq:e,ts:t,turn:n,from:typeof r.from==`string`?r.from:null,to:typeof r.to==`string`?r.to:``,label:typeof r.label==`string`?r.label:``}),"retry.queued":({seq:e,ts:t,turn:n,p:r})=>({viewKind:`lifecycle:retry.queued`,seq:e,ts:t,turn:n,attempt:typeof r.attempt==`number`?r.attempt:void 0,max:typeof r.max==`number`?r.max:void 0,mode:typeof r.mode==`string`?r.mode:void 0,reason:typeof r.reason==`string`?r.reason:void 0}),"retry.cancelled":({seq:e,ts:t,turn:n,p:r})=>({viewKind:`lifecycle:retry.cancelled`,seq:e,ts:t,turn:n,attempt:typeof r.attempt==`number`?r.attempt:void 0}),"retry.reset":({seq:e,ts:t,turn:n})=>({viewKind:`lifecycle:retry.reset`,seq:e,ts:t,turn:n}),"pause.requested":({seq:e,ts:t,turn:n,p:r})=>({viewKind:`lifecycle:pause.requested`,seq:e,ts:t,turn:n,source:typeof r.source==`string`?r.source:void 0}),"pause.paused":({seq:e,ts:t,turn:n,p:r})=>({viewKind:`lifecycle:pause.paused`,seq:e,ts:t,turn:n,source:typeof r.source==`string`?r.source:void 0}),"pause.cancelled":({seq:e,ts:t,turn:n,p:r})=>({viewKind:`lifecycle:pause.cancelled`,seq:e,ts:t,turn:n,source:typeof r.source==`string`?r.source:void 0}),"pause.resumed":({seq:e,ts:t,turn:n,p:r})=>({viewKind:`lifecycle:pause.resumed`,seq:e,ts:t,turn:n,source:typeof r.source==`string`?r.source:void 0})},i={"tool.call.start":({seq:e,ts:t,turn:n,p:r})=>({viewKind:`tool:call.start`,seq:e,ts:t,turn:n??0,id:typeof r.id==`string`?r.id:``,name:typeof r.name==`string`?r.name:``,arguments:typeof r.arguments==`string`?r.arguments:JSON.stringify(r.arguments??``),toolCallIndex:typeof r.toolCallIndex==`number`?r.toolCallIndex:0}),"tool.call.end":({seq:e,ts:t,turn:n,p:r})=>{let i=typeof r.resultSummary==`object`&&r.resultSummary!==null?r.resultSummary:void 0;return{viewKind:`tool:call.end`,seq:e,ts:t,turn:n??0,id:typeof r.id==`string`?r.id:``,name:typeof r.name==`string`?r.name:``,durationMs:typeof r.durationMs==`number`?r.durationMs:void 0,result:l(i),errorCategory:typeof r.errorCategory==`string`?r.errorCategory:void 0,toolCallIndex:typeof r.toolCallIndex==`number`?r.toolCallIndex:0}}};function a(e,t,n,r,i,a){return{viewKind:`generic`,seq:e,ts:t,turn:n,eventKind:r,node:i,payload:a}}function o(e,t){let{ts:n,kind:o,node:s,turn:c,payload:l}=t;if(typeof l!=`object`||!l)return a(e,n,c,o,s,{});let u=l,d={seq:e,ts:n,turn:c,p:u};if(o===`lifecycle`){let t=s?r[s]:void 0;return t?t(d):a(e,n,c,o,s,u)}if(o===`stream`&&s===`stream.event`){let t=typeof u.type==`string`?u.type:``;return t===`text_delta`||t===`thinking_delta`?{viewKind:`stream:delta`,seq:e,ts:n,turn:c,deltaType:t,text:typeof u.delta==`string`?u.delta:``}:t===`done`||t===`message_stop`?{viewKind:`stream:done`,seq:e,ts:n,turn:c,reason:typeof u.reason==`string`?u.reason:void 0}:a(e,n,c,o,s,u)}if(o===`tool`){let t=s?i[s]:void 0;return t?t(d):a(e,n,c,o,s,u)}return a(e,n,c,o,s,u)}async function s(t){let r=[];for await(let e of t){if(r.length>=5e3)break;r.push(e)}let i=r.length>=n,a=r.length;if(r.length===0)return{schemaVersion:3,sessionId:``,turns:[],orphanEvents:[],causalEdges:[],truncated:!1};let s=r[0].entry.sessionId,c=r.map(({seq:e,entry:t})=>o(e,t)),l=new Map,d=[];for(let e of c)if(e.turn!==void 0){let t=l.get(e.turn);t?t.push(e):l.set(e.turn,[e])}else d.push(e);let f=[...l.keys()].sort((e,t)=>e-t).map(e=>{let t=l.get(e),n=u(t),r=t.map(e=>e.ts);return{turnIndex:e,startTs:Math.min(...r),endTs:Math.max(...r),events:t,stats:n}}),p=c.map(e=>e.ts);return{schemaVersion:3,sessionId:s,totalDurationMs:p.length>0?Math.max(...p)-Math.min(...p):void 0,turns:f,orphanEvents:d,causalEdges:e(r).edges.map(e=>({fromSeq:Number(e.from),toSeq:Number(e.to)})),truncated:i,totalEventCount:i?a:void 0}}function c(e){return e===`escalate`||e===`continue`||e===`refusal`}function l(e){return e?{isError:!!e.isError,contentCount:typeof e.contentCount==`number`?e.contentCount:0,contentPreview:Array.isArray(e.contentPreview)?e.contentPreview:[],details:typeof e.details==`object`&&e.details!==null?e.details:void 0}:{isError:!1,contentCount:0,contentPreview:[]}}function u(e){let t=0,n=0,r=0,i=0,a,o,s;for(let c of e)switch(c.viewKind){case`tool:call.end`:t++;break;case`lifecycle:agent.error`:n++;break;case`lifecycle:stream.retry`:r++;break;case`lifecycle:stream.auth-failed`:i++;break;case`lifecycle:turn.end`:a===void 0&&(a=c.inputTokens),o===void 0&&(o=c.outputTokens),s===void 0&&(s=c.stopReason);break}let c=e.map(e=>e.ts),l=c.length>1?Math.max(...c)-Math.min(...c):0;return{eventCount:e.length,toolCallCount:t,inputTokens:a,outputTokens:o,durationMs:l,stopReason:s,errorCount:n,retryCount:r,authFailureCount:i}}export{n as MAX_TIMELINE_EVENTS,t as TRACE_VIEW_SCHEMA_VERSION,o as dispatchEventView,s as traceEventsToTimeline};
|
|
2
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/trace-event-view.ts"],"sourcesContent":["/**\n * trace-event-view.ts — 结构化 Trace 视图模型层 (RFC-060)\n *\n * 提供 TraceEvent → TraceEventView 的分派转换,以及 TraceRecord → TraceTimelineView\n * 的按 turn 聚合。Web 和 TUI 消费同一模型,避免重复实现。\n *\n * 命名:trace-event-view.ts(非 trace-view.ts),避免与现有 trace-views.ts 文件名冲突。\n */\n\nimport type { TraceEvent, TraceRecord } from '@x-otto/agent'\nimport { causalDag } from '@x-otto/agent'\n\n// ════════════════════════════════════════════════\n// 常量\n// ════════════════════════════════════════════════\n\n/** 视图模型版本号,初始 = 1。字段变更时递增。 */\n// v2:TurnStats 新增 authFailureCount 字段(stream.auth-failed 认证失败计数,见 RFC-060\n// §TurnStats 计算公式表)。向后兼容扩展(加字段不破坏既有消费方解析),按 RFC-060 设计\n// 文档\"字段变更时递增\"的既定规则递增。\n// v3:StreamAuthFailedView 新增 errorType 字段(RFC-139 D3——credential-missing /\n// refresh-failed / server-rejected 三值细分;旧 trace 缺字段回退 server-rejected)。\nexport const TRACE_VIEW_SCHEMA_VERSION = 3\n\n/** traceEventsToTimeline() 最大处理事件数。超限返回 truncated: true。 */\nexport const MAX_TIMELINE_EVENTS = 5000\n\n// ════════════════════════════════════════════════\n// ToolResultView(复用 compressToolResult 形状)\n// ════════════════════════════════════════════════\n\nexport interface ContentPreviewItem {\n type: string\n text?: string\n preview?: string\n totalLength?: number\n}\n\nexport interface ToolResultView {\n isError: boolean\n contentCount: number\n contentPreview: ContentPreviewItem[]\n details?: Record<string, unknown>\n}\n\n// ════════════════════════════════════════════════\n// 视图类型定义(15 种核心 viewKind + GenericEventView 兜底)\n// ════════════════════════════════════════════════\n\n/** 源: kind='lifecycle' node='prompt.before'\n * payload → { sessionId, isFirstMessage, text } */\nexport interface PromptBeforeView {\n viewKind: 'lifecycle:prompt.before'\n seq: number\n ts: number\n turn?: number\n text: string\n isFirstMessage: boolean\n}\n\n/** 源: kind='lifecycle' node='context.build'\n * payload → { sessionId, hasSummary, messages }(messages 为数量) */\nexport interface ContextBuildView {\n viewKind: 'lifecycle:context.build'\n seq: number\n ts: number\n turn?: number\n hasSummary: boolean\n messageCount: number // ← payload.messages (消息数量)\n}\n\n/** 源: kind='lifecycle' node='turn.start'\n * payload → {}(无载荷) */\nexport interface TurnStartView {\n viewKind: 'lifecycle:turn.start'\n seq: number\n ts: number\n turn: number\n}\n\n/** 源: kind='lifecycle' node='turn.end'\n * payload → { stopReason, usage: { inputTokens, outputTokens }, model, provider }\n * model/provider 自 2026-07-30 起随 payload 落盘(同一会话可跨回合切模型,回放需按\n * model 归因单个回合);旧 trace 无此字段 → undefined(白名单拾取,不抛)。 */\nexport interface TurnEndView {\n viewKind: 'lifecycle:turn.end'\n seq: number\n ts: number\n turn: number\n stopReason?: string\n inputTokens?: number // ← payload.usage.inputTokens\n outputTokens?: number // ← payload.usage.outputTokens\n /** 本回合实际执行的模型 id;旧 trace(无该字段)为 undefined。 */\n model?: string // ← payload.model\n /** 本回合实际执行的 provider;旧 trace 为 undefined。 */\n provider?: string // ← payload.provider\n}\n\n/** 源: kind='stream' node='stream.event'\n * payload.type in ('text_delta','thinking_delta') → StreamDeltaView\n * payload.delta → text */\nexport interface StreamDeltaView {\n viewKind: 'stream:delta'\n seq: number\n ts: number\n turn?: number\n deltaType: 'text_delta' | 'thinking_delta'\n text: string // ← payload.delta\n}\n\n/** 源: kind='stream' node='stream.event'\n * payload.type in ('done','message_stop') → StreamDoneView\n * payload.reason → reason */\nexport interface StreamDoneView {\n viewKind: 'stream:done'\n seq: number\n ts: number\n turn?: number\n reason?: string // ← payload.reason\n}\n\n/** 源: kind='tool' node='tool.call.start'\n * payload → { id, name, arguments, turnIndex, toolCallIndex } */\nexport interface ToolCallStartView {\n viewKind: 'tool:call.start'\n seq: number\n ts: number\n turn: number\n id: string\n name: string\n arguments: string\n toolCallIndex: number\n}\n\n/** 源: kind='tool' node='tool.call.end'\n * payload → { id, name, resultSummary, durationMs, errorCategory, toolCallIndex } */\nexport interface ToolCallEndView {\n viewKind: 'tool:call.end'\n seq: number\n ts: number\n turn: number\n id: string\n name: string\n durationMs?: number\n result: ToolResultView // ← payload.resultSummary (compressToolResult 形状)\n errorCategory?: string\n toolCallIndex: number\n}\n\n/** 源: kind='lifecycle' node='turn.truncated'\n * payload → { action, maxTokens?, continuation? } */\nexport interface TruncatedView {\n viewKind: 'lifecycle:turn.truncated'\n seq: number\n ts: number\n turn?: number\n action: 'escalate' | 'continue' | 'refusal'\n maxTokens?: number\n continuation?: string\n}\n\n/** 源: kind='lifecycle' node='compaction.reactive'\n * payload → { messagesBefore, messagesAfter } */\nexport interface CompactionView {\n viewKind: 'lifecycle:compaction.reactive'\n seq: number\n ts: number\n turn?: number\n messagesBefore: number\n messagesAfter: number\n}\n\n/** 源: kind='lifecycle' node='stream.retry'\n * payload → { attempt, maxRetries, delayMs, reason } */\nexport interface StreamRetryView {\n viewKind: 'lifecycle:stream.retry'\n seq: number\n ts: number\n turn?: number\n attempt: number\n maxRetries: number\n delayMs: number\n reason: string\n}\n\n/** 源: kind='lifecycle' node='stream.retry.accelerated'\n * payload → { attempt, maxRetries, elapsedMs } */\nexport interface StreamRetryAccelView {\n viewKind: 'lifecycle:stream.retry.accelerated'\n seq: number\n ts: number\n turn?: number\n attempt: number\n maxRetries: number\n elapsedMs: number // ← payload.elapsedMs\n}\n\n/** 源: kind='lifecycle' node='stream.auth-failed'\n * payload → { provider, modelId, reason, errorType }\n * errorType(RFC-139 D3,schema v3):credential-missing / refresh-failed / server-rejected;\n * 旧 trace 无此字段时回退 'server-rejected'(与分类器的无 code 回退一致)。 */\nexport interface StreamAuthFailedView {\n viewKind: 'lifecycle:stream.auth-failed'\n seq: number\n ts: number\n turn?: number\n provider: string\n modelId: string\n reason: string\n errorType: string\n}\n\n/** 源: kind='lifecycle' node='agent.error'\n * payload → { message } */\nexport interface AgentErrorView {\n viewKind: 'lifecycle:agent.error'\n seq: number\n ts: number\n turn?: number\n message: string\n}\n\n/** 源: kind='lifecycle' node='memory.prune'\n * payload → { trigger, prunedCount?, tokensSaved?, messagesBefore?, messagesAfter? } */\nexport interface MemoryPruneView {\n viewKind: 'lifecycle:memory.prune'\n seq: number\n ts: number\n turn?: number\n trigger: string\n prunedCount?: number\n tokensSaved?: number\n messagesBefore?: number\n messagesAfter?: number\n}\n\n/** 源: kind='lifecycle' node='memory.archive'\n * payload → { trigger, archivePath?, archivedCount? } */\nexport interface MemoryArchiveView {\n viewKind: 'lifecycle:memory.archive'\n seq: number\n ts: number\n turn?: number\n trigger: string\n archivePath?: string\n archivedCount?: number\n}\n\n/** 源: kind='lifecycle' node='max.turns'\n * payload → { maxTurns, extended, steered, extension?, maxExtensions? }\n * extension 和 maxExtensions 为「派生字段」:dispatchEventView 从 payload 取值,\n * traceEventsToTimeline 可跨 turn 补全。 */\nexport interface MaxTurnsView {\n viewKind: 'lifecycle:max.turns'\n seq: number\n ts: number\n turn?: number\n maxTurns: number\n extended?: boolean\n steered?: boolean\n /** RFC-104 D1:窗口无进展被闸门拒绝(区别于额度耗尽)。 */\n progressDenied?: boolean\n extension?: number\n maxExtensions?: number\n}\n\n/** 源: kind='lifecycle' node='model.switch'\n * payload → { from, to, label }——CLI 侧 applyModel 三层记录(model-effort-record.ts)。 */\nexport interface ModelSwitchView {\n viewKind: 'lifecycle:model.switch'\n seq: number\n ts: number\n turn?: number\n /** 旧模型 id(首次切换/未命中时可为 null)。 */\n from: string | null\n /** 新模型 id。 */\n to: string\n /** 格式化显示串(如 \"claude-sonnet-5 (200K)\")。 */\n label: string\n}\n\n/** 源: kind='lifecycle' node='effort.change'\n * payload → { from, to, label }——CLI 侧 applyEffort 三层记录(model-effort-record.ts)。 */\nexport interface EffortChangeView {\n viewKind: 'lifecycle:effort.change'\n seq: number\n ts: number\n turn?: number\n /** 旧档位(首次切换可为 null)。 */\n from: string | null\n /** 新档位。 */\n to: string\n /** 格式化显示串(如 \"High\")。 */\n label: string\n}\n\n/** 源: kind='lifecycle' node='retry.queued|cancelled|reset'——CLI 侧回合级重试卡动作\n * (ErrorRetryCard,model-effort-record.ts)。 */\nexport interface RetryActionView {\n viewKind: 'lifecycle:retry.queued' | 'lifecycle:retry.cancelled' | 'lifecycle:retry.reset'\n seq: number\n ts: number\n turn?: number\n /** retry.queued:本次为第几次(1-based);retry.cancelled:取消时的已重试次数。 */\n attempt?: number\n /** retry.queued:per-prompt 总配额。 */\n max?: number\n /** retry.queued:重试模式(auto=30s×N 自动 / resetsAt=官方倒计时 / manual=手动)。 */\n mode?: string\n /** retry.queued:触发错误原因(截断)。 */\n reason?: string\n}\n\n/** 源: kind='lifecycle' node='pause.requested|paused|cancelled|resumed'——流程暂停四态\n * (PauseDialog / turn 边界暂停,interactive-events.ts 事件驱动记录)。 */\nexport interface PauseStateView {\n viewKind: 'lifecycle:pause.requested' | 'lifecycle:pause.paused' | 'lifecycle:pause.cancelled' | 'lifecycle:pause.resumed'\n seq: number\n ts: number\n turn?: number\n /** 触发入口(如 'session-event')。 */\n source?: string\n}\n\n/** 兜底:不匹配上述任何 kind×node 组合的事件。\n * 包含: loop.start/end, prompt.after, agent.done/aborted, messages.persist,\n * rate.limit.exhausted, 未知节点等。 */\nexport interface GenericEventView {\n viewKind: 'generic'\n seq: number\n ts: number\n turn?: number\n eventKind: string\n node?: string\n payload: Record<string, unknown>\n}\n\n// ════════════════════════════════════════════════\n// discriminated union\n// ════════════════════════════════════════════════\n\nexport type TraceEventView =\n | PromptBeforeView\n | ContextBuildView\n | TurnStartView\n | TurnEndView\n | StreamDeltaView\n | StreamDoneView\n | ToolCallStartView\n | ToolCallEndView\n | TruncatedView\n | CompactionView | StreamRetryView\n | StreamRetryAccelView\n | StreamAuthFailedView\n | AgentErrorView\n | MemoryPruneView\n | MemoryArchiveView\n | MaxTurnsView\n | ModelSwitchView\n | EffortChangeView\n | RetryActionView\n | PauseStateView\n | GenericEventView\n\n// ════════════════════════════════════════════════\n// 聚合类型\n// ════════════════════════════════════════════════\n\nexport interface TurnStats {\n eventCount: number\n toolCallCount: number\n inputTokens?: number\n outputTokens?: number\n durationMs?: number\n stopReason?: string\n errorCount: number\n retryCount: number\n /** 本轮内 stream.auth-failed 事件次数(对抗审核 finding B:与 retryCount 同粒度统计,\n * 否则 `otto observe --json` 结构化输出路径只能看展示层文本摘要,看不到统计维度)。 */\n authFailureCount: number\n}\n\nexport interface CausalEdge {\n fromSeq: number\n toSeq: number\n}\n\nexport interface TraceTurnView {\n turnIndex: number\n startTs: number\n endTs?: number\n events: TraceEventView[]\n stats: TurnStats\n}\n\nexport interface TraceTimelineView {\n /** 视图模型版本号,初始 = 1。字段变更时递增 */\n schemaVersion: number\n sessionId: string\n totalDurationMs?: number\n turns: TraceTurnView[]\n /** turn 归属不明的事件(TurnStartView 固定放每轮首条,不在此列) */\n orphanEvents: TraceEventView[]\n causalEdges: CausalEdge[]\n /** 超 5000 事件截断标志 */\n truncated?: boolean\n /** 截断时记录总事件数 */\n totalEventCount?: number\n}\n\n// ════════════════════════════════════════════════\n// dispatchEventView —— 单一事件分派\n// ════════════════════════════════════════════════\n\n/**\n * TraceEvent → TraceEventView(单一事件分派)。\n * 确定性:同一输入始终返回相同输出。\n *\n * 内部先 guard typeof event.payload === 'object'。\n * 无匹配的分支回退到 GenericEventView —— 不抛异常。\n * 缺失字段设 undefined,不设默认值 0(区分「无数据」与「零值」)。\n *\n * 实现为注册表模式(F10,post-RFC-092 review):`lifecycle`/`tool` 两个 kind 各自按 node\n * 建一张 `node → 转换函数` 的 Map,新增 viewKind 只需在对应表里加一条目,不改分派主体。\n * `stream` kind 按 payload.type(非 node)分派,量少(2 种)且非新增热点,维持内联分支。\n */\ninterface DispatchContext {\n seq: number\n ts: number\n turn: number | undefined\n p: Record<string, unknown>\n}\n\ntype ViewBuilder = (ctx: DispatchContext) => TraceEventView\n\nconst LIFECYCLE_VIEW_BUILDERS: Record<string, ViewBuilder> = {\n 'prompt.before': ({ seq, ts, turn, p }) => ({\n viewKind: 'lifecycle:prompt.before',\n seq,\n ts,\n turn,\n text: typeof p.text === 'string' ? p.text : '',\n isFirstMessage: Boolean(p.isFirstMessage),\n }),\n\n 'context.build': ({ seq, ts, turn, p }) => ({\n viewKind: 'lifecycle:context.build',\n seq,\n ts,\n turn,\n hasSummary: Boolean(p.hasSummary),\n messageCount: typeof p.messages === 'number' ? p.messages : 0,\n }),\n\n 'turn.start': ({ seq, ts, turn }) => ({\n viewKind: 'lifecycle:turn.start',\n seq,\n ts,\n turn: turn ?? 0,\n }),\n\n 'turn.end': ({ seq, ts, turn, p }) => {\n const usage =\n typeof p.usage === 'object' && p.usage !== null\n ? (p.usage as Record<string, unknown>)\n : undefined\n return {\n viewKind: 'lifecycle:turn.end',\n seq,\n ts,\n turn: turn ?? 0,\n stopReason: typeof p.stopReason === 'string' ? p.stopReason : undefined,\n inputTokens:\n usage && typeof usage.inputTokens === 'number' ? usage.inputTokens : undefined,\n outputTokens:\n usage && typeof usage.outputTokens === 'number' ? usage.outputTokens : undefined,\n model: typeof p.model === 'string' ? p.model : undefined,\n provider: typeof p.provider === 'string' ? p.provider : undefined,\n }\n },\n\n 'turn.truncated': ({ seq, ts, turn, p }) => ({\n viewKind: 'lifecycle:turn.truncated',\n seq,\n ts,\n turn,\n action: isTruncatedAction(p.action) ? p.action : 'continue',\n maxTokens: typeof p.maxTokens === 'number' ? p.maxTokens : undefined,\n continuation: typeof p.continuation === 'number' ? String(p.continuation) : undefined,\n }),\n\n 'compaction.reactive': ({ seq, ts, turn, p }) => ({\n viewKind: 'lifecycle:compaction.reactive',\n seq,\n ts,\n turn,\n messagesBefore: typeof p.messagesBefore === 'number' ? p.messagesBefore : 0,\n messagesAfter: typeof p.messagesAfter === 'number' ? p.messagesAfter : 0,\n }),\n\n 'stream.retry': ({ seq, ts, turn, p }) => ({\n viewKind: 'lifecycle:stream.retry',\n seq,\n ts,\n turn,\n attempt: typeof p.attempt === 'number' ? p.attempt : 0,\n maxRetries: typeof p.maxRetries === 'number' ? p.maxRetries : 0,\n delayMs: typeof p.delayMs === 'number' ? p.delayMs : 0,\n reason: typeof p.reason === 'string' ? p.reason : '',\n }),\n\n 'stream.retry.accelerated': ({ seq, ts, turn, p }) => ({\n viewKind: 'lifecycle:stream.retry.accelerated',\n seq,\n ts,\n turn,\n attempt: typeof p.attempt === 'number' ? p.attempt : 0,\n maxRetries: typeof p.maxRetries === 'number' ? p.maxRetries : 0,\n elapsedMs: typeof p.elapsedMs === 'number' ? p.elapsedMs : 0,\n }),\n\n 'stream.auth-failed': ({ seq, ts, turn, p }) => ({\n viewKind: 'lifecycle:stream.auth-failed',\n seq,\n ts,\n turn,\n provider: typeof p.provider === 'string' ? p.provider : '',\n modelId: typeof p.modelId === 'string' ? p.modelId : '',\n reason: typeof p.reason === 'string' ? p.reason : '',\n // RFC-139 D3(v3):旧 trace 无 errorType 时回退 server-rejected(同分类器无 code 回退)\n errorType: typeof p.errorType === 'string' ? p.errorType : 'server-rejected',\n }),\n\n 'agent.error': ({ seq, ts, turn, p }) => ({\n viewKind: 'lifecycle:agent.error',\n seq,\n ts,\n turn,\n message: typeof p.message === 'string' ? p.message : '',\n }),\n\n 'memory.prune': ({ seq, ts, turn, p }) => ({\n viewKind: 'lifecycle:memory.prune',\n seq,\n ts,\n turn,\n trigger: typeof p.trigger === 'string' ? p.trigger : '',\n prunedCount: typeof p.prunedCount === 'number' ? p.prunedCount : undefined,\n tokensSaved: typeof p.tokensSaved === 'number' ? p.tokensSaved : undefined,\n messagesBefore: typeof p.messagesBefore === 'number' ? p.messagesBefore : undefined,\n messagesAfter: typeof p.messagesAfter === 'number' ? p.messagesAfter : undefined,\n }),\n\n 'memory.archive': ({ seq, ts, turn, p }) => ({\n viewKind: 'lifecycle:memory.archive',\n seq,\n ts,\n turn,\n trigger: typeof p.trigger === 'string' ? p.trigger : '',\n archivePath: typeof p.archivePath === 'string' ? p.archivePath : undefined,\n archivedCount: typeof p.archivedCount === 'number' ? p.archivedCount : undefined,\n }),\n\n 'max.turns': ({ seq, ts, turn, p }) => ({\n viewKind: 'lifecycle:max.turns',\n seq,\n ts,\n turn,\n maxTurns: typeof p.maxTurns === 'number' ? p.maxTurns : 0,\n extended: typeof p.extended === 'boolean' ? p.extended : undefined,\n steered: typeof p.steered === 'boolean' ? p.steered : undefined,\n progressDenied: typeof p.progressDenied === 'boolean' ? p.progressDenied : undefined,\n extension: typeof p.extension === 'number' ? p.extension : undefined,\n maxExtensions: typeof p.maxExtensions === 'number' ? p.maxExtensions : undefined,\n }),\n\n // 终局(2026-07-31):model/effort 切换审计——CLI 侧 applyModel/applyEffort 经\n // model-effort-record.ts 写入,trace-view 显式建模(from→to),不再走 generic 兜底。\n 'model.switch': ({ seq, ts, turn, p }) => ({\n viewKind: 'lifecycle:model.switch',\n seq,\n ts,\n turn,\n from: typeof p.from === 'string' ? p.from : null,\n to: typeof p.to === 'string' ? p.to : '',\n label: typeof p.label === 'string' ? p.label : '',\n }),\n\n 'effort.change': ({ seq, ts, turn, p }) => ({\n viewKind: 'lifecycle:effort.change',\n seq,\n ts,\n turn,\n from: typeof p.from === 'string' ? p.from : null,\n to: typeof p.to === 'string' ? p.to : '',\n label: typeof p.label === 'string' ? p.label : '',\n }),\n\n // CLI 回合级重试卡动作(ErrorRetryCard)\n 'retry.queued': ({ seq, ts, turn, p }) => ({\n viewKind: 'lifecycle:retry.queued',\n seq,\n ts,\n turn,\n attempt: typeof p.attempt === 'number' ? p.attempt : undefined,\n max: typeof p.max === 'number' ? p.max : undefined,\n mode: typeof p.mode === 'string' ? p.mode : undefined,\n reason: typeof p.reason === 'string' ? p.reason : undefined,\n }),\n 'retry.cancelled': ({ seq, ts, turn, p }) => ({\n viewKind: 'lifecycle:retry.cancelled',\n seq,\n ts,\n turn,\n attempt: typeof p.attempt === 'number' ? p.attempt : undefined,\n }),\n 'retry.reset': ({ seq, ts, turn }) => ({\n viewKind: 'lifecycle:retry.reset',\n seq,\n ts,\n turn,\n }),\n\n // 流程暂停四态(PauseDialog / turn 边界暂停)\n 'pause.requested': ({ seq, ts, turn, p }) => ({\n viewKind: 'lifecycle:pause.requested',\n seq,\n ts,\n turn,\n source: typeof p.source === 'string' ? p.source : undefined,\n }),\n 'pause.paused': ({ seq, ts, turn, p }) => ({\n viewKind: 'lifecycle:pause.paused',\n seq,\n ts,\n turn,\n source: typeof p.source === 'string' ? p.source : undefined,\n }),\n 'pause.cancelled': ({ seq, ts, turn, p }) => ({\n viewKind: 'lifecycle:pause.cancelled',\n seq,\n ts,\n turn,\n source: typeof p.source === 'string' ? p.source : undefined,\n }),\n 'pause.resumed': ({ seq, ts, turn, p }) => ({\n viewKind: 'lifecycle:pause.resumed',\n seq,\n ts,\n turn,\n source: typeof p.source === 'string' ? p.source : undefined,\n }),\n}\n\nconst TOOL_VIEW_BUILDERS: Record<string, ViewBuilder> = {\n 'tool.call.start': ({ seq, ts, turn, p }) => ({\n viewKind: 'tool:call.start',\n seq,\n ts,\n turn: turn ?? 0,\n id: typeof p.id === 'string' ? p.id : '',\n name: typeof p.name === 'string' ? p.name : '',\n arguments: typeof p.arguments === 'string' ? p.arguments : JSON.stringify(p.arguments ?? ''),\n toolCallIndex: typeof p.toolCallIndex === 'number' ? p.toolCallIndex : 0,\n }),\n\n 'tool.call.end': ({ seq, ts, turn, p }) => {\n const resultSummary =\n typeof p.resultSummary === 'object' && p.resultSummary !== null\n ? (p.resultSummary as Record<string, unknown>)\n : undefined\n return {\n viewKind: 'tool:call.end',\n seq,\n ts,\n turn: turn ?? 0,\n id: typeof p.id === 'string' ? p.id : '',\n name: typeof p.name === 'string' ? p.name : '',\n durationMs: typeof p.durationMs === 'number' ? p.durationMs : undefined,\n result: buildToolResultView(resultSummary),\n errorCategory: typeof p.errorCategory === 'string' ? p.errorCategory : undefined,\n toolCallIndex: typeof p.toolCallIndex === 'number' ? p.toolCallIndex : 0,\n }\n },\n}\n\nfunction genericView(\n seq: number,\n ts: number,\n turn: number | undefined,\n eventKind: string,\n node: string | undefined,\n payload: Record<string, unknown>,\n): GenericEventView {\n return { viewKind: 'generic', seq, ts, turn, eventKind, node, payload }\n}\n\nexport function dispatchEventView(seq: number, event: TraceEvent): TraceEventView {\n const { ts, kind, node, turn, payload } = event\n\n // Guard: payload 非 object → GenericEventView\n if (typeof payload !== 'object' || payload === null) {\n return genericView(seq, ts, turn, kind, node, {})\n }\n\n const p = payload as Record<string, unknown>\n const ctx: DispatchContext = { seq, ts, turn, p }\n\n if (kind === 'lifecycle') {\n const builder = node ? LIFECYCLE_VIEW_BUILDERS[node] : undefined\n return builder ? builder(ctx) : genericView(seq, ts, turn, kind, node, p)\n }\n\n // ── stream 分派:按 payload.type(非 node)分派,量少维持内联 ──\n if (kind === 'stream' && node === 'stream.event') {\n const type = typeof p.type === 'string' ? p.type : ''\n if (type === 'text_delta' || type === 'thinking_delta') {\n return {\n viewKind: 'stream:delta',\n seq,\n ts,\n turn,\n deltaType: type,\n text: typeof p.delta === 'string' ? p.delta : '',\n }\n }\n if (type === 'done' || type === 'message_stop') {\n return {\n viewKind: 'stream:done',\n seq,\n ts,\n turn,\n reason: typeof p.reason === 'string' ? p.reason : undefined,\n }\n }\n // 其他 stream.event 类型 → GenericEventView\n return genericView(seq, ts, turn, kind, node, p)\n }\n\n if (kind === 'tool') {\n const builder = node ? TOOL_VIEW_BUILDERS[node] : undefined\n return builder ? builder(ctx) : genericView(seq, ts, turn, kind, node, p)\n }\n\n // ── 兜底:所有未匹配的 kind(nondet, checkpoint, 未知)→ GenericEventView ──\n return genericView(seq, ts, turn, kind, node, p)\n}\n\n// ════════════════════════════════════════════════\n// traceEventsToTimeline —— 全量转换 + 按 turn 聚合\n// ════════════════════════════════════════════════\n\n/**\n * TraceRecord 迭代器 → TraceTimelineView(全量转换 + 按 turn 聚合)。\n *\n * 全量物化——超大会话(>5000 事件)截断。\n * 空 trace → turns:[], orphanEvents:[], causalEdges:[], totalDurationMs: undefined。\n * causalEdges 调用 causalDag()(从 trace-views.ts 导入复用)。\n * TurnStats 按计算公式表填充。\n */\nexport async function traceEventsToTimeline(\n records: AsyncIterable<TraceRecord> | Iterable<TraceRecord>,\n): Promise<TraceTimelineView> {\n // 物化全部事件(截断上限)\n const allRecords: TraceRecord[] = []\n for await (const record of records) {\n if (allRecords.length >= MAX_TIMELINE_EVENTS) {\n break\n }\n allRecords.push(record)\n }\n\n const truncated = allRecords.length >= MAX_TIMELINE_EVENTS\n // totalEventCount: 需要先迭代计数才能准确,这里用已物化数近似\n // (服务端可在调用前自己统计并通过 meta 传入,此处用物化数)\n const totalEventCount = allRecords.length\n\n if (allRecords.length === 0) {\n return {\n schemaVersion: TRACE_VIEW_SCHEMA_VERSION,\n sessionId: '',\n turns: [],\n orphanEvents: [],\n causalEdges: [],\n truncated: false,\n }\n }\n\n const sessionId = allRecords[0]!.entry.sessionId\n\n // dispatch 所有事件为视图\n const views: TraceEventView[] = allRecords.map(({ seq, entry }) =>\n dispatchEventView(seq, entry),\n )\n\n // 按 turn 分组\n const turnMap = new Map<number, TraceEventView[]>()\n const orphans: TraceEventView[] = []\n\n for (const view of views) {\n if (view.turn !== undefined) {\n const bucket = turnMap.get(view.turn)\n if (bucket) {\n bucket.push(view)\n } else {\n turnMap.set(view.turn, [view])\n }\n } else {\n orphans.push(view)\n }\n }\n\n const turnIndexes = [...turnMap.keys()].sort((a, b) => a - b)\n const turns: TraceTurnView[] = turnIndexes.map((turnIndex) => {\n const events = turnMap.get(turnIndex)!\n const stats = computeTurnStats(events)\n const timestamps = events.map((e) => e.ts)\n return {\n turnIndex,\n startTs: Math.min(...timestamps),\n endTs: Math.max(...timestamps),\n events,\n stats,\n }\n })\n\n const allTimestamps = views.map((e) => e.ts)\n const totalDurationMs =\n allTimestamps.length > 0\n ? Math.max(...allTimestamps) - Math.min(...allTimestamps)\n : undefined\n\n const dag = causalDag(allRecords)\n const causalEdges: CausalEdge[] = dag.edges.map((e) => ({\n fromSeq: Number(e.from),\n toSeq: Number(e.to),\n }))\n\n return {\n schemaVersion: TRACE_VIEW_SCHEMA_VERSION,\n sessionId,\n totalDurationMs,\n turns,\n orphanEvents: orphans,\n causalEdges,\n truncated,\n totalEventCount: truncated ? totalEventCount : undefined,\n }\n}\n\n// ════════════════════════════════════════════════\n// 内部辅助\n// ════════════════════════════════════════════════\n\nfunction isTruncatedAction(\n value: unknown,\n): value is 'escalate' | 'continue' | 'refusal' {\n return value === 'escalate' || value === 'continue' || value === 'refusal'\n}\n\nfunction buildToolResultView(\n summary: Record<string, unknown> | undefined,\n): ToolResultView {\n if (!summary) {\n return { isError: false, contentCount: 0, contentPreview: [] }\n }\n return {\n isError: Boolean(summary.isError),\n contentCount:\n typeof summary.contentCount === 'number' ? summary.contentCount : 0,\n contentPreview: Array.isArray(summary.contentPreview)\n ? (summary.contentPreview as ContentPreviewItem[])\n : [],\n details:\n typeof summary.details === 'object' && summary.details !== null\n ? (summary.details as Record<string, unknown>)\n : undefined,\n }\n}\n\nfunction computeTurnStats(events: TraceEventView[]): TurnStats {\n let toolCallCount = 0\n let errorCount = 0\n let retryCount = 0\n let authFailureCount = 0\n let inputTokens: number | undefined\n let outputTokens: number | undefined\n let stopReason: string | undefined\n\n for (const ev of events) {\n switch (ev.viewKind) {\n case 'tool:call.end':\n toolCallCount++\n break\n case 'lifecycle:agent.error':\n errorCount++\n break\n case 'lifecycle:stream.retry':\n retryCount++\n break\n case 'lifecycle:stream.auth-failed':\n authFailureCount++\n break\n case 'lifecycle:turn.end':\n if (inputTokens === undefined) inputTokens = ev.inputTokens\n if (outputTokens === undefined) outputTokens = ev.outputTokens\n if (stopReason === undefined) stopReason = ev.stopReason\n break\n }\n }\n\n const timestamps = events.map((e) => e.ts)\n const durationMs =\n timestamps.length > 1\n ? Math.max(...timestamps) - Math.min(...timestamps)\n : 0\n\n return {\n eventCount: events.length,\n toolCallCount,\n inputTokens,\n outputTokens,\n durationMs,\n stopReason,\n errorCount,\n retryCount,\n authFailureCount,\n }\n}\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@x-otto/trace-view",
|
|
3
|
+
"version": "0.0.1-alpha.0",
|
|
4
|
+
"files": [
|
|
5
|
+
"dist"
|
|
6
|
+
],
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public",
|
|
18
|
+
"registry": "https://registry.npmjs.org",
|
|
19
|
+
"tag": "alpha"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@x-otto/agent": "0.0.1-alpha.0"
|
|
23
|
+
},
|
|
24
|
+
"private": false,
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsdown",
|
|
27
|
+
"typecheck:project": "tsc -p tsconfig.json --noEmit",
|
|
28
|
+
"typecheck": "tsc --noEmit",
|
|
29
|
+
"clean": "rm -rf dist"
|
|
30
|
+
}
|
|
31
|
+
}
|