@flowy-agent-store/client 0.1.0-beta.10

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/dist/index.cjs ADDED
@@ -0,0 +1,2750 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let _flowy_agent_store_protocol = require("@flowy-agent-store/protocol");
3
+ //#region src/transport.ts
4
+ /**
5
+ * JSON-RPC 2.0 transport over WebSocket for the App Server protocol.
6
+ *
7
+ * Works in browsers and in Node 22+/Bun (global `WebSocket`). The browser
8
+ * WebSocket API cannot set custom headers, so an optional bearer token is
9
+ * appended as a query parameter (`?token=...`) when provided; HTTP calls in
10
+ * the SDK use the `Authorization` header instead.
11
+ */
12
+ /**
13
+ * True for loopback-only URLs (`127.0.0.0/8`, `::1`, `localhost`).
14
+ * SDK spawn contract (docs/agent-store/12-sdk-packaging.md P0-4): an SDK
15
+ * must only speak the local-trusted mode over loopback; anything else is
16
+ * rejected before connecting.
17
+ */
18
+ function isLoopbackUrl(raw) {
19
+ try {
20
+ const host = new URL(raw).hostname.toLowerCase();
21
+ if (host === "localhost" || host === "::1") return true;
22
+ if (host.startsWith("[") && host.endsWith("]")) return host === "[::1]";
23
+ const parts = host.split(".").map(Number);
24
+ return parts.length === 4 && parts[0] === 127 && parts.every((n) => Number.isInteger(n) && n >= 0 && n <= 255);
25
+ } catch {
26
+ return false;
27
+ }
28
+ }
29
+ /** How long an unopened `connect()` waits before it fails. */
30
+ const DEFAULT_CONNECT_TIMEOUT_MS = 1e4;
31
+ var WebSocketTransport = class {
32
+ socket = null;
33
+ nextId = 1;
34
+ pending = /* @__PURE__ */ new Map();
35
+ listeners = /* @__PURE__ */ new Set();
36
+ lifecycleListeners = /* @__PURE__ */ new Set();
37
+ lifecycle = null;
38
+ closedByCaller = false;
39
+ /** Single in-flight connect shared by concurrent callers; null once settled. */
40
+ connecting = null;
41
+ requestTimeoutMs;
42
+ connectTimeoutMs;
43
+ url;
44
+ token;
45
+ constructor(url, options = {}) {
46
+ this.url = url;
47
+ this.token = options.token;
48
+ this.requestTimeoutMs = options.requestTimeoutMs ?? 3e4;
49
+ this.connectTimeoutMs = options.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS;
50
+ }
51
+ get connected() {
52
+ return this.socket?.readyState === WebSocket.OPEN;
53
+ }
54
+ connect() {
55
+ if (this.connected) return Promise.resolve();
56
+ if (this.connecting) return this.connecting.promise;
57
+ this.closedByCaller = false;
58
+ const target = this.token ? `${this.url}${this.url.includes("?") ? "&" : "?"}token=${encodeURIComponent(this.token)}` : this.url;
59
+ const socket = new WebSocket(target);
60
+ this.socket = socket;
61
+ let resolveConnect;
62
+ let rejectConnect;
63
+ const promise = new Promise((resolve, reject) => {
64
+ resolveConnect = resolve;
65
+ rejectConnect = reject;
66
+ });
67
+ const timer = setTimeout(() => {
68
+ this.detachSocket(socket);
69
+ this.rejectConnect(new _flowy_agent_store_protocol.TransportError("connect", `app-server connect timed out after ${this.connectTimeoutMs}ms`, { retryable: true }));
70
+ }, this.connectTimeoutMs);
71
+ this.connecting = {
72
+ promise,
73
+ resolve: resolveConnect,
74
+ reject: rejectConnect,
75
+ timer
76
+ };
77
+ socket.onopen = () => {
78
+ if (this.socket !== socket) return;
79
+ this.resolveConnect();
80
+ this.setLifecycle("open");
81
+ };
82
+ socket.onmessage = (event) => {
83
+ if (this.socket !== socket) return;
84
+ this.handleMessage(event.data);
85
+ };
86
+ socket.onclose = (event) => {
87
+ if (this.socket !== socket) return;
88
+ this.failAllPending(new _flowy_agent_store_protocol.TransportError("close", `app-server connection closed (code ${event.code})`, { retryable: !this.closedByCaller }));
89
+ this.socket = null;
90
+ this.setLifecycle("closed");
91
+ this.rejectConnect(new _flowy_agent_store_protocol.TransportError("connect", "app-server connection closed before opening"));
92
+ };
93
+ socket.onerror = () => {
94
+ if (this.socket === socket) socket.close();
95
+ };
96
+ return promise;
97
+ }
98
+ request(method, params) {
99
+ const socket = this.requireSocket();
100
+ const id = this.nextId++;
101
+ const payload = {
102
+ jsonrpc: "2.0",
103
+ id,
104
+ method,
105
+ params
106
+ };
107
+ return new Promise((resolve, reject) => {
108
+ const timer = setTimeout(() => {
109
+ this.pending.delete(id);
110
+ reject(new _flowy_agent_store_protocol.RequestTimeoutError(method, this.requestTimeoutMs));
111
+ }, this.requestTimeoutMs);
112
+ this.pending.set(id, {
113
+ resolve,
114
+ reject,
115
+ timer,
116
+ method
117
+ });
118
+ try {
119
+ socket.send(JSON.stringify(payload));
120
+ } catch (error) {
121
+ clearTimeout(timer);
122
+ this.pending.delete(id);
123
+ reject(new _flowy_agent_store_protocol.TransportError("send", `failed to send ${method}`, { cause: error }));
124
+ }
125
+ });
126
+ }
127
+ notify(method, params) {
128
+ const socket = this.requireSocket();
129
+ const payload = {
130
+ jsonrpc: "2.0",
131
+ method,
132
+ params
133
+ };
134
+ try {
135
+ socket.send(JSON.stringify(payload));
136
+ } catch (error) {
137
+ throw new _flowy_agent_store_protocol.TransportError("send", `failed to send notification ${method}`, { cause: error });
138
+ }
139
+ }
140
+ onNotification(listener) {
141
+ this.listeners.add(listener);
142
+ return () => {
143
+ this.listeners.delete(listener);
144
+ };
145
+ }
146
+ /**
147
+ * Observe the channel lifecycle. `closed` is only reported when an
148
+ * established connection is lost (a failed first dial is not a disconnect),
149
+ * and `open` follows it on the next successful dial — that pair is what a
150
+ * host should treat as a reconnect. Unlike `onNotification`, these listeners
151
+ * survive `close()`, since they exist to drive the reconnection itself.
152
+ */
153
+ onLifecycle(listener) {
154
+ this.lifecycleListeners.add(listener);
155
+ return () => {
156
+ this.lifecycleListeners.delete(listener);
157
+ };
158
+ }
159
+ close() {
160
+ this.closedByCaller = true;
161
+ this.detachSocket(this.socket);
162
+ this.rejectConnect(new _flowy_agent_store_protocol.TransportError("close", "app-server transport closed by caller"));
163
+ for (const [, pending] of this.pending) {
164
+ clearTimeout(pending.timer);
165
+ pending.reject(new _flowy_agent_store_protocol.TransportError("close", "app-server transport closed by caller"));
166
+ }
167
+ this.pending.clear();
168
+ this.listeners.clear();
169
+ this.lifecycle = null;
170
+ }
171
+ setLifecycle(state) {
172
+ if (state === "closed" && this.lifecycle !== "open") return;
173
+ if (this.lifecycle === state) return;
174
+ this.lifecycle = state;
175
+ for (const listener of [...this.lifecycleListeners]) try {
176
+ listener(state);
177
+ } catch {}
178
+ }
179
+ /** Detach the current socket (if any) and close it; its later events are ignored. */
180
+ detachSocket(socket) {
181
+ if (!socket) return;
182
+ if (this.socket === socket) this.socket = null;
183
+ try {
184
+ socket.close();
185
+ } catch {}
186
+ }
187
+ /** Settle the in-flight `connect()` successfully, when there is one. */
188
+ resolveConnect() {
189
+ const connecting = this.connecting;
190
+ if (!connecting) return;
191
+ this.connecting = null;
192
+ clearTimeout(connecting.timer);
193
+ connecting.resolve();
194
+ }
195
+ /** Fail the in-flight `connect()`, when there is one. */
196
+ rejectConnect(error) {
197
+ const connecting = this.connecting;
198
+ if (!connecting) return;
199
+ this.connecting = null;
200
+ clearTimeout(connecting.timer);
201
+ connecting.reject(error);
202
+ }
203
+ requireSocket() {
204
+ if (!this.socket || this.socket.readyState !== WebSocket.OPEN) throw new _flowy_agent_store_protocol.TransportError("send", "app-server transport is not connected", { retryable: true });
205
+ return this.socket;
206
+ }
207
+ handleMessage(raw) {
208
+ let message;
209
+ try {
210
+ message = JSON.parse(String(raw));
211
+ } catch {
212
+ return;
213
+ }
214
+ if ("id" in message && message.id !== void 0) {
215
+ const response = message;
216
+ const pending = response.id === null ? void 0 : this.pending.get(Number(response.id));
217
+ if (!pending) return;
218
+ clearTimeout(pending.timer);
219
+ this.pending.delete(Number(response.id));
220
+ if (response.error) pending.reject(new _flowy_agent_store_protocol.AppServerError(response.error));
221
+ else pending.resolve(response.result);
222
+ return;
223
+ }
224
+ if ("method" in message) {
225
+ const notification = message;
226
+ for (const listener of [...this.listeners]) try {
227
+ listener(notification);
228
+ } catch {}
229
+ return;
230
+ }
231
+ }
232
+ failAllPending(error) {
233
+ for (const [, pending] of this.pending) {
234
+ clearTimeout(pending.timer);
235
+ pending.reject(error);
236
+ }
237
+ this.pending.clear();
238
+ }
239
+ };
240
+ //#endregion
241
+ //#region src/http-transport.ts
242
+ /**
243
+ * HTTP transport binding (doc `16` R2 / `05` §2.1.1).
244
+ *
245
+ * ## What this is
246
+ *
247
+ * The App Server exposes one method semantics over two bindings. This class is
248
+ * the **request/response** binding: it maps a protocol method name onto the
249
+ * server's hand-written REST route and returns the same result body the
250
+ * WebSocket binding would.
251
+ *
252
+ * ## What this is not
253
+ *
254
+ * - **No server push.** HTTP has no notification channel, so `onNotification`
255
+ * hands back a no-op unsubscribe and `notify` throws. Live conversation / run
256
+ * events and subscription methods (`conversation/subscribe`, `run/subscribe`)
257
+ * exist **only** over `WebSocketTransport`; an `HttpTransport` is not a
258
+ * drop-in replacement for it. Consumers that need events must connect over
259
+ * WS (or run both).
260
+ * - **One-shot handshake per call.** `05` §2.1.1: *each* HTTP call performs its
261
+ * own `POST /initialize` → `POST /initialized` → business call, after which
262
+ * the connection id expires. `connect()` therefore has nothing to hold open
263
+ * and is a no-op; it does not warm anything up.
264
+ * - **Host file services stay out.** `/api/fs/*` (browse / list / read /
265
+ * metadata) is a host file service on the server *root*, explicitly not a
266
+ * protocol method (`05` §2.1.1), so it is not reachable through this class.
267
+ *
268
+ * ## The route table
269
+ *
270
+ * Every entry was read out of the handler bodies in
271
+ * `crates/backend/nomifun-app-server/src/lib.rs` — the `source` field records
272
+ * which handler (and, where one exists, which shared `*_impl`) proves the two
273
+ * bindings run the same operation. Methods with no entry throw
274
+ * `TransportError("send", …)` instead of silently degrading.
275
+ */
276
+ /** Header the server mints the per-call connection id under. */
277
+ const CONNECTION_HEADER = "x-app-server-connection-id";
278
+ /** Protocol version this binding announces on the one-shot handshake. */
279
+ const PROTOCOL_VERSION = "fp-14";
280
+ /**
281
+ * Protocol method → REST route. Verified from handler bodies, not from the
282
+ * route list (a route existing does not prove which method it mirrors).
283
+ */
284
+ const HTTP_ROUTES = {
285
+ ping: {
286
+ verb: "POST",
287
+ path: "/ping",
288
+ source: "ping() -> registry.require_ready"
289
+ },
290
+ "workspace/list": {
291
+ verb: "GET",
292
+ path: "/workspaces",
293
+ source: "workspace_list() -> owner-scoped active rows"
294
+ },
295
+ "workspace/revoke": {
296
+ verb: "DELETE",
297
+ path: "/workspaces/:workspace_id",
298
+ source: "workspace_delete() -> workspace_revoke_impl (shared with the WS arm)"
299
+ },
300
+ "conversation/create": {
301
+ verb: "POST",
302
+ path: "/conversations",
303
+ source: "conversation_create() -> create_conversation_for_user (shared)"
304
+ },
305
+ "conversation/list": {
306
+ verb: "GET",
307
+ path: "/conversations",
308
+ query: ["limit"],
309
+ source: "conversation_list() -> list_app_server_chats (shared); Query<limit>"
310
+ },
311
+ "conversation/get": {
312
+ verb: "GET",
313
+ path: "/conversations/:conversation_id",
314
+ source: "conversation_get() -> project_conversation_view (shared)"
315
+ },
316
+ "conversation/delete": {
317
+ verb: "DELETE",
318
+ path: "/conversations/:conversation_id",
319
+ source: "conversation_delete() -> conversation/delete arm semantics"
320
+ },
321
+ "conversation/messages": {
322
+ verb: "GET",
323
+ path: "/conversations/:conversation_id/messages",
324
+ query: [
325
+ "page",
326
+ "page_size",
327
+ "cursor"
328
+ ],
329
+ source: "conversation_messages() -> list_conversation_messages_for_user (shared)"
330
+ },
331
+ "conversation/send": {
332
+ verb: "POST",
333
+ path: "/conversations/:conversation_id/messages",
334
+ body: [
335
+ "content",
336
+ "idempotency_key",
337
+ "attachments",
338
+ "mentions",
339
+ "model",
340
+ "reasoning_effort"
341
+ ],
342
+ source: "conversation_send() -> send_conversation_message_for_user (shared with the WS arm)"
343
+ },
344
+ "conversation/cancel": {
345
+ verb: "POST",
346
+ path: "/conversations/:conversation_id/cancel",
347
+ source: "conversation_cancel() -> cancel arm semantics"
348
+ },
349
+ "agent/run": {
350
+ verb: "POST",
351
+ path: "/agent/run",
352
+ source: "agent_run() -> same request/receipt as the `agent/run` arm"
353
+ },
354
+ "team/run": {
355
+ verb: "POST",
356
+ path: "/team/run",
357
+ source: "team_run() -> execute_team_run() (same as the WS arm); Leader Conversation + planned delegation, `TeamRunReceipt`"
358
+ },
359
+ "run/get": {
360
+ verb: "GET",
361
+ path: "/run/:run_id",
362
+ source: "run_get()"
363
+ },
364
+ "run/result": {
365
+ verb: "GET",
366
+ path: "/run/:run_id/result",
367
+ source: "run_result()"
368
+ },
369
+ "run/plan": {
370
+ verb: "GET",
371
+ path: "/run/:run_id/plan",
372
+ source: "run_plan() -> get_run_plan_for_user() (same as the WS arm)"
373
+ },
374
+ "run/events": {
375
+ verb: "GET",
376
+ path: "/run/:run_id/events",
377
+ query: ["after_sequence", "limit"],
378
+ source: "run_events() -> runtime.list_events(after_sequence, limit) (same as the WS arm)"
379
+ },
380
+ "run/cancel": {
381
+ verb: "POST",
382
+ path: "/run/:run_id/cancel",
383
+ body: [
384
+ "expected_version",
385
+ "reason",
386
+ "idempotency_key"
387
+ ],
388
+ source: "run_cancel() -> same CancelRunRequest as the WS arm"
389
+ },
390
+ "run/steer": {
391
+ verb: "POST",
392
+ path: "/run/:run_id/steer",
393
+ body: [
394
+ "text",
395
+ "expected_version",
396
+ "command_id",
397
+ "idempotency_key"
398
+ ],
399
+ source: "run_steer() -> execute_steer_run (shared with the WS arm)"
400
+ },
401
+ "run/answer-decision": {
402
+ verb: "POST",
403
+ path: "/run/:run_id/answer-decision",
404
+ body: [
405
+ "step_id",
406
+ "attempt_id",
407
+ "answer",
408
+ "expected_execution_version",
409
+ "expected_step_version",
410
+ "expected_attempt_version"
411
+ ],
412
+ source: "run_answer_decision() -> execute_answer_decision (shared with the WS arm; engine answer gate, no always_allow)"
413
+ },
414
+ "skill/list": {
415
+ verb: "GET",
416
+ path: "/skills",
417
+ source: "list_skills_route() -> list_skills_impl"
418
+ },
419
+ "skill/get": {
420
+ verb: "GET",
421
+ path: "/skills/:skill_id",
422
+ source: "get_skill_route() -> get_skill_impl"
423
+ },
424
+ "skill/files": {
425
+ verb: "GET",
426
+ path: "/skills/:skill_id/files",
427
+ source: "list_skill_files_route() -> list_skill_files_impl"
428
+ },
429
+ "connector/list": {
430
+ verb: "GET",
431
+ path: "/connectors",
432
+ source: "list_connectors_route() -> list_connectors_impl"
433
+ },
434
+ "models/list": {
435
+ verb: "GET",
436
+ path: "/models",
437
+ source: "list_models_route() -> list_models_impl"
438
+ },
439
+ "connector/get": {
440
+ verb: "GET",
441
+ path: "/connectors/:connector_id",
442
+ source: "get_connector_route() -> get_connector_impl"
443
+ },
444
+ "connector/status": {
445
+ verb: "GET",
446
+ path: "/connectors/:connector_id/status",
447
+ source: "connector_status_route() -> connector_status_impl"
448
+ },
449
+ "connector/test": {
450
+ verb: "POST",
451
+ path: "/connectors/:connector_id/test",
452
+ source: "connector_test_route() -> connector_test_impl"
453
+ },
454
+ "connector/call": {
455
+ verb: "POST",
456
+ path: "/connectors/:connector_id/call",
457
+ source: "connector_call_route() -> connector_call_impl"
458
+ },
459
+ "connector/auth/start": {
460
+ verb: "POST",
461
+ path: "/connectors/:connector_id/auth-start",
462
+ source: "connector_auth_start_route() -> connector_auth_start_impl (WS name uses slashes)"
463
+ },
464
+ "connector/auth/status": {
465
+ verb: "GET",
466
+ path: "/connectors/:connector_id/auth-status",
467
+ source: "connector_auth_status_route() -> connector_auth_status_impl"
468
+ },
469
+ "connector/auth/logout": {
470
+ verb: "POST",
471
+ path: "/connectors/:connector_id/auth-logout",
472
+ source: "connector_auth_logout_route() -> connector_auth_logout_impl"
473
+ },
474
+ "connector/credential/get": {
475
+ verb: "GET",
476
+ path: "/connectors/:connector_id/credential",
477
+ source: "connector_credential_get_route() -> connector_credential_get_impl"
478
+ },
479
+ "connector/credential/set": {
480
+ verb: "POST",
481
+ path: "/connectors/:connector_id/credential",
482
+ source: "connector_credential_set_route() -> connector_credential_set_impl"
483
+ },
484
+ "connector/credential/clear": {
485
+ verb: "POST",
486
+ path: "/connectors/:connector_id/credential/clear",
487
+ source: "connector_credential_clear_route() -> connector_credential_clear_impl"
488
+ },
489
+ "connector/register": {
490
+ verb: "POST",
491
+ path: "/connectors",
492
+ source: "register_connector_route() -> register_connector_impl"
493
+ },
494
+ "import/run": {
495
+ verb: "POST",
496
+ path: "/imports",
497
+ source: "run_import_route() -> run_import_impl"
498
+ },
499
+ "import/list": {
500
+ verb: "GET",
501
+ path: "/imports",
502
+ source: "list_imports_route() -> list_imports_impl"
503
+ },
504
+ "import/get": {
505
+ verb: "GET",
506
+ path: "/imports/:snapshot_id",
507
+ source: "get_import_route() -> get_import_impl"
508
+ },
509
+ "install/run": {
510
+ verb: "POST",
511
+ path: "/installs",
512
+ source: "run_install_route() -> install_impl"
513
+ },
514
+ "install/status": {
515
+ verb: "GET",
516
+ path: "/installs/:snapshot_id",
517
+ source: "install_status_route() -> install_status_impl"
518
+ },
519
+ "install/disable": {
520
+ verb: "POST",
521
+ path: "/installs/:snapshot_id/disable",
522
+ body: ["component_ids"],
523
+ source: "install_disable_route() -> install_disable_impl; body `component_ids`"
524
+ },
525
+ "install/enable": {
526
+ verb: "POST",
527
+ path: "/installs/:snapshot_id/enable",
528
+ body: ["component_ids"],
529
+ source: "install_enable_route() -> install_enable_impl; body `component_ids`"
530
+ },
531
+ "install/uninstall": {
532
+ verb: "POST",
533
+ path: "/installs/:snapshot_id/uninstall",
534
+ body: ["component_ids"],
535
+ source: "install_uninstall_route() -> install_uninstall_impl; body `component_ids`"
536
+ },
537
+ "market/add": {
538
+ verb: "POST",
539
+ path: "/markets",
540
+ source: "market_add_route() -> market_add_impl"
541
+ },
542
+ "market/list": {
543
+ verb: "GET",
544
+ path: "/markets",
545
+ source: "market_list_route() -> market_list_impl"
546
+ },
547
+ "market/get": {
548
+ verb: "GET",
549
+ path: "/markets/:marketplace_id",
550
+ source: "market_get_route() -> market_get_impl"
551
+ },
552
+ "market/remove": {
553
+ verb: "POST",
554
+ path: "/markets/:marketplace_id/remove",
555
+ body: ["cascade"],
556
+ source: "market_remove_route() -> market_remove_impl; body `cascade` defaults to true"
557
+ },
558
+ "market/auto-update": {
559
+ verb: "POST",
560
+ path: "/markets/:marketplace_id/auto-update",
561
+ body: ["enabled"],
562
+ source: "market_auto_update_route() -> market_auto_update_impl; body `enabled` defaults to true"
563
+ },
564
+ "market/refresh": {
565
+ verb: "POST",
566
+ path: "/markets/:marketplace_id/refresh",
567
+ source: "market_refresh_route() -> market_refresh_impl"
568
+ },
569
+ "market/entry-import": {
570
+ verb: "POST",
571
+ path: "/markets/:marketplace_id/entries/:entry_name/import",
572
+ source: "market_entry_import_route() -> market_entry_import_impl"
573
+ },
574
+ "market/settings": {
575
+ verb: "GET",
576
+ path: "/market-settings",
577
+ source: "market_settings_route() -> market_settings_impl"
578
+ },
579
+ "market/settings-set": {
580
+ verb: "POST",
581
+ path: "/market-settings",
582
+ body: ["auto_update_interval_hours", "entry_auto_update_kinds"],
583
+ source: "market_settings_set_route() -> market_settings_set_impl"
584
+ },
585
+ "store/list": {
586
+ verb: "GET",
587
+ path: "/store",
588
+ source: "store_list_route() -> store_list_impl"
589
+ },
590
+ "store/install-entry": {
591
+ verb: "POST",
592
+ path: "/store/:marketplace_id/entries/:entry_name/install",
593
+ source: "store_install_entry_route() -> store_install_entry_impl"
594
+ },
595
+ "store/update-entry": {
596
+ verb: "POST",
597
+ path: "/store/:marketplace_id/entries/:entry_name/update",
598
+ source: "store_update_entry_route() -> store_update_entry_impl"
599
+ }
600
+ };
601
+ /** Methods that are WebSocket-only by design — named so the error explains itself. */
602
+ const WS_ONLY_METHODS = /* @__PURE__ */ new Set([
603
+ "initialize",
604
+ "initialized",
605
+ "conversation/subscribe",
606
+ "conversation/unsubscribe",
607
+ "run/subscribe",
608
+ "run/unsubscribe"
609
+ ]);
610
+ var HttpTransport = class {
611
+ routes;
612
+ options;
613
+ closed = false;
614
+ constructor(options, routes = HTTP_ROUTES) {
615
+ this.options = options;
616
+ this.routes = routes;
617
+ }
618
+ /**
619
+ * No-op: the HTTP binding handshakes per call (`05` §2.1.1), so there is no
620
+ * connection to open. Kept for `Transport` compatibility.
621
+ */
622
+ async connect() {
623
+ this.closed = false;
624
+ }
625
+ /**
626
+ * Perform one business call over its own handshake.
627
+ *
628
+ * Throws `TransportError("send", …)` for a method with no HTTP binding —
629
+ * never a silent fallback to another binding.
630
+ */
631
+ async request(method, params) {
632
+ if (this.closed) throw new _flowy_agent_store_protocol.TransportError("connect", "http transport is closed");
633
+ const route = this.routes[method];
634
+ if (!route) {
635
+ const reason = WS_ONLY_METHODS.has(method) ? `"${method}" is WebSocket-only (doc 05 §2.1.1); use WebSocketTransport for subscriptions and live events` : `"${method}" has no HTTP binding in this SDK (doc 16 R2 route table)`;
636
+ throw new _flowy_agent_store_protocol.TransportError("send", reason);
637
+ }
638
+ const { path, query, body } = this.splitParams(route, params);
639
+ const connectionId = await this.openConnection();
640
+ const response = await this.fetchWithTimeout(route.verb, path, {
641
+ query,
642
+ body,
643
+ connectionId
644
+ });
645
+ return await this.readResult(response, method);
646
+ }
647
+ /**
648
+ * Not supported: HTTP carries no server-push. Throws instead of pretending.
649
+ */
650
+ notify(method, _params) {
651
+ throw new _flowy_agent_store_protocol.TransportError("send", `HTTP binding cannot notify ("${method}"): notifications are WebSocket-only (doc 05 §2.1.1)`);
652
+ }
653
+ /**
654
+ * Returns a no-op unsubscribe: this binding receives **no** notifications.
655
+ *
656
+ * Not equivalent to the WebSocket binding — callers that need live events
657
+ * must use `WebSocketTransport` (or run both).
658
+ */
659
+ onNotification(_listener) {
660
+ return () => void 0;
661
+ }
662
+ /** No-op: every call is independent, so nothing is held open. */
663
+ close() {
664
+ this.closed = true;
665
+ }
666
+ /**
667
+ * One-shot handshake: `POST /initialize` (connection id header) →
668
+ * `POST /initialized`. Returns the connection id.
669
+ *
670
+ * Public because host-side services that sit *outside* the protocol surface
671
+ * (`/api/fs/*`, the HTTP-only `POST /workspaces` register route) still need a
672
+ * ready connection id; they are not protocol methods, so they cannot go
673
+ * through `request()`.
674
+ */
675
+ async openConnection() {
676
+ const connectionId = (await this.fetchWithTimeout("POST", "/initialize", { body: {
677
+ protocol_version: PROTOCOL_VERSION,
678
+ client: this.options.client ?? {
679
+ name: "flowy-agent-store-client",
680
+ version: "0.1.0"
681
+ },
682
+ ...this.options.capabilities === void 0 ? {} : { capabilities: this.options.capabilities }
683
+ } })).headers.get(CONNECTION_HEADER);
684
+ if (!connectionId) throw new _flowy_agent_store_protocol.TransportError("connect", `handshake response is missing the ${CONNECTION_HEADER} header`, { retryable: true });
685
+ await this.fetchWithTimeout("POST", "/initialized", { connectionId });
686
+ return connectionId;
687
+ }
688
+ splitParams(route, params) {
689
+ const source = isRecord(params) ? params : {};
690
+ const pathParams = [...route.path.matchAll(/:([A-Za-z0-9_]+)/g)].map((match) => match[1]);
691
+ const path = route.path.replace(/:([A-Za-z0-9_]+)/g, (_full, name) => {
692
+ const value = source[name];
693
+ if (value === void 0 || value === null) throw new _flowy_agent_store_protocol.TransportError("send", `missing required path param "${name}" for ${route.verb} ${route.path}`);
694
+ return encodeURIComponent(String(value));
695
+ });
696
+ const query = {};
697
+ for (const key of route.query ?? []) {
698
+ const value = source[key];
699
+ if (value !== void 0 && value !== null) query[key] = String(value);
700
+ }
701
+ if (route.verb === "GET" || route.verb === "DELETE") return {
702
+ path,
703
+ query,
704
+ body: void 0
705
+ };
706
+ const carried = route.body ?? [];
707
+ const body = {};
708
+ for (const key of Object.keys(source)) {
709
+ if (pathParams.includes(key) || (route.query ?? []).includes(key)) continue;
710
+ if (carried.length > 0 && !carried.includes(key)) continue;
711
+ body[key] = source[key];
712
+ }
713
+ return {
714
+ path,
715
+ query,
716
+ body
717
+ };
718
+ }
719
+ async fetchWithTimeout(verb, path, request) {
720
+ const url = new URL(`${this.baseUrl()}${path}`);
721
+ for (const [key, value] of Object.entries(request.query ?? {})) url.searchParams.set(key, value);
722
+ const headers = { accept: "application/json" };
723
+ if (request.body !== void 0) headers["content-type"] = "application/json";
724
+ if (request.connectionId) headers[CONNECTION_HEADER] = request.connectionId;
725
+ if (this.options.token) headers.authorization = `Bearer ${this.options.token}`;
726
+ const controller = new AbortController();
727
+ const timeoutMs = this.options.requestTimeoutMs ?? 3e4;
728
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
729
+ try {
730
+ const fetchImpl = this.options.fetch ?? globalThis.fetch;
731
+ if (!fetchImpl) throw new _flowy_agent_store_protocol.TransportError("connect", "no fetch implementation available");
732
+ return await fetchImpl(url.toString(), {
733
+ method: verb,
734
+ headers,
735
+ body: request.body === void 0 ? void 0 : JSON.stringify(request.body),
736
+ signal: controller.signal
737
+ });
738
+ } catch (error) {
739
+ if (error instanceof _flowy_agent_store_protocol.TransportError) throw error;
740
+ const aborted = error?.name === "AbortError";
741
+ throw new _flowy_agent_store_protocol.TransportError("send", aborted ? `${verb} ${path} timed out after ${timeoutMs}ms` : `${verb} ${path} failed: ${String(error)}`, {
742
+ retryable: true,
743
+ cause: error
744
+ });
745
+ } finally {
746
+ clearTimeout(timer);
747
+ }
748
+ }
749
+ /** 2xx → result body; otherwise the server's wire error becomes an `AppServerError`. */
750
+ async readResult(response, method) {
751
+ let payload = void 0;
752
+ const text = await response.text();
753
+ if (text.length > 0) try {
754
+ payload = JSON.parse(text);
755
+ } catch {
756
+ if (response.ok) throw new _flowy_agent_store_protocol.TransportError("receive", `${method}: response is not JSON`, { retryable: true });
757
+ }
758
+ if (response.ok) return payload;
759
+ throw appServerErrorFromWire(payload, response.status, method);
760
+ }
761
+ baseUrl() {
762
+ return this.options.baseUrl.replace(/\/+$/, "").replace(/\/ws$/, "");
763
+ }
764
+ };
765
+ function isRecord(value) {
766
+ return typeof value === "object" && value !== null && !Array.isArray(value);
767
+ }
768
+ /**
769
+ * Turn an HTTP failure body into the SDK error model.
770
+ *
771
+ * Shared by this transport and by host-side helpers (webui `lib/client.ts`) so
772
+ * a second spelling of the wire-error mapping cannot drift. Two wire contracts
773
+ * reach this function, both of them `AppError` on the server:
774
+ *
775
+ * - the App Server emits `{code, message, retryable, details, request_id}`
776
+ * (`into_wire_error`), which is exactly the `AppServerError` shape;
777
+ * - the standalone host file service (`/api/fs/*`) emits the `ErrorResponse`
778
+ * envelope `{success: false, error, code, details?}` — same error, another
779
+ * spelling: `error` carries the human message, and there is no retry hint, so
780
+ * a 5xx status stays the retry signal.
781
+ *
782
+ * Anything else becomes a retryable-on-5xx `TransportError`.
783
+ */
784
+ function appServerErrorFromWire(payload, status, context = "request") {
785
+ if (isRecord(payload) && typeof payload.code === "string" && typeof payload.message === "string") return new _flowy_agent_store_protocol.AppServerError({
786
+ code: payload.code,
787
+ message: payload.message,
788
+ retryable: payload.retryable === true,
789
+ request_id: payload.request_id ?? null,
790
+ details: isRecord(payload.details) ? payload.details : {}
791
+ });
792
+ if (isRecord(payload) && payload.success === false && typeof payload.code === "string" && typeof payload.error === "string") return new _flowy_agent_store_protocol.AppServerError({
793
+ code: payload.code,
794
+ message: payload.error,
795
+ retryable: status >= 500,
796
+ request_id: null,
797
+ details: isRecord(payload.details) ? payload.details : {}
798
+ });
799
+ return new _flowy_agent_store_protocol.TransportError("receive", `${context}: HTTP ${status} without a wire error body`, { retryable: status >= 500 });
800
+ }
801
+ /** Exposed for tests and docs (R5): the verified method → route table. */
802
+ function httpRouteTable() {
803
+ return HTTP_ROUTES;
804
+ }
805
+ //#endregion
806
+ //#region src/agents.ts
807
+ var AgentClient = class {
808
+ transport;
809
+ constructor(transport) {
810
+ this.transport = transport;
811
+ }
812
+ /** List Agent Store AgentDefinitions (public summaries only). */
813
+ list() {
814
+ return this.transport.request("agent/list", {});
815
+ }
816
+ /** Fetch one AgentDefinition; raw prompts never cross the wire. */
817
+ get(agentId) {
818
+ return this.transport.request("agent/get", { agent_id: agentId });
819
+ }
820
+ /**
821
+ * Export one AgentDefinition as a portable {@link ExpertPack}.
822
+ *
823
+ * Requires WebSocket: like the rest of this family, `agent/export` has no HTTP
824
+ * route. The expert must be installed and enabled, or the host answers
825
+ * `agent_not_installed` / `agent_disabled`; a host whose `[expert_export]`
826
+ * table refuses the id answers `policy_denied`. Check
827
+ * `capabilities.expert_export` for the *seam* — it does not promise this id is
828
+ * exportable.
829
+ */
830
+ export(agentId) {
831
+ return this.transport.request("agent/export", { agent_id: agentId });
832
+ }
833
+ };
834
+ //#endregion
835
+ //#region src/skills.ts
836
+ var SkillClient = class {
837
+ transport;
838
+ constructor(transport) {
839
+ this.transport = transport;
840
+ }
841
+ /** List the Agent Store Skill catalog (public summaries only). */
842
+ list() {
843
+ return this.transport.request("skill/list", {});
844
+ }
845
+ /** Fetch one Skill detail; instructions are a bounded public summary. */
846
+ get(skillId) {
847
+ return this.transport.request("skill/get", { skill_id: skillId });
848
+ }
849
+ /**
850
+ * Every readable file inside a Skill's directory, with a tree digest.
851
+ *
852
+ * This is the only way to learn what a Skill actually ships: `get()` returns
853
+ * a bounded summary of `SKILL.md` and says nothing about the `references/`,
854
+ * `scripts/`, `templates/` and `assets/` beside it. Check
855
+ * `capabilities.skill_files` first — a host may wire the catalog without
856
+ * this face, in which case it answers `unsupported_operation`.
857
+ */
858
+ files(skillId) {
859
+ return this.transport.request("skill/files", { skill_id: skillId });
860
+ }
861
+ /**
862
+ * One skill file's bytes, over the WebSocket binding.
863
+ *
864
+ * The wire carries base64 because JSON has no byte string, so this decodes
865
+ * back to `Uint8Array` for you. The HTTP route
866
+ * (`GET /api/app-server/skills/{id}/files/{path}`) returns the raw body
867
+ * instead, but that is deliberately not on the typed client's JSON
868
+ * transport — use this method, or `fetch` the route directly if you are on a
869
+ * host token.
870
+ */
871
+ async readFile(skillId, path) {
872
+ return decodeBase64((await this.transport.request("skill/file", {
873
+ skill_id: skillId,
874
+ path
875
+ })).content);
876
+ }
877
+ /** Same as {@link readFile}, but also returns the server's content type. */
878
+ readFileWithType(skillId, path) {
879
+ return this.transport.request("skill/file", {
880
+ skill_id: skillId,
881
+ path
882
+ });
883
+ }
884
+ };
885
+ /**
886
+ * Decode base64 to bytes (shared with the snapshot asset face, doc 38).
887
+ *
888
+ * `atob` is used when present (browsers, Bun, Node 16+) and a manual decode
889
+ * otherwise, so the package keeps working on a bare Node without a DOM lib.
890
+ */
891
+ function decodeBase64(content) {
892
+ if (typeof atob === "function") {
893
+ const binary = atob(content);
894
+ const bytes = new Uint8Array(binary.length);
895
+ for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
896
+ return bytes;
897
+ }
898
+ const bufferCtor = globalThis.Buffer;
899
+ if (bufferCtor) return bufferCtor.from(content, "base64");
900
+ throw new Error("no base64 decoder available in this environment");
901
+ }
902
+ //#endregion
903
+ //#region src/conversations.ts
904
+ var ConversationClient = class {
905
+ transport;
906
+ constructor(transport) {
907
+ this.transport = transport;
908
+ }
909
+ create(input) {
910
+ const model = input.model ? {
911
+ provider_id: input.model.provider_id,
912
+ model: input.model.model
913
+ } : void 0;
914
+ return this.transport.request("conversation/create", {
915
+ name: input.name,
916
+ model,
917
+ workspace: input.workspaceId ? { id: input.workspaceId } : void 0,
918
+ reasoning_effort: input.reasoningEffort || void 0,
919
+ agent_id: input.agentId || void 0,
920
+ team_id: input.teamId || void 0
921
+ });
922
+ }
923
+ update(conversationId, input) {
924
+ return this.transport.request("conversation/update", {
925
+ conversation_id: conversationId,
926
+ name: input.name,
927
+ model: input.model ? {
928
+ provider_id: input.model.provider_id,
929
+ model: input.model.model
930
+ } : void 0,
931
+ reasoning_effort: input.reasoningEffort || void 0
932
+ });
933
+ }
934
+ modelOptions() {
935
+ return this.transport.request("conversation/model-options", {});
936
+ }
937
+ list(limit = 100) {
938
+ return this.transport.request("conversation/list", { limit });
939
+ }
940
+ get(conversationId) {
941
+ return this.transport.request("conversation/get", { conversation_id: conversationId });
942
+ }
943
+ messages(query) {
944
+ return this.transport.request("conversation/messages", {
945
+ conversation_id: query.conversationId,
946
+ page: query.page,
947
+ page_size: query.pageSize,
948
+ cursor: query.cursor
949
+ });
950
+ }
951
+ /**
952
+ * R15(W10):`attachments` 是**会话工作区内的绝对路径**(图片附件)。
953
+ * doc `27` §4.1:`mentions` 是**本轮挂载的技能**(`kind: "skill"`)——技能的正文与
954
+ * 不可变快照都随这一轮走;`agent` / `connector` 在 send 上没有载体,服务端会以
955
+ * `invalid_request` **明确拒绝**(不是静默不挂)。
956
+ *
957
+ * 两者缺省都不上 wire,保持老调用方的形状逐字不变;服务端只接受会话工作区内的真实
958
+ * 文件(越界 / 相对路径 / 不存在一律拒),准入见 `nomifun-app-server` 的
959
+ * `resolve_conversation_attachments` 与 `send_mention_skills`。
960
+ *
961
+ * 第 4 个参数同时接受旧的 `string[]`(附件)与选项对象——再堆一个位置参数会让调用点
962
+ * 变成一串没有名字的布尔/数组。
963
+ */
964
+ send(conversationId, content, idempotencyKey, options = {}) {
965
+ const attachments = Array.isArray(options) ? options : options.attachments ?? [];
966
+ const mentions = Array.isArray(options) ? [] : options.mentions ?? [];
967
+ const model = Array.isArray(options) ? void 0 : options.model;
968
+ const reasoningEffort = Array.isArray(options) ? void 0 : options.reasoningEffort;
969
+ return this.transport.request("conversation/send", {
970
+ conversation_id: conversationId,
971
+ content,
972
+ idempotency_key: idempotencyKey,
973
+ ...attachments.length > 0 ? { attachments } : {},
974
+ ...mentions.length > 0 ? { mentions } : {},
975
+ ...model ? { model: {
976
+ provider_id: model.provider_id,
977
+ model: model.model
978
+ } } : {},
979
+ ...reasoningEffort ? { reasoning_effort: reasoningEffort } : {}
980
+ });
981
+ }
982
+ cancel(conversationId) {
983
+ return this.transport.request("conversation/cancel", { conversation_id: conversationId });
984
+ }
985
+ delete(conversationId) {
986
+ return this.transport.request("conversation/delete", { conversation_id: conversationId });
987
+ }
988
+ /**
989
+ * Subscribe to one conversation's realtime stream (doc `16` R1).
990
+ *
991
+ * `options.fetchMessages` is what makes auto catch-up possible: a detected
992
+ * sequence gap or a `conversation/resync-required` triggers one transcript
993
+ * fetch whose result is handed to `onBackfill`. Conversations have no event
994
+ * replay API (`05` §「V1 不提供 after_cursor 事件追平」), so the transcript is
995
+ * the recovery medium — unlike Runs, where `run/events` replays the events
996
+ * themselves.
997
+ */
998
+ async follow(conversationId, options = {}) {
999
+ await this.transport.request("conversation/subscribe", { conversation_id: conversationId });
1000
+ return new ConversationSubscription(this.transport, conversationId, {
1001
+ fetchMessages: (id) => this.messages({
1002
+ conversationId: id,
1003
+ pageSize: 50
1004
+ }),
1005
+ ...options
1006
+ });
1007
+ }
1008
+ };
1009
+ /** Events held before the first listener attaches (see `pendingLimit`). */
1010
+ const DEFAULT_PENDING_LIMIT = 256;
1011
+ var ConversationSubscription = class {
1012
+ transport;
1013
+ conversationId;
1014
+ options;
1015
+ events = /* @__PURE__ */ new Set();
1016
+ resyncs = /* @__PURE__ */ new Set();
1017
+ errors = /* @__PURE__ */ new Set();
1018
+ backfills = /* @__PURE__ */ new Set();
1019
+ closed = false;
1020
+ lastSeenSequence = 0;
1021
+ removeListener;
1022
+ /**
1023
+ * `follow()` resolves as soon as the server acknowledged the subscription,
1024
+ * but the caller attaches listeners one tick later. Events landing in that
1025
+ * window used to be dropped silently; they are held here (oldest first) and
1026
+ * flushed to the first `onEvent` listener.
1027
+ */
1028
+ pending = [];
1029
+ pendingDropped = 0;
1030
+ backfillInFlight = null;
1031
+ constructor(transport, conversationId, options = {}) {
1032
+ this.transport = transport;
1033
+ this.conversationId = conversationId;
1034
+ this.options = options;
1035
+ this.removeListener = transport.onNotification((notification) => this.dispatch(notification));
1036
+ }
1037
+ get lastSequence() {
1038
+ return this.lastSeenSequence;
1039
+ }
1040
+ /** Events dropped because nobody was listening and the buffer was full. */
1041
+ get droppedPendingCount() {
1042
+ return this.pendingDropped;
1043
+ }
1044
+ onEvent(listener) {
1045
+ this.events.add(listener);
1046
+ if (this.events.size === 1 && this.pending.length > 0) {
1047
+ const queued = this.pending;
1048
+ this.pending = [];
1049
+ for (const event of queued) this.deliver(event);
1050
+ }
1051
+ return () => this.events.delete(listener);
1052
+ }
1053
+ onResync(listener) {
1054
+ this.resyncs.add(listener);
1055
+ return () => this.resyncs.delete(listener);
1056
+ }
1057
+ /**
1058
+ * Auto catch-up result (R1). Fires only when `autoResync` is on and the
1059
+ * transcript fetch succeeded; the listener owns replacing the transcript.
1060
+ */
1061
+ onBackfill(listener) {
1062
+ this.backfills.add(listener);
1063
+ return () => this.backfills.delete(listener);
1064
+ }
1065
+ /**
1066
+ * Failures that used to be swallowed: the catch-up fetch, and a failed
1067
+ * re-subscribe during `rearm()`. Listener isolation keeps one bad listener
1068
+ * from hiding the error from the others.
1069
+ */
1070
+ onError(listener) {
1071
+ this.errors.add(listener);
1072
+ return () => this.errors.delete(listener);
1073
+ }
1074
+ async close() {
1075
+ if (this.closed) return;
1076
+ this.closed = true;
1077
+ this.removeListener?.();
1078
+ this.removeListener = null;
1079
+ this.events.clear();
1080
+ this.resyncs.clear();
1081
+ this.errors.clear();
1082
+ this.backfills.clear();
1083
+ this.pending = [];
1084
+ try {
1085
+ await this.transport.request("conversation/unsubscribe", { conversation_id: this.conversationId });
1086
+ } catch {}
1087
+ }
1088
+ /**
1089
+ * Re-arm after a reconnect (docs/agent-store/16 T8): re-register the
1090
+ * notification listener, reset the cursor and re-issue
1091
+ * `conversation/subscribe`. The server restarts a conversation's sequence at
1092
+ * 1 for a fresh subscription, which is exactly why the cursor resets. On a
1093
+ * failed re-subscribe the error reaches `onError` and the caller can still
1094
+ * backfill the outage window via `conversation/messages`.
1095
+ */
1096
+ async rearm() {
1097
+ if (this.closed) return;
1098
+ this.removeListener?.();
1099
+ this.removeListener = this.transport.onNotification((notification) => this.dispatch(notification));
1100
+ this.lastSeenSequence = 0;
1101
+ try {
1102
+ await this.transport.request("conversation/subscribe", { conversation_id: this.conversationId });
1103
+ } catch (error) {
1104
+ this.emitError(error);
1105
+ throw error;
1106
+ }
1107
+ }
1108
+ dispatch(notification) {
1109
+ if (notification.method === "conversation/event") {
1110
+ const event = notification.params;
1111
+ if (event.conversation_id !== this.conversationId) return;
1112
+ if (!Number.isFinite(event.sequence)) return;
1113
+ if (event.sequence <= this.lastSeenSequence) return;
1114
+ const gap = this.lastSeenSequence > 0 && event.sequence > this.lastSeenSequence + 1;
1115
+ this.lastSeenSequence = event.sequence;
1116
+ if (this.events.size === 0) this.buffer(event);
1117
+ else this.deliver(event);
1118
+ if (gap) {
1119
+ this.emitResync("gap");
1120
+ this.requestBackfill("gap");
1121
+ }
1122
+ return;
1123
+ }
1124
+ if (notification.method === "conversation/resync-required") {
1125
+ if (!notification.params.conversation_ids.includes(this.conversationId)) return;
1126
+ this.emitResync(notification.params.reason);
1127
+ this.requestBackfill(notification.params.reason);
1128
+ }
1129
+ }
1130
+ buffer(event) {
1131
+ const limit = this.options.pendingLimit ?? DEFAULT_PENDING_LIMIT;
1132
+ this.pending.push(event);
1133
+ while (this.pending.length > limit) {
1134
+ this.pending.shift();
1135
+ this.pendingDropped += 1;
1136
+ }
1137
+ }
1138
+ deliver(event) {
1139
+ for (const listener of [...this.events]) try {
1140
+ listener(event);
1141
+ } catch {}
1142
+ }
1143
+ emitResync(reason) {
1144
+ for (const listener of [...this.resyncs]) try {
1145
+ listener(reason);
1146
+ } catch {}
1147
+ }
1148
+ emitError(error) {
1149
+ for (const listener of [...this.errors]) try {
1150
+ listener(error);
1151
+ } catch {}
1152
+ }
1153
+ /**
1154
+ * One in-flight catch-up at a time: a burst of resync signals collapses into
1155
+ * a single transcript fetch (the fetch itself returns the newest page, so the
1156
+ * result supersedes every signal that arrived before it).
1157
+ */
1158
+ requestBackfill(reason) {
1159
+ if (this.closed || this.options.autoResync === false) return;
1160
+ const fetchMessages = this.options.fetchMessages;
1161
+ if (!fetchMessages || this.backfillInFlight) return;
1162
+ const task = (async () => {
1163
+ try {
1164
+ const page = await fetchMessages(this.conversationId);
1165
+ if (this.closed) return;
1166
+ const backfill = {
1167
+ conversationId: this.conversationId,
1168
+ reason,
1169
+ messages: page?.items ?? [],
1170
+ hasMore: page?.has_more === true
1171
+ };
1172
+ for (const listener of [...this.backfills]) try {
1173
+ listener(backfill);
1174
+ } catch {}
1175
+ } catch (error) {
1176
+ this.emitError(error);
1177
+ }
1178
+ })();
1179
+ this.backfillInFlight = task;
1180
+ task.finally(() => {
1181
+ if (this.backfillInFlight === task) this.backfillInFlight = null;
1182
+ });
1183
+ }
1184
+ };
1185
+ //#endregion
1186
+ //#region src/connectors.ts
1187
+ /**
1188
+ * How long {@link ConnectorClient.waitForAuth} waits by default: the host's own
1189
+ * callback window is 120s, so a shorter client budget would give up on flows the
1190
+ * host is still willing to finish.
1191
+ */
1192
+ const DEFAULT_AUTH_TIMEOUT_MS = 12e4;
1193
+ const DEFAULT_AUTH_POLL_MS = 500;
1194
+ var ConnectorClient = class {
1195
+ transport;
1196
+ constructor(transport) {
1197
+ this.transport = transport;
1198
+ }
1199
+ /** List the Agent Store Connector catalog (public summaries only). */
1200
+ list() {
1201
+ return this.transport.request("connector/list", {});
1202
+ }
1203
+ /** Fetch one Connector detail: namespaced tools + auth state. */
1204
+ get(connectorId) {
1205
+ return this.transport.request("connector/get", { connector_id: connectorId });
1206
+ }
1207
+ /** Combined auth × probe status. `connected` only when auth is ready AND the last probe succeeded. */
1208
+ status(connectorId) {
1209
+ return this.transport.request("connector/status", { connector_id: connectorId });
1210
+ }
1211
+ /**
1212
+ * Run a connection probe and report what the connector currently offers —
1213
+ * including each tool's `input_schema`.
1214
+ *
1215
+ * It really connects: a stdio connector gets its child process spawned, an
1216
+ * HTTP/SSE one gets `initialize` + `tools/list`. The result is **persisted**,
1217
+ * so this is also what refreshes the tool list `get()` reads back.
1218
+ *
1219
+ * Named `test` and not `probe` on purpose: this repo's split is `test` for the
1220
+ * action (wire `connector/test`, `test_connection`) and `probe` for its
1221
+ * artifact (`ConnectorProbeResult`, `probe_status`), and every method here is
1222
+ * the last segment of its wire method.
1223
+ */
1224
+ test(connectorId) {
1225
+ return this.transport.request("connector/test", { connector_id: connectorId });
1226
+ }
1227
+ /**
1228
+ * Check the current OAuth state for a connector.
1229
+ *
1230
+ * Read `error` too: a flow that failed after the browser step is reported as
1231
+ * `state: "not_authenticated"` **plus** `error`, because the wire has no
1232
+ * failing state to flip to. {@link waitForAuth} is the loop that does this.
1233
+ */
1234
+ authStatus(connectorId) {
1235
+ return this.transport.request("connector/auth/status", { connector_id: connectorId });
1236
+ }
1237
+ /**
1238
+ * The credential form a connector needs filled in, as **this caller** sees it
1239
+ * (doc `34` §6.1).
1240
+ *
1241
+ * `mode` says which kind of authentication applies (`none` / `oauth` / `token`),
1242
+ * `status` whether anything is still missing, and `missing` names the keys.
1243
+ * `fields` is everything needed to render the form — labels, placeholders,
1244
+ * descriptions and the marketplace's "where do I get a key" link, in both
1245
+ * languages. No secret ever comes back: only `plain` fields carry a `value`.
1246
+ */
1247
+ credentials(connectorId) {
1248
+ return this.transport.request("connector/credential/get", { connector_id: connectorId });
1249
+ }
1250
+ /**
1251
+ * Store what the user typed and get the new state back.
1252
+ *
1253
+ * Only keys the connector's own declaration names are accepted — the form is
1254
+ * the whole write surface. `secret` fields go to the host's credential store
1255
+ * under the caller's own namespace, `plain` fields into the connector's
1256
+ * configuration, so a shared host never resolves one user's token for another.
1257
+ */
1258
+ setCredentials(connectorId, values) {
1259
+ return this.transport.request("connector/credential/set", {
1260
+ connector_id: connectorId,
1261
+ values
1262
+ });
1263
+ }
1264
+ /**
1265
+ * Forget stored credentials. `keys` omitted = every secret field of this
1266
+ * connector. Idempotent: clearing what is not there succeeds.
1267
+ */
1268
+ clearCredentials(connectorId, keys) {
1269
+ return this.transport.request("connector/credential/clear", {
1270
+ connector_id: connectorId,
1271
+ ...keys ? { keys } : {}
1272
+ });
1273
+ }
1274
+ /**
1275
+ * Register a connector this host never imported — your own MCP server, handed
1276
+ * over together with the template that says what it needs (doc `34` §6.5).
1277
+ *
1278
+ * The **template is the whole declaration**: whatever `${secret:NAME}` the URL,
1279
+ * headers or env name becomes that connector's credential form, so there is no
1280
+ * `token-schema.json` to ship and nothing to install from a marketplace. The
1281
+ * connector's own non-secret settings travel in `values`.
1282
+ *
1283
+ * Secrets do **not** travel here: hand them over with {@link setCredentials},
1284
+ * which keeps exactly one write surface for values. The returned detail already
1285
+ * carries the credential block, so `missing` tells you which keys are still
1286
+ * needed.
1287
+ *
1288
+ * Re-registering the same name updates it; a builtin server's name is refused.
1289
+ * The row is created **disabled** — enabling it still requires a probe that
1290
+ * passes — and this method is on the installation-owner-only surface, because
1291
+ * choosing where the host reaches is the owner's call.
1292
+ */
1293
+ register(registration) {
1294
+ return this.transport.request("connector/register", registration);
1295
+ }
1296
+ /**
1297
+ * Kick off the OAuth browser flow on the trusted host. Returns the start
1298
+ * acknowledgement immediately; follow it with {@link waitForAuth}.
1299
+ *
1300
+ * `state: "error"` here is a failure that happened **before** the browser was
1301
+ * opened (endpoint discovery, client identity, binding the callback, launching
1302
+ * the browser): nothing was shown to the user, so `error` is the whole story
1303
+ * and there is nothing to wait for.
1304
+ */
1305
+ authStart(connectorId) {
1306
+ return this.transport.request("connector/auth/start", { connector_id: connectorId });
1307
+ }
1308
+ /**
1309
+ * Wait for an authorization that was started (in the browser, or by a
1310
+ * previous `authStart`) to finish — and **report why** when it does not.
1311
+ *
1312
+ * A hand-rolled `while (state !== "authenticated") poll()` loop ends in "it
1313
+ * never became authenticated", with the reason sitting unread in the status it
1314
+ * already fetched: a token exchange the authorization server refused (a
1315
+ * throttling gateway answers `slow_down`), a callback timeout, a CSRF/path
1316
+ * mismatch. Those land in `authStatus().error` *only*, and the state stays
1317
+ * `not_authenticated` — there is no failing state on the wire.
1318
+ *
1319
+ * Resolves for every domain outcome (`authenticated` / `error` / `timeout`);
1320
+ * rejects only when the status read itself fails (transport or protocol), the
1321
+ * same rule as `connector/call`. The wait is bounded by `timeoutMs`; a caller
1322
+ * that needs to stop earlier can race the returned promise.
1323
+ */
1324
+ async waitForAuth(connectorId, options = {}) {
1325
+ const deadline = Date.now() + (options.timeoutMs ?? DEFAULT_AUTH_TIMEOUT_MS);
1326
+ const pollMs = options.pollMs ?? DEFAULT_AUTH_POLL_MS;
1327
+ for (;;) {
1328
+ const status = await this.authStatus(connectorId);
1329
+ if (status.state === "authenticated") return { state: "authenticated" };
1330
+ if (status.error) return {
1331
+ state: "error",
1332
+ error: status.error
1333
+ };
1334
+ if (Date.now() >= deadline) return { state: "timeout" };
1335
+ await new Promise((resolve) => setTimeout(resolve, pollMs));
1336
+ }
1337
+ }
1338
+ /** Revoke the OAuth token for a connector. */
1339
+ logout(connectorId) {
1340
+ return this.transport.request("connector/auth/logout", { connector_id: connectorId }).then(() => void 0);
1341
+ }
1342
+ /**
1343
+ * Run one tool on a registered connector, through the host's own connection.
1344
+ *
1345
+ * The host holds the transport, its headers and its OAuth token; this sends a
1346
+ * tool name and an argument object and nothing else — you cannot name a URL,
1347
+ * a command or a header. Whether the pair is callable at all is the host's
1348
+ * `[connector_proxy]` policy: once its operator turns the proxy on, the
1349
+ * enabled connectors are callable, and `allow` / `deny` are that operator's
1350
+ * optional narrowing and subtraction. So `policy_denied` means either that
1351
+ * the host never turned the proxy on, or that this pair was narrowed out or
1352
+ * explicitly denied — not that a tool name was misspelled in a list you were
1353
+ * supposed to maintain.
1354
+ *
1355
+ * Arguments are passed through verbatim (there is no client-side schema
1356
+ * check); `get()` / `test()` return each tool's `input_schema` so you can see
1357
+ * what it takes, and a parameter the server rejects comes back as
1358
+ * `is_error: true`, not as a rejected promise.
1359
+ *
1360
+ * Resolves even when the tool itself failed — check `is_error`. It rejects
1361
+ * only when the call never reached the tool: `connector_call_timeout`,
1362
+ * `connector_call_failed`, `response_too_large`, `connector_unavailable`,
1363
+ * `policy_denied`, `not_found`.
1364
+ */
1365
+ call(connectorId, tool, args = {}) {
1366
+ return this.transport.request("connector/call", {
1367
+ connector_id: connectorId,
1368
+ tool,
1369
+ arguments: args
1370
+ });
1371
+ }
1372
+ };
1373
+ //#endregion
1374
+ //#region src/runs.ts
1375
+ /**
1376
+ * Run client: agent/run, run/get, run/result, run/events, run/cancel plus the
1377
+ * realtime `follow` subscription over the WebSocket transport.
1378
+ */
1379
+ var RunClient = class {
1380
+ transport;
1381
+ constructor(transport) {
1382
+ this.transport = transport;
1383
+ }
1384
+ /** Start an Agent run and return its asynchronous receipt. */
1385
+ agent(input) {
1386
+ return this.transport.request("agent/run", toWire(input));
1387
+ }
1388
+ /**
1389
+ * Start a Team run (`16` §7 决策 3): the server creates the Leader
1390
+ * Conversation, materializes/binds the Team's execution template and returns
1391
+ * the public run id of the execution the Leader started.
1392
+ *
1393
+ * The receipt is `TeamRunReceipt`, not `RunReceipt` — a Team Run has no lead
1394
+ * preset revision/digest to report. Member pool and concurrency limits are not
1395
+ * inputs; sending `planning` is rejected as `invalid_request`.
1396
+ */
1397
+ team(input) {
1398
+ return this.transport.request("team/run", toTeamWire(input));
1399
+ }
1400
+ /** Read the authoritative run state. */
1401
+ get(runId) {
1402
+ return this.transport.request("run/get", { run_id: runId });
1403
+ }
1404
+ /** Read the terminal result (fails until the run reaches a terminal state). */
1405
+ result(runId) {
1406
+ return this.transport.request("run/result", { run_id: runId });
1407
+ }
1408
+ /**
1409
+ * Read the authoritative plan / step snapshot (W4 / W6, resolves D-W6-1):
1410
+ * titles, statuses, retries with their reason, errors and timings — none of
1411
+ * which the append-only event log carries.
1412
+ */
1413
+ plan(runId) {
1414
+ return this.transport.request("run/plan", { run_id: runId });
1415
+ }
1416
+ /** Replay persisted events after a cursor. */
1417
+ events(query) {
1418
+ return this.transport.request("run/events", {
1419
+ run_id: query.runId,
1420
+ after_sequence: query.afterSequence,
1421
+ limit: query.limit
1422
+ });
1423
+ }
1424
+ /** Request cancellation; the terminal state is confirmed by events/run/get. */
1425
+ cancel(input) {
1426
+ return this.transport.request("run/cancel", {
1427
+ run_id: input.runId,
1428
+ expected_version: input.expectedVersion,
1429
+ command_id: input.commandId,
1430
+ idempotency_key: input.idempotencyKey
1431
+ });
1432
+ }
1433
+ /**
1434
+ * Inject steering text into the currently active step of a running Agent
1435
+ * (runtime durable conversation effect; server resolves the opaque step).
1436
+ */
1437
+ steer(input) {
1438
+ return this.transport.request("run/steer", {
1439
+ run_id: input.runId,
1440
+ text: input.text,
1441
+ expected_version: input.expectedVersion,
1442
+ command_id: input.commandId,
1443
+ idempotency_key: input.idempotencyKey
1444
+ });
1445
+ }
1446
+ /**
1447
+ * Answer the pending decision of a `waiting_input` attempt.
1448
+ *
1449
+ * The three `expected*Version` tokens are mandatory CAS inputs, normally the
1450
+ * ones projected on the pending `approval.requested` event. The server passes
1451
+ * them straight to the engine's single answer gate, so a stale token is a
1452
+ * `conflict`, never a silent overwrite. There is no `always_allow` here: the
1453
+ * desktop confirmation route's approve-all flag is not part of this protocol.
1454
+ */
1455
+ answerDecision(input) {
1456
+ return this.transport.request("run/answer-decision", {
1457
+ run_id: input.runId,
1458
+ step_id: input.stepId,
1459
+ attempt_id: input.attemptId,
1460
+ answer: input.answer,
1461
+ expected_execution_version: input.expectedExecutionVersion,
1462
+ expected_step_version: input.expectedStepVersion,
1463
+ expected_attempt_version: input.expectedAttemptVersion
1464
+ });
1465
+ }
1466
+ /** Subscribe to best-effort realtime events for one run. */
1467
+ async follow(runId, options) {
1468
+ const params = { run_id: runId };
1469
+ await this.transport.request("run/subscribe", params);
1470
+ const subscription = new EventSubscription(this.transport, runId, (afterSequence) => this.events({
1471
+ runId,
1472
+ afterSequence
1473
+ }), options);
1474
+ if (options?.catchUp !== false) await subscription.catchUp().catch(() => void 0);
1475
+ return subscription;
1476
+ }
1477
+ };
1478
+ /** Upper bound for the silent catch-up page (cursor baseline, not delivery). */
1479
+ const CATCH_UP_LIMIT = 500;
1480
+ /** Prune the dedup set past this size (sequences are per-run monotonic). */
1481
+ const SEEN_SET_PRUNE_AT = 2e3;
1482
+ const SEEN_SET_KEEP_BELOW_MAX = 500;
1483
+ var EventSubscription = class {
1484
+ transport;
1485
+ runId;
1486
+ fetchAfter;
1487
+ eventListeners = /* @__PURE__ */ new Set();
1488
+ resyncListeners = /* @__PURE__ */ new Set();
1489
+ errorListeners = /* @__PURE__ */ new Set();
1490
+ closed = false;
1491
+ unsubscribeNotification = null;
1492
+ lastSeenSequence = 0;
1493
+ seenSequences = /* @__PURE__ */ new Set();
1494
+ resyncInFlight = null;
1495
+ autoResync;
1496
+ constructor(transport, runId, fetchAfter, options) {
1497
+ this.transport = transport;
1498
+ this.runId = runId;
1499
+ this.fetchAfter = fetchAfter;
1500
+ this.autoResync = options?.autoResync !== false;
1501
+ this.unsubscribeNotification = transport.onNotification((notification) => {
1502
+ this.dispatch(notification);
1503
+ });
1504
+ }
1505
+ onEvent(listener) {
1506
+ this.eventListeners.add(listener);
1507
+ return () => {
1508
+ this.eventListeners.delete(listener);
1509
+ };
1510
+ }
1511
+ onResync(listener) {
1512
+ this.resyncListeners.add(listener);
1513
+ return () => {
1514
+ this.resyncListeners.delete(listener);
1515
+ };
1516
+ }
1517
+ onError(listener) {
1518
+ this.errorListeners.add(listener);
1519
+ return () => {
1520
+ this.errorListeners.delete(listener);
1521
+ };
1522
+ }
1523
+ get lastSequence() {
1524
+ return this.lastSeenSequence;
1525
+ }
1526
+ async close() {
1527
+ if (this.closed) return;
1528
+ this.closed = true;
1529
+ this.unsubscribeNotification?.();
1530
+ this.eventListeners.clear();
1531
+ this.resyncListeners.clear();
1532
+ this.errorListeners.clear();
1533
+ try {
1534
+ await this.transport.request("run/unsubscribe", { run_id: this.runId });
1535
+ } catch {}
1536
+ }
1537
+ dispatch(notification) {
1538
+ if (this.closed) return;
1539
+ if (notification.method === "event") {
1540
+ const event = notification.params;
1541
+ if (event.run_id !== this.runId) return;
1542
+ if (!Number.isFinite(event.sequence) || this.seenSequences.has(event.sequence)) return;
1543
+ this.markSeen(event.sequence);
1544
+ this.emitEvent(event);
1545
+ return;
1546
+ }
1547
+ if (notification.method === "run/resync-required") {
1548
+ const params = notification.params;
1549
+ if (params.run_ids.length > 0 && !params.run_ids.includes(this.runId)) return;
1550
+ for (const listener of [...this.resyncListeners]) try {
1551
+ listener(params);
1552
+ } catch {}
1553
+ if (this.autoResync && this.fetchAfter) this.resync().catch((error) => this.emitError(error));
1554
+ return;
1555
+ }
1556
+ }
1557
+ /**
1558
+ * Silent cursor baseline: mark already-persisted events seen WITHOUT
1559
+ * dispatching, so the first resync never replays history as "new".
1560
+ * For explicit history use `RunClient.events({ afterSequence: 0 })`.
1561
+ */
1562
+ async catchUp() {
1563
+ if (this.closed || !this.fetchAfter) return;
1564
+ const history = await this.fetchAfter(0);
1565
+ const page = Array.isArray(history) ? history.slice(0, CATCH_UP_LIMIT) : [];
1566
+ for (const event of page) if (event.run_id === this.runId && Number.isFinite(event.sequence)) this.markSeen(event.sequence);
1567
+ }
1568
+ /**
1569
+ *追平 missed events via `run/events` after the cursor. Returned (and
1570
+ * dispatched) in sequence order, deduplicated against live delivery.
1571
+ * Concurrent calls share one flight.
1572
+ */
1573
+ async resync() {
1574
+ if (this.closed) return [];
1575
+ if (this.resyncInFlight) return this.resyncInFlight;
1576
+ if (!this.fetchAfter) return [];
1577
+ const fetchAfter = this.fetchAfter;
1578
+ const task = (async () => {
1579
+ const missed = await fetchAfter(this.lastSeenSequence);
1580
+ const fresh = (Array.isArray(missed) ? [...missed] : []).filter((event) => event.run_id === this.runId && Number.isFinite(event.sequence)).sort((left, right) => left.sequence - right.sequence).filter((event) => {
1581
+ if (this.seenSequences.has(event.sequence)) return false;
1582
+ this.markSeen(event.sequence);
1583
+ return true;
1584
+ });
1585
+ for (const event of fresh) this.emitEvent(event);
1586
+ return fresh;
1587
+ })();
1588
+ this.resyncInFlight = task;
1589
+ try {
1590
+ return await task;
1591
+ } finally {
1592
+ if (this.resyncInFlight === task) this.resyncInFlight = null;
1593
+ }
1594
+ }
1595
+ /**
1596
+ * Re-arm after the connection was re-established (docs/agent-store/16 T8).
1597
+ * A new socket means the server dropped its subscriptions and the transport
1598
+ * dropped its notification listeners, so this:
1599
+ *
1600
+ * 1. re-registers this subscription's notification listener,
1601
+ * 2. resets the cursor to 0 and forgets the dedupe set, then
1602
+ * 3. re-issues `run/subscribe` and replays **all** persisted events.
1603
+ *
1604
+ * The replay is deliberate (the plan's "重连后重置 lastSeenSequence"): a
1605
+ * consumer must dedupe by `sequence`, since events seen before the outage
1606
+ * are delivered again. Call it only once the session handshake has completed
1607
+ * again — the transport's `open` event fires before `initialize`. A no-op
1608
+ * once closed.
1609
+ */
1610
+ async rearm() {
1611
+ if (this.closed || !this.fetchAfter) return [];
1612
+ this.unsubscribeNotification?.();
1613
+ this.unsubscribeNotification = this.transport.onNotification((notification) => {
1614
+ this.dispatch(notification);
1615
+ });
1616
+ this.lastSeenSequence = 0;
1617
+ this.seenSequences.clear();
1618
+ this.resyncInFlight = null;
1619
+ await this.transport.request("run/subscribe", { run_id: this.runId });
1620
+ return this.resync();
1621
+ }
1622
+ markSeen(sequence) {
1623
+ this.seenSequences.add(sequence);
1624
+ if (sequence > this.lastSeenSequence) this.lastSeenSequence = sequence;
1625
+ if (this.seenSequences.size > SEEN_SET_PRUNE_AT) {
1626
+ const floor = this.lastSeenSequence - SEEN_SET_KEEP_BELOW_MAX;
1627
+ for (const seen of this.seenSequences) if (seen < floor) this.seenSequences.delete(seen);
1628
+ }
1629
+ }
1630
+ emitEvent(event) {
1631
+ for (const listener of [...this.eventListeners]) try {
1632
+ listener(event);
1633
+ } catch {}
1634
+ }
1635
+ emitError(error) {
1636
+ for (const listener of [...this.errorListeners]) try {
1637
+ listener(error);
1638
+ } catch {}
1639
+ }
1640
+ /** Forward transport-level errors into subscription error listeners. */
1641
+ deliverError(error) {
1642
+ if (error instanceof _flowy_agent_store_protocol.TransportError) for (const listener of [...this.errorListeners]) try {
1643
+ listener(error);
1644
+ } catch {}
1645
+ }
1646
+ };
1647
+ function toWire(input) {
1648
+ return {
1649
+ agent_id: input.agentId,
1650
+ agent_version: input.agentVersion,
1651
+ goal: input.goal,
1652
+ input: input.input,
1653
+ workspace: input.workspaceId ? { id: input.workspaceId } : void 0,
1654
+ steps: input.steps,
1655
+ command_id: input.commandId,
1656
+ idempotency_key: input.idempotencyKey,
1657
+ mentions: input.mentions,
1658
+ model: input.model ? {
1659
+ provider_id: input.model.provider_id,
1660
+ model: input.model.model
1661
+ } : void 0,
1662
+ reasoning_effort: input.reasoningEffort || void 0
1663
+ };
1664
+ }
1665
+ function toTeamWire(input) {
1666
+ return {
1667
+ team_id: input.teamId,
1668
+ team_version: input.teamVersion,
1669
+ goal: input.goal,
1670
+ input: input.input,
1671
+ workspace: input.workspaceId ? { id: input.workspaceId } : void 0,
1672
+ command_id: input.commandId,
1673
+ idempotency_key: input.idempotencyKey
1674
+ };
1675
+ }
1676
+ //#endregion
1677
+ //#region src/store.ts
1678
+ var StoreError = class extends Error {
1679
+ code;
1680
+ constructor(code, message) {
1681
+ super(message);
1682
+ this.code = code;
1683
+ this.name = "StoreError";
1684
+ }
1685
+ };
1686
+ const DEFAULT_READY_TIMEOUT_MS = 3e4;
1687
+ const DEFAULT_READY_POLL_MS = 400;
1688
+ const DEFAULT_READY_POLL_MAX_MS = 4e3;
1689
+ const DEFAULT_READY_PROBE_MS = 5e3;
1690
+ /** Every string a `LocalizedText` carries, for haystack building. */
1691
+ function localizedValues(text) {
1692
+ if (typeof text === "string") return [text];
1693
+ if (!text || typeof text !== "object") return [];
1694
+ return Object.values(text).filter((value) => typeof value === "string");
1695
+ }
1696
+ function haystack(item) {
1697
+ return [
1698
+ item.entry_name,
1699
+ item.name,
1700
+ item.id,
1701
+ item.description ?? "",
1702
+ item.version,
1703
+ ...localizedValues(item.display_name),
1704
+ ...localizedValues(item.display_description),
1705
+ ...localizedValues(item.profession),
1706
+ ...(item.tags ?? []).flatMap(localizedValues)
1707
+ ].join("\n").toLowerCase();
1708
+ }
1709
+ function abortableDelay(ms, signal) {
1710
+ return new Promise((resolve, reject) => {
1711
+ if (signal?.aborted) {
1712
+ reject(new StoreError("aborted", "the operation was aborted"));
1713
+ return;
1714
+ }
1715
+ const timer = setTimeout(() => {
1716
+ signal?.removeEventListener("abort", onAbort);
1717
+ resolve();
1718
+ }, ms);
1719
+ const onAbort = () => {
1720
+ clearTimeout(timer);
1721
+ reject(new StoreError("aborted", "the operation was aborted"));
1722
+ };
1723
+ signal?.addEventListener("abort", onAbort, { once: true });
1724
+ });
1725
+ }
1726
+ var StoreClient = class {
1727
+ host;
1728
+ readyTimeoutMs;
1729
+ readyPollMs;
1730
+ readyPollMaxMs;
1731
+ readyProbeMs;
1732
+ constructor(host, options = {}) {
1733
+ this.host = host;
1734
+ this.readyTimeoutMs = options.readyTimeoutMs ?? DEFAULT_READY_TIMEOUT_MS;
1735
+ this.readyPollMs = options.readyPollMs ?? DEFAULT_READY_POLL_MS;
1736
+ this.readyPollMaxMs = options.readyPollMaxMs ?? DEFAULT_READY_POLL_MAX_MS;
1737
+ this.readyProbeMs = options.readyProbeMs ?? DEFAULT_READY_PROBE_MS;
1738
+ }
1739
+ /** Every item across enabled marketplaces, with install state. */
1740
+ async list() {
1741
+ return (await this.host.listStore()).items;
1742
+ }
1743
+ /** Case-insensitive substring search over the fields a user can see. */
1744
+ async search(query, filter = {}) {
1745
+ const needle = query.trim().toLowerCase();
1746
+ return (await this.list()).filter((item) => {
1747
+ if (filter.kind && item.kind !== filter.kind) return false;
1748
+ if (!needle) return true;
1749
+ return haystack(item).includes(needle);
1750
+ });
1751
+ }
1752
+ /** Items currently installed on this host. */
1753
+ async installed() {
1754
+ return (await this.list()).filter((item) => item.installed);
1755
+ }
1756
+ /**
1757
+ * The installed item that owns a given connector, or `null` when none does.
1758
+ *
1759
+ * A store item carries no field for the connector it installed — its
1760
+ * `entry_name` is the *market's* name (real example: the entry
1761
+ * `fenbi-baokao-decision` installs a server called `fenbi_mcp`), so a caller
1762
+ * holding an id from `connectors.list()` cannot reach the `StoreItem` that
1763
+ * `setEnabled()` / `uninstall()` require. `search()` cannot bridge that
1764
+ * either: it is a substring scan over catalogue text, and the runtime name is
1765
+ * not in it. This is the link, walked through the snapshot each installed item
1766
+ * already points at.
1767
+ *
1768
+ * Both identifiers a caller may hold are accepted: the connector's own `id`
1769
+ * (`connector/get`) and its `name` (`fenbi_mcp` — the MCP row's name and the
1770
+ * tool namespace's middle segment). Matching is exact; call `list()`/`search()`
1771
+ * when the question is "which catalogue entries mention X".
1772
+ */
1773
+ async findByConnector(connectorId) {
1774
+ const needle = connectorId.trim();
1775
+ if (!needle) return null;
1776
+ for (const item of await this.installed()) {
1777
+ const snapshotId = item.snapshot_id;
1778
+ if (!snapshotId) continue;
1779
+ if ((await this.host.getInstallStatus(snapshotId)).components.some((component) => component.kind === "connector" && (component.name === needle || component.id === needle))) return item;
1780
+ }
1781
+ return null;
1782
+ }
1783
+ /**
1784
+ * Installed items whose marketplace offers a different version.
1785
+ *
1786
+ * Both conditions matter: `update_available` is computed from the imported
1787
+ * snapshot even when nothing is installed, so filtering on it alone would
1788
+ * report an "update" for an entry the user has never installed.
1789
+ */
1790
+ async checkUpdates() {
1791
+ return (await this.list()).filter((item) => item.installed && item.update_available);
1792
+ }
1793
+ /**
1794
+ * What a caller can offer for an item's pending update.
1795
+ *
1796
+ * `"none"` means there is nothing to do; `"update"` means the entry is
1797
+ * installed at a different version and `update()` is the action that lands it
1798
+ * (`store/update-entry`, `36` D3); `"unknown"` is returned rather than
1799
+ * guessing when the item carries no snapshot to replace.
1800
+ */
1801
+ updateHint(item) {
1802
+ if (!item.installed || !item.update_available) return "none";
1803
+ return item.snapshot_id ? "update" : "unknown";
1804
+ }
1805
+ /**
1806
+ * Upgrade an installed item to the version its marketplace advertises.
1807
+ *
1808
+ * The host does the ordering: new version installed first, previous one
1809
+ * released only when that succeeded. So a failure resolves `ok: false` with
1810
+ * the previous installation still in place (`releasedCount === 0`) — the
1811
+ * caller never has to reason about a half-upgraded item.
1812
+ *
1813
+ * Refuses before the round trip when there is nothing to replace: an item that
1814
+ * is not installed (`not_installed`) or is already current is not a failure of
1815
+ * *this* call, and `updateHint()` is the way to ask first.
1816
+ */
1817
+ async update(item, options = {}) {
1818
+ this.assertUsable(item, options.signal);
1819
+ this.requireSnapshot(item);
1820
+ const result = await this.host.updateStoreEntry(item.marketplace_id, item.entry_name);
1821
+ const components = result.outcomes ?? [];
1822
+ const outcome = {
1823
+ snapshotId: result.snapshot_id,
1824
+ reused: result.reused,
1825
+ components,
1826
+ ok: result.errors.length === 0 && components.every((component) => component.ok),
1827
+ errors: result.errors,
1828
+ warnings: result.warnings,
1829
+ installedCount: result.installed_count,
1830
+ fromVersion: result.previous_version ?? void 0,
1831
+ toVersion: result.version,
1832
+ releasedCount: result.released_count ?? 0
1833
+ };
1834
+ if (options.waitForReady === false) return outcome;
1835
+ return this.awaitReady(outcome, options);
1836
+ }
1837
+ /**
1838
+ * Import (when needed) and install one store item, then wait until it is
1839
+ * usable.
1840
+ */
1841
+ async install(item, options = {}) {
1842
+ this.assertUsable(item, options.signal);
1843
+ const result = await this.host.installStoreEntry(item.marketplace_id, item.entry_name);
1844
+ const components = result.outcomes ?? [];
1845
+ const outcome = {
1846
+ snapshotId: result.snapshot_id,
1847
+ reused: result.reused,
1848
+ components,
1849
+ ok: result.errors.length === 0 && components.every((component) => component.ok),
1850
+ errors: result.errors,
1851
+ warnings: result.warnings,
1852
+ installedCount: result.installed_count
1853
+ };
1854
+ if (options.waitForReady === false) return outcome;
1855
+ return this.awaitReady(outcome, options);
1856
+ }
1857
+ /**
1858
+ * Release an item's runtime artifacts and clear its install record.
1859
+ *
1860
+ * The server only clears the record for components it actually released, so a
1861
+ * partial failure leaves `ok: false` with the failing component named — and
1862
+ * the item stays installed, which is what makes a retry meaningful.
1863
+ */
1864
+ async uninstall(item, options = {}) {
1865
+ const snapshotId = this.requireSnapshot(item);
1866
+ const componentIds = options.componentIds ?? await this.installedComponentIds(snapshotId);
1867
+ if (componentIds.length === 0) return {
1868
+ snapshotId,
1869
+ reused: true,
1870
+ components: [],
1871
+ ok: true,
1872
+ errors: [],
1873
+ warnings: []
1874
+ };
1875
+ const status = await this.host.uninstallInstall(snapshotId, componentIds);
1876
+ return this.outcomeFromStatus(snapshotId, status, false);
1877
+ }
1878
+ /**
1879
+ * Switch an item's components on or off.
1880
+ *
1881
+ * Idempotent: the server sets the state it is asked for instead of toggling.
1882
+ * A skill reports `skill_disable_flag_only` because the skill corpus has no
1883
+ * enable state — that code is passed through, not swallowed, so a caller can
1884
+ * tell "switched off" from "marked in the catalogue".
1885
+ */
1886
+ async setEnabled(item, enabled, options = {}) {
1887
+ const snapshotId = this.requireSnapshot(item);
1888
+ const componentIds = options.componentIds ?? await this.installedComponentIds(snapshotId);
1889
+ if (componentIds.length === 0) return {
1890
+ snapshotId,
1891
+ reused: true,
1892
+ components: [],
1893
+ ok: true,
1894
+ errors: [],
1895
+ warnings: []
1896
+ };
1897
+ const status = enabled ? await this.host.enableInstall(snapshotId, componentIds) : await this.host.disableInstall(snapshotId, componentIds);
1898
+ return this.outcomeFromStatus(snapshotId, status, false);
1899
+ }
1900
+ assertUsable(item, signal) {
1901
+ if (signal?.aborted) throw new StoreError("aborted", "the operation was aborted before it started");
1902
+ if (item.blocked_reason) throw new StoreError("item_blocked", item.blocked_reason);
1903
+ }
1904
+ requireSnapshot(item) {
1905
+ const snapshotId = item.snapshot_id;
1906
+ if (!snapshotId) throw new StoreError("not_installed", `store item ${item.id} has no imported snapshot to act on`);
1907
+ return snapshotId;
1908
+ }
1909
+ async installedComponentIds(snapshotId) {
1910
+ return (await this.host.getInstallStatus(snapshotId)).components.filter((component) => component.state !== "not-installed").map((component) => component.id);
1911
+ }
1912
+ outcomeFromStatus(snapshotId, status, reused) {
1913
+ const components = status.outcomes ?? [];
1914
+ const errors = status.errors ?? [];
1915
+ return {
1916
+ snapshotId,
1917
+ reused,
1918
+ components,
1919
+ ok: errors.length === 0 && components.every((component) => component.ok),
1920
+ errors,
1921
+ warnings: []
1922
+ };
1923
+ }
1924
+ /**
1925
+ * Poll until every component of the snapshot is usable.
1926
+ *
1927
+ * A timeout does **not** discard the install: the caller gets the successful
1928
+ * install plus `ready: false` and `readyIssue: "ready_timeout"`, so a slow
1929
+ * connector never turns into "the install failed".
1930
+ */
1931
+ async awaitReady(outcome, options) {
1932
+ const snapshotId = outcome.snapshotId;
1933
+ if (!snapshotId) return outcome;
1934
+ const status = await this.host.getInstallStatus(snapshotId);
1935
+ const connectors = status.components.filter((component) => component.kind === "connector" && component.state !== "not-installed");
1936
+ if (connectors.length > 0) await this.host.enableInstall(snapshotId, connectors.map((component) => component.id));
1937
+ const deadline = Date.now() + (options.timeoutMs ?? this.readyTimeoutMs);
1938
+ let interval = this.readyPollMs;
1939
+ let nextProbeAt = 0;
1940
+ let last = status.components;
1941
+ for (;;) {
1942
+ const now = Date.now();
1943
+ const allowProbe = now >= nextProbeAt;
1944
+ if (allowProbe) nextProbeAt = now + this.readyProbeMs;
1945
+ const blocked = (await Promise.all(last.filter((component) => component.state !== "not-installed").map(async (component) => ({
1946
+ component,
1947
+ verdict: await this.readiness(component, allowProbe)
1948
+ })))).find((entry) => !entry.verdict.ready);
1949
+ if (!blocked) return {
1950
+ ...outcome,
1951
+ ready: true
1952
+ };
1953
+ if (!blocked.verdict.ready && blocked.verdict.terminal) return {
1954
+ ...outcome,
1955
+ ready: false,
1956
+ readyIssue: blocked.verdict.terminal,
1957
+ readyComponentId: blocked.component.id
1958
+ };
1959
+ if (Date.now() >= deadline) return {
1960
+ ...outcome,
1961
+ ready: false,
1962
+ readyIssue: "ready_timeout",
1963
+ readyComponentId: blocked.component.id
1964
+ };
1965
+ await abortableDelay(interval, options.signal);
1966
+ interval = Math.min(interval * 2, this.readyPollMaxMs);
1967
+ last = (await this.host.getInstallStatus(snapshotId)).components;
1968
+ }
1969
+ }
1970
+ /**
1971
+ * Is this one component usable yet?
1972
+ *
1973
+ * Each kind answers with the narrowest read the protocol already offers. An
1974
+ * unknown kind is reported ready rather than blocking: this layer cannot judge
1975
+ * a component it does not understand, and a false timeout on a future kind
1976
+ * would be worse than passing it through.
1977
+ *
1978
+ * `allowProbe` gates the one kind whose readiness needs a real connection (a
1979
+ * connector); the other kinds answer from local reads, so the cadence does not
1980
+ * apply to them.
1981
+ */
1982
+ async readiness(component, allowProbe) {
1983
+ try {
1984
+ switch (component.kind) {
1985
+ case "skill": return (await this.host.skills.list()).some((skill) => skill.name === component.name) ? { ready: true } : { ready: false };
1986
+ case "agent": return (await this.host.agents.get(component.id)).preset_id ? { ready: true } : { ready: false };
1987
+ case "team":
1988
+ await this.host.teams.get(component.id);
1989
+ return { ready: true };
1990
+ case "connector": {
1991
+ if (allowProbe && (await this.host.connectors.test(component.id)).success) return { ready: true };
1992
+ const status = await this.host.connectors.status(component.id);
1993
+ if (status.status === "connected") return { ready: true };
1994
+ if (status.status === "authorization_required" || status.status === "reauthorization_required") return {
1995
+ ready: false,
1996
+ terminal: "authorization_required"
1997
+ };
1998
+ return { ready: false };
1999
+ }
2000
+ default: return { ready: true };
2001
+ }
2002
+ } catch {
2003
+ return { ready: false };
2004
+ }
2005
+ }
2006
+ };
2007
+ //#endregion
2008
+ //#region src/teams.ts
2009
+ var TeamClient = class {
2010
+ transport;
2011
+ constructor(transport) {
2012
+ this.transport = transport;
2013
+ }
2014
+ /** List AgentTeamDefinitions (fixed-member rosters). */
2015
+ list() {
2016
+ return this.transport.request("team/list", {});
2017
+ }
2018
+ /** Fetch one TeamDefinition; planning-context bodies are never returned. */
2019
+ get(teamId) {
2020
+ return this.transport.request("team/get", { team_id: teamId });
2021
+ }
2022
+ /**
2023
+ * Export one TeamDefinition as a portable {@link ExpertPack}, with every member
2024
+ * expanded and the **leader first**.
2025
+ *
2026
+ * All-or-nothing: if any member is uninstalled, disabled, or refused by the
2027
+ * host's `[expert_export]` table, the whole call fails rather than returning a
2028
+ * partial roster — the same posture `team/run` takes before it creates
2029
+ * anything. `teamVersion` is an optional pin; a mismatch is `version_mismatch`.
2030
+ */
2031
+ export(teamId, teamVersion) {
2032
+ return this.transport.request("team/export", {
2033
+ team_id: teamId,
2034
+ team_version: teamVersion
2035
+ });
2036
+ }
2037
+ };
2038
+ //#endregion
2039
+ //#region src/models.ts
2040
+ var ModelClient = class {
2041
+ transport;
2042
+ constructor(transport) {
2043
+ this.transport = transport;
2044
+ }
2045
+ /** Enabled providers' models; `is_default` marks the `agent/run` fallback. */
2046
+ async list() {
2047
+ return (await this.transport.request("models/list", {})).items;
2048
+ }
2049
+ };
2050
+ //#endregion
2051
+ //#region src/workspaces.ts
2052
+ var WorkspaceClient = class {
2053
+ transport;
2054
+ constructor(transport) {
2055
+ this.transport = transport;
2056
+ }
2057
+ list() {
2058
+ return this.transport.request("workspace/list", {});
2059
+ }
2060
+ /**
2061
+ * Validate + register (idempotently per canonical root) an absolute local
2062
+ * directory chosen by the owner. The server canonicalizes the path and
2063
+ * rejects links/reparse points; the returned view only carries the
2064
+ * canonical path back to this same authenticated connection.
2065
+ */
2066
+ create(path) {
2067
+ return this.transport.request("workspace/create", { path });
2068
+ }
2069
+ /**
2070
+ * Revoke (soft-delete) an owner's active workspace. Its conversations are
2071
+ * kept and their `workspace_id` reference is preserved, so re-registering
2072
+ * the same path re-activates the same workspace and those conversations
2073
+ * become visible again.
2074
+ */
2075
+ revoke(workspaceId) {
2076
+ return this.transport.request("workspace/revoke", { workspace_id: workspaceId });
2077
+ }
2078
+ };
2079
+ //#endregion
2080
+ //#region src/client.ts
2081
+ /**
2082
+ * Typed App Server client (`@flowy-agent-store/client`).
2083
+ *
2084
+ * Transport-agnostic: all business methods go through the injected
2085
+ * `Transport` (07 §2.3). Web-only helpers (asset URLs, `/api/fs/browse`,
2086
+ * one-shot HTTP registration) live in the host app, not here.
2087
+ *
2088
+ * Connection lifecycle: transport connect → `initialize` → protocol version
2089
+ * check → `initialized` notification → ready. Business methods require a
2090
+ * ready client; the server rejects them with `not_initialized` otherwise.
2091
+ */
2092
+ var AppServerClient = class {
2093
+ transport;
2094
+ /** Legacy preset/execution workflow client. */
2095
+ runs;
2096
+ /** Persistent presetless Nomi chat client. */
2097
+ conversations;
2098
+ /** Owner-scoped workspace registry client (user-chosen paths). */
2099
+ workspaces;
2100
+ /** Agent Store Skill catalog client (`skill/list`, `skill/get`). */
2101
+ skills;
2102
+ /** Agent Store Connector catalog/status/probe/OAuth client. */
2103
+ connectors;
2104
+ /** Agent Store AgentDefinition catalog client (`agent/list`, `agent/get`). */
2105
+ agents;
2106
+ /** Agent Store Team catalog client (`team/list`, `team/get`). */
2107
+ teams;
2108
+ /** Public model directory (`models/list`, REQ-PAR-05b). */
2109
+ models;
2110
+ /**
2111
+ * Agent Store lifecycle as one state machine: search → install (wait until
2112
+ * usable) → enable/disable → uninstall. Composes the flat methods below; it
2113
+ * adds no wire surface.
2114
+ */
2115
+ store;
2116
+ clientInfo;
2117
+ capabilities;
2118
+ initializeResult = null;
2119
+ notificationListeners = /* @__PURE__ */ new Set();
2120
+ /** Transport → client notification bridge; re-armed after a transport close. */
2121
+ unbridgeNotifications = null;
2122
+ constructor(options) {
2123
+ this.clientInfo = options.client;
2124
+ this.capabilities = options.capabilities;
2125
+ this.transport = options.transport;
2126
+ this.runs = new RunClient(this.transport);
2127
+ this.conversations = new ConversationClient(this.transport);
2128
+ this.workspaces = new WorkspaceClient(this.transport);
2129
+ this.skills = new SkillClient(this.transport);
2130
+ this.connectors = new ConnectorClient(this.transport);
2131
+ this.agents = new AgentClient(this.transport);
2132
+ this.teams = new TeamClient(this.transport);
2133
+ this.models = new ModelClient(this.transport);
2134
+ this.store = new StoreClient(this, options.store);
2135
+ this.bridgeNotifications();
2136
+ }
2137
+ /**
2138
+ * Subscribe the client-level fan-out on the transport exactly once.
2139
+ * `transport.close()` drops its listeners, so this is also called on
2140
+ * `connect()`: otherwise a reconnect would leave notifications silent.
2141
+ */
2142
+ bridgeNotifications() {
2143
+ if (this.unbridgeNotifications) return;
2144
+ this.unbridgeNotifications = this.transport.onNotification((notification) => {
2145
+ for (const listener of [...this.notificationListeners]) try {
2146
+ listener(notification);
2147
+ } catch {}
2148
+ });
2149
+ }
2150
+ get ready() {
2151
+ return this.initializeResult !== null;
2152
+ }
2153
+ get initializeInfo() {
2154
+ return this.initializeResult;
2155
+ }
2156
+ /** Connect and perform the initialize/initialized handshake. */
2157
+ async connect() {
2158
+ this.bridgeNotifications();
2159
+ await this.transport.connect();
2160
+ const request = {
2161
+ protocol_version: _flowy_agent_store_protocol.APP_SERVER_PROTOCOL_VERSION,
2162
+ client: this.clientInfo,
2163
+ capabilities: this.capabilities
2164
+ };
2165
+ const result = await this.transport.request("initialize", request);
2166
+ if (result.protocol_version !== _flowy_agent_store_protocol.APP_SERVER_PROTOCOL_VERSION) throw new _flowy_agent_store_protocol.ProtocolError("version_mismatch", `server protocol version ${result.protocol_version} is not supported (client ${_flowy_agent_store_protocol.APP_SERVER_PROTOCOL_VERSION})`);
2167
+ this.transport.notify("initialized", {});
2168
+ this.initializeResult = result;
2169
+ return result;
2170
+ }
2171
+ onNotification(listener) {
2172
+ this.notificationListeners.add(listener);
2173
+ return () => {
2174
+ this.notificationListeners.delete(listener);
2175
+ };
2176
+ }
2177
+ /** Close the transport; the server revokes the connection immediately. */
2178
+ close() {
2179
+ this.unbridgeNotifications?.();
2180
+ this.unbridgeNotifications = null;
2181
+ this.transport.close();
2182
+ this.initializeResult = null;
2183
+ this.notificationListeners.clear();
2184
+ }
2185
+ /** Import a local CodeBuddy/WorkBuddy source directory (roadmap Phase 1). */
2186
+ async runImport(input) {
2187
+ return this.transport.request("import/run", input);
2188
+ }
2189
+ async listImports() {
2190
+ return this.transport.request("import/list", {});
2191
+ }
2192
+ /** One immutable snapshot with its standardized components. */
2193
+ async getImport(snapshotId) {
2194
+ return this.transport.request("import/get", { snapshot_id: snapshotId });
2195
+ }
2196
+ /**
2197
+ * The plugin-level asset inventory of one snapshot (`import/assets`, doc 38):
2198
+ * the `avatars/`, `bin/` and `scripts/` files beside the skills, sorted by
2199
+ * path with a sha256 each. This is the same address space `readImportAsset`
2200
+ * accepts and an `ExpertPack.assets` names — one provider, one whitelist.
2201
+ * Check `capabilities.snapshot_assets` first.
2202
+ */
2203
+ async listImportAssets(snapshotId) {
2204
+ return this.transport.request("import/assets", { snapshot_id: snapshotId });
2205
+ }
2206
+ /**
2207
+ * One plugin-level asset's bytes, over the WebSocket binding (`import/asset`,
2208
+ * doc 38). Base64 on the wire, decoded to `Uint8Array` here — the same
2209
+ * envelope `skills.readFile()` decodes. Persona text and skill bodies are
2210
+ * deliberately not reachable through this face.
2211
+ */
2212
+ async readImportAsset(snapshotId, path) {
2213
+ return decodeBase64((await this.transport.request("import/asset", {
2214
+ snapshot_id: snapshotId,
2215
+ path
2216
+ })).content);
2217
+ }
2218
+ /** Install a snapshot into the runtime (roadmap Phase 2). */
2219
+ async runInstall(input) {
2220
+ return this.transport.request("install/run", input);
2221
+ }
2222
+ /** Per-component installation state of a snapshot. */
2223
+ async getInstallStatus(snapshotId) {
2224
+ return this.transport.request("install/status", { snapshot_id: snapshotId });
2225
+ }
2226
+ /**
2227
+ * Disable installed components.
2228
+ *
2229
+ * The runtime state moves with the flag — a connector's MCP server is switched
2230
+ * off, an expert's Preset is disabled — while the artifacts stay on disk. A
2231
+ * skill is the documented exception: the corpus has no enable state, so it
2232
+ * reports `skill_disable_flag_only` and the flag stays a catalogue marker.
2233
+ */
2234
+ async disableInstall(snapshotId, componentIds) {
2235
+ return this.transport.request("install/disable", {
2236
+ snapshot_id: snapshotId,
2237
+ component_ids: componentIds
2238
+ });
2239
+ }
2240
+ /** Re-enable previously disabled components. */
2241
+ async enableInstall(snapshotId, componentIds) {
2242
+ return this.transport.request("install/enable", {
2243
+ snapshot_id: snapshotId,
2244
+ component_ids: componentIds
2245
+ });
2246
+ }
2247
+ /** Uninstall: remove runtime artifacts and clear the state (snapshot kept). */
2248
+ async uninstallInstall(snapshotId, componentIds) {
2249
+ return this.transport.request("install/uninstall", {
2250
+ snapshot_id: snapshotId,
2251
+ component_ids: componentIds
2252
+ });
2253
+ }
2254
+ /** Register a marketplace source on the trusted host. */
2255
+ async addMarketplace(input) {
2256
+ return this.transport.request("market/add", input);
2257
+ }
2258
+ /** All active marketplaces. */
2259
+ async listMarketplaces() {
2260
+ return this.transport.request("market/list", {});
2261
+ }
2262
+ /** One marketplace with its discovered entries. */
2263
+ async getMarketplace(marketplaceId) {
2264
+ return this.transport.request("market/get", { marketplace_id: marketplaceId });
2265
+ }
2266
+ /** Remove a marketplace (`cascade` uninstalls snapshots installed from it). */
2267
+ async removeMarketplace(marketplaceId, cascade) {
2268
+ return this.transport.request("market/remove", {
2269
+ marketplace_id: marketplaceId,
2270
+ cascade
2271
+ });
2272
+ }
2273
+ /** Toggle auto-update for a marketplace. */
2274
+ async setMarketplaceAutoUpdate(marketplaceId, enabled) {
2275
+ return this.transport.request("market/auto-update", {
2276
+ marketplace_id: marketplaceId,
2277
+ enabled
2278
+ });
2279
+ }
2280
+ /** Re-fetch a marketplace source and rebuild entries when the revision changed. */
2281
+ async refreshMarketplace(marketplaceId) {
2282
+ return this.transport.request("market/refresh", { marketplace_id: marketplaceId });
2283
+ }
2284
+ /** Import one discovered marketplace entry (provenance linked). */
2285
+ async importMarketplaceEntry(marketplaceId, entryName) {
2286
+ return this.transport.request("market/entry-import", {
2287
+ marketplace_id: marketplaceId,
2288
+ entry_name: entryName
2289
+ });
2290
+ }
2291
+ /**
2292
+ * The host's market policy: sweep cadence, auto-upgrade kind whitelist and the
2293
+ * last sweep's reading (doc `37` §3.2).
2294
+ *
2295
+ * Host-level rather than per-market: it answers "when does the sweep run and
2296
+ * what may it touch", while {@link setMarketplaceAutoUpdate} answers "which
2297
+ * market participates".
2298
+ */
2299
+ async getMarketSettings() {
2300
+ return this.transport.request("market/settings", {});
2301
+ }
2302
+ /**
2303
+ * Update the host's market policy. An absent field is left alone; the host
2304
+ * writes the value into its `config.toml` and answers with the file re-read,
2305
+ * so a caller can never see an optimistic echo.
2306
+ */
2307
+ async setMarketSettings(patch) {
2308
+ return this.transport.request("market/settings-set", patch);
2309
+ }
2310
+ /** All store items across enabled marketplaces (display + install state). */
2311
+ async listStore() {
2312
+ return this.transport.request("store/list", {});
2313
+ }
2314
+ /** One-click install: import (when missing) + register in one call. */
2315
+ async installStoreEntry(marketplaceId, entryName) {
2316
+ return this.transport.request("store/install-entry", {
2317
+ marketplace_id: marketplaceId,
2318
+ entry_name: entryName
2319
+ });
2320
+ }
2321
+ /**
2322
+ * Upgrade an installed entry to the version its marketplace advertises.
2323
+ *
2324
+ * The host installs the new version before releasing the old one, so a failure
2325
+ * leaves the previous installation usable (`released_count: 0` is the machine
2326
+ * readable form of that). Refused for an entry that is not installed — this is
2327
+ * never a second way to install something.
2328
+ */
2329
+ async updateStoreEntry(marketplaceId, entryName) {
2330
+ return this.transport.request("store/update-entry", {
2331
+ marketplace_id: marketplaceId,
2332
+ entry_name: entryName
2333
+ });
2334
+ }
2335
+ };
2336
+ //#endregion
2337
+ //#region src/conversation-handle.ts
2338
+ /** How long `send` waits for a terminal event before returning a still-open turn. */
2339
+ const TURN_TIMEOUT_MS = 6e5;
2340
+ function stringValue(value) {
2341
+ return typeof value === "string" ? value : void 0;
2342
+ }
2343
+ /** Aggregate assistant text from `message.delta` events (fallback path). */
2344
+ function aggregateAssistantText(events) {
2345
+ const byId = /* @__PURE__ */ new Map();
2346
+ for (const event of events) {
2347
+ if (event.event_type !== "message.delta") continue;
2348
+ if (typeof event.payload["content"] !== "string") continue;
2349
+ const id = stringValue(event.payload["message_id"]) ?? "";
2350
+ if (!id) continue;
2351
+ byId.set(id, (byId.get(id) ?? "") + event.payload["content"]);
2352
+ }
2353
+ let last = null;
2354
+ for (const value of byId.values()) if (value) last = value;
2355
+ return last;
2356
+ }
2357
+ function lastUsage(events) {
2358
+ for (let index = events.length - 1; index >= 0; index -= 1) if (events[index].event_type === "context.usage") {
2359
+ const value = events[index].payload["context_usage"];
2360
+ return value && typeof value === "object" ? value : null;
2361
+ }
2362
+ }
2363
+ var ConversationHandle = class ConversationHandle {
2364
+ conversationId;
2365
+ conversations;
2366
+ subscription;
2367
+ constructor(conversations, conversationId, subscription) {
2368
+ this.conversations = conversations;
2369
+ this.conversationId = conversationId;
2370
+ this.subscription = subscription;
2371
+ }
2372
+ /** Create a new conversation and subscribe to its live event stream. */
2373
+ static async open(conversations, input) {
2374
+ const view = await conversations.create(input ?? {});
2375
+ const subscription = await conversations.follow(view.conversation_id);
2376
+ return new ConversationHandle(conversations, view.conversation_id, subscription);
2377
+ }
2378
+ /** Attach to an existing conversation (by id) and subscribe. */
2379
+ static async attach(conversations, conversationId) {
2380
+ const subscription = await conversations.follow(conversationId);
2381
+ return new ConversationHandle(conversations, conversationId, subscription);
2382
+ }
2383
+ /** Register a listener for every conversation event (returns an unsubscribe). */
2384
+ onEvent(listener) {
2385
+ return this.subscription.onEvent(listener);
2386
+ }
2387
+ /**
2388
+ * Send a message (a "turn") and await its terminal state. The event collector
2389
+ * is installed before the send so streaming deltas / usage are captured even
2390
+ * when they arrive before the send acknowledgment is parsed.
2391
+ */
2392
+ async send(content, options = {}) {
2393
+ const collected = [];
2394
+ let terminalResolve;
2395
+ let terminal = false;
2396
+ const terminalPromise = new Promise((resolve) => {
2397
+ terminalResolve = resolve;
2398
+ });
2399
+ const markTerminal = () => {
2400
+ if (terminal) return;
2401
+ terminal = true;
2402
+ terminalResolve();
2403
+ };
2404
+ const off = this.subscription.onEvent((event) => {
2405
+ collected.push(event);
2406
+ if (event.event_type === "turn.status" && stringValue(event.payload["status"]) === "completed") markTerminal();
2407
+ else if (event.event_type === "message.error") markTerminal();
2408
+ });
2409
+ let receipt;
2410
+ try {
2411
+ receipt = await this.conversations.send(this.conversationId, content, options.idempotencyKey ?? crypto.randomUUID());
2412
+ if (!receipt.completed && !terminal) await Promise.race([terminalPromise, new Promise((resolve) => setTimeout(resolve, TURN_TIMEOUT_MS))]);
2413
+ } finally {
2414
+ off();
2415
+ }
2416
+ return {
2417
+ conversation_id: this.conversationId,
2418
+ content,
2419
+ message_id: receipt.message_id,
2420
+ turn_id: receipt.turn_id,
2421
+ accepted: receipt.accepted,
2422
+ completed: receipt.completed || terminal,
2423
+ events: collected,
2424
+ assistant_text: receipt.result_text ?? aggregateAssistantText(collected),
2425
+ isError: Boolean(receipt.result_error) || collected.some((event) => event.event_type === "message.error"),
2426
+ result_error: receipt.result_error ?? null,
2427
+ result_error_code: receipt.result_error_code ?? null,
2428
+ usage: lastUsage(collected)
2429
+ };
2430
+ }
2431
+ /** Cancel the in-flight turn (idempotent; erases the current transcript row). */
2432
+ cancel() {
2433
+ return this.conversations.cancel(this.conversationId).then(() => void 0);
2434
+ }
2435
+ /** Fetch the transcript (paginated). */
2436
+ messages(query) {
2437
+ return this.conversations.messages({
2438
+ conversationId: this.conversationId,
2439
+ ...query
2440
+ });
2441
+ }
2442
+ /** Unsubscribe and release the handle. */
2443
+ close() {
2444
+ return this.subscription.close();
2445
+ }
2446
+ };
2447
+ /** Convenience: simple transcript snapshot (all roles, newest last). */
2448
+ function conversationMessages(page) {
2449
+ return page.items;
2450
+ }
2451
+ //#endregion
2452
+ //#region src/turn-result.ts
2453
+ const KIND_BY_EVENT = {
2454
+ "run.plan_changed": "plan",
2455
+ "task.updated": "task",
2456
+ "attempt.updated": "attempt",
2457
+ "run.status_changed": "status",
2458
+ "approval.requested": "approval",
2459
+ "approval.responded": "approval"
2460
+ };
2461
+ function asRecord(payload) {
2462
+ return payload && typeof payload === "object" ? payload : {};
2463
+ }
2464
+ function pickString(source, key) {
2465
+ const value = source[key];
2466
+ return typeof value === "string" ? value : void 0;
2467
+ }
2468
+ function toItem(event) {
2469
+ const payload = asRecord(event.payload);
2470
+ return {
2471
+ sequence: event.sequence,
2472
+ event_type: event.event_type,
2473
+ kind: KIND_BY_EVENT[event.event_type] ?? "other",
2474
+ ref_id: pickString(payload, "attempt_id") ?? pickString(payload, "step_id"),
2475
+ status: pickString(payload, "status"),
2476
+ reason: pickString(payload, "reason") ?? pickString(payload, "change")
2477
+ };
2478
+ }
2479
+ function extractUsage(events) {
2480
+ for (const event of events) {
2481
+ const payload = asRecord(event.payload);
2482
+ const candidate = asRecord(payload["usage"] ?? payload["context_usage"]);
2483
+ const input = candidate["input_tokens"] ?? candidate["inputTokens"];
2484
+ const output = candidate["output_tokens"] ?? candidate["outputTokens"];
2485
+ const total = candidate["total_tokens"] ?? candidate["totalTokens"];
2486
+ if (typeof input === "number" || typeof output === "number" || typeof total === "number") return {
2487
+ ...typeof input === "number" ? { input_tokens: input } : {},
2488
+ ...typeof output === "number" ? { output_tokens: output } : {},
2489
+ ...typeof total === "number" ? { total_tokens: total } : {}
2490
+ };
2491
+ }
2492
+ }
2493
+ /** Build the terminal turn result from the authoritative view + event stream. */
2494
+ function aggregateTurnResult(view, events) {
2495
+ const ordered = [...events].sort((left, right) => left.sequence - right.sequence);
2496
+ const usage = extractUsage(ordered);
2497
+ return {
2498
+ run_id: view.run_id,
2499
+ status: view.status,
2500
+ version: view.version,
2501
+ final_response: view.summary ?? null,
2502
+ output_files: view.output_files ?? [],
2503
+ preset_revision: view.preset_revision ?? null,
2504
+ content_digest: view.content_digest ?? null,
2505
+ events: ordered,
2506
+ items: ordered.map(toItem),
2507
+ ...usage ? { usage } : {}
2508
+ };
2509
+ }
2510
+ //#endregion
2511
+ //#region src/run-handle.ts
2512
+ /** Poll cadence while waiting for a terminal state (authoritative backstop). */
2513
+ const POLL_INTERVAL_MS = 3e3;
2514
+ /** Absolute cap so a wedged run cannot hang `finished` forever. */
2515
+ const FINISHED_TIMEOUT_MS = 18e5;
2516
+ /** Live events kept for the terminal aggregation (bounded). */
2517
+ const MAX_BUFFERED_EVENTS = 2e3;
2518
+ const TERMINAL = [
2519
+ "completed",
2520
+ "failed",
2521
+ "cancelled"
2522
+ ];
2523
+ /**
2524
+ * A launched Agent run. Async-iterable over live events; await `.finished`
2525
+ * for the blocking Codex `thread.run()` shape.
2526
+ */
2527
+ var AgentRunHandle = class {
2528
+ runId;
2529
+ receipt;
2530
+ runClient;
2531
+ poll;
2532
+ subscription;
2533
+ terminalView = null;
2534
+ terminalResult = null;
2535
+ finishWaiters = [];
2536
+ iteratorQueue = [];
2537
+ iteratorWaiters = [];
2538
+ iteratorClosed = false;
2539
+ bufferedEvents = [];
2540
+ bufferedSequences = /* @__PURE__ */ new Set();
2541
+ constructor(runClient, receipt, subscription, poll) {
2542
+ this.runId = receipt.run_id;
2543
+ this.receipt = receipt;
2544
+ this.runClient = runClient;
2545
+ this.subscription = subscription;
2546
+ this.poll = poll;
2547
+ subscription?.onEvent((event) => this.pushEvent(event));
2548
+ subscription?.onError(() => void 0);
2549
+ }
2550
+ /**
2551
+ * Resolves with the aggregated terminal turn result (≈ `thread.run()`):
2552
+ * authoritative status/version plus `final_response`, output files, the
2553
+ * persisted event stream and derived items.
2554
+ */
2555
+ get finished() {
2556
+ if (this.terminalResult) return Promise.resolve(this.terminalResult);
2557
+ return new Promise((resolve, reject) => {
2558
+ this.finishWaiters.push((error, result) => {
2559
+ if (error) {
2560
+ reject(error);
2561
+ return;
2562
+ }
2563
+ resolve(result);
2564
+ });
2565
+ this.ensureTerminalWatch();
2566
+ });
2567
+ }
2568
+ /** Latest live cursor (proxied from the subscription when attached). */
2569
+ get lastSequence() {
2570
+ return this.subscription?.lastSequence ?? 0;
2571
+ }
2572
+ /** Ask the server to cancel; terminal confirmation still arrives via events/get. */
2573
+ async cancel() {
2574
+ await this.runClient.cancel({
2575
+ runId: this.runId,
2576
+ expectedVersion: this.receipt.version
2577
+ });
2578
+ }
2579
+ /**
2580
+ * Inject steering text into the currently active step (≈ Codex turn steer).
2581
+ * Throws `conflict` when the run already changed or has no active step —
2582
+ * refresh `receipt`-level state via `runClient.get()` and retry.
2583
+ */
2584
+ async steer(text) {
2585
+ return this.runClient.steer({
2586
+ runId: this.runId,
2587
+ text,
2588
+ expectedVersion: this.receipt.version
2589
+ });
2590
+ }
2591
+ /** Detach the live subscription (events may still be polled via run/events). */
2592
+ async close() {
2593
+ this.iteratorClosed = true;
2594
+ const waiters = this.iteratorWaiters;
2595
+ this.iteratorWaiters = [];
2596
+ for (const waiter of waiters) waiter({
2597
+ value: void 0,
2598
+ done: true
2599
+ });
2600
+ const subscription = this.subscription;
2601
+ this.subscription = null;
2602
+ await subscription?.close();
2603
+ }
2604
+ /** Async iterator over live events (≈ `thread.runStreamed()`). */
2605
+ [Symbol.asyncIterator]() {
2606
+ return { next: () => {
2607
+ if (this.iteratorQueue.length > 0) {
2608
+ const event = this.iteratorQueue.shift();
2609
+ return Promise.resolve({
2610
+ value: event,
2611
+ done: false
2612
+ });
2613
+ }
2614
+ if (this.iteratorClosed) return Promise.resolve({
2615
+ value: void 0,
2616
+ done: true
2617
+ });
2618
+ return new Promise((resolve) => {
2619
+ this.iteratorWaiters.push(resolve);
2620
+ });
2621
+ } };
2622
+ }
2623
+ pushEvent(event) {
2624
+ if (!this.bufferedSequences.has(event.sequence)) {
2625
+ this.bufferedSequences.add(event.sequence);
2626
+ this.bufferedEvents.push(event);
2627
+ if (this.bufferedEvents.length > MAX_BUFFERED_EVENTS) {
2628
+ const dropped = this.bufferedEvents.shift();
2629
+ if (dropped) this.bufferedSequences.delete(dropped.sequence);
2630
+ }
2631
+ }
2632
+ const waiter = this.iteratorWaiters.shift();
2633
+ if (waiter) {
2634
+ waiter({
2635
+ value: event,
2636
+ done: false
2637
+ });
2638
+ return;
2639
+ }
2640
+ this.iteratorQueue.push(event);
2641
+ }
2642
+ /**
2643
+ * Authoritative backfill for `finished`: `run/result` (terminal summary)
2644
+ * plus the persisted `run/events` stream, falling back to the live buffer
2645
+ * when the query fails.
2646
+ */
2647
+ async aggregate(view) {
2648
+ const result = await this.runClient.result(this.runId).catch(() => null);
2649
+ const events = await this.runClient.events({
2650
+ runId: this.runId,
2651
+ afterSequence: 0,
2652
+ limit: MAX_BUFFERED_EVENTS
2653
+ }).catch(() => this.bufferedEvents);
2654
+ const source = events.length > 0 ? events : this.bufferedEvents;
2655
+ return aggregateTurnResult(result ?? view, source);
2656
+ }
2657
+ /** Poll run/get until terminal, then resolve all `finished` waiters once. */
2658
+ ensureTerminalWatch() {
2659
+ (async () => {
2660
+ const deadline = Date.now() + FINISHED_TIMEOUT_MS;
2661
+ try {
2662
+ while (!this.terminalView && Date.now() < deadline) {
2663
+ const view = await this.poll(this.runId);
2664
+ if (TERMINAL.includes(view.status)) this.terminalView = view;
2665
+ else await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
2666
+ }
2667
+ const waiters = this.finishWaiters;
2668
+ this.finishWaiters = [];
2669
+ if (!this.terminalView) {
2670
+ const timeout = /* @__PURE__ */ new Error(`run ${this.runId} did not reach a terminal state in time`);
2671
+ for (const waiter of waiters) waiter(timeout);
2672
+ return;
2673
+ }
2674
+ this.terminalResult = await this.aggregate(this.terminalView);
2675
+ for (const waiter of waiters) waiter(void 0, this.terminalResult);
2676
+ } catch (error) {
2677
+ const waiters = this.finishWaiters;
2678
+ this.finishWaiters = [];
2679
+ for (const waiter of waiters) waiter(error);
2680
+ }
2681
+ })();
2682
+ }
2683
+ };
2684
+ /** Codex-parity launch: start the run and return a live handle immediately. */
2685
+ async function launchRun(runClient, input, options = {}) {
2686
+ const receipt = await runClient.agent(input);
2687
+ return new AgentRunHandle(runClient, receipt, options.follow === false ? null : await runClient.follow(receipt.run_id), (runId) => runClient.get(runId));
2688
+ }
2689
+ //#endregion
2690
+ //#region src/retry.ts
2691
+ /**
2692
+ * Retry helper (REQ-PAR-05e, ≈ Codex `retry_on_overload`): exponential backoff
2693
+ * with jitter, driven by the protocol's stable `retryable` hint.
2694
+ *
2695
+ * Only errors the server/transport marks retryable are retried; protocol
2696
+ * errors that carry `retryable: false` (validation, not_found, ...) surface
2697
+ * immediately. `command_id` / `idempotency_key` callers stay safe because the
2698
+ * App Server deduplicates replayed mutations.
2699
+ */
2700
+ /** Run `operation`, retrying retryable failures with exponential backoff. */
2701
+ async function withRetry(operation, options = {}) {
2702
+ const maxAttempts = Math.max(1, options.maxAttempts ?? 3);
2703
+ const baseDelayMs = Math.max(0, options.baseDelayMs ?? 500);
2704
+ const maxDelayMs = Math.max(baseDelayMs, options.maxDelayMs ?? 8e3);
2705
+ const jitter = Math.min(1, Math.max(0, options.jitter ?? .25));
2706
+ const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
2707
+ const shouldRetry = options.shouldRetry ?? ((error) => (0, _flowy_agent_store_protocol.isRetryableError)(error));
2708
+ let lastError;
2709
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) try {
2710
+ return await operation();
2711
+ } catch (error) {
2712
+ lastError = error;
2713
+ if (attempt >= maxAttempts || !shouldRetry(error, attempt)) throw error;
2714
+ const ceiling = Math.min(maxDelayMs, baseDelayMs * 2 ** (attempt - 1));
2715
+ const delayMs = Math.round(ceiling * (1 - jitter) + Math.random() * ceiling * jitter);
2716
+ options.onRetry?.({
2717
+ attempt,
2718
+ delayMs,
2719
+ error
2720
+ });
2721
+ await sleep(delayMs);
2722
+ }
2723
+ throw lastError;
2724
+ }
2725
+ //#endregion
2726
+ exports.AgentClient = AgentClient;
2727
+ exports.AgentRunHandle = AgentRunHandle;
2728
+ exports.AppServerClient = AppServerClient;
2729
+ exports.ConnectorClient = ConnectorClient;
2730
+ exports.ConversationClient = ConversationClient;
2731
+ exports.ConversationHandle = ConversationHandle;
2732
+ exports.ConversationSubscription = ConversationSubscription;
2733
+ exports.EventSubscription = EventSubscription;
2734
+ exports.HttpTransport = HttpTransport;
2735
+ exports.ModelClient = ModelClient;
2736
+ exports.RunClient = RunClient;
2737
+ exports.SkillClient = SkillClient;
2738
+ exports.StoreClient = StoreClient;
2739
+ exports.StoreError = StoreError;
2740
+ exports.TeamClient = TeamClient;
2741
+ exports.WebSocketTransport = WebSocketTransport;
2742
+ exports.WorkspaceClient = WorkspaceClient;
2743
+ exports.aggregateTurnResult = aggregateTurnResult;
2744
+ exports.appServerErrorFromWire = appServerErrorFromWire;
2745
+ exports.conversationMessages = conversationMessages;
2746
+ exports.decodeBase64 = decodeBase64;
2747
+ exports.httpRouteTable = httpRouteTable;
2748
+ exports.isLoopbackUrl = isLoopbackUrl;
2749
+ exports.launchRun = launchRun;
2750
+ exports.withRetry = withRetry;