Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `createAgent()` agents can pause for approval (LOU-D21): a `needsApproval` tool no longer fails the run with "requires approval but no approvalStore". The run pauses (`finishReason: 'awaiting-approval'`) in a per-agent `InMemoryApprovalStore` (or the new `approvalStore` option), and `agent.approvals.list()` / `agent.approvals.resolve({ id, approved, note? })` continue it, in its session if it paused in one. The `approve` option decides calls in code without pausing.
- Tools own their contract (LOU-D22): `ToolDescriptor` gains optional `inputSchema` (zod) and `execute(args, ctx)`, which are now the canonical fields. `defineTool()` sets both and no longer calls `ai`'s `tool()`. Argument validation, the schema sent to the model, and tool execution read them first and fall back to `tool.parameters` / `tool.execute`. `ToolDescriptor.tool` (the `ai` v4 `Tool`) is now legacy: it is still built by `defineTool()` and still accepted on hand-written descriptors this release, but new code should set `inputSchema` and `execute`.

### Docs
- README revamped into a short front page (LOU-D52); the details it dropped moved to `docs/` (new pages: tools, approvals, providers, cli, flows, guardrails, utilities).

### Changed
- `AgentType` is optional and deprecated (LOU-D34, not breaking): `AgentBuilder.build()` no longer requires `setType()` and `AgentConfig.agentType` is now optional (`createAgent()` and `specToAgent()` agents carry no type). `AgentType`, `setType()` and the `agent-types` registry/validators are marked `@deprecated`: they have no runtime effect and will be removed in the next minor; they stay exported for now. Drop your `setType(...)` calls.
- BREAKING: `encrypt()` now generates a random salt per call instead of a hardcoded one. Ciphertext produced before this change cannot be decrypted with the new code and must be re-encrypted.
Expand Down
930 changes: 191 additions & 739 deletions README.md

Large diffs are not rendered by default.

48 changes: 42 additions & 6 deletions docs/api-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,35 @@ comment), build the TypeDoc site:
npm run docs:build # writes docs/api/index.html
```

How the pieces fit:

```text
┌─────────────────────────────────────┐
│ Your Application │
│ (React, Vue, Express, etc.) │
└──────────────┬──────────────────────┘
│
┌──────────────▼──────────────────────┐
│ @loushy/build-ai-agent │
│ ┌────────────────────────────┐ │
│ │ createAgent / Builder / │ │
│ │ Executor (approvals, │ │
│ │ checkpoints, tracing) │ │
│ ├────────────────────────────┤ │
│ │ Tools │ Delegation │ MCP │ │
│ │ Flows │ Guardrails │ Evals │ │
│ ├────────────────────────────┤ │
│ │ Core Engine │ │
│ └────────────────────────────┘ │
└──────────────┬──────────────────────┘
│
┌──────────────▼──────────────────────┐
│ Your LLM Provider & Deploy Target │
│ (OpenAI/Anthropic/Ollama/OpenRouter,│
│ Node server / Docker / Workers) │
└─────────────────────────────────────┘
```

## Building and running agents

| Export | Description |
Expand All @@ -18,6 +47,8 @@ npm run docs:build # writes docs/api/index.html
| `AgentType` | Deprecated, no runtime effect: agents need no type (removed next minor). |
| `resumeAfterApproval()` | Resume an execution paused for human approval. |
| `InMemoryApprovalStore` | Process-local `ApprovalStore`; the default store of `createAgent()` agents. |
| `StorageServiceApprovalStore`, `LocalStorageCheckpointStore` | File-backed approval and checkpoint stores over a `StorageService` (see [Approvals](./approvals.md), [Durable execution](./durable-execution.md)). |
| `SqliteStore` (from `/sqlite`) | Sessions, checkpoints and approvals in one SQLite file (see [Sessions](./sessions.md#choosing-a-store)). |
| `SessionAwaitingApprovalError` | Thrown by `execute()` when its `sessionId` is paused on an approval (see [Durable execution](./durable-execution.md)). |
| `createDelegateTool()` | Wrap a child agent as a tool for multi-agent delegation. |

Expand All @@ -41,7 +72,7 @@ resolves with the continued run's result (continuing the session it paused
in). Pauses are kept in a per-agent `InMemoryApprovalStore` unless you pass
`approvalStore` (e.g. `SqliteStore.approvals`); `approve: (call) => boolean`
decides each call in code without pausing (`stream()` still ends at the
pause). See [Human-in-the-loop approval gates](../README.md#human-in-the-loop-approval-gates).
pause). See [Approvals](./approvals.md).

### Skills

Expand Down Expand Up @@ -185,6 +216,8 @@ Guarantees, whatever the limit:
| `createMockProvider()`, `MockLLMProvider` | Deterministic mock provider for tests and demos. |
| `withRetry(provider, opts?)`, `withFallback(providers, opts?)` | Retry transient provider failures with backoff; fall back to the next provider. See [Configuration](configuration.md#provider-retries-and-fallback). |

See [Providers](./providers.md) for how a model string is resolved and which model runs.

## Testing

Exported from `@loushy/build-ai-agent/testing` (see [Testing agents](testing.md)).
Expand All @@ -207,6 +240,8 @@ Exported from `@loushy/build-ai-agent/testing` (see [Testing agents](testing.md)
| `createTodoTools({ store?, onChange? })` | `todo_write` / `todo_read` tools (plus `getTodos()`) so agents can plan and track multi-step work; see [Todo tools](#todo-tools). |
| `loadMcpTools(client, connectionName)` | Load a connected MCP server's tools as `ToolDescriptor`s. Available from the package root, `@loushy/build-ai-agent/tools`, and `@loushy/build-ai-agent/mcp`. |

See [Tools](./tools.md) for a guide to defining and registering tools.

```ts
import { defineTool } from '@loushy/build-ai-agent';
import { z } from 'zod';
Expand Down Expand Up @@ -432,8 +467,8 @@ built-in and pluggable strategies. See [Context compaction](./compaction.md).

## Flows, evals, observability and security

- `FlowBuilder` / `FlowExecutor` - multi-step workflow graphs.
- `defineEval()`, scorers such as `exactMatch` and `toolCallOrder`, checks such
- `FlowBuilder` / `FlowExecutor` - multi-step workflow graphs; see [Flows](./flows.md).
- `defineEval()`, scorers such as `exactMatch`, `toolCallOrder` and `budget`, checks such
as `includes` and `atLeast`, and `llmJudge()` - agent evals run under vitest
or `loushy eval`; see [Evals](evals.md).
- `withSpan()` and `TraceExporter` - tracing for `AgentExecutor.execute()`.
Expand All @@ -446,8 +481,9 @@ built-in and pluggable strategies. See [Context compaction](./compaction.md).
semantic conventions (flows are traced too); see
[observability](observability.md).
- `NoopSandbox` / `SubprocessSandbox` - sandboxing for tools that opt in via
`requiresSandbox`; guardrails such as `createCommandGuardrail()` and
`secretScanGuardrail`.
`requiresSandbox`; `runGuardrails()` and guardrails such as
`createCommandGuardrail()`, `createDiffSizeGuardrail()` and
`secretScanGuardrail`. See [Guardrails and sandboxing](./guardrails.md).
- `HookRegistry`, `AgentHook`, `HookContext`, `ToolCallHookContext`,
`GenerateHookContext` - pre/post agent hooks (run before/after a tool call
or an LLM `generate`, can mutate args/messages/results or throw to abort
Expand All @@ -470,7 +506,7 @@ built-in and pluggable strategies. See [Context compaction](./compaction.md).
hooks.register(redactPii);
```
- `EncryptionUtils`, `sha256`, `StorageService`, `renderTemplate`,
`MemoryManager` - supporting utilities.
`MemoryManager` - supporting utilities; see [Utilities](./utilities.md).

### Flow expressions

Expand Down
153 changes: 153 additions & 0 deletions docs/approvals.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
# Approvals

Flag a tool `needsApproval` and the run pauses before calling it, until a human
(or your code) approves or rejects the call. The pause is saved in an
`ApprovalStore`, so the decision can come minutes or days later, from another
request or another process, and the run then continues where it stopped.

```ts
import { createAgent, defineTool } from '@loushy/build-ai-agent';
import { z } from 'zod';

const sendEmail = defineTool({
name: 'send_email',
description: 'Send an email',
input: z.object({ to: z.string() }),
needsApproval: true,
execute: async ({ to }) => `sent to ${to}`,
});
const agent = createAgent({ provider, instructions: 'You send emails.', tools: [sendEmail] });

const paused = await agent.send('Email the report to sam@example.com');
if (paused.finishReason === 'awaiting-approval') {
console.log(await agent.approvals.list()); // [{ id, toolName: 'send_email', args: { to: '...' }, ... }]
const result = await agent.approvals.resolve({ id: paused.approvalId!, approved: true }); // or approved: false, note: 'why'
console.log(result.text);
}
```

## Which calls pause

`needsApproval` is `true`, or a predicate that receives the validated
arguments, typed from the tool's zod `input`:

```ts
import { defineTool } from '@loushy/build-ai-agent';
import { z } from 'zod';

const sendEmail = defineTool({
name: 'send_email',
description: 'Send an email',
input: z.object({ to: z.string().email(), subject: z.string(), body: z.string() }),
needsApproval: ({ to }) => !to.endsWith('@mycompany.com'), // only external recipients pause
async execute({ to, subject, body }) {
return { messageId: '...' };
},
});
```

When the model asks for several tools in one turn, the first call that needs
approval stops the batch: the calls before it run, the run pauses on it, and
the calls after it run once it is decided (see
[Approvals in the middle of a tool batch](./durable-execution.md#approvals-in-the-middle-of-a-tool-batch)).

## `createAgent()` agents

- A paused `send()` resolves (it does not throw) with
`finishReason: 'awaiting-approval'` and an `approvalId`.
- `agent.approvals.list()` returns the pending calls this agent paused on in
this process, oldest first: `{ id, toolCallId, toolName, args, createdAt }`,
plus `subagentPath` when the call belongs to a sub-agent.
- `agent.approvals.resolve({ id, approved, note? })` runs the call (approved)
or gives the model a rejection with your `note` (rejected), continues the
run, and resolves with the continued run's result, which may pause again.
It throws when `id` is unknown or already resolved.
- A run that paused inside `agent.session()` continues in that session: the
tool call, its result and the final answer join the session's transcript.
- `agent.stream()` and `session.stream()` end at the pause with an
`approval.requested` event and `run.done` (`'awaiting-approval'`); resolve it
the same way.

Pauses are kept in a per-agent `InMemoryApprovalStore` unless you pass
`approvalStore`. To decide a pause after a restart, give the agent a durable
store, such as the SQLite one (see [Choosing a store](./sessions.md#choosing-a-store)):

```ts
import { createAgent } from '@loushy/build-ai-agent';
import { SqliteStore } from '@loushy/build-ai-agent/sqlite';

const store = new SqliteStore('./.loushy/agent.db');
const agent = createAgent({ provider, tools: [emailTool], approvalStore: store.approvals });
```

`list()` only knows the pauses made by this agent object; keep the
`approvalId` (or read the store) to resolve a pause from somewhere else. A
continued run joins a session only when it is resolved through the agent that
owns that session object.

### Deciding in code

Pass `approve` to decide each call as it comes up instead of pausing: `true`
runs the tool, `false` sends the model a rejection. It applies to `send()`,
sessions and `agent.approvals.resolve()`; `stream()` still ends at the pause.

```ts
import { createAgent } from '@loushy/build-ai-agent';

const trusted = createAgent({
provider,
tools: [emailTool],
approve: ({ toolName, args }) => toolName !== 'send_email' || String(args.to).endsWith('@example.com'),
});
```

## `AgentExecutor` and `resumeAfterApproval()`

With `AgentExecutor.execute()` directly, pass an `approvalStore`. On a gated
call the executor persists an `ExecutionSnapshot` instead of invoking the
tool. Resume later, after a real restart if you like, with
`resumeAfterApproval()`:

```ts
import { AgentExecutor, resumeAfterApproval, StorageServiceApprovalStore } from '@loushy/build-ai-agent';

const approvalStore = new StorageServiceApprovalStore(storage);

const paused = await AgentExecutor.execute({
agent, input, provider, toolRegistry, approvalStore,
});
// paused.finishReason === 'awaiting-approval', paused.approvalId is set

// ...later, from any process, after a human approves...
const result = await resumeAfterApproval(
{ id: paused.approvalId!, approved: true },
approvalStore,
toolRegistry,
provider,
);
```

`resumeAfterApproval(decision, store, registry, provider, options?, checkpointStore?)`
takes most of the execution options of `execute()` (for example `signal`,
`onEvent`, `exporter`). With `sessionId` + `checkpointStore`, the pause is also
checkpointed, and `execute()` with that `sessionId` throws
`SessionAwaitingApprovalError` until the approval is decided, so a pending
approval cannot be bypassed (see [Durable execution](./durable-execution.md)).

| Store | Where the pause lives |
| ----- | --------------------- |
| `InMemoryApprovalStore` | This process only; the default for `createAgent()`. |
| `StorageServiceApprovalStore(storage)` | JSON files through a `StorageService`. |
| `SqliteStore.approvals` | One SQLite file, shared safely by several processes; an approval can be resolved by only one of them. |

## Elsewhere

- **Sub-agents.** A gated tool inside a sub-agent pauses the lead run; the
lead's `agent.approvals` resolves it. See
[Approvals inside a sub-agent](./sub-agents.md#approvals-inside-a-sub-agent).
- **Workspace tools.** The shell tool is approval-gated by default. See
[Workspace tools](./workspace-tools.md).
- **MCP.** Approval-gated tools cannot be approved over MCP; see
[Serve an agent over MCP](./configuration.md#serve-an-agent-over-mcp).
- **Agent Forge** shows pending approvals as inline cards in its chat; see
[Agent Forge](./agent-forge.md).
73 changes: 73 additions & 0 deletions docs/cli.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# CLI

Installing the package also installs the `loushy` command. It scaffolds
projects, checks your setup, runs an agent locally, serves it over MCP, runs
evals, builds a deployable artifact and launches the Agent Forge dashboard.
Run it with `npx loushy <command>` inside a project that has the SDK installed.

| Command | What it does | Details |
| ------- | ------------ | ------- |
| `loushy init [dir]` | Scaffold a project: an agent with an example tool, an offline test, `.env.example`; installs dependencies and runs `git init`. | [Installation](./installation.md#scaffolding-a-new-project) |
| `loushy doctor [spec] [--json]` | Check Node, peer packages, provider keys, and optionally a spec file; prints a fix for every problem. | [Installation](./installation.md#troubleshooting-loushy-doctor) |
| `loushy dev <spec>` | Local dev server for a spec file: chat UI, `POST /chat`, hot reload on save. | [Below](#loushy-dev) |
| `loushy mcp <spec>` | Serve the agent as an MCP server (stdio, or HTTP with `--http`). | [Configuration](./configuration.md#serve-an-agent-over-mcp) |
| `loushy eval [globs...]` | Run `*.eval.ts` files under vitest; print a summary and write JUnit/JSON reports. | [Evals](./evals.md#loushy-eval) |
| `loushy build --target=<t> --agent=<spec>` | Build a deployable Node server, Docker image or Cloudflare Worker. | [Deployment](./deployment.md) |
| `loushy studio` | Launch Agent Forge, the visual dashboard, on one local port. | [Agent Forge](./agent-forge.md) |

Every command that takes a `<spec>` reads an agent spec file (`.yaml`,
`.yml` or `.json`, see [Configuration](./configuration.md#agent-spec-files-agentspec)).

## Usage

```text
loushy init [dir] [--provider P] [--template T] [--yes] [--no-install] [--no-git] [--package-manager PM] [--force]
loushy dev <config.yaml|config.json> [--port N] [--host H]
loushy build --target=<name> --agent=<path> [--out=<dir>]
loushy studio [--port N] [--host H] [--prod|--dev]
loushy mcp <agent.yaml|json> [--http --port N --host H]
loushy doctor [agent.yaml|json] [--json]
loushy eval [globs...] [--tag t] [--junit path] [--json path] [--strict] [--judge]
```

`npm create loushy-agent my-agent` runs `loushy init` with the same arguments.
While the package is not on npm, scaffold from a checkout of this repository
with `node bin/loushy.js init my-agent --sdk-path .` (see
[Installing from a local build](./installation.md#installing-from-a-local-build)).

## `loushy dev`

```bash
npx loushy dev agent.yaml
npx loushy dev agent.yaml --port 4000 --host 0.0.0.0
```

Serves `GET /` (a chat UI), `GET /health` and `POST /chat`
(`{ "message": "..." }`, 1MB body limit). The agent is reloaded whenever the
spec file changes, and the last good config is kept if an edit is invalid.

- `--port` - default `3737`.
- `--host` - default `127.0.0.1` (localhost only). Pass e.g. `--host=0.0.0.0`
to opt in to LAN access.

## `loushy build`

```bash
npx loushy build --target=node-server --agent=agent.yaml # or docker / cloudflare-worker
```

Writes the artifact to `--out` (default `.loushy/build/<target>`) and prints
the command to run or deploy it. Building needs `tsup`
(`npm install --save-dev tsup`). Targets, the HTTP API they serve and the
Workers limits are in [Deployment](./deployment.md).

## `loushy studio`

```bash
npx loushy studio # http://127.0.0.1:4750
npx loushy studio --port 5000
```

Starts one local server for the Agent Forge API and UI. Agents and run state
are stored under `.loushy/` in the current directory. See
[Agent Forge](./agent-forge.md) for the walkthrough.
Loading
Loading