loopengine 0.0.10 → 0.0.16
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 +306 -423
- package/dist/agent-as-tool.d.ts +7 -0
- package/dist/agent-as-tool.js +32 -0
- package/dist/agent-config.d.ts +139 -9
- package/dist/cli.d.ts +31 -0
- package/dist/cli.js +174 -0
- package/dist/discover-agents.d.ts +18 -10
- package/dist/discover-agents.js +87 -26
- package/dist/index.d.ts +11 -10
- package/dist/index.js +6 -5
- package/dist/{anthropic-model-call.d.ts → model-calls/anthropic-model-call.d.ts} +1 -1
- package/dist/{anthropic-model-call.js → model-calls/anthropic-model-call.js} +8 -6
- package/dist/{deepseek-model-call.d.ts → model-calls/deepseek-model-call.d.ts} +1 -1
- package/dist/{openai-model-call.d.ts → model-calls/openai-model-call.d.ts} +1 -1
- package/dist/run-agent.d.ts +31 -1
- package/dist/run-agent.js +219 -8
- package/dist/session-store.d.ts +1 -1
- package/package.json +21 -10
- /package/dist/{deepseek-model-call.js → model-calls/deepseek-model-call.js} +0 -0
- /package/dist/{openai-model-call.js → model-calls/openai-model-call.js} +0 -0
package/README.md
CHANGED
|
@@ -2,40 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/evanyan22/loopengine/actions/workflows/ci.yml)
|
|
4
4
|
|
|
5
|
-
LoopEngine is a runtime for
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
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,466 +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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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.
|
|
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.
|
|
56
47
|
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
51
|
+
npx loopengine add-agent weather-agent
|
|
52
|
+
# -> Created agents/weather-agent/index.ts
|
|
65
53
|
```
|
|
66
54
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
it
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
```ts
|
|
75
|
-
import { runAgent, type AgentConfig, type ToolDefinition } from 'loopengine'
|
|
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.
|
|
76
61
|
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
}
|
|
62
|
+
Most fields don't even need to be written out — they default to a
|
|
63
|
+
conventional path under the agent's own folder:
|
|
83
64
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
defaultDecision: 'ask',
|
|
90
|
-
}
|
|
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) |
|
|
91
70
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
```
|
|
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.
|
|
96
74
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
`embed`/`cosineSimilarity` (for RAG — see below), `discoverAgents` (scans a
|
|
100
|
-
directory for `config`/`createModelCall` modules and builds a name → entry
|
|
101
|
-
map, keyed by `AgentConfig.name` — see `agent-registry.ts` for the ~15-line
|
|
102
|
-
wrapper this repo's own adapters use), and `createAnthropicModelCall`/
|
|
103
|
-
`createOpenAIModelCall`/`createDeepSeekModelCall` (real, ready-to-use
|
|
104
|
-
`ModelCall`s). See `index.ts` for the exact list.
|
|
105
|
-
|
|
106
|
-
**Not** part of the package: `agents/*.ts` (this repo's own demo agents),
|
|
107
|
-
`agent-registry.ts` (this repo's own name → config lookup table, built on
|
|
108
|
-
`discoverAgents` — build your own the same way), `adapters/cli.ts`/
|
|
109
|
-
`adapters/http.ts` (one specific CLI/HTTP wiring choice, not imposed on
|
|
110
|
-
every consumer), and the `Dockerfile`. Everything below this point
|
|
111
|
-
describes that reference app — read it as a worked example of what to
|
|
112
|
-
build with the library, not as library API itself.
|
|
75
|
+
`rules` is how you gate what a tool can do without approval — see
|
|
76
|
+
"Tool permission and multi-tenancy" below.
|
|
113
77
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
npm install
|
|
118
|
-
npx tsx agents/file-agent.ts # run the file-summarizer demo agent
|
|
119
|
-
npx tsx agents/customer-service-agent.ts # run the customer-service demo agent
|
|
120
|
-
npx tsx agents/rag-agent.ts # run the retrieval-augmented demo agent
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
All demo agents use a **simulated** model call (no `ANTHROPIC_API_KEY` is
|
|
124
|
-
wired up) — see "Wiring a real model" below.
|
|
125
|
-
|
|
126
|
-
## Defining your own agent
|
|
127
|
-
|
|
128
|
-
An agent is an `AgentConfig` (`agent-config.ts`): a name, a system prompt, a
|
|
129
|
-
list of tools, and ActAuth permission rules.
|
|
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/`:
|
|
130
81
|
|
|
131
82
|
```ts
|
|
132
|
-
import type { AgentConfig } from '
|
|
83
|
+
import type { AgentConfig } from '../../agent-config.js'
|
|
133
84
|
|
|
134
85
|
export const config: AgentConfig = {
|
|
135
|
-
name: '
|
|
136
|
-
systemPrompt: 'You
|
|
137
|
-
|
|
138
|
-
{
|
|
139
|
-
name: 'do_thing',
|
|
140
|
-
description: 'What the model sees when deciding to call this',
|
|
141
|
-
input_schema: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] },
|
|
142
|
-
execute: async (input) => {
|
|
143
|
-
// real implementation — DB call, HTTP request, whatever
|
|
144
|
-
return { ok: true }
|
|
145
|
-
},
|
|
146
|
-
},
|
|
147
|
-
],
|
|
148
|
-
rules: [
|
|
149
|
-
{ scopePattern: 'default/production/my-agent', tool: 'do_thing', decision: 'allow' },
|
|
150
|
-
],
|
|
151
|
-
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
|
|
152
89
|
}
|
|
153
90
|
```
|
|
154
91
|
|
|
155
|
-
`agents/
|
|
156
|
-
examples
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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:
|
|
162
159
|
|
|
163
|
-
|
|
160
|
+
```ts
|
|
161
|
+
// Global skill: add the root path alongside the agent's own
|
|
162
|
+
skillsDirs: ['agents/my-agent/skills', 'skills']
|
|
164
163
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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'
|
|
168
168
|
|
|
169
|
-
|
|
170
|
-
const searchDocs: ToolDefinition = {
|
|
171
|
-
name: 'search_docs',
|
|
172
|
-
description: 'Search the knowledge base for relevant passages',
|
|
173
|
-
input_schema: { type: 'object', properties: { query: { type: 'string' } }, required: ['query'] },
|
|
174
|
-
execute: async (input) => {
|
|
175
|
-
const hits = index.search(input.query as string, 3) // top-3 chunks
|
|
176
|
-
return hits.map((h) => h.text).join('\n\n')
|
|
177
|
-
},
|
|
178
|
-
}
|
|
169
|
+
tools: [...ownTools, sharedTool]
|
|
179
170
|
```
|
|
180
171
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
`
|
|
187
|
-
|
|
188
|
-
`Summarizer` or ActAuth's `Approver`. The retrieved text comes back as an
|
|
189
|
-
ordinary tool result, subject to the same ActAuth gating, ToolLane
|
|
190
|
-
scheduling, and ContextClip budget tracking as any other tool call.
|
|
191
|
-
|
|
192
|
-
## External tool gateways
|
|
193
|
-
|
|
194
|
-
A hand-written tool needs an `execute` function you write yourself; an
|
|
195
|
-
external SaaS action (list GitHub repos, send a Slack message) needs
|
|
196
|
-
someone to own that vendor's auth. Connecting straight to a vendor's own
|
|
197
|
-
MCP server means owning its OAuth flow (authorization code, PKCE, token
|
|
198
|
-
storage/refresh) yourself, repeated per vendor. Routing through a gateway
|
|
199
|
-
like [Composio](https://composio.dev) avoids that — it already holds the
|
|
200
|
-
OAuth relationship with 1000+ apps — and [`mcpplug`](https://www.npmjs.com/package/mcpplug)
|
|
201
|
-
wraps that gateway behind the same `ToolDefinition` shape as everything
|
|
202
|
-
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:
|
|
203
179
|
|
|
204
180
|
```ts
|
|
205
181
|
import { connectComposioSource } from 'mcpplug'
|
|
206
182
|
|
|
207
|
-
// Resolved once, at module-eval time — not per runAgent() call. A gateway
|
|
208
|
-
// connection is exactly the kind of setup cost runAgent() shouldn't pay on
|
|
209
|
-
// every turn; see agents/file-agent.ts for the working example this is
|
|
210
|
-
// drawn from.
|
|
211
183
|
const composioTools = await connectComposioSource('composio', {
|
|
212
184
|
slugs: ['GITHUB_LIST_REPOSITORIES_FOR_THE_AUTHENTICATED_USER'],
|
|
213
185
|
}).then((source) => source.loadTools())
|
|
214
186
|
|
|
215
187
|
const config: AgentConfig = {
|
|
216
188
|
name: 'my-agent',
|
|
217
|
-
systemPrompt: 'You are ...',
|
|
218
189
|
tools: [...handWrittenTools, ...composioTools],
|
|
219
|
-
rules: [
|
|
220
|
-
// mcpplug namespaces each tool `${sourceName}_${slug}` — falls
|
|
221
|
-
// through to defaultDecision unless a rule names it explicitly.
|
|
222
|
-
{ scopePattern: 'default/production/my-agent', tool: 'composio_GITHUB_LIST_REPOSITORIES_FOR_THE_AUTHENTICATED_USER', decision: 'allow' },
|
|
223
|
-
],
|
|
190
|
+
rules: [{ scopePattern: 'default/production/my-agent', tool: 'composio_GITHUB_LIST_REPOSITORIES_FOR_THE_AUTHENTICATED_USER', decision: 'allow' }],
|
|
224
191
|
defaultDecision: 'ask',
|
|
225
192
|
}
|
|
226
193
|
```
|
|
227
194
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
`agents/file-agent.ts` is a complete, working example, verified end-to-end
|
|
232
|
-
against a real linked GitHub account. `slugs` is explicit, not
|
|
233
|
-
auto-discovered — an agent should only see the Composio actions its own
|
|
234
|
-
config actually lists; find them with the `composio` CLI's
|
|
235
|
-
`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.
|
|
236
198
|
|
|
237
|
-
|
|
199
|
+
### 4. Subagents — an agent as another agent's tool
|
|
238
200
|
|
|
239
|
-
|
|
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:
|
|
240
204
|
|
|
241
|
-
```
|
|
242
|
-
|
|
205
|
+
```
|
|
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
|
|
243
211
|
```
|
|
244
212
|
|
|
245
|
-
|
|
213
|
+
Scaffold one directly:
|
|
246
214
|
|
|
247
215
|
```bash
|
|
248
|
-
npx
|
|
249
|
-
|
|
250
|
-
-H 'content-type: application/json' \
|
|
251
|
-
-d '{"customerEmail":"a@example.com","message":"order A-1001 arrived broken"}'
|
|
216
|
+
npx loopengine add-subagent support-orchestrator billing-agent
|
|
217
|
+
# -> Created agents/support-orchestrator/subagents/billing-agent/index.ts
|
|
252
218
|
```
|
|
253
219
|
|
|
254
|
-
|
|
255
|
-
`
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
as `sessionId` so you can pass it in explicitly to continue that same
|
|
259
|
-
conversation on a later request):
|
|
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):
|
|
260
224
|
|
|
261
|
-
```
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
+
}
|
|
265
232
|
```
|
|
266
233
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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.
|
|
271
240
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
a `session` event first (announcing whatever `sessionId` got used, generated
|
|
275
|
-
or not), then `contextclip:check`, `actauth:decision`, `toollane:result`,
|
|
276
|
-
..., a final `done` with the answer:
|
|
241
|
+
Subagents nest — a subagent's own `subagents/` folder works the same way,
|
|
242
|
+
one level down, addressed by joining names with `/`:
|
|
277
243
|
|
|
278
244
|
```bash
|
|
279
|
-
|
|
280
|
-
-H 'content-type: application/json' \
|
|
281
|
-
-d '{"customerEmail":"a@example.com","message":"order A-1001 arrived broken"}'
|
|
245
|
+
npx loopengine add-subagent support-orchestrator/billing-agent disputes-agent
|
|
282
246
|
```
|
|
283
247
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
`
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
as resumed-after-interruption, and injects a note into context saying so
|
|
317
|
-
— instead of silently resending an incomplete turn as if it were clean.
|
|
318
|
-
|
|
319
|
-
Two implementations, same `SessionStore` interface:
|
|
320
|
-
|
|
321
|
-
- `FileSessionStore` — one JSONL entry log per session under `.sessions/`,
|
|
322
|
-
locked in-process. Fine for local dev and the CLI.
|
|
323
|
-
- `RedisSessionStore` — same log shape (a Redis list of entries, one
|
|
324
|
-
`RPUSH` per append), with a real distributed lock, safe across multiple
|
|
325
|
-
server instances. The lock renews itself (`lockTtlMs / 3` by default)
|
|
326
|
-
for as long as a turn runs, instead of a flat TTL that a long turn (a
|
|
327
|
-
real model call plus several tool round-trips) could outlast — if
|
|
328
|
-
renewal ever confirms the lock was lost anyway (not just a failed
|
|
329
|
-
renewal attempt — a network blip there isn't proof of loss), the turn's
|
|
330
|
-
result is rejected rather than silently returned as if mutual exclusion
|
|
331
|
-
held the whole time.
|
|
332
|
-
|
|
333
|
-
`createSessionStore()` picks between them based on `REDIS_URL` — set it and
|
|
334
|
-
you get Redis, otherwise it falls back to the file store. Either way,
|
|
335
|
-
turn-level exclusivity (no two concurrent turns for the same session
|
|
336
|
-
interleaving) is still this module's job, not SessionKnit's — SessionKnit's
|
|
337
|
-
own topology repair (reattaching sibling branches under a shared
|
|
338
|
-
`parentId`) is a defensive read-side repair for crash/corruption recovery,
|
|
339
|
-
not something normal operation exercises. `run-agent.ts` bundles a whole
|
|
340
|
-
turn's `tool_use`/`tool_result` blocks into one message each, and
|
|
341
|
-
`withSession` appends new messages sequentially, so even a turn with
|
|
342
|
-
several parallel tool calls only ever produces a linear chain in the
|
|
343
|
-
durable log — not races between two full concurrent turns, which is what
|
|
344
|
-
the lock above actually prevents.
|
|
345
|
-
|
|
346
|
-
Session-key derivation is `AgentConfig.sessionIdFor(body)` — deliberately
|
|
347
|
-
not the HTTP adapter's call, since what counts as "one conversation" is
|
|
348
|
-
business logic specific to what the agent is for, not a transport concern.
|
|
349
|
-
`customer-service-agent.ts` defines its own, hashing `customerEmail` so raw
|
|
350
|
-
addresses never end up in storage keys — different customers can never
|
|
351
|
-
read or write each other's history, and concurrent messages from the same
|
|
352
|
-
customer are serialized rather than dropped. A missing `customerEmail` is a
|
|
353
|
-
real validation failure for that agent (`400`), not something to paper over.
|
|
354
|
-
|
|
355
|
-
An agent that doesn't define `sessionIdFor` falls back to a plain
|
|
356
|
-
client-supplied `sessionId` field (`adapters/http.ts`'s
|
|
357
|
-
`defaultSessionIdFor`) — the same shape `adapters/cli.ts`'s `--session`
|
|
358
|
-
flag already uses — and if that's missing too, one gets generated rather
|
|
359
|
-
than the request being rejected, mirroring what omitting `--session` now
|
|
360
|
-
does for the CLI: a fresh, isolated, one-off session rather than an error
|
|
361
|
-
or (worse) a shared bucket every untagged caller collides into. The
|
|
362
|
-
generated id comes back in the response (`sessionId` in the JSON body, a
|
|
363
|
-
`session` SSE event first for the streamed route) so a caller that wants to
|
|
364
|
-
continue that exact conversation can pass it in explicitly next time.
|
|
365
|
-
|
|
366
|
-
`SessionStore` itself is agent-agnostic — nothing in `session-store.ts` or
|
|
367
|
-
`sessionknit` knows which agent is calling `withSession`, so both adapters
|
|
368
|
-
namespace the *actual* key by agent name before it ever reaches
|
|
369
|
-
`SessionStore`: `` `${agentName}:${sessionIdFor(...)}` `` in
|
|
370
|
-
`adapters/http.ts`, `` `${agent}:${session}` `` in `adapters/cli.ts`.
|
|
371
|
-
Without that, two different agents given the same session identifier (a
|
|
372
|
-
client reusing one `sessionId`/`--session` value across agents, most
|
|
373
|
-
likely) would read and write the exact same underlying log — one agent's
|
|
374
|
-
tool calls and results spliced straight into another's conversation as if
|
|
375
|
-
it had always been there.
|
|
376
|
-
|
|
377
|
-
## Deployment
|
|
378
|
-
|
|
379
|
-
```bash
|
|
380
|
-
docker compose up --build
|
|
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
|
|
381
280
|
```
|
|
382
281
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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.
|
|
388
308
|
|
|
389
|
-
##
|
|
309
|
+
## Running an agent
|
|
390
310
|
|
|
391
|
-
|
|
392
|
-
`agents/rag-agent.ts`) still uses a canned, turn-counting `ModelCall` so the
|
|
393
|
-
whole loop is runnable and testable with no API key. To go live, swap it for
|
|
394
|
-
one of the two real `ModelCall` implementations this repo ships:
|
|
311
|
+
**CLI:**
|
|
395
312
|
|
|
396
|
-
```
|
|
397
|
-
|
|
313
|
+
```bash
|
|
314
|
+
npx tsx adapters/cli.ts --agent customer-service --session s1 "order A-1001 arrived broken"
|
|
315
|
+
```
|
|
398
316
|
|
|
399
|
-
|
|
317
|
+
**HTTP:**
|
|
400
318
|
|
|
401
|
-
|
|
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"}'
|
|
402
324
|
```
|
|
403
325
|
|
|
404
|
-
|
|
405
|
-
|
|
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`.
|
|
406
355
|
|
|
407
|
-
|
|
408
|
-
// name changes too often to hardcode safely; reads OPENAI_API_KEY from the env
|
|
409
|
-
const modelCall = createOpenAIModelCall({ model: 'gpt-4.1' })
|
|
356
|
+
## Wiring a real model
|
|
410
357
|
|
|
411
|
-
|
|
412
|
-
|
|
358
|
+
Declare `AgentConfig.model` and the runtime builds a real `ModelCall` for
|
|
359
|
+
you, using the matching API key from the environment:
|
|
413
360
|
|
|
414
|
-
|
|
415
|
-
|
|
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 |
|
|
416
366
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
// neither that nor apiKey is set, rather than the OpenAI SDK's own
|
|
420
|
-
// constructor silently accepting OPENAI_API_KEY instead.
|
|
421
|
-
const modelCall = createDeepSeekModelCall({ model: 'deepseek-chat' })
|
|
422
|
-
|
|
423
|
-
const result = await runAgent(config, modelCall, 'order A-1001 arrived broken', [])
|
|
424
|
-
```
|
|
367
|
+
For anything else (a custom SDK client, a canned/simulated model for
|
|
368
|
+
testing), export your own `createModelCall(): ModelCall` instead.
|
|
425
369
|
|
|
426
|
-
|
|
427
|
-
change — `ModelCall` is the only seam a real API call needs.
|
|
428
|
-
|
|
429
|
-
loopengine's own `Message` type carries real block-native history: `content`
|
|
430
|
-
is either a plain string or a `ModelContentBlock[]` — the same shape
|
|
431
|
-
`ModelResponse.content` already used — so a model's `tool_use` requests and
|
|
432
|
-
this loop's `tool_result` replies round-trip with real per-call identity
|
|
433
|
-
(`tool_use_id`) rather than being flattened into prose. That's what makes
|
|
434
|
-
parallel tool calls unambiguous (a result links back to the exact call that
|
|
435
|
-
requested it, not just "a call to this tool name") and lets a denied or
|
|
436
|
-
failed call carry `is_error: true`, which Claude is specifically trained to
|
|
437
|
-
react to. `anthropic-model-call.ts` translates directly to and from
|
|
438
|
-
Anthropic's native `TextBlockParam`/`ToolUseBlockParam`/`ToolResultBlockParam`
|
|
439
|
-
types — no flattening step in between. This was verified against the real
|
|
440
|
-
SDK (with a stubbed `fetch`, not a live call) — request shape, tool schemas,
|
|
441
|
-
and response mapping all round-trip correctly end to end.
|
|
442
|
-
|
|
443
|
-
`openai-model-call.ts` translates to and from OpenAI's Chat Completions
|
|
444
|
-
shape instead, verified the same way (stubbed `fetch`, real SDK, full
|
|
445
|
-
`runAgent` round trip including a tool call). The one structural mismatch:
|
|
446
|
-
loopengine bundles a whole turn's `tool_result` blocks into a single
|
|
447
|
-
user-role message (mirroring Anthropic's shape); OpenAI has no equivalent —
|
|
448
|
-
each becomes its own top-level `role: 'tool'` message, so one loopengine
|
|
449
|
-
`Message` can expand into several OpenAI messages, never the reverse.
|
|
450
|
-
|
|
451
|
-
`deepseek-model-call.ts` reuses `openai-model-call.ts`'s translation
|
|
452
|
-
verbatim (exported, not duplicated) rather than reimplementing it —
|
|
453
|
-
DeepSeek's Chat Completions API is wire-compatible with OpenAI's (verified
|
|
454
|
-
against DeepSeek's own docs), down to the same `tool_calls` response
|
|
455
|
-
shape. The one real difference: DeepSeek documents `max_tokens`, not the
|
|
456
|
-
newer `max_completion_tokens` OpenAI's endpoint expects, so only the
|
|
457
|
-
request-building call site differs, not the shared message/response
|
|
458
|
-
translation functions.
|
|
459
|
-
|
|
460
|
-
## Skills
|
|
461
|
-
|
|
462
|
-
Agents can discover and invoke `SKILL.md` files (see `skills/`), the same
|
|
463
|
-
convention production coding agents use: a short frontmatter index (`name`,
|
|
464
|
-
`description`) stays in context at all times, and the full body is loaded
|
|
465
|
-
only when the model actually invokes that skill — through a real `Skill`
|
|
466
|
-
tool schema (`{skill: string, args?: string}`) declared to the model
|
|
467
|
-
whenever `skillsDirs` is set, not just handled after the fact. Set
|
|
468
|
-
`skillsDirs` on an `AgentConfig` to enable it; omit it for agents that
|
|
469
|
-
don't need skills (`agents/rag-agent.ts` doesn't).
|
|
470
|
-
|
|
471
|
-
Point `skillsDirs` at a subdirectory scoped to that agent
|
|
472
|
-
(`skills/file-agent/`, not the shared `skills/` root) — `SkillGarden`'s
|
|
473
|
-
discovery recursively walks whatever root it's given with no per-agent
|
|
474
|
-
filtering, so two agents both pointed at `skills/` would each see every
|
|
475
|
-
skill under it, not just their own.
|
|
476
|
-
|
|
477
|
-
To install one of SkillGarden's bundled skills (e.g. `firecrawl`) into an
|
|
478
|
-
agent's own subdirectory, use its `add` CLI:
|
|
370
|
+
## Built on
|
|
479
371
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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) |
|
|
484
381
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
already set `skillsDirs: 'skills/file-agent'` on that agent's config.
|
|
382
|
+
Installing `loopengine` pulls all of these in as regular dependencies —
|
|
383
|
+
install one directly only if you want to use it standalone.
|
|
488
384
|
|
|
489
|
-
##
|
|
385
|
+
## Deployment
|
|
490
386
|
|
|
387
|
+
```bash
|
|
388
|
+
docker compose up --build
|
|
491
389
|
```
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
discover-agents.ts discoverAgents — scans a directory, builds {name -> {config, createModelCall}}
|
|
497
|
-
agent-registry.ts This repo's ~15-line wrapper: discoverAgents('agents/') + listAgents()/getEntry()
|
|
498
|
-
session-store.ts SessionStore: FileSessionStore, RedisSessionStore, createSessionStore()
|
|
499
|
-
anthropic-model-call.ts createAnthropicModelCall — a real ModelCall this repo ships
|
|
500
|
-
openai-model-call.ts createOpenAIModelCall — same seam, OpenAI's Chat Completions API
|
|
501
|
-
deepseek-model-call.ts createDeepSeekModelCall — same seam, reuses openai-model-call.ts's translation
|
|
502
|
-
agents/file-agent.ts Example agent: summarizes text files
|
|
503
|
-
agents/customer-service-agent.ts Example agent: order/shipment lookup, refund, email
|
|
504
|
-
agents/rag-agent.ts Example agent: retrieves from an in-memory vector index
|
|
505
|
-
adapters/cli.ts Channel adapter: command line
|
|
506
|
-
adapters/http.ts Channel adapter: HTTP API
|
|
507
|
-
skills/ SKILL.md files discoverable by agents, scoped per agent subdirectory
|
|
508
|
-
examples/file-agent/ Sample input files + generated output for the file-agent demo
|
|
509
|
-
Dockerfile, docker-compose.yml Container build + local Redis for testing
|
|
510
|
-
```
|
|
390
|
+
|
|
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.
|