@swarm-agent/sdk 0.1.49 → 0.1.50
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/HEADLESS_UI.md +4 -6
- package/README.md +121 -56
- package/dist/agents.d.ts +114 -0
- package/dist/agents.js +183 -0
- package/dist/auth.d.ts +14 -0
- package/dist/auth.js +16 -0
- package/dist/box.d.ts +68 -0
- package/dist/box.js +13 -0
- package/dist/browser.d.ts +6 -2
- package/dist/browser.js +11 -3
- package/dist/chat.d.ts +41 -7
- package/dist/chat.js +148 -83
- package/dist/client.d.ts +12 -0
- package/dist/client.js +19 -6
- package/dist/copy-tags.d.ts +26 -0
- package/dist/copy-tags.js +133 -0
- package/dist/errors.d.ts +16 -0
- package/dist/errors.js +21 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/local-connection.d.ts +39 -0
- package/dist/local-connection.js +56 -0
- package/dist/permission-ui.d.ts +1 -0
- package/dist/permission-ui.js +37 -0
- package/dist/permissions.d.ts +4 -0
- package/dist/permissions.js +7 -0
- package/dist/projects.d.ts +25 -3
- package/dist/projects.js +65 -6
- package/dist/provider-auth.d.ts +5 -3
- package/dist/provider-auth.js +6 -8
- package/dist/realtime.d.ts +40 -6
- package/dist/realtime.js +156 -52
- package/dist/remote.d.ts +59 -0
- package/dist/remote.js +46 -0
- package/dist/sessions.d.ts +31 -1
- package/dist/sessions.js +49 -1
- package/dist/settings.d.ts +3 -0
- package/dist/settings.js +19 -3
- package/dist/signals.d.ts +148 -0
- package/dist/signals.js +97 -0
- package/dist/types.d.ts +70 -2
- package/dist/websocket.d.ts +3 -1
- package/dist/websocket.js +1 -1
- package/dist/workspaces.d.ts +11 -0
- package/dist/workspaces.js +17 -0
- package/package.json +1 -1
package/HEADLESS_UI.md
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# Build a real headless UI with the SDK
|
|
2
2
|
|
|
3
|
-
For planned SDK completeness, safe tool control and non-production qualification, see the [five-step headless application plan](HEADLESS_PLAN.md). That roadmap is not a claim of implemented capabilities.
|
|
4
|
-
|
|
5
3
|
## Trust boundary
|
|
6
4
|
|
|
7
5
|
Run the administrative Node SDK in your backend-for-frontend **inside the same container as swarmd**, using its configured private Unix socket. Do not mount/export that socket, put daemon credentials in browser code, or forward arbitrary browser-selected daemon paths. Authenticate your app's users, check Origin/CSRF, and allowlist app operations. Never log credential request bodies. The separate `--container-sdk-port` listener remains session-only: it cannot onboard providers, change settings, or create workspaces.
|
|
@@ -18,7 +16,7 @@ const providers = await swarm.settings.providers(); // readiness and supported a
|
|
|
18
16
|
const credentials = await swarm.auth.credentials.list(); // redacted status only
|
|
19
17
|
</copy>
|
|
20
18
|
|
|
21
|
-
Your app must supply its configured socket path and authenticated local identity. This is not a recipe to import host credentials.
|
|
19
|
+
Your app must supply its configured socket path and authenticated local identity. This is not a recipe to import host credentials. Create the daemon identity with `onboarding.ensureBootstrapped({ username, swarm_name })` when needed (the name is kept as typed; repeating it is a no-op). It does not mark onboarding finished: once your setup (provider key, models, workspace) is done, call `onboarding.update({ desktop_onboarding_complete: true })`.
|
|
22
20
|
|
|
23
21
|
For an API key collected through your app UI, pass the user-selected provider and its advertised credential type. The first-provider endpoint requires zero existing credentials/agents; use the ordinary save endpoint afterward. Errors are not silently retried on a different route.
|
|
24
22
|
|
|
@@ -86,13 +84,13 @@ await watch.done; // rejects on terminal transport/protocol/auth errors
|
|
|
86
84
|
// On app disconnect: watch.dispose() or abort.abort().
|
|
87
85
|
</copy>
|
|
88
86
|
|
|
89
|
-
The watcher hydrates one session (including its session view), waits for `hello` (`v3.realtime`, version 1), and resumes from the **snapshot handoff cursor**, not the newer hello head. The server replays to the subscription before delivering live updates. It advertises `live_patch_v1` only when the server offers it. UTF-8 byte offsets and
|
|
87
|
+
The watcher hydrates one session (including its session view), waits for `hello` (`v3.realtime`, version 1), and resumes from the **snapshot handoff cursor**, not the newer hello head. The server replays to the subscription before delivering live updates. It advertises `live_patch_v1` only when the server offers it. UTF-8 byte offsets place live text: duplicates and overlaps are trimmed, and output that starts beyond the durable prefix (normal when joining or reconnecting mid-turn, since durable checkpoints trail live text by up to 250ms) is held until a durable checkpoint fills the gap, never shown with a hole. Committed messages remain authoritative. Foreign-session frames never update the view.
|
|
90
88
|
|
|
91
|
-
Durable events trigger coalesced `/v3/sync/hydrate` reads, not interval polling. Disconnects, cursor errors and slow consumers start fresh scoped hydration/replay with bounded exponential backoff (six retries, 250ms to 8s). Authentication/protocol errors terminate. No cursor is parsed, numerically compared, or persisted across watches/principals. `dispose` cancels HTTP, closes WebSocket, and ends `done`; a disposed-before-ready watcher rejects `ready`.
|
|
89
|
+
Durable events delivered on the socket are merged into `state.snapshot.events_by_session` immediately (deduplicated by sequence, newest 1000 kept); other than streaming `*.delta` checkpoints they also trigger coalesced `/v3/sync/hydrate` reads, not interval polling. Disconnects, cursor errors and slow consumers start fresh scoped hydration/replay with bounded exponential backoff (six consecutive retries per outage, 250ms to 8s; a replayed connection restores the budget). While reconnecting, `state` keeps the last rendered conversation with `status: 'reconnecting'`. Authentication/protocol errors terminate. No cursor is parsed, numerically compared, or persisted across watches/principals. `dispose` cancels HTTP, closes WebSocket, and ends `done`; a disposed-before-ready watcher rejects `ready`.
|
|
92
90
|
|
|
93
91
|
### Bounded view and raw access
|
|
94
92
|
|
|
95
|
-
The high-level view is a **200-message / 200-event tail**, not the complete transcript. Inspect `snapshot.pagination`, `omissions`, history manifests and session views before claiming complete history. A mid-stream reconnect whose retained event tail lacks the stream prefix may show no partial draft until a durable complete message arrives; it never guesses missing text.
|
|
93
|
+
The high-level view is a **200-message / 200-event tail**, not the complete transcript. Inspect `snapshot.pagination`, `omissions`, history manifests and session views before claiming complete history. A mid-stream reconnect whose retained event tail lacks the stream prefix may show no partial draft until a durable complete message arrives; it never guesses missing text. Live overlays are bounded to 64 streams / 4 MiB per stream. Tool, plan, permission and other resources stay in the canonical raw snapshot; the SDK does not invent a second plan authority.
|
|
96
94
|
|
|
97
95
|
`realtime.bootstrap(request)`, `hydrate(request)` and `replay({...request, endpoint_cursor, limit})` expose typed canonical V3 envelopes for advanced consumers. Keep surface, selector and resources identical when using `/v3/sync/stream` cursors. `realtime.socket(path)` exposes transport only; raw consumers own reducer/replay correctness. The convenience watcher uses fresh durable repair on reconnect rather than retaining a possibly stale speculative cache.
|
|
98
96
|
|
package/README.md
CHANGED
|
@@ -2,26 +2,46 @@
|
|
|
2
2
|
|
|
3
3
|
Official TypeScript SDK for [Swarm](https://github.com/swarm-agent/swarm), the local-first AI coding workspace.
|
|
4
4
|
|
|
5
|
-
`@swarm-agent/sdk` provides a typed
|
|
5
|
+
`@swarm-agent/sdk` provides a typed client to interact programmatically with the Swarm daemon (`swarmd`) over HTTP and its local owner socket.
|
|
6
6
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
9
|
-
- **
|
|
10
|
-
- **
|
|
9
|
+
- **Finds your local Swarm**: In Node, `new SwarmClient()` connects to the owner socket of a system install (`/var/lib/swarmd/local-transport/api.sock`) when the process may use it, otherwise to `http://127.0.0.1:7781`.
|
|
10
|
+
- **Owner Socket**: The owner socket is trusted daemon-owner IPC: run as the user that runs Swarm and no token is needed.
|
|
11
11
|
- **Granular Scoped Tokens**: Create, list, revoke, and purge scoped deploy tokens (`swk_...`) with fine-grained permissions (`automations:trigger`, `sessions:read`, `sessions:write`, `admin`).
|
|
12
12
|
- **Worker / Automation V2 Triggers**: Trigger workers on-demand with dynamic caller context (e.g. GitHub Webhooks, CI/CD events, alert payloads).
|
|
13
13
|
- **Session & Workspace Lifecycle**: Create durable V3 sessions, manage runs, send messages, poll for completion, and inspect workspaces.
|
|
14
14
|
- **Typed Error Hierarchy**: `SwarmAuthError` (401), `SwarmForbiddenError` (403), `SwarmNotFoundError` (404), `SwarmConflictError` (409), `SwarmTimeoutError`.
|
|
15
15
|
|
|
16
|
-
##
|
|
16
|
+
## Install
|
|
17
17
|
|
|
18
|
-
Node 22
|
|
19
|
-
is not a claim of registry ownership or publication. Install the local candidate
|
|
20
|
-
from a clean consumer directory (no Swarm checkout or Desktop required):
|
|
18
|
+
Requires Node 22 or newer (ESM). The package is public on npm:
|
|
21
19
|
|
|
22
20
|
<copy>
|
|
23
|
-
npm install
|
|
24
|
-
|
|
21
|
+
npm install @swarm-agent/sdk
|
|
22
|
+
</copy>
|
|
23
|
+
|
|
24
|
+
You need a running Swarm on Linux x86_64. Install one with
|
|
25
|
+
`curl -fsSL https://swarmagent.dev/install | sh -s -- --yes`.
|
|
26
|
+
|
|
27
|
+
## Connect to your Swarm
|
|
28
|
+
|
|
29
|
+
`new SwarmClient()` with no options picks the first of:
|
|
30
|
+
|
|
31
|
+
1. `socketPath` or `baseUrl` passed to the constructor;
|
|
32
|
+
2. `SWARM_SOCKET_PATH` (a Unix socket);
|
|
33
|
+
3. `SWARM_API_URL` (or `SWARM_DESKTOP_URL`);
|
|
34
|
+
4. Node only: the owner socket `/var/lib/swarmd/local-transport/api.sock`, if it
|
|
35
|
+
exists, is a socket and this process can read and write it;
|
|
36
|
+
5. `http://127.0.0.1:7781`, the daemon's default API address.
|
|
37
|
+
|
|
38
|
+
The owner socket's directory is private to the user that runs Swarm, so other
|
|
39
|
+
users fall through to HTTP, which needs a token (`token` or `SWARM_AUTH_TOKEN`).
|
|
40
|
+
Browser code never touches the filesystem; use `@swarm-agent/sdk/browser` there.
|
|
41
|
+
`resolveSwarmConnection()` returns the choice without connecting.
|
|
42
|
+
|
|
43
|
+
<copy>
|
|
44
|
+
node --input-type=module -e "import { SwarmClient } from '@swarm-agent/sdk'; const c = new SwarmClient(); console.log(c.config.socketPath ?? c.config.baseUrl)"
|
|
25
45
|
</copy>
|
|
26
46
|
|
|
27
47
|
For maintainers, the SDK builds independently with its own locked dev dependencies:
|
|
@@ -38,7 +58,7 @@ example, not source tests or node_modules. `prepack` builds locally; there are n
|
|
|
38
58
|
install/prepare hooks. TypeScript/tsx are build/test tools, not runtime dependencies.
|
|
39
59
|
CommonJS `require` and non-Node runtimes are not validated by this packaging step.
|
|
40
60
|
|
|
41
|
-
For the headless container, use the matching `@swarm-agent/cli`
|
|
61
|
+
For the headless container, use the matching `@swarm-agent/cli` package and explicitly
|
|
42
62
|
export a scoped token through `swarm-headless setup -- sdk-token`. Connect to
|
|
43
63
|
`http://127.0.0.1:7783`; **do not** use Desktop bootstrap or a privileged socket.
|
|
44
64
|
The headless listener allows session operations only, not the complete API below.
|
|
@@ -76,10 +96,9 @@ The SDK can automatically discover credentials from your environment or Codex OA
|
|
|
76
96
|
```typescript
|
|
77
97
|
import { SwarmClient } from '@swarm-agent/sdk';
|
|
78
98
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
});
|
|
99
|
+
// Owner socket when this process may use it, else http://127.0.0.1:7781
|
|
100
|
+
// (see "Connect to your Swarm"; pass socketPath/baseUrl/token to choose).
|
|
101
|
+
const client = new SwarmClient();
|
|
83
102
|
|
|
84
103
|
// 1. Ensure headless identity is bootstrapped
|
|
85
104
|
await client.onboarding.ensureBootstrapped({ username: 'developer', swarm_name: 'My Swarm' });
|
|
@@ -133,7 +152,9 @@ await client.settings.restoreDefaults();
|
|
|
133
152
|
|
|
134
153
|
Use `describePermission(record)` to render tool details, conservative per-request `actions`,
|
|
135
154
|
question IDs, options and custom-response fields. Treat all text/arguments as untrusted text,
|
|
136
|
-
not HTML. `
|
|
155
|
+
not HTML. `title` is always a plain-text heading: the request's own title, else the bash
|
|
156
|
+
command, the first ask-user question, or the humanized tool name and action (for example
|
|
157
|
+
`Manage projects: propose task (Add login)`). `parseError` leaves malformed questions deny-only. These helpers do not grant authority:
|
|
137
158
|
the backend enforces policy, project scope and specialized plan/proposal acceptance.
|
|
138
159
|
|
|
139
160
|
```typescript
|
|
@@ -172,51 +193,68 @@ always requires actual user input.
|
|
|
172
193
|
|
|
173
194
|
### 4. Interactive Chat & Live Tool Stream
|
|
174
195
|
|
|
175
|
-
|
|
196
|
+
One `chat.stream()` watch per selected conversation serves every turn. It hydrates the
|
|
197
|
+
session, resumes `/v3/realtime/stream` from the snapshot cursor, and reconnects on its own.
|
|
176
198
|
|
|
177
199
|
```typescript
|
|
178
|
-
|
|
179
|
-
const session = await client.chat.createSession({
|
|
180
|
-
title: 'Repository Exploration',
|
|
181
|
-
workspace_path: '/path/to/project',
|
|
182
|
-
});
|
|
200
|
+
const session = await client.chat.createSession({ title: 'Repo tour', workspace_path: '/path/to/repo' });
|
|
183
201
|
|
|
184
|
-
|
|
202
|
+
let answer = '';
|
|
185
203
|
const stream = await client.chat.stream(session.id, {
|
|
186
|
-
onText: (delta
|
|
187
|
-
onReasoning: (
|
|
188
|
-
onToolStarted: (tool) =>
|
|
189
|
-
onToolDelta: (tool,
|
|
190
|
-
onToolCompleted: (tool) =>
|
|
191
|
-
onPermissionRequested:
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
? answerPermission(perm, userInput) : {});
|
|
197
|
-
showConfirmedDecision(result.permission);
|
|
198
|
-
} catch (error) { showPermissionError(perm.id, error); }
|
|
199
|
-
});
|
|
204
|
+
onText: (delta) => { answer += delta; render(answer); }, // deltas join to exactly the reply
|
|
205
|
+
onReasoning: (_delta, full) => renderThinking(full),
|
|
206
|
+
onToolStarted: (tool) => showTool(tool.callId, tool.name, tool.arguments),
|
|
207
|
+
onToolDelta: (tool, chunk) => appendToolOutput(tool.callId, chunk),
|
|
208
|
+
onToolCompleted: (tool) => finishTool(tool.callId, tool.status), // completed | failed | cancelled
|
|
209
|
+
onPermissionRequested: (perm, resolve) => askUser(perm, resolve), // see section 3; never auto-answer
|
|
210
|
+
onState: (state) => setStatus(state.status), // syncing | live | reconnecting
|
|
211
|
+
onComplete: (messages, run) => { // once per run, success or not
|
|
212
|
+
if (run.status !== 'completed') showFailure(run.status, run.blocked_reason, messages);
|
|
213
|
+
answer = '';
|
|
200
214
|
},
|
|
201
|
-
|
|
202
|
-
onPermissionError: (error, perm) => showPermissionError(perm.id, error),
|
|
203
|
-
onState: (state) => renderConversation(state.messages, state.live, state.snapshot),
|
|
204
|
-
onComplete: () => console.log('Turn finished; stream remains open'),
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
await stream.ready; // Hydration and replay MUST finish before dispatch.
|
|
208
|
-
await client.chat.sendMessage(session.id, 'Find all configuration files and summarize them.');
|
|
209
|
-
// Keep this watch for subsequent messages. Dispose only on switch/unmount/shutdown.
|
|
210
|
-
// stream.done means the watch ended, not that a single assistant turn finished.
|
|
211
|
-
|
|
212
|
-
// 3. Or use chat.run while the watch above serves the explicit permission UI
|
|
213
|
-
const response = await client.chat.run(session.id, {
|
|
214
|
-
message: 'What is the git status of this repository?',
|
|
215
|
-
timeoutMs: 120_000,
|
|
215
|
+
onError: (error) => showDisconnected(error), // terminal: auth/protocol/retries exhausted
|
|
216
216
|
});
|
|
217
|
-
|
|
217
|
+
await stream.ready; // hydrated and replayed: safe to send
|
|
218
|
+
await client.chat.sendMessage(session.id, 'Summarize the build setup.');
|
|
219
|
+
// Send further turns on the same watch. Switching conversations: stream.dispose(), then
|
|
220
|
+
// chat.stream(otherId). client.sessions.list() lists conversations.
|
|
218
221
|
```
|
|
219
222
|
|
|
223
|
+
Guarantees, verified end to end against the API server (see "Streaming verification" below):
|
|
224
|
+
|
|
225
|
+
- `onText` deltas concatenate to exactly the assistant text, including when the socket drops
|
|
226
|
+
mid-turn: nothing is repeated and nothing is skipped; the last call carries any committed
|
|
227
|
+
text beyond what streamed. Use `fullText` (2nd argument) to replace instead of append.
|
|
228
|
+
- A dropped socket reconnects (exponential backoff, 6 consecutive attempts per outage) and
|
|
229
|
+
resumes from durable state; `state.status` is `reconnecting` meanwhile and keeps the last view.
|
|
230
|
+
- `onComplete(messages, run)` fires once per finished run, whatever its outcome.
|
|
231
|
+
A failed provider turn has `run.status === 'failed'` and a system message in `messages`.
|
|
232
|
+
- Durable events (newest 1000) are in `state.snapshot.events_by_session[id]` once, in sequence order.
|
|
233
|
+
- Live tool objects carry `callId`, equal to `payload.call_id` on the durable `session.tool.*`
|
|
234
|
+
events. When you merge history cards with live cards, match on `callId`: `tool.id` is kept
|
|
235
|
+
for compatibility and can be the step-scoped instance id (`step-2:call_…`).
|
|
236
|
+
- A conversation whose history no longer fits the hydrate window (for example after a very
|
|
237
|
+
large tool result) still opens: the watch retries with `history.manifest_policy: 'manifest'`,
|
|
238
|
+
keeps the newest window, and sets `state.historyOmitted = true` with
|
|
239
|
+
`state.historyOmissions` (resource and `next_cursor`) so you can show "older history not
|
|
240
|
+
loaded" instead of presenting the window as complete.
|
|
241
|
+
|
|
242
|
+
`state.messages` (a 200-message tail) and `state.live` are the rendering authority;
|
|
243
|
+
callbacks are conveniences. Clearing context: `client.sessions.clearContext(id, { expectedLastEventSeq })`
|
|
244
|
+
starts a fresh model context in the same conversation, but the daemon allows it only for an
|
|
245
|
+
idle project conversation (`SwarmConflictError` otherwise, nothing changes); for a standalone
|
|
246
|
+
chat, create a new session.
|
|
247
|
+
|
|
248
|
+
**Streaming verification.** `swarmd/internal/api` `TestSDKChatStreamE2E` (opt-in:
|
|
249
|
+
`npm run build`, then `SWARM_SDK_E2E=1 go test ./internal/api -run TestSDKChatStreamE2E`)
|
|
250
|
+
drives the built package (`e2e/chat-stream.e2e.mjs`) against the real API server, store,
|
|
251
|
+
executor and realtime socket with a scripted provider: incremental text, a socket drop mid-turn
|
|
252
|
+
while durable checkpoints lag, completion, failure, session switching and context clear. It does
|
|
253
|
+
not exercise a real model, tool calls or permission prompts, or token authentication.
|
|
254
|
+
|
|
255
|
+
`chat.run()` (polling, returns the final reply) remains available for scripts; keep a
|
|
256
|
+
`chat.stream()` watch open when tool permissions need an explicit answer.
|
|
257
|
+
|
|
220
258
|
---
|
|
221
259
|
|
|
222
260
|
### 5. Native WebSocket Streaming Bridge & Browser Client
|
|
@@ -257,9 +295,9 @@ chat.on('disconnected', disableComposer);
|
|
|
257
295
|
// 1. Hook into live tool execution and token streams
|
|
258
296
|
chat.on('text', (delta, fullText) => renderLiveMarkdown(fullText));
|
|
259
297
|
chat.on('reasoning', (delta, fullText) => renderModelThinking(fullText));
|
|
260
|
-
chat.on('tool_start', (tool) => renderToolStartedCard(tool.
|
|
261
|
-
chat.on('tool_delta', (tool, delta) => appendToolOutput(tool.
|
|
262
|
-
chat.on('tool_done', (tool) => markToolCompleted(tool.
|
|
298
|
+
chat.on('tool_start', (tool) => renderToolStartedCard(tool.callId, tool.name, tool.arguments));
|
|
299
|
+
chat.on('tool_delta', (tool, delta) => appendToolOutput(tool.callId, delta));
|
|
300
|
+
chat.on('tool_done', (tool) => markToolCompleted(tool.callId, tool.durationMs));
|
|
263
301
|
// Replace pending cards on hydration/live updates/reconnect, keyed by permission.id.
|
|
264
302
|
// Preserve unsent input for unchanged requests; do not submit during render.
|
|
265
303
|
chat.on('permissions', pending => renderPendingForms(pending.map(describePermission)));
|
|
@@ -285,7 +323,9 @@ const requestId = chat.sendMessage('Inspect the git repository and list modified
|
|
|
285
323
|
// `accepted` acknowledges durable dispatch. Never blindly resend an uncertain message.
|
|
286
324
|
// Retain requestId if your UI offers an explicit retry, and pass it as argument 4.
|
|
287
325
|
// Switching: await chat.subscribe(otherSession.id). Reconnect rehydrates selected history.
|
|
288
|
-
// Unmount: chat.dispose().
|
|
326
|
+
// Unmount: chat.dispose(). Sends that cannot go out throw SwarmChatNotReadyError (nothing was
|
|
327
|
+
// sent): its message is fit to show people, and `code` is chat_reconnecting (retry shortly),
|
|
328
|
+
// chat_disconnected (call connect()) or chat_not_subscribed (subscribe first).
|
|
289
329
|
```
|
|
290
330
|
|
|
291
331
|
Backend conversation routes should use the same project identity for listing and creation:
|
|
@@ -298,6 +338,17 @@ const created = await client.projects.createConversation(project.id, {
|
|
|
298
338
|
// Return created.id to the browser; await chat.subscribe(created.id) before sending.
|
|
299
339
|
```
|
|
300
340
|
|
|
341
|
+
Conversation titles come from Swarm: a conversation created without a `title` is named from
|
|
342
|
+
its first message (refined later when a title model is available), and `listConversations`
|
|
343
|
+
returns it. Rename with `client.sessions.rename(id, title)` (or
|
|
344
|
+
`client.projects.renameConversation(id, title)`); a renamed title is kept.
|
|
345
|
+
|
|
346
|
+
Assistant text can contain `<copy label="…">…</copy>` snippet tags, which Swarm's own apps
|
|
347
|
+
render as copy buttons. `chat.run`, `chat.listMessages` and `chat.stream` (and the browser
|
|
348
|
+
bridge built on it) turn them into Markdown code blocks by default; pass `rawCopyTags: true`
|
|
349
|
+
to keep them. For text read elsewhere (`sessions.get`, `realtime.watchSession`), use the
|
|
350
|
+
exported `copyTagsToMarkdown(text)`.
|
|
351
|
+
|
|
301
352
|
Use `state` as the conversation rendering authority, including initial history, failed/empty
|
|
302
353
|
turns and durable reconnect recovery. `text`/tool callbacks are convenience notifications.
|
|
303
354
|
History is a bounded 200-message tail; use canonical sync hydration for additional history.
|
|
@@ -442,8 +493,22 @@ await client.projects.refineTask(project.id, task.id, {
|
|
|
442
493
|
|
|
443
494
|
// Stream or replay outbox events starting after an opaque cursor
|
|
444
495
|
const streamResult = await client.projects.replayEvents('opaque_cursor_token');
|
|
496
|
+
|
|
497
|
+
// Integrate a finished task's commits into its captured target branch
|
|
498
|
+
const integration = await client.projects.integrateTask(project.id, task.id);
|
|
445
499
|
```
|
|
446
500
|
|
|
501
|
+
`integrateTask` reads the task first (`getTask`, which observes Git) and sends the exact
|
|
502
|
+
selection the daemon checks: `session_id` (the task's current session) and the captured
|
|
503
|
+
`source_branch`/`target_branch` (from `task.delivery_assessment`, else the task's
|
|
504
|
+
`worktree_branch`/`base_branch`, else its active attempt), plus `revision`, `attempt_id` and the
|
|
505
|
+
observed `source_head`/`target_head`. Override any field with
|
|
506
|
+
`integrateTask(projectId, taskId, { selection: { target_branch: 'main' } })`; supplying
|
|
507
|
+
`session_id`, `source_branch` and `target_branch` skips the read. A task with no captured
|
|
508
|
+
worktree lane throws `SwarmValidationError` without calling the daemon; a stale selection, dirty
|
|
509
|
+
checkout or non-actionable delivery state comes back as `SwarmConflictError` and nothing is
|
|
510
|
+
integrated.
|
|
511
|
+
|
|
447
512
|
### 7. Programmable Push Alert Webhooks
|
|
448
513
|
|
|
449
514
|
Register push alert webhook destinations to receive HMAC-signed real-time notifications on worker lifecycle events (`started`, `succeeded`, `failed`, `retry_exhausted`):
|
package/dist/agents.d.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { SwarmTransport } from './transport.js';
|
|
2
|
+
/** A typed tool the application answers. Swarm validates and waits; it never executes it. */
|
|
3
|
+
export interface ClientToolDefinition {
|
|
4
|
+
name: string;
|
|
5
|
+
description: string;
|
|
6
|
+
/** JSON Schema for the arguments; must be `type: "object"` with only local `$ref`s. */
|
|
7
|
+
input_schema: Record<string, unknown>;
|
|
8
|
+
/** `write` marks a tool with external effects (confirm with a person before running it). */
|
|
9
|
+
effect?: 'read' | 'write';
|
|
10
|
+
/** How long a call waits for an answer before the model gets "client tool timed out" (max 300000). */
|
|
11
|
+
timeout_ms?: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* An agent whose only capabilities are the client tools listed. It has no
|
|
15
|
+
* built-in tools: no shell, files, web or delegation. Swarm enforces this
|
|
16
|
+
* when each call executes, not only in what the model is shown.
|
|
17
|
+
*/
|
|
18
|
+
export interface SealedAgentDefinition {
|
|
19
|
+
name: string;
|
|
20
|
+
prompt: string;
|
|
21
|
+
description?: string;
|
|
22
|
+
tools: string[];
|
|
23
|
+
provider?: string;
|
|
24
|
+
model?: string;
|
|
25
|
+
thinking?: string;
|
|
26
|
+
/** Bounds one message's cost. Unset fields use the sealed defaults (4 model calls, 1024 output tokens, last 40 messages, 60 s). */
|
|
27
|
+
limits?: {
|
|
28
|
+
max_steps?: number;
|
|
29
|
+
max_output_tokens?: number;
|
|
30
|
+
max_history_messages?: number;
|
|
31
|
+
run_timeout_ms?: number;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/** The account's spend today and its daily cap, as the daemon enforces it. */
|
|
35
|
+
export interface SpendToday {
|
|
36
|
+
today_cost_usd: number;
|
|
37
|
+
daily_cost_limit_usd: number;
|
|
38
|
+
enabled: boolean;
|
|
39
|
+
limit_exceeded: boolean;
|
|
40
|
+
}
|
|
41
|
+
/** A pending client tool call your application must answer. */
|
|
42
|
+
export interface ClientToolCall {
|
|
43
|
+
/** Pass to `answer`/`fail`. */
|
|
44
|
+
id: string;
|
|
45
|
+
sessionId: string;
|
|
46
|
+
runId: string;
|
|
47
|
+
tool: string;
|
|
48
|
+
/** Already validated by Swarm against the tool's input_schema. */
|
|
49
|
+
arguments: Record<string, unknown>;
|
|
50
|
+
createdAt: number;
|
|
51
|
+
}
|
|
52
|
+
/** Throw from a handler to send this message to the model; other errors send "tool failed". */
|
|
53
|
+
export declare class ClientToolError extends Error {
|
|
54
|
+
constructor(message: string);
|
|
55
|
+
}
|
|
56
|
+
export type ClientToolHandler = (args: Record<string, unknown>, call: ClientToolCall) => Promise<unknown> | unknown;
|
|
57
|
+
/** One visitor turn: the agent's reply and every message the turn added (including tool results). */
|
|
58
|
+
export interface ConverseResult {
|
|
59
|
+
reply: string;
|
|
60
|
+
messages: Array<Record<string, any>>;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Sealed agents and client tools (`/v2/agents`, `/v2/custom-tools`). Defining
|
|
64
|
+
* them needs the owner or an `admin` token: keep this in a trusted backend.
|
|
65
|
+
*/
|
|
66
|
+
export declare class SwarmAgentsNamespace {
|
|
67
|
+
private readonly transport;
|
|
68
|
+
constructor(transport: SwarmTransport);
|
|
69
|
+
defineClientTool(definition: ClientToolDefinition): Promise<Record<string, unknown>>;
|
|
70
|
+
defineSealedAgent(definition: SealedAgentDefinition): Promise<Record<string, unknown>>;
|
|
71
|
+
/**
|
|
72
|
+
* Mints the token a gateway holds to serve one sealed agent. It can only
|
|
73
|
+
* create and use that agent's sessions and answer its client tool calls;
|
|
74
|
+
* it stops working if the agent is given any built-in tool. Owner only.
|
|
75
|
+
*/
|
|
76
|
+
createGatewayToken(agentName: string, options?: {
|
|
77
|
+
name?: string;
|
|
78
|
+
expiresInSeconds?: number;
|
|
79
|
+
messagesPerMinute?: number;
|
|
80
|
+
sessionsPerHour?: number;
|
|
81
|
+
}): Promise<{
|
|
82
|
+
token: string;
|
|
83
|
+
record: Record<string, unknown>;
|
|
84
|
+
}>;
|
|
85
|
+
/** Today's spend against the daily cap. A gateway token may read it. */
|
|
86
|
+
spendToday(): Promise<SpendToday>;
|
|
87
|
+
removeAgent(name: string): Promise<void>;
|
|
88
|
+
removeClientTool(name: string): Promise<void>;
|
|
89
|
+
/** Client tool calls waiting in a session, limited to the tool names you answer. */
|
|
90
|
+
pendingCalls(sessionId: string, tools: Iterable<string>): Promise<ClientToolCall[]>;
|
|
91
|
+
/** Returns `result` (any JSON value) to the model as the tool's output. */
|
|
92
|
+
answer(sessionId: string, callId: string, result: unknown): Promise<void>;
|
|
93
|
+
/** Returns `reason` to the model as the tool's error. */
|
|
94
|
+
fail(sessionId: string, callId: string, reason: string): Promise<void>;
|
|
95
|
+
/**
|
|
96
|
+
* Answers one session's client tool calls with `handlers` until `signal`
|
|
97
|
+
* aborts. Each call is answered at most once; a handler error fails the
|
|
98
|
+
* call with "tool failed" unless it is a ClientToolError.
|
|
99
|
+
*/
|
|
100
|
+
serve(sessionId: string, handlers: Record<string, ClientToolHandler>, options?: {
|
|
101
|
+
signal?: AbortSignal;
|
|
102
|
+
intervalMs?: number;
|
|
103
|
+
}): Promise<void>;
|
|
104
|
+
/**
|
|
105
|
+
* Sends one user message to a sealed agent's session, answers its client
|
|
106
|
+
* tool calls with `handlers` while it runs, and returns once the run has
|
|
107
|
+
* finished. This is the whole loop a gateway needs per visitor message.
|
|
108
|
+
*/
|
|
109
|
+
converse(sessionId: string, message: string, handlers: Record<string, ClientToolHandler>, options?: {
|
|
110
|
+
timeoutMs?: number;
|
|
111
|
+
pollIntervalMs?: number;
|
|
112
|
+
}): Promise<ConverseResult>;
|
|
113
|
+
private resolve;
|
|
114
|
+
}
|
package/dist/agents.js
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { SwarmSessionsNamespace } from './sessions.js';
|
|
2
|
+
/** Throw from a handler to send this message to the model; other errors send "tool failed". */
|
|
3
|
+
export class ClientToolError extends Error {
|
|
4
|
+
constructor(message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = 'ClientToolError';
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
const NAME = /^[a-z][a-z0-9_]{0,63}$/;
|
|
10
|
+
function toolName(name) {
|
|
11
|
+
if (typeof name !== 'string' || !NAME.test(name))
|
|
12
|
+
throw new Error('tool and agent names are lowercase letters, digits and underscores');
|
|
13
|
+
return name;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Sealed agents and client tools (`/v2/agents`, `/v2/custom-tools`). Defining
|
|
17
|
+
* them needs the owner or an `admin` token: keep this in a trusted backend.
|
|
18
|
+
*/
|
|
19
|
+
export class SwarmAgentsNamespace {
|
|
20
|
+
transport;
|
|
21
|
+
constructor(transport) {
|
|
22
|
+
this.transport = transport;
|
|
23
|
+
}
|
|
24
|
+
async defineClientTool(definition) {
|
|
25
|
+
const name = toolName(definition.name);
|
|
26
|
+
const res = await this.transport.request(`/v2/custom-tools/${name}`, {
|
|
27
|
+
method: 'PUT',
|
|
28
|
+
body: {
|
|
29
|
+
kind: 'client',
|
|
30
|
+
description: definition.description,
|
|
31
|
+
input_schema: definition.input_schema,
|
|
32
|
+
effect: definition.effect ?? 'read',
|
|
33
|
+
...(definition.timeout_ms !== undefined ? { timeout_ms: definition.timeout_ms } : {}),
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
return res.data.custom_tool;
|
|
37
|
+
}
|
|
38
|
+
async defineSealedAgent(definition) {
|
|
39
|
+
const name = toolName(definition.name);
|
|
40
|
+
if (!Array.isArray(definition.tools))
|
|
41
|
+
throw new Error('tools must be an array of client tool names');
|
|
42
|
+
const tools = {};
|
|
43
|
+
for (const tool of definition.tools)
|
|
44
|
+
tools[toolName(tool)] = { enabled: true };
|
|
45
|
+
const res = await this.transport.request(`/v2/agents/${name}`, {
|
|
46
|
+
method: 'PUT',
|
|
47
|
+
body: {
|
|
48
|
+
mode: 'subagent',
|
|
49
|
+
description: definition.description ?? '',
|
|
50
|
+
prompt: definition.prompt,
|
|
51
|
+
...(definition.provider ? { provider: definition.provider } : {}),
|
|
52
|
+
...(definition.model ? { model: definition.model } : {}),
|
|
53
|
+
...(definition.thinking ? { thinking: definition.thinking } : {}),
|
|
54
|
+
...(definition.limits ? { limits: definition.limits } : {}),
|
|
55
|
+
tool_contract: { preset: 'custom', tools },
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
return res.data.profile;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Mints the token a gateway holds to serve one sealed agent. It can only
|
|
62
|
+
* create and use that agent's sessions and answer its client tool calls;
|
|
63
|
+
* it stops working if the agent is given any built-in tool. Owner only.
|
|
64
|
+
*/
|
|
65
|
+
async createGatewayToken(agentName, options = {}) {
|
|
66
|
+
const agent = toolName(agentName);
|
|
67
|
+
const res = await this.transport.request('/v3/auth/tokens', {
|
|
68
|
+
method: 'POST',
|
|
69
|
+
body: {
|
|
70
|
+
name: options.name ?? `${agent} gateway`, agent_name: agent,
|
|
71
|
+
...(options.expiresInSeconds ? { expires_in_seconds: options.expiresInSeconds } : {}),
|
|
72
|
+
...(options.messagesPerMinute ? { messages_per_minute: options.messagesPerMinute } : {}),
|
|
73
|
+
...(options.sessionsPerHour ? { sessions_per_hour: options.sessionsPerHour } : {}),
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
return { token: res.data.token, record: res.data.record };
|
|
77
|
+
}
|
|
78
|
+
/** Today's spend against the daily cap. A gateway token may read it. */
|
|
79
|
+
async spendToday() {
|
|
80
|
+
const res = await this.transport.request('/v3/sessions:usage-limits');
|
|
81
|
+
return res.data.limits;
|
|
82
|
+
}
|
|
83
|
+
async removeAgent(name) {
|
|
84
|
+
await this.transport.request(`/v2/agents/${toolName(name)}`, { method: 'DELETE' });
|
|
85
|
+
}
|
|
86
|
+
async removeClientTool(name) {
|
|
87
|
+
await this.transport.request(`/v2/custom-tools/${toolName(name)}`, { method: 'DELETE' });
|
|
88
|
+
}
|
|
89
|
+
/** Client tool calls waiting in a session, limited to the tool names you answer. */
|
|
90
|
+
async pendingCalls(sessionId, tools) {
|
|
91
|
+
const names = new Set(tools);
|
|
92
|
+
const res = await this.transport.request(`/v3/sessions/${encodeURIComponent(sessionId)}/permissions?status=pending&limit=100`);
|
|
93
|
+
const calls = [];
|
|
94
|
+
for (const record of res.data.permissions ?? []) {
|
|
95
|
+
if (!names.has(record.tool_name) || record.session_id !== sessionId)
|
|
96
|
+
continue;
|
|
97
|
+
let args;
|
|
98
|
+
try {
|
|
99
|
+
args = JSON.parse(record.tool_call_arguments || record.tool_arguments || '{}');
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
args = null;
|
|
103
|
+
}
|
|
104
|
+
if (!args || typeof args !== 'object' || Array.isArray(args))
|
|
105
|
+
continue;
|
|
106
|
+
calls.push({ id: record.id, sessionId, runId: record.run_id, tool: record.tool_name, arguments: args, createdAt: record.created_at });
|
|
107
|
+
}
|
|
108
|
+
return calls;
|
|
109
|
+
}
|
|
110
|
+
/** Returns `result` (any JSON value) to the model as the tool's output. */
|
|
111
|
+
async answer(sessionId, callId, result) {
|
|
112
|
+
await this.resolve(sessionId, callId, { action: 'allow_once', reason: '', approved_arguments: { result: JSON.parse(JSON.stringify(result ?? null)) } });
|
|
113
|
+
}
|
|
114
|
+
/** Returns `reason` to the model as the tool's error. */
|
|
115
|
+
async fail(sessionId, callId, reason) {
|
|
116
|
+
await this.resolve(sessionId, callId, { action: 'deny', reason: String(reason || 'tool failed') });
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Answers one session's client tool calls with `handlers` until `signal`
|
|
120
|
+
* aborts. Each call is answered at most once; a handler error fails the
|
|
121
|
+
* call with "tool failed" unless it is a ClientToolError.
|
|
122
|
+
*/
|
|
123
|
+
async serve(sessionId, handlers, options = {}) {
|
|
124
|
+
const handled = new Set();
|
|
125
|
+
const interval = Math.max(50, options.intervalMs ?? 250);
|
|
126
|
+
while (!options.signal?.aborted) {
|
|
127
|
+
for (const call of await this.pendingCalls(sessionId, Object.keys(handlers))) {
|
|
128
|
+
if (handled.has(call.id))
|
|
129
|
+
continue;
|
|
130
|
+
handled.add(call.id);
|
|
131
|
+
try {
|
|
132
|
+
await this.answer(sessionId, call.id, await handlers[call.tool](call.arguments, call));
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
await this.fail(sessionId, call.id, error instanceof ClientToolError ? error.message : 'tool failed').catch(() => undefined);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
await new Promise((resolve) => {
|
|
139
|
+
const timer = setTimeout(resolve, interval);
|
|
140
|
+
options.signal?.addEventListener('abort', () => { clearTimeout(timer); resolve(); }, { once: true });
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Sends one user message to a sealed agent's session, answers its client
|
|
146
|
+
* tool calls with `handlers` while it runs, and returns once the run has
|
|
147
|
+
* finished. This is the whole loop a gateway needs per visitor message.
|
|
148
|
+
*/
|
|
149
|
+
async converse(sessionId, message, handlers, options = {}) {
|
|
150
|
+
const sessions = new SwarmSessionsNamespace(this.transport);
|
|
151
|
+
const before = new Set(((await sessions.get(sessionId)).messages ?? []).map((m) => m.id));
|
|
152
|
+
const controller = new AbortController();
|
|
153
|
+
const serving = this.serve(sessionId, handlers, { signal: controller.signal, intervalMs: options.pollIntervalMs ?? 200 });
|
|
154
|
+
const deadline = Date.now() + (options.timeoutMs ?? 120_000);
|
|
155
|
+
try {
|
|
156
|
+
await sessions.sendMessage(sessionId, { content: message });
|
|
157
|
+
for (;;) {
|
|
158
|
+
const detail = await sessions.get(sessionId);
|
|
159
|
+
const raw = (detail.raw ?? {});
|
|
160
|
+
const added = (detail.messages ?? []).filter((m) => !before.has(m.id));
|
|
161
|
+
const pending = Array.isArray(raw.pending_permissions) ? raw.pending_permissions.length : 0;
|
|
162
|
+
const failure = added.find((m) => m.role === 'system' && m.metadata?.message_kind === 'run_failure');
|
|
163
|
+
if (!raw.active_run_intent && failure)
|
|
164
|
+
throw new Error(`The agent's run failed: ${String(failure.content ?? '').slice(0, 300)}`);
|
|
165
|
+
if (!raw.active_run_intent && pending === 0 && added.some((m) => m.role === 'assistant')) {
|
|
166
|
+
return { reply: added.filter((m) => m.role === 'assistant').map((m) => String(m.content ?? '')).join('\n\n'), messages: added };
|
|
167
|
+
}
|
|
168
|
+
if (Date.now() > deadline)
|
|
169
|
+
throw new Error('The agent did not finish in time; the run may still complete');
|
|
170
|
+
await new Promise((resolve) => setTimeout(resolve, options.pollIntervalMs ?? 400));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
finally {
|
|
174
|
+
controller.abort();
|
|
175
|
+
await serving.catch(() => undefined);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
async resolve(sessionId, callId, body) {
|
|
179
|
+
if (typeof callId !== 'string' || !callId)
|
|
180
|
+
throw new Error('callId is required');
|
|
181
|
+
await this.transport.request(`/v3/sessions/${encodeURIComponent(sessionId)}/permissions/${encodeURIComponent(callId)}/resolve`, { method: 'POST', body });
|
|
182
|
+
}
|
|
183
|
+
}
|
package/dist/auth.d.ts
CHANGED
|
@@ -22,6 +22,20 @@ export declare class SwarmAuthNamespace {
|
|
|
22
22
|
* Store the token securely—it cannot be retrieved again in plaintext!
|
|
23
23
|
*/
|
|
24
24
|
createScopedToken(params: CreateScopedTokenParams): Promise<CreateScopedTokenResult>;
|
|
25
|
+
/**
|
|
26
|
+
* Creates an AI key for Swarm Control (`/mcp` on the scoped-token gateway).
|
|
27
|
+
* `read` keys see and call only read tools; `write` keys may also start and
|
|
28
|
+
* steer sessions; neither may approve tool calls or manage anything. `full`
|
|
29
|
+
* keys are for an AI the owner trusts to run the box: they also approve
|
|
30
|
+
* tool calls and manage models, workers, limits, custom agents, client app
|
|
31
|
+
* keys and ChatGPT sign-in. Every level reaches only `/mcp`. Lifetime
|
|
32
|
+
* defaults to 30 days (at most 365). Owner only.
|
|
33
|
+
*/
|
|
34
|
+
createAIKey(params: {
|
|
35
|
+
name: string;
|
|
36
|
+
access: 'read' | 'write' | 'full';
|
|
37
|
+
expires_in_seconds?: number;
|
|
38
|
+
}): Promise<CreateScopedTokenResult>;
|
|
25
39
|
/**
|
|
26
40
|
* Lists all scoped deploy tokens for the active account.
|
|
27
41
|
* Secret hashes are masked; hints (e.g. 'swk_...abcd') are displayed.
|
package/dist/auth.js
CHANGED
|
@@ -54,6 +54,22 @@ export class SwarmAuthNamespace {
|
|
|
54
54
|
});
|
|
55
55
|
return res.data;
|
|
56
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Creates an AI key for Swarm Control (`/mcp` on the scoped-token gateway).
|
|
59
|
+
* `read` keys see and call only read tools; `write` keys may also start and
|
|
60
|
+
* steer sessions; neither may approve tool calls or manage anything. `full`
|
|
61
|
+
* keys are for an AI the owner trusts to run the box: they also approve
|
|
62
|
+
* tool calls and manage models, workers, limits, custom agents, client app
|
|
63
|
+
* keys and ChatGPT sign-in. Every level reaches only `/mcp`. Lifetime
|
|
64
|
+
* defaults to 30 days (at most 365). Owner only.
|
|
65
|
+
*/
|
|
66
|
+
async createAIKey(params) {
|
|
67
|
+
const res = await this.transport.request('/v3/auth/tokens', {
|
|
68
|
+
method: 'POST',
|
|
69
|
+
body: { name: params.name, ai_access: params.access, expires_in_seconds: params.expires_in_seconds },
|
|
70
|
+
});
|
|
71
|
+
return res.data;
|
|
72
|
+
}
|
|
57
73
|
/**
|
|
58
74
|
* Lists all scoped deploy tokens for the active account.
|
|
59
75
|
* Secret hashes are masked; hints (e.g. 'swk_...abcd') are displayed.
|