npm package: @s1ahs3r/wiener
Description: A tasteful, minimal Agent SDK
Latest version: 0.1.0

<p align="center">
  <h1 align="center">Wiener</h1>
  <p align="center">
    <strong>A tasteful, minimal Agent SDK for TypeScript.</strong>
  </p>
  <p align="center">
    6 exports &nbsp;·&nbsp; ~900 lines &nbsp;·&nbsp; zero dependencies beyond Zod
  </p>
</p>

---

## 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 memory
```

That's the entire public API.

## Install

```bash
pnpm add wiener
```

## Quick Start

```typescript
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:

```typescript
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:

```typescript
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[]`:

```typescript
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:

```typescript
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:

```typescript
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:

```typescript
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:

```typescript
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.

```typescript
// 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 provider
```

## Design 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 `Provider` interface 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