@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.
package/CONTRACT.md ADDED
@@ -0,0 +1,276 @@
1
+ # Agent 实验室 · 内部实现契约(v1)
2
+
3
+ 本文件是并行开发的唯一接口约定。所有文件都是**纯 JavaScript**,最终由 `build.mjs`
4
+ 按文件名顺序拼进同一个 Client factory 作用域,因此:
5
+
6
+ - 不用 `import` / `export` / JSX / TypeScript / Node API。
7
+ - React 由外层提供:文件里可直接用 `React`、`e`(= `React.createElement`),以及
8
+ `useState`、`useEffect`、`useMemo`、`useRef`、`useCallback` 这些解构名。
9
+ - 跨文件函数优先用 `function` 声明(会提升);常量用 `const`,且要保证被引用前已定义。
10
+ - 不直接访问 `document` / `window`(除 `globalThis.localStorage` 的受保护访问,
11
+ 已封装在 core 里)。不读其他插件 DOM。
12
+ - 所有用户可见文案为中文;代码注释中文。meta 的 zh/en locale 由 Lead 维护。
13
+
14
+ ## 目标形态
15
+
16
+ DSH Web UI 左侧栏一个独立入口「Agent 实验室」,点击打开一个整页插件:
17
+
18
+ 1. **体验**:三栏布局 —— 左:会话列表;中:模拟 TUI 终端;右:检查器
19
+ (流转 / 上下文 / 记忆)。输入一句话,实时看到它如何经过
20
+ 上下文组装 → LLM → 工具调用 → 记忆 → 输出。
21
+ 2. **源码**:浏览本插件真实运行的源码(构建时内嵌为 `SOURCES`)。
22
+ 3. **原理**:图文讲解最小 agent 循环、上下文与压缩、记忆、会话、工具协议,
23
+ 以及如何替换成真实 LLM。
24
+
25
+ ## 文件与归属
26
+
27
+ | 文件 | 角色 | 归属 |
28
+ |---|---|---|
29
+ | `src/00-core.js` | 常量、token 估算、纯工具函数 | engine-dev |
30
+ | `src/10-engine.js` | 会话/记忆/上下文/压缩/持久化/`createLab` | engine-dev |
31
+ | `src/20-tools.js` | 工具注册表与四个工具实现 | engine-dev |
32
+ | `src/30-llm.js` | 规则驱动的模拟 LLM(计划 + 流式输出) | engine-dev |
33
+ | `src/40-ui-kit.js` | 全局 CSS、图标、按钮等 UI 原语 | Lead |
34
+ | `src/50-tui.js` | TUI 终端组件 | Lead |
35
+ | `src/60-inspector.js` | 检查器三个页签 | Lead |
36
+ | `src/70-docs.js` | 原理页组件与内容 | docs-dev |
37
+ | `src/80-source.js` | 源码浏览器与高亮 | docs-dev |
38
+ | `src/90-page.js` | 页面外壳、顶部页签、会话栏、组装 | Lead |
39
+ | `src/95-apply.js` | 注册 slot / sidebar | Lead |
40
+ | `test/engine.test.mjs` | 引擎无头测试 | engine-dev |
41
+ | `README.md` | 使用与架构说明 | docs-dev(Lead 终审) |
42
+
43
+ **不要修改不属于自己的文件。** 需要改契约时先告知 Lead。
44
+
45
+ ## 引擎 API(engine-dev 必须实现;Lead/UI 按此消费)
46
+
47
+ ### 常量与纯函数(00-core.js)
48
+
49
+ ```js
50
+ const LAB_BUDGET = 420; // 上下文 token 预算(估算值)
51
+ const LAB_RECENT_KEEP = 4; // 压缩时保留的最近消息条数
52
+ const LAB_MAX_STEPS = 4; // 单轮最多模型步数
53
+ const LAB_STORAGE_KEY = 'dsh.agent-lab.v1';
54
+ const LAB_SYSTEM_PROMPT = '……'; // 迷你 agent 的 system prompt(中文,说明自己是教学用模拟模型)
55
+
56
+ function estimateTokens(text) {} // 估算:CJK 约 1 token/字,其他约 4 字符/token,向上取整
57
+ function fmtClock(ts) {} // HH:MM:SS
58
+ function fmtTime(ts) {} // 2026-11-07 13:02
59
+ function truncate(text, n) {}
60
+ function uid(prefix) {} // 可选注入 id 生成器
61
+ ```
62
+
63
+ ### `createLab(options)`(10-engine.js)
64
+
65
+ ```js
66
+ // options(全部可选):
67
+ // storage: { getItem(k), setItem(k,v) } 默认受保护的 localStorage,失败则内存态
68
+ // sleep: (ms) => Promise<void> 默认 setTimeout;测试注入 no-op
69
+ // now: () => number 默认 Date.now
70
+ // id: (prefix) => string 默认 uid
71
+ // pace: 'fast' | 'normal' | 'slow' 默认 'normal'
72
+ // budget?: number 默认 LAB_BUDGET
73
+ ```
74
+
75
+ 返回对象:
76
+
77
+ ```js
78
+ lab.snapshot() // 见下方 Snapshot;每次返回新对象,可安全交给 React
79
+ lab.onEvent(fn) // 订阅事件,返回取消函数
80
+ lab.submit(text) // Promise<void>;空串/busy 时直接 resolve;内部异常转为错误事件
81
+ lab.compact() // 手动压缩;无消息时给出 info 行
82
+ lab.newSession() // 新建并切换;busy 时返回 false
83
+ lab.switchSession(id) // boolean
84
+ lab.deleteSession(id) // 删除;删当前会话时自动切到下一个/新建
85
+ lab.clearMemory()
86
+ lab.deleteMemory(id)
87
+ lab.resetAll() // 清空本地存储并重建
88
+ lab.setPace(pace) // 'fast' | 'normal' | 'slow'
89
+ lab.dispose() // 取消进行中的 turn、退订;之后 snapshot 仍可用
90
+ ```
91
+
92
+ ### 事件(每次状态变化至少发一个;UI 收到后重取 `snapshot()`)
93
+
94
+ ```js
95
+ { type: 'turn_start', turnId }
96
+ { type: 'stage' } // 某阶段状态变化
97
+ { type: 'context' } // 上下文重新组装
98
+ { type: 'token', text } // 流式输出增量(用于触发重渲染;以 snapshot 为准)
99
+ { type: 'line' } // transcript 新增/更新
100
+ { type: 'message' }
101
+ { type: 'tool_call', name }
102
+ { type: 'tool_result', name, ok }
103
+ { type: 'memory' }
104
+ { type: 'compression' }
105
+ { type: 'turn_end', turnId }
106
+ { type: 'error', message }
107
+ { type: 'state' } // 会话切换/删除/清空等
108
+ ```
109
+
110
+ ### Snapshot
111
+
112
+ ```js
113
+ {
114
+ busy: boolean,
115
+ pace: 'fast'|'normal'|'slow',
116
+ activeId: string,
117
+ sessions: [{
118
+ id, title, createdAt, updatedAt,
119
+ messageCount, tokens, compressions, turns,
120
+ }],
121
+ messages: [{
122
+ id, role: 'user'|'assistant'|'tool'|'system',
123
+ kind?: 'tool_call'|'tool_result'|'summary'|'memory'|'final',
124
+ content: string, tokens: number, at: number,
125
+ toolName?: string,
126
+ }],
127
+ transcript: [{
128
+ id,
129
+ kind: 'banner'|'user'|'meta'|'llm'|'tool'|'toolresult'|'memory'|'compress'|'error'|'agent'|'info',
130
+ text: string, // 纯文本;TUI 直接渲染
131
+ at: number,
132
+ streaming?: true, // 正在流式输出的最后一行
133
+ }],
134
+ memory: [{
135
+ id, key, value, source, sessionId, createdAt, updatedAt,
136
+ }],
137
+ context: {
138
+ limit, used,
139
+ segments: [{ id:'system'|'summary'|'memory'|'recent'|'input', label, tokens }],
140
+ hits: [{ id, key, value }], // 本轮自动召回的长记忆
141
+ summary: string|null,
142
+ messageCount: number,
143
+ },
144
+ turn: null | {
145
+ id, input,
146
+ status: 'running'|'done'|'error',
147
+ startedAt, endedAt,
148
+ stages: [{
149
+ id, kind: 'input'|'context'|'compress'|'model'|'tool'|'memory'|'output',
150
+ title: string,
151
+ meta: string, // 一行摘要,例如 "8 条消息 · 186 tok"
152
+ status: 'active'|'done'|'error',
153
+ detail: any, // 可 JSON 序列化的细节,检查器展开显示
154
+ at: number,
155
+ }],
156
+ },
157
+ streaming: null | { lineId: string, text: string },
158
+ stats: {
159
+ turns, toolCalls, compressions, tokensSaved, totalTokens,
160
+ },
161
+ lastInput: string,
162
+ }
163
+ ```
164
+
165
+ ### 行为规范
166
+
167
+ - **上下文组装**(每次模型调用前):
168
+ `system`(`LAB_SYSTEM_PROMPT`)+
169
+ `summary`(历史摘要,若有)+
170
+ `memory`(按用户输入关键词召回最多 3 条长记忆,作为 system 消息注入)+
171
+ `recent`(当前会话消息,压缩后新旧)+
172
+ `input`(本轮用户输入)。
173
+ 同时更新 `context.segments/used/hits`,并 emit `context`。
174
+ - **压缩**:模型调用前若 `used > limit * 0.9`,把除最近 `LAB_RECENT_KEEP` 条之外的
175
+ 消息折叠进 `summary`(累计式、抽取式:保留用户意图、工具名与结果要点)。
176
+ 记录压缩次数,`stats.tokensSaved += before - after`,emit `compression`,
177
+ transcript 加一条 `compress` 行,turn 加一个 `compress` stage。
178
+ 手动 `compact()` 无条件执行(消息不足 3 条时只加 info 行)。
179
+ - **一轮 turn 的 stages**(按发生顺序):`input` → `context` →(`compress`)→
180
+ `model`(第 1 步)→ `tool`(每次调用一个)→ `model`(第 2 步)… → `output`。
181
+ `model` stage 的 `detail` 要包含 `{ step, messages: 发给模型的消息数组, toolSchemas, response }`,
182
+ 其中 `response` 为 `{ type:'tool_call', name, args }` 或 `{ type:'final', text }`。
183
+ - **max steps**:超过 `LAB_MAX_STEPS` 仍未收敛 → 错误行 + turn error。
184
+ - **transcript**:每轮至少产生 user、llm、tool(可选)、agent、meta 行;工具调用行形如
185
+ `⚙ calculator {"expression":"(12+30)*3"}`,工具结果行 `← 126`;
186
+ 记忆写入 `◈ 记忆已保存 · 项目代号 = Orion`;压缩 `⇣ 上下文已压缩 · 186 → 92 tok`。
187
+ - **多步**:`(12+30)*3 然后记住结果` 这类输入应产生两次工具调用再回答,展示 agent loop。
188
+ - **持久化**:turn 结束、记忆变化、会话操作后写 storage;最多 20 个会话,
189
+ 每会话消息上限 120 条(超出丢最旧);transcript 每会话上限 200 行。
190
+ - **首次运行**:自动建一个会话,transcript 首行 banner:
191
+ `Agent Lab v0.1 · 规则驱动的迷你 Agent(离线可用) · 输入 /help 查看命令`。
192
+ - **/help、/compact、/new、/memory、/clear** 等斜杠命令也可由 UI 直接映射到 API;
193
+ 引擎自身不要求解析斜杠命令,但 `submit('/compact')` 之类可由 Lead 在 UI 处理。
194
+
195
+ ### 模拟 LLM 必须覆盖的意图(30-llm.js)
196
+
197
+ 规则驱动、可复现、不联网。至少覆盖:
198
+
199
+ 1. 打招呼 / 自我介绍(简短说明自己是教学用模拟模型)。
200
+ 2. `帮助`、`/help`、`你能做什么` → 能力清单 + 可试示例。
201
+ 3. 算术表达式(含 `+ - * / % ^ ( )`)→ 调 `calculator`;若同时出现
202
+ `记住/记下/保存`,在计算后再调 `memory_write` 存结果。
203
+ 4. `几点/时间/日期` → 调 `clock`;可链式 `记住`。
204
+ 5. `记住/记一下/记下/保存 X`(支持 `我的X是Y`、`记住 X = Y`、`记住:X`)
205
+ → 调 `memory_write`,回答已保存。
206
+ 6. `我的X是什么`、`你还记得`、`回忆`、`记忆` → 调 `memory_search`,基于结果回答;
207
+ 查不到时明确说没有。
208
+ 7. `压缩/compact/总结上下文` → 由 UI 触发 compact;模型回答压缩前后对比(可用
209
+ turn 里的 compression 数据;若 engine 实现为模型先回答再压缩也可,保持可解释)。
210
+ 8. `工具/有哪些工具` → 列出四个工具及作用。
211
+ 9. `你是谁/怎么工作/原理` → 用 3–5 句解释 agent 循环与自己的定位。
212
+ 10. `忘掉/清空记忆` → 提示在「记忆」面板可清空(不真正删除,避免误操作)。
213
+ 11. 兜底:承认自己是规则模型,复述输入,结合记忆中的事实回应,并给 2 个可试命令。
214
+ 12. 模型回答用中文、口语、简洁(一般 ≤ 120 字),可带入 token/步骤等教学解释。
215
+
216
+ 流式:最终回答按字符/词组分块输出,`pace` 控制间隔(fast≈3ms、normal≈14ms、slow≈30ms),
217
+ 总时长上限约 1.5s;工具调用步骤不用流式文本。
218
+
219
+ ### 工具注册表(20-tools.js)
220
+
221
+ ```js
222
+ const LAB_TOOLS = [{
223
+ name: 'calculator',
224
+ description: '安全计算四则/幂/括号表达式',
225
+ parameters: { type:'object', properties:{ expression:{type:'string'} }, required:['expression'] },
226
+ run(args, ctx) -> { ok: boolean, result: any, display: string },
227
+ }, ...];
228
+ ```
229
+
230
+ 四个工具:`calculator`、`clock`、`memory_write`、`memory_search`。
231
+ `run` 可以是 async。`ctx` 提供 `{ now, memory, sessionId, writeMemory, searchMemory }`。
232
+ 错误不抛异常,返回 `{ ok:false, result:{error}, display }`。
233
+
234
+ ### 测试(test/engine.test.mjs)
235
+
236
+ 用 Node 跑:`node test/engine.test.mjs`。因为源文件是普通脚本,用 `node:vm`:
237
+ 拼接 `00-core.js + 10-engine.js + 20-tools.js + 30-llm.js`,追加
238
+ `;globalThis.__LAB__ = { createLab, estimateTokens, LAB_TOOLS, LAB_BUDGET };`,
239
+ 在 sandbox 里执行(sandbox 需提供 `console`、`setTimeout`、`clearTimeout`、`Date`、`JSON`、
240
+ `Math`、`globalThis`)。`createLab({ sleep: async () => {}, storage: mockStorage() })`
241
+ 让测试瞬间完成。需覆盖:
242
+
243
+ - token 估算与工具 schema 存在。
244
+ - 算术:`算一下 (12+30)*3` → 出现 tool_call/工具结果/最终含 `126`;`messages` 含 tool 角色。
245
+ - 记忆:`记住 我的项目代号是 Orion` 后,新会话里问 `我的项目代号是什么` → 回答含 Orion。
246
+ - 压缩:连续提交 ≥ 10 轮 → `stats.compressions > 0`、`context.summary` 非空、
247
+ `context.used <= context.limit`、transcript 有 `compress` 行。
248
+ - 会话:newSession 后 messages 隔离、memory 共享;switch/delete 正确。
249
+ - 持久化:dispose 后用同一 storage 重建 → memory 与 sessions 仍在。
250
+ - 手动 compact。
251
+ - 失败工具:`计算 1/0` 之类 → 工具返回 ok:false 或明确错误,最终回答提及失败。
252
+
253
+ 测试输出 `PASS` / `FAIL` 行,失败以非零退出码结束。
254
+
255
+ ## 源码浏览(docs-dev 消费)
256
+
257
+ `build.mjs` 会在 factory 顶部注入:
258
+
259
+ ```js
260
+ const SOURCES = [{ id:'10-engine', name:'10-engine.js', role:'…', code:'…' }, ...];
261
+ ```
262
+
263
+ `70-docs.js`、`80-source.js` 里可直接读 `SOURCES`(做 `typeof SOURCES === 'undefined'` 保护)。
264
+ 源码页需:左侧文件列表(名称 + 一行角色说明),右侧代码区(行号、等宽、深浅色都清晰、
265
+ 复制按钮、横向滚动),并标注「这就是当前页面正在运行的代码」。
266
+
267
+ ## UI 约定(Lead 负责 40/50/60/90;docs-dev 只需遵守配色与命名)
268
+
269
+ - 页面根 class:`al-root`;所有自写 class 加前缀:Lead `al-`,docs `al-doc-` / `al-src-`。
270
+ - 主题:优先用 `--dsw-alias-*` tokens(见 Theme inspect);TUI 终端是「艺术品」,
271
+ 允许固定的深色配色(`#0b0e14` 底、`#7ee787` 绿、`#79c0ff` 蓝、`#d2a8ff` 紫、
272
+ `#ffa657` 橙、`#ff7b72` 红、`#8b949e` 灰),正文等 UI 用主题 token。
273
+ - 页面顶部用 `padding-top: calc(var(--dsh-frame-top-clearance, 0px) + 24px)` 避开标题栏。
274
+ - 不使用 `document.body` 追加、不替换 app root、不读写其他插件 DOM。
275
+ - 组件用函数组件 + hooks;资源(timer/listener)在 `useEffect` 清理。
276
+ - docs 页若需要样式,组件内渲染 `<style dangerouslySetInnerHTML={{__html: CSS}} />` 即可。
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 wonanut
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,300 @@
1
- # Temporary Holding Version
1
+ # Agent 实验室 · Agent Lab
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ 在 DSH Web UI 里打开的一间「Agent 解剖教室」:左边是会话,中间是一个模拟的 TUI 终端,右边是实时检查器。输入一句话,就能看到它如何经过 **上下文组装 → 模型决策 → 工具调用 → 记忆写入 → 输出**,并且能随时翻到「源码」页看正在运行的代码、到「原理」页把 agent 的骨架读明白。
4
+
5
+ - **默认可离线,也能接真模型**:默认是规则驱动的模拟模型,不联网、无需 API key、回答可复现;终端右上角可切到「真实」模式,用 DSH 当前登录的 DeepSeek 账号模型跑同一个循环。
6
+ - **完全可读**:插件自己的 13 个客户端源文件 + Host 半侧 `index.js` 在构建时全部内嵌,「源码」页所见即所跑。
7
+ - **教学向**:三栏界面 + 12 节中文原理长文 + 轻量语法高亮源码浏览器。
8
+ - **零依赖**:Client 半侧只依赖 DSH 提供的 React,没有第三方包。
9
+
10
+ ---
11
+
12
+ ## 预览
13
+
14
+ 浅色主题(真实 DSH Web UI 截图,左中右三栏:会话 / TUI / 检查器):
15
+
16
+ ![体验 TUI(浅色)](preview/overview-light.png)
17
+
18
+ 深色主题,右侧「流转」里可以看到压缩阶段与本轮的工具调用链:
19
+
20
+ ![体验 TUI(深色)](preview/overview-dark.png)
21
+
22
+ 「上下文」检查器:token 分段、本轮召回、压缩摘要与模型实际收到的消息:
23
+
24
+ ![上下文检查器](preview/inspector-context.png)
25
+
26
+ 「源码」页与「原理」页:
27
+
28
+ ![源码浏览器](preview/source.png)
29
+
30
+ ![Agent 原理](preview/principles.png)
31
+
32
+ ---
33
+
34
+ ## 快速开始
35
+
36
+ ### 0. 前置条件
37
+
38
+ - DSH Web UI(当前 profile 已启用 Web 客户端)。
39
+ - Node.js 18+,用于构建 `client.js` 与运行无头测试。
40
+
41
+ ### 1. 构建
42
+
43
+ ```bash
44
+ cd agent-lab-plugin
45
+ node build.mjs # 生成 client.js,并用 node:vm 做语法校验
46
+ node build.mjs --check # 只校验、不写文件
47
+ ```
48
+
49
+ `build.mjs` 会把 `src/` 下按编号排序的源文件拼成一个 Client factory,同时把每个源文件的内容以 JSON 字符串注入为全局 `SOURCES` —— 这就是「源码」页的数据来源。**修改 `src/` 后必须重新构建**,源码页和界面才会更新。
50
+
51
+ ### 2. 安装
52
+
53
+ **方式 A:npm(推荐)**
54
+
55
+ 在 DSH 会话里让 Agent 执行 `plugin_manager install_bundle`,target 填包名与版本:
56
+
57
+ ```text
58
+ @wonanut/dsh-agent-lab@0.2.0
59
+ ```
60
+
61
+ (npm 包地址:https://www.npmjs.com/package/@wonanut/dsh-agent-lab)
62
+
63
+ **方式 B:从 GitHub 安装**
64
+
65
+ 在 DSH 会话里让 Agent 执行 `plugin_manager install_bundle`,target 使用「commit + 子目录」形式(把 `<commit>` 换成 [main 分支提交](https://github.com/wonanut/dsh-plugins/commits/main) 的 sha):
66
+
67
+ ```text
68
+ git+https://github.com/wonanut/dsh-plugins.git#<commit>&path:/agent-lab-plugin
69
+ ```
70
+
71
+ 安装后 bundle 的 `cordis.patch.yml` 会把 `@wonanut/dsh-agent-lab` 插入当前 profile,DSH 客户端随即可用。
72
+
73
+ **方式 C:本地开发安装**
74
+
75
+ 克隆仓库后,把插件子目录的**绝对路径**作为 target:
76
+
77
+ ```text
78
+ plugin_manager install_bundle
79
+ target: /path/to/dsh-plugins/agent-lab-plugin
80
+ ```
81
+
82
+ ### 3. 启用与打开
83
+
84
+ - 刷新 DSH Web UI。
85
+ - 左侧导航栏出现「**Agent 实验室**」入口(sidebar.panellist),点击即可进入整页。
86
+ - 如需确认安装状态:`plugin_manager list_bundles`(或 `list_plugins`)应能看到 `@wonanut/dsh-agent-lab`;需要移除时用 `plugin_manager remove_bundle`,目标为包名 `@wonanut/dsh-agent-lab`。
87
+
88
+ > 提示:`index.js`(Host 半侧)提供真实模型路由与状态接口;页面主体与 Agent 引擎都在 `client.js` 里。仓库已提交构建产物,安装者不需要本地构建。
89
+
90
+ ---
91
+
92
+ ## 界面与交互
93
+
94
+ 页面由同一套外壳承载四个顶部页签:**体验 TUI / 数据流 / 源码 / 原理**。
95
+
96
+ ### 真实 DeepSeek 模式
97
+
98
+ 页面右上角「模型模式」或终端右上角的「模拟 / 真实」开关可以随时切换模型决策层:
99
+
100
+ | | 模拟模式 | 真实模式 |
101
+ | --- | --- | --- |
102
+ | 模型 | `30-llm.js` 规则驱动,离线可复现 | DSH 当前选中的模型(本机 `deepseek-account` / `deepseek-flash`) |
103
+ | 网络 | 无 | 通过 Host 同源路由请求 |
104
+ | 账号 | 不需要 | 使用 DSH 已登录的 DeepSeek 账号,**会消耗额度** |
105
+ | 工具 / 记忆 / 压缩 | 插件自己执行 | 插件自己执行(只有模型决策换人) |
106
+
107
+ - 浏览器只调用同源 `POST /agent-lab/complete`;Host 半侧 `index.js` 用 `ctx.agentDefaultModel.currentSelection()` + `ctx.llm.stream()` 调模型,账号 token 始终留在 Host,页面与构建产物里没有密钥。
108
+ - 模型使用原生 tool-calling:请求携带 calculator / clock / dice_roll / random_pick / password_generate / unit_convert / memory_write / memory_search 的 schema,返回 `tool_calls` 或最终文本;引擎按同一套循环执行并回填。
109
+ - 为让工具循环保持最小可解释,真实模式关闭了 thinking(`reasoningEffort: "off"`),并且当前版本是完整响应后回传(终端仍用本地打字动画)。
110
+ - 真实模式不可用(Host 路由未加载 / 未登录)时开关会禁用并显示原因;错误会以中文错误行出现在终端和「流转」里。
111
+ - 想换成别的模型,只需在 DSH 设置 → 模型里切换当前模型,下一次请求生效。
112
+
113
+ ## 体验 TUI(默认)
114
+
115
+ - **左栏 · 会话**:会话列表(标题、消息数、token、更新时间)。点「+」新建;点会话切换(运行中的 turn 不可切换);删除需再点一次确认;底部显示长期记忆条数,并支持「重置全部本地数据」。
116
+ - **中栏 · 模拟终端**:输入一句话回车发送。终端里会逐行出现用户、模型、工具调用(`⚙`)、工具结果(`←`)、记忆(`◈`)、压缩(`⇣`)与最终回答(`agent`)。底部有命令快捷 chips 与输出速度控制(fast / normal / slow)。
117
+ - **右栏 · 检查器**:三个页签
118
+ - **流转**:本轮 turn 的阶段时间线(input → context → compress → model → tool → memory → output),可展开看到该阶段细节。
119
+ - **上下文**:本轮真实拼装的上下文分段(system / summary / memory / recent / input)、用量与压缩摘要。
120
+ - **记忆**:长期记忆条目,可单条删除或全部清空。
121
+ - **顶部动作**:「新建会话」;「真实 Agent 跑一遍」会把当前这轮输入(没有则用一句示例提示)交给真实 DSH Agent,在新会话里再跑一遍(依赖 DSH 的 workspace 能力,失败会有 toast 提示)。
122
+
123
+ ### 源码
124
+
125
+ 左侧是构建快照里的文件列表(文件名 + 角色 + 行数),右侧是带行号、可横向滚动、带轻量语法高亮(注释 / 字符串 / 模板字符串 / 关键字 / 数字)的代码区,右上角「复制源码」一键复制当前文件全文。顶部明确写着:**这就是当前页面正在运行的代码**。
126
+
127
+ ### 原理
128
+
129
+ 12 节中文长文 + 图解:Agent = LLM + 工具 + 循环 + 记忆、最小循环图、一次输入的旅程、上下文预算与压缩、短期/长期记忆、会话、工具调用协议、pi 的极简哲学、与真实 DSH Agent 的对照、换成真实 LLM(含 key 安全与前端 CORS)、新增工具、源码阅读顺序。左侧目录可点击跳转。
130
+
131
+ ### 数据流
132
+
133
+ 第四个顶部页签「数据流」把当前轮次展开成一条可点击、可回放的流水线:输入 → 上下文拼装 → 压缩(如有)→ 模型请求/响应 → 工具执行 → 记忆写入 → 输出。每个节点都能单独查看本步数据、完整消息与结构化 `stage.detail`;支持「上一步 / 播放 / 下一步」,运行中自动跟随最新步骤。顶部也可以直接调整教学上下文预算(1K / 2K / 8K / 32K / 128K / 1M,默认 1K)。
134
+
135
+ ![数据流视图](preview/dataflow.png)
136
+
137
+ ### 3 分钟体验脚本
138
+
139
+ 1. 在终端输入 `算一下 (12+30)*3 然后记住结果`:看「流转」出现两次工具调用,终端出现 `⚙ calculator`、`← 126` 与 `◈ 记忆已保存`。
140
+ 2. 输入 `记住 我的项目代号是 Orion`,然后新建会话,再问 `我的项目代号是什么`:会话是新的,长期记忆还在。
141
+ 3. 连续提交 8~10 轮对话:观察 `⇣ 上下文已压缩` 行与「上下文」页签的 summary/用量变化;也可以直接输入 `/compact`。
142
+ 4. 切到「源码」页,选 `10-engine.js`,对照「流转」页签找到上下文组装与压缩逻辑。
143
+ 5. 切到「原理」页,按目录读一遍 §03 与 §07,再回来对照实现。
144
+
145
+ ### 常用斜杠命令
146
+
147
+ | 命令 | 作用 |
148
+ | --- | --- |
149
+ | `/help` | 让模型列出能力与可试示例 |
150
+ | `/compact` | 手动压缩上下文 |
151
+ | `/new` | 新建会话 |
152
+ | `/memory` | 切到检查器「记忆」页签 |
153
+ | `/flow` | 切到检查器「流转」页签 |
154
+ | `/clear` | 开始新会话(清空当前上下文视图) |
155
+
156
+ ---
157
+
158
+ ## 数据与隐私
159
+
160
+ - 会话、消息、长期记忆、transcript 全部存在浏览器 `localStorage`,键为 `dsh.agent-lab.v1`;删除该站点的本地数据或点「重置全部本地数据」即可清空。
161
+ - 体验页的模型是**规则驱动的模拟模型**,不会发起网络请求,回答也不代表任何真实 LLM。
162
+ - 唯一会「离开本地」的动作是主动点击「真实 Agent 跑一遍」:它把当前输入交给 DSH 的真实 Agent 会话,之后的行为由 DSH 的模型与工具配置决定。
163
+
164
+ ---
165
+
166
+ ## 架构与文件地图
167
+
168
+ ### 运行链路
169
+
170
+ ```
171
+ package.json (dsh.bundle.patch / dsh.client)
172
+ │
173
+ cordis.patch.yml ── 把 @wonanut/dsh-agent-lab 插入 profile
174
+ │
175
+ build.mjs ── 按编号拼接 src/*.js → client.js,并注入 SOURCES
176
+ │
177
+ DSH Web ── window.__ModuleLoader__ 加载 factory(React / e / hooks 由外层提供)
178
+ │
179
+ 95-apply.js ── 注册 sidebar.panellist(左侧入口)与 main(整页内容)
180
+ │
181
+ 90-page.js ── 页面外壳:页签、会话栏、命令、toast;组装 TUI + 检查器 + 源码 + 原理
182
+ ```
183
+
184
+ ### 文件职责
185
+
186
+ | 文件 | 角色 |
187
+ | --- | --- |
188
+ | `src/00-core.js` | 常量(默认预算 1000、保留 4 条、最多 4 步、存储键等)、token 估算、时间与截断工具 |
189
+ | `src/10-engine.js` | `createLab`:会话、长期记忆、上下文组装、压缩、turn 循环、持久化、事件与 snapshot |
190
+ | `src/20-tools.js` | `LAB_TOOLS`:calculator / clock / dice_roll / random_pick / password_generate / unit_convert / memory_write / memory_search |
191
+ | `src/30-llm.js` | 规则驱动的模拟 LLM:意图识别、工具调用计划、流式最终回答 |
192
+ | `src/40-ui-kit.js` | 全局样式、图标、按钮、chip 等 UI 原语 |
193
+ | `src/50-tui.js` | 模拟 TUI 终端组件 |
194
+ | `src/60-inspector.js` | 检查器:流转 / 上下文 / 记忆 |
195
+ | `src/70-docs.js` | 原理页 `DocsPage`:中文教学长文 + 纯 CSS 图解 |
196
+ | `src/80-source.js` | 源码页 `SourcePage`:文件列表 + 行号代码区 + 语法高亮 + 复制 |
197
+ | `src/90-page.js` | 页面外壳、顶部页签、会话栏、生命周期与命令 |
198
+ | `src/95-apply.js` | DSH slot 注册(sidebar.panellist / main) |
199
+ | `test/engine.test.mjs` | 引擎无头测试(node:vm 加载 00/10/20/30) |
200
+ | `build.mjs` | 构建 + 语法校验 + SOURCES 注入 |
201
+ | `package.json` | bundle 声明、exports、files |
202
+ | `cordis.patch.yml` | bundle 的 Cordis patch(insert agent-lab) |
203
+ | `locale/zh.json`、`locale/en.json` | bundle 元数据文案(插件管理器里显示的名称/描述) |
204
+ | `icon.svg` | bundle 图标 |
205
+ | `index.js` | Host 半侧:真实模型 status / complete 路由 |
206
+ | `README.md` | 本文件 |
207
+ | `CONTRACT.md` | 内部并行开发接口契约(实现依据) |
208
+
209
+ ### 关键设计
210
+
211
+ - **单向数据流**:`createLab` 只广播事件(`turn_start` / `stage` / `token` / `memory` / `compression` / `turn_end` …),UI 收到后重新读取 `snapshot()` 渲染。界面不自己编造状态。
212
+ - **上下文拼装**:`system`(`LAB_SYSTEM_PROMPT`)→ `summary` → `memory`(关键词召回 ≤ 3 条)→ `recent` → `input`;超过预算 90% 触发压缩,把除最近 4 条外的消息折叠进累计摘要。
213
+ - **源码即产物**:`SOURCES` 在构建时把 `src/*.js` 原样写入 `client.js`,源码页读的就是正在运行的字符串。
214
+
215
+ ---
216
+
217
+ ## 如何扩展
218
+
219
+ ### 调整「教学尺度」
220
+
221
+ 常量集中在 `src/00-core.js`,例如:
222
+
223
+ - `LAB_BUDGET = 1000`:默认教学上下文预算;页面「上下文」检查器或「数据流」页可直接切换 1K / 2K / 8K / 32K / 128K / 1M,无需重建。
224
+ - `LAB_RECENT_KEEP = 4`:压缩时保留的最近消息条数。
225
+ - `LAB_MAX_STEPS = 4`:单轮最多模型步数。
226
+ - `LAB_STORAGE_KEY = 'dsh.agent-lab.v1'`:本地存储键。
227
+ - `LAB_MAX_SESSIONS / LAB_MAX_MESSAGES / LAB_MAX_TRANSCRIPT`:持久化上限。
228
+
229
+ ### 新增一个工具
230
+
231
+ 1. 在 `src/20-tools.js` 的 `LAB_TOOLS` 里加一个对象:`name`、`description`、`parameters`(JSON Schema)、`run(args, ctx)`,返回 `{ ok, result, display }`(失败也返回 `ok:false`,不要抛异常)。
232
+ 2. 让模型会调用:规则模型在 `src/30-llm.js` 里加一条意图规则并返回 `type: 'tool_call'`;换成真实 LLM 后无需写规则,schema 随请求发出即可。
233
+ 3. 重新构建后,「体验」页触发它、「流转」页签出现 tool 节点、「源码」页自动包含新代码。
234
+ > 原理页 §11 有一份可直接照抄的 `dice_roll` 完整示例。
235
+
236
+ ### 换成真实 LLM
237
+
238
+ 替换点只有 `src/30-llm.js` 的模拟模型:保持返回 `{ type: 'final', text }` 或 `{ type: 'tool_call', name, args }`,把规则函数换成一次 HTTP 请求(OpenAI 兼容 / Anthropic 等)。**不要把 API key 写进前端**,并注意浏览器直连模型 API 的 CORS 限制 —— 正确做法是走自己的后端 / DSH Host 侧代理。原理页 §10 给出了可读的 `fetch` 示例、消息格式映射、最小代理示意与安全说明。
239
+
240
+ ### 改界面 / 文案
241
+
242
+ - 样式与 UI 原语:`src/40-ui-kit.js`(`al-` 前缀、主题 token)。
243
+ - 终端与检查器:`src/50-tui.js`、`src/60-inspector.js`。
244
+ - 页面外壳与页签:`src/90-page.js`。
245
+ - bundle 元数据(插件管理器中的名称/描述):`locale/zh.json`、`locale/en.json`;左侧入口名称在 `src/95-apply.js` 的 `label` 中。
246
+ - 语法高亮与 tokenizer:`src/80-source.js`(只上色,不改变源码文本)。
247
+
248
+ ---
249
+
250
+ ## 如何验证
251
+
252
+ ### 构建与测试
253
+
254
+ ```bash
255
+ cd agent-lab-plugin
256
+
257
+ # 1) 源文件逐个语法检查
258
+ for f in src/*.js; do node --check "$f"; done
259
+
260
+ # 2) 构建并做整包语法校验
261
+ node build.mjs
262
+
263
+ # 3) 引擎无头测试(契约要求全 PASS)
264
+ node test/engine.test.mjs
265
+ ```
266
+
267
+ ### 安装后检查
268
+
269
+ - `plugin_manager list_bundles` / `list_plugins` 能看到 `@wonanut/dsh-agent-lab`。
270
+ - 用 Slot 检查确认注册成功:`sidebar.panellist` 中有 `agent-lab`,`main` 中有 key 为 `agent-lab` 的页面。
271
+ - 刷新 DSH Web UI,左侧出现「Agent 实验室」,三个页签均可打开。
272
+
273
+ ### 手动验收清单
274
+
275
+ - [ ] 输入算术表达式,出现工具调用与正确结果,最终回答包含结果。
276
+ - [ ] `记住 X` 后新建会话仍能回忆(长期记忆共享)。
277
+ - [ ] 连续多轮或 `/compact` 触发压缩,`⇣` 行与上下文摘要出现。
278
+ - [ ] 会话新建 / 切换 / 删除 / 重置全部数据均正常。
279
+ - [ ] 检查器三个页签随运行实时更新。
280
+ - [ ] 源码页:文件切换、行号对齐、横向滚动、复制可用,高亮不破坏源码。
281
+ - [ ] 原理页:目录跳转正常,深浅色下文字与代码均清晰。
282
+ - [ ] 窄屏(约 900px)下两页不横向溢出。
283
+ - [ ] 刷新页面后会话与记忆仍在。
284
+ - [ ] 「真实 Agent 跑一遍」能创建 DSH 会话(环境支持时)。
285
+
286
+ ---
287
+
288
+ ## 已知限制
289
+
290
+ - **模拟模型**:规则驱动、覆盖常见意图,但不是真 LLM;回答质量与泛化能力有限,这是教学取舍。
291
+ - **界面语言**:页面与教学内容为中文;bundle 元数据提供中英双语(`locale/zh.json`、`locale/en.json`)。
292
+ - **教学尺度**:默认 1000 token 预算(可在页面调大)、4 步上限、4 条召回,能较快触发压缩,适合观察上下文管理。
293
+ - **存储**:仅浏览器 localStorage,不跨设备同步;清站点数据即丢失。
294
+ - **安全边界**:教学插件省略了真实产品需要的权限、沙箱、审计与限流;接入真实模型/工具时务必补上(原理页 §07、§10 有说明)。
295
+
296
+ ---
297
+
298
+ ## 设计参考
299
+
300
+ 原理页 §08 讨论了 [pi](https://pi.dev)(Mario Zechner 的极简 coding agent harness)的设计取向:小核心、最大可扩展、把上下文管理与压缩当作 harness 的关键部件。本插件在「少量内置工具 + 全部源码可读 + 复杂度集中可见」上借鉴了这一取向,但代码与工具均为本项目自己的教学实现,与 pi 无代码关系。