Skip to main content
An agent can be defined as a directory. loadAgentDir() reads it and calls createAgent() with the options the files describe, so it returns exactly what createAgent() returns. The typed code API stays the source of truth: a directory is a different way to write the same options, and you can switch to code at any time without a rewrite.
Security: loading an agent directory executes its code (agent.ts, everything in tools/). Only load directories you trust. No sandboxing is implied; the files run with your process’s full permissions.

Layout

Only one config file may exist. Files inside tools/ and the entries of skills/ and subagents/ are read in sorted order, so loading is deterministic.

Config file

Unknown keys are an error with a suggestion ('modle' (did you mean 'model'?)).

Tools

Each file in tools/ default-exports a defineTool() tool, and may also export more tools by name (or an array of tools). Other exports are ignored, but a file that exports no tool at all is an error, as are two tools with the same name (both files are named in the message).

Skills

skills/ accepts what loadSkills() accepts: skills/<name>/SKILL.md and skills/<name>.md, each with a description in its frontmatter. See Skills.

Sub-agents

Every directory in subagents/ is itself an agent directory (and can have its own subagents/). It must have a description in its config. The parent gets a delegate_to_<name> tool that runs the sub-agent with the task text and returns its final answer. A sub-agent uses its own model if it sets one, otherwise it inherits the parent’s; a provider override passed to loadAgentDir() reaches all of them.

Channels

Each file in channels/ default-exports a channel made with defineChannel() or a built-in factory (webhookChannel(), httpChannel(), slackChannel()). The channel’s name is the one it sets, else the file name. A file that does not export a channel fails with LOUSHO_CHANNEL_INVALID naming the file. resolveAgentDir() returns them as channels (and their names as manifest.channels); loadAgentDir() does not mount them. The node server (createDeployedServer(agent, { channels })) mounts them under /channels next to the chat routes; with your own server, use mountChannels():
lousho dev mounts them too, and lousho build deploys them (see below).

Memory

Each file in memory/ default-exports a memory slot: the result of defineMemory({ ... }), or the same options without a name, in which case the file name is the slot name. Unlike schedules and channels, slots are part of the agent: loadAgentDir() passes them to createAgent({ memory }), so the remember_<name> / recall_<name> tools and recall into the prompt work with no extra code, and manifest.memory lists their names. Each slot uses its own provider. A file that does not export a slot fails with LOUSHO_MEMORY_INVALID naming the file. A memory override passed to loadAgentDir(dir, { overrides }) is merged with the directory’s slots by name: the override wins a name clash.

How it maps to createAgent()

resolveAgentDir() returns the assembled options and a manifest, which is handy for tests and tooling:

Overrides

The second argument takes the same options as createAgent() and wins over the files. Use it to swap the model in tests:
tools and skills overrides replace the discovered ones (they are not merged). A provider/model string in a file is dropped when you override provider, so the instance is used as-is.

Moving from files to code

Call resolveAgentDir(), print config, and paste what you need into a createAgent() call; or load the directory and override just the parts you want to own in code. Tools are plain defineTool() values, so a tool file can be imported from code unchanged.

Loading TypeScript

.ts files are loaded with a dynamic import(), so the process must already run under a TypeScript loader: npx tsx your-script.ts (or ts-node, bun, deno, or Node’s built-in type stripping). Without one, loading fails with an error that says so. Compile the directory first, or use .js/.mjs tools with an agent.json / agent.yaml config, which work everywhere. A .js tool using import syntax needs "type": "module" in the nearest package.json (or the .mjs extension).

Run it with lousho dev

Serves the chat UI and POST /chat for the directory and reloads it when instructions.md, the config file, tools/, skills/ or subagents/ change (see lousho dev for the details). A tool file is imported afresh on each reload, so an edit to tools/*.ts takes effect on the next message. A failed reload (a syntax error, an empty instructions.md) is logged and shown in the chat page, and the previous agent keeps answering. The directory’s channels/ are mounted under /channels and its schedules/ are started, and a reload swaps both: the old schedules are stopped before the new ones start, so no timer or route outlives its file. Pass --no-schedules to mount the channels but not fire the crons (see Schedules in dev).

Deploy it with lousho build

The agent directory is the unit of deployment: the built server loads it with resolveAgentDir() at start-up, starts its schedules/ and mounts its channels/ under /channels, and prints which it found. The code files (the config, tools/, schedules/, channels/, memory/, and the same in each sub-agent) are bundled to dist/agent/**.js, and instructions.md, skills/ and JSON/YAML config are copied next to them, so the server needs no TypeScript loader, sources or node_modules. See Deployment. The Cloudflare Worker target takes spec files only.

What is not covered

lousho mcp still takes an agent spec file (Configuration), not a directory.