@copilotkit/channels 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +204 -0
  3. package/dist/action-registry.d.ts +59 -0
  4. package/dist/action-registry.d.ts.map +1 -0
  5. package/dist/action-registry.js +217 -0
  6. package/dist/action-registry.test.d.ts +2 -0
  7. package/dist/action-registry.test.d.ts.map +1 -0
  8. package/dist/action-registry.test.js +115 -0
  9. package/dist/action-store.d.ts +21 -0
  10. package/dist/action-store.d.ts.map +1 -0
  11. package/dist/action-store.js +23 -0
  12. package/dist/action-store.test.d.ts +2 -0
  13. package/dist/action-store.test.d.ts.map +1 -0
  14. package/dist/action-store.test.js +27 -0
  15. package/dist/codec.d.ts +25 -0
  16. package/dist/codec.d.ts.map +1 -0
  17. package/dist/codec.js +1 -0
  18. package/dist/commands.d.ts +73 -0
  19. package/dist/commands.d.ts.map +1 -0
  20. package/dist/commands.js +38 -0
  21. package/dist/commands.test.d.ts +2 -0
  22. package/dist/commands.test.d.ts.map +1 -0
  23. package/dist/commands.test.js +23 -0
  24. package/dist/create-bot.d.ts +171 -0
  25. package/dist/create-bot.d.ts.map +1 -0
  26. package/dist/create-bot.foundations.test.d.ts +2 -0
  27. package/dist/create-bot.foundations.test.d.ts.map +1 -0
  28. package/dist/create-bot.foundations.test.js +147 -0
  29. package/dist/create-bot.js +576 -0
  30. package/dist/create-bot.test.d.ts +2 -0
  31. package/dist/create-bot.test.d.ts.map +1 -0
  32. package/dist/create-bot.test.js +936 -0
  33. package/dist/index.d.ts +26 -0
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +32 -0
  36. package/dist/mint-id.d.ts +3 -0
  37. package/dist/mint-id.d.ts.map +1 -0
  38. package/dist/mint-id.js +20 -0
  39. package/dist/mint-id.test.d.ts +2 -0
  40. package/dist/mint-id.test.d.ts.map +1 -0
  41. package/dist/mint-id.test.js +16 -0
  42. package/dist/modal-submit.test.d.ts +2 -0
  43. package/dist/modal-submit.test.d.ts.map +1 -0
  44. package/dist/modal-submit.test.js +68 -0
  45. package/dist/platform-adapter.d.ts +312 -0
  46. package/dist/platform-adapter.d.ts.map +1 -0
  47. package/dist/platform-adapter.js +1 -0
  48. package/dist/platform-adapter.test.d.ts +2 -0
  49. package/dist/platform-adapter.test.d.ts.map +1 -0
  50. package/dist/platform-adapter.test.js +40 -0
  51. package/dist/reactions.test.d.ts +2 -0
  52. package/dist/reactions.test.d.ts.map +1 -0
  53. package/dist/reactions.test.js +197 -0
  54. package/dist/run-loop.d.ts +41 -0
  55. package/dist/run-loop.d.ts.map +1 -0
  56. package/dist/run-loop.js +102 -0
  57. package/dist/run-loop.test.d.ts +2 -0
  58. package/dist/run-loop.test.d.ts.map +1 -0
  59. package/dist/run-loop.test.js +125 -0
  60. package/dist/standard-schema.d.ts +52 -0
  61. package/dist/standard-schema.d.ts.map +1 -0
  62. package/dist/standard-schema.js +66 -0
  63. package/dist/state/duration.d.ts +3 -0
  64. package/dist/state/duration.d.ts.map +1 -0
  65. package/dist/state/duration.js +16 -0
  66. package/dist/state/duration.test.d.ts +2 -0
  67. package/dist/state/duration.test.d.ts.map +1 -0
  68. package/dist/state/duration.test.js +13 -0
  69. package/dist/state/kv-action-store.d.ts +7 -0
  70. package/dist/state/kv-action-store.d.ts.map +1 -0
  71. package/dist/state/kv-action-store.js +9 -0
  72. package/dist/state/kv-action-store.test.d.ts +2 -0
  73. package/dist/state/kv-action-store.test.d.ts.map +1 -0
  74. package/dist/state/kv-action-store.test.js +21 -0
  75. package/dist/state/memory-store.d.ts +41 -0
  76. package/dist/state/memory-store.d.ts.map +1 -0
  77. package/dist/state/memory-store.js +110 -0
  78. package/dist/state/memory-store.test.d.ts +2 -0
  79. package/dist/state/memory-store.test.d.ts.map +1 -0
  80. package/dist/state/memory-store.test.js +22 -0
  81. package/dist/state/state-conversation-store.d.ts +14 -0
  82. package/dist/state/state-conversation-store.d.ts.map +1 -0
  83. package/dist/state/state-conversation-store.js +22 -0
  84. package/dist/state/state-conversation-store.test.d.ts +2 -0
  85. package/dist/state/state-conversation-store.test.d.ts.map +1 -0
  86. package/dist/state/state-conversation-store.test.js +58 -0
  87. package/dist/state/state-store.d.ts +63 -0
  88. package/dist/state/state-store.d.ts.map +1 -0
  89. package/dist/state/state-store.js +1 -0
  90. package/dist/telemetry/bot-telemetry.d.ts +31 -0
  91. package/dist/telemetry/bot-telemetry.d.ts.map +1 -0
  92. package/dist/telemetry/bot-telemetry.js +70 -0
  93. package/dist/telemetry/bot-telemetry.test.d.ts +2 -0
  94. package/dist/telemetry/bot-telemetry.test.d.ts.map +1 -0
  95. package/dist/telemetry/bot-telemetry.test.js +56 -0
  96. package/dist/telemetry/create-bot-telemetry.test.d.ts +2 -0
  97. package/dist/telemetry/create-bot-telemetry.test.d.ts.map +1 -0
  98. package/dist/telemetry/create-bot-telemetry.test.js +71 -0
  99. package/dist/telemetry/e2e-telemetry.test.d.ts +2 -0
  100. package/dist/telemetry/e2e-telemetry.test.d.ts.map +1 -0
  101. package/dist/telemetry/e2e-telemetry.test.js +56 -0
  102. package/dist/telemetry/events-catalog.test.d.ts +2 -0
  103. package/dist/telemetry/events-catalog.test.d.ts.map +1 -0
  104. package/dist/telemetry/events-catalog.test.js +8 -0
  105. package/dist/telemetry/install-id.d.ts +8 -0
  106. package/dist/telemetry/install-id.d.ts.map +1 -0
  107. package/dist/telemetry/install-id.js +43 -0
  108. package/dist/telemetry/install-id.test.d.ts +2 -0
  109. package/dist/telemetry/install-id.test.d.ts.map +1 -0
  110. package/dist/telemetry/install-id.test.js +43 -0
  111. package/dist/telemetry/platform-allowlist-coverage.test.d.ts +2 -0
  112. package/dist/telemetry/platform-allowlist-coverage.test.d.ts.map +1 -0
  113. package/dist/telemetry/platform-allowlist-coverage.test.js +45 -0
  114. package/dist/telemetry/sanitize-error.d.ts +3 -0
  115. package/dist/telemetry/sanitize-error.d.ts.map +1 -0
  116. package/dist/telemetry/sanitize-error.js +31 -0
  117. package/dist/telemetry/sanitize-error.test.d.ts +2 -0
  118. package/dist/telemetry/sanitize-error.test.d.ts.map +1 -0
  119. package/dist/telemetry/sanitize-error.test.js +33 -0
  120. package/dist/testing/fake-adapter.d.ts +137 -0
  121. package/dist/testing/fake-adapter.d.ts.map +1 -0
  122. package/dist/testing/fake-adapter.js +273 -0
  123. package/dist/testing/fake-agent.d.ts +28 -0
  124. package/dist/testing/fake-agent.d.ts.map +1 -0
  125. package/dist/testing/fake-agent.js +36 -0
  126. package/dist/testing/state-store-conformance.d.ts +4 -0
  127. package/dist/testing/state-store-conformance.d.ts.map +1 -0
  128. package/dist/testing/state-store-conformance.js +117 -0
  129. package/dist/thread-capabilities.test.d.ts +2 -0
  130. package/dist/thread-capabilities.test.d.ts.map +1 -0
  131. package/dist/thread-capabilities.test.js +77 -0
  132. package/dist/thread-ephemeral.test.d.ts +2 -0
  133. package/dist/thread-ephemeral.test.d.ts.map +1 -0
  134. package/dist/thread-ephemeral.test.js +45 -0
  135. package/dist/thread-reactions.test.d.ts +2 -0
  136. package/dist/thread-reactions.test.d.ts.map +1 -0
  137. package/dist/thread-reactions.test.js +38 -0
  138. package/dist/thread.d.ts +152 -0
  139. package/dist/thread.d.ts.map +1 -0
  140. package/dist/thread.js +329 -0
  141. package/dist/thread.test.d.ts +2 -0
  142. package/dist/thread.test.d.ts.map +1 -0
  143. package/dist/thread.test.js +83 -0
  144. package/dist/tools.d.ts +67 -0
  145. package/dist/tools.d.ts.map +1 -0
  146. package/dist/tools.js +38 -0
  147. package/dist/tools.test.d.ts +2 -0
  148. package/dist/tools.test.d.ts.map +1 -0
  149. package/dist/tools.test.js +31 -0
  150. package/dist/transcripts.d.ts +52 -0
  151. package/dist/transcripts.d.ts.map +1 -0
  152. package/dist/transcripts.js +67 -0
  153. package/dist/transcripts.test.d.ts +2 -0
  154. package/dist/transcripts.test.d.ts.map +1 -0
  155. package/dist/transcripts.test.js +108 -0
  156. package/package.json +72 -0
@@ -0,0 +1,23 @@
1
+ /** @deprecated Configure `createBot({ state })` instead. Action snapshots are stored via `StateStore.kv`. */
2
+ export class InMemoryActionStore {
3
+ map = new Map();
4
+ async put(id, snap, ttlMs) {
5
+ this.map.set(id, {
6
+ snap,
7
+ expiresAt: ttlMs ? Date.now() + ttlMs : undefined,
8
+ });
9
+ }
10
+ async get(id) {
11
+ const e = this.map.get(id);
12
+ if (!e)
13
+ return undefined;
14
+ if (e.expiresAt !== undefined && Date.now() > e.expiresAt) {
15
+ this.map.delete(id);
16
+ return undefined;
17
+ }
18
+ return e.snap;
19
+ }
20
+ async delete(id) {
21
+ this.map.delete(id);
22
+ }
23
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=action-store.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"action-store.test.d.ts","sourceRoot":"","sources":["../src/action-store.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,27 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { InMemoryActionStore } from "./action-store.js";
3
+ describe("InMemoryActionStore", () => {
4
+ it("puts and gets a snapshot", async () => {
5
+ const s = new InMemoryActionStore();
6
+ await s.put("id1", { path: [0], conversationKey: "c" });
7
+ expect(await s.get("id1")).toMatchObject({ path: [0] });
8
+ });
9
+ it("expires entries past ttl", async () => {
10
+ vi.useFakeTimers();
11
+ try {
12
+ const s = new InMemoryActionStore();
13
+ await s.put("id1", { path: [0], conversationKey: "c" }, 1000);
14
+ vi.advanceTimersByTime(1500);
15
+ expect(await s.get("id1")).toBeUndefined();
16
+ }
17
+ finally {
18
+ vi.useRealTimers();
19
+ }
20
+ });
21
+ it("deletes entries", async () => {
22
+ const s = new InMemoryActionStore();
23
+ await s.put("id1", { path: [0], conversationKey: "c" });
24
+ await s.delete("id1");
25
+ expect(await s.get("id1")).toBeUndefined();
26
+ });
27
+ });
@@ -0,0 +1,25 @@
1
+ import type { BotNode } from "@copilotkit/channels-ui";
2
+ /**
3
+ * A pure, dependency-free, per-platform codec shared by the local adapter and
4
+ * the Intelligence side. It exists so platform *semantics* (how to render IR to
5
+ * a native payload, and — later — how to normalize a native event to the
6
+ * neutral ingress shape) live in ONE place, instead of being duplicated between
7
+ * a credentialed local adapter (Bolt/discord.js) and the Connector Outbox /
8
+ * webhook ingress.
9
+ *
10
+ * Only the two creds/connection-bound concerns stay per-side: the transport
11
+ * (who holds the platform connection) and the credentialed send. The codec
12
+ * excludes both — `renderEgress` is pure (IR → native payload); the actual
13
+ * send happens in the Connector Outbox with Intelligence-owned credentials.
14
+ *
15
+ * TODO(OSS-363): add `normalizeIngress(raw): NeutralEvent` once the pure Slack
16
+ * ingress mapping (mention stripping, stable event-id derivation, real-user
17
+ * filtering, field extraction) is extracted from the Bolt listener so both the
18
+ * local adapter and Intelligence's webhook ingress consume the same logic.
19
+ */
20
+ export interface PlatformCodec {
21
+ readonly platform: string;
22
+ /** IR → native payload. Pure; opaque to bot core. */
23
+ renderEgress(ir: BotNode[]): unknown;
24
+ }
25
+ //# sourceMappingURL=codec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codec.d.ts","sourceRoot":"","sources":["../src/codec.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAC;AAEvD;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,qDAAqD;IACrD,YAAY,CAAC,EAAE,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC;CACtC"}
package/dist/codec.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,73 @@
1
+ import type { InferSchemaOutput, ObjectSchema } from "./standard-schema.js";
2
+ import type { Thread, PlatformUser, ModalView } from "@copilotkit/channels-ui";
3
+ /**
4
+ * Context handed to a slash-command handler. `text` is the raw argument string
5
+ * (the form Slack delivers); `options` is the parsed, typed form, populated by
6
+ * surfaces that deliver structured args natively (e.g. Discord). On text-only
7
+ * surfaces `options` is empty — read `text` there.
8
+ */
9
+ export interface CommandContext<TOptions = Record<string, never>> {
10
+ /** The conversation the command was invoked in. */
11
+ thread: Thread;
12
+ /** The invoked command name, normalized (no leading slash, lower-cased). */
13
+ command: string;
14
+ /** Raw argument string after the command name. */
15
+ text: string;
16
+ /** Parsed, typed options (empty on surfaces that only deliver `text`). */
17
+ options: TOptions;
18
+ /** The invoking user, when the surface provides it. */
19
+ user?: PlatformUser;
20
+ platform: string;
21
+ /** Open a modal in response to this command (requires the surface's trigger; `undefined` when unavailable). */
22
+ openModal?(view: ModalView): Promise<{
23
+ ok: boolean;
24
+ error?: string;
25
+ }>;
26
+ }
27
+ /**
28
+ * A slash command. Defined like a {@link import("./tools.js").BotTool}: a name,
29
+ * an optional Standard Schema for typed options, and a handler. The `options`
30
+ * schema maps natively to surfaces with structured args (Discord) and is used
31
+ * to register/validate there; on text-only surfaces (Slack) args arrive via
32
+ * `ctx.text`.
33
+ */
34
+ export interface BotCommand<Schema extends ObjectSchema = ObjectSchema> {
35
+ /** Command name without the leading slash (e.g. `"triage"`). Matched case-insensitively. */
36
+ name: string;
37
+ /** Human description — help text, and the registration label on surfaces like Discord. */
38
+ description?: string;
39
+ /** Optional Standard Schema for typed options. */
40
+ options?: Schema;
41
+ handler(ctx: CommandContext<InferSchemaOutput<Schema>>): void | Promise<void>;
42
+ }
43
+ /**
44
+ * Define a {@link BotCommand} with full type inference: `ctx.options` is
45
+ * inferred from `options`.
46
+ *
47
+ * ```ts
48
+ * const triage = defineBotCommand({
49
+ * name: "triage",
50
+ * description: "Summarize and file the current thread.",
51
+ * options: z.object({ priority: z.enum(["low", "high"]).optional() }),
52
+ * async handler({ thread, text, options }) {
53
+ * await thread.runAgent({ prompt: `Triage: ${text}`, });
54
+ * },
55
+ * });
56
+ * ```
57
+ */
58
+ export declare function defineBotCommand<Schema extends ObjectSchema>(command: BotCommand<Schema>): BotCommand<Schema>;
59
+ /**
60
+ * Platform-neutral descriptor an adapter may use to register a command with the
61
+ * surface (e.g. Discord's application-command API). Produced from a
62
+ * {@link BotCommand} by {@link toCommandSpec}.
63
+ */
64
+ export interface CommandSpec {
65
+ name: string;
66
+ description: string;
67
+ /** JSON Schema for the options, or `undefined` for a free-text command. */
68
+ options?: Record<string, unknown>;
69
+ }
70
+ /** Normalize a command name for matching: drop a leading slash, lower-case, trim. */
71
+ export declare function normalizeCommandName(name: string): string;
72
+ export declare function toCommandSpec(command: BotCommand): CommandSpec;
73
+ //# sourceMappingURL=commands.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../src/commands.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAE5E,OAAO,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAE/E;;;;;GAKG;AACH,MAAM,WAAW,cAAc,CAAC,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC;IAC9D,mDAAmD;IACnD,MAAM,EAAE,MAAM,CAAC;IACf,4EAA4E;IAC5E,OAAO,EAAE,MAAM,CAAC;IAChB,kDAAkD;IAClD,IAAI,EAAE,MAAM,CAAC;IACb,0EAA0E;IAC1E,OAAO,EAAE,QAAQ,CAAC;IAClB,uDAAuD;IACvD,IAAI,CAAC,EAAE,YAAY,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,+GAA+G;IAC/G,SAAS,CAAC,CAAC,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACvE;AAED;;;;;;GAMG;AACH,MAAM,WAAW,UAAU,CAAC,MAAM,SAAS,YAAY,GAAG,YAAY;IACpE,4FAA4F;IAC5F,IAAI,EAAE,MAAM,CAAC;IACb,0FAA0F;IAC1F,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,kDAAkD;IAClD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,GAAG,EAAE,cAAc,CAAC,iBAAiB,CAAC,MAAM,CAAC,CAAC,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/E;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,SAAS,YAAY,EAC1D,OAAO,EAAE,UAAU,CAAC,MAAM,CAAC,GAC1B,UAAU,CAAC,MAAM,CAAC,CAEpB;AAED;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,2EAA2E;IAC3E,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AAED,qFAAqF;AACrF,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAMzD;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,UAAU,GAAG,WAAW,CAU9D"}
@@ -0,0 +1,38 @@
1
+ import { toJsonSchema } from "./standard-schema.js";
2
+ /**
3
+ * Define a {@link BotCommand} with full type inference: `ctx.options` is
4
+ * inferred from `options`.
5
+ *
6
+ * ```ts
7
+ * const triage = defineBotCommand({
8
+ * name: "triage",
9
+ * description: "Summarize and file the current thread.",
10
+ * options: z.object({ priority: z.enum(["low", "high"]).optional() }),
11
+ * async handler({ thread, text, options }) {
12
+ * await thread.runAgent({ prompt: `Triage: ${text}`, });
13
+ * },
14
+ * });
15
+ * ```
16
+ */
17
+ export function defineBotCommand(command) {
18
+ return command;
19
+ }
20
+ /** Normalize a command name for matching: drop a leading slash, lower-case, trim. */
21
+ export function normalizeCommandName(name) {
22
+ // Collapse "-"→"_" so a command routes the same whether invoked as
23
+ // /file-issue (Slack/Discord allow hyphens) or /file_issue (Telegram forbids
24
+ // them; its adapter converts on registration). This is the ROUTING key only —
25
+ // per-adapter display names are decided in each adapter / `toCommandSpec`.
26
+ return name.trim().replace(/^\//, "").toLowerCase().replace(/-/g, "_");
27
+ }
28
+ export function toCommandSpec(command) {
29
+ return {
30
+ // Preserve hyphens here (display/registration name): Discord and Slack
31
+ // accept them, so `/file-issue` stays `/file-issue` there. Telegram's
32
+ // adapter converts hyphens to underscores itself; routing still matches
33
+ // because `normalizeCommandName` collapses both forms.
34
+ name: command.name.trim().replace(/^\//, "").toLowerCase(),
35
+ description: command.description ?? "",
36
+ options: command.options ? toJsonSchema(command.options) : undefined,
37
+ };
38
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=commands.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"commands.test.d.ts","sourceRoot":"","sources":["../src/commands.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,23 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { normalizeCommandName, toCommandSpec, defineBotCommand, } from "./commands.js";
3
+ describe("normalizeCommandName", () => {
4
+ it("strips a leading slash, lowercases, and collapses hyphens to underscores", () => {
5
+ expect(normalizeCommandName("/File-Issue")).toBe("file_issue");
6
+ expect(normalizeCommandName("file-issue")).toBe("file_issue");
7
+ expect(normalizeCommandName("file_issue")).toBe("file_issue");
8
+ expect(normalizeCommandName("agent")).toBe("agent");
9
+ });
10
+ it("routes /file-issue and /file_issue to the same key (so Telegram's converted name still matches)", () => {
11
+ expect(normalizeCommandName("file-issue")).toBe(normalizeCommandName("file_issue"));
12
+ });
13
+ });
14
+ describe("toCommandSpec", () => {
15
+ it("preserves hyphens in the display name (Slack/Discord allow them)", () => {
16
+ const cmd = defineBotCommand({
17
+ name: "file-issue",
18
+ description: "File an issue",
19
+ handler: async () => { },
20
+ });
21
+ expect(toCommandSpec(cmd).name).toBe("file-issue");
22
+ });
23
+ });
@@ -0,0 +1,171 @@
1
+ import type { PlatformAdapter, ModalSubmitResult } from "./platform-adapter.js";
2
+ import type { ActionStore } from "./action-store.js";
3
+ import type { StateStore } from "./state/state-store.js";
4
+ import type { BotTool, ContextEntry } from "./tools.js";
5
+ import type { BotCommand, CommandContext } from "./commands.js";
6
+ import { Thread } from "./thread.js";
7
+ import type { AbstractAgent } from "@ag-ui/client";
8
+ import type { InteractionContext, IncomingMessage, PlatformUser, EmojiValue, ComponentFn, MessageRef } from "@copilotkit/channels-ui";
9
+ import { Transcripts } from "./transcripts.js";
10
+ import type { Identity, TranscriptsConfig } from "./transcripts.js";
11
+ import type { StandardSchemaV1, InferSchemaOutput } from "./standard-schema.js";
12
+ export type LockConflictDecision = "drop" | "force";
13
+ /**
14
+ * Any `@copilotkit/channels-ui` component function, regardless of its props type.
15
+ * Accepting `(props: never)` lets a component with required, strongly-typed
16
+ * props (e.g. `({ title }: { title: string }) => …`) be passed to
17
+ * `createBot({ components })` without a cast — the registry only ever calls it
18
+ * with the props persisted in the store, so the specific shape isn't needed here.
19
+ */
20
+ export type BotComponent = (props: never) => ReturnType<ComponentFn>;
21
+ export type BotHandler<TState = unknown> = (ctx: {
22
+ thread: StatefulThread<TState>;
23
+ message: IncomingMessage;
24
+ }) => void | Promise<void>;
25
+ /** Handler for a "conversation opened" lifecycle event (e.g. the Slack assistant pane). */
26
+ export type ThreadStartHandler<TState = unknown> = (ctx: {
27
+ thread: StatefulThread<TState>;
28
+ user?: PlatformUser;
29
+ }) => void | Promise<void>;
30
+ /** Event passed to an `onReaction` handler. */
31
+ export interface ReactionEvent {
32
+ /** Normalized name when recognized, else the raw platform token. */
33
+ emoji: EmojiValue;
34
+ /** Platform-native token. */
35
+ rawEmoji: string;
36
+ /** true = added, false = removed. */
37
+ added: boolean;
38
+ /** The reacting user, when the platform reports one. */
39
+ user?: PlatformUser;
40
+ messageId: string;
41
+ /** Update-capable ref to the reacted message (`thread.update(messageRef, ui)`). */
42
+ messageRef: MessageRef;
43
+ threadId?: string;
44
+ thread: Thread;
45
+ adapter: PlatformAdapter;
46
+ raw: unknown;
47
+ }
48
+ export type ReactionHandler = (evt: ReactionEvent) => void | Promise<void>;
49
+ /** Event passed to an `onModalSubmit` handler. */
50
+ export interface ModalSubmitEvent {
51
+ callbackId: string;
52
+ values: Record<string, unknown>;
53
+ user?: PlatformUser;
54
+ /** Present when the submission carried a conversation context. */
55
+ thread?: Thread;
56
+ privateMetadata?: string;
57
+ raw: unknown;
58
+ }
59
+ export type ModalSubmitHandler = (evt: ModalSubmitEvent) => ModalSubmitResult | void | Promise<ModalSubmitResult | void>;
60
+ /** Event passed to an `onModalClose` handler. */
61
+ export interface ModalCloseEvent {
62
+ callbackId: string;
63
+ user?: PlatformUser;
64
+ privateMetadata?: string;
65
+ raw: unknown;
66
+ }
67
+ export type ModalCloseHandler = (evt: ModalCloseEvent) => void | Promise<void>;
68
+ /** The per-thread state type implied by the configured `store.state` schema. */
69
+ type ThreadStateOf<TSchema extends StandardSchemaV1 | undefined> = TSchema extends StandardSchemaV1 ? InferSchemaOutput<TSchema> : unknown;
70
+ /** A Thread whose state()/setState() are narrowed to the configured state type. */
71
+ export type StatefulThread<TState> = Omit<Thread, "setState" | "state"> & {
72
+ setState(value: TState): Promise<void>;
73
+ state(): Promise<TState | undefined>;
74
+ };
75
+ /**
76
+ * Persistence and per-thread state configuration. Groups the pluggable
77
+ * backend, optional state schema, transcript storage, and turn-lock/dedup
78
+ * tuning under a single `store` option.
79
+ */
80
+ export interface StoreConfig<TStateSchema extends StandardSchemaV1 | undefined = undefined> {
81
+ /** Pluggable persistence backend. Defaults to in-memory MemoryStore (lost on restart). */
82
+ adapter?: StateStore;
83
+ /** Standard Schema for per-thread state. When set, thread.state()/setState() are typed to its output and setState validates at runtime. */
84
+ state?: TStateSchema;
85
+ /** Resolve a stable cross-platform identity key (e.g. email). Paired with `transcripts`. */
86
+ identity?: Identity;
87
+ /** Cross-platform transcript storage config. Paired with `identity`. */
88
+ transcripts?: TranscriptsConfig;
89
+ /** What to do when a turn arrives while a prior turn on the same conversationKey is processing. */
90
+ onLockConflict?: LockConflictDecision | ((conversationKey: string, message: IncomingMessage) => LockConflictDecision | Promise<LockConflictDecision>);
91
+ /** TTL (ms) for the per-conversation turn lock. Default 60_000. */
92
+ lockTtl?: number;
93
+ /** TTL (ms) for the inbound event dedup window. Default 300_000. */
94
+ dedupTtl?: number;
95
+ }
96
+ export interface CreateBotOptions<TStateSchema extends StandardSchemaV1 | undefined = undefined> {
97
+ /**
98
+ * Project-unique bot identifier. Required for managed (Intelligence-delivered)
99
+ * bots — it ties the runtime declaration to the Intelligence setup — and
100
+ * optional for local/custom adapters. Validated by the managed runtime
101
+ * (`startManagedBots`), not here.
102
+ */
103
+ name?: string;
104
+ /**
105
+ * Adapters supplied at construction. Optional — adapters can also be attached
106
+ * before `start()` via {@link Bot.addAdapter} (the managed runtime uses this).
107
+ */
108
+ adapters?: PlatformAdapter[];
109
+ agent?: AbstractAgent | ((threadId: string) => AbstractAgent);
110
+ /** @deprecated Pass `store.adapter` instead. */
111
+ actionStore?: ActionStore;
112
+ tools?: BotTool[];
113
+ context?: ContextEntry[];
114
+ /**
115
+ * Named JSX components used in interactive messages. Registering them here
116
+ * lets the bot re-render and re-fire their handlers after a restart (durable
117
+ * actions); without registration, a click on a message posted before the
118
+ * restart degrades to "action expired".
119
+ */
120
+ components?: BotComponent[];
121
+ /** Slash commands. Forwarded to adapters that support them; ignored elsewhere. */
122
+ commands?: BotCommand[];
123
+ /** Persistence, per-thread state schema, transcripts, and lock/dedup tuning. */
124
+ store?: StoreConfig<TStateSchema>;
125
+ }
126
+ export interface Bot<TState = unknown> {
127
+ /** Project-unique identifier from `createBot({ name })`; used by the managed runtime. */
128
+ readonly name?: string;
129
+ /** Declared slash-command names (normalized). Surfaced for managed activation metadata. */
130
+ readonly commandNames: string[];
131
+ onMention(h: BotHandler<TState>): void;
132
+ onMessage(h: BotHandler<TState>): void;
133
+ /**
134
+ * A conversation surface opened (e.g. the Slack assistant pane). Greet, set
135
+ * suggested prompts, set a title, or run the agent. Adapters without the
136
+ * concept never fire this.
137
+ */
138
+ onThreadStarted(h: ThreadStartHandler<TState>): void;
139
+ /** Handle clicks on a specific action `id`. `ctx.action.value` is typed as `TValue`. */
140
+ onInteraction<TValue = unknown>(id: string, h: (ctx: InteractionContext<TValue>) => void | Promise<void>): void;
141
+ /**
142
+ * Handle an agent interrupt (an `on_interrupt` custom event). `payload` is the
143
+ * event's value; pass `TPayload` to type it, e.g.
144
+ * `onInterrupt<{ question: string }>("ask", ...)`.
145
+ */
146
+ onInterrupt<TPayload = unknown>(eventName: string, h: (args: {
147
+ payload: TPayload;
148
+ thread: StatefulThread<TState>;
149
+ }) => void | Promise<void>): void;
150
+ /** Register a slash command (with optional typed options). */
151
+ onCommand(command: BotCommand): void;
152
+ /** Register a free-text slash command by name. */
153
+ onCommand(name: string, handler: (ctx: CommandContext) => void | Promise<void>): void;
154
+ /** React to emoji reactions. Pass emoji name(s) for a specific match, or omit for a catch-all. */
155
+ onReaction(handler: ReactionHandler): void;
156
+ onReaction(emoji: EmojiValue | EmojiValue[], handler: ReactionHandler): void;
157
+ /** Handle a modal submission for `callbackId`. Return `{ errors }` to keep it open. */
158
+ onModalSubmit(callbackId: string, handler: ModalSubmitHandler): void;
159
+ /** Handle a modal dismissal for `callbackId` (Slack `view_closed`). */
160
+ onModalClose(callbackId: string, handler: ModalCloseHandler): void;
161
+ tool(t: BotTool): void;
162
+ /** Attach an adapter before `start()`. Throws if called after the bot has started. */
163
+ addAdapter(adapter: PlatformAdapter): void;
164
+ start(): Promise<void>;
165
+ stop(): Promise<void>;
166
+ /** Cross-platform transcript store. Append, list, and delete entries per user. */
167
+ transcripts: Transcripts;
168
+ }
169
+ export declare function createBot<TStateSchema extends StandardSchemaV1 | undefined = undefined>(opts: CreateBotOptions<TStateSchema>): Bot<ThreadStateOf<TStateSchema>>;
170
+ export {};
171
+ //# sourceMappingURL=create-bot.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-bot.d.ts","sourceRoot":"","sources":["../src/create-bot.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=create-bot.foundations.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-bot.foundations.test.d.ts","sourceRoot":"","sources":["../src/create-bot.foundations.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,147 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { createBot } from "./create-bot.js";
3
+ import { FakeAdapter } from "./testing/fake-adapter.js";
4
+ import { FakeAgent } from "./testing/fake-agent.js";
5
+ import { MemoryStore } from "./state/memory-store.js";
6
+ const tick = () => new Promise((r) => setTimeout(r, 0));
7
+ describe("createBot — optional adapters + addAdapter", () => {
8
+ it("starts with no adapters and runs one added before start()", async () => {
9
+ const fake = new FakeAdapter();
10
+ const bot = createBot({ agent: () => new FakeAgent() });
11
+ bot.addAdapter(fake);
12
+ await bot.start();
13
+ expect(fake.started).toBe(true);
14
+ });
15
+ it("throws when addAdapter is called after start()", async () => {
16
+ const bot = createBot({
17
+ adapters: [new FakeAdapter()],
18
+ agent: () => new FakeAgent(),
19
+ });
20
+ await bot.start();
21
+ expect(() => bot.addAdapter(new FakeAdapter())).toThrow(/start/i);
22
+ });
23
+ it("is idempotent: a second start() does not re-start adapters or rebuild state", async () => {
24
+ const fake = new FakeAdapter();
25
+ const bot = createBot({
26
+ adapters: [fake],
27
+ agent: () => new FakeAgent(),
28
+ });
29
+ const startSpy = vi.spyOn(fake, "start");
30
+ await bot.start();
31
+ const transcriptsAfterFirst = bot.transcripts;
32
+ await bot.start(); // second call must be a no-op
33
+ expect(startSpy).toHaveBeenCalledTimes(1);
34
+ // Same transcript-store instance → state (locks/dedup/actions) not wiped.
35
+ expect(bot.transcripts).toBe(transcriptsAfterFirst);
36
+ });
37
+ it("allows a real restart after stop() (start → stop → start re-inits)", async () => {
38
+ const fake = new FakeAdapter();
39
+ const bot = createBot({
40
+ adapters: [fake],
41
+ agent: () => new FakeAgent(),
42
+ });
43
+ const startSpy = vi.spyOn(fake, "start");
44
+ await bot.start();
45
+ await bot.stop();
46
+ await bot.start(); // stop() cleared `started`, so this is a real restart
47
+ expect(startSpy).toHaveBeenCalledTimes(2);
48
+ });
49
+ });
50
+ describe("createBot — transcripts deferred to start()", () => {
51
+ it("throws if bot.transcripts is accessed before start()", () => {
52
+ const bot = createBot({
53
+ adapters: [new FakeAdapter()],
54
+ agent: () => new FakeAgent(),
55
+ store: {
56
+ adapter: new MemoryStore(),
57
+ identity: () => "u@x.com",
58
+ transcripts: {},
59
+ },
60
+ });
61
+ expect(() => bot.transcripts).toThrow(/start/i);
62
+ });
63
+ });
64
+ describe("createBot — store resolution", () => {
65
+ it("uses an adapter-provided stateStore when no explicit store.adapter", async () => {
66
+ const adapterStore = new MemoryStore();
67
+ // Seed the adapter's store via a throwaway bot so we can prove the real
68
+ // bot reads from that exact instance.
69
+ const seeder = createBot({
70
+ adapters: [new FakeAdapter()],
71
+ agent: () => new FakeAgent(),
72
+ store: {
73
+ adapter: adapterStore,
74
+ identity: () => "u@x.com",
75
+ transcripts: {},
76
+ },
77
+ });
78
+ await seeder.start();
79
+ await seeder.transcripts.append({ platform: "fake", conversationKey: "c" }, { role: "user", text: "seeded" }, { userKey: "u@x.com" });
80
+ const fake = new FakeAdapter();
81
+ fake.stateStore = adapterStore;
82
+ const bot = createBot({
83
+ adapters: [fake],
84
+ agent: () => new FakeAgent(),
85
+ store: { identity: () => "u@x.com", transcripts: {} },
86
+ });
87
+ await bot.start();
88
+ const entries = await bot.transcripts.list({ userKey: "u@x.com" });
89
+ expect(entries.map((e) => e.text)).toContain("seeded");
90
+ });
91
+ it("explicit store.adapter wins over an adapter-provided one, silently", async () => {
92
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => { });
93
+ const fake = new FakeAdapter();
94
+ fake.stateStore = new MemoryStore();
95
+ const explicit = new MemoryStore();
96
+ const bot = createBot({
97
+ adapters: [fake],
98
+ agent: () => new FakeAgent(),
99
+ store: {
100
+ adapter: explicit,
101
+ identity: () => "u@x.com",
102
+ transcripts: {},
103
+ },
104
+ });
105
+ await bot.start();
106
+ expect(warn).not.toHaveBeenCalled();
107
+ warn.mockRestore();
108
+ });
109
+ it("warns when ≥2 adapters provide a stateStore", async () => {
110
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => { });
111
+ const a = new FakeAdapter({ platform: "a" });
112
+ a.stateStore = new MemoryStore();
113
+ const b = new FakeAdapter({ platform: "b" });
114
+ b.stateStore = new MemoryStore();
115
+ const bot = createBot({
116
+ adapters: [a, b],
117
+ agent: () => new FakeAgent(),
118
+ });
119
+ await bot.start();
120
+ expect(warn).toHaveBeenCalledWith(expect.stringContaining("state store"));
121
+ warn.mockRestore();
122
+ });
123
+ });
124
+ describe("createBot — id propagation to handler context", () => {
125
+ it("threads turnId/deliveryId from IncomingTurn onto message", async () => {
126
+ const fake = new FakeAdapter();
127
+ const bot = createBot({ adapters: [fake], agent: () => new FakeAgent() });
128
+ let seen = {};
129
+ bot.onMessage(async ({ message }) => {
130
+ seen = {
131
+ turnId: message.turnId,
132
+ deliveryId: message.deliveryId,
133
+ eventId: message.eventId,
134
+ };
135
+ });
136
+ await bot.start();
137
+ fake.emitTurn({
138
+ userText: "hi",
139
+ conversationKey: "c1",
140
+ eventId: "e1",
141
+ turnId: "t1",
142
+ deliveryId: "d1",
143
+ });
144
+ await tick();
145
+ expect(seen).toEqual({ turnId: "t1", deliveryId: "d1", eventId: "e1" });
146
+ });
147
+ });