Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
12dbbde
docs(agents): restructure site docs and remove workflow references
zrosenbauer Mar 18, 2026
0df0dd6
Merge branch 'main' into docs/restructure-site
zrosenbauer Mar 23, 2026
570d947
chore: update @zpress/kit to 0.2.8 and add empty changeset
zrosenbauer Mar 23, 2026
5067d02
style: format docs and zpress config with oxfmt
zrosenbauer Mar 23, 2026
e691232
fix(docs): address PR review feedback
zrosenbauer Mar 23, 2026
5697e90
fix: use zpress diff for Vercel ignore command
zrosenbauer Mar 23, 2026
2c5aa6e
fix(docs): migrate zpress config to v0.4 schema and fix vercel ignore
zrosenbauer Mar 23, 2026
7379df3
chore: remove .zpress-bugs.md
zrosenbauer Mar 23, 2026
1546e16
fix(docs): pin @zpress/kit to 0.2.4 and revert config to v0.3 schema
zrosenbauer Mar 23, 2026
43c0875
feat(docs): upgrade @zpress/kit to 0.2.8 and migrate config to v0.4 s…
zrosenbauer Mar 23, 2026
5c9dd64
chore(docs): upgrade @zpress/kit to 0.2.9
zrosenbauer Mar 23, 2026
a96e373
fix(docs): use zpress diff for Vercel ignore command
zrosenbauer Mar 23, 2026
c2e140f
refactor(docs): restructure site config for better UX
zrosenbauer Mar 23, 2026
ade58dc
feat(docs): add workspaces, hero actions, and mdi icons
zrosenbauer Mar 23, 2026
2224f43
refactor(docs): restructure site into packages, reference, and guides
zrosenbauer Mar 23, 2026
42df8ab
refactor(docs): flatten site structure — packages own their docs
zrosenbauer Mar 23, 2026
30b0bdf
docs: fix outdated code examples across all packages
zrosenbauer Mar 23, 2026
7255b4e
docs: restructure site — flatten docs, add Getting Started, integrate…
zrosenbauer Mar 23, 2026
b94ed4e
docs: overhaul feature cards and fold CLI into Prompts nav
zrosenbauer Mar 23, 2026
7501d95
docs: trim feature cards to 3 — functions, single API, prompts
zrosenbauer Mar 23, 2026
90a844f
deps: upgrade @zpress/kit to 0.2.12
zrosenbauer Mar 23, 2026
63cb0d4
docs: restructure site nav, fix code examples, and format
zrosenbauer Mar 24, 2026
89bb4be
docs: fix API inaccuracies across all documentation
zrosenbauer Mar 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changeset/tough-planes-lie.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
78 changes: 78 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Architecture

## Package dependency graph

```mermaid
graph TD
agents["@funkai/agents"]
models["@funkai/models"]
prompts["@funkai/prompts"]
cli["@funkai/cli"]
ai["ai (Vercel AI SDK)"]

agents --> models
agents --> ai
cli --> prompts
models --> ai
```

- **`@funkai/agents`** depends on `@funkai/models` (for `ProviderRegistry` type) and `ai` (Vercel AI SDK).
- **`@funkai/models`** depends on `ai` for the `LanguageModel` type. Standalone otherwise.
- **`@funkai/prompts`** is fully standalone -- no dependency on other funkai packages.
- **`@funkai/cli`** depends on `@funkai/prompts` for prompt generation and linting.

## Agent

`agent()` wraps the AI SDK's `generateText` and `streamText` with:

- **Typed input** -- Optional Zod schema + prompt template. When provided, `.generate()` accepts typed input and validates it before calling the model. In simple mode, raw strings or message arrays pass through directly.
- **Tools** -- A record of `tool()` instances exposed to the model for function calling.
- **Subagents** -- Other agents passed via the `agents` config, automatically wrapped as callable tools with abort signal propagation.
- **Hooks** -- `onStart`, `onFinish`, `onError`, `onStepFinish` for observability. Per-call overrides merge with base hooks.
- **Result** -- Every method returns `Result<T>`. Success fields are flat on the object. Errors carry `code`, `message`, and optional `cause`.

The tool loop runs up to `maxSteps` iterations (default 20), where each iteration may invoke tools or subagents before producing a final response.

## FlowAgent

`flowAgent()` provides code-driven orchestration. The handler receives `{ input, $, log }`:

- **`input`** -- Validated against the input Zod schema.
- **`$`** (StepBuilder) -- Traced operations: `$.step()`, `$.agent()`, `$.map()`, `$.each()`, `$.reduce()`, `$.while()`, `$.all()`, `$.race()`. Each call becomes an entry in the execution trace.
- **`log`** -- Scoped logger with contextual bindings.

The handler returns the output value, validated against the optional output Zod schema. Each `$` operation is modeled as a synthetic tool call in the message history, making flow agents compatible with the same `GenerateResult` and `StreamResult` types as regular agents.

FlowAgent results include additional fields: `trace` (array of step entries) and `duration` (wall-clock milliseconds).

## The Runnable interface

Both `Agent` and `FlowAgent` satisfy the `Runnable` interface:

```typescript
interface Runnable<TInput, TOutput> {
generate(input: TInput, config?): Promise<Result<{ output: TOutput }>>;
stream(input: TInput, config?): Promise<Result<{ output: Promise<TOutput>; fullStream }>>;
fn(): (input: TInput, config?) => Promise<Result<{ output: TOutput }>>;
}
```

This enables nesting: a `FlowAgent` can call any `Agent` or `FlowAgent` via `$.agent()`. An `Agent` can delegate to subagents via the `agents` config. The framework uses a symbol-keyed metadata property (`RUNNABLE_META`) to extract the name and input schema when wrapping runnables as tools.

## @funkai/models

Provides three capabilities:

- **Model catalog** -- `model(id)` and `models()` for querying model definitions (capabilities, modalities, pricing) sourced from OpenRouter. The catalog is generated at build time via `pnpm --filter=@funkai/models generate:models`.
- **Provider registry** -- `createProviderRegistry()` maps provider names to AI SDK provider instances, enabling string-based model resolution (e.g., `'openai/gpt-4.1'`).
- **Cost calculation** -- `calculateCost()` computes dollar costs from token usage and model pricing data.

## @funkai/prompts

Build-time prompt templating:

- Prompts are defined as `.prompt` files with YAML frontmatter (metadata, Zod schema reference) and LiquidJS template bodies.
- `createPromptRegistry()` loads compiled prompt modules and provides type-safe rendering at runtime.
- The `@funkai/cli` package provides commands for creating, generating, and linting prompt files.

Prompts are independent of the agents package -- they can be used standalone or integrated into agent prompt functions.
74 changes: 74 additions & 0 deletions docs/introduction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Introduction

funkai is a composable, functional TypeScript microframework for AI agent orchestration. It is built on the [Vercel AI SDK](https://ai-sdk.dev) -- not a replacement, but a composition layer on top of it.

## The problem

The AI SDK gives you powerful primitives like `generateText` and `streamText`. But when you start building real applications, you need more: typed agents with validated I/O, multi-step orchestration with observable traces, consistent error handling that does not rely on try/catch, and a model catalog for cost tracking. funkai adds all of this without introducing classes or hidden state.

## Two core primitives

funkai provides two agent primitives that share the same `Runnable` interface:

- **`agent()`** -- A single LLM boundary with a tool loop. Wraps `generateText`/`streamText` with typed input, tools, subagents, hooks, and `Result`-based error handling. Use this when a single model call (with optional tool iterations) is sufficient.

- **`flowAgent()`** -- Multi-step, code-driven orchestration. Your handler function receives `{ input, $, log }` where `$` is the StepBuilder providing traced operations like `$.step()`, `$.agent()`, `$.map()`, and `$.reduce()`. Use this when you need to coordinate multiple agents, run parallel work, or implement custom control flow.

Both return `Result<T>` from every public method -- a discriminated union you pattern-match on `ok` instead of catching exceptions.

## Packages

| Package | Name | Description |
| ----------------- | ------- | ----------------------------------------------------------------------------- |
| `@funkai/agents` | Agents | Agent orchestration -- `agent()`, `flowAgent()`, `tool()`, `Result` utilities |
| `@funkai/models` | Models | Model catalog, provider registry, and cost calculation |
| `@funkai/prompts` | Prompts | Build-time prompt templating with LiquidJS and Zod validation |
| `@funkai/cli` | CLI | Command-line tooling for prompt generation, linting, and setup |

## Design at a glance

```typescript
import { agent, flowAgent, tool } from "@funkai/agents";
import { openai } from "@ai-sdk/openai";
import { z } from "zod";

// Single LLM boundary
const writer = agent({
name: "writer",
model: openai("gpt-4.1"),
system: "You write concise technical docs.",
});

// Multi-step orchestration
const pipeline = flowAgent(
{
name: "pipeline",
input: z.object({ topics: z.array(z.string()) }),
output: z.object({ docs: z.array(z.string()) }),
},
async ({ input, $ }) => {
const docs = await $.map({
id: "write-docs",
input: input.topics,
execute: async ({ item, $ }) => {
const result = await $.agent({ id: "write", agent: writer, input: item });
return result.ok ? result.value.output : "";
},
concurrency: 3,
});
return { docs: docs.ok ? docs.value : [] };
},
);

// Both satisfy Runnable -- same .generate(), .stream(), .fn()
const result = await pipeline.generate({ topics: ["TypeScript", "Zod"] });
if (result.ok) {
console.log(result.output);
}
```

## Next steps

- [Quick Start](./quick-start.md) -- Install and build your first agent in minutes.
- [Principles](./principles.md) -- The design philosophy behind funkai.
- [Architecture](./architecture.md) -- How the packages fit together.
136 changes: 136 additions & 0 deletions docs/principles.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# Principles

## Functions all the way down

`agent()`, `flowAgent()`, and `tool()` are factory functions that return plain objects. There are no classes, no `new`, no `this`. The returned objects expose methods like `.generate()` and `.stream()`, but they are closures over configuration -- not class instances.

```typescript
const myAgent = agent({
name: "helper",
model: openai("gpt-4.1"),
system: "You are helpful.",
});

// myAgent is a plain object: { generate, stream, fn }
const generate = myAgent.fn();
const result = await generate("Hello");
```

## Result, never throw

Every public method returns `Result<T>` -- a discriminated union. Pattern-match on `ok` instead of wrapping calls in try/catch. Success fields are flat on the object alongside `ok: true`. Failure carries a structured `error` with `code`, `message`, and optional `cause`.

```typescript
const result = await myAgent.generate("Hello");

if (!result.ok) {
// result.error.code: 'VALIDATION_ERROR' | 'AGENT_ERROR' | ...
console.error(result.error.code, result.error.message);
return;
}

// Success -- fields are flat
console.log(result.output);
console.log(result.usage.totalTokens);
```

## Closures are state

Flow agent state is just variables in your handler function. There is no state machine, no reducer, no context object to thread through. Your handler is an async function -- use `let`, loops, and conditionals as you normally would.

```typescript
const counter = flowAgent(
{
name: "retry-loop",
input: z.object({ prompt: z.string() }),
output: z.object({ answer: z.string() }),
},
async ({ input, $ }) => {
let attempts = 0;
let answer = "";

while (attempts < 3 && answer.length === 0) {
const result = await $.agent({
id: `attempt-${attempts}`,
agent: writer,
input: input.prompt,
});
if (result.ok) {
answer = result.value.output;
}
attempts += 1;
}

return { answer };
},
);
```

## Composition over configuration

Small functions composed together, not large option bags. An agent with tools is just an agent config with a `tools` record. A flow agent that calls agents is just a handler that calls `$.agent()`. Subagents are agents passed in the `agents` config -- automatically wrapped as tools.

```typescript
const researcher = agent({ name: "researcher", model, system: "..." });
const writer = agent({ name: "writer", model, system: "..." });

// Subagents as tools -- the orchestrator delegates via LLM decisions
const orchestrator = agent({
name: "orchestrator",
model,
system: "Coordinate research and writing.",
agents: { researcher, writer },
});
```

## Zero hidden state

There are no singletons, no module-level registries, no global configuration. Every agent carries its own config. Loggers are passed explicitly. Provider registries are created and injected, not imported from a shared module.

```typescript
// Each agent is self-contained
const a = agent({ name: "a", model: openai("gpt-4.1"), system: "..." });
const b = agent({ name: "b", model: openai("gpt-4.1-mini"), system: "..." });
// No shared state between a and b
```

## `$` is optional sugar

The StepBuilder (`$`) provides observability -- every `$.step()`, `$.agent()`, `$.map()` call becomes an entry in the execution trace. But it is not required. You can call agents directly, use plain loops, or mix traced and untraced operations. The trace just will not include untraced work.

```typescript
const pipeline = flowAgent(
{
name: "mixed",
input: z.object({ text: z.string() }),
output: z.object({ result: z.string() }),
},
async ({ input, $ }) => {
// Traced -- appears in result.trace
const analysis = await $.agent({ id: "analyze", agent: analyzer, input: input.text });

// Untraced -- plain function call, not in trace
const cleaned = cleanText(analysis.ok ? analysis.value.output : input.text);

// Traced again
const final = await $.step({ id: "format", execute: () => formatOutput(cleaned) });

return { result: final.ok ? final.value : cleaned };
},
);
```

## Agent vs FlowAgent

Both satisfy the `Runnable` interface -- same `.generate()`, `.stream()`, `.fn()`. The difference is internal:

| | `agent()` | `flowAgent()` |
| ------------------------- | -------------------------------------------------- | -------------------------------------------------------- |
| **What drives execution** | LLM tool loop | Your handler code |
| **When to use** | Single model call, optionally with tools/subagents | Multi-step orchestration, conditional logic, parallelism |
| **State** | Managed by AI SDK | Variables in your handler closure |
| **Trace** | Tool-loop steps via hooks | `$` step builder entries |
| **Model required** | Yes | No (sub-agents provide their own models) |
| **Nesting** | Can delegate to subagents | Can call `$.agent()` with any `Agent` or `FlowAgent` |

Use `agent()` when a single LLM boundary is enough. Use `flowAgent()` when you need to coordinate multiple agents, implement retry logic, run parallel work, or apply custom control flow around LLM calls.
Loading