@swarm-agent/sdk 0.1.49 → 0.1.51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/HEADLESS_UI.md +4 -6
- package/README.md +138 -58
- package/dist/agents.d.ts +114 -0
- package/dist/agents.js +183 -0
- package/dist/auth.d.ts +14 -0
- package/dist/auth.js +16 -0
- package/dist/box.d.ts +88 -0
- package/dist/box.js +13 -0
- package/dist/browser.d.ts +6 -2
- package/dist/browser.js +11 -3
- package/dist/chat.d.ts +41 -7
- package/dist/chat.js +148 -83
- package/dist/client.d.ts +12 -0
- package/dist/client.js +19 -6
- package/dist/copy-tags.d.ts +26 -0
- package/dist/copy-tags.js +133 -0
- package/dist/errors.d.ts +16 -0
- package/dist/errors.js +21 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/local-connection.d.ts +39 -0
- package/dist/local-connection.js +56 -0
- package/dist/permission-ui.d.ts +1 -0
- package/dist/permission-ui.js +37 -0
- package/dist/permissions.d.ts +4 -0
- package/dist/permissions.js +7 -0
- package/dist/projects.d.ts +25 -7
- package/dist/projects.js +65 -17
- package/dist/provider-auth.d.ts +5 -3
- package/dist/provider-auth.js +6 -8
- package/dist/realtime.d.ts +40 -6
- package/dist/realtime.js +156 -52
- package/dist/remote.d.ts +59 -0
- package/dist/remote.js +46 -0
- package/dist/sessions.d.ts +31 -1
- package/dist/sessions.js +49 -1
- package/dist/settings.d.ts +3 -0
- package/dist/settings.js +19 -3
- package/dist/signals.d.ts +148 -0
- package/dist/signals.js +97 -0
- package/dist/types.d.ts +97 -6
- package/dist/websocket.d.ts +3 -1
- package/dist/websocket.js +1 -1
- package/dist/workers.d.ts +6 -0
- package/dist/workers.js +27 -0
- package/dist/workspaces.d.ts +20 -0
- package/dist/workspaces.js +24 -0
- package/package.json +1 -1
package/dist/auth.d.ts
CHANGED
|
@@ -22,6 +22,20 @@ export declare class SwarmAuthNamespace {
|
|
|
22
22
|
* Store the token securely—it cannot be retrieved again in plaintext!
|
|
23
23
|
*/
|
|
24
24
|
createScopedToken(params: CreateScopedTokenParams): Promise<CreateScopedTokenResult>;
|
|
25
|
+
/**
|
|
26
|
+
* Creates an AI key for Swarm Control (`/mcp` on the scoped-token gateway).
|
|
27
|
+
* `read` keys see and call only read tools; `write` keys may also start and
|
|
28
|
+
* steer sessions; neither may approve tool calls or manage anything. `full`
|
|
29
|
+
* keys are for an AI the owner trusts to run the box: they also approve
|
|
30
|
+
* tool calls and manage models, workers, limits, custom agents, client app
|
|
31
|
+
* keys and ChatGPT sign-in. Every level reaches only `/mcp`. Lifetime
|
|
32
|
+
* defaults to 30 days (at most 365). Owner only.
|
|
33
|
+
*/
|
|
34
|
+
createAIKey(params: {
|
|
35
|
+
name: string;
|
|
36
|
+
access: 'read' | 'write' | 'full';
|
|
37
|
+
expires_in_seconds?: number;
|
|
38
|
+
}): Promise<CreateScopedTokenResult>;
|
|
25
39
|
/**
|
|
26
40
|
* Lists all scoped deploy tokens for the active account.
|
|
27
41
|
* Secret hashes are masked; hints (e.g. 'swk_...abcd') are displayed.
|
package/dist/auth.js
CHANGED
|
@@ -54,6 +54,22 @@ export class SwarmAuthNamespace {
|
|
|
54
54
|
});
|
|
55
55
|
return res.data;
|
|
56
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Creates an AI key for Swarm Control (`/mcp` on the scoped-token gateway).
|
|
59
|
+
* `read` keys see and call only read tools; `write` keys may also start and
|
|
60
|
+
* steer sessions; neither may approve tool calls or manage anything. `full`
|
|
61
|
+
* keys are for an AI the owner trusts to run the box: they also approve
|
|
62
|
+
* tool calls and manage models, workers, limits, custom agents, client app
|
|
63
|
+
* keys and ChatGPT sign-in. Every level reaches only `/mcp`. Lifetime
|
|
64
|
+
* defaults to 30 days (at most 365). Owner only.
|
|
65
|
+
*/
|
|
66
|
+
async createAIKey(params) {
|
|
67
|
+
const res = await this.transport.request('/v3/auth/tokens', {
|
|
68
|
+
method: 'POST',
|
|
69
|
+
body: { name: params.name, ai_access: params.access, expires_in_seconds: params.expires_in_seconds },
|
|
70
|
+
});
|
|
71
|
+
return res.data;
|
|
72
|
+
}
|
|
57
73
|
/**
|
|
58
74
|
* Lists all scoped deploy tokens for the active account.
|
|
59
75
|
* Secret hashes are masked; hints (e.g. 'swk_...abcd') are displayed.
|
package/dist/box.d.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
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
|
+
version?: string;
|
|
14
|
+
commit?: string;
|
|
15
|
+
/** The installer's update state (install.sh update), when it has one. */
|
|
16
|
+
update?: {
|
|
17
|
+
auto?: 'on' | 'off';
|
|
18
|
+
channel?: string;
|
|
19
|
+
/** current, waiting, waiting_review, blocked, building, waiting_idle, installed, rolled_back or failed. */
|
|
20
|
+
state?: string;
|
|
21
|
+
reason?: string;
|
|
22
|
+
running?: string;
|
|
23
|
+
candidate?: string;
|
|
24
|
+
at?: number;
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
/** What a restart of Swarm would interrupt now, across the machine. */
|
|
28
|
+
busy: {
|
|
29
|
+
busy: boolean;
|
|
30
|
+
sessions: number;
|
|
31
|
+
worker_runs: number;
|
|
32
|
+
environment_builds: number;
|
|
33
|
+
};
|
|
34
|
+
sandbox: {
|
|
35
|
+
mode: string;
|
|
36
|
+
active: boolean;
|
|
37
|
+
runtime?: string;
|
|
38
|
+
reason?: string;
|
|
39
|
+
};
|
|
40
|
+
secrets_gateway: boolean;
|
|
41
|
+
permissions: {
|
|
42
|
+
bypass: boolean;
|
|
43
|
+
bypass_blocked_reason?: string;
|
|
44
|
+
};
|
|
45
|
+
attention: {
|
|
46
|
+
blocked_sessions: number;
|
|
47
|
+
oldest_pending_at?: number;
|
|
48
|
+
sessions: {
|
|
49
|
+
session_id: string;
|
|
50
|
+
pending: number;
|
|
51
|
+
oldest_pending_at: number;
|
|
52
|
+
}[];
|
|
53
|
+
};
|
|
54
|
+
workers: {
|
|
55
|
+
total: number;
|
|
56
|
+
truncated?: boolean;
|
|
57
|
+
active_runs: number;
|
|
58
|
+
today_runs: number;
|
|
59
|
+
today_failed: number;
|
|
60
|
+
/** UTC day the counts cover, YYYY-MM-DD. */
|
|
61
|
+
day?: string;
|
|
62
|
+
failing: {
|
|
63
|
+
worker_id: string;
|
|
64
|
+
name: string;
|
|
65
|
+
today_runs: number;
|
|
66
|
+
today_failed: number;
|
|
67
|
+
active_runs: number;
|
|
68
|
+
}[];
|
|
69
|
+
};
|
|
70
|
+
usage: {
|
|
71
|
+
today_cost_usd?: number;
|
|
72
|
+
today_tokens?: number;
|
|
73
|
+
daily_cost_limit_usd?: number;
|
|
74
|
+
limit_enabled?: boolean;
|
|
75
|
+
limit_exceeded?: boolean;
|
|
76
|
+
};
|
|
77
|
+
signals: {
|
|
78
|
+
latest_seq?: number;
|
|
79
|
+
oldest_seq?: number;
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/** Machine-level state. Needs a key with `signals:read`. */
|
|
83
|
+
export declare class SwarmBoxNamespace {
|
|
84
|
+
private readonly transport;
|
|
85
|
+
constructor(transport: SwarmTransport);
|
|
86
|
+
/** Is this machine all right, and does anything need a person? One cheap call. */
|
|
87
|
+
summary(): Promise<BoxSummary>;
|
|
88
|
+
}
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
316
|
-
throw new
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
90
|
-
|
|
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'
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
157
|
-
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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);
|