@copilotkit/runtime 1.63.2 → 1.64.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.
- package/dist/agent/index.cjs +19 -3
- package/dist/agent/index.cjs.map +1 -1
- package/dist/agent/index.d.cts +3 -1
- package/dist/agent/index.d.cts.map +1 -1
- package/dist/agent/index.d.mts +3 -1
- package/dist/agent/index.d.mts.map +1 -1
- package/dist/agent/index.mjs +19 -3
- package/dist/agent/index.mjs.map +1 -1
- package/dist/package.cjs +2 -2
- package/dist/runtime/package.mjs +2 -2
- package/dist/service-adapters/anthropic/anthropic-adapter.cjs +2 -2
- package/dist/service-adapters/anthropic/anthropic-adapter.cjs.map +1 -1
- package/dist/service-adapters/anthropic/anthropic-adapter.d.cts.map +1 -1
- package/dist/service-adapters/anthropic/anthropic-adapter.d.mts.map +1 -1
- package/dist/service-adapters/anthropic/anthropic-adapter.mjs +3 -3
- package/dist/service-adapters/anthropic/anthropic-adapter.mjs.map +1 -1
- package/dist/service-adapters/anthropic/utils.cjs +20 -0
- package/dist/service-adapters/anthropic/utils.cjs.map +1 -1
- package/dist/service-adapters/anthropic/utils.mjs +20 -1
- package/dist/service-adapters/anthropic/utils.mjs.map +1 -1
- package/dist/v2/node.d.cts +2 -2
- package/dist/v2/node.d.mts +2 -2
- package/dist/v2/runtime/core/channel-manager.cjs +125 -56
- package/dist/v2/runtime/core/channel-manager.cjs.map +1 -1
- package/dist/v2/runtime/core/channel-manager.d.cts +20 -9
- package/dist/v2/runtime/core/channel-manager.d.cts.map +1 -1
- package/dist/v2/runtime/core/channel-manager.d.mts +20 -9
- package/dist/v2/runtime/core/channel-manager.d.mts.map +1 -1
- package/dist/v2/runtime/core/channel-manager.mjs +125 -56
- package/dist/v2/runtime/core/channel-manager.mjs.map +1 -1
- package/dist/v2/runtime/endpoints/express.cjs.map +1 -1
- package/dist/v2/runtime/endpoints/express.d.cts +5 -4
- package/dist/v2/runtime/endpoints/express.d.cts.map +1 -1
- package/dist/v2/runtime/endpoints/express.d.mts +5 -4
- package/dist/v2/runtime/endpoints/express.d.mts.map +1 -1
- package/dist/v2/runtime/endpoints/express.mjs.map +1 -1
- package/dist/v2/runtime/endpoints/hono.cjs.map +1 -1
- package/dist/v2/runtime/endpoints/hono.d.cts +5 -4
- package/dist/v2/runtime/endpoints/hono.d.cts.map +1 -1
- package/dist/v2/runtime/endpoints/hono.d.mts +5 -4
- package/dist/v2/runtime/endpoints/hono.d.mts.map +1 -1
- package/dist/v2/runtime/endpoints/hono.mjs.map +1 -1
- package/dist/v2/runtime/endpoints/node.cjs +0 -26
- package/dist/v2/runtime/endpoints/node.cjs.map +1 -1
- package/dist/v2/runtime/endpoints/node.d.cts +54 -6
- package/dist/v2/runtime/endpoints/node.d.cts.map +1 -1
- package/dist/v2/runtime/endpoints/node.d.mts +54 -6
- package/dist/v2/runtime/endpoints/node.d.mts.map +1 -1
- package/dist/v2/runtime/endpoints/node.mjs +0 -26
- package/dist/v2/runtime/endpoints/node.mjs.map +1 -1
- package/dist/v2/runtime/intelligence-platform/client.cjs +73 -2
- package/dist/v2/runtime/intelligence-platform/client.cjs.map +1 -1
- package/dist/v2/runtime/intelligence-platform/client.d.cts +51 -25
- package/dist/v2/runtime/intelligence-platform/client.d.cts.map +1 -1
- package/dist/v2/runtime/intelligence-platform/client.d.mts +51 -25
- package/dist/v2/runtime/intelligence-platform/client.d.mts.map +1 -1
- package/dist/v2/runtime/intelligence-platform/client.mjs +73 -2
- package/dist/v2/runtime/intelligence-platform/client.mjs.map +1 -1
- package/dist/v2/runtime/node.d.cts +1 -1
- package/dist/v2/runtime/node.d.mts +1 -1
- package/dist/v2/runtime/open-generative-ui-middleware.cjs +4 -1
- package/dist/v2/runtime/open-generative-ui-middleware.cjs.map +1 -1
- package/dist/v2/runtime/open-generative-ui-middleware.mjs +4 -1
- package/dist/v2/runtime/open-generative-ui-middleware.mjs.map +1 -1
- package/package.json +7 -7
- package/skills/runtime/references/intelligence-mode.md +50 -17
|
@@ -105,9 +105,16 @@ function withTimeout(inner, timeoutMs, timeoutMessage) {
|
|
|
105
105
|
});
|
|
106
106
|
}
|
|
107
107
|
/**
|
|
108
|
-
* Drives
|
|
109
|
-
*
|
|
110
|
-
*
|
|
108
|
+
* Drives Channel activation for an Intelligence runtime: lazily activates each
|
|
109
|
+
* declared Channel, tracks per-Channel lifecycle status, exposes readiness, and
|
|
110
|
+
* tears everything down. A MANAGED Channel (empty `adapters`) is activated
|
|
111
|
+
* through the injected engine over the Intelligence gateway; a DIRECT Channel
|
|
112
|
+
* (developer-supplied adapter) is started through its own transport seam
|
|
113
|
+
* (`channel.ɵruntime.start()`) — driven by the manager, but running only its own
|
|
114
|
+
* adapter transport, not the gateway path (direct Channels stay below the
|
|
115
|
+
* canonical/reliability layer by design — see {@link ChannelStatus} and OSS-599).
|
|
116
|
+
* Its very existence means Intelligence is configured, so there is no
|
|
117
|
+
* standalone/self-started path.
|
|
111
118
|
*
|
|
112
119
|
* Activation is lazy and idempotent — constructing the manager does nothing;
|
|
113
120
|
* {@link activate} starts it and a second call is a no-op. Activation throws
|
|
@@ -159,13 +166,7 @@ var ChannelManager = class {
|
|
|
159
166
|
this.activated = true;
|
|
160
167
|
for (const channel of this.channels) {
|
|
161
168
|
if (channel.adapters.some((a) => !a.__intelligenceChannel)) {
|
|
162
|
-
this.
|
|
163
|
-
this.entries.set(channel.name, {
|
|
164
|
-
status: "unmanaged",
|
|
165
|
-
handle: void 0,
|
|
166
|
-
handleStopped: false,
|
|
167
|
-
settled: Promise.resolve()
|
|
168
|
-
});
|
|
169
|
+
this.startDirectChannel(channel);
|
|
169
170
|
continue;
|
|
170
171
|
}
|
|
171
172
|
const name = channel.name;
|
|
@@ -225,6 +226,76 @@ var ChannelManager = class {
|
|
|
225
226
|
}
|
|
226
227
|
}
|
|
227
228
|
/**
|
|
229
|
+
* Start a DIRECT-adapter Channel through its own transport seam
|
|
230
|
+
* ({@link Channel.ɵruntime}`.start()`), recording a live entry so
|
|
231
|
+
* {@link ready}/{@link status}/{@link stop} all cover it. A direct Channel is
|
|
232
|
+
* driven by the manager — but only because the Intelligence runtime constructed
|
|
233
|
+
* this manager at all — and runs its OWN adapter transport, NOT the Intelligence
|
|
234
|
+
* gateway path. It is deliberately NOT wired into the gateway/canonical/
|
|
235
|
+
* reliability layer — that boundary is permanent, not a deferral (OSS-599).
|
|
236
|
+
*
|
|
237
|
+
* Mirrors the managed path's settle machinery so teardown resilience is shared:
|
|
238
|
+
* the entry's `handle` is a synthetic {@link ChannelsHandle} whose `stop()`
|
|
239
|
+
* calls `channel.ɵruntime.stop()`, so the SAME idempotent, bounded, resilient
|
|
240
|
+
* {@link stopEntry} that tears down a managed handle tears down a direct Channel
|
|
241
|
+
* too. The handle is assigned only AFTER `start()` resolves (exactly as the
|
|
242
|
+
* managed path assigns its handle only on resolve), so a `stop()` during a
|
|
243
|
+
* still-starting direct Channel returns promptly with nothing to stop and the
|
|
244
|
+
* post-settle guard tears down the late transport. A direct Channel exposes no
|
|
245
|
+
* managed-session drop signal, so no connection observer is wired and it never
|
|
246
|
+
* reaches `reconnecting`.
|
|
247
|
+
*
|
|
248
|
+
* @param channel - The direct-adapter Channel to start.
|
|
249
|
+
*/
|
|
250
|
+
startDirectChannel(channel) {
|
|
251
|
+
const name = channel.name;
|
|
252
|
+
this.log?.(`channel "${name}" carries a direct adapter — starting its own transport via channel.ɵruntime.start() (the Intelligence runtime drives its lifecycle; it runs its own adapter transport, NOT the managed gateway path — direct Channels stay below the canonical/reliability layer by design (OSS-599); managed+direct coexistence deferred (OSS-484))`);
|
|
253
|
+
let resolveSettled;
|
|
254
|
+
let rejectSettled;
|
|
255
|
+
const settled = new Promise((resolve, reject) => {
|
|
256
|
+
resolveSettled = resolve;
|
|
257
|
+
rejectSettled = reject;
|
|
258
|
+
});
|
|
259
|
+
settled.catch(() => {});
|
|
260
|
+
const directHandle = {
|
|
261
|
+
metadata: {},
|
|
262
|
+
stop: () => channel.ɵruntime.stop()
|
|
263
|
+
};
|
|
264
|
+
let activation;
|
|
265
|
+
try {
|
|
266
|
+
activation = channel.ɵruntime.start();
|
|
267
|
+
} catch (err) {
|
|
268
|
+
activation = Promise.reject(err);
|
|
269
|
+
}
|
|
270
|
+
const entry = {
|
|
271
|
+
status: "connecting",
|
|
272
|
+
handle: void 0,
|
|
273
|
+
handleStopped: false,
|
|
274
|
+
settled
|
|
275
|
+
};
|
|
276
|
+
activation.then(async () => {
|
|
277
|
+
entry.handle = directHandle;
|
|
278
|
+
if (this.stopped) {
|
|
279
|
+
await this.stopEntry(entry);
|
|
280
|
+
resolveSettled();
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
entry.status = "online";
|
|
284
|
+
resolveSettled();
|
|
285
|
+
}, async (err) => {
|
|
286
|
+
if (this.stopped) {
|
|
287
|
+
entry.handle = directHandle;
|
|
288
|
+
await this.stopEntry(entry);
|
|
289
|
+
resolveSettled();
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
entry.status = "error";
|
|
293
|
+
this.log?.(`channel "${name}" failed to start its direct transport`, err);
|
|
294
|
+
rejectSettled(err);
|
|
295
|
+
}).catch(() => {});
|
|
296
|
+
this.entries.set(name, entry);
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
228
299
|
* Throw if two declared Channels share a `name`. `entries` is keyed by name,
|
|
229
300
|
* so a duplicate would overwrite the first Channel's entry and leak its live
|
|
230
301
|
* session. Called at the very start of {@link activate}, before any engine
|
|
@@ -243,12 +314,12 @@ var ChannelManager = class {
|
|
|
243
314
|
}
|
|
244
315
|
}
|
|
245
316
|
/**
|
|
246
|
-
* Resolve when every
|
|
317
|
+
* Resolve when every declared Channel — managed OR direct — has settled to
|
|
318
|
+
* `online`/`setup_required`.
|
|
247
319
|
*
|
|
248
|
-
* A direct-adapter
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
* `unmanaged`, never `online`), not in `ready()` resolving.
|
|
320
|
+
* A direct-adapter Channel is awaited too: its `settled` resolves once its own
|
|
321
|
+
* transport is up (`channel.ɵruntime.start()` settling) and rejects if that
|
|
322
|
+
* start fails, exactly as a managed Channel's `settled` tracks its activation.
|
|
252
323
|
*
|
|
253
324
|
* Activates lazily if not already started — so a first call rejects with the
|
|
254
325
|
* same {@link ChannelConfigError} as the synchronous throw from
|
|
@@ -282,22 +353,18 @@ var ChannelManager = class {
|
|
|
282
353
|
if (errors.length > 0) throw new AggregateError(errors, `ChannelManager.ready: ${errors.length} channel(s) failed to activate or settle in time`);
|
|
283
354
|
}
|
|
284
355
|
/**
|
|
285
|
-
* Snapshot status. Every declared Channel — managed OR direct
|
|
286
|
-
*
|
|
287
|
-
*
|
|
356
|
+
* Snapshot status. Every declared Channel — managed OR direct — appears keyed
|
|
357
|
+
* by name in `channels`; a direct-adapter Channel reads `online` once its own
|
|
358
|
+
* transport is up, exactly like a managed one.
|
|
288
359
|
*
|
|
289
|
-
* `overall` is folded over
|
|
360
|
+
* `overall` is folded over ALL declared Channels (see {@link computeOverall}),
|
|
290
361
|
* by precedence `error` > `reconnecting` > `setup_required` > `connecting` >
|
|
291
|
-
* `online`. `online` means every
|
|
292
|
-
*
|
|
293
|
-
*
|
|
294
|
-
*
|
|
295
|
-
*
|
|
296
|
-
*
|
|
297
|
-
* stays visible per-Channel. When every declared Channel is `unmanaged`,
|
|
298
|
-
* `overall` is `unmanaged` (NOT `online`). With no declared Channels at all,
|
|
299
|
-
* `overall` is `online` (nothing is degraded); once every managed Channel has
|
|
300
|
-
* been stopped, `overall` is `stopped`.
|
|
362
|
+
* `online`. `online` means every Channel can currently send. `reconnecting`
|
|
363
|
+
* outranks `setup_required` because a dropped-but-retrying Channel is an active
|
|
364
|
+
* outage, louder than a steadily-degraded unprovisioned one (only managed
|
|
365
|
+
* Channels ever reach `reconnecting`; a direct Channel has no managed-session
|
|
366
|
+
* drop signal). With no declared Channels at all, `overall` is `online` (nothing
|
|
367
|
+
* is degraded); once every Channel has been stopped, `overall` is `stopped`.
|
|
301
368
|
*/
|
|
302
369
|
status() {
|
|
303
370
|
const channels = {};
|
|
@@ -318,25 +385,22 @@ var ChannelManager = class {
|
|
|
318
385
|
/**
|
|
319
386
|
* Fold per-Channel statuses into a single overall status (see {@link status}).
|
|
320
387
|
*
|
|
321
|
-
*
|
|
322
|
-
*
|
|
323
|
-
* outage
|
|
388
|
+
* Every declared Channel — managed OR direct — participates: a started direct
|
|
389
|
+
* Channel reads `online` and counts toward health exactly like a managed one,
|
|
390
|
+
* and its `error` is a real outage that must dominate. Statuses are ranked
|
|
324
391
|
* `error` > `reconnecting` > `setup_required` > `connecting` > `online`, so a
|
|
325
|
-
* genuine
|
|
326
|
-
*
|
|
327
|
-
* declared
|
|
328
|
-
*
|
|
329
|
-
* stays `online` (nothing is degraded).
|
|
392
|
+
* genuine failure still dominates a healthy sibling. (Only managed Channels ever
|
|
393
|
+
* reach `reconnecting`; a direct Channel has no managed-session drop signal.)
|
|
394
|
+
* The empty-input case (no declared Channels at all) stays `online` (nothing is
|
|
395
|
+
* degraded).
|
|
330
396
|
*/
|
|
331
397
|
computeOverall(values) {
|
|
332
398
|
if (values.length === 0) return "online";
|
|
333
|
-
|
|
334
|
-
if (
|
|
335
|
-
if (
|
|
336
|
-
if (
|
|
337
|
-
if (
|
|
338
|
-
if (managed.includes("setup_required")) return "setup_required";
|
|
339
|
-
if (managed.includes("connecting")) return "connecting";
|
|
399
|
+
if (values.every((v) => v === "stopped")) return "stopped";
|
|
400
|
+
if (values.includes("error")) return "error";
|
|
401
|
+
if (values.includes("reconnecting")) return "reconnecting";
|
|
402
|
+
if (values.includes("setup_required")) return "setup_required";
|
|
403
|
+
if (values.includes("connecting")) return "connecting";
|
|
340
404
|
return "online";
|
|
341
405
|
}
|
|
342
406
|
/**
|
|
@@ -346,7 +410,8 @@ var ChannelManager = class {
|
|
|
346
410
|
*
|
|
347
411
|
* - `reconnecting` → status `reconnecting` (dropped, Phoenix retrying);
|
|
348
412
|
* - `online` → status `online` (rejoined, sendable again);
|
|
349
|
-
* - `gave_up` → status `error` (dead after the bounded reconnect window)
|
|
413
|
+
* - `gave_up` → status `error` (dead after the bounded reconnect window);
|
|
414
|
+
* - `fenced` → status `error` immediately (another activation superseded it).
|
|
350
415
|
*
|
|
351
416
|
* Makes NO re-activation — reconnection is delegated to the Phoenix connection
|
|
352
417
|
* layer (see {@link ChannelManager}), which auto-rejoins under the persistent
|
|
@@ -366,9 +431,12 @@ var ChannelManager = class {
|
|
|
366
431
|
} else if (state === "online") {
|
|
367
432
|
entry.status = "online";
|
|
368
433
|
this.log?.(`channel "${name}" managed session back online`);
|
|
369
|
-
} else {
|
|
434
|
+
} else if (state === "gave_up") {
|
|
370
435
|
entry.status = "error";
|
|
371
436
|
this.log?.(`channel "${name}" managed session gave up reconnecting; marking error`);
|
|
437
|
+
} else {
|
|
438
|
+
entry.status = "error";
|
|
439
|
+
this.log?.(`channel "${name}" managed session was generation-fenced by a replacement; marking error`);
|
|
372
440
|
}
|
|
373
441
|
});
|
|
374
442
|
}
|
|
@@ -379,10 +447,14 @@ var ChannelManager = class {
|
|
|
379
447
|
* not-yet-stopped handle (gated by {@link ChannelEntry.handleStopped}).
|
|
380
448
|
*
|
|
381
449
|
* This is the ONE guarded teardown path shared by both `stop()` and the
|
|
382
|
-
* post-settle guard in {@link activate}. Because the
|
|
383
|
-
* idempotent, a handle assigned in the same tick as
|
|
384
|
-
* exactly once even when both callers reach the entry, and a
|
|
385
|
-
* never resurrect a `stopped` entry.
|
|
450
|
+
* post-settle guard in {@link activate}/{@link startDirectChannel}. Because the
|
|
451
|
+
* guard is per-entry and idempotent, a handle assigned in the same tick as
|
|
452
|
+
* `stop()` is stopped exactly once even when both callers reach the entry, and a
|
|
453
|
+
* late settle can never resurrect a `stopped` entry. It is transport-agnostic: a
|
|
454
|
+
* managed entry's `handle.stop()` releases the gateway session, and a direct
|
|
455
|
+
* entry's synthetic handle (assigned in {@link startDirectChannel}) routes the
|
|
456
|
+
* same `handle.stop()` to `channel.ɵruntime.stop()` — so direct and managed
|
|
457
|
+
* Channels share one bounded, resilient teardown.
|
|
386
458
|
*
|
|
387
459
|
* `handle.stop()` failures are logged (via {@link ChannelManager.log}) but NOT
|
|
388
460
|
* rethrown: the real launcher's `stop()` rethrows after `session.disconnect()`,
|
|
@@ -392,11 +464,9 @@ var ChannelManager = class {
|
|
|
392
464
|
* `.catch` — otherwise the sync throw would escape, skip `resolveSettled()` in
|
|
393
465
|
* the fulfilled-then-stopped branch of {@link activate}, and hang `settled`.
|
|
394
466
|
*
|
|
395
|
-
* An
|
|
396
|
-
*
|
|
397
|
-
*
|
|
398
|
-
* The developer's `channel.start()`/stop path is unaffected by manager
|
|
399
|
-
* teardown.
|
|
467
|
+
* An entry with no handle yet (a still-`connecting` Channel whose transport has
|
|
468
|
+
* not come up) is only marked `stopped`: there is nothing to tear down, and the
|
|
469
|
+
* post-settle guard releases the transport if it arrives after `stop()`.
|
|
400
470
|
*
|
|
401
471
|
* A WEDGED `handle.stop()` (one that never settles) is bounded by
|
|
402
472
|
* {@link ChannelManagerArgs.stopHandleTimeoutMs}: after the deadline the call
|
|
@@ -406,7 +476,6 @@ var ChannelManager = class {
|
|
|
406
476
|
* @param entry - The Channel entry to stop.
|
|
407
477
|
*/
|
|
408
478
|
async stopEntry(entry) {
|
|
409
|
-
if (entry.status === "unmanaged") return;
|
|
410
479
|
entry.status = "stopped";
|
|
411
480
|
if (entry.handle && !entry.handleStopped) {
|
|
412
481
|
entry.handleStopped = true;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"channel-manager.mjs","names":[],"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/**\n * Runtime installs this pure-ESM package as a direct dependency, but the\n * specifier must stay non-literal so it never becomes a static dependency of\n * the runtime's CJS build. The packed-consumer contract is enforced by\n * `scripts/release/verify-runtime-package.ts`.\n */\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":"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"}
|
|
1
|
+
{"version":3,"file":"channel-manager.mjs","names":[],"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 * - `error`: activation rejected with a non-setup error, OR a previously-online\n * session gave up reconnecting after its bounded reconnect window or was\n * generation-fenced by a replacement.\n *\n * Both MANAGED (Intelligence-gateway) and DIRECT (developer-supplied adapter)\n * Channels move through these states. A direct Channel is driven by the manager\n * too — it is started via {@link Channel.ɵruntime}`.start()` and reaches `online`\n * once its own transport is up — but it is NOT wired into the Intelligence\n * gateway/canonical/reliability layer: it simply runs its own adapter transport,\n * started and stopped by the manager. A direct Channel has no managed-session\n * drop signal, so it never reports `reconnecting`.\n *\n * That gap is BY DESIGN, not a deferral. Run-correctness (canonical\n * cross-surface history, fenced outer-run/single-terminal, durable\n * HITL-resume-across-restart, selection pinning) and the reliability layer are\n * Intelligence-side only — see OSS-599's boundary discipline. A direct Channel's\n * ceiling is the SDK's in-process run loop. Do not \"finish\" this by pulling the\n * canonical/reliability layer down into the SDK: that is explicitly the thing\n * OSS-599 forbids.\n */\nexport type ChannelStatus =\n | \"connecting\"\n | \"online\"\n | \"setup_required\"\n | \"reconnecting\"\n | \"stopped\"\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\" | \"fenced\") => 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/**\n * Runtime installs this pure-ESM package as a direct dependency, but the\n * specifier must stay non-literal so it never becomes a static dependency of\n * the runtime's CJS build. The packed-consumer contract is enforced by\n * `scripts/release/verify-runtime-package.ts`.\n */\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 Channel activation for an Intelligence runtime: lazily activates each\n * declared Channel, tracks per-Channel lifecycle status, exposes readiness, and\n * tears everything down. A MANAGED Channel (empty `adapters`) is activated\n * through the injected engine over the Intelligence gateway; a DIRECT Channel\n * (developer-supplied adapter) is started through its own transport seam\n * (`channel.ɵruntime.start()`) — driven by the manager, but running only its own\n * adapter transport, not the gateway path (direct Channels stay below the\n * canonical/reliability layer by design — see {@link ChannelStatus} and OSS-599).\n * Its very existence means Intelligence is configured, so there is no\n * standalone/self-started path.\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. The manager still owns its lifecycle — but a direct Channel\n // runs its OWN adapter transport rather than the Intelligence gateway path:\n // it is started here via `channel.ɵruntime.start()` (not the managed engine)\n // and torn down via `channel.ɵruntime.stop()`. The distinction is EXCLUSIVE\n // PER CHANNEL, not per platform — a Channel served by a direct adapter is not\n // also managed: ANY direct adapter makes the WHOLE Channel direct, regardless\n // of platform. Attaching the managed adapter alongside a direct one would\n // double-deliver every turn (and trip the SDK's `assertExclusive` guard,\n // moving the Channel to `error`). Per the SoT rule, never infer managed intent\n // from a local direct adapter — a managed-eligible Channel has an empty\n // `adapters` at declaration time. Managed+direct coexistence on the same\n // Channel is NOT supported today; it is deferred (OSS-484). Direct Channels\n // run only their own transport here and stay below the Intelligence\n // canonical/reliability layer by design, not pending work (OSS-599).\n for (const channel of this.channels) {\n const isDirect = channel.adapters.some((a) => !a.__intelligenceChannel);\n if (isDirect) {\n this.startDirectChannel(channel);\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 * Start a DIRECT-adapter Channel through its own transport seam\n * ({@link Channel.ɵruntime}`.start()`), recording a live entry so\n * {@link ready}/{@link status}/{@link stop} all cover it. A direct Channel is\n * driven by the manager — but only because the Intelligence runtime constructed\n * this manager at all — and runs its OWN adapter transport, NOT the Intelligence\n * gateway path. It is deliberately NOT wired into the gateway/canonical/\n * reliability layer — that boundary is permanent, not a deferral (OSS-599).\n *\n * Mirrors the managed path's settle machinery so teardown resilience is shared:\n * the entry's `handle` is a synthetic {@link ChannelsHandle} whose `stop()`\n * calls `channel.ɵruntime.stop()`, so the SAME idempotent, bounded, resilient\n * {@link stopEntry} that tears down a managed handle tears down a direct Channel\n * too. The handle is assigned only AFTER `start()` resolves (exactly as the\n * managed path assigns its handle only on resolve), so a `stop()` during a\n * still-starting direct Channel returns promptly with nothing to stop and the\n * post-settle guard tears down the late transport. A direct Channel exposes no\n * managed-session drop signal, so no connection observer is wired and it never\n * reaches `reconnecting`.\n *\n * @param channel - The direct-adapter Channel to start.\n */\n private startDirectChannel(channel: Channel): void {\n const name = channel.name!;\n this.log?.(\n `channel \"${name}\" carries a direct adapter — starting its own transport via channel.ɵruntime.start() (the Intelligence runtime drives its lifecycle; it runs its own adapter transport, NOT the managed gateway path — direct Channels stay below the canonical/reliability layer by design (OSS-599); managed+direct coexistence deferred (OSS-484))`,\n );\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`; attach a no-op catch so a rejection is always\n // considered handled (ready() still sees the reason).\n settled.catch(() => {});\n\n // Synthetic handle wrapping the Channel's own stop seam. Assigned to the\n // entry only on successful start (below) so the shared teardown machinery\n // (handleStopped guard, withTimeout bounding, resilient allSettled) stops a\n // direct Channel exactly as it stops a managed handle.\n const directHandle: ChannelsHandle = {\n metadata: {},\n stop: () => channel.ɵruntime.stop(),\n };\n\n // Start the direct transport synchronously so it is observably started the\n // moment activate() returns (callers see `connecting` before awaiting ready).\n // A synchronous throw becomes this Channel's status rather than throwing out\n // of activate().\n let activation: Promise<void>;\n try {\n activation = channel.ɵruntime.start();\n } catch (err) {\n activation = Promise.reject(err);\n }\n\n const entry: ChannelEntry = {\n status: \"connecting\",\n handle: undefined,\n handleStopped: false,\n settled,\n };\n\n activation\n .then(\n async () => {\n entry.handle = directHandle;\n if (this.stopped) {\n // stop() ran before start() settled, so it could not tear down a\n // transport that was not up yet. Release it now (idempotent) and keep\n // the Channel `stopped`.\n await this.stopEntry(entry);\n resolveSettled();\n return;\n }\n entry.status = \"online\";\n resolveSettled();\n },\n async (err: unknown) => {\n if (this.stopped) {\n // A start rejection that arrives AFTER stop() must NOT resurrect the\n // entry into `error`: the Channel is already being torn down. Keep it\n // `stopped` and resolve `settled` so a later ready() does not reject.\n entry.handle = directHandle;\n await this.stopEntry(entry);\n resolveSettled();\n return;\n }\n entry.status = \"error\";\n this.log?.(\n `channel \"${name}\" failed to start its direct transport`,\n err,\n );\n rejectSettled(err);\n },\n )\n .catch(() => {});\n\n this.entries.set(name, entry);\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 declared Channel — managed OR direct — has settled to\n * `online`/`setup_required`.\n *\n * A direct-adapter Channel is awaited too: its `settled` resolves once its own\n * transport is up (`channel.ɵruntime.start()` settling) and rejects if that\n * start fails, exactly as a managed Channel's `settled` tracks its activation.\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 — appears keyed\n * by name in `channels`; a direct-adapter Channel reads `online` once its own\n * transport is up, exactly like a managed one.\n *\n * `overall` is folded over ALL declared Channels (see {@link computeOverall}),\n * by precedence `error` > `reconnecting` > `setup_required` > `connecting` >\n * `online`. `online` means every Channel can currently send. `reconnecting`\n * outranks `setup_required` because a dropped-but-retrying Channel is an active\n * outage, louder than a steadily-degraded unprovisioned one (only managed\n * Channels ever reach `reconnecting`; a direct Channel has no managed-session\n * drop signal). With no declared Channels at all, `overall` is `online` (nothing\n * is degraded); once every Channel has 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 * Every declared Channel — managed OR direct — participates: a started direct\n * Channel reads `online` and counts toward health exactly like a managed one,\n * and its `error` is a real outage that must dominate. Statuses are ranked\n * `error` > `reconnecting` > `setup_required` > `connecting` > `online`, so a\n * genuine failure still dominates a healthy sibling. (Only managed Channels ever\n * reach `reconnecting`; a direct Channel has no managed-session drop signal.)\n * The empty-input case (no declared Channels at all) stays `online` (nothing is\n * degraded).\n */\n private computeOverall(values: ChannelStatus[]): ChannelStatus {\n if (values.length === 0) {\n return \"online\";\n }\n if (values.every((v) => v === \"stopped\")) {\n return \"stopped\";\n }\n if (values.includes(\"error\")) {\n return \"error\";\n }\n if (values.includes(\"reconnecting\")) {\n return \"reconnecting\";\n }\n if (values.includes(\"setup_required\")) {\n return \"setup_required\";\n }\n if (values.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 * - `fenced` → status `error` immediately (another activation superseded it).\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 if (state === \"gave_up\") {\n entry.status = \"error\";\n this.log?.(\n `channel \"${name}\" managed session gave up reconnecting; marking error`,\n );\n } else {\n entry.status = \"error\";\n this.log?.(\n `channel \"${name}\" managed session was generation-fenced by a replacement; 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}/{@link startDirectChannel}. Because the\n * guard is per-entry and idempotent, a handle assigned in the same tick as\n * `stop()` is stopped exactly once even when both callers reach the entry, and a\n * late settle can never resurrect a `stopped` entry. It is transport-agnostic: a\n * managed entry's `handle.stop()` releases the gateway session, and a direct\n * entry's synthetic handle (assigned in {@link startDirectChannel}) routes the\n * same `handle.stop()` to `channel.ɵruntime.stop()` — so direct and managed\n * Channels share one bounded, resilient teardown.\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 entry with no handle yet (a still-`connecting` Channel whose transport has\n * not come up) is only marked `stopped`: there is nothing to tear down, and the\n * post-settle guard releases the transport if it arrives after `stop()`.\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 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":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"express.cjs","names":["createCopilotRuntimeHandler","createExpressNodeHandler"],"sources":["../../../../src/v2/runtime/endpoints/express.ts"],"sourcesContent":["import express from \"express\";\nimport type {\n Request as ExpressRequest,\n Response as ExpressResponse,\n NextFunction,\n Router,\n} from \"express\";\nimport cors from \"cors\";\nimport type { CorsOptions } from \"cors\";\nimport type { CopilotRuntimeLike } from \"../core/runtime\";\nimport { createCopilotRuntimeHandler } from \"../core/fetch-handler\";\nimport type {\n ActivateChannelEngine,\n ChannelsControl,\n} from \"../core/channel-manager\";\nimport { createExpressNodeHandler } from \"./express-fetch-bridge\";\nimport type { CopilotRuntimeHooks } from \"../core/hooks\";\n\n/**\n * An Express {@link Router} that may also carry an optional\n * {@link ChannelsControl} surface. Express's Router is a request-scoped\n * middleware object, not a long-running process owner — Node\n * (`createCopilotNodeListener`) is the lifecycle-owning surface for\n * `.channels`. It is attached here too, best-effort, for callers that mount\n * the router directly and want to observe
|
|
1
|
+
{"version":3,"file":"express.cjs","names":["createCopilotRuntimeHandler","createExpressNodeHandler"],"sources":["../../../../src/v2/runtime/endpoints/express.ts"],"sourcesContent":["import express from \"express\";\nimport type {\n Request as ExpressRequest,\n Response as ExpressResponse,\n NextFunction,\n Router,\n} from \"express\";\nimport cors from \"cors\";\nimport type { CorsOptions } from \"cors\";\nimport type { CopilotRuntimeLike } from \"../core/runtime\";\nimport { createCopilotRuntimeHandler } from \"../core/fetch-handler\";\nimport type {\n ActivateChannelEngine,\n ChannelsControl,\n} from \"../core/channel-manager\";\nimport { createExpressNodeHandler } from \"./express-fetch-bridge\";\nimport type { CopilotRuntimeHooks } from \"../core/hooks\";\n\n/**\n * An Express {@link Router} that may also carry an optional\n * {@link ChannelsControl} surface. Express's Router is a request-scoped\n * middleware object, not a long-running process owner — Node\n * (`createCopilotNodeListener`) is the lifecycle-owning surface for\n * `.channels`. It is attached here too, best-effort, for callers that mount\n * the router directly and want to start (`ready()`), observe, or stop managed\n * Channel activation without also standing up a Node listener.\n */\nexport type CopilotExpressRouter = Router & { channels?: ChannelsControl };\n\nexport interface CopilotExpressEndpointParams {\n runtime: CopilotRuntimeLike;\n basePath: string;\n\n /**\n * Endpoint mode.\n * - `\"multi-route\"` (default): separate routes for each operation\n * - `\"single-route\"`: single POST endpoint with JSON envelope dispatch\n */\n mode?: \"multi-route\" | \"single-route\";\n\n /**\n * CORS configuration for the Express router.\n * - `true` (default): permissive CORS (`origin: \"*\"`, all methods, all headers).\n * - `false`: no CORS middleware is applied — handle it yourself.\n * - object: passed directly to the Express `cors()` middleware.\n */\n cors?: boolean | CorsOptions;\n\n /**\n * Lifecycle hooks for request processing.\n */\n hooks?: CopilotRuntimeHooks;\n\n /**\n * Whether the underlying handler builds the control surface for the runtime's\n * declared managed Channels. Defaults to `true`. Building it opens no\n * connection — activation is deferred to the first `channels.ready()`. See\n * `CopilotRuntimeHandlerOptions.activateChannels`.\n */\n activateChannels?: boolean;\n\n /**\n * @internal Test seam: inject a fake Channel activation engine. Forwarded\n * to `createCopilotRuntimeHandler`. Not part of the public API.\n */\n __channelEngine?: ActivateChannelEngine;\n}\n\n/**\n * Creates an Express router that serves the CopilotKit runtime.\n *\n * In **multi-route** mode (default) the router exposes:\n * - `GET {basePath}/info` — runtime info\n * - `POST {basePath}/agent/:agentId/run` — start an agent run\n * - `POST {basePath}/agent/:agentId/connect` — connect to an agent run\n * - `POST {basePath}/agent/:agentId/stop/:threadId` — stop an agent run\n * - `POST {basePath}/transcribe` — transcribe audio\n *\n * In **single-route** mode a single `POST {basePath}` endpoint accepts a JSON\n * envelope `{ method, params, body }` and dispatches to the appropriate handler.\n *\n * @example\n * ```typescript\n * import express from \"express\";\n * import { CopilotRuntime } from \"@copilotkit/runtime/v2\";\n * import { createCopilotExpressHandler } from \"@copilotkit/runtime/v2/express\";\n *\n * const runtime = new CopilotRuntime({\n * agents: { default: new BuiltInAgent({ model: \"openai/gpt-4o-mini\" }) },\n * });\n *\n * const app = express();\n * app.use(createCopilotExpressHandler({\n * runtime,\n * basePath: \"/api/copilotkit\",\n * cors: true,\n * }));\n * app.listen(4000);\n * ```\n *\n * @example Single-route mode with lifecycle hooks\n * ```typescript\n * app.use(createCopilotExpressHandler({\n * runtime,\n * basePath: \"/api/copilotkit\",\n * mode: \"single-route\",\n * hooks: {\n * onRequest: ({ request }) => {\n * if (!request.headers.get(\"authorization\")) {\n * throw new Response(\"Unauthorized\", { status: 401 });\n * }\n * },\n * },\n * }));\n * ```\n */\n/** @deprecated Use `createCopilotExpressHandler` instead. */\nexport { createCopilotExpressHandler as createCopilotEndpointExpress };\n\nexport function createCopilotExpressHandler({\n runtime,\n basePath,\n mode = \"multi-route\",\n cors: corsOption = true,\n hooks,\n activateChannels,\n __channelEngine,\n}: CopilotExpressEndpointParams): CopilotExpressRouter {\n const normalizedBase = normalizeBasePath(basePath);\n\n const handler = createCopilotRuntimeHandler({\n runtime,\n basePath: normalizedBase,\n mode,\n cors: false, // CORS is handled at the Express middleware layer\n hooks,\n activateChannels,\n __channelEngine,\n });\n\n const nodeHandler = createExpressNodeHandler(handler);\n\n const expressHandler = async (\n req: ExpressRequest,\n res: ExpressResponse,\n next: NextFunction,\n ) => {\n try {\n await nodeHandler(req, res);\n } catch (err) {\n next(err);\n }\n };\n\n const router = express.Router();\n\n // CORS middleware\n if (corsOption) {\n const corsConfig: CorsOptions =\n corsOption === true\n ? {\n origin: \"*\",\n methods: [\n \"GET\",\n \"HEAD\",\n \"PUT\",\n \"POST\",\n \"DELETE\",\n \"PATCH\",\n \"OPTIONS\",\n ],\n allowedHeaders: [\"*\"],\n }\n : corsOption;\n router.use(cors(corsConfig));\n }\n\n // Route mounting\n if (mode === \"single-route\") {\n router.post(normalizedBase, expressHandler);\n router.options(normalizedBase, expressHandler);\n } else if (normalizedBase === \"/\") {\n router.all(/.*/, expressHandler);\n } else {\n router.all(\n new RegExp(`^${escapeRegExp(normalizedBase)}(\\\\/.*)?$`),\n expressHandler,\n );\n }\n\n const exposedRouter: CopilotExpressRouter = router;\n exposedRouter.channels = handler.channels;\n return exposedRouter;\n}\n\nfunction escapeRegExp(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\nfunction normalizeBasePath(path: string): string {\n if (!path) {\n throw new Error(\"basePath must be provided for Express endpoint\");\n }\n\n if (!path.startsWith(\"/\")) {\n return `/${path}`;\n }\n\n if (path.length > 1 && path.endsWith(\"/\")) {\n return path.slice(0, -1);\n }\n\n return path;\n}\n"],"mappings":";;;;;;;;;;AAuHA,SAAgB,4BAA4B,EAC1C,SACA,UACA,OAAO,eACP,MAAM,aAAa,MACnB,OACA,kBACA,mBACqD;CACrD,MAAM,iBAAiB,kBAAkB,SAAS;CAElD,MAAM,UAAUA,kDAA4B;EAC1C;EACA,UAAU;EACV;EACA,MAAM;EACN;EACA;EACA;EACD,CAAC;CAEF,MAAM,cAAcC,sDAAyB,QAAQ;CAErD,MAAM,iBAAiB,OACrB,KACA,KACA,SACG;AACH,MAAI;AACF,SAAM,YAAY,KAAK,IAAI;WACpB,KAAK;AACZ,QAAK,IAAI;;;CAIb,MAAM,SAAS,gBAAQ,QAAQ;AAG/B,KAAI,YAAY;EACd,MAAM,aACJ,eAAe,OACX;GACE,QAAQ;GACR,SAAS;IACP;IACA;IACA;IACA;IACA;IACA;IACA;IACD;GACD,gBAAgB,CAAC,IAAI;GACtB,GACD;AACN,SAAO,sBAAS,WAAW,CAAC;;AAI9B,KAAI,SAAS,gBAAgB;AAC3B,SAAO,KAAK,gBAAgB,eAAe;AAC3C,SAAO,QAAQ,gBAAgB,eAAe;YACrC,mBAAmB,IAC5B,QAAO,IAAI,MAAM,eAAe;KAEhC,QAAO,IACL,IAAI,OAAO,IAAI,aAAa,eAAe,CAAC,WAAW,EACvD,eACD;CAGH,MAAM,gBAAsC;AAC5C,eAAc,WAAW,QAAQ;AACjC,QAAO;;AAGT,SAAS,aAAa,GAAmB;AACvC,QAAO,EAAE,QAAQ,uBAAuB,OAAO;;AAGjD,SAAS,kBAAkB,MAAsB;AAC/C,KAAI,CAAC,KACH,OAAM,IAAI,MAAM,iDAAiD;AAGnE,KAAI,CAAC,KAAK,WAAW,IAAI,CACvB,QAAO,IAAI;AAGb,KAAI,KAAK,SAAS,KAAK,KAAK,SAAS,IAAI,CACvC,QAAO,KAAK,MAAM,GAAG,GAAG;AAG1B,QAAO"}
|
|
@@ -12,8 +12,8 @@ import { CorsOptions } from "cors";
|
|
|
12
12
|
* middleware object, not a long-running process owner — Node
|
|
13
13
|
* (`createCopilotNodeListener`) is the lifecycle-owning surface for
|
|
14
14
|
* `.channels`. It is attached here too, best-effort, for callers that mount
|
|
15
|
-
* the router directly and want to observe
|
|
16
|
-
* without also standing up a Node listener.
|
|
15
|
+
* the router directly and want to start (`ready()`), observe, or stop managed
|
|
16
|
+
* Channel activation without also standing up a Node listener.
|
|
17
17
|
*/
|
|
18
18
|
type CopilotExpressRouter = Router & {
|
|
19
19
|
channels?: ChannelsControl;
|
|
@@ -39,8 +39,9 @@ interface CopilotExpressEndpointParams {
|
|
|
39
39
|
*/
|
|
40
40
|
hooks?: CopilotRuntimeHooks;
|
|
41
41
|
/**
|
|
42
|
-
* Whether the underlying handler
|
|
43
|
-
*
|
|
42
|
+
* Whether the underlying handler builds the control surface for the runtime's
|
|
43
|
+
* declared managed Channels. Defaults to `true`. Building it opens no
|
|
44
|
+
* connection — activation is deferred to the first `channels.ready()`. See
|
|
44
45
|
* `CopilotRuntimeHandlerOptions.activateChannels`.
|
|
45
46
|
*/
|
|
46
47
|
activateChannels?: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"express.d.cts","names":[],"sources":["../../../../src/v2/runtime/endpoints/express.ts"],"mappings":";;;;;;;;;;AA2BA;;;;;;;KAAY,oBAAA,GAAuB,MAAA;EAAW,QAAA,GAAW,eAAA;AAAA;AAAA,UAExC,4BAAA;EACf,OAAA,EAAS,kBAAA;EACT,QAAA;EAeiB;;;;;EARjB,IAAA;EARS;;;;;;EAgBT,IAAA,aAAiB,WAAA;
|
|
1
|
+
{"version":3,"file":"express.d.cts","names":[],"sources":["../../../../src/v2/runtime/endpoints/express.ts"],"mappings":";;;;;;;;;;AA2BA;;;;;;;KAAY,oBAAA,GAAuB,MAAA;EAAW,QAAA,GAAW,eAAA;AAAA;AAAA,UAExC,4BAAA;EACf,OAAA,EAAS,kBAAA;EACT,QAAA;EAeiB;;;;;EARjB,IAAA;EARS;;;;;;EAgBT,IAAA,aAAiB,WAAA;EAajB;;;EARA,KAAA,GAAQ,mBAAA;EAc+B;AAsDzC;;;;;EA5DE,gBAAA;EAgEM;;;;EA1DN,eAAA,GAAkB,qBAAA;AAAA;AAAA,iBAsDJ,2BAAA,CAAA;EACd,OAAA;EACA,QAAA;EACA,IAAA;EACA,IAAA,EAAM,UAAA;EACN,KAAA;EACA,gBAAA;EACA;AAAA,GACC,4BAAA,GAA+B,oBAAA"}
|
|
@@ -12,8 +12,8 @@ import { CorsOptions } from "cors";
|
|
|
12
12
|
* middleware object, not a long-running process owner — Node
|
|
13
13
|
* (`createCopilotNodeListener`) is the lifecycle-owning surface for
|
|
14
14
|
* `.channels`. It is attached here too, best-effort, for callers that mount
|
|
15
|
-
* the router directly and want to observe
|
|
16
|
-
* without also standing up a Node listener.
|
|
15
|
+
* the router directly and want to start (`ready()`), observe, or stop managed
|
|
16
|
+
* Channel activation without also standing up a Node listener.
|
|
17
17
|
*/
|
|
18
18
|
type CopilotExpressRouter = Router & {
|
|
19
19
|
channels?: ChannelsControl;
|
|
@@ -39,8 +39,9 @@ interface CopilotExpressEndpointParams {
|
|
|
39
39
|
*/
|
|
40
40
|
hooks?: CopilotRuntimeHooks;
|
|
41
41
|
/**
|
|
42
|
-
* Whether the underlying handler
|
|
43
|
-
*
|
|
42
|
+
* Whether the underlying handler builds the control surface for the runtime's
|
|
43
|
+
* declared managed Channels. Defaults to `true`. Building it opens no
|
|
44
|
+
* connection — activation is deferred to the first `channels.ready()`. See
|
|
44
45
|
* `CopilotRuntimeHandlerOptions.activateChannels`.
|
|
45
46
|
*/
|
|
46
47
|
activateChannels?: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"express.d.mts","names":[],"sources":["../../../../src/v2/runtime/endpoints/express.ts"],"mappings":";;;;;;;;;;AA2BA;;;;;;;KAAY,oBAAA,GAAuB,MAAA;EAAW,QAAA,GAAW,eAAA;AAAA;AAAA,UAExC,4BAAA;EACf,OAAA,EAAS,kBAAA;EACT,QAAA;EAeiB;;;;;EARjB,IAAA;EARS;;;;;;EAgBT,IAAA,aAAiB,WAAA;
|
|
1
|
+
{"version":3,"file":"express.d.mts","names":[],"sources":["../../../../src/v2/runtime/endpoints/express.ts"],"mappings":";;;;;;;;;;AA2BA;;;;;;;KAAY,oBAAA,GAAuB,MAAA;EAAW,QAAA,GAAW,eAAA;AAAA;AAAA,UAExC,4BAAA;EACf,OAAA,EAAS,kBAAA;EACT,QAAA;EAeiB;;;;;EARjB,IAAA;EARS;;;;;;EAgBT,IAAA,aAAiB,WAAA;EAajB;;;EARA,KAAA,GAAQ,mBAAA;EAc+B;AAsDzC;;;;;EA5DE,gBAAA;EAgEM;;;;EA1DN,eAAA,GAAkB,qBAAA;AAAA;AAAA,iBAsDJ,2BAAA,CAAA;EACd,OAAA;EACA,QAAA;EACA,IAAA;EACA,IAAA,EAAM,UAAA;EACN,KAAA;EACA,gBAAA;EACA;AAAA,GACC,4BAAA,GAA+B,oBAAA"}
|