@migor/cli 0.10.3 → 0.10.4
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/CHANGELOG.md +419 -30
- package/LICENSE +1 -1
- package/README.md +33 -11
- package/dist/AGENTS.md +143 -20
- package/dist/cli.js +30 -22
- package/dist/create-minimal.js +46 -0
- package/dist/create.js +16 -6
- package/dist/doctor.js +40 -0
- package/dist/harvest.js +28 -7
- package/dist/inspector/fromTrace.js +9 -1
- package/dist/inspector/trace-view.bundle.js +694 -0
- package/dist/inspector-routes.js +7 -0
- package/dist/redact.js +82 -0
- package/dist/templates/README.md +55 -2
- package/dist/templates/gitignore +2 -1
- package/dist/templates/minimal/README.md +36 -0
- package/dist/templates/minimal/src/main.ts +35 -0
- package/dist/templates/package.json +2 -2
- package/dist/templates/src/app.ts +27 -12
- package/dist/templates/src/registry.ts +9 -7
- package/dist/templates/src/tools/read-file/index.ts +18 -7
- package/dist/templates.js +6 -0
- package/dist/version.js +31 -0
- package/dist/view.js +361 -0
- package/package.json +14 -6
package/dist/AGENTS.md
CHANGED
|
@@ -41,10 +41,41 @@
|
|
|
41
41
|
|
|
42
42
|
## 1. 最小可运行示例
|
|
43
43
|
|
|
44
|
+
### 1.1 先跑一个裸工具(不需要装饰器 / DI)
|
|
45
|
+
|
|
44
46
|
```ts
|
|
45
|
-
import {
|
|
47
|
+
import { createApp } from '@migor/agentia';
|
|
48
|
+
|
|
49
|
+
const app = createApp({
|
|
50
|
+
system: '你是友好的助手。',
|
|
51
|
+
tools: [
|
|
52
|
+
{
|
|
53
|
+
name: 'say_hello',
|
|
54
|
+
description: '向某人打招呼',
|
|
55
|
+
inputSchema: {
|
|
56
|
+
type: 'object',
|
|
57
|
+
properties: { name: { type: 'string' } },
|
|
58
|
+
required: ['name'],
|
|
59
|
+
},
|
|
60
|
+
run: (input) => `你好,${(input as { name: string }).name}`,
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const { result } = await app.run([{ role: 'user', content: '跟小明打个招呼' }]);
|
|
66
|
+
if (result.error) throw new Error(`${result.stopReason}: ${result.error.message}`);
|
|
67
|
+
console.log(result.finalText, result.trace.totalUsage);
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
这是最短路径:`tools + createApp + app.run`。`inputSchema` 约束模型入参,`run` 是实际函数;
|
|
71
|
+
`result.error` 必须检查,不能只打印 `finalText`。`trace` 从第一次 run 就在,不需要先配观测后端。
|
|
72
|
+
|
|
73
|
+
也可以直接 `npx @migor/cli create my-app --minimal` 生成同形态的单文件工程(见下一节)。
|
|
74
|
+
|
|
75
|
+
### 1.2 能力变多后再用装饰器组织
|
|
46
76
|
|
|
47
|
-
|
|
77
|
+
```ts
|
|
78
|
+
import { Tool, createApp, SystemPrompt } from '@migor/agentia';
|
|
48
79
|
|
|
49
80
|
class Greeter {
|
|
50
81
|
@Tool({
|
|
@@ -80,21 +111,30 @@ console.log(result.finalText, result.stopReason, result.trace.totalUsage);
|
|
|
80
111
|
## 2. 项目结构(CLI 约定)
|
|
81
112
|
|
|
82
113
|
```bash
|
|
83
|
-
|
|
114
|
+
# 只想先跑一个 agent:单文件 + 裸工具,无装饰器 / registry / HTTP 宿主
|
|
115
|
+
npx @migor/cli create my-app --minimal
|
|
84
116
|
cd my-app && npm install # 框架与 CLI 都装进工程
|
|
85
117
|
$EDITOR .env # 填 ANTHROPIC_API_KEY(脚手架已生成,且已被 .gitignore 挡住)
|
|
118
|
+
npm run dev -- "你的问题" # = tsx src/main.ts,跑一次并退出
|
|
119
|
+
|
|
120
|
+
# 需要 inspector、能力目录与生产服务时:不带 --minimal 的默认完整模板
|
|
121
|
+
npx @migor/cli create my-service
|
|
122
|
+
cd my-service && npm install
|
|
86
123
|
npm run dev # = agentia dev:本地 inspector 面板(输入 prompt / 选能力 / 选工作目录)
|
|
87
|
-
npx agentia g tool fetch-weather
|
|
124
|
+
npx agentia g tool fetch-weather # 生成到 src/tools/fetch-weather/(skill/prompt/subagent 同理)
|
|
88
125
|
npx agentia doctor # 静态体检(未登记/悬空/命名/重复)
|
|
89
126
|
npx agentia --version # CLI 版本(= -v)
|
|
90
127
|
```
|
|
91
128
|
|
|
129
|
+
**两种模板只是起点不同**:minimal 用于先跑通模型与工具,完整模板用于继续组织能力和交付服务;
|
|
130
|
+
默认仍是不带 `--minimal` 的完整服务模板,既有项目与脚本不变。
|
|
131
|
+
|
|
92
132
|
> **命令从哪来**:脚手架把 `@migor/cli` 装进工程的 `devDependencies`,所以**工程内**用短名
|
|
93
133
|
> `npx agentia …` 即可(走本地 bin:离线可用、版本与工程一同 pin)。**首次创建**必须用带 scope 的
|
|
94
134
|
> `npx @migor/cli create` —— npm 上另有一个别人的 `agentia` 包,短名会装错东西。
|
|
95
135
|
> 想全局装上(到处都能敲 `agentia`):`npm i -g @migor/cli`。
|
|
96
136
|
|
|
97
|
-
四分类目录,一能力一文件夹:`src/tools/` · `src/skills/` · `src/prompts/` · `src/subagents/` —— **目录名就是类型**,不用记别名。每个文件夹的 `index.ts` 是入口,`default export` 支持三种形态:**类**(token = 文件夹名)、**Provider 对象**、**Provider
|
|
137
|
+
四分类目录,一能力一文件夹:`src/tools/` · `src/skills/` · `src/prompts/` · `src/subagents/` —— **目录名就是类型**,不用记别名。每个文件夹的 `index.ts` 是入口,`default export` 支持三种形态:**类**(token = 文件夹名)、**Provider 对象**、**Provider 数组**。类形态要注入依赖时,在类上写 `static readonly deps = ['WORKDIR']` —— `discover` 会把它带上(**不必**把该能力挪到显式 `providers` 再注册一遍,那样同一 token 会有两份、靠「后注册覆盖先注册」碰巧成立);`deps` 写成非字符串数组会**响亮报错**,不当成「没有依赖」。显式注册表在 `src/registry.ts`(`agentia g` 自动维护,也可手改)。
|
|
98
138
|
|
|
99
139
|
### 2.1 脚手架生成的文件(**装配与启动是分开的**)
|
|
100
140
|
|
|
@@ -206,15 +246,40 @@ token 的作用是挡住**本机其它进程**,别把它当网络边界:这
|
|
|
206
246
|
|
|
207
247
|
```bash
|
|
208
248
|
agentia report .agentia/traces.jsonl # 哪个能力慢 / 贵 / 爱失败
|
|
249
|
+
agentia view .agentia/traces.jsonl --open # 把落盘的 trace 画成一棵树(单文件 HTML,见下)
|
|
209
250
|
agentia diff .agentia/traces.jsonl ... # 改 prompt 前后逐 span 比对(有差异时退出码 1)
|
|
210
251
|
agentia harvest .agentia/traces.jsonl --failed # 失败的 run → eval 用例骨架
|
|
211
252
|
agentia export .agentia/traces.jsonl # → 训练数据集 JSONL
|
|
212
253
|
```
|
|
213
254
|
|
|
255
|
+
**`agentia view` —— 事后看一棵树**(面板只管「在飞」的,这个管「盘上」的):
|
|
256
|
+
|
|
257
|
+
```bash
|
|
258
|
+
agentia view .agentia/traces.jsonl --open # 渲完直接用默认浏览器打开
|
|
259
|
+
agentia view .agentia/traces.jsonl -o r.html # 指定产物名(缺省 = 输入名去扩展名 .html)
|
|
260
|
+
agentia view .agentia/traces.jsonl --trace-id <id> # 只看一条
|
|
261
|
+
agentia view .agentia/traces.jsonl --limit 20 # 默认最近 50 条(超了会明说,不静默截断)
|
|
262
|
+
agentia view .agentia/traces.jsonl --limit 20 --head # 从头看(排查「第一次出现的问题」时用)
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
⚠️ **`--limit` 缺省取「最近」N 条,不是前 N 条**:`.agentia/traces.jsonl` 只增不减,最新的一次
|
|
266
|
+
run 在**文件末尾** —— 取最早那种写法在长跑 dev 环里等于「想复盘刚才那次,给你今早第一批」。
|
|
267
|
+
同一节奏:产物缺省写在输入旁边,但输入在**点目录**里(`.agentia/`)时**上跳到最近的可见目录**,
|
|
268
|
+
不把 HTML 藏进隐藏目录;产物路径等于输入文件本身会直接报错(否则把 trace 数据覆盖成 HTML);
|
|
269
|
+
覆盖同名文件会在 stdout 明说(HTML 是派生物,重跑是常态,所以不像 `harvest` 那样要 `--force`)。
|
|
270
|
+
|
|
271
|
+
产物是**单文件自包含 HTML**:CSS 与渲染器**全部内联**,零外链、离线可开、可以直接贴进 PR 或
|
|
272
|
+
发给同事(业界同类工具在 2026 已收敛到这个形态)。⚠️ 三件事要知道:
|
|
273
|
+
① **默认脱敏**(`--raw` 关闭,且此时 stderr 会警告)—— 产物的默认命运是「被转发」,而 trace
|
|
274
|
+
含完整 prompt 与工具出入参;② 脱敏是 **best-effort**,覆盖 6 类值内凭证形态
|
|
275
|
+
(bearer / jwt / aws-access-key / llm-api-key / email / phone-cn),**`password=` 这类不在内**,
|
|
276
|
+
业务自有字段用 `--redact-key <字段名>`(按字段名整字段抹平,与前者是两种机制);
|
|
277
|
+
③ 多条 trace 时顶部下拉切换,`#N` 锚点可直达。
|
|
278
|
+
|
|
214
279
|
落盘**不替代**右栏的实时树:那是两条缝(落盘管「事后能查」,`onTraceEvent` 管「此刻看到」)。
|
|
215
280
|
三条如实标注:① 文件**只增不减**(要清是你自己 `rm`);② 落盘是**同步写**,单条 trace 以百 KB
|
|
216
281
|
计、且 `traceLimits.maxEvents` 本来兜上限;③ 面板的 run 列表是**内存** ring(50 条),
|
|
217
|
-
重启就空了 ——
|
|
282
|
+
重启就空了 —— 落盘让数据**没丢**,但面板不会替你回看历史,**回看历史走 `agentia view`**。
|
|
218
283
|
|
|
219
284
|
**并排 A/B(比两条 run差在哪)**:run 列表里 **⌘/Ctrl + 单击**选两条(再点一次取消、
|
|
220
285
|
选第三条挤掉最早那条),右栏切**双栏**并排两棵调用树,顶部一条差异摘要
|
|
@@ -445,7 +510,7 @@ const { result } = await app.run(messages, {
|
|
|
445
510
|
result.typed; // { answer: string } | undefined
|
|
446
511
|
```
|
|
447
512
|
|
|
448
|
-
|
|
513
|
+
模型没提交时 `typed` 为 `undefined`,且 run 以 `stopReason='result_missing'` **失败**(trace 同样记 `error`,`error.type='result_missing'`);不再把「声明了结构化结果却没收尾」当成功。子 agent 声明 `resultSchema` 后未提交也会以 `is_error` 回主 agent。
|
|
449
514
|
|
|
450
515
|
### 5.4 消息类型族(自有公共类型,与厂商 SDK 结构兼容)
|
|
451
516
|
|
|
@@ -660,6 +725,38 @@ npm run client # 另一个终端:把四个 RPC 跑一遍
|
|
|
660
725
|
方法不符 → 405(带 `Allow` 头);路径不符 → 404;body 非法 JSON → 400;body 超 `maxBodyBytes` → 413;
|
|
661
726
|
`POST /run` 超 `maxConcurrentRuns` → 503 + `Retry-After`;停机中 `POST /run`、`POST /tasks` → 503。
|
|
662
727
|
|
|
728
|
+
#### 本地怎么调(可直接复制的命令)
|
|
729
|
+
|
|
730
|
+
⚠️ 先认下这条,否则会照着别的框架的经验找错东西:**没有「一个业务一个 URL」的接口层**。
|
|
731
|
+
HTTP 面就上面那几条**固定**端点 —— 加一个 `@Tool` 不会多出一条路,**调哪个能力由模型决定,
|
|
732
|
+
不在 URL 里**(这是「换宿主不换语义」的另一半:换 HTTP / gRPC / MCP,能力代码一行不动)。
|
|
733
|
+
⇒ 要「一个能力一个接口」的形态,走下面的 ④;要常规联调,走 ①②③。
|
|
734
|
+
|
|
735
|
+
| # | 想干什么 | 怎么做 |
|
|
736
|
+
|---|---|---|
|
|
737
|
+
| ① | **不起服务**,最快跑一次 | `npm run start:batch -- "你的问题"`(脚手架的 `batch.ts`:只跑一轮、打印 `finalText`、失败置非零退出码) |
|
|
738
|
+
| ② | 起了服务后用 curl 调 | 见下面的命令块 |
|
|
739
|
+
| ③ | 带调用树可视化地调 | `agentia dev` —— 终端打出 `Inspector: http://127.0.0.1:<随机端口>/?t=<token>`;面板里选能力 / 工作目录、输 prompt,右栏实时调用树,改代码自动重启 |
|
|
740
|
+
| ④ | **单挑某一个工具**(不让模型决定) | `createMcpServer(app, { transport: 'stdio' })` ⇒ 每个 `@Tool` 变成一个标准 MCP tool,用 MCP inspector 或 `tools/call` 直接调(见 §6.5「MCP 反向桥」) |
|
|
741
|
+
|
|
742
|
+
```bash
|
|
743
|
+
curl -s localhost:3000/healthz # 先确认活着(不鉴权)
|
|
744
|
+
curl -s localhost:3000/run \
|
|
745
|
+
-H 'content-type: application/json' \
|
|
746
|
+
-d '{"prompt":"介绍一下你自己"}' # body 也收裸字符串 "..." 与 messages 数组
|
|
747
|
+
curl -sN localhost:3000/run \
|
|
748
|
+
-H 'content-type: application/json' \
|
|
749
|
+
-H 'Accept: text/event-stream' -d '"你的问题"' # -N 关掉 curl 缓冲 ⇒ 逐帧滚动
|
|
750
|
+
curl -s localhost:3000/tasks \
|
|
751
|
+
-H 'content-type: application/json' \
|
|
752
|
+
-d '{"input":"你的问题"}' # → 202 TaskRecord(拿 id 去 GET /tasks/:id 轮询)
|
|
753
|
+
```
|
|
754
|
+
|
|
755
|
+
⚠️ **两个别只看 curl 退出码的判据**:
|
|
756
|
+
① `POST /run` **失败也回 200** —— 硬失败写在响应体的 `status: "failed"` + `error` 字段里
|
|
757
|
+
(`rethrow:false` 语义:不用 HTTP 错误码表达失败)⇒ 脚本里要判 `status`,不是判 `$?`;
|
|
758
|
+
② SSE 那条要加 `-N`,否则 curl 自己缓冲着,看起来像「服务没吐东西」。
|
|
759
|
+
|
|
663
760
|
**换 model client 的缝**:HTTP 宿主**不持有 client** —— 同步 `/run` 走的是 `app.run(messages, opts)`,
|
|
664
761
|
而 `AppCallable` 就是 `{ name, run }`。所以要换 provider(OpenAI 兼容端点 / 自建 client),
|
|
665
762
|
**包一层**把 `client` 补进 `opts` 即可:
|
|
@@ -671,7 +768,7 @@ const handler = createHttpHandler(callable, { runner });
|
|
|
671
768
|
|
|
672
769
|
异步侧更直接:`AsyncRunner` 的构造选项就有 `client`。完整可跑写法见仓库 `examples/complete/`
|
|
673
770
|
(**不随 npm 包发布** —— 包里只有 `dist/`、README、LICENSE 与本说明):
|
|
674
|
-
<https://github.com/
|
|
771
|
+
<https://github.com/migoric/agentia/tree/main/examples/complete>
|
|
675
772
|
|
|
676
773
|
#### `createHttpHandler(app, opts?: HttpHandlerOptions)`
|
|
677
774
|
|
|
@@ -695,21 +792,43 @@ const handler = createHttpHandler(callable, { runner });
|
|
|
695
792
|
`abort` 收尾的,不是排空的;只关掉**旁观者长连**(`GET /tasks/:id/stream`)⇒ 不损失任何工作,
|
|
696
793
|
不影响这个布尔。宿主的 SIGTERM 决策(退出 / 再等)就依赖它。
|
|
697
794
|
**框架不订阅信号** —— `process.on('SIGTERM', () => handler.drain())` 是宿主的事(同「框架不读 env」)。
|
|
795
|
+
不想自己写那 ~20 行停机样板就用 `serve()` + `gracefulShutdown()`(2026-10-22 ref-serve-graceful-shutdown):它们把
|
|
796
|
+
「起监听 / 收连接 / 二次信号 / 停完回调」收进实现,但仍要**宿主显式调用、显式传 `signals`**
|
|
797
|
+
—— 不传就一个信号都不订阅。
|
|
698
798
|
- **`handler.runner`** —— 内部 `AsyncRunner`,需要时手动控制(`resumePending` / `awaitTask` / `list`)。
|
|
699
799
|
|
|
700
800
|
```ts
|
|
801
|
+
import {
|
|
802
|
+
createHttpHandler,
|
|
803
|
+
gracefulShutdown,
|
|
804
|
+
serve,
|
|
805
|
+
HttpException,
|
|
806
|
+
} from '@migor/agentia';
|
|
807
|
+
|
|
701
808
|
const handler = createHttpHandler(app, {
|
|
702
809
|
// 只给缝:token/JWT/签名策略由你的宿主或反代实现
|
|
703
810
|
authenticate: (req) => {
|
|
704
811
|
if (req.headers['x-api-key'] !== expected) throw new HttpException(401, { error: '无效凭据' });
|
|
705
812
|
},
|
|
706
813
|
});
|
|
707
|
-
http.createServer(handler).listen(3000);
|
|
708
814
|
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
815
|
+
// `serve` = createServer + listen + 等真的在听;`port` 是**实际**端口(传 0 时由 OS 分配)。
|
|
816
|
+
// 端口冲突 / 权限不足直接 reject —— boot 阶段静默起不来比崩更难查。
|
|
817
|
+
serve(handler, { port: 3000 }).then(({ server }) => {
|
|
818
|
+
gracefulShutdown({
|
|
819
|
+
server,
|
|
820
|
+
drain: () => handler.drain({ timeoutMs: 15_000 }),
|
|
821
|
+
// 不传 = 一个信号都不订阅(框架不做进程级决策);传了即宿主显式认领
|
|
822
|
+
signals: ['SIGTERM', 'SIGINT'],
|
|
823
|
+
timeoutMs: 15_000,
|
|
824
|
+
// 第二次信号:本原语**不吞** —— 给了这个钩子就交给你,没给就摘掉监听让 Node 默认行为接管
|
|
825
|
+
onSecondSignal: () => process.exit(1),
|
|
826
|
+
// 停机完成(`clean` = 排空且连接关干净)。本原语自己不 exit —— 退出码是宿主的编排
|
|
827
|
+
onShutdown: (clean) => {
|
|
828
|
+
console.log(clean ? '已排空' : '超时收口,剩余任务下次启动续跑');
|
|
829
|
+
process.exit(clean ? 0 : 1);
|
|
830
|
+
},
|
|
831
|
+
});
|
|
713
832
|
});
|
|
714
833
|
```
|
|
715
834
|
|
|
@@ -825,12 +944,12 @@ trace 出去之后能干什么:指标、调用树面板、调优报告、生
|
|
|
825
944
|
|
|
826
945
|
| API | 说明 |
|
|
827
946
|
|---|---|
|
|
828
|
-
| `TraceSink` | `{ export(trace) }`,run
|
|
947
|
+
| `TraceSink` | `{ export(trace) }`,run 收尾(成功/失败)都投递。抛错只告警、不影响 run;单个 sink 默认最多等待 10 秒,超时同样告警并放弃等待(**不取消底层导出**),后续 sink 仍照常接收。run 宿主与 MCP 反向桥共用这套口径 |
|
|
829
948
|
| `registerDefaultTraceSink` | 注册全局默认 sink(构造期快照合并) |
|
|
830
949
|
| `TraceRecorder` | 内存 recorder(一次 run 一个);`addLink(spanId, { traceId, spanId? })` 记一条跨 trace 链路(见 §6「跨进程关联」) |
|
|
831
950
|
| `parseTraceparent` | 解析 W3C `traceparent` 头 → `{ traceId, spanId? }`;**非法 / 缺头一律返回 `undefined`**(不抛、不打 400)—— 结果直接交给 `traceContext` 选项,见 §6「跨进程关联」 |
|
|
832
951
|
| `createOtlpExporter` | OTLP/JSON 导出,零依赖;选项见下面「`OtlpExporterOptions`」表 |
|
|
833
|
-
| `jsonlTraceSink` | JSONL 文件 sink:`jsonlTraceSink({ path })` → `TraceSink`,一行一条裸 Trace 追加落盘(父目录自动创建、追加不覆盖)—— **`agentia report` / `diff` / `harvest` / `export` 的输入格式**,见 §6
|
|
952
|
+
| `jsonlTraceSink` | JSONL 文件 sink:`jsonlTraceSink({ path })` → `TraceSink`,一行一条裸 Trace 追加落盘(父目录自动创建、追加不覆盖)—— **`agentia report` / `diff` / `harvest` / `export` / `view` 的输入格式**,见 §6「调优报告」。⚠️ **同一路径进程内幂等**(两次调用返回同一个 sink):两条 sink 对口一个文件会让一次 run 落两行同 traceId,而下面的命令按「一行 = 一次 run」计数 ⇒ 一次 run 被数两次(不报错,比不落盘更难发现)。所以「应用级 `sinks` 与 `registerDefaultTraceSink` 都往 `.agentia/traces.jsonl` 写」是安全的 |
|
|
834
953
|
| `TraceRecordEvent` | **增量记账事件**(`onTraceEvent` 的回调参数):`span.begin` / `span.end` / `span.event` / `span.attribute` / `span.link`,每条带单调 `seq`。载荷是**增量 + 此刻的拷贝**(`span.begin` 只给初始形状,属性/事件/链路各走自己的类型);按 `seq` 升序折回必须**逐字等于**收尾时的 trace |
|
|
835
954
|
| `metricsSink` | 指标累加器(Prometheus 文本 / OTLP metrics),满足 `TraceSink` 即接入 —— 见 §6「指标」 |
|
|
836
955
|
| `buildRunReport` | 从一条 trace 生成**调优报告**(能力/模型的耗时、token、成本、错误率排行)—— 见 §6「调优报告」 |
|
|
@@ -1108,14 +1227,15 @@ calls / err / 耗时照记(那是它自己这次调用的真账)。
|
|
|
1108
1227
|
> CLI 报告的聚合口径与 `agentia dev` 面板的能力排行同源(同一份 `@migor/trace-view` 实现)。
|
|
1109
1228
|
|
|
1110
1229
|
那个 jsonl 从哪来 —— 框架内置了文件 sink,一行接入(零依赖)。它产出的格式就是
|
|
1111
|
-
**CLI
|
|
1112
|
-
都吃这个文件(一行一个 JSON,裸 Trace):
|
|
1230
|
+
**CLI 五件套的输入**:`agentia report` / `agentia diff` / `agentia harvest` / `agentia export` /
|
|
1231
|
+
`agentia view` 都吃这个文件(一行一个 JSON,裸 Trace):
|
|
1113
1232
|
|
|
1114
1233
|
```ts
|
|
1115
1234
|
import { createApp, jsonlTraceSink } from '@migor/agentia';
|
|
1116
1235
|
|
|
1117
1236
|
const app = await createApp({ /* … */ sinks: [jsonlTraceSink({ path: 'trace.jsonl' })] });
|
|
1118
|
-
// 之后:agentia report trace.jsonl / agentia diff a.jsonl b.jsonl / agentia harvest trace.jsonl /
|
|
1237
|
+
// 之后:agentia report trace.jsonl / agentia diff a.jsonl b.jsonl / agentia harvest trace.jsonl /
|
|
1238
|
+
// agentia export trace.jsonl / agentia view trace.jsonl --open
|
|
1119
1239
|
```
|
|
1120
1240
|
|
|
1121
1241
|
父目录不存在会自动递归创建;**追加不覆盖**(多跑几次就累积多行);写失败抛给 `flushSinks`
|
|
@@ -1415,7 +1535,10 @@ agentia harvest trace.jsonl --out evals/harvested.ts --force # 覆盖已存在
|
|
|
1415
1535
|
- 产物是**可粘贴进 eval 文件的用例字面量**:`client: scriptedClient([...])` 按 trace 的主循环 llm.turn 逐回合重建,`expect` 预填「主循环工具序列」的轨迹断言(文件顶部附跑法注释)。
|
|
1416
1536
|
- `--out` 指向已存在的文件时**默认拒绝覆盖**(产物是要人工核对的脚手架,重跑一次就抹掉你改过的断言与 input);要覆盖显式加 `--force`。
|
|
1417
1537
|
- ⚠️ **脚手架不是成品,人工核对后再进 CI**:
|
|
1418
|
-
- **
|
|
1538
|
+
- **assistant 文本要看 run 开没开 `traceContent: 'full'`**(§4 的 opt-in 开关):开了就取
|
|
1539
|
+
llm.turn 的 `output.text` 真值;没开则脚本**只含工具调用**,终端回合的 text 是占位
|
|
1540
|
+
`'[harvest] assistant 文本未入 trace'`(纯 tool_use 回合**不补占位** —— 那会变成「模型
|
|
1541
|
+
说过这句话」的假事实)。骨架头注会写明这一次是哪一种,不会让你自己去猜;
|
|
1419
1542
|
- 只重建**直属 run 根**的主循环回合 —— 子 agent 的嵌套回合不走主循环脚本(要覆盖子 agent 请单独写 eval);
|
|
1420
1543
|
- 预填的 `expect` 是从原 trace **抄录的实际轨迹** —— 发生过 ≠ 应该发生;
|
|
1421
1544
|
- `EvalCase` 没有 `expect` 字段,粘贴时把断言搬进 `defineEval({ expect })`(脚手架注释会教)。
|
|
@@ -1796,7 +1919,7 @@ const callable = {
|
|
|
1796
1919
|
| 观测失败被吞 | sink 抛错不影响 run(观测是辅助动作);但吞之前落一条 `console.warn`(含「trace sink」字样)—— sink 天天挂不再零信号。同理记忆水合/回写失败也不击穿 run |
|
|
1797
1920
|
| 框架不自动读 .env | 除 `AGENTIA_MODEL`(缺省模型覆盖)与 `OPENAI_API_KEY`(OpenAI 适配器)外,框架自己不去翻环境变量,也不读 `.env`;要读就在启动代码里调 `loadEnvFile()`(脚手架已内置那行),**真实环境变量优先**于文件 |
|
|
1798
1921
|
| 鉴权只是缝 | 框架**不实现** token / JWT / 签名策略,也不碰凭据 env —— `authenticate` 只承诺「拦在入口、读 body 之前」;策略是宿主或反代的事 |
|
|
1799
|
-
|
|
|
1922
|
+
| 运行时:Node 承诺,Bun / Deno 实测 | 按 Node ≥ 18 设计与测试(`engines` 写明;CI 覆盖 18 / 20 / 24 —— `import-floor` 矩阵跑下限、其余 job 跟当前 LTS);Bun 1.4 / Deno 2.9 已实测可跑(四步判定 + 各宿主最小代码见 `docs/frameworks.md`),但**不在 `engines` 承诺内**;**edge 未验证**。`SqliteTaskStore` 需 Node ≥ 22.5(`node:sqlite`),未提供时构造期抛可读报错 |
|
|
1800
1923
|
| 停机不由框架触发 | 框架给 `drain()` 但**不订阅** `SIGTERM`/`SIGINT`(不做进程级决策);信号处理是宿主的 |
|
|
1801
1924
|
| 停机可能切断 SSE | `drain()` 收口时会强制关闭仍开着的 SSE 流,`/run` 的流被收口时其 run 以 `stopReason='aborted'` 收尾(客户端应把断流当作可重试)。**没有 `timeoutMs` 时也收口**(否则等待没有终点,见上);只关旁观者长连(`/tasks/:id/stream`)**不损失工作**,那种收口不影响 `drain()` 的返回值 |
|
|
1802
1925
|
| 多进程共库:续跑认领靠**租约**,不靠时间(2026-09-28) | `resumePending({ staleAfterMs })` 的 `staleAfterMs` **不是**租约而是**兜底启发式**。`> 0` 时按两档判他进程的记录:① `ownerId` 里的主机名是本机 ⇒ **问那个 pid 还在不在**(`process.kill(pid, 0)`)——**在的绝不抢**(不管记录多老)、**不在的立刻可抢**(崩溃孤儿不等保鲜期);② **判不了**(异主机 / 升级前写下的旧格式 `ownerId` / 自定义串)⇒ 才用 `staleAfterMs`(「记录看起来还新」)。缺省 `0` = 不问他进程,与升级前逐字一致 |
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/** agentia CLI 入口:手写参数解析 + 命令分发(零依赖) */
|
|
3
|
-
import { readFileSync } from 'node:fs';
|
|
4
|
-
import { fileURLToPath } from 'node:url';
|
|
5
3
|
import { createProject } from './create.js';
|
|
6
4
|
import { generateCapability } from './generate.js';
|
|
7
5
|
import { isCapabilityType, isValidName, CAPABILITY_TYPES } from './templates.js';
|
|
@@ -13,22 +11,33 @@ import { reportCommand, USAGE as REPORT_USAGE } from './report.js';
|
|
|
13
11
|
import { harvestCommand, USAGE as HARVEST_USAGE } from './harvest.js';
|
|
14
12
|
import { diffCommand, USAGE as DIFF_USAGE } from './diff.js';
|
|
15
13
|
import { exportCommand, USAGE as EXPORT_USAGE } from './export.js';
|
|
14
|
+
import { viewCommand, USAGE as VIEW_USAGE } from './view.js';
|
|
15
|
+
import { readCliVersion } from './version.js';
|
|
16
16
|
const USAGE = `agentia —— Agentia 框架命令行工具
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
agentia create <name> [--dir <parent>]
|
|
18
|
+
核心工作流:
|
|
19
|
+
agentia create <name> [--dir <parent>] [--minimal]
|
|
20
|
+
创建项目(默认完整服务;--minimal = 单文件 + 裸工具)
|
|
20
21
|
agentia g <type> <name> 在当前目录生成能力(别名:generate)
|
|
21
22
|
type: ${CAPABILITY_TYPES.join(' | ')}
|
|
22
23
|
agentia dev [-- "问题"] 启动开发模式(常驻 runner + 本地 inspector 面板:
|
|
23
24
|
在面板里选能力/工作目录、输入 prompt 驱动一次 run,
|
|
24
25
|
并看它的调用树;改代码或 .md 自动重启)
|
|
25
26
|
agentia doctor [--json] 装配体检(未登记/悬空能力/命名规范/重复条目)
|
|
26
|
-
|
|
27
|
+
|
|
28
|
+
观测与调优:
|
|
29
|
+
agentia view <trace.jsonl> 落盘 trace → 单文件自包含 HTML(离线可开、可转发)
|
|
30
|
+
([-o <out.html>] [--open] [--raw] [--limit N] [--trace-id <id>])
|
|
31
|
+
agentia report <trace.jsonl> [--json] 生成调优报告(能力耗时/成本/错误率排行)
|
|
32
|
+
agentia diff <a.jsonl> <b.jsonl> [--json] 两条 trace 的调用树 A/B 比对(有差异时退出码 1)
|
|
33
|
+
|
|
34
|
+
评测与数据:
|
|
27
35
|
agentia harvest <trace.jsonl> 把线上 trace 翻成 eval 用例骨架
|
|
28
36
|
([--out <file.ts>] [--force] [--failed] [--limit N])
|
|
29
|
-
agentia diff <a.jsonl> <b.jsonl> [--json] 两条 trace 的调用树 A/B 比对(有差异时退出码 1)
|
|
30
37
|
agentia export <trace.jsonl> trace 落盘文件 → 训练数据集(JSONL,一行一份 messages)
|
|
31
38
|
([--out <file>] [--ok-only] [--min-score n])
|
|
39
|
+
|
|
40
|
+
能力包与其他:
|
|
32
41
|
agentia add <pkg> 安装第三方能力包并登记到 src/registry.ts
|
|
33
42
|
agentia --help 显示本帮助
|
|
34
43
|
agentia --version 显示版本(等价 -v)
|
|
@@ -40,7 +49,7 @@ name 规则:小写字母开头的小写 kebab-case(如 hello、doc-reviewer
|
|
|
40
49
|
`;
|
|
41
50
|
// 各子命令自己的用法串 —— 同时供 fail() 与「子命令级 --help」使用(单源,不写两遍)。
|
|
42
51
|
// report / harvest / diff 的用法串在各自模块里(那里本来就有),此处 import 复用。
|
|
43
|
-
const CREATE_USAGE = '用法:agentia create <name> [--dir <parent>]';
|
|
52
|
+
const CREATE_USAGE = '用法:agentia create <name> [--dir <parent>] [--minimal]';
|
|
44
53
|
const G_USAGE = `用法:agentia g <type> <name>(type: ${CAPABILITY_TYPES.join(' | ')})`;
|
|
45
54
|
const ADD_USAGE = '用法:agentia add <pkg>(npm 包名或本地包路径)';
|
|
46
55
|
/** 子命令 → 自己的用法串。`--help` 落在子命令上时打它。 */
|
|
@@ -54,6 +63,7 @@ const SUB_USAGE = {
|
|
|
54
63
|
harvest: HARVEST_USAGE,
|
|
55
64
|
diff: DIFF_USAGE,
|
|
56
65
|
export: EXPORT_USAGE,
|
|
66
|
+
view: VIEW_USAGE,
|
|
57
67
|
add: ADD_USAGE,
|
|
58
68
|
};
|
|
59
69
|
function fail(message) {
|
|
@@ -61,20 +71,6 @@ function fail(message) {
|
|
|
61
71
|
process.exitCode = 1;
|
|
62
72
|
return 1;
|
|
63
73
|
}
|
|
64
|
-
/**
|
|
65
|
-
* CLI 版本 = 本包 package.json 的 `version`(**单源**:`scripts/check-release.mjs` 已保证它
|
|
66
|
-
* 与根包一致,所以这里不另存一份常量 —— 常量会漂,读到的文件不会)。
|
|
67
|
-
* 源码直跑(`src/cli.ts`)与发布物(`dist/cli.js`)的 `../package.json` 都是包根那一份。
|
|
68
|
-
*/
|
|
69
|
-
function readCliVersion() {
|
|
70
|
-
try {
|
|
71
|
-
const pkg = JSON.parse(readFileSync(fileURLToPath(new URL('../package.json', import.meta.url)), 'utf8'));
|
|
72
|
-
return pkg.version ?? 'unknown';
|
|
73
|
-
}
|
|
74
|
-
catch {
|
|
75
|
-
return 'unknown';
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
74
|
function checkName(name) {
|
|
79
75
|
if (name === undefined)
|
|
80
76
|
return false;
|
|
@@ -103,6 +99,7 @@ function main(argv) {
|
|
|
103
99
|
if (command === 'create') {
|
|
104
100
|
const name = rest[0];
|
|
105
101
|
let parent;
|
|
102
|
+
let minimal = false;
|
|
106
103
|
for (let i = 1; i < rest.length; i += 1) {
|
|
107
104
|
if (rest[i] === '--dir') {
|
|
108
105
|
parent = rest[i + 1];
|
|
@@ -110,6 +107,9 @@ function main(argv) {
|
|
|
110
107
|
return fail('--dir 需要一个目录参数');
|
|
111
108
|
i += 1;
|
|
112
109
|
}
|
|
110
|
+
else if (rest[i] === '--minimal') {
|
|
111
|
+
minimal = true;
|
|
112
|
+
}
|
|
113
113
|
else {
|
|
114
114
|
return fail(`未知参数:${rest[i]}`);
|
|
115
115
|
}
|
|
@@ -119,7 +119,7 @@ function main(argv) {
|
|
|
119
119
|
if (!checkName(name)) {
|
|
120
120
|
return fail(`非法项目名「${name}」:需匹配小写 kebab-case(如 my-app)`);
|
|
121
121
|
}
|
|
122
|
-
return createProject(name, parent);
|
|
122
|
+
return createProject(name, parent, { minimal });
|
|
123
123
|
}
|
|
124
124
|
if (command === 'g' || command === 'generate') {
|
|
125
125
|
const [type, name, ...extra] = rest;
|
|
@@ -185,6 +185,14 @@ function main(argv) {
|
|
|
185
185
|
});
|
|
186
186
|
return 0;
|
|
187
187
|
}
|
|
188
|
+
if (command === 'view') {
|
|
189
|
+
// 异步命令(读文件/读构建期资源/写文件):同 report 的受理模式
|
|
190
|
+
void viewCommand(rest).catch((e) => {
|
|
191
|
+
console.error(`错误:${e.message}`);
|
|
192
|
+
process.exitCode = 1;
|
|
193
|
+
});
|
|
194
|
+
return 0;
|
|
195
|
+
}
|
|
188
196
|
if (command === 'add') {
|
|
189
197
|
const [pkg, ...extra] = rest;
|
|
190
198
|
if (pkg === undefined || extra.length > 0) {
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** create --minimal:单文件 + 裸工具的项目生成(与完整服务模板分文件,避免 create.ts 继续膨胀)。 */
|
|
2
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { minimalMainTs, minimalReadme, projectDotEnv, projectDotEnvExample, projectGitignore, projectPackageJson, projectTsconfig, } from './templates.js';
|
|
5
|
+
import { GUIDE_VERSION_MARKER, readCliVersion } from './version.js';
|
|
6
|
+
/** 与完整模板共享依赖和版本 pin,只收敛 scripts。 */
|
|
7
|
+
export function minimalPackageJson(name) {
|
|
8
|
+
const pkg = JSON.parse(projectPackageJson(name));
|
|
9
|
+
pkg.scripts = {
|
|
10
|
+
...pkg.scripts,
|
|
11
|
+
dev: 'tsx src/main.ts',
|
|
12
|
+
build: 'tsc -p tsconfig.json',
|
|
13
|
+
start: 'node dist/main.js',
|
|
14
|
+
};
|
|
15
|
+
delete pkg.scripts['start:prod'];
|
|
16
|
+
delete pkg.scripts['start:batch'];
|
|
17
|
+
return `${JSON.stringify(pkg, null, 2)}\n`;
|
|
18
|
+
}
|
|
19
|
+
function write(dir, rel, content) {
|
|
20
|
+
const file = join(dir, rel);
|
|
21
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
22
|
+
writeFileSync(file, content, 'utf8');
|
|
23
|
+
}
|
|
24
|
+
export function createMinimalProject(dir, name, guide) {
|
|
25
|
+
write(dir, 'package.json', minimalPackageJson(name));
|
|
26
|
+
write(dir, 'tsconfig.json', projectTsconfig());
|
|
27
|
+
write(dir, 'src/main.ts', minimalMainTs(name));
|
|
28
|
+
write(dir, 'README.md', minimalReadme(name));
|
|
29
|
+
write(dir, '.gitignore', projectGitignore());
|
|
30
|
+
write(dir, '.env', projectDotEnv());
|
|
31
|
+
write(dir, '.env.example', projectDotEnvExample());
|
|
32
|
+
write(dir, 'AGENTS.md', `${GUIDE_VERSION_MARKER(readCliVersion())}\n${guide}`);
|
|
33
|
+
console.log(`已创建最小项目 ${dir}
|
|
34
|
+
|
|
35
|
+
后续步骤:
|
|
36
|
+
cd ${dir}
|
|
37
|
+
npm install
|
|
38
|
+
把 API key 填进 .env(已生成,且已被 .gitignore 忽略)
|
|
39
|
+
npm run dev -- "你的问题" # 一个入口文件 + 一个裸工具,跑一次并退出
|
|
40
|
+
|
|
41
|
+
构建后运行:npm run typecheck && npm run build && npm start -- "你的问题"
|
|
42
|
+
|
|
43
|
+
需要 inspector、能力目录/registry 或 HTTP 服务时,用默认完整模板另建项目:
|
|
44
|
+
npx @migor/cli create my-service`);
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
package/dist/create.js
CHANGED
|
@@ -4,6 +4,8 @@ import { dirname, join, resolve } from 'node:path';
|
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { appTs, batchTs, CAPABILITY_DIR_LIST, cleanMjs, copyAssetsMjs, devConfigTs, emptyRegistryTemplate, mainTs, projectDotEnv, projectDotEnvExample, projectGitignore, projectPackageJson, projectReadme, projectTsconfig, readFileToolIndexTs, REGISTRY_PATH, sessionStoreTs, toolIndexTs, } from './templates.js';
|
|
6
6
|
import { registerCapability } from './registry.js';
|
|
7
|
+
import { GUIDE_VERSION_MARKER, readCliVersion } from './version.js';
|
|
8
|
+
import { createMinimalProject } from './create-minimal.js';
|
|
7
9
|
function write(dir, rel, content) {
|
|
8
10
|
const file = join(dir, rel);
|
|
9
11
|
mkdirSync(dirname(file), { recursive: true });
|
|
@@ -27,7 +29,7 @@ function readUsageGuide() {
|
|
|
27
29
|
}
|
|
28
30
|
throw new Error(`找不到 AI 使用说明(docs/usage-guide.md)。查找过:\n ${candidates.join('\n ')}`);
|
|
29
31
|
}
|
|
30
|
-
export function createProject(name, parent) {
|
|
32
|
+
export function createProject(name, parent, options = {}) {
|
|
31
33
|
const dir = resolve(parent ?? process.cwd(), name);
|
|
32
34
|
let guide;
|
|
33
35
|
try {
|
|
@@ -52,6 +54,8 @@ export function createProject(name, parent) {
|
|
|
52
54
|
return 1;
|
|
53
55
|
}
|
|
54
56
|
mkdirSync(dir, { recursive: true });
|
|
57
|
+
if (options.minimal === true)
|
|
58
|
+
return createMinimalProject(dir, name, guide);
|
|
55
59
|
write(dir, 'package.json', projectPackageJson(name));
|
|
56
60
|
write(dir, 'tsconfig.json', projectTsconfig());
|
|
57
61
|
// 装配(app.ts)与启动分离:dev 环要复用 app.ts 的工厂,
|
|
@@ -88,12 +92,18 @@ export function createProject(name, parent) {
|
|
|
88
92
|
// 生成 .env 却不忽略它,等于把 key 直接送进用户的第一个 commit。
|
|
89
93
|
write(dir, '.env', projectDotEnv());
|
|
90
94
|
write(dir, '.env.example', projectDotEnvExample());
|
|
91
|
-
// AI 使用说明:让 Claude Code / Cursor / Copilot 等一进项目就拿到权威 API
|
|
92
|
-
|
|
95
|
+
// AI 使用说明:让 Claude Code / Cursor / Copilot 等一进项目就拿到权威 API 速查。
|
|
96
|
+
// ⚠️ 头部带一行**版本标记**(2026-10-16 ref-scaffold-observability-gap):这份说明是
|
|
97
|
+
// `docs/usage-guide.md` 的**快照**,而 CLI **没有任何命令会更新它** —— 升级框架后它会
|
|
98
|
+
// 静默过期,偏偏 AI 就是照它写代码的。有了标记,`agentia doctor` 才能把
|
|
99
|
+
// 「说明是 vX 写的、当前 CLI 是 vY」说出口(此前只能靠人记得对拍)。
|
|
100
|
+
write(dir, 'AGENTS.md', `${GUIDE_VERSION_MARKER(readCliVersion())}\n${guide}`);
|
|
93
101
|
registerCapability(dir, 'hello', 'tool');
|
|
94
|
-
// read-file 的构造器要一个工作目录(DI
|
|
95
|
-
|
|
96
|
-
//
|
|
102
|
+
// read-file 的构造器要一个工作目录(DI 注入)。它的依赖声明**写在能力类上**
|
|
103
|
+
//(`static readonly deps = ['WORKDIR']`,discover 会带上),所以默认路由(discover)
|
|
104
|
+
// 下 `app.ts` 不必为它再注册一遍 —— 这里登记是给**显式装配路线**(registry.ts)
|
|
105
|
+
// 用的:那份 providers 里要带上 deps,顺带也让 `agentia doctor` 不报「存在但未登记」
|
|
106
|
+
//(新生成的工程不该自带一条警告)。
|
|
97
107
|
registerCapability(dir, 'read-file', 'tool', ['WORKDIR']);
|
|
98
108
|
console.log(`已创建项目 ${dir}
|
|
99
109
|
|
package/dist/doctor.js
CHANGED
|
@@ -3,6 +3,7 @@ import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { CAPABILITY_DIRS, CAPABILITY_DIR_LIST, REGISTRY_PATH, isValidName } from './templates.js';
|
|
5
5
|
import { legacyLayout, legacyMigrationHint } from './layout.js';
|
|
6
|
+
import { GUIDE_VERSION_RE, readCliVersion } from './version.js';
|
|
6
7
|
/** 能力入口候选(须与框架 src/toolkit/discover.ts 的 ENTRY_CANDIDATES 一致) */
|
|
7
8
|
const ENTRY_CANDIDATES = ['index.ts', 'index.mts', 'index.js', 'index.mjs'];
|
|
8
9
|
/**
|
|
@@ -153,6 +154,45 @@ export function doctor(opts = {}) {
|
|
|
153
154
|
warnings.push(`${REGISTRY_PATH} 条目 { provide: '${e.token}' } 指向 ${source},但该目录已不存在(悬空条目,请删除条目或恢复目录)`);
|
|
154
155
|
}
|
|
155
156
|
}
|
|
157
|
+
// trace 出口未接 → 警告(2026-10-16 ref-scaffold-observability-gap)。
|
|
158
|
+
// 框架**刻意不内建**任何出口(spec §9.3,出口只有 `sinks` 这条缝),所以「缝在那儿、
|
|
159
|
+
// 没人接」不会有任何报错:同步 `/run` 的 trace 随响应回去了,而异步 `/tasks` 的 trace
|
|
160
|
+
// 落进任务库里**没人导出** —— 等你要查「这个任务为什么慢」时它正好不在。
|
|
161
|
+
// 判据只认**代码里的** `sinks:`:整行注释掉的不算(那种情况下出口确实没接上)。
|
|
162
|
+
const appPath = join(cwd, 'src', 'app.ts');
|
|
163
|
+
if (existsSync(appPath)) {
|
|
164
|
+
const code = readFileSync(appPath, 'utf8')
|
|
165
|
+
.split('\n')
|
|
166
|
+
.filter((l) => !l.trimStart().startsWith('//'))
|
|
167
|
+
.join('\n');
|
|
168
|
+
if (/sinks\s*:/.test(code)) {
|
|
169
|
+
oks.push('src/app.ts:已配 trace 出口(sinks)');
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
warnings.push('src/app.ts 未配 sinks ⇒ trace 没有任何出口(异步 /tasks 的 trace 只会躺在任务库里、没人导出);' +
|
|
173
|
+
'接一行 jsonlTraceSink 即可,配方见框架 docs/observability.md');
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
// 项目内 AGENTS.md 的版本漂移(2026-10-16 ref-scaffold-observability-gap):它是 `create` 时
|
|
177
|
+
// 拷进来的一份**快照**,之后没有任何命令更新它,而 AI 正是照它写代码 ⇒ 框架升级后
|
|
178
|
+
// 「权威入口」静默过期。有版本标记才对得出来;没标记(旧版脚手架 / 手写)也要说一声。
|
|
179
|
+
const agentsFile = join(cwd, 'AGENTS.md');
|
|
180
|
+
if (existsSync(agentsFile)) {
|
|
181
|
+
const agentsSrc = readFileSync(agentsFile, 'utf8');
|
|
182
|
+
const marker = agentsSrc.match(GUIDE_VERSION_RE);
|
|
183
|
+
const current = readCliVersion();
|
|
184
|
+
if (marker === null) {
|
|
185
|
+
warnings.push(`AGENTS.md 没有版本标记(旧版脚手架生成或手写)⇒ 无法判断它与当前 CLI(v${current})` +
|
|
186
|
+
'是否同一版;升级后它可能与框架 API 已经不一致');
|
|
187
|
+
}
|
|
188
|
+
else if (marker[1] !== current) {
|
|
189
|
+
warnings.push(`AGENTS.md 是 v${marker[1]} 写的,当前 CLI 是 v${current} ⇒ 说明可能已过期` +
|
|
190
|
+
'(AI 照它写代码):重新 create 一份,或对照 docs/usage-guide.md 手工更新');
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
oks.push(`AGENTS.md:版本标记与 CLI 一致(v${current})`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
156
196
|
if (json) {
|
|
157
197
|
const payload = {
|
|
158
198
|
ok: oks,
|
package/dist/harvest.js
CHANGED
|
@@ -13,8 +13,13 @@
|
|
|
13
13
|
import { existsSync } from 'node:fs';
|
|
14
14
|
import { readFile, writeFile } from 'node:fs/promises';
|
|
15
15
|
import { extractTrace } from './report.js';
|
|
16
|
-
/** trace
|
|
16
|
+
/** trace 没开 `traceContent`(或该回合确实没说话)时,脚本 text 块的统一占位(与框架侧同一个词) */
|
|
17
17
|
const ASSISTANT_TEXT_PLACEHOLDER = '[harvest] assistant 文本未入 trace';
|
|
18
|
+
/** 取本回合的真实 assistant 文本 —— 只认 opt-in 的 `output.text`(与框架侧逐字同形) */
|
|
19
|
+
function assistantTextOf(turn) {
|
|
20
|
+
const text = turn.attributes?.['output.text'];
|
|
21
|
+
return typeof text === 'string' && text !== '' ? text : null;
|
|
22
|
+
}
|
|
18
23
|
/** 任意值 → 字符串(core/json.ts stringifySafe 的移植) */
|
|
19
24
|
function stringifySafe(x) {
|
|
20
25
|
if (typeof x === 'string')
|
|
@@ -96,9 +101,15 @@ export function harvestEvalCase(input) {
|
|
|
96
101
|
let tuSeq = 0;
|
|
97
102
|
const steps = mainTurns.map((turn, i) => {
|
|
98
103
|
const inputs = toolInputsOf(turn);
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
104
|
+
const text = assistantTextOf(turn);
|
|
105
|
+
const content = [];
|
|
106
|
+
if (text !== null) {
|
|
107
|
+
content.push({ type: 'text', text });
|
|
108
|
+
}
|
|
109
|
+
else if (inputs.length === 0) {
|
|
110
|
+
// 只有「既没文本又没工具」的终端回合才放占位(与框架侧逐字同步)
|
|
111
|
+
content.push({ type: 'text', text: ASSISTANT_TEXT_PLACEHOLDER });
|
|
112
|
+
}
|
|
102
113
|
for (const e of inputs) {
|
|
103
114
|
content.push({
|
|
104
115
|
type: 'tool_use',
|
|
@@ -117,11 +128,20 @@ export function harvestEvalCase(input) {
|
|
|
117
128
|
});
|
|
118
129
|
const toolSeq = mainTurns.flatMap((t) => toolInputsOf(t).map((e) => e.tool));
|
|
119
130
|
const userText = lastUserText(input.messages);
|
|
131
|
+
const textTurns = mainTurns.filter((t) => assistantTextOf(t) !== null).length;
|
|
120
132
|
const head = [
|
|
121
133
|
`// ┄┄ harvest 用例骨架:${name}(trace ${oneLine(trace.traceId ?? '')}${input.source ? `,来源 ${oneLine(input.source)}` : ''})┄┄`,
|
|
122
134
|
'// ⚠️ 脚手架,不是成品 —— 人工核对后再进 CI:',
|
|
123
|
-
'// · trace 不记 assistant 文本(llm.turn 只记 usage/事件),脚本里的 text 块是占位;',
|
|
124
135
|
];
|
|
136
|
+
if (textTurns > 0) {
|
|
137
|
+
head.push(`// · assistant 文本取自 trace 的 output.text(该 run 开了 traceContent: 'full')` +
|
|
138
|
+
(textTurns < mainTurns.length
|
|
139
|
+
? `;${mainTurns.length - textTurns} 个回合没有文本(纯 tool_use 回合本来就没文本);`
|
|
140
|
+
: ';'));
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
head.push("// · trace 里没有 assistant 文本(该 run 未开 traceContent: 'full')—— 脚本只含工具调用,", '// 终端回合的 text 是占位;要真文本请在 app 里开 traceContent 后重跑再 harvest;');
|
|
144
|
+
}
|
|
125
145
|
if (nestedCount > 0) {
|
|
126
146
|
head.push(`// · 只含主循环 ${mainTurns.length} 个回合;${nestedCount} 个子 agent 嵌套回合已略去`, '// (嵌套回合不走主循环脚本,scriptedClient 驱动不了它们;要覆盖子 agent 请单独写 eval);');
|
|
127
147
|
}
|
|
@@ -175,8 +195,9 @@ function renderEvalFile(opts) {
|
|
|
175
195
|
(opts.badLines > 0 ? `,跳过无法解析 ${opts.badLines} 行` : '');
|
|
176
196
|
const cases = opts.cases.map((c) => c.replace(/\n$/, '').replace(/^/gm, ' ')).join(',\n');
|
|
177
197
|
return `// 由 \`agentia harvest\` 生成的 eval 用例脚手架(${note})
|
|
178
|
-
// ⚠️ 这是脚手架:assistant
|
|
179
|
-
//
|
|
198
|
+
// ⚠️ 这是脚手架:assistant 文本视 trace 而定(run 开了 traceContent: 'full' 则取自
|
|
199
|
+
// output.text,否则只含工具调用);每个用例里的 expect 是
|
|
200
|
+
// 从原 trace 抄录的「实际轨迹」—— 发生过 ≠ 应该发生,人工核对后再进 CI。
|
|
180
201
|
import assert from 'node:assert/strict';
|
|
181
202
|
import { defineEval, scriptedClient } from '@migor/agentia';
|
|
182
203
|
|