@notionhq/workers 0.2.0 → 0.4.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 (60) hide show
  1. package/dist/capabilities/ai_connector.d.ts +97 -0
  2. package/dist/capabilities/ai_connector.d.ts.map +1 -0
  3. package/dist/capabilities/ai_connector.js +54 -0
  4. package/dist/capabilities/ai_connector.test.d.ts +2 -0
  5. package/dist/capabilities/ai_connector.test.d.ts.map +1 -0
  6. package/dist/capabilities/automation.d.ts.map +1 -1
  7. package/dist/capabilities/automation.js +11 -11
  8. package/dist/capabilities/context.d.ts +2 -1
  9. package/dist/capabilities/context.d.ts.map +1 -1
  10. package/dist/capabilities/context.js +21 -3
  11. package/dist/capabilities/output.d.ts +5 -0
  12. package/dist/capabilities/output.d.ts.map +1 -0
  13. package/dist/capabilities/output.js +10 -0
  14. package/dist/capabilities/stateful-capability.d.ts +30 -0
  15. package/dist/capabilities/stateful-capability.d.ts.map +1 -0
  16. package/dist/capabilities/stateful-capability.js +50 -0
  17. package/dist/capabilities/stateful-capability.test.d.ts +2 -0
  18. package/dist/capabilities/stateful-capability.test.d.ts.map +1 -0
  19. package/dist/capabilities/sync.d.ts +9 -19
  20. package/dist/capabilities/sync.d.ts.map +1 -1
  21. package/dist/capabilities/sync.js +34 -61
  22. package/dist/capabilities/tool.d.ts +21 -0
  23. package/dist/capabilities/tool.d.ts.map +1 -1
  24. package/dist/capabilities/tool.js +8 -22
  25. package/dist/capabilities/webhook.d.ts +101 -0
  26. package/dist/capabilities/webhook.d.ts.map +1 -0
  27. package/dist/capabilities/webhook.js +47 -0
  28. package/dist/error.d.ts +36 -0
  29. package/dist/error.d.ts.map +1 -1
  30. package/dist/error.js +14 -0
  31. package/dist/index.d.ts +5 -1
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +4 -0
  34. package/dist/pacer_internal.d.ts +7 -0
  35. package/dist/pacer_internal.d.ts.map +1 -1
  36. package/dist/pacer_internal.js +17 -0
  37. package/dist/schema.d.ts +28 -11
  38. package/dist/schema.d.ts.map +1 -1
  39. package/dist/schema.js +2 -2
  40. package/dist/worker.d.ts +92 -9
  41. package/dist/worker.d.ts.map +1 -1
  42. package/dist/worker.js +111 -1
  43. package/package.json +2 -2
  44. package/src/capabilities/ai_connector.test.ts +341 -0
  45. package/src/capabilities/ai_connector.ts +194 -0
  46. package/src/capabilities/automation.ts +11 -7
  47. package/src/capabilities/context.ts +45 -3
  48. package/src/capabilities/output.ts +8 -0
  49. package/src/capabilities/stateful-capability.test.ts +25 -0
  50. package/src/capabilities/stateful-capability.ts +87 -0
  51. package/src/capabilities/sync.test.ts +197 -4
  52. package/src/capabilities/sync.ts +58 -87
  53. package/src/capabilities/tool.test.ts +63 -0
  54. package/src/capabilities/tool.ts +28 -13
  55. package/src/capabilities/webhook.ts +148 -0
  56. package/src/error.ts +40 -0
  57. package/src/index.ts +28 -1
  58. package/src/pacer_internal.ts +34 -0
  59. package/src/schema.ts +29 -12
  60. package/src/worker.ts +139 -10
package/dist/worker.js CHANGED
@@ -1,7 +1,12 @@
1
+ import { createAiConnectorCapability } from "./capabilities/ai_connector.js";
1
2
  import { createAutomationCapability } from "./capabilities/automation.js";
2
3
  import { createOAuthCapability } from "./capabilities/oauth.js";
3
4
  import { createSyncCapability } from "./capabilities/sync.js";
4
5
  import { createToolCapability } from "./capabilities/tool.js";
6
+ import {
7
+ createWebhookCapability,
8
+ WebhookVerificationError
9
+ } from "./capabilities/webhook.js";
5
10
  import { pacerWait } from "./pacer_internal.js";
6
11
  import { createRequire } from "node:module";
7
12
  const SDK_VERSION = (() => {
@@ -111,7 +116,74 @@ class Worker {
111
116
  */
112
117
  sync(key, config) {
113
118
  this.#validateUniqueKey(key);
114
- const capability = createSyncCapability(key, config);
119
+ const capability = createSyncCapability(
120
+ key,
121
+ config,
122
+ Array.from(this.#pacers.values())
123
+ );
124
+ this.#capabilities.set(key, capability);
125
+ return capability;
126
+ }
127
+ /**
128
+ * Register an AI connector capability.
129
+ *
130
+ * Example:
131
+ *
132
+ * ```ts
133
+ * const worker = new Worker();
134
+ * export default worker;
135
+ *
136
+ * worker.aiConnector("supportChats", {
137
+ * aiConnectorId: "connector-record-id",
138
+ * archetype: "chat",
139
+ * mode: "incremental",
140
+ * schedule: "15m",
141
+ * execute: async (state) => ({
142
+ * changes: [
143
+ * {
144
+ * type: "upsert",
145
+ * key: "thread-1",
146
+ * record: {
147
+ * createdAt: "2026-01-01T00:00:00Z",
148
+ * updatedAt: "2026-01-01T00:00:00Z",
149
+ * channel: {
150
+ * id: "channel-1",
151
+ * name: "Support",
152
+ * },
153
+ * threadId: "thread-1",
154
+ * messages: [
155
+ * {
156
+ * id: "message-1",
157
+ * content: "Hello from support",
158
+ * timestamp: "2026-01-01T00:00:00Z",
159
+ * author: {
160
+ * id: "user-1",
161
+ * username: "alice",
162
+ * },
163
+ * },
164
+ * ],
165
+ * },
166
+ * },
167
+ * ],
168
+ * hasMore: false,
169
+ * }),
170
+ * });
171
+ * ```
172
+ *
173
+ * `aiConnectorId` must be the Notion record ID of an existing custom
174
+ * connector in the workspace where you deploy this worker.
175
+ *
176
+ * @param key - The unique key for this capability.
177
+ * @param config - The AI connector configuration.
178
+ * @returns The capability object.
179
+ */
180
+ aiConnector(key, config) {
181
+ this.#validateUniqueKey(key);
182
+ const capability = createAiConnectorCapability(
183
+ key,
184
+ config,
185
+ Array.from(this.#pacers.values())
186
+ );
115
187
  this.#capabilities.set(key, capability);
116
188
  return capability;
117
189
  }
@@ -181,6 +253,40 @@ class Worker {
181
253
  this.#capabilities.set(key, capability);
182
254
  return capability;
183
255
  }
256
+ /**
257
+ * Register a webhook capability.
258
+ *
259
+ * Webhooks expose an HTTP endpoint that external services can call.
260
+ * When hit, the request is processed by the worker's execute function.
261
+ *
262
+ * Example:
263
+ *
264
+ * ```ts
265
+ * const worker = new Worker();
266
+ * export default worker;
267
+ *
268
+ * worker.webhook("onGithubPush", {
269
+ * title: "GitHub Push Webhook",
270
+ * description: "Handles push events from GitHub",
271
+ * execute: async (events, { notion }) => {
272
+ * for (const event of events) {
273
+ * console.log("Received webhook:", event.body);
274
+ * console.log("Headers:", event.headers);
275
+ * }
276
+ * },
277
+ * })
278
+ * ```
279
+ *
280
+ * @param key - The unique key for this capability. Used as the webhook name in the URL.
281
+ * @param config - The webhook configuration.
282
+ * @returns The capability object.
283
+ */
284
+ webhook(key, config) {
285
+ this.#validateUniqueKey(key);
286
+ const capability = createWebhookCapability(key, config);
287
+ this.#capabilities.set(key, capability);
288
+ return capability;
289
+ }
184
290
  /**
185
291
  * Register an OAuth capability.
186
292
  *
@@ -264,6 +370,9 @@ class Worker {
264
370
  `Cannot run OAuth capability "${key}" - OAuth capabilities only provide configuration`
265
371
  );
266
372
  }
373
+ if (!capability.handler) {
374
+ throw new Error(`Capability "${key}" cannot be executed`);
375
+ }
267
376
  return capability.handler(context, options);
268
377
  }
269
378
  #validateUniqueKey(key) {
@@ -276,5 +385,6 @@ class Worker {
276
385
  }
277
386
  }
278
387
  export {
388
+ WebhookVerificationError,
279
389
  Worker
280
390
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@notionhq/workers",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "An SDK for building workers for the Notion Workers platform",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -70,7 +70,7 @@
70
70
  "vitest": "^4.0.8"
71
71
  },
72
72
  "peerDependencies": {
73
- "@notionhq/client": "^2.2.15"
73
+ "@notionhq/client": ">=2.2.15"
74
74
  },
75
75
  "dependencies": {
76
76
  "ajv": "^8.17.1",
@@ -0,0 +1,341 @@
1
+ import {
2
+ afterEach,
3
+ beforeEach,
4
+ describe,
5
+ expect,
6
+ it,
7
+ type Mock,
8
+ vi,
9
+ } from "vitest";
10
+ import { ExecutionError } from "../error.js";
11
+ import { getPacerState } from "../pacer_internal.js";
12
+ import { Worker } from "../worker.js";
13
+ import type { AiConnectorExecutionResult } from "./ai_connector.js";
14
+ import { createAiConnectorCapability } from "./ai_connector.js";
15
+
16
+ describe("Worker.aiConnector", () => {
17
+ let stdoutSpy: Mock<typeof process.stdout.write>;
18
+
19
+ beforeEach(() => {
20
+ stdoutSpy = vi
21
+ .spyOn(process.stdout, "write")
22
+ .mockImplementation(() => true);
23
+ });
24
+
25
+ afterEach(() => {
26
+ vi.restoreAllMocks();
27
+ });
28
+
29
+ it("registers an ai_connector capability with normalized config", () => {
30
+ const capability = createAiConnectorCapability("supportChats", {
31
+ aiConnectorId: "connector-record-id",
32
+ archetype: "chat",
33
+ mode: "incremental",
34
+ schedule: "15m",
35
+ execute: async () => ({
36
+ changes: [],
37
+ hasMore: false,
38
+ }),
39
+ });
40
+
41
+ expect(capability._tag).toBe("ai_connector");
42
+ expect(capability.config).toEqual({
43
+ aiConnectorId: "connector-record-id",
44
+ archetype: "chat",
45
+ mode: "incremental",
46
+ schedule: {
47
+ type: "interval",
48
+ intervalMs: 15 * 60 * 1000,
49
+ },
50
+ });
51
+ });
52
+
53
+ it("wraps successful handler output in the success envelope", async () => {
54
+ const capability = createAiConnectorCapability("supportChats", {
55
+ aiConnectorId: "connector-record-id",
56
+ archetype: "chat",
57
+ execute: async () => ({
58
+ changes: [
59
+ {
60
+ type: "upsert",
61
+ key: "thread-1",
62
+ record: {
63
+ createdAt: "2026-01-01T00:00:00Z",
64
+ updatedAt: "2026-01-01T00:00:00Z",
65
+ channel: {
66
+ id: "channel-1",
67
+ name: "Support",
68
+ },
69
+ messages: [
70
+ {
71
+ id: "message-1",
72
+ content: "Hello",
73
+ timestamp: "2026-01-01T00:00:00Z",
74
+ author: {
75
+ id: "user-1",
76
+ username: "alice",
77
+ },
78
+ },
79
+ ],
80
+ },
81
+ },
82
+ ],
83
+ hasMore: false,
84
+ }),
85
+ });
86
+
87
+ await capability.handler();
88
+
89
+ expect(stdoutSpy).toHaveBeenCalledWith(
90
+ expect.stringContaining(`"_tag":"success"`),
91
+ );
92
+ expect(stdoutSpy).toHaveBeenCalledWith(
93
+ expect.stringContaining(`"thread-1"`),
94
+ );
95
+ });
96
+
97
+ it("passes state through from legacy runtime userContext", async () => {
98
+ const execute = vi.fn(async (state?: { cursor: string }) => ({
99
+ changes: [],
100
+ hasMore: false,
101
+ ...(state === undefined ? {} : { nextState: state }),
102
+ }));
103
+ const capability = createAiConnectorCapability("supportChats", {
104
+ aiConnectorId: "connector-record-id",
105
+ archetype: "chat",
106
+ execute,
107
+ });
108
+
109
+ await capability.handler(
110
+ { userContext: { cursor: "cursor-1" } },
111
+ { concreteOutput: true },
112
+ );
113
+
114
+ expect(execute).toHaveBeenCalledWith(
115
+ { cursor: "cursor-1" },
116
+ expect.anything(),
117
+ );
118
+ expect(stdoutSpy).not.toHaveBeenCalled();
119
+ });
120
+
121
+ it("hydrates and persists pacer state", async () => {
122
+ const worker = new Worker();
123
+ const pacer = worker.pacer("api", {
124
+ allowedRequests: 1,
125
+ intervalMs: 1000,
126
+ });
127
+ const capability = worker.aiConnector("supportChats", {
128
+ aiConnectorId: "connector-record-id",
129
+ archetype: "chat",
130
+ execute: async () => {
131
+ await pacer.wait();
132
+ return {
133
+ changes: [],
134
+ hasMore: false,
135
+ };
136
+ },
137
+ });
138
+
139
+ const before = Date.now() - 1000;
140
+ const result = await capability.handler(
141
+ {
142
+ pacers: {
143
+ api: {
144
+ lastScheduledAtMs: before,
145
+ allowedRequests: 1,
146
+ intervalMs: 1000,
147
+ },
148
+ },
149
+ },
150
+ { concreteOutput: true },
151
+ );
152
+
153
+ expect(result).toEqual({
154
+ changes: [],
155
+ hasMore: false,
156
+ nextPacerStates: {
157
+ api: {
158
+ lastScheduledAtMs: expect.any(Number),
159
+ allowedRequests: 1,
160
+ intervalMs: 1000,
161
+ },
162
+ },
163
+ });
164
+ expect(
165
+ result.nextPacerStates.api?.lastScheduledAtMs,
166
+ ).toBeGreaterThanOrEqual(before);
167
+ });
168
+
169
+ describe("pacer local fallback", () => {
170
+ it("seeds pacer state from declarations when runtime context has no pacers", async () => {
171
+ const worker = new Worker();
172
+ const pacer = worker.pacer("myApi", {
173
+ allowedRequests: 10,
174
+ intervalMs: 1000,
175
+ });
176
+
177
+ let pacerWaitCalled = false;
178
+ const capability = worker.aiConnector("supportChats", {
179
+ aiConnectorId: "connector-record-id",
180
+ archetype: "chat",
181
+ execute: async () => {
182
+ await pacer.wait();
183
+ pacerWaitCalled = true;
184
+ return { changes: [], hasMore: false };
185
+ },
186
+ });
187
+
188
+ // No runtimeContext → no pacers from server → should fall back to declarations
189
+ await capability.handler(undefined, { concreteOutput: true });
190
+
191
+ expect(pacerWaitCalled).toBe(true);
192
+ const state = getPacerState();
193
+ expect(state.pacers.myApi).toBeDefined();
194
+ expect(state.pacers.myApi?.allowedRequests).toBe(10);
195
+ expect(state.pacers.myApi?.intervalMs).toBe(1000);
196
+ });
197
+
198
+ it("uses server-provided pacers when available", async () => {
199
+ const worker = new Worker();
200
+ const pacer = worker.pacer("myApi", {
201
+ allowedRequests: 10,
202
+ intervalMs: 1000,
203
+ });
204
+
205
+ let pacerWaitCalled = false;
206
+ const capability = worker.aiConnector("supportChats", {
207
+ aiConnectorId: "connector-record-id",
208
+ archetype: "chat",
209
+ execute: async () => {
210
+ await pacer.wait();
211
+ pacerWaitCalled = true;
212
+ return { changes: [], hasMore: false };
213
+ },
214
+ });
215
+
216
+ // Server provides apportioned budget (e.g., 5 instead of 10)
217
+ await capability.handler(
218
+ {
219
+ pacers: {
220
+ myApi: {
221
+ lastScheduledAtMs: 0,
222
+ allowedRequests: 5,
223
+ intervalMs: 1000,
224
+ },
225
+ },
226
+ },
227
+ { concreteOutput: true },
228
+ );
229
+
230
+ expect(pacerWaitCalled).toBe(true);
231
+ // Should use server-provided value, not the declaration
232
+ expect(getPacerState().pacers.myApi?.allowedRequests).toBe(5);
233
+ });
234
+ });
235
+
236
+ it("wraps handler errors in the shared error envelope", async () => {
237
+ const capability = createAiConnectorCapability("supportChats", {
238
+ aiConnectorId: "connector-record-id",
239
+ archetype: "chat",
240
+ execute: async () => {
241
+ throw new Error("failed");
242
+ },
243
+ });
244
+
245
+ await expect(capability.handler()).rejects.toThrow(ExecutionError);
246
+ expect(stdoutSpy).toHaveBeenCalledWith(
247
+ expect.stringContaining(`"_tag":"error"`),
248
+ );
249
+ });
250
+
251
+ it("accepts chat records at compile time", () => {
252
+ const result = {
253
+ changes: [
254
+ {
255
+ type: "upsert",
256
+ key: "thread-1",
257
+ record: {
258
+ createdAt: "2026-01-01T00:00:00Z",
259
+ updatedAt: "2026-01-01T00:00:00Z",
260
+ channel: {
261
+ id: "channel-1",
262
+ name: "Support",
263
+ },
264
+ messages: [
265
+ {
266
+ id: "message-1",
267
+ content: "Hello",
268
+ timestamp: "2026-01-01T00:00:00Z",
269
+ author: {
270
+ id: "user-1",
271
+ username: "alice",
272
+ },
273
+ },
274
+ ],
275
+ },
276
+ },
277
+ ],
278
+ hasMore: false,
279
+ } satisfies AiConnectorExecutionResult<"chat">;
280
+
281
+ expect(result.hasMore).toBe(false);
282
+ });
283
+
284
+ it("accepts project records with required blocks at compile time", () => {
285
+ const result = {
286
+ changes: [
287
+ {
288
+ type: "upsert",
289
+ key: "project-1",
290
+ record: {
291
+ name: "Roadmap",
292
+ createdAt: "2026-01-01T00:00:00Z",
293
+ updatedAt: "2026-01-01T00:00:00Z",
294
+ blocks: [
295
+ {
296
+ id: "block-1",
297
+ type: "paragraph",
298
+ content: "Ship the roadmap",
299
+ },
300
+ ],
301
+ },
302
+ },
303
+ ],
304
+ hasMore: false,
305
+ } satisfies AiConnectorExecutionResult<"project">;
306
+
307
+ expect(result.changes).toHaveLength(1);
308
+ });
309
+
310
+ it("rejects project records without blocks at compile time", () => {
311
+ const changes = [
312
+ {
313
+ type: "upsert",
314
+ key: "project-1",
315
+ // @ts-expect-error Project records must include blocks.
316
+ record: {
317
+ name: "Roadmap",
318
+ createdAt: "2026-01-01T00:00:00Z",
319
+ updatedAt: "2026-01-01T00:00:00Z",
320
+ },
321
+ },
322
+ ] satisfies AiConnectorExecutionResult<"project">["changes"];
323
+
324
+ expect(changes).toHaveLength(1);
325
+ });
326
+
327
+ it("rejects replace mode at compile time", () => {
328
+ createAiConnectorCapability("supportChats", {
329
+ aiConnectorId: "connector-record-id",
330
+ archetype: "chat",
331
+ // @ts-expect-error AI connector capabilities are incremental-only for now.
332
+ mode: "replace",
333
+ execute: async () => ({
334
+ changes: [],
335
+ hasMore: false,
336
+ }),
337
+ });
338
+
339
+ expect(true).toBe(true);
340
+ });
341
+ });
@@ -0,0 +1,194 @@
1
+ import { ExecutionError } from "../error.js";
2
+ import {
3
+ getPacerState,
4
+ initPacerState,
5
+ type PacerDeclaration,
6
+ type PacerEntry,
7
+ } from "../pacer_internal.js";
8
+ import type { HandlerOptions, Schedule } from "../types.js";
9
+ import type { CapabilityContext } from "./context.js";
10
+ import { createCapabilityContext } from "./context.js";
11
+ import { writeOutput } from "./output.js";
12
+ import {
13
+ parseSchedule,
14
+ type StatefulCapabilityMode,
15
+ type StatefulExecutionResult,
16
+ type RuntimeContext as StatefulRuntimeContext,
17
+ } from "./stateful-capability.js";
18
+
19
+ export type AiConnectorArchetype = "chat" | "project";
20
+ /**
21
+ * AI connector syncs are incremental-only for now.
22
+ *
23
+ * Unlike collection syncs, AI connector ingestion does not yet support
24
+ * replace-style reconciliation semantics, so exposing `replace` here would
25
+ * encourage a misleading reset-and-replay model.
26
+ */
27
+ export type AiConnectorMode = Exclude<StatefulCapabilityMode, "replace">;
28
+
29
+ export type AiConnectorChatAuthor = {
30
+ id: string;
31
+ username: string;
32
+ };
33
+
34
+ export type AiConnectorChatMessage = {
35
+ id: string;
36
+ content: string;
37
+ timestamp: string;
38
+ editedTimestamp?: string;
39
+ author: AiConnectorChatAuthor;
40
+ };
41
+
42
+ export type AiConnectorChatChannel = {
43
+ id: string;
44
+ name: string;
45
+ };
46
+
47
+ export type AiConnectorChatRecord = {
48
+ createdAt: string;
49
+ updatedAt: string;
50
+ externalUrl?: string;
51
+ channel: AiConnectorChatChannel;
52
+ threadId?: string;
53
+ messages: AiConnectorChatMessage[];
54
+ chatType?: string;
55
+ };
56
+
57
+ export type AiConnectorProjectBlock = {
58
+ id: string;
59
+ type: string;
60
+ content: string;
61
+ };
62
+
63
+ export type AiConnectorProjectRecord = {
64
+ name: string;
65
+ createdAt: string;
66
+ updatedAt: string;
67
+ externalUrl?: string;
68
+ priority?: string;
69
+ teams?: string[];
70
+ targetEnd?: string;
71
+ createdBy?: {
72
+ id: string;
73
+ };
74
+ blocks: AiConnectorProjectBlock[];
75
+ };
76
+
77
+ export type AiConnectorRecordByArchetype = {
78
+ chat: AiConnectorChatRecord;
79
+ project: AiConnectorProjectRecord;
80
+ };
81
+
82
+ export type AiConnectorChangeUpsert<A extends AiConnectorArchetype> = {
83
+ type: "upsert";
84
+ key: string;
85
+ record: AiConnectorRecordByArchetype[A];
86
+ };
87
+
88
+ export type AiConnectorChange<A extends AiConnectorArchetype> =
89
+ AiConnectorChangeUpsert<A>;
90
+
91
+ export type AiConnectorExecutionResult<
92
+ A extends AiConnectorArchetype,
93
+ State = unknown,
94
+ > = StatefulExecutionResult<AiConnectorChange<A>, State>;
95
+
96
+ export type AiConnectorConfiguration<
97
+ A extends AiConnectorArchetype,
98
+ State = unknown,
99
+ > = {
100
+ /**
101
+ * The Notion record ID of an existing custom connector in the target workspace.
102
+ *
103
+ * This must be the connector record UUID that Notion created, not an external
104
+ * source identifier.
105
+ */
106
+ aiConnectorId: string;
107
+ archetype: A;
108
+ mode?: AiConnectorMode;
109
+ schedule?: Schedule;
110
+ execute: (
111
+ state: State | undefined,
112
+ context: CapabilityContext,
113
+ ) => Promise<AiConnectorExecutionResult<A, State>>;
114
+ };
115
+
116
+ export type AiConnectorCapability = ReturnType<
117
+ typeof createAiConnectorCapability
118
+ >;
119
+
120
+ type NormalizedAiConnectorConfiguration<A extends AiConnectorArchetype> = {
121
+ aiConnectorId: string;
122
+ archetype: A;
123
+ mode?: AiConnectorMode;
124
+ schedule?: ReturnType<typeof parseSchedule>;
125
+ };
126
+
127
+ type AiConnectorHandlerResult<A extends AiConnectorArchetype, State> = {
128
+ changes: AiConnectorChange<A>[];
129
+ hasMore: boolean;
130
+ nextUserContext?: State;
131
+ nextPacerStates: Record<string, PacerEntry>;
132
+ };
133
+
134
+ export function createAiConnectorCapability<
135
+ A extends AiConnectorArchetype,
136
+ State = unknown,
137
+ >(
138
+ key: string,
139
+ configuration: AiConnectorConfiguration<A, State>,
140
+ pacerDeclarations?: readonly PacerDeclaration[],
141
+ ) {
142
+ return {
143
+ _tag: "ai_connector" as const,
144
+ key,
145
+ config: {
146
+ aiConnectorId: configuration.aiConnectorId,
147
+ archetype: configuration.archetype,
148
+ ...(configuration.mode === undefined ? {} : { mode: configuration.mode }),
149
+ ...(configuration.schedule === undefined
150
+ ? {}
151
+ : { schedule: parseSchedule(configuration.schedule) }),
152
+ } satisfies NormalizedAiConnectorConfiguration<A>,
153
+ async handler(
154
+ runtimeContext?: StatefulRuntimeContext<State>,
155
+ options?: HandlerOptions,
156
+ ) {
157
+ const capabilityContext = createCapabilityContext("ai_connector");
158
+ const state = runtimeContext?.state ?? runtimeContext?.userContext;
159
+
160
+ initPacerState(runtimeContext?.pacers, pacerDeclarations);
161
+
162
+ let executionResult: AiConnectorExecutionResult<A, State>;
163
+ try {
164
+ executionResult = await configuration.execute(state, capabilityContext);
165
+ } catch (err) {
166
+ const error = new ExecutionError(err);
167
+ if (!options?.concreteOutput) {
168
+ writeOutput({
169
+ _tag: "error",
170
+ error: { name: error.name, message: error.message },
171
+ });
172
+ }
173
+ throw error;
174
+ }
175
+
176
+ const result = {
177
+ changes: executionResult.changes,
178
+ hasMore: executionResult.hasMore,
179
+ ...(executionResult.nextState === undefined
180
+ ? {}
181
+ : { nextUserContext: executionResult.nextState }),
182
+ nextPacerStates: getPacerState().pacers,
183
+ } satisfies AiConnectorHandlerResult<A, State>;
184
+
185
+ if (options?.concreteOutput) {
186
+ return result;
187
+ }
188
+
189
+ writeOutput({ _tag: "success", value: result });
190
+
191
+ return result;
192
+ },
193
+ };
194
+ }