chivgent 0.6.0

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 (84) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +359 -0
  3. package/README.zh-CN.md +345 -0
  4. package/dist/agent.d.ts +58 -0
  5. package/dist/agent.d.ts.map +1 -0
  6. package/dist/agent.js +281 -0
  7. package/dist/agent.js.map +1 -0
  8. package/dist/cli-options.d.ts +33 -0
  9. package/dist/cli-options.d.ts.map +1 -0
  10. package/dist/cli-options.js +151 -0
  11. package/dist/cli-options.js.map +1 -0
  12. package/dist/cli.d.ts +3 -0
  13. package/dist/cli.d.ts.map +1 -0
  14. package/dist/cli.js +233 -0
  15. package/dist/cli.js.map +1 -0
  16. package/dist/events.d.ts +79 -0
  17. package/dist/events.d.ts.map +1 -0
  18. package/dist/events.js +12 -0
  19. package/dist/events.js.map +1 -0
  20. package/dist/llm.d.ts +38 -0
  21. package/dist/llm.d.ts.map +1 -0
  22. package/dist/llm.js +17 -0
  23. package/dist/llm.js.map +1 -0
  24. package/dist/messages.d.ts +23 -0
  25. package/dist/messages.d.ts.map +1 -0
  26. package/dist/messages.js +2 -0
  27. package/dist/messages.js.map +1 -0
  28. package/dist/providers/deepseek.d.ts +12 -0
  29. package/dist/providers/deepseek.d.ts.map +1 -0
  30. package/dist/providers/deepseek.js +15 -0
  31. package/dist/providers/deepseek.js.map +1 -0
  32. package/dist/providers/openai-compatible-chat.d.ts +25 -0
  33. package/dist/providers/openai-compatible-chat.d.ts.map +1 -0
  34. package/dist/providers/openai-compatible-chat.js +293 -0
  35. package/dist/providers/openai-compatible-chat.js.map +1 -0
  36. package/dist/providers/openai.d.ts +16 -0
  37. package/dist/providers/openai.d.ts.map +1 -0
  38. package/dist/providers/openai.js +183 -0
  39. package/dist/providers/openai.js.map +1 -0
  40. package/dist/render.d.ts +26 -0
  41. package/dist/render.d.ts.map +1 -0
  42. package/dist/render.js +95 -0
  43. package/dist/render.js.map +1 -0
  44. package/dist/repl.d.ts +29 -0
  45. package/dist/repl.d.ts.map +1 -0
  46. package/dist/repl.js +119 -0
  47. package/dist/repl.js.map +1 -0
  48. package/dist/retry.d.ts +42 -0
  49. package/dist/retry.d.ts.map +1 -0
  50. package/dist/retry.js +186 -0
  51. package/dist/retry.js.map +1 -0
  52. package/dist/session-store.d.ts +66 -0
  53. package/dist/session-store.d.ts.map +1 -0
  54. package/dist/session-store.js +183 -0
  55. package/dist/session-store.js.map +1 -0
  56. package/dist/session.d.ts +49 -0
  57. package/dist/session.d.ts.map +1 -0
  58. package/dist/session.js +115 -0
  59. package/dist/session.js.map +1 -0
  60. package/dist/tools/list-files.d.ts +24 -0
  61. package/dist/tools/list-files.d.ts.map +1 -0
  62. package/dist/tools/list-files.js +80 -0
  63. package/dist/tools/list-files.js.map +1 -0
  64. package/dist/tools/output.d.ts +10 -0
  65. package/dist/tools/output.d.ts.map +1 -0
  66. package/dist/tools/output.js +44 -0
  67. package/dist/tools/output.js.map +1 -0
  68. package/dist/tools/read-file.d.ts +29 -0
  69. package/dist/tools/read-file.d.ts.map +1 -0
  70. package/dist/tools/read-file.js +150 -0
  71. package/dist/tools/read-file.js.map +1 -0
  72. package/dist/tools/search-text.d.ts +28 -0
  73. package/dist/tools/search-text.d.ts.map +1 -0
  74. package/dist/tools/search-text.js +94 -0
  75. package/dist/tools/search-text.js.map +1 -0
  76. package/dist/tools/tool.d.ts +22 -0
  77. package/dist/tools/tool.d.ts.map +1 -0
  78. package/dist/tools/tool.js +2 -0
  79. package/dist/tools/tool.js.map +1 -0
  80. package/dist/workspace.d.ts +72 -0
  81. package/dist/workspace.d.ts.map +1 -0
  82. package/dist/workspace.js +504 -0
  83. package/dist/workspace.js.map +1 -0
  84. package/package.json +55 -0
@@ -0,0 +1,345 @@
1
+ # chivgent
2
+
3
+ [English](README.md) | [简体中文](README.zh-CN.md)
4
+
5
+ > 一个小巧、易读的 Coding Agent CLI,用来理解 Agent Harness 的真实工作原理。
6
+
7
+ ![Node.js](https://img.shields.io/badge/Node.js-%3E%3D20-339933?logo=nodedotjs&logoColor=white)
8
+ ![TypeScript](https://img.shields.io/badge/TypeScript-ESM-3178C6?logo=typescript&logoColor=white)
9
+ [![CI](https://github.com/chivopic/chivgent/actions/workflows/ci.yml/badge.svg)](https://github.com/chivopic/chivgent/actions/workflows/ci.yml)
10
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
11
+ ![Status](https://img.shields.io/badge/status-MVP-orange)
12
+
13
+ `chivgent` 将 Provider 无关的 Agent Loop 与 LLM API、工具和工作区边界连接起来。
14
+ 当前 MVP 可以先发现文件、搜索源码并分段读取内容,再使用 OpenAI、DeepSeek 或
15
+ 任意兼容 Chat Completions 的 API 回答代码问题。
16
+
17
+ 这个项目刻意保持精简:先让 Tool Calling、Conversation State、Provider Adapter
18
+ 和循环终止条件容易理解,再逐步加入成熟 Agent Harness 所需的工程复杂度。
19
+
20
+ ## 功能
21
+
22
+ - 完整的多轮 Agent Loop:模型 -> Tool Call -> Tool Result -> 模型。
23
+ - Provider 无关的运行时消息和工具契约。
24
+ - 通过 Responses API 支持 OpenAI。
25
+ - 通过通用 OpenAI-compatible Chat Completions 客户端支持 DeepSeek。
26
+ - 无需修改代码即可配置自定义 OpenAI-compatible API。
27
+ - 既可以进入带斜杠命令的交互式会话,也可以单次提问后退出。
28
+ - Session 以 JSON Lines 持久化,可以在之后的进程中恢复。
29
+ - 提供 `--json` 事件流,便于脚本和其他前端消费。
30
+ - 基于类型化运行时事件流的流式输出。
31
+ - 可中断的运行:Ctrl+C 结束当前运行,且不会丢失已产生的 transcript。
32
+ - Provider 调用具备单次超时和有上限的指数退避重试。
33
+ - 通过 `list_files` 和字面量 `search_text` 确定性地发现项目内容。
34
+ - 支持带续读提示的分段 `read_file`,所有工具结果都有容量上限。
35
+ - 安全、只读的工作区访问,阻止路径穿越和符号链接逃逸。
36
+ - 支持根 `.gitignore`、生成目录和敏感路径过滤。
37
+ - 工具参数验证、明确的工具错误和最多八轮的安全限制。
38
+ - 可打包安装的 Node.js CLI,不依赖 Agent 框架。
39
+ - 默认测试不调用真实 API,不消耗模型额度。
40
+
41
+ ## 快速开始
42
+
43
+ ### 环境要求
44
+
45
+ - Node.js 20 或更高版本
46
+ - npm
47
+ - OpenAI、DeepSeek 或其他兼容供应商的 API Key
48
+
49
+ ### 从 npm 安装
50
+
51
+ ```bash
52
+ npm install -g chivgent
53
+ chivgent --version
54
+ ```
55
+
56
+ 如果希望安装当前源码版本:
57
+
58
+ ```bash
59
+ git clone https://github.com/chivopic/chivgent.git
60
+ cd chivgent
61
+ npm install
62
+ npm run build
63
+ npm install -g .
64
+ ```
65
+
66
+ ### 分析一个项目
67
+
68
+ 进入你希望 Agent 分析的项目目录,然后运行 `chivgent`。
69
+
70
+ 使用 OpenAI:
71
+
72
+ ```bash
73
+ export OPENAI_API_KEY="your-api-key"
74
+ chivgent "src/agent.ts 是做什么的?"
75
+ ```
76
+
77
+ 使用 DeepSeek:
78
+
79
+ ```bash
80
+ export DEEPSEEK_API_KEY="your-api-key"
81
+ chivgent --provider deepseek "解释 src/ 目录的架构"
82
+ ```
83
+
84
+ 使用任意 OpenAI-compatible Chat Completions API:
85
+
86
+ ```bash
87
+ export OPENAI_API_KEY="your-provider-api-key"
88
+ export OPENAI_BASE_URL="https://api.vendor.example/v1"
89
+ export OPENAI_MODEL="vendor-model"
90
+
91
+ chivgent --provider openai-compatible "解释 src/ 目录的架构"
92
+ ```
93
+
94
+ 不带问题直接运行 `chivgent` 会进入交互式会话:
95
+
96
+ ```bash
97
+ chivgent
98
+ › src/agent.ts 在做什么?
99
+ › 这个循环在哪里测试?
100
+ › /exit
101
+ ```
102
+
103
+ 会话会跨多轮保留上下文,因此追问不需要重复之前的信息。之后可以用
104
+ `chivgent --continue`(或 `chivgent --resume <id>`)恢复;`chivgent --sessions`
105
+ 可以列出已记录的会话。
106
+
107
+ 答案会随模型生成实时写入 stdout,因此 stdout 仍然可以直接管道使用。工具活动、
108
+ 重试和运行状态写入 stderr;Provider 错误会返回非零退出码。使用 `--no-stream`
109
+ 可改为一次性输出完整答案,`--quiet` 可隐藏工具活动。
110
+
111
+ ## CLI 参考
112
+
113
+ ```text
114
+ chivgent [选项] "问题" 回答一次后退出
115
+ chivgent [选项] 进入交互式会话
116
+
117
+ 选项:
118
+ --provider NAME openai、deepseek 或 openai-compatible(默认:openai)
119
+ --model MODEL 覆盖 Provider 模型
120
+ --max-turns N 工具调用轮次上限(默认:8)
121
+ --no-stream 关闭流式输出,等待完整答案
122
+ -q, --quiet 不在 stderr 打印工具活动
123
+ --json 以 JSON Lines 输出整次运行,而不是渲染文本
124
+ -c, --continue 恢复当前工作区最近的一次 Session
125
+ --resume ID 恢复指定 Session
126
+ --sessions 列出已记录的 Session 并退出
127
+ --no-session 不记录本次运行
128
+ -h, --help 显示帮助
129
+ -v, --version 显示版本
130
+ ```
131
+
132
+ 交互式会话中 `/help` 会列出全部斜杠命令:`/session`、`/tools`、`/clear` 和
133
+ `/exit`。Ctrl+C 只中断当前回答,不会退出会话;Ctrl+D 才会离开。
134
+
135
+ 退出码:`0` 正常回答,`1` 配置或 Provider 失败,`2` 达到轮次上限,`130` 被
136
+ Ctrl+C 中断。
137
+
138
+ ### Provider 配置
139
+
140
+ | Provider | API Key | 模型环境变量 | 默认模型 | API 形式 |
141
+ | --- | --- | --- | --- | --- |
142
+ | OpenAI | `OPENAI_API_KEY` | `OPENAI_MODEL` | `gpt-5.6` | Responses API |
143
+ | DeepSeek | `DEEPSEEK_API_KEY` | `DEEPSEEK_MODEL` | `deepseek-v4-flash` | OpenAI-compatible Chat Completions |
144
+ | 自定义兼容供应商 | `OPENAI_API_KEY` | `OPENAI_MODEL` | 必填 | OpenAI-compatible Chat Completions |
145
+
146
+ 显式传入的 `--model` 优先于 Provider 对应的模型环境变量。自定义兼容供应商还必须
147
+ 配置 `OPENAI_BASE_URL`。Session 记录在 `CHIVGENT_HOME`(默认 `~/.chivgent`)下。
148
+
149
+ ```bash
150
+ chivgent --provider openai --model gpt-5.6 "解释 package.json"
151
+ chivgent --provider deepseek --model deepseek-v4-pro "解释 package.json"
152
+ chivgent --provider openai-compatible --model vendor-model "解释 package.json"
153
+ ```
154
+
155
+ ## 架构
156
+
157
+ ```text
158
+ +-> OpenAI Responses API
159
+ 用户 -> CLI -> Agent -> LLMClient |
160
+ | +-> OpenAI-compatible Chat -> DeepSeek / 自定义
161
+ |
162
+ +-> Tool Registry -> list_files / search_text / read_file -> Workspace
163
+ ```
164
+
165
+ Agent Runtime 拥有自己的消息模型。Provider 特有的数据结构只在 `LLMClient` 边界
166
+ 进行转换:
167
+
168
+ ```text
169
+ Agent Message[] -> Provider Adapter -> Provider Request
170
+ <- Provider Response
171
+ AssistantMessage <- Normalized Result
172
+ ```
173
+
174
+ 因此 Agent、工具和 CLI 不会依赖任何单一供应商的消息格式。
175
+
176
+ ### 运行时事件
177
+
178
+ Agent Loop 自己不打印任何内容,而是通过类型化的事件流对外汇报。一次运行会产生:
179
+
180
+ ```text
181
+ agent_start
182
+ turn_start -> message_start -> message_update* -> message_end
183
+ tool_execution_start -> tool_execution_end (每个 Tool Call 一次)
184
+ turn_end
185
+ ...
186
+ agent_end (completed | max_turns | aborted | error)
187
+ ```
188
+
189
+ `message_update` 只携带增量,不携带累计快照,因此事件流的体积与答案长度保持线性
190
+ 关系。事件都是可结构化克隆的,且每个监听者拿到的是副本,渲染层无法修改
191
+ transcript。`src/render.ts` 中的 CLI 渲染器只是其中一个消费者,日志、JSON 流或
192
+ TUI 同样可以消费。
193
+
194
+ `LLMClient.stream` 是可选的。Provider 未实现时,Agent 会退回 `complete`,事件序列
195
+ 不变,只是没有增量事件。
196
+
197
+ ### OpenAI-compatible 供应商
198
+
199
+ 兼容供应商可以通过修改 `baseURL`、凭据和模型名称,复用官方 `openai` npm 包。
200
+ CLI 用户无需修改代码:
201
+
202
+ ```bash
203
+ export OPENAI_API_KEY="your-provider-api-key"
204
+ export OPENAI_BASE_URL="https://api.vendor.example/v1"
205
+ export OPENAI_MODEL="vendor-model"
206
+
207
+ chivgent --provider openai-compatible "src/agent.ts 是做什么的?"
208
+ ```
209
+
210
+ `OPENAI_BASE_URL` 必须指向供应商的 OpenAI-compatible API 根地址。供应商至少需要
211
+ 实现 `POST /chat/completions` 和 Function Tool Calling。
212
+
213
+ 如果要在源码中增加一个具名 Provider,可以复用相同的 Adapter:
214
+
215
+ ```ts
216
+ const client = new OpenAICompatibleChatClient({
217
+ apiKey: process.env.VENDOR_API_KEY!,
218
+ baseURL: "https://api.vendor.example/v1",
219
+ model: "vendor-model",
220
+ continuationTag: "vendor-chat",
221
+ });
222
+ ```
223
+
224
+ `DeepSeekChatClient` 就是共享客户端之上的轻量配置包装器。兼容层还会把 DeepSeek
225
+ 的 `reasoning_content` 等 Provider 私有字段保存在不透明的 continuation state 中。
226
+
227
+ 修改 `baseURL` 不代表所有能力都能完全兼容。不同供应商的模型名称、鉴权方式、
228
+ 工具 Schema、Strict Mode、推理字段、流式事件和错误结构都可能不同。供应商差异
229
+ 应保留在轻量 Provider Adapter 内,而不是泄漏到 Agent Loop。
230
+
231
+ ## 项目结构
232
+
233
+ ```text
234
+ src/
235
+ cli.ts CLI 入口与进程边界
236
+ cli-options.ts 参数和 Provider 配置
237
+ agent.ts Agent Loop 与运行状态
238
+ events.ts 运行时事件模型
239
+ render.ts 运行时事件的终端渲染
240
+ llm.ts Provider 无关的 LLM 契约
241
+ retry.ts Provider 超时与重试装饰器
242
+ messages.ts 运行时消息模型
243
+ session.ts 会话状态与事件分发
244
+ session-store.ts JSONL 会话日志与恢复
245
+ repl.ts 交互式输入与斜杠命令
246
+ workspace.ts 安全的本地工作区访问
247
+ providers/
248
+ openai.ts OpenAI Responses Adapter
249
+ openai-compatible-chat.ts 通用 Chat Completions Adapter
250
+ deepseek.ts DeepSeek 配置包装器
251
+ tools/
252
+ tool.ts Tool 契约
253
+ output.ts 共享的 64 KiB 工具输出边界
254
+ list-files.ts 确定性的项目树发现工具
255
+ search-text.ts 有界的源码字面量搜索工具
256
+ read-file.ts 分段文本读取工具
257
+ tests/ Provider、Agent Loop 和 Workspace 测试
258
+ docs/ 架构与学习文档
259
+ ```
260
+
261
+ ## 开发
262
+
263
+ ```bash
264
+ npm install
265
+ npm run check
266
+ npm test
267
+ npm run build
268
+ ```
269
+
270
+ 运行完整的发布检查(包含 npm tarball dry run):
271
+
272
+ ```bash
273
+ npm run release:check
274
+ ```
275
+
276
+ 构建一个可以在本地安装的 tarball:
277
+
278
+ ```bash
279
+ npm pack
280
+ npm install -g ./chivgent-0.6.0.tgz
281
+ ```
282
+
283
+ 测试使用脚本化或 Mock LLM Client。真实 API Smoke Test 需要手工执行,因此默认
284
+ 测试不会消耗 API 额度。
285
+
286
+ ## 安全模型
287
+
288
+ - API Key 只从环境变量读取,绝不能提交到仓库。
289
+ - 自定义 `OPENAI_BASE_URL` 会收到配置的 API Key 和提示词,只能使用可信端点。
290
+ - 当前所有工作区工具都是只读工具。
291
+ - 文件路径必须位于当前工作区内。
292
+ - Real Path 检查会阻止 `..` 路径穿越和符号链接逃逸。
293
+ - 文件大小和二进制内容检查会限制不安全的读取。
294
+ - 自动发现遵循根 `.gitignore` 和固定的生成目录忽略规则。
295
+ - 所有工具统一禁止常见凭据、私钥和敏感配置路径。
296
+ - 工具结果限制为 64 KiB,读取、扫描、深度和结果数量都有硬上限。
297
+ - 工具输入是不可信数据,执行前必须验证。
298
+ - Agent 会在有限的模型轮数后终止。
299
+ - `~/.chivgent/sessions` 下的会话日志包含提问、回答和工具结果(含文件片段)。
300
+ 在敏感项目中请使用 `--no-session`,并像对待项目本身一样对待该目录。
301
+ - Session id 在拼接成文件路径前会先做校验。
302
+
303
+ 这是一个用于学习的 MVP,并不是经过加固的 Sandbox。在为它增加写文件或 Shell
304
+ 工具,并允许其访问敏感项目之前,请先审查代码和威胁模型。
305
+
306
+ ## 路线图
307
+
308
+ - [x] 最小 Tool Calling Agent Loop
309
+ - [x] 安全的 `read_file` 工具
310
+ - [x] OpenAI 和 DeepSeek Provider
311
+ - [x] 通用 OpenAI-compatible Chat Completions Adapter
312
+ - [x] 自定义 OpenAI-compatible CLI Provider
313
+ - [x] 项目发现工具:`list_files`、`search_text` 和分段 `read_file`
314
+ - [x] 流式输出和运行时事件
315
+ - [x] 持久化多轮 Session
316
+ - [ ] Context Window 管理和压缩
317
+ - [ ] 需要权限确认的 `write_file`、`edit_file` 和 Shell 工具
318
+ - [ ] Provider Registry 和用户配置文件
319
+ - [ ] TUI、Extensions、Telemetry 和 Evals
320
+
321
+ ## 文档
322
+
323
+ - [Stage 1:Minimal Agent 设计](docs/stage-1-minimal-agent.md)
324
+ - [DeepSeek Provider 设计](docs/deepseek-provider.md)
325
+ - [Stage 2:Project Discovery 实现设计](docs/stage-2-project-discovery.md)
326
+ - [Stage 3:Runtime Events 与流式输出设计](docs/stage-3-runtime-events.md)
327
+ - [Stage 4:Session 与交互模式设计](docs/stage-4-sessions.md)
328
+ - [发布流程](docs/releasing.md)
329
+
330
+ ## 参与贡献
331
+
332
+ 欢迎提交 Issue 和范围明确的 Pull Request。提交修改前请运行:
333
+
334
+ ```bash
335
+ npm run check
336
+ npm test
337
+ npm run build
338
+ ```
339
+
340
+ 请将 Provider 特有的类型保留在 `src/providers/` 中,并确保核心 Agent Runtime
341
+ 不依赖供应商 SDK 的数据结构。
342
+
343
+ ## 许可证
344
+
345
+ 本项目使用 [MIT License](LICENSE)。
@@ -0,0 +1,58 @@
1
+ import { type AgentEventListener } from "./events.js";
2
+ import { type LLMClient } from "./llm.js";
3
+ import type { AssistantMessage, Message } from "./messages.js";
4
+ import type { Tool } from "./tools/tool.js";
5
+ import type { Workspace } from "./workspace.js";
6
+ export interface AgentOptions {
7
+ readonly systemPrompt: string;
8
+ readonly maxTurns: number;
9
+ readonly llm: LLMClient;
10
+ readonly tools: readonly Tool[];
11
+ readonly workspace: Workspace;
12
+ /** Receives runtime events synchronously, in emission order. */
13
+ readonly onEvent?: AgentEventListener;
14
+ /** Use the Provider's streaming API when it offers one. Defaults to true. */
15
+ readonly streaming?: boolean;
16
+ }
17
+ export interface AgentRunOptions {
18
+ /** Cancels the run between turns, during a Provider call, and between tools. */
19
+ readonly signal?: AbortSignal;
20
+ /**
21
+ * Messages that precede this prompt. The Agent never mutates the array it is
22
+ * given; a session owns its transcript and passes a snapshot in.
23
+ */
24
+ readonly history?: readonly Message[];
25
+ }
26
+ export type AgentRunResult = {
27
+ readonly status: "completed";
28
+ readonly finalMessage: AssistantMessage;
29
+ readonly messages: readonly Message[];
30
+ readonly turnCount: number;
31
+ } | {
32
+ readonly status: "max_turns" | "aborted";
33
+ readonly messages: readonly Message[];
34
+ readonly turnCount: number;
35
+ };
36
+ export declare class AgentProtocolError extends Error {
37
+ constructor(message: string);
38
+ }
39
+ export declare class Agent {
40
+ private readonly systemPrompt;
41
+ private readonly maxTurns;
42
+ private readonly llm;
43
+ private readonly registry;
44
+ private readonly toolDefinitions;
45
+ private readonly workspace;
46
+ private readonly onEvent?;
47
+ private readonly streaming;
48
+ constructor(options: AgentOptions);
49
+ get toolNames(): readonly string[];
50
+ run(userInput: string, options?: AgentRunOptions): Promise<AgentRunResult>;
51
+ private loop;
52
+ private requestAssistantMessage;
53
+ private assertUniqueToolCallIds;
54
+ private executeToolCall;
55
+ private emit;
56
+ private emitEnd;
57
+ }
58
+ //# sourceMappingURL=agent.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../src/agent.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,kBAAkB,EAExB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAgB,KAAK,SAAS,EAAwB,MAAM,UAAU,CAAC;AAC9E,OAAO,KAAK,EACV,gBAAgB,EAChB,OAAO,EAGR,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EAAE,IAAI,EAA8B,MAAM,iBAAiB,CAAC;AACxE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAEhD,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,GAAG,EAAE,SAAS,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,SAAS,IAAI,EAAE,CAAC;IAChC,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC;IAC9B,gEAAgE;IAChE,QAAQ,CAAC,OAAO,CAAC,EAAE,kBAAkB,CAAC;IACtC,6EAA6E;IAC7E,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;CAC9B;AAED,MAAM,WAAW,eAAe;IAC9B,gFAAgF;IAChF,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;IAC9B;;;OAGG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,OAAO,EAAE,CAAC;CACvC;AAED,MAAM,MAAM,cAAc,GACtB;IACE,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,QAAQ,CAAC,YAAY,EAAE,gBAAgB,CAAC;IACxC,QAAQ,CAAC,QAAQ,EAAE,SAAS,OAAO,EAAE,CAAC;IACtC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B,GACD;IACE,QAAQ,CAAC,MAAM,EAAE,WAAW,GAAG,SAAS,CAAC;IACzC,QAAQ,CAAC,QAAQ,EAAE,SAAS,OAAO,EAAE,CAAC;IACtC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B,CAAC;AASN,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,YAAY,OAAO,EAAE,MAAM,EAG1B;CACF;AAED,qBAAa,KAAK;IAChB,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAY;IAChC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA4B;IACrD,OAAO,CAAC,QAAQ,CAAC,eAAe,CAA4B;IAC5D,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAY;IACtC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAqB;IAC9C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAU;IAEpC,YAAY,OAAO,EAAE,YAAY,EAmBhC;IAED,IAAI,SAAS,IAAI,SAAS,MAAM,EAAE,CAEjC;IAEK,GAAG,CACP,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,cAAc,CAAC,CAqCzB;YAEa,IAAI;YAuDJ,uBAAuB;IA2BrC,OAAO,CAAC,uBAAuB;YAcjB,eAAe;IA0D7B,OAAO,CAAC,IAAI;IAIZ,OAAO,CAAC,OAAO;CAahB"}
package/dist/agent.js ADDED
@@ -0,0 +1,281 @@
1
+ import { emitEvent, } from "./events.js";
2
+ import { isAbortError } from "./llm.js";
3
+ export class AgentProtocolError extends Error {
4
+ constructor(message) {
5
+ super(message);
6
+ this.name = "AgentProtocolError";
7
+ }
8
+ }
9
+ export class Agent {
10
+ systemPrompt;
11
+ maxTurns;
12
+ llm;
13
+ registry;
14
+ toolDefinitions;
15
+ workspace;
16
+ onEvent;
17
+ streaming;
18
+ constructor(options) {
19
+ if (!Number.isSafeInteger(options.maxTurns) || options.maxTurns <= 0) {
20
+ throw new TypeError("maxTurns must be a positive safe integer.");
21
+ }
22
+ this.systemPrompt = options.systemPrompt;
23
+ this.maxTurns = options.maxTurns;
24
+ this.llm = options.llm;
25
+ this.workspace = options.workspace;
26
+ this.registry = createToolRegistry(options.tools);
27
+ this.toolDefinitions = [...this.registry.values()].map((tool) => ({
28
+ name: tool.name,
29
+ description: tool.description,
30
+ inputSchema: structuredClone(tool.inputSchema),
31
+ }));
32
+ if (options.onEvent !== undefined) {
33
+ this.onEvent = options.onEvent;
34
+ }
35
+ this.streaming = options.streaming ?? true;
36
+ }
37
+ get toolNames() {
38
+ return [...this.registry.keys()];
39
+ }
40
+ async run(userInput, options = {}) {
41
+ if (userInput.trim().length === 0) {
42
+ throw new TypeError("User input must not be empty.");
43
+ }
44
+ const state = {
45
+ messages: [
46
+ ...structuredClone(options.history ?? []),
47
+ { role: "user", content: userInput },
48
+ ],
49
+ seenToolCallIds: new Set(),
50
+ turnCount: 0,
51
+ };
52
+ const signal = options.signal;
53
+ this.emit({
54
+ type: "agent_start",
55
+ prompt: userInput,
56
+ maxTurns: this.maxTurns,
57
+ });
58
+ try {
59
+ const result = await this.loop(state, signal);
60
+ this.emitEnd(result.status, state);
61
+ return result;
62
+ }
63
+ catch (error) {
64
+ if (isAbortError(error) || isAborted(signal)) {
65
+ this.emitEnd("aborted", state);
66
+ return abortedResult(state);
67
+ }
68
+ this.emitEnd("error", state, error instanceof Error ? error.message : "Unknown error");
69
+ throw error;
70
+ }
71
+ }
72
+ async loop(state, signal) {
73
+ while (state.turnCount < this.maxTurns) {
74
+ if (isAborted(signal)) {
75
+ return abortedResult(state);
76
+ }
77
+ state.turnCount += 1;
78
+ const turn = state.turnCount;
79
+ this.emit({ type: "turn_start", turn });
80
+ this.emit({ type: "message_start", turn });
81
+ const response = await this.requestAssistantMessage(state, turn, signal);
82
+ const assistant = validateAndCloneAssistantMessage(response.message);
83
+ this.assertUniqueToolCallIds(assistant.toolCalls, state.seenToolCallIds);
84
+ state.messages.push(assistant);
85
+ state.continuation = response.continuation;
86
+ this.emit({ type: "message_end", turn, message: assistant });
87
+ if (assistant.toolCalls.length === 0) {
88
+ this.emit({
89
+ type: "turn_end",
90
+ turn,
91
+ message: assistant,
92
+ toolResults: [],
93
+ });
94
+ return {
95
+ status: "completed",
96
+ finalMessage: assistant,
97
+ messages: snapshotMessages(state.messages),
98
+ turnCount: state.turnCount,
99
+ };
100
+ }
101
+ const toolResults = [];
102
+ for (const toolCall of assistant.toolCalls) {
103
+ if (isAborted(signal)) {
104
+ return abortedResult(state);
105
+ }
106
+ const result = await this.executeToolCall(toolCall, turn, signal);
107
+ toolResults.push(result);
108
+ state.messages.push(result);
109
+ }
110
+ this.emit({ type: "turn_end", turn, message: assistant, toolResults });
111
+ }
112
+ return {
113
+ status: "max_turns",
114
+ messages: snapshotMessages(state.messages),
115
+ turnCount: state.turnCount,
116
+ };
117
+ }
118
+ async requestAssistantMessage(state, turn, signal) {
119
+ const request = {
120
+ systemPrompt: this.systemPrompt,
121
+ messages: snapshotMessages(state.messages),
122
+ tools: this.toolDefinitions,
123
+ ...(state.continuation === undefined
124
+ ? {}
125
+ : { continuation: state.continuation }),
126
+ ...(signal === undefined ? {} : { signal }),
127
+ };
128
+ if (this.streaming && this.llm.stream !== undefined) {
129
+ return this.llm.stream(request, {
130
+ onTextDelta: (delta) => {
131
+ if (delta.length > 0) {
132
+ this.emit({ type: "message_update", turn, delta });
133
+ }
134
+ },
135
+ });
136
+ }
137
+ return this.llm.complete(request);
138
+ }
139
+ assertUniqueToolCallIds(toolCalls, seenIds) {
140
+ for (const toolCall of toolCalls) {
141
+ if (seenIds.has(toolCall.id)) {
142
+ throw new AgentProtocolError(`Provider returned a duplicate tool call id: ${toolCall.id}`);
143
+ }
144
+ seenIds.add(toolCall.id);
145
+ }
146
+ }
147
+ async executeToolCall(toolCall, turn, signal) {
148
+ this.emit({
149
+ type: "tool_execution_start",
150
+ turn,
151
+ toolCallId: toolCall.id,
152
+ toolName: toolCall.name,
153
+ arguments: toolCall.arguments,
154
+ });
155
+ const tool = this.registry.get(toolCall.name);
156
+ let output;
157
+ if (tool === undefined) {
158
+ output = {
159
+ content: `Unknown tool: ${toolCall.name}`,
160
+ isError: true,
161
+ };
162
+ }
163
+ else {
164
+ try {
165
+ output = validateToolOutput(await tool.execute(toolCall.arguments, {
166
+ workspace: this.workspace,
167
+ ...(signal === undefined ? {} : { signal }),
168
+ }));
169
+ }
170
+ catch (error) {
171
+ if (isAbortError(error)) {
172
+ throw error;
173
+ }
174
+ output = {
175
+ content: `Tool execution failed: ${toolCall.name}`,
176
+ isError: true,
177
+ };
178
+ }
179
+ }
180
+ this.emit({
181
+ type: "tool_execution_end",
182
+ turn,
183
+ toolCallId: toolCall.id,
184
+ toolName: toolCall.name,
185
+ content: output.content,
186
+ isError: output.isError,
187
+ });
188
+ return {
189
+ role: "tool",
190
+ toolCallId: toolCall.id,
191
+ toolName: toolCall.name,
192
+ content: output.content,
193
+ isError: output.isError,
194
+ };
195
+ }
196
+ emit(event) {
197
+ emitEvent(this.onEvent, event);
198
+ }
199
+ emitEnd(status, state, error) {
200
+ this.emit({
201
+ type: "agent_end",
202
+ status,
203
+ turnCount: state.turnCount,
204
+ messages: snapshotMessages(state.messages),
205
+ ...(error === undefined ? {} : { error }),
206
+ });
207
+ }
208
+ }
209
+ /** Kept as a call so narrowing never hides a signal that aborts mid-run. */
210
+ function isAborted(signal) {
211
+ return signal !== undefined && signal.aborted;
212
+ }
213
+ function abortedResult(state) {
214
+ return {
215
+ status: "aborted",
216
+ messages: snapshotMessages(state.messages),
217
+ turnCount: state.turnCount,
218
+ };
219
+ }
220
+ function createToolRegistry(tools) {
221
+ const registry = new Map();
222
+ for (const tool of tools) {
223
+ if (tool.name.length === 0) {
224
+ throw new TypeError("Tool names must not be empty.");
225
+ }
226
+ if (registry.has(tool.name)) {
227
+ throw new TypeError(`Duplicate tool name: ${tool.name}`);
228
+ }
229
+ registry.set(tool.name, tool);
230
+ }
231
+ return registry;
232
+ }
233
+ function validateAndCloneAssistantMessage(value) {
234
+ if (typeof value !== "object" || value === null) {
235
+ throw new AgentProtocolError("Provider returned an invalid assistant message.");
236
+ }
237
+ const candidate = value;
238
+ if (candidate.role !== "assistant" ||
239
+ typeof candidate.content !== "string" ||
240
+ !Array.isArray(candidate.toolCalls)) {
241
+ throw new AgentProtocolError("Provider returned an invalid assistant message.");
242
+ }
243
+ const toolCalls = candidate.toolCalls.map((toolCall) => {
244
+ if (typeof toolCall !== "object" ||
245
+ toolCall === null ||
246
+ typeof toolCall.id !== "string" ||
247
+ toolCall.id.length === 0 ||
248
+ typeof toolCall.name !== "string" ||
249
+ toolCall.name.length === 0) {
250
+ throw new AgentProtocolError("Provider returned an invalid tool call.");
251
+ }
252
+ return {
253
+ id: toolCall.id,
254
+ name: toolCall.name,
255
+ arguments: structuredClone(toolCall.arguments),
256
+ };
257
+ });
258
+ if (candidate.content.length === 0 && toolCalls.length === 0) {
259
+ throw new AgentProtocolError("Provider returned an assistant message with no text or tool calls.");
260
+ }
261
+ return {
262
+ role: "assistant",
263
+ content: candidate.content,
264
+ toolCalls,
265
+ };
266
+ }
267
+ function validateToolOutput(value) {
268
+ if (typeof value !== "object" ||
269
+ value === null ||
270
+ !("content" in value) ||
271
+ !("isError" in value) ||
272
+ typeof value.content !== "string" ||
273
+ typeof value.isError !== "boolean") {
274
+ throw new TypeError("Tool returned an invalid output.");
275
+ }
276
+ return { content: value.content, isError: value.isError };
277
+ }
278
+ function snapshotMessages(messages) {
279
+ return structuredClone(messages);
280
+ }
281
+ //# sourceMappingURL=agent.js.map