@notionhq/workers 0.0.82
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -0
- package/dist/block.d.ts +321 -0
- package/dist/block.d.ts.map +1 -0
- package/dist/block.js +0 -0
- package/dist/builder.d.ts +117 -0
- package/dist/builder.d.ts.map +1 -0
- package/dist/builder.js +168 -0
- package/dist/capabilities/automation.d.ts +91 -0
- package/dist/capabilities/automation.d.ts.map +1 -0
- package/dist/capabilities/automation.js +40 -0
- package/dist/capabilities/automation.test.d.ts +2 -0
- package/dist/capabilities/automation.test.d.ts.map +1 -0
- package/dist/capabilities/context.d.ts +7 -0
- package/dist/capabilities/context.d.ts.map +1 -0
- package/dist/capabilities/context.js +15 -0
- package/dist/capabilities/oauth.d.ts +120 -0
- package/dist/capabilities/oauth.d.ts.map +1 -0
- package/dist/capabilities/oauth.js +55 -0
- package/dist/capabilities/oauth.test.d.ts +2 -0
- package/dist/capabilities/oauth.test.d.ts.map +1 -0
- package/dist/capabilities/sync.d.ts +180 -0
- package/dist/capabilities/sync.d.ts.map +1 -0
- package/dist/capabilities/sync.js +84 -0
- package/dist/capabilities/sync.test.d.ts +2 -0
- package/dist/capabilities/sync.test.d.ts.map +1 -0
- package/dist/capabilities/tool.d.ts +68 -0
- package/dist/capabilities/tool.d.ts.map +1 -0
- package/dist/capabilities/tool.js +174 -0
- package/dist/capabilities/tool.test.d.ts +2 -0
- package/dist/capabilities/tool.test.d.ts.map +1 -0
- package/dist/error.d.ts +12 -0
- package/dist/error.d.ts.map +1 -0
- package/dist/error.js +15 -0
- package/dist/icon-names.d.ts +6 -0
- package/dist/icon-names.d.ts.map +1 -0
- package/dist/icon-names.js +0 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/json-schema.d.ts +117 -0
- package/dist/json-schema.d.ts.map +1 -0
- package/dist/json-schema.js +0 -0
- package/dist/json-schema.test.d.ts +2 -0
- package/dist/json-schema.test.d.ts.map +1 -0
- package/dist/schema.d.ts +178 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +66 -0
- package/dist/types.d.ts +206 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +0 -0
- package/dist/worker.d.ts +177 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +219 -0
- package/package.json +68 -0
- package/src/block.ts +529 -0
- package/src/builder.ts +299 -0
- package/src/capabilities/automation.test.ts +152 -0
- package/src/capabilities/automation.ts +130 -0
- package/src/capabilities/context.ts +23 -0
- package/src/capabilities/oauth.test.ts +52 -0
- package/src/capabilities/oauth.ts +157 -0
- package/src/capabilities/sync.test.ts +104 -0
- package/src/capabilities/sync.ts +311 -0
- package/src/capabilities/tool.test.ts +351 -0
- package/src/capabilities/tool.ts +279 -0
- package/src/error.ts +19 -0
- package/src/icon-names.ts +890 -0
- package/src/index.ts +34 -0
- package/src/json-schema.test.ts +719 -0
- package/src/json-schema.ts +179 -0
- package/src/schema.ts +241 -0
- package/src/types.ts +272 -0
- package/src/worker.ts +285 -0
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import {
|
|
2
|
+
afterEach,
|
|
3
|
+
beforeEach,
|
|
4
|
+
describe,
|
|
5
|
+
expect,
|
|
6
|
+
it,
|
|
7
|
+
type Mock,
|
|
8
|
+
vi,
|
|
9
|
+
} from "vitest";
|
|
10
|
+
import {
|
|
11
|
+
createToolCapability,
|
|
12
|
+
InvalidToolInputError,
|
|
13
|
+
InvalidToolOutputError,
|
|
14
|
+
ToolExecutionError,
|
|
15
|
+
} from "./tool.js";
|
|
16
|
+
|
|
17
|
+
describe("Worker.tool", () => {
|
|
18
|
+
let stdoutSpy: Mock<typeof process.stdout.write>;
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
stdoutSpy = vi
|
|
22
|
+
.spyOn(process.stdout, "write")
|
|
23
|
+
.mockImplementation(() => true);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
vi.restoreAllMocks();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("sync execution", async () => {
|
|
31
|
+
const capability = createToolCapability<{ name: string }, string>(
|
|
32
|
+
"sayHello",
|
|
33
|
+
{
|
|
34
|
+
title: "Say Hello",
|
|
35
|
+
description: "Greet a user",
|
|
36
|
+
schema: {
|
|
37
|
+
type: "object",
|
|
38
|
+
properties: {
|
|
39
|
+
name: { type: "string" },
|
|
40
|
+
},
|
|
41
|
+
required: ["name"],
|
|
42
|
+
additionalProperties: false,
|
|
43
|
+
},
|
|
44
|
+
execute: (input: { name: string }) => {
|
|
45
|
+
return `Hello, ${input.name}!`;
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const result = await capability.handler({ name: "Alice" });
|
|
51
|
+
|
|
52
|
+
expect(result).toEqual({ _tag: "success", value: "Hello, Alice!" });
|
|
53
|
+
|
|
54
|
+
expect(stdoutSpy).toHaveBeenCalledWith(
|
|
55
|
+
`\n<output>"Hello, Alice!"</output>\n`,
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("async execution", async () => {
|
|
60
|
+
const capability = createToolCapability<{ id: number }, { data: string }>(
|
|
61
|
+
"fetchData",
|
|
62
|
+
{
|
|
63
|
+
title: "Fetch Data",
|
|
64
|
+
description: "Fetch data asynchronously",
|
|
65
|
+
schema: {
|
|
66
|
+
type: "object",
|
|
67
|
+
properties: {
|
|
68
|
+
id: { type: "number" },
|
|
69
|
+
},
|
|
70
|
+
required: ["id"],
|
|
71
|
+
additionalProperties: false,
|
|
72
|
+
},
|
|
73
|
+
execute: async (input: { id: number }) => {
|
|
74
|
+
// Simulate async operation
|
|
75
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
76
|
+
return { data: `Data for ID ${input.id}` };
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const result = await capability.handler({ id: 42 });
|
|
82
|
+
|
|
83
|
+
expect(result).toEqual({
|
|
84
|
+
_tag: "success",
|
|
85
|
+
value: { data: "Data for ID 42" },
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
expect(stdoutSpy).toHaveBeenCalledWith(
|
|
89
|
+
`\n<output>{"data":"Data for ID 42"}</output>\n`,
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("execution error", async () => {
|
|
94
|
+
const capability = createToolCapability<Record<string, never>, string>(
|
|
95
|
+
"throwError",
|
|
96
|
+
{
|
|
97
|
+
title: "Throw Error",
|
|
98
|
+
description: "Throws an error",
|
|
99
|
+
schema: {
|
|
100
|
+
type: "object",
|
|
101
|
+
properties: {},
|
|
102
|
+
required: [],
|
|
103
|
+
additionalProperties: false,
|
|
104
|
+
},
|
|
105
|
+
execute: () => {
|
|
106
|
+
throw new Error("Something went wrong");
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
const result = (await capability.handler({})) as {
|
|
112
|
+
_tag: "error";
|
|
113
|
+
error: ToolExecutionError;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
expect(result._tag).toBe("error");
|
|
117
|
+
expect(result.error).toBeInstanceOf(ToolExecutionError);
|
|
118
|
+
expect(result.error.message).toBe("Something went wrong");
|
|
119
|
+
|
|
120
|
+
expect(stdoutSpy).toHaveBeenCalledWith(
|
|
121
|
+
`\n<output>{"_tag":"error","error":{"name":"ToolExecutionError","message":"Something went wrong"}}</output>\n`,
|
|
122
|
+
);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("invalid input", async () => {
|
|
126
|
+
const capability = createToolCapability<{ name: string }, string>(
|
|
127
|
+
"sayHelloStrict",
|
|
128
|
+
{
|
|
129
|
+
title: "Say Hello",
|
|
130
|
+
description: "Requires a name property",
|
|
131
|
+
schema: {
|
|
132
|
+
type: "object",
|
|
133
|
+
properties: {
|
|
134
|
+
name: { type: "string" },
|
|
135
|
+
},
|
|
136
|
+
required: ["name"],
|
|
137
|
+
additionalProperties: false,
|
|
138
|
+
},
|
|
139
|
+
execute: (input: { name: string }) => {
|
|
140
|
+
return `Hello, ${input.name}!`;
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
const result = (await capability.handler({ age: 25 })) as {
|
|
146
|
+
_tag: "error";
|
|
147
|
+
error: InvalidToolInputError;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
expect(result._tag).toBe("error");
|
|
151
|
+
expect(result.error).toBeInstanceOf(InvalidToolInputError);
|
|
152
|
+
expect(result.error.message).toContain("name");
|
|
153
|
+
|
|
154
|
+
expect(stdoutSpy).toHaveBeenCalledWith(
|
|
155
|
+
expect.stringContaining(
|
|
156
|
+
`\n<output>{"_tag":"error","error":{"name":"InvalidToolInputError"`,
|
|
157
|
+
),
|
|
158
|
+
);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("invalid output", async () => {
|
|
162
|
+
const capability = createToolCapability<
|
|
163
|
+
Record<string, never>,
|
|
164
|
+
{ result: string }
|
|
165
|
+
>("invalidOutput", {
|
|
166
|
+
title: "Return Invalid Output",
|
|
167
|
+
description: "Returns output that doesn't match schema",
|
|
168
|
+
schema: {
|
|
169
|
+
type: "object",
|
|
170
|
+
properties: {},
|
|
171
|
+
required: [],
|
|
172
|
+
additionalProperties: false,
|
|
173
|
+
},
|
|
174
|
+
outputSchema: {
|
|
175
|
+
type: "object",
|
|
176
|
+
properties: {
|
|
177
|
+
result: { type: "string" },
|
|
178
|
+
},
|
|
179
|
+
required: ["result"],
|
|
180
|
+
additionalProperties: false,
|
|
181
|
+
},
|
|
182
|
+
// @ts-expect-error Testing invalid output - intentionally returning number instead of string
|
|
183
|
+
execute: () => {
|
|
184
|
+
return { result: 123 };
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const result = (await capability.handler({})) as {
|
|
189
|
+
_tag: "error";
|
|
190
|
+
error: InvalidToolOutputError;
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
expect(result._tag).toBe("error");
|
|
194
|
+
expect(result.error).toBeInstanceOf(InvalidToolOutputError);
|
|
195
|
+
expect(result.error.message).toContain("result");
|
|
196
|
+
|
|
197
|
+
expect(stdoutSpy).toHaveBeenCalledWith(
|
|
198
|
+
expect.stringContaining(
|
|
199
|
+
`\n<output>{"_tag":"error","error":{"name":"InvalidToolOutputError"`,
|
|
200
|
+
),
|
|
201
|
+
);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("rejects schema where required is missing properties", () => {
|
|
205
|
+
expect(() =>
|
|
206
|
+
createToolCapability<{ name: string; age: number }, string>("bad", {
|
|
207
|
+
title: "Bad",
|
|
208
|
+
description: "Missing required",
|
|
209
|
+
schema: {
|
|
210
|
+
type: "object",
|
|
211
|
+
properties: {
|
|
212
|
+
name: { type: "string" },
|
|
213
|
+
age: { type: "number" },
|
|
214
|
+
},
|
|
215
|
+
required: ["name"],
|
|
216
|
+
additionalProperties: false,
|
|
217
|
+
} as never,
|
|
218
|
+
execute: () => "",
|
|
219
|
+
}),
|
|
220
|
+
).toThrow(/properties \[age\] must be listed in "required"/);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("rejects nested schema where required is missing properties", () => {
|
|
224
|
+
expect(() =>
|
|
225
|
+
createToolCapability<{ user: { name: string; email: string } }, string>(
|
|
226
|
+
"bad",
|
|
227
|
+
{
|
|
228
|
+
title: "Bad",
|
|
229
|
+
description: "Nested missing required",
|
|
230
|
+
schema: {
|
|
231
|
+
type: "object",
|
|
232
|
+
properties: {
|
|
233
|
+
user: {
|
|
234
|
+
type: "object",
|
|
235
|
+
properties: {
|
|
236
|
+
name: { type: "string" },
|
|
237
|
+
email: { type: "string" },
|
|
238
|
+
},
|
|
239
|
+
required: ["name"],
|
|
240
|
+
additionalProperties: false,
|
|
241
|
+
} as never,
|
|
242
|
+
},
|
|
243
|
+
required: ["user"],
|
|
244
|
+
additionalProperties: false,
|
|
245
|
+
},
|
|
246
|
+
execute: () => "",
|
|
247
|
+
},
|
|
248
|
+
),
|
|
249
|
+
).toThrow(/at properties\.user.*properties \[email\]/);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it("accepts schema with nullable (string | null) properties", () => {
|
|
253
|
+
const capability = createToolCapability<
|
|
254
|
+
{ name: string; nickname: string | null },
|
|
255
|
+
string
|
|
256
|
+
>("nullable", {
|
|
257
|
+
title: "Nullable",
|
|
258
|
+
description: "Has nullable properties",
|
|
259
|
+
schema: {
|
|
260
|
+
type: "object",
|
|
261
|
+
properties: {
|
|
262
|
+
name: { type: "string" },
|
|
263
|
+
nickname: {
|
|
264
|
+
anyOf: [{ type: "string" }, { type: "null" }],
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
required: ["name", "nickname"],
|
|
268
|
+
additionalProperties: false,
|
|
269
|
+
},
|
|
270
|
+
execute: ({ name, nickname }) => `${name} (${nickname ?? "no nickname"})`,
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
expect(capability.config.schema).toBeDefined();
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("rejects output schema where required is missing properties", () => {
|
|
277
|
+
expect(() =>
|
|
278
|
+
createToolCapability<
|
|
279
|
+
Record<string, never>,
|
|
280
|
+
{ result: string; count: number }
|
|
281
|
+
>("bad", {
|
|
282
|
+
title: "Bad",
|
|
283
|
+
description: "Output missing required",
|
|
284
|
+
schema: {
|
|
285
|
+
type: "object",
|
|
286
|
+
properties: {},
|
|
287
|
+
required: [],
|
|
288
|
+
additionalProperties: false,
|
|
289
|
+
},
|
|
290
|
+
outputSchema: {
|
|
291
|
+
type: "object",
|
|
292
|
+
properties: {
|
|
293
|
+
result: { type: "string" },
|
|
294
|
+
count: { type: "number" },
|
|
295
|
+
},
|
|
296
|
+
required: ["result"],
|
|
297
|
+
additionalProperties: false,
|
|
298
|
+
} as never,
|
|
299
|
+
execute: () => ({ result: "", count: 0 }),
|
|
300
|
+
}),
|
|
301
|
+
).toThrow(/properties \[count\] must be listed in "required"/);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it("invalid output with custom output schema", async () => {
|
|
305
|
+
const capability = createToolCapability<
|
|
306
|
+
{ value: number },
|
|
307
|
+
{ doubled: number; message: string }
|
|
308
|
+
>("customOutput", {
|
|
309
|
+
title: "Custom Output",
|
|
310
|
+
description: "Has custom output schema",
|
|
311
|
+
schema: {
|
|
312
|
+
type: "object",
|
|
313
|
+
properties: {
|
|
314
|
+
value: { type: "number" },
|
|
315
|
+
},
|
|
316
|
+
required: ["value"],
|
|
317
|
+
additionalProperties: false,
|
|
318
|
+
},
|
|
319
|
+
outputSchema: {
|
|
320
|
+
type: "object",
|
|
321
|
+
properties: {
|
|
322
|
+
doubled: { type: "number" },
|
|
323
|
+
message: { type: "string" },
|
|
324
|
+
},
|
|
325
|
+
required: ["doubled", "message"],
|
|
326
|
+
additionalProperties: false,
|
|
327
|
+
},
|
|
328
|
+
// @ts-expect-error Testing invalid output - intentionally missing message property
|
|
329
|
+
execute: (input: { value: number }) => {
|
|
330
|
+
return {
|
|
331
|
+
doubled: input.value * 2,
|
|
332
|
+
};
|
|
333
|
+
},
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
const result = (await capability.handler({ value: 5 })) as {
|
|
337
|
+
_tag: "error";
|
|
338
|
+
error: InvalidToolOutputError;
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
expect(result._tag).toBe("error");
|
|
342
|
+
expect(result.error).toBeInstanceOf(InvalidToolOutputError);
|
|
343
|
+
expect(result.error.message).toContain("message");
|
|
344
|
+
|
|
345
|
+
expect(stdoutSpy).toHaveBeenCalledWith(
|
|
346
|
+
expect.stringContaining(
|
|
347
|
+
`\n<output>{"_tag":"error","error":{"name":"InvalidToolOutputError"`,
|
|
348
|
+
),
|
|
349
|
+
);
|
|
350
|
+
});
|
|
351
|
+
});
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { Ajv } from "ajv";
|
|
2
|
+
import type { AnyJSONSchema, JSONSchema } from "../json-schema.js";
|
|
3
|
+
import type { HandlerOptions, JSONValue } from "../types.js";
|
|
4
|
+
import type { CapabilityContext } from "./context.js";
|
|
5
|
+
import { createCapabilityContext } from "./context.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Validates that every object schema in the tree has all its `properties`
|
|
9
|
+
* keys listed in `required`. Both providers require this.
|
|
10
|
+
*
|
|
11
|
+
* Throws a descriptive error at tool registration time (not request time)
|
|
12
|
+
* so the developer sees it immediately.
|
|
13
|
+
*/
|
|
14
|
+
function validateRequiredProperties(
|
|
15
|
+
schema: AnyJSONSchema,
|
|
16
|
+
path: string = "",
|
|
17
|
+
): void {
|
|
18
|
+
if ("type" in schema && schema.type === "object" && "properties" in schema) {
|
|
19
|
+
const propKeys = Object.keys(schema.properties).sort();
|
|
20
|
+
const required = [...(schema.required ?? [])].sort();
|
|
21
|
+
const missing = propKeys.filter((k) => !required.includes(k));
|
|
22
|
+
if (missing.length > 0) {
|
|
23
|
+
throw new Error(
|
|
24
|
+
`Invalid schema${path ? ` at ${path}` : ""}: ` +
|
|
25
|
+
`properties [${missing.join(", ")}] must be listed in "required". ` +
|
|
26
|
+
"Both model providers require all properties to be required. " +
|
|
27
|
+
'Use anyOf with { type: "null" } for optional fields.',
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Recurse into nested property schemas
|
|
32
|
+
for (const [key, propSchema] of Object.entries(schema.properties)) {
|
|
33
|
+
validateRequiredProperties(
|
|
34
|
+
propSchema as AnyJSONSchema,
|
|
35
|
+
path ? `${path}.properties.${key}` : `properties.${key}`,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Recurse into $defs
|
|
40
|
+
if ("$defs" in schema && schema.$defs) {
|
|
41
|
+
for (const [key, defSchema] of Object.entries(schema.$defs)) {
|
|
42
|
+
validateRequiredProperties(
|
|
43
|
+
defSchema,
|
|
44
|
+
path ? `${path}.$defs.${key}` : `$defs.${key}`,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Recurse into array items
|
|
51
|
+
if ("type" in schema && schema.type === "array" && "items" in schema) {
|
|
52
|
+
validateRequiredProperties(
|
|
53
|
+
schema.items as AnyJSONSchema,
|
|
54
|
+
path ? `${path}.items` : "items",
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Recurse into anyOf branches
|
|
59
|
+
if ("anyOf" in schema && Array.isArray(schema.anyOf)) {
|
|
60
|
+
for (let i = 0; i < schema.anyOf.length; i++) {
|
|
61
|
+
validateRequiredProperties(
|
|
62
|
+
schema.anyOf[i] as AnyJSONSchema,
|
|
63
|
+
path ? `${path}.anyOf[${i}]` : `anyOf[${i}]`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface ToolConfiguration<
|
|
70
|
+
I extends JSONValue,
|
|
71
|
+
O extends JSONValue = JSONValue,
|
|
72
|
+
> {
|
|
73
|
+
title: string;
|
|
74
|
+
description: string;
|
|
75
|
+
schema: JSONSchema<I>;
|
|
76
|
+
outputSchema?: JSONSchema<O>;
|
|
77
|
+
execute: (input: I, context: CapabilityContext) => O | Promise<O>;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* An error returned when the input to a tool doesn't match the input schema.
|
|
82
|
+
*/
|
|
83
|
+
export class InvalidToolInputError extends Error {
|
|
84
|
+
constructor(message: string) {
|
|
85
|
+
super(message);
|
|
86
|
+
this.name = "InvalidToolInputError";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
toJSON() {
|
|
90
|
+
return {
|
|
91
|
+
name: this.name,
|
|
92
|
+
message: this.message,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* An error returned when the output from a tool doesn't match the output schema.
|
|
99
|
+
*/
|
|
100
|
+
export class InvalidToolOutputError extends Error {
|
|
101
|
+
constructor(message: string) {
|
|
102
|
+
super(message);
|
|
103
|
+
this.name = "InvalidToolOutputError";
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
toJSON() {
|
|
107
|
+
return {
|
|
108
|
+
name: this.name,
|
|
109
|
+
message: this.message,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* An error returned when the tool execution fails.
|
|
116
|
+
*/
|
|
117
|
+
export class ToolExecutionError extends Error {
|
|
118
|
+
constructor(message: string) {
|
|
119
|
+
super(message);
|
|
120
|
+
this.name = "ToolExecutionError";
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
toJSON() {
|
|
124
|
+
return {
|
|
125
|
+
name: this.name,
|
|
126
|
+
message: this.message,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export type ToolCapability<
|
|
132
|
+
I extends JSONValue,
|
|
133
|
+
O extends JSONValue = JSONValue,
|
|
134
|
+
> = ReturnType<typeof createToolCapability<I, O>>;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Creates a capability definition for a tool to be used by an agent.
|
|
138
|
+
*
|
|
139
|
+
* @param config - The configuration for the tool.
|
|
140
|
+
* @returns A capability definition for the tool.
|
|
141
|
+
*/
|
|
142
|
+
export function createToolCapability<
|
|
143
|
+
I extends JSONValue,
|
|
144
|
+
O extends JSONValue = JSONValue,
|
|
145
|
+
>(key: string, config: ToolConfiguration<I, O>) {
|
|
146
|
+
validateRequiredProperties(config.schema as AnyJSONSchema);
|
|
147
|
+
if (config.outputSchema) {
|
|
148
|
+
validateRequiredProperties(config.outputSchema as AnyJSONSchema);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const ajv = new Ajv();
|
|
152
|
+
const validateInput = ajv.compile(config.schema);
|
|
153
|
+
const validateOutput = config.outputSchema
|
|
154
|
+
? ajv.compile(config.outputSchema)
|
|
155
|
+
: null;
|
|
156
|
+
|
|
157
|
+
async function handler(input: JSONValue, options: HandlerOptions): Promise<O>;
|
|
158
|
+
async function handler(input: JSONValue): Promise<
|
|
159
|
+
| {
|
|
160
|
+
_tag: "success";
|
|
161
|
+
value: O;
|
|
162
|
+
}
|
|
163
|
+
| {
|
|
164
|
+
_tag: "error";
|
|
165
|
+
error:
|
|
166
|
+
| InvalidToolInputError
|
|
167
|
+
| InvalidToolOutputError
|
|
168
|
+
| ToolExecutionError;
|
|
169
|
+
}
|
|
170
|
+
>;
|
|
171
|
+
async function handler(
|
|
172
|
+
input: JSONValue,
|
|
173
|
+
options?: HandlerOptions,
|
|
174
|
+
): Promise<
|
|
175
|
+
| O
|
|
176
|
+
| {
|
|
177
|
+
_tag: "success";
|
|
178
|
+
value: O;
|
|
179
|
+
}
|
|
180
|
+
| {
|
|
181
|
+
_tag: "error";
|
|
182
|
+
error:
|
|
183
|
+
| InvalidToolInputError
|
|
184
|
+
| InvalidToolOutputError
|
|
185
|
+
| ToolExecutionError;
|
|
186
|
+
}
|
|
187
|
+
> {
|
|
188
|
+
if (!validateInput(input)) {
|
|
189
|
+
// ajv resets the "errors" property on each validation call.
|
|
190
|
+
if (validateInput.errors == null) {
|
|
191
|
+
throw new Error(
|
|
192
|
+
"Unexpected: No validation errors after failed validation",
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const error = new InvalidToolInputError(
|
|
197
|
+
JSON.stringify(validateInput.errors, null, 2),
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
if (options?.concreteOutput) {
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const result = {
|
|
205
|
+
_tag: "error" as const,
|
|
206
|
+
error,
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
process.stdout.write(`\n<output>${JSON.stringify(result)}</output>\n`);
|
|
210
|
+
|
|
211
|
+
return result;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
const capabilityContext = createCapabilityContext();
|
|
216
|
+
const result = await config.execute(input, capabilityContext);
|
|
217
|
+
if (validateOutput && !validateOutput(result)) {
|
|
218
|
+
const error = new InvalidToolOutputError(
|
|
219
|
+
JSON.stringify(validateOutput.errors, null, 2),
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
if (options?.concreteOutput) {
|
|
223
|
+
throw error;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const result = {
|
|
227
|
+
_tag: "error" as const,
|
|
228
|
+
error: new InvalidToolOutputError(
|
|
229
|
+
JSON.stringify(validateOutput.errors, null, 2),
|
|
230
|
+
),
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
process.stdout.write(`\n<output>${JSON.stringify(result)}</output>\n`);
|
|
234
|
+
|
|
235
|
+
return result;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (options?.concreteOutput) {
|
|
239
|
+
return result;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
process.stdout.write(`\n<output>${JSON.stringify(result)}</output>\n`);
|
|
243
|
+
|
|
244
|
+
return {
|
|
245
|
+
_tag: "success" as const,
|
|
246
|
+
value: result,
|
|
247
|
+
};
|
|
248
|
+
} catch (err) {
|
|
249
|
+
const error = new ToolExecutionError(
|
|
250
|
+
err instanceof Error ? err.message : String(err),
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
if (options?.concreteOutput) {
|
|
254
|
+
throw error;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const result = {
|
|
258
|
+
_tag: "error" as const,
|
|
259
|
+
error,
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
process.stdout.write(`\n<output>${JSON.stringify(result)}</output>\n`);
|
|
263
|
+
|
|
264
|
+
return result;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
_tag: "tool" as const,
|
|
270
|
+
key,
|
|
271
|
+
config: {
|
|
272
|
+
title: config.title,
|
|
273
|
+
description: config.description,
|
|
274
|
+
schema: config.schema,
|
|
275
|
+
outputSchema: config.outputSchema,
|
|
276
|
+
},
|
|
277
|
+
handler,
|
|
278
|
+
};
|
|
279
|
+
}
|
package/src/error.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An error that occurred during the execution of a worker capability.
|
|
3
|
+
*/
|
|
4
|
+
export class ExecutionError extends Error {
|
|
5
|
+
readonly cause: unknown;
|
|
6
|
+
|
|
7
|
+
constructor(cause: unknown) {
|
|
8
|
+
super(`Error during worker execution: ${cause}`);
|
|
9
|
+
this.name = "ExecutionError";
|
|
10
|
+
this.cause = cause;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Helper for exhaustive switch statements. TypeScript will error if a case is not handled.
|
|
16
|
+
*/
|
|
17
|
+
export function unreachable(value: never): never {
|
|
18
|
+
throw new Error(`Unreachable: ${value}`);
|
|
19
|
+
}
|