@swarm-agent/sdk 0.1.47

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 (74) hide show
  1. package/APPLICATIONS.md +133 -0
  2. package/HEADLESS_UI.md +97 -0
  3. package/LICENSE +201 -0
  4. package/README.md +473 -0
  5. package/dist/app-results.d.ts +24 -0
  6. package/dist/app-results.js +167 -0
  7. package/dist/apps.d.ts +82 -0
  8. package/dist/apps.js +89 -0
  9. package/dist/auth.d.ts +45 -0
  10. package/dist/auth.js +89 -0
  11. package/dist/automations.d.ts +74 -0
  12. package/dist/automations.js +159 -0
  13. package/dist/client.d.ts +61 -0
  14. package/dist/client.js +103 -0
  15. package/dist/deliverables.d.ts +42 -0
  16. package/dist/deliverables.js +83 -0
  17. package/dist/deploy/dockerfile.d.ts +14 -0
  18. package/dist/deploy/dockerfile.js +14 -0
  19. package/dist/deploy/index.d.ts +67 -0
  20. package/dist/deploy/index.js +226 -0
  21. package/dist/deploy/manifest.d.ts +17 -0
  22. package/dist/deploy/manifest.js +220 -0
  23. package/dist/deploy/providers/gcp-cloud-run.d.ts +9 -0
  24. package/dist/deploy/providers/gcp-cloud-run.js +12 -0
  25. package/dist/deploy/providers/gcp-compute.d.ts +9 -0
  26. package/dist/deploy/providers/gcp-compute.js +12 -0
  27. package/dist/deploy/types.d.ts +211 -0
  28. package/dist/deploy/types.js +4 -0
  29. package/dist/deploy/worker-cloud-deployer.d.ts +33 -0
  30. package/dist/deploy/worker-cloud-deployer.js +14 -0
  31. package/dist/errors.d.ts +36 -0
  32. package/dist/errors.js +68 -0
  33. package/dist/index.d.ts +23 -0
  34. package/dist/index.js +20 -0
  35. package/dist/notifications.d.ts +28 -0
  36. package/dist/notifications.js +65 -0
  37. package/dist/projects.d.ts +147 -0
  38. package/dist/projects.js +513 -0
  39. package/dist/provider-auth.d.ts +160 -0
  40. package/dist/provider-auth.js +54 -0
  41. package/dist/realtime.d.ts +240 -0
  42. package/dist/realtime.js +312 -0
  43. package/dist/sessions.d.ts +60 -0
  44. package/dist/sessions.js +194 -0
  45. package/dist/settings.d.ts +85 -0
  46. package/dist/settings.js +26 -0
  47. package/dist/storage/adapters/memory.d.ts +13 -0
  48. package/dist/storage/adapters/memory.js +35 -0
  49. package/dist/storage/adapters/s3.d.ts +31 -0
  50. package/dist/storage/adapters/s3.js +171 -0
  51. package/dist/storage/client.d.ts +174 -0
  52. package/dist/storage/client.js +121 -0
  53. package/dist/storage/hub.d.ts +30 -0
  54. package/dist/storage/hub.js +372 -0
  55. package/dist/storage/index.d.ts +6 -0
  56. package/dist/storage/index.js +6 -0
  57. package/dist/storage/runner.d.ts +48 -0
  58. package/dist/storage/runner.js +233 -0
  59. package/dist/storage/types.d.ts +198 -0
  60. package/dist/storage/types.js +1 -0
  61. package/dist/system.d.ts +14 -0
  62. package/dist/system.js +52 -0
  63. package/dist/transport.d.ts +17 -0
  64. package/dist/transport.js +243 -0
  65. package/dist/types.d.ts +1137 -0
  66. package/dist/types.js +1 -0
  67. package/dist/usage.d.ts +106 -0
  68. package/dist/usage.js +82 -0
  69. package/dist/workers.d.ts +146 -0
  70. package/dist/workers.js +822 -0
  71. package/dist/workspaces.d.ts +81 -0
  72. package/dist/workspaces.js +58 -0
  73. package/examples/headless-session.ts +85 -0
  74. package/package.json +56 -0
package/README.md ADDED
@@ -0,0 +1,473 @@
1
+ # @swarm-agent/sdk
2
+
3
+ Official TypeScript SDK for [Swarm](https://github.com/swarm-agent/swarm), the local-first AI coding workspace.
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.
6
+
7
+ ## Features
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.
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
+ - **Worker / Automation V2 Triggers**: Trigger workers on-demand with dynamic caller context (e.g. GitHub Webhooks, CI/CD events, alert payloads).
13
+ - **Session & Workspace Lifecycle**: Create durable V3 sessions, manage runs, send messages, poll for completion, and inspect workspaces.
14
+ - **Typed Error Hierarchy**: `SwarmAuthError` (401), `SwarmForbiddenError` (403), `SwarmNotFoundError` (404), `SwarmConflictError` (409), `SwarmTimeoutError`.
15
+
16
+ ## Unpublished npm candidate
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):
21
+
22
+ <copy>
23
+ npm install "$SDK_TARBALL"
24
+ node --input-type=module -e "import { SwarmClient } from '@swarm-agent/sdk'; console.log(typeof SwarmClient)"
25
+ </copy>
26
+
27
+ For maintainers, the SDK builds independently with its own locked dev dependencies:
28
+
29
+ <copy>
30
+ cd packages/sdk
31
+ npm ci --ignore-scripts
32
+ npm run build
33
+ npm pack --pack-destination "$PACKAGE_OUTPUT_DIR"
34
+ </copy>
35
+
36
+ The pack contains compiled ESM, declarations, license, README and the headless
37
+ example, not source tests or node_modules. `prepack` builds locally; there are no
38
+ install/prepare hooks. TypeScript/tsx are build/test tools, not runtime dependencies.
39
+ CommonJS `require` and non-Node runtimes are not validated by this packaging step.
40
+
41
+ For the headless container, use the matching `@swarm-agent/cli` candidate and explicitly
42
+ export a scoped token through `swarm-headless setup -- sdk-token`. Connect to
43
+ `http://127.0.0.1:7783`; **do not** use Desktop bootstrap or a privileged socket.
44
+ The headless listener allows session operations only, not the complete API below.
45
+ The packaged `examples/headless-session.ts` imports `@swarm-agent/sdk` and can be copied
46
+ into your consumer and run with an explicitly installed `tsx` runner. Protect its
47
+ `SWARM_SDK_TOKEN_FILE` (0600); the example asks before each tool approval and uses
48
+ explicit refresh, not timer polling. Live agent verification is a separate step.
49
+
50
+ ## Quick Start
51
+
52
+ ### 1. Initialize Client
53
+
54
+ ```typescript
55
+ import { createSwarmClient } from '@swarm-agent/sdk';
56
+
57
+ // Connect to local Swarm daemon (defaults to http://127.0.0.1:18080 or SWARM_API_URL)
58
+ const client = createSwarmClient({
59
+ baseUrl: process.env.SWARM_API_URL || 'http://127.0.0.1:18080',
60
+ token: process.env.SWARM_DEPLOY_TOKEN, // Optional: scoped deploy token
61
+ });
62
+ ```
63
+
64
+ ### 2. Local Desktop Bootstrap & Scoped Tokens
65
+
66
+ ```typescript
67
+ // Bootstrap local session credentials from Desktop
68
+ const session = await client.auth.bootstrapDesktopSession();
69
+ console.log(`Authenticated as user ${session.user_id}`);
70
+
71
+ // Create a scoped deploy token for CI/CD or webhooks
72
+ const { token, record } = await client.auth.createScopedToken({
73
+ name: 'GitHub Actions Trigger',
74
+ scopes: ['automations:trigger'],
75
+ expires_in_seconds: 86400, // 24 hours
76
+ });
77
+ console.log(`Created deploy token: ${token} (hint: ${record.token_hint})`);
78
+
79
+ // List active tokens
80
+ const tokens = await client.auth.listScopedTokens();
81
+ ```
82
+
83
+ ### 3. Trigger Workers On-Demand with Runtime Context
84
+
85
+ ```typescript
86
+ // Trigger an automation with webhook/CI context
87
+ const result = await client.automations.trigger({
88
+ worker_id: 'ci_test_analyzer',
89
+ context: {
90
+ event: 'push',
91
+ commit_sha: '44a93badc',
92
+ branch: 'dev',
93
+ failed_tests: ['TestWorkerRetry'],
94
+ },
95
+ });
96
+
97
+ console.log(`Trigger admitted! Occurrence ID: ${result.occurrence.id}`);
98
+ console.log(`Worker execution session: ${result.occurrence.session_id}`);
99
+ ```
100
+
101
+ ### 4. Create and Manage Sessions
102
+
103
+ ```typescript
104
+ // Create a new autonomous coding session
105
+ const newSession = await client.sessions.create({
106
+ title: 'Investigate Test Flakes',
107
+ workspace_path: '/path/to/project',
108
+ agent: 'coder',
109
+ });
110
+
111
+ // Send a message
112
+ await client.sessions.sendMessage(newSession.id, {
113
+ content: 'Please analyze why TestWorkerRetry is timing out.',
114
+ });
115
+
116
+ // Poll until the agent run finishes
117
+ const finishedSession = await client.sessions.waitForRun(newSession.id, {
118
+ timeoutMs: 120_000,
119
+ });
120
+ ```
121
+
122
+ ### 5. Workspaces and Health
123
+
124
+ ```typescript
125
+ // Check daemon health
126
+ const health = await client.system.health();
127
+ console.log('Daemon healthy:', health.ok);
128
+
129
+ // List registered workspaces
130
+ const workspaces = await client.workspaces.list();
131
+ for (const ws of workspaces) {
132
+ console.log(`- ${ws.name} (${ws.path})`);
133
+ }
134
+ ```
135
+
136
+ ### 6. Projects, Autonomous Tasks, and Orchestrate Primitives
137
+
138
+ Manage multi-workspace projects, autonomous tasks, route previews, plan review lifecycles, and opaque event sync streams:
139
+
140
+ ```typescript
141
+ // Create a new project aggregating workspaces
142
+ const project = await client.projects.create({
143
+ name: 'Mobile Core Redesign',
144
+ description: 'Refactor mobile navigation and auth screens',
145
+ workspaces: [{ path: '/workspaces/mobile-app', role: 'primary_code' }],
146
+ });
147
+
148
+ // Create an autonomous task with agent routing
149
+ const { task, model_preview } = await client.projects.createTask(project.id, {
150
+ title: 'Implement OAuth refresh token rotation',
151
+ agent: 'coder',
152
+ feature_size: 'small',
153
+ prompt: 'Add proactive token refresh before expiration with retry backoff',
154
+ });
155
+
156
+ // Preview how Orchestrate routes and models a proposed task before creation
157
+ const preview = await client.projects.previewTask(project.id, {
158
+ title: 'Audit API authorization checks',
159
+ agent: 'finder',
160
+ });
161
+
162
+ // Guarded plan acceptance: enforce exact session, plan, and definition revision
163
+ const approved = await client.projects.approveTask(project.id, task.id, {
164
+ session_id: task.session_id,
165
+ plan_id: task.plan_binding?.plan_id,
166
+ definition_revision: task.plan_binding?.definition_revision,
167
+ });
168
+
169
+ // Reopen a task with focused human feedback directives
170
+ await client.projects.reopenTask(project.id, task.id, {
171
+ feedback: 'Unit test flake on Node 20; please address.',
172
+ });
173
+
174
+ // Refine a plan based on feedback or error recovery
175
+ await client.projects.refineTask(project.id, task.id, {
176
+ feedback: 'Split implementation into schema migration then service logic',
177
+ agent: 'coder',
178
+ });
179
+
180
+ // Stream or replay outbox events starting after an opaque cursor
181
+ const streamResult = await client.projects.replayEvents('opaque_cursor_token');
182
+ ```
183
+
184
+ ### 7. Programmable Push Alert Webhooks
185
+
186
+ Register push alert webhook destinations to receive HMAC-signed real-time notifications on worker lifecycle events (`started`, `succeeded`, `failed`, `retry_exhausted`):
187
+
188
+ ```typescript
189
+ // Register a signed webhook destination
190
+ const webhook = await client.automations.createWebhook({
191
+ url: 'https://api.mycompany.com/webhooks/swarm',
192
+ secret: 'whk_hmac_secret_key',
193
+ format: 'generic', // 'generic' | 'slack' | 'discord' | 'telegram'
194
+ events: ['failed', 'retry_exhausted'], // or ['*'] for all events
195
+ });
196
+
197
+ // Test the webhook with an immediate ping
198
+ const testResult = await client.automations.testWebhook({
199
+ id: webhook.id,
200
+ worker_title: 'Database Backup',
201
+ });
202
+ console.log(`Ping delivered: HTTP ${testResult.result?.status_code} in ${testResult.result?.duration_ms}ms`);
203
+
204
+ // List configured webhooks
205
+ const webhooks = await client.automations.listWebhooks();
206
+
207
+ // Delete a webhook
208
+ await client.automations.deleteWebhook(webhook.id);
209
+ ```
210
+
211
+ ### 8. Deliverables Hub & Agent Mailbox
212
+
213
+ Submit completed work (social media drafts, reports, test alerts) from background workers to the local Swarm Agent Mailbox for human review, editing, and one-click publication actions:
214
+
215
+ ```typescript
216
+ // Submit finished worker output to the Mailbox
217
+ const deliverable = await client.deliverables.submit({
218
+ title: 'Weekly Community Launch Thread',
219
+ kind: 'social_post',
220
+ worker_id: 'social_media_bot',
221
+ payload: {
222
+ posts: [
223
+ { text: '1/3 Announcing our new distributed agent architecture...' },
224
+ { text: '2/3 Run workers in the cloud, review in your local mailbox.' },
225
+ { text: '3/3 Try it today with @swarm-agent/sdk.' }
226
+ ]
227
+ },
228
+ action_contract: {
229
+ action: 'publish_x_post',
230
+ target_secret_ref: 'gcp:x-api-key'
231
+ }
232
+ });
233
+
234
+ // List unreviewed deliverables in the inbox
235
+ const pending = await client.deliverables.list({ status: 'pending_review' });
236
+ console.log(`Pending inbox items: ${pending.length}`);
237
+
238
+ // Approve deliverable and trigger publication action
239
+ const result = await client.deliverables.approve(deliverable.id);
240
+ console.log('Published:', result.action_result);
241
+ ```
242
+
243
+ ### 9. Withdrawn cloud recipes — migration required
244
+
245
+ **The historical recipes below are unsupported and all built-in generation/execution calls now reject them.** They are retained here only as migration context, not runnable instructions or verified measurements. The old latency, capacity, cost and image-size claims were not qualified for the canonical daemon and must not be relied upon.
246
+
247
+ Use `generateOptimizedDockerfile({ runtimeImage })` with the exact
248
+ `ghcr.io/swarm-agent/swarm-headless@sha256:<digest>` from the qualified release manifest.
249
+ No digest is invented or selected automatically. See
250
+ [the supported application recipe](../../containers/headless/app/README.md).
251
+ Cloud Run, Cloud Run worker actors, and the old Compute Engine source installer
252
+ are withdrawn: they do not preserve canonical runtime, private listener, and
253
+ single-writer durable state contracts. Legacy manifest parsing remains available
254
+ for migration, not as a supported deployment preset.
255
+
256
+ <details>
257
+ <summary>Historical unsupported API examples (do not execute)</summary>
258
+
259
+ Deploy Swarm as a self-contained, serverless or dedicated cloud service on Google Cloud Platform with sub-1.5s cold starts and true scale-to-zero ($0 idle cost).
260
+
261
+ The deployment module supports both programmatic definitions and file-driven workflows (`swarm.deploy.json`), producing optimized multi-stage distroless containers and execution pipelines directly from the canonical repository ([https://github.com/swarm-agent/swarm](https://github.com/swarm-agent/swarm)).
262
+
263
+ #### The Easy 1-2-3 Deployment Flow for AI Agents & Developers
264
+
265
+ ##### Step 1: Create or Load a Deployment Manifest
266
+
267
+ Use pre-tuned presets (`latency-optimized`, `zero-idle-cost`, `free-tier-vm`, `always-on`) or define a custom `swarm.deploy.json`:
268
+
269
+ ```typescript
270
+ import { createSwarmClient } from '@swarm-agent/sdk';
271
+
272
+ const client = createSwarmClient();
273
+
274
+ // Programmatic creation with latency-optimized preset
275
+ const manifest = client.deploy.createManifest({
276
+ preset: 'latency-optimized',
277
+ name: 'swarm-cloud-agent',
278
+ gcpCloudRun: {
279
+ serviceName: 'swarm-agent-service',
280
+ region: 'us-central1',
281
+ memory: '512Mi',
282
+ cpu: '1',
283
+ minInstances: 0, // True scale-to-zero ($0 idle spend)
284
+ cpuBoost: true, // Startup CPU Boost for <1.5s cold boot
285
+ },
286
+ env: {
287
+ SWARM_ENV: 'production',
288
+ },
289
+ });
290
+ ```
291
+
292
+ Or load from a `swarm.deploy.json` file in your workspace:
293
+
294
+ ```json
295
+ {
296
+ "version": "1.0",
297
+ "name": "swarm-gcp-service",
298
+ "target": "gcp-cloud-run",
299
+ "source": {
300
+ "type": "github",
301
+ "repository": "https://github.com/swarm-agent/swarm",
302
+ "branch": "main",
303
+ "buildStrategy": "distroless"
304
+ },
305
+ "gcpCloudRun": {
306
+ "serviceName": "swarmd",
307
+ "region": "us-central1",
308
+ "cpu": "1",
309
+ "memory": "512Mi",
310
+ "minInstances": 0,
311
+ "maxInstances": 10,
312
+ "concurrency": 80,
313
+ "port": 18080,
314
+ "cpuBoost": true,
315
+ "executionEnvironment": "gen2"
316
+ }
317
+ }
318
+ ```
319
+
320
+ ##### Step 2: Generate Deployment Plan & Artifacts
321
+
322
+ Generate the complete deployment plan, including standalone multi-stage Dockerfiles, Cloud Build configurations, and deployment scripts:
323
+
324
+ ```typescript
325
+ // Generate the full structured deployment plan
326
+ const plan = client.deploy.plan(manifest);
327
+ console.log('Target Architecture:', plan.target);
328
+ console.log('Cost Summary:', plan.summary);
329
+ console.log('Recommendations:', plan.recommendations);
330
+
331
+ // Or generate files ready to be written to disk
332
+ const files = client.deploy.generateArtifacts(manifest);
333
+ // files['Dockerfile'] -> Stripped multi-stage distroless container (<35MB)
334
+ // files['.dockerignore'] -> Excludes git, node_modules, logs, and secrets
335
+ // files['cloudbuild.yaml'] -> Google Cloud Build pipeline
336
+ // files['deploy.sh'] -> Executable bash deployment script
337
+ ```
338
+
339
+ ##### Step 3: Execute Deployment (Programmatic or Command-Driven)
340
+
341
+ You can either execute the deployment pipeline programmatically in a single SDK call, write artifacts to disk, or inspect the shell commands:
342
+
343
+ ```typescript
344
+ // Option A: Programmatic Execution (0-1 in 1 line of code)
345
+ const result = await client.deploy.execute(manifest, {
346
+ // dryRun: true, // Optional: simulate pipeline without mutating cloud resources
347
+ onLog: (line, phase) => console.log(`[${phase}] ${line}`),
348
+ });
349
+
350
+ console.log('Deploy Status:', result.success ? 'SUCCESS' : 'FAILED');
351
+ console.log('Elapsed Time:', `${result.durationMs}ms`);
352
+ console.log('Service URL:', result.serviceUrl || result.externalIp);
353
+
354
+ // Option B: Write artifacts to directory
355
+ await client.deploy.writeArtifacts('./deploy-output', manifest);
356
+
357
+ // Option C: Inspect sequential shell commands
358
+ const commands = client.deploy.generateCommands(manifest);
359
+ for (const step of commands) {
360
+ console.log(`[${step.phase.toUpperCase()}] ${step.title}`);
361
+ console.log(`Command: ${step.command}`);
362
+ console.log(`Why: ${step.explanation}\n`);
363
+ }
364
+ ```
365
+
366
+ The generated deployment commands execute:
367
+ 1. **Cloud Build**: Compiles a static Go binary with stripped symbol tables (`-ldflags="-s -w"`) inside a distroless runtime (`gcr.io/distroless/static-debian12:nonroot`).
368
+ 2. **Cloud Run Deploy**: Deploys the service with `--cpu-boost` (Startup CPU Boost) and `--execution-environment gen2`.
369
+ 3. **Health Verification**: Queries the service URL and verifies `/v1/system/health`.
370
+
371
+ The former benchmark table has been withdrawn: there is no revision-bound live
372
+ qualification evidence for those image-size, latency, capacity or cost claims.
373
+
374
+ #### Free Tier Compute Engine (GCE) & Dynamic Context Management
375
+
376
+ For persistent background daemons on Google Cloud's Free Tier ($0/month) with dynamic context management:
377
+
378
+ ```typescript
379
+ const gceManifest = client.deploy.createManifest({
380
+ preset: 'free-tier-vm',
381
+ name: 'swarm-free-tier',
382
+ context: {
383
+ instructions: 'You are an autonomous background support agent.',
384
+ prompt: 'Monitor incoming notifications and update status.',
385
+ model: 'gemini-3.8-flash',
386
+ thinking: 'low',
387
+ workspaceUrl: 'https://github.com/my-org/my-project.git',
388
+ },
389
+ env: {
390
+ APP_ENV: 'production',
391
+ },
392
+ gcpCompute: {
393
+ instanceName: 'swarm-free-vm',
394
+ machineType: 'e2-micro', // 2 vCPU, 1GB RAM (Free Tier eligible)
395
+ diskSizeGb: 30, // 30GB Standard persistent disk (Free Tier eligible)
396
+ },
397
+ });
398
+
399
+ const gcePlan = client.deploy.plan(gceManifest);
400
+ // Generates:
401
+ // 1. startup-script.sh with automated swapfile creation, systemd unit, and /usr/local/bin/swarm-sync-context.
402
+ // 2. deploy-vm.sh provisioning the GCE VM unattended.
403
+ // 3. AGENTS.md & PROMPT.txt context files.
404
+
405
+ // Execute headless deployment
406
+ const gceResult = await client.deploy.execute(gcePlan);
407
+ ```
408
+
409
+ ##### Managing Context Dynamically via Instance Metadata (No Rebuilds)
410
+ Once the VM is running, users and orchestrators can dynamically update the agent's instructions, prompts, and environment without recreating the VM:
411
+ ```bash
412
+ # Update instructions or prompt on a live VM
413
+ gcloud compute instances add-metadata swarm-free-vm \
414
+ --metadata swarm-prompt="Audit and fix outstanding PR comments" \
415
+ --zone us-central1-a
416
+
417
+ # The VM's /usr/local/bin/swarm-sync-context automatically applies the new context and reloads swarmd!
418
+ ```
419
+
420
+ #### Multi-Cloud Extensibility
421
+
422
+ The `@swarm-agent/sdk` deployment architecture is designed for multi-cloud extensibility. Custom providers (for AWS ECS, Azure Container Apps, Fly.io, or Kubernetes) can be registered at runtime:
423
+
424
+ ```typescript
425
+ import type { CloudDeployProvider } from '@swarm-agent/sdk';
426
+
427
+ class CustomCloudProvider implements CloudDeployProvider {
428
+ readonly target = 'my-cloud';
429
+ validate(manifest) { return { valid: true, errors: [], warnings: [] }; }
430
+ plan(manifest) { /* ... */ }
431
+ generateArtifacts(manifest) { return { 'deploy.yaml': '...' }; }
432
+ generateCommands(manifest) { return [/* ... */]; }
433
+ }
434
+
435
+ client.deploy.registerProvider(new CustomCloudProvider());
436
+ ```
437
+
438
+ </details>
439
+
440
+ ## Error Handling
441
+
442
+ All failed API responses throw typed `SwarmApiError` instances:
443
+
444
+ ```typescript
445
+ import { SwarmForbiddenError, SwarmNotFoundError, SwarmTimeoutError } from '@swarm-agent/sdk';
446
+
447
+ try {
448
+ await client.sessions.get('non_existent_session');
449
+ } catch (err) {
450
+ if (err instanceof SwarmNotFoundError) {
451
+ console.error('Session not found (404)');
452
+ } else if (err instanceof SwarmForbiddenError) {
453
+ console.error('Missing required scope (403):', err.message);
454
+ } else if (err instanceof SwarmTimeoutError) {
455
+ console.error('Request timed out after', err.timeoutMs, 'ms');
456
+ }
457
+ }
458
+ ```
459
+
460
+ ## License
461
+
462
+ Apache-2.0
463
+
464
+ ### Application-owned agents and conversations
465
+
466
+ `client.apps` provides durable named instructions/context, revision-pinned V3 conversations,
467
+ owned reopen/result reads and idempotent event messages. See [Application agents](APPLICATIONS.md)
468
+ for examples, revision semantics and the private local-API transport requirement. Existing
469
+ `projects` and `workers` remain the task and background-execution authorities.
470
+
471
+ ## Custom headless UI
472
+
473
+ See [HEADLESS_UI.md](./HEADLESS_UI.md) for private in-container administrative setup, typed provider/settings discovery, API keys and Codex device/manual/browser sign-in, workspace creation, and `client.realtime.watchSession()` with canonical V3 replay and cancellation. The container's session-only listener does not expose administrative APIs.
@@ -0,0 +1,24 @@
1
+ import type { SwarmTransport } from './transport.js';
2
+ import type { RealtimeSocketFactory, SessionWatch } from './realtime.js';
3
+ import type { SwarmAppsNamespace, ApplicationAgent } from './apps.js';
4
+ import type { ProjectTaskRecord, WorkerRecord, WorkerRunRecord } from './types.js';
5
+ export interface ApplicationResults {
6
+ agent: ApplicationAgent;
7
+ tasks: ProjectTaskRecord[];
8
+ workers: Array<{
9
+ worker: WorkerRecord;
10
+ runs: WorkerRunRecord[];
11
+ }>;
12
+ }
13
+ export interface ApplicationResultsOptions {
14
+ signal?: AbortSignal;
15
+ socketFactory?: RealtimeSocketFactory;
16
+ onChange(results: ApplicationResults): void;
17
+ onError?(error: Error): void;
18
+ maxReconnects?: number;
19
+ }
20
+ /** Resource notifications invalidate authorized snapshots, never supply app data.
21
+ * Each connection starts with a durable bootstrap cursor BEFORE reading resources.
22
+ * Reconnect rehydrates everything; cursors are never parsed or shared across scopes.
23
+ */
24
+ export declare function watchApplicationResults(apps: SwarmAppsNamespace, transport: SwarmTransport, id: string, options: ApplicationResultsOptions): SessionWatch;
@@ -0,0 +1,167 @@
1
+ import { SwarmRealtimeNamespace } from './realtime.js';
2
+ /** Resource notifications invalidate authorized snapshots, never supply app data.
3
+ * Each connection starts with a durable bootstrap cursor BEFORE reading resources.
4
+ * Reconnect rehydrates everything; cursors are never parsed or shared across scopes.
5
+ */
6
+ export function watchApplicationResults(apps, transport, id, options) {
7
+ const controller = new AbortController(), signal = controller.signal;
8
+ const abort = () => controller.abort();
9
+ options.signal?.addEventListener('abort', abort, { once: true });
10
+ if (options.signal?.aborted)
11
+ abort();
12
+ let resolveReady, rejectReady;
13
+ const ready = new Promise((resolve, reject) => { resolveReady = resolve; rejectReady = reject; });
14
+ void ready.catch(() => { });
15
+ const realtime = new SwarmRealtimeNamespace(transport);
16
+ const snapshot = async (readSignal = signal) => {
17
+ const agent = await apps.get(id, readSignal);
18
+ const tasks = agent.project_id ? (await apps.tasks(id, readSignal)).tasks : [];
19
+ const workers = [];
20
+ // Sequential resource reads bound concurrency even with 100 linked workers.
21
+ for (const workerId of agent.worker_ids ?? []) {
22
+ if (signal.aborted)
23
+ throw new Error('Watch disposed');
24
+ const { worker } = await apps.worker(id, workerId, readSignal);
25
+ const { runs } = await apps.runs(id, workerId, readSignal);
26
+ workers.push({ worker, runs });
27
+ }
28
+ return { agent, tasks, workers };
29
+ };
30
+ const connect = async () => {
31
+ await apps.get(id, signal); // Reject foreign app ownership before opening any event scope.
32
+ // Principal-scoped global workset is the existing resource event contract.
33
+ // A bounded recent membership is requested; unrelated session payloads are ignored.
34
+ const initial = (await transport.request('/v3/sync/bootstrap', {
35
+ method: 'POST', signal, body: { surface: 'desktop', selector: { kind: 'global', global: true, recent: { limit: 1 } }, history: { mode: 'none' } },
36
+ })).data;
37
+ if (signal.aborted)
38
+ return;
39
+ if (!initial.realtime?.resume.endpoint_cursor)
40
+ throw new Error('Resource realtime unavailable');
41
+ let state = await snapshot();
42
+ if (signal.aborted)
43
+ return;
44
+ const socket = await (options.socketFactory ?? (path => realtime.socket(path)))('/v3/realtime/stream?surface=desktop');
45
+ if (signal.aborted) {
46
+ socket.close();
47
+ return;
48
+ }
49
+ await new Promise((resolve, reject) => {
50
+ const requests = new AbortController();
51
+ let ended = false, hello = false, confirmed = false, refreshing = false, dirty = false;
52
+ const finish = (error) => {
53
+ if (ended)
54
+ return;
55
+ ended = true;
56
+ requests.abort();
57
+ clearTimeout(timer);
58
+ signal.removeEventListener('abort', cancel);
59
+ socket.close();
60
+ error ? reject(error) : resolve();
61
+ };
62
+ const cancel = () => finish();
63
+ const timer = setTimeout(() => finish(new Error('Resource handshake timed out')), 30_000);
64
+ const refresh = async () => {
65
+ dirty = true;
66
+ if (refreshing)
67
+ return;
68
+ refreshing = true;
69
+ try {
70
+ while (dirty && !ended) {
71
+ dirty = false;
72
+ const next = await snapshot(requests.signal);
73
+ if (ended)
74
+ return;
75
+ state = next;
76
+ if (confirmed) {
77
+ options.onChange(state);
78
+ resolveReady();
79
+ }
80
+ }
81
+ }
82
+ catch (error) {
83
+ finish(error instanceof Error ? error : new Error('Resource refresh failed'));
84
+ }
85
+ finally {
86
+ refreshing = false;
87
+ }
88
+ };
89
+ signal.addEventListener('abort', cancel, { once: true });
90
+ socket.addEventListener('close', () => finish(new Error('Resource stream disconnected')));
91
+ socket.addEventListener('error', () => finish(new Error('Resource stream failed')));
92
+ socket.addEventListener('message', ({ data }) => {
93
+ if (ended)
94
+ return;
95
+ try {
96
+ const text = String(data);
97
+ if (text.length > 8 * 1024 * 1024)
98
+ throw new Error('Resource frame too large');
99
+ const frame = JSON.parse(text);
100
+ if (frame.protocol !== 'v3.realtime' || frame.protocol_version !== 1)
101
+ throw new Error('Unsupported resource protocol');
102
+ if (frame.kind === 'auth.denied')
103
+ throw Object.assign(new Error('Resource authorization denied'), { status: 403 });
104
+ if (['cursor.error', 'slow_consumer.reconnect_required'].includes(frame.kind))
105
+ throw new Error('Resource stream requires reauthorization');
106
+ if (frame.kind === 'hello') {
107
+ if (hello)
108
+ throw new Error('Duplicate hello');
109
+ hello = true;
110
+ socket.send(JSON.stringify({ ...initial.realtime.resume, subscriptions: [] }));
111
+ return;
112
+ }
113
+ if (!hello)
114
+ throw new Error('Resource frame before hello');
115
+ // Resource-only resumes have no replay.done acknowledgment. The first
116
+ // resource/watermark/keepalive is a liveness fence, not an atomic snapshot.
117
+ // Re-read after it while coalescing replay invalidations; hello alone is
118
+ // not readiness and an immediate resume denial must never emit data.
119
+ if (!confirmed && ['keepalive', 'endpoint.watermark', 'project.updated', 'worker.updated', 'automation.updated'].includes(frame.kind)) {
120
+ confirmed = true;
121
+ clearTimeout(timer);
122
+ void refresh();
123
+ return;
124
+ }
125
+ if ((frame.kind === 'project.updated' && frame.project_id === state.agent.project_id) ||
126
+ (['worker.updated', 'automation.updated'].includes(frame.kind) && state.workers.length > 0))
127
+ void refresh();
128
+ }
129
+ catch (error) {
130
+ finish(error instanceof Error ? error : new Error('Invalid resource frame'));
131
+ }
132
+ });
133
+ });
134
+ };
135
+ const done = (async () => {
136
+ for (let attempt = 0; !signal.aborted; attempt++) {
137
+ try {
138
+ await connect();
139
+ return;
140
+ }
141
+ catch (error) {
142
+ if (signal.aborted)
143
+ return;
144
+ // Authorization errors are terminal, never retried against a different route.
145
+ const status = error.status;
146
+ if ([401, 403, 404].includes(status ?? 0) || attempt >= (options.maxReconnects ?? 3))
147
+ throw error;
148
+ await new Promise(resolve => {
149
+ const finish = () => { clearTimeout(timer); signal.removeEventListener('abort', finish); resolve(); };
150
+ const timer = setTimeout(finish, Math.min(250 * 2 ** attempt, 4000));
151
+ signal.addEventListener('abort', finish, { once: true });
152
+ if (signal.aborted)
153
+ finish();
154
+ });
155
+ }
156
+ }
157
+ })().catch((error) => {
158
+ const err = error instanceof Error ? error : new Error('Result watch failed');
159
+ rejectReady(err);
160
+ if (!signal.aborted) {
161
+ options.onError?.(err);
162
+ throw err;
163
+ }
164
+ }).finally(() => { rejectReady(new Error('Watch disposed')); options.signal?.removeEventListener('abort', abort); });
165
+ void done.catch(() => { });
166
+ return { ready, done, dispose: abort };
167
+ }