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
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
api/** text eol=lf
bin/** text eol=lf
packages/*/bin/** text eol=lf
82 changes: 26 additions & 56 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,13 @@ jobs:
# this test always failed once it actually got a chance to run.
- run: npm run build
# The dual-entry build's DTS pass (worker + executor entries) exceeds
# the default heap, like the peers job's build below.
# the default heap, like the compat job's build below.
env:
NODE_OPTIONS: --max-old-space-size=12288
# API surface gate: every exports entry has a checked-in api/*.api.md
# report; a public-API change fails here until `npm run api:update` is run
# and the diff committed. Runs only in the main job so the report reflects
# the default peers, not the `peers` matrix's ai/zod pairings.
# the default peers, not the compat job's ai/zod pairing.
- run: npm run api:check
# packages/create-lousho-agent's own tests execFileSync its bin/cli.js,
# which requires ITS dist/index.js to exist too - build it before the
Expand Down Expand Up @@ -73,73 +73,43 @@ jobs:
# Dead code, duplication and complexity gate (config: .fallowrc.json).
# Bare `fallow` fails on any finding. Runs after the coverage step because
# its CRAP scores read coverage/coverage-final.json.
- run: npm run fallow
# Every `ai` and zod pairing the peer ranges accept, installed for real on top of the
# default install (`npm ci`; `--no-save` leaves the lockfile alone). The default dev install is
# `ai` 4 with zod 3 and runs in `ci`. `ollama-ai-provider-v2` peers on zod 4, so only the zod 4
# entries install it; ollamaV2.contract.test.ts runs against the real package in those two.
# `flags` for those two: ollama-ai-provider-v2 is an optional peer of this package, which npm does not
# install by name unless `--save-prod` (in-memory only with --no-save) makes it a dependency, and the dev
# install's ollama-ai-provider (v1, zod 3 peer) conflicts with zod 4, hence `--legacy-peer-deps`.
# The v4-only tests are gated by itOnAiV4/describeOnAiV4; Worker bundle tests run on all.
peers:
name: peers (${{ matrix.name }})
- run: npm run fallow || (npx fallow health; npx fallow dupes; exit 1)
# What `npm pack` produces must install and work in a fresh project: builds, packs the SDK and
# create-lousho-agent, `npm publish --dry-run` (never a real publish), installs the tarballs plus
# peers in a fresh project, loads every export in ESM and CJS, a mock-model turn, the bin and tsc.
- run: npm run pack-smoke
env:
NODE_OPTIONS: --max-old-space-size=12288
# Hello-world bundle and tarball unpacked size against scripts/size-budget.json (after pack-smoke's builds).
- run: npm run size-check
# Source and tests also type-check against zod 4 (last: it changes node_modules).
- run: npm install --no-save zod@4
- run: npx tsc --noEmit
- run: npm run typecheck:tests
# The newest peers the ranges accept (`ai` 7, `@ai-sdk/*` 4, `ollama-ai-provider-v2` 4, zod 4), installed
# on top of the default `ai` 4 / zod 3 install (`--no-save` leaves the lockfile alone; `--save-prod
# --legacy-peer-deps` because ollama-ai-provider-v2 is an optional peer and the dev install's v1 conflicts
# with zod 4). Then the sandbox egress, credential broker and docker deploy image against the runner's
# real Docker Engine (`*.docker.test.ts`, which fail instead of skipping with LOUSHO_DOCKER_TESTS=1).
compat:
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
include:
- name: ai4-zod4
install: zod@4
typecheck-tests: true
- name: ai6-zod3
install: ai@6 @ai-sdk/openai@3 @ai-sdk/anthropic@3
- name: ai6-zod4
install: ai@6 @ai-sdk/openai@3 @ai-sdk/anthropic@3 ollama-ai-provider-v2@3 zod@4
flags: --save-prod --legacy-peer-deps
- name: ai7-zod3
install: ai@7 @ai-sdk/openai@4 @ai-sdk/anthropic@4
- name: ai7-zod4
install: ai@7 @ai-sdk/openai@4 @ai-sdk/anthropic@4 ollama-ai-provider-v2@4 zod@4
flags: --save-prod --legacy-peer-deps
timeout-minutes: 25
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22.19.0'
cache: 'npm'
- run: npm ci
- run: npm install --no-save ${{ matrix.flags }} ${{ matrix.install }}
- run: npm install --no-save --save-prod --legacy-peer-deps ai@7 @ai-sdk/openai@4 @ai-sdk/anthropic@4 ollama-ai-provider-v2@4 zod@4
- run: npx tsc --noEmit
# The tests are written against `ai` 4's types (v4-shaped Tool, MockLanguageModelV1), so they only
# type-check there; ai4-zod4 is the zod 4 entry (it was the old typecheck-zod4 job).
- run: npm run typecheck:tests
if: matrix.typecheck-tests
- run: npm run test:types
# The DTS build of the ai7-zod4 install runs out of the default heap (seen on #322, #335).
- run: npm run build
env:
NODE_OPTIONS: --max-old-space-size=12288
- run: npm run build --workspace=packages/create-lousho-agent
- run: npx vitest run
# What `npm pack` produces must install and work in a fresh project. The script
# builds, packs the SDK and create-lousho-agent, runs `npm publish --dry-run` (never a real
# publish), installs the tarballs plus peers from the registry, and loads every export in
# ESM and CJS, runs a mock-model turn, the lousho bin and tsc (bundler and node16).
pack-smoke:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22.19.0'
cache: 'npm'
- run: npm ci
- run: npm run pack-smoke
# pack-smoke runs `npm run build` itself, which needs the same heap.
- run: docker pull curlimages/curl:8.10.1 && docker pull node:20-alpine && docker pull node:22-slim
- run: npm run test:docker
env:
NODE_OPTIONS: --max-old-space-size=12288
# Hello-world bundle and tarball unpacked size against scripts/size-budget.json.
# pack-smoke above leaves the SDK and Agent Forge builds in place, so the tarball figure is the real one.
- run: npm run size-check
LOUSHO_DOCKER_TESTS: '1'
51 changes: 0 additions & 51 deletions .github/workflows/docker.yml

This file was deleted.

47 changes: 0 additions & 47 deletions .github/workflows/e2e-studio.yml

This file was deleted.

35 changes: 0 additions & 35 deletions .github/workflows/registry-smoke.yml

This file was deleted.

4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ What is on `main` and not yet on npm. Entries marked **Behavior change** can
break working code; [Upgrading](docs/upgrading.md) lists each one with its
opt-out.

### Changed

- CI is one workflow with two jobs: `ci` (type checks, lint, build, tests with coverage, docs, API and fallow gates, pack-smoke, size check, and a zod 4 type check) and `compat` (the newest peers, `ai` 7 with zod 4, plus the real-Docker sandbox tests). The separate Docker Engine, Agent Forge E2E and weekly registry-smoke workflows and the `ai` 6 / zod 3 matrix entries are gone; `npm run registry-smoke` and `npm run test:e2e:studio` still run locally. When the fallow gate fails, CI now prints its full health and duplication reports.

### Added
- `fromAiSdkTools(record)` converts Vercel AI SDK tools (`tool({ ... })` or plain `{ description, inputSchema | parameters, execute, needsApproval? }` objects) into Lousho tools, keyed by name: zod and Standard Schemas are kept, `jsonSchema()` wrappers and plain JSON Schemas are converted (their own `validate` still runs), `needsApproval` (boolean or function) pauses the run for approval, and `execute` gets the call id and the run's abort signal. It imports nothing from `ai`, so tools built on `ai` 4 to 7 work with any installed version. New guide: [Migrating from the AI SDK](docs/migrating-from-ai-sdk.md).
- `lousho eval` without vitest installed now says it runs `*.eval.ts` files through vitest, the directory it searched from, and the install command; [Evals](docs/evals.md) states the requirement up front.
Expand Down
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ By participating in this project, you agree to maintain a respectful and inclusi
npm test # Tests
npm run build # Build
```
Changes to the Docker sandbox, its network egress, the credential broker or the docker deploy adapter also have real-daemon tests, `npm run test:docker` (`*.docker.test.ts`, not part of `npm test`). They skip without a Docker daemon (the egress cases also skip on Docker Desktop and rootless Docker, where egress is refused by design), so they run in the "Docker Engine" CI workflow on Linux; set `LOUSHO_DOCKER_TESTS=1` to make a missing daemon fail the run instead.
Changes to the Docker sandbox, its network egress, the credential broker or the docker deploy adapter also have real-daemon tests, `npm run test:docker` (`*.docker.test.ts`, not part of `npm test`). They skip without a Docker daemon (the egress cases also skip on Docker Desktop and rootless Docker, where egress is refused by design), so they run in CI's `compat` job on Linux; set `LOUSHO_DOCKER_TESTS=1` to make a missing daemon fail the run instead.

4. Commit your changes with a descriptive commit message

Expand All @@ -78,7 +78,7 @@ By participating in this project, you agree to maintain a respectful and inclusi
- Ensure all existing tests pass
- Aim for good test coverage
- Tests are located alongside source files with `.test.ts` extension
- `npm run pack-smoke` installs the tarball built from your checkout into a fresh project and exercises it. `npm run registry-smoke` does the same for what is published on npm: `npm create lousho-agent`, the generated project's tests and type check, `lousho doctor`, every export in ESM and CJS, and `lousho studio`. Run it right after a publish (`-- --version <semver|dist-tag>` picks the version; `-- --live` adds one real OpenRouter turn and needs `OPENROUTER_API_KEY`). A weekly CI job runs it on Node 22 and 26 and on Windows.
- `npm run pack-smoke` installs the tarball built from your checkout into a fresh project and exercises it. `npm run registry-smoke` does the same for what is published on npm: `npm create lousho-agent`, the generated project's tests and type check, `lousho doctor`, every export in ESM and CJS, and `lousho studio`. Run it right after a publish (`-- --version <semver|dist-tag>` picks the version; `-- --live` adds one real OpenRouter turn and needs `OPENROUTER_API_KEY`).

### Documentation

Expand Down
20 changes: 15 additions & 5 deletions bin/lousho.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,17 +84,27 @@ const BYTE_ORDER_MARK = 0xfeff;
* silently missing.
*/
function loadDotEnv() {
const text = readDotEnv();
if (text === undefined) return;
for (const [name, value] of Object.entries(parseEnv(withoutByteOrderMark(text)))) {
if (process.env[name] === undefined) process.env[name] = value;
}
}

/** ./.env's text, or undefined when there is none (any other read error is reported). */
function readDotEnv() {
try {
const text = fs.readFileSync(path.resolve('.env'), 'utf8');
const vars = parseEnv(text.charCodeAt(0) === BYTE_ORDER_MARK ? text.slice(1) : text);
for (const [name, value] of Object.entries(vars)) {
if (process.env[name] === undefined) process.env[name] = value;
}
return fs.readFileSync(path.resolve('.env'), 'utf8');
} catch (error) {
if (error.code !== 'ENOENT') console.error(`lousho: could not load .env: ${error.message}`);
return undefined;
}
}

function withoutByteOrderMark(text) {
return text.charCodeAt(0) === BYTE_ORDER_MARK ? text.slice(1) : text;
}

// `lousho --version` / `-v` / `version` print the package version (kept in sync with VERSION by scripts/sync-version.mjs).
function runVersion(rest) {
if (rest.length > 0) return usageError(`'version' takes no arguments; got '${rest[0]}'.`);
Expand Down
4 changes: 2 additions & 2 deletions docs/workspace-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -502,8 +502,8 @@ What this enforces, and where:
the allowlist check. Raw IP connections have no route. Non-HTTP protocols
(SSH, raw TCP, UDP) are blocked, because only the proxy is reachable, and
`CONNECT` tunnels are checked against the allowlist like any request.
This is tested against a real daemon: the Docker Engine CI workflow
(`.github/workflows/docker.yml`, `npm run test:docker`) runs on GitHub's
This is tested against a real daemon: CI's `compat` job
(`npm run test:docker`) runs on GitHub's
`ubuntu-latest` runner (rootful Docker Engine 28.0.4 on Ubuntu 24.04 when
it was added) and checks that an allowed host answers through the broker,
that other hosts get a 403, that bypassing the proxy or dialling a raw IP
Expand Down
4 changes: 2 additions & 2 deletions llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20495,8 +20495,8 @@ What this enforces, and where:
the allowlist check. Raw IP connections have no route. Non-HTTP protocols
(SSH, raw TCP, UDP) are blocked, because only the proxy is reachable, and
`CONNECT` tunnels are checked against the allowlist like any request.
This is tested against a real daemon: the Docker Engine CI workflow
(`.github/workflows/docker.yml`, `npm run test:docker`) runs on GitHub's
This is tested against a real daemon: CI's `compat` job
(`npm run test:docker`) runs on GitHub's
`ubuntu-latest` runner (rootful Docker Engine 28.0.4 on Ubuntu 24.04 when
it was added) and checks that an allowed host answers through the broker,
that other hosts get a 403, that bypassing the proxy or dialling a raw IP
Expand Down
11 changes: 10 additions & 1 deletion src/cli/doctor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { describe, it, expect } from 'vitest';
import * as os from 'node:os';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { buildEnvironment, parseDoctorArgs, runDoctorCommand } from './doctor';
import { buildEnvironment, parseDoctorArgs, runDoctorCommand, toolNames } from './doctor';
import { satisfiesRange } from './versionRange';

describe('satisfiesRange', () => {
Expand Down Expand Up @@ -99,3 +99,12 @@ describe('runDoctorCommand', () => {
expect(String(lines[0])).toContain('lousho doctor');
});
});

describe('toolNames', () => {
it('collects names from tools, nested arrays and records; none for a function or a nameless value', () => {
expect(toolNames([{ name: 'a' }, [{ name: 'b' }], { group: { name: 'c' }, other: [{ name: 'd' }] }])).toEqual(['a', 'b', 'c', 'd']);
expect(toolNames(() => [])).toEqual([]);
expect(toolNames(null)).toEqual([]);
expect(toolNames({ name: 42 })).toEqual([]);
});
});
2 changes: 1 addition & 1 deletion src/cli/doctor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ function readSdkManifest(): SdkManifest {
}

/** The names of a createAgent() `tools` option: tools, arrays and records of them (nested); none for a function of the run. */
function toolNames(tools: unknown): string[] {
export function toolNames(tools: unknown): string[] {
if (Array.isArray(tools)) return tools.flatMap(toolNames);
if (typeof tools !== 'object' || tools === null) return [];
const { name } = tools as { name?: unknown };
Expand Down
Loading
Loading