@copilotkit/channels-core 0.4.1-canary.parallel1 → 0.5.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/README.md +7 -8
- package/dist/canonical-run-loop.test.d.ts +2 -0
- package/dist/canonical-run-loop.test.d.ts.map +1 -0
- package/dist/canonical-run-loop.test.js +453 -0
- package/dist/codec.d.ts +2 -3
- package/dist/codec.d.ts.map +1 -1
- package/dist/create-channel.d.ts +110 -4
- package/dist/create-channel.d.ts.map +1 -1
- package/dist/create-channel.js +258 -92
- package/dist/create-channel.test.js +552 -29
- package/dist/delivery-error.d.ts +17 -0
- package/dist/delivery-error.d.ts.map +1 -0
- package/dist/delivery-error.js +22 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/managed-v1-await-choice-guard.test.d.ts +2 -0
- package/dist/managed-v1-await-choice-guard.test.d.ts.map +1 -0
- package/dist/managed-v1-await-choice-guard.test.js +52 -0
- package/dist/platform-adapter.d.ts +74 -11
- package/dist/platform-adapter.d.ts.map +1 -1
- package/dist/run-loop.d.ts +25 -6
- package/dist/run-loop.d.ts.map +1 -1
- package/dist/run-loop.js +289 -39
- package/dist/run-loop.test.js +37 -0
- package/dist/sanitize-agent-events.d.ts +24 -0
- package/dist/sanitize-agent-events.d.ts.map +1 -0
- package/dist/sanitize-agent-events.js +88 -0
- package/dist/sanitize-agent-events.test.d.ts +2 -0
- package/dist/sanitize-agent-events.test.d.ts.map +1 -0
- package/dist/sanitize-agent-events.test.js +194 -0
- package/dist/source-platform.test.d.ts +2 -0
- package/dist/source-platform.test.d.ts.map +1 -0
- package/dist/source-platform.test.js +149 -0
- package/dist/testing/fake-adapter.d.ts +8 -1
- package/dist/testing/fake-adapter.d.ts.map +1 -1
- package/dist/testing/fake-adapter.js +30 -1
- package/dist/testing/fake-agent.d.ts +5 -0
- package/dist/testing/fake-agent.d.ts.map +1 -1
- package/dist/testing/fake-agent.js +12 -0
- package/dist/thread-promise-contract.test.d.ts +2 -0
- package/dist/thread-promise-contract.test.d.ts.map +1 -0
- package/dist/thread-promise-contract.test.js +37 -0
- package/dist/thread.d.ts +5 -0
- package/dist/thread.d.ts.map +1 -1
- package/dist/thread.js +338 -243
- package/package.json +4 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create-channel.d.ts","sourceRoot":"","sources":["../src/create-channel.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EASf,iBAAiB,EAClB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAGrD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAEzD,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE5D,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AACpE,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAErC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"create-channel.d.ts","sourceRoot":"","sources":["../src/create-channel.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EASf,iBAAiB,EAClB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAGrD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAEzD,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE5D,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AACpE,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAErC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAEnD,OAAO,KAAK,EACV,cAAc,EACd,kBAAkB,EAClB,eAAe,EACf,YAAY,EACZ,UAAU,EAGV,WAAW,EACX,UAAU,EACX,MAAM,yBAAyB,CAAC;AAMjC,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,KAAK,EAAE,QAAQ,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACpE,OAAO,KAAK,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AA8BhF,MAAM,MAAM,oBAAoB,GAAG,MAAM,GAAG,OAAO,CAAC;AAEpD;;;;;;GAMG;AACH,MAAM,MAAM,kBAAkB,GAAG,UAAU,GAAG,QAAQ,GAAG,MAAM,CAAC;AAEhE;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,aAAa,EACxB,QAAQ,EAAE,MAAM,GACf,aAAa,CAkCf;AAyCD;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,GAAG,EAAE;IAC7C,WAAW,CAAC,EAAE,kBAAkB,CAAC;IACjC,cAAc,CAAC,EACX,oBAAoB,GACpB,CAAC,CACC,eAAe,EAAE,MAAM,EACvB,OAAO,EAAE,eAAe,KACrB,oBAAoB,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAAC,CAAC;CAChE,GAAG,kBAAkB,GAAG,iBAAiB,CAMzC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,MAAM,sBAAsB,GAAG,OAAO,GAAG,OAAO,CAAC;AAEvD;;;;;;GAMG;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,KAAK,EAAE,KAAK,KAAK,UAAU,CAAC,WAAW,CAAC,CAAC;AAEzE,MAAM,MAAM,cAAc,CAAC,MAAM,GAAG,OAAO,IAAI,CAAC,GAAG,EAAE;IACnD,MAAM,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IAC/B,OAAO,EAAE,cAAc,CAAC;CACzB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAE3B,2FAA2F;AAC3F,MAAM,MAAM,kBAAkB,CAAC,MAAM,GAAG,OAAO,IAAI,CAAC,GAAG,EAAE;IACvD,MAAM,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IAC/B,IAAI,CAAC,EAAE,YAAY,CAAC;CACrB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAE3B,+CAA+C;AAC/C,MAAM,WAAW,aAAa;IAC5B,oEAAoE;IACpE,KAAK,EAAE,UAAU,CAAC;IAClB,6BAA6B;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,qCAAqC;IACrC,KAAK,EAAE,OAAO,CAAC;IACf,wDAAwD;IACxD,IAAI,CAAC,EAAE,YAAY,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,mFAAmF;IACnF,UAAU,EAAE,UAAU,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,eAAe,CAAC;IACzB,GAAG,EAAE,OAAO,CAAC;CACd;AACD,MAAM,MAAM,eAAe,GAAG,CAAC,GAAG,EAAE,aAAa,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAE3E,kDAAkD;AAClD,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,IAAI,CAAC,EAAE,YAAY,CAAC;IACpB,kEAAkE;IAClE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,GAAG,EAAE,OAAO,CAAC;CACd;AACD,MAAM,MAAM,kBAAkB,GAAG,CAC/B,GAAG,EAAE,gBAAgB,KAClB,iBAAiB,GAAG,IAAI,GAAG,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAAC;AAElE,iDAAiD;AACjD,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,YAAY,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,GAAG,EAAE,OAAO,CAAC;CACd;AACD,MAAM,MAAM,iBAAiB,GAAG,CAAC,GAAG,EAAE,eAAe,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAE/E,gFAAgF;AAChF,KAAK,aAAa,CAAC,OAAO,SAAS,gBAAgB,GAAG,SAAS,IAC7D,OAAO,SAAS,gBAAgB,GAAG,iBAAiB,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC;AAE1E,mFAAmF;AACnF,MAAM,MAAM,cAAc,CAAC,MAAM,IAAI,IAAI,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,GAAG;IACxE,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvC,KAAK,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;CACtC,CAAC;AAEF;;;;GAIG;AACH,MAAM,WAAW,WAAW,CAC1B,YAAY,SAAS,gBAAgB,GAAG,SAAS,GAAG,SAAS;IAE7D,0FAA0F;IAC1F,OAAO,CAAC,EAAE,UAAU,CAAC;IACrB,2IAA2I;IAC3I,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,4FAA4F;IAC5F,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,wEAAwE;IACxE,WAAW,CAAC,EAAE,iBAAiB,CAAC;IAChC;;;OAGG;IACH,WAAW,CAAC,EAAE,kBAAkB,CAAC;IACjC;;;;;;OAMG;IACH,cAAc,CAAC,EACX,oBAAoB,GACpB,CAAC,CACC,eAAe,EAAE,MAAM,EACvB,OAAO,EAAE,eAAe,KACrB,oBAAoB,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAAC,CAAC;IAC/D,kGAAkG;IAClG,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,oEAAoE;IACpE,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,wBAAwB;IACvC;;;;OAIG;IACH,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IACnC;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,+EAA+E;IAC/E,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;CACpC;AAED,MAAM,WAAW,oBAAoB,CACnC,YAAY,SAAS,gBAAgB,GAAG,SAAS,GAAG,SAAS;IAE7D;;;;;OAKG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,QAAQ,CAAC,EAAE,eAAe,EAAE,CAAC;IAC7B;;;;;;;;;;;;;;OAcG;IACH,QAAQ,CAAC,EAAE,sBAAsB,CAAC;IAClC;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,wBAAwB,CAAC;IAC7C,KAAK,CAAC,EAAE,aAAa,GAAG,CAAC,CAAC,QAAQ,EAAE,MAAM,KAAK,aAAa,CAAC,CAAC;IAC9D;;;;;;;;;;;;OAYG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,gDAAgD;IAChD,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,KAAK,CAAC,EAAE,WAAW,EAAE,CAAC;IACtB,OAAO,CAAC,EAAE,YAAY,EAAE,CAAC;IACzB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,gBAAgB,EAAE,CAAC;IAChC,kFAAkF;IAClF,QAAQ,CAAC,EAAE,cAAc,EAAE,CAAC;IAC5B,gFAAgF;IAChF,KAAK,CAAC,EAAE,WAAW,CAAC,YAAY,CAAC,CAAC;CACnC;AAED,MAAM,WAAW,OAAO,CAAC,MAAM,GAAG,OAAO;IACvC,6FAA6F;IAC7F,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,oNAAoN;IACpN,QAAQ,CAAC,QAAQ,EAAE,SAAS,eAAe,EAAE,CAAC;IAC9C;;;;;OAKG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,sBAAsB,CAAC;IAC3C;;;;OAIG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,OAAO,CAAC;IAClC;;;OAGG;IACH,QAAQ,CAAC,iBAAiB,CAAC,EAAE,wBAAwB,CAAC;IACtD,2FAA2F;IAC3F,QAAQ,CAAC,YAAY,EAAE,MAAM,EAAE,CAAC;IAChC,SAAS,CAAC,CAAC,EAAE,cAAc,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC;IAC3C,SAAS,CAAC,CAAC,EAAE,cAAc,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC;IAC3C;;;;OAIG;IACH,eAAe,CAAC,CAAC,EAAE,kBAAkB,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC;IACrD,wFAAwF;IACxF,aAAa,CAAC,MAAM,GAAG,OAAO,EAC5B,EAAE,EAAE,MAAM,EACV,CAAC,EAAE,CAAC,GAAG,EAAE,kBAAkB,CAAC,MAAM,CAAC,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,GAC3D,IAAI,CAAC;IACR;;;;OAIG;IACH,WAAW,CAAC,QAAQ,GAAG,OAAO,EAC5B,SAAS,EAAE,MAAM,EACjB,CAAC,EAAE,CAAC,IAAI,EAAE;QACR,OAAO,EAAE,QAAQ,CAAC;QAClB,MAAM,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;KAChC,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,GACzB,IAAI,CAAC;IACR,8DAA8D;IAC9D,SAAS,CAAC,OAAO,EAAE,cAAc,GAAG,IAAI,CAAC;IACzC,kDAAkD;IAClD,SAAS,CACP,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,CAAC,GAAG,EAAE,cAAc,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,GACrD,IAAI,CAAC;IACR,kGAAkG;IAClG,UAAU,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI,CAAC;IAC3C,UAAU,CAAC,KAAK,EAAE,UAAU,GAAG,UAAU,EAAE,EAAE,OAAO,EAAE,eAAe,GAAG,IAAI,CAAC;IAC7E,uFAAuF;IACvF,aAAa,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,kBAAkB,GAAG,IAAI,CAAC;IACrE,uEAAuE;IACvE,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,iBAAiB,GAAG,IAAI,CAAC;IACnE,IAAI,CAAC,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;IAC3B,kFAAkF;IAClF,WAAW,EAAE,WAAW,CAAC;IACzB;;;;;;OAMG;IACH,QAAQ,EAAE;QACR,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;QACvB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;QACtB,UAAU,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI,CAAC;KAC5C,CAAC;CACH;AA8DD,wBAAgB,aAAa,CAC3B,YAAY,SAAS,gBAAgB,GAAG,SAAS,GAAG,SAAS,EAE7D,IAAI,EAAE,oBAAoB,CAAC,YAAY,CAAC,GACvC,OAAO,CAAC,aAAa,CAAC,YAAY,CAAC,CAAC,CAuuBtC"}
|
package/dist/create-channel.js
CHANGED
|
@@ -4,6 +4,7 @@ import { kvActionStore } from "./state/kv-action-store.js";
|
|
|
4
4
|
import { toAgentToolDescriptors, parseToolArgs } from "./tools.js";
|
|
5
5
|
import { normalizeCommandName, toCommandSpec } from "./commands.js";
|
|
6
6
|
import { Thread } from "./thread.js";
|
|
7
|
+
import { sanitizeAgentEventStream } from "./sanitize-agent-events.js";
|
|
7
8
|
import { normalizeEmoji, toCanonicalEmoji, renderToIR, } from "@copilotkit/channels-ui";
|
|
8
9
|
import { Transcripts } from "./transcripts.js";
|
|
9
10
|
import { ChannelTelemetry } from "./telemetry/channel-telemetry.js";
|
|
@@ -31,6 +32,107 @@ const EMOJI_PLATFORMS = new Set([
|
|
|
31
32
|
function isEmojiPlatform(platform) {
|
|
32
33
|
return EMOJI_PLATFORMS.has(platform);
|
|
33
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Isolate an agent for one turn via `clone()`.
|
|
37
|
+
*
|
|
38
|
+
* Applied to every configured shape, so the object a turn runs on is never one
|
|
39
|
+
* the caller still holds a reference to:
|
|
40
|
+
* - `createChannel({ agent: shared })` — singleton config
|
|
41
|
+
* - `agent: (id) => new Agent()` — fresh factory, cloning an unused agent
|
|
42
|
+
* - `agent: (id) => shared` — factory returning the same object every call
|
|
43
|
+
*
|
|
44
|
+
* The last shape is the one that needs this, and it is easy to write by accident
|
|
45
|
+
* (it is also what a singleton becomes when someone refactors to get at the
|
|
46
|
+
* `threadId`). Sharing one `AbstractAgent` across turns is not safe, because
|
|
47
|
+
* turn concurrency defaults to `"parallel"` — see {@link ChannelConcurrency} —
|
|
48
|
+
* and only the managed adapter serializes same-thread deliveries. On a directly
|
|
49
|
+
* connected adapter two turns in one conversation can run at the same time, and
|
|
50
|
+
* on the same instance they corrupt each other: `messages` is a single array
|
|
51
|
+
* both runs append into, so each run's new-message diff picks up the other's,
|
|
52
|
+
* and `isRunning` / `activeRunDetach$` / `activeRunCompletionPromise` are
|
|
53
|
+
* single-slot fields the second run overwrites while the first is still
|
|
54
|
+
* streaming. Managed delivery serializes them instead, on object identity, which
|
|
55
|
+
* head-of-line blocks two *different* conversations that share one instance.
|
|
56
|
+
*
|
|
57
|
+
* Fails loud on all three ways cloning can fail to isolate: a missing `clone()`,
|
|
58
|
+
* a `clone()` that hands back the same object, and a `clone()` that silently
|
|
59
|
+
* drops subclass state (see {@link assertCloneKeptOwnFields}).
|
|
60
|
+
*/
|
|
61
|
+
export function isolateAgentInstance(prototype, threadId) {
|
|
62
|
+
if (typeof prototype.clone !== "function") {
|
|
63
|
+
throw new Error("createChannel: agent must implement clone() that returns a new instance " +
|
|
64
|
+
"(HttpAgent, BuiltInAgent, etc.). Every turn is isolated via clone(), " +
|
|
65
|
+
"including agents returned from an agent: (threadId) => ... factory.");
|
|
66
|
+
}
|
|
67
|
+
const cloned = prototype.clone();
|
|
68
|
+
if (cloned == null || cloned === prototype) {
|
|
69
|
+
throw new Error("createChannel: agent.clone() must return a distinct instance for concurrent turns");
|
|
70
|
+
}
|
|
71
|
+
assertCloneKeptOwnFields(prototype, cloned);
|
|
72
|
+
cloned.threadId = threadId;
|
|
73
|
+
// `clone()` copies `isRunning` from the source, and a source that has already
|
|
74
|
+
// run can be mid-run at the moment it is cloned. A fresh turn is not.
|
|
75
|
+
//
|
|
76
|
+
// Hygiene, not a fix for a dead turn: `runAgent` assigns
|
|
77
|
+
// `abortController = params?.abortController ?? new AbortController()` before
|
|
78
|
+
// each run and the run loop passes no controller, so an inherited aborted
|
|
79
|
+
// controller cannot reach the next request on its own. Reset it anyway so
|
|
80
|
+
// anything reading the signal between isolation and the run sees a live one.
|
|
81
|
+
// Note this deliberately discards `HttpAgent.clone()`'s propagation of the
|
|
82
|
+
// source's aborted state, which exists for callers that clone mid-run.
|
|
83
|
+
cloned.isRunning = false;
|
|
84
|
+
const withAbort = cloned;
|
|
85
|
+
if ("abortController" in withAbort) {
|
|
86
|
+
withAbort.abortController = new AbortController();
|
|
87
|
+
}
|
|
88
|
+
return cloned;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Fail loud when `clone()` silently drops subclass state.
|
|
92
|
+
*
|
|
93
|
+
* `AbstractAgent.prototype.clone()` copies a fixed field list, so a subclass
|
|
94
|
+
* that declares its own fields (an auth client, config, a cache) gets them back
|
|
95
|
+
* as `undefined` on the clone — and because the base implementation always
|
|
96
|
+
* exists and returns a correctly-typed instance, nothing else surfaces it. The
|
|
97
|
+
* agent just runs gutted.
|
|
98
|
+
*
|
|
99
|
+
* Comparing own enumerable keys catches exactly that and stays quiet for the
|
|
100
|
+
* agents that do override `clone()` (`HttpAgent`, `LangGraphAgent`,
|
|
101
|
+
* `BuiltInAgent`, `IntelligenceAgent`). Symbol-keyed and non-enumerable fields
|
|
102
|
+
* are not covered.
|
|
103
|
+
*
|
|
104
|
+
* Own *functions* are deliberately exempt. Assigning a method on the instance is
|
|
105
|
+
* how spies and instrumentation wrap an agent, and losing that wrapper leaves
|
|
106
|
+
* the class's prototype method intact — the clone still behaves correctly, it
|
|
107
|
+
* just isn't wrapped. Only dropped state leaves an agent genuinely gutted.
|
|
108
|
+
*/
|
|
109
|
+
function assertCloneKeptOwnFields(prototype, cloned) {
|
|
110
|
+
const source = prototype;
|
|
111
|
+
const dropped = Object.keys(prototype).filter((key) => typeof source[key] !== "function" &&
|
|
112
|
+
!Object.prototype.hasOwnProperty.call(cloned, key));
|
|
113
|
+
if (dropped.length === 0)
|
|
114
|
+
return;
|
|
115
|
+
const name = prototype.constructor?.name ?? "the configured agent";
|
|
116
|
+
throw new Error(`createChannel: ${name}.clone() dropped ${dropped.join(", ")}. ` +
|
|
117
|
+
"Every turn runs on a clone, and AbstractAgent's clone() only copies its " +
|
|
118
|
+
`own fixed field list, so those fields would read as undefined. Override ` +
|
|
119
|
+
`clone() on ${name} to copy them (HttpAgent.clone() is the reference).`);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Resolve effective turn concurrency from `store.concurrency` and legacy
|
|
123
|
+
* `store.onLockConflict`. Prefer `concurrency` when both are set.
|
|
124
|
+
*/
|
|
125
|
+
export function resolveChannelConcurrency(cfg) {
|
|
126
|
+
if (cfg.concurrency)
|
|
127
|
+
return cfg.concurrency;
|
|
128
|
+
if (typeof cfg.onLockConflict === "function")
|
|
129
|
+
return "legacy-callback";
|
|
130
|
+
if (cfg.onLockConflict === "drop")
|
|
131
|
+
return "drop";
|
|
132
|
+
if (cfg.onLockConflict === "force")
|
|
133
|
+
return "parallel";
|
|
134
|
+
return "parallel";
|
|
135
|
+
}
|
|
34
136
|
/** Build the IncomingMessage object from an IncomingTurn (shared by lock-conflict callback and handler path). */
|
|
35
137
|
function msgFromTurn(turn) {
|
|
36
138
|
return {
|
|
@@ -39,22 +141,25 @@ function msgFromTurn(turn) {
|
|
|
39
141
|
user: turn.user ?? { id: "" },
|
|
40
142
|
ref: { id: "" },
|
|
41
143
|
platform: turn.platform,
|
|
144
|
+
operation: turn.operation,
|
|
42
145
|
eventId: turn.eventId,
|
|
43
146
|
turnId: turn.turnId,
|
|
44
147
|
deliveryId: turn.deliveryId,
|
|
45
148
|
};
|
|
46
149
|
}
|
|
150
|
+
/** Resolve the provider that produced an event without changing adapter identity. */
|
|
151
|
+
function ingressPlatform(adapter, event) {
|
|
152
|
+
return event.platform ?? adapter.platform;
|
|
153
|
+
}
|
|
47
154
|
/**
|
|
48
|
-
* Enforce
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* the runtime does, never both.
|
|
155
|
+
* Enforce managed Intelligence Channel exclusivity. Managed and direct
|
|
156
|
+
* delivery are alternative modes on a Channel: Intelligence holds provider
|
|
157
|
+
* credentials, or the runtime does, never both.
|
|
52
158
|
*/
|
|
53
159
|
function assertExclusive(adapters) {
|
|
54
160
|
if (adapters.some((a) => a.__intelligenceChannel) && adapters.length > 1) {
|
|
55
|
-
throw new Error("
|
|
56
|
-
"direct delivery are alternative modes.
|
|
57
|
-
"direct adapters (slack/discord/...), not both.");
|
|
161
|
+
throw new Error("The managed Intelligence adapter must be the only adapter on a Channel — " +
|
|
162
|
+
"managed and direct delivery are alternative modes.");
|
|
58
163
|
}
|
|
59
164
|
}
|
|
60
165
|
/**
|
|
@@ -96,15 +201,37 @@ export function createChannel(opts) {
|
|
|
96
201
|
let registry;
|
|
97
202
|
let telemetry;
|
|
98
203
|
const agentFactory = (() => {
|
|
204
|
+
// Applied here rather than at each call site so a developer never has to
|
|
205
|
+
// know the workaround exists. Idempotent, so reusing one agent instance
|
|
206
|
+
// across threads (or being handed an already-sanitized one) is fine.
|
|
207
|
+
const sanitize = opts.sanitizeAgentEvents === false
|
|
208
|
+
? (agent) => agent
|
|
209
|
+
: sanitizeAgentEventStream;
|
|
99
210
|
const a = opts.agent;
|
|
100
|
-
if (
|
|
101
|
-
return
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
211
|
+
if (!a) {
|
|
212
|
+
return () => {
|
|
213
|
+
throw new Error("createChannel: no agent configured (pass `agent` to use runAgent)");
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
// Clone per turn for both shapes, so concurrent turns never share one
|
|
217
|
+
// mutable agent. Isolating only the singleton config is not enough: a
|
|
218
|
+
// factory is free to return the same object on every call. Cloning a fresh
|
|
219
|
+
// factory's result costs an unused instance and closes the shared case —
|
|
220
|
+
// see `isolateAgentInstance`.
|
|
221
|
+
if (typeof a === "function") {
|
|
222
|
+
return (threadId) => sanitize(isolateAgentInstance(a(threadId), threadId));
|
|
223
|
+
}
|
|
224
|
+
return (threadId) => sanitize(isolateAgentInstance(a, threadId));
|
|
107
225
|
})();
|
|
226
|
+
/** Per-conversation serial turn queue (error-boundaried so one failure cannot poison the chain). */
|
|
227
|
+
const conversationTurnQueues = new Map();
|
|
228
|
+
function enqueueConversationTurn(conversationKey, work) {
|
|
229
|
+
const previous = conversationTurnQueues.get(conversationKey) ?? Promise.resolve();
|
|
230
|
+
const run = previous.then(work, work);
|
|
231
|
+
// Keep the map tail settled so a rejected turn does not block later ones forever.
|
|
232
|
+
conversationTurnQueues.set(conversationKey, run.then(() => undefined, () => undefined));
|
|
233
|
+
return run;
|
|
234
|
+
}
|
|
108
235
|
const toolMap = new Map();
|
|
109
236
|
for (const t of opts.tools ?? [])
|
|
110
237
|
toolMap.set(t.name, t);
|
|
@@ -129,6 +256,7 @@ export function createChannel(opts) {
|
|
|
129
256
|
}
|
|
130
257
|
const deps = {
|
|
131
258
|
adapter,
|
|
259
|
+
platform: extras?.platform,
|
|
132
260
|
replyTarget,
|
|
133
261
|
conversationKey,
|
|
134
262
|
registry,
|
|
@@ -161,83 +289,114 @@ export function createChannel(opts) {
|
|
|
161
289
|
// backend/registry are resolved in start() before any adapter.start() runs,
|
|
162
290
|
// so they are always set by the time the sink receives an event.
|
|
163
291
|
const store = backend;
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
292
|
+
/**
|
|
293
|
+
* Core turn body (dedup → identity → handlers). Used by all concurrency modes.
|
|
294
|
+
* Dedup marks the event seen at the start so a Slack redelivery of an
|
|
295
|
+
* in-flight event does not double-run under parallel mode either.
|
|
296
|
+
*/
|
|
297
|
+
async function processTurn(turn) {
|
|
298
|
+
const platform = ingressPlatform(adapter, turn);
|
|
299
|
+
if (!adapter.skipIngressDedup) {
|
|
300
|
+
const dupKey = [
|
|
301
|
+
"message",
|
|
302
|
+
platform,
|
|
303
|
+
turn.operation.kind,
|
|
304
|
+
turn.operation.logicalMessageId,
|
|
305
|
+
turn.operation.revisionId,
|
|
306
|
+
].join(":");
|
|
307
|
+
try {
|
|
308
|
+
if (await store.dedup.seen(dupKey, cfg.dedupTtl ?? 300_000))
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
catch (err) {
|
|
312
|
+
console.warn(`[channel] dedup check failed for ${platform}; processing without dedup`, err);
|
|
178
313
|
}
|
|
314
|
+
}
|
|
315
|
+
// Resolve cross-platform identity key (if configured) and stamp it on
|
|
316
|
+
// the message so handlers and transcript storage can use it. Done
|
|
317
|
+
// BEFORE makeThread so the thread carries the userKey + message for
|
|
318
|
+
// the transcript auto-bridge (runAgent({ transcript: true })).
|
|
319
|
+
let userKey;
|
|
320
|
+
if (cfg.identity) {
|
|
179
321
|
try {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
try {
|
|
187
|
-
if (await store.dedup.seen(dupKey, cfg.dedupTtl ?? 300_000))
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
|
-
catch (err) {
|
|
191
|
-
console.warn(`[channel] dedup check failed for ${adapter.platform}; processing without dedup`, err);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
// Resolve cross-platform identity key (if configured) and stamp it on
|
|
195
|
-
// the message so handlers and transcript storage can use it. Done
|
|
196
|
-
// BEFORE makeThread so the thread carries the userKey + message for
|
|
197
|
-
// the transcript auto-bridge (runAgent({ transcript: true })).
|
|
198
|
-
let userKey;
|
|
199
|
-
if (cfg.identity) {
|
|
200
|
-
try {
|
|
201
|
-
const resolved = await cfg.identity({
|
|
202
|
-
adapter: adapter.platform,
|
|
203
|
-
author: turn.user ?? { id: "" },
|
|
204
|
-
message: msgFromTurn(turn),
|
|
205
|
-
});
|
|
206
|
-
userKey = resolved ?? undefined;
|
|
207
|
-
}
|
|
208
|
-
catch (err) {
|
|
209
|
-
console.warn(`[channel] identity resolution failed for ${adapter.platform}; continuing without userKey`, err);
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
const message = { ...msgFromTurn(turn), userKey };
|
|
213
|
-
const thread = makeThread(adapter, turn.replyTarget, turn.conversationKey, { userKey, message });
|
|
214
|
-
// v1 routing: there is no turn `kind`, so prefer mention handlers; if
|
|
215
|
-
// none are registered, fall back to message handlers. (The reference
|
|
216
|
-
// example registers identical handlers on both, so this avoids
|
|
217
|
-
// double-firing while still invoking whatever is registered.)
|
|
218
|
-
const handlers = mentionHandlers.length > 0 ? mentionHandlers : messageHandlers;
|
|
219
|
-
for (const h of handlers)
|
|
220
|
-
await h({ thread, message });
|
|
322
|
+
const resolved = await cfg.identity({
|
|
323
|
+
adapter: platform,
|
|
324
|
+
author: turn.user ?? { id: "" },
|
|
325
|
+
message: msgFromTurn(turn),
|
|
326
|
+
});
|
|
327
|
+
userKey = resolved ?? undefined;
|
|
221
328
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
if (acquired)
|
|
225
|
-
await store.lock.release(lockKey, acquired.token);
|
|
329
|
+
catch (err) {
|
|
330
|
+
console.warn(`[channel] identity resolution failed for ${platform}; continuing without userKey`, err);
|
|
226
331
|
}
|
|
332
|
+
}
|
|
333
|
+
const message = { ...msgFromTurn(turn), userKey };
|
|
334
|
+
const thread = makeThread(adapter, turn.replyTarget, turn.conversationKey, { platform, userKey, message });
|
|
335
|
+
const handlers = turn.operation.mentioned
|
|
336
|
+
? mentionHandlers.length > 0
|
|
337
|
+
? mentionHandlers
|
|
338
|
+
: messageHandlers
|
|
339
|
+
: messageHandlers;
|
|
340
|
+
for (const h of handlers)
|
|
341
|
+
await h({ thread, message });
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Exclusive-lock path for `drop` and legacy `onLockConflict` callback.
|
|
345
|
+
* A turn dropped on lock-conflict must NOT burn its eventId, so Slack's
|
|
346
|
+
* retry can still be processed once the lock frees — hence dedup lives
|
|
347
|
+
* inside `processTurn` only after we hold the lock (or force through).
|
|
348
|
+
*/
|
|
349
|
+
async function processTurnWithLock(turn) {
|
|
350
|
+
const lockKey = `turn:${turn.conversationKey}`;
|
|
351
|
+
const acquired = await store.lock.acquire(lockKey, {
|
|
352
|
+
ttlMs: cfg.lockTtl ?? 60_000,
|
|
353
|
+
});
|
|
354
|
+
if (!acquired) {
|
|
355
|
+
const decision = typeof cfg.onLockConflict === "function"
|
|
356
|
+
? await cfg.onLockConflict(turn.conversationKey, msgFromTurn(turn))
|
|
357
|
+
: (cfg.onLockConflict ?? "drop");
|
|
358
|
+
if (decision === "drop")
|
|
359
|
+
return; // discard overlapping turn
|
|
360
|
+
// "force": proceed WITHOUT a lock token. Does NOT cancel the
|
|
361
|
+
// in-flight handler — cooperative cancellation is a future extension.
|
|
362
|
+
}
|
|
363
|
+
try {
|
|
364
|
+
await processTurn(turn);
|
|
365
|
+
}
|
|
366
|
+
finally {
|
|
367
|
+
// acquired is null on "force" — naturally skips release.
|
|
368
|
+
if (acquired)
|
|
369
|
+
await store.lock.release(lockKey, acquired.token);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return {
|
|
373
|
+
async onTurn(turn) {
|
|
374
|
+
const mode = resolveChannelConcurrency(cfg);
|
|
375
|
+
if (mode === "parallel") {
|
|
376
|
+
await processTurn(turn);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
if (mode === "serial") {
|
|
380
|
+
await enqueueConversationTurn(turn.conversationKey, () => processTurn(turn));
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
// "drop" or legacy callback — exclusive lock + drop/force.
|
|
384
|
+
await processTurnWithLock(turn);
|
|
227
385
|
},
|
|
228
386
|
async onInteraction(evt) {
|
|
387
|
+
const platform = ingressPlatform(adapter, evt);
|
|
229
388
|
// Dedup guard: drop duplicate deliveries of the same event within the TTL window.
|
|
230
389
|
if (evt.eventId && !adapter.skipIngressDedup) {
|
|
231
|
-
const dupKey = `evt:${
|
|
390
|
+
const dupKey = `evt:${platform}:${evt.eventId}`;
|
|
232
391
|
try {
|
|
233
392
|
if (await store.dedup.seen(dupKey, cfg.dedupTtl ?? 300_000))
|
|
234
393
|
return;
|
|
235
394
|
}
|
|
236
395
|
catch (err) {
|
|
237
|
-
console.warn(`[channel] dedup check failed for ${
|
|
396
|
+
console.warn(`[channel] dedup check failed for ${platform}; processing without dedup`, err);
|
|
238
397
|
}
|
|
239
398
|
}
|
|
240
|
-
const thread = makeThread(adapter, evt.replyTarget, evt.conversationKey);
|
|
399
|
+
const thread = makeThread(adapter, evt.replyTarget, evt.conversationKey, { platform });
|
|
241
400
|
const user = evt.user ?? { id: "" };
|
|
242
401
|
const ctx = {
|
|
243
402
|
thread,
|
|
@@ -245,12 +404,12 @@ export function createChannel(opts) {
|
|
|
245
404
|
text: "",
|
|
246
405
|
user,
|
|
247
406
|
ref: evt.messageRef ?? { id: "" },
|
|
248
|
-
platform
|
|
407
|
+
platform,
|
|
249
408
|
},
|
|
250
409
|
action: { id: evt.id, value: evt.value },
|
|
251
410
|
values: {},
|
|
252
411
|
user,
|
|
253
|
-
platform
|
|
412
|
+
platform,
|
|
254
413
|
};
|
|
255
414
|
const openModal = makeOpenModal(adapter, evt.replyTarget, evt.triggerId);
|
|
256
415
|
if (openModal)
|
|
@@ -284,21 +443,22 @@ export function createChannel(opts) {
|
|
|
284
443
|
}
|
|
285
444
|
},
|
|
286
445
|
async onCommand(cmd) {
|
|
446
|
+
const platform = ingressPlatform(adapter, cmd);
|
|
287
447
|
// Dedup guard: drop duplicate deliveries of the same event within the TTL window.
|
|
288
448
|
if (cmd.eventId && !adapter.skipIngressDedup) {
|
|
289
|
-
const dupKey = `evt:${
|
|
449
|
+
const dupKey = `evt:${platform}:${cmd.eventId}`;
|
|
290
450
|
try {
|
|
291
451
|
if (await store.dedup.seen(dupKey, cfg.dedupTtl ?? 300_000))
|
|
292
452
|
return;
|
|
293
453
|
}
|
|
294
454
|
catch (err) {
|
|
295
|
-
console.warn(`[channel] dedup check failed for ${
|
|
455
|
+
console.warn(`[channel] dedup check failed for ${platform}; processing without dedup`, err);
|
|
296
456
|
}
|
|
297
457
|
}
|
|
298
458
|
const command = commandHandlers.get(normalizeCommandName(cmd.command));
|
|
299
459
|
if (!command)
|
|
300
460
|
return; // unregistered command → skip
|
|
301
|
-
const thread = makeThread(adapter, cmd.replyTarget, cmd.conversationKey);
|
|
461
|
+
const thread = makeThread(adapter, cmd.replyTarget, cmd.conversationKey, { platform });
|
|
302
462
|
// Resolve typed options from any structured args the surface supplied
|
|
303
463
|
// (e.g. Discord); text-only surfaces (Slack) leave `options` empty and
|
|
304
464
|
// the handler reads `text`.
|
|
@@ -314,7 +474,7 @@ export function createChannel(opts) {
|
|
|
314
474
|
text: cmd.text,
|
|
315
475
|
options,
|
|
316
476
|
user: cmd.user,
|
|
317
|
-
platform
|
|
477
|
+
platform,
|
|
318
478
|
};
|
|
319
479
|
const openModal = makeOpenModal(adapter, cmd.replyTarget, cmd.triggerId);
|
|
320
480
|
if (openModal)
|
|
@@ -325,23 +485,19 @@ export function createChannel(opts) {
|
|
|
325
485
|
// The adapter has already applied its static defaults (greeting /
|
|
326
486
|
// prompts) before emitting this, so handlers layer on top and never
|
|
327
487
|
// race. Zero handlers → no-op.
|
|
328
|
-
const thread = makeThread(adapter, evt.replyTarget, evt.conversationKey);
|
|
488
|
+
const thread = makeThread(adapter, evt.replyTarget, evt.conversationKey, { platform: ingressPlatform(adapter, evt) });
|
|
329
489
|
for (const h of threadStartedHandlers)
|
|
330
490
|
await h({ thread, user: evt.user });
|
|
331
491
|
},
|
|
332
492
|
async onReaction(evt) {
|
|
333
|
-
// Normalize by
|
|
334
|
-
//
|
|
335
|
-
|
|
336
|
-
// originating provider (e.g. "teams"/"slack") so central normalization
|
|
337
|
-
// still runs. Only normalize when that platform is one the emoji table
|
|
338
|
-
// knows; otherwise the raw token passes through unchanged.
|
|
339
|
-
const sourcePlatform = evt.platform ?? adapter.platform;
|
|
493
|
+
// Normalize by source platform, falling back to adapter identity for
|
|
494
|
+
// direct adapters that do not need per-event provider routing.
|
|
495
|
+
const sourcePlatform = ingressPlatform(adapter, evt);
|
|
340
496
|
const normalized = isEmojiPlatform(sourcePlatform)
|
|
341
497
|
? normalizeEmoji(evt.rawEmoji, sourcePlatform)
|
|
342
498
|
: undefined;
|
|
343
499
|
const value = normalized ?? evt.rawEmoji;
|
|
344
|
-
const thread = makeThread(adapter, evt.replyTarget, evt.conversationKey);
|
|
500
|
+
const thread = makeThread(adapter, evt.replyTarget, evt.conversationKey, { platform: sourcePlatform });
|
|
345
501
|
// Prefer the adapter's update-capable ref; fall back to the bare id.
|
|
346
502
|
const messageRef = evt.messageRef ?? { id: evt.messageId };
|
|
347
503
|
const reactionEvt = {
|
|
@@ -383,7 +539,9 @@ export function createChannel(opts) {
|
|
|
383
539
|
if (!handler)
|
|
384
540
|
return; // unregistered → closes
|
|
385
541
|
const thread = evt.conversationKey !== undefined && evt.replyTarget !== undefined
|
|
386
|
-
? makeThread(adapter, evt.replyTarget, evt.conversationKey
|
|
542
|
+
? makeThread(adapter, evt.replyTarget, evt.conversationKey, {
|
|
543
|
+
platform: evt.platform,
|
|
544
|
+
})
|
|
387
545
|
: undefined;
|
|
388
546
|
const result = await handler({
|
|
389
547
|
callbackId: evt.callbackId,
|
|
@@ -414,6 +572,9 @@ export function createChannel(opts) {
|
|
|
414
572
|
...(opts.showToolStatus !== undefined
|
|
415
573
|
? { showToolStatus: opts.showToolStatus }
|
|
416
574
|
: {}),
|
|
575
|
+
...(opts.replyContinuation !== undefined
|
|
576
|
+
? { replyContinuation: opts.replyContinuation }
|
|
577
|
+
: {}),
|
|
417
578
|
get adapters() {
|
|
418
579
|
// Defensive read-only copy: mutating the returned array must not affect
|
|
419
580
|
// the Channel's private adapter list.
|
|
@@ -443,6 +604,11 @@ export function createChannel(opts) {
|
|
|
443
604
|
// and real adapters would connect/port-bind twice.
|
|
444
605
|
if (started)
|
|
445
606
|
return;
|
|
607
|
+
if (adapters.some((adapter) => adapter.capabilities.supportsMessageEvents) &&
|
|
608
|
+
mentionHandlers.length === 0 &&
|
|
609
|
+
messageHandlers.length === 0) {
|
|
610
|
+
throw new Error(`channel "${opts.name ?? "(unnamed)"}" must register onMention or onMessage before activation`);
|
|
611
|
+
}
|
|
446
612
|
started = true;
|
|
447
613
|
assertExclusive(adapters);
|
|
448
614
|
// Resolve persistence now that all adapters (including any attached via
|