Skip to content

[M3b] Cloudflare Worker target: build an agent directory with TypeScript tools #224

Description

@LinuxDevil

Goal

lousho build ./my-agent --target=cloudflare-worker works for an agent directory (config, instructions.md, tools/*.ts, skills/), as it already does for node-server and docker. Today the Worker target takes spec files only, so any custom tool forces users off Workers. Completes the "Edge runtime" row (AUDIT-2 section 2, our-report row 38) with M3a. Opus because the generated module must reproduce resolveAgentDir() without a filesystem.

Current state

  • src/deploy/adapters/cloudflare.ts:286-295 (scaffold) calls loadAgentSpecForDeploy(agentPath) (imported from ./node-server, line 38) and never checks isAgentDir(). A directory fails in src/deploy/adapters/node-server.ts:42-58 with "unsupported agent config '...': expected an AgentSpec .yaml/.yml/.json file" (LOUSHO_DEPLOY_FAILED).
  • src/cli/build.ts:29 already advertises lousho build <agent-dir|spec> --target=<name> for every target.
  • Node path: node-server.ts:196 if (isAgentDir(agentPath)) return scaffoldAgentDir(...); the generated server calls resolveAgentDir() at run time (agentDirVariant, 91-102). Helpers in src/deploy/adapters/node-server-dir.ts: isAgentDir (27), writeAgentDirPointer (32-38, rejects a directory with no instructions.md or agent.*), agentDirEntries (63-67), copyAgentDirAssets (70-77).
  • resolveAgentDir() cannot run on a Worker: src/agentDir/fsUtil.ts:1 imports node:fs; loadTools.ts:1 and readConfig.ts:1 import node:path; importModule.ts:1-2 imports node:async_hooks and node:url and does await import(pathToFileURL(file).href) (46-47); src/skills/loadSkills.ts:1-2 imports node:fs/node:path. None is in NODE_SHIMMED_IMPORTERS (src/deploy/bundle.ts:103), so the leak check (cloudflare.ts:255-261) fails such a bundle.
  • What resolveWith assembles (src/agentDir/loadAgentDir.ts:162-217): name, instructions, provider/model, tools (from loadTools, plus sub-agent delegate tools), memory, skills, maxSteps, toolConcurrency, projectInstructions; plus schedules and channels returned separately. Config keys: src/agentDir/readConfig.ts:40-49. Layout doc: loadAgentDir.ts:231-243. Skill is plain data { name, description, content } (src/skills/defineSkill.ts:21-25).
  • src/deploy/bundle.ts:61-77 (sdkRuntimePlugin) already maps @lousho/build-ai-agent imports in agent code to the SDK source, and @lousho/build-ai-agent/deploy-runtime-worker to src/deploy/runtime.worker.ts.
  • Worker runtime: workerAgent(spec, env) (runtime.worker.ts:149-159) builds createAgent({ name, prompt, provider, tools, store: workerStore(env) }); workerResolvers(env) resolves a provider type with the key from env (providerEnvKey, 89-91). The generated worker.ts template is WORKER_TS (cloudflare.ts:201-236).
  • Docs: docs/deployment.md:24-46 ("Agent directories"; line 45-46 "The Cloudflare Worker target still takes spec files only."). docs/agent-directories.md describes the layout.

Scope

In:

  • CloudflareWorkerAdapter.scaffold: when isAgentDir(agentPath), generate, at build time on Node:
    • agent.module.ts: static imports of every tools/*.{ts,js,mjs,mts} file (same selection rules as src/agentDir/loadTools.ts:7-8, 57-59) and of an agent.ts/agent.js config if present; the JSON/YAML config, instructions.md and the skills (read with the existing loadSkills on Node) embedded as JSON literals. It exports agentDir: WorkerAgentDir = { name, instructions, model?, maxSteps?, toolConcurrency?, toolModules: [mod0, mod1, ...], skills, config? }.
    • worker.ts from a second template that imports agentDir and exports fetch calling a new runtime function handleWorkerAgentDirRequest(request, env, agentDir) (same routes and bearer auth as handleWorkerRequest). No scheduled export (schedules are out, below).
    • wrangler.toml as today (no crons for directories).
  • In runtime.worker.ts: workerAgentFromDir(dir: WorkerAgentDir, env) collects defineTool exports from toolModules with the same rules as loadTools (any export that is a tool descriptor; duplicate names throw LOUSHO_AGENT_DIR_INVALID), resolves model: 'provider/name' through workerResolvers(env) (only providers in WORKER_SUPPORTED_PROVIDERS), and calls createAgent({ name, instructions, provider, tools, skills, maxSteps, toolConcurrency, store: workerStore(env) }). Factor the tool-collection rule out of loadTools.ts into a Node-free helper both use, so the two cannot drift.
  • Rejected at scaffold, with LOUSHO_DEPLOY_FAILED naming the folder and pointing to --target=node-server: subagents/, schedules/, channels/, memory/, projectInstructions: true in config, and a config that names an unsupported provider. Decision: these need a filesystem, a long-running process or more Worker plumbing than one ticket; the error tells the user exactly what to remove.
  • Tools are bundled with the existing plugins; a tool that imports a Node builtin fails the leak check, and the error message is extended to name the agent-directory file that imported it when it can be determined from the esbuild metafile.
  • Docs: docs/deployment.md "Agent directories" section: replace line 45-46 with what the Worker supports and rejects; add a short example (npx lousho build ./my-agent --target=cloudflare-worker). docs/agent-directories.md: one sentence linking to it. No heading changes.
  • CHANGELOG entry.

Out:

  • OpenRouter and http on Workers: M3a.
  • Sub-agents, schedules (cron triggers from schedules/), channels and memory slots from a directory on Workers: a follow-up ticket if users ask; open an issue and link it.
  • A code-first createAgent() module as a build input: not planned.

Acceptance criteria

  • Fixture src/deploy/adapters/__fixtures__/worker-agent-dir/ with agent.json (model: 'mock/test'), instructions.md, tools/echo.ts (a defineTool importing zod and @lousho/build-ai-agent) and skills/notes/SKILL.md.
  • src/deploy/adapters/cloudflare.dir.test.ts: scaffold plus build of the fixture under withBuildLock passes the Node-builtin leak check; the bundle's /chat (in-process handler.fetch, as cloudflare.test.ts:445-450 does) runs a scripted mock turn that calls echo and returns its result; the skill is visible to the model (the load_skill tool or the skills list in the system prompt, whichever createAgent uses).
  • Rejection tests: a directory with subagents/, schedules/, channels/ or memory/ fails with a message naming the folder; a tool importing node:fs fails the leak check with the file named.
  • loadTools tests still pass after the shared helper is extracted.
  • docs/deployment.md and docs/agent-directories.md updated; docs:verify-snippets and docs:llms:check pass.
  • CHANGELOG: "Cloudflare Worker target builds agent directories (config, instructions, TypeScript tools, skills)."
  • All BRIEF-2.md verification commands pass.

Live test

None: this ticket spends nothing.

Dependencies

M3a must merge first (same files: cloudflare.ts, runtime.worker.ts, cloudflare.test.ts, docs/deployment.md; and the provider list this ticket validates against). G6 (Cloudflare Workers page) should be updated by whichever merges second.

Notes for the implementer

  • Workers have no filesystem and no dynamic import() of arbitrary paths: everything must be a static import resolved by tsup at build time. Generate import specifiers relative to the scaffold output and copy the agent directory into the output (as copyAgentDirAssets does for node-server) so relative imports inside tools keep working.
  • Embedding: use JSON.stringify for instructions, config and skills; never template raw file contents into TypeScript source.
  • An agent.ts config that constructs a provider instance is allowed (it becomes provider); a config model string must use a Worker-supported provider.
  • The esbuild metafile is available from tsup with metafile: true; use its inputs[...].imports to find which agent file pulled a builtin. If that proves unreliable, name the builtin and list the agent's tool files instead; do not block on it.
  • Keep scaffold() for spec files byte-for-byte unchanged; existing cloudflare tests must pass without edits beyond M3a's.

Round 2 ticket M3b. Before starting, read the agent brief (worktree rules, verification list, live-test budget) and the plan. One ticket is one pull request; put Closes #<this issue> in it.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    model:opusRun loop, security or API design; needs Opusround-2Round 2 plan ticketwave-2Round 2, wave 2

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions