loopengine 0.0.14 → 0.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,40 +2,35 @@
2
2
 
3
3
  [![CI](https://github.com/evanyan22/loopengine/actions/workflows/ci.yml/badge.svg)](https://github.com/evanyan22/loopengine/actions/workflows/ci.yml)
4
4
 
5
- LoopEngine is a runtime for defining and running multiple AI agents through a
6
- transparent ReAct loop. Write an agent as a plain config object — persona,
7
- tools, permission rules — and run it over the CLI, an HTTP API, or both. No
8
- chain DSL, no hidden control flow: `run-agent.ts` is a single readable
9
- function you can step through top to bottom.
5
+ LoopEngine is a runtime for building AI agents: a persona, a set of tools,
6
+ and permission rules, run through a transparent ReAct loop. No chain DSL,
7
+ no hidden control flow — `run-agent.ts` is a single function you can read
8
+ top to bottom. Run agents over a CLI, an HTTP API, or both.
9
+
10
+ Once you define an agent, the loop turns it into something you can put in
11
+ front of real users or real systems:
12
+
13
+ - **Callable over CLI or HTTP** — a script you run locally, or a real
14
+ service other apps can hit.
15
+ - **Does real work, safely** — tools can hit a database, send emails, call
16
+ GitHub/Slack, anything with an `execute` function. Permission rules gate
17
+ what happens without a human: safe reads auto-run, risky actions (a
18
+ refund, a send) get approval or are denied outright, so you can wire up
19
+ powerful tools without trusting the model blindly.
20
+ - **Remembers** — conversations persist across requests, so a caller can
21
+ send one message, get a reply, and continue later.
22
+ - **Multi-tenant out of the box** — the same deployed agent can serve
23
+ different customers/orgs with different permission levels, with no
24
+ forking required.
25
+ - **Composable into bigger agents** — drop one agent's folder under
26
+ another's `subagents/` and it becomes a tool the parent can delegate
27
+ to, own tools/rules/loop intact. No chain DSL for orchestration either:
28
+ the parent's model just decides when to call it, like any other tool.
29
+
30
+ So the payoff: you write one `AgentConfig`, and get a deployable,
31
+ persistent, permission-safe service — not just a prompt-and-response demo.
10
32
 
11
- It's built on six small, independently-published packages that each solve
12
- one piece of the agent-loop problem:
13
-
14
- | Package | Responsibility |
15
- |---|---|
16
- | [`actauth`](https://www.npmjs.com/package/actauth) | Rule-based permission gating (allow/ask/deny) with human-approval hooks |
17
- | [`contextclip`](https://www.npmjs.com/package/contextclip) | Context-window budget tracking with staged recovery |
18
- | [`reflowkit`](https://www.npmjs.com/package/reflowkit) | Retries prompt-too-long / truncated-output failures |
19
- | [`sessionknit`](https://www.npmjs.com/package/sessionknit) | Durable, DAG-shaped session log with crash-interruption detection |
20
- | [`skillgarden`](https://www.npmjs.com/package/skillgarden) | `SKILL.md`-based skill discovery and lazy loading |
21
- | [`toollane`](https://www.npmjs.com/package/toollane) | Schedules approved tool calls into parallel/solo execution lanes |
22
-
23
- `run-agent.ts` wires the first five together — `sessionknit` sits one layer
24
- out, in `session-store.ts`, since it persists what `run-agent.ts` produces
25
- rather than participating in the loop itself. Everything else in this repo
26
- — agent definitions, channel adapters — is built on top of those.
27
-
28
- A seventh package, [`mcpplug`](https://www.npmjs.com/package/mcpplug), sits
29
- one layer further out still: it's not part of the loop either, just a
30
- `ToolSource` an agent's own config can draw from — `loadTools()` returns
31
- `ToolDefinition[]`, the exact shape `AgentConfig.tools` already expects, so
32
- its output merges straight in with no adapter code. See "External tool
33
- gateways" below.
34
-
35
- ## Creating a new project
36
-
37
- The fastest way to get a running agent server, in your own repo, is
38
- [`create-loopengine`](https://www.npmjs.com/package/create-loopengine):
33
+ ## Quick start
39
34
 
40
35
  ```bash
41
36
  npx create-loopengine@latest my-agents
@@ -45,619 +40,354 @@ cp .env.example .env # fill in ANTHROPIC_API_KEY
45
40
  npm run dev # HTTP server on :8787
46
41
  ```
47
42
 
48
- That scaffolds a real, standalone project — its own `package.json`
49
- depending on `loopengine` (not a clone of this repo), an HTTP + CLI
50
- adapter, and one starter agent — rather than a bare library import. `npm
51
- install loopengine` alone (below) only gets you the library; the server,
52
- adapters, and a runnable example are deliberately not part of that
53
- package, which is the gap `create-loopengine` fills. See
54
- [`create-loopengine`'s own README](https://github.com/evanyan22/create-loopengine)
55
- for what the generated project looks like.
56
-
57
- ## Using loopengine as a library
43
+ This scaffolds a standalone project — its own repo, with `loopengine` as
44
+ a dependency, a starter agent, and CLI + HTTP adapters already wired up.
45
+ That's the intended way to use LoopEngine: an independent project you own
46
+ and build agents in, not a library you import into an existing app.
58
47
 
59
- This repo is two things in one: a small library (the loop itself) and a
60
- reference app built on top of it (demo agents, a CLI, an HTTP server). Only
61
- the library half ships to npm:
48
+ ### Define your first agent
62
49
 
63
50
  ```bash
64
- npm install loopengine
51
+ npx loopengine add-agent weather-agent
52
+ # -> Created agents/weather-agent/index.ts
65
53
  ```
66
54
 
67
- `actauth`, `contextclip`, `reflowkit`, `toollane`, `skillgarden`, and the
68
- rest are regular dependencies of `loopengine` itself — installing it alone
69
- already pulls them into `node_modules`, fully resolvable for both imports
70
- and TypeScript types. Install one of them directly only if you want to use
71
- it standalone, outside `runAgent` (e.g. just `toollane`'s parallel/solo
72
- scheduling in your own loop).
73
-
74
- ```ts
75
- import { runAgent, type AgentConfig, type ToolDefinition } from 'loopengine'
76
-
77
- const getWeather: ToolDefinition = {
78
- name: 'get_weather',
79
- description: 'Look up the weather for a city',
80
- input_schema: { type: 'object', properties: { city: { type: 'string' } }, required: ['city'] },
81
- execute: async (input) => `Sunny in ${input.city}`,
82
- }
55
+ That's the fastest way to start: it generates `agents/<name>/index.ts`
56
+ with a working `AgentConfig` stub, so you don't have to memorize the
57
+ folder shape by hand. Or write it yourself — an agent is just an
58
+ `AgentConfig`: `name`, `systemPrompt`, `tools`, `rules`, and a `model`.
59
+ Drop it in a folder under `agents/` — `agents/<name>/index.ts` — and it's
60
+ picked up automatically, no registry to edit, no import to add.
83
61
 
84
- const config: AgentConfig = {
85
- name: 'weather-agent',
86
- systemPrompt: 'You answer questions about the weather.',
87
- tools: [getWeather],
88
- rules: [{ scopePattern: 'default/production/weather-agent', tool: 'get_weather', decision: 'allow' }],
89
- defaultDecision: 'ask',
90
- }
62
+ Most fields don't even need to be written out — they default to a
63
+ conventional path under the agent's own folder:
91
64
 
92
- // bring your own ModelCall, or use the one below
93
- const result = await runAgent(config, myModelCall, "What's the weather in Boston?", [])
94
- console.log(result.text)
95
- ```
65
+ | Field | Defaults to |
66
+ | --- | --- |
67
+ | `rules` | `agents/<name>/actauth.yml` (missing → deny everything) |
68
+ | `tools` | `agents/<name>/tools/index.ts`'s exported `tools` (missing → no tools) |
69
+ | `skillsDirs` | `agents/<name>/skills` (missing → no skills) |
96
70
 
97
- The full exported surface: `runAgent`,
98
- `AgentConfig`/`AgentModelConfig`/`ToolSchema`/`ToolDefinition`,
99
- `FileSessionStore`/`RedisSessionStore`/`createSessionStore`, `VectorIndex`/
100
- `embed`/`cosineSimilarity` (for RAG — see below), `discoverAgents` (scans a
101
- directory for modules exporting `config` — a flat `.ts`/`.js` file, or a
102
- subdirectory with an `index.ts`/`index.js`, the same "a directory can be a
103
- module" convention Node's own `require()` already uses, for an agent whose
104
- own implementation outgrows one file, like `agents/customer-service/` —
105
- plus either that module's own `createModelCall` or an `AgentConfig.model`
106
- to synthesize one from, and builds a name → entry map, keyed by
107
- `AgentConfig.name` — see `agent-registry.ts` for the ~15-line wrapper this
108
- repo's own adapters use), and `createAnthropicModelCall`/
109
- `createOpenAIModelCall`/`createDeepSeekModelCall` (real, ready-to-use
110
- `ModelCall`s — what `AgentConfig.model` builds under the hood). See
111
- `index.ts` for the exact list.
112
-
113
- **Not** part of the package: `agents/*.ts` (this repo's own demo agents),
114
- `agent-registry.ts` (this repo's own name → config lookup table, built on
115
- `discoverAgents` — build your own the same way), `adapters/cli.ts`/
116
- `adapters/http.ts` (one specific CLI/HTTP wiring choice, not imposed on
117
- every consumer), and the `Dockerfile`. Everything below this point
118
- describes that reference app — read it as a worked example of what to
119
- build with the library, not as library API itself.
71
+ There's no `subagents` field to omit — `agents/<name>/subagents/*` is its
72
+ own folder convention, always merged into `tools` on top of whatever the
73
+ table above resolves to. See "Subagents" below.
120
74
 
121
- ## Quick start
122
-
123
- ```bash
124
- npm install
125
- npx tsx adapters/cli.ts --agent file-agent "Summarize examples/file-agent/a.txt and examples/file-agent/b.txt into examples/file-agent/summary.txt."
126
- npx tsx adapters/cli.ts --agent customer-service "order A-1001 arrived broken and wants a refund" # needs DEEPSEEK_API_KEY
127
- npx tsx adapters/cli.ts --agent rag-agent "How does ActAuth record human approval decisions?"
128
- ```
75
+ `rules` is how you gate what a tool can do without approval — see
76
+ "Tool permission and multi-tenancy" below.
129
77
 
130
- `file-agent` and `rag-agent` still use a **simulated**, turn-counting model
131
- call (no API key needed) — see "Wiring a real model" below.
132
- `customer-service` declares `model: { provider: 'deepseek', ... }` and
133
- needs `DEEPSEEK_API_KEY` set in the environment.
134
-
135
- ## Defining your own agent
136
-
137
- An agent is an `AgentConfig` (`agent-config.ts`): a name, a system prompt, a
138
- list of tools, and ActAuth permission rules. There are two ways to add one
139
- under `agents/`, and `agent-registry.ts` (built on `discoverAgents`) finds
140
- either automatically at startup — by `AgentConfig.name`, not the filename
141
- or folder name. Nothing to edit, no import to add, no adapter change,
142
- either way:
143
-
144
- 1. **A flat file**, `agents/<name>.ts` — the default; start here.
145
- `agents/rag-agent.ts` is a complete, working example.
146
- 2. **A folder**, `agents/<name>/index.ts` — for an agent whose
147
- implementation outgrows one file. `discoverAgents` finds a
148
- subdirectory's `index.ts`/`index.js` the same way it finds a flat
149
- file, the same "a directory can be a module" convention Node's own
150
- `require()` resolution already uses. `agents/customer-service/` and
151
- `agents/file-agent/` are complete, working examples.
152
-
153
- Either way, the module must export `config`, plus either its own
154
- `createModelCall` or an `AgentConfig.model` for `discoverAgents` to
155
- synthesize one from (see "Wiring a real model" below) — `discoverAgents`
156
- throws at startup on a module in `agents/` that exports neither (a real,
157
- load-bearing check: a module you forgot to finish wiring up, or an
158
- unrelated `.ts` file that doesn't belong in `agents/` at all, should fail
159
- loudly here rather than silently not showing up):
78
+ The simplest possible agent, `agents/weather-agent/index.ts` — `tools`,
79
+ `rules`, and `skillsDirs` all omitted, so they default to
80
+ `agents/weather-agent/tools/`, `actauth.yml`, and `skills/`:
160
81
 
161
82
  ```ts
162
- import type { AgentConfig } from './agent-config.js'
83
+ import type { AgentConfig } from '../../agent-config.js'
163
84
 
164
85
  export const config: AgentConfig = {
165
- name: 'my-agent',
166
- systemPrompt: 'You are ...',
167
- model: { provider: 'anthropic', model: 'claude-sonnet-5' }, // reads ANTHROPIC_API_KEY from the env
168
- tools: [
169
- {
170
- name: 'do_thing',
171
- description: 'What the model sees when deciding to call this',
172
- input_schema: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] },
173
- execute: async (input) => {
174
- // real implementation — DB call, HTTP request, whatever
175
- return { ok: true }
176
- },
177
- },
178
- ],
179
- rules: [
180
- { scopePattern: 'default/production/my-agent', tool: 'do_thing', decision: 'allow' },
181
- ],
182
- defaultDecision: 'ask', // anything not covered by a rule requires approval
86
+ name: 'weather-agent',
87
+ systemPrompt: 'You answer questions about the weather.',
88
+ model: { provider: 'anthropic', model: 'claude-sonnet-5' }, // reads ANTHROPIC_API_KEY
183
89
  }
184
90
  ```
185
91
 
186
- `agents/file-agent/index.ts` and `agents/customer-service/index.ts` are two
187
- complete, working examples — same `runAgent` loop, entirely different
188
- personas and tools. `agents/customer-service/` shows the folder form fully
189
- filled in — everything specific to one agent lives under its own folder,
190
- not scattered across separate top-level trees connected only by a
191
- matching name (nothing enforces that staying in sync):
192
-
193
- ```
194
- agents/customer-service/
195
- index.ts AgentConfig assembly + tenant/session resolution (tenantFor, sessionIdFor)
196
- actauth.yml ActAuth rules
197
- tools/
198
- index.ts Aggregates the tools below into AgentConfig.tools
199
- lookup_order.ts One file per tool
200
- get_shipment_details.ts
201
- issue_refund.ts
202
- send_email.ts
203
- orders-store.ts Shared in-memory store the tools above read/write
204
- skills/
205
- firecrawl/SKILL.md One <skill-name>/SKILL.md per skill (see "Skills" below)
206
- ```
207
-
208
- `agents/file-agent/` follows the same shape, minus `orders-store.ts` —
209
- its one difference is a Composio-sourced GitHub tool, which isn't in
210
- `tools/` at all since it's fetched dynamically at runtime (see "External
211
- tool gateways" below), not static code.
92
+ See `agents/customer-service/` and `agents/file-agent/` for complete,
93
+ working examples.
94
+
95
+ ## Core concepts
96
+
97
+ ### 1. The Loop
98
+
99
+ `run-agent.ts` is the whole engine — a single, readable ReAct loop with no
100
+ hidden control flow: call the model, act on what it asks for, repeat.
101
+
102
+ 1. Call the model with the conversation so far.
103
+ 2. If it responds with tool calls, each one is checked against
104
+ `AgentConfig.rules` — allowed calls run, denied calls are refused,
105
+ "ask" calls wait on `AgentConfig.approver` (see "Tool permission and
106
+ multi-tenancy" below).
107
+ 3. Approved calls execute and their results feed back into the
108
+ conversation.
109
+ 4. Repeat from step 1 — until the model stops requesting tools
110
+ (`end_turn`) or `AgentConfig.maxTurns` (default 25) is hit, which ends
111
+ the turn with `RunAgentResult.stopReason: 'max_turns'`, a real result
112
+ rather than a thrown error.
113
+
114
+ `runAgent` itself does no I/O — callers own conversation history and the
115
+ model call — which is what lets the same loop serve a one-shot CLI call,
116
+ a long-lived HTTP chat session, and a fully simulated test, unchanged.
117
+
118
+ ### 2. Agents — convention over configuration
119
+
120
+ Beyond the `AgentConfig` shape and folder convention shown in "Define
121
+ your first agent" above, the discovery mechanism is what makes this
122
+ convention-over-configuration: `discoverAgents` scans `agents/` at
123
+ startup, keyed by each module's own `AgentConfig.name` — not the
124
+ filename or folder name — and builds the name → config lookup every
125
+ adapter uses. Nothing to register by hand; a module that exports neither
126
+ `config` nor a way to build a model call fails loudly at startup instead
127
+ of silently not showing up.
128
+
129
+ ### 3. Skills and tools: agent-specific vs. global
130
+
131
+ Both skills and tools follow the same rule: **default to agent-specific,
132
+ promote to global only once something is genuinely shared.**
133
+
134
+ - **Agent-specific** (the default) — lives under that agent's own folder,
135
+ seen only by that agent:
136
+ ```
137
+ agents/<name>/skills/<skill>/SKILL.md
138
+ agents/<name>/tools/<tool>.ts
139
+ ```
140
+ - **Global** — lives at the repo root, seen by every agent that opts in:
141
+ ```
142
+ skills/<skill>/SKILL.md # pointed at by any agent whose skillsDirs includes it
143
+ tools/<tool>.ts # imported explicitly into any agent's tools array
144
+ ```
145
+
146
+ Skills are discovered via `AgentConfig.skillsDirs` (an array of
147
+ directories, `SKILL.md`-based: a short description stays in context, the
148
+ full body loads only when the model invokes it). Tools have no
149
+ auto-discovery — an agent's `tools` array just imports whichever tool
150
+ files it wants, agent-specific or global.
151
+
152
+ Both root directories (`tools/`, `skills/`) are empty today — every skill
153
+ and tool in this repo is agent-specific. Move something there only when a
154
+ second agent actually needs the exact same one, not because it feels
155
+ reusable in theory.
156
+
157
+ **Opting an agent into a global skill or tool** is explicit either way —
158
+ nothing scans the root for you:
212
159
 
213
- ### Smart defaults
214
-
215
- `rules`, `tools`, and `skillsDirs` can all be omitted — each then defaults
216
- to a conventional path under this agent's own folder, `agents/<name>/…`,
217
- instead of something you write out:
160
+ ```ts
161
+ // Global skill: add the root path alongside the agent's own
162
+ skillsDirs: ['agents/my-agent/skills', 'skills']
218
163
 
219
- | Field | Omitted, defaults to | Missing there means |
220
- | --- | --- | --- |
221
- | `rules` | `agents/<name>/actauth.yml` | empty ruleset, **deny** everything (no permission story was ever written — stricter than the inline-array form's `'ask'` default) |
222
- | `tools` | import `agents/<name>/tools/index.{ts,js}`, use its exported `tools` | `[]` — same as an agent with no tools at all |
223
- | `skillsDirs` | `['agents/<name>/skills']` | no skills discovered, no `Skill` tool declared |
224
-
225
- None of these throw over a missing file/folder — only `rules` changes
226
- behavior when it's missing (deny-by-default, not "no rules apply"), since
227
- skipping every tool is safer than silently allowing them all. Pass an
228
- explicit empty value (`[]`, or a `rules` path that doesn't exist) instead
229
- of omitting the field to opt out of a default on purpose — an
230
- *explicitly* wrong path is a real configuration bug and still throws.
231
-
232
- The `actauth.yml` form is close to the shape `examples/actauth.yml` in the
233
- actauth package itself uses, but each rule's `scope` only needs
234
- *tenant/environment* (e.g. `"*/*"`, `"acme-corp/production"`) — no agent
235
- segment, since a loopengine actauth.yml is always one-file-per-agent by
236
- convention. `run-agent.ts` appends `/<name>` to every rule loaded from a
237
- YAML file automatically; writing the agent's own name into every single
238
- rule would be pure repetition. See `agents/customer-service/actauth.yml`.
239
-
240
- An agent that needs to merge tools in from somewhere else too —
241
- `agents/file-agent/index.ts`'s Composio-sourced ones — can't rely on the
242
- `tools` default and sets it explicitly instead.
243
-
244
- ### Parallel tool execution
245
-
246
- When a model requests several tool calls in one turn, `ToolLane` decides
247
- which can run concurrently: mark a tool `safe: true` on its
248
- `ToolDefinition` (read-only, no side effects, no shared mutable state —
249
- `agents/customer-service/tools/lookup_order.ts`) and consecutive safe
250
- calls run together in a parallel lane; anything else gets its own solo
251
- lane. For per-call nuance beyond a tool's static definition (safety that
252
- depends on the specific call's arguments, or varies by agent), set
253
- `AgentConfig.isSafeTool: (call) => boolean` instead — it takes full
254
- precedence over every tool's own `safe` flag when set
255
- (`agents/file-agent/index.ts` uses this form). Neither set: every tool
256
- runs solo.
257
-
258
- ### Human approval
259
-
260
- `AgentConfig.approver` — an `Approver` from `actauth` — is what an `ask`
261
- decision routes to; the default `ConsoleApprover` blocks on stdin, fine
262
- for the CLI but wrong for an unattended server (see
263
- `agents/customer-service/index.ts`'s own auto-approving demo `approver`,
264
- and swap in a real one, e.g. Slack-backed, for production).
265
- `AgentConfig.maxTurns` (default 25) caps model calls in one `runAgent()`
266
- turn — the only thing stopping a model stuck re-requesting the same tool
267
- forever; hitting it ends the turn with `RunAgentResult.stopReason` set to
268
- `'max_turns'`, a real result rather than a thrown error.
269
-
270
- ## Retrieval (RAG)
271
-
272
- RAG is just another tool — the model decides when to call it, `run-agent.ts`
273
- doesn't know or care that "executing" this particular tool means a vector
274
- search instead of a plain HTTP call:
164
+ // Global tool: tools has no auto-discovery, so import it and merge it
165
+ // into the agent's own array by hand
166
+ import { sharedTool } from '../../tools/shared-tool.js'
167
+ import { tools as ownTools } from './tools/index.js'
275
168
 
276
- ```ts
277
- const searchDocs: ToolDefinition = {
278
- name: 'search_docs',
279
- description: 'Search the knowledge base for relevant passages',
280
- input_schema: { type: 'object', properties: { query: { type: 'string' } }, required: ['query'] },
281
- execute: async (input) => {
282
- const hits = index.search(input.query as string, 3) // top-3 chunks
283
- return hits.map((h) => h.text).join('\n\n')
284
- },
285
- }
169
+ tools: [...ownTools, sharedTool]
286
170
  ```
287
171
 
288
- `agents/rag-agent.ts` is a complete example, backed by `vector-index.ts` — a
289
- small, dependency-free in-memory vector index (feature-hashing embeddings,
290
- cosine-similarity search) good enough to prove the retrieval mechanics
291
- end-to-end without wiring up a real embedding API. Swap `embed()` for a real
292
- embedding model, or swap `VectorIndex` for a real vector DB client, and the
293
- `search_docs` tool works unchanged — same "host provides the real
294
- implementation, the package proves the pipeline" pattern as ContextClip's
295
- `Summarizer` or ActAuth's `Approver`. The retrieved text comes back as an
296
- ordinary tool result, subject to the same ActAuth gating, ToolLane
297
- scheduling, and ContextClip budget tracking as any other tool call.
298
-
299
- ## External tool gateways
300
-
301
- A hand-written tool needs an `execute` function you write yourself; an
302
- external SaaS action (list GitHub repos, send a Slack message) needs
303
- someone to own that vendor's auth. Connecting straight to a vendor's own
304
- MCP server means owning its OAuth flow (authorization code, PKCE, token
305
- storage/refresh) yourself, repeated per vendor. Routing through a gateway
306
- like [Composio](https://composio.dev) avoids that — it already holds the
307
- OAuth relationship with 1000+ apps — and [`mcpplug`](https://www.npmjs.com/package/mcpplug)
308
- wraps that gateway behind the same `ToolDefinition` shape as everything
309
- else:
172
+ Tools don't have to be hand-written either. For SaaS actions (GitHub,
173
+ Slack, etc.) where you'd rather not own the vendor's OAuth flow yourself,
174
+ [`mcpplug`](https://www.npmjs.com/package/mcpplug) connects through
175
+ [Composio](https://composio.dev), which already holds the OAuth
176
+ relationship with 1000+ apps, and returns tools in the same
177
+ `ToolDefinition` shape — so they drop into `tools` right alongside your
178
+ own:
310
179
 
311
180
  ```ts
312
181
  import { connectComposioSource } from 'mcpplug'
313
182
 
314
- // Resolved once, at module-eval time — not per runAgent() call. A gateway
315
- // connection is exactly the kind of setup cost runAgent() shouldn't pay on
316
- // every turn; see agents/file-agent/index.ts for the working example this is
317
- // drawn from.
318
183
  const composioTools = await connectComposioSource('composio', {
319
184
  slugs: ['GITHUB_LIST_REPOSITORIES_FOR_THE_AUTHENTICATED_USER'],
320
185
  }).then((source) => source.loadTools())
321
186
 
322
187
  const config: AgentConfig = {
323
188
  name: 'my-agent',
324
- systemPrompt: 'You are ...',
325
189
  tools: [...handWrittenTools, ...composioTools],
326
- rules: [
327
- // mcpplug namespaces each tool `${sourceName}_${slug}` — falls
328
- // through to defaultDecision unless a rule names it explicitly.
329
- { scopePattern: 'default/production/my-agent', tool: 'composio_GITHUB_LIST_REPOSITORIES_FOR_THE_AUTHENTICATED_USER', decision: 'allow' },
330
- ],
190
+ rules: [{ scopePattern: 'default/production/my-agent', tool: 'composio_GITHUB_LIST_REPOSITORIES_FOR_THE_AUTHENTICATED_USER', decision: 'allow' }],
331
191
  defaultDecision: 'ask',
332
192
  }
333
193
  ```
334
194
 
335
- Same ActAuth gating, ToolLane scheduling, and ContextClip budget tracking
336
- as any other tool call — `runAgent` has no idea one `execute` shells out
337
- through Composio instead of touching the filesystem or a vector index.
338
- `agents/file-agent/index.ts` is a complete, working example, verified end-to-end
339
- against a real linked GitHub account. `slugs` is explicit, not
340
- auto-discovered — an agent should only see the Composio actions its own
341
- config actually lists; find them with the `composio` CLI's
342
- `composio tools list <toolkit>` or `composio search "<use case>"`.
195
+ Either kind of tool goes through the same permission gating, parallel
196
+ scheduling, and context-budget tracking. See `agents/file-agent/index.ts`
197
+ for a working example.
343
198
 
344
- ## Running an agent
199
+ ### 4. Subagents — an agent as another agent's tool
345
200
 
346
- **CLI:**
201
+ Drop a folder under `agents/<name>/subagents/<child>/` and `child` becomes
202
+ one of `name`'s tools automatically — no import, no `AgentConfig.tools`
203
+ edit, nothing to register by hand:
347
204
 
348
- ```bash
349
- npx tsx adapters/cli.ts --agent customer-service --session s1 "order A-1001 arrived broken"
350
205
  ```
351
-
352
- **HTTP:**
353
-
354
- ```bash
355
- npx tsx adapters/http.ts
356
- curl -X POST localhost:8787/agents/customer-service/messages \
357
- -H 'content-type: application/json' \
358
- -d '{"customerEmail":"a@example.com","message":"order A-1001 arrived broken"}'
206
+ agents/support-orchestrator/
207
+ index.ts # the orchestrator's own AgentConfig
208
+ subagents/
209
+ billing-agent/index.ts # a full AgentConfig, same shape as a top-level agent
210
+ refunds-agent/index.ts
359
211
  ```
360
212
 
361
- `customerEmail` is specific to `customer-service` — it defines its own
362
- `AgentConfig.sessionIdFor` (see "Sessions" below). Other
363
- agents take a plain `sessionId` field instead — and it's optional: omit it
364
- for a fresh, one-off session (a generated id, echoed back in the response
365
- as `sessionId` so you can pass it in explicitly to continue that same
366
- conversation on a later request):
213
+ Scaffold one directly:
367
214
 
368
215
  ```bash
369
- curl -X POST localhost:8787/agents/file-agent/messages \
370
- -H 'content-type: application/json' \
371
- -d '{"message":"Summarize a.txt and b.txt into summary.txt."}'
216
+ npx loopengine add-subagent support-orchestrator billing-agent
217
+ # -> Created agents/support-orchestrator/subagents/billing-agent/index.ts
372
218
  ```
373
219
 
374
- `customer-service` is the exception here too — a request with no
375
- `customerEmail` is a genuine validation error (`400`), not "start me an
376
- anonymous session," since `customerEmail` is a real, meaningful identity
377
- for that agent, not an arbitrary key.
378
-
379
- `customer-service` also sets `AgentConfig.tenantFor` (see "Multi-tenancy"
380
- below), resolving tenant from an `x-api-key` header. The request above
381
- with no header behaves
382
- exactly as before — `issue_refund` still asks for approval. Add a
383
- trusted key and it doesn't:
220
+ A subagent is a normal `AgentConfig` plus one required field —
221
+ `toolDescription`, the text the *parent's* model reads to decide when to
222
+ delegate to it (its own `systemPrompt` is instructions for itself, not a
223
+ pitch to a caller deciding whether to invoke it):
384
224
 
385
- ```bash
386
- curl -X POST localhost:8787/agents/customer-service/messages \
387
- -H 'content-type: application/json' -H 'x-api-key: acme-trusted-key' \
388
- -d '{"customerEmail":"a@example.com","message":"order A-1001 arrived broken"}'
389
- ```
390
-
391
- **HTTP, streamed:** same request, `/stream` suffix, one Server-Sent Event per
392
- loop step instead of a single response after the whole thing finishes —
393
- a `session` event first (announcing whatever `sessionId` got used, generated
394
- or not), then `contextclip:check`, `actauth:decision`, `toollane:result`,
395
- ..., a final `done` with the answer:
396
-
397
- ```bash
398
- curl -N -X POST localhost:8787/agents/customer-service/messages/stream \
399
- -H 'content-type: application/json' \
400
- -d '{"customerEmail":"a@example.com","message":"order A-1001 arrived broken"}'
225
+ ```ts
226
+ export const config: AgentConfig = {
227
+ name: 'billing-agent',
228
+ systemPrompt: 'You answer billing questions.',
229
+ toolDescription: 'Call this for any billing-related question.',
230
+ model: { provider: 'anthropic', model: 'claude-sonnet-5' },
231
+ }
401
232
  ```
402
233
 
403
- Both adapters are thin: they own request parsing and session persistence,
404
- and call the same `runAgent(config, modelCall, message, history)` underneath.
405
- `runAgent` itself does no I/O — callers own conversation history, which is
406
- what lets it serve a one-shot CLI call and a long-lived chat session with no
407
- code difference. The streaming route isn't a separate code path through
408
- `runAgent` either — it forwards the same `onEvent` hook the non-streaming
409
- route just discards, over SSE instead of into a log. What this streams is
410
- loop *steps* (tool calls, permission decisions, budget checks), not model
411
- output tokens — no `ModelCall` implementation here (fake or
412
- `createAnthropicModelCall`) streams partial text from the model itself, so
413
- there's nothing at the token level to forward yet.
414
-
415
- ## Sessions
416
-
417
- A session is one ongoing conversation — its message history persists
418
- between requests via a `SessionStore`, so a caller can send one message,
419
- get a reply, and come back later and continue where it left off.
420
-
421
- **Storage.** `SessionStore` exposes one method, `withSession(sessionId,
422
- fn)`: it loads that session's history, runs `fn` **exclusively** for that
423
- session (so two concurrent requests for the same conversation can't race
424
- on read-modify-write), and appends whatever new messages `fn` produced.
425
- History is an append-only [`sessionknit`](https://www.npmjs.com/package/sessionknit)
426
- log rather than a flat file rewritten every turn, which buys two things:
427
-
428
- - **Non-blocking, debounced writes** — a turn only appends, it never
429
- rewrites what's already durable.
430
- - **Interruption detection** — a turn's assistant response (including any
431
- tool calls) is saved *before* those tools run, then the results are
432
- saved once they've all settled. If the process dies in between, the
433
- next resume sees an unanswered tool call, flags the session as
434
- resumed-after-interruption, and tells the model so in context — instead
435
- of silently replaying a half-finished turn as if it were clean.
436
-
437
- Two implementations, same interface — `createSessionStore()` picks
438
- between them based on `REDIS_URL` (set it and you get Redis, otherwise
439
- the file store):
440
-
441
- - `FileSessionStore` — one JSONL log per session under `.sessions/`,
442
- locked in-process. Fine for local dev and the CLI.
443
- - `RedisSessionStore` — same log shape in a Redis list, with a real
444
- distributed lock (safe across multiple server instances) that renews
445
- itself for as long as a turn runs, instead of a flat TTL a long turn
446
- could outlast.
447
-
448
- **Session keys.** Two questions decide which log a message lands in:
449
- *which conversation* is this, and *which agent's* conversation is it?
450
-
451
- - *Which conversation* — `AgentConfig.sessionIdFor(body)`. This is
452
- business logic ("what counts as one conversation" depends on what the
453
- agent is for), so it lives on the agent, not the adapter.
454
- `agents/customer-service/index.ts` hashes `customerEmail` from the
455
- request body into a session id, so different customers can never read
456
- or write each other's history, and a request with no `customerEmail` is
457
- rejected (`400`) rather than silently starting an anonymous session. An
458
- agent that doesn't define `sessionIdFor` falls back to a plain
459
- client-supplied `sessionId` field — or, if that's missing too, a
460
- generated one, returned in the response so the caller can pass it back
461
- in to continue that exact conversation later.
462
- - *Which agent* — `SessionStore` itself doesn't know or care which agent
463
- is calling it, so both adapters prefix the id with the agent's name
464
- before it ever reaches `SessionStore`. Without that, two different
465
- agents handed the same raw session id (a client reusing one `sessionId`
466
- value across agents) would read and write the exact same log.
467
-
468
- `adapters/http.ts` also folds the caller's tenant and environment into
469
- that key (see "Multi-tenancy" below) — without that, two different
470
- tenants whose `sessionIdFor` happens to produce the same raw id would
471
- collide on the same log too. Verified live: the same raw `sessionId` sent
472
- under two different tenants lands in two separate session files, not one.
473
-
474
- ## Multi-tenancy
475
-
476
- ActAuth's permission rules are scoped by `tenant`/`environment`/`agent` —
477
- a support agent might allow more for a trusted tenant, or relax approval
478
- requirements outside production. `agent` is always `AgentConfig.name`;
479
- the other two work differently, because only one of them varies per request:
480
-
481
- - **`environment`** is a deployment-wide setting, not a per-agent or
482
- per-request one — always `process.env.LOOPENGINE_ENV` (default
483
- `'production'`), the same for every agent this process runs. There's no
484
- `AgentConfig` field for it at all.
485
- - **`tenant`** varies by *who's calling*, so it's resolved per request
486
- via `AgentConfig.tenantFor?: (headers, body) => string | undefined`.
487
- Only `headers`, never `body` — tenant feeds permission decisions
488
- directly, so it has to come from something verified (an
489
- `Authorization`/API-key header checked against your own mapping), never
490
- a client-asserted body field anyone could fake to claim another
491
- tenant's permissions. Omit `tenantFor` entirely and every request is
492
- the `'default'` tenant.
493
-
494
- Only `adapters/http.ts` actually calls `tenantFor`, via `resolveTenant`.
495
- Two rules govern the result:
496
-
497
- - Returning `undefined` is a real auth failure (`401`) — not "fall back
498
- to `'default'`." If no header at all should mean `'default'` rather
499
- than a rejection, the resolver must return `'default'` itself.
500
- - No `tenantFor` at all isn't a failure — it just means this agent has no
501
- per-request tenants, so every request resolves to `'default'`.
502
-
503
- `run-agent.ts` never sees a request, so it can't call `tenantFor` either
504
- — the standalone/CLI paths that pass `config` to `runAgent` directly
505
- (skipping `adapters/http.ts`'s resolution) always get `'default'`, the
506
- same as `RunAgentOptions.tenant` being omitted. Verified live end to end:
507
- a header-derived tenant resolves to different ActAuth rules per tenant (a
508
- specific tenant's rule overrides a wildcard fallback), and an omitted
509
- `tenant` option resolves the same as an explicit `'default'`.
234
+ Calling that tool runs the subagent's whole ReAct loop — its own tools,
235
+ rules, permissions — to completion, and only its final text comes back;
236
+ the parent never sees the subagent's turns or tool calls. Orchestration
237
+ needs no separate concept on top of this: an "orchestrator" is just an
238
+ agent whose tools happen to be other agents, and the model decides which
239
+ to call, in what order, the same as any other tool — no chain DSL.
510
240
 
511
- ## Deployment
241
+ Subagents nest — a subagent's own `subagents/` folder works the same way,
242
+ one level down, addressed by joining names with `/`:
512
243
 
513
244
  ```bash
514
- docker compose up --build
245
+ npx loopengine add-subagent support-orchestrator/billing-agent disputes-agent
515
246
  ```
516
247
 
517
- builds the HTTP adapter into a container (`Dockerfile`, multi-stage:
518
- `npm ci` + `tsc`, then a slim runtime image) and starts it alongside a Redis
519
- container (`docker-compose.yml`). For a real deployment, push the same
520
- image to any container platform (Cloud Run, Fargate, Fly.io) with `REDIS_URL`
521
- and any tool-specific secrets set as environment variables.
248
+ A few things worth knowing before reaching for this:
249
+
250
+ - **The call blocks.** Every tool call in the loop is awaited before the
251
+ next model turn, so a slow subagent blocks its parent's turn — and if
252
+ the parent is itself a subagent of another agent, that blocks too.
253
+ There's no background/async or streaming pattern here.
254
+ - **Permissions don't inherit.** Each agent's own `actauth.yml` governs
255
+ only its own tools; delegating to a subagent neither grants nor
256
+ inherits permissions in either direction.
257
+ - **`maxTurns` compounds.** A request that reaches a grandchild subagent
258
+ spends turns at every level (default 25 each) — a "simple" answer can
259
+ cost far more than any one agent's own turn budget suggests.
260
+
261
+ ### 5. Tool permission and multi-tenancy
262
+
263
+ Every tool call is gated by [`actauth`](https://www.npmjs.com/package/actauth):
264
+ each rule in `AgentConfig.rules` maps a `scope` (tenant/environment) + tool
265
+ name to `allow` / `ask` / `deny`. Anything not covered falls through to
266
+ `defaultDecision`. An `ask` decision routes to `AgentConfig.approver`
267
+ (defaults to blocking on stdin — swap in a real one, e.g. Slack-backed,
268
+ for production).
269
+
270
+ A real agent's rules live in `agents/<name>/actauth.yml`:
271
+
272
+ ```yaml
273
+ default_decision: deny
274
+
275
+ rules:
276
+ - name: weather-lookup-always-allowed
277
+ scope: "*/*"
278
+ tool: get_weather
279
+ decision: allow
280
+ ```
522
281
 
523
- ## Wiring a real model
282
+ `scope` is `tenant/environment` — `"*/*"` here means "every tenant, every
283
+ environment." Rules are matched most-specific-scope-first, so an agent
284
+ that needs different behavior per tenant just adds a more specific rule
285
+ (e.g. `acme-corp/production`) alongside the wildcard fallback.
286
+
287
+ That `scope` is what makes rules multi-tenant: the same agent can behave
288
+ differently for different customers. `environment` is a deployment-wide
289
+ setting (`LOOPENGINE_ENV`, default `production`). `tenant` is resolved per
290
+ request via `AgentConfig.tenantFor?: (headers, body) => string |
291
+ undefined` — from headers only, never the request body, since it feeds
292
+ permission decisions directly. No `tenantFor` means every request is the
293
+ `'default'` tenant.
294
+
295
+ ### 6. Sessions
296
+
297
+ A session is one ongoing conversation. Message history persists between
298
+ requests automatically — send a message, get a reply, come back later with
299
+ the same `sessionId` and continue where you left off. Two storage backends,
300
+ picked automatically: a local file store, or Redis if `REDIS_URL` is set
301
+ (needed for multiple server instances).
302
+
303
+ An agent can define `AgentConfig.sessionIdFor(body)` to control what
304
+ counts as "one conversation" for its own domain (e.g. `customer-service`
305
+ hashes `customerEmail`, so a request with no email is rejected rather than
306
+ starting an anonymous session). Without it, the caller just supplies a
307
+ `sessionId`, or gets one generated for them.
524
308
 
525
- `agents/file-agent/index.ts` and `agents/rag-agent.ts` still use a canned,
526
- turn-counting `ModelCall` so the whole loop runs with no API key.
527
- `agents/customer-service/index.ts` is already wired to a real one — a
528
- working example, not just a snippet.
309
+ ## Running an agent
529
310
 
530
- **The easy way** — declare `AgentConfig.model` and skip `createModelCall`
531
- entirely; `discoverAgents` builds it for you:
311
+ **CLI:**
532
312
 
533
- ```ts
534
- export const config: AgentConfig = {
535
- name: 'my-agent',
536
- // ...
537
- model: { provider: 'anthropic', model: 'claude-sonnet-5' }, // reads ANTHROPIC_API_KEY
538
- }
313
+ ```bash
314
+ npx tsx adapters/cli.ts --agent customer-service --session s1 "order A-1001 arrived broken"
539
315
  ```
540
316
 
541
- | `provider` | env var read | `model` required? | example |
542
- | --- | --- | --- | --- |
543
- | `'anthropic'` | `ANTHROPIC_API_KEY` | no — defaults to `claude-sonnet-5` | `{ provider: 'anthropic' }` |
544
- | `'openai'` | `OPENAI_API_KEY` | yes — no safe hardcoded default | `{ provider: 'openai', model: 'gpt-4.1' }` |
545
- | `'deepseek'` | `DEEPSEEK_API_KEY` | yes — no safe hardcoded default | `{ provider: 'deepseek', model: 'deepseek-chat' }` |
546
-
547
- `agents/customer-service/index.ts` uses exactly this —
548
- `model: { provider: 'deepseek', model: 'deepseek-v4-pro' }`, nothing
549
- else. Built lazily, on first actual call, and memoized after that, not at
550
- module load: `discoverAgents()` imports every agent at startup, so
551
- building the real client eagerly would crash the whole server without
552
- `DEEPSEEK_API_KEY`, even just to run `file-agent`.
553
-
554
- **The manual way** — for anything the table above can't express (a
555
- canned/simulated `ModelCall` like `file-agent`/`rag-agent`'s own, a
556
- custom SDK client, a provider not listed), export `createModelCall`
557
- yourself and call the same factories `AgentConfig.model` uses under the
558
- hood:
559
-
560
- ```ts
561
- import { createAnthropicModelCall } from './model-calls/anthropic-model-call.js'
562
- // createOpenAIModelCall / createDeepSeekModelCall work the same way
317
+ **HTTP:**
563
318
 
564
- export function createModelCall(): ModelCall {
565
- return createAnthropicModelCall({ model: 'claude-sonnet-5' })
566
- }
319
+ ```bash
320
+ npx tsx adapters/http.ts
321
+ curl -X POST localhost:8787/agents/customer-service/messages \
322
+ -H 'content-type: application/json' \
323
+ -d '{"customerEmail":"a@example.com","message":"order A-1001 arrived broken"}'
567
324
  ```
568
325
 
569
- Either way, nothing else in `run-agent.ts`, the adapters, or any
570
- `AgentConfig` needs to change — `ModelCall` is the only seam a real API
571
- call needs.
572
-
573
- All three factories translate loopengine's `Message`/`ModelResponse` to
574
- and from their provider's real request/response shape (verified against
575
- the real SDKs with a stubbed `fetch`, tool calls included).
576
- `model-calls/deepseek-model-call.ts` reuses `openai-model-call.ts`'s
577
- translation directly rather than reimplementing it, since DeepSeek's
578
- Chat Completions API is wire-compatible with OpenAI's.
326
+ Add `/stream` to the URL for a Server-Sent Events response — one event per
327
+ loop step (tool call, permission decision, budget check) instead of a
328
+ single reply at the end.
329
+
330
+ Three small browser pages, all cross-linked, share one look
331
+ (`adapters/dev-ui-styles.ts`), and never need a build step — each is a
332
+ self-contained HTML string served straight out of `adapters/http.ts`:
333
+
334
+ **Agents list:** open `http://localhost:8787/agents` in a browser (the
335
+ same route returns plain `{agents: [...]}` JSON to a non-browser client —
336
+ content-negotiated on the `Accept` header, so nothing that already calls
337
+ it as an API needs to change) to see every registered agent with links
338
+ into the playground and config page below.
339
+
340
+ **Dev playground:** open `http://localhost:8787/playground` (optionally
341
+ `?agent=<name>` to preselect one) — pick an agent, chat with it, and watch
342
+ that same loop-step event stream render live instead of reading raw SSE
343
+ frames. Same `/messages/stream` route underneath.
344
+
345
+ **Agent config page:** open `http://localhost:8787/agents/config`
346
+ (optionally `?agent=<name>`) to browse every registered agent's resolved
347
+ config — system prompt, model, tools (with their JSON schemas and
348
+ parallel-safety), the actual ActAuth rules that would be enforced (source,
349
+ default decision, per-rule scope/tool/decision/`when`), and whether
350
+ `sessionIdFor`/`tenantFor`/`isSafeTool`/`approver` are custom or
351
+ defaulted. Backed by `GET /agents/:name/config`, which reuses the same
352
+ rule/tool resolution `runAgent()` itself uses, so it can't drift out of
353
+ sync with what a real request actually gets. Never returns
354
+ `AgentModelConfig.apiKey`.
579
355
 
580
- ## Skills
356
+ ## Wiring a real model
581
357
 
582
- Agents can discover and invoke `SKILL.md` files, the same convention
583
- production coding agents use: a short frontmatter index (`name`,
584
- `description`) stays in context at all times, and the full body is loaded
585
- only when the model actually invokes that skill — through a real `Skill`
586
- tool schema (`{skill: string, args?: string}`) declared to the model
587
- whenever any skills are actually found, not just handled after the fact.
358
+ Declare `AgentConfig.model` and the runtime builds a real `ModelCall` for
359
+ you, using the matching API key from the environment:
588
360
 
589
- `AgentConfig.skillsDirs` (an array of directory paths) controls where
590
- from — see "Smart defaults" above for what it defaults to when omitted.
591
- `SkillGarden` discovery recursively walks every directory it's given with
592
- **no per-agent filtering** — whatever `SKILL.md` files live under a
593
- `skillsDirs` path, the agent sees all of them, which is the whole reason
594
- the two kinds below live in different places.
361
+ | `provider` | env var | `model` required? |
362
+ | --- | --- | --- |
363
+ | `'anthropic'` | `ANTHROPIC_API_KEY` | no — defaults to `claude-sonnet-5` |
364
+ | `'openai'` | `OPENAI_API_KEY` | yes |
365
+ | `'deepseek'` | `DEEPSEEK_API_KEY` | yes |
595
366
 
596
- **Agent-specific** — a skill only one agent needs, under that agent's own
597
- folder, pointed at by only that agent's `skillsDirs`:
367
+ For anything else (a custom SDK client, a canned/simulated model for
368
+ testing), export your own `createModelCall(): ModelCall` instead.
598
369
 
599
- ```
600
- agents/customer-service/skills/
601
- firecrawl/SKILL.md Only customer-service's skillsDirs points here
602
- ```
370
+ ## Built on
603
371
 
604
- **Global** — a skill genuinely meant to be shared, at the repo-root
605
- `skills/` (see `skills/README.md`), pointed at by every agent's
606
- `skillsDirs` that wants it:
607
-
608
- ```
609
- skills/
610
- README.md Explains the convention; no shared skill exists yet
611
- (some-shared-skill)/SKILL.md Would be seen by every agent pointed here
612
- ```
372
+ | Package | Responsibility |
373
+ |---|---|
374
+ | [`actauth`](https://www.npmjs.com/package/actauth) | Permission gating (allow/ask/deny) with human-approval hooks |
375
+ | [`contextclip`](https://www.npmjs.com/package/contextclip) | Context-window budget tracking |
376
+ | [`reflowkit`](https://www.npmjs.com/package/reflowkit) | Retries prompt-too-long / truncated-output failures |
377
+ | [`sessionknit`](https://www.npmjs.com/package/sessionknit) | Durable session log with crash-interruption detection |
378
+ | [`skillgarden`](https://www.npmjs.com/package/skillgarden) | `SKILL.md` discovery and lazy loading |
379
+ | [`toollane`](https://www.npmjs.com/package/toollane) | Parallel/solo tool-call scheduling |
380
+ | [`mcpplug`](https://www.npmjs.com/package/mcpplug) | Gateway tool sourcing (e.g. Composio) |
613
381
 
614
- Nothing lives in the global root today — every skill this repo ships is
615
- agent-specific. Don't point an agent at the global root *and* its own
616
- folder unless you mean for it to see both; and don't move a skill to the
617
- global root just because it feels reusable in theory — wait until a
618
- second agent actually needs the exact same one.
382
+ Installing `loopengine` pulls all of these in as regular dependencies —
383
+ install one directly only if you want to use it standalone.
619
384
 
620
- To install one of SkillGarden's bundled skills (e.g. `firecrawl`) into an
621
- agent-specific folder, pass `--agent` and no `--dir` — it defaults to
622
- `agents/<agent>/skills/<skill>/SKILL.md`, matching how every skill in this
623
- repo is actually laid out:
385
+ ## Deployment
624
386
 
625
387
  ```bash
626
- npx skillgarden add firecrawl --agent file-agent
627
- # -> Added file-agent:firecrawl -> agents/file-agent/skills/firecrawl/SKILL.md
628
- ```
629
-
630
- ## Project layout
631
-
632
- ```
633
- index.ts Public API surface — what actually ships to npm
634
- agent-config.ts AgentConfig type — the thing you fill in to define an agent
635
- run-agent.ts The generic ReAct loop every agent and adapter runs through
636
- vector-index.ts Dependency-free embeddings + cosine-similarity search, for RAG
637
- discover-agents.ts discoverAgents — scans a directory, builds {name -> {config, createModelCall}}
638
- agent-registry.ts This repo's ~15-line wrapper: discoverAgents('agents/') + listAgents()/getEntry()
639
- session-store.ts SessionStore: FileSessionStore, RedisSessionStore, createSessionStore()
640
- model-calls/anthropic-model-call.ts createAnthropicModelCall — a real ModelCall this repo ships
641
- model-calls/openai-model-call.ts createOpenAIModelCall — same seam, OpenAI's Chat Completions API
642
- model-calls/deepseek-model-call.ts createDeepSeekModelCall — same seam, reuses openai-model-call.ts's translation
643
- agents/file-agent/index.ts Example agent: summarizes text files
644
- agents/file-agent/tools/ Its hand-written tools, one file per tool
645
- agents/file-agent/skills/ Its SKILL.md files
646
- agents/file-agent/actauth.yml Its ActAuth rules
647
- agents/customer-service/index.ts Example agent: order/shipment lookup, refund, email
648
- agents/customer-service/tools/ Its tools, one file per tool
649
- agents/customer-service/skills/ Its SKILL.md files
650
- agents/customer-service/actauth.yml Its ActAuth rules
651
- agents/rag-agent.ts Example agent: retrieves from an in-memory vector index
652
- adapters/cli.ts Channel adapter: command line
653
- adapters/http.ts Channel adapter: HTTP API
654
- tools/README.md Root tools/ — for a tool genuinely shared across agents; empty today
655
- skills/README.md Root skills/ — same, for a shared SKILL.md; empty today
656
- examples/file-agent/ Sample input files + generated output for the file-agent demo
657
- Dockerfile, docker-compose.yml Container build + local Redis for testing
388
+ docker compose up --build
658
389
  ```
659
390
 
660
- Every agent's own tools/skills live under `agents/<name>/` — the root
661
- `tools/`/`skills/` directories are reserved for something genuinely meant
662
- to be shared across every agent that opts in, which doesn't exist in this
663
- repo yet (see each directory's own `README.md`).
391
+ Builds the HTTP adapter into a container and starts it alongside Redis.
392
+ For production, push the image to any container platform with `REDIS_URL`
393
+ and your model/tool API keys set as environment variables.