@s1ahs3r/wiener
v0.1.0
Published
A tasteful, minimal Agent SDK
Readme
Why Wiener?
Most agent frameworks grow into sprawling platforms. Wiener goes the other way — the smallest useful abstraction over the LLM agent loop.
agent() → create an agent skill() → composable presets
tool() → define a tool fromMcp() → adapt MCP tools
z → Zod (re-export) createMemory → conversation memoryThat's the entire public API.
Install
pnpm add wienerQuick Start
import { agent, tool, z } from "wiener";
import { openai } from "wiener/providers/openai";
// Define a tool — Zod schema in, typed args out
const calculate = tool("calculate", "Do math", {
a: z.number(),
b: z.number(),
op: z.enum(["+", "-", "*", "/"]),
}, async ({ a, b, op }) => {
const ops = { "+": a + b, "-": a - b, "*": a * b, "/": a / b };
return { content: String(ops[op]) };
});
// Create an agent
const a = agent({
provider: openai({ apiKey: "sk-...", baseURL: "https://api.openai.com" }),
model: "gpt-4o",
tools: [calculate],
});
// Run it
const result = await a.run("What is 42 * 17?");
console.log(result.text); // "42 × 17 = 714"
console.log(result.usage); // { inputTokens: 320, outputTokens: 48 }Streaming
Every agent exposes an AsyncGenerator that yields typed events:
for await (const event of a.stream("What is 42 * 17?")) {
switch (event.type) {
case "message:delta": process.stdout.write(event.delta); break;
case "tool:start": console.log(`→ ${event.name}`); break;
case "tool:end": console.log(`✓ ${event.result.content}`); break;
}
}Event types: turn:start · message:start · message:delta · message:end · tool:start · tool:end · turn:end · compaction · error · done
Skills — Composable Presets
A skill bundles a system prompt + tools into a reusable unit:
import { agent, skill, tool, z } from "wiener";
const coder = skill("coder", "A coding assistant", {
system: "You are an expert TypeScript developer.",
tools: [readFile, writeFile, bash],
});
const reviewer = skill("reviewer", "A code reviewer", {
system: "Review code for bugs and style issues.",
tools: [readFile],
});
// Compose skills — system prompts merge, tools deduplicate
const a = agent({
provider, model,
skills: [coder, reviewer],
});MCP Integration
Bring your own MCP client. Wiener converts its tools to ToolDefinition[]:
import { agent, fromMcp } from "wiener";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
const mcpClient = new Client({ name: "my-app", version: "1.0" });
await mcpClient.connect(transport);
const tools = await fromMcp(mcpClient);
const a = agent({ provider, model, tools });Memory
Conversation history persists across runs:
import { agent, createMemory } from "wiener";
// In-memory (default)
const mem = createMemory();
// File-based
const mem = createMemory({ type: "file", path: ".wiener/memory.json" });
const a = agent({ provider, model, memory: mem });
await a.run("My name is Alice.");
await a.run("What's my name?"); // "Your name is Alice."Hooks — The Harness
Hooks give you control over the agent loop without subclassing anything:
const a = agent({
provider, model, tools,
hooks: {
// Gate tool calls
beforeToolCall: async (name, args) => {
if (name === "rm_rf") return "deny";
},
// Transform tool results
afterToolCall: async (name, result) => {
return { result: { ...result, content: result.content + " [audited]" } };
},
// Harness control — the most powerful hook
afterTurn: async (ctx) => {
// Force self-verification when the model stops without tool calls
if (!ctx.hasToolCalls && !ctx.text.includes("VERIFIED")) {
return { continue: "Verify your answer. Say VERIFIED when done." };
}
// Force stop if something looks wrong
if (dangerDetected(ctx.messages)) return "stop";
},
},
});| Hook | When | Can return |
|------|------|------------|
| beforeTurn | Before each turn | void · "skip" |
| afterTurn | After each turn | void · "stop" · "continue" · { continue: "message" } |
| beforeToolCall | Before tool execution | void · "deny" · { args } |
| afterToolCall | After tool execution | void · { result } |
Multi-Modal Tool Results
Tools can return rich content — text, images, or structured JSON:
const screenshot = tool("screenshot", "Take a screenshot", {
url: z.string(),
}, async ({ url }) => {
const buffer = await captureScreenshot(url);
return {
content: [
{ type: "image", data: buffer.toString("base64"), mimeType: "image/png" },
{ type: "text", text: `Screenshot of ${url}` },
],
};
});Context Compaction
For long-running agents, auto-summarize when context grows too large:
const a = agent({
provider, model, tools,
compaction: {
maxChars: 50_000,
// Optional: custom compaction strategy
compact: async (messages, provider, model) => {
// your logic — return a shorter message array
},
},
});Providers
Wiener ships two providers. Both implement Provider — an async generator interface you can also implement yourself.
// OpenAI-compatible (works with OpenAI, Azure, LongCat, OpenRouter, etc.)
import { openai } from "wiener/providers/openai";
const provider = openai({ apiKey: "...", baseURL: "https://api.openai.com" });
// Anthropic
import { anthropic } from "wiener/providers/anthropic";
const provider = anthropic({ apiKey: "..." });
// Custom — just implement the interface
const custom: Provider = {
async *chat(messages, options) {
yield { type: "content_start" };
yield { type: "content_delta", delta: "Hello!" };
yield { type: "content_end" };
yield { type: "done", message: { role: "assistant", content: [{ type: "text", text: "Hello!" }] } };
},
};Architecture
src/
index.ts 50 lines Public API — 6 exports
agent.ts 73 lines agent() factory → { run, stream, abort }
loop.ts 362 lines AsyncGenerator agent loop + compaction
tool.ts 61 lines tool() + Zod-validated executeTool()
skill.ts 81 lines skill() + resolveSkills()
mcp.ts 78 lines fromMcp() adapter
memory.ts 74 lines createMemory() — in-memory & file-based
types.ts 210 lines All type definitions
result.ts 12 lines Result<T, E> utility
providers/
openai.ts 332 lines OpenAI-compatible provider
anthropic.ts 227 lines Anthropic providerDesign Principles
- 6 exports solve 80% of use cases. If you need more, compose them.
- Generator-based loop. You own the iteration. Cancel, transform, or forward events however you want.
- No classes, pure functions. Configuration objects in, capabilities out.
- Provider-agnostic. The
Providerinterface is 4 lines. Adapt anything. - Result pattern internally. No surprise throws from tool execution.
- Hooks over inheritance. 4 precise interception points, not a class hierarchy.
License
MIT
