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
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ opt-out.
- Agent Forge: a run on a real provider without an API key fails with `LOUSHO_PROVIDER_MISSING_API_KEY` (HTTP 422) instead of silently using the mock provider, and the provider pill shows what actually ran.
- The examples import `@lousho/build-ai-agent` and its public subpaths instead of the repository's `src/`, so they can be copied into an app as they are.

- The `package.json` that `lousho build --target=node-server|docker` writes lists only the optional peers the bundled server loads (the provider package, `@modelcontextprotocol/sdk` with `mcpServers`), no longer `@lousho/build-ai-agent`: `dist/server.js` bundles the SDK, `ai` and `zod`, so the image's install is smaller.
- The npm package no longer ships the CommonJS build of the CLI (`dist/cli/*.js` and maps) or the CLI's type declarations: `bin/lousho.js` loads only `dist/cli/*.mjs`.
### Added
- Providers: a `timeout` option (default 10 minutes until the response starts and between chunks; `0` or `Infinity` turns it off) and a `fetch` option on every built-in provider. `OpenAIProvider({ api: 'chat' })` and `LOUSHO_OPENAI_API=chat` use Chat Completions for OpenAI-compatible servers (LM Studio, Ollama, vLLM, llama.cpp; the built-in hosted tools are then refused with `LOUSHO_HOSTED_TOOL_UNSUPPORTED`); see [Local and OpenAI-compatible servers](docs/providers.md#local-and-openai-compatible-servers). `withRateLimit()` / `createRateLimiter()` and `createAgent({ rateLimit })` queue model calls client-side. `promptCaching: 'auto'` (the default) marks Anthropic cache breakpoints for Claude on `anthropic` and `openrouter`. `withRetry({ idleTimeoutMs })` fails a stream that stalls between chunks (`timeoutMs` now bounds only the wait for the first chunk). `withFallback()` results carry `servedBy`.
- Model settings: `createAgent({ modelSettings })` and per-call `modelSettings` (`maxTokens`, `temperature`, `topP`, penalties, `stop`, `seed`, `toolChoice`), range-checked up front, also in `agent.json`. Per-call `maxSteps` and `instructions` on `send()` / `stream()`. See [Configuration](docs/configuration.md#model-settings).
Expand All @@ -79,6 +81,9 @@ opt-out.
- Channels and schedules: `githubChannel({ pullRequestOpened: true })`, `webhookChannel({ principal })`, exported `fireSchedule()`, `startSchedules({ keepAlive: true })` and an awaitable `stop()`, and `mountFetchChannels({ waitUntil })`.
- CLI: `lousho doctor --ping` and base URLs in its output, and `lousho --version`.

- `lousho build --target=node-server|docker` builds a `.ts`/`.js` agent module (the file `lousho dev src/agent.ts` serves: a default or `agent` export holding `createAgent()` options or a built agent). The `cloudflare-worker` target says it builds only a spec or an agent directory. The minimal and tools scaffolds export their options as `src/agent.ts`'s default export, so `lousho dev|chat|mcp|acp|build src/agent.ts` work on a new project, and every scaffold has a `build` script (and `tsup`); the minimal and tools ones also have `dev:ui` (`lousho dev src/agent.ts`).
- The Docker image `lousho build --target=docker` writes has a `HEALTHCHECK` on `GET /health`, and its `.dockerignore` keeps `.env`, `.env.*` and `*.tgz` out of the image.
- `lousho add`, `studio`, `eval` and `traces --help` describe every flag; `lousho help <command>` prints that command's help.
### Fixed
- Durability: concurrent commits on one session no longer lose a turn (new optional `SessionStore.saveIf()` compare-and-swap; `KVStore` stays best effort), and session objects re-read the store instead of caching the transcript. Concurrent `send({ sessionId })` calls in one process run in order. Parallel tool calls are checkpointed as each settles, so a crash no longer re-runs finished calls. A restarted process that resolves a session's approval commits the turn instead of dropping it. `session.clear()` deletes every turn's checkpoints and history, finished turns stay forkable, `session.fork()` works for 122-128 character ids, and sub-agent task ids are never reused.
- Stores: `fileStore` file names are case-safe on Windows and macOS (`Alice` and `alice` no longer share a file), long ids and device names are hashed, older long names are still read, writes are `fsync`ed and Windows `EPERM` / `EBUSY` is retried. A corrupt checkpoint fails with `LOUSHO_CHECKPOINT_CORRUPT` and can be discarded. `SqliteStore` waits 50 ms instead of freezing the event loop, retries locked writes without blocking (`LOUSHO_STORAGE_BUSY`) and `prune()` keeps paused turns. `KVStore` approval claims run a tool once in the common case (`ctx.approval.id` serves as an idempotency key). Image and file bytes survive every store. Taking over a stale file lock is serialized, and `KVCheckpointStore.list()` throws without a `list` binding instead of returning `[]`. `agent.approvals.get()` returns `undefined` for an id that cannot name an approval. `fileMemory` and `sqliteMemory` keep every item with several writers.
Expand Down Expand Up @@ -112,6 +117,13 @@ opt-out.
- Approve-with-edits on a sub-agent's *grouped* approval validates the edited args against the tool's schema like a top-level or single sub-agent decision does: before, `snapshot.subagent` stored `decision.args` raw, so an invalid edit (e.g. a string where `z.number()` wants a number) ran the tool with garbage args; it now throws `LOUSHO_TOOL_ARGS_INVALID` and leaves the approval pending. (Eve2-F4)
- Aborting a run during a tool call no longer hangs `send()` / `stream()` / `approvals.resolve()` forever on a tool that ignores its `abortSignal`: the tool batch and the approved-tool wait of `resumeAfterApproval()` are raced against the run's signal the way the model call already was (Eve CORE-F15), the run ends `finishReason: 'aborted'` with a 'cancelled' result for calls still running past a short grace window (a result that lands within it is still kept), and the tool keeps running in the background. (Eve2-F5)
- A run that re-pauses on several approvals after a resume persists every pending approval id in its checkpoint: `markAwaitingApproval()` wrote only `approvalId`, so durable discovery (`pausedApprovalIds`, `agent.approvals.list()`) named 1 of N approvals; it now carries the group's `approvalIds` like `markGroupPending()` does. (Eve2-F6)
- The `inbox-triage` registry kit loads under `lousho dev`, `chat`, `acp`, `mcp` and `doctor`: its relative imports carry the `.ts` extension Node's type stripping needs (only bundlers resolved them). A test now installs every registry kit and loads it in a plain `node` process, as the CLI does. An extensionless relative import in an agent directory gets a hint naming the specifier to write (`'./lib/store.ts'`), and on a Node without built-in type stripping (before 22.18) the `.ts` load error says Node 22.19+ is required.
- `OPENAI_BASE_URL` (and `ANTHROPIC_BASE_URL`) is used with `@ai-sdk/openai` 1.x (`@ai-sdk/anthropic` 1.x), the line that pairs with `ai` 4: the provider passes it when no `baseURL` is configured. Before, `model: 'openai/...'` called api.openai.com while `lousho doctor --ping` reported the custom endpoint.
- `PropagatingToolError`, `McpToolError` and `WorkspaceError` pass `instanceof` across the ESM and CJS copies of the SDK, so a `.cjs` tool's "do not retry" error stops the run under the ESM `lousho` CLI instead of being handed to the model (and retried).
- `lousho dev` re-evaluates the local helper files an edited tool or agent module imports (the `?t=` cache-bust is passed on to relative imports), instead of logging "reloaded" while serving the stale helper; editor temp files no longer show in the "changed:" list.
- `lousho eval`: a case that vitest fails after the case finished (it ran past its time limit) is reported as that case failing, not as a pass plus an extra `vitest` row; a run that records no case at all (a mistyped `--tag`) exits 1 like a glob that matches nothing.
- The yarn scaffold's `yarn test` works: the templates list `vite`, vitest's peer, which yarn 1 does not install. The README's "Next 3 commands" lists three, and the tools template's README counts its three tools.
- CLI messages: a module that exports no agent names the command that loaded it (`lousho chat:`, not `lousho dev:`); `lousho mcp` without a path says a spec, directory or module is accepted, and `--port`/`--host` without `--http` are a usage error instead of ignored; `lousho version <arg>` and `init --sdk-path <missing>` exit 2; the unknown-provider error lists `mock`.

## [1.0.0-alpha.19] - 2026-10-08

Expand Down
3 changes: 3 additions & 0 deletions api/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -3711,6 +3711,7 @@ export interface McpToolAnnotations {

// @public
export class McpToolError extends Error {
static [Symbol.hasInstance](value: unknown): boolean;
constructor(message: string);
readonly toolErrorKind: "mcp";
}
Expand Down Expand Up @@ -4769,6 +4770,7 @@ export interface PromptScheduleInput extends ScheduleBase {

// @public
export class PropagatingToolError extends Error {
static [Symbol.hasInstance](value: unknown): boolean;
}

// @public
Expand Down Expand Up @@ -7137,6 +7139,7 @@ export type WorkspaceEntryType = 'file' | 'directory' | 'symlink' | 'other';

// @public
export class WorkspaceError extends Error {
static [Symbol.hasInstance](value: unknown): boolean;
constructor(message: string);
}

Expand Down
1 change: 1 addition & 0 deletions api/mcp.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1128,6 +1128,7 @@ interface McpToolAnnotations {

// @public
export class McpToolError extends Error {
static [Symbol.hasInstance](value: unknown): boolean;
constructor(message: string);
readonly toolErrorKind: "mcp";
}
Expand Down
2 changes: 2 additions & 0 deletions api/tools.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1376,6 +1376,7 @@ interface McpToolAnnotations {

// @public
export class McpToolError extends Error {
static [Symbol.hasInstance](value: unknown): boolean;
constructor(message: string);
readonly toolErrorKind: "mcp";
}
Expand Down Expand Up @@ -2864,6 +2865,7 @@ export type WorkspaceEntryType = 'file' | 'directory' | 'symlink' | 'other';

// @public
export class WorkspaceError extends Error {
static [Symbol.hasInstance](value: unknown): boolean;
constructor(message: string);
}

Expand Down
23 changes: 16 additions & 7 deletions bin/lousho.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const USAGE = [
' Serve an agent over the Agent Client Protocol on stdio (for editors).',
' lousho add <name> [--registry <url-or-path>] [--dir <agent-dir>] [--yes] [--allow <list>] [--overwrite] [--dry-run] | --list',
' Install a tool, skill, channel, schedule or kit from a registry into an agent directory.',
' lousho build <agent-dir|spec> --target=<node-server|cloudflare-worker|docker> [--out=<dir>] (or --agent=<path>)',
' lousho build <agent-dir|spec|agent.ts> --target=<node-server|cloudflare-worker|docker> [--out=<dir>] (or --agent=<path>)',
' Bundle an agent for deployment (default output: .lousho/build/<target>).',
' lousho studio [--port N] [--host H] [--prod|--dev]',
' Open Agent Forge, the visual agent builder.',
Expand Down Expand Up @@ -83,12 +83,17 @@ function loadDotEnv() {
}

// `lousho --version` / `-v` / `version` print the package version (kept in sync with VERSION by scripts/sync-version.mjs).
function runVersion() {
function runVersion(rest) {
if (rest.length > 0) return usageError(`'version' takes no arguments; got '${rest[0]}'.`);
console.log(require(path.join(__dirname, '..', 'package.json')).version);
}

function runHelp() {
console.log(USAGE);
// `lousho help <command>` is `lousho <command> --help`.
function runHelp(rest) {
if (rest.length === 0) return console.log(USAGE);
const entry = COMMANDS.get(rest[0]);
if (!entry) return unknownCommand(rest[0]);
return runCommand(entry, ['--help']);
}

// `lousho --help` / `-h` / `help` print the usage and succeed (a bare `lousho` is still an error).
Expand All @@ -104,11 +109,15 @@ const BUILTINS = new Map([
/** The exit code for a usage error, as every command returns it (src/cli/args.ts USAGE_EXIT_CODE). */
const USAGE_EXIT_CODE = 2;

function unknownCommand(command) {
console.error(`lousho: unknown command '${command ?? ''}'.\n${USAGE}`);
function usageError(message) {
console.error(`lousho: ${message}\n${USAGE}`);
process.exitCode = USAGE_EXIT_CODE;
}

function unknownCommand(command) {
usageError(`unknown command '${command ?? ''}'.`);
}

/**
* Drops Node's MODULE_TYPELESS_PACKAGE_JSON warning: the CLI imports a
* `tools/*.ts` file as an ES module even in a project whose package.json has
Expand All @@ -135,7 +144,7 @@ async function runCommand(entry, rest) {
async function main() {
const [command, ...rest] = process.argv.slice(2);
const builtin = BUILTINS.get(command);
if (builtin) return builtin();
if (builtin) return builtin(rest);
const entry = COMMANDS.get(command);
return entry ? runCommand(entry, rest) : unknownCommand(command);
}
Expand Down
13 changes: 8 additions & 5 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ lousho dev <spec.yaml|spec.json|agent-dir|agent.ts> [--port N] [--host H] [--all
lousho chat <spec.yaml|spec.json|agent-dir|agent.ts> [--model provider/model] [--session id] [--store sqlite:<file>] [--traces[=dir]]
lousho acp <spec.yaml|spec.json|agent-dir|agent.ts> [--model provider/model]
lousho add <name> [--registry <url-or-path>] [--dir <agent-dir>] [--yes] [--allow <list>] [--overwrite] [--dry-run] (or --list)
lousho build <agent-dir|spec> --target=<node-server|cloudflare-worker|docker> [--out=<dir>] (or --agent=<path>)
lousho build <agent-dir|spec|agent.ts> --target=<node-server|cloudflare-worker|docker> [--out=<dir>] (or --agent=<path>)
lousho studio [--port N] [--host H] [--prod|--dev]
lousho mcp <agent.yaml|json|agent-dir|agent.ts> [--http --port N --host H]
lousho doctor [agent.yaml|json|agent-dir] [--json] [--ping]
Expand All @@ -41,8 +41,8 @@ lousho --version

Every command parses its flags the same way (Node's `parseArgs`, strict):
`--flag value` and `--flag=value` both work, the last of a repeated flag wins
(`--tag` accumulates), `--` ends the flags, and `-h` / `--help` prints the
command's usage and exits 0. An unknown flag, a flag without its value (it
(`--tag` accumulates), `--` ends the flags, and `-h` / `--help` (or `lousho help
<command>`) prints the command's usage and flags and exits 0. An unknown flag, a flag without its value (it
never takes the next flag as its value) or an extra argument fails with
`LOUSHO_CONFIG_INVALID` and the usage line. A value that starts with `-` needs
the `=` form: `--model=-x`. `--traces` is the one flag whose value is optional:
Expand Down Expand Up @@ -147,8 +147,10 @@ without restarting the server or dropping the port:
`require` specifiers, found once at startup; no bundler).

Directory and module files are imported with a `?t=<time>` cache-busting query,
so an edited tool or agent file is re-evaluated. A file *imported by* that file
(a shared helper) stays cached by Node: restart `lousho dev` after editing one.
so an edited tool or agent file is re-evaluated, and so are the local files it
imports by relative specifier (a shared `./lib/helper.ts`): the query is passed
on to them. Packages under `node_modules` stay cached; restart `lousho dev`
after changing one.
The console logs what reloaded. The new agent is built (and `ready()`, so MCP
servers connect) before it replaces the old one, and the old one is then
`close()`d. If the rebuild fails, the last good agent keeps answering, the error
Expand Down Expand Up @@ -242,6 +244,7 @@ scripted lines and a `mockModel` agent and no terminal.
```bash
npx lousho build --target=node-server --agent=agent.yaml # or docker / cloudflare-worker
npx lousho build ./my-agent --target=node-server # an agent directory (node-server, docker)
npx lousho build src/agent.ts --target=node-server # an agent module (node-server, docker)
```

Writes the artifact to `--out` (default `.lousho/build/<target>`) and prints
Expand Down
26 changes: 23 additions & 3 deletions docs/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,26 @@ only a Worker-safe part of `@lousho/build-ai-agent`
npx lousho build ./my-agent --target=cloudflare-worker
```

## Agent modules

The `node-server` and `docker` targets also build a `.ts`/`.js` module whose
default (or `agent`) export is `createAgent()` options or a built agent: the
same file `lousho dev src/agent.ts` serves, and what the minimal and tools
scaffolds have (their `npm run build` runs this):

```bash
npx lousho build src/agent.ts --target=node-server
node .lousho/build/node-server/dist/server.js
```

The generated `server.ts` imports the module, so tsup bundles it with its local
imports and the SDK into an ESM `dist/server.js`. With an options export the
server builds the agent with `LOUSHO_STORE` (unless the options name a
`store`); an exported agent keeps its own. The build's `package.json` names
the provider package when the module has a literal `model: '<provider>/...'`;
add any other optional peer it needs yourself. The `cloudflare-worker` target
does not build a module: use a spec or an agent directory.

## HTTP API

Every target serves the same `/chat` protocol as `lousho dev`
Expand Down Expand Up @@ -203,10 +223,10 @@ browser-platform ES module; the build fails if any `node:` import ends up in the
`dist/server.js` bundles the SDK but leaves its optional peers (the `peerDependenciesMeta`
entries of its package.json: provider packages, `dockerode`, the MCP SDK, `prompts`, ...) external, so a build
never needs one you do not use. The generated `package.json` already lists the
ones the agent needs where the server runs - `@lousho/build-ai-agent` (at
`^` the version that built it, a caret range, not an exact pin), the provider package the spec names (at the
ones the agent needs where the server runs - the provider package the spec names (at the
range pairing with the build's `ai` major, e.g. `@ai-sdk/openai` for an OpenAI
agent) and `@modelcontextprotocol/sdk` when the spec has `mcpServers` - so the
agent) and `@modelcontextprotocol/sdk` when the spec has `mcpServers` - and
nothing else: `@lousho/build-ai-agent`, `ai` and `zod` are inside the bundle, so the
docker image's `npm install --omit=dev` or a `npm install` next to the
node-server output installs them. Anything else (an agent directory's own
imports, `dockerode` for sandboxed tools) you add yourself; a code path that
Expand Down
6 changes: 4 additions & 2 deletions docs/evals.md
Original file line number Diff line number Diff line change
Expand Up @@ -263,8 +263,10 @@ failures and soft failures listed separately.

**Exit code:** `1` on any gate failure (or soft failure with `--strict`, or when
vitest itself fails, for example a file that does not load, or is not installed,
or a `--junit`/`--json` report cannot be written), `2` for a usage error (bad
arguments), otherwise `0`.
or a `--junit`/`--json` report cannot be written, or when no case ran at all:
no eval file matched, or a `--tag` matched no case), `2` for a usage error (bad
arguments), otherwise `0`. A case that vitest fails after the case itself
finished (it ran past its time limit) is reported as that case failing.

How results are collected: each case appends one JSON line to a file named by
the `LOUSHO_EVAL_RESULTS` environment variable, which `lousho eval` sets and
Expand Down
5 changes: 3 additions & 2 deletions docs/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -428,8 +428,9 @@ const agent = createAgent({
`registerModel({ id, provider: 'openai', contextWindow })` once (see
[Local and unknown models](./compaction.md#local-and-unknown-models)).

**The `openai/<model>` string with `OPENAI_BASE_URL`.** `@ai-sdk/openai` reads
`OPENAI_BASE_URL`, so `createAgent({ model: 'openai/qwen3-8b' })` with
**The `openai/<model>` string with `OPENAI_BASE_URL`.** `OpenAIProvider` passes
`OPENAI_BASE_URL` to `@ai-sdk/openai` when no `baseURL` is configured (on every
major, including the 1.x line that pairs with `ai` 4), so `createAgent({ model: 'openai/qwen3-8b' })` with
`OPENAI_BASE_URL=http://localhost:1234/v1` and a dummy `OPENAI_API_KEY=any`
calls the local server without code changes. That route uses the Responses
API unless `LOUSHO_OPENAI_API=chat` is set too, which selects Chat Completions
Expand Down
Loading
Loading