@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,197 @@
1
+ // packages/channels/src/reactions.test.ts
2
+ import { describe, it, expect } from "vitest";
3
+ import { emoji, Message } from "@copilotkit/channels-ui";
4
+ import { createBot } from "./create-bot.js";
5
+ import { FakeAdapter } from "./testing/fake-adapter.js";
6
+ import { MemoryStore } from "./state/memory-store.js";
7
+ const tick = () => new Promise((r) => setTimeout(r, 0));
8
+ describe("bot.onReaction", () => {
9
+ it("routes a specific reaction, normalizing the platform token", async () => {
10
+ const fake = new FakeAdapter();
11
+ const bot = createBot({ adapters: [fake] });
12
+ const seen = [];
13
+ bot.onReaction([emoji.thumbs_up], (evt) => {
14
+ seen.push({ emoji: evt.emoji, raw: evt.rawEmoji, added: evt.added });
15
+ });
16
+ await bot.start();
17
+ // FakeAdapter.platform === "fake": normalizeEmoji falls through, but engine
18
+ // normalizes by adapter.platform. Use a Slack-style token via a fake whose
19
+ // platform normalizes — here we assert passthrough + catch-all instead.
20
+ fake.emitReaction({ rawEmoji: "👍", added: true });
21
+ await tick();
22
+ // "fake" platform isn't in the table, so the raw token passes through.
23
+ expect(seen).toEqual([]); // specific match on "thumbs_up" did not fire for raw "👍" on platform "fake"
24
+ });
25
+ it("fires a catch-all for any reaction and reports added/removed", async () => {
26
+ const fake = new FakeAdapter();
27
+ const bot = createBot({ adapters: [fake] });
28
+ const seen = [];
29
+ bot.onReaction((evt) => {
30
+ seen.push({
31
+ raw: evt.rawEmoji,
32
+ added: evt.added,
33
+ user: evt.user?.id,
34
+ messageId: evt.messageId,
35
+ platform: evt.thread.platform,
36
+ });
37
+ });
38
+ await bot.start();
39
+ fake.emitReaction({
40
+ rawEmoji: "🎉",
41
+ added: true,
42
+ user: { id: "U1" },
43
+ messageId: "m9",
44
+ });
45
+ fake.emitReaction({ rawEmoji: "🎉", added: false, messageId: "m9" });
46
+ await tick();
47
+ expect(seen).toEqual([
48
+ { raw: "🎉", added: true, user: "U1", messageId: "m9", platform: "fake" },
49
+ {
50
+ raw: "🎉",
51
+ added: false,
52
+ user: undefined,
53
+ messageId: "m9",
54
+ platform: "fake",
55
+ },
56
+ ]);
57
+ });
58
+ it("matches a normalized name on a platform in the emoji table", async () => {
59
+ // A fake whose platform === "slack" so normalizeEmoji maps the shortcode.
60
+ const fake = new FakeAdapter();
61
+ Object.defineProperty(fake, "platform", { value: "slack" });
62
+ const bot = createBot({ adapters: [fake] });
63
+ const hits = [];
64
+ bot.onReaction(["thumbs_up"], (evt) => {
65
+ hits.push(evt.emoji);
66
+ });
67
+ await bot.start();
68
+ fake.emitReaction({ rawEmoji: "thumbsup", added: true }); // Slack alias
69
+ await tick();
70
+ expect(hits).toEqual(["thumbs_up"]);
71
+ });
72
+ it("routes a reaction on a posted message to its <Message onReaction>", async () => {
73
+ const fake = new FakeAdapter();
74
+ const bot = createBot({ adapters: [fake] });
75
+ const seen = [];
76
+ bot.onMessage(async ({ thread }) => {
77
+ await thread.post(Message({
78
+ onReaction: (e, r) => {
79
+ seen.push({ emoji: e, added: r.added });
80
+ },
81
+ children: "hi",
82
+ }));
83
+ });
84
+ await bot.start();
85
+ fake.emitTurn({});
86
+ await tick();
87
+ // The handler is a closure, never serialized into the native payload.
88
+ expect(fake.posted[0]?.[0]?.props.onReaction).toBeUndefined();
89
+ // First post → "msg-1" (FakeAdapter counter).
90
+ fake.emitReaction({ rawEmoji: "🎉", added: true, messageId: "msg-1" });
91
+ fake.emitReaction({ rawEmoji: "🎉", added: false, messageId: "msg-1" });
92
+ await tick();
93
+ expect(seen).toEqual([
94
+ { emoji: "🎉", added: true },
95
+ { emoji: "🎉", added: false },
96
+ ]);
97
+ });
98
+ it("re-derives a registered component's onReaction from the store after a restart", async () => {
99
+ const backend = new MemoryStore(); // shared store survives the simulated restart
100
+ const seen = [];
101
+ // A named component so it can be re-registered + re-rendered after restart.
102
+ const Card = () => Message({
103
+ onReaction: (e) => {
104
+ seen.push(e);
105
+ },
106
+ children: "deploy done",
107
+ });
108
+ // Bot 1 posts the component message, persisting a reaction snapshot.
109
+ const fake1 = new FakeAdapter();
110
+ const bot1 = createBot({
111
+ adapters: [fake1],
112
+ store: { adapter: backend },
113
+ components: [Card],
114
+ });
115
+ bot1.onMessage(async ({ thread }) => {
116
+ // A component element ({ type: fn }) — the path that persists, unlike a
117
+ // pre-rendered Message() node.
118
+ await thread.post({ type: Card, props: {} });
119
+ });
120
+ await bot1.start();
121
+ fake1.emitTurn({});
122
+ await tick();
123
+ // "Restart": a fresh bot + registry sharing the same store, Card re-registered.
124
+ // Its reaction hot cache is empty, so it must resolve via the durable snapshot.
125
+ const fake2 = new FakeAdapter();
126
+ const bot2 = createBot({
127
+ adapters: [fake2],
128
+ store: { adapter: backend },
129
+ components: [Card],
130
+ });
131
+ await bot2.start();
132
+ fake2.emitReaction({ rawEmoji: "🎉", added: true, messageId: "msg-1" });
133
+ await tick();
134
+ expect(seen).toEqual(["🎉"]);
135
+ });
136
+ it("gives the handler a thread to post new UI and the reacted message's ref", async () => {
137
+ const fake = new FakeAdapter();
138
+ const bot = createBot({ adapters: [fake] });
139
+ let seenRefId;
140
+ bot.onMessage(async ({ thread }) => {
141
+ await thread.post(Message({
142
+ onReaction: async (_e, r) => {
143
+ seenRefId = r.messageRef.id;
144
+ await r.thread.post("thanks for the reaction"); // post new UI like onClick can
145
+ },
146
+ children: "hi",
147
+ }));
148
+ });
149
+ await bot.start();
150
+ fake.emitTurn({});
151
+ await tick();
152
+ const before = fake.posted.length;
153
+ fake.emitReaction({ rawEmoji: "🎉", added: true, messageId: "msg-1" });
154
+ await tick();
155
+ // The handler posted a second message via its thread.
156
+ expect(fake.posted.length).toBe(before + 1);
157
+ // …and received an update-capable ref to the reacted message (fallback id here).
158
+ expect(seenRefId).toBe("msg-1");
159
+ });
160
+ it("does not fire a message handler for a reaction on a different message", async () => {
161
+ const fake = new FakeAdapter();
162
+ const bot = createBot({ adapters: [fake] });
163
+ let fired = false;
164
+ bot.onMessage(async ({ thread }) => {
165
+ await thread.post(Message({
166
+ onReaction: () => {
167
+ fired = true;
168
+ },
169
+ children: "hi",
170
+ }));
171
+ });
172
+ await bot.start();
173
+ fake.emitTurn({});
174
+ await tick();
175
+ fake.emitReaction({ rawEmoji: "🎉", added: true, messageId: "other" });
176
+ await tick();
177
+ expect(fired).toBe(false);
178
+ });
179
+ it("normalizes a raw-token filter (unicode / slack alias) to canonical", async () => {
180
+ // Caller registers a raw unicode token; ingress normalizes the inbound
181
+ // Slack alias to the canonical "thumbs_up", so the filter must too.
182
+ const fake = new FakeAdapter();
183
+ Object.defineProperty(fake, "platform", { value: "slack" });
184
+ const bot = createBot({ adapters: [fake] });
185
+ const hits = [];
186
+ bot.onReaction(["👍"], (evt) => {
187
+ hits.push(evt.emoji);
188
+ });
189
+ bot.onReaction(["thumbsup"], (evt) => {
190
+ hits.push(`alias:${evt.emoji}`);
191
+ });
192
+ await bot.start();
193
+ fake.emitReaction({ rawEmoji: "thumbsup", added: true }); // Slack alias
194
+ await tick();
195
+ expect(hits).toEqual(["thumbs_up", "alias:thumbs_up"]);
196
+ });
197
+ });
@@ -0,0 +1,41 @@
1
+ import type { AbstractAgent } from "@ag-ui/client";
2
+ import type { RunRenderer, CapturedToolCall, CapturedInterrupt } from "./platform-adapter.js";
3
+ import type { BotTool, BotToolContext, AgentToolDescriptor, ContextEntry } from "./tools.js";
4
+ export interface RunLoopArgs {
5
+ agent: AbstractAgent;
6
+ renderer: RunRenderer;
7
+ tools: Map<string, BotTool>;
8
+ toolDescriptors: AgentToolDescriptor[];
9
+ context: ContextEntry[];
10
+ /** ctx passed to tool.handler (thread + platform). */
11
+ makeToolCtx: (call: CapturedToolCall) => BotToolContext;
12
+ /** Invoke the registered onInterrupt handler (posts a picker); the loop then ends. */
13
+ handleInterrupt?: (interrupt: CapturedInterrupt) => Promise<void> | void;
14
+ isAborted?: () => boolean;
15
+ /** Hard cap on loop iterations. Default 6. */
16
+ maxIterations?: number;
17
+ /** When re-entering via thread.resume, the resume command to replay. */
18
+ initialResume?: {
19
+ resume: unknown;
20
+ };
21
+ }
22
+ /**
23
+ * Drive the agent, executing frontend-tool calls and re-invoking until the
24
+ * agent stops calling them (or we hit the iteration cap). On a captured
25
+ * LangGraph-style interrupt the loop posts the picker via `handleInterrupt`
26
+ * and returns immediately ("ack-first") — `thread.resume` re-enters later
27
+ * with `initialResume` set.
28
+ */
29
+ export declare function runAgentLoop(args: RunLoopArgs): Promise<{
30
+ iterations: number;
31
+ interrupted: boolean;
32
+ }>;
33
+ /**
34
+ * If the agent's latest message isn't already the assistant message that
35
+ * issued these tool calls, append one. AG-UI's agent middleware *should*
36
+ * populate this from the streamed events, but we defensively reconcile here
37
+ * so the next `runAgent` sees a valid transcript even on backends that don't.
38
+ */
39
+ export declare function ensureAssistantToolCallMessage(agent: AbstractAgent, calls: ReadonlyArray<CapturedToolCall>): void;
40
+ export declare function pushToolResult(agent: AbstractAgent, toolCallId: string, content: string): void;
41
+ //# sourceMappingURL=run-loop.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run-loop.d.ts","sourceRoot":"","sources":["../src/run-loop.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAEnD,OAAO,KAAK,EACV,WAAW,EACX,gBAAgB,EAChB,iBAAiB,EAClB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EACV,OAAO,EACP,cAAc,EACd,mBAAmB,EACnB,YAAY,EACb,MAAM,YAAY,CAAC;AAGpB,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,aAAa,CAAC;IACrB,QAAQ,EAAE,WAAW,CAAC;IACtB,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC5B,eAAe,EAAE,mBAAmB,EAAE,CAAC;IACvC,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,sDAAsD;IACtD,WAAW,EAAE,CAAC,IAAI,EAAE,gBAAgB,KAAK,cAAc,CAAC;IACxD,sFAAsF;IACtF,eAAe,CAAC,EAAE,CAAC,SAAS,EAAE,iBAAiB,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACzE,SAAS,CAAC,EAAE,MAAM,OAAO,CAAC;IAC1B,8CAA8C;IAC9C,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,wEAAwE;IACxE,aAAa,CAAC,EAAE;QAAE,MAAM,EAAE,OAAO,CAAA;KAAE,CAAC;CACrC;AAED;;;;;;GAMG;AACH,wBAAsB,YAAY,CAChC,IAAI,EAAE,WAAW,GAChB,OAAO,CAAC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,OAAO,CAAA;CAAE,CAAC,CAsEvD;AAED;;;;;GAKG;AACH,wBAAgB,8BAA8B,CAC5C,KAAK,EAAE,aAAa,EACpB,KAAK,EAAE,aAAa,CAAC,gBAAgB,CAAC,GACrC,IAAI,CA4BN;AAED,wBAAgB,cAAc,CAC5B,KAAK,EAAE,aAAa,EACpB,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,MAAM,GACd,IAAI,CAQN"}
@@ -0,0 +1,102 @@
1
+ import { parseToolArgs, stringifyHandlerResult } from "./tools.js";
2
+ /**
3
+ * Drive the agent, executing frontend-tool calls and re-invoking until the
4
+ * agent stops calling them (or we hit the iteration cap). On a captured
5
+ * LangGraph-style interrupt the loop posts the picker via `handleInterrupt`
6
+ * and returns immediately ("ack-first") — `thread.resume` re-enters later
7
+ * with `initialResume` set.
8
+ */
9
+ export async function runAgentLoop(args) {
10
+ const { agent, renderer, tools, toolDescriptors, context, makeToolCtx, handleInterrupt, isAborted, initialResume, } = args;
11
+ const maxIterations = args.maxIterations ?? 6;
12
+ const executed = new Set();
13
+ let resume = initialResume;
14
+ for (let i = 0; i < maxIterations; i++) {
15
+ if (resume) {
16
+ await agent.runAgent({ forwardedProps: { command: resume } }, renderer.subscriber);
17
+ resume = undefined;
18
+ }
19
+ else {
20
+ await agent.runAgent({ tools: toolDescriptors, context: context }, renderer.subscriber);
21
+ }
22
+ if (isAborted?.())
23
+ return { iterations: i + 1, interrupted: false };
24
+ const pending = renderer.getPendingInterrupt();
25
+ if (pending) {
26
+ renderer.clearPendingInterrupt();
27
+ if (handleInterrupt)
28
+ await handleInterrupt(pending);
29
+ // ack-first: picker posted; thread.resume re-enters later
30
+ return { iterations: i + 1, interrupted: true };
31
+ }
32
+ const calls = renderer
33
+ .getCapturedToolCalls()
34
+ .filter((c) => tools.has(c.toolCallName) && !executed.has(c.toolCallId));
35
+ if (calls.length === 0)
36
+ return { iterations: i + 1, interrupted: false };
37
+ ensureAssistantToolCallMessage(agent, calls);
38
+ for (const call of calls) {
39
+ const tool = tools.get(call.toolCallName);
40
+ let result;
41
+ const parsed = await parseToolArgs(tool.parameters, call.toolCallArgs);
42
+ if (!parsed.ok) {
43
+ result = JSON.stringify({
44
+ error: `invalid arguments: ${parsed.error}`,
45
+ });
46
+ }
47
+ else {
48
+ try {
49
+ result = stringifyHandlerResult(await tool.handler(parsed.value, makeToolCtx(call)));
50
+ }
51
+ catch (err) {
52
+ result = JSON.stringify({ error: err.message });
53
+ }
54
+ }
55
+ pushToolResult(agent, call.toolCallId, result);
56
+ executed.add(call.toolCallId);
57
+ }
58
+ }
59
+ return { iterations: maxIterations, interrupted: false };
60
+ }
61
+ /**
62
+ * If the agent's latest message isn't already the assistant message that
63
+ * issued these tool calls, append one. AG-UI's agent middleware *should*
64
+ * populate this from the streamed events, but we defensively reconcile here
65
+ * so the next `runAgent` sees a valid transcript even on backends that don't.
66
+ */
67
+ export function ensureAssistantToolCallMessage(agent, calls) {
68
+ const messages = agent.messages;
69
+ const last = messages[messages.length - 1];
70
+ const lastIsAssistantWithCalls = last !== undefined &&
71
+ last.role === "assistant" &&
72
+ Array.isArray(last.toolCalls);
73
+ if (lastIsAssistantWithCalls) {
74
+ const existing = (last.toolCalls ?? []).map((tc) => tc.id);
75
+ const allPresent = calls.every((c) => existing.includes(c.toolCallId));
76
+ if (allPresent)
77
+ return;
78
+ }
79
+ const assistant = {
80
+ id: `${calls[0].toolCallId}-assistant`,
81
+ role: "assistant",
82
+ content: "",
83
+ toolCalls: calls.map((c) => ({
84
+ id: c.toolCallId,
85
+ type: "function",
86
+ function: {
87
+ name: c.toolCallName,
88
+ arguments: JSON.stringify(c.toolCallArgs),
89
+ },
90
+ })),
91
+ };
92
+ agent.addMessage(assistant);
93
+ }
94
+ export function pushToolResult(agent, toolCallId, content) {
95
+ const toolMessage = {
96
+ id: `${toolCallId}-result`,
97
+ role: "tool",
98
+ toolCallId,
99
+ content,
100
+ };
101
+ agent.addMessage(toolMessage);
102
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=run-loop.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run-loop.test.d.ts","sourceRoot":"","sources":["../src/run-loop.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,125 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { z } from "zod";
3
+ import { runAgentLoop } from "./run-loop.js";
4
+ import { makeFakeRunRenderer } from "./testing/fake-adapter.js";
5
+ import { FakeAgent } from "./testing/fake-agent.js";
6
+ const toolDescriptors = [];
7
+ const context = [];
8
+ describe("runAgentLoop", () => {
9
+ it("executes a frontend tool call and pushes the result, then terminates", async () => {
10
+ const renderer = makeFakeRunRenderer();
11
+ const recorded = [];
12
+ const echo = {
13
+ name: "echo",
14
+ description: "echo back",
15
+ parameters: z.object({ msg: z.string() }),
16
+ handler: (args) => {
17
+ recorded.push(args);
18
+ return { ok: true };
19
+ },
20
+ };
21
+ const tools = new Map([["echo", echo]]);
22
+ // Step 1: agent emits an `echo` tool call, then finishes.
23
+ // Step 2: agent just finishes (no further tool calls) -> loop terminates.
24
+ const agent = new FakeAgent([
25
+ (sub) => {
26
+ sub.onToolCallEndEvent?.({
27
+ event: { toolCallId: "t1" },
28
+ toolCallName: "echo",
29
+ toolCallArgs: { msg: "hi" },
30
+ });
31
+ sub.onRunFinishedEvent?.({ event: {} });
32
+ },
33
+ (sub) => {
34
+ sub.onRunFinishedEvent?.({ event: {} });
35
+ },
36
+ ]);
37
+ await runAgentLoop({
38
+ agent,
39
+ renderer,
40
+ tools,
41
+ toolDescriptors,
42
+ context,
43
+ makeToolCtx: () => ({ thread: {}, platform: "fake" }),
44
+ });
45
+ expect(recorded).toEqual([{ msg: "hi" }]);
46
+ expect(agent.runAgentCalls).toBe(2);
47
+ const toolResult = agent.messages.find((m) => m.role === "tool");
48
+ expect(toolResult).toBeDefined();
49
+ expect(toolResult.toolCallId).toBe("t1");
50
+ });
51
+ it("posts the picker via handleInterrupt and returns without running tools", async () => {
52
+ const renderer = makeFakeRunRenderer();
53
+ const tools = new Map();
54
+ const handleInterrupt = vi.fn();
55
+ const agent = new FakeAgent([
56
+ (sub) => {
57
+ sub.onCustomEvent?.({
58
+ event: { name: "on_interrupt", value: { q: 1 } },
59
+ });
60
+ sub.onRunFinishedEvent?.({ event: {} });
61
+ },
62
+ ]);
63
+ await runAgentLoop({
64
+ agent,
65
+ renderer,
66
+ tools,
67
+ toolDescriptors,
68
+ context,
69
+ makeToolCtx: () => ({ thread: {}, platform: "fake" }),
70
+ handleInterrupt,
71
+ });
72
+ expect(handleInterrupt).toHaveBeenCalledTimes(1);
73
+ expect(handleInterrupt).toHaveBeenCalledWith({
74
+ eventName: "on_interrupt",
75
+ value: { q: 1 },
76
+ });
77
+ expect(agent.runAgentCalls).toBe(1);
78
+ expect(agent.messages.some((m) => m.role === "tool")).toBe(false);
79
+ });
80
+ it("returns interrupted=true and an iteration count when the agent interrupts", async () => {
81
+ const renderer = makeFakeRunRenderer();
82
+ const tools = new Map();
83
+ const handleInterrupt = vi.fn();
84
+ const agent = new FakeAgent([
85
+ (sub) => {
86
+ sub.onCustomEvent?.({
87
+ event: { name: "on_interrupt", value: { q: 1 } },
88
+ });
89
+ sub.onRunFinishedEvent?.({ event: {} });
90
+ },
91
+ ]);
92
+ const args = {
93
+ agent,
94
+ renderer,
95
+ tools,
96
+ toolDescriptors,
97
+ context,
98
+ makeToolCtx: () => ({ thread: {}, platform: "fake" }),
99
+ handleInterrupt,
100
+ };
101
+ const result = await runAgentLoop(args);
102
+ expect(result.interrupted).toBe(true);
103
+ expect(result.iterations).toBeGreaterThanOrEqual(1);
104
+ });
105
+ it("returns interrupted=false on a normal completion", async () => {
106
+ const renderer = makeFakeRunRenderer();
107
+ const tools = new Map();
108
+ const agent = new FakeAgent([
109
+ (sub) => {
110
+ sub.onRunFinishedEvent?.({ event: {} });
111
+ },
112
+ ]);
113
+ const args = {
114
+ agent,
115
+ renderer,
116
+ tools,
117
+ toolDescriptors,
118
+ context,
119
+ makeToolCtx: () => ({ thread: {}, platform: "fake" }),
120
+ };
121
+ const result = await runAgentLoop(args);
122
+ expect(result.interrupted).toBe(false);
123
+ expect(result.iterations).toBeGreaterThanOrEqual(1);
124
+ });
125
+ });
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Schema-library-agnostic helpers for the Slack SDK.
3
+ *
4
+ * The public API accepts any [Standard Schema](https://standardschema.dev)
5
+ * validator — Zod 3.24+, Valibot v1+, ArkType v2+, and anything else that
6
+ * implements the `~standard` protocol. We deliberately keep `zod` out of
7
+ * the public type surface so consumers aren't locked to a single library
8
+ * or a single Zod major (the v3 ↔ v4 type split otherwise leaks into
9
+ * every signature that mentions `z.ZodType`).
10
+ *
11
+ * These wrap the canonical primitives from `@copilotkit/shared` (the same
12
+ * ones `@copilotkit/core` uses for its `FrontendTool` parameters) plus a
13
+ * couple of Slack-local conveniences.
14
+ */
15
+ import { type StandardSchemaV1, type InferSchemaOutput } from "@copilotkit/shared";
16
+ export type { StandardSchemaV1, InferSchemaOutput };
17
+ /**
18
+ * A Standard Schema whose validated output is an object record.
19
+ *
20
+ * Tool args, component props, HITL props, and interrupt payloads are all
21
+ * objects (and `@copilotkit/core` constrains tool args to
22
+ * `Record<string, unknown>`). Bounding those generics by `ObjectSchema`
23
+ * — rather than coercing a non-object output to `Record` after the fact —
24
+ * makes a primitive/array schema (e.g. `z.string()`) a compile error at
25
+ * the `parameters`/`props` field instead of silently widening it.
26
+ */
27
+ export type ObjectSchema = StandardSchemaV1<unknown, Record<string, unknown>>;
28
+ /**
29
+ * Convert any Standard Schema to a JSON Schema object suitable for an LLM
30
+ * tool/parameter descriptor. Prefers a native JSON Schema (Standard JSON
31
+ * Schema for Valibot/ArkType, `toJSONSchema()` for Zod v4); falls back to
32
+ * `zod-to-json-schema` only for Zod v3 schemas, which don't emit JSON
33
+ * Schema themselves. `$ref`s are inlined (`$refStrategy: "none"`) because
34
+ * most LLM tool-call APIs reject composite `$ref` schemas.
35
+ */
36
+ export declare function toJsonSchema(schema: StandardSchemaV1): Record<string, unknown>;
37
+ /** Discriminated result of validating a value against a Standard Schema. */
38
+ export type SchemaParseResult<T> = {
39
+ ok: true;
40
+ value: T;
41
+ } | {
42
+ ok: false;
43
+ error: string;
44
+ };
45
+ /**
46
+ * Validate a value against a Standard Schema, awaiting async validators.
47
+ * Returns a discriminated result with a path-qualified, human-readable
48
+ * error string on failure — the caller (turn-runner) turns the error into
49
+ * a JSON tool result so the agent can recover from a bad tool call.
50
+ */
51
+ export declare function validateSchema<S extends StandardSchemaV1>(schema: S, value: unknown): Promise<SchemaParseResult<InferSchemaOutput<S>>>;
52
+ //# sourceMappingURL=standard-schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"standard-schema.d.ts","sourceRoot":"","sources":["../src/standard-schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,EAEL,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACvB,MAAM,oBAAoB,CAAC;AAG5B,YAAY,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,CAAC;AAEpD;;;;;;;;;GASG;AACH,MAAM,MAAM,YAAY,GAAG,gBAAgB,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;AAE9E;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAC1B,MAAM,EAAE,gBAAgB,GACvB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAWzB;AAED,4EAA4E;AAC5E,MAAM,MAAM,iBAAiB,CAAC,CAAC,IAC3B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,CAAC,CAAA;CAAE,GACtB;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAEjC;;;;;GAKG;AACH,wBAAsB,cAAc,CAAC,CAAC,SAAS,gBAAgB,EAC7D,MAAM,EAAE,CAAC,EACT,KAAK,EAAE,OAAO,GACb,OAAO,CAAC,iBAAiB,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,CAAC,CAWlD"}
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Schema-library-agnostic helpers for the Slack SDK.
3
+ *
4
+ * The public API accepts any [Standard Schema](https://standardschema.dev)
5
+ * validator — Zod 3.24+, Valibot v1+, ArkType v2+, and anything else that
6
+ * implements the `~standard` protocol. We deliberately keep `zod` out of
7
+ * the public type surface so consumers aren't locked to a single library
8
+ * or a single Zod major (the v3 ↔ v4 type split otherwise leaks into
9
+ * every signature that mentions `z.ZodType`).
10
+ *
11
+ * These wrap the canonical primitives from `@copilotkit/shared` (the same
12
+ * ones `@copilotkit/core` uses for its `FrontendTool` parameters) plus a
13
+ * couple of Slack-local conveniences.
14
+ */
15
+ import { schemaToJsonSchema as sharedSchemaToJsonSchema, } from "@copilotkit/shared";
16
+ import { zodToJsonSchema } from "zod-to-json-schema";
17
+ /**
18
+ * Convert any Standard Schema to a JSON Schema object suitable for an LLM
19
+ * tool/parameter descriptor. Prefers a native JSON Schema (Standard JSON
20
+ * Schema for Valibot/ArkType, `toJSONSchema()` for Zod v4); falls back to
21
+ * `zod-to-json-schema` only for Zod v3 schemas, which don't emit JSON
22
+ * Schema themselves. `$ref`s are inlined (`$refStrategy: "none"`) because
23
+ * most LLM tool-call APIs reject composite `$ref` schemas.
24
+ */
25
+ export function toJsonSchema(schema) {
26
+ return sharedSchemaToJsonSchema(schema, {
27
+ // Adapt `zod-to-json-schema`'s `(schema: ZodType, ...)` signature to
28
+ // shared's `(schema: unknown, ...)` injection point. Only invoked for
29
+ // Zod v3 inputs that don't emit Standard JSON Schema themselves.
30
+ zodToJsonSchema: (s, options) => zodToJsonSchema(s, options),
31
+ });
32
+ }
33
+ /**
34
+ * Validate a value against a Standard Schema, awaiting async validators.
35
+ * Returns a discriminated result with a path-qualified, human-readable
36
+ * error string on failure — the caller (turn-runner) turns the error into
37
+ * a JSON tool result so the agent can recover from a bad tool call.
38
+ */
39
+ export async function validateSchema(schema, value) {
40
+ const raw = schema["~standard"].validate(value);
41
+ // The Standard Schema spec permits `validate` to return any thenable,
42
+ // not strictly a native `Promise` (e.g. a Promise from another realm or
43
+ // a library's custom async result). Detect thenables, not `Promise`
44
+ // instances, so async validators are always awaited.
45
+ const result = isThenable(raw) ? await raw : raw;
46
+ if (result.issues) {
47
+ return { ok: false, error: formatIssues(result.issues) };
48
+ }
49
+ return { ok: true, value: result.value };
50
+ }
51
+ function isThenable(value) {
52
+ return (value != null && typeof value.then === "function");
53
+ }
54
+ /** Format Standard Schema issues as `path: message; path: message`. */
55
+ function formatIssues(issues) {
56
+ return issues
57
+ .map((issue) => {
58
+ const path = (issue.path ?? [])
59
+ .map((segment) => typeof segment === "object" && segment !== null
60
+ ? String(segment.key)
61
+ : String(segment))
62
+ .join(".");
63
+ return `${path || "(root)"}: ${issue.message}`;
64
+ })
65
+ .join("; ");
66
+ }
@@ -0,0 +1,3 @@
1
+ /** Parse "30d" | "12h" | "500ms" | 5000 → milliseconds. Throws on bad input. */
2
+ export declare function parseDuration(input: string | number): number;
3
+ //# sourceMappingURL=duration.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"duration.d.ts","sourceRoot":"","sources":["../../src/state/duration.ts"],"names":[],"mappings":"AAOA,gFAAgF;AAChF,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAK5D"}
@@ -0,0 +1,16 @@
1
+ const UNITS = {
2
+ ms: 1,
3
+ s: 1000,
4
+ m: 60_000,
5
+ h: 3_600_000,
6
+ d: 86_400_000,
7
+ };
8
+ /** Parse "30d" | "12h" | "500ms" | 5000 → milliseconds. Throws on bad input. */
9
+ export function parseDuration(input) {
10
+ if (typeof input === "number")
11
+ return input;
12
+ const m = /^(\d+)(ms|s|m|h|d)$/.exec(input.trim());
13
+ if (!m)
14
+ throw new Error(`Invalid duration: ${input}`);
15
+ return Number(m[1]) * UNITS[m[2]];
16
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=duration.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"duration.test.d.ts","sourceRoot":"","sources":["../../src/state/duration.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,13 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { parseDuration } from "./duration.js";
3
+ describe("parseDuration", () => {
4
+ it("parses units", () => {
5
+ expect(parseDuration("500ms")).toBe(500);
6
+ expect(parseDuration("30s")).toBe(30_000);
7
+ expect(parseDuration("5m")).toBe(300_000);
8
+ expect(parseDuration("2h")).toBe(7_200_000);
9
+ expect(parseDuration("30d")).toBe(2_592_000_000);
10
+ });
11
+ it("passes numbers through", () => expect(parseDuration(1234)).toBe(1234));
12
+ it("throws on garbage", () => expect(() => parseDuration("nope")).toThrow());
13
+ });
@@ -0,0 +1,7 @@
1
+ import type { ActionStore } from "../action-store.js";
2
+ import type { StateStore } from "./state-store.js";
3
+ /** Back the legacy ActionStore shape with state.kv. Durable iff `state` is durable. */
4
+ export declare function kvActionStore(state: StateStore, opts?: {
5
+ defaultTtlMs?: number;
6
+ }): ActionStore;
7
+ //# sourceMappingURL=kv-action-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"kv-action-store.d.ts","sourceRoot":"","sources":["../../src/state/kv-action-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAkB,MAAM,oBAAoB,CAAC;AACtE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAInD,uFAAuF;AACvF,wBAAgB,aAAa,CAC3B,KAAK,EAAE,UAAU,EACjB,IAAI,CAAC,EAAE;IAAE,YAAY,CAAC,EAAE,MAAM,CAAA;CAAE,GAC/B,WAAW,CAOb"}