Skip to content

LOU-I: Deploy portability & documentation - #10

Merged
LinuxDevil merged 8 commits into
mainfrom
loushy/lou-i
Sep 28, 2026
Merged

LinuxDevil merged 8 commits into
mainfrom
loushy/lou-i

Conversation

@LinuxDevil

Copy link
Copy Markdown
Owner

Summary

Implements all 7 tickets from LOU-I (Phase 2-3, final epic), giving Loushy a real multi-target deployment story and closing the README's broken-doc-link credibility gap:

  • LOU-I1: DeploymentAdapter interface (scaffold()/build()/describe()) + registry, loushy build --target=<name> --agent=<path> CLI.
  • LOU-I2: Node-server adapter — wraps AgentExecutor in an HTTP server (/health, /chat), carrying forward LOU-H6's localhost-only-by-default binding and request body size cap.
  • LOU-I3: Cloudflare Worker adapter — browser-platform ESM bundle with zero node: imports, generated wrangler.toml.
  • LOU-I4: Docker adapter — delegates to the node-server adapter's scaffold/build (no duplicated logic), generates a Dockerfile.
  • LOU-I5: TypeDoc-generated API reference (docs/api/).
  • LOU-I6: Real content for every README "Documentation"/"Examples" link — zero broken links.
  • LOU-I7: scripts/verify-docs-snippets.ts — packs the SDK for real, installs it into a throwaway project, and executes every Quick Start snippet against the actual built package.

A real pre-existing production bug found and fixed

While verifying LOU-I7's snippets against a real, non-test consumer, a genuine bug on main surfaced: importing the package root threw at runtime ("Vitest failed to access its internal state") for any real application, because defineEval() value-imported vitest at module scope, bundling the test runner into the published package. Fixed by resolving vitest lazily at call time instead of at import time. Verified with a real reproduction: packed the SDK, installed the tarball into an isolated temp project, and confirmed a plain node script (no test runner involved) can now import @loushy/build-ai-agent cleanly.

QA-caught AC gap, also fixed

QA found the Cloudflare adapter built and deployed correctly via real wrangler tooling, but loushy build's own lifecycle never measured or reported bundle size against Workers' size limit — an explicit AC. Fixed with real fs.statSync/gzip-based measurement in build()/describe(), cross-checked against wrangler deploy --dry-run's own reported size (247.1 KB raw / 46.0 KB gzip on both sides, consistent) and against the current documented Workers limit (64 MiB uncompressed, verified via Cloudflare's own changelog rather than assumed from memory).

Validation results

  • npx vitest run: 680 passing, 3 skipped (2 pre-existing Docker-daemon-unavailable skips + 1 new). 3 known pre-existing provider-suite failures (missing optional peer deps) — unrelated to this epic.
  • npx tsc --noEmit: clean.
  • Real, not mocked, verification: started the built dist/server.js as an actual background process and curled /health+/chat for a genuine AgentExecutor response; ran npx wrangler dev/wrangler deploy --dry-run for real against the Cloudflare adapter's output (wrangler was available in this environment); Docker's daemon wasn't running here, so its integration test skips gracefully (matching the pattern already established in LOU-F6) while the generated Dockerfile content and delegation-not-duplication are verified directly.

Known, documented, out-of-scope limitation

QA also surfaced a related but separate pre-existing issue: the package root eagerly imports all three provider SDKs even though they're marked optional in peerDependenciesMeta, so importing without all three installed still throws — contradicting the "optional" framing. Not introduced by this epic and out of scope for it; worth a follow-up ticket.

🤖 Generated with Claude Code

LinuxDevil and others added 8 commits September 28, 2026 22:39
- src/deploy/types.ts: DeploymentAdapter {scaffold, build, describe} plus a
  Map-backed registerAdapter/getAdapter/listAdapters registry.
- src/cli/build.ts: `loushy build --target=<name> --agent=<path> [--out=<dir>]`
  using bin/loushy.js's --flag=value / --flag value convention; unknown
  target -> `Error: unknown target "<t>"` on stderr + exit 1; otherwise
  scaffold() -> build() -> print describe(). Registers a 'stub' adapter that
  records its call order.
- bin/loushy.js: first-arg subcommand switch (dev | build).
- tsup: new cli/build entry.
- Tests: direct runBuild() tests + bin subprocess smoke test.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- src/deploy/adapters/node-server.ts: scaffold() loads + validates an
  AgentSpec (.yaml/.yml/.json, via LOU-H9's loadSpec) and writes
  agent.config.js, server.ts and package.json; build() bundles server.ts
  with tsup's programmatic build() into a self-contained dist/server.js;
  describe() -> 'node dist/server.js'.
- Generated server.ts calls the static AgentExecutor.execute() directly
  (GET /health, POST /chat) and keeps loushy dev's security properties:
  127.0.0.1 by default with --host/HOST opt-in, 1MB body cap (413).
- src/deploy/runtime.ts + specExecution.ts: runtime the scaffold imports via
  a virtual '@loushy/build-ai-agent/deploy-runtime' specifier, resolved at
  build time to the SDK's own source (src/deploy/bundle.ts plugin).
- Split NoopSandbox + SandboxAdapter into src/security/sandboxCore.ts so the
  execution core no longer imports dockerode (whose native ssh2 subtree
  cannot be bundled); sandbox.ts re-exports it, public API unchanged.
- Export resolveSpecTool from specToAgent; explicit
  registerBuiltInAdapters() (a top-level side-effect registration was
  tree-shaken out of dist/cli/build.js).
- package.json: ship src (runtime sources the adapters bundle from), tsup
  as an optional peer dependency (loaded lazily only by `loushy build`).
- Tests: real scaffold + build + subprocess server integration test
  (/health, /chat, 400, 413, 127.0.0.1 default, --host opt-in) and a bin
  subprocess test building node-server end to end.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- src/deploy/adapters/cloudflare.ts: scaffold() writes worker.ts (module
  Worker exporting fetch(request, env) that wraps the static
  AgentExecutor.execute(): GET /health, POST /chat, 1MB body cap),
  agent.config.js and wrangler.toml (name/main/compatibility_date,
  no_bundle so the tsup output is uploaded as built); rejects tools and
  providers that cannot run on Workers. build() bundles with tsup
  (format esm, platform browser) and fails if any `node:` specifier leaks
  into dist/worker.js. describe() -> 'wrangler deploy'.
- src/deploy/runtime.worker.ts: Node-free runtime (mock provider, env-binding
  API keys, current-date/day-name tools).
- src/deploy/shims/sandboxCore.worker.ts + workerSandboxShimPlugin: swaps
  the host-exec NoopSandbox (node:child_process) for a Worker-safe stub in
  the Worker bundle only.
- wrangler added as a devDependency. Tests: wrangler.toml/scaffold unit
  tests, zero-`node:` grep of the real bundle, bundle fetch() exercised
  in-process, plus (guarded on wrangler being installed, LOU-F6 style)
  `wrangler deploy --dry-run` and `wrangler dev` on local workerd.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- src/deploy/adapters/docker.ts: scaffold() calls NodeServerAdapter.scaffold()
  directly and adds a Dockerfile (node:20-slim, WORKDIR /app, COPY
  package.json, RUN npm install --omit=dev, COPY dist/, EXPOSE 3000,
  CMD node dist/server.js) plus .dockerignore; build() calls
  NodeServerAdapter.build(); describe() -> 'docker build -t loushy-agent .
  && docker run -p 3000:3000 loushy-agent'. The image sets HOST=0.0.0.0 as
  the explicit bind opt-in a container needs for -p to reach the server.
- Registered as 'docker' in registerBuiltInAdapters().
- Tests: Dockerfile content, spy assertions that scaffold()/build()
  delegate to NodeServerAdapter; real docker build/run integration test
  behind LOU-F6's dockerode-ping describe.skipIf guard (skipped here: no
  Docker daemon running).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- typedoc added as a devDependency; typedoc.json (entryPoints
  src/index.ts, out docs/api, excludePrivate, excludeInternal).
- "docs:build": "typedoc" script. `npm run docs:build` exits 0 (0 errors,
  2 pre-existing @variablename tag warnings in src/flows/inputs.ts) and
  generates pages for AgentBuilder, AgentExecutor, createAgent,
  ToolRegistry, LLMProviderRegistry, FlowBuilder, loadSpec, specToAgent,
  registerAdapter, DeploymentAdapter, etc.
- docs/api/ is generated output and is gitignored.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- README.md had no "## Documentation" section at all (the header nav's
  #documentation anchor pointed nowhere) and its "## Examples" section had
  no links. Added a Documentation section linking five new pages, pointed
  Examples at the real examples/README.md index (LOU-H11), added a Quick
  Start pointer to the verified docs page, and replaced the API Reference
  link to the gitignored ./dist/index.d.ts with docs/api-overview.md +
  `npm run docs:build`.
- New docs, written from what the codebase actually does: installation.md,
  quick-start.md, configuration.md, deployment.md (LOU-I1..I4 targets),
  api-overview.md.
- docs/docs-links.test.ts: every relative link under README's
  Documentation/Examples headings (and inside docs/*.md) resolves from the
  repo root to an existing, non-empty file; nav anchors have headings.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
No docs framework / embed mechanism (CodeSandbox, StackBlitz) exists in
this repo and the ticket rules out introducing one for this ticket alone,
so the deliverable is the verifiable core: every Quick Start snippet is
proven to type-check and run against the locally built package. The
interactive embed UI is deferred as a follow-up.

- scripts/verify-docs-snippets.ts (+ `npm run docs:verify-snippets`):
  extracts ```ts/```typescript fences from docs/quick-start.md (or given
  files; `no-verify` opts a fence out), builds + `npm pack`s the SDK,
  installs the tarball into a throwaway temp project (LOU-H4's pattern),
  runs strict `tsc --noEmit` (module nodenext) over all snippets, then
  runs each with tsx (credential env vars stripped -> deterministic mock
  path). Non-zero exit on any type or runtime failure, reported per
  snippet with its markdown file:line. All 5 Quick Start snippets pass.
- Fix found by the verifier: src/evals/defineEval.ts value-imported
  vitest, so tsup bundled vitest into dist/index.js/.mjs and importing the
  package root threw "Vitest failed to access its internal state" in any
  non-test process. defineEval() now resolves test/expect at call time
  from the running vitest worker (globalThis.__vitest_index__, falling
  back to vitest globals) with only a type import; its signature and
  behaviour under vitest are unchanged (src/evals tests pass).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…orkers free-tier limit

QA found the cloudflare-worker adapter's build() never measured or
reported bundle size against Cloudflare Workers' script size limit, even
though it's an explicit LOU-I3 AC and wrangler dry-run only prints it to
an external console loushy build never surfaces.

- Add measureBundleSize() (raw fs.statSync + zlib.gzipSync) and
  WORKER_SIZE_LIMIT_BYTES, documented against Cloudflare's 2026-09-04
  changelog (64 MiB uncompressed, unified across Free/paid plans,
  replacing the old 3 MB/10 MB compressed limits).
- build() measures the bundle after the node: builtin check and warns
  (not throws) via console.warn if it's over budget.
- describe(outDir) reports the bundle's raw/gzip size and a pass/WARNING
  verdict when a built bundle exists at outDir.
- Tests: cross-check the adapter's measurement against fs.statSync ground
  truth, prove the WARNING path fires for a genuinely oversized on-disk
  bundle, and cross-check against wrangler deploy --dry-run's own
  "Total Upload" figure for the same build (247.12 KiB raw / 46.05 KiB
  gzip from wrangler vs. 247.1 KB / 46.0 KB measured).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant