@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,621 @@
1
+ /* ─────────────────────────────────────────────────────────────
2
+ * Agent 实验室 · 检查器
3
+ * Three live views over the same snapshot:
4
+ * 流转 – the stage-by-stage pipeline of the current turn
5
+ * 上下文 – token budget, retrieved memories, summary and the exact messages
6
+ * 记忆 – short-term (session) vs long-term (cross-session) memory
7
+ * ───────────────────────────────────────────────────────────── */
8
+
9
+ const INSPECTOR_CSS = `
10
+ .al-inspector-panel { grid-area: inspector; }
11
+ .al-inspector-tabs { display: flex; gap: 3px; padding: 7px 8px; border-bottom: 1px solid var(--al-border); flex: none; background: color-mix(in srgb, var(--al-layer) 50%, transparent); }
12
+ .al-itab {
13
+ flex: 1; min-width: 0; display: inline-flex; align-items: center; justify-content: center; gap: 4px;
14
+ height: 27px; padding: 0 6px; border: 1px solid transparent; border-radius: 7px;
15
+ background: transparent; color: var(--al-ter); font-size: 11.5px; font-weight: 560; white-space: nowrap;
16
+ transition: color .15s ease, background .15s ease, border-color .15s ease;
17
+ }
18
+ .al-itab:hover { color: var(--al-text); background: var(--al-layer2); }
19
+ .al-itab.on { color: var(--al-accent); background: var(--al-accent-soft); border-color: color-mix(in srgb, var(--al-accent) 22%, transparent); }
20
+ .al-itab-badge { font-size: 9.5px; font-weight: 650; padding: 0 4px; height: 14px; line-height: 14px; border-radius: 999px; background: var(--al-layer2); color: var(--al-ter); }
21
+ .al-itab.on .al-itab-badge { background: color-mix(in srgb, var(--al-accent) 18%, transparent); color: var(--al-accent); }
22
+ .al-insp-body { flex: 1; min-height: 0; overflow-y: auto; padding: 10px 12px 16px; }
23
+ .al-insp-section { margin-bottom: 14px; }
24
+ .al-insp-section:last-child { margin-bottom: 2px; }
25
+ .al-insp-label {
26
+ display: flex; align-items: center; justify-content: space-between; gap: 8px;
27
+ margin: 0 0 6px; font-size: 10.5px; font-weight: 620; color: var(--al-ter);
28
+ letter-spacing: .04em; text-transform: none;
29
+ }
30
+ .al-insp-label .al-chip { height: 19px; font-size: 10px; }
31
+ .al-insp-note { font-size: 10.5px; color: var(--al-ter); line-height: 1.55; }
32
+ .al-insp-note code, .al-insp-note .al-code-inline { font-size: 10px; }
33
+
34
+ /* flow */
35
+ .al-flow-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 10px; }
36
+ .al-flow-input {
37
+ margin: 0 0 10px; padding: 8px 10px; border-radius: 9px;
38
+ border: 1px solid var(--al-border); border-left: 3px solid var(--al-accent);
39
+ background: color-mix(in srgb, var(--al-accent) 6%, transparent);
40
+ font-size: 11.5px; line-height: 1.55; word-break: break-word;
41
+ }
42
+ .al-flow-input b { font-size: 10px; color: var(--al-ter); font-weight: 600; display: block; margin-bottom: 2px; }
43
+ .al-stage { display: flex; gap: 8px; }
44
+ .al-stage-rail { flex: none; display: flex; flex-direction: column; align-items: center; width: 14px; padding-top: 5px; }
45
+ .al-stage-dot {
46
+ width: 9px; height: 9px; border-radius: 50%; flex: none;
47
+ border: 2px solid var(--al-border2); background: var(--al-layer);
48
+ transition: background .2s ease, border-color .2s ease, box-shadow .2s ease;
49
+ }
50
+ .al-stage-dot.done { background: var(--al-good); border-color: var(--al-good); }
51
+ .al-stage-dot.active { background: var(--al-accent); border-color: var(--al-accent); animation: al-stage-pulse 1.6s infinite; }
52
+ .al-stage-dot.error { background: var(--al-danger); border-color: var(--al-danger); }
53
+ @keyframes al-stage-pulse {
54
+ 0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--al-accent) 42%, transparent); }
55
+ 70% { box-shadow: 0 0 0 7px transparent; }
56
+ 100% { box-shadow: 0 0 0 0 transparent; }
57
+ }
58
+ .al-stage-line { flex: 1; width: 1px; background: var(--al-border); min-height: 12px; margin: 3px 0; }
59
+ .al-stage:last-child .al-stage-line { display: none; }
60
+ .al-stage-main { flex: 1; min-width: 0; padding-bottom: 9px; }
61
+ .al-stage-head {
62
+ width: 100%; display: flex; align-items: center; gap: 7px; padding: 4px 6px;
63
+ border: 1px solid transparent; border-radius: 8px; background: transparent; text-align: left;
64
+ transition: background .15s ease, border-color .15s ease;
65
+ }
66
+ .al-stage-head:hover { background: var(--al-layer2); }
67
+ .al-stage[data-open='true'] .al-stage-head { background: var(--al-layer2); border-color: var(--al-border); }
68
+ .al-stage-kind { width: 22px; height: 22px; flex: none; border-radius: 6px; display: grid; place-items: center; background: var(--al-layer2); color: var(--al-sub); }
69
+ .al-stage-kind.active { color: var(--al-accent); background: var(--al-accent-soft); }
70
+ .al-stage-kind.error { color: var(--al-danger); background: color-mix(in srgb, var(--al-danger) 10%, transparent); }
71
+ .al-stage-title { flex: 1; min-width: 0; font-size: 11.5px; font-weight: 560; color: var(--al-text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
72
+ .al-stage-meta { flex: none; font-size: 10px; color: var(--al-ter); font-variant-numeric: tabular-nums; max-width: 92px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
73
+ .al-stage-chev { flex: none; color: var(--al-ter); transition: transform .15s ease; }
74
+ .al-stage[data-open='true'] .al-stage-chev { transform: rotate(90deg); }
75
+ .al-stage-detail {
76
+ margin: 3px 0 0 28px; padding: 8px; border-radius: 8px;
77
+ border: 1px solid var(--al-border); background: color-mix(in srgb, var(--al-layer2) 52%, transparent);
78
+ font-size: 11px; color: var(--al-sub); animation: al-detail-in .16s ease;
79
+ }
80
+ @keyframes al-detail-in { from { opacity: 0; transform: translateY(-2px); } to { opacity: 1; transform: translateY(0); } }
81
+ .al-detail-text { margin: 0; font-size: 11px; line-height: 1.65; white-space: pre-wrap; word-break: break-word; }
82
+ .al-detail-row { display: flex; align-items: baseline; gap: 6px; margin-bottom: 5px; }
83
+ .al-detail-row:last-child { margin-bottom: 0; }
84
+ .al-detail-k { flex: none; font-size: 10px; color: var(--al-ter); min-width: 42px; }
85
+ .al-detail-v { flex: 1; min-width: 0; font-size: 11px; color: var(--al-text); word-break: break-word; }
86
+ .al-detail-kv { display: flex; flex-wrap: wrap; gap: 4px; }
87
+ .al-kv-pill { display: inline-flex; gap: 4px; align-items: baseline; padding: 2px 7px; border-radius: 6px; background: var(--al-layer2); font-size: 10.5px; }
88
+ .al-kv-pill b { color: var(--al-accent); font-weight: 600; }
89
+ .al-role { display: inline-flex; align-items: center; height: 15px; padding: 0 5px; border-radius: 4px; font-size: 9px; font-weight: 650; letter-spacing: .02em; }
90
+ .al-role-user { background: rgba(34, 197, 94, .13); color: #15803d; }
91
+ .al-role-assistant { background: rgba(139, 92, 246, .13); color: #7c3aed; }
92
+ .al-role-tool { background: rgba(245, 158, 11, .14); color: #b45309; }
93
+ .al-role-system { background: rgba(107, 114, 128, .14); color: var(--al-sub); }
94
+ .al-stage-io { margin-top: 6px; }
95
+ .al-stage-io > * + * { margin-top: 5px; }
96
+ .al-mini-msg { display: flex; gap: 5px; align-items: baseline; padding: 2px 0; font-size: 10.5px; line-height: 1.5; }
97
+ .al-mini-msg .al-role { flex: none; transform: scale(.92); transform-origin: left center; }
98
+ .al-mini-msg span:last-child { color: var(--al-sub); word-break: break-word; }
99
+ .al-flow-stats { display: flex; flex-wrap: wrap; gap: 5px 12px; margin-top: 8px; padding-top: 8px; border-top: 1px dashed var(--al-border); font-size: 10px; color: var(--al-ter); }
100
+ .al-flow-stats b { color: var(--al-text); font-weight: 600; font-variant-numeric: tabular-nums; }
101
+
102
+ /* context */
103
+ .al-budget { padding: 10px 11px; border-radius: 10px; border: 1px solid var(--al-border); background: color-mix(in srgb, var(--al-layer2) 45%, transparent); margin-bottom: 12px; }
104
+ .al-budget-top { display: flex; align-items: flex-end; justify-content: space-between; gap: 8px; margin-bottom: 8px; }
105
+ .al-budget-num { font-size: 20px; font-weight: 700; letter-spacing: -.03em; line-height: 1; font-variant-numeric: tabular-nums; }
106
+ .al-budget-num span { font-size: 10.5px; font-weight: 500; color: var(--al-ter); letter-spacing: 0; }
107
+ .al-budget-sub { font-size: 10px; color: var(--al-ter); text-align: right; line-height: 1.4; }
108
+ .al-win-row { display: flex; flex-wrap: wrap; gap: 5px 14px; margin-top: 9px; padding-top: 8px; border-top: 1px dashed var(--al-border); font-size: 10.5px; color: var(--al-sub); }
109
+ .al-win-row b { color: var(--al-text); font-weight: 620; font-variant-numeric: tabular-nums; }
110
+ .al-win-row .al-win-real b { color: #7c3aed; }
111
+ .al-budget-control { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-top: 8px; padding-top: 8px; border-top: 1px dashed var(--al-border); }
112
+ .al-budget-control span { font-size: 10.5px; color: var(--al-ter); }
113
+ .al-budget-control select {
114
+ height: 26px; border: 1px solid var(--al-border2); border-radius: 7px;
115
+ background: var(--al-layer); color: var(--al-text); font-size: 11px; padding: 0 6px;
116
+ }
117
+ .al-hit-list { display: flex; flex-direction: column; gap: 5px; }
118
+ .al-hit { padding: 6px 8px; border-radius: 7px; border: 1px solid color-mix(in srgb, #14b8a6 24%, transparent); background: color-mix(in srgb, #14b8a6 7%, transparent); }
119
+ .al-hit b { display: block; font-size: 10px; color: #0f766e; font-weight: 640; }
120
+ .al-hit span { font-size: 10.5px; color: var(--al-sub); word-break: break-word; }
121
+ .al-summary-box { padding: 8px 10px; border-radius: 8px; border: 1px dashed color-mix(in srgb, var(--al-warn) 40%, transparent); background: color-mix(in srgb, var(--al-warn) 7%, transparent); }
122
+ .al-summary-box p { margin: 0; font-size: 10.5px; line-height: 1.6; color: var(--al-sub); white-space: pre-wrap; word-break: break-word; max-height: 130px; overflow: auto; }
123
+ .al-msg-list { display: flex; flex-direction: column; gap: 5px; }
124
+ .al-msg {
125
+ border: 1px solid var(--al-border); border-radius: 8px; background: color-mix(in srgb, var(--al-layer) 70%, transparent);
126
+ overflow: hidden;
127
+ }
128
+ .al-msg-head { display: flex; align-items: center; gap: 5px; padding: 5px 7px; background: color-mix(in srgb, var(--al-layer2) 48%, transparent); }
129
+ .al-msg-kind { font-size: 9px; color: var(--al-ter); padding: 0 4px; border-radius: 3px; background: var(--al-layer2); }
130
+ .al-msg-tok { margin-left: auto; font-size: 9.5px; color: var(--al-ter); font-variant-numeric: tabular-nums; flex: none; }
131
+ .al-msg-body {
132
+ padding: 5px 8px 7px; font-size: 10.5px; line-height: 1.58; color: var(--al-sub);
133
+ white-space: pre-wrap; word-break: break-word; max-height: 92px; overflow: hidden;
134
+ display: -webkit-box; -webkit-line-clamp: 5; -webkit-box-orient: vertical;
135
+ }
136
+ .al-msg-body.open { display: block; max-height: 260px; overflow: auto; }
137
+ .al-msg-more { border: 0; background: transparent; color: var(--al-accent); font-size: 10px; padding: 0 8px 6px; }
138
+ .al-context-foot { margin-top: 10px; padding-top: 8px; border-top: 1px dashed var(--al-border); }
139
+
140
+ /* memory */
141
+ .al-mem-list { display: flex; flex-direction: column; gap: 6px; }
142
+ .al-mem {
143
+ display: flex; align-items: flex-start; gap: 7px; padding: 7px 8px;
144
+ border: 1px solid var(--al-border); border-radius: 8px; background: color-mix(in srgb, var(--al-layer2) 40%, transparent);
145
+ }
146
+ .al-mem-icon { flex: none; width: 20px; height: 20px; border-radius: 6px; display: grid; place-items: center; background: var(--al-accent-soft); color: var(--al-accent); margin-top: 1px; }
147
+ .al-mem-main { flex: 1; min-width: 0; }
148
+ .al-mem-key { font-size: 11px; font-weight: 620; color: var(--al-text); word-break: break-word; }
149
+ .al-mem-value { font-size: 10.5px; color: var(--al-sub); line-height: 1.55; word-break: break-word; margin-top: 1px; white-space: pre-wrap; }
150
+ .al-mem-meta { display: flex; align-items: center; gap: 6px; margin-top: 3px; font-size: 9px; color: var(--al-ter); }
151
+ .al-mem-del { flex: none; }
152
+ .al-mem-del.confirm { color: var(--al-danger); }
153
+ .al-mini-stats { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 6px; margin-bottom: 12px; }
154
+ `;
155
+
156
+ const STAGE_META = {
157
+ input: { icon: 'terminal', label: '输入' },
158
+ context: { icon: 'layers', label: '上下文' },
159
+ compress: { icon: 'compress', label: '压缩' },
160
+ model: { icon: 'cpu', label: '模型' },
161
+ tool: { icon: 'tools', label: '工具' },
162
+ memory: { icon: 'memory', label: '记忆' },
163
+ output: { icon: 'send', label: '输出' },
164
+ };
165
+
166
+ function fmtDuration(ms) {
167
+ const value = Math.max(0, Math.round(ms || 0));
168
+ if (value < 1000) return value + ' ms';
169
+ return (value / 1000).toFixed(value < 10000 ? 1 : 0) + ' s';
170
+ }
171
+
172
+ function truncateText(text, max) {
173
+ const value = String(text == null ? '' : text);
174
+ return value.length > max ? value.slice(0, max) + '…' : value;
175
+ }
176
+
177
+ function StageDetail(props) {
178
+ const stage = props.stage;
179
+ const detail = stage.detail;
180
+ if (detail == null) return e('p', { className: 'al-detail-text' }, '(无额外细节)');
181
+
182
+ if (stage.kind === 'input') {
183
+ return e('p', { className: 'al-detail-text' }, typeof detail === 'string' ? detail : (detail.text || prettyJson(detail)));
184
+ }
185
+
186
+ if (stage.kind === 'context') {
187
+ const info = detail || {};
188
+ return e(
189
+ 'div',
190
+ null,
191
+ info.segments
192
+ ? e('div', { style: { marginBottom: 8 } }, e(TokenBar, { segments: info.segments, limit: info.limit, height: 7, showLegend: true }))
193
+ : null,
194
+ e('div', { className: 'al-detail-row' }, e('span', { className: 'al-detail-k' }, '消息'), e('span', { className: 'al-detail-v' }, (info.messageCount != null ? info.messageCount : (info.messages || []).length) + ' 条 · 约 ' + (info.used != null ? info.used : '?') + ' tok')),
195
+ info.hits && info.hits.length
196
+ ? e('div', { className: 'al-detail-row' }, e('span', { className: 'al-detail-k' }, '召回'), e('div', { className: 'al-detail-v al-detail-kv' }, info.hits.map((hit) => e('span', { key: hit.id, className: 'al-kv-pill' }, e('b', null, hit.key), '= ' + truncateText(hit.value, 26)))))
197
+ : null,
198
+ info.summary ? e('div', { className: 'al-detail-row' }, e('span', { className: 'al-detail-k' }, '摘要'), e('span', { className: 'al-detail-v' }, truncateText(info.summary, 160))) : null,
199
+ info.messages && info.messages.length
200
+ ? e('div', { className: 'al-stage-io' }, info.messages.map((message, index) => e('div', { className: 'al-mini-msg', key: index }, e('span', { className: 'al-role al-role-' + (message.role || 'system') }, message.role || 'system'), e('span', null, truncateText(message.content, 110)))))
201
+ : null,
202
+ );
203
+ }
204
+
205
+ if (stage.kind === 'compress') {
206
+ return e(
207
+ 'div',
208
+ null,
209
+ e('div', { className: 'al-detail-row' }, e('span', { className: 'al-detail-k' }, 'token'), e('span', { className: 'al-detail-v' }, (detail.before != null ? detail.before : '?') + ' → ' + (detail.after != null ? detail.after : '?') + ' tok · 折叠 ' + (detail.removed != null ? detail.removed : '?') + ' 条消息')),
210
+ detail.summary ? e('div', { className: 'al-summary-box' }, e('p', null, detail.summary)) : null,
211
+ );
212
+ }
213
+
214
+ if (stage.kind === 'model') {
215
+ const response = detail.response || {};
216
+ const isTool = response.type === 'tool_call';
217
+ return e(
218
+ 'div',
219
+ null,
220
+ e('div', { className: 'al-detail-row' }, e('span', { className: 'al-detail-k' }, '第 ' + (detail.step || 1) + ' 步'), e('span', { className: 'al-detail-v' }, isTool ? '模型决定调用工具 ' + response.name : '模型直接给出最终回答')),
221
+ detail.model
222
+ ? e('div', { className: 'al-detail-row' }, e('span', { className: 'al-detail-k' }, '模型'), e('span', { className: 'al-detail-v' }, (detail.model.provider || '') + ' / ' + (detail.model.model || '') + (detail.model.latencyMs != null ? ' · ' + detail.model.latencyMs + ' ms' : '') + (detail.model.usage ? ' · 输入 ' + (detail.model.usage.inputTokens || 0) + ' / 输出 ' + (detail.model.usage.outputTokens || 0) + ' tok' : '')))
223
+ : null,
224
+ isTool
225
+ ? e('div', { className: 'al-stage-io' }, e(JsonView, { value: response.args, style: { maxHeight: 120 } }))
226
+ : e('p', { className: 'al-detail-text' }, truncateText(response.text || '', 260)),
227
+ detail.toolSchemas
228
+ ? e('details', { className: 'al-stage-io' }, e('summary', { className: 'al-insp-note', style: { cursor: 'pointer', marginBottom: 4 } }, '模型这次看到的工具定义(' + detail.toolSchemas.length + ' 个)'), e(JsonView, { value: detail.toolSchemas, style: { maxHeight: 180 } }))
229
+ : null,
230
+ detail.messages
231
+ ? e('details', { className: 'al-stage-io' }, e('summary', { className: 'al-insp-note', style: { cursor: 'pointer', marginBottom: 4 } }, '发给模型的消息(' + detail.messages.length + ' 条)'), e(JsonView, { value: detail.messages, style: { maxHeight: 180 } }))
232
+ : null,
233
+ );
234
+ }
235
+
236
+ if (stage.kind === 'tool') {
237
+ const info = detail || {};
238
+ return e(
239
+ 'div',
240
+ null,
241
+ e('div', { className: 'al-detail-row' }, e('span', { className: 'al-detail-k' }, '工具'), e('span', { className: 'al-detail-v' }, info.name || stage.title)),
242
+ info.args ? e('div', { className: 'al-stage-io' }, e(JsonView, { value: info.args, style: { maxHeight: 110 } })) : null,
243
+ e('div', { className: 'al-stage-io' }, e('div', { className: 'al-insp-label', style: { marginBottom: 4 } }, info.ok === false ? '结果 · 失败' : '结果'), e(JsonView, { value: info.result, style: { maxHeight: 130 } })),
244
+ );
245
+ }
246
+
247
+ if (stage.kind === 'output') {
248
+ return e('p', { className: 'al-detail-text' }, typeof detail === 'string' ? detail : (detail.text || prettyJson(detail)));
249
+ }
250
+
251
+ return e(JsonView, { value: detail, style: { maxHeight: 220 } });
252
+ }
253
+
254
+ function StageRow(props) {
255
+ const stage = props.stage;
256
+ const meta = STAGE_META[stage.kind] || STAGE_META.input;
257
+ const open = props.open;
258
+ return e(
259
+ 'div',
260
+ { className: 'al-stage', 'data-open': open ? 'true' : 'false' },
261
+ e(
262
+ 'div',
263
+ { className: 'al-stage-rail' },
264
+ e('span', { className: 'al-stage-dot ' + (stage.status || '') }),
265
+ e('span', { className: 'al-stage-line' }),
266
+ ),
267
+ e(
268
+ 'div',
269
+ { className: 'al-stage-main' },
270
+ e(
271
+ 'button',
272
+ { type: 'button', className: 'al-stage-head', onClick: props.onToggle, 'aria-expanded': open ? 'true' : 'false' },
273
+ e('span', { className: 'al-stage-kind ' + (stage.status || '') }, e(Icon, { name: meta.icon, size: 12 })),
274
+ e('span', { className: 'al-stage-title' }, stage.title || meta.label),
275
+ e('span', { className: 'al-stage-meta' }, stage.meta || ''),
276
+ e('span', { className: 'al-stage-chev' }, e(Icon, { name: 'chevronRight', size: 12 })),
277
+ ),
278
+ open ? e('div', { className: 'al-stage-detail' }, e(StageDetail, { stage })) : null,
279
+ ),
280
+ );
281
+ }
282
+
283
+ function FlowTab(props) {
284
+ const view = props.view;
285
+ const turn = view.turn;
286
+ const [openId, setOpenId] = useState(null);
287
+ const activeStage = turn ? (turn.stages || []).slice().reverse().find((stage) => stage.status === 'active') : null;
288
+ const activeId = activeStage ? activeStage.id : null;
289
+
290
+ useEffect(() => {
291
+ if (!turn) {
292
+ setOpenId(null);
293
+ return;
294
+ }
295
+ if (activeId) setOpenId(activeId);
296
+ }, [turn && turn.id, activeId]);
297
+
298
+ if (!turn) {
299
+ return e(
300
+ 'div',
301
+ null,
302
+ e(
303
+ 'div',
304
+ { className: 'al-flow-head' },
305
+ e(Chip, { tone: 'brand', icon: 'route' }, '等待输入'),
306
+ e('span', { className: 'al-insp-note' }, '实时'),
307
+ ),
308
+ e(Empty, {
309
+ icon: 'route',
310
+ title: '还没有处理链路',
311
+ body: '在左边终端发一条消息,这里会逐步出现:输入 → 上下文组装 → 模型推理 → 工具执行 → 记忆 → 输出。点任一环节可展开细节。',
312
+ }),
313
+ e(
314
+ 'div',
315
+ { className: 'al-insp-section', style: { marginTop: 10 } },
316
+ e('div', { className: 'al-insp-label' }, '一轮消息里发生了什么'),
317
+ e(
318
+ 'div',
319
+ { className: 'al-stages-preview', style: { display: 'flex', flexDirection: 'column', gap: 7 } },
320
+ [
321
+ ['terminal', '输入被追加到会话日志'],
322
+ ['layers', '系统提示 + 摘要 + 召回记忆 + 最近消息 → 上下文'],
323
+ ['compress', '超过 token 预算时,旧消息折叠成摘要'],
324
+ ['cpu', '模型只返回「工具调用」或「最终回答」'],
325
+ ['tools', '框架执行工具,把结果作为消息写回上下文'],
326
+ ['memory', '需要记住的事实写进跨会话长期记忆'],
327
+ ['send', '最终回答流式显示,并持久化整个会话'],
328
+ ].map((row) =>
329
+ e(
330
+ 'div',
331
+ { key: row[1], style: { display: 'flex', gap: 7, alignItems: 'flex-start', fontSize: 10.5, color: 'var(--al-sub)', lineHeight: 1.5 } },
332
+ e('span', { style: { color: 'var(--al-accent)', flex: 'none', marginTop: 1 } }, e(Icon, { name: row[0], size: 12 })),
333
+ e('span', null, row[1]),
334
+ ),
335
+ ),
336
+ ),
337
+ ),
338
+ );
339
+ }
340
+
341
+ const stages = turn.stages || [];
342
+ const toolCalls = stages.filter((stage) => stage.kind === 'tool').length;
343
+ const modelCalls = stages.filter((stage) => stage.kind === 'model').length;
344
+ const duration = turn.endedAt ? turn.endedAt - turn.startedAt : Date.now() - turn.startedAt;
345
+
346
+ return e(
347
+ 'div',
348
+ null,
349
+ e(
350
+ 'div',
351
+ { className: 'al-flow-head' },
352
+ e(Chip, { tone: turn.status === 'running' ? 'brand' : turn.status === 'error' ? 'danger' : 'good', icon: turn.status === 'running' ? 'bolt' : turn.status === 'error' ? 'alert' : 'check' }, turn.status === 'running' ? '运行中' : turn.status === 'error' ? '出错' : '完成'),
353
+ e(Chip, { tone: view.modelMode === 'real' ? 'violet' : 'neutral', icon: view.modelMode === 'real' ? 'bolt' : 'cpu' }, view.modelMode === 'real' ? '真实模型' : '模拟模型'),
354
+ e('span', { className: 'al-insp-note' }, fmtDuration(duration)),
355
+ ),
356
+ e('div', { className: 'al-flow-input' }, e('b', null, '用户输入'), turn.input),
357
+ e(
358
+ 'div',
359
+ null,
360
+ stages.map((stage) =>
361
+ e(StageRow, {
362
+ key: stage.id,
363
+ stage,
364
+ open: openId === stage.id,
365
+ onToggle: () => setOpenId(openId === stage.id ? null : stage.id),
366
+ }),
367
+ ),
368
+ ),
369
+ e(
370
+ 'div',
371
+ { className: 'al-flow-stats' },
372
+ e('span', null, '模型调用 ', e('b', null, String(modelCalls))),
373
+ e('span', null, '工具调用 ', e('b', null, String(toolCalls))),
374
+ e('span', null, '上下文 ', e('b', null, (view.context ? view.context.used : '?') + ' tok')),
375
+ e('span', null, '压缩 ', e('b', null, String(view.stats ? view.stats.compressions : 0))),
376
+ ),
377
+ e(
378
+ 'div',
379
+ { className: 'al-insp-section', style: { marginTop: 10 } },
380
+ e('div', { className: 'al-insp-note' }, '提示:模型只看得到「上下文」里的消息;工具结果必须写回上下文,模型才能继续。'),
381
+ ),
382
+ );
383
+ }
384
+
385
+ function ContextMsg(props) {
386
+ const message = props.message;
387
+ const [open, setOpen] = useState(false);
388
+ const content = message.content || '';
389
+ const canExpand = content.length > 180;
390
+ return e(
391
+ 'div',
392
+ { className: 'al-msg' },
393
+ e(
394
+ 'div',
395
+ { className: 'al-msg-head' },
396
+ e('span', { className: 'al-role al-role-' + (message.role || 'system') }, message.role || 'system'),
397
+ message.kind ? e('span', { className: 'al-msg-kind' }, message.kind) : null,
398
+ message.toolName ? e('span', { className: 'al-msg-kind' }, message.toolName) : null,
399
+ e('span', { className: 'al-msg-tok' }, message.tokens + ' tok'),
400
+ ),
401
+ e('div', { className: 'al-msg-body' + (open ? ' open' : '') }, content || '(空)'),
402
+ canExpand ? e('button', { type: 'button', className: 'al-msg-more', onClick: () => setOpen(!open) }, open ? '收起' : '展开全文') : null,
403
+ );
404
+ }
405
+
406
+ function ContextTab(props) {
407
+ const view = props.view;
408
+ const budgetOptions = [1000, 2000, 8000, 32000, 128000, 1048576];
409
+ const context = view.context || { limit: 0, used: 0, segments: [], hits: [], summary: null };
410
+ const messages = view.messages || [];
411
+ const pct = context.limit > 0 ? Math.round((context.used / context.limit) * 100) : 0;
412
+ return e(
413
+ 'div',
414
+ null,
415
+ e(
416
+ 'div',
417
+ { className: 'al-budget' },
418
+ e(
419
+ 'div',
420
+ { className: 'al-budget-top' },
421
+ e('div', { className: 'al-budget-num' }, String(context.used), e('span', null, ' / ' + context.limit + ' tok')),
422
+ e('div', { className: 'al-budget-sub' }, '预算占用 ' + pct + '%', e('br'), '超出 90% 自动压缩'),
423
+ ),
424
+ e(TokenBar, { segments: context.segments || [], limit: context.limit, height: 9 }),
425
+ e(
426
+ 'div',
427
+ { className: 'al-win-row' },
428
+ e('span', null, '教学预算 ', e('b', null, alFormatTokens(context.limit) + ' tok')),
429
+ e(
430
+ 'span',
431
+ { className: 'al-win-real' },
432
+ '真实模型窗口 ',
433
+ e('b', null, view.model && view.model.contextWindow ? alFormatTokens(view.model.contextWindow) + ' tok' : '—'),
434
+ ),
435
+ view.model && view.model.maxOutputTokens
436
+ ? e('span', null, '输出上限 ', e('b', null, alFormatTokens(view.model.maxOutputTokens) + ' tok'))
437
+ : null,
438
+ ),
439
+ e(
440
+ 'div',
441
+ { className: 'al-budget-control' },
442
+ e('span', null, '教学预算(实时生效)'),
443
+ e(
444
+ 'select',
445
+ {
446
+ value: context.limit,
447
+ onChange: (event) => props.onBudget && props.onBudget(Number(event.target.value)),
448
+ title: '调整教学上下文预算;数值越大压缩触发越少',
449
+ },
450
+ (budgetOptions.indexOf(context.limit) >= 0 ? budgetOptions : budgetOptions.concat([context.limit]))
451
+ .filter((n) => Number.isFinite(n) && n > 0)
452
+ .map((n) => e('option', { key: n, value: n }, alFormatTokens(n) + ' tok')),
453
+ ),
454
+ ),
455
+ e(
456
+ 'div',
457
+ { className: 'al-insp-note', style: { marginTop: 5 } },
458
+ '压缩阈值基于教学预算;真实模型窗口来自 DSH 模型元数据,只作参考。默认 8000 tok,可按需调大。',
459
+ ),
460
+ ),
461
+ e(
462
+ 'div',
463
+ { className: 'al-insp-section' },
464
+ e('div', { className: 'al-insp-label' }, e('span', null, '本轮自动召回'), e(Chip, { tone: (context.hits || []).length ? 'good' : 'neutral' }, String((context.hits || []).length) + ' 条')),
465
+ (context.hits || []).length
466
+ ? e('div', { className: 'al-hit-list' }, context.hits.map((hit) => e('div', { key: hit.id, className: 'al-hit' }, e('b', null, hit.key), e('span', null, hit.value))))
467
+ : e('div', { className: 'al-insp-note' }, '没有匹配的长期记忆。长期记忆只在关键词相关时才会注入,避免污染上下文。'),
468
+ ),
469
+ e(
470
+ 'div',
471
+ { className: 'al-insp-section' },
472
+ e('div', { className: 'al-insp-label' }, e('span', null, '历史摘要 · 关键内容 / 未完成任务 / 下一步'), context.summary ? e(Chip, { tone: 'warn' }, '已生效') : null),
473
+ context.summary
474
+ ? e('div', { className: 'al-summary-box' }, e('p', null, context.summary))
475
+ : e('div', { className: 'al-insp-note' }, '还没有压缩。等上下文接近预算,旧消息会被折叠成摘要;也可以点终端里的 /compact。'),
476
+ ),
477
+ e(
478
+ 'div',
479
+ { className: 'al-insp-section' },
480
+ e('div', { className: 'al-insp-label' }, e('span', null, '模型将看到的完整消息'), e('span', { className: 'al-insp-note' }, String(messages.length) + ' 条')),
481
+ messages.length
482
+ ? e('div', { className: 'al-msg-list' }, messages.map((message) => e(ContextMsg, { key: message.id, message })))
483
+ : e('div', { className: 'al-insp-note' }, '还没有消息。'),
484
+ e(
485
+ 'div',
486
+ { className: 'al-context-foot al-insp-note' },
487
+ '顺序:system 提示 → 历史摘要 → 召回记忆 → 对话消息 → 本轮输入。工具调用和结果也是消息,所以模型能「看见」自己刚才做了什么。',
488
+ ),
489
+ ),
490
+ );
491
+ }
492
+
493
+ function MemoryTab(props) {
494
+ const view = props.view;
495
+ const memory = view.memory || [];
496
+ const [confirmId, setConfirmId] = useState(null);
497
+ const [confirmClear, setConfirmClear] = useState(false);
498
+ const stats = view.stats || {};
499
+ const session = (view.sessions || []).find((item) => item.id === view.activeId);
500
+ return e(
501
+ 'div',
502
+ null,
503
+ e(
504
+ 'div',
505
+ { className: 'al-mini-stats' },
506
+ e(Stat, { value: String(memory.length), label: '长期记忆' }),
507
+ e(Stat, { value: String((view.messages || []).length), label: '本会话消息' }),
508
+ e(Stat, { value: String(stats.compressions || 0), label: '累计压缩' }),
509
+ ),
510
+ e(
511
+ 'div',
512
+ { className: 'al-insp-section' },
513
+ e('div', { className: 'al-insp-label' }, e('span', null, '长期记忆 · 跨会话'), e(Chip, { tone: 'violet', icon: 'memory' }, 'localStorage')),
514
+ memory.length
515
+ ? e(
516
+ 'div',
517
+ { className: 'al-mem-list' },
518
+ memory.map((entry) =>
519
+ e(
520
+ 'div',
521
+ { key: entry.id, className: 'al-mem' },
522
+ e('span', { className: 'al-mem-icon' }, e(Icon, { name: 'key', size: 11 })),
523
+ e(
524
+ 'div',
525
+ { className: 'al-mem-main' },
526
+ e('div', { className: 'al-mem-key' }, entry.key),
527
+ e('div', { className: 'al-mem-value' }, entry.value),
528
+ e(
529
+ 'div',
530
+ { className: 'al-mem-meta' },
531
+ e('span', null, entry.source || '记忆工具'),
532
+ e('span', null, '·'),
533
+ e('span', null, fmtTime(entry.updatedAt || entry.createdAt)),
534
+ ),
535
+ ),
536
+ e(IconBtn, {
537
+ icon: confirmId === entry.id ? 'alert' : 'trash',
538
+ size: 13,
539
+ danger: confirmId === entry.id,
540
+ className: 'al-mem-del' + (confirmId === entry.id ? ' confirm' : ''),
541
+ title: confirmId === entry.id ? '再点一次删除' : '删除这条记忆',
542
+ onClick: () => {
543
+ if (confirmId === entry.id) {
544
+ props.onDeleteMemory(entry.id);
545
+ setConfirmId(null);
546
+ } else {
547
+ setConfirmId(entry.id);
548
+ }
549
+ },
550
+ }),
551
+ ),
552
+ ),
553
+ )
554
+ : e(Empty, { icon: 'memory', title: '还没有长期记忆', body: '对它说「记住 我的项目代号是 Orion」,下次换一个会话它也能想起来。' }),
555
+ ),
556
+ e(
557
+ 'div',
558
+ { className: 'al-insp-section' },
559
+ e('div', { className: 'al-insp-label' }, e('span', null, '短期记忆 · 当前会话'), session ? e('span', { className: 'al-insp-note' }, session.messageCount + ' 条 / ' + session.tokens + ' tok') : null),
560
+ e('div', { className: 'al-insp-note' }, '短期记忆就是当前会话的消息窗口:每次请求都会把最近的消息重新发回模型。窗口满之前靠压缩摘要续命,跨会话则靠上面的长期记忆。'),
561
+ e(
562
+ 'div',
563
+ { style: { marginTop: 8 } },
564
+ confirmClear
565
+ ? e(
566
+ 'div',
567
+ { style: { display: 'flex', gap: 6, flexWrap: 'wrap' } },
568
+ e(Btn, { size: 'sm', variant: 'danger', onClick: () => { props.onClearMemory(); setConfirmClear(false); } }, '确认清空 ' + memory.length + ' 条'),
569
+ e(Btn, { size: 'sm', variant: 'ghost', onClick: () => setConfirmClear(false) }, '取消'),
570
+ )
571
+ : e(Btn, { size: 'sm', variant: 'ghost', icon: 'trash', disabled: memory.length === 0, onClick: () => setConfirmClear(true) }, '清空长期记忆'),
572
+ ),
573
+ ),
574
+ e(
575
+ 'div',
576
+ { className: 'al-insp-section' },
577
+ e('div', { className: 'al-insp-label' }, '记忆的写入路径'),
578
+ e('div', { className: 'al-insp-note' }, '模型决定调用 memory_write → 工具把 key/value 写进长期记忆 → 下一轮按输入关键词召回并注入 system 消息。所有过程都能在「流转」里看到。'),
579
+ ),
580
+ );
581
+ }
582
+
583
+ function Inspector(props) {
584
+ const view = props.view;
585
+ const tab = props.tab;
586
+ const memoryCount = (view.memory || []).length;
587
+ const context = view.context || { used: 0 };
588
+ const stages = view.turn ? (view.turn.stages || []).length : 0;
589
+ return e(
590
+ 'section',
591
+ { className: 'al-panel al-inspector-panel', 'aria-label': '检查器' },
592
+ e(
593
+ 'header',
594
+ { className: 'al-panel-head' },
595
+ e('div', { className: 'al-panel-title' }, e(Icon, { name: 'search', size: 14 }), '检查器', e('span', { className: 'al-panel-sub' }, '实时')),
596
+ e(Chip, { tone: view.busy ? 'brand' : 'neutral', icon: view.busy ? 'bolt' : 'check' }, view.busy ? '运行中' : '空闲'),
597
+ ),
598
+ e(
599
+ 'div',
600
+ { className: 'al-inspector-tabs', role: 'tablist', 'aria-label': '检查器视图' },
601
+ [
602
+ { id: 'flow', icon: 'route', label: '流转', badge: stages || null },
603
+ { id: 'context', icon: 'layers', label: '上下文', badge: context.used + '' },
604
+ { id: 'memory', icon: 'memory', label: '记忆', badge: memoryCount + '' },
605
+ ].map((item) =>
606
+ e(
607
+ 'button',
608
+ { key: item.id, type: 'button', role: 'tab', 'aria-selected': tab === item.id, className: 'al-itab' + (tab === item.id ? ' on' : ''), onClick: () => props.onTab(item.id) },
609
+ e(Icon, { name: item.icon, size: 12 }),
610
+ item.label,
611
+ item.badge ? e('span', { className: 'al-itab-badge' }, item.badge) : null,
612
+ ),
613
+ ),
614
+ ),
615
+ e(
616
+ 'div',
617
+ { className: 'al-insp-body' },
618
+ tab === 'flow' ? e(FlowTab, { view }) : tab === 'context' ? e(ContextTab, { view, onBudget: props.onBudget }) : e(MemoryTab, { view, onDeleteMemory: props.onDeleteMemory, onClearMemory: props.onClearMemory }),
619
+ ),
620
+ );
621
+ }