moongazer
A lightweight, framework-agnostic TypeScript library for building LLM agent loops with tool-use support.
English | Chinese
Overview
moongazer abstracts LLM streaming completions behind a ChatTransport interface, then provides an event-driven agent runtime on top. It does not tie to any specific model provider — you can use the built-in OpenAI adapter or write a custom adapter for any other provider.
Features
- Provider-agnostic — adapt any LLM provider via the
ChatTransportinterface - Type-safe tools — define tools with TypeBox schemas; the
executeargument type is inferred from the schema, and the model's JSON is validated at runtime (defaults applied, invalid args rejected) viaValue.Default+Value.Assert - Reasoning content — streams
reasoningdeltas from models that emitreasoning_content(e.g. OpenAI o1/o3) - Tool calls — native function calling with automatic reassembly of streaming tool-call deltas
- Lifecycle hooks — modify model requests, authorize and audit tool calls, rewrite results, and control continuation at agent, run, or tool scope
- Event-driven — the agent runtime exposes
AgentEventvia a subscriber pattern, making it easy to integrate with logging, storage, and UI - Abort support — safely abort an in-flight run while keeping content already received
- Minimal dependencies — only
@sinclair/typeboxas a runtime dependency (the OpenAI adapter defines TypeScript types only, noopenaipackage)
Installation
pnpm add @pulonia/moongazer
API Documentation
Quick Start
import { createAgent, createOpenAITransport, defineTool, Type } from "@pulonia/moongazer";
import type { OpenAIRawStream } from "@pulonia/moongazer";
// 1. Define a tool
const getWeather = defineTool({
name: "get_weather",
description: "Get weather for a city",
parameters: Type.Object({
city: Type.String(),
}),
execute: async ({ city }) => {
return `Weather in ${city}: sunny, 22°C`;
},
});
// 2. Create OpenAI transport
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();
// ... parse SSE chunks and yield OpenAIChatChunk objects
};
const transport = createOpenAITransport(rawStream);
// 3. Create agent and run
const agent = createAgent({ transport, tools: [getWeather] });
const handle = agent.run({
messages: [{ role: "user", content: "What is the weather in Beijing today?" }],
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);
});