@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/dist/box.d.ts ADDED
@@ -0,0 +1,68 @@
1
+ import type { SwarmTransport } from './transport.js';
2
+ /** `ok`, `attention` (something waits on a person or failed today) or `degraded` (agents unconfined, or spending over the limit). */
3
+ export type BoxStatus = 'ok' | 'attention' | 'degraded';
4
+ /** One machine's state for a monitor: ids, names and counts only. */
5
+ export interface BoxSummary {
6
+ generated_at: number;
7
+ status: BoxStatus;
8
+ /** Why the status is not `ok`. */
9
+ reasons: string[];
10
+ machine: {
11
+ name?: string;
12
+ uptime_ms: number;
13
+ };
14
+ sandbox: {
15
+ mode: string;
16
+ active: boolean;
17
+ runtime?: string;
18
+ reason?: string;
19
+ };
20
+ secrets_gateway: boolean;
21
+ permissions: {
22
+ bypass: boolean;
23
+ bypass_blocked_reason?: string;
24
+ };
25
+ attention: {
26
+ blocked_sessions: number;
27
+ oldest_pending_at?: number;
28
+ sessions: {
29
+ session_id: string;
30
+ pending: number;
31
+ oldest_pending_at: number;
32
+ }[];
33
+ };
34
+ workers: {
35
+ total: number;
36
+ truncated?: boolean;
37
+ active_runs: number;
38
+ today_runs: number;
39
+ today_failed: number;
40
+ /** UTC day the counts cover, YYYY-MM-DD. */
41
+ day?: string;
42
+ failing: {
43
+ worker_id: string;
44
+ name: string;
45
+ today_runs: number;
46
+ today_failed: number;
47
+ active_runs: number;
48
+ }[];
49
+ };
50
+ usage: {
51
+ today_cost_usd?: number;
52
+ today_tokens?: number;
53
+ daily_cost_limit_usd?: number;
54
+ limit_enabled?: boolean;
55
+ limit_exceeded?: boolean;
56
+ };
57
+ signals: {
58
+ latest_seq?: number;
59
+ oldest_seq?: number;
60
+ };
61
+ }
62
+ /** Machine-level state. Needs a key with `signals:read`. */
63
+ export declare class SwarmBoxNamespace {
64
+ private readonly transport;
65
+ constructor(transport: SwarmTransport);
66
+ /** Is this machine all right, and does anything need a person? One cheap call. */
67
+ summary(): Promise<BoxSummary>;
68
+ }
package/dist/box.js ADDED
@@ -0,0 +1,13 @@
1
+ /** Machine-level state. Needs a key with `signals:read`. */
2
+ export class SwarmBoxNamespace {
3
+ transport;
4
+ constructor(transport) {
5
+ this.transport = transport;
6
+ }
7
+ /** Is this machine all right, and does anything need a person? One cheap call. */
8
+ async summary() {
9
+ const res = await this.transport.request('/v3/box/summary', { method: 'GET' });
10
+ const { ok: _ok, ...summary } = res.data;
11
+ return summary;
12
+ }
13
+ }
package/dist/browser.d.ts CHANGED
@@ -2,7 +2,10 @@ import type { ToolStreamItem } from './chat.js';
2
2
  import type { PendingPermissionRecord, ResolvePermissionResult } from './permissions.js';
3
3
  import type { PermissionAction, ResolvePermissionOptions } from './permission-ui.js';
4
4
  export * from './permission-ui.js';
5
- import type { SessionWatchState, V3Message } from './realtime.js';
5
+ export { copyTagsToMarkdown, hasCopyTags } from './copy-tags.js';
6
+ export { SwarmChatNotReadyError } from './errors.js';
7
+ export type { SwarmChatNotReadyCode } from './errors.js';
8
+ import type { SessionWatchState, V3Message, V3RunState } from './realtime.js';
6
9
  export interface SwarmBrowserChatOptions {
7
10
  /** WebSocket URL, e.g. ws://localhost:3456/ws. Defaults to ws(s)://${location.host}/ws */
8
11
  url?: string;
@@ -36,7 +39,8 @@ type EventMap = {
36
39
  permission_removed: (permissionId: string) => void;
37
40
  permission_error: (permissionId: string, error: Error) => void;
38
41
  permissions: (pending: PendingPermissionRecord[]) => void;
39
- done: (messages: V3Message[]) => void;
42
+ /** Once per finished run; `run.status` distinguishes completed from failed or cancelled. */
43
+ done: (messages: V3Message[], run?: V3RunState) => void;
40
44
  error: (error: string) => void;
41
45
  };
42
46
  /**
package/dist/browser.js CHANGED
@@ -1,5 +1,8 @@
1
1
  import { confirmPermissionResolution, permissionId as validatePermissionId, permissionResolutionBody, PermissionResolutionError } from './permission-ui.js';
2
2
  export * from './permission-ui.js';
3
+ export { copyTagsToMarkdown, hasCopyTags } from './copy-tags.js';
4
+ import { SwarmChatNotReadyError } from './errors.js';
5
+ export { SwarmChatNotReadyError } from './errors.js';
3
6
  /**
4
7
  * Universal browser-side WebSocket client for Swarm.
5
8
  * Hooks directly into the Swarm WebSocket Bridge with typed callbacks for live
@@ -241,7 +244,7 @@ export class SwarmBrowserChat {
241
244
  break;
242
245
  }
243
246
  case 'done':
244
- this.emit('done', msg.messages || []);
247
+ this.emit('done', msg.messages || [], msg.run);
245
248
  break;
246
249
  case 'error':
247
250
  this.emit('error', msg.error || 'Unknown error');
@@ -312,8 +315,13 @@ export class SwarmBrowserChat {
312
315
  if (!targetSession) {
313
316
  throw new Error('No active session. Call subscribe(sessionId) or provide sessionId');
314
317
  }
315
- if (!this.isSubscribed || targetSession !== this.currentSessionId)
316
- throw new Error('Await subscribe(sessionId) before sending');
318
+ if (targetSession !== this.currentSessionId)
319
+ throw new SwarmChatNotReadyError('chat_not_subscribed');
320
+ if (!this.isSubscribed) {
321
+ // Connected or about to reconnect: the subscription is restored automatically.
322
+ const recovering = this.isConnected || !!this.connectPromise || !!this.reconnectTimer;
323
+ throw new SwarmChatNotReadyError(recovering && !this.isDisposed ? 'chat_reconnecting' : 'chat_disconnected');
324
+ }
317
325
  const id = requestId ?? globalThis.crypto.randomUUID();
318
326
  this.send({
319
327
  type: 'chat_message',
package/dist/chat.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { PendingPermissionRecord } from './permissions.js';
2
2
  import type { PermissionResolver } from './permissions.js';
3
- import type { SessionWatch, SessionWatchState, V3Message } from './realtime.js';
3
+ import type { SessionWatch, SessionWatchState, V3Message, V3RunState } from './realtime.js';
4
4
  import type { SwarmTransport } from './transport.js';
5
5
  import type { SessionDetail, SessionRecord } from './types.js';
6
6
  import { SwarmWebSocketBridge, type SwarmWebSocketBridgeOptions } from './websocket.js';
@@ -23,7 +23,13 @@ export interface CreateChatSessionOptions {
23
23
  mode?: 'auto' | 'plan';
24
24
  }
25
25
  export interface ToolStreamItem {
26
+ /** Live stream id (kept for compatibility; may be the `step-<n>:<call_id>` instance id). */
26
27
  id: string;
28
+ /**
29
+ * Durable tool-call id: equals `payload.call_id` on the session's `session.tool.*` history
30
+ * events. Match live tool cards to history cards on this field, not on `id`.
31
+ */
32
+ callId: string;
27
33
  name: string;
28
34
  identity?: string;
29
35
  display?: string;
@@ -49,6 +55,11 @@ export interface ChatRunOptions {
49
55
  onToolEvent?: (event: ToolStreamItem) => void;
50
56
  /** Optional AbortSignal to cancel waiting */
51
57
  signal?: AbortSignal;
58
+ /**
59
+ * Keep Swarm's `<copy label="…">…</copy>` snippet tags in `reply` and assistant
60
+ * `messages`. By default they become Markdown code blocks (see copyTagsToMarkdown).
61
+ */
62
+ rawCopyTags?: boolean;
52
63
  }
53
64
  export interface ChatRunResult {
54
65
  /** The final text response from the assistant */
@@ -70,9 +81,21 @@ export interface ChatStreamOptions {
70
81
  socketFactory?: import('./realtime.js').RealtimeSocketFactory;
71
82
  /** Authoritative conversation snapshot on hydrate, live change and reconnect. */
72
83
  onState?: (state: SessionWatchState) => void;
73
- /** Invoked with incremental and cumulative assistant response text */
84
+ /**
85
+ * Incremental and cumulative assistant text per run/stream. Concatenating `delta` values
86
+ * yields exactly `fullText`, including across reconnects (never repeated, never skipped);
87
+ * a turn's final call carries any committed text beyond what streamed.
88
+ */
74
89
  onText?: (delta: string, fullText: string) => void;
75
- /** Invoked with incremental and cumulative reasoning/thinking text */
90
+ /**
91
+ * Keep Swarm's `<copy label="…">…</copy>` snippet tags in `onText` and `onComplete`.
92
+ * By default they become Markdown code blocks while streaming (a half-written tag is
93
+ * held back until it is complete). Only if a snippet's own payload contains a run of
94
+ * four or more backticks is it re-fenced; that call's `delta` is then empty and
95
+ * `fullText` stays exact.
96
+ */
97
+ rawCopyTags?: boolean;
98
+ /** Incremental and cumulative reasoning text from durable reasoning checkpoints. */
76
99
  onReasoning?: (delta: string, fullText: string) => void;
77
100
  /** Invoked when a tool call starts running */
78
101
  onToolStarted?: (tool: ToolStreamItem) => void;
@@ -86,8 +109,12 @@ export interface ChatStreamOptions {
86
109
  onPermissionRemoved?: (permissionId: string) => void;
87
110
  /** Recoverable UI/resolution failure; the watch stays open. Resolver also rejects. */
88
111
  onPermissionError?: (error: Error, permission: PendingPermissionRecord) => void;
89
- /** Invoked when the agent turn completes */
90
- onComplete?: (messages: V3Message[]) => void;
112
+ /**
113
+ * Once per finished run, including failed, cancelled and empty turns. `run.status`
114
+ * says which (`completed`, `failed`, ...); a failure's reason is in `run.blocked_reason`
115
+ * and its system message in `messages`. The watch stays open for the next turn.
116
+ */
117
+ onComplete?: (messages: V3Message[], run: V3RunState) => void;
91
118
  /** Invoked if an unrecoverable streaming error occurs */
92
119
  onError?: (err: Error) => void;
93
120
  }
@@ -116,11 +143,13 @@ export declare class SwarmChatNamespace {
116
143
  /**
117
144
  * Appends a message to a chat conversation.
118
145
  */
119
- sendMessage(sessionId: string, content: string, role?: 'user' | 'assistant' | 'system', clientRequestId?: string): Promise<any>;
146
+ sendMessage(sessionId: string, content: string, role?: 'user', clientRequestId?: string): Promise<any>;
120
147
  /**
121
148
  * Lists messages from a session conversation.
122
149
  */
123
- listMessages(sessionId: string): Promise<V3Message[]>;
150
+ listMessages(sessionId: string, options?: {
151
+ rawCopyTags?: boolean;
152
+ }): Promise<V3Message[]>;
124
153
  /**
125
154
  * High-level chat execution: sends a message, monitors execution, handles or surfaces
126
155
  * pending permissions, and returns the final assistant reply.
@@ -132,3 +161,8 @@ export declare class SwarmChatNamespace {
132
161
  */
133
162
  stream(sessionId: string, options?: ChatStreamOptions): Promise<SessionWatch>;
134
163
  }
164
+ /**
165
+ * The durable tool-call id (`payload.call_id` on `session.tool.*` events). When an event
166
+ * carries only `tool_instance_id` (`step-<n>:<call_id>`), the step prefix is removed.
167
+ */
168
+ export declare function toolCallIdFromPayload(payload: Record<string, unknown> | undefined): string;
package/dist/chat.js CHANGED
@@ -2,6 +2,7 @@ import { SwarmPermissionsNamespace, isAskUserPermission, permissionId } from './
2
2
  import { SwarmRealtimeNamespace } from './realtime.js';
3
3
  import { SwarmSessionsNamespace } from './sessions.js';
4
4
  import { SwarmTimeoutError } from './errors.js';
5
+ import { copyTagsToMarkdown } from './copy-tags.js';
5
6
  import { SwarmWebSocketBridge } from './websocket.js';
6
7
  /**
7
8
  * Dedicated high-level Chat namespace.
@@ -35,7 +36,7 @@ export class SwarmChatNamespace {
35
36
  if (options.project_id)
36
37
  throw new Error('Use projects.createConversation(projectId) for project orchestrator conversations');
37
38
  return this.sessions.create({
38
- title: options.title ?? 'Chat Session',
39
+ title: options.title,
39
40
  agent_name: options.agent_name ?? 'swarm',
40
41
  workspace_path: options.workspace_path,
41
42
  workspace_id: options.workspace_id,
@@ -52,9 +53,10 @@ export class SwarmChatNamespace {
52
53
  /**
53
54
  * Lists messages from a session conversation.
54
55
  */
55
- async listMessages(sessionId) {
56
+ async listMessages(sessionId, options = {}) {
56
57
  const detail = await this.sessions.get(sessionId);
57
- return detail.messages || [];
58
+ const messages = detail.messages || [];
59
+ return options.rawCopyTags ? messages : messages.map(cleanAssistantMessage);
58
60
  }
59
61
  /**
60
62
  * High-level chat execution: sends a message, monitors execution, handles or surfaces
@@ -99,10 +101,10 @@ export class SwarmChatNamespace {
99
101
  reply = sysMsg ? sysMsg.content : 'No response generated from model.';
100
102
  }
101
103
  return {
102
- reply,
104
+ reply: options.rawCopyTags ? reply : copyTagsToMarkdown(reply),
103
105
  sessionId: id,
104
106
  runId: activeRun?.run_id || detail.last_run_id,
105
- messages: msgs,
107
+ messages: options.rawCopyTags ? msgs : msgs.map(cleanAssistantMessage),
106
108
  pendingPermissions: pending,
107
109
  session: detail,
108
110
  };
@@ -123,7 +125,10 @@ export class SwarmChatNamespace {
123
125
  const id = permissionId(sessionId, 'sessionId');
124
126
  const activeTools = new Map();
125
127
  const seenEventIds = new Set();
126
- const previousStreams = new Map();
128
+ // Text already reported through onText per run/stream: a high-water mark, never shrunk
129
+ // by a reconnect, so deltas are neither repeated nor lost across reconnects.
130
+ const reportedText = new Map();
131
+ const reasoning = new Map();
127
132
  const completedRuns = new Set();
128
133
  const pendingSeen = new Map();
129
134
  const resolving = new Set();
@@ -131,6 +136,97 @@ export class SwarmChatNamespace {
131
136
  let permissionsLive = false;
132
137
  let initialized = false;
133
138
  let observedRun;
139
+ // Only extensions are reported: a shorter text (reconnect before durable catch-up) or a
140
+ // committed message that differs from the streamed prefix is never re-sent as a delta.
141
+ // Copy tags are converted on the cumulative text (in streaming mode, which only ever
142
+ // appends), and the delta is taken from the converted text.
143
+ const shownText = new Map();
144
+ const reportText = (key, runId, streamId, text) => {
145
+ const previous = reportedText.get(key)?.text ?? '';
146
+ if (text.length <= previous.length || !text.startsWith(previous))
147
+ return;
148
+ reportedText.set(key, { runId, streamId, text });
149
+ observedRun = runId;
150
+ if (!options.onText)
151
+ return;
152
+ if (options.rawCopyTags) {
153
+ options.onText(text.slice(previous.length), text);
154
+ return;
155
+ }
156
+ const shown = copyTagsToMarkdown(text, { streaming: true });
157
+ const before = shownText.get(key) ?? '';
158
+ if (shown === before)
159
+ return;
160
+ shownText.set(key, shown);
161
+ options.onText(shown.startsWith(before) ? shown.slice(before.length) : '', shown);
162
+ };
163
+ const forgetText = (key) => {
164
+ reportedText.delete(key);
165
+ shownText.delete(key);
166
+ };
167
+ // Applies one durable event. Hydrated history is applied silently: it is neither a new
168
+ // turn nor a new tool call, but running tools and partial reasoning must still be known.
169
+ const applyEvent = (evt, notify) => {
170
+ const payload = evt.payload || {};
171
+ const eventType = evt.event_type;
172
+ const toolId = String(payload.call_id || payload.tool_instance_id || '');
173
+ // Durable history keys tool calls by payload.call_id; tool_instance_id is `step-N:<call_id>`.
174
+ const callId = toolCallIdFromPayload(payload);
175
+ const toolKey = callId || toolId;
176
+ if (eventType === 'session.tool.started') {
177
+ const toolItem = {
178
+ id: toolId || evt.id,
179
+ callId: callId || toolId || evt.id,
180
+ name: String(payload.tool_name || 'tool'),
181
+ identity: payload.tool_identity ? String(payload.tool_identity) : undefined,
182
+ display: payload.tool_display ? String(payload.tool_display) : undefined,
183
+ arguments: payload.arguments ? String(payload.arguments) : undefined,
184
+ status: 'running',
185
+ };
186
+ activeTools.set(toolKey || toolItem.id, toolItem);
187
+ if (notify)
188
+ options.onToolStarted?.(toolItem);
189
+ }
190
+ else if (eventType === 'session.tool.delta') {
191
+ const toolItem = activeTools.get(toolKey);
192
+ const delta = String(payload.output || '');
193
+ if (toolItem && delta) {
194
+ toolItem.output = (toolItem.output || '') + delta;
195
+ if (notify)
196
+ options.onToolDelta?.(toolItem, delta);
197
+ }
198
+ }
199
+ else if (TERMINAL_TOOL_STATUS[eventType]) {
200
+ let toolItem = activeTools.get(toolKey);
201
+ if (!toolItem) {
202
+ toolItem = { id: toolId, callId: callId || toolId, name: String(payload.tool_name || 'tool'), status: TERMINAL_TOOL_STATUS[eventType] };
203
+ }
204
+ activeTools.delete(toolKey);
205
+ toolItem.status = TERMINAL_TOOL_STATUS[eventType];
206
+ if (payload.output)
207
+ toolItem.output = String(payload.output);
208
+ if (toolItem.status === 'failed')
209
+ toolItem.error = String(payload.error || 'Tool execution failed');
210
+ else if (payload.error)
211
+ toolItem.error = String(payload.error);
212
+ if (payload.duration_ms)
213
+ toolItem.durationMs = Number(payload.duration_ms);
214
+ if (notify)
215
+ options.onToolCompleted?.(toolItem);
216
+ }
217
+ else if (eventType === 'session.reasoning.delta' && typeof payload.delta === 'string') {
218
+ const key = JSON.stringify([payload.run_id, payload.reasoning_id ?? payload.reasoning_key]);
219
+ const previous = reasoning.get(key) ?? '';
220
+ const full = payload.delta_mode === 'replace' ? payload.delta : previous + payload.delta;
221
+ if (full === previous)
222
+ return;
223
+ reasoning.set(key, full);
224
+ if (reasoning.size > 64)
225
+ reasoning.delete(reasoning.keys().next().value);
226
+ if (notify)
227
+ options.onReasoning?.(full.startsWith(previous) ? full.slice(previous.length) : full, full);
228
+ }
229
+ };
134
230
  return this.realtime.watchSession(id, {
135
231
  signal: options.signal,
136
232
  surface: options.surface ?? 'chat',
@@ -143,96 +239,41 @@ export class SwarmChatNamespace {
143
239
  const sessionEvents = state.snapshot.events_by_session?.[id] || [];
144
240
  if (!initialized) {
145
241
  initialized = true;
146
- // Hydrated history is not a new turn or a new permission request.
147
- for (const evt of sessionEvents)
242
+ for (const evt of sessionEvents) {
148
243
  seenEventIds.add(evt.id);
244
+ applyEvent(evt, false);
245
+ }
149
246
  if (run?.run_id && !run.active)
150
247
  completedRuns.add(run.run_id);
151
248
  }
152
249
  if (run?.active)
153
250
  observedRun = run.run_id;
251
+ // 1. Assistant text: live text per run/stream, then the committed message's tail.
252
+ const liveKeys = new Set();
154
253
  for (const item of state.live || []) {
155
254
  const key = JSON.stringify([item.runId, item.streamId]);
156
- const previous = previousStreams.get(key) ?? '';
157
- if (item.text === previous)
255
+ liveKeys.add(key);
256
+ reportText(key, item.runId, item.streamId, item.text);
257
+ }
258
+ for (const [key, entry] of reportedText) {
259
+ if (liveKeys.has(key))
158
260
  continue;
159
- previousStreams.set(key, item.text);
160
- observedRun = item.runId;
161
- const delta = item.text.startsWith(previous) ? item.text.slice(previous.length) : item.text;
162
- if (item.streamId.includes('reasoning'))
163
- options.onReasoning?.(delta, item.text);
164
- else
165
- options.onText?.(delta, item.text);
261
+ const committed = state.messages.find(m => m.role === 'assistant'
262
+ && m.metadata?.run_id === entry.runId && m.metadata?.stream_id === entry.streamId);
263
+ if (committed && typeof committed.content === 'string') {
264
+ reportText(key, entry.runId, entry.streamId, committed.content);
265
+ forgetText(key);
266
+ }
267
+ else if (state.status === 'live' && !(run?.active && run.run_id === entry.runId)) {
268
+ forgetText(key);
269
+ }
166
270
  }
167
- // Bound transient state to streams still in the current snapshot.
168
- const liveKeys = new Set(state.live.map(item => JSON.stringify([item.runId, item.streamId])));
169
- for (const key of previousStreams.keys())
170
- if (!liveKeys.has(key))
171
- previousStreams.delete(key);
172
- // 2. Process Realtime Events (Tool streams & Permissions)
271
+ // 2. Durable events (tools, reasoning), each exactly once and in sequence order.
173
272
  for (const evt of sessionEvents) {
174
273
  if (seenEventIds.has(evt.id))
175
274
  continue;
176
275
  seenEventIds.add(evt.id);
177
- const payload = evt.payload || {};
178
- const eventType = evt.event_type;
179
- if (eventType === 'session.tool.started') {
180
- const toolId = String(payload.call_id || payload.tool_instance_id || evt.id);
181
- const toolItem = {
182
- id: toolId,
183
- name: String(payload.tool_name || 'tool'),
184
- identity: payload.tool_identity ? String(payload.tool_identity) : undefined,
185
- display: payload.tool_display ? String(payload.tool_display) : undefined,
186
- arguments: payload.arguments ? String(payload.arguments) : undefined,
187
- status: 'running',
188
- };
189
- activeTools.set(toolId, toolItem);
190
- options.onToolStarted?.(toolItem);
191
- }
192
- else if (eventType === 'session.tool.delta') {
193
- const toolId = String(payload.call_id || payload.tool_instance_id || '');
194
- const toolItem = activeTools.get(toolId);
195
- const delta = String(payload.output || '');
196
- if (toolItem && delta) {
197
- toolItem.output = (toolItem.output || '') + delta;
198
- options.onToolDelta?.(toolItem, delta);
199
- }
200
- }
201
- else if (eventType === 'session.tool.completed') {
202
- const toolId = String(payload.call_id || payload.tool_instance_id || '');
203
- let toolItem = activeTools.get(toolId);
204
- if (!toolItem) {
205
- toolItem = {
206
- id: toolId,
207
- name: String(payload.tool_name || 'tool'),
208
- status: 'completed',
209
- };
210
- activeTools.set(toolId, toolItem);
211
- }
212
- toolItem.status = 'completed';
213
- if (payload.output)
214
- toolItem.output = String(payload.output);
215
- if (payload.duration_ms)
216
- toolItem.durationMs = Number(payload.duration_ms);
217
- options.onToolCompleted?.(toolItem);
218
- }
219
- else if (eventType === 'session.tool.failed') {
220
- const toolId = String(payload.call_id || payload.tool_instance_id || '');
221
- let toolItem = activeTools.get(toolId);
222
- if (!toolItem) {
223
- toolItem = {
224
- id: toolId,
225
- name: String(payload.tool_name || 'tool'),
226
- status: 'failed',
227
- };
228
- activeTools.set(toolId, toolItem);
229
- }
230
- toolItem.status = 'failed';
231
- toolItem.error = String(payload.error || 'Tool execution failed');
232
- if (payload.duration_ms)
233
- toolItem.durationMs = Number(payload.duration_ms);
234
- options.onToolCompleted?.(toolItem);
235
- }
276
+ applyEvent(evt, true);
236
277
  if (seenEventIds.size > 4096)
237
278
  seenEventIds.delete(seenEventIds.values().next().value);
238
279
  }
@@ -296,7 +337,7 @@ export class SwarmChatNamespace {
296
337
  if (completedRuns.size > 4096)
297
338
  completedRuns.delete(completedRuns.values().next().value);
298
339
  if (observedRun === run.run_id || run.completed_at)
299
- options.onComplete?.(state.messages);
340
+ options.onComplete?.(options.rawCopyTags ? state.messages : state.messages.map(cleanAssistantMessage), run);
300
341
  }
301
342
  },
302
343
  onError: (err) => {
@@ -305,3 +346,27 @@ export class SwarmChatNamespace {
305
346
  });
306
347
  }
307
348
  }
349
+ const TERMINAL_TOOL_STATUS = {
350
+ 'session.tool.completed': 'completed',
351
+ 'session.tool.failed': 'failed',
352
+ 'session.tool.cancelled': 'cancelled',
353
+ 'session.tool.canceled': 'cancelled',
354
+ };
355
+ /**
356
+ * The durable tool-call id (`payload.call_id` on `session.tool.*` events). When an event
357
+ * carries only `tool_instance_id` (`step-<n>:<call_id>`), the step prefix is removed.
358
+ */
359
+ export function toolCallIdFromPayload(payload) {
360
+ const callId = typeof payload?.call_id === 'string' ? payload.call_id.trim() : '';
361
+ if (callId)
362
+ return callId;
363
+ const instance = typeof payload?.tool_instance_id === 'string' ? payload.tool_instance_id.trim() : '';
364
+ return instance.replace(/^step-\d+:/, '');
365
+ }
366
+ /** Assistant message with Swarm copy tags turned into Markdown code blocks. */
367
+ function cleanAssistantMessage(message) {
368
+ if (message?.role !== 'assistant' || typeof message.content !== 'string')
369
+ return message;
370
+ const content = copyTagsToMarkdown(message.content);
371
+ return content === message.content ? message : { ...message, content };
372
+ }
package/dist/client.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { SwarmAgentsNamespace } from './agents.js';
1
2
  import { SwarmAppsNamespace } from './apps.js';
2
3
  import { SwarmChatNamespace } from './chat.js';
3
4
  import { SwarmPermissionsNamespace } from './permissions.js';
@@ -11,7 +12,10 @@ import { SwarmWorkersNamespace } from './workers.js';
11
12
  import { SwarmDeliverablesNamespace } from './deliverables.js';
12
13
  import { SwarmDeployNamespace } from './deploy/index.js';
13
14
  import { SwarmNotificationsNamespace } from './notifications.js';
15
+ import { SwarmSignalsNamespace } from './signals.js';
16
+ import { SwarmBoxNamespace } from './box.js';
14
17
  import { SwarmProjectsNamespace } from './projects.js';
18
+ import { SwarmRemoteNamespace } from './remote.js';
15
19
  import { SwarmSessionsNamespace } from './sessions.js';
16
20
  import { SwarmSystemNamespace } from './system.js';
17
21
  import { SwarmStorageNamespace } from './storage/index.js';
@@ -52,7 +56,15 @@ export declare class SwarmClient {
52
56
  readonly notifications: SwarmNotificationsNamespace;
53
57
  /** Convenient alias for notifications namespace: inbox */
54
58
  readonly inbox: SwarmNotificationsNamespace;
59
+ /** Machine signal feed: blocked agents, failures, refused keys, reported events. */
60
+ readonly signals: SwarmSignalsNamespace;
61
+ /** Machine state for monitors: sandbox, blocked sessions, failing workers, usage. */
62
+ readonly box: SwarmBoxNamespace;
55
63
  readonly storage: SwarmStorageNamespace;
64
+ /** Owner administration of the relay connection (private transport only) */
65
+ readonly remote: SwarmRemoteNamespace;
66
+ /** Sealed agents and the client tools your application answers */
67
+ readonly agents: SwarmAgentsNamespace;
56
68
  /** Convenient alias for cloud storage connections: cloud */
57
69
  readonly cloud: SwarmStorageNamespace;
58
70
  constructor(config?: SwarmClientConfig);
package/dist/client.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { SwarmAgentsNamespace } from './agents.js';
1
2
  import { SwarmAppsNamespace } from './apps.js';
2
3
  import { SwarmChatNamespace } from './chat.js';
3
4
  import { SwarmPermissionsNamespace } from './permissions.js';
@@ -11,10 +12,14 @@ import { SwarmWorkersNamespace } from './workers.js';
11
12
  import { SwarmDeliverablesNamespace } from './deliverables.js';
12
13
  import { SwarmDeployNamespace } from './deploy/index.js';
13
14
  import { SwarmNotificationsNamespace } from './notifications.js';
15
+ import { SwarmSignalsNamespace } from './signals.js';
16
+ import { SwarmBoxNamespace } from './box.js';
14
17
  import { SwarmProjectsNamespace } from './projects.js';
18
+ import { SwarmRemoteNamespace } from './remote.js';
15
19
  import { SwarmSessionsNamespace } from './sessions.js';
16
20
  import { SwarmSystemNamespace } from './system.js';
17
21
  import { SwarmStorageNamespace } from './storage/index.js';
22
+ import { resolveSwarmConnection } from './local-connection.js';
18
23
  import { SwarmTransport } from './transport.js';
19
24
  import { SwarmWorkspacesNamespace } from './workspaces.js';
20
25
  export class SwarmClient {
@@ -51,19 +56,23 @@ export class SwarmClient {
51
56
  notifications;
52
57
  /** Convenient alias for notifications namespace: inbox */
53
58
  inbox;
59
+ /** Machine signal feed: blocked agents, failures, refused keys, reported events. */
60
+ signals;
61
+ /** Machine state for monitors: sandbox, blocked sessions, failing workers, usage. */
62
+ box;
54
63
  storage;
64
+ /** Owner administration of the relay connection (private transport only) */
65
+ remote;
66
+ /** Sealed agents and the client tools your application answers */
67
+ agents;
55
68
  /** Convenient alias for cloud storage connections: cloud */
56
69
  cloud;
57
70
  constructor(config = {}) {
58
71
  const env = typeof process !== 'undefined' && process?.env ? process.env : {};
59
- const baseUrl = config.baseUrl ||
60
- env.SWARM_API_URL ||
61
- env.SWARM_DESKTOP_URL ||
62
- 'http://127.0.0.1:18080';
63
- const socketPath = config.socketPath || env.SWARM_SOCKET_PATH;
72
+ const { baseUrl, socketPath } = resolveSwarmConnection(config, { env });
64
73
  const token = config.token || env.SWARM_AUTH_TOKEN || env.SWARM_DEPLOY_TOKEN;
65
74
  const resolved = {
66
- baseUrl: baseUrl.replace(/\/+$/, ''),
75
+ baseUrl,
67
76
  token,
68
77
  socketPath,
69
78
  defaultHeaders: config.defaultHeaders ?? {},
@@ -92,8 +101,12 @@ export class SwarmClient {
92
101
  this.deploy = new SwarmDeployNamespace(this.transport);
93
102
  this.notifications = new SwarmNotificationsNamespace(this.transport);
94
103
  this.inbox = this.notifications;
104
+ this.signals = new SwarmSignalsNamespace(this.transport);
105
+ this.box = new SwarmBoxNamespace(this.transport);
106
+ this.agents = new SwarmAgentsNamespace(this.transport);
95
107
  this.storage = new SwarmStorageNamespace(this.transport);
96
108
  this.cloud = this.storage;
109
+ this.remote = new SwarmRemoteNamespace(this.transport);
97
110
  }
98
111
  /**
99
112
  * Updates the bearer authentication token for all subsequent client requests.
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Swarm agents wrap copyable snippets (commands, config, file payloads) in
3
+ * `<copy label="...">…</copy>` tags. Swarm's own desktop and terminal UIs render
4
+ * them as copy buttons; everywhere else they are noise. `copyTagsToMarkdown`
5
+ * turns each tag into a plain Markdown code block (with its label, if any, as a
6
+ * bold line above it), so any Markdown renderer shows it correctly.
7
+ *
8
+ * Rules (same parsing as Swarm's own renderers):
9
+ * - Tags inside fenced code blocks (``` or ~~~) are left untouched.
10
+ * - The label comes from `label`, `title` or `name`.
11
+ * - A tag that is never closed becomes a code block running to the end.
12
+ * - Text without copy tags is returned unchanged.
13
+ *
14
+ * Pass `{ streaming: true }` for text that is still arriving: a half-written tag
15
+ * at the very end is hidden, so converting the growing text only ever appends.
16
+ *
17
+ * The SDK applies it by default to assistant text from `chat.run`,
18
+ * `chat.listMessages` and `chat.stream` (`onText`, `onComplete`); pass
19
+ * `rawCopyTags: true` there to get the original text. Lower-level reads
20
+ * (`sessions.get`, `realtime.watchSession`) return the stored text unchanged.
21
+ */
22
+ export declare function copyTagsToMarkdown(text: string, options?: {
23
+ streaming?: boolean;
24
+ }): string;
25
+ /** True when `text` contains at least one `<copy>` tag outside fenced code. */
26
+ export declare function hasCopyTags(text: string): boolean;