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
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.
Goal
lousho build ./my-agent --target=cloudflare-workerworks for an agent directory (config,instructions.md,tools/*.ts,skills/), as it already does fornode-serveranddocker. 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 reproduceresolveAgentDir()without a filesystem.Current state
src/deploy/adapters/cloudflare.ts:286-295(scaffold) callsloadAgentSpecForDeploy(agentPath)(imported from./node-server, line 38) and never checksisAgentDir(). A directory fails insrc/deploy/adapters/node-server.ts:42-58with "unsupported agent config '...': expected an AgentSpec .yaml/.yml/.json file" (LOUSHO_DEPLOY_FAILED).src/cli/build.ts:29already advertiseslousho build <agent-dir|spec> --target=<name>for every target.node-server.ts:196if (isAgentDir(agentPath)) return scaffoldAgentDir(...); the generated server callsresolveAgentDir()at run time (agentDirVariant, 91-102). Helpers insrc/deploy/adapters/node-server-dir.ts:isAgentDir(27),writeAgentDirPointer(32-38, rejects a directory with noinstructions.mdoragent.*),agentDirEntries(63-67),copyAgentDirAssets(70-77).resolveAgentDir()cannot run on a Worker:src/agentDir/fsUtil.ts:1importsnode:fs;loadTools.ts:1andreadConfig.ts:1importnode:path;importModule.ts:1-2importsnode:async_hooksandnode:urland doesawait import(pathToFileURL(file).href)(46-47);src/skills/loadSkills.ts:1-2importsnode:fs/node:path. None is inNODE_SHIMMED_IMPORTERS(src/deploy/bundle.ts:103), so the leak check (cloudflare.ts:255-261) fails such a bundle.resolveWithassembles (src/agentDir/loadAgentDir.ts:162-217):name,instructions,provider/model,tools(fromloadTools, plus sub-agent delegate tools),memory,skills,maxSteps,toolConcurrency,projectInstructions; plusschedulesandchannelsreturned separately. Config keys:src/agentDir/readConfig.ts:40-49. Layout doc:loadAgentDir.ts:231-243.Skillis plain data{ name, description, content }(src/skills/defineSkill.ts:21-25).src/deploy/bundle.ts:61-77(sdkRuntimePlugin) already maps@lousho/build-ai-agentimports in agent code to the SDK source, and@lousho/build-ai-agent/deploy-runtime-workertosrc/deploy/runtime.worker.ts.workerAgent(spec, env)(runtime.worker.ts:149-159) buildscreateAgent({ name, prompt, provider, tools, store: workerStore(env) });workerResolvers(env)resolves a provider type with the key fromenv(providerEnvKey, 89-91). The generatedworker.tstemplate isWORKER_TS(cloudflare.ts:201-236).docs/deployment.md:24-46("Agent directories"; line 45-46 "The Cloudflare Worker target still takes spec files only.").docs/agent-directories.mddescribes the layout.Scope
In:
CloudflareWorkerAdapter.scaffold: whenisAgentDir(agentPath), generate, at build time on Node:agent.module.ts: static imports of everytools/*.{ts,js,mjs,mts}file (same selection rules assrc/agentDir/loadTools.ts:7-8, 57-59) and of anagent.ts/agent.jsconfig if present; the JSON/YAML config,instructions.mdand the skills (read with the existingloadSkillson Node) embedded as JSON literals. It exportsagentDir: WorkerAgentDir = { name, instructions, model?, maxSteps?, toolConcurrency?, toolModules: [mod0, mod1, ...], skills, config? }.worker.tsfrom a second template that importsagentDirand exportsfetchcalling a new runtime functionhandleWorkerAgentDirRequest(request, env, agentDir)(same routes and bearer auth ashandleWorkerRequest). Noscheduledexport (schedules are out, below).wrangler.tomlas today (no crons for directories).runtime.worker.ts:workerAgentFromDir(dir: WorkerAgentDir, env)collectsdefineToolexports fromtoolModuleswith the same rules asloadTools(any export that is a tool descriptor; duplicate names throwLOUSHO_AGENT_DIR_INVALID), resolvesmodel: 'provider/name'throughworkerResolvers(env)(only providers inWORKER_SUPPORTED_PROVIDERS), and callscreateAgent({ name, instructions, provider, tools, skills, maxSteps, toolConcurrency, store: workerStore(env) }). Factor the tool-collection rule out ofloadTools.tsinto a Node-free helper both use, so the two cannot drift.LOUSHO_DEPLOY_FAILEDnaming the folder and pointing to--target=node-server:subagents/,schedules/,channels/,memory/,projectInstructions: truein 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.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.Out:
httpon Workers: M3a.schedules/), channels and memory slots from a directory on Workers: a follow-up ticket if users ask; open an issue and link it.createAgent()module as a build input: not planned.Acceptance criteria
src/deploy/adapters/__fixtures__/worker-agent-dir/withagent.json(model: 'mock/test'),instructions.md,tools/echo.ts(adefineToolimportingzodand@lousho/build-ai-agent) andskills/notes/SKILL.md.src/deploy/adapters/cloudflare.dir.test.ts: scaffold plus build of the fixture underwithBuildLockpasses the Node-builtin leak check; the bundle's/chat(in-processhandler.fetch, ascloudflare.test.ts:445-450does) runs a scriptedmockturn that callsechoand returns its result; the skill is visible to the model (theload_skilltool or the skills list in the system prompt, whichevercreateAgentuses).subagents/,schedules/,channels/ormemory/fails with a message naming the folder; a tool importingnode:fsfails the leak check with the file named.loadToolstests still pass after the shared helper is extracted.docs/deployment.mdanddocs/agent-directories.mdupdated;docs:verify-snippetsanddocs:llms:checkpass.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
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 (ascopyAgentDirAssetsdoes for node-server) so relative imports inside tools keep working.JSON.stringifyfor instructions, config and skills; never template raw file contents into TypeScript source.agent.tsconfig that constructs a provider instance is allowed (it becomesprovider); a configmodelstring must use a Worker-supported provider.metafile: true; use itsinputs[...].importsto 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.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; putCloses #<this issue>in it.