@copilotkit/runtime 1.62.2 → 1.63.0

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.
Files changed (217) hide show
  1. package/dist/_virtual/_rolldown/runtime.mjs +15 -1
  2. package/dist/agent/converters/aisdk.d.cts +1 -1
  3. package/dist/agent/converters/tanstack.d.cts +1 -1
  4. package/dist/agent/index.d.cts +1 -1
  5. package/dist/channels/dist/index.d.cts +10 -0
  6. package/dist/channels/dist/index.d.cts.map +1 -0
  7. package/dist/channels/dist/index.d.mts +10 -0
  8. package/dist/channels/dist/index.d.mts.map +1 -0
  9. package/dist/graphql/inputs/action.input.d.cts +1 -1
  10. package/dist/graphql/inputs/agent-session.input.d.cts +1 -1
  11. package/dist/graphql/inputs/agent-state.input.d.cts +1 -1
  12. package/dist/graphql/inputs/extensions.input.d.cts +1 -1
  13. package/dist/graphql/inputs/forwarded-parameters.input.d.cts +1 -1
  14. package/dist/graphql/inputs/message.input.d.cts +1 -1
  15. package/dist/graphql/types/base/index.d.cts +1 -1
  16. package/dist/graphql/types/converted/index.d.cts +1 -1
  17. package/dist/graphql/types/copilot-response.type.d.cts +1 -1
  18. package/dist/graphql/types/enums.d.cts +1 -1
  19. package/dist/graphql/types/extensions-response.type.d.cts +1 -1
  20. package/dist/graphql/types/message-status.type.d.cts +1 -1
  21. package/dist/graphql/types/response-status.type.d.cts +1 -1
  22. package/dist/index.d.cts +1 -1
  23. package/dist/langgraph.d.cts +1 -1
  24. package/dist/lib/cloud/index.d.cts +1 -1
  25. package/dist/lib/index.d.cts +1 -1
  26. package/dist/lib/integrations/index.d.cts +1 -1
  27. package/dist/lib/integrations/nest/index.d.cts +1 -1
  28. package/dist/lib/integrations/nextjs/app-router.d.cts +1 -1
  29. package/dist/lib/integrations/nextjs/pages-router.d.cts +1 -1
  30. package/dist/lib/integrations/node-express/index.d.cts +1 -1
  31. package/dist/lib/integrations/node-http/index.d.cts +1 -1
  32. package/dist/lib/integrations/shared.d.cts +1 -1
  33. package/dist/lib/logger.d.cts +1 -1
  34. package/dist/lib/observability.d.cts +1 -1
  35. package/dist/lib/runtime/agent-integrations/langgraph/agent.d.cts +1 -1
  36. package/dist/lib/runtime/agent-integrations/langgraph/consts.d.cts +1 -1
  37. package/dist/lib/runtime/agent-integrations/langgraph/index.d.cts +1 -1
  38. package/dist/lib/runtime/copilot-runtime.d.cts +1 -1
  39. package/dist/lib/runtime/mcp-tools-utils.d.cts +1 -1
  40. package/dist/lib/runtime/telemetry-agent-runner.d.cts +1 -1
  41. package/dist/lib/runtime/types.d.cts +1 -1
  42. package/dist/lib/telemetry-client.mjs +1 -1
  43. package/dist/package.cjs +5 -1
  44. package/dist/{package.mjs → runtime/package.mjs} +6 -2
  45. package/dist/runtime/package.mjs.map +1 -0
  46. package/dist/service-adapters/anthropic/anthropic-adapter.d.cts +1 -1
  47. package/dist/service-adapters/bedrock/bedrock-adapter.d.cts +1 -1
  48. package/dist/service-adapters/empty/empty-adapter.d.cts +1 -1
  49. package/dist/service-adapters/events.d.cts +1 -1
  50. package/dist/service-adapters/experimental/ollama/ollama-adapter.d.cts +1 -1
  51. package/dist/service-adapters/google/google-genai-adapter.d.cts +1 -1
  52. package/dist/service-adapters/groq/groq-adapter.d.cts +1 -1
  53. package/dist/service-adapters/index.d.cts +1 -1
  54. package/dist/service-adapters/langchain/langchain-adapter.d.cts +1 -1
  55. package/dist/service-adapters/langchain/langserve.d.cts +1 -1
  56. package/dist/service-adapters/langchain/types.d.cts +1 -1
  57. package/dist/service-adapters/openai/openai-adapter.d.cts +1 -1
  58. package/dist/service-adapters/openai/openai-assistant-adapter.d.cts +1 -1
  59. package/dist/service-adapters/service-adapter.d.cts +1 -1
  60. package/dist/service-adapters/shared/error-utils.d.cts +1 -1
  61. package/dist/service-adapters/shared/sdk-client-utils.d.cts +1 -1
  62. package/dist/service-adapters/unify/unify-adapter.d.cts +1 -1
  63. package/dist/utils/failed-response-status-reasons.d.cts +1 -1
  64. package/dist/v2/express.d.cts +3 -3
  65. package/dist/v2/express.d.mts +2 -2
  66. package/dist/v2/hono.d.cts +3 -3
  67. package/dist/v2/hono.d.mts +2 -2
  68. package/dist/v2/index.d.cts +7 -6
  69. package/dist/v2/index.d.mts +6 -5
  70. package/dist/v2/node.d.cts +1 -1
  71. package/dist/v2/runtime/core/channel-activation-config.cjs +93 -0
  72. package/dist/v2/runtime/core/channel-activation-config.cjs.map +1 -0
  73. package/dist/v2/runtime/core/channel-activation-config.d.cts +34 -0
  74. package/dist/v2/runtime/core/channel-activation-config.d.cts.map +1 -0
  75. package/dist/v2/runtime/core/channel-activation-config.d.mts +34 -0
  76. package/dist/v2/runtime/core/channel-activation-config.d.mts.map +1 -0
  77. package/dist/v2/runtime/core/channel-activation-config.mjs +91 -0
  78. package/dist/v2/runtime/core/channel-activation-config.mjs.map +1 -0
  79. package/dist/v2/runtime/core/channel-manager.cjs +444 -0
  80. package/dist/v2/runtime/core/channel-manager.cjs.map +1 -0
  81. package/dist/v2/runtime/core/channel-manager.d.cts +90 -0
  82. package/dist/v2/runtime/core/channel-manager.d.cts.map +1 -0
  83. package/dist/v2/runtime/core/channel-manager.d.mts +90 -0
  84. package/dist/v2/runtime/core/channel-manager.d.mts.map +1 -0
  85. package/dist/v2/runtime/core/channel-manager.mjs +443 -0
  86. package/dist/v2/runtime/core/channel-manager.mjs.map +1 -0
  87. package/dist/v2/runtime/core/debug-event-bus.d.cts +1 -1
  88. package/dist/v2/runtime/core/fetch-cors.d.cts +1 -1
  89. package/dist/v2/runtime/core/fetch-handler.cjs +98 -2
  90. package/dist/v2/runtime/core/fetch-handler.cjs.map +1 -1
  91. package/dist/v2/runtime/core/fetch-handler.d.cts +60 -4
  92. package/dist/v2/runtime/core/fetch-handler.d.cts.map +1 -1
  93. package/dist/v2/runtime/core/fetch-handler.d.mts +59 -3
  94. package/dist/v2/runtime/core/fetch-handler.d.mts.map +1 -1
  95. package/dist/v2/runtime/core/fetch-handler.mjs +98 -2
  96. package/dist/v2/runtime/core/fetch-handler.mjs.map +1 -1
  97. package/dist/v2/runtime/core/fetch-router.cjs +18 -0
  98. package/dist/v2/runtime/core/fetch-router.cjs.map +1 -1
  99. package/dist/v2/runtime/core/fetch-router.mjs +18 -0
  100. package/dist/v2/runtime/core/fetch-router.mjs.map +1 -1
  101. package/dist/v2/runtime/core/hooks.cjs.map +1 -1
  102. package/dist/v2/runtime/core/hooks.d.cts +11 -1
  103. package/dist/v2/runtime/core/hooks.d.cts.map +1 -1
  104. package/dist/v2/runtime/core/hooks.d.mts +10 -0
  105. package/dist/v2/runtime/core/hooks.d.mts.map +1 -1
  106. package/dist/v2/runtime/core/hooks.mjs.map +1 -1
  107. package/dist/v2/runtime/core/middleware-sse-parser.d.cts +1 -1
  108. package/dist/v2/runtime/core/middleware.d.cts +1 -1
  109. package/dist/v2/runtime/core/runtime.cjs +29 -1
  110. package/dist/v2/runtime/core/runtime.cjs.map +1 -1
  111. package/dist/v2/runtime/core/runtime.d.cts +95 -26
  112. package/dist/v2/runtime/core/runtime.d.cts.map +1 -1
  113. package/dist/v2/runtime/core/runtime.d.mts +94 -25
  114. package/dist/v2/runtime/core/runtime.d.mts.map +1 -1
  115. package/dist/v2/runtime/core/runtime.mjs +30 -2
  116. package/dist/v2/runtime/core/runtime.mjs.map +1 -1
  117. package/dist/v2/runtime/endpoints/express-single.d.cts +1 -1
  118. package/dist/v2/runtime/endpoints/express.cjs +10 -5
  119. package/dist/v2/runtime/endpoints/express.cjs.map +1 -1
  120. package/dist/v2/runtime/endpoints/express.d.cts +30 -4
  121. package/dist/v2/runtime/endpoints/express.d.cts.map +1 -1
  122. package/dist/v2/runtime/endpoints/express.d.mts +29 -3
  123. package/dist/v2/runtime/endpoints/express.d.mts.map +1 -1
  124. package/dist/v2/runtime/endpoints/express.mjs +10 -5
  125. package/dist/v2/runtime/endpoints/express.mjs.map +1 -1
  126. package/dist/v2/runtime/endpoints/hono-single.d.cts +1 -1
  127. package/dist/v2/runtime/endpoints/hono.cjs +7 -3
  128. package/dist/v2/runtime/endpoints/hono.cjs.map +1 -1
  129. package/dist/v2/runtime/endpoints/hono.d.cts +30 -16
  130. package/dist/v2/runtime/endpoints/hono.d.cts.map +1 -1
  131. package/dist/v2/runtime/endpoints/hono.d.mts +29 -15
  132. package/dist/v2/runtime/endpoints/hono.d.mts.map +1 -1
  133. package/dist/v2/runtime/endpoints/hono.mjs +7 -3
  134. package/dist/v2/runtime/endpoints/hono.mjs.map +1 -1
  135. package/dist/v2/runtime/endpoints/index.d.cts +3 -3
  136. package/dist/v2/runtime/endpoints/index.d.mts +2 -2
  137. package/dist/v2/runtime/endpoints/node-fetch-handler.d.cts +1 -1
  138. package/dist/v2/runtime/endpoints/node.cjs +12 -1
  139. package/dist/v2/runtime/endpoints/node.cjs.map +1 -1
  140. package/dist/v2/runtime/endpoints/node.d.cts +22 -2
  141. package/dist/v2/runtime/endpoints/node.d.cts.map +1 -1
  142. package/dist/v2/runtime/endpoints/node.d.mts +21 -1
  143. package/dist/v2/runtime/endpoints/node.d.mts.map +1 -1
  144. package/dist/v2/runtime/endpoints/node.mjs +12 -1
  145. package/dist/v2/runtime/endpoints/node.mjs.map +1 -1
  146. package/dist/v2/runtime/endpoints/single-route-helpers.cjs +1 -0
  147. package/dist/v2/runtime/endpoints/single-route-helpers.cjs.map +1 -1
  148. package/dist/v2/runtime/endpoints/single-route-helpers.mjs +1 -0
  149. package/dist/v2/runtime/endpoints/single-route-helpers.mjs.map +1 -1
  150. package/dist/v2/runtime/express.d.cts +2 -2
  151. package/dist/v2/runtime/express.d.mts +1 -1
  152. package/dist/v2/runtime/handlers/get-runtime-info.cjs +1 -0
  153. package/dist/v2/runtime/handlers/get-runtime-info.cjs.map +1 -1
  154. package/dist/v2/runtime/handlers/get-runtime-info.mjs +1 -0
  155. package/dist/v2/runtime/handlers/get-runtime-info.mjs.map +1 -1
  156. package/dist/v2/runtime/handlers/handle-connect.cjs +2 -1
  157. package/dist/v2/runtime/handlers/handle-connect.cjs.map +1 -1
  158. package/dist/v2/runtime/handlers/handle-connect.mjs +2 -1
  159. package/dist/v2/runtime/handlers/handle-connect.mjs.map +1 -1
  160. package/dist/v2/runtime/handlers/handle-suggest.cjs +83 -0
  161. package/dist/v2/runtime/handlers/handle-suggest.cjs.map +1 -0
  162. package/dist/v2/runtime/handlers/handle-suggest.mjs +82 -0
  163. package/dist/v2/runtime/handlers/handle-suggest.mjs.map +1 -0
  164. package/dist/v2/runtime/handlers/header-utils.cjs +169 -9
  165. package/dist/v2/runtime/handlers/header-utils.cjs.map +1 -1
  166. package/dist/v2/runtime/handlers/header-utils.d.cts +54 -0
  167. package/dist/v2/runtime/handlers/header-utils.d.cts.map +1 -0
  168. package/dist/v2/runtime/handlers/header-utils.d.mts +54 -0
  169. package/dist/v2/runtime/handlers/header-utils.d.mts.map +1 -0
  170. package/dist/v2/runtime/handlers/header-utils.mjs +168 -9
  171. package/dist/v2/runtime/handlers/header-utils.mjs.map +1 -1
  172. package/dist/v2/runtime/handlers/intelligence/memories.cjs +209 -0
  173. package/dist/v2/runtime/handlers/intelligence/memories.cjs.map +1 -0
  174. package/dist/v2/runtime/handlers/intelligence/memories.mjs +204 -0
  175. package/dist/v2/runtime/handlers/intelligence/memories.mjs.map +1 -0
  176. package/dist/v2/runtime/handlers/shared/agent-utils.cjs +18 -5
  177. package/dist/v2/runtime/handlers/shared/agent-utils.cjs.map +1 -1
  178. package/dist/v2/runtime/handlers/shared/agent-utils.mjs +18 -5
  179. package/dist/v2/runtime/handlers/shared/agent-utils.mjs.map +1 -1
  180. package/dist/v2/runtime/handlers/shared/sse-response.cjs +4 -4
  181. package/dist/v2/runtime/handlers/shared/sse-response.cjs.map +1 -1
  182. package/dist/v2/runtime/handlers/shared/sse-response.mjs +4 -4
  183. package/dist/v2/runtime/handlers/shared/sse-response.mjs.map +1 -1
  184. package/dist/v2/runtime/handlers/sse/connect.cjs +2 -2
  185. package/dist/v2/runtime/handlers/sse/connect.cjs.map +1 -1
  186. package/dist/v2/runtime/handlers/sse/connect.mjs +3 -3
  187. package/dist/v2/runtime/handlers/sse/connect.mjs.map +1 -1
  188. package/dist/v2/runtime/hono.d.cts +2 -2
  189. package/dist/v2/runtime/hono.d.mts +1 -1
  190. package/dist/v2/runtime/index.d.cts +6 -5
  191. package/dist/v2/runtime/index.d.cts.map +1 -1
  192. package/dist/v2/runtime/index.d.mts +5 -4
  193. package/dist/v2/runtime/index.d.mts.map +1 -1
  194. package/dist/v2/runtime/intelligence-platform/client.cjs +71 -2
  195. package/dist/v2/runtime/intelligence-platform/client.cjs.map +1 -1
  196. package/dist/v2/runtime/intelligence-platform/client.d.cts +113 -1
  197. package/dist/v2/runtime/intelligence-platform/client.d.cts.map +1 -1
  198. package/dist/v2/runtime/intelligence-platform/client.d.mts +112 -0
  199. package/dist/v2/runtime/intelligence-platform/client.d.mts.map +1 -1
  200. package/dist/v2/runtime/intelligence-platform/client.mjs +71 -2
  201. package/dist/v2/runtime/intelligence-platform/client.mjs.map +1 -1
  202. package/dist/v2/runtime/node.d.cts +1 -1
  203. package/dist/v2/runtime/runner/agent-runner.d.cts +1 -1
  204. package/dist/v2/runtime/runner/in-memory.cjs +35 -19
  205. package/dist/v2/runtime/runner/in-memory.cjs.map +1 -1
  206. package/dist/v2/runtime/runner/in-memory.d.cts +13 -1
  207. package/dist/v2/runtime/runner/in-memory.d.cts.map +1 -1
  208. package/dist/v2/runtime/runner/in-memory.d.mts +12 -0
  209. package/dist/v2/runtime/runner/in-memory.d.mts.map +1 -1
  210. package/dist/v2/runtime/runner/in-memory.mjs +35 -19
  211. package/dist/v2/runtime/runner/in-memory.mjs.map +1 -1
  212. package/dist/v2/runtime/runner/index.d.cts +1 -1
  213. package/dist/v2/runtime/runner/intelligence.d.cts +1 -1
  214. package/dist/v2/runtime/telemetry/telemetry-client.mjs +1 -1
  215. package/dist/v2/runtime/transcription-service/transcription-service.d.cts +1 -1
  216. package/package.json +9 -3
  217. package/dist/package.mjs.map +0 -1
@@ -0,0 +1,444 @@
1
+ require("reflect-metadata");
2
+ const require_runtime = require('../../../_virtual/_rolldown/runtime.cjs');
3
+ const require_channel_activation_config = require('./channel-activation-config.cjs');
4
+ let node_crypto = require("node:crypto");
5
+
6
+ //#region src/v2/runtime/core/channel-manager.ts
7
+ /**
8
+ * Signals that a declared Channel cannot be activated because no managed
9
+ * provider exists for it yet. The engine throws this (or any error whose
10
+ * `code === "SETUP_REQUIRED"`) to move a Channel to `setup_required` rather
11
+ * than `error` — a declared-but-unprovisioned Channel is a valid degraded
12
+ * state, not a failure.
13
+ */
14
+ var ChannelSetupRequiredError = class extends Error {
15
+ constructor(message) {
16
+ super(message);
17
+ this.name = "ChannelSetupRequiredError";
18
+ }
19
+ };
20
+ /** Non-literal specifier so the pure-ESM channels-intelligence package never
21
+ * becomes a static dependency of this CJS package (mirrors the runtime's other
22
+ * channels seams). */
23
+ const CHANNELS_INTELLIGENCE_SPECIFIER = "@copilotkit/channels-intelligence";
24
+ /**
25
+ * Default engine: wrap the channels-intelligence Realtime Gateway launcher.
26
+ *
27
+ * The module is reached through an injectable importer that defaults to a
28
+ * dynamic `import()` of a non-literal specifier, so the pure-ESM
29
+ * `@copilotkit/channels-intelligence` never becomes a static dependency of this
30
+ * CJS package (mirrors the runtime's other channels seams). The `import`
31
+ * seam is a parameter purely so this function's config→opts mapping and its
32
+ * module-not-found / generic-error branches are unit-testable WITHOUT the real
33
+ * package installed; production always uses the default importer.
34
+ *
35
+ * Passes NO `org`/`channelId` — the launcher's realtime scope treats them as
36
+ * optional.
37
+ *
38
+ * @param config - Resolved activation config for the Channel.
39
+ * @param channel - The Channel to activate.
40
+ * @param importChannelsIntelligence - Test seam; loads the channels-intelligence
41
+ * module. Defaults to a dynamic import of the real package.
42
+ * @param log - Optional diagnostic sink forwarded to the launcher/transport so
43
+ * transport-level drop diagnostics are not silent in the managed path.
44
+ * @returns The launcher's {@link ChannelsHandle}.
45
+ */
46
+ async function defaultActivateChannel(config, channel, importChannelsIntelligence = () => import(CHANNELS_INTELLIGENCE_SPECIFIER), log) {
47
+ let mod;
48
+ try {
49
+ mod = await importChannelsIntelligence();
50
+ } catch (err) {
51
+ if (isModuleNotFound(err)) throw new Error("Managed Channels require '@copilotkit/channels-intelligence' to be installed. Add it to your app's dependencies.", { cause: err });
52
+ throw err;
53
+ }
54
+ return mod.startChannelsOverRealtimeGateway([channel], {
55
+ wsUrl: config.wsUrl,
56
+ apiKey: config.apiKey,
57
+ scope: {
58
+ projectId: config.projectId,
59
+ channelName: config.channelName
60
+ },
61
+ runtimeInstanceId: config.runtimeInstanceId,
62
+ adapter: config.adapter,
63
+ appApiBaseUrl: config.apiUrl,
64
+ ...log ? { log } : {}
65
+ });
66
+ }
67
+ /** Whether `err` signals a missing managed provider rather than a hard failure. */
68
+ function isSetupRequired(err) {
69
+ return err instanceof ChannelSetupRequiredError || typeof err === "object" && err !== null && err.code === "SETUP_REQUIRED";
70
+ }
71
+ /**
72
+ * Whether `err` is a Node/runtime module-resolution failure — i.e. the error
73
+ * a dynamic `import()` throws when the target package is not installed.
74
+ * Exported so the friendly-error path in {@link defaultActivateChannel} can be
75
+ * unit-tested without forcing a real failing import.
76
+ */
77
+ function isModuleNotFound(err) {
78
+ if (typeof err !== "object" || err === null) return false;
79
+ const code = err.code;
80
+ return code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND";
81
+ }
82
+ /** Default deadline (ms) for a single `handle.stop()` during teardown. */
83
+ const DEFAULT_STOP_HANDLE_TIMEOUT_MS = 5e3;
84
+ /**
85
+ * Reject with `timeoutMessage` after `timeoutMs` if `inner` has not settled,
86
+ * otherwise pass `inner` through. When `timeoutMs` is undefined, `inner` is
87
+ * returned unchanged. The timer is `unref`'d so a pending deadline never keeps
88
+ * the process alive, and `inner` always has a settle handler attached, so a
89
+ * timed-out promise that later settles never surfaces as unhandled.
90
+ */
91
+ function withTimeout(inner, timeoutMs, timeoutMessage) {
92
+ if (timeoutMs === void 0) return inner;
93
+ return new Promise((resolve, reject) => {
94
+ const timer = setTimeout(() => reject(new Error(timeoutMessage)), timeoutMs);
95
+ timer.unref?.();
96
+ inner.then((value) => {
97
+ clearTimeout(timer);
98
+ resolve(value);
99
+ }, (err) => {
100
+ clearTimeout(timer);
101
+ reject(err);
102
+ });
103
+ });
104
+ }
105
+ /**
106
+ * Drives managed Channel activation for an Intelligence runtime: lazily
107
+ * activates each declared Channel through an engine, tracks per-Channel
108
+ * lifecycle status, exposes readiness, and tears everything down.
109
+ *
110
+ * Activation is lazy and idempotent — constructing the manager does nothing;
111
+ * {@link activate} starts it and a second call is a no-op. Activation throws
112
+ * SYNCHRONOUSLY (a {@link ChannelConfigError}) only for a misconfiguration it
113
+ * can detect up front — a duplicate or missing Channel name. Every OTHER
114
+ * activation failure is recorded as the Channel's status (`error`, or
115
+ * `setup_required` for a missing provider) and surfaced through {@link status}
116
+ * and {@link ready} rather than thrown.
117
+ *
118
+ * Reconnection is NOT handled here — it is delegated to the Phoenix connection
119
+ * layer that backs the launcher. When a managed socket drops, Phoenix's `Socket`
120
+ * auto-reconnects and auto-rejoins, re-sending the channel's join declaration;
121
+ * the Intelligence gateway's `join/3` re-runs `record_heartbeat` (re-registering
122
+ * the runtime's listener) and its `terminate/2` releases the dead socket's
123
+ * leases (verified against Intelligence #511 `sdk_channel.ex`). So the transport
124
+ * self-heals under the persistent adapter and a re-activation here would be both
125
+ * redundant AND broken: re-invoking the engine on an already-started `Channel`
126
+ * throws in `channel.addAdapter` (started=true). The manager therefore never
127
+ * re-activates on a drop.
128
+ *
129
+ * It DOES, however, reflect real connection health through the session's
130
+ * `onStateChange` observer so {@link ChannelManager.status} stays honest rather
131
+ * than reporting `online` forever after a drop: a drop moves the Channel to
132
+ * `reconnecting`, a successful rejoin restores `online`, and a bounded give-up
133
+ * (Phoenix would otherwise retry forever) moves it to `error`.
134
+ */
135
+ var ChannelManager = class {
136
+ /** @param args - See {@link ChannelManagerArgs}. */
137
+ constructor(args) {
138
+ this.entries = /* @__PURE__ */ new Map();
139
+ this.activated = false;
140
+ this.stopped = false;
141
+ this.intelligence = args.intelligence;
142
+ this.channels = args.channels;
143
+ this.log = args.log;
144
+ this.activateChannel = args.activateChannel ?? ((config, channel) => defaultActivateChannel(config, channel, void 0, this.log));
145
+ this.mintRuntimeInstanceId = args.mintRuntimeInstanceId ?? (() => `rti_${(0, node_crypto.randomUUID)().replace(/-/g, "")}`);
146
+ this.stopHandleTimeoutMs = args.stopHandleTimeoutMs ?? DEFAULT_STOP_HANDLE_TIMEOUT_MS;
147
+ }
148
+ /**
149
+ * Start activation of every declared Channel (lazy + idempotent). Mints a
150
+ * distinct runtime instance id per Channel, derives its activation config,
151
+ * and calls the engine. Records each Channel as `connecting`, transitioning
152
+ * to `online`/`setup_required`/`error` as its activation settles.
153
+ */
154
+ activate() {
155
+ if (this.activated || this.stopped) return;
156
+ this.assertUniqueChannelNames();
157
+ this.activated = true;
158
+ for (const channel of this.channels) {
159
+ if (channel.adapters.some((a) => !a.__intelligenceChannel)) {
160
+ this.log?.(`channel "${channel.name}" carries a direct adapter — recording status "unmanaged" and skipping managed activation (this handler does not own its lifecycle; start it via channel.start(); exclusive per Channel: a Channel served by a direct adapter is not also managed, regardless of platform — managed+direct coexistence deferred (OSS-484); routing of direct channels deferred (OSS-486))`);
161
+ this.entries.set(channel.name, {
162
+ status: "unmanaged",
163
+ handle: void 0,
164
+ handleStopped: false,
165
+ settled: Promise.resolve()
166
+ });
167
+ continue;
168
+ }
169
+ const name = channel.name;
170
+ const runtimeInstanceId = this.mintRuntimeInstanceId();
171
+ let resolveSettled;
172
+ let rejectSettled;
173
+ const settled = new Promise((resolve, reject) => {
174
+ resolveSettled = resolve;
175
+ rejectSettled = reject;
176
+ });
177
+ settled.catch(() => {});
178
+ let activation;
179
+ let config;
180
+ try {
181
+ config = require_channel_activation_config.deriveChannelActivationConfig({
182
+ intelligence: this.intelligence,
183
+ channel,
184
+ runtimeInstanceId
185
+ });
186
+ activation = this.activateChannel(config, channel);
187
+ } catch (err) {
188
+ activation = Promise.reject(err);
189
+ }
190
+ const entry = {
191
+ status: "connecting",
192
+ handle: void 0,
193
+ handleStopped: false,
194
+ settled
195
+ };
196
+ activation.then(async (handle) => {
197
+ entry.handle = handle;
198
+ if (this.stopped) {
199
+ await this.stopEntry(entry);
200
+ resolveSettled();
201
+ return;
202
+ }
203
+ entry.status = "online";
204
+ this.registerConnectionObserver(name, entry);
205
+ resolveSettled();
206
+ }, async (err) => {
207
+ if (this.stopped) {
208
+ await this.stopEntry(entry);
209
+ resolveSettled();
210
+ return;
211
+ }
212
+ if (isSetupRequired(err)) {
213
+ entry.status = "setup_required";
214
+ this.log?.(`channel "${name}" requires setup`, err);
215
+ resolveSettled();
216
+ } else {
217
+ entry.status = "error";
218
+ this.log?.(`channel "${name}" failed to activate`, err);
219
+ rejectSettled(err);
220
+ }
221
+ }).catch(() => {});
222
+ this.entries.set(name, entry);
223
+ }
224
+ }
225
+ /**
226
+ * Throw if two declared Channels share a `name`. `entries` is keyed by name,
227
+ * so a duplicate would overwrite the first Channel's entry and leak its live
228
+ * session. Called at the very start of {@link activate}, before any engine
229
+ * call, so a misconfiguration fails loud instead of silently.
230
+ *
231
+ * @throws {ChannelConfigError} If any Channel is missing a name, or if any
232
+ * name appears more than once.
233
+ */
234
+ assertUniqueChannelNames() {
235
+ const seen = /* @__PURE__ */ new Set();
236
+ for (const channel of this.channels) {
237
+ const name = channel.name;
238
+ if (!name) throw new require_channel_activation_config.ChannelConfigError("A managed Channel is missing a `name` — every declared Channel must have a unique, non-empty name (pass createChannel({ name })).");
239
+ if (seen.has(name)) throw new require_channel_activation_config.ChannelConfigError(`Duplicate managed Channel name "${name}" — every declared Channel must have a unique name.`);
240
+ seen.add(name);
241
+ }
242
+ }
243
+ /**
244
+ * Resolve when every managed Channel has settled to `online`/`setup_required`.
245
+ *
246
+ * A direct-adapter (`unmanaged`) Channel has an already-resolved `settled` and
247
+ * so never blocks — but its resolution implies NO health: this handler does not
248
+ * own it. Truthfulness about direct Channels lives in {@link status} (they read
249
+ * `unmanaged`, never `online`), not in `ready()` resolving.
250
+ *
251
+ * Activates lazily if not already started — so a first call rejects with the
252
+ * same {@link ChannelConfigError} as the synchronous throw from
253
+ * {@link activate} for an up-front misconfiguration (duplicate/missing Channel
254
+ * names). Once activation has been kicked off, all OTHER failures are surfaced
255
+ * here instead: this rejects with an `AggregateError` if any Channel settled
256
+ * to `error` OR — when `timeoutMs` is given — did not settle in time. The
257
+ * `timeoutMs` deadline is applied PER CHANNEL, so the aggregate carries each
258
+ * failed Channel's real reason AND a named timeout for each Channel still
259
+ * hanging: a genuine activation error is never masked by a sibling that hangs
260
+ * (a pre-fix set-wide timeout discarded the real reason in that case).
261
+ *
262
+ * A STOPPED manager short-circuits and resolves: a Channel that settled to
263
+ * `error` BEFORE {@link stop} already rejected its `settled` promise, so
264
+ * awaiting it here would throw an `AggregateError` even though
265
+ * {@link status}.overall is `"stopped"` — inconsistent with the case where the
266
+ * Channel was still online at stop() (which resolves). A stopped manager has
267
+ * nothing left to be ready for, so resolve uniformly.
268
+ *
269
+ * `ready()` is ONE-SHOT: it settles on the INITIAL activation outcome. Later
270
+ * connection-health transitions (a live Channel dropping to `reconnecting`, or
271
+ * giving up to `error`) are reported through {@link status} — where `online`
272
+ * means currently-sendable — but do NOT re-arm or re-reject an already-settled
273
+ * `ready()`.
274
+ */
275
+ async ready(opts) {
276
+ if (this.stopped) return;
277
+ this.activate();
278
+ const entries = [...this.entries.entries()];
279
+ const errors = (await Promise.allSettled(entries.map(([name, e]) => withTimeout(e.settled, opts?.timeoutMs, `channel "${name}" did not settle within ${opts?.timeoutMs}ms`)))).filter((r) => r.status === "rejected").map((r) => r.reason);
280
+ if (errors.length > 0) throw new AggregateError(errors, `ChannelManager.ready: ${errors.length} channel(s) failed to activate or settle in time`);
281
+ }
282
+ /**
283
+ * Snapshot status. Every declared Channel — managed OR direct/`unmanaged` —
284
+ * appears keyed by name in `channels`; a direct-adapter Channel this handler
285
+ * does not own is always surfaced as `unmanaged`, never `online`.
286
+ *
287
+ * `overall` is folded over the MANAGED Channels only (see {@link computeOverall}),
288
+ * by precedence `error` > `reconnecting` > `setup_required` > `connecting` >
289
+ * `online`. `online` means every managed Channel can currently send.
290
+ * `reconnecting` outranks `setup_required` because a dropped-but-retrying
291
+ * Channel is an active outage, louder than a steadily-degraded unprovisioned
292
+ * one. `unmanaged` Channels are EXCLUDED from that fold — they carry no health
293
+ * this handler established — so a healthy managed Channel alongside an
294
+ * `unmanaged` one still reports `overall: "online"` while the `unmanaged` one
295
+ * stays visible per-Channel. When every declared Channel is `unmanaged`,
296
+ * `overall` is `unmanaged` (NOT `online`). With no declared Channels at all,
297
+ * `overall` is `online` (nothing is degraded); once every managed Channel has
298
+ * been stopped, `overall` is `stopped`.
299
+ */
300
+ status() {
301
+ const channels = {};
302
+ for (const [name, entry] of this.entries) channels[name] = entry.status;
303
+ if (this.stopped) return {
304
+ overall: "stopped",
305
+ channels
306
+ };
307
+ if (!this.activated && this.channels.length > 0) return {
308
+ overall: "connecting",
309
+ channels
310
+ };
311
+ return {
312
+ overall: this.computeOverall(Object.values(channels)),
313
+ channels
314
+ };
315
+ }
316
+ /**
317
+ * Fold per-Channel statuses into a single overall status (see {@link status}).
318
+ *
319
+ * `unmanaged` Channels are folded out FIRST: they carry no lifecycle this
320
+ * handler owns, so they must neither count as `online` nor mask a real managed
321
+ * outage. The remaining MANAGED statuses are ranked
322
+ * `error` > `reconnecting` > `setup_required` > `connecting` > `online`, so a
323
+ * genuine managed failure still dominates while a healthy managed Channel
324
+ * beside an `unmanaged` one reads `online`. If NO managed Channels remain (every
325
+ * declared Channel is direct/`unmanaged`) the result is `unmanaged` — never the
326
+ * false-healthy `online`. The empty-input case (no declared Channels at all)
327
+ * stays `online` (nothing is degraded).
328
+ */
329
+ computeOverall(values) {
330
+ if (values.length === 0) return "online";
331
+ const managed = values.filter((v) => v !== "unmanaged");
332
+ if (managed.length === 0) return "unmanaged";
333
+ if (managed.every((v) => v === "stopped")) return "stopped";
334
+ if (managed.includes("error")) return "error";
335
+ if (managed.includes("reconnecting")) return "reconnecting";
336
+ if (managed.includes("setup_required")) return "setup_required";
337
+ if (managed.includes("connecting")) return "connecting";
338
+ return "online";
339
+ }
340
+ /**
341
+ * Wire the Channel's connection-health observer (if the handle exposes the
342
+ * optional `onStateChange` seam) so {@link ChannelManager.status} reflects real
343
+ * health instead of reporting `online` forever after a drop:
344
+ *
345
+ * - `reconnecting` → status `reconnecting` (dropped, Phoenix retrying);
346
+ * - `online` → status `online` (rejoined, sendable again);
347
+ * - `gave_up` → status `error` (dead after the bounded reconnect window).
348
+ *
349
+ * Makes NO re-activation — reconnection is delegated to the Phoenix connection
350
+ * layer (see {@link ChannelManager}), which auto-rejoins under the persistent
351
+ * adapter. A STOPPED manager (or an already-stopped entry) ignores late
352
+ * connection events, so a drop that fires after {@link ChannelManager.stop}
353
+ * never resurrects the Channel out of `stopped`.
354
+ *
355
+ * @param name - The Channel name (map key).
356
+ * @param entry - The Channel's activation entry.
357
+ */
358
+ registerConnectionObserver(name, entry) {
359
+ entry.handle?.onStateChange?.((state) => {
360
+ if (this.stopped || entry.status === "stopped") return;
361
+ if (state === "reconnecting") {
362
+ entry.status = "reconnecting";
363
+ this.log?.(`channel "${name}" managed session dropped; reconnecting (Phoenix auto-rejoin)`);
364
+ } else if (state === "online") {
365
+ entry.status = "online";
366
+ this.log?.(`channel "${name}" managed session back online`);
367
+ } else {
368
+ entry.status = "error";
369
+ this.log?.(`channel "${name}" managed session gave up reconnecting; marking error`);
370
+ }
371
+ });
372
+ }
373
+ /**
374
+ * Drive a single entry to its terminal `stopped` state, tearing down its
375
+ * handle AT MOST ONCE. Idempotent: it always sets `status = "stopped"`, and
376
+ * only calls `handle.stop()` on the first invocation that sees a live,
377
+ * not-yet-stopped handle (gated by {@link ChannelEntry.handleStopped}).
378
+ *
379
+ * This is the ONE guarded teardown path shared by both `stop()` and the
380
+ * post-settle guard in {@link activate}. Because the guard is per-entry and
381
+ * idempotent, a handle assigned in the same tick as `stop()` is stopped
382
+ * exactly once even when both callers reach the entry, and a late settle can
383
+ * never resurrect a `stopped` entry.
384
+ *
385
+ * `handle.stop()` failures are logged (via {@link ChannelManager.log}) but NOT
386
+ * rethrown: the real launcher's `stop()` rethrows after `session.disconnect()`,
387
+ * and teardown must still complete for every other entry. The call is wrapped
388
+ * in `Promise.resolve().then(...)` so a foreign/injected handle whose `stop()`
389
+ * throws SYNCHRONOUSLY (before any promise is created) is caught by the same
390
+ * `.catch` — otherwise the sync throw would escape, skip `resolveSettled()` in
391
+ * the fulfilled-then-stopped branch of {@link activate}, and hang `settled`.
392
+ *
393
+ * An `unmanaged` entry (a direct-adapter Channel this handler never activated)
394
+ * is left untouched: the manager owns no handle and no lifecycle for it, so
395
+ * claiming to have `stopped` it would be as untruthful as calling it `online`.
396
+ * The developer's `channel.start()`/stop path is unaffected by manager
397
+ * teardown.
398
+ *
399
+ * A WEDGED `handle.stop()` (one that never settles) is bounded by
400
+ * {@link ChannelManagerArgs.stopHandleTimeoutMs}: after the deadline the call
401
+ * is logged and abandoned so it can't hang `stop()` — and thus SIGTERM
402
+ * shutdown — forever.
403
+ *
404
+ * @param entry - The Channel entry to stop.
405
+ */
406
+ async stopEntry(entry) {
407
+ if (entry.status === "unmanaged") return;
408
+ entry.status = "stopped";
409
+ if (entry.handle && !entry.handleStopped) {
410
+ entry.handleStopped = true;
411
+ const handle = entry.handle;
412
+ await withTimeout(Promise.resolve().then(() => handle.stop()), this.stopHandleTimeoutMs, `channel handle stop() timed out after ${this.stopHandleTimeoutMs}ms during teardown`).catch((err) => this.log?.("channel handle stop() failed during teardown", err));
413
+ }
414
+ }
415
+ /**
416
+ * Stop every activated Channel exactly once and mark all statuses `stopped`.
417
+ * Idempotent — a second call is a no-op.
418
+ *
419
+ * Resolves promptly: {@link stopEntry} stops only the handles that already
420
+ * exist and never blocks on activations that have not settled. A hung connect
421
+ * (which `ready({ timeoutMs })` tolerates) has no handle to stop yet, and
422
+ * awaiting it here would hang teardown — and thus SIGTERM shutdown — forever.
423
+ * Any handle that arrives after this point is torn down by the post-settle
424
+ * guard in {@link activate}, which routes through the same idempotent
425
+ * {@link stopEntry}, so nothing leaks and nothing double-stops.
426
+ *
427
+ * Teardown is resilient to a throwing `handle.stop()`: `Promise.allSettled`
428
+ * over the per-entry `stopEntry` calls guarantees one rejection can't abort
429
+ * the rest, so every entry reaches `stopped` and `stop()` always resolves.
430
+ * It is equally resilient to a WEDGED `handle.stop()` that never settles: each
431
+ * is bounded by {@link ChannelManagerArgs.stopHandleTimeoutMs} inside
432
+ * {@link stopEntry}, so a single hung handle can't hang SIGTERM shutdown.
433
+ */
434
+ async stop() {
435
+ if (this.stopped) return;
436
+ this.stopped = true;
437
+ const entries = [...this.entries.values()];
438
+ await Promise.allSettled(entries.map((entry) => this.stopEntry(entry)));
439
+ }
440
+ };
441
+
442
+ //#endregion
443
+ exports.ChannelManager = ChannelManager;
444
+ //# sourceMappingURL=channel-manager.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"channel-manager.cjs","names":["deriveChannelActivationConfig","ChannelConfigError"],"sources":["../../../../src/v2/runtime/core/channel-manager.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport {\n ChannelConfigError,\n deriveChannelActivationConfig,\n} from \"./channel-activation-config\";\nimport type { ChannelActivationConfig } from \"./channel-activation-config\";\nimport type { CopilotKitIntelligence } from \"../intelligence-platform\";\n// Type-only: @copilotkit/channels is pure-ESM, so a value import would break this\n// package's CJS output (see `core/runtime.ts` and `channel-activation-config.ts`\n// for the same constraint).\nimport type { Channel } from \"@copilotkit/channels\";\n\n/**\n * Lifecycle status of a single Channel activation, or of the manager overall.\n *\n * - `connecting`: activation in flight, not yet settled.\n * - `online`: activation resolved AND the managed session can currently send.\n * A drop moves the Channel to `reconnecting` (not `online`); a successful\n * rejoin restores `online`.\n * - `setup_required`: the Channel is declared but has no managed provider yet —\n * a valid degraded state, not a failure.\n * - `reconnecting`: the managed session dropped and Phoenix is retrying — not\n * currently sendable. The manager does NOT re-activate (reconnection is\n * delegated to the Phoenix connection layer); it only reflects the health the\n * session reports via its `onStateChange` observer.\n * - `stopped`: {@link ChannelManager.stop} has torn the Channel down.\n * - `unmanaged`: the Channel carries a developer-supplied direct adapter, so this\n * handler does NOT own its lifecycle — the developer starts it via\n * `channel.start()`. The manager records the Channel with this status purely so\n * its presence is observable and never misreported as `online`. It is neither\n * activated, awaited, nor stopped here. Real routing of direct channels is\n * deferred (tracked in OSS-486).\n * - `error`: activation rejected with a non-setup error, OR a previously-online\n * session gave up reconnecting after its bounded reconnect window.\n */\nexport type ChannelStatus =\n | \"connecting\"\n | \"online\"\n | \"setup_required\"\n | \"reconnecting\"\n | \"stopped\"\n | \"unmanaged\"\n | \"error\";\n\n/**\n * The lifecycle control surface a Channel host uses to drive and observe\n * managed Channel activation.\n */\nexport interface ChannelsControl {\n /**\n * Resolve once every declared Channel has settled to a terminal, non-connecting\n * state (`online` or `setup_required`). Rejects if any Channel is in `error`,\n * or — when `timeoutMs` is given — if the whole set has not settled in time.\n */\n ready(opts?: { timeoutMs?: number }): Promise<void>;\n /** Snapshot the overall status and the per-Channel status map. */\n status(): { overall: ChannelStatus; channels: Record<string, ChannelStatus> };\n /** Tear down every activated Channel. Idempotent. */\n stop(): Promise<void>;\n}\n\n/**\n * Signals that a declared Channel cannot be activated because no managed\n * provider exists for it yet. The engine throws this (or any error whose\n * `code === \"SETUP_REQUIRED\"`) to move a Channel to `setup_required` rather\n * than `error` — a declared-but-unprovisioned Channel is a valid degraded\n * state, not a failure.\n */\nexport class ChannelSetupRequiredError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"ChannelSetupRequiredError\";\n }\n}\n\n/**\n * The activation engine: given a resolved {@link ChannelActivationConfig} and\n * the declared {@link Channel}, bring the Channel online and return its handle.\n * Injected in tests (a fake engine); defaults to the Realtime Gateway launcher.\n */\nexport type ActivateChannelEngine = (\n config: ChannelActivationConfig,\n channel: Channel,\n) => Promise<ChannelsHandle>;\n\n/**\n * Minimal structural view of the `@copilotkit/channels-intelligence`\n * `ChannelsHandle`. Declared locally (not imported) because the runtime is a\n * CJS package that must not take a static dependency on the pure-ESM\n * channels-intelligence package — the default engine reaches its launcher\n * through a dynamic `import()` instead. The manager only ever needs `stop()`.\n */\nexport interface ChannelsHandle {\n /** Activation metadata declared to Intelligence. Unused by the manager. */\n metadata: unknown;\n /** Stop the underlying Channel(s) and release transports. */\n stop(): Promise<void>;\n /**\n * Optional seam: register a callback the handle fires when its managed\n * session drops. Retained as a per-episode drop breadcrumb; the manager drives\n * status from {@link ChannelsHandle.onStateChange} instead. Present on the\n * Realtime Gateway launcher handle; optional for non-gateway/test handles.\n */\n onClose?(cb: () => void): void;\n /**\n * Optional seam: register a connection-health observer the handle fires as its\n * managed session moves between `online` (sendable), `reconnecting` (dropped,\n * Phoenix retrying), and `gave_up` (dead after the bounded reconnect window).\n * The manager uses this to keep {@link ChannelManager.status} honest — it does\n * NOT re-activate on a drop (reconnection is delegated to the Phoenix\n * connection layer; see {@link ChannelManager}). Optional so non-gateway or\n * test handles that do not implement it are always invoked as\n * `handle.onStateChange?.(cb)`.\n */\n onStateChange?(\n cb: (state: \"online\" | \"reconnecting\" | \"gave_up\") => void,\n ): void;\n}\n\n/** Constructor arguments for {@link ChannelManager}. */\nexport interface ChannelManagerArgs {\n /** The Intelligence runtime client the activation config is derived from. */\n intelligence: CopilotKitIntelligence;\n /** The declared framework Channels to activate. */\n channels: Channel[];\n /**\n * Activation engine. Defaults to a wrapper over the channels-intelligence\n * Realtime Gateway launcher (`startChannelsOverRealtimeGateway`), reached via\n * dynamic import so this CJS package keeps no static ESM dependency.\n */\n activateChannel?: ActivateChannelEngine;\n /** Mint a runtime instance id per Channel. Defaults to `rti_{uuid-no-dashes}`. */\n mintRuntimeInstanceId?: () => string;\n /** Diagnostic sink. Forwarded to the launcher/transport when the default\n * activation engine is used, so transport-level drops surface in the managed\n * path (not just activation-level events). */\n log?: (msg: string, meta?: unknown) => void;\n /** Per-handle deadline (ms) for `handle.stop()` during {@link ChannelManager.stop}\n * so a wedged stop can't hang SIGTERM shutdown. Default 5000. */\n stopHandleTimeoutMs?: number;\n}\n\n/** Per-Channel mutable activation entry tracked by the manager. */\ninterface ChannelEntry {\n status: ChannelStatus;\n /** Resolves on `online`/`setup_required`; rejects on `error`. Awaited by `ready`. */\n readonly settled: Promise<void>;\n handle?: ChannelsHandle;\n /**\n * Whether {@link ChannelManager.stopEntry} has already stopped `handle`. Gates\n * the single-stop guarantee: the success settle handler and `stop()` can both\n * reach the same entry in the same tick, but the handle is torn down at most\n * once.\n */\n handleStopped: boolean;\n}\n\n/** Non-literal specifier so the pure-ESM channels-intelligence package never\n * becomes a static dependency of this CJS package (mirrors the runtime's other\n * channels seams). */\nconst CHANNELS_INTELLIGENCE_SPECIFIER = \"@copilotkit/channels-intelligence\";\n\n/**\n * Structural view of the `@copilotkit/channels-intelligence` module surface the\n * default engine consumes. Declared locally (not imported) for the same\n * CJS/ESM-boundary reason the {@link ChannelsHandle} view is.\n */\nexport interface ChannelsIntelligenceModule {\n startChannelsOverRealtimeGateway: (\n channels: Channel[],\n opts: {\n wsUrl: string;\n apiKey: string;\n scope: { projectId: number; channelName: string };\n runtimeInstanceId: string;\n adapter?: string;\n /** Intelligence app-api HTTP base URL, forwarded to the transport so the\n * managed realtime path enables file/history parity (HTTP-only) — OSS-476. */\n appApiBaseUrl?: string;\n /** Diagnostic sink forwarded to the launcher/transport so transport-level\n * drop diagnostics (e.g. a version-skew missing-leaseToken outage) are not\n * silent in the managed path. */\n log?: (msg: string, meta?: unknown) => void;\n },\n ) => Promise<ChannelsHandle>;\n}\n\n/**\n * Default engine: wrap the channels-intelligence Realtime Gateway launcher.\n *\n * The module is reached through an injectable importer that defaults to a\n * dynamic `import()` of a non-literal specifier, so the pure-ESM\n * `@copilotkit/channels-intelligence` never becomes a static dependency of this\n * CJS package (mirrors the runtime's other channels seams). The `import`\n * seam is a parameter purely so this function's config→opts mapping and its\n * module-not-found / generic-error branches are unit-testable WITHOUT the real\n * package installed; production always uses the default importer.\n *\n * Passes NO `org`/`channelId` — the launcher's realtime scope treats them as\n * optional.\n *\n * @param config - Resolved activation config for the Channel.\n * @param channel - The Channel to activate.\n * @param importChannelsIntelligence - Test seam; loads the channels-intelligence\n * module. Defaults to a dynamic import of the real package.\n * @param log - Optional diagnostic sink forwarded to the launcher/transport so\n * transport-level drop diagnostics are not silent in the managed path.\n * @returns The launcher's {@link ChannelsHandle}.\n */\nexport async function defaultActivateChannel(\n config: ChannelActivationConfig,\n channel: Channel,\n importChannelsIntelligence: () => Promise<ChannelsIntelligenceModule> = () =>\n import(\n CHANNELS_INTELLIGENCE_SPECIFIER\n ) as Promise<ChannelsIntelligenceModule>,\n log?: (msg: string, meta?: unknown) => void,\n): Promise<ChannelsHandle> {\n let mod: ChannelsIntelligenceModule;\n try {\n mod = await importChannelsIntelligence();\n } catch (err) {\n if (isModuleNotFound(err)) {\n throw new Error(\n \"Managed Channels require '@copilotkit/channels-intelligence' to be installed. Add it to your app's dependencies.\",\n { cause: err },\n );\n }\n throw err;\n }\n return mod.startChannelsOverRealtimeGateway([channel], {\n wsUrl: config.wsUrl,\n apiKey: config.apiKey,\n scope: { projectId: config.projectId, channelName: config.channelName },\n runtimeInstanceId: config.runtimeInstanceId,\n adapter: config.adapter,\n // Forward the app-api HTTP base URL so the transport wires file/history\n // (HTTP-only) on the NORMAL managed path — without this, Channels started by\n // the CopilotRuntime handler run with no history/file support (OSS-476).\n appApiBaseUrl: config.apiUrl,\n // Forward the manager's diagnostic sink down to the launcher/transport so a\n // transport-level drop (e.g. a version-skew missing-leaseToken outage) is\n // observable in the managed path, not just activation-level events.\n ...(log ? { log } : {}),\n });\n}\n\n/** Whether `err` signals a missing managed provider rather than a hard failure. */\nfunction isSetupRequired(err: unknown): boolean {\n return (\n err instanceof ChannelSetupRequiredError ||\n (typeof err === \"object\" &&\n err !== null &&\n (err as { code?: unknown }).code === \"SETUP_REQUIRED\")\n );\n}\n\n/**\n * Whether `err` is a Node/runtime module-resolution failure — i.e. the error\n * a dynamic `import()` throws when the target package is not installed.\n * Exported so the friendly-error path in {@link defaultActivateChannel} can be\n * unit-tested without forcing a real failing import.\n */\nexport function isModuleNotFound(err: unknown): boolean {\n if (typeof err !== \"object\" || err === null) {\n return false;\n }\n const code = (err as { code?: unknown }).code;\n return code === \"ERR_MODULE_NOT_FOUND\" || code === \"MODULE_NOT_FOUND\";\n}\n\n/** Default deadline (ms) for a single `handle.stop()` during teardown. */\nconst DEFAULT_STOP_HANDLE_TIMEOUT_MS = 5_000;\n\n/**\n * Reject with `timeoutMessage` after `timeoutMs` if `inner` has not settled,\n * otherwise pass `inner` through. When `timeoutMs` is undefined, `inner` is\n * returned unchanged. The timer is `unref`'d so a pending deadline never keeps\n * the process alive, and `inner` always has a settle handler attached, so a\n * timed-out promise that later settles never surfaces as unhandled.\n */\nfunction withTimeout<T>(\n inner: Promise<T>,\n timeoutMs: number | undefined,\n timeoutMessage: string,\n): Promise<T> {\n if (timeoutMs === undefined) {\n return inner;\n }\n return new Promise<T>((resolve, reject) => {\n const timer = setTimeout(\n () => reject(new Error(timeoutMessage)),\n timeoutMs,\n );\n (timer as unknown as { unref?: () => void }).unref?.();\n inner.then(\n (value) => {\n clearTimeout(timer);\n resolve(value);\n },\n (err) => {\n clearTimeout(timer);\n reject(err);\n },\n );\n });\n}\n\n/**\n * Drives managed Channel activation for an Intelligence runtime: lazily\n * activates each declared Channel through an engine, tracks per-Channel\n * lifecycle status, exposes readiness, and tears everything down.\n *\n * Activation is lazy and idempotent — constructing the manager does nothing;\n * {@link activate} starts it and a second call is a no-op. Activation throws\n * SYNCHRONOUSLY (a {@link ChannelConfigError}) only for a misconfiguration it\n * can detect up front — a duplicate or missing Channel name. Every OTHER\n * activation failure is recorded as the Channel's status (`error`, or\n * `setup_required` for a missing provider) and surfaced through {@link status}\n * and {@link ready} rather than thrown.\n *\n * Reconnection is NOT handled here — it is delegated to the Phoenix connection\n * layer that backs the launcher. When a managed socket drops, Phoenix's `Socket`\n * auto-reconnects and auto-rejoins, re-sending the channel's join declaration;\n * the Intelligence gateway's `join/3` re-runs `record_heartbeat` (re-registering\n * the runtime's listener) and its `terminate/2` releases the dead socket's\n * leases (verified against Intelligence #511 `sdk_channel.ex`). So the transport\n * self-heals under the persistent adapter and a re-activation here would be both\n * redundant AND broken: re-invoking the engine on an already-started `Channel`\n * throws in `channel.addAdapter` (started=true). The manager therefore never\n * re-activates on a drop.\n *\n * It DOES, however, reflect real connection health through the session's\n * `onStateChange` observer so {@link ChannelManager.status} stays honest rather\n * than reporting `online` forever after a drop: a drop moves the Channel to\n * `reconnecting`, a successful rejoin restores `online`, and a bounded give-up\n * (Phoenix would otherwise retry forever) moves it to `error`.\n */\nexport class ChannelManager implements ChannelsControl {\n private readonly intelligence: CopilotKitIntelligence;\n private readonly channels: Channel[];\n private readonly activateChannel: ActivateChannelEngine;\n private readonly mintRuntimeInstanceId: () => string;\n private readonly log?: (msg: string, meta?: unknown) => void;\n private readonly stopHandleTimeoutMs: number;\n\n private readonly entries = new Map<string, ChannelEntry>();\n private activated = false;\n private stopped = false;\n\n /** @param args - See {@link ChannelManagerArgs}. */\n constructor(args: ChannelManagerArgs) {\n this.intelligence = args.intelligence;\n this.channels = args.channels;\n this.log = args.log;\n // When using the default engine, forward the manager's log DOWN to the\n // launcher/transport (via defaultActivateChannel's log param) so a\n // transport-level drop is observable in the managed path. `this.log` is read\n // lazily at activation time, so this closure always sees the assigned sink.\n this.activateChannel =\n args.activateChannel ??\n ((config, channel) =>\n defaultActivateChannel(config, channel, undefined, this.log));\n this.mintRuntimeInstanceId =\n args.mintRuntimeInstanceId ??\n (() => `rti_${randomUUID().replace(/-/g, \"\")}`);\n this.stopHandleTimeoutMs =\n args.stopHandleTimeoutMs ?? DEFAULT_STOP_HANDLE_TIMEOUT_MS;\n }\n\n /**\n * Start activation of every declared Channel (lazy + idempotent). Mints a\n * distinct runtime instance id per Channel, derives its activation config,\n * and calls the engine. Records each Channel as `connecting`, transitioning\n * to `online`/`setup_required`/`error` as its activation settles.\n */\n activate(): void {\n // Short-circuit on BOTH latches: `activated` makes activation idempotent,\n // and `stopped` prevents a post-`stop()` activate() from opening transports\n // on a dead manager. (A late activation self-heals via the post-settle guard,\n // but never starting it is cheaper and clearer.)\n if (this.activated || this.stopped) {\n return;\n }\n // Reject duplicate Channel names BEFORE kicking off any engine call. The\n // manager keys `entries` by name, so a duplicate would let the second\n // activation's entry silently overwrite the first — leaking the first\n // Channel's live session out of status()/ready()/stop(). Fail loud here so\n // nothing is ever activated in that state.\n this.assertUniqueChannelNames();\n this.activated = true;\n\n // Partition declared Channels by transport. A Channel carrying ANY adapter\n // that is NOT the Intelligence managed adapter (a developer-supplied\n // slack/discord/... adapter, which lacks `__intelligenceChannel`) is a\n // DIRECT channel: it is started by the developer via `channel.start()`, not\n // managed-activated here. The skip is EXCLUSIVE PER CHANNEL, not per platform\n // — a Channel served by a direct adapter is not also managed: ANY direct\n // adapter makes the WHOLE Channel `unmanaged` and skips managed activation,\n // regardless of platform. Attaching the managed adapter alongside a direct\n // one would double-deliver every turn (and trip the SDK's `assertExclusive`\n // guard, moving the Channel to `error`). Per the SoT rule, never infer\n // managed intent from a local direct adapter — a managed-eligible Channel has\n // an empty `adapters` at declaration time. Managed+direct coexistence on the\n // same Channel is NOT supported today; it is deferred (OSS-484), as is real\n // routing of direct channels (OSS-486).\n for (const channel of this.channels) {\n const isDirect = channel.adapters.some((a) => !a.__intelligenceChannel);\n if (isDirect) {\n this.log?.(\n `channel \"${channel.name!}\" carries a direct adapter — recording status \"unmanaged\" and skipping managed activation (this handler does not own its lifecycle; start it via channel.start(); exclusive per Channel: a Channel served by a direct adapter is not also managed, regardless of platform — managed+direct coexistence deferred (OSS-484); routing of direct channels deferred (OSS-486))`,\n );\n // Record an EXPLICIT `unmanaged` entry rather than skipping silently.\n // A skipped Channel with no entry vanishes from status()/computeOverall,\n // so a runtime whose only Channel is direct would falsely read `online`\n // and ready() would imply a health this handler never established. The\n // entry keeps the Channel observable and truthful: it is never\n // activated, its `settled` is already resolved (nothing on the managed\n // path to wait for), and stopEntry leaves it untouched (see stopEntry).\n this.entries.set(channel.name!, {\n status: \"unmanaged\",\n handle: undefined,\n handleStopped: false,\n settled: Promise.resolve(),\n });\n continue;\n }\n const name = channel.name!;\n const runtimeInstanceId = this.mintRuntimeInstanceId();\n\n let resolveSettled!: () => void;\n let rejectSettled!: (err: unknown) => void;\n const settled = new Promise<void>((resolve, reject) => {\n resolveSettled = resolve;\n rejectSettled = reject;\n });\n // ready() awaits `settled`; if nothing ever handles a rejection there,\n // Node reports an unhandled rejection. Attach a no-op catch so the\n // promise is always considered handled — ready() still sees the reason.\n settled.catch(() => {});\n\n // Invoke the engine synchronously so activation is observably started the\n // moment activate() returns (callers assert the engine was called and see\n // `connecting` before awaiting ready). A synchronous config/engine throw is\n // turned into a rejected activation so it becomes this channel's status\n // rather than throwing out of activate().\n let activation: Promise<ChannelsHandle>;\n let config: ChannelActivationConfig | undefined;\n try {\n config = deriveChannelActivationConfig({\n intelligence: this.intelligence,\n channel,\n runtimeInstanceId,\n });\n activation = this.activateChannel(config, channel);\n } catch (err) {\n activation = Promise.reject(err);\n }\n\n // The deferred `.then` callbacks capture `entry` and run only after the\n // literal has fully initialized, so referencing it here is safe.\n const entry: ChannelEntry = {\n status: \"connecting\",\n handle: undefined,\n handleStopped: false,\n settled,\n };\n\n // Anchor the settle handlers. Both branches route every teardown through\n // the idempotent `stopEntry`, so a late settle can never resurrect a\n // `stopped` entry and a handle is torn down at most once. The handlers\n // only mutate state (never throw), so the trailing no-op catch just keeps\n // the chain from surfacing as an unhandled rejection.\n activation\n .then(\n async (handle) => {\n entry.handle = handle;\n if (this.stopped) {\n // stop() ran before this activation settled, so it could not tear\n // down a handle that did not exist yet. Release it now (idempotent)\n // and keep the Channel `stopped`.\n await this.stopEntry(entry);\n resolveSettled();\n return;\n }\n entry.status = \"online\";\n this.registerConnectionObserver(name, entry);\n resolveSettled();\n },\n async (err: unknown) => {\n if (this.stopped) {\n // A rejection that arrives AFTER stop() must NOT resurrect the\n // entry into `error`/`setup_required`: the Channel is already\n // being torn down. Keep it `stopped` and resolve `settled` so a\n // subsequent ready() does not reject on a stopped Channel.\n await this.stopEntry(entry);\n resolveSettled();\n return;\n }\n if (isSetupRequired(err)) {\n entry.status = \"setup_required\";\n this.log?.(`channel \"${name}\" requires setup`, err);\n resolveSettled();\n } else {\n entry.status = \"error\";\n this.log?.(`channel \"${name}\" failed to activate`, err);\n rejectSettled(err);\n }\n },\n )\n .catch(() => {});\n\n this.entries.set(name, entry);\n }\n }\n\n /**\n * Throw if two declared Channels share a `name`. `entries` is keyed by name,\n * so a duplicate would overwrite the first Channel's entry and leak its live\n * session. Called at the very start of {@link activate}, before any engine\n * call, so a misconfiguration fails loud instead of silently.\n *\n * @throws {ChannelConfigError} If any Channel is missing a name, or if any\n * name appears more than once.\n */\n private assertUniqueChannelNames(): void {\n const seen = new Set<string>();\n for (const channel of this.channels) {\n const name = channel.name;\n // Check for a missing/empty name FIRST: `channel.name!` on a nameless\n // Channel keys as the string \"undefined\", which would otherwise report a\n // spurious duplicate for two nameless Channels before the accurate\n // missing-name error. Fail with the precise error instead.\n if (!name) {\n throw new ChannelConfigError(\n \"A managed Channel is missing a `name` — every declared Channel must \" +\n \"have a unique, non-empty name (pass createChannel({ name })).\",\n );\n }\n if (seen.has(name)) {\n throw new ChannelConfigError(\n `Duplicate managed Channel name \"${name}\" — every declared Channel ` +\n `must have a unique name.`,\n );\n }\n seen.add(name);\n }\n }\n\n /**\n * Resolve when every managed Channel has settled to `online`/`setup_required`.\n *\n * A direct-adapter (`unmanaged`) Channel has an already-resolved `settled` and\n * so never blocks — but its resolution implies NO health: this handler does not\n * own it. Truthfulness about direct Channels lives in {@link status} (they read\n * `unmanaged`, never `online`), not in `ready()` resolving.\n *\n * Activates lazily if not already started — so a first call rejects with the\n * same {@link ChannelConfigError} as the synchronous throw from\n * {@link activate} for an up-front misconfiguration (duplicate/missing Channel\n * names). Once activation has been kicked off, all OTHER failures are surfaced\n * here instead: this rejects with an `AggregateError` if any Channel settled\n * to `error` OR — when `timeoutMs` is given — did not settle in time. The\n * `timeoutMs` deadline is applied PER CHANNEL, so the aggregate carries each\n * failed Channel's real reason AND a named timeout for each Channel still\n * hanging: a genuine activation error is never masked by a sibling that hangs\n * (a pre-fix set-wide timeout discarded the real reason in that case).\n *\n * A STOPPED manager short-circuits and resolves: a Channel that settled to\n * `error` BEFORE {@link stop} already rejected its `settled` promise, so\n * awaiting it here would throw an `AggregateError` even though\n * {@link status}.overall is `\"stopped\"` — inconsistent with the case where the\n * Channel was still online at stop() (which resolves). A stopped manager has\n * nothing left to be ready for, so resolve uniformly.\n *\n * `ready()` is ONE-SHOT: it settles on the INITIAL activation outcome. Later\n * connection-health transitions (a live Channel dropping to `reconnecting`, or\n * giving up to `error`) are reported through {@link status} — where `online`\n * means currently-sendable — but do NOT re-arm or re-reject an already-settled\n * `ready()`.\n */\n async ready(opts?: { timeoutMs?: number }): Promise<void> {\n if (this.stopped) {\n return;\n }\n this.activate();\n const entries = [...this.entries.entries()];\n // Apply `timeoutMs` PER CHANNEL rather than to the whole set. A single\n // set-wide timeout wrapping `allSettled` would, when one channel settles to\n // `error` while a sibling hangs, reject with only a generic timeout and\n // DISCARD the erroring channel's real reason. Timing out each channel's\n // `settled` independently lets `allSettled` collect BOTH a hung channel's\n // named timeout AND a failed channel's real error into one AggregateError.\n const results = await Promise.allSettled(\n entries.map(([name, e]) =>\n withTimeout(\n e.settled,\n opts?.timeoutMs,\n `channel \"${name}\" did not settle within ${opts?.timeoutMs}ms`,\n ),\n ),\n );\n const errors = results\n .filter((r): r is PromiseRejectedResult => r.status === \"rejected\")\n .map((r) => r.reason);\n if (errors.length > 0) {\n throw new AggregateError(\n errors,\n `ChannelManager.ready: ${errors.length} channel(s) failed to activate or settle in time`,\n );\n }\n }\n\n /**\n * Snapshot status. Every declared Channel — managed OR direct/`unmanaged` —\n * appears keyed by name in `channels`; a direct-adapter Channel this handler\n * does not own is always surfaced as `unmanaged`, never `online`.\n *\n * `overall` is folded over the MANAGED Channels only (see {@link computeOverall}),\n * by precedence `error` > `reconnecting` > `setup_required` > `connecting` >\n * `online`. `online` means every managed Channel can currently send.\n * `reconnecting` outranks `setup_required` because a dropped-but-retrying\n * Channel is an active outage, louder than a steadily-degraded unprovisioned\n * one. `unmanaged` Channels are EXCLUDED from that fold — they carry no health\n * this handler established — so a healthy managed Channel alongside an\n * `unmanaged` one still reports `overall: \"online\"` while the `unmanaged` one\n * stays visible per-Channel. When every declared Channel is `unmanaged`,\n * `overall` is `unmanaged` (NOT `online`). With no declared Channels at all,\n * `overall` is `online` (nothing is degraded); once every managed Channel has\n * been stopped, `overall` is `stopped`.\n */\n status(): {\n overall: ChannelStatus;\n channels: Record<string, ChannelStatus>;\n } {\n const channels: Record<string, ChannelStatus> = {};\n for (const [name, entry] of this.entries) {\n channels[name] = entry.status;\n }\n // A stopped manager is `stopped` regardless of whether it was ever activated.\n // stop() before activate() (e.g. SIGTERM during startup) leaves `entries`\n // empty, and the empty-set fold below returns `online` — a torn-down manager\n // must never read healthy. Short-circuit before that fold. (After a normal\n // activate→stop, every entry is already `stopped` and the fold agrees, so\n // this is also consistent with the populated case.)\n if (this.stopped) {\n return { overall: \"stopped\", channels };\n }\n // Before activate() has run, `entries` is empty. Folding an empty set gives\n // `online` — correct for a manager that declares NO channels (nothing is\n // degraded), but a LIE for one that declares channels and simply has not\n // opened its socket yet: activation is lazy (deferred to the first\n // `ready()`), so a not-yet-activated manager must never read `online`.\n // Report `connecting` (\"not started\") for that case so `status()` is honest\n // before any `ready()`.\n if (!this.activated && this.channels.length > 0) {\n return { overall: \"connecting\", channels };\n }\n return { overall: this.computeOverall(Object.values(channels)), channels };\n }\n\n /**\n * Fold per-Channel statuses into a single overall status (see {@link status}).\n *\n * `unmanaged` Channels are folded out FIRST: they carry no lifecycle this\n * handler owns, so they must neither count as `online` nor mask a real managed\n * outage. The remaining MANAGED statuses are ranked\n * `error` > `reconnecting` > `setup_required` > `connecting` > `online`, so a\n * genuine managed failure still dominates while a healthy managed Channel\n * beside an `unmanaged` one reads `online`. If NO managed Channels remain (every\n * declared Channel is direct/`unmanaged`) the result is `unmanaged` — never the\n * false-healthy `online`. The empty-input case (no declared Channels at all)\n * stays `online` (nothing is degraded).\n */\n private computeOverall(values: ChannelStatus[]): ChannelStatus {\n if (values.length === 0) {\n return \"online\";\n }\n const managed = values.filter((v) => v !== \"unmanaged\");\n if (managed.length === 0) {\n return \"unmanaged\";\n }\n if (managed.every((v) => v === \"stopped\")) {\n return \"stopped\";\n }\n if (managed.includes(\"error\")) {\n return \"error\";\n }\n if (managed.includes(\"reconnecting\")) {\n return \"reconnecting\";\n }\n if (managed.includes(\"setup_required\")) {\n return \"setup_required\";\n }\n if (managed.includes(\"connecting\")) {\n return \"connecting\";\n }\n return \"online\";\n }\n\n /**\n * Wire the Channel's connection-health observer (if the handle exposes the\n * optional `onStateChange` seam) so {@link ChannelManager.status} reflects real\n * health instead of reporting `online` forever after a drop:\n *\n * - `reconnecting` → status `reconnecting` (dropped, Phoenix retrying);\n * - `online` → status `online` (rejoined, sendable again);\n * - `gave_up` → status `error` (dead after the bounded reconnect window).\n *\n * Makes NO re-activation — reconnection is delegated to the Phoenix connection\n * layer (see {@link ChannelManager}), which auto-rejoins under the persistent\n * adapter. A STOPPED manager (or an already-stopped entry) ignores late\n * connection events, so a drop that fires after {@link ChannelManager.stop}\n * never resurrects the Channel out of `stopped`.\n *\n * @param name - The Channel name (map key).\n * @param entry - The Channel's activation entry.\n */\n private registerConnectionObserver(name: string, entry: ChannelEntry): void {\n entry.handle?.onStateChange?.((state) => {\n // A stopped manager (or a stopped entry) ignores late connection events.\n if (this.stopped || entry.status === \"stopped\") {\n return;\n }\n if (state === \"reconnecting\") {\n entry.status = \"reconnecting\";\n this.log?.(\n `channel \"${name}\" managed session dropped; reconnecting (Phoenix auto-rejoin)`,\n );\n } else if (state === \"online\") {\n entry.status = \"online\";\n this.log?.(`channel \"${name}\" managed session back online`);\n } else {\n entry.status = \"error\";\n this.log?.(\n `channel \"${name}\" managed session gave up reconnecting; marking error`,\n );\n }\n });\n }\n\n /**\n * Drive a single entry to its terminal `stopped` state, tearing down its\n * handle AT MOST ONCE. Idempotent: it always sets `status = \"stopped\"`, and\n * only calls `handle.stop()` on the first invocation that sees a live,\n * not-yet-stopped handle (gated by {@link ChannelEntry.handleStopped}).\n *\n * This is the ONE guarded teardown path shared by both `stop()` and the\n * post-settle guard in {@link activate}. Because the guard is per-entry and\n * idempotent, a handle assigned in the same tick as `stop()` is stopped\n * exactly once even when both callers reach the entry, and a late settle can\n * never resurrect a `stopped` entry.\n *\n * `handle.stop()` failures are logged (via {@link ChannelManager.log}) but NOT\n * rethrown: the real launcher's `stop()` rethrows after `session.disconnect()`,\n * and teardown must still complete for every other entry. The call is wrapped\n * in `Promise.resolve().then(...)` so a foreign/injected handle whose `stop()`\n * throws SYNCHRONOUSLY (before any promise is created) is caught by the same\n * `.catch` — otherwise the sync throw would escape, skip `resolveSettled()` in\n * the fulfilled-then-stopped branch of {@link activate}, and hang `settled`.\n *\n * An `unmanaged` entry (a direct-adapter Channel this handler never activated)\n * is left untouched: the manager owns no handle and no lifecycle for it, so\n * claiming to have `stopped` it would be as untruthful as calling it `online`.\n * The developer's `channel.start()`/stop path is unaffected by manager\n * teardown.\n *\n * A WEDGED `handle.stop()` (one that never settles) is bounded by\n * {@link ChannelManagerArgs.stopHandleTimeoutMs}: after the deadline the call\n * is logged and abandoned so it can't hang `stop()` — and thus SIGTERM\n * shutdown — forever.\n *\n * @param entry - The Channel entry to stop.\n */\n private async stopEntry(entry: ChannelEntry): Promise<void> {\n if (entry.status === \"unmanaged\") {\n return;\n }\n entry.status = \"stopped\";\n if (entry.handle && !entry.handleStopped) {\n entry.handleStopped = true;\n const handle = entry.handle;\n // Bound handle.stop(): a wedged stop() (e.g. a socket.disconnect that\n // never returns) must not hang teardown — and thus SIGTERM shutdown —\n // forever. On timeout, log and abandon it (the call keeps running with a\n // settle handler attached inside withTimeout, so it never surfaces as an\n // unhandled rejection) so every OTHER entry still reaches `stopped`. The\n // `Promise.resolve().then(...)` wrap also routes a SYNCHRONOUS throw from\n // a foreign handle through the same timeout+catch.\n await withTimeout(\n Promise.resolve().then(() => handle.stop()),\n this.stopHandleTimeoutMs,\n `channel handle stop() timed out after ${this.stopHandleTimeoutMs}ms during teardown`,\n ).catch((err: unknown) =>\n this.log?.(\"channel handle stop() failed during teardown\", err),\n );\n }\n }\n\n /**\n * Stop every activated Channel exactly once and mark all statuses `stopped`.\n * Idempotent — a second call is a no-op.\n *\n * Resolves promptly: {@link stopEntry} stops only the handles that already\n * exist and never blocks on activations that have not settled. A hung connect\n * (which `ready({ timeoutMs })` tolerates) has no handle to stop yet, and\n * awaiting it here would hang teardown — and thus SIGTERM shutdown — forever.\n * Any handle that arrives after this point is torn down by the post-settle\n * guard in {@link activate}, which routes through the same idempotent\n * {@link stopEntry}, so nothing leaks and nothing double-stops.\n *\n * Teardown is resilient to a throwing `handle.stop()`: `Promise.allSettled`\n * over the per-entry `stopEntry` calls guarantees one rejection can't abort\n * the rest, so every entry reaches `stopped` and `stop()` always resolves.\n * It is equally resilient to a WEDGED `handle.stop()` that never settles: each\n * is bounded by {@link ChannelManagerArgs.stopHandleTimeoutMs} inside\n * {@link stopEntry}, so a single hung handle can't hang SIGTERM shutdown.\n */\n async stop(): Promise<void> {\n if (this.stopped) {\n return;\n }\n this.stopped = true;\n\n const entries = [...this.entries.values()];\n await Promise.allSettled(entries.map((entry) => this.stopEntry(entry)));\n }\n}\n"],"mappings":";;;;;;;;;;;;;AAoEA,IAAa,4BAAb,cAA+C,MAAM;CACnD,YAAY,SAAiB;AAC3B,QAAM,QAAQ;AACd,OAAK,OAAO;;;;;;AAyFhB,MAAM,kCAAkC;;;;;;;;;;;;;;;;;;;;;;;AAiDxC,eAAsB,uBACpB,QACA,SACA,mCACE,OACE,kCAEJ,KACyB;CACzB,IAAI;AACJ,KAAI;AACF,QAAM,MAAM,4BAA4B;UACjC,KAAK;AACZ,MAAI,iBAAiB,IAAI,CACvB,OAAM,IAAI,MACR,oHACA,EAAE,OAAO,KAAK,CACf;AAEH,QAAM;;AAER,QAAO,IAAI,iCAAiC,CAAC,QAAQ,EAAE;EACrD,OAAO,OAAO;EACd,QAAQ,OAAO;EACf,OAAO;GAAE,WAAW,OAAO;GAAW,aAAa,OAAO;GAAa;EACvE,mBAAmB,OAAO;EAC1B,SAAS,OAAO;EAIhB,eAAe,OAAO;EAItB,GAAI,MAAM,EAAE,KAAK,GAAG,EAAE;EACvB,CAAC;;;AAIJ,SAAS,gBAAgB,KAAuB;AAC9C,QACE,eAAe,6BACd,OAAO,QAAQ,YACd,QAAQ,QACP,IAA2B,SAAS;;;;;;;;AAU3C,SAAgB,iBAAiB,KAAuB;AACtD,KAAI,OAAO,QAAQ,YAAY,QAAQ,KACrC,QAAO;CAET,MAAM,OAAQ,IAA2B;AACzC,QAAO,SAAS,0BAA0B,SAAS;;;AAIrD,MAAM,iCAAiC;;;;;;;;AASvC,SAAS,YACP,OACA,WACA,gBACY;AACZ,KAAI,cAAc,OAChB,QAAO;AAET,QAAO,IAAI,SAAY,SAAS,WAAW;EACzC,MAAM,QAAQ,iBACN,OAAO,IAAI,MAAM,eAAe,CAAC,EACvC,UACD;AACD,EAAC,MAA4C,SAAS;AACtD,QAAM,MACH,UAAU;AACT,gBAAa,MAAM;AACnB,WAAQ,MAAM;MAEf,QAAQ;AACP,gBAAa,MAAM;AACnB,UAAO,IAAI;IAEd;GACD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiCJ,IAAa,iBAAb,MAAuD;;CAarD,YAAY,MAA0B;iCALX,IAAI,KAA2B;mBACtC;iBACF;AAIhB,OAAK,eAAe,KAAK;AACzB,OAAK,WAAW,KAAK;AACrB,OAAK,MAAM,KAAK;AAKhB,OAAK,kBACH,KAAK,qBACH,QAAQ,YACR,uBAAuB,QAAQ,SAAS,QAAW,KAAK,IAAI;AAChE,OAAK,wBACH,KAAK,gCACE,oCAAmB,CAAC,QAAQ,MAAM,GAAG;AAC9C,OAAK,sBACH,KAAK,uBAAuB;;;;;;;;CAShC,WAAiB;AAKf,MAAI,KAAK,aAAa,KAAK,QACzB;AAOF,OAAK,0BAA0B;AAC/B,OAAK,YAAY;AAgBjB,OAAK,MAAM,WAAW,KAAK,UAAU;AAEnC,OADiB,QAAQ,SAAS,MAAM,MAAM,CAAC,EAAE,sBAAsB,EACzD;AACZ,SAAK,MACH,YAAY,QAAQ,KAAM,2WAC3B;AAQD,SAAK,QAAQ,IAAI,QAAQ,MAAO;KAC9B,QAAQ;KACR,QAAQ;KACR,eAAe;KACf,SAAS,QAAQ,SAAS;KAC3B,CAAC;AACF;;GAEF,MAAM,OAAO,QAAQ;GACrB,MAAM,oBAAoB,KAAK,uBAAuB;GAEtD,IAAI;GACJ,IAAI;GACJ,MAAM,UAAU,IAAI,SAAe,SAAS,WAAW;AACrD,qBAAiB;AACjB,oBAAgB;KAChB;AAIF,WAAQ,YAAY,GAAG;GAOvB,IAAI;GACJ,IAAI;AACJ,OAAI;AACF,aAASA,gEAA8B;KACrC,cAAc,KAAK;KACnB;KACA;KACD,CAAC;AACF,iBAAa,KAAK,gBAAgB,QAAQ,QAAQ;YAC3C,KAAK;AACZ,iBAAa,QAAQ,OAAO,IAAI;;GAKlC,MAAM,QAAsB;IAC1B,QAAQ;IACR,QAAQ;IACR,eAAe;IACf;IACD;AAOD,cACG,KACC,OAAO,WAAW;AAChB,UAAM,SAAS;AACf,QAAI,KAAK,SAAS;AAIhB,WAAM,KAAK,UAAU,MAAM;AAC3B,qBAAgB;AAChB;;AAEF,UAAM,SAAS;AACf,SAAK,2BAA2B,MAAM,MAAM;AAC5C,oBAAgB;MAElB,OAAO,QAAiB;AACtB,QAAI,KAAK,SAAS;AAKhB,WAAM,KAAK,UAAU,MAAM;AAC3B,qBAAgB;AAChB;;AAEF,QAAI,gBAAgB,IAAI,EAAE;AACxB,WAAM,SAAS;AACf,UAAK,MAAM,YAAY,KAAK,mBAAmB,IAAI;AACnD,qBAAgB;WACX;AACL,WAAM,SAAS;AACf,UAAK,MAAM,YAAY,KAAK,uBAAuB,IAAI;AACvD,mBAAc,IAAI;;KAGvB,CACA,YAAY,GAAG;AAElB,QAAK,QAAQ,IAAI,MAAM,MAAM;;;;;;;;;;;;CAajC,AAAQ,2BAAiC;EACvC,MAAM,uBAAO,IAAI,KAAa;AAC9B,OAAK,MAAM,WAAW,KAAK,UAAU;GACnC,MAAM,OAAO,QAAQ;AAKrB,OAAI,CAAC,KACH,OAAM,IAAIC,qDACR,oIAED;AAEH,OAAI,KAAK,IAAI,KAAK,CAChB,OAAM,IAAIA,qDACR,mCAAmC,KAAK,qDAEzC;AAEH,QAAK,IAAI,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoClB,MAAM,MAAM,MAA8C;AACxD,MAAI,KAAK,QACP;AAEF,OAAK,UAAU;EACf,MAAM,UAAU,CAAC,GAAG,KAAK,QAAQ,SAAS,CAAC;EAgB3C,MAAM,UATU,MAAM,QAAQ,WAC5B,QAAQ,KAAK,CAAC,MAAM,OAClB,YACE,EAAE,SACF,MAAM,WACN,YAAY,KAAK,0BAA0B,MAAM,UAAU,IAC5D,CACF,CACF,EAEE,QAAQ,MAAkC,EAAE,WAAW,WAAW,CAClE,KAAK,MAAM,EAAE,OAAO;AACvB,MAAI,OAAO,SAAS,EAClB,OAAM,IAAI,eACR,QACA,yBAAyB,OAAO,OAAO,kDACxC;;;;;;;;;;;;;;;;;;;;CAsBL,SAGE;EACA,MAAM,WAA0C,EAAE;AAClD,OAAK,MAAM,CAAC,MAAM,UAAU,KAAK,QAC/B,UAAS,QAAQ,MAAM;AAQzB,MAAI,KAAK,QACP,QAAO;GAAE,SAAS;GAAW;GAAU;AASzC,MAAI,CAAC,KAAK,aAAa,KAAK,SAAS,SAAS,EAC5C,QAAO;GAAE,SAAS;GAAc;GAAU;AAE5C,SAAO;GAAE,SAAS,KAAK,eAAe,OAAO,OAAO,SAAS,CAAC;GAAE;GAAU;;;;;;;;;;;;;;;CAgB5E,AAAQ,eAAe,QAAwC;AAC7D,MAAI,OAAO,WAAW,EACpB,QAAO;EAET,MAAM,UAAU,OAAO,QAAQ,MAAM,MAAM,YAAY;AACvD,MAAI,QAAQ,WAAW,EACrB,QAAO;AAET,MAAI,QAAQ,OAAO,MAAM,MAAM,UAAU,CACvC,QAAO;AAET,MAAI,QAAQ,SAAS,QAAQ,CAC3B,QAAO;AAET,MAAI,QAAQ,SAAS,eAAe,CAClC,QAAO;AAET,MAAI,QAAQ,SAAS,iBAAiB,CACpC,QAAO;AAET,MAAI,QAAQ,SAAS,aAAa,CAChC,QAAO;AAET,SAAO;;;;;;;;;;;;;;;;;;;;CAqBT,AAAQ,2BAA2B,MAAc,OAA2B;AAC1E,QAAM,QAAQ,iBAAiB,UAAU;AAEvC,OAAI,KAAK,WAAW,MAAM,WAAW,UACnC;AAEF,OAAI,UAAU,gBAAgB;AAC5B,UAAM,SAAS;AACf,SAAK,MACH,YAAY,KAAK,+DAClB;cACQ,UAAU,UAAU;AAC7B,UAAM,SAAS;AACf,SAAK,MAAM,YAAY,KAAK,+BAA+B;UACtD;AACL,UAAM,SAAS;AACf,SAAK,MACH,YAAY,KAAK,uDAClB;;IAEH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoCJ,MAAc,UAAU,OAAoC;AAC1D,MAAI,MAAM,WAAW,YACnB;AAEF,QAAM,SAAS;AACf,MAAI,MAAM,UAAU,CAAC,MAAM,eAAe;AACxC,SAAM,gBAAgB;GACtB,MAAM,SAAS,MAAM;AAQrB,SAAM,YACJ,QAAQ,SAAS,CAAC,WAAW,OAAO,MAAM,CAAC,EAC3C,KAAK,qBACL,yCAAyC,KAAK,oBAAoB,oBACnE,CAAC,OAAO,QACP,KAAK,MAAM,gDAAgD,IAAI,CAChE;;;;;;;;;;;;;;;;;;;;;;CAuBL,MAAM,OAAsB;AAC1B,MAAI,KAAK,QACP;AAEF,OAAK,UAAU;EAEf,MAAM,UAAU,CAAC,GAAG,KAAK,QAAQ,QAAQ,CAAC;AAC1C,QAAM,QAAQ,WAAW,QAAQ,KAAK,UAAU,KAAK,UAAU,MAAM,CAAC,CAAC"}
@@ -0,0 +1,90 @@
1
+ require("reflect-metadata");
2
+ import { index_d_exports } from "../../../channels/dist/index.cjs";
3
+ import { ChannelActivationConfig } from "./channel-activation-config.cjs";
4
+
5
+ //#region src/v2/runtime/core/channel-manager.d.ts
6
+ /**
7
+ * Lifecycle status of a single Channel activation, or of the manager overall.
8
+ *
9
+ * - `connecting`: activation in flight, not yet settled.
10
+ * - `online`: activation resolved AND the managed session can currently send.
11
+ * A drop moves the Channel to `reconnecting` (not `online`); a successful
12
+ * rejoin restores `online`.
13
+ * - `setup_required`: the Channel is declared but has no managed provider yet —
14
+ * a valid degraded state, not a failure.
15
+ * - `reconnecting`: the managed session dropped and Phoenix is retrying — not
16
+ * currently sendable. The manager does NOT re-activate (reconnection is
17
+ * delegated to the Phoenix connection layer); it only reflects the health the
18
+ * session reports via its `onStateChange` observer.
19
+ * - `stopped`: {@link ChannelManager.stop} has torn the Channel down.
20
+ * - `unmanaged`: the Channel carries a developer-supplied direct adapter, so this
21
+ * handler does NOT own its lifecycle — the developer starts it via
22
+ * `channel.start()`. The manager records the Channel with this status purely so
23
+ * its presence is observable and never misreported as `online`. It is neither
24
+ * activated, awaited, nor stopped here. Real routing of direct channels is
25
+ * deferred (tracked in OSS-486).
26
+ * - `error`: activation rejected with a non-setup error, OR a previously-online
27
+ * session gave up reconnecting after its bounded reconnect window.
28
+ */
29
+ type ChannelStatus = "connecting" | "online" | "setup_required" | "reconnecting" | "stopped" | "unmanaged" | "error";
30
+ /**
31
+ * The lifecycle control surface a Channel host uses to drive and observe
32
+ * managed Channel activation.
33
+ */
34
+ interface ChannelsControl {
35
+ /**
36
+ * Resolve once every declared Channel has settled to a terminal, non-connecting
37
+ * state (`online` or `setup_required`). Rejects if any Channel is in `error`,
38
+ * or — when `timeoutMs` is given — if the whole set has not settled in time.
39
+ */
40
+ ready(opts?: {
41
+ timeoutMs?: number;
42
+ }): Promise<void>;
43
+ /** Snapshot the overall status and the per-Channel status map. */
44
+ status(): {
45
+ overall: ChannelStatus;
46
+ channels: Record<string, ChannelStatus>;
47
+ };
48
+ /** Tear down every activated Channel. Idempotent. */
49
+ stop(): Promise<void>;
50
+ }
51
+ /**
52
+ * The activation engine: given a resolved {@link ChannelActivationConfig} and
53
+ * the declared {@link Channel}, bring the Channel online and return its handle.
54
+ * Injected in tests (a fake engine); defaults to the Realtime Gateway launcher.
55
+ */
56
+ type ActivateChannelEngine = (config: ChannelActivationConfig, channel: index_d_exports.Channel) => Promise<ChannelsHandle>;
57
+ /**
58
+ * Minimal structural view of the `@copilotkit/channels-intelligence`
59
+ * `ChannelsHandle`. Declared locally (not imported) because the runtime is a
60
+ * CJS package that must not take a static dependency on the pure-ESM
61
+ * channels-intelligence package — the default engine reaches its launcher
62
+ * through a dynamic `import()` instead. The manager only ever needs `stop()`.
63
+ */
64
+ interface ChannelsHandle {
65
+ /** Activation metadata declared to Intelligence. Unused by the manager. */
66
+ metadata: unknown;
67
+ /** Stop the underlying Channel(s) and release transports. */
68
+ stop(): Promise<void>;
69
+ /**
70
+ * Optional seam: register a callback the handle fires when its managed
71
+ * session drops. Retained as a per-episode drop breadcrumb; the manager drives
72
+ * status from {@link ChannelsHandle.onStateChange} instead. Present on the
73
+ * Realtime Gateway launcher handle; optional for non-gateway/test handles.
74
+ */
75
+ onClose?(cb: () => void): void;
76
+ /**
77
+ * Optional seam: register a connection-health observer the handle fires as its
78
+ * managed session moves between `online` (sendable), `reconnecting` (dropped,
79
+ * Phoenix retrying), and `gave_up` (dead after the bounded reconnect window).
80
+ * The manager uses this to keep {@link ChannelManager.status} honest — it does
81
+ * NOT re-activate on a drop (reconnection is delegated to the Phoenix
82
+ * connection layer; see {@link ChannelManager}). Optional so non-gateway or
83
+ * test handles that do not implement it are always invoked as
84
+ * `handle.onStateChange?.(cb)`.
85
+ */
86
+ onStateChange?(cb: (state: "online" | "reconnecting" | "gave_up") => void): void;
87
+ }
88
+ //#endregion
89
+ export { ActivateChannelEngine, ChannelStatus, ChannelsControl };
90
+ //# sourceMappingURL=channel-manager.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"channel-manager.d.cts","names":[],"sources":["../../../../src/v2/runtime/core/channel-manager.ts"],"mappings":";;;;;;AAmCA;;;;;AAaA;;;;;;;;;;;;;;;;;KAbY,aAAA;;;;;UAaK,eAAA;EAUA;AAsBjB;;;;EA1BE,KAAA,CAAM,IAAA;IAAS,SAAA;EAAA,IAAuB,OAAA;EA6B5B;EA3BV,MAAA;IAAY,OAAA,EAAS,aAAA;IAAe,QAAA,EAAU,MAAA,SAAe,aAAA;EAAA;EA0B7D;EAxBA,IAAA,IAAQ,OAAA;AAAA;;;;;;KAsBE,qBAAA,IACV,MAAA,EAAQ,uBAAA,EACR,OAAA,EAAS,eAAA,CAAA,OAAA,KACN,OAAA,CAAQ,cAAA;;;;;;;;UASI,cAAA;;EAEf,QAAA;;EAEA,IAAA,IAAQ,OAAA;;;;;;;EAOR,OAAA,EAAS,EAAA;;;;;;;;;;;EAWT,aAAA,EACE,EAAA,GAAK,KAAA;AAAA"}