Skip to content

Commit eaae2d5

Browse files
committed
Use compiler-backed schema previews
1 parent 50d7a91 commit eaae2d5

64 files changed

Lines changed: 1154 additions & 530 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

bun.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/core/api/src/handlers/sources.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,9 @@ export const SourcesHandlers = HttpApiBuilder.group(ExecutorApi, "sources", (han
6161
});
6262
return tools.map((t) => ({
6363
id: ToolId.make(t.id),
64-
pluginId: t.pluginId,
65-
sourceId: t.sourceId,
66-
name: t.name,
67-
description: t.description,
68-
mayElicit: t.annotations?.mayElicit,
69-
requiresApproval: t.annotations?.requiresApproval,
70-
approvalDescription: t.annotations?.approvalDescription,
64+
...(t.annotations?.requiresApproval !== undefined
65+
? { requiresApproval: t.annotations.requiresApproval }
66+
: {}),
7167
}));
7268
}),
7369
),

packages/core/api/src/handlers/tools.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,21 +21,20 @@ export const ToolsHandlers = HttpApiBuilder.group(ExecutorApi, "tools", (handler
2121
});
2222
return tools.map((t) => ({
2323
id: ToolId.make(t.id),
24-
pluginId: t.pluginId,
25-
sourceId: t.sourceId,
26-
name: t.name,
27-
description: t.description,
28-
mayElicit: t.annotations?.mayElicit,
29-
requiresApproval: t.annotations?.requiresApproval,
24+
...(t.annotations?.requiresApproval !== undefined
25+
? { requiresApproval: t.annotations.requiresApproval }
26+
: {}),
3027
}));
3128
}),
3229
),
3330
)
34-
.handle("schema", ({ params: path }) =>
31+
.handle("schema", ({ params: path, query }) =>
3532
capture(
3633
Effect.gen(function* () {
3734
const executor = yield* ExecutorService;
38-
const schema = yield* executor.tools.schema(path.toolId);
35+
const schema = yield* executor.tools.schema(path.toolId, {
36+
includeTypeScript: query.includeTypeScript === "true",
37+
});
3938
if (schema === null) {
4039
return yield* new ToolNotFoundError({ toolId: path.toolId });
4140
}

packages/core/api/src/sources/api.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,15 +37,9 @@ const SourceRefreshResponse = Schema.Struct({
3737

3838
const ToolMetadataResponse = Schema.Struct({
3939
id: ToolId,
40-
pluginId: Schema.String,
41-
sourceId: Schema.String,
42-
name: Schema.String,
43-
description: Schema.optional(Schema.String),
44-
mayElicit: Schema.optional(Schema.Boolean),
4540
/** Plugin-derived default approval annotation. Surfaces in the UI as
4641
* the "default" policy when no user `tool_policy` rule matches. */
4742
requiresApproval: Schema.optional(Schema.Boolean),
48-
approvalDescription: Schema.optional(Schema.String),
4943
});
5044

5145
const DetectRequest = Schema.Struct({

packages/core/api/src/tools/api.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,27 +13,29 @@ const PathParams = {
1313
toolId: ToolId,
1414
};
1515

16+
const ToolSchemaQuery = Schema.Struct({
17+
includeTypeScript: Schema.optional(Schema.Literal("true")),
18+
});
19+
1620
// ---------------------------------------------------------------------------
1721
// Response schemas
1822
// ---------------------------------------------------------------------------
1923

2024
const ToolMetadataResponse = Schema.Struct({
2125
id: ToolId,
22-
pluginId: Schema.String,
23-
sourceId: Schema.String,
24-
name: Schema.String,
25-
description: Schema.optional(Schema.String),
26-
mayElicit: Schema.optional(Schema.Boolean),
2726
requiresApproval: Schema.optional(Schema.Boolean),
2827
});
2928

3029
const ToolSchemaResponse = Schema.Struct({
3130
id: ToolId,
31+
name: Schema.optional(Schema.String),
32+
description: Schema.optional(Schema.String),
3233
inputTypeScript: Schema.optional(Schema.String),
3334
outputTypeScript: Schema.optional(Schema.String),
3435
typeScriptDefinitions: Schema.optional(Schema.Record(Schema.String, Schema.String)),
3536
inputSchema: Schema.optional(Schema.Unknown),
3637
outputSchema: Schema.optional(Schema.Unknown),
38+
schemaDefinitions: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
3739
});
3840

3941
// ---------------------------------------------------------------------------
@@ -57,6 +59,7 @@ export const ToolsApi = HttpApiGroup.make("tools")
5759
.add(
5860
HttpApiEndpoint.get("schema", "/scopes/:scopeId/tools/:toolId/schema", {
5961
params: PathParams,
62+
query: ToolSchemaQuery,
6063
success: ToolSchemaResponse,
6164
error: [InternalError, ToolNotFound],
6265
}),

packages/core/execution/src/tool-invoker.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,7 @@ describe("tool discovery", () => {
341341
expect(described.path).toBe("github.listRepositoryIssues");
342342
expect(described.name).toBe("listRepositoryIssues");
343343
expect(described.description).toBe("List issues for a repository");
344-
expect(described.inputTypeScript).toBe("{ owner: string; repo: string }");
344+
expect(described.inputTypeScript).toBe("{ owner: string; repo: string; }");
345345
expect(described.outputTypeScript).toBeUndefined();
346346
expect(described.typeScriptDefinitions).toBeUndefined();
347347
}),

packages/core/sdk/package.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"exports": {
1919
".": "./src/index.ts",
2020
"./core": "./src/index.ts",
21+
"./shared": "./src/shared.ts",
2122
"./promise": "./src/promise.ts",
2223
"./client": "./src/client.ts",
2324
"./testing": "./src/testing.ts"
@@ -37,6 +38,12 @@
3738
"default": "./dist/core.js"
3839
}
3940
},
41+
"./shared": {
42+
"import": {
43+
"types": "./dist/shared.d.ts",
44+
"default": "./dist/shared.js"
45+
}
46+
},
4047
"./client": {
4148
"import": {
4249
"types": "./dist/client.d.ts",
@@ -62,6 +69,7 @@
6269
"effect": "catalog:",
6370
"fractional-indexing": "^3.2.0",
6471
"fumadb": "workspace:*",
72+
"json-schema-to-typescript": "15.0.4",
6573
"oauth4webapi": "^3.8.5"
6674
},
6775
"devDependencies": {

packages/core/sdk/src/executor.test.ts

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,78 @@ const detector = (id: string, confidence: SourceDetectionResult["confidence"]) =
9999
),
100100
}))();
101101

102+
const schemaProbePlugin = definePlugin(() => ({
103+
id: "schemaProbe" as const,
104+
storage: () => ({}),
105+
extension: (ctx) => ({
106+
registerSource: () =>
107+
ctx.transaction(
108+
Effect.gen(function* () {
109+
const scope = String(ctx.scopes[0]!.id);
110+
yield* ctx.core.sources.register({
111+
id: "schema-source",
112+
scope,
113+
kind: "schema",
114+
name: "Schema Source",
115+
tools: [
116+
{
117+
name: "inspect",
118+
description: "inspect",
119+
inputSchema: {
120+
type: "object",
121+
properties: {
122+
pet: { $ref: "#/$defs/Pet" },
123+
},
124+
required: ["pet"],
125+
},
126+
outputSchema: { $ref: "#/$defs/Owner" },
127+
},
128+
],
129+
});
130+
yield* ctx.core.definitions.register({
131+
sourceId: "schema-source",
132+
scope,
133+
definitions: {
134+
Pet: {
135+
anyOf: [{ $ref: "#/$defs/Dog" }, { $ref: "#/$defs/Cat" }],
136+
},
137+
Dog: {
138+
type: "object",
139+
properties: {
140+
collar: { $ref: "#/$defs/Collar" },
141+
},
142+
},
143+
Cat: {
144+
type: "object",
145+
properties: {
146+
lives: { type: "number" },
147+
},
148+
},
149+
Collar: {
150+
type: "object",
151+
properties: {
152+
id: { type: "string" },
153+
},
154+
},
155+
Owner: {
156+
type: "object",
157+
properties: {
158+
pet: { $ref: "#/$defs/Pet" },
159+
},
160+
},
161+
Unused: {
162+
type: "object",
163+
properties: {
164+
value: { type: "string" },
165+
},
166+
},
167+
},
168+
});
169+
}),
170+
),
171+
}),
172+
}))();
173+
102174
describe("createExecutor", () => {
103175
it.effect("rolls back plugin and core writes from ctx.transaction failures", () =>
104176
Effect.gen(function* () {
@@ -204,4 +276,46 @@ describe("createExecutor", () => {
204276
expect(called).toBe(false);
205277
}),
206278
);
279+
280+
it.effect("returns schema roots with shared reachable definitions", () =>
281+
Effect.gen(function* () {
282+
const executor = yield* makeTestExecutor({ plugins: [schemaProbePlugin] as const });
283+
284+
yield* executor.schemaProbe.registerSource();
285+
286+
const schema = yield* executor.tools.schema("schema-source.inspect", {
287+
includeTypeScript: false,
288+
});
289+
290+
expect(schema?.inputSchema).toEqual({
291+
type: "object",
292+
properties: {
293+
pet: { $ref: "#/$defs/Pet" },
294+
},
295+
required: ["pet"],
296+
});
297+
expect(schema?.outputSchema).toEqual({ $ref: "#/$defs/Owner" });
298+
expect(schema?.schemaDefinitions).toEqual({
299+
Cat: expect.any(Object),
300+
Collar: expect.any(Object),
301+
Dog: expect.any(Object),
302+
Owner: expect.any(Object),
303+
Pet: expect.any(Object),
304+
});
305+
expect(schema?.schemaDefinitions).not.toHaveProperty("Unused");
306+
expect(schema?.inputTypeScript).toBeUndefined();
307+
expect(schema?.outputTypeScript).toBeUndefined();
308+
expect(schema?.typeScriptDefinitions).toBeUndefined();
309+
310+
const schemaWithTypes = yield* executor.tools.schema("schema-source.inspect");
311+
expect(schemaWithTypes?.inputTypeScript).toContain("pet: Pet");
312+
expect(schemaWithTypes?.outputTypeScript).toBe("Owner");
313+
expect(schemaWithTypes?.typeScriptDefinitions).toEqual(
314+
expect.objectContaining({
315+
Pet: expect.any(String),
316+
Owner: expect.any(String),
317+
}),
318+
);
319+
}),
320+
);
207321
});

0 commit comments

Comments
 (0)