@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.
- package/APPLICATIONS.md +133 -0
- package/HEADLESS_UI.md +97 -0
- package/LICENSE +201 -0
- package/README.md +473 -0
- package/dist/app-results.d.ts +24 -0
- package/dist/app-results.js +167 -0
- package/dist/apps.d.ts +82 -0
- package/dist/apps.js +89 -0
- package/dist/auth.d.ts +45 -0
- package/dist/auth.js +89 -0
- package/dist/automations.d.ts +74 -0
- package/dist/automations.js +159 -0
- package/dist/client.d.ts +61 -0
- package/dist/client.js +103 -0
- package/dist/deliverables.d.ts +42 -0
- package/dist/deliverables.js +83 -0
- package/dist/deploy/dockerfile.d.ts +14 -0
- package/dist/deploy/dockerfile.js +14 -0
- package/dist/deploy/index.d.ts +67 -0
- package/dist/deploy/index.js +226 -0
- package/dist/deploy/manifest.d.ts +17 -0
- package/dist/deploy/manifest.js +220 -0
- package/dist/deploy/providers/gcp-cloud-run.d.ts +9 -0
- package/dist/deploy/providers/gcp-cloud-run.js +12 -0
- package/dist/deploy/providers/gcp-compute.d.ts +9 -0
- package/dist/deploy/providers/gcp-compute.js +12 -0
- package/dist/deploy/types.d.ts +211 -0
- package/dist/deploy/types.js +4 -0
- package/dist/deploy/worker-cloud-deployer.d.ts +33 -0
- package/dist/deploy/worker-cloud-deployer.js +14 -0
- package/dist/errors.d.ts +36 -0
- package/dist/errors.js +68 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +20 -0
- package/dist/notifications.d.ts +28 -0
- package/dist/notifications.js +65 -0
- package/dist/projects.d.ts +147 -0
- package/dist/projects.js +513 -0
- package/dist/provider-auth.d.ts +160 -0
- package/dist/provider-auth.js +54 -0
- package/dist/realtime.d.ts +240 -0
- package/dist/realtime.js +312 -0
- package/dist/sessions.d.ts +60 -0
- package/dist/sessions.js +194 -0
- package/dist/settings.d.ts +85 -0
- package/dist/settings.js +26 -0
- package/dist/storage/adapters/memory.d.ts +13 -0
- package/dist/storage/adapters/memory.js +35 -0
- package/dist/storage/adapters/s3.d.ts +31 -0
- package/dist/storage/adapters/s3.js +171 -0
- package/dist/storage/client.d.ts +174 -0
- package/dist/storage/client.js +121 -0
- package/dist/storage/hub.d.ts +30 -0
- package/dist/storage/hub.js +372 -0
- package/dist/storage/index.d.ts +6 -0
- package/dist/storage/index.js +6 -0
- package/dist/storage/runner.d.ts +48 -0
- package/dist/storage/runner.js +233 -0
- package/dist/storage/types.d.ts +198 -0
- package/dist/storage/types.js +1 -0
- package/dist/system.d.ts +14 -0
- package/dist/system.js +52 -0
- package/dist/transport.d.ts +17 -0
- package/dist/transport.js +243 -0
- package/dist/types.d.ts +1137 -0
- package/dist/types.js +1 -0
- package/dist/usage.d.ts +106 -0
- package/dist/usage.js +82 -0
- package/dist/workers.d.ts +146 -0
- package/dist/workers.js +822 -0
- package/dist/workspaces.d.ts +81 -0
- package/dist/workspaces.js +58 -0
- package/examples/headless-session.ts +85 -0
- 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
|
+
}
|