@migor/cli 0.7.2 → 0.8.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/dist/report.js CHANGED
@@ -31,12 +31,16 @@ export function extractTrace(v) {
31
31
  }
32
32
  /** 打印一张按总耗时降序的能力排行(跨多行记录时按能力合并) */
33
33
  /** 用法串(cli.ts 的子命令 `--help` 也从这里取,避免两处各写一份) */
34
- export const USAGE = '用法:agentia report <trace.jsonl>';
34
+ export const USAGE = '用法:agentia report <trace.jsonl> [--json]';
35
35
  export async function reportCommand(args) {
36
- const file = args[0];
36
+ // `--json`:机器可读输出(stdout 只有一个 JSON 文档,无任何人类装饰)—— 便于脚本/CI 串接。
37
+ // 出错仍走 stderr 的 `错误:…` + 退出码 1、stdout 为空,脚本据此区分「有结果」与「没跑成」。
38
+ const json = args.includes('--json');
39
+ const positional = args.filter((a) => a !== '--json');
40
+ const file = positional[0];
37
41
  // 失败一律**抛错**(而不是就地设 process.exitCode)—— 本命令是异步的,
38
42
  // 就地设的 exitCode 会被 cli.ts 末尾那句 `process.exitCode = main(...)` 覆盖成 0。
39
- if (file === undefined || args.length > 1) {
43
+ if (file === undefined || positional.length > 1 || file.startsWith('--')) {
40
44
  throw new Error(USAGE);
41
45
  }
42
46
  let raw;
@@ -101,37 +105,54 @@ export async function reportCommand(args) {
101
105
  }
102
106
  }
103
107
  const rows = [...merged.values()].sort((a, b) => b.totalMs - a.totalMs || b.calls - a.calls);
108
+ const failRows = traces
109
+ .filter((t) => t.status === 'error')
110
+ .map((t) => {
111
+ const spans = (t.spans ?? []);
112
+ return {
113
+ traceId: t.traceId ?? null,
114
+ message: spans.find((s) => s?.error?.message)?.error?.message ?? null,
115
+ };
116
+ });
117
+ const totalMs = rows.reduce((n, r) => n + r.totalMs, 0);
118
+ const errTotal = rows.reduce((n, r) => n + r.errors, 0);
119
+ if (json) {
120
+ const payload = {
121
+ file,
122
+ runs: traces.length,
123
+ failed: traces.length - okRuns,
124
+ skippedLines: badLines,
125
+ failures: failRows,
126
+ capabilities: rows,
127
+ totals: { totalMs, errors: errTotal, capabilities: rows.length },
128
+ };
129
+ console.log(JSON.stringify(payload, null, 2));
130
+ return 0;
131
+ }
104
132
  const pad = (s, n) => (s.length >= n ? s : s + ' '.repeat(n - s.length));
105
133
  const fmtMs = (ms) => (ms >= 1000 ? `${(ms / 1000).toFixed(2)}s` : `${ms}ms`);
106
134
  console.log(`trace 文件 ${file}`);
107
135
  console.log(`runs ${traces.length}(失败 ${traces.length - okRuns})${badLines > 0 ? ` · 跳过无法解析 ${badLines} 行` : ''}`);
108
136
  // 失败 run 要说清原因:只报「失败 N」而不给理由,对调优没用。而且首次运行最常见的失败
109
137
  // (没配 API key)恰恰不产出任何 capability 行 —— 会走到下面「没有可归因的能力」提前返回。
110
- const failed = traces.filter((t) => t.status === 'error');
111
- for (const t of failed.slice(0, 3)) {
112
- const spans = (t.spans ?? []);
113
- const msg = spans.find((s) => s?.error?.message)?.error?.message;
114
- console.log(` ✗ ${t.traceId ?? '(无 traceId)'}${msg ? ` ${msg}` : ''}`);
138
+ for (const f of failRows.slice(0, 3)) {
139
+ console.log(` ✗ ${f.traceId ?? '(无 traceId)'}${f.message ? ` ${f.message}` : ''}`);
115
140
  }
116
- if (failed.length > 3)
117
- console.log(` …另有 ${failed.length - 3} 条失败 run`);
141
+ if (failRows.length > 3)
142
+ console.log(` …另有 ${failRows.length - 3} 条失败 run`);
118
143
  console.log('');
119
144
  if (rows.length === 0) {
120
145
  console.log('(没有可归因的能力:这些 trace 里既没有 capability span,也没有 tool.output 事件)');
121
146
  return 0;
122
147
  }
123
148
  console.log(`${pad('capability', 34)} ${pad('calls', 6)} ${pad('err', 5)} ${pad('total', 9)} ${pad('max', 9)} ${pad('tokens', 9)} ${pad('cost', 12)}`);
124
- let total = 0;
125
- let errs = 0;
126
149
  for (const r of rows) {
127
- total += r.totalMs;
128
- errs += r.errors;
129
150
  console.log(`${pad(r.capability, 34)} ${pad(String(r.calls), 6)} ${pad(String(r.errors), 5)} ` +
130
151
  `${pad(fmtMs(r.totalMs), 9)} ${pad(fmtMs(r.maxMs), 9)} ` +
131
152
  `${pad(r.tokens != null ? String(r.tokens) : '-', 9)} ` +
132
153
  `${pad(r.costUsd != null ? r.costUsd.toFixed(6) : '-', 12)}`);
133
154
  }
134
155
  console.log('');
135
- console.log(`合计耗时 ${fmtMs(total)} · 失败 ${errs} 次 · 能力 ${rows.length} 个`);
156
+ console.log(`合计耗时 ${fmtMs(totalMs)} · 失败 ${errTotal} 次 · 能力 ${rows.length} 个`);
136
157
  return 0;
137
158
  }
@@ -0,0 +1,62 @@
1
+ # __PROJECT_NAME__
2
+
3
+ 基于 [Agentia](https://github.com/retrychx/agentia) 框架的 agent 应用。
4
+
5
+ ## 目录约定
6
+
7
+ 四分类目录,一能力一文件夹,每个能力是一个 default export 的类,用装饰器声明:
8
+
9
+ - `src/tools/<name>/` —— `@Tool` 工具:主 agent 可调用(input → value)
10
+ - `src/skills/<name>/` —— `@Skill` 技能:方法体内通过 `ctx.llm()` 调 LLM
11
+ - `src/subagents/<name>/` —— `@SubAgent` 子代理:按 system 角色设定独立跑一轮
12
+ - `src/prompts/<name>/` —— `@Prompt` 文本资产:.md 文件,按需拉取进上下文
13
+
14
+ 目录名就是类型,不用记别名。
15
+
16
+ ## 两条装配路线
17
+
18
+ 1. **目录扫描**:`createApp({ discover: [...] })` 启动期按给定顺序扫各目录下的 `<name>/index.ts`,default export 为类时以文件夹名为 DI token 注册(见 `src/main.ts`)。
19
+ 2. **显式装配**:`createApp({ providers, system })`,providers 来自 `src/registry.ts` 注册表(由 `agentia g` 自动维护,也可手工编辑)。
20
+
21
+ 两者二选一或混用。
22
+
23
+ ## 生成能力
24
+
25
+ ```bash
26
+ agentia g tool my-tool # → src/tools/my-tool/
27
+ agentia g skill my-skill # → src/skills/my-skill/
28
+ agentia g prompt my-prompt # → src/prompts/my-prompt/(含 asset.md)
29
+ agentia g subagent my-agent # → src/subagents/my-agent/(含 system.md)
30
+ ```
31
+
32
+ 生成的能力自动登记到 `src/registry.ts`。
33
+
34
+ ## 运行
35
+
36
+ 需要 Anthropic API key —— 填进脚手架已生成的 `.env` 即可(本文件在 `.gitignore` 里):
37
+
38
+ ```bash
39
+ # .env
40
+ ANTHROPIC_API_KEY=sk-ant-...
41
+ ```
42
+
43
+ ```bash
44
+ npm run dev -- "你的问题"
45
+ ```
46
+
47
+ ## 构建与生产运行
48
+
49
+ ```bash
50
+ npm run build # tsc → dist/ + .md 文本资产跟随拷贝(asset() 按文件位置解析,必须跟着 .js 走)
51
+ npm start -- "你的问题" # 跑编译产物 dist/main.js(部署/Docker 用这条)
52
+ ```
53
+
54
+ 也可以用环境变量(适合 CI / 容器)——**真实环境变量优先,不会被 `.env` 覆盖**:
55
+
56
+ ```bash
57
+ export ANTHROPIC_API_KEY=sk-ant-...
58
+ npm run dev -- "你的问题"
59
+ ```
60
+
61
+ `.env` 由 `src/main.ts` 首行的 `loadEnvFile()` 读取。框架**不会自动读**它 ——
62
+ 读哪个文件、什么时候读由你的启动代码决定(这样「换目录跑」不会悄悄改变行为)。
@@ -0,0 +1,3 @@
1
+ # __NAME__
2
+
3
+ 在此编写提示文本资产(按需被主 agent 拉取进上下文)。
@@ -0,0 +1,9 @@
1
+ import { Prompt, asset } from '@migor/agentia';
2
+
3
+ /** __NAME__ 文本资产能力 */
4
+ export default class __CLASS_NAME__ {
5
+ @Prompt({ description: '__NAME__ 文本资产(描述何时该拉取)' })
6
+ __METHOD_NAME__(): string {
7
+ return asset(import.meta.url, './asset.md');
8
+ }
9
+ }
@@ -0,0 +1,19 @@
1
+ import { Skill } from '@migor/agentia';
2
+ import type { SkillContext } from '@migor/agentia';
3
+
4
+ /** __NAME__ 技能能力 */
5
+ export default class __CLASS_NAME__ {
6
+ @Skill({
7
+ description: '示例技能:就给定主题调用 LLM 产出要点',
8
+ schema: {
9
+ type: 'object',
10
+ properties: { topic: { type: 'string' } },
11
+ required: ['topic'],
12
+ additionalProperties: false,
13
+ },
14
+ })
15
+ async __METHOD_NAME__(input: { topic: string }, ctx: SkillContext): Promise<string> {
16
+ const r = await ctx.llm({ prompt: `就「${input.topic}」给出三个要点` });
17
+ return r.text;
18
+ }
19
+ }
@@ -0,0 +1,17 @@
1
+ import { SubAgent, asset } from '@migor/agentia';
2
+
3
+ /** __NAME__ 子代理能力 */
4
+ export default class __CLASS_NAME__ {
5
+ @SubAgent({
6
+ description: '示例子代理:按 system.md 的角色设定独立处理任务',
7
+ schema: {
8
+ type: 'object',
9
+ properties: { task: { type: 'string' } },
10
+ required: ['task'],
11
+ additionalProperties: false,
12
+ },
13
+ system: asset(import.meta.url, './system.md'),
14
+ })
15
+ // 方法体不会执行:@SubAgent 只读取方法名与装饰器元数据,调用时由框架按 system 另起 agent 执行
16
+ __METHOD_NAME__(_input: { task: string }): void {}
17
+ }
@@ -0,0 +1,3 @@
1
+ # __NAME__
2
+
3
+ 在此编写子代理的角色描述(作为其子 agent 的 system prompt)。
@@ -0,0 +1,18 @@
1
+ import { Tool } from '@migor/agentia';
2
+
3
+ /** __NAME__ 工具能力 */
4
+ export default class __CLASS_NAME__ {
5
+ @Tool({
6
+ description: '示例工具:回显输入',
7
+ schema: {
8
+ type: 'object',
9
+ properties: { text: { type: 'string' } },
10
+ required: ['text'],
11
+ additionalProperties: false,
12
+ },
13
+ strict: true,
14
+ })
15
+ __METHOD_NAME__(input: { text: string }): string {
16
+ return `echo: ${input.text}`;
17
+ }
18
+ }
@@ -0,0 +1,3 @@
1
+ # 模型 API key —— 填上后 npm run dev / npm start 直接可用
2
+ # 本文件已被 .gitignore 忽略:不要提交,也不要把 key 写进 README / AGENTS.md
3
+ ANTHROPIC_API_KEY=
@@ -0,0 +1,6 @@
1
+ # 复制成 .env 再填值(.env 已 gitignore,不要提交)
2
+ ANTHROPIC_API_KEY=
3
+
4
+ # 可选
5
+ # ANTHROPIC_BASE_URL=https://api.anthropic.com
6
+ # AGENTIA_MODEL=claude-opus-5
@@ -0,0 +1,6 @@
1
+ node_modules
2
+ dist
3
+
4
+ # 本地环境变量(可能含 key)—— 绝不提交
5
+ .env
6
+ .env.local
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "__PROJECT_NAME__",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "agentia dev",
7
+ "build": "tsc -p tsconfig.json && node scripts/copy-assets.mjs",
8
+ "start": "node dist/main.js",
9
+ "typecheck": "tsc --noEmit -p tsconfig.json"
10
+ },
11
+ "dependencies": {
12
+ "@migor/agentia": "^0.8.1"
13
+ },
14
+ "devDependencies": {
15
+ "@migor/cli": "^0.8.1",
16
+ "tsx": "^4.19.0",
17
+ "typescript": "^7.0.2",
18
+ "@types/node": "^22.0.0"
19
+ }
20
+ }
@@ -0,0 +1,26 @@
1
+ // 把能力文件夹里的 .md 文本资产拷进 dist/(与编译产物同相对路径)。
2
+ // asset(import.meta.url, './x.md') 按**文件位置**解析,所以 .md 必须跟着 .js 走。
3
+ import { cpSync, readdirSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ const here = fileURLToPath(new URL('.', import.meta.url));
8
+ const srcDir = join(here, '..', 'src');
9
+ const outDir = join(here, '..', 'dist');
10
+
11
+ function walk(dir) {
12
+ const out = [];
13
+ for (const e of readdirSync(dir, { withFileTypes: true })) {
14
+ const p = join(dir, e.name);
15
+ if (e.isDirectory()) out.push(...walk(p));
16
+ else if (e.name.endsWith('.md')) out.push(p);
17
+ }
18
+ return out;
19
+ }
20
+
21
+ let n = 0;
22
+ for (const f of walk(srcDir)) {
23
+ cpSync(f, join(outDir, f.slice(srcDir.length + 1)));
24
+ n++;
25
+ }
26
+ console.log(`[copy-assets] ${n} 个 .md 资产 → dist/`);
@@ -0,0 +1,37 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { createApp, loadEnvFile, SystemPrompt } from '@migor/agentia';
4
+
5
+ // 读同目录的 .env(key 写文件里即可,不必每次 export)。框架**不自动**读 .env ——
6
+ // 读哪个文件、什么时候读由这里决定;已存在的真实环境变量优先,不会被文件覆盖。
7
+ // 想换路径/顺序:loadEnvFile({ path: '.env.local' }) 或直接删掉这一行改用自己的加载器。
8
+ loadEnvFile();
9
+
10
+ // 能力目录按**本文件位置**解析,不是 cwd:开发态(src/main.ts)解析到 src/<分类>/,
11
+ // 构建后(dist/main.js)解析到 dist/<分类>/ —— 从任何目录启动都成立。
12
+ // ⚠️ 别改回 cwd 相对写法(形如 src/tools 的字符串):那样 `node dist/main.js` 会去加载 src 下的
13
+ // .ts 源码,而装饰器不是可擦除的类型语法,Node 直接跑不了("Invalid or unexpected token")。
14
+ // filter:空分类目录在构建后不存在(tsc 不为空目录产出 dist/<分类>/),而 discover 对
15
+ // 显式给出的不存在路径是报错的 —— 「这类暂时没有能力」不该让启动失败。
16
+ const CAPABILITY_DIRS = ['tools', 'skills', 'prompts', 'subagents'];
17
+ const app = await createApp({
18
+ name: '__PROJECT_NAME__',
19
+ discover: CAPABILITY_DIRS.map((d) => fileURLToPath(new URL(d + '/', import.meta.url))).filter(
20
+ (dir) => existsSync(dir),
21
+ ),
22
+ system: new SystemPrompt().add('role', '你是 __PROJECT_NAME__ 的主 agent,按任务自主调度菜单里的能力。', true),
23
+ });
24
+
25
+ const { result } = await app.run(
26
+ [{ role: 'user', content: process.argv[2] ?? '介绍一下你自己' }],
27
+ );
28
+
29
+ // 注意:run 失败**不会抛**(硬失败被记进 result.error 与 trace 后正常返回)—— 不显式检查就会
30
+ // 「打印一行空白 + 退出 0」,让首次运行(比如忘了配 ANTHROPIC_API_KEY)看起来像成功。
31
+ if (result.error) {
32
+ console.error(`run 失败(stopReason=${result.stopReason}):${result.error.message}`);
33
+ console.error('提示:模型调用读 ANTHROPIC_API_KEY —— 填进 .env(首行 loadEnvFile() 会读)或 export 均可;');
34
+ console.error(' 换端点 / 注入自定义 client 见项目内 AGENTS.md。');
35
+ process.exitCode = 1;
36
+ }
37
+ if (result.finalText) console.log(result.finalText);
@@ -0,0 +1,10 @@
1
+ // Agentia 能力注册表 —— 由 agentia CLI 维护(agentia g 自动更新,也可手工编辑)
2
+ // @agentia:imports
3
+ // @agentia:imports-end
4
+ import type { Provider } from '@migor/agentia';
5
+
6
+ /** 显式装配路线:createApp({ providers, system: ... })(与 discover 目录扫描二选一或混用) */
7
+ export const providers: Provider[] = [
8
+ // @agentia:entries
9
+ // @agentia:entries-end
10
+ ];
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "lib": [
7
+ "ES2022"
8
+ ],
9
+ "strict": true,
10
+ "rootDir": "src",
11
+ "outDir": "dist",
12
+ "types": [
13
+ "node"
14
+ ],
15
+ "esModuleInterop": true,
16
+ "skipLibCheck": true
17
+ },
18
+ "include": [
19
+ "src"
20
+ ]
21
+ }