Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 @@
---
---
17 changes: 9 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@

## Features

- :zap: **Functions all the way down** — `agent`, `tool`, `workflow` are functions returning plain objects.
- :zap: **Functions all the way down** — `agent`, `tool`, `flowAgent` are functions returning plain objects.
- :jigsaw: **Composition over configuration** — Combine small pieces instead of configuring large ones.
- :shield: **Result, never throw** — Every public method returns `Result<T>`.
- :lock: **Closures are state** — Workflow state is just variables in your handler.
- :lock: **Closures are state** — Flow agent state is just variables in your handler.
- :triangular_ruler: **Type-driven design** — Zod schemas, discriminated unions, exhaustive matching.

## Install
Expand All @@ -28,16 +28,16 @@ npm install @funkai/agents @funkai/prompts

```ts
import { agent } from "@funkai/agents";
import { prompts } from "~prompts";
import { openai } from "@ai-sdk/openai";

const writer = agent({
name: "writer",
model: "openai/gpt-4.1",
system: prompts("writer"),
model: openai("gpt-4.1"),
system: "You are a helpful writer.",
tools: { search },
});

const result = await writer.generate("Write about closures");
const result = await writer.generate({ prompt: "Write about closures" });
```

### Define a prompt
Expand All @@ -54,14 +54,15 @@ You are a {{ tone }} writer.
### Generate typed prompts

```bash
npx funkai prompts generate --out .prompts/client --roots src/agents
npx funkai prompts generate --out .prompts/client --includes "src/agents/**"
```

## Packages

| Package | Description |
| ------------------------------------- | -------------------------------------------------------------------- |
| [`@funkai/agents`](packages/agents) | Lightweight agent, tool, and workflow orchestration |
| [`@funkai/agents`](packages/agents) | Lightweight agent, tool, and flow agent orchestration |
| [`@funkai/models`](packages/models) | Model catalog, provider resolution, and cost calculations |
| [`@funkai/prompts`](packages/prompts) | Prompt SDK with LiquidJS templating, Zod validation, and CLI codegen |
| [`@funkai/cli`](packages/cli) | CLI for the funkai prompt SDK |

Expand Down
4 changes: 2 additions & 2 deletions contributing/concepts/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ The models package provides the model catalog, provider resolution, and cost cal
| Module | Purpose |
| -------- | -------------------------------------------------------------------- |
| Catalog | Model definitions with pricing data, lookup by ID, filtered queries |
| Provider | OpenRouter integration, `createModelResolver()` for multi-provider |
| Provider | Provider registry, `createProviderRegistry()` for multi-provider |
| Cost | `calculateCost()` to compute dollar costs from token usage + pricing |

### Generated Data
Expand Down Expand Up @@ -142,7 +142,7 @@ The prompts package provides a prompt authoring SDK with two surfaces:
4. **Type-driven** -- Discriminated unions, branded types, exhaustive matching via ts-pattern
5. **Zod at boundaries** -- Runtime validation for configs, user input, and external data
6. **Vercel AI SDK foundation** -- Built on `ai` package for model interaction, tool calling, and streaming
7. **Multi-provider support** -- Model resolution via `createModelResolver()` with OpenRouter as default fallback
7. **Multi-provider support** -- Model resolution via `createProviderRegistry()` with configurable provider mappings
8. **Composition over inheritance** -- Small, focused interfaces composed together

## Package Conventions
Expand Down
2 changes: 1 addition & 1 deletion contributing/guides/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ Read the project docs in this order:
2. [`contributing/concepts/architecture.md`](../concepts/architecture.md) -- package ecosystem, design principles, data flow
3. [`contributing/concepts/tech-stack.md`](../concepts/tech-stack.md) -- tools, libraries, and design rationale
4. Relevant standards in `contributing/standards/` as needed
5. Package docs: [`@funkai/agents`](/agents/) and [`@funkai/prompts`](/prompts/)
5. Package docs: [`@funkai/agents`](/concepts/agents) and [`@funkai/prompts`](/concepts/prompts)

### 6. Set up Claude Code (optional)

Expand Down
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.
134 changes: 134 additions & 0 deletions docs/concepts/agents.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# Agents

`agent()` creates a single LLM boundary — one model, an optional tool loop, typed I/O, and `Result`-based returns. It wraps the AI SDK's `generateText`/`streamText` without hiding them.

Every call to `.generate()` or `.stream()` returns `Result<T>`. Check `.ok` before accessing values — there is no try/catch.

## Two modes

**Simple** — no input schema. Pass a prompt object at call time.

```typescript
import { agent } from "@funkai/agents";
import { openai } from "@ai-sdk/openai";

const assistant = agent({
name: "assistant",
model: openai("gpt-4.1"),
system: "You are a helpful assistant.",
});

const result = await assistant.generate({ prompt: "What is TypeScript?" });

if (!result.ok) {
console.error(result.error.message);
process.exit(1);
}

console.log(result.output); // string
```

**Typed** — declare `input` (Zod schema) and `prompt` (render function) together. Call time is fully type-checked.

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

const summarizer = agent({
name: "summarizer",
model: openai("gpt-4.1"),
input: z.object({ text: z.string(), maxWords: z.number() }),
prompt: ({ input }) => `Summarize the following in ${input.maxWords} words:\n\n${input.text}`,
});

const result = await summarizer.generate({ input: { text: "Long article...", maxWords: 50 } });

if (result.ok) {
console.log(result.output);
}
```

`input` and `prompt` must be provided together — one without the other is a type error.

## Streaming

Use `.stream()` instead of `.generate()`. Consume `result.fullStream` for incremental output; `result.output` resolves after the stream ends.

```typescript
const result = await assistant.stream({ prompt: "Tell me a story." });

if (result.ok) {
for await (const part of result.fullStream) {
if (part.type === "text-delta") {
process.stdout.write(part.textDelta);
}
}
const final = await result.output;
}
```

## Tools and subagents

Pass a `tools` record for function calling. Pass an `agents` record to expose other agents as callable tools — abort signals propagate automatically.

```typescript
const analyst = agent({
name: "analyst",
model: openai("gpt-4.1"),
system: "You analyze data. Delegate searches to the searcher.",
tools: { calculator },
agents: { searcher: searchAgent },
});
```

## Output strategies

The `output` field controls the return type of `result.output`:

| Strategy | Result type | Description |
| ---------------------------- | ----------- | -------------------------------------------------------- |
| `Output.text()` | `string` | Plain text (default) |
| `Output.object({ schema })` | `T` | Validated structured object matching the Zod schema |
| `Output.array({ element })` | `T[]` | Validated array of objects matching the element schema |
| `Output.choice({ options })` | `string` | One of the provided string options (enum/classification) |
| `z.object({ ... })` | `T` | Shorthand — auto-wrapped as `Output.object()` |
| `z.array(z.object({ ... }))` | `T[]` | Shorthand — auto-wrapped as `Output.array()` |

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

const classifier = agent({
name: "classifier",
model: openai("gpt-4.1"),
output: Output.object({
schema: z.object({
category: z.enum(["bug", "feature", "question"]),
confidence: z.number(),
}),
}),
});

const result = await classifier.generate({ prompt: "App crashes on login" });
if (result.ok) {
console.log(result.output.category); // "bug"
console.log(result.output.confidence); // 0.95
}
```

## Hooks

`onStart`, `onFinish`, `onError`, and `onStepFinish` fire at lifecycle points. Set them on the config or pass them per-call as overrides.

## When to use `agent()` vs `flowAgent()`

Use `agent()` when a single model call (with optional tool iterations) is sufficient — question answering, classification, summarization, or single-turn tool use. Use `flowAgent()` when you need to coordinate multiple agents, run parallel work, or implement custom control flow with traced steps. See [Flow Agents](/concepts/flow-agents) for details.

## References

- [`agent()` reference](/reference/agents/agent)
- [Streaming guide](/guides/streaming)
- [Tools](/concepts/tools)
- [Flow Agents](/concepts/flow-agents)
99 changes: 99 additions & 0 deletions docs/concepts/flow-agents.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Flow Agents

`flowAgent()` creates a multi-step agent whose logic is plain imperative TypeScript. There are no step arrays or definition objects — you write a handler function and use `$` for tracked operations.

Flow agents always require a typed `input` Zod schema, validated on entry. The `output` schema is optional — when provided, the handler's return value is validated against it before being returned to the caller. When omitted, the handler returns `void` and the collected text from sub-agent responses becomes a `string` output.

## Basic example

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

const writer = agent({
name: "writer",
model: openai("gpt-4.1"),
input: z.object({ topic: z.string() }),
prompt: ({ input }) => `Write a short paragraph about: ${input.topic}`,
});

const pipeline = flowAgent(
{
name: "write-and-review",
input: z.object({ topic: z.string() }),
output: z.object({ text: z.string() }),
},
async ({ input, $ }) => {
// $.step — tracked unit of synchronous or async work
const slug = await $.step({
id: "slugify",
execute: async () => input.topic.toLowerCase().replace(/\s+/g, "-"),
});

// $.agent — tracked agent call, returns StepResult<GenerateResult>
const draft = await $.agent({
id: "write-draft",
agent: writer,
input: { topic: input.topic },
});

if (!draft.ok) {
return { text: "Generation failed." };
}

return { text: draft.value.output };
},
);

const result = await pipeline.generate({ input: { topic: "pattern matching" } });

if (result.ok) {
console.log(result.output.text);
console.log("Duration:", result.duration, "ms");
console.log("Trace:", result.trace); // full execution tree
}
```

## The $ step builder

`$` provides operations that are tracked in the execution trace. All return `Promise<StepResult<T>>` — check `.ok` before using `.value`.

| Operation | Description |
| ---------- | ------------------------------------------------------ |
| `$.step` | Single unit of work |
| `$.agent` | Call an `agent()` as a tracked step |
| `$.map` | Map over an array with optional concurrency |
| `$.each` | Iterate an array sequentially |
| `$.reduce` | Reduce an array to a single value |
| `$.while` | Loop while a condition holds |
| `$.all` | Run multiple operations in parallel (all must succeed) |
| `$.race` | Run multiple operations in parallel (first one wins) |

State lives in plain variables — use closures. There is no shared state object.

## Trace and usage

`result.trace` is a readonly tree of every `$` operation: its id, type, duration, and nested children. `result.usage` aggregates token counts from all `$.agent` calls in the flow.

## Streaming step progress

`.stream()` emits `StepEvent` objects (`step:start`, `step:finish`, `step:error`, `flow:finish`) as each `$` operation runs. Use this to push real-time progress to a UI.

```typescript
const result = await pipeline.stream({ input: { topic: "closures" } });

if (result.ok) {
for await (const event of result.fullStream) {
if (event.type === "step:finish") {
console.log(event.step.id, "done in", event.duration, "ms");
}
}
}
```

## References

- [`flowAgent()` reference](/reference/agents/flow-agent)
- [Multi-Agent Orchestration guide](/guides/multi-agent)
- [Agents](/concepts/agents)
Loading