loopengine 0.1.14 → 0.1.22

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.
Files changed (38) hide show
  1. package/README.md +124 -299
  2. package/dist/bin/package-manager.js +10 -1
  3. package/dist/core/agent-config.d.ts +1 -1
  4. package/dist/core/gateway-tools.js +4 -4
  5. package/dist/core/mcpplug.d.ts +23 -0
  6. package/dist/core/mcpplug.js +64 -0
  7. package/dist/core/run-agent.js +2 -2
  8. package/dist/core/session-store.d.ts +1 -1
  9. package/dist/core/session-store.js +1 -1
  10. package/dist/core/sessionknit.d.ts +87 -0
  11. package/dist/core/sessionknit.js +189 -0
  12. package/dist/core/skillgarden/activation.d.ts +4 -0
  13. package/dist/core/skillgarden/activation.js +16 -0
  14. package/dist/core/skillgarden/budget.d.ts +20 -0
  15. package/dist/core/skillgarden/budget.js +28 -0
  16. package/dist/core/skillgarden/discovery.d.ts +11 -0
  17. package/dist/core/skillgarden/discovery.js +40 -0
  18. package/dist/core/skillgarden/frontmatter.d.ts +10 -0
  19. package/dist/core/skillgarden/frontmatter.js +35 -0
  20. package/dist/core/skillgarden/index.d.ts +8 -0
  21. package/dist/core/skillgarden/index.js +12 -0
  22. package/dist/core/skillgarden/loader.d.ts +31 -0
  23. package/dist/core/skillgarden/loader.js +70 -0
  24. package/dist/core/skillgarden/substitute.d.ts +4 -0
  25. package/dist/core/skillgarden/substitute.js +13 -0
  26. package/dist/core/skillgarden/types.d.ts +23 -0
  27. package/dist/core/skillgarden/types.js +1 -0
  28. package/dist/core/toollane.d.ts +46 -0
  29. package/dist/core/toollane.js +69 -0
  30. package/dist/index.d.ts +8 -2
  31. package/dist/index.js +4 -1
  32. package/dist/web/agents-config-page.js +1 -197
  33. package/dist/web/http-tool-admin.d.ts +11 -0
  34. package/dist/web/http-tool-admin.js +39 -2
  35. package/dist/web/skills-admin.js +1 -1
  36. package/package.json +8 -5
  37. package/dist/web/skillgarden-admin.d.ts +0 -24
  38. package/dist/web/skillgarden-admin.js +0 -76
package/README.md CHANGED
@@ -7,25 +7,36 @@ and permission rules, run through a transparent ReAct loop. No chain DSL,
7
7
  no hidden control flow — `core/run-agent.ts` is a single function you can read
8
8
  top to bottom. Run agents over a CLI, an HTTP API, or both.
9
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
10
+ ## Why LoopEngine
11
+
12
+ - **Up and running in one command.** `npx create-loopengine@latest` scaffolds
13
+ a real, standalone project — its own repo, a starter agent, CLI and HTTP
14
+ adapters already wired up. `npm install && npm run dev` and you have a
15
+ running agent server, not a half-finished template to wire up yourself.
16
+ - **A real Admin UI, not just logs.** Open `/agents/config` in a browser to
17
+ see and edit a live agent: its resolved config, permission rules, skills,
18
+ tool connections, and secrets — no redeploy to change a rule or add a
19
+ skill. See [Admin UI](#admin-ui) below.
20
+ - **Human-in-the-loop, wired for real notification channels.** A risky tool
21
+ call or a genuinely ambiguous question routes to a human — live in a chat
22
+ session, or durably via Slack, Lark, email, a generic webhook, or a
23
+ polling queue, resumable minutes or days later. See
24
+ [Human in the loop](#human-in-the-loop) below.
25
+ - **Sessions persist automatically.** Send a message, get a reply, come
26
+ back later with the same session id and continue — no database to wire
27
+ up yourself, and it survives a crash mid-turn cleanly.
28
+ - **A package system for reusable capabilities.** Install a bundle of
29
+ tools, a skill, and the permission rules that gate them with one command
30
+ — real, reviewable files in your own repo, not an opaque dependency. See
31
+ [Package system](#package-system) below.
32
+ - **Does real work, safely.** Tools can hit a database, send emails, call
16
33
  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.
34
+ what happens without a human: safe reads auto-run, risky actions get
35
+ approval or are denied outright.
36
+ - **Multi-tenant and composable out of the box.** The same deployed agent
37
+ serves different customers with different permission levels, no forking
38
+ required — and one agent's folder drops under another's `subagents/` to
39
+ become a tool the parent can delegate to, own tools/rules/loop intact.
29
40
 
30
41
  So the payoff: you write one `AgentConfig`, and get a deployable,
31
42
  persistent, permission-safe service — not just a prompt-and-response demo.
@@ -69,11 +80,14 @@ conventional path under the agent's own folder:
69
80
  | `skillsDirs` | `agents/<name>/skills` (missing → no skills) |
70
81
 
71
82
  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.
83
+ own folder convention (drop another agent's folder there and it becomes
84
+ one of this agent's tools automatically), always merged into `tools` on
85
+ top of whatever the table above resolves to.
74
86
 
75
- `rules` is how you gate what a tool can do without approval — see
76
- "Tool permission and multi-tenancy" below.
87
+ `rules` is how you gate what a tool can do without approval — each rule
88
+ maps a `scope` (tenant/environment) + tool name to `allow`/`ask`/`deny`,
89
+ with an `ask` decision routing to a human — see
90
+ [Human in the loop](#human-in-the-loop) below.
77
91
 
78
92
  The simplest possible agent, `agents/weather-agent/index.ts` — `tools`,
79
93
  `rules`, and `skillsDirs` all omitted, so they default to
@@ -92,281 +106,101 @@ export const config: AgentConfig = {
92
106
  See `agents/customer-service/` and `agents/file-agent/` for complete,
93
107
  working examples.
94
108
 
95
- ## Core concepts
96
-
97
- ### 1. The Loop
98
-
99
- `core/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 go to whatever approver applies for the call's own channel
106
- (see "Tool permission and multi-tenancy" below) — a live approver is
107
- awaited right there; a durable one returns instantly and the call is
108
- resolved later instead.
109
- 3. Approved calls execute and their results feed back into the
110
- conversation.
111
- 4. Repeat from step 1 — until the model stops requesting tools
112
- (`end_turn`) or `AgentConfig.maxTurns` (default 25) is hit, which ends
113
- the turn with `RunAgentResult.stopReason: 'max_turns'`, a real result
114
- rather than a thrown error.
115
-
116
- `runAgent` itself does no I/O — callers own conversation history and the
117
- model call — which is what lets the same loop serve a one-shot CLI call,
118
- a long-lived HTTP chat session, and a fully simulated test, unchanged.
119
-
120
- ### 2. Agents — convention over configuration
121
-
122
- Beyond the `AgentConfig` shape and folder convention shown in "Define
123
- your first agent" above, the discovery mechanism is what makes this
124
- convention-over-configuration: `discoverAgents` scans `agents/` at
125
- startup, keyed by each module's own `AgentConfig.name` — not the
126
- filename or folder name — and builds the name → config lookup every
127
- adapter uses. Nothing to register by hand; a module that exports neither
128
- `config` nor a way to build a model call fails loudly at startup instead
129
- of silently not showing up.
130
-
131
- ### 3. Skills and tools: agent-specific vs. global
132
-
133
- Both skills and tools follow the same rule: **default to agent-specific,
134
- promote to global only once something is genuinely shared.**
135
-
136
- - **Agent-specific** (the default) — lives under that agent's own folder,
137
- seen only by that agent:
138
- ```
139
- agents/<name>/skills/<skill>/SKILL.md
140
- agents/<name>/tools/<tool>.ts
141
- ```
142
- - **Global** — lives at the repo root, seen by every agent that opts in:
143
- ```
144
- skills/<skill>/SKILL.md # pointed at by any agent whose skillsDirs includes it
145
- tools/<tool>.ts # imported explicitly into any agent's tools array
146
- ```
147
-
148
- Skills are discovered via `AgentConfig.skillsDirs` (an array of
149
- directories, `SKILL.md`-based: a short description stays in context, the
150
- full body loads only when the model invokes it). Tools have no
151
- auto-discovery — an agent's `tools` array just imports whichever tool
152
- files it wants, agent-specific or global.
153
-
154
- Both root directories (`tools/`, `skills/`) are empty today — every skill
155
- and tool in this repo is agent-specific. Move something there only when a
156
- second agent actually needs the exact same one, not because it feels
157
- reusable in theory.
158
-
159
- **Opting an agent into a global skill or tool** is explicit either way —
160
- nothing scans the root for you:
161
-
162
- ```ts
163
- // Global skill: add the root path alongside the agent's own
164
- skillsDirs: ['agents/my-agent/skills', 'skills']
165
-
166
- // Global tool: tools has no auto-discovery, so import it and merge it
167
- // into the agent's own array by hand
168
- import { sharedTool } from '../../tools/shared-tool.js'
169
- import { tools as ownTools } from './tools/index.js'
170
-
171
- tools: [...ownTools, sharedTool]
172
- ```
173
-
174
- Tools don't have to be hand-written either. For SaaS actions (GitHub,
175
- Slack, etc.) where you'd rather not own the vendor's OAuth flow yourself,
176
- [`mcpplug`](https://www.npmjs.com/package/mcpplug) connects through
177
- [Composio](https://composio.dev), which already holds the OAuth
178
- relationship with 1000+ apps, and returns tools in the same
179
- `ToolDefinition` shape — so they drop into `tools` right alongside your
180
- own:
181
-
182
- ```ts
183
- import { connectComposioSource } from 'mcpplug'
184
-
185
- const composioTools = await connectComposioSource('composio', {
186
- slugs: ['GITHUB_LIST_REPOSITORIES_FOR_THE_AUTHENTICATED_USER'],
187
- }).then((source) => source.loadTools())
188
-
189
- const config: AgentConfig = {
190
- name: 'my-agent',
191
- tools: [...handWrittenTools, ...composioTools],
192
- rules: [{ scopePattern: 'default/production/my-agent', tool: 'composio_GITHUB_LIST_REPOSITORIES_FOR_THE_AUTHENTICATED_USER', decision: 'allow' }],
193
- defaultDecision: 'ask',
194
- }
195
- ```
196
-
197
- Either kind of tool goes through the same permission gating, parallel
198
- scheduling, and context-budget tracking. See `agents/file-agent/index.ts`
199
- for a working example.
200
-
201
- ### 4. Subagents — an agent as another agent's tool
109
+ ## Admin UI
202
110
 
203
- Drop a folder under `agents/<name>/subagents/<child>/` and `child` becomes
204
- one of `name`'s tools automatically — no import, no `AgentConfig.tools`
205
- edit, nothing to register by hand:
111
+ Run the HTTP adapter and open `http://localhost:8787/agents/config` — a
112
+ live, editable view of every registered agent, no code changes or
113
+ redeploy needed for most of it:
206
114
 
207
- ```
208
- agents/support-orchestrator/
209
- index.ts # the orchestrator's own AgentConfig
210
- subagents/
211
- billing-agent/index.ts # a full AgentConfig, same shape as a top-level agent
212
- refunds-agent/index.ts
213
- ```
214
-
215
- Scaffold one directly:
216
-
217
- ```bash
218
- npx loopengine add-subagent support-orchestrator billing-agent
219
- # -> Created agents/support-orchestrator/subagents/billing-agent/index.ts
220
- ```
221
-
222
- A subagent is a normal `AgentConfig` plus one required field —
223
- `toolDescription`, the text the *parent's* model reads to decide when to
224
- delegate to it (its own `systemPrompt` is instructions for itself, not a
225
- pitch to a caller deciding whether to invoke it):
115
+ | Tab | What you can do |
116
+ | --- | --- |
117
+ | **Overview** | System prompt, model, every tool (with its JSON schema and parallel-safety), and a read-only view of the rules that would actually apply. |
118
+ | **Skills** | Create, edit, and delete `SKILL.md` files for this agent directly in the browser — write the body, preview the rendered markdown, save. |
119
+ | **Tools** | Local hand-written tools, gateway-sourced tools (e.g. [Composio](https://composio.dev)), and subagents-as-tools, in one place. Connect a new external gateway source or add/remove a tool without touching a file. |
120
+ | **ActAuth** | Add, edit, and delete permission rules — scope, tool, condition, decision — and change `default_decision`, live. |
121
+ | **Environment** | Every env var a package (see [Package system](#package-system)) declared it needs, across everything installed for this agent — which ones are set, which are missing, and a form to set one. A value is never echoed back once set. |
122
+
123
+ Every tab is backed by a real API (`GET /agents/:name/config`, `.../actauth`,
124
+ `.../env`, ...) that reuses the exact same resolution `runAgent()` itself
125
+ uses — so what you see here can't drift out of sync with what a real
126
+ request actually gets, and never returns a model API key.
127
+
128
+ ## Human in the loop
129
+
130
+ A risky tool call (`actauth`'s `ask` decision) or a genuinely ambiguous
131
+ question the model itself raises both need a human — and LoopEngine
132
+ handles both the same way, live or durable:
133
+
134
+ - **Live** — someone's actively watching right now (a terminal, an open
135
+ chat session). The turn just waits; `cli`/`http_stream` get this
136
+ automatically, with nothing to configure.
137
+ - **Durable** — nobody's watching (a webhook-triggered run, a ticket that
138
+ came in overnight). The turn ends immediately with a resumable pending
139
+ state, and a real notification goes out on whichever channel you've
140
+ configured — resolvable minutes or days later without holding any
141
+ process open in between.
142
+
143
+ Six channels ship as real, working notifiers — configure one and the
144
+ "send a notification" side is done for you:
145
+
146
+ | Channel | What it does |
147
+ | --- | --- |
148
+ | `webhook` | Signed HMAC-SHA256 POST to any URL you own |
149
+ | `slack` | An interactive message via `chat.postMessage`, with Approve/Deny buttons |
150
+ | `lark` | A Lark/Feishu interactive card |
151
+ | `email` | A signed, expiring magic-link per decision |
152
+ | `database` | Writes a row for your own worker/dashboard to poll |
153
+ | `redis` | Pushes a queue entry for your own worker to consume |
226
154
 
227
155
  ```ts
228
156
  export const config: AgentConfig = {
229
- name: 'billing-agent',
230
- systemPrompt: 'You answer billing questions.',
231
- toolDescription: 'Call this for any billing-related question.',
232
- model: { provider: 'anthropic', model: 'claude-sonnet-5' },
157
+ // ...
158
+ httpNotifier: {
159
+ channel: 'slack',
160
+ config: { botToken: process.env.SLACK_BOT_TOKEN!, channelId: process.env.SLACK_CHANNEL_ID! },
161
+ events: ['approval', 'question'],
162
+ },
233
163
  }
234
164
  ```
235
165
 
236
- Calling that tool runs the subagent's whole ReAct loop — its own tools,
237
- rules, permissions — to completion, and only its final text comes back;
238
- the parent never sees the subagent's turns or tool calls. Orchestration
239
- needs no separate concept on top of this: an "orchestrator" is just an
240
- agent whose tools happen to be other agents, and the model decides which
241
- to call, in what order, the same as any other tool — no chain DSL.
166
+ Reference implementations for the receiving side (verifying a Slack
167
+ click, a webhook HMAC signature, a magic-link token) ship under
168
+ `examples/notifier-handler/` for the four channels that need one. See
169
+ [`HUMAN_IN_THE_LOOP.md`](HUMAN_IN_THE_LOOP.md) for the full setup guide —
170
+ live and durable, worked examples, and how resumption actually works
171
+ under the hood.
242
172
 
243
- Subagents nest — a subagent's own `subagents/` folder works the same way,
244
- one level down, addressed by joining names with `/`:
173
+ ## Package system
174
+
175
+ Giving an agent a new capability is usually three separate, hand-authored
176
+ things: a tool file, a `SKILL.md` teaching the model when to use it, and
177
+ an `actauth` rule allowing it to actually run. A **loopengine package**
178
+ bundles all three into one installable unit:
245
179
 
246
180
  ```bash
247
- npx loopengine add-subagent support-orchestrator/billing-agent disputes-agent
181
+ npx loopengine add-package <spec> --agent customer-service
248
182
  ```
249
183
 
250
- A few things worth knowing before reaching for this:
251
-
252
- - **The call blocks.** Every tool call in the loop is awaited before the
253
- next model turn, so a slow subagent blocks its parent's turn — and if
254
- the parent is itself a subagent of another agent, that blocks too.
255
- There's no background/async or streaming pattern here.
256
- - **Permissions don't inherit.** Each agent's own `actauth.yml` governs
257
- only its own tools; delegating to a subagent neither grants nor
258
- inherits permissions in either direction.
259
- - **`maxTurns` compounds.** A request that reaches a grandchild subagent
260
- spends turns at every level (default 25 each) — a "simple" answer can
261
- cost far more than any one agent's own turn budget suggests.
262
-
263
- ### 5. Gateway tool sources — connecting external tools from a web page
264
-
265
- Beyond hand-written tools and subagents, an agent can pull tools from an
266
- external gateway — [Composio](https://composio.dev) today, with
267
- [`mcpplug`](https://www.npmjs.com/package/mcpplug)'s `ToolSource`
268
- interface designed so more providers (Nango, Arcade, Scalekit, ...) slot
269
- in later as thin adapters, same shape. Unlike hand-written tools, these
270
- are meant to be managed by an operator at runtime, not committed to code:
271
- run the HTTP adapter, open `/agents/config`, pick an agent, and switch to
272
- its "Gateway tools" tab — alongside "Overview" and "Actauth" — to see its
273
- connected sources and add or remove one.
184
+ `<spec>` is anything `npm pack` understands — a public or private npm
185
+ package, a scoped package on a private registry, or a plain git repo. The
186
+ tool files, skill directory, and actauth rules all land as real files in
187
+ the agent's own tree, copied in rather than imported as a `node_modules`
188
+ dependency — reviewable, diffable, and editable the same as anything you
189
+ would have hand-written, not an opaque black box.
274
190
 
275
191
  ```bash
276
- npx tsx --env-file-if-exists=.env adapters/http.ts
277
- # open http://localhost:8787/agents/config
192
+ npx loopengine upgrade-package <packageName> --agent customer-service
193
+ npx loopengine remove-package <packageName> --agent customer-service
278
194
  ```
279
195
 
280
- Adding a source (via the page, or `POST /agents/:name/gateway-tools` with
281
- `{ provider: 'composio', name: 'gh', slugs: [...] }`) writes
282
- `agents/<name>/gateway-tools.yml` — read fresh off disk on every request,
283
- same "no restart to see an edit" behavior `actauth.yml` already has.
284
- Composio's own OAuth is out of band: run `composio link <toolkit>` once
285
- on the machine running the server, then use the page purely to pick which
286
- already-authorized slugs to expose to which agent.
287
-
288
- A newly-added tool is **denied by default**, same "opt-in, not silently
289
- allowed" rule every tool in this repo follows — pass a `decision` when
290
- adding a source (or use the page's permission dropdown) to seed an
291
- `actauth.yml` rule for it instead, or add the rule by hand afterward.
292
-
293
- ### 6. Tool permission and multi-tenancy
294
-
295
- Every tool call is gated by [`actauth`](https://www.npmjs.com/package/actauth):
296
- each rule in `AgentConfig.rules` maps a `scope` (tenant/environment) + tool
297
- name to `allow` / `ask` / `deny`. Anything not covered falls through to
298
- `defaultDecision`. An `ask` decision routes to whichever approver applies
299
- for the call's own channel: `cli`/`http_stream` always get the library's
300
- own live default (`ConsoleApprover`, blocking on stdin, for `cli`; a live
301
- approval popup for `http_stream`) — nothing to configure there — and
302
- `http` gets a durable one whenever `AgentConfig.httpNotifier` is
303
- configured for it (see below), falling back to the same kind of live
304
- default otherwise.
305
-
306
- `ask` approvers come in two shapes. A **live** one (`WebchatApprover`,
307
- `SlackChatApprover`, `ConsoleApprover`) is awaited directly — the turn stays
308
- suspended until a human decides, which only makes sense when someone's
309
- actually there right now (a terminal, an open chat connection). A
310
- **durable** one (`WebhookApprover`) fires a signed webhook and returns
311
- instantly instead — the turn ends with `stopReason: 'pending_approval'`,
312
- durably resumable minutes or days later via
313
- `POST /pending-approvals/:pendingId/resolve`, without holding any
314
- process open in between. See `HUMAN_IN_THE_LOOP.md` for the full design.
315
-
316
- The system `ask_user` tool (a model asking a human a genuinely ambiguous
317
- clarifying question mid-turn) gets the same live/durable split, via
318
- `RunAgentOptions.questionHandler` (or `AgentConfig.httpNotifier`'s own
319
- `'question'` event, for the `http` channel specifically): live by default
320
- (blocks on the channel it's raised on), or durable — a
321
- `DurableQuestionHandler` (e.g. `core/http-notify-triggers/webhook.ts`'s
322
- `WebhookNotifier`) instead ends the turn with `stopReason:
323
- 'pending_question'`, resumable via
324
- `POST /pending-questions/:pendingId/answer`. See `HUMAN_IN_THE_LOOP.md`'s
325
- own "Durable questions" section.
326
-
327
- For the practical "what do I actually need to build" version of all of
328
- this — live chat setup, durable setup, and reference implementations for
329
- Slack/Lark/email/generic-webhook notification channels — see
330
- [`HUMAN_IN_THE_LOOP.md`](HUMAN_IN_THE_LOOP.md).
331
-
332
- A real agent's rules live in `agents/<name>/actauth.yml`:
333
-
334
- ```yaml
335
- default_decision: deny
336
-
337
- rules:
338
- - name: weather-lookup-always-allowed
339
- scope: "*/*"
340
- tool: get_weather
341
- decision: allow
342
- ```
343
-
344
- `scope` is `tenant/environment` — `"*/*"` here means "every tenant, every
345
- environment." Rules are matched most-specific-scope-first, so an agent
346
- that needs different behavior per tenant just adds a more specific rule
347
- (e.g. `acme-corp/production`) alongside the wildcard fallback.
348
-
349
- That `scope` is what makes rules multi-tenant: the same agent can behave
350
- differently for different customers. `environment` is a deployment-wide
351
- setting (`LOOPENGINE_ENV`, default `production`). `tenant` is resolved per
352
- request via `AgentConfig.tenantFor?: (headers, body) => string |
353
- undefined` — from headers only, never the request body, since it feeds
354
- permission decisions directly. No `tenantFor` means every request is the
355
- `'default'` tenant.
356
-
357
- ### 7. Sessions
358
-
359
- A session is one ongoing conversation. Message history persists between
360
- requests automatically — send a message, get a reply, come back later with
361
- the same `sessionId` and continue where you left off. Two storage backends,
362
- picked automatically: a local file store, or Redis if `REDIS_URL` is set
363
- (needed for multiple server instances).
364
-
365
- An agent can define `AgentConfig.sessionIdFor(body)` to control what
366
- counts as "one conversation" for its own domain (e.g. `customer-service`
367
- hashes `customerEmail`, so a request with no email is rejected rather than
368
- starting an anonymous session). Without it, the caller just supplies a
369
- `sessionId`, or gets one generated for them.
196
+ Upgrading does a real three-way merge per file — a hand-edit since install
197
+ survives, a genuine conflict leaves `<<<<<<<` markers to resolve by hand
198
+ instead of silently overwriting either side. Removing refuses a file
199
+ that's been modified since install unless you pass `--force`. A package
200
+ can also declare the env vars its tools need, which then show up in the
201
+ Admin UI's [Environment tab](#admin-ui) automatically once installed. See
202
+ [`PACKAGES.md`](PACKAGES.md) for the full design — format, publishing,
203
+ and how the merge/upgrade mechanics work in detail.
370
204
 
371
205
  ## Running an agent
372
206
 
@@ -443,18 +277,8 @@ plain-HTTP/CLI transport bindings, and a JSON Schema
443
277
  `core/client.ts` and the `examples/chatbox/react`/`examples/chatbox/vue`
444
278
  hooks are reference implementations of it.
445
279
 
446
- **Agent config page:** open `http://localhost:8787/agents/config`
447
- (optionally `?agent=<name>`) to browse every registered agent's resolved
448
- config — system prompt, model, tools (with their JSON schemas and
449
- parallel-safety), the actual ActAuth rules that would be enforced (source,
450
- default decision, per-rule scope/tool/decision/`when`), and whether
451
- `sessionIdFor`/`tenantFor`/`isSafeTool` are custom or defaulted, and
452
- whether the `http` channel's approver is custom (`httpNotifier` covers
453
- `'approval'`) or the library's own default. Backed
454
- by `GET /agents/:name/config`, which reuses the same
455
- rule/tool resolution `runAgent()` itself uses, so it can't drift out of
456
- sync with what a real request actually gets. Never returns
457
- `AgentModelConfig.apiKey`.
280
+ **Agent config page:** this is the [Admin UI](#admin-ui) described above
281
+ — open `http://localhost:8787/agents/config` (optionally `?agent=<name>`).
458
282
 
459
283
  ## Wiring a real model
460
284
 
@@ -490,13 +314,14 @@ testing), export your own `createModelCall(): ModelCall` instead.
490
314
  | `core/budget.ts` / `core/compaction.ts` | Context-window budget tracking and tail-preserving compaction (vendored in-repo, not an external dependency) |
491
315
  | `core/recovery.ts` | Retries prompt-too-long / truncated-output failures (vendored in-repo, not an external dependency) |
492
316
  | `core/durable-approvals.ts` | `TurnCheckpoint`/`CheckpointStore` (file/Redis) backing durable, resumable `ask` decisions — see `HUMAN_IN_THE_LOOP.md` |
493
- | [`sessionknit`](https://www.npmjs.com/package/sessionknit) | Durable session log with crash-interruption detection |
494
- | [`skillgarden`](https://www.npmjs.com/package/skillgarden) | `SKILL.md` discovery and lazy loading |
495
- | [`toollane`](https://www.npmjs.com/package/toollane) | Parallel/solo tool-call scheduling |
496
- | [`mcpplug`](https://www.npmjs.com/package/mcpplug) | Gateway tool sourcing (e.g. Composio) |
497
-
498
- Installing `loopengine` pulls all of these in as regular dependencies —
499
- install one directly only if you want to use it standalone.
317
+ | `core/sessionknit.ts` | Durable session log with crash-interruption detection (vendored in-repo, not an external dependency) |
318
+ | `core/skillgarden/` | `SKILL.md` discovery and lazy loading (vendored in-repo, not an external dependency) |
319
+ | `core/toollane.ts` | Parallel/solo tool-call scheduling (vendored in-repo, not an external dependency) |
320
+ | `core/mcpplug.ts` | Gateway tool sourcing (e.g. Composio) (vendored in-repo, not an external dependency) |
321
+
322
+ Installing `loopengine` pulls in `actauth` as a regular dependency —
323
+ install it directly only if you want to use it standalone. Everything
324
+ else in this table ships inside `loopengine` itself.
500
325
 
501
326
  ## Deployment
502
327
 
@@ -24,7 +24,7 @@ import { tmpdir } from 'node:os';
24
24
  import { parse as parseYaml } from 'yaml';
25
25
  import semver from 'semver';
26
26
  import { agentDir } from '../core/gateway-tools.js';
27
- import { addToolToIndex, toCamelCase } from '../web/http-tool-admin.js';
27
+ import { addToolToIndex, removeToolFromIndex, toCamelCase } from '../web/http-tool-admin.js';
28
28
  import { addActauthRule, updateActauthRule, removeActauthRule, readActauthConfig } from '../web/actauth-admin.js';
29
29
  export class PackageManifestError extends Error {
30
30
  }
@@ -484,6 +484,7 @@ export function removePackage(agentName, packageName, force = false) {
484
484
  const refused = [];
485
485
  const remainingTools = [];
486
486
  const remainingSkills = [];
487
+ const toolsIndexPath = join(agentDir(agentName), 'tools', 'index.ts');
487
488
  for (const toolName of record.tools) {
488
489
  const relPath = `tools/${toolName}.ts`;
489
490
  const fullPath = join(agentDir(agentName), relPath);
@@ -493,6 +494,14 @@ export function removePackage(agentName, packageName, force = false) {
493
494
  continue;
494
495
  }
495
496
  rmSync(fullPath, { force: true });
497
+ // Undoes installPackage's own addToolToIndex call — without this, a
498
+ // deleted tool's import survives in tools/index.ts, and either fails
499
+ // the next build (a dangling import to a file that no longer exists)
500
+ // or, worse, silently duplicates on a later add-package of the same
501
+ // tool (addToolToIndex used to have no idempotence check at all —
502
+ // now it does, but this is the actual fix: the stale entry shouldn't
503
+ // be there to begin with).
504
+ removeToolFromIndex(toolsIndexPath, toolName, toCamelCase(toolName));
496
505
  removed.push(relPath);
497
506
  }
498
507
  for (const skillId of record.skills) {
@@ -1,6 +1,6 @@
1
1
  import type { Rule, Decision, Scope } from 'actauth';
2
2
  import type { Redis } from 'ioredis';
3
- import type { SafetyClassifier } from 'toollane';
3
+ import type { SafetyClassifier } from './toollane.js';
4
4
  import type { Summarizer } from './compaction.js';
5
5
  /** Which channel a runAgent() call is on — the value RunAgentOptions.channel
6
6
  * carries, checked against directly by AgentConfig.httpNotifier's own
@@ -1,7 +1,7 @@
1
1
  // Lets an operator register external tool gateways (Composio today —
2
- // Nango, Arcade, Scalekit are meant to slot in later as thin mcpplug
3
- // ToolSource adapters, same shape, once this mechanism is proven) against
4
- // an agent, without hand-writing a tools/index.ts. The registry is
2
+ // Nango, Arcade, Scalekit are meant to slot in later as thin
3
+ // mcpplug.ts ToolSource adapters, same shape, once this mechanism is
4
+ // proven) against an agent, without hand-writing a tools/index.ts. The registry is
5
5
  // agents/<name>/gateway-tools.yml — plain data, read fresh off disk every
6
6
  // call, same "no restart to see an edit" behavior actauth.yml already
7
7
  // has (see run-agent.ts's loadRules). adapters/http.ts's admin routes are
@@ -12,7 +12,7 @@ import { join } from 'node:path';
12
12
  import { execFile } from 'node:child_process';
13
13
  import { promisify } from 'node:util';
14
14
  import { parse as parseYaml, parseDocument, stringify as stringifyYaml } from 'yaml';
15
- import { connectComposioSource } from 'mcpplug';
15
+ import { connectComposioSource } from './mcpplug.js';
16
16
  const execFileAsync = promisify(execFile);
17
17
  // gateway-tools.yml is plain data (see this file's own header comment),
18
18
  // not compiled code — same "resolved against process.cwd(), like
@@ -0,0 +1,23 @@
1
+ import type { ToolDefinition } from './agent-config.js';
2
+ /** One connection to one external tool gateway (Composio today; a raw MCP
3
+ * client is a later, separate source). `name` is used to namespace every
4
+ * tool it produces, so two sources never collide. */
5
+ export interface ToolSource {
6
+ name: string;
7
+ loadTools(): Promise<ToolDefinition[]>;
8
+ close(): Promise<void>;
9
+ }
10
+ export interface ComposioSourceOptions {
11
+ /** Composio tool slugs to expose, e.g.
12
+ * "GITHUB_LIST_REPOSITORIES_FOR_THE_AUTHENTICATED_USER". Explicit, not
13
+ * auto-discovered — same reasoning actauth rules are opt-in per tool
14
+ * rather than a blanket allow; an agent should only see the Composio
15
+ * actions its config actually lists. */
16
+ slugs: string[];
17
+ /** Defaults to 'composio' — override to point at a stand-in binary in tests. */
18
+ cliCommand?: string;
19
+ }
20
+ /** Sources ToolDefinitions from the Composio CLI — already-authenticated
21
+ * (`composio link <toolkit>`) tool execution across 1000+ apps, so this
22
+ * source needs no OAuth code of its own; Composio owns that entirely. */
23
+ export declare function connectComposioSource(name: string, options: ComposioSourceOptions): Promise<ToolSource>;
@@ -0,0 +1,64 @@
1
+ // Formerly the standalone `mcpplug` package — folded in here since it
2
+ // had exactly one real consumer (gateway-tools.ts, below) and the
3
+ // "usable by any agent runtime" framing it shipped with was aspirational,
4
+ // never actually exercised by a second host. Reuses this repo's own
5
+ // ToolDefinition (agent-config.ts) directly instead of mcpplug's own
6
+ // parallel-but-identical copy — that duplication only existed to keep
7
+ // mcpplug import-free of loopengine, which stopped mattering once it
8
+ // moved in.
9
+ import { execFile } from 'node:child_process';
10
+ import { promisify } from 'node:util';
11
+ const run = promisify(execFile);
12
+ function humanize(slug) {
13
+ return slug.toLowerCase().split('_').join(' ');
14
+ }
15
+ async function runCli(cliCommand, args) {
16
+ let stdout;
17
+ try {
18
+ ;
19
+ ({ stdout } = await run(cliCommand, args));
20
+ }
21
+ catch (err) {
22
+ const message = err instanceof Error ? err.message : String(err);
23
+ throw new Error(`composio CLI call failed (${args.join(' ')}): ${message}`);
24
+ }
25
+ try {
26
+ return JSON.parse(stdout);
27
+ }
28
+ catch {
29
+ throw new Error(`composio CLI returned non-JSON output for ${args.join(' ')}: ${stdout}`);
30
+ }
31
+ }
32
+ /** Sources ToolDefinitions from the Composio CLI — already-authenticated
33
+ * (`composio link <toolkit>`) tool execution across 1000+ apps, so this
34
+ * source needs no OAuth code of its own; Composio owns that entirely. */
35
+ export async function connectComposioSource(name, options) {
36
+ const cliCommand = options.cliCommand ?? 'composio';
37
+ return {
38
+ name,
39
+ async loadTools() {
40
+ const tools = [];
41
+ for (const slug of options.slugs) {
42
+ const schema = await runCli(cliCommand, ['execute', slug, '--get-schema']);
43
+ const inputSchema = schema.inputSchema ?? {};
44
+ tools.push({
45
+ name: `${name}_${slug}`,
46
+ description: humanize(slug),
47
+ input_schema: inputSchema,
48
+ execute: async (input) => {
49
+ const result = await runCli(cliCommand, ['execute', slug, '-d', JSON.stringify(input)]);
50
+ if (result.successful === false || result.error) {
51
+ throw new Error(`composio tool ${slug} failed: ${JSON.stringify(result.error ?? result)}`);
52
+ }
53
+ return result.data ?? result;
54
+ },
55
+ });
56
+ }
57
+ return tools;
58
+ },
59
+ // Each call is its own subprocess — nothing held open between calls,
60
+ // so there's nothing to tear down. Kept for symmetry with sources
61
+ // that do hold a live connection (e.g. a future raw MCP client).
62
+ close: async () => { },
63
+ };
64
+ }