@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.
- package/LICENSE +21 -0
- package/README.md +204 -0
- package/dist/action-registry.d.ts +59 -0
- package/dist/action-registry.d.ts.map +1 -0
- package/dist/action-registry.js +217 -0
- package/dist/action-registry.test.d.ts +2 -0
- package/dist/action-registry.test.d.ts.map +1 -0
- package/dist/action-registry.test.js +115 -0
- package/dist/action-store.d.ts +21 -0
- package/dist/action-store.d.ts.map +1 -0
- package/dist/action-store.js +23 -0
- package/dist/action-store.test.d.ts +2 -0
- package/dist/action-store.test.d.ts.map +1 -0
- package/dist/action-store.test.js +27 -0
- package/dist/codec.d.ts +25 -0
- package/dist/codec.d.ts.map +1 -0
- package/dist/codec.js +1 -0
- package/dist/commands.d.ts +73 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/commands.js +38 -0
- package/dist/commands.test.d.ts +2 -0
- package/dist/commands.test.d.ts.map +1 -0
- package/dist/commands.test.js +23 -0
- package/dist/create-bot.d.ts +171 -0
- package/dist/create-bot.d.ts.map +1 -0
- package/dist/create-bot.foundations.test.d.ts +2 -0
- package/dist/create-bot.foundations.test.d.ts.map +1 -0
- package/dist/create-bot.foundations.test.js +147 -0
- package/dist/create-bot.js +576 -0
- package/dist/create-bot.test.d.ts +2 -0
- package/dist/create-bot.test.d.ts.map +1 -0
- package/dist/create-bot.test.js +936 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +32 -0
- package/dist/mint-id.d.ts +3 -0
- package/dist/mint-id.d.ts.map +1 -0
- package/dist/mint-id.js +20 -0
- package/dist/mint-id.test.d.ts +2 -0
- package/dist/mint-id.test.d.ts.map +1 -0
- package/dist/mint-id.test.js +16 -0
- package/dist/modal-submit.test.d.ts +2 -0
- package/dist/modal-submit.test.d.ts.map +1 -0
- package/dist/modal-submit.test.js +68 -0
- package/dist/platform-adapter.d.ts +312 -0
- package/dist/platform-adapter.d.ts.map +1 -0
- package/dist/platform-adapter.js +1 -0
- package/dist/platform-adapter.test.d.ts +2 -0
- package/dist/platform-adapter.test.d.ts.map +1 -0
- package/dist/platform-adapter.test.js +40 -0
- package/dist/reactions.test.d.ts +2 -0
- package/dist/reactions.test.d.ts.map +1 -0
- package/dist/reactions.test.js +197 -0
- package/dist/run-loop.d.ts +41 -0
- package/dist/run-loop.d.ts.map +1 -0
- package/dist/run-loop.js +102 -0
- package/dist/run-loop.test.d.ts +2 -0
- package/dist/run-loop.test.d.ts.map +1 -0
- package/dist/run-loop.test.js +125 -0
- package/dist/standard-schema.d.ts +52 -0
- package/dist/standard-schema.d.ts.map +1 -0
- package/dist/standard-schema.js +66 -0
- package/dist/state/duration.d.ts +3 -0
- package/dist/state/duration.d.ts.map +1 -0
- package/dist/state/duration.js +16 -0
- package/dist/state/duration.test.d.ts +2 -0
- package/dist/state/duration.test.d.ts.map +1 -0
- package/dist/state/duration.test.js +13 -0
- package/dist/state/kv-action-store.d.ts +7 -0
- package/dist/state/kv-action-store.d.ts.map +1 -0
- package/dist/state/kv-action-store.js +9 -0
- package/dist/state/kv-action-store.test.d.ts +2 -0
- package/dist/state/kv-action-store.test.d.ts.map +1 -0
- package/dist/state/kv-action-store.test.js +21 -0
- package/dist/state/memory-store.d.ts +41 -0
- package/dist/state/memory-store.d.ts.map +1 -0
- package/dist/state/memory-store.js +110 -0
- package/dist/state/memory-store.test.d.ts +2 -0
- package/dist/state/memory-store.test.d.ts.map +1 -0
- package/dist/state/memory-store.test.js +22 -0
- package/dist/state/state-conversation-store.d.ts +14 -0
- package/dist/state/state-conversation-store.d.ts.map +1 -0
- package/dist/state/state-conversation-store.js +22 -0
- package/dist/state/state-conversation-store.test.d.ts +2 -0
- package/dist/state/state-conversation-store.test.d.ts.map +1 -0
- package/dist/state/state-conversation-store.test.js +58 -0
- package/dist/state/state-store.d.ts +63 -0
- package/dist/state/state-store.d.ts.map +1 -0
- package/dist/state/state-store.js +1 -0
- package/dist/telemetry/bot-telemetry.d.ts +31 -0
- package/dist/telemetry/bot-telemetry.d.ts.map +1 -0
- package/dist/telemetry/bot-telemetry.js +70 -0
- package/dist/telemetry/bot-telemetry.test.d.ts +2 -0
- package/dist/telemetry/bot-telemetry.test.d.ts.map +1 -0
- package/dist/telemetry/bot-telemetry.test.js +56 -0
- package/dist/telemetry/create-bot-telemetry.test.d.ts +2 -0
- package/dist/telemetry/create-bot-telemetry.test.d.ts.map +1 -0
- package/dist/telemetry/create-bot-telemetry.test.js +71 -0
- package/dist/telemetry/e2e-telemetry.test.d.ts +2 -0
- package/dist/telemetry/e2e-telemetry.test.d.ts.map +1 -0
- package/dist/telemetry/e2e-telemetry.test.js +56 -0
- package/dist/telemetry/events-catalog.test.d.ts +2 -0
- package/dist/telemetry/events-catalog.test.d.ts.map +1 -0
- package/dist/telemetry/events-catalog.test.js +8 -0
- package/dist/telemetry/install-id.d.ts +8 -0
- package/dist/telemetry/install-id.d.ts.map +1 -0
- package/dist/telemetry/install-id.js +43 -0
- package/dist/telemetry/install-id.test.d.ts +2 -0
- package/dist/telemetry/install-id.test.d.ts.map +1 -0
- package/dist/telemetry/install-id.test.js +43 -0
- package/dist/telemetry/platform-allowlist-coverage.test.d.ts +2 -0
- package/dist/telemetry/platform-allowlist-coverage.test.d.ts.map +1 -0
- package/dist/telemetry/platform-allowlist-coverage.test.js +45 -0
- package/dist/telemetry/sanitize-error.d.ts +3 -0
- package/dist/telemetry/sanitize-error.d.ts.map +1 -0
- package/dist/telemetry/sanitize-error.js +31 -0
- package/dist/telemetry/sanitize-error.test.d.ts +2 -0
- package/dist/telemetry/sanitize-error.test.d.ts.map +1 -0
- package/dist/telemetry/sanitize-error.test.js +33 -0
- package/dist/testing/fake-adapter.d.ts +137 -0
- package/dist/testing/fake-adapter.d.ts.map +1 -0
- package/dist/testing/fake-adapter.js +273 -0
- package/dist/testing/fake-agent.d.ts +28 -0
- package/dist/testing/fake-agent.d.ts.map +1 -0
- package/dist/testing/fake-agent.js +36 -0
- package/dist/testing/state-store-conformance.d.ts +4 -0
- package/dist/testing/state-store-conformance.d.ts.map +1 -0
- package/dist/testing/state-store-conformance.js +117 -0
- package/dist/thread-capabilities.test.d.ts +2 -0
- package/dist/thread-capabilities.test.d.ts.map +1 -0
- package/dist/thread-capabilities.test.js +77 -0
- package/dist/thread-ephemeral.test.d.ts +2 -0
- package/dist/thread-ephemeral.test.d.ts.map +1 -0
- package/dist/thread-ephemeral.test.js +45 -0
- package/dist/thread-reactions.test.d.ts +2 -0
- package/dist/thread-reactions.test.d.ts.map +1 -0
- package/dist/thread-reactions.test.js +38 -0
- package/dist/thread.d.ts +152 -0
- package/dist/thread.d.ts.map +1 -0
- package/dist/thread.js +329 -0
- package/dist/thread.test.d.ts +2 -0
- package/dist/thread.test.d.ts.map +1 -0
- package/dist/thread.test.js +83 -0
- package/dist/tools.d.ts +67 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +38 -0
- package/dist/tools.test.d.ts +2 -0
- package/dist/tools.test.d.ts.map +1 -0
- package/dist/tools.test.js +31 -0
- package/dist/transcripts.d.ts +52 -0
- package/dist/transcripts.d.ts.map +1 -0
- package/dist/transcripts.js +67 -0
- package/dist/transcripts.test.d.ts +2 -0
- package/dist/transcripts.test.d.ts.map +1 -0
- package/dist/transcripts.test.js +108 -0
- 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"}
|
package/dist/run-loop.js
ADDED
|
@@ -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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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"}
|