@ai-zen/air 0.1.3

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.
Files changed (43) hide show
  1. package/README.md +85 -0
  2. package/TODO.md +8 -0
  3. package/dist/agent-factory.d.ts +3 -0
  4. package/dist/agent-factory.d.ts.map +1 -0
  5. package/dist/agent-factory.js +21 -0
  6. package/dist/agent-factory.js.map +1 -0
  7. package/dist/agent-runtime.d.ts +2 -0
  8. package/dist/agent-runtime.d.ts.map +1 -0
  9. package/dist/agent-runtime.js +292 -0
  10. package/dist/agent-runtime.js.map +1 -0
  11. package/dist/cli.d.ts +3 -0
  12. package/dist/cli.d.ts.map +1 -0
  13. package/dist/cli.js +40 -0
  14. package/dist/cli.js.map +1 -0
  15. package/dist/config.d.ts +17 -0
  16. package/dist/config.d.ts.map +1 -0
  17. package/dist/config.js +90 -0
  18. package/dist/config.js.map +1 -0
  19. package/dist/delta-renderer.d.ts +19 -0
  20. package/dist/delta-renderer.d.ts.map +1 -0
  21. package/dist/delta-renderer.js +104 -0
  22. package/dist/delta-renderer.js.map +1 -0
  23. package/dist/migration.d.ts +5 -0
  24. package/dist/migration.d.ts.map +1 -0
  25. package/dist/migration.js +70 -0
  26. package/dist/migration.js.map +1 -0
  27. package/dist/tools.d.ts +3 -0
  28. package/dist/tools.d.ts.map +1 -0
  29. package/dist/tools.js +24 -0
  30. package/dist/tools.js.map +1 -0
  31. package/package.json +28 -0
  32. package/src/__tests__/config.test.ts +111 -0
  33. package/src/__tests__/main.test.ts +43 -0
  34. package/src/__tests__/tools.test.ts +15 -0
  35. package/src/agent-factory.ts +23 -0
  36. package/src/agent-runtime.ts +256 -0
  37. package/src/cli.ts +48 -0
  38. package/src/config.ts +109 -0
  39. package/src/delta-renderer.ts +127 -0
  40. package/src/migration.ts +72 -0
  41. package/src/tools.ts +23 -0
  42. package/tsconfig.json +18 -0
  43. package/vitest.config.ts +8 -0
@@ -0,0 +1,104 @@
1
+ import chalk from "chalk";
2
+ import { AgentNS } from "@ai-zen/agents-core";
3
+ export class DeltaRenderer {
4
+ reasoningPrinted = false;
5
+ contentPrinted = false;
6
+ toolPrints = {};
7
+ opts;
8
+ constructor(opts = {}) {
9
+ this.opts = {
10
+ reasoningHeader: opts.reasoningHeader ?? "",
11
+ contentHeader: opts.contentHeader ?? "",
12
+ reasoningStyle: opts.reasoningStyle ?? chalk.blue,
13
+ contentStyle: opts.contentStyle ?? chalk.white,
14
+ indent: opts.indent ?? "",
15
+ };
16
+ }
17
+ render(delta, finishReason = null) {
18
+ if (delta.tool_calls) {
19
+ this.renderToolCalls(delta, finishReason);
20
+ }
21
+ if (delta.reasoning_content) {
22
+ if (!this.reasoningPrinted && this.opts.reasoningHeader) {
23
+ process.stdout.write(this.opts.indent + this.opts.reasoningHeader);
24
+ this.reasoningPrinted = true;
25
+ }
26
+ process.stdout.write(this.opts.reasoningStyle(delta.reasoning_content));
27
+ }
28
+ if (delta.content) {
29
+ if (!this.contentPrinted && this.opts.contentHeader) {
30
+ process.stdout.write(this.opts.indent + this.opts.contentHeader);
31
+ this.contentPrinted = true;
32
+ }
33
+ if (typeof delta.content === "string") {
34
+ process.stdout.write(this.opts.contentStyle(delta.content));
35
+ }
36
+ else if (Array.isArray(delta.content)) {
37
+ for (const s of delta.content) {
38
+ if (s.type === "text" && s.text) {
39
+ process.stdout.write(this.opts.contentStyle(s.text));
40
+ }
41
+ }
42
+ }
43
+ }
44
+ }
45
+ reset() {
46
+ this.reasoningPrinted = false;
47
+ this.contentPrinted = false;
48
+ for (const k in this.toolPrints)
49
+ delete this.toolPrints[k];
50
+ }
51
+ renderToolCalls(delta, finishReason) {
52
+ if (!delta.tool_calls || delta.tool_calls.length === 0)
53
+ return;
54
+ const isFirstToolCall = Object.keys(this.toolPrints).length === 0 &&
55
+ delta.tool_calls.some((tc) => tc.function?.name || tc.function?.arguments);
56
+ if (isFirstToolCall)
57
+ process.stdout.write(chalk.blue.bold("\n\n💭 工具调用中..."));
58
+ for (const tc of delta.tool_calls) {
59
+ const index = tc.index ?? 0;
60
+ const func = tc.function;
61
+ if (!this.toolPrints[index]) {
62
+ this.toolPrints[index] = {
63
+ name: "",
64
+ arguments: "",
65
+ namePrinted: false,
66
+ argsPrinted: false,
67
+ completed: false,
68
+ };
69
+ }
70
+ const p = this.toolPrints[index];
71
+ if (func?.name)
72
+ p.name += func.name;
73
+ if (func?.arguments)
74
+ p.arguments += func.arguments;
75
+ if (p.name && !p.namePrinted) {
76
+ process.stdout.write(chalk.magenta.bold(`\n🔧 ${index} ${p.name}\n`));
77
+ p.namePrinted = true;
78
+ }
79
+ if (p.arguments && !p.argsPrinted && p.namePrinted)
80
+ p.argsPrinted = true;
81
+ if (func?.arguments && p.argsPrinted)
82
+ process.stdout.write(chalk.gray(func.arguments));
83
+ if (finishReason === AgentNS.FinishReason.ToolCalls)
84
+ p.completed = true;
85
+ }
86
+ if (finishReason === AgentNS.FinishReason.ToolCalls) {
87
+ for (const idx of Object.keys(this.toolPrints).map(Number)) {
88
+ const p = this.toolPrints[idx];
89
+ if (p.completed && p.arguments) {
90
+ process.stdout.write("\n");
91
+ try {
92
+ const parsed = JSON.parse(p.arguments);
93
+ process.stdout.write(chalk.gray(` ${JSON.stringify(parsed, null, 4)}\n`));
94
+ }
95
+ catch {
96
+ process.stdout.write(chalk.gray(` ${p.arguments}\n`));
97
+ }
98
+ }
99
+ }
100
+ process.stdout.write("\n");
101
+ }
102
+ }
103
+ }
104
+ //# sourceMappingURL=delta-renderer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"delta-renderer.js","sourceRoot":"","sources":["../src/delta-renderer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ export declare const MAX_CONTEXT_CHARS = 500000;
2
+ export declare function contextSize(messages: any[]): number;
3
+ export declare function shouldMigrate(messages: any[]): boolean;
4
+ export declare function generateMigrationDoc(messages: any[]): Promise<string>;
5
+ //# sourceMappingURL=migration.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migration.d.ts","sourceRoot":"","sources":["../src/migration.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,iBAAiB,SAAS,CAAC;AAExC,wBAAgB,WAAW,CAAC,QAAQ,EAAE,GAAG,EAAE,GAAG,MAAM,CAEnD;AAED,wBAAgB,aAAa,CAAC,QAAQ,EAAE,GAAG,EAAE,GAAG,OAAO,CAEtD;AAED,wBAAsB,oBAAoB,CAAC,QAAQ,EAAE,GAAG,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CA0D3E"}
@@ -0,0 +1,70 @@
1
+ import { Agent, Message, OpenAI, ChatGPT } from "@ai-zen/agents-core";
2
+ import { readConfig } from "./config.js";
3
+ export const MAX_CONTEXT_CHARS = 500000;
4
+ export function contextSize(messages) {
5
+ return JSON.stringify(messages).length;
6
+ }
7
+ export function shouldMigrate(messages) {
8
+ return contextSize(messages) >= MAX_CONTEXT_CHARS;
9
+ }
10
+ export async function generateMigrationDoc(messages) {
11
+ const config = readConfig();
12
+ const endpoint = new OpenAI({ openai_endpoint: "https://api.deepseek.com/v1", api_key: config.apiKey });
13
+ const model = new ChatGPT({
14
+ model_config: {},
15
+ request_config: await endpoint.chatCompletion("deepseek-v4-flash"),
16
+ });
17
+ const agent = new Agent({
18
+ model,
19
+ messages: [Message.System(`你是一个专业的任务交接分析师。你的任务是阅读一段完整的AI助手与用户的对话历史,筛选出对后续工作有用的关键信息,生成一份结构清晰的交接文档。
20
+
21
+ 这份交接文档将作为新会话的第一条用户消息,让新的AI助手了解任务背景并继续工作。
22
+
23
+ 请按以下模板生成 Markdown 格式的文档:
24
+
25
+ ## ✅ 已完成的任务
26
+ 列出已经完成的任务及其产出物(文件路径、代码片段位置等)。
27
+ 如果没有已完成的任务,保留此标题并注明"无"。
28
+ 注意:只记录任务标题和产出路径,不需要描述完成过程的细节。
29
+
30
+ ## 📋 未完成的任务
31
+ 列出所有待继续完成的任务,包含:
32
+ - 任务描述
33
+ - 当前进度
34
+ - 下一步需要做什么
35
+ - 相关的文件路径
36
+ - 优先级(如果对话中提到过)
37
+
38
+ ## 🧠 重要记忆
39
+ 记录所有对后续工作有影响的信息,包括但不限于:
40
+ - 用户的技术偏好和约定
41
+ - 踩过的坑和教训
42
+ - 项目特定的架构决策
43
+ - 任何对后续工作有指导意义的信息
44
+
45
+ ## 📁 文件索引
46
+ 按用途分类列出对话中涉及的重要文件路径,方便新 Agent 按需阅读。
47
+ **请对每个文件注明其用途或作用**,让接手者能快速判断哪些文件需要优先阅读。
48
+
49
+ ## 🔔 接手指令
50
+ 接手后请按以下步骤操作:
51
+ 1. **读文件** — 使用 shell 工具(cat、grep、find 等)读取「文件索引」中列出的关键文件,以实际代码为准,不要依赖训练数据或过往经验
52
+ 2. **对状态** — 确认代码中的关键常量、函数签名、配置等是否与文档描述一致
53
+ 3. **再行动** — 确认无误后再继续未完成任务,做任何改动前先跟用户商量
54
+
55
+ ---
56
+
57
+ 注意事项:
58
+ 1. 只包含确定的信息,不要猜测或补充对话中没有的内容
59
+ 2. 已完成的任务只需列出任务标题和产出路径,不要罗列完成过程的每一步
60
+ 3. 如果某个部分没有需要记录的内容,标注"无"即可
61
+ 4. 语言风格与原始对话一致(中文)`)],
62
+ tools: [],
63
+ });
64
+ const result = await agent.send(`请阅读以下对话历史,生成交接文档:\n\n${messages.map((m) => `[${m.role}] ${typeof m.content === "string" ? m.content : ""}`).join("\n\n")}`);
65
+ const last = result.at(-1);
66
+ if (!last || last.status === "error")
67
+ throw new Error("生成交接文档失败");
68
+ return typeof last.content === "string" ? last.content : "";
69
+ }
70
+ //# sourceMappingURL=migration.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migration.js","sourceRoot":"","sources":["../src/migration.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AACtE,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC,MAAM,CAAC,MAAM,iBAAiB,GAAG,MAAM,CAAC;AAExC,MAAM,UAAU,WAAW,CAAC,QAAe;IACzC,OAAO,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC;AACzC,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,QAAe;IAC3C,OAAO,WAAW,CAAC,QAAQ,CAAC,IAAI,iBAAiB,CAAC;AACpD,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,oBAAoB,CAAC,QAAe;IACxD,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;IAC5B,MAAM,QAAQ,GAAG,IAAI,MAAM,CAAC,EAAE,eAAe,EAAE,6BAA6B,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC;IACxG,MAAM,KAAK,GAAG,IAAI,OAAO,CAAC;QACxB,YAAY,EAAE,EAAE;QAChB,cAAc,EAAE,MAAM,QAAQ,CAAC,cAAc,CAAC,mBAAmB,CAAC;KACnE,CAAC,CAAC;IACH,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC;QACtB,KAAK;QACL,QAAQ,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;mBA0CX,CAAC,CAAC;QACjB,KAAK,EAAE,EAAE;KACV,CAAC,CAAC;IACH,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,IAAI,CAAC,wBAAwB,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,IAAI,KAAK,OAAO,CAAC,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IAC7J,MAAM,IAAI,GAAG,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IAC3B,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,MAAM,KAAK,OAAO;QAAE,MAAM,IAAI,KAAK,CAAC,UAAU,CAAC,CAAC;IAClE,OAAO,OAAO,IAAI,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;AAC9D,CAAC"}
@@ -0,0 +1,3 @@
1
+ import { CallbackTool } from "@ai-zen/agents-core";
2
+ export declare const shellTool: CallbackTool;
3
+ //# sourceMappingURL=tools.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAGnD,eAAO,MAAM,SAAS,cAmBpB,CAAC"}
package/dist/tools.js ADDED
@@ -0,0 +1,24 @@
1
+ import { CallbackTool } from "@ai-zen/agents-core";
2
+ import { execSync } from "node:child_process";
3
+ export const shellTool = new CallbackTool({
4
+ function: {
5
+ name: "shell",
6
+ description: "执行 shell 命令并返回输出",
7
+ parameters: {
8
+ type: "object",
9
+ properties: {
10
+ command: { type: "string", description: "要执行的命令" },
11
+ },
12
+ required: ["command"],
13
+ },
14
+ },
15
+ callback(args) {
16
+ try {
17
+ return execSync(args.command, { encoding: "utf-8", timeout: 30000 });
18
+ }
19
+ catch (e) {
20
+ return `错误: ${e.message}`;
21
+ }
22
+ },
23
+ });
24
+ //# sourceMappingURL=tools.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tools.js","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAE9C,MAAM,CAAC,MAAM,SAAS,GAAG,IAAI,YAAY,CAAC;IACxC,QAAQ,EAAE;QACR,IAAI,EAAE,OAAO;QACb,WAAW,EAAE,kBAAkB;QAC/B,UAAU,EAAE;YACV,IAAI,EAAE,QAAQ;YACd,UAAU,EAAE;gBACV,OAAO,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,QAAQ,EAAE;aACnD;YACD,QAAQ,EAAE,CAAC,SAAS,CAAC;SACtB;KACF;IACD,QAAQ,CAAO,IAAyB;QACtC,IAAI,CAAC;YACH,OAAO,QAAQ,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;QACvE,CAAC;QAAC,OAAO,CAAM,EAAE,CAAC;YAChB,OAAO,OAAO,CAAC,CAAC,OAAO,EAAE,CAAC;QAC5B,CAAC;IACH,CAAC;CACF,CAAC,CAAC"}
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@ai-zen/air",
3
+ "version": "0.1.3",
4
+ "description": "极简 AI 命令行助手",
5
+ "type": "module",
6
+ "main": "./dist/agent-runtime.js",
7
+ "bin": {
8
+ "air": "./dist/cli.js"
9
+ },
10
+ "dependencies": {
11
+ "@ai-zen/agents-core": "file:../agents/packages/core",
12
+ "chalk": "^5.6.2",
13
+ "commander": "^15.0.0",
14
+ "inquirer": "^9.3.8"
15
+ },
16
+ "devDependencies": {
17
+ "@types/inquirer": "^9.0.10",
18
+ "@types/node": "^24.12.2",
19
+ "typescript": "^5.3.3",
20
+ "vitest": "^4.1.9"
21
+ },
22
+ "scripts": {
23
+ "build": "tsc",
24
+ "start": "npm run build && node ./dist/cli.js",
25
+ "test": "vitest run",
26
+ "test:watch": "vitest"
27
+ }
28
+ }
@@ -0,0 +1,111 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import { existsSync, rmSync, mkdirSync, writeFileSync, readFileSync, readdirSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { tmpdir } from "node:os";
5
+ import { readConfig, saveConfig, readMessages, saveMessages, clearMessages, saveSnapshot, listSnapshots, loadSnapshot } from "../config.js";
6
+
7
+ describe("config", () => {
8
+ let tempDir: string;
9
+ let origHome: string | undefined;
10
+
11
+ beforeEach(() => {
12
+ tempDir = join(tmpdir(), `air-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
13
+ mkdirSync(tempDir, { recursive: true });
14
+ origHome = process.env.HOME;
15
+ process.env.HOME = tempDir;
16
+ });
17
+
18
+ afterEach(() => {
19
+ if (origHome) process.env.HOME = origHome;
20
+ else delete process.env.HOME;
21
+ try { rmSync(tempDir, { recursive: true, force: true }); } catch {}
22
+ });
23
+
24
+ describe("config (apiKey)", () => {
25
+ it("默认 apiKey 为空", () => {
26
+ expect(readConfig().apiKey).toBe("");
27
+ });
28
+
29
+ it("保存和读取 apiKey", () => {
30
+ saveConfig("sk-test-key-12345");
31
+ expect(readConfig().apiKey).toBe("sk-test-key-12345");
32
+ });
33
+
34
+ it("配置文件损坏时返回空 key", () => {
35
+ mkdirSync(join(tempDir, ".ai-zen", "air"), { recursive: true });
36
+ writeFileSync(join(tempDir, ".ai-zen", "air", "config.json"), "不是JSON", "utf-8");
37
+ expect(readConfig().apiKey).toBe("");
38
+ });
39
+ });
40
+
41
+ describe("context (messages 数组)", () => {
42
+ it("不存在时返回空数组", () => {
43
+ expect(readMessages()).toEqual([]);
44
+ });
45
+
46
+ it("保存和读取 messages", () => {
47
+ const msgs = [
48
+ { role: "user", content: "你好" },
49
+ { role: "assistant", content: "你好!" },
50
+ ];
51
+ saveMessages(msgs);
52
+ expect(readMessages()).toEqual(msgs);
53
+ });
54
+
55
+ it("clearMessages 清空为 []", () => {
56
+ saveMessages([{ role: "user", content: "hi" }]);
57
+ clearMessages();
58
+ expect(readMessages()).toEqual([]);
59
+ });
60
+
61
+ it("文件内容不是数组时返回空数组", () => {
62
+ mkdirSync(join(tempDir, ".ai-zen", "air"), { recursive: true });
63
+ writeFileSync(join(tempDir, ".ai-zen", "air", "context.json"), '"不是数组"', "utf-8");
64
+ expect(readMessages()).toEqual([]);
65
+ });
66
+
67
+ it("文件损坏时返回空数组", () => {
68
+ mkdirSync(join(tempDir, ".ai-zen", "air"), { recursive: true });
69
+ writeFileSync(join(tempDir, ".ai-zen", "air", "context.json"), "不是JSON", "utf-8");
70
+ expect(readMessages()).toEqual([]);
71
+ });
72
+ });
73
+
74
+ describe("snapshots", () => {
75
+ it("保存快照,文件名是 ISO 时间", () => {
76
+ const msgs = [{ role: "user", content: "测试快照" }];
77
+ const name = saveSnapshot(msgs);
78
+ expect(name).toBeTruthy();
79
+
80
+ const snapDir = join(tempDir, ".ai-zen", "air", "snapshots");
81
+ expect(existsSync(snapDir)).toBe(true);
82
+ const files = readdirSync(snapDir).filter((f) => f.endsWith(".json"));
83
+ expect(files.length).toBe(1);
84
+ expect(files[0]).toBe(name + ".json");
85
+
86
+ const loaded = JSON.parse(readFileSync(join(snapDir, files[0]), "utf-8"));
87
+ expect(loaded).toEqual(msgs);
88
+ });
89
+ });
90
+
91
+
92
+ describe("listSnapshots / loadSnapshot", () => {
93
+ it("加载不存在的快照返回空数组", () => {
94
+ expect(loadSnapshot("nonexistent")).toEqual([]);
95
+ });
96
+
97
+ it("保存后能列出并加载快照", () => {
98
+ const msgs = [{ role: "user", content: "测试" }];
99
+ const name = saveSnapshot(msgs);
100
+
101
+ const list = listSnapshots();
102
+ expect(list.length).toBe(1);
103
+ expect(list[0].name).toBe(name);
104
+ expect(list[0].date).toBeTruthy();
105
+
106
+ const loaded = loadSnapshot(name);
107
+ expect(loaded).toEqual(msgs);
108
+ });
109
+ });
110
+
111
+ });
@@ -0,0 +1,43 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { contextSize, shouldMigrate } from "../migration.js";
3
+
4
+ describe("contextSize", () => {
5
+ it("空数组", () => {
6
+ expect(contextSize([])).toBe(2);
7
+ });
8
+
9
+ it("一条消息", () => {
10
+ const msgs = [{ role: "user", content: "你好" }];
11
+ expect(contextSize(msgs)).toBe(JSON.stringify(msgs).length);
12
+ });
13
+
14
+ it("多条消息", () => {
15
+ const msgs = [
16
+ { role: "user", content: "你好" },
17
+ { role: "assistant", content: "你好!有什么可以帮你的吗?" },
18
+ ];
19
+ expect(contextSize(msgs)).toBe(JSON.stringify(msgs).length);
20
+ });
21
+ });
22
+
23
+ describe("shouldMigrate", () => {
24
+ it("空消息不迁移", () => {
25
+ expect(shouldMigrate([])).toBe(false);
26
+ });
27
+
28
+ it("小消息不迁移", () => {
29
+ expect(shouldMigrate([{ role: "user", content: "hi" }])).toBe(false);
30
+ });
31
+
32
+ it("达到 66 万时迁移", () => {
33
+ const n = 500000 - JSON.stringify([{ role: "user", content: "" }]).length;
34
+ const bigMsg = { role: "user", content: "x".repeat(n) };
35
+ expect(shouldMigrate([bigMsg])).toBe(true);
36
+ });
37
+
38
+ it("差一点不到时不迁移", () => {
39
+ const n = 500000 - JSON.stringify([{ role: "user", content: "" }]).length - 1;
40
+ const bigMsg = { role: "user", content: "x".repeat(n) };
41
+ expect(shouldMigrate([bigMsg])).toBe(false);
42
+ });
43
+ });
@@ -0,0 +1,15 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { shellTool } from "../tools.js";
3
+
4
+ describe("shellTool", () => {
5
+ it("具有正确的名称和描述", () => {
6
+ expect(shellTool.function.name).toBe("shell");
7
+ expect(shellTool.function.description).toBe("执行 shell 命令并返回输出");
8
+ });
9
+
10
+ it("参数定义正确", () => {
11
+ const params = shellTool.function.parameters;
12
+ expect(params.properties.command.type).toBe("string");
13
+ expect(params.required).toEqual(["command"]);
14
+ });
15
+ });
@@ -0,0 +1,23 @@
1
+ import { Agent, Message, OpenAI, ChatGPT } from "@ai-zen/agents-core";
2
+ import type { AgentNS } from "@ai-zen/agents-core";
3
+ import { readConfig } from "./config.js";
4
+ import { shellTool } from "./tools.js";
5
+
6
+ const MODEL_NAME = "deepseek-v4-flash";
7
+ const API_ENDPOINT = "https://api.deepseek.com/v1";
8
+
9
+ async function buildModel(apiKey: string) {
10
+ const endpoint = new OpenAI({ openai_endpoint: API_ENDPOINT, api_key: apiKey });
11
+ return new ChatGPT({
12
+ model_config: {},
13
+ request_config: await endpoint.chatCompletion(MODEL_NAME),
14
+ });
15
+ }
16
+
17
+ export async function buildAgent(savedMessages: any[]): Promise<Agent> {
18
+ const config = readConfig();
19
+ const model = await buildModel(config.apiKey);
20
+ const messages: AgentNS.Message[] = [];
21
+ for (const m of savedMessages) messages.push(new Message(m));
22
+ return new Agent({ model, messages, tools: [shellTool] });
23
+ }