History

schema

Rhys Sullivan committed 8 months ago

Commit ef83afa1 parent bc109b49 files · +182 −4Browse files

Unified · Split

9 files
  1. bun.lock +1 −0
  2. packages/codemode-core/package.json +1 −0
  3. packages/codemode-core/src/index.test.ts +45 −0
  4. packages/codemode-core/src/index.ts +1 −0
  5. packages/codemode-core/src/json-schema.ts +73 −0
  6. packages/codemode-core/src/tool-map.ts +3 −1
  7. packages/codemode-core/src/types.ts +12 −1
  8. packages/codemode-mcp/src/mcp-tools.ts +23 −1
  9. packages/codemode-openapi/src/openapi-tools.ts +23 −1
bun.lock+1−0
@@ -26,6 +26,7 @@ · 25 unmodified lines
26 "name": "@executor-v3/codemode-core",26 "name": "@executor-v3/codemode-core",
27 "dependencies": {27 "dependencies": {
28 "@standard-schema/spec": "^1.0.0",28 "@standard-schema/spec": "^1.0.0",
29 "ajv": "^8.17.1",
29 "effect": "^3.19.19",30 "effect": "^3.19.19",
30 },31 },
31 "devDependencies": {32 "devDependencies": {
packages/codemode-core/package.json+1−0
@@ -12,6 +12,7 @@ · 11 unmodified lines
12 },12 },
13 "dependencies": {13 "dependencies": {
14 "@standard-schema/spec": "^1.0.0",14 "@standard-schema/spec": "^1.0.0",
15 "ajv": "^8.17.1",
15 "effect": "^3.19.19"16 "effect": "^3.19.19"
16 },17 },
17 "devDependencies": {18 "devDependencies": {
packages/codemode-core/src/index.test.ts+45−0
@@ -9,6 +9,7 @@ import { · 8 unmodified lines
9 executeCodeWithTools,9 executeCodeWithTools,
10 makeToolInvokerFromTools,10 makeToolInvokerFromTools,
11 mergeToolMaps,11 mergeToolMaps,
12 standardSchemaFromJsonSchema,
12 toTool,13 toTool,
13 type CodeExecutor,14 type CodeExecutor,
14 type ToolDescriptor,15 type ToolDescriptor,
@@ -79,6 +80,50 @@ describe("codemode-core", () => { · 64 unmodified lines
79 }),80 }),
80 );81 );
81 82
83
84 it.effect("builds Standard Schema validators from JSON Schema", () =>
85 Effect.gen(function* () {
86 const tools = {
87 "math.add": {
88 description: "Add two numbers",
89 inputSchema: standardSchemaFromJsonSchema({
90 type: "object",
91 required: ["a", "b"],
92 properties: {
93 a: { type: "number" },
94 b: { type: "number" },
95 },
96 additionalProperties: false,
97 }),
98 execute: ({ a, b }: { a: number; b: number }) => ({ sum: a + b }),
99 },
100 } satisfies ToolMap;
101
102 const invoker = makeToolInvokerFromTools({ tools });
103
104 const success = yield* invoker.invoke({
105 path: "math.add",
106 args: { a: 1, b: 2 },
107 });
108 expect(success).toEqual({ sum: 3 });
109
110 const failure = yield* Effect.either(
111 invoker.invoke({
112 path: "math.add",
113 args: { a: "1", b: 2 },
114 }),
115 );
116
117 expect(failure._tag).toBe("Left");
118 if (failure._tag === "Left") {
119 expect(failure.left).toBeInstanceOf(Error);
120 if (failure.left instanceof Error) {
121 expect(failure.left.message).toContain("Input validation failed");
122 }
123 }
124 }),
125 );
126
82 it.effect("hydrates dynamic discover results via search + directory", () =>127 it.effect("hydrates dynamic discover results via search + directory", () =>
83 Effect.gen(function* () {128 Effect.gen(function* () {
84 const descriptors: Record<string, ToolDescriptor> = {129 const descriptors: Record<string, ToolDescriptor> = {
packages/codemode-core/src/index.ts+1−0
@@ -1,4 +1,5 @@
1export * from "./types";1export * from "./types";
2export * from "./json-schema";
2export * from "./tool-map";3export * from "./tool-map";
3export * from "./discovery";4export * from "./discovery";
4export * from "./system-tools";5export * from "./system-tools";
packages/codemode-core/src/json-schema.ts+73−0
@@ -0,0 +1,73 @@
1import type { ErrorObject } from "ajv";
2import Ajv2020 from "ajv/dist/2020";
3
4import type { StandardSchema } from "./types";
5import { unknownInputSchema } from "./types";
6
7const ajv = new Ajv2020({
8 allErrors: true,
9 strict: false,
10 validateSchema: false,
11 allowUnionTypes: true,
12});
13
14const decodePointerSegment = (segment: string): PropertyKey => {
15 const decoded = segment.replaceAll("~1", "/").replaceAll("~0", "~");
16 return /^\d+$/.test(decoded) ? Number(decoded) : decoded;
17};
18
19const pointerToPath = (pointer: string | undefined): ReadonlyArray<PropertyKey> | undefined => {
20 if (!pointer || pointer.length === 0 || pointer === "/") {
21 return undefined;
22 }
23
24 return pointer
25 .split("/")
26 .slice(1)
27 .filter((segment) => segment.length > 0)
28 .map(decodePointerSegment);
29};
30
31const toIssueMessage = (error: ErrorObject): string => {
32 const keyword = error.keyword.trim();
33 const message = (error.message ?? "Invalid value").trim();
34 return keyword.length > 0 ? `${keyword}: ${message}` : message;
35};
36
37export const standardSchemaFromJsonSchema = (
38 schema: unknown,
39 options?: {
40 vendor?: string;
41 fallback?: StandardSchema;
42 },
43): StandardSchema => {
44 try {
45 const validate = ajv.compile(schema as Record<string, unknown>);
46
47 return {
48 "~standard": {
49 version: 1,
50 vendor: options?.vendor ?? "json-schema",
51 validate: (value: unknown) => {
52 const valid = validate(value);
53 if (valid) {
54 return { value };
55 }
56
57 const issues = (validate.errors ?? []).map((error) => ({
58 message: toIssueMessage(error),
59 path: pointerToPath(error.instancePath),
60 }));
61
62 return {
63 issues: issues.length > 0
64 ? issues
65 : [{ message: "Invalid value" }],
66 };
67 },
68 },
69 };
70 } catch {
71 return options?.fallback ?? unknownInputSchema;
72 }
73};
packages/codemode-core/src/tool-map.ts+3−1
@@ -76,7 +76,9 @@ const parseInput = (input: { · 75 unmodified lines
76}): Effect.Effect<unknown, Error> => {76}): Effect.Effect<unknown, Error> => {
77 const validate = getSchemaValidator(input.schema);77 const validate = getSchemaValidator(input.schema);
78 if (!validate) {78 if (!validate) {
79 return Effect.succeed(input.value);79 return Effect.fail(
80 new Error(`Tool ${input.path} has no Standard Schema validator on inputSchema`),
81 );
80 }82 }
81 83
82 return Effect.tryPromise({84 return Effect.tryPromise({
packages/codemode-core/src/types.ts+12−1
@@ -18,12 +18,23 @@ export type ToolMetadata = { · 17 unmodified lines
18 18
19export type ExecutableTool = {19export type ExecutableTool = {
20 description?: string;20 description?: string;
21 inputSchema?: StandardSchema;21 inputSchema: StandardSchema;
22 outputSchema?: StandardSchema;22 outputSchema?: StandardSchema;
23 parameters?: StandardSchema;23 parameters?: StandardSchema;
24 execute: (...args: any[]) => unknown;24 execute: (...args: any[]) => unknown;
25};25};
26 26
27export const unknownInputSchema: StandardSchema = {
28 "~standard": {
29 version: 1,
30 vendor: "@executor-v3/codemode-core",
31 validate: (value: unknown) => ({
32 value,
33 }),
34 },
35};
36
37
27export type ToolDefinition = {38export type ToolDefinition = {
28 tool: ExecutableTool;39 tool: ExecutableTool;
29 metadata?: ToolMetadata;40 metadata?: ToolMetadata;
packages/codemode-mcp/src/mcp-tools.ts+23−1
@@ -1,7 +1,12 @@
1import * as Data from "effect/Data";1import * as Data from "effect/Data";
2import * as Effect from "effect/Effect";2import * as Effect from "effect/Effect";
3 3
4import { toTool, type ToolMap } from "@executor-v3/codemode-core";4import {
5 standardSchemaFromJsonSchema,
6 toTool,
7 type ToolMap,
8 unknownInputSchema,
9} from "@executor-v3/codemode-core";
5 10
6export type McpClientLike = {11export type McpClientLike = {
7 listTools: () => Promise<unknown>;12 listTools: () => Promise<unknown>;
@@ -77,6 +82,22 @@ const stringifyJson = (value: unknown): string | undefined => { · 69 unmodified lines
77 }82 }
78};83};
79 84
85const inputSchemaFromManifest = (inputSchemaJson: string | undefined) => {
86 if (!inputSchemaJson) {
87 return unknownInputSchema;
88 }
89
90 try {
91 return standardSchemaFromJsonSchema(JSON.parse(inputSchemaJson), {
92 vendor: "mcp",
93 fallback: unknownInputSchema,
94 });
95 } catch {
96 return unknownInputSchema;
97 }
98};
99
100
80const readListedTools = (value: unknown): Array<Record<string, unknown>> => {101const readListedTools = (value: unknown): Array<Record<string, unknown>> => {
81 const root = toRecord(value);102 const root = toRecord(value);
82 const tools = root.tools;103 const tools = root.tools;
@@ -167,6 +188,7 @@ export const createMcpToolsFromManifest = (input: { · 84 unmodified lines
167 toTool({188 toTool({
168 tool: {189 tool: {
169 description: entry.description ?? `MCP tool: ${entry.toolName}`,190 description: entry.description ?? `MCP tool: ${entry.toolName}`,
191 inputSchema: inputSchemaFromManifest(entry.inputSchemaJson),
170 execute: async (args: unknown) =>192 execute: async (args: unknown) =>
171 withConnection(input.connect, async (connection) => {193 withConnection(input.connect, async (connection) => {
172 const payloadArgs = toRecord(args);194 const payloadArgs = toRecord(args);
packages/codemode-openapi/src/openapi-tools.ts+23−1
@@ -1,7 +1,12 @@
1import * as Data from "effect/Data";1import * as Data from "effect/Data";
2import * as Effect from "effect/Effect";2import * as Effect from "effect/Effect";
3 3
4import { toTool, type ToolMap } from "@executor-v3/codemode-core";4import {
5 standardSchemaFromJsonSchema,
6 toTool,
7 type ToolMap,
8 unknownInputSchema,
9} from "@executor-v3/codemode-core";
5 10
6import {11import {
7 extractOpenApiManifest,12 extractOpenApiManifest,
@@ -179,6 +184,22 @@ const decodeFetchResponseBody = async (response: Response): Promise<unknown> => · 171 unmodified lines
179 return response.text();184 return response.text();
180};185};
181 186
187const inputSchemaFromTypingJson = (inputSchemaJson: string | undefined) => {
188 if (!inputSchemaJson) {
189 return unknownInputSchema;
190 }
191
192 try {
193 return standardSchemaFromJsonSchema(JSON.parse(inputSchemaJson), {
194 vendor: "openapi",
195 fallback: unknownInputSchema,
196 });
197 } catch {
198 return unknownInputSchema;
199 }
200};
201
202
182type CreateOpenApiToolsFromManifestInput = {203type CreateOpenApiToolsFromManifestInput = {
183 manifest: OpenApiToolManifest;204 manifest: OpenApiToolManifest;
184 baseUrl: string;205 baseUrl: string;
@@ -297,6 +318,7 @@ export const createOpenApiToolsFromManifest = ( · 112 unmodified lines
297 result[toolPath] = toTool({318 result[toolPath] = toTool({
298 tool: {319 tool: {
299 description,320 description,
321 inputSchema: inputSchemaFromTypingJson(extracted.typing?.inputSchemaJson),
300 execute: async (args: unknown) => {322 execute: async (args: unknown) => {
301 const decodedArgs = asToolArgs(args);323 const decodedArgs = asToolArgs(args);
302 const request = buildFetchRequest({324 const request = buildFetchRequest({