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
632 changes: 205 additions & 427 deletions CHANGELOG.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion api/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1921,7 +1921,7 @@ export const ERROR_CODES: {
readonly LOUSHO_REQUEST_FORBIDDEN: "Call from the server's own origin (or add the page's origin to allowedOrigins) and use a Host the server allows (allowedHosts).";
readonly LOUSHO_UNSUPPORTED_MEDIA_TYPE: "Send the request body as JSON with the header 'Content-Type: application/json'.";
readonly LOUSHO_SESSION_AWAITING_APPROVAL: "Resolve the pending approval first (agent.approvals.resolve() or resumeAfterApproval()), then send again.";
readonly LOUSHO_SESSION_ID_INVALID: "Session ids: 1-128 characters from A-Z, a-z, 0-9, '_' and '-'. Run ids (send({ sessionId })): up to 200 and '.' too, not ending in '.turn-<n>' or '.paused'.";
readonly LOUSHO_SESSION_ID_INVALID: "Session ids (agent.session({ id }), POST /chat): 1-128 characters from A-Z, a-z, 0-9, '_' and '-'. Run ids (send({ sessionId })): up to 200 and '.' too, not ending in '.turn-<n>', '.paused' or '.fire'.";
readonly LOUSHO_SESSION_BUSY: "Wait for the running turn to finish (await its send(), or abort it), then call again.";
readonly LOUSHO_SESSION_TURN_PENDING: "Finish the interrupted turn with session.resume(), or drop it with session.discardPending(), then call again.";
readonly LOUSHO_SESSION_TURN_FAILED: "Drop the failed turn with session.discardPending() and send again, or fix the cause and retry it with session.resume().";
Expand Down
2 changes: 1 addition & 1 deletion api/mcp.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -581,7 +581,7 @@ const ERROR_CODES: {
readonly LOUSHO_REQUEST_FORBIDDEN: "Call from the server's own origin (or add the page's origin to allowedOrigins) and use a Host the server allows (allowedHosts).";
readonly LOUSHO_UNSUPPORTED_MEDIA_TYPE: "Send the request body as JSON with the header 'Content-Type: application/json'.";
readonly LOUSHO_SESSION_AWAITING_APPROVAL: "Resolve the pending approval first (agent.approvals.resolve() or resumeAfterApproval()), then send again.";
readonly LOUSHO_SESSION_ID_INVALID: "Session ids: 1-128 characters from A-Z, a-z, 0-9, '_' and '-'. Run ids (send({ sessionId })): up to 200 and '.' too, not ending in '.turn-<n>' or '.paused'.";
readonly LOUSHO_SESSION_ID_INVALID: "Session ids (agent.session({ id }), POST /chat): 1-128 characters from A-Z, a-z, 0-9, '_' and '-'. Run ids (send({ sessionId })): up to 200 and '.' too, not ending in '.turn-<n>', '.paused' or '.fire'.";
readonly LOUSHO_SESSION_BUSY: "Wait for the running turn to finish (await its send(), or abort it), then call again.";
readonly LOUSHO_SESSION_TURN_PENDING: "Finish the interrupted turn with session.resume(), or drop it with session.discardPending(), then call again.";
readonly LOUSHO_SESSION_TURN_FAILED: "Drop the failed turn with session.discardPending() and send again, or fix the cause and retry it with session.resume().";
Expand Down
2 changes: 1 addition & 1 deletion api/testing.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ const ERROR_CODES: {
readonly LOUSHO_REQUEST_FORBIDDEN: "Call from the server's own origin (or add the page's origin to allowedOrigins) and use a Host the server allows (allowedHosts).";
readonly LOUSHO_UNSUPPORTED_MEDIA_TYPE: "Send the request body as JSON with the header 'Content-Type: application/json'.";
readonly LOUSHO_SESSION_AWAITING_APPROVAL: "Resolve the pending approval first (agent.approvals.resolve() or resumeAfterApproval()), then send again.";
readonly LOUSHO_SESSION_ID_INVALID: "Session ids: 1-128 characters from A-Z, a-z, 0-9, '_' and '-'. Run ids (send({ sessionId })): up to 200 and '.' too, not ending in '.turn-<n>' or '.paused'.";
readonly LOUSHO_SESSION_ID_INVALID: "Session ids (agent.session({ id }), POST /chat): 1-128 characters from A-Z, a-z, 0-9, '_' and '-'. Run ids (send({ sessionId })): up to 200 and '.' too, not ending in '.turn-<n>', '.paused' or '.fire'.";
readonly LOUSHO_SESSION_BUSY: "Wait for the running turn to finish (await its send(), or abort it), then call again.";
readonly LOUSHO_SESSION_TURN_PENDING: "Finish the interrupted turn with session.resume(), or drop it with session.discardPending(), then call again.";
readonly LOUSHO_SESSION_TURN_FAILED: "Drop the failed turn with session.discardPending() and send again, or fix the cause and retry it with session.resume().";
Expand Down
2 changes: 1 addition & 1 deletion api/tools.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -676,7 +676,7 @@ const ERROR_CODES: {
readonly LOUSHO_REQUEST_FORBIDDEN: "Call from the server's own origin (or add the page's origin to allowedOrigins) and use a Host the server allows (allowedHosts).";
readonly LOUSHO_UNSUPPORTED_MEDIA_TYPE: "Send the request body as JSON with the header 'Content-Type: application/json'.";
readonly LOUSHO_SESSION_AWAITING_APPROVAL: "Resolve the pending approval first (agent.approvals.resolve() or resumeAfterApproval()), then send again.";
readonly LOUSHO_SESSION_ID_INVALID: "Session ids: 1-128 characters from A-Z, a-z, 0-9, '_' and '-'. Run ids (send({ sessionId })): up to 200 and '.' too, not ending in '.turn-<n>' or '.paused'.";
readonly LOUSHO_SESSION_ID_INVALID: "Session ids (agent.session({ id }), POST /chat): 1-128 characters from A-Z, a-z, 0-9, '_' and '-'. Run ids (send({ sessionId })): up to 200 and '.' too, not ending in '.turn-<n>', '.paused' or '.fire'.";
readonly LOUSHO_SESSION_BUSY: "Wait for the running turn to finish (await its send(), or abort it), then call again.";
readonly LOUSHO_SESSION_TURN_PENDING: "Finish the interrupted turn with session.resume(), or drop it with session.discardPending(), then call again.";
readonly LOUSHO_SESSION_TURN_FAILED: "Drop the failed turn with session.discardPending() and send again, or fix the cause and retry it with session.resume().";
Expand Down
2 changes: 1 addition & 1 deletion api/worker.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1305,7 +1305,7 @@ const ERROR_CODES: {
readonly LOUSHO_REQUEST_FORBIDDEN: "Call from the server's own origin (or add the page's origin to allowedOrigins) and use a Host the server allows (allowedHosts).";
readonly LOUSHO_UNSUPPORTED_MEDIA_TYPE: "Send the request body as JSON with the header 'Content-Type: application/json'.";
readonly LOUSHO_SESSION_AWAITING_APPROVAL: "Resolve the pending approval first (agent.approvals.resolve() or resumeAfterApproval()), then send again.";
readonly LOUSHO_SESSION_ID_INVALID: "Session ids: 1-128 characters from A-Z, a-z, 0-9, '_' and '-'. Run ids (send({ sessionId })): up to 200 and '.' too, not ending in '.turn-<n>' or '.paused'.";
readonly LOUSHO_SESSION_ID_INVALID: "Session ids (agent.session({ id }), POST /chat): 1-128 characters from A-Z, a-z, 0-9, '_' and '-'. Run ids (send({ sessionId })): up to 200 and '.' too, not ending in '.turn-<n>', '.paused' or '.fire'.";
readonly LOUSHO_SESSION_BUSY: "Wait for the running turn to finish (await its send(), or abort it), then call again.";
readonly LOUSHO_SESSION_TURN_PENDING: "Finish the interrupted turn with session.resume(), or drop it with session.discardPending(), then call again.";
readonly LOUSHO_SESSION_TURN_FAILED: "Drop the failed turn with session.discardPending() and send again, or fix the cause and retry it with session.resume().";
Expand Down
2 changes: 1 addition & 1 deletion docs/agent-directories.md
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,7 @@ files. Every option is passed on to the agent, including the ones no file
declares (`guardrails`, `onEvent`, `retry`, `reasoning`, `redactContent`,
`output`, ...). Use it to swap the model in tests:

```ts
```ts no-run
import { loadAgentDir } from '@lousho/build-ai-agent';
import { mockModel } from '@lousho/build-ai-agent/testing';

Expand Down
2 changes: 1 addition & 1 deletion docs/auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ const notes = defineMemory({
});

const agent = createAgent({
provider: mockModel(['Hi!']),
provider: mockModel(['Hi!', 'Hi again!']), // one scripted reply per turn below
model: ({ principal }) => (principal?.claims?.plan === 'pro' ? 'openai/gpt-4o' : 'openai/gpt-4o-mini'),
instructions: ({ principal }) => (principal?.type === 'service' ? 'Answer in JSON.' : 'Be friendly.'),
memory: [notes],
Expand Down
2 changes: 1 addition & 1 deletion docs/channels.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ const channels = mountFetchChannels(agent, [slack], { waitUntil: (turn) => void

`httpChannel()` is the reference channel: `{ sessionKey, input }` in, JSON out.

```ts
```ts no-run
import * as http from 'node:http';
import { createAgent, createMockProvider, httpChannel, mountChannels } from '@lousho/build-ai-agent';

Expand Down
7 changes: 5 additions & 2 deletions docs/durable-execution.md
Original file line number Diff line number Diff line change
Expand Up @@ -631,8 +631,11 @@ if (original && replayed) {
leases, the last save wins.
- A run id (`sessionId` of `send()` / `stream()`) is 1-200 characters of
letters, digits, `_`, `-` and `.`, not starting or ending with `.`, on every
store, and may not end with `.turn-<n>` or `.paused`: those are a session's
own checkpoint keys. Anything else fails with `LOUSHO_SESSION_ID_INVALID`.
store, and may not end with `.turn-<n>` or `.paused` (a session's own
checkpoint keys) or `.fire` (a schedule's fire claim). Anything else fails
with `LOUSHO_SESSION_ID_INVALID`. A session id (`agent.session({ id })`, the
routes' `sessionId`) has a stricter rule: 1-128 characters of
`A-Za-z0-9_-`, with no `.`.
- If you call `resumeAfterApproval()` (the function behind
`agent.approvals.resolve()`) without the `checkpointStore`, the session
keeps its `'awaiting-approval'` mark and later `send()` (or `execute()`)
Expand Down
4 changes: 3 additions & 1 deletion docs/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -517,7 +517,9 @@ paused on an approval (`error.approvalId`), so it cannot take new input yet.
not 1-200 characters of letters, digits, `_`, `-` and `.` (not starting or
ending with `.`), the same rule on every store; or it ends with `.turn-<n>` or
`.paused`, which are a session's own checkpoint keys (a run under
`'victim.turn-2'` would write into session `victim`'s turn).
`'victim.turn-2'` would write into session `victim`'s turn), or `.fire` (a
schedule's fire claim). The message says which of the two rules applies: an id
with a `.` or over 128 characters is a valid run id but not a valid session id.

**Fix:** use an id such as `'chat-9f2c4e7a1b'`, or omit it to get a generated one. On a
route that serves several users, do not make ids guessable from a user's name or number
Expand Down
6 changes: 3 additions & 3 deletions docs/evals.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ agent's provider makes the eval deterministic: no network, no API key, no
flakiness. **This is the default way to write an eval**; use a real model only
for the [judge evals](#judge-evals) below.

```ts
```ts no-run
// refund.eval.ts
import { z } from 'zod';
import { createAgent, defineEval, defineTool, includes } from '@lousho/build-ai-agent';
Expand Down Expand Up @@ -159,7 +159,7 @@ its own cassette. `timeoutMs` covers all runs of a case, so raise it with `repea
Pass `cases` and `test` runs once per case, each reported separately. A case is
any object; its `label` (or `name`, or its `input`) names it.

```ts
```ts no-run
import { z } from 'zod';
import { createAgent, defineEval, defineTool } from '@lousho/build-ai-agent';
import { mockModel } from '@lousho/build-ai-agent/testing';
Expand Down Expand Up @@ -203,7 +203,7 @@ model runs out of turns on the second case.
configured one: without `judge`, `t.judge()` throws an error saying how to fix
it.

```ts
```ts no-run
// tone.judge.eval.ts: run with `lousho eval --judge`
import { createAgent, defineEval, atLeast } from '@lousho/build-ai-agent';
import { mockModel } from '@lousho/build-ai-agent/testing';
Expand Down
2 changes: 1 addition & 1 deletion docs/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ const custom = LLMProviderRegistry.create('openai', {
`ANTHROPIC_API_KEY`, `OPENROUTER_API_KEY`, `OLLAMA_BASE_URL` that is
present. With none set it throws an error listing the fixes.

```ts
```ts no-run
import { createAgent, createMockProvider, resolveProvider } from '@lousho/build-ai-agent';

const fromString = createAgent({ model: 'anthropic/claude-sonnet-5-5' });
Expand Down
2 changes: 1 addition & 1 deletion docs/schedules.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ The timers are unref'd: they never keep a process alive on their own, so a
server or a test exits normally. A script that does nothing but run schedules
would therefore exit straight away; pass `keepAlive: true` for that:

```ts
```ts no-run
import { createAgent, createMockProvider, defineSchedule, startSchedules } from '@lousho/build-ai-agent';

const agent = createAgent({ instructions: 'You write reports.', provider: createMockProvider() });
Expand Down
8 changes: 4 additions & 4 deletions docs/triggers.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ handle.stop();

**Deprecated.** `WebhookTriggerAdapter` is replaced by `webhookChannel()` mounted with `mountChannels()` (see [Channels](./channels.md)), which takes the same `auth` option and adds sessions and approvals. The `WebhookAuth` shapes below stay and are the reference for `webhookChannel({ auth })`.

```ts
```ts no-run
import { createAgent, createMockProvider, mountChannels, webhookChannel } from '@lousho/build-ai-agent';

const agent = createAgent({ instructions: 'You are helpful.', provider: createMockProvider() });
Expand All @@ -95,7 +95,7 @@ through `options.logger`. Without `auth` on loopback, a request whose `Host`
is not a loopback name (DNS rebinding) or that carries an `Origin` header (a
web page) gets `403`.

```ts
```ts no-run
import { createAgent, createMockProvider } from '@lousho/build-ai-agent';
import { WebhookTriggerAdapter } from '@lousho/build-ai-agent/triggers';

Expand Down Expand Up @@ -144,7 +144,7 @@ tokens and payloads are not sent in clear text.

**Deprecated.** `SlackTriggerAdapter` is replaced by `slackChannel()` mounted with `mountChannels()` (see [Slack in Channels](./channels.md#slack)): one session per Slack thread, approval buttons, and the signature check built in. `verifySlackSignature()` stays and is not deprecated.

```ts
```ts no-run
import { createAgent, createMockProvider, mountChannels, slackChannel } from '@lousho/build-ai-agent';

const agent = createAgent({ instructions: 'You are helpful.', provider: createMockProvider() });
Expand All @@ -171,7 +171,7 @@ rejected (the timestamp is in seconds, as Slack sends it). Failures get a generi
logged at `warn` level. The signed `url_verification` handshake is answered
after verification.

```ts
```ts no-run
import { createAgent, createMockProvider } from '@lousho/build-ai-agent';
import { SlackTriggerAdapter, verifySlackSignature } from '@lousho/build-ai-agent/triggers';
import * as http from 'node:http';
Expand Down
6 changes: 3 additions & 3 deletions docs/upgrading.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@

Anyone with code written against `1.0.0-alpha.8` or an earlier `1.0.0-alpha.*`
release. Between those alphas and the 1.0 line (the changes listed under
`1.0.0-rc.0` in the changelog, published in the later `1.0.0-alpha.*` releases) the package's public surface
was reorganized: imports moved to subpaths, dead or superseded APIs were
`1.0.0-alpha.19` in the changelog, first published in `1.0.0-alpha.15`) the
package's public surface was reorganized: imports moved to subpaths, dead or superseded APIs were
removed, and the patch guardrails were renamed patch checks. `createAgent()`
and its options did not change - an agent that only does
`createAgent({ model, instructions, tools })` and `agent.send()` upgrades by
Expand Down Expand Up @@ -224,7 +224,7 @@ there is one. A change marked "no opt-out" closed a security or data-loss bug.
| Resolving an approval whose run was deleted or replaced fails with `LOUSHO_APPROVAL_ORPHANED` and withdraws it (it used to overwrite the newer conversation). | no opt-out |
| A session turn paused for approval locks the session id: a new turn on it throws `SessionAwaitingApprovalError` (`LOUSHO_SESSION_AWAITING_APPROVAL`). | decide the approval, or `session.discardPending()` |
| Session turns keep the checkpoint history of the latest 10 committed turns. See [Durable sessions](./sessions.md#durable-sessions). | `agent.session({ id, turnHistoryLimit })` with a larger whole number |
| `sessionId` must be 1-200 characters of `A-Za-z0-9_.-` on every store, and may not end in `.turn-<n>`, `.paused` or `.fire` (a schedule's fire claim); `''` is refused. | no opt-out |
| A run id (`agent.send()` / `agent.stream()`'s `sessionId`, a fork's `newSessionId`) must be 1-200 characters of `A-Za-z0-9_.-`, not starting or ending with `.`, on every store, and may not end in `.turn-<n>`, `.paused` or `.fire` (a schedule's fire claim); `''` is refused. This is not the session id rule: a session id (`agent.session({ id })`, the routes' and `lousho dev`'s `sessionId`, `lousho chat --session`) is still 1-128 characters of `A-Za-z0-9_-`, with no `.`. Both fail with `LOUSHO_SESSION_ID_INVALID`. | no opt-out |
| A lease left by a process that died on another machine (or in another container with the same hostname) is taken over 30 to 60 seconds after its last heartbeat (a grace for clock skew), not at 30 seconds. See [Run leases](./durable-execution.md#run-leases). | no opt-out |
| During a rolling upgrade, replicas still on an earlier release neither take nor respect run leases, so a turn can run twice until every replica is upgraded. A SQLite database opened by this release is migrated and then refused by older releases. | upgrade all replicas together; keep a copy of the database to roll back |
| `approvalTtlMs` (and an `ask` rule's `ttlMs`) above 100 years throws `LOUSHO_CONFIG_INVALID` instead of meaning "never expires". | leave it out for pauses that never expire |
Expand Down
3 changes: 3 additions & 0 deletions docs/workspace-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,9 @@ refused command is never offered for approval:
returns `true` for the rest of the line (`''` when there is none):

```ts
import { createShellTool, NodeWorkspace } from '@lousho/build-ai-agent';

const workspace = new NodeWorkspace({ root: '.' });
createShellTool(workspace, {
needsApproval: false,
allow: [
Expand Down
6 changes: 6 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ is runnable directly (see its own README for exact instructions); most use
`tsx` and a free mock provider by default, so nothing here needs an API key
to try.

The examples import the SDK by its package name and public subpaths
(`@lousho/build-ai-agent`, `@lousho/build-ai-agent/testing`, ...), exactly as
your app would, so you can copy a file out as it is. Inside this repo the
`paths` in `tsconfig.json` (and a resolver in `vitest.config.ts`) point those
imports at `src/`, so nothing needs building first.

## [agent-dir](./agent-dir)

An agent defined as a directory (`instructions.md`, `tools/`, `skills/`, `agent.json`) and loaded with `loadAgentDir()`; runs offline with a mock model (LOU-Y5).
Expand Down
4 changes: 2 additions & 2 deletions examples/agent-dir/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, it, expect } from 'vitest';
import { loadAgentDir } from '../../src';
import { mockModel } from '../../src/testing';
import { loadAgentDir } from '@lousho/build-ai-agent';
import { mockModel } from '@lousho/build-ai-agent/testing';

describe('examples/agent-dir', () => {
it('loads from disk and its tool and skill are callable offline', async () => {
Expand Down
4 changes: 2 additions & 2 deletions examples/agent-dir/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
* you trust. Run with: npx tsx examples/agent-dir/index.ts
*/
import path from 'node:path';
import { loadAgentDir } from '../../src';
import { mockModel } from '../../src/testing';
import { loadAgentDir } from '@lousho/build-ai-agent';
import { mockModel } from '@lousho/build-ai-agent/testing';

const provider = mockModel([
{
Expand Down
2 changes: 1 addition & 1 deletion examples/agent-dir/tools/word_count.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { z } from 'zod';
import { defineTool } from '../../../src/index.js';
import { defineTool } from '@lousho/build-ai-agent';

export default defineTool({
name: 'word_count',
Expand Down
6 changes: 3 additions & 3 deletions examples/coding-agent-workflows/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { z } from 'zod';
import { describe, expect, it } from 'vitest';
import { mockModel } from '../../src/testing';
import { defineTool } from '../../src';
import { ToolRegistry } from '../../src/tools';
import { mockModel } from '@lousho/build-ai-agent/testing';
import { defineTool } from '@lousho/build-ai-agent';
import { ToolRegistry } from '@lousho/build-ai-agent/executor';
import { codingTools, memoryWorkspace, runCodingAgent, triage } from './index';

const decision = (route: string, confidence: number) =>
Expand Down
8 changes: 4 additions & 4 deletions examples/coding-agent-workflows/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@
import { realpathSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { z } from 'zod';
import { createAgent, defineTool, resolveProvider, type LLMProvider, type SimpleAgent } from '../../src';
import { ToolRegistry } from '../../src/tools';
import { FlowBuilder, FlowExecutor, type EditorStep, type FlowExecutionResult } from '../../src/flows';
import { mockModel } from '../../src/testing';
import { createAgent, defineTool, resolveProvider, type LLMProvider, type SimpleAgent } from '@lousho/build-ai-agent';
import { ToolRegistry } from '@lousho/build-ai-agent/executor';
import { FlowBuilder, FlowExecutor, type EditorStep, type FlowExecutionResult } from '@lousho/build-ai-agent/flows';
import { mockModel } from '@lousho/build-ai-agent/testing';

export const LIVE_MODEL = 'openrouter/openai/gpt-4o-mini';

Expand Down
Loading
Loading