@borgee/agents-host 0.1.6 → 0.1.7

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
@@ -1,40 +1,63 @@
1
1
  # @borgee/agents-host
2
2
 
3
- Minimal local runtime host: connects a local **Claude Code CLI** (`claude`) or
4
- **GitHub Copilot CLI** (`copilot`) process to a single Borgee agent over
5
- [`@borgee/plugin-sdk`](../sdk/plugin-ts) — the same BPP (`/ws/plugin`)
6
- protocol the official OpenClaw plugin uses.
3
+ Minimal local runtime host for Borgee agents.
4
+
5
+ - **Single-agent mode** keeps the existing one-process / one-agent flow.
6
+ - **Local-config mode** adds first-step multi-agent local hosting from a host
7
+ config file plus one config file per agent, with hot reload.
7
8
 
8
9
  ## What this is (and isn't)
9
10
 
10
- This package is intentionally the *smallest* slice that proves the full loop:
11
+ This package intentionally focuses on the smallest local-hosting loop:
11
12
 
12
- ```
13
+ ```text
13
14
  Borgee channel message
14
15
  → /ws/plugin (BPP)
15
16
  → @borgee/plugin-sdk
16
17
  → AgentsHost.handleMessage
17
- → local `claude`/`copilot` CLI subprocess
18
+ → local claude / copilot CLI subprocess
18
19
  → BPP reply
19
20
  → Borgee channel
20
21
  ```
21
22
 
22
- **In scope:** one agent per process, per-channel conversation memory via each
23
- provider's native session mechanism (Claude CLI resume, Copilot ACP sessions),
24
- and dispatching to a local CLI provider.
23
+ **In scope:**
24
+
25
+ - single-agent env / CLI startup
26
+ - local-config multi-agent startup
27
+ - one isolated `AgentsHost` per effective agent key
28
+ - per-channel conversation memory via each provider's native session mechanism
29
+ - hot reload for host config and agent file add / update / remove
30
+
31
+ **Out of scope:**
32
+
33
+ - remote command execution / shell dispatch
34
+ - node provisioning
35
+ - systemd/service installation
36
+ - scheduled / periodic prompts
25
37
 
26
- **Out of scope** (see the larger `feat/multi-agent-runtime-host` work for
27
- these): remote command execution / shell dispatch, node provisioning,
28
- multi-agent discovery/sync, systemd service install, and scheduled/periodic
29
- prompts. To run several agents, run several processes with different
30
- `BORGEE_AGENT_API_KEY` values.
38
+ ## Running
31
39
 
32
- ## Setup
40
+ ```bash
41
+ pnpm --filter @borgee/agents-host dev # tsx, env-var single-agent mode
42
+ pnpm --filter @borgee/agents-host build # tsc -> dist/
43
+ pnpm --filter @borgee/agents-host start # node dist/index.js, env-var single-agent mode
44
+ ```
33
45
 
34
- 1. In the Borgee web UI, create an Agent and reveal its API key.
35
- 2. Install the Claude Code CLI (`claude`) or GitHub Copilot CLI (`copilot`)
36
- locally and make sure it works standalone.
37
- 3. Run this package with the environment variables below.
46
+ For the published CLI entry point:
47
+
48
+ ```bash
49
+ agents-host start <serverUrl> <apiKey> [options]
50
+ agents-host start --config <path-to-host-config>
51
+ agents-host validate --config <path-to-host-config>
52
+ agents-host print-layout --root <dir>
53
+ agents-host generate-config --root <dir> --stdin
54
+ agents-host generate-config --root <dir> --spec-json <json>
55
+ ```
56
+
57
+ ## Single-agent mode
58
+
59
+ Use the existing env-var flow when you want exactly one hosted agent per
60
+ process.
38
61
 
39
62
  ```bash
40
63
  BORGEE_BASE_URL=https://your-borgee-server \
@@ -44,48 +67,265 @@ RUNTIME_PROVIDER=claude \
44
67
  pnpm --filter @borgee/agents-host dev
45
68
  ```
46
69
 
47
- ## Environment variables
70
+ Or with the CLI:
48
71
 
49
- | Variable | Required | Default | Description |
50
- | --- | --- | --- | --- |
51
- | `BORGEE_BASE_URL` | yes | — | Borgee server base URL (e.g. `https://borgee.example.com`) |
52
- | `BORGEE_AGENT_API_KEY` | yes | — | The agent's API key, revealed from the web UI |
53
- | `BORGEE_AGENT_NAME` | no | `Assistant` | Display name used in prompts |
54
- | `RUNTIME_PROVIDER` | no | `claude` | `claude` or `copilot` |
55
- | `CLAUDE_COMMAND` / `CLAUDE_ARGS` | no | `claude` / `--print` | Local Claude CLI command + args |
56
- | `COPILOT_COMMAND` / `COPILOT_ARGS` | no | `copilot` / empty | Local Copilot CLI command. `COPILOT_ARGS` is accepted for backward-compatible config/CLI parsing but ignored by the persistent Copilot ACP prototype, which always launches `copilot --acp`. |
72
+ ```bash
73
+ agents-host start https://your-borgee-server bgr_xxxxxxxx --provider copilot
74
+ ```
57
75
 
58
- ## Conversation memory
76
+ ### Environment variables / equivalent single-agent CLI flags
59
77
 
60
- Each Borgee channel is mapped 1:1 to a provider-native session for the
61
- process's lifetime:
78
+ | Variable | CLI flag | Required | Default | Description |
79
+ | ---------------------------------- | -------------------------------------- | -------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------ |
80
+ | `BORGEE_BASE_URL` | positional `<serverUrl>` | yes | — | Borgee server base URL |
81
+ | `BORGEE_AGENT_API_KEY` | positional `<apiKey>` | yes | — | Agent API key from the web UI |
82
+ | `BORGEE_AGENT_NAME` | `--name` | no | `Assistant` | Display name used in prompts |
83
+ | `RUNTIME_PROVIDER` | `--provider` | no | `claude` | `claude` or `copilot` |
84
+ | `CLAUDE_COMMAND` / `CLAUDE_ARGS` | `--claude-command` / `--claude-args` | no | `claude` / `--print` | Local Claude CLI command + args |
85
+ | `COPILOT_COMMAND` / `COPILOT_ARGS` | `--copilot-command` / `--copilot-args` | no | `copilot` / parsed but ignored by ACP runtime | Local Copilot CLI command. The persistent Copilot ACP prototype always launches `copilot --acp`. |
86
+ | `COPILOT_SESSION_TTL_MINUTES` | `--copilot-session-ttl-minutes` | no | `2880` | Idle TTL for per-channel Copilot ACP sessions |
62
87
 
63
- - **Claude**: first turn for a channel uses `--session-id <uuid>`; every
64
- turn after uses `-r/--resume <uuid>` (Claude treats these as distinct
65
- operations).
66
- - **Copilot**: one persistent `copilot --acp` subprocess is shared by the
67
- host, with one ACP session per Borgee channel. Same-channel turns are
68
- serialized so they cannot interleave; different channels keep isolated ACP
69
- sessions.
88
+ ## Local-config mode
70
89
 
71
- There is no separate history/context store on our side; if the runtime host
72
- process restarts, each channel starts a fresh CLI session (no session-id
73
- persistence across restarts in this minimal version).
90
+ Run one supervisor process from a host config file:
74
91
 
75
- ## Running
92
+ ```bash
93
+ agents-host start --config ./agents-host.yaml
94
+ ```
95
+
96
+ Validate the same local-config files without starting agents, the supervisor,
97
+ watchers, control-plane connections, or provider subprocesses:
76
98
 
77
99
  ```bash
78
- pnpm --filter @borgee/agents-host dev # tsx, no build step
79
- pnpm --filter @borgee/agents-host build # tsc -> dist/
80
- pnpm --filter @borgee/agents-host start # node dist/index.js
100
+ agents-host validate --config ./agents-host.yaml
101
+ ```
102
+
103
+ `validate` supports local-config mode only and reuses the same
104
+ `loadLocalConfigSnapshot()` validation path as `start --config`.
105
+
106
+ Print the canonical default local-config layout for a root directory without
107
+ writing files or starting any runtime processes:
108
+
109
+ ```bash
110
+ agents-host print-layout --root ./runtime-root
81
111
  ```
82
112
 
83
- There is no systemd/service installer here — run it as a plain foreground
84
- process (or wrap it with your own process manager) and stop it with
85
- `Ctrl+C`/`SIGTERM`.
113
+ This command prints JSON only. It describes the default bootstrap layout:
114
+
115
+ - host config file: `<root>/agents-host.yaml`
116
+ - agents directory: `<root>/agents`
117
+
118
+ It does **not** read an existing host config file, so it does **not** reflect a
119
+ custom `agentsDir` override from `agents-host.yaml`.
120
+
121
+ Materialize the canonical default local-config files under that root. Pass the
122
+ secret-bearing JSON spec on standard input so it is not exposed in process
123
+ arguments:
124
+
125
+ ```bash
126
+ printf '%s' '{"host":{"borgeeBaseUrl":"https://borgee.example.com"},"agents":[{"key":"cp1","name":"Copilot","apiKey":"bgr_xxx","provider":"copilot"}]}' \
127
+ | agents-host generate-config --root ./runtime-root --stdin
128
+ ```
129
+
130
+ `generate-config` is machine-oriented:
131
+
132
+ - it accepts one JSON spec object via `--stdin`; `--spec-json` remains only as
133
+ a compatibility input and exposes the JSON in process arguments
134
+ - it always writes the canonical default layout from `print-layout`
135
+ - it publishes a complete validated generation atomically; a failed generation
136
+ leaves the prior active set in place
137
+ - concurrent writers for the same managed root are serialized through a
138
+ root-local lock; a writer waits up to 10 seconds, then fails clearly if the
139
+ lock remains held
140
+ - it retains the active and immediately previous generations and reader-leased
141
+ generations, then removes older generated generations (including superseded API keys)
142
+ - if that post-publication pruning fails, the apply remains successful and its
143
+ JSON summary includes a `warnings` entry with code
144
+ `PRUNE_SUPERSEDED_GENERATIONS_FAILED`
145
+ - it uses a dedicated managed root: `<root>/agents-host.yaml` and
146
+ `<root>/agents` are stable links to the active private generation
147
+ - it replaces stale supported agent config files as part of that atomic
148
+ full-set publication, never by pruning the active set first
149
+ - it creates and normalizes the managed root and generation directories to
150
+ mode `0700`, and generated host and agent config files to mode `0600`
151
+ - it prints a JSON summary without echoing agent API keys
152
+
153
+ Use a new empty directory as the managed root. `generate-config` refuses a
154
+ root whose managed links or generation storage have been replaced with
155
+ unexpected file types or targets. Existing hand-managed local-config layouts
156
+ continue to work with `start --config` and `validate --config`; they are not
157
+ converted in place by this command.
158
+
159
+ The spec shape is:
160
+
161
+ ```json
162
+ {
163
+ "host": {
164
+ "borgeeBaseUrl": "https://borgee.example.com",
165
+ "defaults": {
166
+ "claudeCommand": "claude",
167
+ "claudeArgs": ["--print"],
168
+ "copilotCommand": "copilot",
169
+ "copilotArgs": ["-s", "--no-color", "--allow-all-tools", "--output-format", "text"],
170
+ "copilotSessionTtlMinutes": 2880
171
+ }
172
+ },
173
+ "agents": [
174
+ {
175
+ "key": "cp1",
176
+ "name": "Copilot",
177
+ "apiKey": "bgr_xxx",
178
+ "provider": "copilot",
179
+ "enabled": true
180
+ }
181
+ ]
182
+ }
183
+ ```
184
+
185
+ `host.agentsDir` is intentionally **not** accepted here. Canonical generation
186
+ always targets `<root>/agents`, and validation is re-run after generation using
187
+ the same local-config loader as `start --config` / `validate --config`. Start
188
+ the generated layout with `agents-host start --config <root>/agents-host.yaml`;
189
+ that stable path follows the active atomically published generation.
190
+ The loader preserves that stable path for validation output and publication
191
+ watches, while pinning resolved host and agents-directory paths before reading.
192
+ Managed snapshots hold a short-lived reader lease until the read finishes, so
193
+ each snapshot uses one generation even if publication changes `current`
194
+ concurrently.
195
+
196
+ The supervisor loads:
197
+
198
+ - one **host config file**
199
+ - one **agent config file per discovered file** under the agents directory
200
+ - one isolated `AgentsHost` per enabled agent key
201
+
202
+ ### Supported file types
203
+
204
+ Host and agent config files may be:
205
+
206
+ - `*.yaml`
207
+ - `*.yml`
208
+ - `*.json`
209
+
210
+ Parsing uses the `yaml` package for all supported file types.
211
+
212
+ ### Recommended layout
213
+
214
+ ```text
215
+ packages/agents-host/
216
+ agents-host.yaml
217
+ agents/
218
+ support.yaml
219
+ triage.json
220
+ ```
221
+
222
+ ### Host config
223
+
224
+ `agentsDir` defaults to `./agents` relative to the host config file. Absolute
225
+ paths are also allowed.
226
+
227
+ ```yaml
228
+ borgeeBaseUrl: https://borgee.example.com
229
+ agentsDir: ./agents
230
+ defaults:
231
+ claudeCommand: claude
232
+ claudeArgs:
233
+ - --print
234
+ copilotCommand: copilot
235
+ copilotArgs:
236
+ - -s
237
+ - --no-color
238
+ - --allow-all-tools
239
+ - --output-format
240
+ - text
241
+ copilotSessionTtlMinutes: 2880
242
+ ```
243
+
244
+ Host config fields:
245
+
246
+ - `borgeeBaseUrl` (**required**)
247
+ - `agentsDir` (optional, default `./agents` relative to the host config file)
248
+ - `defaults.claudeCommand`
249
+ - `defaults.claudeArgs`
250
+ - `defaults.copilotCommand`
251
+ - `defaults.copilotArgs`
252
+ - `defaults.copilotSessionTtlMinutes`
253
+
254
+ Absent host defaults fall back to the same code-level defaults used by the
255
+ single-agent env / CLI flow.
256
+
257
+ ### Agent config
258
+
259
+ ```yaml
260
+ key: support-bot
261
+ name: Support Bot
262
+ apiKey: bgr_support_xxxxxxxx
263
+ provider: claude
264
+ enabled: true
265
+ claudeArgs:
266
+ - --print
267
+ - --model
268
+ - sonnet
269
+ ```
270
+
271
+ Agent config fields:
272
+
273
+ - `key` (**required**): stable identity used for diffing and reload decisions
274
+ - `name` (**required**)
275
+ - `apiKey` (**required**)
276
+ - `provider` (**required**): `claude` or `copilot`
277
+ - `enabled` (optional, default `true`)
278
+ - optional overrides for `claudeCommand`, `claudeArgs`, `copilotCommand`,
279
+ `copilotArgs`, and `copilotSessionTtlMinutes`
280
+
281
+ Array overrides are **replacement**, not concatenation. For example, an agent
282
+ `claudeArgs` value replaces the host default `claudeArgs` entirely.
283
+
284
+ ## Hot reload behavior
285
+
286
+ The supervisor watches:
287
+
288
+ - the stable host-config parent, so an atomic `current` publication triggers a reload
289
+ - the resolved host-config generation directory
290
+ - the resolved configured agents directory
291
+
292
+ Every `rename` / `change` event is treated as a reload trigger. Reloads are
293
+ serialized with initial startup and debounced by at least 300ms. Each reload
294
+ does a full rescan, validation pass, diff, and apply cycle.
295
+
296
+ Diff rules:
297
+
298
+ - `enabled: false` agents are excluded before diffing
299
+ - new `key` → start a new isolated `AgentsHost`
300
+ - removed `key` → stop that agent
301
+ - same `key` + same effective config → no-op
302
+ - same `key` + changed effective config → stop old runner, then start replacement
303
+
304
+ Changing an agent `key` is treated as **remove old + add new**, so provider-side
305
+ session state is not preserved across the key change.
306
+
307
+ If a reload candidate is invalid (malformed host config, malformed agent config,
308
+ or duplicate agent keys), the supervisor logs the error and keeps the last
309
+ successfully applied snapshot running.
310
+
311
+ ## Conversation memory
312
+
313
+ Each Borgee channel is mapped 1:1 to a provider-native session while it stays
314
+ active:
315
+
316
+ - **Claude**: first turn for a channel uses `--session-id <uuid>`; every turn
317
+ after uses `-r/--resume <uuid>`.
318
+ - **Copilot**: one persistent `copilot --acp` subprocess is shared by a single
319
+ `AgentsHost`, with one ACP session per Borgee channel. Same-channel turns are
320
+ serialized; different channels keep isolated ACP sessions.
321
+
322
+ There is no separate history store on our side. If a runner restarts, that
323
+ agent's provider-side channel sessions start fresh.
86
324
 
87
325
  ## Testing
88
326
 
89
327
  ```bash
90
- pnpm --filter @borgee/agents-host test
328
+ pnpm --filter @borgee/agents-host exec vitest run --testTimeout=10000
329
+ pnpm --filter @borgee/agents-host typecheck
330
+ pnpm --filter @borgee/agents-host build
91
331
  ```
@@ -0,0 +1,54 @@
1
+ import type { AgentsHostConfig, LocalConfigSnapshot } from './types.js';
2
+ export interface HostRunner {
3
+ start(): Promise<void>;
4
+ stop(): Promise<void>;
5
+ }
6
+ export interface WatchHandle {
7
+ close(): void;
8
+ }
9
+ export interface WatchEventInfo {
10
+ eventType?: string;
11
+ filename?: string;
12
+ }
13
+ export interface LoggerLike {
14
+ log(...args: unknown[]): void;
15
+ error(...args: unknown[]): void;
16
+ }
17
+ export interface AgentsHostSupervisorDeps {
18
+ createHost?: (config: AgentsHostConfig) => HostRunner;
19
+ loadSnapshot?: (configPath: string) => Promise<LocalConfigSnapshot>;
20
+ watchPath?: (path: string, onEvent: (event?: WatchEventInfo) => void) => WatchHandle;
21
+ logger?: LoggerLike;
22
+ }
23
+ export declare class AgentsHostSupervisor {
24
+ private readonly hostConfigPath;
25
+ private readonly createHost;
26
+ private readonly loadSnapshot;
27
+ private readonly watchPath;
28
+ private readonly logger;
29
+ private readonly activeHosts;
30
+ private readonly watchers;
31
+ private currentSnapshot;
32
+ private reloadTimer;
33
+ private reloadInFlight;
34
+ private pendingReload;
35
+ private runningReloadPromise;
36
+ private shuttingDown;
37
+ private started;
38
+ constructor(hostConfigPath: string, deps?: AgentsHostSupervisorDeps);
39
+ start(): Promise<void>;
40
+ stop(): Promise<void>;
41
+ private scheduleReload;
42
+ private clearReloadTimer;
43
+ private runReloadLoop;
44
+ private reloadOnce;
45
+ private refreshWatchers;
46
+ private getWatchPaths;
47
+ private handleWatchEvent;
48
+ private installStableWatcher;
49
+ private closeAllWatchers;
50
+ private cleanUpFailedStart;
51
+ private applySnapshot;
52
+ private startDesiredAgent;
53
+ private stopActiveHost;
54
+ }