@wonanut/dsh-agent-lab 0.0.0-stage → 0.2.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.
@@ -0,0 +1,341 @@
1
+ /* ─────────────────────────────────────────────────────────────
2
+ * Agent 实验室 · 数据流转视图
3
+ * 把当前轮次的 turn.stages 渲染成一条可点击、可回放的流水线:
4
+ * 每一步的输入、上下文、模型响应、工具结果、记忆写入与输出都能单独查看。
5
+ * 数据全部来自 snapshot().turn.stages[].detail —— 与检查器同一份事实来源。
6
+ * ───────────────────────────────────────────────────────────── */
7
+
8
+ const DF_BUDGETS = [1000, 2000, 8000, 32000, 128000, 1048576];
9
+
10
+ const DF_STAGE_META = {
11
+ input: { icon: 'terminal', label: '输入', tone: 'neutral' },
12
+ context: { icon: 'layers', label: '上下文', tone: 'brand' },
13
+ compress: { icon: 'compress', label: '压缩', tone: 'warn' },
14
+ model: { icon: 'cpu', label: '模型', tone: 'violet' },
15
+ tool: { icon: 'tools', label: '工具', tone: 'warn' },
16
+ memory: { icon: 'memory', label: '记忆', tone: 'good' },
17
+ output: { icon: 'send', label: '输出', tone: 'good' },
18
+ };
19
+
20
+ const DF_EXPLAIN = {
21
+ input: '用户输入被记录为一条 user 消息,并创建本轮 turn。它后面的所有步骤都以这条消息为起点。',
22
+ context: '引擎把 system prompt、历史摘要、长期记忆、最近消息和本轮输入拼成一次模型请求。右侧「结构化数据」里的 segments 就是这次请求的 token 构成。',
23
+ compress: '上下文接近预算时,旧消息被折叠进历史摘要。before / after 是压缩前后的真实需求 token;removed 是被折叠的消息。',
24
+ model: '模型只做一次决策:返回一句最终回答,或者发起工具调用。真实模式下这里还能看到 provider、model、耗时与 token 用量。',
25
+ tool: '工具由插件执行,不是模型执行。模型只给出 name + args;结果作为 tool 消息写回上下文,供下一步模型读取。',
26
+ memory: 'memory_write 成功后,插件把这条事实写入跨会话长期记忆;下一轮组装上下文时可能被关键词召回。',
27
+ output: '最终回答写回会话并流式显示;同时重新组装一次上下文,让检查器看到本轮结束后的真实占用。',
28
+ };
29
+
30
+ const DF_CSS = `
31
+ .df-page {
32
+ --df-line: var(--al-border);
33
+ max-width: 1180px; margin: 0 auto; padding: 4px 4px 66px;
34
+ color: var(--al-text); font-size: 13px;
35
+ }
36
+ .df-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 18px; flex-wrap: wrap; padding: 14px 2px 18px; border-bottom: 1px solid var(--df-line); }
37
+ .df-eyebrow { font-family: var(--al-mono); font-size: 10px; letter-spacing: .16em; color: var(--al-ter); font-weight: 600; text-transform: uppercase; }
38
+ .df-title { margin: 7px 0 6px; font-size: 26px; line-height: 1.25; letter-spacing: -.02em; font-weight: 700; }
39
+ .df-sub { margin: 0; color: var(--al-sub); font-size: 12.5px; line-height: 1.7; max-width: 72ch; }
40
+ .df-head-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
41
+ .df-budget { display: inline-flex; align-items: center; gap: 6px; height: 32px; padding: 0 8px; border: 1px solid var(--al-border); border-radius: 9px; background: var(--al-layer); font-size: 11px; color: var(--al-sub); }
42
+ .df-budget select { height: 24px; border: 1px solid var(--al-border2); border-radius: 6px; background: var(--al-layer2); color: var(--al-text); font-size: 11px; padding: 0 4px; }
43
+ .df-controls { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; padding: 14px 2px 4px; }
44
+ .df-replay { display: flex; align-items: center; gap: 5px; }
45
+ .df-replay .al-btn { height: 30px; }
46
+ .df-stepinfo { font-size: 11px; color: var(--al-ter); font-variant-numeric: tabular-nums; }
47
+ .df-flow {
48
+ display: flex; align-items: stretch; gap: 0; overflow-x: auto; padding: 12px 2px 14px;
49
+ scrollbar-width: thin;
50
+ }
51
+ .df-node {
52
+ flex: none; width: 118px; display: flex; flex-direction: column; gap: 5px; align-items: flex-start;
53
+ padding: 9px 10px; border: 1px solid var(--al-border); border-radius: 10px;
54
+ background: color-mix(in srgb, var(--al-layer) 86%, transparent);
55
+ color: var(--al-text); text-align: left; position: relative;
56
+ transition: border-color .15s ease, background .15s ease, transform .15s ease, box-shadow .15s ease;
57
+ }
58
+ .df-node:hover { border-color: var(--al-border2); transform: translateY(-1px); }
59
+ .df-node.on { border-color: var(--al-accent); background: var(--al-accent-soft); box-shadow: 0 0 0 1px var(--al-accent) inset; }
60
+ .df-node .df-node-top { display: flex; align-items: center; gap: 5px; width: 100%; }
61
+ .df-node .df-node-kind { font-size: 9px; font-weight: 650; letter-spacing: .04em; color: var(--al-ter); }
62
+ .df-node.on .df-node-kind { color: var(--al-accent); }
63
+ .df-node .df-node-title { font-size: 11.5px; font-weight: 600; line-height: 1.35; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; width: 100%; }
64
+ .df-node .df-node-meta { font-size: 9.5px; color: var(--al-ter); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; width: 100%; font-variant-numeric: tabular-nums; }
65
+ .df-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--al-ter); flex: none; }
66
+ .df-dot.done { background: var(--al-good); }
67
+ .df-dot.active { background: var(--al-accent); animation: al-stage-pulse 1.6s infinite; }
68
+ .df-dot.error { background: var(--al-danger); }
69
+ .df-arrow { align-self: center; flex: none; padding: 0 4px; color: var(--al-ter); font-size: 12px; user-select: none; }
70
+ .df-body { display: grid; grid-template-columns: minmax(0, 1.45fr) minmax(260px, .85fr); gap: 13px; align-items: start; }
71
+ .df-card { border: 1px solid var(--al-border); border-radius: 13px; background: color-mix(in srgb, var(--al-layer) 88%, transparent); overflow: hidden; }
72
+ .df-card-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 9px 12px; border-bottom: 1px solid var(--al-border); }
73
+ .df-card-title { display: flex; align-items: center; gap: 6px; font-size: 12px; font-weight: 640; }
74
+ .df-card-title svg { color: var(--al-accent); }
75
+ .df-card-meta { font-size: 10px; color: var(--al-ter); font-variant-numeric: tabular-nums; }
76
+ .df-card-body { padding: 11px 12px 13px; }
77
+ .df-explain { margin: 0 0 10px; padding: 8px 10px; border-left: 3px solid var(--al-accent); border-radius: 0 7px 7px 0; background: var(--al-accent-soft); color: var(--al-sub); font-size: 11.5px; line-height: 1.65; }
78
+ .df-row { display: flex; align-items: flex-start; gap: 8px; padding: 4px 0; border-bottom: 1px dashed color-mix(in srgb, var(--al-border) 70%, transparent); }
79
+ .df-row:last-child { border-bottom: 0; }
80
+ .df-k { flex: none; width: 56px; padding-top: 8px; font-size: 10px; color: var(--al-ter); }
81
+ .df-v { flex: 1; min-width: 0; font-size: 11.5px; color: var(--al-text); word-break: break-word; }
82
+ .df-text { display: block; margin: 0; padding: 8px 9px; border-radius: 8px; background: var(--al-layer2); border: 1px solid var(--al-border); font-family: var(--al-mono); font-size: 11px; line-height: 1.65; white-space: pre-wrap; word-break: break-word; max-height: 240px; overflow: auto; }
83
+ .df-msgs { display: flex; flex-direction: column; gap: 5px; }
84
+ .df-msg { display: flex; gap: 6px; align-items: flex-start; padding: 4px 6px; border-radius: 6px; background: color-mix(in srgb, var(--al-layer2) 55%, transparent); }
85
+ .df-msg .al-role { flex: none; margin-top: 1px; }
86
+ .df-msg span:last-child { font-size: 10.5px; line-height: 1.55; color: var(--al-sub); word-break: break-word; }
87
+ .df-hits { display: flex; flex-direction: column; gap: 4px; }
88
+ .df-hit { font-size: 10.5px; padding: 5px 7px; border-radius: 6px; background: color-mix(in srgb, #14b8a6 8%, transparent); border: 1px solid color-mix(in srgb, #14b8a6 22%, transparent); }
89
+ .df-hit b { color: #0f766e; }
90
+ .df-note { margin-top: 10px; font-size: 10.5px; color: var(--al-ter); line-height: 1.6; }
91
+ .df-empty { padding: 50px 0; }
92
+ .df-raw .al-json { max-height: 420px; margin: 0; }
93
+ @media (max-width: 900px) {
94
+ .df-body { grid-template-columns: minmax(0, 1fr); }
95
+ .df-title { font-size: 22px; }
96
+ .df-node { width: 106px; }
97
+ }
98
+ `;
99
+
100
+ function dfToneClass(tone) {
101
+ return tone === 'brand' ? 'al-chip-brand'
102
+ : tone === 'warn' ? 'al-chip-warn'
103
+ : tone === 'good' ? 'al-chip-good'
104
+ : tone === 'violet' ? 'al-chip-violet' : '';
105
+ }
106
+
107
+ function dfRoleLabel(role) {
108
+ if (role === 'user') return 'user';
109
+ if (role === 'assistant') return 'assistant';
110
+ if (role === 'tool') return 'tool';
111
+ return role || 'system';
112
+ }
113
+
114
+ function dfTokenText(tokens) {
115
+ return tokens === null || tokens === undefined ? '—' : alFormatTokens(tokens) + ' tok';
116
+ }
117
+
118
+ function DFStageDetail(props) {
119
+ const stage = props.stage;
120
+ const detail = stage.detail || {};
121
+ const view = props.view;
122
+ if (stage.kind === 'input') {
123
+ return e('div', null,
124
+ e(DFRow, { label: '输入' }, e('span', { className: 'df-text' }, detail.text || view.turn.input)),
125
+ e(DFRow, { label: '规模' }, (detail.chars || 0) + ' 字 · ' + dfTokenText(detail.tokens)),
126
+ );
127
+ }
128
+ if (stage.kind === 'context') {
129
+ return e('div', null,
130
+ e(TokenBar, { segments: detail.segments || [], limit: detail.limit || view.context.limit, height: 8 }),
131
+ e(DFRow, { label: '消息' }, String(detail.messageCount || 0) + ' 条 · ' + dfTokenText(detail.used)),
132
+ detail.summary ? e(DFRow, { label: '摘要' }, e('span', { className: 'df-text', style: { maxHeight: 130 } }, detail.summary)) : null,
133
+ (detail.hits || []).length
134
+ ? e(DFRow, { label: '召回' }, e('div', { className: 'df-hits' }, detail.hits.map((hit) => e('div', { key: hit.id, className: 'df-hit' }, e('b', null, hit.key), ':' + hit.value))))
135
+ : e(DFRow, { label: '召回' }, '本轮没有召回长期记忆'),
136
+ );
137
+ }
138
+ if (stage.kind === 'compress') {
139
+ return e('div', null,
140
+ e(DFRow, { label: '前后' }, (detail.before || 0) + ' → ' + (detail.after || 0) + ' tok'),
141
+ e(DFRow, { label: '折叠' }, String(detail.removed || 0) + ' 条消息 · 保留最近 ' + String(detail.keep || 0) + ' 条'),
142
+ e(DFRow, { label: '触发' }, detail.reason === 'manual' ? '手动 /compact' : '自动(超过预算 90%)'),
143
+ detail.summary ? e(DFRow, { label: '摘要' }, e('span', { className: 'df-text', style: { maxHeight: 150 } }, detail.summary)) : null,
144
+ );
145
+ }
146
+ if (stage.kind === 'model') {
147
+ const response = detail.response || {};
148
+ const real = detail.model;
149
+ return e('div', null,
150
+ e(DFRow, { label: '步骤' }, '第 ' + String(detail.step || 1) + ' 步'),
151
+ real
152
+ ? e(DFRow, { label: '真实模型' }, (real.provider || '') + ' / ' + (real.model || '') + (real.latencyMs != null ? ' · ' + real.latencyMs + ' ms' : ''))
153
+ : e(DFRow, { label: '模型' }, '本地规则模拟模型'),
154
+ real && real.usage
155
+ ? e(DFRow, { label: '用量' }, '输入 ' + (real.usage.inputTokens || 0) + ' / 输出 ' + (real.usage.outputTokens || 0) + ' tok')
156
+ : null,
157
+ e(DFRow, { label: '响应' }, response.type === 'tool_call'
158
+ ? 'tool_call · ' + response.name + ' ' + JSON.stringify(response.args || {})
159
+ : ('final · ' + (response.text || ''))),
160
+ (detail.messages || []).length
161
+ ? e(DFRow, { label: '请求消息' }, e('div', { className: 'df-msgs' }, detail.messages.map((message, index) => e('div', { key: index, className: 'df-msg' },
162
+ e('span', { className: 'al-role al-role-' + dfRoleLabel(message.role) }, dfRoleLabel(message.role)),
163
+ e('span', null, (message.content || '').slice(0, 160)),
164
+ ))))
165
+ : null,
166
+ e(DFRow, { label: '工具' }, String((detail.toolSchemas || []).length) + ' 个 schema 随请求发送'),
167
+ );
168
+ }
169
+ if (stage.kind === 'tool') {
170
+ return e('div', null,
171
+ e(DFRow, { label: '工具' }, detail.name || stage.title),
172
+ e(DFRow, { label: '参数' }, e(JsonView, { value: detail.args || {}, style: { maxHeight: 130 } })),
173
+ e(DFRow, { label: '结果' }, e(JsonView, { value: detail.result, style: { maxHeight: 160 } })),
174
+ e(DFRow, { label: '状态' }, detail.ok ? '成功 · ' + (detail.durationMs != null ? detail.durationMs + ' ms' : '') : '失败'),
175
+ );
176
+ }
177
+ if (stage.kind === 'memory') {
178
+ return e('div', null,
179
+ e(DFRow, { label: '写入' }, (detail.key || '') + ' = ' + (detail.value || '')),
180
+ e(DFRow, { label: '记忆 ID' }, detail.id || '—'),
181
+ );
182
+ }
183
+ if (stage.kind === 'output') {
184
+ return e('div', null,
185
+ e(DFRow, { label: '回答' }, e('span', { className: 'df-text', style: { maxHeight: 220 } }, detail.text || '')),
186
+ e(DFRow, { label: '本轮' }, String(detail.steps || 0) + ' 步 · ' + String(detail.toolCalls || 0) + ' 次工具 · ' + dfTokenText(detail.tokens)),
187
+ );
188
+ }
189
+ return e(JsonView, { value: detail });
190
+ }
191
+
192
+ function DFRow(props) {
193
+ return e('div', { className: 'df-row' }, e('span', { className: 'df-k' }, props.label), e('div', { className: 'df-v' }, props.children));
194
+ }
195
+
196
+ function DataFlowPage(props) {
197
+ const view = props.view;
198
+ const turn = view.turn;
199
+ const stages = turn ? turn.stages || [] : [];
200
+ const [selected, setSelected] = useState(0);
201
+ const [playing, setPlaying] = useState(false);
202
+ const timer = useRef(null);
203
+
204
+ const lastActive = useMemo(() => {
205
+ for (let i = stages.length - 1; i >= 0; i -= 1) if (stages[i].status === 'active') return i;
206
+ return stages.length ? stages.length - 1 : 0;
207
+ }, [stages]);
208
+
209
+ useEffect(() => {
210
+ setSelected(stages.length ? lastActive : 0);
211
+ setPlaying(false);
212
+ }, [turn && turn.id]);
213
+
214
+ useEffect(() => {
215
+ if (turn && turn.status === 'running' && stages.length) setSelected(stages.length - 1);
216
+ }, [stages.length, turn && turn.status]);
217
+
218
+ useEffect(() => {
219
+ if (!playing) {
220
+ if (timer.current) { clearInterval(timer.current); timer.current = null; }
221
+ return undefined;
222
+ }
223
+ timer.current = setInterval(() => {
224
+ setSelected((prev) => {
225
+ if (prev >= stages.length - 1) { setPlaying(false); return prev; }
226
+ return prev + 1;
227
+ });
228
+ }, 900);
229
+ return () => { if (timer.current) { clearInterval(timer.current); timer.current = null; } };
230
+ }, [playing, stages.length]);
231
+
232
+ useEffect(() => () => { if (timer.current) clearInterval(timer.current); }, []);
233
+
234
+ const current = stages[selected] || null;
235
+ const meta = current ? DF_STAGE_META[current.kind] || DF_STAGE_META.input : DF_STAGE_META.input;
236
+ const options = (DF_BUDGETS.indexOf(view.context.limit) >= 0 ? DF_BUDGETS : DF_BUDGETS.concat([view.context.limit])).filter((n) => Number.isFinite(n) && n > 0);
237
+
238
+ return e(
239
+ 'div',
240
+ { className: 'df-page' },
241
+ e('style', { dangerouslySetInnerHTML: { __html: DF_CSS } }),
242
+ e(
243
+ 'header',
244
+ { className: 'df-head' },
245
+ e(
246
+ 'div',
247
+ null,
248
+ e('div', { className: 'df-eyebrow' }, 'AGENT DATAFLOW'),
249
+ e('h1', { className: 'df-title' }, 'Agent 数据流转'),
250
+ e('p', { className: 'df-sub' }, '还原当前轮次的每一个小步骤:输入、上下文拼装、压缩、模型请求与响应、工具执行、记忆写入、输出。点节点可跳转,也可以逐步回放。'),
251
+ ),
252
+ e(
253
+ 'div',
254
+ { className: 'df-head-actions' },
255
+ e(
256
+ 'label',
257
+ { className: 'df-budget' },
258
+ '教学预算',
259
+ e(
260
+ 'select',
261
+ { value: view.context.limit, onChange: (event) => props.onBudget && props.onBudget(Number(event.target.value)) },
262
+ options.map((n) => e('option', { key: n, value: n }, alFormatTokens(n) + ' tok')),
263
+ ),
264
+ ),
265
+ e(Chip, { tone: turn && turn.status === 'running' ? 'brand' : turn && turn.status === 'error' ? 'danger' : 'good', icon: turn && turn.status === 'running' ? 'bolt' : 'check' }, turn ? (turn.status === 'running' ? '运行中' : turn.status === 'error' ? '出错' : '完成') : '等待输入'),
266
+ e(Chip, { icon: 'layers' }, view.modelMode === 'real' ? '真实模型' : '模拟模型'),
267
+ ),
268
+ ),
269
+ !turn
270
+ ? e('div', { className: 'df-empty' }, e(Empty, { icon: 'route', title: '还没有可回放的数据', body: '先到「体验 TUI」发一条消息;这里会把本轮从输入到输出的每一步展开,可逐节点查看数据。' }))
271
+ : e(
272
+ 'div',
273
+ null,
274
+ e(
275
+ 'div',
276
+ { className: 'df-controls' },
277
+ e(
278
+ 'div',
279
+ { className: 'df-replay' },
280
+ e(Btn, { size: 'sm', variant: 'ghost', icon: 'chevronLeft', disabled: selected <= 0, onClick: () => { setPlaying(false); setSelected(Math.max(0, selected - 1)); } }, '上一步'),
281
+ e(Btn, { size: 'sm', variant: playing ? 'soft' : 'primary', icon: playing ? 'pause' : 'play', onClick: () => { if (selected >= stages.length - 1) setSelected(0); setPlaying(!playing); } }, playing ? '暂停' : '播放'),
282
+ e(Btn, { size: 'sm', variant: 'ghost', icon: 'chevronRight', disabled: selected >= stages.length - 1, onClick: () => { setPlaying(false); setSelected(Math.min(stages.length - 1, selected + 1)); } }, '下一步'),
283
+ ),
284
+ e('div', { className: 'df-stepinfo' }, '第 ' + String(selected + 1) + ' / ' + String(stages.length) + ' 步 · ' + (current ? current.title : '') + (turn.status === 'running' ? ' · 实时跟随中' : '')),
285
+ ),
286
+ e(
287
+ 'div',
288
+ { className: 'df-flow' },
289
+ stages.map((stage, index) =>
290
+ e(
291
+ 'div',
292
+ { key: stage.id, style: { display: 'flex', alignItems: 'stretch' } },
293
+ e(
294
+ 'button',
295
+ {
296
+ type: 'button',
297
+ className: 'df-node' + (index === selected ? ' on' : ''),
298
+ onClick: () => { setPlaying(false); setSelected(index); },
299
+ title: stage.title,
300
+ },
301
+ e('div', { className: 'df-node-top' },
302
+ e('span', { className: 'df-dot ' + (stage.status || '') }),
303
+ e('span', { className: 'df-node-kind' }, (DF_STAGE_META[stage.kind] || DF_STAGE_META.input).label),
304
+ ),
305
+ e('span', { className: 'df-node-title' }, stage.title),
306
+ e('span', { className: 'df-node-meta' }, stage.meta || ''),
307
+ ),
308
+ index < stages.length - 1 ? e('span', { className: 'df-arrow' }, '→') : null,
309
+ ),
310
+ ),
311
+ ),
312
+ e(
313
+ 'div',
314
+ { className: 'df-body' },
315
+ e(
316
+ 'section',
317
+ { className: 'df-card' },
318
+ e(
319
+ 'div',
320
+ { className: 'df-card-head' },
321
+ e('div', { className: 'df-card-title' }, e(Icon, { name: meta.icon, size: 14 }), '第 ' + String(selected + 1) + ' 步 · ' + (current ? current.title : '')),
322
+ e('span', { className: 'df-card-meta' }, (current && current.meta) || ''),
323
+ ),
324
+ e(
325
+ 'div',
326
+ { className: 'df-card-body' },
327
+ e('p', { className: 'df-explain' }, DF_EXPLAIN[current ? current.kind : 'input'] || ''),
328
+ current ? e(DFStageDetail, { stage: current, view }) : null,
329
+ ),
330
+ ),
331
+ e(
332
+ 'aside',
333
+ { className: 'df-card df-raw' },
334
+ e('div', { className: 'df-card-head' }, e('div', { className: 'df-card-title' }, e(Icon, { name: 'code', size: 13 }), '结构化数据'), e('span', { className: 'df-card-meta' }, 'stage.detail')),
335
+ e('div', { className: 'df-card-body' }, e(JsonView, { value: current ? current.detail : null, style: { maxHeight: 420 } })),
336
+ ),
337
+ ),
338
+ e('p', { className: 'df-note' }, '数据来源:lab.snapshot().turn.stages[].detail;页面只渲染快照,不额外编造状态。运行中会自动跟随最新步骤;点击任意节点或「上一步 / 下一步」可脱离实时回看。'),
339
+ ),
340
+ );
341
+ }