@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
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export class SwarmCredentialsNamespace {
|
|
2
|
+
transport;
|
|
3
|
+
constructor(transport) {
|
|
4
|
+
this.transport = transport;
|
|
5
|
+
}
|
|
6
|
+
async list(options = {}) {
|
|
7
|
+
const query = new URLSearchParams();
|
|
8
|
+
for (const [key, value] of Object.entries(options))
|
|
9
|
+
if (value !== undefined)
|
|
10
|
+
query.set(key, String(value));
|
|
11
|
+
return (await this.transport.request(`/v1/auth/credentials?${query}`)).data;
|
|
12
|
+
}
|
|
13
|
+
async save(input) {
|
|
14
|
+
return (await this.transport.request('/v1/auth/credentials', { method: 'POST', body: input })).data;
|
|
15
|
+
}
|
|
16
|
+
async activate(provider, id) {
|
|
17
|
+
return (await this.transport.request('/v1/auth/credentials/active', { method: 'POST', body: { provider, id } })).data;
|
|
18
|
+
}
|
|
19
|
+
async verify(provider, id) {
|
|
20
|
+
return (await this.transport.request('/v1/auth/credentials/verify', { method: 'POST', body: { provider, id } })).data;
|
|
21
|
+
}
|
|
22
|
+
async delete(provider, id) {
|
|
23
|
+
return (await this.transport.request('/v1/auth/credentials/delete', { method: 'POST', body: { provider, id } })).data;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export class SwarmCodexAuthNamespace {
|
|
27
|
+
transport;
|
|
28
|
+
constructor(transport) {
|
|
29
|
+
this.transport = transport;
|
|
30
|
+
}
|
|
31
|
+
async start(input) {
|
|
32
|
+
return (await this.transport.request('/v1/auth/codex/oauth/start', { method: 'POST', body: { ...input, provider: 'codex' } })).data;
|
|
33
|
+
}
|
|
34
|
+
async status(session_id) {
|
|
35
|
+
return (await this.transport.request(`/v1/auth/codex/oauth/status?${new URLSearchParams({ session_id })}`)).data;
|
|
36
|
+
}
|
|
37
|
+
async complete(session_id, callback_input) {
|
|
38
|
+
return (await this.transport.request('/v1/auth/codex/oauth/complete', { method: 'POST', body: { session_id, callback_input } })).data;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export class SwarmOnboardingNamespace {
|
|
42
|
+
transport;
|
|
43
|
+
constructor(transport) {
|
|
44
|
+
this.transport = transport;
|
|
45
|
+
}
|
|
46
|
+
async get() { return (await this.transport.request('/v1/onboarding')).data; }
|
|
47
|
+
async update(body) {
|
|
48
|
+
return (await this.transport.request('/v1/onboarding', { method: 'POST', body })).data;
|
|
49
|
+
}
|
|
50
|
+
/** First credential only. Use auth.credentials.save for subsequent credentials. */
|
|
51
|
+
async credential(body) {
|
|
52
|
+
return (await this.transport.request('/v1/onboarding/provider/credential', { method: 'POST', body })).data;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import type { SwarmTransport } from './transport.js';
|
|
2
|
+
export interface V3Message {
|
|
3
|
+
id: string;
|
|
4
|
+
session_id: string;
|
|
5
|
+
global_seq: number;
|
|
6
|
+
role: string;
|
|
7
|
+
content: string;
|
|
8
|
+
created_at: number;
|
|
9
|
+
metadata?: Record<string, unknown>;
|
|
10
|
+
[key: string]: unknown;
|
|
11
|
+
}
|
|
12
|
+
export interface V3Event {
|
|
13
|
+
id: string;
|
|
14
|
+
session_id: string;
|
|
15
|
+
seq: number;
|
|
16
|
+
event_type: string;
|
|
17
|
+
payload: Record<string, unknown>;
|
|
18
|
+
ts_unix_ms: number;
|
|
19
|
+
causation_id?: string;
|
|
20
|
+
correlation_id?: string;
|
|
21
|
+
epoch_id?: string;
|
|
22
|
+
}
|
|
23
|
+
export interface V3Projection {
|
|
24
|
+
session_id: string;
|
|
25
|
+
last_event_seq: number;
|
|
26
|
+
projection_high_watermark_seq: number;
|
|
27
|
+
updated_at: number;
|
|
28
|
+
}
|
|
29
|
+
export interface V3Subscription {
|
|
30
|
+
session_id: string;
|
|
31
|
+
subscription_id: string;
|
|
32
|
+
endpoint_cursor?: string;
|
|
33
|
+
}
|
|
34
|
+
export interface V3Resume {
|
|
35
|
+
protocol: 'v3.realtime';
|
|
36
|
+
protocol_version: 1;
|
|
37
|
+
kind: 'resume';
|
|
38
|
+
endpoint_cursor: string;
|
|
39
|
+
subscriptions: V3Subscription[];
|
|
40
|
+
capabilities?: string[];
|
|
41
|
+
}
|
|
42
|
+
export interface V3LivePatch {
|
|
43
|
+
session_id: string;
|
|
44
|
+
run_id: string;
|
|
45
|
+
stream_id: string;
|
|
46
|
+
stream_kind: string;
|
|
47
|
+
operation: string;
|
|
48
|
+
step: number;
|
|
49
|
+
step_id: string;
|
|
50
|
+
live_seq_start: number;
|
|
51
|
+
live_seq_end: number;
|
|
52
|
+
offset_start: number;
|
|
53
|
+
offset_end: number;
|
|
54
|
+
text: string;
|
|
55
|
+
recorded_at: number;
|
|
56
|
+
}
|
|
57
|
+
export interface V3Frame {
|
|
58
|
+
protocol: 'v3.realtime';
|
|
59
|
+
protocol_version: 1;
|
|
60
|
+
kind: string;
|
|
61
|
+
session_id?: string;
|
|
62
|
+
subscription_id?: string;
|
|
63
|
+
endpoint_cursor?: string;
|
|
64
|
+
capabilities?: string[];
|
|
65
|
+
event?: V3Event;
|
|
66
|
+
projection?: V3Projection;
|
|
67
|
+
live?: V3LivePatch;
|
|
68
|
+
error?: string;
|
|
69
|
+
error_code?: string;
|
|
70
|
+
bootstrap_required?: boolean;
|
|
71
|
+
[key: string]: unknown;
|
|
72
|
+
}
|
|
73
|
+
export interface V3SyncRequest {
|
|
74
|
+
surface?: string;
|
|
75
|
+
session_ids: string[];
|
|
76
|
+
selector?: {
|
|
77
|
+
kind: 'session_ids';
|
|
78
|
+
session_ids: string[];
|
|
79
|
+
};
|
|
80
|
+
history?: {
|
|
81
|
+
mode?: string;
|
|
82
|
+
max_messages_per_session?: number;
|
|
83
|
+
max_events_per_session?: number;
|
|
84
|
+
include_events?: boolean;
|
|
85
|
+
manifest_policy?: string;
|
|
86
|
+
};
|
|
87
|
+
resources?: {
|
|
88
|
+
messages?: boolean;
|
|
89
|
+
events?: boolean;
|
|
90
|
+
run_intents?: boolean;
|
|
91
|
+
current_run_state?: boolean;
|
|
92
|
+
session_view?: boolean;
|
|
93
|
+
permission_summaries?: boolean;
|
|
94
|
+
active_plan?: boolean;
|
|
95
|
+
plan_revisions?: boolean;
|
|
96
|
+
};
|
|
97
|
+
include_active?: boolean;
|
|
98
|
+
}
|
|
99
|
+
export interface V3ReplayResponse {
|
|
100
|
+
ok: boolean;
|
|
101
|
+
endpoint_cursor: string;
|
|
102
|
+
has_more: boolean;
|
|
103
|
+
events: {
|
|
104
|
+
session_id: string;
|
|
105
|
+
event_type: string;
|
|
106
|
+
event: V3Event;
|
|
107
|
+
projection: V3Projection;
|
|
108
|
+
[key: string]: unknown;
|
|
109
|
+
}[];
|
|
110
|
+
selector: {
|
|
111
|
+
kind?: string;
|
|
112
|
+
session_ids?: string[];
|
|
113
|
+
[key: string]: unknown;
|
|
114
|
+
};
|
|
115
|
+
replay_instructions: Record<string, unknown>;
|
|
116
|
+
}
|
|
117
|
+
export interface V3Permission {
|
|
118
|
+
id: string;
|
|
119
|
+
session_id: string;
|
|
120
|
+
run_id: string;
|
|
121
|
+
call_id: string;
|
|
122
|
+
tool_name: string;
|
|
123
|
+
tool_arguments: string;
|
|
124
|
+
requirement: string;
|
|
125
|
+
mode: string;
|
|
126
|
+
status: string;
|
|
127
|
+
decision: string;
|
|
128
|
+
reason: string;
|
|
129
|
+
created_at: number;
|
|
130
|
+
updated_at: number;
|
|
131
|
+
resolved_at: number;
|
|
132
|
+
tool_call_arguments?: string;
|
|
133
|
+
approved_arguments?: string;
|
|
134
|
+
proposal_revision?: number;
|
|
135
|
+
}
|
|
136
|
+
export interface V3RunState {
|
|
137
|
+
session_id: string;
|
|
138
|
+
run_id: string;
|
|
139
|
+
active: boolean;
|
|
140
|
+
status: string;
|
|
141
|
+
blocked_reason?: string;
|
|
142
|
+
created_at: number;
|
|
143
|
+
updated_at: number;
|
|
144
|
+
event_seq: number;
|
|
145
|
+
started_at?: number;
|
|
146
|
+
completed_at?: number;
|
|
147
|
+
epoch_id?: string;
|
|
148
|
+
plan_id?: string;
|
|
149
|
+
checkpoint_id?: string;
|
|
150
|
+
attempt_id?: string;
|
|
151
|
+
}
|
|
152
|
+
export interface V3SessionView {
|
|
153
|
+
pending_permissions?: V3Permission[];
|
|
154
|
+
current_run_state?: V3RunState;
|
|
155
|
+
active_plan?: Record<string, unknown>;
|
|
156
|
+
[key: string]: unknown;
|
|
157
|
+
}
|
|
158
|
+
export interface V3SyncSnapshot {
|
|
159
|
+
ok: boolean;
|
|
160
|
+
rev: number;
|
|
161
|
+
snapshot_endpoint_cursor: string;
|
|
162
|
+
sessions_by_id: Record<string, Record<string, unknown>>;
|
|
163
|
+
projections_by_session: Record<string, V3Projection>;
|
|
164
|
+
messages_by_session: Record<string, V3Message[]>;
|
|
165
|
+
events_by_session: Record<string, V3Event[]>;
|
|
166
|
+
current_run_state_by_session?: Record<string, V3RunState>;
|
|
167
|
+
session_views_by_id?: Record<string, V3SessionView>;
|
|
168
|
+
realtime?: {
|
|
169
|
+
stream_path: string;
|
|
170
|
+
resume: V3Resume;
|
|
171
|
+
};
|
|
172
|
+
omissions: {
|
|
173
|
+
session_id?: string;
|
|
174
|
+
reason?: string;
|
|
175
|
+
[key: string]: unknown;
|
|
176
|
+
}[];
|
|
177
|
+
pagination: Record<string, unknown>;
|
|
178
|
+
[key: string]: unknown;
|
|
179
|
+
}
|
|
180
|
+
/** Minimal WebSocket contract, also usable by authenticated same-origin browser proxies. */
|
|
181
|
+
export interface RealtimeSocket {
|
|
182
|
+
send(data: string): void;
|
|
183
|
+
close(): void;
|
|
184
|
+
addEventListener(type: 'message', listener: (event: {
|
|
185
|
+
data: unknown;
|
|
186
|
+
}) => void): void;
|
|
187
|
+
addEventListener(type: 'close' | 'error', listener: () => void): void;
|
|
188
|
+
}
|
|
189
|
+
export type RealtimeSocketFactory = (path: string) => Promise<RealtimeSocket>;
|
|
190
|
+
export interface SessionWatchState {
|
|
191
|
+
sessionId: string;
|
|
192
|
+
snapshot: V3SyncSnapshot;
|
|
193
|
+
messages: V3Message[];
|
|
194
|
+
/** Transient full text by run/stream. Replace on every update, never append to durable messages. */
|
|
195
|
+
live: ReadonlyArray<{
|
|
196
|
+
runId: string;
|
|
197
|
+
streamId: string;
|
|
198
|
+
text: string;
|
|
199
|
+
}>;
|
|
200
|
+
status: 'syncing' | 'live' | 'reconnecting';
|
|
201
|
+
}
|
|
202
|
+
export interface SessionWatchOptions {
|
|
203
|
+
signal?: AbortSignal;
|
|
204
|
+
surface?: string;
|
|
205
|
+
socketFactory?: RealtimeSocketFactory;
|
|
206
|
+
onChange(state: SessionWatchState): void;
|
|
207
|
+
onError?(error: Error): void;
|
|
208
|
+
maxReconnects?: number;
|
|
209
|
+
}
|
|
210
|
+
export interface SessionWatch {
|
|
211
|
+
ready: Promise<void>;
|
|
212
|
+
done: Promise<void>;
|
|
213
|
+
dispose(): void;
|
|
214
|
+
}
|
|
215
|
+
/** Offset authority is UTF-8 bytes (not JavaScript UTF-16 string length). */
|
|
216
|
+
export declare class V3LiveText {
|
|
217
|
+
private streams;
|
|
218
|
+
private committed;
|
|
219
|
+
reset(): void;
|
|
220
|
+
reconcile(messages: V3Message[]): void;
|
|
221
|
+
durable(events: V3Event[]): void;
|
|
222
|
+
accept(p: V3LivePatch): boolean;
|
|
223
|
+
values(): SessionWatchState['live'];
|
|
224
|
+
}
|
|
225
|
+
export declare class SwarmRealtimeNamespace {
|
|
226
|
+
private readonly transport;
|
|
227
|
+
constructor(transport: SwarmTransport);
|
|
228
|
+
bootstrap(body: V3SyncRequest, signal?: AbortSignal): Promise<V3SyncSnapshot>;
|
|
229
|
+
hydrate(body: V3SyncRequest, signal?: AbortSignal): Promise<V3SyncSnapshot>;
|
|
230
|
+
/** Bounded durable replay. Keep the identical selector/resources/surface for every cursor. */
|
|
231
|
+
replay(body: V3SyncRequest & {
|
|
232
|
+
endpoint_cursor: string;
|
|
233
|
+
limit?: number;
|
|
234
|
+
}, signal?: AbortSignal): Promise<V3ReplayResponse>;
|
|
235
|
+
/** Node uses ws over the private Unix socket or header-authenticated HTTP upgrade. */
|
|
236
|
+
socket(path: string): Promise<RealtimeSocket>;
|
|
237
|
+
watchSession(sessionId: string, options: SessionWatchOptions): SessionWatch;
|
|
238
|
+
private runWatch;
|
|
239
|
+
private connection;
|
|
240
|
+
}
|
package/dist/realtime.js
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
/** Offset authority is UTF-8 bytes (not JavaScript UTF-16 string length). */
|
|
2
|
+
export class V3LiveText {
|
|
3
|
+
streams = new Map();
|
|
4
|
+
committed = new Set();
|
|
5
|
+
reset() { this.streams.clear(); this.committed.clear(); }
|
|
6
|
+
reconcile(messages) {
|
|
7
|
+
for (const m of messages) {
|
|
8
|
+
if (m.role !== 'assistant')
|
|
9
|
+
continue;
|
|
10
|
+
const run = m.metadata?.run_id ?? m.metadata?.runId;
|
|
11
|
+
const stream = m.metadata?.stream_id;
|
|
12
|
+
if (typeof run === 'string' && typeof stream === 'string') {
|
|
13
|
+
const key = JSON.stringify([run, stream]);
|
|
14
|
+
if (!this.committed.has(key) && this.committed.size >= 4096)
|
|
15
|
+
throw new Error('Live stream history limit reached; reconnect required');
|
|
16
|
+
this.streams.delete(key);
|
|
17
|
+
this.committed.add(key);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
durable(events) {
|
|
22
|
+
for (const event of events) {
|
|
23
|
+
const p = event.payload;
|
|
24
|
+
if (event.event_type === 'session.assistant.completed') {
|
|
25
|
+
const message = p.message;
|
|
26
|
+
if (message?.role === 'assistant')
|
|
27
|
+
this.reconcile([message]);
|
|
28
|
+
const run = p.run_id ?? message?.metadata?.run_id;
|
|
29
|
+
const stream = p.stream_id ?? message?.metadata?.stream_id;
|
|
30
|
+
if (typeof run === 'string' && typeof stream === 'string') {
|
|
31
|
+
const key = JSON.stringify([run, stream]);
|
|
32
|
+
if (!this.committed.has(key) && this.committed.size >= 4096)
|
|
33
|
+
throw new Error('Live history limit; reconnect required');
|
|
34
|
+
this.streams.delete(key);
|
|
35
|
+
this.committed.add(key);
|
|
36
|
+
}
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (!['session.assistant.delta', 'session.message.delta'].includes(event.event_type))
|
|
40
|
+
continue;
|
|
41
|
+
const run = p.run_id, stream = p.stream_id, start = p.offset_start, end = p.offset_end;
|
|
42
|
+
const text = p.delta ?? p.text_delta ?? p.content_delta;
|
|
43
|
+
if (typeof run !== 'string' || typeof stream !== 'string' || typeof start !== 'number' || typeof end !== 'number' || typeof text !== 'string')
|
|
44
|
+
continue;
|
|
45
|
+
const key = JSON.stringify([run, stream]);
|
|
46
|
+
const old = this.streams.get(key);
|
|
47
|
+
if (this.committed.has(key) || end <= (old?.offset ?? 0))
|
|
48
|
+
continue;
|
|
49
|
+
const offset = old?.offset ?? 0;
|
|
50
|
+
const bytes = new TextEncoder().encode(text);
|
|
51
|
+
if (start < 0 || start > offset || end !== start + bytes.length || end > 4 * 1024 * 1024 || (!old && this.streams.size >= 64))
|
|
52
|
+
continue;
|
|
53
|
+
const suffix = new TextDecoder('utf-8', { fatal: true }).decode(bytes.subarray(offset - start));
|
|
54
|
+
this.streams.set(key, { runId: run, streamId: stream, text: (old?.text ?? '') + suffix, seq: 0, offset: end });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
accept(p) {
|
|
58
|
+
if (p.stream_kind !== 'assistant_text')
|
|
59
|
+
return true;
|
|
60
|
+
const key = JSON.stringify([p.run_id, p.stream_id]);
|
|
61
|
+
if (this.committed.has(key))
|
|
62
|
+
return true;
|
|
63
|
+
const old = this.streams.get(key);
|
|
64
|
+
if (old && p.offset_end <= old.offset)
|
|
65
|
+
return true;
|
|
66
|
+
if (p.operation !== 'append' || (old?.seq !== 0 && p.live_seq_start !== (old?.seq ?? 0) + 1) ||
|
|
67
|
+
p.offset_start !== (old?.offset ?? 0) || p.live_seq_end < p.live_seq_start ||
|
|
68
|
+
p.offset_end !== p.offset_start + new TextEncoder().encode(p.text).length ||
|
|
69
|
+
p.offset_end > 4 * 1024 * 1024 || (!old && this.streams.size >= 64))
|
|
70
|
+
return false;
|
|
71
|
+
this.streams.set(key, { runId: p.run_id, streamId: p.stream_id, text: (old?.text ?? '') + p.text, seq: p.live_seq_end, offset: p.offset_end });
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
values() { return [...this.streams.values()].map(({ runId, streamId, text }) => ({ runId, streamId, text })); }
|
|
75
|
+
}
|
|
76
|
+
export class SwarmRealtimeNamespace {
|
|
77
|
+
transport;
|
|
78
|
+
constructor(transport) {
|
|
79
|
+
this.transport = transport;
|
|
80
|
+
}
|
|
81
|
+
async bootstrap(body, signal) {
|
|
82
|
+
return (await this.transport.request('/v3/sync/bootstrap', { method: 'POST', body, signal })).data;
|
|
83
|
+
}
|
|
84
|
+
async hydrate(body, signal) {
|
|
85
|
+
return (await this.transport.request('/v3/sync/hydrate', { method: 'POST', body, signal })).data;
|
|
86
|
+
}
|
|
87
|
+
/** Bounded durable replay. Keep the identical selector/resources/surface for every cursor. */
|
|
88
|
+
async replay(body, signal) {
|
|
89
|
+
return (await this.transport.request('/v3/sync/stream', { method: 'POST', body, signal })).data;
|
|
90
|
+
}
|
|
91
|
+
/** Node uses ws over the private Unix socket or header-authenticated HTTP upgrade. */
|
|
92
|
+
async socket(path) {
|
|
93
|
+
const config = this.transport.getConfig();
|
|
94
|
+
if (!path.startsWith('/v3/realtime/stream?'))
|
|
95
|
+
throw new Error('Invalid realtime path');
|
|
96
|
+
if (typeof process === 'undefined' || !process.versions?.node) {
|
|
97
|
+
if (config.token || config.socketPath || Object.keys(config.defaultHeaders).length)
|
|
98
|
+
throw new Error('Browser realtime requires a same-origin authenticated proxy; do not expose admin tokens');
|
|
99
|
+
const url = new URL(config.baseUrl);
|
|
100
|
+
if (typeof location === 'undefined' || url.origin !== location.origin)
|
|
101
|
+
throw new Error('Browser realtime requires same-origin proxy');
|
|
102
|
+
return new WebSocket(`${url.origin.replace(/^http/, 'ws')}${path}`);
|
|
103
|
+
}
|
|
104
|
+
const moduleName = 'ws';
|
|
105
|
+
const module = await import(moduleName);
|
|
106
|
+
const headers = { ...config.defaultHeaders, ...(config.token ? { Authorization: `Bearer ${config.token}` } : {}) };
|
|
107
|
+
const url = config.socketPath ? `ws+unix://${config.socketPath}:${path}` : `${config.baseUrl.replace(/^http/, 'ws')}${path}`;
|
|
108
|
+
const socket = new module.default(url, { headers, followRedirects: false, handshakeTimeout: config.timeoutMs, maxPayload: 8 * 1024 * 1024 });
|
|
109
|
+
// ws emits an error when a connecting socket is disposed before upgrade.
|
|
110
|
+
socket.addEventListener('error', () => { });
|
|
111
|
+
return socket;
|
|
112
|
+
}
|
|
113
|
+
watchSession(sessionId, options) {
|
|
114
|
+
if (!sessionId.trim())
|
|
115
|
+
throw new Error('sessionId is required');
|
|
116
|
+
const controller = new AbortController();
|
|
117
|
+
const signal = controller.signal;
|
|
118
|
+
const abort = () => controller.abort();
|
|
119
|
+
options.signal?.addEventListener('abort', abort, { once: true });
|
|
120
|
+
if (options.signal?.aborted)
|
|
121
|
+
abort();
|
|
122
|
+
let resolveReady;
|
|
123
|
+
let rejectReady;
|
|
124
|
+
const ready = new Promise((resolve, reject) => { resolveReady = resolve; rejectReady = reject; });
|
|
125
|
+
// A caller may await only done; prevent an unobserved ready rejection.
|
|
126
|
+
void ready.catch(() => { });
|
|
127
|
+
const done = this.runWatch(sessionId, options, signal, resolveReady).catch((error) => {
|
|
128
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
129
|
+
rejectReady(err);
|
|
130
|
+
if (!signal.aborted) {
|
|
131
|
+
options.onError?.(err);
|
|
132
|
+
throw err;
|
|
133
|
+
}
|
|
134
|
+
}).finally(() => { rejectReady(new Error('Watch disposed')); options.signal?.removeEventListener('abort', abort); });
|
|
135
|
+
void done.catch(() => { });
|
|
136
|
+
return { ready, done, dispose: abort };
|
|
137
|
+
}
|
|
138
|
+
async runWatch(id, options, signal, ready) {
|
|
139
|
+
const surface = options.surface ?? 'desktop';
|
|
140
|
+
const body = { surface, session_ids: [id], selector: { kind: 'session_ids', session_ids: [id] },
|
|
141
|
+
history: { mode: 'tail', max_messages_per_session: 200, max_events_per_session: 200, include_events: true },
|
|
142
|
+
resources: { messages: true, events: true, current_run_state: true, session_view: true } };
|
|
143
|
+
let attempts = 0;
|
|
144
|
+
while (!signal.aborted) {
|
|
145
|
+
// Every reconnect hydrates fresh durable authority before replay; no timer polling.
|
|
146
|
+
const snapshot = await this.hydrate(body, signal);
|
|
147
|
+
if (signal.aborted)
|
|
148
|
+
return;
|
|
149
|
+
if (!snapshot.sessions_by_id[id] || !snapshot.realtime?.resume.endpoint_cursor)
|
|
150
|
+
throw new Error('Session omitted or realtime bootstrap unavailable');
|
|
151
|
+
try {
|
|
152
|
+
await this.connection(id, body, snapshot, options, signal, ready);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
if (signal.aborted)
|
|
157
|
+
return;
|
|
158
|
+
if (error instanceof FatalRealtimeError || attempts >= (options.maxReconnects ?? 6))
|
|
159
|
+
throw error;
|
|
160
|
+
options.onChange({ sessionId: id, snapshot, messages: snapshot.messages_by_session[id] ?? [], live: [], status: 'reconnecting' });
|
|
161
|
+
await delay(Math.min(250 * 2 ** attempts++, 8000), signal);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
async connection(id, body, initial, options, signal, ready) {
|
|
166
|
+
const socket = await (options.socketFactory ?? ((path) => this.socket(path)))(`/v3/realtime/stream?${new URLSearchParams({ surface: body.surface })}`);
|
|
167
|
+
if (signal.aborted) {
|
|
168
|
+
socket.close();
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
return new Promise((resolve, reject) => {
|
|
172
|
+
const requests = new AbortController();
|
|
173
|
+
let snapshot = initial;
|
|
174
|
+
let ended = false;
|
|
175
|
+
let replayed = false;
|
|
176
|
+
let hello = false;
|
|
177
|
+
let liveNegotiated = false;
|
|
178
|
+
let refreshing = false;
|
|
179
|
+
let dirty = false;
|
|
180
|
+
let readyPending = false;
|
|
181
|
+
let lastSeq = snapshot.projections_by_session[id]?.last_event_seq ?? 0;
|
|
182
|
+
const live = new V3LiveText();
|
|
183
|
+
live.reconcile(snapshot.messages_by_session[id] ?? []);
|
|
184
|
+
live.durable(snapshot.events_by_session[id] ?? []);
|
|
185
|
+
const subscription = `sdk:session:${id}`;
|
|
186
|
+
const timer = setTimeout(() => finish(new Error('Realtime handshake timed out')), 30_000);
|
|
187
|
+
const emit = () => { if (!ended)
|
|
188
|
+
options.onChange({ sessionId: id, snapshot, messages: snapshot.messages_by_session[id] ?? [], live: live.values(), status: replayed ? 'live' : 'syncing' }); };
|
|
189
|
+
const finish = (error) => {
|
|
190
|
+
if (ended)
|
|
191
|
+
return;
|
|
192
|
+
ended = true;
|
|
193
|
+
requests.abort();
|
|
194
|
+
clearTimeout(timer);
|
|
195
|
+
signal.removeEventListener('abort', cancel);
|
|
196
|
+
socket.close();
|
|
197
|
+
if (error)
|
|
198
|
+
reject(error);
|
|
199
|
+
else
|
|
200
|
+
resolve();
|
|
201
|
+
};
|
|
202
|
+
const cancel = () => finish();
|
|
203
|
+
const refresh = async () => {
|
|
204
|
+
dirty = true;
|
|
205
|
+
if (refreshing)
|
|
206
|
+
return;
|
|
207
|
+
refreshing = true;
|
|
208
|
+
try {
|
|
209
|
+
while (dirty && !ended) {
|
|
210
|
+
dirty = false;
|
|
211
|
+
const next = await this.hydrate(body, requests.signal);
|
|
212
|
+
if (ended)
|
|
213
|
+
return;
|
|
214
|
+
if (!next.sessions_by_id[id])
|
|
215
|
+
throw new FatalRealtimeError('Session no longer available');
|
|
216
|
+
snapshot = next;
|
|
217
|
+
lastSeq = Math.max(lastSeq, next.projections_by_session[id]?.last_event_seq ?? 0);
|
|
218
|
+
live.reconcile(next.messages_by_session[id] ?? []);
|
|
219
|
+
live.durable(next.events_by_session[id] ?? []);
|
|
220
|
+
emit();
|
|
221
|
+
}
|
|
222
|
+
if (readyPending && !ended) {
|
|
223
|
+
readyPending = false;
|
|
224
|
+
ready();
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
catch (e) {
|
|
228
|
+
finish(e instanceof Error ? e : new Error(String(e)));
|
|
229
|
+
}
|
|
230
|
+
finally {
|
|
231
|
+
refreshing = false;
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
signal.addEventListener('abort', cancel, { once: true });
|
|
235
|
+
socket.addEventListener('close', () => finish(new Error('Realtime disconnected')));
|
|
236
|
+
socket.addEventListener('error', () => finish(new Error('Realtime transport failed')));
|
|
237
|
+
socket.addEventListener('message', ({ data }) => {
|
|
238
|
+
if (ended)
|
|
239
|
+
return;
|
|
240
|
+
try {
|
|
241
|
+
const frame = JSON.parse(String(data));
|
|
242
|
+
if (frame.protocol !== 'v3.realtime' || frame.protocol_version !== 1)
|
|
243
|
+
throw new FatalRealtimeError('Unsupported realtime protocol');
|
|
244
|
+
if (frame.kind === 'auth.denied')
|
|
245
|
+
throw new FatalRealtimeError(frame.error ?? 'Realtime authentication denied');
|
|
246
|
+
if (frame.kind === 'cursor.error' || frame.kind === 'slow_consumer.reconnect_required')
|
|
247
|
+
throw new Error(frame.error_code ?? frame.kind);
|
|
248
|
+
if (frame.kind === 'hello') {
|
|
249
|
+
if (hello || !frame.endpoint_cursor)
|
|
250
|
+
throw new FatalRealtimeError('Invalid realtime hello');
|
|
251
|
+
hello = true;
|
|
252
|
+
liveNegotiated = frame.capabilities?.includes('live_patch_v1') ?? false;
|
|
253
|
+
// Use the server-provided snapshot handoff cursor, never the later hello head.
|
|
254
|
+
const resume = { protocol: 'v3.realtime', protocol_version: 1, kind: 'resume',
|
|
255
|
+
endpoint_cursor: initial.realtime.resume.endpoint_cursor,
|
|
256
|
+
subscriptions: [{ session_id: id, subscription_id: subscription, endpoint_cursor: initial.realtime.resume.endpoint_cursor }],
|
|
257
|
+
capabilities: liveNegotiated ? ['live_patch_v1'] : [] };
|
|
258
|
+
socket.send(JSON.stringify(resume));
|
|
259
|
+
emit();
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (!hello)
|
|
263
|
+
throw new FatalRealtimeError('Realtime frame before hello');
|
|
264
|
+
if (frame.session_id !== id)
|
|
265
|
+
return;
|
|
266
|
+
if (frame.subscription_id && frame.subscription_id !== subscription)
|
|
267
|
+
return;
|
|
268
|
+
if (frame.kind === 'replay.complete') {
|
|
269
|
+
replayed = true;
|
|
270
|
+
clearTimeout(timer);
|
|
271
|
+
readyPending = true;
|
|
272
|
+
void refresh();
|
|
273
|
+
}
|
|
274
|
+
else if (frame.kind === 'live.patch' && frame.live?.session_id === id) {
|
|
275
|
+
if (!liveNegotiated || !replayed)
|
|
276
|
+
return;
|
|
277
|
+
if (!live.accept(frame.live)) {
|
|
278
|
+
finish(new Error('Live stream gap; durable rehydration required'));
|
|
279
|
+
}
|
|
280
|
+
else
|
|
281
|
+
emit();
|
|
282
|
+
}
|
|
283
|
+
else if (frame.kind === 'event' && frame.event?.session_id === id) {
|
|
284
|
+
if (frame.event.seq <= lastSeq)
|
|
285
|
+
return;
|
|
286
|
+
lastSeq = frame.event.seq;
|
|
287
|
+
live.durable([frame.event]);
|
|
288
|
+
emit();
|
|
289
|
+
void refresh();
|
|
290
|
+
}
|
|
291
|
+
else if (frame.kind === 'projection.high_watermark') {
|
|
292
|
+
void refresh();
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
catch (e) {
|
|
296
|
+
finish(e instanceof Error ? e : new Error(String(e)));
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
class FatalRealtimeError extends Error {
|
|
303
|
+
}
|
|
304
|
+
function delay(ms, signal) {
|
|
305
|
+
return new Promise((resolve) => {
|
|
306
|
+
const finish = () => { clearTimeout(timer); signal.removeEventListener('abort', finish); resolve(); };
|
|
307
|
+
const timer = setTimeout(finish, ms);
|
|
308
|
+
signal.addEventListener('abort', finish, { once: true });
|
|
309
|
+
if (signal.aborted)
|
|
310
|
+
finish();
|
|
311
|
+
});
|
|
312
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { SwarmTransport } from './transport.js';
|
|
2
|
+
import type { CreateSessionParams, ListSessionsParams, SessionDetail, SessionRecord } from './types.js';
|
|
3
|
+
export declare class SwarmSessionsNamespace {
|
|
4
|
+
private transport;
|
|
5
|
+
constructor(transport: SwarmTransport);
|
|
6
|
+
/**
|
|
7
|
+
* Creates a new durable V3 session.
|
|
8
|
+
*/
|
|
9
|
+
create(params: CreateSessionParams): Promise<SessionRecord>;
|
|
10
|
+
/**
|
|
11
|
+
* Lists durable sessions for the active account.
|
|
12
|
+
*/
|
|
13
|
+
list(params?: ListSessionsParams): Promise<SessionRecord[]>;
|
|
14
|
+
/**
|
|
15
|
+
* Retrieves full details, history, and state for a single session.
|
|
16
|
+
*/
|
|
17
|
+
get(sessionId: string): Promise<SessionDetail>;
|
|
18
|
+
/** Approve one exact pending tool call; no persistent rule or bypass is created. */
|
|
19
|
+
approvePermissionOnce(sessionId: string, permissionId: string, reason?: string): Promise<{
|
|
20
|
+
ok: boolean;
|
|
21
|
+
}>;
|
|
22
|
+
/** Resolve one exact pending tool permission; never changes account-wide policy. */
|
|
23
|
+
denyPermission(sessionId: string, permissionId: string, reason?: string): Promise<{
|
|
24
|
+
ok: boolean;
|
|
25
|
+
}>;
|
|
26
|
+
/**
|
|
27
|
+
* Archives a session.
|
|
28
|
+
*/
|
|
29
|
+
archive(sessionId: string): Promise<boolean>;
|
|
30
|
+
/**
|
|
31
|
+
* Unarchives a previously archived session.
|
|
32
|
+
*/
|
|
33
|
+
unarchive(sessionId: string): Promise<boolean>;
|
|
34
|
+
/**
|
|
35
|
+
* Deletes a session and triggers cascading cancellation of associated runs and worktrees.
|
|
36
|
+
*/
|
|
37
|
+
delete(sessionId: string): Promise<boolean>;
|
|
38
|
+
/**
|
|
39
|
+
* Appends a message to a session conversation.
|
|
40
|
+
*/
|
|
41
|
+
sendMessage(sessionId: string, params: {
|
|
42
|
+
content: string;
|
|
43
|
+
role?: 'user' | 'assistant' | 'system';
|
|
44
|
+
client_request_id?: string;
|
|
45
|
+
}): Promise<any>;
|
|
46
|
+
/**
|
|
47
|
+
* Stops an active execution run for a session.
|
|
48
|
+
*/
|
|
49
|
+
stopRun(sessionId: string, params: {
|
|
50
|
+
run_id: string;
|
|
51
|
+
target_swarm_id?: string;
|
|
52
|
+
}): Promise<boolean>;
|
|
53
|
+
/**
|
|
54
|
+
* Helper that polls a session until its active run completes, or times out.
|
|
55
|
+
*/
|
|
56
|
+
waitForRun(sessionId: string, options?: {
|
|
57
|
+
timeoutMs?: number;
|
|
58
|
+
pollIntervalMs?: number;
|
|
59
|
+
}): Promise<SessionDetail>;
|
|
60
|
+
}
|