@migor/cli 0.10.3 → 0.10.5
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 +572 -30
- package/LICENSE +1 -1
- package/README.md +33 -11
- package/dist/AGENTS.md +227 -30
- package/dist/cli.js +38 -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/manifest-runner.js +21 -0
- package/dist/manifest.js +23 -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/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,35 @@ 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 { manifestCommand } from './manifest.js';
|
|
16
|
+
import { readCliVersion } from './version.js';
|
|
16
17
|
const USAGE = `agentia —— Agentia 框架命令行工具
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
agentia create <name> [--dir <parent>]
|
|
19
|
+
核心工作流:
|
|
20
|
+
agentia create <name> [--dir <parent>] [--minimal]
|
|
21
|
+
创建项目(默认完整服务;--minimal = 单文件 + 裸工具)
|
|
20
22
|
agentia g <type> <name> 在当前目录生成能力(别名:generate)
|
|
21
23
|
type: ${CAPABILITY_TYPES.join(' | ')}
|
|
22
24
|
agentia dev [-- "问题"] 启动开发模式(常驻 runner + 本地 inspector 面板:
|
|
23
25
|
在面板里选能力/工作目录、输入 prompt 驱动一次 run,
|
|
24
26
|
并看它的调用树;改代码或 .md 自动重启)
|
|
25
27
|
agentia doctor [--json] 装配体检(未登记/悬空能力/命名规范/重复条目)
|
|
26
|
-
agentia
|
|
28
|
+
agentia manifest 真实装配 app → 能力契约 JSON(schema/owner/风险/审批/hash)
|
|
29
|
+
|
|
30
|
+
观测与调优:
|
|
31
|
+
agentia view <trace.jsonl> 落盘 trace → 单文件自包含 HTML(离线可开、可转发)
|
|
32
|
+
([-o <out.html>] [--open] [--raw] [--limit N] [--trace-id <id>])
|
|
33
|
+
agentia report <trace.jsonl> [--json] 生成调优报告(能力耗时/成本/错误率排行)
|
|
34
|
+
agentia diff <a.jsonl> <b.jsonl> [--json] 两条 trace 的调用树 A/B 比对(有差异时退出码 1)
|
|
35
|
+
|
|
36
|
+
评测与数据:
|
|
27
37
|
agentia harvest <trace.jsonl> 把线上 trace 翻成 eval 用例骨架
|
|
28
38
|
([--out <file.ts>] [--force] [--failed] [--limit N])
|
|
29
|
-
agentia diff <a.jsonl> <b.jsonl> [--json] 两条 trace 的调用树 A/B 比对(有差异时退出码 1)
|
|
30
39
|
agentia export <trace.jsonl> trace 落盘文件 → 训练数据集(JSONL,一行一份 messages)
|
|
31
40
|
([--out <file>] [--ok-only] [--min-score n])
|
|
41
|
+
|
|
42
|
+
能力包与其他:
|
|
32
43
|
agentia add <pkg> 安装第三方能力包并登记到 src/registry.ts
|
|
33
44
|
agentia --help 显示本帮助
|
|
34
45
|
agentia --version 显示版本(等价 -v)
|
|
@@ -40,7 +51,7 @@ name 规则:小写字母开头的小写 kebab-case(如 hello、doc-reviewer
|
|
|
40
51
|
`;
|
|
41
52
|
// 各子命令自己的用法串 —— 同时供 fail() 与「子命令级 --help」使用(单源,不写两遍)。
|
|
42
53
|
// report / harvest / diff 的用法串在各自模块里(那里本来就有),此处 import 复用。
|
|
43
|
-
const CREATE_USAGE = '用法:agentia create <name> [--dir <parent>]';
|
|
54
|
+
const CREATE_USAGE = '用法:agentia create <name> [--dir <parent>] [--minimal]';
|
|
44
55
|
const G_USAGE = `用法:agentia g <type> <name>(type: ${CAPABILITY_TYPES.join(' | ')})`;
|
|
45
56
|
const ADD_USAGE = '用法:agentia add <pkg>(npm 包名或本地包路径)';
|
|
46
57
|
/** 子命令 → 自己的用法串。`--help` 落在子命令上时打它。 */
|
|
@@ -50,10 +61,12 @@ const SUB_USAGE = {
|
|
|
50
61
|
generate: G_USAGE,
|
|
51
62
|
dev: '用法:agentia dev [-- "首次 run 的 prompt"]',
|
|
52
63
|
doctor: '用法:agentia doctor [--json]',
|
|
64
|
+
manifest: '用法:agentia manifest(真实装配 src/app.ts,不调用模型)',
|
|
53
65
|
report: REPORT_USAGE,
|
|
54
66
|
harvest: HARVEST_USAGE,
|
|
55
67
|
diff: DIFF_USAGE,
|
|
56
68
|
export: EXPORT_USAGE,
|
|
69
|
+
view: VIEW_USAGE,
|
|
57
70
|
add: ADD_USAGE,
|
|
58
71
|
};
|
|
59
72
|
function fail(message) {
|
|
@@ -61,20 +74,6 @@ function fail(message) {
|
|
|
61
74
|
process.exitCode = 1;
|
|
62
75
|
return 1;
|
|
63
76
|
}
|
|
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
77
|
function checkName(name) {
|
|
79
78
|
if (name === undefined)
|
|
80
79
|
return false;
|
|
@@ -103,6 +102,7 @@ function main(argv) {
|
|
|
103
102
|
if (command === 'create') {
|
|
104
103
|
const name = rest[0];
|
|
105
104
|
let parent;
|
|
105
|
+
let minimal = false;
|
|
106
106
|
for (let i = 1; i < rest.length; i += 1) {
|
|
107
107
|
if (rest[i] === '--dir') {
|
|
108
108
|
parent = rest[i + 1];
|
|
@@ -110,6 +110,9 @@ function main(argv) {
|
|
|
110
110
|
return fail('--dir 需要一个目录参数');
|
|
111
111
|
i += 1;
|
|
112
112
|
}
|
|
113
|
+
else if (rest[i] === '--minimal') {
|
|
114
|
+
minimal = true;
|
|
115
|
+
}
|
|
113
116
|
else {
|
|
114
117
|
return fail(`未知参数:${rest[i]}`);
|
|
115
118
|
}
|
|
@@ -119,7 +122,7 @@ function main(argv) {
|
|
|
119
122
|
if (!checkName(name)) {
|
|
120
123
|
return fail(`非法项目名「${name}」:需匹配小写 kebab-case(如 my-app)`);
|
|
121
124
|
}
|
|
122
|
-
return createProject(name, parent);
|
|
125
|
+
return createProject(name, parent, { minimal });
|
|
123
126
|
}
|
|
124
127
|
if (command === 'g' || command === 'generate') {
|
|
125
128
|
const [type, name, ...extra] = rest;
|
|
@@ -152,6 +155,11 @@ function main(argv) {
|
|
|
152
155
|
return fail(`未知参数:${extra[0]}`);
|
|
153
156
|
return doctor({ json: rest.includes('--json') });
|
|
154
157
|
}
|
|
158
|
+
if (command === 'manifest') {
|
|
159
|
+
if (rest.length > 0)
|
|
160
|
+
return fail(`未知参数:${rest[0]}`);
|
|
161
|
+
return manifestCommand();
|
|
162
|
+
}
|
|
155
163
|
if (command === 'report') {
|
|
156
164
|
// 异步命令(要 dynamic import 构建期拷进来的聚合实现):自己设 exitCode,
|
|
157
165
|
// 返回值仅表示「已受理」—— 挂着的 Promise 会让 Node 等到它 settle 再退出。
|
|
@@ -185,6 +193,14 @@ function main(argv) {
|
|
|
185
193
|
});
|
|
186
194
|
return 0;
|
|
187
195
|
}
|
|
196
|
+
if (command === 'view') {
|
|
197
|
+
// 异步命令(读文件/读构建期资源/写文件):同 report 的受理模式
|
|
198
|
+
void viewCommand(rest).catch((e) => {
|
|
199
|
+
console.error(`错误:${e.message}`);
|
|
200
|
+
process.exitCode = 1;
|
|
201
|
+
});
|
|
202
|
+
return 0;
|
|
203
|
+
}
|
|
188
204
|
if (command === 'add') {
|
|
189
205
|
const [pkg, ...extra] = rest;
|
|
190
206
|
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
|
|
|
@@ -93,7 +93,15 @@ function eventFull(ev) {
|
|
|
93
93
|
*/
|
|
94
94
|
export function playTrace(view, trace) {
|
|
95
95
|
const spans = Array.isArray(trace?.spans) ? trace.spans : [];
|
|
96
|
-
|
|
96
|
+
// ⚠️ 空 spans 也**必须 reset**:playTrace 的契约是「把这个 view 变成这棵树」——
|
|
97
|
+
// 直接 return 会把**上一条 trace 的树**留在页面上,而下拉里选中的是这一条
|
|
98
|
+
// (切换后看到别人的数据,比看到空白更坏;2026-10-21 在 `agentia view` 的多 trace
|
|
99
|
+
// 下拉里实测到:先选一个正常 trace,再选一个 0 spans 的,树原样不动)。
|
|
100
|
+
if (!spans.length) {
|
|
101
|
+
const label = trace?.traceId ? String(trace.traceId) : '空 trace';
|
|
102
|
+
view.reset(`run · ${label}(无 spans)`);
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
97
105
|
|
|
98
106
|
const byId = new Map(spans.map((s) => [s.spanId, s]));
|
|
99
107
|
const root = spans.find((s) => s.kind === 'run') || spans[0];
|