@migor/cli 0.2.1 → 0.2.2
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/LICENSE +21 -0
- package/README.md +64 -0
- package/dist/AGENTS.md +811 -0
- package/dist/add.js +14 -12
- package/dist/cli.js +25 -15
- package/dist/create.js +47 -7
- package/dist/dev.js +61 -12
- package/dist/doctor.js +60 -32
- package/dist/generate.js +27 -15
- package/dist/inspector/fromTrace.js +144 -0
- package/dist/inspector/index.js +6 -0
- package/dist/inspector/summary.js +106 -0
- package/dist/inspector/trace-view.css +166 -0
- package/dist/inspector/view.js +274 -0
- package/dist/inspector-page.html +157 -0
- package/dist/inspector-preload.js +31 -0
- package/dist/inspector-sink.js +18 -0
- package/dist/inspector.js +166 -0
- package/dist/layout.js +44 -0
- package/dist/registry.js +19 -12
- package/dist/report.js +125 -0
- package/dist/templates.js +56 -31
- package/package.json +9 -3
package/dist/add.js
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
|
-
/** add 命令:npm install
|
|
1
|
+
/** add 命令:npm install 第三方能力包并登记进 src/registry.ts 注册表 */
|
|
2
2
|
import { spawnSync } from 'node:child_process';
|
|
3
3
|
import { existsSync, readFileSync } from 'node:fs';
|
|
4
4
|
import { join, resolve } from 'node:path';
|
|
5
5
|
import { registerPackage } from './registry.js';
|
|
6
|
-
/** 从 add 参数解析真实包名:支持 name[@version]、@scope/name[@version]
|
|
7
|
-
function resolvePackageName(arg) {
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
/** 从 add 参数解析真实包名:支持 name[@version]、@scope/name[@version]、本地路径(. / 前缀,或 file: 协议) */
|
|
7
|
+
export function resolvePackageName(arg) {
|
|
8
|
+
// file: 协议(npm install file:./pkg 的写法)先剥前缀,再按本地路径处理
|
|
9
|
+
const local = arg.startsWith('file:') ? arg.slice('file:'.length) : arg;
|
|
10
|
+
if (local.startsWith('.') || local.startsWith('/')) {
|
|
11
|
+
const pkgFile = join(resolve(local), 'package.json');
|
|
10
12
|
if (!existsSync(pkgFile)) {
|
|
11
|
-
throw new Error(`本地包 ${
|
|
13
|
+
throw new Error(`本地包 ${local} 缺少 package.json`);
|
|
12
14
|
}
|
|
13
15
|
const name = JSON.parse(readFileSync(pkgFile, 'utf8')).name;
|
|
14
16
|
if (typeof name !== 'string' || name.length === 0) {
|
|
15
|
-
throw new Error(`本地包 ${
|
|
17
|
+
throw new Error(`本地包 ${local} 的 package.json 缺少 name 字段`);
|
|
16
18
|
}
|
|
17
19
|
return name;
|
|
18
20
|
}
|
|
@@ -33,7 +35,7 @@ export function addPackage(arg) {
|
|
|
33
35
|
const cwd = process.cwd();
|
|
34
36
|
const r = spawnSync('npm', ['install', arg], { cwd, stdio: 'inherit' });
|
|
35
37
|
if (r.error || r.status !== 0) {
|
|
36
|
-
console.error(`错误:npm install ${arg} 失败,
|
|
38
|
+
console.error(`错误:npm install ${arg} 失败,src/registry.ts 未改动`);
|
|
37
39
|
process.exitCode = 1;
|
|
38
40
|
return 1;
|
|
39
41
|
}
|
|
@@ -49,12 +51,12 @@ export function addPackage(arg) {
|
|
|
49
51
|
const token = packageNameToToken(pkgName);
|
|
50
52
|
const result = registerPackage(cwd, token, pkgName);
|
|
51
53
|
if (result === 'registered') {
|
|
52
|
-
console.log(`已登记到
|
|
54
|
+
console.log(`已登记到 src/registry.ts:import ${pkgName} → { provide: '${token}' }`);
|
|
53
55
|
}
|
|
54
56
|
else {
|
|
55
|
-
console.log(`
|
|
57
|
+
console.log(`src/registry.ts 中「${token}」已注册,跳过登记`);
|
|
56
58
|
}
|
|
57
|
-
console.log('
|
|
58
|
-
'若该包不符合约定,请手工调整
|
|
59
|
+
console.log('约定提示:第三方能力包应 default export 一个 provider 类(或 Provider 对象);' +
|
|
60
|
+
'若该包不符合约定,请手工调整 src/registry.ts 中的 import 与条目');
|
|
59
61
|
return 0;
|
|
60
62
|
}
|
package/dist/cli.js
CHANGED
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/** agentia CLI 入口:手写参数解析 + 命令分发(零依赖) */
|
|
3
3
|
import { createProject } from './create.js';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { generateCapability } from './generate.js';
|
|
5
|
+
import { isCapabilityType, isValidName, CAPABILITY_TYPES } from './templates.js';
|
|
6
6
|
import { RegistryError } from './registry.js';
|
|
7
7
|
import { devServer } from './dev.js';
|
|
8
8
|
import { doctor } from './doctor.js';
|
|
9
9
|
import { addPackage } from './add.js';
|
|
10
|
+
import { reportCommand } from './report.js';
|
|
10
11
|
const USAGE = `agentia —— Agentia 框架命令行工具
|
|
11
12
|
|
|
12
13
|
用法:
|
|
13
14
|
agentia create <name> [--dir <parent>] 创建项目脚手架(目录 <parent|当前目录>/<name>/)
|
|
14
|
-
agentia g <type> <name>
|
|
15
|
-
type: ${
|
|
16
|
-
agentia dev 启动开发模式(tsx watch
|
|
17
|
-
agentia doctor
|
|
18
|
-
agentia
|
|
15
|
+
agentia g <type> <name> 在当前目录生成能力(别名:generate)
|
|
16
|
+
type: ${CAPABILITY_TYPES.join(' | ')}
|
|
17
|
+
agentia dev 启动开发模式(tsx watch 热重载 + 本地 inspector 面板)
|
|
18
|
+
agentia doctor 装配体检(未登记/悬空能力/命名规范/重复条目)
|
|
19
|
+
agentia report <trace.jsonl> 从 trace 落盘文件生成调优报告(能力耗时/成本/错误率排行)
|
|
20
|
+
agentia add <pkg> 安装第三方能力包并登记到 src/registry.ts
|
|
19
21
|
agentia --help 显示本帮助
|
|
20
22
|
|
|
21
23
|
name 规则:小写字母开头的小写 kebab-case(如 hello、doc-reviewer)
|
|
@@ -60,16 +62,16 @@ function main(argv) {
|
|
|
60
62
|
if (command === 'g' || command === 'generate') {
|
|
61
63
|
const [type, name, ...extra] = rest;
|
|
62
64
|
if (type === undefined || name === undefined || extra.length > 0) {
|
|
63
|
-
return fail(
|
|
65
|
+
return fail(`用法:agentia g <type> <name>(type: ${CAPABILITY_TYPES.join(' | ')})`);
|
|
64
66
|
}
|
|
65
|
-
if (!
|
|
66
|
-
return fail(
|
|
67
|
+
if (!isCapabilityType(type)) {
|
|
68
|
+
return fail(`未知能力类型「${type}」,可选:${CAPABILITY_TYPES.join(' | ')}`);
|
|
67
69
|
}
|
|
68
70
|
if (!checkName(name)) {
|
|
69
|
-
return fail(
|
|
71
|
+
return fail(`非法能力名「${name}」:需匹配小写 kebab-case(如 doc-reviewer)`);
|
|
70
72
|
}
|
|
71
73
|
try {
|
|
72
|
-
return
|
|
74
|
+
return generateCapability(type, name);
|
|
73
75
|
}
|
|
74
76
|
catch (err) {
|
|
75
77
|
if (err instanceof RegistryError)
|
|
@@ -87,6 +89,15 @@ function main(argv) {
|
|
|
87
89
|
return fail(`未知参数:${rest[0]}`);
|
|
88
90
|
return doctor();
|
|
89
91
|
}
|
|
92
|
+
if (command === 'report') {
|
|
93
|
+
// 异步命令(要 dynamic import 构建期拷进来的聚合实现):自己设 exitCode,
|
|
94
|
+
// 返回值仅表示「已受理」—— 挂着的 Promise 会让 Node 等到它 settle 再退出。
|
|
95
|
+
void reportCommand(rest).catch((e) => {
|
|
96
|
+
console.error(`错误:${e.message}`);
|
|
97
|
+
process.exitCode = 1;
|
|
98
|
+
});
|
|
99
|
+
return 0;
|
|
100
|
+
}
|
|
90
101
|
if (command === 'add') {
|
|
91
102
|
const [pkg, ...extra] = rest;
|
|
92
103
|
if (pkg === undefined || extra.length > 0) {
|
|
@@ -101,9 +112,8 @@ function main(argv) {
|
|
|
101
112
|
throw err;
|
|
102
113
|
}
|
|
103
114
|
}
|
|
104
|
-
|
|
115
|
+
const code = fail(`未知命令「${command}」\n`);
|
|
105
116
|
console.error(USAGE);
|
|
106
|
-
|
|
107
|
-
return 1;
|
|
117
|
+
return code;
|
|
108
118
|
}
|
|
109
119
|
process.exitCode = main(process.argv.slice(2));
|
package/dist/create.js
CHANGED
|
@@ -1,15 +1,43 @@
|
|
|
1
1
|
/** create 命令:生成 Agentia 项目脚手架 */
|
|
2
|
-
import { existsSync, mkdirSync, readdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { dirname, join, resolve } from 'node:path';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { CAPABILITY_DIR_LIST, emptyRegistryTemplate, mainTs, projectGitignore, projectPackageJson, projectReadme, projectTsconfig, REGISTRY_PATH, toolIndexTs, } from './templates.js';
|
|
6
|
+
import { registerCapability } from './registry.js';
|
|
6
7
|
function write(dir, rel, content) {
|
|
7
8
|
const file = join(dir, rel);
|
|
8
9
|
mkdirSync(dirname(file), { recursive: true });
|
|
9
10
|
writeFileSync(file, content, 'utf8');
|
|
10
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* 使用者向 AI 说明(项目里的 AGENTS.md)的单源 = 仓库根 `docs/usage-guide.md`。
|
|
14
|
+
* - 发布物:构建时由 `scripts/copy-assets.mjs` 拷成 `dist/AGENTS.md`(here = dist);
|
|
15
|
+
* - 源码直跑(tsx):here = src,回退三级到仓库根。
|
|
16
|
+
* 找不到就**明确失败**(不生成缺说明的项目)—— 说明是 AI 写对代码的前提。
|
|
17
|
+
*/
|
|
18
|
+
function readUsageGuide() {
|
|
19
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const candidates = [
|
|
21
|
+
join(here, 'AGENTS.md'), // 发布物:dist/AGENTS.md
|
|
22
|
+
join(here, '..', '..', '..', 'docs', 'usage-guide.md'), // 源码直跑:packages/cli/src → 仓库根
|
|
23
|
+
];
|
|
24
|
+
for (const p of candidates) {
|
|
25
|
+
if (existsSync(p))
|
|
26
|
+
return readFileSync(p, 'utf8');
|
|
27
|
+
}
|
|
28
|
+
throw new Error(`找不到 AI 使用说明(docs/usage-guide.md)。查找过:\n ${candidates.join('\n ')}`);
|
|
29
|
+
}
|
|
11
30
|
export function createProject(name, parent) {
|
|
12
31
|
const dir = resolve(parent ?? process.cwd(), name);
|
|
32
|
+
let guide;
|
|
33
|
+
try {
|
|
34
|
+
guide = readUsageGuide();
|
|
35
|
+
}
|
|
36
|
+
catch (e) {
|
|
37
|
+
console.error(`错误:${e instanceof Error ? e.message : String(e)}`);
|
|
38
|
+
process.exitCode = 1;
|
|
39
|
+
return 1;
|
|
40
|
+
}
|
|
13
41
|
if (existsSync(dir) && readdirSync(dir).length > 0) {
|
|
14
42
|
console.error(`错误:目录 ${dir} 已存在且非空`);
|
|
15
43
|
process.exitCode = 1;
|
|
@@ -19,17 +47,29 @@ export function createProject(name, parent) {
|
|
|
19
47
|
write(dir, 'package.json', projectPackageJson(name));
|
|
20
48
|
write(dir, 'tsconfig.json', projectTsconfig());
|
|
21
49
|
write(dir, 'src/main.ts', mainTs(name));
|
|
22
|
-
write(dir, '
|
|
23
|
-
write(dir,
|
|
50
|
+
write(dir, 'src/tools/hello/index.ts', toolIndexTs('hello'));
|
|
51
|
+
write(dir, REGISTRY_PATH, emptyRegistryTemplate());
|
|
24
52
|
write(dir, 'README.md', projectReadme(name));
|
|
25
53
|
write(dir, '.gitignore', projectGitignore());
|
|
26
|
-
|
|
54
|
+
// 把四个分类目录都建出来:目录名自解释,用户一眼知道「新能力往哪放」
|
|
55
|
+
// (.gitkeep 让空目录能进版本库;discover 只认目录,会忽略它)
|
|
56
|
+
for (const relDir of CAPABILITY_DIR_LIST) {
|
|
57
|
+
write(dir, `${relDir}/.gitkeep`, '');
|
|
58
|
+
}
|
|
59
|
+
// AI 使用说明:让 Claude Code / Cursor / Copilot 等一进项目就拿到权威 API 速查
|
|
60
|
+
write(dir, 'AGENTS.md', guide);
|
|
61
|
+
registerCapability(dir, 'hello', 'tool');
|
|
27
62
|
console.log(`已创建项目 ${dir}
|
|
28
63
|
|
|
29
64
|
后续步骤:
|
|
30
65
|
cd ${dir}
|
|
31
66
|
npm install
|
|
32
67
|
export ANTHROPIC_API_KEY=sk-ant-...
|
|
33
|
-
npm run dev
|
|
68
|
+
npm run dev
|
|
69
|
+
|
|
70
|
+
目录约定:src/tools/ · src/skills/ · src/prompts/ · src/subagents/(一能力一文件夹)
|
|
71
|
+
|
|
72
|
+
提示:项目内 AGENTS.md 是本框架的使用说明(API 速查 + 已知边界),
|
|
73
|
+
交给 AI 辅助编码时会自动被读,能显著减少猜 API 的错。`);
|
|
34
74
|
return 0;
|
|
35
75
|
}
|
package/dist/dev.js
CHANGED
|
@@ -1,7 +1,30 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* dev 命令:热重载开发模式(tsx watch src/main.ts)+ 本地 inspector 面板。
|
|
3
|
+
*
|
|
4
|
+
* inspector 的 trace 从哪来 —— dev 起的是【子进程】,而框架的 trace sink 必须是
|
|
5
|
+
* 进程内注册(registerDefaultTraceSink 是进程级注册表)。所以:
|
|
6
|
+
* dev 侧起 inspector 服务 → 用 NODE_OPTIONS=--import 把 inspector-preload 注入子进程 →
|
|
7
|
+
* preload 从【用户项目】解析 @migor/agentia 并注册 dev sink(POST /ingest)。
|
|
8
|
+
* 框架侧完全不感知 dev:不读 env、不含 dev 逻辑(决策见 spec §10)。
|
|
9
|
+
*
|
|
10
|
+
* 面板是增强项,任何一步失败都只告警、不阻断 dev。
|
|
11
|
+
*/
|
|
2
12
|
import { spawn } from 'node:child_process';
|
|
3
13
|
import { existsSync } from 'node:fs';
|
|
4
|
-
import { join } from 'node:path';
|
|
14
|
+
import { dirname, join } from 'node:path';
|
|
15
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
16
|
+
import { startInspector } from './inspector.js';
|
|
17
|
+
/** --import 可用性:Node ≥20.6(≥18.19 已回移植) */
|
|
18
|
+
function supportsImport() {
|
|
19
|
+
const [major, minor] = process.versions.node.split('.').map(Number);
|
|
20
|
+
if (major > 20)
|
|
21
|
+
return true;
|
|
22
|
+
if (major === 20)
|
|
23
|
+
return minor >= 6;
|
|
24
|
+
if (major === 18)
|
|
25
|
+
return minor >= 19;
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
5
28
|
export function devServer() {
|
|
6
29
|
const cwd = process.cwd();
|
|
7
30
|
const entry = join(cwd, 'src', 'main.ts');
|
|
@@ -10,21 +33,47 @@ export function devServer() {
|
|
|
10
33
|
process.exitCode = 1;
|
|
11
34
|
return 1;
|
|
12
35
|
}
|
|
13
|
-
|
|
14
|
-
|
|
36
|
+
let child = null;
|
|
37
|
+
let inspector = null;
|
|
38
|
+
let closing = false;
|
|
15
39
|
const forward = (sig) => () => {
|
|
16
|
-
if (!child.killed)
|
|
40
|
+
if (child && !child.killed)
|
|
17
41
|
child.kill(sig);
|
|
18
42
|
};
|
|
19
43
|
process.on('SIGINT', forward('SIGINT'));
|
|
20
44
|
process.on('SIGTERM', forward('SIGTERM'));
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
45
|
+
const startChild = (env) => {
|
|
46
|
+
child = spawn('npx', ['tsx', 'watch', 'src/main.ts'], { cwd, stdio: 'inherit', env });
|
|
47
|
+
child.on('error', (err) => {
|
|
48
|
+
console.error(`错误:启动 dev 失败:${err.message}`);
|
|
49
|
+
process.exitCode = 1;
|
|
50
|
+
});
|
|
51
|
+
child.on('exit', (code, sig) => {
|
|
52
|
+
// 子进程退出后同步退出码;被信号杀死视为正常(用户 Ctrl+C)
|
|
53
|
+
process.exitCode = sig ? 0 : (code ?? 0);
|
|
54
|
+
if (inspector && !closing) {
|
|
55
|
+
closing = true;
|
|
56
|
+
void inspector.close();
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
};
|
|
60
|
+
if (!supportsImport()) {
|
|
61
|
+
console.warn(`[agentia] Node ${process.versions.node} 不支持 --import(需 ≥20.6 或 ≥18.19),inspector 面板不可用,dev 照常`);
|
|
62
|
+
startChild(process.env);
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
65
|
+
startInspector()
|
|
66
|
+
.then((srv) => {
|
|
67
|
+
inspector = srv;
|
|
68
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
69
|
+
const preload = pathToFileURL(join(here, 'inspector-preload.js')).href;
|
|
70
|
+
const merged = `${process.env.NODE_OPTIONS ? process.env.NODE_OPTIONS + ' ' : ''}--import "${preload}"`;
|
|
71
|
+
console.log(`Inspector: http://127.0.0.1:${srv.port} (看每次 run 的调用树与能力执行)`);
|
|
72
|
+
startChild({ ...process.env, NODE_OPTIONS: merged, AGENTIA_INSPECT_PORT: String(srv.port) });
|
|
73
|
+
})
|
|
74
|
+
.catch((err) => {
|
|
75
|
+
console.warn(`[agentia] inspector 起不来(${err.message}),dev 照常运行`);
|
|
76
|
+
startChild(process.env);
|
|
28
77
|
});
|
|
29
78
|
return 0;
|
|
30
79
|
}
|
package/dist/doctor.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
/** doctor 命令:静态体检装配健康度(只读文件系统 +
|
|
1
|
+
/** doctor 命令:静态体检装配健康度(只读文件系统 + src/registry.ts 文本,不 import 用户代码) */
|
|
2
2
|
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
-
import { isValidName } from './templates.js';
|
|
5
|
-
|
|
4
|
+
import { CAPABILITY_DIRS, CAPABILITY_DIR_LIST, REGISTRY_PATH, isValidName } from './templates.js';
|
|
5
|
+
import { legacyLayout, legacyMigrationHint } from './layout.js';
|
|
6
|
+
/** 能力入口候选(须与框架 src/toolkit/discover.ts 的 ENTRY_CANDIDATES 一致) */
|
|
7
|
+
const ENTRY_CANDIDATES = ['index.ts', 'index.mts', 'index.js', 'index.mjs'];
|
|
8
|
+
/** 解析 src/registry.ts:imports(标识符 → 来源)+ entries(provide token → useClass 标识符) */
|
|
6
9
|
function parseRegistry(content) {
|
|
7
10
|
const imports = new Map();
|
|
8
11
|
for (const m of content.matchAll(/^import\s+(\w+)\s+from\s+'([^']+)'/gm)) {
|
|
@@ -14,15 +17,19 @@ function parseRegistry(content) {
|
|
|
14
17
|
}
|
|
15
18
|
return { imports, entries };
|
|
16
19
|
}
|
|
20
|
+
/** 分类目录相对注册表的 import 前缀:./tools 等(悬空条目判定用) */
|
|
21
|
+
const LOCAL_PREFIXES = CAPABILITY_DIR_LIST.map((p) => './' + p.replace(/^src\//, '') + '/');
|
|
17
22
|
export function doctor() {
|
|
18
23
|
const cwd = process.cwd();
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
// 老布局:先给迁移提示再收工 —— 去扫一个还不存在的新布局只会刷一屏无关警告
|
|
25
|
+
const legacy = legacyLayout(cwd);
|
|
26
|
+
if (legacy) {
|
|
27
|
+
console.log('agentia doctor —— 装配体检\n');
|
|
28
|
+
console.log(` 警告:${legacyMigrationHint(legacy)}`);
|
|
29
|
+
console.log('\n体检结果:0 错误,1 警告');
|
|
30
|
+
return 0;
|
|
31
|
+
}
|
|
32
|
+
const registryFile = join(cwd, REGISTRY_PATH);
|
|
26
33
|
const content = existsSync(registryFile) ? readFileSync(registryFile, 'utf8') : '';
|
|
27
34
|
const { imports, entries } = parseRegistry(content);
|
|
28
35
|
const warnings = [];
|
|
@@ -34,29 +41,49 @@ export function doctor() {
|
|
|
34
41
|
seen.set(e.token, (seen.get(e.token) ?? 0) + 1);
|
|
35
42
|
for (const [token, n] of seen) {
|
|
36
43
|
if (n > 1)
|
|
37
|
-
errors.push(
|
|
44
|
+
errors.push(`${REGISTRY_PATH} 存在 ${n} 条重复 provide: '${token}',请保留一条`);
|
|
38
45
|
}
|
|
39
|
-
const folderSet = new Set(folders);
|
|
40
46
|
const registeredTokens = new Set(entries.map((e) => e.token));
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
broken = true;
|
|
47
|
+
/** 文件夹名 → 出现的分类目录列表(跨类型同名会让装配期静默覆盖一个) */
|
|
48
|
+
const whereByName = new Map();
|
|
49
|
+
const allFolders = new Set();
|
|
50
|
+
// 逐个分类目录体检:命名规范 / 入口文件 / 是否登记
|
|
51
|
+
for (const [type, relDir] of Object.entries(CAPABILITY_DIRS)) {
|
|
52
|
+
const dir = join(cwd, relDir);
|
|
53
|
+
if (!existsSync(dir)) {
|
|
54
|
+
warnings.push(`${relDir}/ 不存在(该类型暂时没有能力;discover 里若仍列着这个路径会启动即报错)`);
|
|
55
|
+
continue;
|
|
51
56
|
}
|
|
52
|
-
|
|
53
|
-
|
|
57
|
+
const folders = readdirSync(dir, { withFileTypes: true })
|
|
58
|
+
.filter((d) => d.isDirectory())
|
|
59
|
+
.map((d) => d.name);
|
|
60
|
+
for (const name of folders) {
|
|
61
|
+
allFolders.add(name);
|
|
62
|
+
whereByName.set(name, [...(whereByName.get(name) ?? []), relDir]);
|
|
63
|
+
let broken = false;
|
|
64
|
+
if (!isValidName(name)) {
|
|
65
|
+
errors.push(`能力名「${name}」不符合 kebab-case 规范(${relDir}/${name}/)`);
|
|
66
|
+
broken = true;
|
|
67
|
+
}
|
|
68
|
+
if (!ENTRY_CANDIDATES.some((f) => existsSync(join(dir, name, f)))) {
|
|
69
|
+
errors.push(`能力 ${relDir}/${name}/ 缺少入口文件(${ENTRY_CANDIDATES.join(' / ')})`);
|
|
70
|
+
broken = true;
|
|
71
|
+
}
|
|
72
|
+
if (!registeredTokens.has(name)) {
|
|
73
|
+
warnings.push(`${relDir}/${name}/ 存在但未登记(运行 agentia g ${type} ${name} 或手工登记)`);
|
|
74
|
+
}
|
|
75
|
+
else if (!broken) {
|
|
76
|
+
oks.push(`${relDir}/${name}:已登记且入口齐全`);
|
|
77
|
+
}
|
|
54
78
|
}
|
|
55
|
-
|
|
56
|
-
|
|
79
|
+
}
|
|
80
|
+
// 跨分类目录同名 → 错误(四个目录共用一套 token,装配期后者覆盖,菜单只会剩一个)
|
|
81
|
+
for (const [name, dirs] of whereByName) {
|
|
82
|
+
if (dirs.length > 1) {
|
|
83
|
+
errors.push(`能力名「${name}」在多个分类目录重复(${dirs.join('、')})—— DI token 共用,装配期只会保留一个;请改名`);
|
|
57
84
|
}
|
|
58
85
|
}
|
|
59
|
-
//
|
|
86
|
+
// 注册表视角:悬空条目(import 指向本地分类目录但文件夹已不存在;add 引入的包无本地文件夹属正常)
|
|
60
87
|
for (const e of entries) {
|
|
61
88
|
if (!isValidName(e.token)) {
|
|
62
89
|
errors.push(`provide token「${e.token}」不符合 kebab-case 规范`);
|
|
@@ -64,11 +91,12 @@ export function doctor() {
|
|
|
64
91
|
const source = imports.get(e.ident);
|
|
65
92
|
if (source === undefined)
|
|
66
93
|
continue; // 无法定位 import(可能手工编写),跳过
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
94
|
+
const prefix = LOCAL_PREFIXES.find((p) => source.startsWith(p));
|
|
95
|
+
if (prefix === undefined)
|
|
96
|
+
continue;
|
|
97
|
+
const folder = source.slice(prefix.length).split('/')[0];
|
|
98
|
+
if (folder !== undefined && !allFolders.has(folder)) {
|
|
99
|
+
warnings.push(`${REGISTRY_PATH} 条目 { provide: '${e.token}' } 指向 ${source},但该目录已不存在(悬空条目,请删除条目或恢复目录)`);
|
|
72
100
|
}
|
|
73
101
|
}
|
|
74
102
|
console.log('agentia doctor —— 装配体检\n');
|
package/dist/generate.js
CHANGED
|
@@ -1,21 +1,33 @@
|
|
|
1
|
-
/** g / generate 命令:在 cwd 生成
|
|
1
|
+
/** g / generate 命令:在 cwd 生成 src/<分类>/<name>/ 能力并登记注册表 */
|
|
2
2
|
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
-
import { promptAssetMd, promptIndexTs, skillIndexTs, subagentIndexTs, subagentSystemMd, toolIndexTs, } from './templates.js';
|
|
5
|
-
import { ensureRegistry,
|
|
6
|
-
|
|
4
|
+
import { CAPABILITY_DIRS, promptAssetMd, promptIndexTs, skillIndexTs, subagentIndexTs, subagentSystemMd, toolIndexTs, } from './templates.js';
|
|
5
|
+
import { ensureRegistry, registerCapability } from './registry.js';
|
|
6
|
+
import { legacyLayout, legacyMigrationHint } from './layout.js';
|
|
7
|
+
export function generateCapability(type, name) {
|
|
7
8
|
const cwd = process.cwd();
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
// 老布局:绝不悄悄在旁边新建一棵新目录(会让项目里长出两套能力目录)
|
|
10
|
+
const legacy = legacyLayout(cwd);
|
|
11
|
+
if (legacy) {
|
|
12
|
+
console.error(`错误:${legacyMigrationHint(legacy)}`);
|
|
11
13
|
process.exitCode = 1;
|
|
12
14
|
return 1;
|
|
13
15
|
}
|
|
14
|
-
|
|
16
|
+
const relDir = CAPABILITY_DIRS[type];
|
|
17
|
+
const capabilityDir = join(cwd, relDir, name);
|
|
18
|
+
// 四个分类目录共用一套 DI token(缺省 = 文件夹名),跨目录同名会让装配期静默覆盖掉一个。
|
|
19
|
+
// 生成期直接拦住(doctor 另做兜底体检),而不是等运行时菜单少一项才发现。
|
|
20
|
+
const clash = Object.entries(CAPABILITY_DIRS).find(([, d]) => existsSync(join(cwd, d, name)));
|
|
21
|
+
if (clash) {
|
|
22
|
+
console.error(`错误:能力名「${name}」已存在于 ${clash[1]}/${name}/ —— 四个分类目录共用同一套 DI token(缺省取文件夹名),不能重名`);
|
|
23
|
+
process.exitCode = 1;
|
|
24
|
+
return 1;
|
|
25
|
+
}
|
|
26
|
+
mkdirSync(capabilityDir, { recursive: true });
|
|
15
27
|
const files = [];
|
|
16
28
|
const put = (rel, content) => {
|
|
17
|
-
writeFileSync(join(
|
|
18
|
-
files.push(
|
|
29
|
+
writeFileSync(join(capabilityDir, rel), content, 'utf8');
|
|
30
|
+
files.push(`${relDir}/${name}/${rel}`);
|
|
19
31
|
};
|
|
20
32
|
switch (type) {
|
|
21
33
|
case 'tool':
|
|
@@ -34,16 +46,16 @@ export function generateUnit(type, name) {
|
|
|
34
46
|
break;
|
|
35
47
|
}
|
|
36
48
|
ensureRegistry(cwd);
|
|
37
|
-
const result =
|
|
38
|
-
console.log(`已生成 ${type}
|
|
49
|
+
const result = registerCapability(cwd, name, type);
|
|
50
|
+
console.log(`已生成 ${type} 能力:`);
|
|
39
51
|
for (const f of files)
|
|
40
52
|
console.log(` ${f}`);
|
|
41
53
|
if (result === 'registered') {
|
|
42
|
-
console.log(`已登记到
|
|
54
|
+
console.log(`已登记到 src/registry.ts:{ provide: '${name}' }`);
|
|
43
55
|
}
|
|
44
56
|
else {
|
|
45
|
-
console.log(`
|
|
57
|
+
console.log(`src/registry.ts 中「${name}」已注册,跳过登记`);
|
|
46
58
|
}
|
|
47
|
-
console.log('提示:discover 目录扫描路线下无需登记即可生效;显式装配路线从
|
|
59
|
+
console.log('提示:discover 目录扫描路线下无需登记即可生效;显式装配路线从 src/registry.ts 引入 providers');
|
|
48
60
|
return 0;
|
|
49
61
|
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/* 把框架的 Trace.spans[] 归一成渲染动作,喂给 createTraceView 建出的视图。
|
|
2
|
+
*
|
|
3
|
+
* 框架的 trace 是【扁平 spans[] + parentSpanId】,渲染器要的是【嵌套树 + 每个节点内
|
|
4
|
+
* 事件与子 span 按发生顺序混排的 order】。本文件就是那座桥 —— 同一份渲染器因此
|
|
5
|
+
* 既能吃官网的模拟回放,也能吃真实 run 的 trace。
|
|
6
|
+
*
|
|
7
|
+
* 归一规则:
|
|
8
|
+
* 1. 全局时间线:每个 span 的 start / end 与它的每个 event 汇到一起,按时间排序。
|
|
9
|
+
* tie-break:同一毫秒 start(0) → event(1) → end(2),且 start 按深度浅的在前 ——
|
|
10
|
+
* 保证父 span 先于子 span 打开、tool.input 落在它所属 turn 打开之后。
|
|
11
|
+
* 2. 显示名合成:框架的 capability span 名是【裸名】(类型记在 attributes,如 {subagent:'x'}),
|
|
12
|
+
* 这里补成 `type:name`,与渲染器的四类标识符对齐;事件里的工具名若是某个 capability span
|
|
13
|
+
* 的裸名,同样补前缀(否则回落 tool:)。
|
|
14
|
+
* 3. usage 只由 llm.turn 累计(capability span 的 usage 是子 span 聚合,计入会双算)。
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { fmtArg, capabilityTypeOf, CAP_ICO } from './view.js';
|
|
18
|
+
|
|
19
|
+
/** 从 span.attributes 认能力类型(框架用 `setAttribute(capabilityId, 'subagent', name)` 记类型) */
|
|
20
|
+
function spanType(s) {
|
|
21
|
+
const attrs = s.attributes || {};
|
|
22
|
+
for (const t of Object.keys(CAP_ICO)) {
|
|
23
|
+
if (attrs[t] != null) return t;
|
|
24
|
+
}
|
|
25
|
+
return '';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** 显示名:capability span 补 `type:` 前缀(已是 type:name 形态则原样保留) */
|
|
29
|
+
function displayName(s) {
|
|
30
|
+
const raw = String(s.name || '');
|
|
31
|
+
const t = spanType(s);
|
|
32
|
+
if (!raw) return t || s.kind || '';
|
|
33
|
+
if (capabilityTypeOf(raw)) return raw; // 已带前缀
|
|
34
|
+
return t ? `${t}:${raw}` : raw;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function usageOf(s) {
|
|
38
|
+
if (!s.usage) return null;
|
|
39
|
+
return {
|
|
40
|
+
input: s.usage.inputTokens || 0,
|
|
41
|
+
output: s.usage.outputTokens || 0,
|
|
42
|
+
cacheRead: s.usage.cacheReadTokens || 0,
|
|
43
|
+
cacheCreation: s.usage.cacheCreationTokens || 0,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** 事件工具名:**只有 `tool.*` 事件带工具名**。
|
|
48
|
+
* 框架的事件里 `usage.unpriced` / `llm.retry` / `budget.exceeded` / `context.budget` 的事件体
|
|
49
|
+
* 根本没有 `tool` 字段 —— 一律套前缀只会得到 `tool:?` 这种把「无工具」显示成「名字叫 ? 的工具」
|
|
50
|
+
* 的误导标签(面板与官网 playground 共用本渲染器,等于把假工具摆给使用者看)。
|
|
51
|
+
* 工具裸名靠兄弟 capability span 反查类型,找不到按 tool 算。 */
|
|
52
|
+
function eventToolName(evName, bodyTool, typeMap) {
|
|
53
|
+
if (!String(evName || '').startsWith('tool.')) return '';
|
|
54
|
+
const raw = String(bodyTool || '?');
|
|
55
|
+
if (capabilityTypeOf(raw)) return raw;
|
|
56
|
+
return `${typeMap.get(raw) || 'tool'}:${raw}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** 事件摘要:input 走 fmtArg(与 span 入参同一形态),output 走文本内容 */
|
|
60
|
+
function eventText(ev) {
|
|
61
|
+
const body = ev.body;
|
|
62
|
+
if (ev.name === 'tool.input') return fmtArg(body && body.input);
|
|
63
|
+
if (ev.name === 'tool.output') {
|
|
64
|
+
const c = body && body.content;
|
|
65
|
+
return typeof c === 'string' ? c : c == null ? '' : JSON.stringify(c);
|
|
66
|
+
}
|
|
67
|
+
return body == null ? '' : typeof body === 'string' ? body : JSON.stringify(body);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 把真实 Trace 线性回放给视图(同步、一次性;用于 run 结束后的静态展示)。
|
|
72
|
+
* 返回 false 表示 trace 空、未做任何渲染。
|
|
73
|
+
*/
|
|
74
|
+
export function playTrace(view, trace) {
|
|
75
|
+
const spans = Array.isArray(trace && trace.spans) ? trace.spans : [];
|
|
76
|
+
if (!spans.length) return false;
|
|
77
|
+
|
|
78
|
+
const byId = new Map(spans.map((s) => [s.spanId, s]));
|
|
79
|
+
const root = spans.find((s) => s.kind === 'run') || spans[0];
|
|
80
|
+
|
|
81
|
+
// 裸名 → 能力类型(供事件补前缀)
|
|
82
|
+
const typeMap = new Map();
|
|
83
|
+
for (const s of spans) {
|
|
84
|
+
const t = spanType(s);
|
|
85
|
+
if (s.kind === 'capability' && t) typeMap.set(String(s.name || ''), t);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const depthOf = (s) => {
|
|
89
|
+
let d = 0;
|
|
90
|
+
let cur = s;
|
|
91
|
+
const seen = new Set();
|
|
92
|
+
while (cur && cur.parentSpanId && byId.has(cur.parentSpanId) && !seen.has(cur.spanId)) {
|
|
93
|
+
seen.add(cur.spanId);
|
|
94
|
+
d += 1;
|
|
95
|
+
cur = byId.get(cur.parentSpanId);
|
|
96
|
+
}
|
|
97
|
+
return d;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const items = [];
|
|
101
|
+
for (const s of spans) {
|
|
102
|
+
items.push({ t: s.startedAt, ord: 0, d: depthOf(s), s, kind: 'start' });
|
|
103
|
+
if (s.endedAt != null) items.push({ t: s.endedAt, ord: 2, d: 0, s, kind: 'end' });
|
|
104
|
+
for (const ev of s.events || []) items.push({ t: ev.time, ord: 1, d: 0, s, kind: 'event', ev });
|
|
105
|
+
}
|
|
106
|
+
items.sort((a, b) => a.t - b.t || a.ord - b.ord || a.d - b.d);
|
|
107
|
+
|
|
108
|
+
const rootRaw = String(root.name || 'run');
|
|
109
|
+
// 框架的 run 根名可能是 runName(如 'app')或已是 'run · x' 形态,避免重复前缀
|
|
110
|
+
view.reset(rootRaw.startsWith('run') ? rootRaw : 'run · ' + rootRaw);
|
|
111
|
+
|
|
112
|
+
for (const it of items) {
|
|
113
|
+
if (it.kind === 'start') {
|
|
114
|
+
if (it.s === root) continue; // 根由 reset 建,不重复开
|
|
115
|
+
view.start({
|
|
116
|
+
id: it.s.spanId,
|
|
117
|
+
parent: it.s.parentSpanId,
|
|
118
|
+
kind: it.s.kind,
|
|
119
|
+
name: displayName(it.s),
|
|
120
|
+
arg: '',
|
|
121
|
+
});
|
|
122
|
+
} else if (it.kind === 'event') {
|
|
123
|
+
view.event(
|
|
124
|
+
it.s.spanId,
|
|
125
|
+
it.ev.name,
|
|
126
|
+
eventToolName(it.ev.name, it.ev.body && it.ev.body.tool, typeMap),
|
|
127
|
+
eventText(it.ev),
|
|
128
|
+
!it.ev.body || it.ev.body.ok !== false,
|
|
129
|
+
);
|
|
130
|
+
} else {
|
|
131
|
+
view.end({
|
|
132
|
+
id: it.s.spanId,
|
|
133
|
+
ms: it.s.endedAt - it.s.startedAt,
|
|
134
|
+
usage: usageOf(it.s),
|
|
135
|
+
status: it.s.status,
|
|
136
|
+
error: it.s.error,
|
|
137
|
+
arg: '',
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
view.finish(root.endedAt != null ? root.endedAt - root.startedAt : 0, root.status, root.error);
|
|
143
|
+
return true;
|
|
144
|
+
}
|