@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.
Files changed (46) hide show
  1. package/HEADLESS_UI.md +4 -6
  2. package/README.md +121 -56
  3. package/dist/agents.d.ts +114 -0
  4. package/dist/agents.js +183 -0
  5. package/dist/auth.d.ts +14 -0
  6. package/dist/auth.js +16 -0
  7. package/dist/box.d.ts +68 -0
  8. package/dist/box.js +13 -0
  9. package/dist/browser.d.ts +6 -2
  10. package/dist/browser.js +11 -3
  11. package/dist/chat.d.ts +41 -7
  12. package/dist/chat.js +148 -83
  13. package/dist/client.d.ts +12 -0
  14. package/dist/client.js +19 -6
  15. package/dist/copy-tags.d.ts +26 -0
  16. package/dist/copy-tags.js +133 -0
  17. package/dist/errors.d.ts +16 -0
  18. package/dist/errors.js +21 -0
  19. package/dist/index.d.ts +6 -0
  20. package/dist/index.js +6 -0
  21. package/dist/local-connection.d.ts +39 -0
  22. package/dist/local-connection.js +56 -0
  23. package/dist/permission-ui.d.ts +1 -0
  24. package/dist/permission-ui.js +37 -0
  25. package/dist/permissions.d.ts +4 -0
  26. package/dist/permissions.js +7 -0
  27. package/dist/projects.d.ts +25 -3
  28. package/dist/projects.js +65 -6
  29. package/dist/provider-auth.d.ts +5 -3
  30. package/dist/provider-auth.js +6 -8
  31. package/dist/realtime.d.ts +40 -6
  32. package/dist/realtime.js +156 -52
  33. package/dist/remote.d.ts +59 -0
  34. package/dist/remote.js +46 -0
  35. package/dist/sessions.d.ts +31 -1
  36. package/dist/sessions.js +49 -1
  37. package/dist/settings.d.ts +3 -0
  38. package/dist/settings.js +19 -3
  39. package/dist/signals.d.ts +148 -0
  40. package/dist/signals.js +97 -0
  41. package/dist/types.d.ts +70 -2
  42. package/dist/websocket.d.ts +3 -1
  43. package/dist/websocket.js +1 -1
  44. package/dist/workspaces.d.ts +11 -0
  45. package/dist/workspaces.js +17 -0
  46. 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. Complete the daemon's identity/onboarding requirements using `onboarding.update({ username, swarm_name })` when needed.
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 sequence ranges deduplicate live text; durable checkpoints and committed messages remain authoritative. Foreign-session frames never update the view.
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. A live offset gap pauses that stream's speculative patches and relies on durable repair. 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.
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, zero-dependency client to interact programmatically with the Swarm daemon (`swarmd`) over HTTP and local Unix domain sockets.
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
- - **Zero Runtime Dependencies**: Works out-of-the-box in Node.js, Bun, Deno, and modern browser/edge environments.
10
- - **Zero-Conf Unix Sockets**: Native support for daemon Unix domain sockets (`/run/swarmd/swarmd.sock`) for automatic peer identity.
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
- ## Unpublished npm candidate
16
+ ## Install
17
17
 
18
- Node 22+ and ESM imports are the validated packaging target. The prerelease name
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 "$SDK_TARBALL"
24
- node --input-type=module -e "import { SwarmClient } from '@swarm-agent/sdk'; console.log(typeof SwarmClient)"
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` candidate and explicitly
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
- const client = new SwarmClient({
80
- baseUrl: process.env.SWARM_API_URL || 'http://127.0.0.1:5555',
81
- socketPath: process.env.SWARM_SOCKET_PATH, // e.g. /var/lib/swarmd/local-transport/api.sock
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. `parseError` leaves malformed questions deny-only. These helpers do not grant authority:
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
- Build responsive chat UIs that stream model text, reasoning, and live tool execution cards like Swarm Desktop:
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
- // 1. Create a chat session
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
- // 2. Stream live text, reasoning, and tool calls
202
+ let answer = '';
185
203
  const stream = await client.chat.stream(session.id, {
186
- onText: (delta, fullText) => process.stdout.write(delta),
187
- onReasoning: (delta) => console.log('[Thinking]', delta),
188
- onToolStarted: (tool) => console.log(`[Tool Started] ${tool.name}`),
189
- onToolDelta: (tool, delta) => console.log(`[Tool Output] ${delta}`),
190
- onToolCompleted: (tool) => console.log(`[Tool Done] ${tool.name} in ${tool.durationMs}ms`),
191
- onPermissionRequested: async (perm, resolve) => {
192
- // Your UI must wait for a real user gesture. Keep errors visible and retain the form.
193
- renderPermissionForm(describePermission(perm), async (action, userInput) => {
194
- try {
195
- const result = await resolve(action, action === 'allow_once' && describePermission(perm).kind === 'ask-user'
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
- onPermissionRemoved: id => removePermissionForm(id), // No longer pending; not necessarily approved.
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
- console.log(response.reply);
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.id, tool.name, tool.arguments));
261
- chat.on('tool_delta', (tool, delta) => appendToolOutput(tool.id, delta));
262
- chat.on('tool_done', (tool) => markToolCompleted(tool.id, tool.durationMs));
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(). Disconnected sends throw instead of silently disappearing.
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`):
@@ -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.