@pulonia/moongazer
v0.6.0
Published
Provider-agnostic TypeScript agent runtime with typed tool calling and streaming events.
Readme
moongazer
轻量级、框架无关的 TypeScript 库,用于构建带工具调用 (tool-use) 能力的 LLM 代理循环。
English | Chinese
概述
moongazer 将 LLM 流式补全抽象为 ChatTransport 接口,并在此基础上提供事件驱动的代理运行时。它不绑定任何具体模型提供商——你可以直接使用内置的 OpenAI 适配器,或为其他提供商编写自定义适配器。
核心特性
- 提供者无关 — 通过
ChatTransport接口适配任何 LLM 提供商 - 类型安全工具 — 用 TypeBox schema 定义工具;
execute入参类型由 schema 推断,运行时通过Value.Default+Value.Assert对模型 JSON 填充默认值并严格校验(不合法参数会报错而非静默转换) - 推理内容 — 从支持
reasoning_content的模型(如 OpenAI o1/o3)中流式输出reasoning增量事件 - 工具调用 — 原生支持函数调用,自动拼接流式工具参数片段
- 生命周期 hooks — 在模型请求、工具授权/审计、结果改写和继续决策等边界插入 agent 级、run 级或工具级逻辑
- 事件驱动 — 代理运行时通过订阅者模式暴露
AgentEvent,便于日志、存储和 UI 集成 - 中止支持 — 安全地中止正在运行中的轮次,保留已收到的内容
- 最小依赖 — 仅
@sinclair/typebox一个运行时依赖(OpenAI 适配器只定义 TypeScript 类型,无openai包依赖)
安装
pnpm add @pulonia/moongazerAPI 文档
快速开始
import { createAgent, createOpenAITransport, defineTool, Type } from "@pulonia/moongazer";
import type { OpenAIRawStream } from "@pulonia/moongazer";
// 1. 定义一个工具
const getWeather = defineTool({
name: "get_weather",
description: "获取指定城市的天气",
parameters: Type.Object({
city: Type.String(),
}),
execute: async ({ city }) => {
return `Weather in ${city}: sunny, 22°C`;
},
});
// 2. 创建 OpenAI 传输层
const rawStream: OpenAIRawStream = async function* (request, signal) {
const response = await fetch("https://api.openai.com/v1/chat/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
},
body: JSON.stringify({ ...request, model: "gpt-4o", stream: true }),
signal,
});
const reader = response.body!.getReader();
// ... 解析 SSE 分片并 yield OpenAIChatChunk 对象
};
const transport = createOpenAITransport(rawStream);
// 3. 创建代理并运行
const agent = createAgent({ transport, tools: [getWeather] });
const handle = agent.run({
messages: [{ role: "user", content: "北京今天天气怎么样?" }],
hooks: {
beforeToolExecute: ({ tool }) => {
if (tool?.name === "get_weather" && !isLocationAllowed()) {
return { result: "<tool_error>weather access is not allowed</tool_error>" };
}
},
afterToolExecute: ({ result }) => ({ result: redact(result) }),
},
});
handle.subscribe((event) => {
if (event.type === "content") console.log(event.delta);
if (event.type === "reasoning") console.log(event.delta);
});