minimal-agent 0.2.0 → 0.3.1
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 +54 -72
- package/package.json +18 -13
- package/plugins/ralph-wiggum/plugin.js +205 -0
- package/plugins/ralph-wiggum/src/goalState.js +260 -0
- package/plugins/ralph-wiggum/src/{sentinels.ts → sentinels.js} +4 -7
- package/plugins/ralph-wiggum/src/stopHookRunner.js +104 -0
- package/plugins/ralph-wiggum/src/verificationGate.js +202 -0
- package/plugins/workflow-runner/commands/workflow.md +13 -3
- package/plugins/workflow-runner/{plugin.ts → plugin.js} +20 -26
- package/plugins/workflow-runner/src/expressions.js +369 -0
- package/plugins/workflow-runner/src/index.js +216 -0
- package/plugins/workflow-runner/src/loader.js +183 -0
- package/plugins/workflow-runner/src/runner.js +290 -0
- package/plugins/workflow-runner/src/stepExecutors/assert.js +28 -0
- package/plugins/workflow-runner/src/stepExecutors/llm.js +44 -0
- package/plugins/workflow-runner/src/stepExecutors/skill.js +103 -0
- package/plugins/workflow-runner/src/stepExecutors/{tool.ts → tool.js} +19 -25
- package/plugins/workflow-runner/src/types.js +59 -0
- package/plugins/workflow-runner/src/{workflowState.ts → workflowState.js} +21 -40
- package/src/bootstrap/cwdArg.js +22 -0
- package/src/bootstrap/workingDir.js +31 -0
- package/src/cli/configWizard.js +272 -0
- package/src/cli/print.js +197 -0
- package/src/config/configFile.js +78 -0
- package/src/config.js +118 -0
- package/src/context/compact.js +357 -0
- package/src/context/microCompactLite.js +151 -0
- package/src/context/persistContext.js +109 -0
- package/src/context/reactiveCompact.js +121 -0
- package/src/context/sessionPath.js +58 -0
- package/src/context/snipCompact.js +112 -0
- package/src/context/tokenCounter.js +66 -0
- package/src/llm/client.js +182 -0
- package/src/loop.js +230 -0
- package/src/main.js +116 -0
- package/src/plugin-sdk.js +24 -0
- package/src/plugins/commandRouter.js +169 -0
- package/src/plugins/hookEngine.js +258 -0
- package/src/plugins/pluginApi.js +23 -0
- package/src/plugins/pluginLoader.js +71 -0
- package/src/plugins/pluginRunner.js +65 -0
- package/src/plugins/transcript.js +171 -0
- package/src/prompts/projectInstructions.js +48 -0
- package/src/prompts/skillList.js +126 -0
- package/src/prompts/system.js +155 -0
- package/src/session/runTurn.js +41 -0
- package/src/session/sessionState.js +19 -0
- package/src/tools/bash/bash.js +352 -0
- package/src/tools/bash/semantics.js +85 -0
- package/src/tools/bash/warnings.js +98 -0
- package/src/tools/edit/edit.js +253 -0
- package/src/tools/edit/multi-edit.js +155 -0
- package/src/tools/glob/glob.js +97 -0
- package/src/tools/grep/grep.js +185 -0
- package/src/tools/grep/rgPath.js +173 -0
- package/src/tools/index.js +94 -0
- package/src/tools/read/read.js +209 -0
- package/src/tools/shared/fileState.js +61 -0
- package/src/tools/shared/fileUtils.js +281 -0
- package/src/tools/shared/schemas.js +16 -0
- package/src/tools/types.js +21 -0
- package/src/tools/webbrowser/browser.js +55 -0
- package/src/tools/webbrowser/webbrowser.js +194 -0
- package/src/tools/webfetch/preapproved.js +267 -0
- package/src/tools/webfetch/webfetch.js +317 -0
- package/src/tools/websearch/websearch.js +161 -0
- package/src/tools/write/write.js +125 -0
- package/src/types/turndown.d.ts +23 -0
- package/src/types.js +16 -0
- package/src/ui/App.js +37 -0
- package/src/ui/InputBox.js +240 -0
- package/src/ui/MessageList.js +28 -0
- package/src/ui/Root.js +70 -0
- package/src/ui/StatusLine.js +41 -0
- package/src/ui/ToolStatus.js +11 -0
- package/src/ui/hooks/useChat.js +234 -0
- package/src/ui/hooks/usePasteHandler.js +137 -0
- package/src/ui/hooks/useTextBuffer.js +55 -0
- package/src/ui/hooks/useTokenUsage.js +30 -0
- package/src/ui/textBuffer.js +217 -0
- package/src/utils/packageRoot.js +37 -0
- package/src/utils/resourcePaths.js +49 -0
- package/src/utils/zodToJson.js +29 -0
- package/dist/main.js +0 -5315
- package/plugins/ralph-wiggum/plugin.ts +0 -275
- package/plugins/ralph-wiggum/scripts/setup-ralph-loop.sh +0 -203
- package/plugins/ralph-wiggum/src/goalState.ts +0 -310
- package/plugins/ralph-wiggum/src/stopHookRunner.ts +0 -136
- package/plugins/ralph-wiggum/src/verificationGate.ts +0 -252
- package/plugins/ralph-wiggum/test/goalState.test.ts +0 -410
- package/plugins/ralph-wiggum/test/verificationGate.test.ts +0 -122
- package/plugins/workflow-runner/src/expressions.ts +0 -371
- package/plugins/workflow-runner/src/index.ts +0 -194
- package/plugins/workflow-runner/src/loader.ts +0 -193
- package/plugins/workflow-runner/src/runner.ts +0 -313
- package/plugins/workflow-runner/src/stepExecutors/assert.ts +0 -30
- package/plugins/workflow-runner/src/stepExecutors/llm.ts +0 -54
- package/plugins/workflow-runner/src/stepExecutors/skill.ts +0 -115
- package/plugins/workflow-runner/src/types.ts +0 -183
- package/plugins/workflow-runner/test/cli.e2e.test.ts +0 -114
- package/plugins/workflow-runner/test/e2e.test.ts +0 -268
- package/plugins/workflow-runner/test/expressions.test.ts +0 -140
- package/plugins/workflow-runner/test/fixtures/cli-e2e.yaml +0 -27
- package/plugins/workflow-runner/test/fixtures/hello-workflow.yaml +0 -49
- package/plugins/workflow-runner/test/graceful.test.ts +0 -139
- package/plugins/workflow-runner/test/loader.test.ts +0 -216
- package/plugins/workflow-runner/test/pluginRunner.isolation.test.ts +0 -230
- package/plugins/workflow-runner/test/runner.test.ts +0 -511
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
一个用于**学习和教学**的 Agent 项目,从 [kakadeai](../) 主仓库的 Claude Code 源码中抽取并简化而来,把"一个能调工具、能自动压缩上下文、能跑插件、能可视化编排 workflow 的 agent"在合理规模的 TypeScript 里讲清楚。
|
|
7
7
|
|
|
8
|
-
> 项目由**两个独立的 npm 包**组成:`minimal-agent`(主体 ReAct CLI)+ `
|
|
8
|
+
> 项目由**两个独立的 npm 包**组成:`minimal-agent`(主体 ReAct CLI)+ `minimal-workflw`(可视化 workflow 编辑器)。两边只通过 `workflows/*.yaml` 文件耦合,零运行时依赖。
|
|
9
9
|
|
|
10
10
|
---
|
|
11
11
|
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
↓ 读写 workflows/*.yaml
|
|
31
31
|
│
|
|
32
32
|
┌──────────────────────────────────┴───────────────────────────────┐
|
|
33
|
-
│
|
|
33
|
+
│ minimal-workflw (独立编辑器包, editor/) │
|
|
34
34
|
│ │
|
|
35
35
|
│ Bun + Vite + React + React Flow │
|
|
36
36
|
│ ┌────────┐ ┌────────┐ ┌────────┐ │
|
|
@@ -54,13 +54,15 @@
|
|
|
54
54
|
| 🧰 Skills 系统 (23) | 扫描 `skills/<name>/SKILL.md` frontmatter,按需懒加载完整 prompt |
|
|
55
55
|
| 🔌 插件系统 | `plugins/<id>/` drop-in 目录,声明式命令 / 富插件双契约;自带 ralph-loop + workflow-runner |
|
|
56
56
|
| 🎬 Workflow 引擎 | YAML DAG,7 种 step 类型(tool / llm / skill / assert / pause / branch / loop) |
|
|
57
|
-
| 🖼️ 可视化编辑器 | 独立 `
|
|
57
|
+
| 🖼️ 可视化编辑器 | 独立 `minimal-workflw` 包,拖拽组装 workflow,节点面板从 `src/tools/` + `skills/` 自动派生 |
|
|
58
58
|
|
|
59
59
|
---
|
|
60
60
|
|
|
61
|
+
> ⚠️ **0.2.0 已发布到 npm 但插件系统不可用**(dynamic import .ts 在 node_modules 下被 Node 拒绝)。请使用 **0.3.0+**:源码 import 统一 `.js` 后缀 + tsc 原地编译,Bun(dev 模式)和 Node(install 模式)共用一份目录布局。
|
|
62
|
+
|
|
61
63
|
## 系统要求
|
|
62
64
|
|
|
63
|
-
- **
|
|
65
|
+
- **Node.js ≥ 20**(npm install 后跑 bin 的运行时)或 **Bun ≥ 1.1**(推荐,dev 模式必需)
|
|
64
66
|
- 一个 OpenAI 兼容的 LLM API(MiniMax / OpenAI / DeepSeek / Kimi / 自建 vLLM 均可)
|
|
65
67
|
- *可选*:[Tavily](https://tavily.com) API key(开启 WebSearch)
|
|
66
68
|
- *可选*:[OpenRouter](https://openrouter.ai) API key(用 `image-gen-openrouter` skill 生成图片)
|
|
@@ -80,14 +82,16 @@
|
|
|
80
82
|
npm install -g minimal-agent
|
|
81
83
|
|
|
82
84
|
# 2. (可选)装可视化 workflow 编辑器
|
|
83
|
-
|
|
85
|
+
# ⚠ 当前 0.1 版编辑器 bin 直接吃 .ts,运行需要 Bun ≥1.1 在 PATH 上
|
|
86
|
+
# (主包 minimal-agent 在纯 Node 下完全可用,不依赖 Bun)
|
|
87
|
+
npm install -g minimal-workflw # 或:bun install -g minimal-workflw
|
|
84
88
|
|
|
85
89
|
# 3. 跑起来
|
|
86
90
|
minimal-agent # 启 TUI;首次会弹配置向导
|
|
87
|
-
|
|
91
|
+
minimal-workflw ~/my-agent-project # 在指定项目目录里启编辑器
|
|
88
92
|
```
|
|
89
93
|
|
|
90
|
-
两个包**独立发版、独立安装**。只想要 ReAct CLI 就装 `minimal-agent`;想拖拽编排 workflow 再加 `
|
|
94
|
+
两个包**独立发版、独立安装**。只想要 ReAct CLI 就装 `minimal-agent`;想拖拽编排 workflow 再加 `minimal-workflw`。两者通过 `<project>/workflows/*.yaml` 文件耦合,没有进程间通信。
|
|
91
95
|
|
|
92
96
|
### B. 从源码运行(贡献者 / 想读源码)
|
|
93
97
|
|
|
@@ -254,9 +258,13 @@ YAML DAG workflow 执行器,支持 7 种 step:
|
|
|
254
258
|
|
|
255
259
|
```bash
|
|
256
260
|
/workflow youtube-shorts --input topic="猫咪赛博朋克城市探险"
|
|
261
|
+
/workflow youtube-shorts "猫咪赛博朋克城市探险" # 位置参数:按 inputs 声明顺序映射,.yaml 后缀可省
|
|
257
262
|
/workflows # 列出所有 workflow
|
|
258
263
|
```
|
|
259
264
|
|
|
265
|
+
> ⚠ **位置参数依赖 yaml 文件 `inputs:` 数组的声明顺序**。重排 inputs 顺序会破坏所有现有 CLI 位置参数调用(位置参数会映射到错误的 input)。
|
|
266
|
+
> 多 input workflow 推荐使用 `--input key=value` 显式 KV 形式,更稳。
|
|
267
|
+
|
|
260
268
|
### 写自己的插件
|
|
261
269
|
|
|
262
270
|
参考 `plugins/HOW-TO-WRITE-A-PLUGIN.md`。两种契约任选其一即可生效:
|
|
@@ -268,7 +276,7 @@ YAML DAG workflow 执行器,支持 7 种 step:
|
|
|
268
276
|
|
|
269
277
|
---
|
|
270
278
|
|
|
271
|
-
##
|
|
279
|
+
## minimal-workflw 可视化编辑器(editor/ 子项目)
|
|
272
280
|
|
|
273
281
|
独立的 npm 包,运行时与 `minimal-agent` **零耦合**,仅通过 `workflows/*.yaml` + `.editor-cache/manifest.json` 两个文件交换数据。
|
|
274
282
|
|
|
@@ -276,9 +284,9 @@ YAML DAG workflow 执行器,支持 7 种 step:
|
|
|
276
284
|
|
|
277
285
|
```bash
|
|
278
286
|
# 已全局安装时
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
287
|
+
minimal-workflw # 当前 cwd 当 minimal-agent 项目根
|
|
288
|
+
minimal-workflw ~/my-project # 指定项目根
|
|
289
|
+
minimal-workflw --refresh-manifest # 强制刷新工具/skill 清单
|
|
282
290
|
|
|
283
291
|
# 从源码跑(在 editor/ 目录下)
|
|
284
292
|
bun scripts/cli.ts --project .. # 一键起后端 + Vite
|
|
@@ -289,7 +297,7 @@ bun run server # 只起 Bun.serve() 后端
|
|
|
289
297
|
> **为什么是 `bun scripts/cli.ts` 而不是 `bun run dev`?**
|
|
290
298
|
> - `bun run dev` 只起 Vite 前端,UI 能渲染但**没有后端**,文件列表 / 读写 / manifest 端点全是 404
|
|
291
299
|
> - `bun scripts/cli.ts` 是 bin 入口,**同时**起 `Bun.serve()` 文件 IO 后端 + Vite 前端,完整可用
|
|
292
|
-
> - npm 发布后用户执行的 `
|
|
300
|
+
> - npm 发布后用户执行的 `minimal-workflw` 命令就是这个 cli.ts
|
|
293
301
|
|
|
294
302
|
### 启动序列(编辑器 CLI 内部)
|
|
295
303
|
|
|
@@ -335,7 +343,7 @@ export OPENROUTER_API_KEY=sk-or-...
|
|
|
335
343
|
bun run src/main.tsx -p "/workflow youtube-shorts --input topic='猫咪赛博朋克城市探险'"
|
|
336
344
|
```
|
|
337
345
|
|
|
338
|
-
整条流水线**零代码改动** —— 4 个 skill 都是 markdown,1 个 workflow 是 yaml。在 `
|
|
346
|
+
整条流水线**零代码改动** —— 4 个 skill 都是 markdown,1 个 workflow 是 yaml。在 `minimal-workflw` 里打开 `youtube-shorts.yaml`,可视化拖拽改 prompt、换 skill、加节点。
|
|
339
347
|
|
|
340
348
|
---
|
|
341
349
|
|
|
@@ -364,8 +372,8 @@ minimal-agent/
|
|
|
364
372
|
│ ├── book-review-short.yaml
|
|
365
373
|
│ ├── e2e-write-greet.yaml
|
|
366
374
|
│ └── youtube-shorts.yaml
|
|
367
|
-
├── editor/ #
|
|
368
|
-
│ ├── package.json # name:
|
|
375
|
+
├── editor/ # minimal-workflw 独立子包
|
|
376
|
+
│ ├── package.json # name: minimal-workflw, bin: minimal-workflw
|
|
369
377
|
│ ├── scripts/
|
|
370
378
|
│ │ ├── cli.ts # bin 入口(一键起后端+前端)
|
|
371
379
|
│ │ └── server.ts # Bun.serve() 文件 IO + manifest 端点
|
|
@@ -387,9 +395,22 @@ minimal-agent/
|
|
|
387
395
|
| 包 | 路径 | 命令 | 职责 |
|
|
388
396
|
|---|---|---|---|
|
|
389
397
|
| `minimal-agent` | 仓库根 | `minimal-agent` | ReAct CLI + 工具 + 插件 + skills |
|
|
390
|
-
| `
|
|
398
|
+
| `minimal-workflw` | `editor/` | `minimal-workflw` | 可视化 workflow 编辑器 |
|
|
399
|
+
|
|
400
|
+
> **为什么不打成一个包**?90% 的 ReAct CLI 用户不需要编辑器,编辑器的依赖(React / Vite / @xyflow/react / Monaco 等)有几十 MB,不应该污染主包。两边解耦后用户按需取用,通过 `<project>/workflows/*.yaml` 文件交流,无进程间通信。
|
|
401
|
+
|
|
402
|
+
### 模块解析模式(dev vs install 通用范式)
|
|
403
|
+
|
|
404
|
+
主包采用**标准 NodeNext + tsc 原地编译**模式,解决了 npm 包"dev 跑 .ts,install 跑 .js"的通用目录差异问题:
|
|
405
|
+
|
|
406
|
+
- 源码(仓库 / dev)里 import 统一写 `.js` 后缀:`import { x } from './foo.js'`
|
|
407
|
+
- dev 模式跑 `bun run dev`:Bun 把 `'./foo.js'` 透明解析到 `./foo.ts` 源文件,**无需任何构建**
|
|
408
|
+
- publish 前 `tsc --build`:原地输出 `./foo.js`(与 .ts 兄弟位置,路径布局完全一致)
|
|
409
|
+
- `.npmignore` 排除 `*.ts/*.tsx`,tarball 里**只发 .js**
|
|
410
|
+
- install 后 Node 直接吃 .js,Bun 也吃 .js(行为统一)
|
|
411
|
+
- 源码 `plugin.ts` 里 `import '../../src/plugin-sdk.js'` 在 dev / install 两种模式下都指向同一份模块(dev=plugin-sdk.ts,install=plugin-sdk.js),**模块身份天然 singleton**
|
|
391
412
|
|
|
392
|
-
|
|
413
|
+
这是 `execa` / `p-queue` / `ink` 等纯 ESM TypeScript 库的通用做法。
|
|
393
414
|
|
|
394
415
|
### 用户安装(终端)
|
|
395
416
|
|
|
@@ -399,23 +420,19 @@ npm install -g minimal-agent
|
|
|
399
420
|
minimal-agent # 启 ReAct TUI
|
|
400
421
|
|
|
401
422
|
# 加装编辑器
|
|
402
|
-
npm install -g
|
|
403
|
-
|
|
423
|
+
npm install -g minimal-workflw
|
|
424
|
+
minimal-workflw /path/to/your-project # 启编辑器
|
|
404
425
|
```
|
|
405
426
|
|
|
406
|
-
> npm 的 `bin` 字段支持一个包注册多个命令;但本项目选择**两个独立包**而不是"一个包两个 bin",是为了**依赖隔离**(编辑器的 React 依赖不该跟着 ReAct CLI 一起装到所有人的机器上)。
|
|
407
|
-
|
|
408
427
|
### 开发者发布流程
|
|
409
428
|
|
|
410
429
|
#### 1. 发布主包
|
|
411
430
|
|
|
412
431
|
```bash
|
|
413
432
|
cd minimal-agent
|
|
414
|
-
#
|
|
415
|
-
npm
|
|
416
|
-
|
|
417
|
-
npm publish --dry-run # 验证 tarball 内容(dist + vendor + skills + plugins)
|
|
418
|
-
npm publish
|
|
433
|
+
npm version patch # 0.3.0 → 0.3.1
|
|
434
|
+
npm publish --dry-run # 验证 tarball(src/*.js + plugins/*.js + skills + workflows + vendor)
|
|
435
|
+
npm publish # prepublishOnly 自动跑 clean + tsc --build
|
|
419
436
|
```
|
|
420
437
|
|
|
421
438
|
主包 `package.json` 关键字段:
|
|
@@ -423,67 +440,32 @@ npm publish
|
|
|
423
440
|
```json
|
|
424
441
|
{
|
|
425
442
|
"name": "minimal-agent",
|
|
426
|
-
"bin": { "minimal-agent": "
|
|
427
|
-
"files": ["
|
|
443
|
+
"bin": { "minimal-agent": "src/main.js" },
|
|
444
|
+
"files": ["src", "plugins", "skills", "workflows", "vendor/ripgrep", "README.md", "LICENSE"],
|
|
428
445
|
"engines": { "node": ">=20" },
|
|
429
446
|
"scripts": {
|
|
430
|
-
"build": "
|
|
431
|
-
"
|
|
447
|
+
"build": "tsc --build",
|
|
448
|
+
"clean": "bun scripts/clean-build.ts",
|
|
449
|
+
"prepublishOnly": "bun run clean && bun run build"
|
|
432
450
|
}
|
|
433
451
|
}
|
|
434
452
|
```
|
|
435
453
|
|
|
436
|
-
`
|
|
454
|
+
`prepublishOnly` 先清旧产物再跑 tsc 原地编译,`.npmignore` 排除源码 .ts/.tsx 让 tarball 里**只有 .js**。`src/main.tsx` 首行 `#!/usr/bin/env node` 由 tsc 编译产物保留,直接作为 bin。
|
|
437
455
|
|
|
438
456
|
#### 2. 发布编辑器包
|
|
439
457
|
|
|
440
458
|
```bash
|
|
441
459
|
cd minimal-agent/editor
|
|
442
|
-
|
|
443
|
-
# 先把 package.json 里的 "private": true 删掉
|
|
444
|
-
npm version patch # 0.1.0 → 0.1.1
|
|
460
|
+
npm version patch
|
|
445
461
|
bun run build # Vite 把前端打到 editor/dist/
|
|
446
462
|
npm publish --dry-run
|
|
447
463
|
npm publish
|
|
448
464
|
```
|
|
449
465
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
- 删 `"private": true`
|
|
453
|
-
- 加 `"files": ["dist", "scripts", "public/manifest.fallback.json", "index.html", "vite.config.ts"]`(控制 tarball)
|
|
454
|
-
- 加 `"engines": { "bun": ">=1.1" }`(因为 bin 是 .ts,需要 bun shebang)
|
|
455
|
-
- `scripts/cli.ts` 首行确认有 `#!/usr/bin/env bun`
|
|
456
|
-
|
|
457
|
-
#### 3. 一键发布脚本(可选)
|
|
458
|
-
|
|
459
|
-
`scripts/publish-all.ts`(建议加上):
|
|
460
|
-
|
|
461
|
-
```ts
|
|
462
|
-
#!/usr/bin/env bun
|
|
463
|
-
import { $ } from 'bun';
|
|
464
|
-
|
|
465
|
-
const bump = process.argv[2] ?? 'patch'; // patch | minor | major
|
|
466
|
-
|
|
467
|
-
console.log('▶ 发布主包...');
|
|
468
|
-
await $`npm version ${bump}`;
|
|
469
|
-
await $`bun run build`;
|
|
470
|
-
await $`npm publish`;
|
|
471
|
-
|
|
472
|
-
console.log('▶ 发布编辑器...');
|
|
473
|
-
await $`cd editor && npm version ${bump}`;
|
|
474
|
-
await $`cd editor && bun run build`;
|
|
475
|
-
await $`cd editor && npm publish`;
|
|
476
|
-
|
|
477
|
-
console.log('✅ 完成。两个包都已发布到 npm。');
|
|
478
|
-
```
|
|
479
|
-
|
|
480
|
-
跑法:
|
|
481
|
-
|
|
482
|
-
```bash
|
|
483
|
-
bun scripts/publish-all.ts patch # 两个包都升 patch 版并发布
|
|
484
|
-
```
|
|
466
|
+
编辑器是独立子项目,Bun 跑 `.ts` 源码作为 bin(`scripts/cli.ts` 首行有 `#!/usr/bin/env bun`),与主包发版策略不同但**目录耦合零依赖**——只通过文件系统读 `<project>/workflows/*.yaml`。
|
|
485
467
|
|
|
486
|
-
####
|
|
468
|
+
#### 3. 版本独立
|
|
487
469
|
|
|
488
470
|
两个包的 version 字段**互相独立**,不必同步。改主包内核 → 只升主包;改编辑器 UI → 只升编辑器。
|
|
489
471
|
|
|
@@ -564,8 +546,8 @@ minimal-agent 是 Claude Code 源码的**教学版**:
|
|
|
564
546
|
| 改完 `~/.minimal-agent/config.json` 没生效 | 当前进程仍持有旧配置;Ctrl+C 退出后重新 `bun run start` |
|
|
565
547
|
| `rg: command not found` | 不该出现——项目自带 ripgrep;若出现说明 vendor 目录未随 npm tarball 复制 |
|
|
566
548
|
| `WebBrowser 报"无法启动浏览器"` | 需 `npm install playwright-core && npx playwright install chromium`;或改用 WebFetch |
|
|
567
|
-
| `
|
|
568
|
-
| 编辑器空白 / 节点面板没工具 | `.editor-cache/manifest.json` 缺失;`
|
|
549
|
+
| `minimal-workflw` 命令找不到 | 没装编辑器包;`npm install -g minimal-workflw` 即可 |
|
|
550
|
+
| 编辑器空白 / 节点面板没工具 | `.editor-cache/manifest.json` 缺失;`minimal-workflw --refresh-manifest` 强制刷新 |
|
|
569
551
|
| 编辑器看不到自定义 skill | 检查 `<project>/skills/<name>/SKILL.md` frontmatter 是否有 `name` 和 `description` 两个必填字段 |
|
|
570
552
|
| ffmpeg / ffprobe 未找到 | `winget install Gyan.FFmpeg` / `brew install ffmpeg` / `apt install ffmpeg` |
|
|
571
553
|
| 历史"被吃掉"了 | 上次进程崩溃前未保存;`~/.minimal-agent/last-context.json` 是最后写入的一次 |
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "minimal-agent",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "最小化 Agent 系统 ——
|
|
3
|
+
"version": "0.3.1",
|
|
4
|
+
"description": "最小化 Agent 系统 —— 10 工具 + 插件系统 + workflow DSL + 自动压缩 + OpenAI 兼容 + Ink TUI;NodeNext + tsc 原地编译,dev 用 Bun .ts、install 用 Node .js(学习/教学用)",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"author": "Bill Wang <leiwang0359@gmail.com>",
|
|
7
7
|
"repository": {
|
|
@@ -27,28 +27,34 @@
|
|
|
27
27
|
],
|
|
28
28
|
"type": "module",
|
|
29
29
|
"bin": {
|
|
30
|
-
"minimal-agent": "
|
|
30
|
+
"minimal-agent": "src/main.js"
|
|
31
31
|
},
|
|
32
|
-
"main": "dist/main.js",
|
|
33
32
|
"engines": {
|
|
34
33
|
"node": ">=20"
|
|
35
34
|
},
|
|
36
35
|
"files": [
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"plugins",
|
|
41
|
-
"
|
|
36
|
+
"src/**/*.js",
|
|
37
|
+
"src/**/*.d.ts",
|
|
38
|
+
"plugins/**/*.js",
|
|
39
|
+
"plugins/**/*.d.ts",
|
|
40
|
+
"plugins/**/.claude-plugin/**",
|
|
41
|
+
"plugins/**/commands/**",
|
|
42
|
+
"plugins/**/hooks/**",
|
|
43
|
+
"plugins/HOW-TO-WRITE-A-PLUGIN.md",
|
|
44
|
+
"skills/**",
|
|
45
|
+
"workflows/**",
|
|
46
|
+
"vendor/ripgrep/**",
|
|
42
47
|
"README.md",
|
|
43
48
|
"LICENSE"
|
|
44
49
|
],
|
|
45
50
|
"scripts": {
|
|
46
51
|
"start": "bun run src/main.tsx",
|
|
47
52
|
"dev": "bun --watch src/main.tsx",
|
|
48
|
-
"build": "
|
|
53
|
+
"build": "tsc --build",
|
|
54
|
+
"clean": "bun scripts/clean-build.ts",
|
|
49
55
|
"test": "bun test",
|
|
50
|
-
"typecheck": "tsc --noEmit",
|
|
51
|
-
"prepublishOnly": "
|
|
56
|
+
"typecheck": "tsc --noEmit -p tsconfig.typecheck.json",
|
|
57
|
+
"prepublishOnly": "bun run clean && bun run build"
|
|
52
58
|
},
|
|
53
59
|
"dependencies": {
|
|
54
60
|
"fast-glob": "^3.3.2",
|
|
@@ -63,7 +69,6 @@
|
|
|
63
69
|
"@types/node": "^20.14.0",
|
|
64
70
|
"@types/react": "^18.3.0",
|
|
65
71
|
"bun-types": "^1.3.13",
|
|
66
|
-
"tsup": "^8.5.1",
|
|
67
72
|
"typescript": "^5.5.0"
|
|
68
73
|
}
|
|
69
74
|
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================
|
|
3
|
+
* plugins/ralph-wiggum/plugin.ts —— ralph-wiggum 真·插件入口
|
|
4
|
+
* ------------------------------------------------------------
|
|
5
|
+
* 把原 src/plugins/pluginRunner.ts 的 do-while 循环驱动整段搬来。
|
|
6
|
+
* 对外通过 PluginApi.runCommand 暴露 /ralph-loop。
|
|
7
|
+
*
|
|
8
|
+
* 循环契约:
|
|
9
|
+
* - 每轮把 history 重置成进入循环前的快照(fresh context)
|
|
10
|
+
* - 用 GoalState.composeContext() 拼 PLAN/BUILD/VERIFY/HEAL 阶段信息
|
|
11
|
+
* - 跑 runQuery
|
|
12
|
+
* - 检测 <promise>DONE</promise> → runVerification → 通过则退出
|
|
13
|
+
* - 检测 <PROMISE>NEED_REPLAN</PROMISE> → forceSetPhase(PLAN)
|
|
14
|
+
* - executeStopHook 是咨询式:block 才把 reason 注入下一轮,pass 不退出
|
|
15
|
+
*
|
|
16
|
+
* 终止(独占):sentinel + verify 通过 / 达 max-iterations / abort / 安全天花板
|
|
17
|
+
*
|
|
18
|
+
* Windows 上 hooks/stop-hook.sh 不可用,但循环不依赖 hook,功能完整,
|
|
19
|
+
* 只是 hook 的咨询通道失效。
|
|
20
|
+
* ============================================================
|
|
21
|
+
*/
|
|
22
|
+
import { fileURLToPath } from 'node:url';
|
|
23
|
+
import { dirname } from 'node:path';
|
|
24
|
+
import { runQuery, getWorkingDir, } from '../../src/plugin-sdk.js';
|
|
25
|
+
import { GoalState, Phase } from './src/goalState.js';
|
|
26
|
+
import { parseVerifyArgs, runVerification } from './src/verificationGate.js';
|
|
27
|
+
import { hasCompleteSentinel, hasNeedReplanSentinel, } from './src/sentinels.js';
|
|
28
|
+
import { executeStopHook } from './src/stopHookRunner.js';
|
|
29
|
+
const PLUGIN_NAME = 'ralph-wiggum';
|
|
30
|
+
const DEFAULT_MAX_ITERATIONS = 50;
|
|
31
|
+
const SAFETY_CEILING = 200;
|
|
32
|
+
const PLUGIN_ROOT = dirname(fileURLToPath(import.meta.url));
|
|
33
|
+
function extractMaxIterations(args) {
|
|
34
|
+
const match = args.match(/--max-iterations\s+(\d+)/i);
|
|
35
|
+
return match ? parseInt(match[1], 10) : undefined;
|
|
36
|
+
}
|
|
37
|
+
async function* runRalphLoop(args, ctx) {
|
|
38
|
+
const { provider, history, signal } = ctx;
|
|
39
|
+
const maxIter = Math.min(extractMaxIterations(args) ?? DEFAULT_MAX_ITERATIONS, SAFETY_CEILING);
|
|
40
|
+
// 没有 args(用户只敲 /ralph-loop)→ 留个最小 goal placeholder,避免 GoalState.init 拒空串
|
|
41
|
+
const userGoal = args.trim() || '(未提供目标)';
|
|
42
|
+
yield {
|
|
43
|
+
type: 'plugin_progress',
|
|
44
|
+
pluginId: PLUGIN_NAME,
|
|
45
|
+
current: 0,
|
|
46
|
+
max: maxIter,
|
|
47
|
+
message: 'Ralph Wiggum copy-task loop 启动',
|
|
48
|
+
};
|
|
49
|
+
const checks = parseVerifyArgs(args);
|
|
50
|
+
// sessionTag = 插件名 → 多插件可并发不打架,/new 也能扫到清掉
|
|
51
|
+
const goalState = new GoalState(getWorkingDir(), PLUGIN_NAME);
|
|
52
|
+
await goalState.reset();
|
|
53
|
+
await goalState.init(userGoal, checks);
|
|
54
|
+
await goalState.appendProgress(`=== Loop 启动 === 目标: ${userGoal.slice(0, 120)}...`);
|
|
55
|
+
// 进循环前快照 history —— 每轮 runQuery 前用它重置,保证 fresh context
|
|
56
|
+
const baseHistory = history.slice();
|
|
57
|
+
let iterationCount = 0;
|
|
58
|
+
let consecutiveFailures = 0;
|
|
59
|
+
let currentInput = userGoal;
|
|
60
|
+
let finalAssistantMsg = null;
|
|
61
|
+
try {
|
|
62
|
+
do {
|
|
63
|
+
iterationCount++;
|
|
64
|
+
if (iterationCount > maxIter) {
|
|
65
|
+
await goalState.forceSetPhase(Phase.DONE, `达到迭代上限 ${maxIter}`);
|
|
66
|
+
await goalState.appendLearning(`[迭代上限] 循环在 ${iterationCount - 1} 轮后强制终止,可能目标过大或陷入死循环`);
|
|
67
|
+
yield {
|
|
68
|
+
type: 'error',
|
|
69
|
+
error: `Loop 已达迭代上限 ${maxIter},自动停止`,
|
|
70
|
+
};
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (signal?.aborted) {
|
|
74
|
+
yield { type: 'interrupted' };
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
yield {
|
|
78
|
+
type: 'plugin_progress',
|
|
79
|
+
pluginId: PLUGIN_NAME,
|
|
80
|
+
current: iterationCount,
|
|
81
|
+
max: maxIter,
|
|
82
|
+
};
|
|
83
|
+
// fresh context:清空 history,重置为入循环前的快照
|
|
84
|
+
history.length = 0;
|
|
85
|
+
history.push(...baseHistory);
|
|
86
|
+
const freshContext = goalState.composeContext(iterationCount);
|
|
87
|
+
const enhancedInput = `${freshContext}\n\n${currentInput}`;
|
|
88
|
+
yield* runQuery(enhancedInput, {
|
|
89
|
+
provider,
|
|
90
|
+
history,
|
|
91
|
+
signal,
|
|
92
|
+
maxTurns: ctx.maxTurns,
|
|
93
|
+
sessionState: ctx.sessionState,
|
|
94
|
+
});
|
|
95
|
+
if (signal?.aborted) {
|
|
96
|
+
yield { type: 'interrupted' };
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
// 从本轮 history 抓最后一个 assistant 消息(runQuery 已 push 进去)
|
|
100
|
+
const lastAssistantIdx = (() => {
|
|
101
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
102
|
+
if (history[i].role === 'assistant')
|
|
103
|
+
return i;
|
|
104
|
+
}
|
|
105
|
+
return -1;
|
|
106
|
+
})();
|
|
107
|
+
finalAssistantMsg =
|
|
108
|
+
lastAssistantIdx >= 0 ? history[lastAssistantIdx] : null;
|
|
109
|
+
const lastAssistantText = finalAssistantMsg
|
|
110
|
+
? typeof finalAssistantMsg.content === 'string'
|
|
111
|
+
? finalAssistantMsg.content
|
|
112
|
+
: JSON.stringify(finalAssistantMsg.content)
|
|
113
|
+
: '';
|
|
114
|
+
if (hasCompleteSentinel(lastAssistantText)) {
|
|
115
|
+
// 哨兵:可能来自任意阶段(包括 iter 1 的 PLAN),用 force 跳到 VERIFY
|
|
116
|
+
await goalState.forceSetPhase(Phase.VERIFY, '检测到完成哨兵,进入验证');
|
|
117
|
+
await goalState.appendProgress(`迭代 ${iterationCount}: 检测到完成哨兵,运行验证门...`);
|
|
118
|
+
if (checks.length > 0) {
|
|
119
|
+
const vResult = await runVerification(checks);
|
|
120
|
+
if (!vResult.passed) {
|
|
121
|
+
consecutiveFailures++;
|
|
122
|
+
await goalState.appendLearning(`[迭代 ${iterationCount}] 声称完成但验证未通过: ${vResult.summary}`);
|
|
123
|
+
yield {
|
|
124
|
+
type: 'error',
|
|
125
|
+
error: `⚠️ 验证未通过: ${vResult.summary}。继续尝试...`,
|
|
126
|
+
};
|
|
127
|
+
if (consecutiveFailures >= 3) {
|
|
128
|
+
await goalState.forceSetPhase(Phase.HEAL, `连续 ${consecutiveFailures} 次验证失败`);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
await goalState.setPhase(Phase.BUILD, '验证未通过,返回构建');
|
|
132
|
+
}
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
await goalState.appendProgress(`✅ 验证通过: ${vResult.summary}`);
|
|
136
|
+
}
|
|
137
|
+
await goalState.setPhase(Phase.DONE, 'goal complete & verified');
|
|
138
|
+
yield {
|
|
139
|
+
type: 'plugin_iteration',
|
|
140
|
+
pluginName: PLUGIN_NAME,
|
|
141
|
+
current: iterationCount,
|
|
142
|
+
max: maxIter,
|
|
143
|
+
};
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (hasNeedReplanSentinel(lastAssistantText)) {
|
|
147
|
+
await goalState.forceSetPhase(Phase.PLAN, 'agent 请求重新规划');
|
|
148
|
+
await goalState.appendLearning('[NEED_REPLAN] Agent 认为当前方案不可行,需要重新规划');
|
|
149
|
+
await goalState.appendProgress('Agent 请求 NEED_REPLAN,回 PLAN 阶段');
|
|
150
|
+
consecutiveFailures = 0;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
// PLAN 阶段跑过一轮还没哨兵,认为规划已完成,自动推进 BUILD
|
|
154
|
+
if (goalState.currentPhase === Phase.PLAN && iterationCount >= 2) {
|
|
155
|
+
await goalState.setPhase(Phase.BUILD, '规划阶段已完成,进入构建');
|
|
156
|
+
}
|
|
157
|
+
// Stop-hook 咨询式调用:block 才把 reason 注入下一轮 prompt
|
|
158
|
+
// pass / 报错都不再终止循环;终止由 sentinel/maxIter/abort/NEED_REPLAN 独占
|
|
159
|
+
const hookResult = await executeStopHook(PLUGIN_ROOT, lastAssistantText);
|
|
160
|
+
if (hookResult.decision === 'block' && hookResult.reason) {
|
|
161
|
+
currentInput = hookResult.reason;
|
|
162
|
+
consecutiveFailures = 0;
|
|
163
|
+
await goalState.recordDecision({
|
|
164
|
+
iteration: iterationCount,
|
|
165
|
+
phase: goalState.currentPhase,
|
|
166
|
+
summary: 'stop-hook 反馈',
|
|
167
|
+
}, ['继续循环', '终止'], '继续循环', hookResult.reason.slice(0, 200));
|
|
168
|
+
if (hookResult.systemMessage) {
|
|
169
|
+
baseHistory.push({
|
|
170
|
+
role: 'user',
|
|
171
|
+
content: `[Plugin Stop Hook] ${hookResult.systemMessage}`,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
await goalState.appendProgress(`迭代 ${iterationCount}: Stop hook block,注入反馈继续`);
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
await goalState.appendProgress(`迭代 ${iterationCount}: 无哨兵 / hook pass,继续下一轮`);
|
|
178
|
+
}
|
|
179
|
+
} while (true);
|
|
180
|
+
}
|
|
181
|
+
finally {
|
|
182
|
+
// 收尾:把循环里临时累积的 history 还原成 baseHistory + 最后一轮 assistant
|
|
183
|
+
// 这样 TUI 上看到的就是"一次问答",而不是 N 轮重复
|
|
184
|
+
history.length = 0;
|
|
185
|
+
history.push(...baseHistory);
|
|
186
|
+
if (finalAssistantMsg) {
|
|
187
|
+
history.push(finalAssistantMsg);
|
|
188
|
+
}
|
|
189
|
+
await goalState.cleanup();
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
const api = {
|
|
193
|
+
async *runCommand(commandName, args, ctx) {
|
|
194
|
+
if (commandName === 'ralph-loop') {
|
|
195
|
+
yield* runRalphLoop(args, ctx);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
// 其它命令(help / cancel-ralph)→ 不接管,让框架走声明式 fallback
|
|
199
|
+
yield {
|
|
200
|
+
type: 'error',
|
|
201
|
+
error: `ralph-wiggum: 命令 /${commandName} 未由 plugin.ts 接管`,
|
|
202
|
+
};
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
export default api;
|