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 intools/). Only load directories you trust. No sandboxing is implied; the files run with your process’s full permissions.
Layout
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 intools/ 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 insubagents/ 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 inchannels/ 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 inmemory/ 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 ascreateAgent() 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
CallresolveAgentDir(), 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
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
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.