@flowy-agent-store/client 0.1.0-beta.3 → 0.1.0-beta.4

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 CHANGED
@@ -238,6 +238,532 @@ var WebSocketTransport = class {
238
238
  }
239
239
  };
240
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-1";
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
+ ],
339
+ source: "conversation_send() -> send_conversation_message_for_user (shared with the WS arm)"
340
+ },
341
+ "conversation/cancel": {
342
+ verb: "POST",
343
+ path: "/conversations/:conversation_id/cancel",
344
+ source: "conversation_cancel() -> cancel arm semantics"
345
+ },
346
+ "agent/run": {
347
+ verb: "POST",
348
+ path: "/agent/run",
349
+ source: "agent_run() -> same request/receipt as the `agent/run` arm"
350
+ },
351
+ "team/run": {
352
+ verb: "POST",
353
+ path: "/team/run",
354
+ source: "team_run() -> execute_team_run() (same as the WS arm); Leader Conversation + planned delegation, `TeamRunReceipt`"
355
+ },
356
+ "run/get": {
357
+ verb: "GET",
358
+ path: "/run/:run_id",
359
+ source: "run_get()"
360
+ },
361
+ "run/result": {
362
+ verb: "GET",
363
+ path: "/run/:run_id/result",
364
+ source: "run_result()"
365
+ },
366
+ "run/plan": {
367
+ verb: "GET",
368
+ path: "/run/:run_id/plan",
369
+ source: "run_plan() -> get_run_plan_for_user() (same as the WS arm)"
370
+ },
371
+ "run/events": {
372
+ verb: "GET",
373
+ path: "/run/:run_id/events",
374
+ query: ["after_sequence", "limit"],
375
+ source: "run_events() -> runtime.list_events(after_sequence, limit) (same as the WS arm)"
376
+ },
377
+ "run/cancel": {
378
+ verb: "POST",
379
+ path: "/run/:run_id/cancel",
380
+ body: [
381
+ "expected_version",
382
+ "reason",
383
+ "idempotency_key"
384
+ ],
385
+ source: "run_cancel() -> same CancelRunRequest as the WS arm"
386
+ },
387
+ "run/steer": {
388
+ verb: "POST",
389
+ path: "/run/:run_id/steer",
390
+ body: [
391
+ "text",
392
+ "expected_version",
393
+ "command_id",
394
+ "idempotency_key"
395
+ ],
396
+ source: "run_steer() -> execute_steer_run (shared with the WS arm)"
397
+ },
398
+ "run/answer-decision": {
399
+ verb: "POST",
400
+ path: "/run/:run_id/answer-decision",
401
+ body: [
402
+ "step_id",
403
+ "attempt_id",
404
+ "answer",
405
+ "expected_execution_version",
406
+ "expected_step_version",
407
+ "expected_attempt_version"
408
+ ],
409
+ source: "run_answer_decision() -> execute_answer_decision (shared with the WS arm; engine answer gate, no always_allow)"
410
+ },
411
+ "skill/list": {
412
+ verb: "GET",
413
+ path: "/skills",
414
+ source: "list_skills_route() -> list_skills_impl"
415
+ },
416
+ "skill/get": {
417
+ verb: "GET",
418
+ path: "/skills/:skill_id",
419
+ source: "get_skill_route() -> get_skill_impl"
420
+ },
421
+ "skill/files": {
422
+ verb: "GET",
423
+ path: "/skills/:skill_id/files",
424
+ source: "list_skill_files_route() -> list_skill_files_impl"
425
+ },
426
+ "connector/list": {
427
+ verb: "GET",
428
+ path: "/connectors",
429
+ source: "list_connectors_route() -> list_connectors_impl"
430
+ },
431
+ "models/list": {
432
+ verb: "GET",
433
+ path: "/models",
434
+ source: "list_models_route() -> list_models_impl"
435
+ },
436
+ "connector/get": {
437
+ verb: "GET",
438
+ path: "/connectors/:connector_id",
439
+ source: "get_connector_route() -> get_connector_impl"
440
+ },
441
+ "connector/status": {
442
+ verb: "GET",
443
+ path: "/connectors/:connector_id/status",
444
+ source: "connector_status_route() -> connector_status_impl"
445
+ },
446
+ "connector/test": {
447
+ verb: "POST",
448
+ path: "/connectors/:connector_id/test",
449
+ source: "connector_test_route() -> connector_test_impl"
450
+ },
451
+ "connector/call": {
452
+ verb: "POST",
453
+ path: "/connectors/:connector_id/call",
454
+ source: "connector_call_route() -> connector_call_impl"
455
+ },
456
+ "connector/auth/start": {
457
+ verb: "POST",
458
+ path: "/connectors/:connector_id/auth-start",
459
+ source: "connector_auth_start_route() -> connector_auth_start_impl (WS name uses slashes)"
460
+ },
461
+ "connector/auth/status": {
462
+ verb: "GET",
463
+ path: "/connectors/:connector_id/auth-status",
464
+ source: "connector_auth_status_route() -> connector_auth_status_impl"
465
+ },
466
+ "connector/auth/logout": {
467
+ verb: "POST",
468
+ path: "/connectors/:connector_id/auth-logout",
469
+ source: "connector_auth_logout_route() -> connector_auth_logout_impl"
470
+ },
471
+ "import/run": {
472
+ verb: "POST",
473
+ path: "/imports",
474
+ source: "run_import_route() -> run_import_impl"
475
+ },
476
+ "import/list": {
477
+ verb: "GET",
478
+ path: "/imports",
479
+ source: "list_imports_route() -> list_imports_impl"
480
+ },
481
+ "import/get": {
482
+ verb: "GET",
483
+ path: "/imports/:snapshot_id",
484
+ source: "get_import_route() -> get_import_impl"
485
+ },
486
+ "install/run": {
487
+ verb: "POST",
488
+ path: "/installs",
489
+ source: "run_install_route() -> install_impl"
490
+ },
491
+ "install/status": {
492
+ verb: "GET",
493
+ path: "/installs/:snapshot_id",
494
+ source: "install_status_route() -> install_status_impl"
495
+ },
496
+ "install/disable": {
497
+ verb: "POST",
498
+ path: "/installs/:snapshot_id/disable",
499
+ body: ["component_ids"],
500
+ source: "install_disable_route() -> install_disable_impl; body `component_ids`"
501
+ },
502
+ "install/enable": {
503
+ verb: "POST",
504
+ path: "/installs/:snapshot_id/enable",
505
+ body: ["component_ids"],
506
+ source: "install_enable_route() -> install_enable_impl; body `component_ids`"
507
+ },
508
+ "install/uninstall": {
509
+ verb: "POST",
510
+ path: "/installs/:snapshot_id/uninstall",
511
+ body: ["component_ids"],
512
+ source: "install_uninstall_route() -> install_uninstall_impl; body `component_ids`"
513
+ },
514
+ "market/add": {
515
+ verb: "POST",
516
+ path: "/markets",
517
+ source: "market_add_route() -> market_add_impl"
518
+ },
519
+ "market/list": {
520
+ verb: "GET",
521
+ path: "/markets",
522
+ source: "market_list_route() -> market_list_impl"
523
+ },
524
+ "market/get": {
525
+ verb: "GET",
526
+ path: "/markets/:marketplace_id",
527
+ source: "market_get_route() -> market_get_impl"
528
+ },
529
+ "market/remove": {
530
+ verb: "POST",
531
+ path: "/markets/:marketplace_id/remove",
532
+ body: ["cascade"],
533
+ source: "market_remove_route() -> market_remove_impl; body `cascade` defaults to true"
534
+ },
535
+ "market/auto-update": {
536
+ verb: "POST",
537
+ path: "/markets/:marketplace_id/auto-update",
538
+ body: ["enabled"],
539
+ source: "market_auto_update_route() -> market_auto_update_impl; body `enabled` defaults to true"
540
+ },
541
+ "market/refresh": {
542
+ verb: "POST",
543
+ path: "/markets/:marketplace_id/refresh",
544
+ source: "market_refresh_route() -> market_refresh_impl"
545
+ },
546
+ "market/entry-import": {
547
+ verb: "POST",
548
+ path: "/markets/:marketplace_id/entries/:entry_name/import",
549
+ source: "market_entry_import_route() -> market_entry_import_impl"
550
+ },
551
+ "store/list": {
552
+ verb: "GET",
553
+ path: "/store",
554
+ source: "store_list_route() -> store_list_impl"
555
+ },
556
+ "store/install-entry": {
557
+ verb: "POST",
558
+ path: "/store/:marketplace_id/entries/:entry_name/install",
559
+ source: "store_install_entry_route() -> store_install_entry_impl"
560
+ }
561
+ };
562
+ /** Methods that are WebSocket-only by design — named so the error explains itself. */
563
+ const WS_ONLY_METHODS = /* @__PURE__ */ new Set([
564
+ "initialize",
565
+ "initialized",
566
+ "conversation/subscribe",
567
+ "conversation/unsubscribe",
568
+ "run/subscribe",
569
+ "run/unsubscribe"
570
+ ]);
571
+ var HttpTransport = class {
572
+ routes;
573
+ options;
574
+ closed = false;
575
+ constructor(options, routes = HTTP_ROUTES) {
576
+ this.options = options;
577
+ this.routes = routes;
578
+ }
579
+ /**
580
+ * No-op: the HTTP binding handshakes per call (`05` §2.1.1), so there is no
581
+ * connection to open. Kept for `Transport` compatibility.
582
+ */
583
+ async connect() {
584
+ this.closed = false;
585
+ }
586
+ /**
587
+ * Perform one business call over its own handshake.
588
+ *
589
+ * Throws `TransportError("send", …)` for a method with no HTTP binding —
590
+ * never a silent fallback to another binding.
591
+ */
592
+ async request(method, params) {
593
+ if (this.closed) throw new _flowy_agent_store_protocol.TransportError("connect", "http transport is closed");
594
+ const route = this.routes[method];
595
+ if (!route) {
596
+ 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)`;
597
+ throw new _flowy_agent_store_protocol.TransportError("send", reason);
598
+ }
599
+ const { path, query, body } = this.splitParams(route, params);
600
+ const connectionId = await this.openConnection();
601
+ const response = await this.fetchWithTimeout(route.verb, path, {
602
+ query,
603
+ body,
604
+ connectionId
605
+ });
606
+ return await this.readResult(response, method);
607
+ }
608
+ /**
609
+ * Not supported: HTTP carries no server-push. Throws instead of pretending.
610
+ */
611
+ notify(method, _params) {
612
+ throw new _flowy_agent_store_protocol.TransportError("send", `HTTP binding cannot notify ("${method}"): notifications are WebSocket-only (doc 05 §2.1.1)`);
613
+ }
614
+ /**
615
+ * Returns a no-op unsubscribe: this binding receives **no** notifications.
616
+ *
617
+ * Not equivalent to the WebSocket binding — callers that need live events
618
+ * must use `WebSocketTransport` (or run both).
619
+ */
620
+ onNotification(_listener) {
621
+ return () => void 0;
622
+ }
623
+ /** No-op: every call is independent, so nothing is held open. */
624
+ close() {
625
+ this.closed = true;
626
+ }
627
+ /**
628
+ * One-shot handshake: `POST /initialize` (connection id header) →
629
+ * `POST /initialized`. Returns the connection id.
630
+ *
631
+ * Public because host-side services that sit *outside* the protocol surface
632
+ * (`/api/fs/*`, the HTTP-only `POST /workspaces` register route) still need a
633
+ * ready connection id; they are not protocol methods, so they cannot go
634
+ * through `request()`.
635
+ */
636
+ async openConnection() {
637
+ const connectionId = (await this.fetchWithTimeout("POST", "/initialize", { body: {
638
+ protocol_version: PROTOCOL_VERSION,
639
+ client: this.options.client ?? {
640
+ name: "flowy-agent-store-client",
641
+ version: "0.1.0"
642
+ },
643
+ ...this.options.capabilities === void 0 ? {} : { capabilities: this.options.capabilities }
644
+ } })).headers.get(CONNECTION_HEADER);
645
+ if (!connectionId) throw new _flowy_agent_store_protocol.TransportError("connect", `handshake response is missing the ${CONNECTION_HEADER} header`, { retryable: true });
646
+ await this.fetchWithTimeout("POST", "/initialized", { connectionId });
647
+ return connectionId;
648
+ }
649
+ splitParams(route, params) {
650
+ const source = isRecord(params) ? params : {};
651
+ const pathParams = [...route.path.matchAll(/:([A-Za-z0-9_]+)/g)].map((match) => match[1]);
652
+ const path = route.path.replace(/:([A-Za-z0-9_]+)/g, (_full, name) => {
653
+ const value = source[name];
654
+ if (value === void 0 || value === null) throw new _flowy_agent_store_protocol.TransportError("send", `missing required path param "${name}" for ${route.verb} ${route.path}`);
655
+ return encodeURIComponent(String(value));
656
+ });
657
+ const query = {};
658
+ for (const key of route.query ?? []) {
659
+ const value = source[key];
660
+ if (value !== void 0 && value !== null) query[key] = String(value);
661
+ }
662
+ if (route.verb === "GET" || route.verb === "DELETE") return {
663
+ path,
664
+ query,
665
+ body: void 0
666
+ };
667
+ const carried = route.body ?? [];
668
+ const body = {};
669
+ for (const key of Object.keys(source)) {
670
+ if (pathParams.includes(key) || (route.query ?? []).includes(key)) continue;
671
+ if (carried.length > 0 && !carried.includes(key)) continue;
672
+ body[key] = source[key];
673
+ }
674
+ return {
675
+ path,
676
+ query,
677
+ body
678
+ };
679
+ }
680
+ async fetchWithTimeout(verb, path, request) {
681
+ const url = new URL(`${this.baseUrl()}${path}`);
682
+ for (const [key, value] of Object.entries(request.query ?? {})) url.searchParams.set(key, value);
683
+ const headers = { accept: "application/json" };
684
+ if (request.body !== void 0) headers["content-type"] = "application/json";
685
+ if (request.connectionId) headers[CONNECTION_HEADER] = request.connectionId;
686
+ if (this.options.token) headers.authorization = `Bearer ${this.options.token}`;
687
+ const controller = new AbortController();
688
+ const timeoutMs = this.options.requestTimeoutMs ?? 3e4;
689
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
690
+ try {
691
+ const fetchImpl = this.options.fetch ?? globalThis.fetch;
692
+ if (!fetchImpl) throw new _flowy_agent_store_protocol.TransportError("connect", "no fetch implementation available");
693
+ return await fetchImpl(url.toString(), {
694
+ method: verb,
695
+ headers,
696
+ body: request.body === void 0 ? void 0 : JSON.stringify(request.body),
697
+ signal: controller.signal
698
+ });
699
+ } catch (error) {
700
+ if (error instanceof _flowy_agent_store_protocol.TransportError) throw error;
701
+ const aborted = error?.name === "AbortError";
702
+ throw new _flowy_agent_store_protocol.TransportError("send", aborted ? `${verb} ${path} timed out after ${timeoutMs}ms` : `${verb} ${path} failed: ${String(error)}`, {
703
+ retryable: true,
704
+ cause: error
705
+ });
706
+ } finally {
707
+ clearTimeout(timer);
708
+ }
709
+ }
710
+ /** 2xx → result body; otherwise the server's wire error becomes an `AppServerError`. */
711
+ async readResult(response, method) {
712
+ let payload = void 0;
713
+ const text = await response.text();
714
+ if (text.length > 0) try {
715
+ payload = JSON.parse(text);
716
+ } catch {
717
+ if (response.ok) throw new _flowy_agent_store_protocol.TransportError("receive", `${method}: response is not JSON`, { retryable: true });
718
+ }
719
+ if (response.ok) return payload;
720
+ throw appServerErrorFromWire(payload, response.status, method);
721
+ }
722
+ baseUrl() {
723
+ return this.options.baseUrl.replace(/\/+$/, "").replace(/\/ws$/, "");
724
+ }
725
+ };
726
+ function isRecord(value) {
727
+ return typeof value === "object" && value !== null && !Array.isArray(value);
728
+ }
729
+ /**
730
+ * Turn an HTTP failure body into the SDK error model.
731
+ *
732
+ * Shared by this transport and by host-side helpers (webui `lib/client.ts`) so
733
+ * a second spelling of the wire-error mapping cannot drift. Two wire contracts
734
+ * reach this function, both of them `AppError` on the server:
735
+ *
736
+ * - the App Server emits `{code, message, retryable, details, request_id}`
737
+ * (`into_wire_error`), which is exactly the `AppServerError` shape;
738
+ * - the standalone host file service (`/api/fs/*`) emits the `ErrorResponse`
739
+ * envelope `{success: false, error, code, details?}` — same error, another
740
+ * spelling: `error` carries the human message, and there is no retry hint, so
741
+ * a 5xx status stays the retry signal.
742
+ *
743
+ * Anything else becomes a retryable-on-5xx `TransportError`.
744
+ */
745
+ function appServerErrorFromWire(payload, status, context = "request") {
746
+ if (isRecord(payload) && typeof payload.code === "string" && typeof payload.message === "string") return new _flowy_agent_store_protocol.AppServerError({
747
+ code: payload.code,
748
+ message: payload.message,
749
+ retryable: payload.retryable === true,
750
+ request_id: payload.request_id ?? null,
751
+ details: isRecord(payload.details) ? payload.details : {}
752
+ });
753
+ if (isRecord(payload) && payload.success === false && typeof payload.code === "string" && typeof payload.error === "string") return new _flowy_agent_store_protocol.AppServerError({
754
+ code: payload.code,
755
+ message: payload.error,
756
+ retryable: status >= 500,
757
+ request_id: null,
758
+ details: isRecord(payload.details) ? payload.details : {}
759
+ });
760
+ return new _flowy_agent_store_protocol.TransportError("receive", `${context}: HTTP ${status} without a wire error body`, { retryable: status >= 500 });
761
+ }
762
+ /** Exposed for tests and docs (R5): the verified method → route table. */
763
+ function httpRouteTable() {
764
+ return HTTP_ROUTES;
765
+ }
766
+ //#endregion
241
767
  //#region src/agents.ts
242
768
  var AgentClient = class {
243
769
  transport;
@@ -300,11 +826,19 @@ var ConversationClient = class {
300
826
  cursor: query.cursor
301
827
  });
302
828
  }
303
- send(conversationId, content, idempotencyKey) {
829
+ /**
830
+ * R15(W10):`attachments` 是**会话工作区内的绝对路径**(图片附件)。
831
+ *
832
+ * 缺省不传,保持老调用方的 wire 形状逐字不变;服务端只接受会话工作区内的真实
833
+ * 文件(越界 / 相对路径 / 不存在一律拒),具体准入见
834
+ * `nomifun-app-server` 的 `resolve_conversation_attachments`。
835
+ */
836
+ send(conversationId, content, idempotencyKey, attachments = []) {
304
837
  return this.transport.request("conversation/send", {
305
838
  conversation_id: conversationId,
306
839
  content,
307
- idempotency_key: idempotencyKey
840
+ idempotency_key: idempotencyKey,
841
+ ...attachments.length > 0 ? { attachments } : {}
308
842
  });
309
843
  }
310
844
  cancel(conversationId) {
@@ -313,35 +847,92 @@ var ConversationClient = class {
313
847
  delete(conversationId) {
314
848
  return this.transport.request("conversation/delete", { conversation_id: conversationId });
315
849
  }
316
- async follow(conversationId) {
850
+ /**
851
+ * Subscribe to one conversation's realtime stream (doc `16` R1).
852
+ *
853
+ * `options.fetchMessages` is what makes auto catch-up possible: a detected
854
+ * sequence gap or a `conversation/resync-required` triggers one transcript
855
+ * fetch whose result is handed to `onBackfill`. Conversations have no event
856
+ * replay API (`05` §「V1 不提供 after_cursor 事件追平」), so the transcript is
857
+ * the recovery medium — unlike Runs, where `run/events` replays the events
858
+ * themselves.
859
+ */
860
+ async follow(conversationId, options = {}) {
317
861
  await this.transport.request("conversation/subscribe", { conversation_id: conversationId });
318
- return new ConversationSubscription(this.transport, conversationId);
862
+ return new ConversationSubscription(this.transport, conversationId, {
863
+ fetchMessages: (id) => this.messages({
864
+ conversationId: id,
865
+ pageSize: 50
866
+ }),
867
+ ...options
868
+ });
319
869
  }
320
870
  };
871
+ /** Events held before the first listener attaches (see `pendingLimit`). */
872
+ const DEFAULT_PENDING_LIMIT = 256;
321
873
  var ConversationSubscription = class {
322
874
  transport;
323
875
  conversationId;
876
+ options;
324
877
  events = /* @__PURE__ */ new Set();
325
878
  resyncs = /* @__PURE__ */ new Set();
879
+ errors = /* @__PURE__ */ new Set();
880
+ backfills = /* @__PURE__ */ new Set();
326
881
  closed = false;
327
882
  lastSeenSequence = 0;
328
883
  removeListener;
329
- constructor(transport, conversationId) {
884
+ /**
885
+ * `follow()` resolves as soon as the server acknowledged the subscription,
886
+ * but the caller attaches listeners one tick later. Events landing in that
887
+ * window used to be dropped silently; they are held here (oldest first) and
888
+ * flushed to the first `onEvent` listener.
889
+ */
890
+ pending = [];
891
+ pendingDropped = 0;
892
+ backfillInFlight = null;
893
+ constructor(transport, conversationId, options = {}) {
330
894
  this.transport = transport;
331
895
  this.conversationId = conversationId;
896
+ this.options = options;
332
897
  this.removeListener = transport.onNotification((notification) => this.dispatch(notification));
333
898
  }
334
899
  get lastSequence() {
335
900
  return this.lastSeenSequence;
336
901
  }
902
+ /** Events dropped because nobody was listening and the buffer was full. */
903
+ get droppedPendingCount() {
904
+ return this.pendingDropped;
905
+ }
337
906
  onEvent(listener) {
338
907
  this.events.add(listener);
908
+ if (this.events.size === 1 && this.pending.length > 0) {
909
+ const queued = this.pending;
910
+ this.pending = [];
911
+ for (const event of queued) this.deliver(event);
912
+ }
339
913
  return () => this.events.delete(listener);
340
914
  }
341
915
  onResync(listener) {
342
916
  this.resyncs.add(listener);
343
917
  return () => this.resyncs.delete(listener);
344
918
  }
919
+ /**
920
+ * Auto catch-up result (R1). Fires only when `autoResync` is on and the
921
+ * transcript fetch succeeded; the listener owns replacing the transcript.
922
+ */
923
+ onBackfill(listener) {
924
+ this.backfills.add(listener);
925
+ return () => this.backfills.delete(listener);
926
+ }
927
+ /**
928
+ * Failures that used to be swallowed: the catch-up fetch, and a failed
929
+ * re-subscribe during `rearm()`. Listener isolation keeps one bad listener
930
+ * from hiding the error from the others.
931
+ */
932
+ onError(listener) {
933
+ this.errors.add(listener);
934
+ return () => this.errors.delete(listener);
935
+ }
345
936
  async close() {
346
937
  if (this.closed) return;
347
938
  this.closed = true;
@@ -349,6 +940,9 @@ var ConversationSubscription = class {
349
940
  this.removeListener = null;
350
941
  this.events.clear();
351
942
  this.resyncs.clear();
943
+ this.errors.clear();
944
+ this.backfills.clear();
945
+ this.pending = [];
352
946
  try {
353
947
  await this.transport.request("conversation/unsubscribe", { conversation_id: this.conversationId });
354
948
  } catch {}
@@ -356,35 +950,99 @@ var ConversationSubscription = class {
356
950
  /**
357
951
  * Re-arm after a reconnect (docs/agent-store/16 T8): re-register the
358
952
  * notification listener, reset the cursor and re-issue
359
- * `conversation/subscribe`. This subscription has no event-replay API, so the
360
- * caller must still backfill the transcript for the outage window
361
- * (`conversation/messages`). Call it once the handshake completed; a no-op
362
- * once closed.
953
+ * `conversation/subscribe`. The server restarts a conversation's sequence at
954
+ * 1 for a fresh subscription, which is exactly why the cursor resets. On a
955
+ * failed re-subscribe the error reaches `onError` and the caller can still
956
+ * backfill the outage window via `conversation/messages`.
363
957
  */
364
958
  async rearm() {
365
959
  if (this.closed) return;
366
960
  this.removeListener?.();
367
961
  this.removeListener = this.transport.onNotification((notification) => this.dispatch(notification));
368
962
  this.lastSeenSequence = 0;
369
- await this.transport.request("conversation/subscribe", { conversation_id: this.conversationId });
963
+ try {
964
+ await this.transport.request("conversation/subscribe", { conversation_id: this.conversationId });
965
+ } catch (error) {
966
+ this.emitError(error);
967
+ throw error;
968
+ }
370
969
  }
371
970
  dispatch(notification) {
372
971
  if (notification.method === "conversation/event") {
373
972
  const event = notification.params;
374
- if (event.conversation_id !== this.conversationId || event.sequence <= this.lastSeenSequence) return;
973
+ if (event.conversation_id !== this.conversationId) return;
974
+ if (!Number.isFinite(event.sequence)) return;
975
+ if (event.sequence <= this.lastSeenSequence) return;
976
+ const gap = this.lastSeenSequence > 0 && event.sequence > this.lastSeenSequence + 1;
375
977
  this.lastSeenSequence = event.sequence;
376
- for (const listener of [...this.events]) try {
377
- listener(event);
378
- } catch {}
978
+ if (this.events.size === 0) this.buffer(event);
979
+ else this.deliver(event);
980
+ if (gap) {
981
+ this.emitResync("gap");
982
+ this.requestBackfill("gap");
983
+ }
379
984
  return;
380
985
  }
381
986
  if (notification.method === "conversation/resync-required") {
382
987
  if (!notification.params.conversation_ids.includes(this.conversationId)) return;
383
- for (const listener of [...this.resyncs]) try {
384
- listener(notification.params.reason);
385
- } catch {}
988
+ this.emitResync(notification.params.reason);
989
+ this.requestBackfill(notification.params.reason);
386
990
  }
387
991
  }
992
+ buffer(event) {
993
+ const limit = this.options.pendingLimit ?? DEFAULT_PENDING_LIMIT;
994
+ this.pending.push(event);
995
+ while (this.pending.length > limit) {
996
+ this.pending.shift();
997
+ this.pendingDropped += 1;
998
+ }
999
+ }
1000
+ deliver(event) {
1001
+ for (const listener of [...this.events]) try {
1002
+ listener(event);
1003
+ } catch {}
1004
+ }
1005
+ emitResync(reason) {
1006
+ for (const listener of [...this.resyncs]) try {
1007
+ listener(reason);
1008
+ } catch {}
1009
+ }
1010
+ emitError(error) {
1011
+ for (const listener of [...this.errors]) try {
1012
+ listener(error);
1013
+ } catch {}
1014
+ }
1015
+ /**
1016
+ * One in-flight catch-up at a time: a burst of resync signals collapses into
1017
+ * a single transcript fetch (the fetch itself returns the newest page, so the
1018
+ * result supersedes every signal that arrived before it).
1019
+ */
1020
+ requestBackfill(reason) {
1021
+ if (this.closed || this.options.autoResync === false) return;
1022
+ const fetchMessages = this.options.fetchMessages;
1023
+ if (!fetchMessages || this.backfillInFlight) return;
1024
+ const task = (async () => {
1025
+ try {
1026
+ const page = await fetchMessages(this.conversationId);
1027
+ if (this.closed) return;
1028
+ const backfill = {
1029
+ conversationId: this.conversationId,
1030
+ reason,
1031
+ messages: page?.items ?? [],
1032
+ hasMore: page?.has_more === true
1033
+ };
1034
+ for (const listener of [...this.backfills]) try {
1035
+ listener(backfill);
1036
+ } catch {}
1037
+ } catch (error) {
1038
+ this.emitError(error);
1039
+ }
1040
+ })();
1041
+ this.backfillInFlight = task;
1042
+ task.finally(() => {
1043
+ if (this.backfillInFlight === task) this.backfillInFlight = null;
1044
+ });
1045
+ }
388
1046
  };
389
1047
  //#endregion
390
1048
  //#region src/connectors.ts
@@ -424,6 +1082,28 @@ var ConnectorClient = class {
424
1082
  logout(connectorId) {
425
1083
  return this.transport.request("connector/auth/logout", { connector_id: connectorId }).then(() => void 0);
426
1084
  }
1085
+ /**
1086
+ * Run one tool on a registered connector, through the host's own connection.
1087
+ *
1088
+ * The host holds the transport, its headers and its OAuth token; this sends a
1089
+ * tool name and an argument object and nothing else — you cannot name a URL,
1090
+ * a command or a header. Whether the pair is callable at all is the host's
1091
+ * `[connector_proxy]` allowlist, so expect `policy_denied` on a host whose
1092
+ * operator has not listed this tool (that is the default, not a
1093
+ * misconfiguration).
1094
+ *
1095
+ * Resolves even when the tool itself failed — check `is_error`. It rejects
1096
+ * only when the call never reached the tool: `connector_call_timeout`,
1097
+ * `connector_call_failed`, `response_too_large`, `connector_unavailable`,
1098
+ * `policy_denied`, `not_found`.
1099
+ */
1100
+ call(connectorId, tool, args = {}) {
1101
+ return this.transport.request("connector/call", {
1102
+ connector_id: connectorId,
1103
+ tool,
1104
+ arguments: args
1105
+ });
1106
+ }
427
1107
  };
428
1108
  //#endregion
429
1109
  //#region src/runs.ts
@@ -440,6 +1120,18 @@ var RunClient = class {
440
1120
  agent(input) {
441
1121
  return this.transport.request("agent/run", toWire(input));
442
1122
  }
1123
+ /**
1124
+ * Start a Team run (`16` §7 决策 3): the server creates the Leader
1125
+ * Conversation, materializes/binds the Team's execution template and returns
1126
+ * the public run id of the execution the Leader started.
1127
+ *
1128
+ * The receipt is `TeamRunReceipt`, not `RunReceipt` — a Team Run has no lead
1129
+ * preset revision/digest to report. Member pool and concurrency limits are not
1130
+ * inputs; sending `planning` is rejected as `invalid_request`.
1131
+ */
1132
+ team(input) {
1133
+ return this.transport.request("team/run", toTeamWire(input));
1134
+ }
443
1135
  /** Read the authoritative run state. */
444
1136
  get(runId) {
445
1137
  return this.transport.request("run/get", { run_id: runId });
@@ -448,6 +1140,14 @@ var RunClient = class {
448
1140
  result(runId) {
449
1141
  return this.transport.request("run/result", { run_id: runId });
450
1142
  }
1143
+ /**
1144
+ * Read the authoritative plan / step snapshot (W4 / W6, resolves D-W6-1):
1145
+ * titles, statuses, retries with their reason, errors and timings — none of
1146
+ * which the append-only event log carries.
1147
+ */
1148
+ plan(runId) {
1149
+ return this.transport.request("run/plan", { run_id: runId });
1150
+ }
451
1151
  /** Replay persisted events after a cursor. */
452
1152
  events(query) {
453
1153
  return this.transport.request("run/events", {
@@ -478,6 +1178,26 @@ var RunClient = class {
478
1178
  idempotency_key: input.idempotencyKey
479
1179
  });
480
1180
  }
1181
+ /**
1182
+ * Answer the pending decision of a `waiting_input` attempt.
1183
+ *
1184
+ * The three `expected*Version` tokens are mandatory CAS inputs, normally the
1185
+ * ones projected on the pending `approval.requested` event. The server passes
1186
+ * them straight to the engine's single answer gate, so a stale token is a
1187
+ * `conflict`, never a silent overwrite. There is no `always_allow` here: the
1188
+ * desktop confirmation route's approve-all flag is not part of this protocol.
1189
+ */
1190
+ answerDecision(input) {
1191
+ return this.transport.request("run/answer-decision", {
1192
+ run_id: input.runId,
1193
+ step_id: input.stepId,
1194
+ attempt_id: input.attemptId,
1195
+ answer: input.answer,
1196
+ expected_execution_version: input.expectedExecutionVersion,
1197
+ expected_step_version: input.expectedStepVersion,
1198
+ expected_attempt_version: input.expectedAttemptVersion
1199
+ });
1200
+ }
481
1201
  /** Subscribe to best-effort realtime events for one run. */
482
1202
  async follow(runId, options) {
483
1203
  const params = { run_id: runId };
@@ -672,6 +1392,17 @@ function toWire(input) {
672
1392
  mentions: input.mentions
673
1393
  };
674
1394
  }
1395
+ function toTeamWire(input) {
1396
+ return {
1397
+ team_id: input.teamId,
1398
+ team_version: input.teamVersion,
1399
+ goal: input.goal,
1400
+ input: input.input,
1401
+ workspace: input.workspaceId ? { id: input.workspaceId } : void 0,
1402
+ command_id: input.commandId,
1403
+ idempotency_key: input.idempotencyKey
1404
+ };
1405
+ }
675
1406
  //#endregion
676
1407
  //#region src/skills.ts
677
1408
  var SkillClient = class {
@@ -687,6 +1418,309 @@ var SkillClient = class {
687
1418
  get(skillId) {
688
1419
  return this.transport.request("skill/get", { skill_id: skillId });
689
1420
  }
1421
+ /**
1422
+ * Every readable file inside a Skill's directory, with a tree digest.
1423
+ *
1424
+ * This is the only way to learn what a Skill actually ships: `get()` returns
1425
+ * a bounded summary of `SKILL.md` and says nothing about the `references/`,
1426
+ * `scripts/`, `templates/` and `assets/` beside it. Check
1427
+ * `capabilities.skill_files` first — a host may wire the catalog without
1428
+ * this face, in which case it answers `unsupported_operation`.
1429
+ */
1430
+ files(skillId) {
1431
+ return this.transport.request("skill/files", { skill_id: skillId });
1432
+ }
1433
+ /**
1434
+ * One skill file's bytes, over the WebSocket binding.
1435
+ *
1436
+ * The wire carries base64 because JSON has no byte string, so this decodes
1437
+ * back to `Uint8Array` for you. The HTTP route
1438
+ * (`GET /api/app-server/skills/{id}/files/{path}`) returns the raw body
1439
+ * instead, but that is deliberately not on the typed client's JSON
1440
+ * transport — use this method, or `fetch` the route directly if you are on a
1441
+ * host token.
1442
+ */
1443
+ async readFile(skillId, path) {
1444
+ return decodeBase64((await this.transport.request("skill/file", {
1445
+ skill_id: skillId,
1446
+ path
1447
+ })).content);
1448
+ }
1449
+ /** Same as {@link readFile}, but also returns the server's content type. */
1450
+ readFileWithType(skillId, path) {
1451
+ return this.transport.request("skill/file", {
1452
+ skill_id: skillId,
1453
+ path
1454
+ });
1455
+ }
1456
+ };
1457
+ /**
1458
+ * Decode base64 to bytes.
1459
+ *
1460
+ * `atob` is used when present (browsers, Bun, Node 16+) and a manual decode
1461
+ * otherwise, so the package keeps working on a bare Node without a DOM lib.
1462
+ */
1463
+ function decodeBase64(content) {
1464
+ if (typeof atob === "function") {
1465
+ const binary = atob(content);
1466
+ const bytes = new Uint8Array(binary.length);
1467
+ for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
1468
+ return bytes;
1469
+ }
1470
+ const bufferCtor = globalThis.Buffer;
1471
+ if (bufferCtor) return bufferCtor.from(content, "base64");
1472
+ throw new Error("no base64 decoder available in this environment");
1473
+ }
1474
+ //#endregion
1475
+ //#region src/store.ts
1476
+ var StoreError = class extends Error {
1477
+ code;
1478
+ constructor(code, message) {
1479
+ super(message);
1480
+ this.code = code;
1481
+ this.name = "StoreError";
1482
+ }
1483
+ };
1484
+ const DEFAULT_READY_TIMEOUT_MS = 3e4;
1485
+ const DEFAULT_READY_POLL_MS = 400;
1486
+ const DEFAULT_READY_POLL_MAX_MS = 4e3;
1487
+ /** Every string a `LocalizedText` carries, for haystack building. */
1488
+ function localizedValues(text) {
1489
+ if (typeof text === "string") return [text];
1490
+ if (!text || typeof text !== "object") return [];
1491
+ return Object.values(text).filter((value) => typeof value === "string");
1492
+ }
1493
+ function haystack(item) {
1494
+ return [
1495
+ item.entry_name,
1496
+ item.name,
1497
+ item.id,
1498
+ item.description ?? "",
1499
+ item.version,
1500
+ ...localizedValues(item.display_name),
1501
+ ...localizedValues(item.display_description),
1502
+ ...localizedValues(item.profession),
1503
+ ...(item.tags ?? []).flatMap(localizedValues)
1504
+ ].join("\n").toLowerCase();
1505
+ }
1506
+ function abortableDelay(ms, signal) {
1507
+ return new Promise((resolve, reject) => {
1508
+ if (signal?.aborted) {
1509
+ reject(new StoreError("aborted", "the operation was aborted"));
1510
+ return;
1511
+ }
1512
+ const timer = setTimeout(() => {
1513
+ signal?.removeEventListener("abort", onAbort);
1514
+ resolve();
1515
+ }, ms);
1516
+ const onAbort = () => {
1517
+ clearTimeout(timer);
1518
+ reject(new StoreError("aborted", "the operation was aborted"));
1519
+ };
1520
+ signal?.addEventListener("abort", onAbort, { once: true });
1521
+ });
1522
+ }
1523
+ var StoreClient = class {
1524
+ host;
1525
+ readyTimeoutMs;
1526
+ readyPollMs;
1527
+ readyPollMaxMs;
1528
+ constructor(host, options = {}) {
1529
+ this.host = host;
1530
+ this.readyTimeoutMs = options.readyTimeoutMs ?? DEFAULT_READY_TIMEOUT_MS;
1531
+ this.readyPollMs = options.readyPollMs ?? DEFAULT_READY_POLL_MS;
1532
+ this.readyPollMaxMs = options.readyPollMaxMs ?? DEFAULT_READY_POLL_MAX_MS;
1533
+ }
1534
+ /** Every item across enabled marketplaces, with install state. */
1535
+ async list() {
1536
+ return (await this.host.listStore()).items;
1537
+ }
1538
+ /** Case-insensitive substring search over the fields a user can see. */
1539
+ async search(query, filter = {}) {
1540
+ const needle = query.trim().toLowerCase();
1541
+ return (await this.list()).filter((item) => {
1542
+ if (filter.kind && item.kind !== filter.kind) return false;
1543
+ if (!needle) return true;
1544
+ return haystack(item).includes(needle);
1545
+ });
1546
+ }
1547
+ /** Items currently installed on this host. */
1548
+ async installed() {
1549
+ return (await this.list()).filter((item) => item.installed);
1550
+ }
1551
+ /**
1552
+ * Installed items whose marketplace offers a different version.
1553
+ *
1554
+ * Both conditions matter: `update_available` is computed from the imported
1555
+ * snapshot even when nothing is installed, so filtering on it alone would
1556
+ * report an "update" for an entry the user has never installed.
1557
+ */
1558
+ async checkUpdates() {
1559
+ return (await this.list()).filter((item) => item.installed && item.update_available);
1560
+ }
1561
+ /**
1562
+ * What a caller can offer for an item's pending update.
1563
+ *
1564
+ * `"none"` means there is nothing to do; `"uninstall_reinstall"` means the
1565
+ * entry is installed at a different version and the only path the wire offers
1566
+ * is to release it and install again (the version-aware re-import then picks
1567
+ * up the current version). `"unknown"` is returned rather than guessing when
1568
+ * the item carries no snapshot to release.
1569
+ */
1570
+ updateHint(item) {
1571
+ if (!item.installed || !item.update_available) return "none";
1572
+ return item.snapshot_id ? "uninstall_reinstall" : "unknown";
1573
+ }
1574
+ /**
1575
+ * Import (when needed) and install one store item, then wait until it is
1576
+ * usable.
1577
+ */
1578
+ async install(item, options = {}) {
1579
+ this.assertUsable(item, options.signal);
1580
+ const result = await this.host.installStoreEntry(item.marketplace_id, item.entry_name);
1581
+ const components = result.outcomes ?? [];
1582
+ const outcome = {
1583
+ snapshotId: result.snapshot_id,
1584
+ reused: result.reused,
1585
+ components,
1586
+ ok: result.errors.length === 0 && components.every((component) => component.ok)
1587
+ };
1588
+ if (options.waitForReady === false) return outcome;
1589
+ return this.awaitReady(outcome, options);
1590
+ }
1591
+ /**
1592
+ * Release an item's runtime artifacts and clear its install record.
1593
+ *
1594
+ * The server only clears the record for components it actually released, so a
1595
+ * partial failure leaves `ok: false` with the failing component named — and
1596
+ * the item stays installed, which is what makes a retry meaningful.
1597
+ */
1598
+ async uninstall(item, options = {}) {
1599
+ const snapshotId = this.requireSnapshot(item);
1600
+ const componentIds = options.componentIds ?? await this.installedComponentIds(snapshotId);
1601
+ if (componentIds.length === 0) return {
1602
+ snapshotId,
1603
+ reused: true,
1604
+ components: [],
1605
+ ok: true
1606
+ };
1607
+ const status = await this.host.uninstallInstall(snapshotId, componentIds);
1608
+ return this.outcomeFromStatus(snapshotId, status, false);
1609
+ }
1610
+ /**
1611
+ * Switch an item's components on or off.
1612
+ *
1613
+ * Idempotent: the server sets the state it is asked for instead of toggling.
1614
+ * A skill reports `skill_disable_flag_only` because the skill corpus has no
1615
+ * enable state — that code is passed through, not swallowed, so a caller can
1616
+ * tell "switched off" from "marked in the catalogue".
1617
+ */
1618
+ async setEnabled(item, enabled, options = {}) {
1619
+ const snapshotId = this.requireSnapshot(item);
1620
+ const componentIds = options.componentIds ?? await this.installedComponentIds(snapshotId);
1621
+ if (componentIds.length === 0) return {
1622
+ snapshotId,
1623
+ reused: true,
1624
+ components: [],
1625
+ ok: true
1626
+ };
1627
+ const status = enabled ? await this.host.enableInstall(snapshotId, componentIds) : await this.host.disableInstall(snapshotId, componentIds);
1628
+ return this.outcomeFromStatus(snapshotId, status, false);
1629
+ }
1630
+ assertUsable(item, signal) {
1631
+ if (signal?.aborted) throw new StoreError("aborted", "the operation was aborted before it started");
1632
+ if (item.blocked_reason) throw new StoreError("item_blocked", item.blocked_reason);
1633
+ }
1634
+ requireSnapshot(item) {
1635
+ const snapshotId = item.snapshot_id;
1636
+ if (!snapshotId) throw new StoreError("not_installed", `store item ${item.id} has no imported snapshot to act on`);
1637
+ return snapshotId;
1638
+ }
1639
+ async installedComponentIds(snapshotId) {
1640
+ return (await this.host.getInstallStatus(snapshotId)).components.filter((component) => component.state !== "not-installed").map((component) => component.id);
1641
+ }
1642
+ outcomeFromStatus(snapshotId, status, reused) {
1643
+ const components = status.outcomes ?? [];
1644
+ return {
1645
+ snapshotId,
1646
+ reused,
1647
+ components,
1648
+ ok: (status.errors ?? []).length === 0 && components.every((component) => component.ok)
1649
+ };
1650
+ }
1651
+ /**
1652
+ * Poll until every component of the snapshot is usable.
1653
+ *
1654
+ * A timeout does **not** discard the install: the caller gets the successful
1655
+ * install plus `ready: false` and `readyIssue: "ready_timeout"`, so a slow
1656
+ * connector never turns into "the install failed".
1657
+ */
1658
+ async awaitReady(outcome, options) {
1659
+ const snapshotId = outcome.snapshotId;
1660
+ if (!snapshotId) return outcome;
1661
+ const status = await this.host.getInstallStatus(snapshotId);
1662
+ const connectors = status.components.filter((component) => component.kind === "connector" && component.state !== "not-installed");
1663
+ if (connectors.length > 0) await this.host.enableInstall(snapshotId, connectors.map((component) => component.id));
1664
+ const deadline = Date.now() + (options.timeoutMs ?? this.readyTimeoutMs);
1665
+ let interval = this.readyPollMs;
1666
+ let last = status.components;
1667
+ for (;;) {
1668
+ const blocked = (await Promise.all(last.filter((component) => component.state !== "not-installed").map(async (component) => ({
1669
+ component,
1670
+ verdict: await this.readiness(component)
1671
+ })))).find((entry) => !entry.verdict.ready);
1672
+ if (!blocked) return {
1673
+ ...outcome,
1674
+ ready: true
1675
+ };
1676
+ if (!blocked.verdict.ready && blocked.verdict.terminal) return {
1677
+ ...outcome,
1678
+ ready: false,
1679
+ readyIssue: blocked.verdict.terminal,
1680
+ readyComponentId: blocked.component.id
1681
+ };
1682
+ if (Date.now() >= deadline) return {
1683
+ ...outcome,
1684
+ ready: false,
1685
+ readyIssue: "ready_timeout",
1686
+ readyComponentId: blocked.component.id
1687
+ };
1688
+ await abortableDelay(interval, options.signal);
1689
+ interval = Math.min(interval * 2, this.readyPollMaxMs);
1690
+ last = (await this.host.getInstallStatus(snapshotId)).components;
1691
+ }
1692
+ }
1693
+ /**
1694
+ * Is this one component usable yet?
1695
+ *
1696
+ * Each kind answers with the narrowest read the protocol already offers. An
1697
+ * unknown kind is reported ready rather than blocking: this layer cannot judge
1698
+ * a component it does not understand, and a false timeout on a future kind
1699
+ * would be worse than passing it through.
1700
+ */
1701
+ async readiness(component) {
1702
+ try {
1703
+ switch (component.kind) {
1704
+ case "skill": return (await this.host.skills.list()).some((skill) => skill.name === component.name) ? { ready: true } : { ready: false };
1705
+ case "agent": return (await this.host.agents.get(component.id)).preset_id ? { ready: true } : { ready: false };
1706
+ case "team":
1707
+ await this.host.teams.get(component.id);
1708
+ return { ready: true };
1709
+ case "connector": {
1710
+ if ((await this.host.connectors.test(component.id)).success) return { ready: true };
1711
+ const status = await this.host.connectors.status(component.id);
1712
+ if (status.status === "authorization_required" || status.status === "reauthorization_required") return {
1713
+ ready: false,
1714
+ terminal: "authorization_required"
1715
+ };
1716
+ return { ready: false };
1717
+ }
1718
+ default: return { ready: true };
1719
+ }
1720
+ } catch {
1721
+ return { ready: false };
1722
+ }
1723
+ }
690
1724
  };
691
1725
  //#endregion
692
1726
  //#region src/teams.ts
@@ -776,6 +1810,12 @@ var AppServerClient = class {
776
1810
  teams;
777
1811
  /** Public model directory (`models/list`, REQ-PAR-05b). */
778
1812
  models;
1813
+ /**
1814
+ * Agent Store lifecycle as one state machine: search → install (wait until
1815
+ * usable) → enable/disable → uninstall. Composes the flat methods below; it
1816
+ * adds no wire surface.
1817
+ */
1818
+ store;
779
1819
  clientInfo;
780
1820
  capabilities;
781
1821
  initializeResult = null;
@@ -794,6 +1834,7 @@ var AppServerClient = class {
794
1834
  this.agents = new AgentClient(this.transport);
795
1835
  this.teams = new TeamClient(this.transport);
796
1836
  this.models = new ModelClient(this.transport);
1837
+ this.store = new StoreClient(this, options.store);
797
1838
  this.bridgeNotifications();
798
1839
  }
799
1840
  /**
@@ -863,7 +1904,14 @@ var AppServerClient = class {
863
1904
  async getInstallStatus(snapshotId) {
864
1905
  return this.transport.request("install/status", { snapshot_id: snapshotId });
865
1906
  }
866
- /** Disable installed components (runtime artifacts stay). */
1907
+ /**
1908
+ * Disable installed components.
1909
+ *
1910
+ * The runtime state moves with the flag — a connector's MCP server is switched
1911
+ * off, an expert's Preset is disabled — while the artifacts stay on disk. A
1912
+ * skill is the documented exception: the corpus has no enable state, so it
1913
+ * reports `skill_disable_flag_only` and the flag stays a catalogue marker.
1914
+ */
867
1915
  async disableInstall(snapshotId, componentIds) {
868
1916
  return this.transport.request("install/disable", {
869
1917
  snapshot_id: snapshotId,
@@ -1331,14 +2379,19 @@ exports.ConversationClient = ConversationClient;
1331
2379
  exports.ConversationHandle = ConversationHandle;
1332
2380
  exports.ConversationSubscription = ConversationSubscription;
1333
2381
  exports.EventSubscription = EventSubscription;
2382
+ exports.HttpTransport = HttpTransport;
1334
2383
  exports.ModelClient = ModelClient;
1335
2384
  exports.RunClient = RunClient;
1336
2385
  exports.SkillClient = SkillClient;
2386
+ exports.StoreClient = StoreClient;
2387
+ exports.StoreError = StoreError;
1337
2388
  exports.TeamClient = TeamClient;
1338
2389
  exports.WebSocketTransport = WebSocketTransport;
1339
2390
  exports.WorkspaceClient = WorkspaceClient;
1340
2391
  exports.aggregateTurnResult = aggregateTurnResult;
2392
+ exports.appServerErrorFromWire = appServerErrorFromWire;
1341
2393
  exports.conversationMessages = conversationMessages;
2394
+ exports.httpRouteTable = httpRouteTable;
1342
2395
  exports.isLoopbackUrl = isLoopbackUrl;
1343
2396
  exports.launchRun = launchRun;
1344
2397
  exports.withRetry = withRetry;