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

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.mjs CHANGED
@@ -237,6 +237,535 @@ var WebSocketTransport = class {
237
237
  }
238
238
  };
239
239
  //#endregion
240
+ //#region src/http-transport.ts
241
+ /**
242
+ * HTTP transport binding (doc `16` R2 / `05` §2.1.1).
243
+ *
244
+ * ## What this is
245
+ *
246
+ * The App Server exposes one method semantics over two bindings. This class is
247
+ * the **request/response** binding: it maps a protocol method name onto the
248
+ * server's hand-written REST route and returns the same result body the
249
+ * WebSocket binding would.
250
+ *
251
+ * ## What this is not
252
+ *
253
+ * - **No server push.** HTTP has no notification channel, so `onNotification`
254
+ * hands back a no-op unsubscribe and `notify` throws. Live conversation / run
255
+ * events and subscription methods (`conversation/subscribe`, `run/subscribe`)
256
+ * exist **only** over `WebSocketTransport`; an `HttpTransport` is not a
257
+ * drop-in replacement for it. Consumers that need events must connect over
258
+ * WS (or run both).
259
+ * - **One-shot handshake per call.** `05` §2.1.1: *each* HTTP call performs its
260
+ * own `POST /initialize` → `POST /initialized` → business call, after which
261
+ * the connection id expires. `connect()` therefore has nothing to hold open
262
+ * and is a no-op; it does not warm anything up.
263
+ * - **Host file services stay out.** `/api/fs/*` (browse / list / read /
264
+ * metadata) is a host file service on the server *root*, explicitly not a
265
+ * protocol method (`05` §2.1.1), so it is not reachable through this class.
266
+ *
267
+ * ## The route table
268
+ *
269
+ * Every entry was read out of the handler bodies in
270
+ * `crates/backend/nomifun-app-server/src/lib.rs` — the `source` field records
271
+ * which handler (and, where one exists, which shared `*_impl`) proves the two
272
+ * bindings run the same operation. Methods with no entry throw
273
+ * `TransportError("send", …)` instead of silently degrading.
274
+ */
275
+ /** Header the server mints the per-call connection id under. */
276
+ const CONNECTION_HEADER = "x-app-server-connection-id";
277
+ /** Protocol version this binding announces on the one-shot handshake. */
278
+ const PROTOCOL_VERSION = "fp-7";
279
+ /**
280
+ * Protocol method → REST route. Verified from handler bodies, not from the
281
+ * route list (a route existing does not prove which method it mirrors).
282
+ */
283
+ const HTTP_ROUTES = {
284
+ ping: {
285
+ verb: "POST",
286
+ path: "/ping",
287
+ source: "ping() -> registry.require_ready"
288
+ },
289
+ "workspace/list": {
290
+ verb: "GET",
291
+ path: "/workspaces",
292
+ source: "workspace_list() -> owner-scoped active rows"
293
+ },
294
+ "workspace/revoke": {
295
+ verb: "DELETE",
296
+ path: "/workspaces/:workspace_id",
297
+ source: "workspace_delete() -> workspace_revoke_impl (shared with the WS arm)"
298
+ },
299
+ "conversation/create": {
300
+ verb: "POST",
301
+ path: "/conversations",
302
+ source: "conversation_create() -> create_conversation_for_user (shared)"
303
+ },
304
+ "conversation/list": {
305
+ verb: "GET",
306
+ path: "/conversations",
307
+ query: ["limit"],
308
+ source: "conversation_list() -> list_app_server_chats (shared); Query<limit>"
309
+ },
310
+ "conversation/get": {
311
+ verb: "GET",
312
+ path: "/conversations/:conversation_id",
313
+ source: "conversation_get() -> project_conversation_view (shared)"
314
+ },
315
+ "conversation/delete": {
316
+ verb: "DELETE",
317
+ path: "/conversations/:conversation_id",
318
+ source: "conversation_delete() -> conversation/delete arm semantics"
319
+ },
320
+ "conversation/messages": {
321
+ verb: "GET",
322
+ path: "/conversations/:conversation_id/messages",
323
+ query: [
324
+ "page",
325
+ "page_size",
326
+ "cursor"
327
+ ],
328
+ source: "conversation_messages() -> list_conversation_messages_for_user (shared)"
329
+ },
330
+ "conversation/send": {
331
+ verb: "POST",
332
+ path: "/conversations/:conversation_id/messages",
333
+ body: [
334
+ "content",
335
+ "idempotency_key",
336
+ "attachments",
337
+ "mentions",
338
+ "model",
339
+ "reasoning_effort"
340
+ ],
341
+ source: "conversation_send() -> send_conversation_message_for_user (shared with the WS arm)"
342
+ },
343
+ "conversation/cancel": {
344
+ verb: "POST",
345
+ path: "/conversations/:conversation_id/cancel",
346
+ source: "conversation_cancel() -> cancel arm semantics"
347
+ },
348
+ "agent/run": {
349
+ verb: "POST",
350
+ path: "/agent/run",
351
+ source: "agent_run() -> same request/receipt as the `agent/run` arm"
352
+ },
353
+ "team/run": {
354
+ verb: "POST",
355
+ path: "/team/run",
356
+ source: "team_run() -> execute_team_run() (same as the WS arm); Leader Conversation + planned delegation, `TeamRunReceipt`"
357
+ },
358
+ "run/get": {
359
+ verb: "GET",
360
+ path: "/run/:run_id",
361
+ source: "run_get()"
362
+ },
363
+ "run/result": {
364
+ verb: "GET",
365
+ path: "/run/:run_id/result",
366
+ source: "run_result()"
367
+ },
368
+ "run/plan": {
369
+ verb: "GET",
370
+ path: "/run/:run_id/plan",
371
+ source: "run_plan() -> get_run_plan_for_user() (same as the WS arm)"
372
+ },
373
+ "run/events": {
374
+ verb: "GET",
375
+ path: "/run/:run_id/events",
376
+ query: ["after_sequence", "limit"],
377
+ source: "run_events() -> runtime.list_events(after_sequence, limit) (same as the WS arm)"
378
+ },
379
+ "run/cancel": {
380
+ verb: "POST",
381
+ path: "/run/:run_id/cancel",
382
+ body: [
383
+ "expected_version",
384
+ "reason",
385
+ "idempotency_key"
386
+ ],
387
+ source: "run_cancel() -> same CancelRunRequest as the WS arm"
388
+ },
389
+ "run/steer": {
390
+ verb: "POST",
391
+ path: "/run/:run_id/steer",
392
+ body: [
393
+ "text",
394
+ "expected_version",
395
+ "command_id",
396
+ "idempotency_key"
397
+ ],
398
+ source: "run_steer() -> execute_steer_run (shared with the WS arm)"
399
+ },
400
+ "run/answer-decision": {
401
+ verb: "POST",
402
+ path: "/run/:run_id/answer-decision",
403
+ body: [
404
+ "step_id",
405
+ "attempt_id",
406
+ "answer",
407
+ "expected_execution_version",
408
+ "expected_step_version",
409
+ "expected_attempt_version"
410
+ ],
411
+ source: "run_answer_decision() -> execute_answer_decision (shared with the WS arm; engine answer gate, no always_allow)"
412
+ },
413
+ "skill/list": {
414
+ verb: "GET",
415
+ path: "/skills",
416
+ source: "list_skills_route() -> list_skills_impl"
417
+ },
418
+ "skill/get": {
419
+ verb: "GET",
420
+ path: "/skills/:skill_id",
421
+ source: "get_skill_route() -> get_skill_impl"
422
+ },
423
+ "skill/files": {
424
+ verb: "GET",
425
+ path: "/skills/:skill_id/files",
426
+ source: "list_skill_files_route() -> list_skill_files_impl"
427
+ },
428
+ "connector/list": {
429
+ verb: "GET",
430
+ path: "/connectors",
431
+ source: "list_connectors_route() -> list_connectors_impl"
432
+ },
433
+ "models/list": {
434
+ verb: "GET",
435
+ path: "/models",
436
+ source: "list_models_route() -> list_models_impl"
437
+ },
438
+ "connector/get": {
439
+ verb: "GET",
440
+ path: "/connectors/:connector_id",
441
+ source: "get_connector_route() -> get_connector_impl"
442
+ },
443
+ "connector/status": {
444
+ verb: "GET",
445
+ path: "/connectors/:connector_id/status",
446
+ source: "connector_status_route() -> connector_status_impl"
447
+ },
448
+ "connector/test": {
449
+ verb: "POST",
450
+ path: "/connectors/:connector_id/test",
451
+ source: "connector_test_route() -> connector_test_impl"
452
+ },
453
+ "connector/call": {
454
+ verb: "POST",
455
+ path: "/connectors/:connector_id/call",
456
+ source: "connector_call_route() -> connector_call_impl"
457
+ },
458
+ "connector/auth/start": {
459
+ verb: "POST",
460
+ path: "/connectors/:connector_id/auth-start",
461
+ source: "connector_auth_start_route() -> connector_auth_start_impl (WS name uses slashes)"
462
+ },
463
+ "connector/auth/status": {
464
+ verb: "GET",
465
+ path: "/connectors/:connector_id/auth-status",
466
+ source: "connector_auth_status_route() -> connector_auth_status_impl"
467
+ },
468
+ "connector/auth/logout": {
469
+ verb: "POST",
470
+ path: "/connectors/:connector_id/auth-logout",
471
+ source: "connector_auth_logout_route() -> connector_auth_logout_impl"
472
+ },
473
+ "import/run": {
474
+ verb: "POST",
475
+ path: "/imports",
476
+ source: "run_import_route() -> run_import_impl"
477
+ },
478
+ "import/list": {
479
+ verb: "GET",
480
+ path: "/imports",
481
+ source: "list_imports_route() -> list_imports_impl"
482
+ },
483
+ "import/get": {
484
+ verb: "GET",
485
+ path: "/imports/:snapshot_id",
486
+ source: "get_import_route() -> get_import_impl"
487
+ },
488
+ "install/run": {
489
+ verb: "POST",
490
+ path: "/installs",
491
+ source: "run_install_route() -> install_impl"
492
+ },
493
+ "install/status": {
494
+ verb: "GET",
495
+ path: "/installs/:snapshot_id",
496
+ source: "install_status_route() -> install_status_impl"
497
+ },
498
+ "install/disable": {
499
+ verb: "POST",
500
+ path: "/installs/:snapshot_id/disable",
501
+ body: ["component_ids"],
502
+ source: "install_disable_route() -> install_disable_impl; body `component_ids`"
503
+ },
504
+ "install/enable": {
505
+ verb: "POST",
506
+ path: "/installs/:snapshot_id/enable",
507
+ body: ["component_ids"],
508
+ source: "install_enable_route() -> install_enable_impl; body `component_ids`"
509
+ },
510
+ "install/uninstall": {
511
+ verb: "POST",
512
+ path: "/installs/:snapshot_id/uninstall",
513
+ body: ["component_ids"],
514
+ source: "install_uninstall_route() -> install_uninstall_impl; body `component_ids`"
515
+ },
516
+ "market/add": {
517
+ verb: "POST",
518
+ path: "/markets",
519
+ source: "market_add_route() -> market_add_impl"
520
+ },
521
+ "market/list": {
522
+ verb: "GET",
523
+ path: "/markets",
524
+ source: "market_list_route() -> market_list_impl"
525
+ },
526
+ "market/get": {
527
+ verb: "GET",
528
+ path: "/markets/:marketplace_id",
529
+ source: "market_get_route() -> market_get_impl"
530
+ },
531
+ "market/remove": {
532
+ verb: "POST",
533
+ path: "/markets/:marketplace_id/remove",
534
+ body: ["cascade"],
535
+ source: "market_remove_route() -> market_remove_impl; body `cascade` defaults to true"
536
+ },
537
+ "market/auto-update": {
538
+ verb: "POST",
539
+ path: "/markets/:marketplace_id/auto-update",
540
+ body: ["enabled"],
541
+ source: "market_auto_update_route() -> market_auto_update_impl; body `enabled` defaults to true"
542
+ },
543
+ "market/refresh": {
544
+ verb: "POST",
545
+ path: "/markets/:marketplace_id/refresh",
546
+ source: "market_refresh_route() -> market_refresh_impl"
547
+ },
548
+ "market/entry-import": {
549
+ verb: "POST",
550
+ path: "/markets/:marketplace_id/entries/:entry_name/import",
551
+ source: "market_entry_import_route() -> market_entry_import_impl"
552
+ },
553
+ "store/list": {
554
+ verb: "GET",
555
+ path: "/store",
556
+ source: "store_list_route() -> store_list_impl"
557
+ },
558
+ "store/install-entry": {
559
+ verb: "POST",
560
+ path: "/store/:marketplace_id/entries/:entry_name/install",
561
+ source: "store_install_entry_route() -> store_install_entry_impl"
562
+ }
563
+ };
564
+ /** Methods that are WebSocket-only by design — named so the error explains itself. */
565
+ const WS_ONLY_METHODS = /* @__PURE__ */ new Set([
566
+ "initialize",
567
+ "initialized",
568
+ "conversation/subscribe",
569
+ "conversation/unsubscribe",
570
+ "run/subscribe",
571
+ "run/unsubscribe"
572
+ ]);
573
+ var HttpTransport = class {
574
+ routes;
575
+ options;
576
+ closed = false;
577
+ constructor(options, routes = HTTP_ROUTES) {
578
+ this.options = options;
579
+ this.routes = routes;
580
+ }
581
+ /**
582
+ * No-op: the HTTP binding handshakes per call (`05` §2.1.1), so there is no
583
+ * connection to open. Kept for `Transport` compatibility.
584
+ */
585
+ async connect() {
586
+ this.closed = false;
587
+ }
588
+ /**
589
+ * Perform one business call over its own handshake.
590
+ *
591
+ * Throws `TransportError("send", …)` for a method with no HTTP binding —
592
+ * never a silent fallback to another binding.
593
+ */
594
+ async request(method, params) {
595
+ if (this.closed) throw new TransportError("connect", "http transport is closed");
596
+ const route = this.routes[method];
597
+ if (!route) {
598
+ 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)`;
599
+ throw new TransportError("send", reason);
600
+ }
601
+ const { path, query, body } = this.splitParams(route, params);
602
+ const connectionId = await this.openConnection();
603
+ const response = await this.fetchWithTimeout(route.verb, path, {
604
+ query,
605
+ body,
606
+ connectionId
607
+ });
608
+ return await this.readResult(response, method);
609
+ }
610
+ /**
611
+ * Not supported: HTTP carries no server-push. Throws instead of pretending.
612
+ */
613
+ notify(method, _params) {
614
+ throw new TransportError("send", `HTTP binding cannot notify ("${method}"): notifications are WebSocket-only (doc 05 §2.1.1)`);
615
+ }
616
+ /**
617
+ * Returns a no-op unsubscribe: this binding receives **no** notifications.
618
+ *
619
+ * Not equivalent to the WebSocket binding — callers that need live events
620
+ * must use `WebSocketTransport` (or run both).
621
+ */
622
+ onNotification(_listener) {
623
+ return () => void 0;
624
+ }
625
+ /** No-op: every call is independent, so nothing is held open. */
626
+ close() {
627
+ this.closed = true;
628
+ }
629
+ /**
630
+ * One-shot handshake: `POST /initialize` (connection id header) →
631
+ * `POST /initialized`. Returns the connection id.
632
+ *
633
+ * Public because host-side services that sit *outside* the protocol surface
634
+ * (`/api/fs/*`, the HTTP-only `POST /workspaces` register route) still need a
635
+ * ready connection id; they are not protocol methods, so they cannot go
636
+ * through `request()`.
637
+ */
638
+ async openConnection() {
639
+ const connectionId = (await this.fetchWithTimeout("POST", "/initialize", { body: {
640
+ protocol_version: PROTOCOL_VERSION,
641
+ client: this.options.client ?? {
642
+ name: "flowy-agent-store-client",
643
+ version: "0.1.0"
644
+ },
645
+ ...this.options.capabilities === void 0 ? {} : { capabilities: this.options.capabilities }
646
+ } })).headers.get(CONNECTION_HEADER);
647
+ if (!connectionId) throw new TransportError("connect", `handshake response is missing the ${CONNECTION_HEADER} header`, { retryable: true });
648
+ await this.fetchWithTimeout("POST", "/initialized", { connectionId });
649
+ return connectionId;
650
+ }
651
+ splitParams(route, params) {
652
+ const source = isRecord(params) ? params : {};
653
+ const pathParams = [...route.path.matchAll(/:([A-Za-z0-9_]+)/g)].map((match) => match[1]);
654
+ const path = route.path.replace(/:([A-Za-z0-9_]+)/g, (_full, name) => {
655
+ const value = source[name];
656
+ if (value === void 0 || value === null) throw new TransportError("send", `missing required path param "${name}" for ${route.verb} ${route.path}`);
657
+ return encodeURIComponent(String(value));
658
+ });
659
+ const query = {};
660
+ for (const key of route.query ?? []) {
661
+ const value = source[key];
662
+ if (value !== void 0 && value !== null) query[key] = String(value);
663
+ }
664
+ if (route.verb === "GET" || route.verb === "DELETE") return {
665
+ path,
666
+ query,
667
+ body: void 0
668
+ };
669
+ const carried = route.body ?? [];
670
+ const body = {};
671
+ for (const key of Object.keys(source)) {
672
+ if (pathParams.includes(key) || (route.query ?? []).includes(key)) continue;
673
+ if (carried.length > 0 && !carried.includes(key)) continue;
674
+ body[key] = source[key];
675
+ }
676
+ return {
677
+ path,
678
+ query,
679
+ body
680
+ };
681
+ }
682
+ async fetchWithTimeout(verb, path, request) {
683
+ const url = new URL(`${this.baseUrl()}${path}`);
684
+ for (const [key, value] of Object.entries(request.query ?? {})) url.searchParams.set(key, value);
685
+ const headers = { accept: "application/json" };
686
+ if (request.body !== void 0) headers["content-type"] = "application/json";
687
+ if (request.connectionId) headers[CONNECTION_HEADER] = request.connectionId;
688
+ if (this.options.token) headers.authorization = `Bearer ${this.options.token}`;
689
+ const controller = new AbortController();
690
+ const timeoutMs = this.options.requestTimeoutMs ?? 3e4;
691
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
692
+ try {
693
+ const fetchImpl = this.options.fetch ?? globalThis.fetch;
694
+ if (!fetchImpl) throw new TransportError("connect", "no fetch implementation available");
695
+ return await fetchImpl(url.toString(), {
696
+ method: verb,
697
+ headers,
698
+ body: request.body === void 0 ? void 0 : JSON.stringify(request.body),
699
+ signal: controller.signal
700
+ });
701
+ } catch (error) {
702
+ if (error instanceof TransportError) throw error;
703
+ const aborted = error?.name === "AbortError";
704
+ throw new TransportError("send", aborted ? `${verb} ${path} timed out after ${timeoutMs}ms` : `${verb} ${path} failed: ${String(error)}`, {
705
+ retryable: true,
706
+ cause: error
707
+ });
708
+ } finally {
709
+ clearTimeout(timer);
710
+ }
711
+ }
712
+ /** 2xx → result body; otherwise the server's wire error becomes an `AppServerError`. */
713
+ async readResult(response, method) {
714
+ let payload = void 0;
715
+ const text = await response.text();
716
+ if (text.length > 0) try {
717
+ payload = JSON.parse(text);
718
+ } catch {
719
+ if (response.ok) throw new TransportError("receive", `${method}: response is not JSON`, { retryable: true });
720
+ }
721
+ if (response.ok) return payload;
722
+ throw appServerErrorFromWire(payload, response.status, method);
723
+ }
724
+ baseUrl() {
725
+ return this.options.baseUrl.replace(/\/+$/, "").replace(/\/ws$/, "");
726
+ }
727
+ };
728
+ function isRecord(value) {
729
+ return typeof value === "object" && value !== null && !Array.isArray(value);
730
+ }
731
+ /**
732
+ * Turn an HTTP failure body into the SDK error model.
733
+ *
734
+ * Shared by this transport and by host-side helpers (webui `lib/client.ts`) so
735
+ * a second spelling of the wire-error mapping cannot drift. Two wire contracts
736
+ * reach this function, both of them `AppError` on the server:
737
+ *
738
+ * - the App Server emits `{code, message, retryable, details, request_id}`
739
+ * (`into_wire_error`), which is exactly the `AppServerError` shape;
740
+ * - the standalone host file service (`/api/fs/*`) emits the `ErrorResponse`
741
+ * envelope `{success: false, error, code, details?}` — same error, another
742
+ * spelling: `error` carries the human message, and there is no retry hint, so
743
+ * a 5xx status stays the retry signal.
744
+ *
745
+ * Anything else becomes a retryable-on-5xx `TransportError`.
746
+ */
747
+ function appServerErrorFromWire(payload, status, context = "request") {
748
+ if (isRecord(payload) && typeof payload.code === "string" && typeof payload.message === "string") return new AppServerError({
749
+ code: payload.code,
750
+ message: payload.message,
751
+ retryable: payload.retryable === true,
752
+ request_id: payload.request_id ?? null,
753
+ details: isRecord(payload.details) ? payload.details : {}
754
+ });
755
+ if (isRecord(payload) && payload.success === false && typeof payload.code === "string" && typeof payload.error === "string") return new AppServerError({
756
+ code: payload.code,
757
+ message: payload.error,
758
+ retryable: status >= 500,
759
+ request_id: null,
760
+ details: isRecord(payload.details) ? payload.details : {}
761
+ });
762
+ return new TransportError("receive", `${context}: HTTP ${status} without a wire error body`, { retryable: status >= 500 });
763
+ }
764
+ /** Exposed for tests and docs (R5): the verified method → route table. */
765
+ function httpRouteTable() {
766
+ return HTTP_ROUTES;
767
+ }
768
+ //#endregion
240
769
  //#region src/agents.ts
241
770
  var AgentClient = class {
242
771
  transport;
@@ -268,7 +797,9 @@ var ConversationClient = class {
268
797
  name: input.name,
269
798
  model,
270
799
  workspace: input.workspaceId ? { id: input.workspaceId } : void 0,
271
- reasoning_effort: input.reasoningEffort || void 0
800
+ reasoning_effort: input.reasoningEffort || void 0,
801
+ agent_id: input.agentId || void 0,
802
+ team_id: input.teamId || void 0
272
803
  });
273
804
  }
274
805
  update(conversationId, input) {
@@ -299,11 +830,35 @@ var ConversationClient = class {
299
830
  cursor: query.cursor
300
831
  });
301
832
  }
302
- send(conversationId, content, idempotencyKey) {
833
+ /**
834
+ * R15(W10):`attachments` 是**会话工作区内的绝对路径**(图片附件)。
835
+ * doc `27` §4.1:`mentions` 是**本轮挂载的技能**(`kind: "skill"`)——技能的正文与
836
+ * 不可变快照都随这一轮走;`agent` / `connector` 在 send 上没有载体,服务端会以
837
+ * `invalid_request` **明确拒绝**(不是静默不挂)。
838
+ *
839
+ * 两者缺省都不上 wire,保持老调用方的形状逐字不变;服务端只接受会话工作区内的真实
840
+ * 文件(越界 / 相对路径 / 不存在一律拒),准入见 `nomifun-app-server` 的
841
+ * `resolve_conversation_attachments` 与 `send_mention_skills`。
842
+ *
843
+ * 第 4 个参数同时接受旧的 `string[]`(附件)与选项对象——再堆一个位置参数会让调用点
844
+ * 变成一串没有名字的布尔/数组。
845
+ */
846
+ send(conversationId, content, idempotencyKey, options = {}) {
847
+ const attachments = Array.isArray(options) ? options : options.attachments ?? [];
848
+ const mentions = Array.isArray(options) ? [] : options.mentions ?? [];
849
+ const model = Array.isArray(options) ? void 0 : options.model;
850
+ const reasoningEffort = Array.isArray(options) ? void 0 : options.reasoningEffort;
303
851
  return this.transport.request("conversation/send", {
304
852
  conversation_id: conversationId,
305
853
  content,
306
- idempotency_key: idempotencyKey
854
+ idempotency_key: idempotencyKey,
855
+ ...attachments.length > 0 ? { attachments } : {},
856
+ ...mentions.length > 0 ? { mentions } : {},
857
+ ...model ? { model: {
858
+ provider_id: model.provider_id,
859
+ model: model.model
860
+ } } : {},
861
+ ...reasoningEffort ? { reasoning_effort: reasoningEffort } : {}
307
862
  });
308
863
  }
309
864
  cancel(conversationId) {
@@ -312,35 +867,92 @@ var ConversationClient = class {
312
867
  delete(conversationId) {
313
868
  return this.transport.request("conversation/delete", { conversation_id: conversationId });
314
869
  }
315
- async follow(conversationId) {
870
+ /**
871
+ * Subscribe to one conversation's realtime stream (doc `16` R1).
872
+ *
873
+ * `options.fetchMessages` is what makes auto catch-up possible: a detected
874
+ * sequence gap or a `conversation/resync-required` triggers one transcript
875
+ * fetch whose result is handed to `onBackfill`. Conversations have no event
876
+ * replay API (`05` §「V1 不提供 after_cursor 事件追平」), so the transcript is
877
+ * the recovery medium — unlike Runs, where `run/events` replays the events
878
+ * themselves.
879
+ */
880
+ async follow(conversationId, options = {}) {
316
881
  await this.transport.request("conversation/subscribe", { conversation_id: conversationId });
317
- return new ConversationSubscription(this.transport, conversationId);
882
+ return new ConversationSubscription(this.transport, conversationId, {
883
+ fetchMessages: (id) => this.messages({
884
+ conversationId: id,
885
+ pageSize: 50
886
+ }),
887
+ ...options
888
+ });
318
889
  }
319
890
  };
891
+ /** Events held before the first listener attaches (see `pendingLimit`). */
892
+ const DEFAULT_PENDING_LIMIT = 256;
320
893
  var ConversationSubscription = class {
321
894
  transport;
322
895
  conversationId;
896
+ options;
323
897
  events = /* @__PURE__ */ new Set();
324
898
  resyncs = /* @__PURE__ */ new Set();
899
+ errors = /* @__PURE__ */ new Set();
900
+ backfills = /* @__PURE__ */ new Set();
325
901
  closed = false;
326
902
  lastSeenSequence = 0;
327
903
  removeListener;
328
- constructor(transport, conversationId) {
904
+ /**
905
+ * `follow()` resolves as soon as the server acknowledged the subscription,
906
+ * but the caller attaches listeners one tick later. Events landing in that
907
+ * window used to be dropped silently; they are held here (oldest first) and
908
+ * flushed to the first `onEvent` listener.
909
+ */
910
+ pending = [];
911
+ pendingDropped = 0;
912
+ backfillInFlight = null;
913
+ constructor(transport, conversationId, options = {}) {
329
914
  this.transport = transport;
330
915
  this.conversationId = conversationId;
916
+ this.options = options;
331
917
  this.removeListener = transport.onNotification((notification) => this.dispatch(notification));
332
918
  }
333
919
  get lastSequence() {
334
920
  return this.lastSeenSequence;
335
921
  }
922
+ /** Events dropped because nobody was listening and the buffer was full. */
923
+ get droppedPendingCount() {
924
+ return this.pendingDropped;
925
+ }
336
926
  onEvent(listener) {
337
927
  this.events.add(listener);
928
+ if (this.events.size === 1 && this.pending.length > 0) {
929
+ const queued = this.pending;
930
+ this.pending = [];
931
+ for (const event of queued) this.deliver(event);
932
+ }
338
933
  return () => this.events.delete(listener);
339
934
  }
340
935
  onResync(listener) {
341
936
  this.resyncs.add(listener);
342
937
  return () => this.resyncs.delete(listener);
343
938
  }
939
+ /**
940
+ * Auto catch-up result (R1). Fires only when `autoResync` is on and the
941
+ * transcript fetch succeeded; the listener owns replacing the transcript.
942
+ */
943
+ onBackfill(listener) {
944
+ this.backfills.add(listener);
945
+ return () => this.backfills.delete(listener);
946
+ }
947
+ /**
948
+ * Failures that used to be swallowed: the catch-up fetch, and a failed
949
+ * re-subscribe during `rearm()`. Listener isolation keeps one bad listener
950
+ * from hiding the error from the others.
951
+ */
952
+ onError(listener) {
953
+ this.errors.add(listener);
954
+ return () => this.errors.delete(listener);
955
+ }
344
956
  async close() {
345
957
  if (this.closed) return;
346
958
  this.closed = true;
@@ -348,6 +960,9 @@ var ConversationSubscription = class {
348
960
  this.removeListener = null;
349
961
  this.events.clear();
350
962
  this.resyncs.clear();
963
+ this.errors.clear();
964
+ this.backfills.clear();
965
+ this.pending = [];
351
966
  try {
352
967
  await this.transport.request("conversation/unsubscribe", { conversation_id: this.conversationId });
353
968
  } catch {}
@@ -355,35 +970,99 @@ var ConversationSubscription = class {
355
970
  /**
356
971
  * Re-arm after a reconnect (docs/agent-store/16 T8): re-register the
357
972
  * notification listener, reset the cursor and re-issue
358
- * `conversation/subscribe`. This subscription has no event-replay API, so the
359
- * caller must still backfill the transcript for the outage window
360
- * (`conversation/messages`). Call it once the handshake completed; a no-op
361
- * once closed.
973
+ * `conversation/subscribe`. The server restarts a conversation's sequence at
974
+ * 1 for a fresh subscription, which is exactly why the cursor resets. On a
975
+ * failed re-subscribe the error reaches `onError` and the caller can still
976
+ * backfill the outage window via `conversation/messages`.
362
977
  */
363
978
  async rearm() {
364
979
  if (this.closed) return;
365
980
  this.removeListener?.();
366
981
  this.removeListener = this.transport.onNotification((notification) => this.dispatch(notification));
367
982
  this.lastSeenSequence = 0;
368
- await this.transport.request("conversation/subscribe", { conversation_id: this.conversationId });
983
+ try {
984
+ await this.transport.request("conversation/subscribe", { conversation_id: this.conversationId });
985
+ } catch (error) {
986
+ this.emitError(error);
987
+ throw error;
988
+ }
369
989
  }
370
990
  dispatch(notification) {
371
991
  if (notification.method === "conversation/event") {
372
992
  const event = notification.params;
373
- if (event.conversation_id !== this.conversationId || event.sequence <= this.lastSeenSequence) return;
993
+ if (event.conversation_id !== this.conversationId) return;
994
+ if (!Number.isFinite(event.sequence)) return;
995
+ if (event.sequence <= this.lastSeenSequence) return;
996
+ const gap = this.lastSeenSequence > 0 && event.sequence > this.lastSeenSequence + 1;
374
997
  this.lastSeenSequence = event.sequence;
375
- for (const listener of [...this.events]) try {
376
- listener(event);
377
- } catch {}
998
+ if (this.events.size === 0) this.buffer(event);
999
+ else this.deliver(event);
1000
+ if (gap) {
1001
+ this.emitResync("gap");
1002
+ this.requestBackfill("gap");
1003
+ }
378
1004
  return;
379
1005
  }
380
1006
  if (notification.method === "conversation/resync-required") {
381
1007
  if (!notification.params.conversation_ids.includes(this.conversationId)) return;
382
- for (const listener of [...this.resyncs]) try {
383
- listener(notification.params.reason);
384
- } catch {}
1008
+ this.emitResync(notification.params.reason);
1009
+ this.requestBackfill(notification.params.reason);
1010
+ }
1011
+ }
1012
+ buffer(event) {
1013
+ const limit = this.options.pendingLimit ?? DEFAULT_PENDING_LIMIT;
1014
+ this.pending.push(event);
1015
+ while (this.pending.length > limit) {
1016
+ this.pending.shift();
1017
+ this.pendingDropped += 1;
385
1018
  }
386
1019
  }
1020
+ deliver(event) {
1021
+ for (const listener of [...this.events]) try {
1022
+ listener(event);
1023
+ } catch {}
1024
+ }
1025
+ emitResync(reason) {
1026
+ for (const listener of [...this.resyncs]) try {
1027
+ listener(reason);
1028
+ } catch {}
1029
+ }
1030
+ emitError(error) {
1031
+ for (const listener of [...this.errors]) try {
1032
+ listener(error);
1033
+ } catch {}
1034
+ }
1035
+ /**
1036
+ * One in-flight catch-up at a time: a burst of resync signals collapses into
1037
+ * a single transcript fetch (the fetch itself returns the newest page, so the
1038
+ * result supersedes every signal that arrived before it).
1039
+ */
1040
+ requestBackfill(reason) {
1041
+ if (this.closed || this.options.autoResync === false) return;
1042
+ const fetchMessages = this.options.fetchMessages;
1043
+ if (!fetchMessages || this.backfillInFlight) return;
1044
+ const task = (async () => {
1045
+ try {
1046
+ const page = await fetchMessages(this.conversationId);
1047
+ if (this.closed) return;
1048
+ const backfill = {
1049
+ conversationId: this.conversationId,
1050
+ reason,
1051
+ messages: page?.items ?? [],
1052
+ hasMore: page?.has_more === true
1053
+ };
1054
+ for (const listener of [...this.backfills]) try {
1055
+ listener(backfill);
1056
+ } catch {}
1057
+ } catch (error) {
1058
+ this.emitError(error);
1059
+ }
1060
+ })();
1061
+ this.backfillInFlight = task;
1062
+ task.finally(() => {
1063
+ if (this.backfillInFlight === task) this.backfillInFlight = null;
1064
+ });
1065
+ }
387
1066
  };
388
1067
  //#endregion
389
1068
  //#region src/connectors.ts
@@ -404,7 +1083,19 @@ var ConnectorClient = class {
404
1083
  status(connectorId) {
405
1084
  return this.transport.request("connector/status", { connector_id: connectorId });
406
1085
  }
407
- /** Run a connection probe; the server persists the result. */
1086
+ /**
1087
+ * Run a connection probe and report what the connector currently offers —
1088
+ * including each tool's `input_schema`.
1089
+ *
1090
+ * It really connects: a stdio connector gets its child process spawned, an
1091
+ * HTTP/SSE one gets `initialize` + `tools/list`. The result is **persisted**,
1092
+ * so this is also what refreshes the tool list `get()` reads back.
1093
+ *
1094
+ * Named `test` and not `probe` on purpose: this repo's split is `test` for the
1095
+ * action (wire `connector/test`, `test_connection`) and `probe` for its
1096
+ * artifact (`ConnectorProbeResult`, `probe_status`), and every method here is
1097
+ * the last segment of its wire method.
1098
+ */
408
1099
  test(connectorId) {
409
1100
  return this.transport.request("connector/test", { connector_id: connectorId });
410
1101
  }
@@ -423,6 +1114,36 @@ var ConnectorClient = class {
423
1114
  logout(connectorId) {
424
1115
  return this.transport.request("connector/auth/logout", { connector_id: connectorId }).then(() => void 0);
425
1116
  }
1117
+ /**
1118
+ * Run one tool on a registered connector, through the host's own connection.
1119
+ *
1120
+ * The host holds the transport, its headers and its OAuth token; this sends a
1121
+ * tool name and an argument object and nothing else — you cannot name a URL,
1122
+ * a command or a header. Whether the pair is callable at all is the host's
1123
+ * `[connector_proxy]` policy: once its operator turns the proxy on, the
1124
+ * enabled connectors are callable, and `allow` / `deny` are that operator's
1125
+ * optional narrowing and subtraction. So `policy_denied` means either that
1126
+ * the host never turned the proxy on, or that this pair was narrowed out or
1127
+ * explicitly denied — not that a tool name was misspelled in a list you were
1128
+ * supposed to maintain.
1129
+ *
1130
+ * Arguments are passed through verbatim (there is no client-side schema
1131
+ * check); `get()` / `test()` return each tool's `input_schema` so you can see
1132
+ * what it takes, and a parameter the server rejects comes back as
1133
+ * `is_error: true`, not as a rejected promise.
1134
+ *
1135
+ * Resolves even when the tool itself failed — check `is_error`. It rejects
1136
+ * only when the call never reached the tool: `connector_call_timeout`,
1137
+ * `connector_call_failed`, `response_too_large`, `connector_unavailable`,
1138
+ * `policy_denied`, `not_found`.
1139
+ */
1140
+ call(connectorId, tool, args = {}) {
1141
+ return this.transport.request("connector/call", {
1142
+ connector_id: connectorId,
1143
+ tool,
1144
+ arguments: args
1145
+ });
1146
+ }
426
1147
  };
427
1148
  //#endregion
428
1149
  //#region src/runs.ts
@@ -439,6 +1160,18 @@ var RunClient = class {
439
1160
  agent(input) {
440
1161
  return this.transport.request("agent/run", toWire(input));
441
1162
  }
1163
+ /**
1164
+ * Start a Team run (`16` §7 决策 3): the server creates the Leader
1165
+ * Conversation, materializes/binds the Team's execution template and returns
1166
+ * the public run id of the execution the Leader started.
1167
+ *
1168
+ * The receipt is `TeamRunReceipt`, not `RunReceipt` — a Team Run has no lead
1169
+ * preset revision/digest to report. Member pool and concurrency limits are not
1170
+ * inputs; sending `planning` is rejected as `invalid_request`.
1171
+ */
1172
+ team(input) {
1173
+ return this.transport.request("team/run", toTeamWire(input));
1174
+ }
442
1175
  /** Read the authoritative run state. */
443
1176
  get(runId) {
444
1177
  return this.transport.request("run/get", { run_id: runId });
@@ -447,6 +1180,14 @@ var RunClient = class {
447
1180
  result(runId) {
448
1181
  return this.transport.request("run/result", { run_id: runId });
449
1182
  }
1183
+ /**
1184
+ * Read the authoritative plan / step snapshot (W4 / W6, resolves D-W6-1):
1185
+ * titles, statuses, retries with their reason, errors and timings — none of
1186
+ * which the append-only event log carries.
1187
+ */
1188
+ plan(runId) {
1189
+ return this.transport.request("run/plan", { run_id: runId });
1190
+ }
450
1191
  /** Replay persisted events after a cursor. */
451
1192
  events(query) {
452
1193
  return this.transport.request("run/events", {
@@ -477,6 +1218,26 @@ var RunClient = class {
477
1218
  idempotency_key: input.idempotencyKey
478
1219
  });
479
1220
  }
1221
+ /**
1222
+ * Answer the pending decision of a `waiting_input` attempt.
1223
+ *
1224
+ * The three `expected*Version` tokens are mandatory CAS inputs, normally the
1225
+ * ones projected on the pending `approval.requested` event. The server passes
1226
+ * them straight to the engine's single answer gate, so a stale token is a
1227
+ * `conflict`, never a silent overwrite. There is no `always_allow` here: the
1228
+ * desktop confirmation route's approve-all flag is not part of this protocol.
1229
+ */
1230
+ answerDecision(input) {
1231
+ return this.transport.request("run/answer-decision", {
1232
+ run_id: input.runId,
1233
+ step_id: input.stepId,
1234
+ attempt_id: input.attemptId,
1235
+ answer: input.answer,
1236
+ expected_execution_version: input.expectedExecutionVersion,
1237
+ expected_step_version: input.expectedStepVersion,
1238
+ expected_attempt_version: input.expectedAttemptVersion
1239
+ });
1240
+ }
480
1241
  /** Subscribe to best-effort realtime events for one run. */
481
1242
  async follow(runId, options) {
482
1243
  const params = { run_id: runId };
@@ -668,7 +1429,23 @@ function toWire(input) {
668
1429
  steps: input.steps,
669
1430
  command_id: input.commandId,
670
1431
  idempotency_key: input.idempotencyKey,
671
- mentions: input.mentions
1432
+ mentions: input.mentions,
1433
+ model: input.model ? {
1434
+ provider_id: input.model.provider_id,
1435
+ model: input.model.model
1436
+ } : void 0,
1437
+ reasoning_effort: input.reasoningEffort || void 0
1438
+ };
1439
+ }
1440
+ function toTeamWire(input) {
1441
+ return {
1442
+ team_id: input.teamId,
1443
+ team_version: input.teamVersion,
1444
+ goal: input.goal,
1445
+ input: input.input,
1446
+ workspace: input.workspaceId ? { id: input.workspaceId } : void 0,
1447
+ command_id: input.commandId,
1448
+ idempotency_key: input.idempotencyKey
672
1449
  };
673
1450
  }
674
1451
  //#endregion
@@ -686,6 +1463,309 @@ var SkillClient = class {
686
1463
  get(skillId) {
687
1464
  return this.transport.request("skill/get", { skill_id: skillId });
688
1465
  }
1466
+ /**
1467
+ * Every readable file inside a Skill's directory, with a tree digest.
1468
+ *
1469
+ * This is the only way to learn what a Skill actually ships: `get()` returns
1470
+ * a bounded summary of `SKILL.md` and says nothing about the `references/`,
1471
+ * `scripts/`, `templates/` and `assets/` beside it. Check
1472
+ * `capabilities.skill_files` first — a host may wire the catalog without
1473
+ * this face, in which case it answers `unsupported_operation`.
1474
+ */
1475
+ files(skillId) {
1476
+ return this.transport.request("skill/files", { skill_id: skillId });
1477
+ }
1478
+ /**
1479
+ * One skill file's bytes, over the WebSocket binding.
1480
+ *
1481
+ * The wire carries base64 because JSON has no byte string, so this decodes
1482
+ * back to `Uint8Array` for you. The HTTP route
1483
+ * (`GET /api/app-server/skills/{id}/files/{path}`) returns the raw body
1484
+ * instead, but that is deliberately not on the typed client's JSON
1485
+ * transport — use this method, or `fetch` the route directly if you are on a
1486
+ * host token.
1487
+ */
1488
+ async readFile(skillId, path) {
1489
+ return decodeBase64((await this.transport.request("skill/file", {
1490
+ skill_id: skillId,
1491
+ path
1492
+ })).content);
1493
+ }
1494
+ /** Same as {@link readFile}, but also returns the server's content type. */
1495
+ readFileWithType(skillId, path) {
1496
+ return this.transport.request("skill/file", {
1497
+ skill_id: skillId,
1498
+ path
1499
+ });
1500
+ }
1501
+ };
1502
+ /**
1503
+ * Decode base64 to bytes.
1504
+ *
1505
+ * `atob` is used when present (browsers, Bun, Node 16+) and a manual decode
1506
+ * otherwise, so the package keeps working on a bare Node without a DOM lib.
1507
+ */
1508
+ function decodeBase64(content) {
1509
+ if (typeof atob === "function") {
1510
+ const binary = atob(content);
1511
+ const bytes = new Uint8Array(binary.length);
1512
+ for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
1513
+ return bytes;
1514
+ }
1515
+ const bufferCtor = globalThis.Buffer;
1516
+ if (bufferCtor) return bufferCtor.from(content, "base64");
1517
+ throw new Error("no base64 decoder available in this environment");
1518
+ }
1519
+ //#endregion
1520
+ //#region src/store.ts
1521
+ var StoreError = class extends Error {
1522
+ code;
1523
+ constructor(code, message) {
1524
+ super(message);
1525
+ this.code = code;
1526
+ this.name = "StoreError";
1527
+ }
1528
+ };
1529
+ const DEFAULT_READY_TIMEOUT_MS = 3e4;
1530
+ const DEFAULT_READY_POLL_MS = 400;
1531
+ const DEFAULT_READY_POLL_MAX_MS = 4e3;
1532
+ /** Every string a `LocalizedText` carries, for haystack building. */
1533
+ function localizedValues(text) {
1534
+ if (typeof text === "string") return [text];
1535
+ if (!text || typeof text !== "object") return [];
1536
+ return Object.values(text).filter((value) => typeof value === "string");
1537
+ }
1538
+ function haystack(item) {
1539
+ return [
1540
+ item.entry_name,
1541
+ item.name,
1542
+ item.id,
1543
+ item.description ?? "",
1544
+ item.version,
1545
+ ...localizedValues(item.display_name),
1546
+ ...localizedValues(item.display_description),
1547
+ ...localizedValues(item.profession),
1548
+ ...(item.tags ?? []).flatMap(localizedValues)
1549
+ ].join("\n").toLowerCase();
1550
+ }
1551
+ function abortableDelay(ms, signal) {
1552
+ return new Promise((resolve, reject) => {
1553
+ if (signal?.aborted) {
1554
+ reject(new StoreError("aborted", "the operation was aborted"));
1555
+ return;
1556
+ }
1557
+ const timer = setTimeout(() => {
1558
+ signal?.removeEventListener("abort", onAbort);
1559
+ resolve();
1560
+ }, ms);
1561
+ const onAbort = () => {
1562
+ clearTimeout(timer);
1563
+ reject(new StoreError("aborted", "the operation was aborted"));
1564
+ };
1565
+ signal?.addEventListener("abort", onAbort, { once: true });
1566
+ });
1567
+ }
1568
+ var StoreClient = class {
1569
+ host;
1570
+ readyTimeoutMs;
1571
+ readyPollMs;
1572
+ readyPollMaxMs;
1573
+ constructor(host, options = {}) {
1574
+ this.host = host;
1575
+ this.readyTimeoutMs = options.readyTimeoutMs ?? DEFAULT_READY_TIMEOUT_MS;
1576
+ this.readyPollMs = options.readyPollMs ?? DEFAULT_READY_POLL_MS;
1577
+ this.readyPollMaxMs = options.readyPollMaxMs ?? DEFAULT_READY_POLL_MAX_MS;
1578
+ }
1579
+ /** Every item across enabled marketplaces, with install state. */
1580
+ async list() {
1581
+ return (await this.host.listStore()).items;
1582
+ }
1583
+ /** Case-insensitive substring search over the fields a user can see. */
1584
+ async search(query, filter = {}) {
1585
+ const needle = query.trim().toLowerCase();
1586
+ return (await this.list()).filter((item) => {
1587
+ if (filter.kind && item.kind !== filter.kind) return false;
1588
+ if (!needle) return true;
1589
+ return haystack(item).includes(needle);
1590
+ });
1591
+ }
1592
+ /** Items currently installed on this host. */
1593
+ async installed() {
1594
+ return (await this.list()).filter((item) => item.installed);
1595
+ }
1596
+ /**
1597
+ * Installed items whose marketplace offers a different version.
1598
+ *
1599
+ * Both conditions matter: `update_available` is computed from the imported
1600
+ * snapshot even when nothing is installed, so filtering on it alone would
1601
+ * report an "update" for an entry the user has never installed.
1602
+ */
1603
+ async checkUpdates() {
1604
+ return (await this.list()).filter((item) => item.installed && item.update_available);
1605
+ }
1606
+ /**
1607
+ * What a caller can offer for an item's pending update.
1608
+ *
1609
+ * `"none"` means there is nothing to do; `"uninstall_reinstall"` means the
1610
+ * entry is installed at a different version and the only path the wire offers
1611
+ * is to release it and install again (the version-aware re-import then picks
1612
+ * up the current version). `"unknown"` is returned rather than guessing when
1613
+ * the item carries no snapshot to release.
1614
+ */
1615
+ updateHint(item) {
1616
+ if (!item.installed || !item.update_available) return "none";
1617
+ return item.snapshot_id ? "uninstall_reinstall" : "unknown";
1618
+ }
1619
+ /**
1620
+ * Import (when needed) and install one store item, then wait until it is
1621
+ * usable.
1622
+ */
1623
+ async install(item, options = {}) {
1624
+ this.assertUsable(item, options.signal);
1625
+ const result = await this.host.installStoreEntry(item.marketplace_id, item.entry_name);
1626
+ const components = result.outcomes ?? [];
1627
+ const outcome = {
1628
+ snapshotId: result.snapshot_id,
1629
+ reused: result.reused,
1630
+ components,
1631
+ ok: result.errors.length === 0 && components.every((component) => component.ok)
1632
+ };
1633
+ if (options.waitForReady === false) return outcome;
1634
+ return this.awaitReady(outcome, options);
1635
+ }
1636
+ /**
1637
+ * Release an item's runtime artifacts and clear its install record.
1638
+ *
1639
+ * The server only clears the record for components it actually released, so a
1640
+ * partial failure leaves `ok: false` with the failing component named — and
1641
+ * the item stays installed, which is what makes a retry meaningful.
1642
+ */
1643
+ async uninstall(item, options = {}) {
1644
+ const snapshotId = this.requireSnapshot(item);
1645
+ const componentIds = options.componentIds ?? await this.installedComponentIds(snapshotId);
1646
+ if (componentIds.length === 0) return {
1647
+ snapshotId,
1648
+ reused: true,
1649
+ components: [],
1650
+ ok: true
1651
+ };
1652
+ const status = await this.host.uninstallInstall(snapshotId, componentIds);
1653
+ return this.outcomeFromStatus(snapshotId, status, false);
1654
+ }
1655
+ /**
1656
+ * Switch an item's components on or off.
1657
+ *
1658
+ * Idempotent: the server sets the state it is asked for instead of toggling.
1659
+ * A skill reports `skill_disable_flag_only` because the skill corpus has no
1660
+ * enable state — that code is passed through, not swallowed, so a caller can
1661
+ * tell "switched off" from "marked in the catalogue".
1662
+ */
1663
+ async setEnabled(item, enabled, options = {}) {
1664
+ const snapshotId = this.requireSnapshot(item);
1665
+ const componentIds = options.componentIds ?? await this.installedComponentIds(snapshotId);
1666
+ if (componentIds.length === 0) return {
1667
+ snapshotId,
1668
+ reused: true,
1669
+ components: [],
1670
+ ok: true
1671
+ };
1672
+ const status = enabled ? await this.host.enableInstall(snapshotId, componentIds) : await this.host.disableInstall(snapshotId, componentIds);
1673
+ return this.outcomeFromStatus(snapshotId, status, false);
1674
+ }
1675
+ assertUsable(item, signal) {
1676
+ if (signal?.aborted) throw new StoreError("aborted", "the operation was aborted before it started");
1677
+ if (item.blocked_reason) throw new StoreError("item_blocked", item.blocked_reason);
1678
+ }
1679
+ requireSnapshot(item) {
1680
+ const snapshotId = item.snapshot_id;
1681
+ if (!snapshotId) throw new StoreError("not_installed", `store item ${item.id} has no imported snapshot to act on`);
1682
+ return snapshotId;
1683
+ }
1684
+ async installedComponentIds(snapshotId) {
1685
+ return (await this.host.getInstallStatus(snapshotId)).components.filter((component) => component.state !== "not-installed").map((component) => component.id);
1686
+ }
1687
+ outcomeFromStatus(snapshotId, status, reused) {
1688
+ const components = status.outcomes ?? [];
1689
+ return {
1690
+ snapshotId,
1691
+ reused,
1692
+ components,
1693
+ ok: (status.errors ?? []).length === 0 && components.every((component) => component.ok)
1694
+ };
1695
+ }
1696
+ /**
1697
+ * Poll until every component of the snapshot is usable.
1698
+ *
1699
+ * A timeout does **not** discard the install: the caller gets the successful
1700
+ * install plus `ready: false` and `readyIssue: "ready_timeout"`, so a slow
1701
+ * connector never turns into "the install failed".
1702
+ */
1703
+ async awaitReady(outcome, options) {
1704
+ const snapshotId = outcome.snapshotId;
1705
+ if (!snapshotId) return outcome;
1706
+ const status = await this.host.getInstallStatus(snapshotId);
1707
+ const connectors = status.components.filter((component) => component.kind === "connector" && component.state !== "not-installed");
1708
+ if (connectors.length > 0) await this.host.enableInstall(snapshotId, connectors.map((component) => component.id));
1709
+ const deadline = Date.now() + (options.timeoutMs ?? this.readyTimeoutMs);
1710
+ let interval = this.readyPollMs;
1711
+ let last = status.components;
1712
+ for (;;) {
1713
+ const blocked = (await Promise.all(last.filter((component) => component.state !== "not-installed").map(async (component) => ({
1714
+ component,
1715
+ verdict: await this.readiness(component)
1716
+ })))).find((entry) => !entry.verdict.ready);
1717
+ if (!blocked) return {
1718
+ ...outcome,
1719
+ ready: true
1720
+ };
1721
+ if (!blocked.verdict.ready && blocked.verdict.terminal) return {
1722
+ ...outcome,
1723
+ ready: false,
1724
+ readyIssue: blocked.verdict.terminal,
1725
+ readyComponentId: blocked.component.id
1726
+ };
1727
+ if (Date.now() >= deadline) return {
1728
+ ...outcome,
1729
+ ready: false,
1730
+ readyIssue: "ready_timeout",
1731
+ readyComponentId: blocked.component.id
1732
+ };
1733
+ await abortableDelay(interval, options.signal);
1734
+ interval = Math.min(interval * 2, this.readyPollMaxMs);
1735
+ last = (await this.host.getInstallStatus(snapshotId)).components;
1736
+ }
1737
+ }
1738
+ /**
1739
+ * Is this one component usable yet?
1740
+ *
1741
+ * Each kind answers with the narrowest read the protocol already offers. An
1742
+ * unknown kind is reported ready rather than blocking: this layer cannot judge
1743
+ * a component it does not understand, and a false timeout on a future kind
1744
+ * would be worse than passing it through.
1745
+ */
1746
+ async readiness(component) {
1747
+ try {
1748
+ switch (component.kind) {
1749
+ case "skill": return (await this.host.skills.list()).some((skill) => skill.name === component.name) ? { ready: true } : { ready: false };
1750
+ case "agent": return (await this.host.agents.get(component.id)).preset_id ? { ready: true } : { ready: false };
1751
+ case "team":
1752
+ await this.host.teams.get(component.id);
1753
+ return { ready: true };
1754
+ case "connector": {
1755
+ if ((await this.host.connectors.test(component.id)).success) return { ready: true };
1756
+ const status = await this.host.connectors.status(component.id);
1757
+ if (status.status === "authorization_required" || status.status === "reauthorization_required") return {
1758
+ ready: false,
1759
+ terminal: "authorization_required"
1760
+ };
1761
+ return { ready: false };
1762
+ }
1763
+ default: return { ready: true };
1764
+ }
1765
+ } catch {
1766
+ return { ready: false };
1767
+ }
1768
+ }
689
1769
  };
690
1770
  //#endregion
691
1771
  //#region src/teams.ts
@@ -775,6 +1855,12 @@ var AppServerClient = class {
775
1855
  teams;
776
1856
  /** Public model directory (`models/list`, REQ-PAR-05b). */
777
1857
  models;
1858
+ /**
1859
+ * Agent Store lifecycle as one state machine: search → install (wait until
1860
+ * usable) → enable/disable → uninstall. Composes the flat methods below; it
1861
+ * adds no wire surface.
1862
+ */
1863
+ store;
778
1864
  clientInfo;
779
1865
  capabilities;
780
1866
  initializeResult = null;
@@ -793,6 +1879,7 @@ var AppServerClient = class {
793
1879
  this.agents = new AgentClient(this.transport);
794
1880
  this.teams = new TeamClient(this.transport);
795
1881
  this.models = new ModelClient(this.transport);
1882
+ this.store = new StoreClient(this, options.store);
796
1883
  this.bridgeNotifications();
797
1884
  }
798
1885
  /**
@@ -862,7 +1949,14 @@ var AppServerClient = class {
862
1949
  async getInstallStatus(snapshotId) {
863
1950
  return this.transport.request("install/status", { snapshot_id: snapshotId });
864
1951
  }
865
- /** Disable installed components (runtime artifacts stay). */
1952
+ /**
1953
+ * Disable installed components.
1954
+ *
1955
+ * The runtime state moves with the flag — a connector's MCP server is switched
1956
+ * off, an expert's Preset is disabled — while the artifacts stay on disk. A
1957
+ * skill is the documented exception: the corpus has no enable state, so it
1958
+ * reports `skill_disable_flag_only` and the flag stays a catalogue marker.
1959
+ */
866
1960
  async disableInstall(snapshotId, componentIds) {
867
1961
  return this.transport.request("install/disable", {
868
1962
  snapshot_id: snapshotId,
@@ -1322,4 +2416,4 @@ async function withRetry(operation, options = {}) {
1322
2416
  throw lastError;
1323
2417
  }
1324
2418
  //#endregion
1325
- export { AgentClient, AgentRunHandle, AppServerClient, ConnectorClient, ConversationClient, ConversationHandle, ConversationSubscription, EventSubscription, ModelClient, RunClient, SkillClient, TeamClient, WebSocketTransport, WorkspaceClient, aggregateTurnResult, conversationMessages, isLoopbackUrl, launchRun, withRetry };
2419
+ export { AgentClient, AgentRunHandle, AppServerClient, ConnectorClient, ConversationClient, ConversationHandle, ConversationSubscription, EventSubscription, HttpTransport, ModelClient, RunClient, SkillClient, StoreClient, StoreError, TeamClient, WebSocketTransport, WorkspaceClient, aggregateTurnResult, appServerErrorFromWire, conversationMessages, httpRouteTable, isLoopbackUrl, launchRun, withRetry };