@love-moon/app-sdk 0.3.2

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.
@@ -0,0 +1,1387 @@
1
+ // src/types/errors.ts
2
+ var ConductorAppError = class extends Error {
3
+ name = "ConductorAppError";
4
+ code;
5
+ status;
6
+ details;
7
+ requestId;
8
+ constructor(args) {
9
+ super(args.message, args.cause ? { cause: args.cause } : void 0);
10
+ this.code = args.code;
11
+ this.status = args.status;
12
+ this.details = args.details;
13
+ this.requestId = args.requestId;
14
+ }
15
+ };
16
+ function isConductorAppError(error) {
17
+ return typeof error === "object" && error !== null && error.name === "ConductorAppError";
18
+ }
19
+
20
+ // src/types/error-codes.ts
21
+ function extractErrorMarker(details, message) {
22
+ if (details && typeof details === "object" && "error" in details) {
23
+ const raw = details.error;
24
+ if (typeof raw === "string") return raw.toLowerCase();
25
+ }
26
+ if (message) return message.toLowerCase();
27
+ return "";
28
+ }
29
+ function mapHttpStatusToErrorCode(status, details, message) {
30
+ const marker = extractErrorMarker(details, message);
31
+ if (status === 401) return "unauthorized";
32
+ if (status === 403) return "forbidden";
33
+ if (status === 404) {
34
+ if (marker.includes("project")) return "project_not_found";
35
+ if (marker.includes("task")) return "task_not_found";
36
+ if (marker.includes("message")) return "message_not_found";
37
+ return "task_not_found";
38
+ }
39
+ if (status === 408) return "timeout";
40
+ if (status === 409) {
41
+ if (marker.includes("fire owner")) return "task_fire_owner_offline";
42
+ if (marker.includes("task_type")) return "task_type_not_messageable";
43
+ if (marker.includes("not_running") || marker.includes("not running"))
44
+ return "task_not_running";
45
+ if (marker.includes("daemon") || marker.includes("offline")) return "daemon_offline";
46
+ if (marker.includes("binding")) return "binding_validation_failed";
47
+ return "binding_validation_failed";
48
+ }
49
+ if (status === 422) return "invalid_input";
50
+ if (status === 429) return "rate_limited";
51
+ if (status >= 500) return "server_error";
52
+ if (status >= 400) return "invalid_input";
53
+ return "server_error";
54
+ }
55
+
56
+ // src/index.ts
57
+ var SDK_VERSION = "0.3.2";
58
+ var SDK_NAME = "@love-moon/app-sdk";
59
+ var SDK_USER_AGENT = `conductor-app-sdk/${SDK_VERSION}`;
60
+
61
+ // src/server/fetcher.ts
62
+ var Fetcher = class {
63
+ constructor(options) {
64
+ this.options = options;
65
+ this.fetchImpl = options.fetch ?? globalThis.fetch;
66
+ if (typeof this.fetchImpl !== "function") {
67
+ throw new ConductorAppError({
68
+ code: "invalid_input",
69
+ message: "No fetch implementation found. Provide options.fetch (Node \u226518 has globalThis.fetch built in)."
70
+ });
71
+ }
72
+ this.defaultTimeoutMs = options.timeoutMs ?? 3e4;
73
+ }
74
+ options;
75
+ fetchImpl;
76
+ defaultTimeoutMs;
77
+ async request(req) {
78
+ const url = this.buildUrl(req.path, req.query);
79
+ const token = await this.resolveToken();
80
+ const headers = {
81
+ Authorization: `Bearer ${token}`,
82
+ Accept: "application/json",
83
+ "User-Agent": SDK_USER_AGENT,
84
+ ...req.headers ?? {}
85
+ };
86
+ const bodyType = req.bodyType ?? "json";
87
+ let body;
88
+ if (req.body !== void 0 && req.body !== null) {
89
+ if (bodyType === "json") {
90
+ headers["Content-Type"] ??= "application/json";
91
+ body = JSON.stringify(req.body);
92
+ } else {
93
+ body = req.body;
94
+ }
95
+ }
96
+ const timeoutMs = req.timeoutMs ?? this.defaultTimeoutMs;
97
+ const composedSignal = composeAbortSignals(req.signal, timeoutMs);
98
+ let response;
99
+ try {
100
+ response = await this.fetchImpl(url, {
101
+ method: req.method ?? "GET",
102
+ headers,
103
+ body,
104
+ signal: composedSignal.signal
105
+ });
106
+ } catch (cause) {
107
+ composedSignal.dispose();
108
+ if (isAbortError(cause)) {
109
+ throw new ConductorAppError({
110
+ code: composedSignal.timedOut ? "timeout" : "stream_aborted",
111
+ message: composedSignal.timedOut ? `Request timed out after ${timeoutMs}ms: ${req.method ?? "GET"} ${req.path}` : `Request aborted: ${req.method ?? "GET"} ${req.path}`,
112
+ cause
113
+ });
114
+ }
115
+ throw new ConductorAppError({
116
+ code: "network_error",
117
+ message: `Network error on ${req.method ?? "GET"} ${req.path}: ${cause?.message ?? String(cause)}`,
118
+ cause
119
+ });
120
+ }
121
+ composedSignal.dispose();
122
+ if (response.status === 401) {
123
+ try {
124
+ this.options.onUnauthorized?.();
125
+ } catch {
126
+ }
127
+ }
128
+ if (response.status === 404 && req.resolveOn404) {
129
+ return void 0;
130
+ }
131
+ if (!response.ok) {
132
+ const { details, message } = await readErrorBody(response);
133
+ const code = mapHttpStatusToErrorCode(response.status, details, message);
134
+ throw new ConductorAppError({
135
+ code,
136
+ status: response.status,
137
+ message: message || `Conductor returned ${response.status} on ${req.method ?? "GET"} ${req.path}`,
138
+ details,
139
+ requestId: response.headers.get("x-request-id") ?? void 0
140
+ });
141
+ }
142
+ if (response.status === 204) {
143
+ return void 0;
144
+ }
145
+ const text = await response.text();
146
+ if (!text) {
147
+ return void 0;
148
+ }
149
+ try {
150
+ return JSON.parse(text);
151
+ } catch (cause) {
152
+ throw new ConductorAppError({
153
+ code: "server_error",
154
+ message: `Conductor returned non-JSON body on ${req.method ?? "GET"} ${req.path}`,
155
+ details: { rawBody: text.slice(0, 1024) },
156
+ cause
157
+ });
158
+ }
159
+ }
160
+ buildUrl(path, query) {
161
+ const base = this.options.baseUrl.replace(/\/+$/, "");
162
+ const cleanPath = path.startsWith("/") ? path : `/${path}`;
163
+ const url = `${base}${cleanPath}`;
164
+ if (!query) return url;
165
+ const params = new URLSearchParams();
166
+ for (const [key, value] of Object.entries(query)) {
167
+ if (value === void 0 || value === null) continue;
168
+ params.append(key, String(value));
169
+ }
170
+ const queryString = params.toString();
171
+ return queryString ? `${url}?${queryString}` : url;
172
+ }
173
+ async resolveToken() {
174
+ const { bearerToken } = this.options;
175
+ if (typeof bearerToken === "string") return bearerToken;
176
+ const resolved = await bearerToken();
177
+ if (!resolved) {
178
+ throw new ConductorAppError({
179
+ code: "unauthorized",
180
+ message: "bearerToken provider returned empty string"
181
+ });
182
+ }
183
+ return resolved;
184
+ }
185
+ };
186
+ function composeAbortSignals(external, timeoutMs) {
187
+ const controller = new AbortController();
188
+ let timedOut = false;
189
+ let timer = null;
190
+ if (timeoutMs > 0) {
191
+ timer = setTimeout(() => {
192
+ timedOut = true;
193
+ controller.abort();
194
+ }, timeoutMs);
195
+ }
196
+ const onExternalAbort = () => controller.abort();
197
+ if (external) {
198
+ if (external.aborted) {
199
+ controller.abort();
200
+ } else {
201
+ external.addEventListener("abort", onExternalAbort);
202
+ }
203
+ }
204
+ const dispose = () => {
205
+ if (timer) clearTimeout(timer);
206
+ if (external) external.removeEventListener("abort", onExternalAbort);
207
+ };
208
+ return {
209
+ signal: controller.signal,
210
+ dispose,
211
+ get timedOut() {
212
+ return timedOut;
213
+ }
214
+ };
215
+ }
216
+ function isAbortError(error) {
217
+ if (typeof error !== "object" || error === null) return false;
218
+ const name = error.name;
219
+ return name === "AbortError";
220
+ }
221
+ async function readErrorBody(response) {
222
+ const text = await response.text().catch(() => "");
223
+ if (!text) return { details: null, message: null };
224
+ try {
225
+ const parsed = JSON.parse(text);
226
+ const message = typeof parsed.error === "string" ? parsed.error : typeof parsed.message === "string" ? parsed.message : null;
227
+ return { details: parsed, message };
228
+ } catch {
229
+ return { details: { rawBody: text.slice(0, 1024) }, message: null };
230
+ }
231
+ }
232
+
233
+ // src/server/normalize.ts
234
+ var str = (value) => {
235
+ if (typeof value !== "string") return null;
236
+ const trimmed = value.trim();
237
+ return trimmed || null;
238
+ };
239
+ var num = (value) => {
240
+ if (typeof value !== "number" || !Number.isFinite(value)) return null;
241
+ return value;
242
+ };
243
+ var bool = (value) => Boolean(value);
244
+ var pick = (raw, ...keys) => {
245
+ for (const key of keys) {
246
+ if (Object.prototype.hasOwnProperty.call(raw, key)) return raw[key];
247
+ }
248
+ return void 0;
249
+ };
250
+ var isoDate = (value) => {
251
+ if (typeof value === "string") return value;
252
+ if (value instanceof Date) return value.toISOString();
253
+ return (/* @__PURE__ */ new Date(0)).toISOString();
254
+ };
255
+ function normalizeProject(raw) {
256
+ const r = raw ?? {};
257
+ const metadata = pick(r, "metadata") ?? null;
258
+ const ownership = metadata && typeof metadata === "object" ? metadata.ownership : void 0;
259
+ const audit = metadata && typeof metadata === "object" ? metadata.audit : void 0;
260
+ const createdByApp = Boolean(ownership && ownership.kind === "app") || Boolean(audit && audit.createdByApp);
261
+ return {
262
+ id: str(pick(r, "id")) ?? "",
263
+ name: str(pick(r, "name")) ?? "",
264
+ daemonHost: str(pick(r, "daemonHost", "daemon_host")),
265
+ workspacePath: str(pick(r, "workspacePath", "workspace_path")),
266
+ repoRoot: str(pick(r, "repoRoot", "repo_root")),
267
+ worktreeBranch: str(pick(r, "worktreeBranch", "worktree_branch")),
268
+ lastCommit: str(pick(r, "lastCommit", "last_commit")),
269
+ lastCommitAt: str(pick(r, "lastCommitAt", "last_commit_at")),
270
+ fileCount: num(pick(r, "fileCount", "file_count")),
271
+ isDefault: bool(pick(r, "isDefault", "is_default")),
272
+ createdByApp,
273
+ createdAt: isoDate(pick(r, "createdAt", "created_at")),
274
+ updatedAt: isoDate(pick(r, "updatedAt", "updated_at"))
275
+ };
276
+ }
277
+ function normalizeTask(raw) {
278
+ const r = raw ?? {};
279
+ return {
280
+ id: str(pick(r, "id")) ?? "",
281
+ projectId: str(pick(r, "projectId", "project_id")) ?? "",
282
+ title: str(pick(r, "title")) ?? "",
283
+ status: str(pick(r, "status")) ?? "pending",
284
+ backendType: str(pick(r, "backendType", "backend_type")),
285
+ sessionId: str(pick(r, "sessionId", "session_id")),
286
+ sessionFilePath: str(pick(r, "sessionFilePath", "session_file_path")),
287
+ createdAt: isoDate(pick(r, "createdAt", "created_at")),
288
+ updatedAt: isoDate(pick(r, "updatedAt", "updated_at"))
289
+ };
290
+ }
291
+ function normalizeMessage(raw) {
292
+ const r = raw ?? {};
293
+ const metadata = pick(r, "metadata");
294
+ const rawAttachments = pick(r, "attachments");
295
+ const attachments = Array.isArray(rawAttachments) ? rawAttachments.map((a) => normalizeAttachment(a)) : [];
296
+ return {
297
+ id: str(pick(r, "id")) ?? "",
298
+ taskId: str(pick(r, "taskId", "task_id")) ?? "",
299
+ role: str(pick(r, "role")) ?? "sdk",
300
+ content: typeof r.content === "string" ? r.content : "",
301
+ metadata: metadata && typeof metadata === "object" && !Array.isArray(metadata) ? metadata : null,
302
+ attachments,
303
+ createdAt: isoDate(pick(r, "createdAt", "created_at"))
304
+ };
305
+ }
306
+ function normalizeAttachment(raw) {
307
+ const r = raw ?? {};
308
+ return {
309
+ id: str(pick(r, "id")) ?? "",
310
+ filename: str(pick(r, "filename", "name")) ?? "",
311
+ mimeType: str(pick(r, "mimeType", "mime_type", "contentType", "content_type")) ?? "application/octet-stream",
312
+ sizeBytes: num(pick(r, "sizeBytes", "size_bytes", "size")) ?? 0,
313
+ url: str(pick(r, "url")) ?? ""
314
+ };
315
+ }
316
+
317
+ // src/server/http/projects.ts
318
+ var ProjectsApi = class {
319
+ /** @internal — constructed by `AppClient`; not part of the public surface. */
320
+ constructor(fetcher) {
321
+ this.fetcher = fetcher;
322
+ }
323
+ fetcher;
324
+ /**
325
+ * Idempotent find-or-create on (daemonHost, workspacePath).
326
+ *
327
+ * Flow:
328
+ * 1. POST `/api/projects/match-path` with `{ daemonHost, path: workspacePath }`.
329
+ * If a project matches (or is a parent of) the path, return it.
330
+ * 2. Otherwise POST `/api/projects` with binding fields. The server
331
+ * validates the binding with the user's daemon. Success returns the
332
+ * new project; daemon-offline returns 409 → mapped to `daemon_offline`.
333
+ *
334
+ * The created project carries `metadata.audit.createdByApp` so it can be
335
+ * distinguished in the main Conductor UI later. No new schema is required.
336
+ */
337
+ async bind(input, opts) {
338
+ const name = input.name?.trim();
339
+ const daemonHost = input.daemonHost?.trim();
340
+ const workspacePath = input.workspacePath?.trim();
341
+ if (!name || !daemonHost || !workspacePath) {
342
+ throw new ConductorAppError({
343
+ code: "invalid_input",
344
+ message: "projects.bind requires non-empty name, daemonHost, and workspacePath"
345
+ });
346
+ }
347
+ const match = await this.fetcher.request({
348
+ method: "POST",
349
+ path: "/api/projects/match-path",
350
+ body: { daemonHost, path: workspacePath },
351
+ signal: opts?.signal
352
+ });
353
+ if (match?.project) {
354
+ return normalizeProject(match.project);
355
+ }
356
+ const created = await this.fetcher.request({
357
+ method: "POST",
358
+ path: "/api/projects",
359
+ body: {
360
+ name,
361
+ daemonHost,
362
+ workspacePath,
363
+ metadata: {
364
+ audit: {
365
+ createdByApp: {
366
+ name: input.appLabel ?? name,
367
+ sdkName: SDK_NAME,
368
+ sdkVersion: SDK_VERSION
369
+ }
370
+ }
371
+ }
372
+ },
373
+ signal: opts?.signal
374
+ });
375
+ return normalizeProject(created);
376
+ }
377
+ async list(opts) {
378
+ const raw = await this.fetcher.request({
379
+ method: "GET",
380
+ path: "/api/projects",
381
+ signal: opts?.signal
382
+ });
383
+ const list = Array.isArray(raw) ? raw : Array.isArray(raw.projects) ? raw.projects : [];
384
+ return list.map((entry) => normalizeProject(entry));
385
+ }
386
+ async get(projectId, opts) {
387
+ if (!projectId) {
388
+ throw new ConductorAppError({
389
+ code: "invalid_input",
390
+ message: "projects.get requires a projectId"
391
+ });
392
+ }
393
+ const raw = await this.fetcher.request({
394
+ method: "GET",
395
+ path: "/api/projects",
396
+ query: { projectId },
397
+ signal: opts?.signal
398
+ });
399
+ if (raw && typeof raw === "object" && "projects" in raw) {
400
+ const list = raw.projects ?? [];
401
+ if (list.length === 0) {
402
+ throw new ConductorAppError({
403
+ code: "project_not_found",
404
+ message: `Project ${projectId} not found`
405
+ });
406
+ }
407
+ return normalizeProject(list[0]);
408
+ }
409
+ return normalizeProject(raw);
410
+ }
411
+ };
412
+
413
+ // src/server/id.ts
414
+ function generateRequestId() {
415
+ const c = globalThis.crypto;
416
+ if (c?.randomUUID) return c.randomUUID();
417
+ if (c?.getRandomValues) {
418
+ const bytes = new Uint8Array(16);
419
+ c.getRandomValues(bytes);
420
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
421
+ }
422
+ return Math.random().toString(36).slice(2) + Date.now().toString(36);
423
+ }
424
+
425
+ // src/server/http/tasks.ts
426
+ var TasksRestApi = class {
427
+ constructor(fetcher) {
428
+ this.fetcher = fetcher;
429
+ }
430
+ fetcher;
431
+ async create(input, opts) {
432
+ if (!input.projectId) {
433
+ throw new ConductorAppError({
434
+ code: "invalid_input",
435
+ message: "tasks.create requires projectId"
436
+ });
437
+ }
438
+ const trimmedTitle = input.title?.trim() ?? "";
439
+ if (!trimmedTitle) {
440
+ throw new ConductorAppError({
441
+ code: "invalid_input",
442
+ message: "tasks.create requires title"
443
+ });
444
+ }
445
+ const raw = await this.fetcher.request({
446
+ method: "POST",
447
+ path: "/api/tasks",
448
+ body: {
449
+ project_id: input.projectId,
450
+ title: trimmedTitle,
451
+ ...input.initialMessage ? { initial_content: input.initialMessage } : {},
452
+ ...input.backendType ? { backend_type: input.backendType } : {},
453
+ ...input.metadata ? { metadata: input.metadata } : {},
454
+ task_type: "ai_task"
455
+ },
456
+ signal: opts?.signal
457
+ });
458
+ return normalizeTask(raw);
459
+ }
460
+ async get(taskId, opts) {
461
+ if (!taskId) {
462
+ throw new ConductorAppError({
463
+ code: "invalid_input",
464
+ message: "tasks.get requires a taskId"
465
+ });
466
+ }
467
+ const raw = await this.fetcher.request({
468
+ method: "GET",
469
+ path: `/api/tasks/${encodeURIComponent(taskId)}`,
470
+ signal: opts?.signal
471
+ });
472
+ return normalizeTask(raw);
473
+ }
474
+ async list(filter = {}, opts) {
475
+ const raw = await this.fetcher.request({
476
+ method: "GET",
477
+ path: "/api/tasks",
478
+ query: {
479
+ ...filter.projectId ? { project_id: filter.projectId } : {},
480
+ ...filter.status ? { status: filter.status } : {}
481
+ },
482
+ signal: opts?.signal
483
+ });
484
+ const list = Array.isArray(raw) ? raw : Array.isArray(raw.tasks) ? raw.tasks : [];
485
+ return list.map((entry) => normalizeTask(entry));
486
+ }
487
+ async sendMessage(taskId, input, opts) {
488
+ if (!taskId) {
489
+ throw new ConductorAppError({
490
+ code: "invalid_input",
491
+ message: "tasks.sendMessage requires a taskId"
492
+ });
493
+ }
494
+ const normalized = typeof input === "string" ? { content: input } : input;
495
+ if (!normalized.content || !normalized.content.trim()) {
496
+ throw new ConductorAppError({
497
+ code: "invalid_input",
498
+ message: "tasks.sendMessage requires non-empty content"
499
+ });
500
+ }
501
+ const clientRequestId = normalized.clientRequestId ?? generateRequestId();
502
+ const raw = await this.fetcher.request({
503
+ method: "POST",
504
+ path: `/api/tasks/${encodeURIComponent(taskId)}/messages`,
505
+ body: {
506
+ content: normalized.content,
507
+ role: normalized.role ?? "sdk",
508
+ clientRequestId,
509
+ metadata: buildOutboundMetadata(normalized.metadata)
510
+ },
511
+ signal: opts?.signal
512
+ });
513
+ return normalizeMessage(raw);
514
+ }
515
+ /**
516
+ * Paginated history of messages for a task. Returns newest page first
517
+ * (most recent messages have largest createdAt).
518
+ */
519
+ async history(taskId, paging = {}, opts) {
520
+ if (!taskId) {
521
+ throw new ConductorAppError({
522
+ code: "invalid_input",
523
+ message: "tasks.history requires a taskId"
524
+ });
525
+ }
526
+ const raw = await this.fetcher.request({
527
+ method: "GET",
528
+ path: `/api/tasks/${encodeURIComponent(taskId)}/messages`,
529
+ query: {
530
+ pagination: "1",
531
+ ...paging.beforeId ? { before_id: paging.beforeId } : {},
532
+ ...paging.limit ? { limit: String(paging.limit) } : {}
533
+ },
534
+ signal: opts?.signal
535
+ });
536
+ if (!raw || typeof raw !== "object") {
537
+ return { messages: [], hasMoreBefore: false, oldestMessageId: null };
538
+ }
539
+ const messages = Array.isArray(raw.messages) ? raw.messages.map((entry) => normalizeMessage(entry)) : [];
540
+ const pagination = raw.pagination ?? {};
541
+ return {
542
+ messages,
543
+ hasMoreBefore: Boolean(pagination.has_more_before),
544
+ oldestMessageId: typeof pagination.oldest_message_id === "string" ? pagination.oldest_message_id : null
545
+ };
546
+ }
547
+ async interrupt(taskId, opts) {
548
+ if (!taskId) {
549
+ throw new ConductorAppError({
550
+ code: "invalid_input",
551
+ message: "tasks.interrupt requires a taskId"
552
+ });
553
+ }
554
+ if (!opts.targetReplyTo) {
555
+ throw new ConductorAppError({
556
+ code: "invalid_input",
557
+ message: "tasks.interrupt requires targetReplyTo"
558
+ });
559
+ }
560
+ await this.fetcher.request({
561
+ method: "POST",
562
+ path: `/api/tasks/${encodeURIComponent(taskId)}/interrupt`,
563
+ body: { target_reply_to: opts.targetReplyTo },
564
+ signal: opts.signal
565
+ });
566
+ }
567
+ };
568
+ function buildOutboundMetadata(callerMetadata) {
569
+ const base = callerMetadata && typeof callerMetadata === "object" ? { ...callerMetadata } : {};
570
+ const callerAudit = base.audit && typeof base.audit === "object" && !Array.isArray(base.audit) ? { ...base.audit } : {};
571
+ delete callerAudit.actor;
572
+ delete callerAudit.sdkName;
573
+ delete callerAudit.sdkVersion;
574
+ return {
575
+ ...base,
576
+ audit: {
577
+ // Caller-supplied audit fields first…
578
+ ...callerAudit,
579
+ // …then SDK fields LAST so they always win.
580
+ actor: "app",
581
+ sdkName: SDK_NAME,
582
+ sdkVersion: SDK_VERSION
583
+ }
584
+ };
585
+ }
586
+
587
+ // src/server/ws/socket.ts
588
+ import WebSocket from "ws";
589
+ var AppWebSocket = class {
590
+ constructor(options) {
591
+ this.options = options;
592
+ }
593
+ options;
594
+ socket = null;
595
+ reconnectAttempts = 0;
596
+ closed = false;
597
+ reconnectTimer = null;
598
+ rawListeners = /* @__PURE__ */ new Set();
599
+ stateListeners = /* @__PURE__ */ new Set();
600
+ closeListeners = /* @__PURE__ */ new Set();
601
+ currentState = "offline";
602
+ /** Resolved when at least one connection has opened successfully. */
603
+ readyPromise = null;
604
+ readyResolver = null;
605
+ readyRejector = null;
606
+ /** Tracks whether we've ever opened a connection — gates state replay. */
607
+ hasEverConnected = false;
608
+ onEnvelope(listener) {
609
+ this.rawListeners.add(listener);
610
+ return {
611
+ unsubscribe: () => {
612
+ this.rawListeners.delete(listener);
613
+ }
614
+ };
615
+ }
616
+ /**
617
+ * Subscribe to the terminal "client closed" event. Fires exactly once when
618
+ * `close()` is invoked (or zero times if the socket is GC'd without close).
619
+ *
620
+ * Iterators that have already passed `connect()` need this signal — the
621
+ * regular `rawListeners` are cleared at close time, so the inner
622
+ * `subscribeToTask` loop would otherwise wedge waiting on `next()`. Late
623
+ * subscribers (after `close()` has run) get fired synchronously so they
624
+ * can't miss the signal.
625
+ *
626
+ * Returns an unsubscribe callback. Idempotent.
627
+ */
628
+ onClose(listener) {
629
+ if (this.closed) {
630
+ try {
631
+ listener();
632
+ } catch {
633
+ }
634
+ return () => void 0;
635
+ }
636
+ this.closeListeners.add(listener);
637
+ return () => {
638
+ this.closeListeners.delete(listener);
639
+ };
640
+ }
641
+ onConnectionState(listener) {
642
+ if (this.hasEverConnected || this.currentState !== "offline") {
643
+ try {
644
+ listener(this.currentState);
645
+ } catch {
646
+ }
647
+ }
648
+ this.stateListeners.add(listener);
649
+ return {
650
+ unsubscribe: () => {
651
+ this.stateListeners.delete(listener);
652
+ }
653
+ };
654
+ }
655
+ /**
656
+ * Open the connection (idempotent). Returns when the first OPEN event fires.
657
+ *
658
+ * The returned promise rejects only if the *initial* connection attempt
659
+ * fails (e.g. the token provider throws). Once we've ever connected, the
660
+ * promise resolves and subsequent disconnects are handled by the
661
+ * auto-reconnect loop (visible via `onConnectionState`).
662
+ */
663
+ async connect() {
664
+ if (this.closed) {
665
+ throw new ConductorAppError({
666
+ code: "subscribe_failed",
667
+ message: "AppWebSocket was closed; create a new one."
668
+ });
669
+ }
670
+ if (this.socket && this.socket.readyState === WebSocket.OPEN) return;
671
+ if (this.readyPromise) return this.readyPromise;
672
+ const readyPromise = new Promise((resolve, reject) => {
673
+ this.readyResolver = resolve;
674
+ this.readyRejector = reject;
675
+ });
676
+ readyPromise.catch(() => void 0);
677
+ this.readyPromise = readyPromise;
678
+ try {
679
+ await this.openOnce();
680
+ } catch (cause) {
681
+ const rejector = this.readyRejector;
682
+ this.readyPromise = null;
683
+ this.readyResolver = null;
684
+ this.readyRejector = null;
685
+ const error = cause instanceof ConductorAppError ? cause : new ConductorAppError({
686
+ code: "subscribe_failed",
687
+ message: `Failed to open WebSocket: ${cause?.message ?? String(cause)}`,
688
+ cause
689
+ });
690
+ if (rejector) rejector(error);
691
+ throw error;
692
+ }
693
+ return readyPromise;
694
+ }
695
+ close() {
696
+ if (this.closed) return;
697
+ const pendingRejector = this.readyRejector;
698
+ const closeListenersSnapshot = Array.from(this.closeListeners);
699
+ this.closed = true;
700
+ this.rawListeners.clear();
701
+ this.stateListeners.clear();
702
+ this.closeListeners.clear();
703
+ if (this.reconnectTimer) {
704
+ clearTimeout(this.reconnectTimer);
705
+ this.reconnectTimer = null;
706
+ }
707
+ if (this.socket) {
708
+ try {
709
+ this.socket.close();
710
+ } catch {
711
+ }
712
+ }
713
+ this.socket = null;
714
+ this.readyPromise = null;
715
+ this.readyResolver = null;
716
+ this.readyRejector = null;
717
+ this.setState("offline");
718
+ if (pendingRejector) {
719
+ pendingRejector(
720
+ new ConductorAppError({
721
+ code: "subscribe_failed",
722
+ message: "client closed before connect resolved"
723
+ })
724
+ );
725
+ }
726
+ for (const listener of closeListenersSnapshot) {
727
+ try {
728
+ listener();
729
+ } catch {
730
+ }
731
+ }
732
+ }
733
+ async openOnce() {
734
+ const url = await this.buildUrl();
735
+ const Ctor = this.options.webSocketImpl ?? WebSocket;
736
+ const socket = new Ctor(url);
737
+ this.socket = socket;
738
+ socket.addEventListener("open", this.handleOpen);
739
+ socket.addEventListener("message", this.handleMessage);
740
+ socket.addEventListener("close", this.handleClose);
741
+ socket.addEventListener("error", this.handleError);
742
+ }
743
+ handleOpen = () => {
744
+ this.reconnectAttempts = 0;
745
+ this.hasEverConnected = true;
746
+ this.setState("connected");
747
+ if (this.readyResolver) {
748
+ this.readyResolver();
749
+ this.readyResolver = null;
750
+ this.readyRejector = null;
751
+ }
752
+ };
753
+ handleMessage = (event) => {
754
+ const data = event.data;
755
+ let text;
756
+ if (typeof data === "string") {
757
+ text = data;
758
+ } else if (data instanceof Buffer) {
759
+ text = data.toString("utf8");
760
+ } else if (data instanceof ArrayBuffer) {
761
+ text = new TextDecoder().decode(data);
762
+ } else {
763
+ return;
764
+ }
765
+ let envelope;
766
+ try {
767
+ envelope = JSON.parse(text);
768
+ } catch {
769
+ return;
770
+ }
771
+ for (const listener of this.rawListeners) {
772
+ try {
773
+ listener(envelope);
774
+ } catch {
775
+ }
776
+ }
777
+ };
778
+ handleClose = () => {
779
+ this.socket = null;
780
+ if (this.closed) return;
781
+ const pendingRejector = this.readyRejector;
782
+ this.readyPromise = null;
783
+ this.readyResolver = null;
784
+ this.readyRejector = null;
785
+ if (pendingRejector) {
786
+ pendingRejector(
787
+ new ConductorAppError({
788
+ code: "subscribe_failed",
789
+ message: "socket closed before open"
790
+ })
791
+ );
792
+ }
793
+ this.scheduleReconnect();
794
+ };
795
+ handleError = () => {
796
+ };
797
+ scheduleReconnect() {
798
+ const max = this.options.maxReconnects ?? Number.POSITIVE_INFINITY;
799
+ if (this.reconnectAttempts >= max) {
800
+ this.setState("offline");
801
+ this.closed = true;
802
+ return;
803
+ }
804
+ this.setState("reconnecting");
805
+ const backoff = computeBackoff(
806
+ this.reconnectAttempts,
807
+ this.options.initialBackoffMs ?? 250,
808
+ this.options.maxBackoffMs ?? 3e4
809
+ );
810
+ this.reconnectAttempts += 1;
811
+ this.reconnectTimer = setTimeout(() => {
812
+ this.reconnectTimer = null;
813
+ if (this.closed) return;
814
+ void this.openOnce().catch(() => this.scheduleReconnect());
815
+ }, backoff);
816
+ }
817
+ setState(state) {
818
+ if (this.currentState === state) return;
819
+ this.currentState = state;
820
+ for (const listener of this.stateListeners) {
821
+ try {
822
+ listener(state);
823
+ } catch {
824
+ }
825
+ }
826
+ }
827
+ async buildUrl() {
828
+ const token = typeof this.options.bearerToken === "string" ? this.options.bearerToken : await this.options.bearerToken();
829
+ const parsed = new URL(this.options.baseUrl);
830
+ parsed.protocol = parsed.protocol === "https:" ? "wss:" : "ws:";
831
+ const pathname = parsed.pathname.replace(/\/+$/, "");
832
+ parsed.pathname = `${pathname}/ws/app`;
833
+ parsed.search = `?token=${encodeURIComponent(token)}`;
834
+ return parsed.toString();
835
+ }
836
+ };
837
+ function computeBackoff(attempt, initial, max) {
838
+ const exp = Math.min(initial * 2 ** attempt, max);
839
+ const jittered = Math.floor(Math.random() * exp);
840
+ const floor = Math.floor(initial / 2);
841
+ return Math.max(jittered, floor);
842
+ }
843
+
844
+ // src/server/ws/envelope.ts
845
+ function envelopeToChatEvent(raw, filterTaskId) {
846
+ if (!raw || typeof raw !== "object") return null;
847
+ const envelope = raw;
848
+ const type = envelope.type;
849
+ const payload = envelope.payload ?? {};
850
+ const taskId = readString(payload.task_id ?? payload.taskId);
851
+ if (filterTaskId && taskId !== filterTaskId) {
852
+ return null;
853
+ }
854
+ switch (type) {
855
+ case "task_user_message":
856
+ case "task_sdk_message": {
857
+ const message = normalizeMessage({ ...payload, taskId });
858
+ if (!message.id) return null;
859
+ return { type: "message_appended", message };
860
+ }
861
+ case "task_status_update": {
862
+ if (!taskId) return null;
863
+ const status = readString(payload.status);
864
+ if (status === "finished" || status === "completed" || status === "done") {
865
+ return { type: "task_finished", taskId };
866
+ }
867
+ if (status === "failed" || status === "errored") {
868
+ return {
869
+ type: "task_failed",
870
+ taskId,
871
+ error: {
872
+ code: "task_failed",
873
+ message: readString(payload.summary) ?? "Task failed"
874
+ }
875
+ };
876
+ }
877
+ return null;
878
+ }
879
+ case "task_runtime_status": {
880
+ if (!taskId) return null;
881
+ const status = {
882
+ taskId,
883
+ state: readString(payload.state) ?? "idle",
884
+ phase: readString(payload.phase),
885
+ source: readString(payload.source),
886
+ statusLine: readString(payload.status_line ?? payload.statusLine),
887
+ statusDoneLine: readString(payload.status_done_line ?? payload.statusDoneLine),
888
+ replyPreview: readString(payload.reply_preview ?? payload.replyPreview),
889
+ replyTo: readString(payload.reply_to ?? payload.replyTo),
890
+ replyInProgress: readBool(payload.reply_in_progress ?? payload.replyInProgress),
891
+ backend: readString(payload.backend),
892
+ threadId: readString(payload.thread_id ?? payload.threadId),
893
+ daemon: readString(payload.daemon),
894
+ pid: readNumber(payload.pid),
895
+ sessionId: readString(payload.session_id ?? payload.sessionId),
896
+ sessionFilePath: readString(payload.session_file_path ?? payload.sessionFilePath),
897
+ tokenUsagePercent: readNumber(payload.token_usage_percent ?? payload.tokenUsagePercent),
898
+ contextUsagePercent: readNumber(payload.context_usage_percent ?? payload.contextUsagePercent),
899
+ createdAt: readString(payload.created_at ?? payload.createdAt)
900
+ };
901
+ return { type: "runtime_status", status };
902
+ }
903
+ default:
904
+ if (typeof process !== "undefined" && process.env && process.env.NODE_ENV === "development") {
905
+ console.warn(
906
+ `[app-sdk] envelopeToChatEvent: unknown envelope type "${type ?? ""}" (dropped)`
907
+ );
908
+ }
909
+ return null;
910
+ }
911
+ }
912
+ function readString(value) {
913
+ if (typeof value !== "string") return null;
914
+ const trimmed = value.trim();
915
+ return trimmed || null;
916
+ }
917
+ function readNumber(value) {
918
+ if (typeof value !== "number" || !Number.isFinite(value)) return null;
919
+ return value;
920
+ }
921
+ function readBool(value) {
922
+ if (typeof value === "boolean") return value;
923
+ return void 0;
924
+ }
925
+
926
+ // src/server/tasks/subscribe.ts
927
+ function subscribeToTask(socket, taskId, opts) {
928
+ const bufferCap = opts?.bufferCap ?? 1024;
929
+ return {
930
+ [Symbol.asyncIterator]() {
931
+ const queue = [];
932
+ let pendingResolver = null;
933
+ let done = false;
934
+ const drain = () => {
935
+ if (!pendingResolver) return;
936
+ if (queue.length > 0) {
937
+ const value = queue.shift();
938
+ const resolver = pendingResolver;
939
+ pendingResolver = null;
940
+ resolver.resolve({ value, done: false });
941
+ return;
942
+ }
943
+ if (done) {
944
+ const resolver = pendingResolver;
945
+ pendingResolver = null;
946
+ resolver.resolve({ value: void 0, done: true });
947
+ }
948
+ };
949
+ const evictForCap = () => {
950
+ while (queue.length > bufferCap) {
951
+ const idx = queue.findIndex((e) => e.type === "runtime_status");
952
+ if (idx >= 0) {
953
+ queue.splice(idx, 1);
954
+ } else {
955
+ queue.shift();
956
+ }
957
+ }
958
+ };
959
+ const envelopeSub = socket.onEnvelope((envelope) => {
960
+ if (done) return;
961
+ const event = envelopeToChatEvent(envelope, taskId);
962
+ if (!event) return;
963
+ queue.push(event);
964
+ evictForCap();
965
+ drain();
966
+ });
967
+ const stateSub = socket.onConnectionState((state) => {
968
+ if (done) return;
969
+ queue.push({ type: "connection_state", state });
970
+ evictForCap();
971
+ drain();
972
+ });
973
+ const closeUnsub = socket.onClose(() => {
974
+ if (done) return;
975
+ queue.push({
976
+ type: "task_failed",
977
+ taskId,
978
+ error: {
979
+ code: "subscribe_failed",
980
+ message: "client closed"
981
+ }
982
+ });
983
+ evictForCap();
984
+ drain();
985
+ finish();
986
+ });
987
+ const onAbort = () => finish();
988
+ const finish = () => {
989
+ if (done) return;
990
+ done = true;
991
+ envelopeSub.unsubscribe();
992
+ stateSub.unsubscribe();
993
+ closeUnsub();
994
+ if (opts?.signal) {
995
+ opts.signal.removeEventListener("abort", onAbort);
996
+ }
997
+ drain();
998
+ };
999
+ if (opts?.signal) {
1000
+ if (opts.signal.aborted) {
1001
+ finish();
1002
+ } else {
1003
+ opts.signal.addEventListener("abort", onAbort, { once: true });
1004
+ }
1005
+ }
1006
+ return {
1007
+ next() {
1008
+ if (queue.length > 0) {
1009
+ const value = queue.shift();
1010
+ return Promise.resolve({ value, done: false });
1011
+ }
1012
+ if (done) {
1013
+ return Promise.resolve({ value: void 0, done: true });
1014
+ }
1015
+ return new Promise((resolve) => {
1016
+ pendingResolver = { resolve };
1017
+ });
1018
+ },
1019
+ return() {
1020
+ finish();
1021
+ return Promise.resolve({ value: void 0, done: true });
1022
+ }
1023
+ };
1024
+ }
1025
+ };
1026
+ }
1027
+
1028
+ // src/server/tasks/stream-reply.ts
1029
+ function isAppOriginEcho(metadata) {
1030
+ if (!metadata || typeof metadata !== "object") return false;
1031
+ const audit = metadata.audit;
1032
+ if (!audit || typeof audit !== "object") return false;
1033
+ return audit.actor === "app";
1034
+ }
1035
+ function isSyntheticSystemMessage(metadata) {
1036
+ if (!metadata || typeof metadata !== "object") return false;
1037
+ return metadata.synthetic === true;
1038
+ }
1039
+ function streamReplyForTask(socket, taskId, opts) {
1040
+ const emitInitial = opts?.emitInitialPreview ?? true;
1041
+ const idleTimeoutMs = opts?.idleTimeoutMs ?? 12e4;
1042
+ return {
1043
+ async *[Symbol.asyncIterator]() {
1044
+ let lastPreview = "";
1045
+ let lastStatusState = null;
1046
+ let bootstrapped = !emitInitial;
1047
+ let currentReplyTo = null;
1048
+ const idleController = new AbortController();
1049
+ let idleTimer = null;
1050
+ let idleFired = false;
1051
+ const armIdle = () => {
1052
+ if (idleTimeoutMs <= 0) return;
1053
+ if (idleTimer) clearTimeout(idleTimer);
1054
+ idleTimer = setTimeout(() => {
1055
+ idleFired = true;
1056
+ idleController.abort();
1057
+ }, idleTimeoutMs);
1058
+ };
1059
+ const disarmIdle = () => {
1060
+ if (idleTimer) {
1061
+ clearTimeout(idleTimer);
1062
+ idleTimer = null;
1063
+ }
1064
+ };
1065
+ let composedSignal;
1066
+ const externalSignal = opts?.signal;
1067
+ const onExternalAbort = () => idleController.abort();
1068
+ if (externalSignal && externalSignal.aborted) {
1069
+ composedSignal = externalSignal;
1070
+ } else if (externalSignal) {
1071
+ externalSignal.addEventListener("abort", onExternalAbort, { once: true });
1072
+ composedSignal = idleController.signal;
1073
+ } else {
1074
+ composedSignal = idleController.signal;
1075
+ }
1076
+ armIdle();
1077
+ try {
1078
+ for await (const event of subscribeToTask(socket, taskId, {
1079
+ signal: composedSignal
1080
+ })) {
1081
+ if (event.type === "runtime_status") {
1082
+ const status = event.status;
1083
+ const replyTo = status.replyTo ?? currentReplyTo ?? "";
1084
+ if (status.replyTo) currentReplyTo = status.replyTo;
1085
+ const preview = status.replyPreview ?? "";
1086
+ const previewAdvanced = preview !== "" && preview !== lastPreview;
1087
+ if (previewAdvanced) {
1088
+ const isContinuation = preview.startsWith(lastPreview);
1089
+ yield {
1090
+ type: "text",
1091
+ text: bootstrapped && isContinuation ? preview.slice(lastPreview.length) : preview,
1092
+ replyTo
1093
+ };
1094
+ lastPreview = preview;
1095
+ bootstrapped = true;
1096
+ }
1097
+ yield { type: "status", status };
1098
+ const stateChanged = status.state !== lastStatusState;
1099
+ if (previewAdvanced || stateChanged) {
1100
+ armIdle();
1101
+ lastStatusState = status.state;
1102
+ }
1103
+ continue;
1104
+ }
1105
+ if (event.type === "message_appended") {
1106
+ const message = event.message;
1107
+ if (message.role === "user") {
1108
+ continue;
1109
+ }
1110
+ if (isAppOriginEcho(message.metadata)) {
1111
+ continue;
1112
+ }
1113
+ if (isSyntheticSystemMessage(message.metadata)) {
1114
+ continue;
1115
+ }
1116
+ yield { type: "done", message };
1117
+ return;
1118
+ }
1119
+ if (event.type === "task_failed") {
1120
+ yield { type: "error", error: event.error };
1121
+ return;
1122
+ }
1123
+ if (event.type === "task_finished") {
1124
+ yield {
1125
+ type: "error",
1126
+ error: {
1127
+ code: "task_not_running",
1128
+ message: "task finished without reply"
1129
+ }
1130
+ };
1131
+ return;
1132
+ }
1133
+ }
1134
+ if (idleFired) {
1135
+ yield {
1136
+ type: "error",
1137
+ error: {
1138
+ code: "stream_aborted",
1139
+ message: "idle timeout"
1140
+ }
1141
+ };
1142
+ }
1143
+ } finally {
1144
+ disarmIdle();
1145
+ if (externalSignal && !externalSignal.aborted) {
1146
+ externalSignal.removeEventListener("abort", onExternalAbort);
1147
+ }
1148
+ }
1149
+ }
1150
+ };
1151
+ }
1152
+
1153
+ // src/server/client.ts
1154
+ var AppClient = class {
1155
+ projects;
1156
+ tasks;
1157
+ _fetcher;
1158
+ _socket = null;
1159
+ _closed = false;
1160
+ options;
1161
+ constructor(options) {
1162
+ validateOptions(options);
1163
+ this.options = options;
1164
+ this._fetcher = new Fetcher({
1165
+ baseUrl: options.baseUrl,
1166
+ bearerToken: options.bearerToken,
1167
+ fetch: options.fetch,
1168
+ timeoutMs: options.timeoutMs,
1169
+ onUnauthorized: options.onUnauthorized
1170
+ });
1171
+ this.projects = new ProjectsApi(this._fetcher);
1172
+ const rest = new TasksRestApi(this._fetcher);
1173
+ this.tasks = new TasksApi(
1174
+ rest,
1175
+ () => this.getOrCreateSocket(),
1176
+ () => this._closed
1177
+ );
1178
+ }
1179
+ /**
1180
+ * Release the WS connection and mark the client closed. Idempotent —
1181
+ * calling twice is a no-op rather than a NPE. After close, any new
1182
+ * `tasks.subscribe()` / `tasks.streamReply()` / `tasks.*` REST call
1183
+ * throws synchronously instead of returning a hanging iterator.
1184
+ */
1185
+ async close() {
1186
+ if (this._closed) return;
1187
+ this._closed = true;
1188
+ if (this._socket) {
1189
+ this._socket.close();
1190
+ this._socket = null;
1191
+ }
1192
+ }
1193
+ getOrCreateSocket() {
1194
+ if (this._socket) return this._socket;
1195
+ this._socket = new AppWebSocket({
1196
+ baseUrl: this.options.baseUrl,
1197
+ bearerToken: this.options.bearerToken,
1198
+ webSocketImpl: this.options.webSocketImpl
1199
+ });
1200
+ return this._socket;
1201
+ }
1202
+ };
1203
+ async function connect(options) {
1204
+ return new AppClient(options);
1205
+ }
1206
+ var TasksApi = class {
1207
+ /** @internal — constructed by `AppClient`; not part of the public surface. */
1208
+ constructor(rest, getSocket, isClosed = () => false) {
1209
+ this.rest = rest;
1210
+ this.getSocket = getSocket;
1211
+ this.isClosed = isClosed;
1212
+ }
1213
+ rest;
1214
+ getSocket;
1215
+ isClosed;
1216
+ assertOpen() {
1217
+ if (this.isClosed()) {
1218
+ throw new ConductorAppError({
1219
+ code: "subscribe_failed",
1220
+ message: "client is closed"
1221
+ });
1222
+ }
1223
+ }
1224
+ create(input, opts) {
1225
+ this.assertOpen();
1226
+ return this.rest.create(input, opts);
1227
+ }
1228
+ get(taskId, opts) {
1229
+ this.assertOpen();
1230
+ return this.rest.get(taskId, opts);
1231
+ }
1232
+ list(filter, opts) {
1233
+ this.assertOpen();
1234
+ return this.rest.list(filter, opts);
1235
+ }
1236
+ sendMessage(taskId, input, opts) {
1237
+ this.assertOpen();
1238
+ return this.rest.sendMessage(taskId, input, opts);
1239
+ }
1240
+ history(taskId, paging, opts) {
1241
+ this.assertOpen();
1242
+ return this.rest.history(taskId, paging, opts);
1243
+ }
1244
+ interrupt(taskId, opts) {
1245
+ this.assertOpen();
1246
+ return this.rest.interrupt(taskId, opts);
1247
+ }
1248
+ /**
1249
+ * Subscribe to a task's event stream. Yields ChatEvents until the caller
1250
+ * breaks out of the `for await` loop, calls `signal.abort()`, or the
1251
+ * client is closed.
1252
+ *
1253
+ * The first call lazily opens a /ws/app connection; subsequent calls share
1254
+ * the same connection.
1255
+ */
1256
+ subscribe(taskId, opts) {
1257
+ if (!taskId) {
1258
+ throw new ConductorAppError({
1259
+ code: "invalid_input",
1260
+ message: "tasks.subscribe requires a taskId"
1261
+ });
1262
+ }
1263
+ this.assertOpen();
1264
+ const socket = this.getSocket();
1265
+ const inner = subscribeToTask(socket, taskId, opts);
1266
+ return {
1267
+ [Symbol.asyncIterator]() {
1268
+ const innerIter = inner[Symbol.asyncIterator]();
1269
+ const connectPromise = socket.connect().then(
1270
+ () => null,
1271
+ (err) => err
1272
+ );
1273
+ let connectChecked = false;
1274
+ return {
1275
+ async next() {
1276
+ if (!connectChecked) {
1277
+ connectChecked = true;
1278
+ const err = await connectPromise;
1279
+ if (err) {
1280
+ try {
1281
+ await innerIter.return?.();
1282
+ } catch {
1283
+ }
1284
+ const event = {
1285
+ type: "task_failed",
1286
+ taskId,
1287
+ error: errorToChatError(err, "subscribe_failed")
1288
+ };
1289
+ return { value: event, done: false };
1290
+ }
1291
+ }
1292
+ return innerIter.next();
1293
+ },
1294
+ async return() {
1295
+ return await innerIter.return?.() ?? { value: void 0, done: true };
1296
+ }
1297
+ };
1298
+ }
1299
+ };
1300
+ }
1301
+ /**
1302
+ * Higher-level convenience: yield only AI reply deltas. Internally consumes
1303
+ * `subscribe()` and selects runtime_status preview chunks + the final
1304
+ * assistant message.
1305
+ */
1306
+ streamReply(taskId, opts) {
1307
+ if (!taskId) {
1308
+ throw new ConductorAppError({
1309
+ code: "invalid_input",
1310
+ message: "tasks.streamReply requires a taskId"
1311
+ });
1312
+ }
1313
+ this.assertOpen();
1314
+ const socket = this.getSocket();
1315
+ const inner = streamReplyForTask(socket, taskId, opts);
1316
+ return {
1317
+ [Symbol.asyncIterator]() {
1318
+ const innerIter = inner[Symbol.asyncIterator]();
1319
+ const connectPromise = socket.connect().then(
1320
+ () => null,
1321
+ (err) => err
1322
+ );
1323
+ let connectChecked = false;
1324
+ return {
1325
+ async next() {
1326
+ if (!connectChecked) {
1327
+ connectChecked = true;
1328
+ const err = await connectPromise;
1329
+ if (err) {
1330
+ try {
1331
+ await innerIter.return?.();
1332
+ } catch {
1333
+ }
1334
+ const delta = {
1335
+ type: "error",
1336
+ error: errorToChatError(err, "stream_aborted")
1337
+ };
1338
+ return { value: delta, done: false };
1339
+ }
1340
+ }
1341
+ return innerIter.next();
1342
+ },
1343
+ async return() {
1344
+ return await innerIter.return?.() ?? { value: void 0, done: true };
1345
+ }
1346
+ };
1347
+ }
1348
+ };
1349
+ }
1350
+ };
1351
+ function errorToChatError(err, defaultCode = "subscribe_failed") {
1352
+ if (err && typeof err === "object") {
1353
+ const code = String(err.code ?? defaultCode);
1354
+ const message = String(
1355
+ err.message ?? "WebSocket connection failed"
1356
+ );
1357
+ const detailsRaw = err.details;
1358
+ const result = {
1359
+ code,
1360
+ message
1361
+ };
1362
+ if (detailsRaw !== void 0) result.details = detailsRaw;
1363
+ if (err instanceof Error) result.cause = err;
1364
+ return result;
1365
+ }
1366
+ return { code: defaultCode, message: String(err) };
1367
+ }
1368
+ function validateOptions(options) {
1369
+ if (!options.baseUrl) {
1370
+ throw new ConductorAppError({
1371
+ code: "invalid_input",
1372
+ message: "connect(): baseUrl is required"
1373
+ });
1374
+ }
1375
+ if (!options.bearerToken) {
1376
+ throw new ConductorAppError({
1377
+ code: "invalid_input",
1378
+ message: "connect(): bearerToken is required"
1379
+ });
1380
+ }
1381
+ }
1382
+ export {
1383
+ AppClient,
1384
+ ConductorAppError,
1385
+ connect,
1386
+ isConductorAppError
1387
+ };