Showing the changes to packages/codemode-core/src (4 of 16 files). Show all
Unified · Split
| @@ -6,10 +6,14 @@ import type { · 5 unmodified lines | |||
| 6 | DescribePrimitive, | 6 | DescribePrimitive, |
| 7 | DiscoverPrimitive, | 7 | DiscoverPrimitive, |
| 8 | DiscoveryPrimitives, | 8 | DiscoveryPrimitives, |
| 9 | SearchHit, | ||
| 9 | ToolCatalog, | 10 | ToolCatalog, |
| 11 | ToolCatalogEntry, | ||
| 10 | ToolDescriptor, | 12 | ToolDescriptor, |
| 11 | ToolMap, | 13 | ToolMap, |
| 14 | ToolNamespace, | ||
| 12 | ToolPath, | 15 | ToolPath, |
| 16 | ToolSchemaBundle, | ||
| 13 | } from "./types"; | 17 | } from "./types"; |
| 14 | 18 | ||
| 15 | const tokenize = (value: string): string[] => | 19 | const tokenize = (value: string): string[] => |
| @@ -57,18 +61,117 @@ const namespaceFromPath = (path: string): string => { · 41 unmodified lines | |||
| 57 | return second ? `${first}.${second}` : first; | 61 | return second ? `${first}.${second}` : first; |
| 58 | }; | 62 | }; |
| 59 | 63 | ||
| 64 | const namespaceFromCatalogEntry = (entry: ToolCatalogEntry): string => | ||
| 65 | entry.namespace ?? namespaceFromPath(entry.descriptor.path); | ||
| 66 | |||
| 67 | const searchableTextForEntry = (entry: ToolCatalogEntry): string => | ||
| 68 | entry.searchText?.trim().toLowerCase() || searchableTextForTool(entry.descriptor); | ||
| 69 | |||
| 70 | const scoreCatalogEntry = ( | ||
| 71 | queryTokens: readonly string[], | ||
| 72 | entry: ToolCatalogEntry, | ||
| 73 | ): number => { | ||
| 74 | if (entry.score) { | ||
| 75 | return entry.score(queryTokens); | ||
| 76 | } | ||
| 77 | |||
| 78 | const haystack = searchableTextForEntry(entry); | ||
| 79 | return queryTokens.reduce( | ||
| 80 | (total, token) => total + (haystack.includes(token) ? 1 : 0), | ||
| 81 | 0, | ||
| 82 | ); | ||
| 83 | }; | ||
| 84 | |||
| 85 | const mergeToolNamespaces = ( | ||
| 86 | groups: ReadonlyArray<readonly ToolNamespace[]>, | ||
| 87 | ): ToolNamespace[] => { | ||
| 88 | const merged = new Map<string, ToolNamespace>(); | ||
| 89 | |||
| 90 | for (const group of groups) { | ||
| 91 | for (const namespace of group) { | ||
| 92 | const existing = merged.get(namespace.namespace); | ||
| 93 | merged.set(namespace.namespace, { | ||
| 94 | namespace: namespace.namespace, | ||
| 95 | displayName: existing?.displayName ?? namespace.displayName, | ||
| 96 | ...(existing?.toolCount !== undefined || namespace.toolCount !== undefined | ||
| 97 | ? { | ||
| 98 | toolCount: | ||
| 99 | (existing?.toolCount ?? 0) + (namespace.toolCount ?? 0), | ||
| 100 | } | ||
| 101 | : {}), | ||
| 102 | }); | ||
| 103 | } | ||
| 104 | } | ||
| 105 | |||
| 106 | return [...merged.values()].sort((left, right) => | ||
| 107 | left.namespace.localeCompare(right.namespace), | ||
| 108 | ); | ||
| 109 | }; | ||
| 110 | |||
| 111 | const dedupeToolDescriptors = ( | ||
| 112 | groups: ReadonlyArray<readonly ToolDescriptor[]>, | ||
| 113 | ): ToolDescriptor[] => { | ||
| 114 | const merged = new Map<string, ToolDescriptor>(); | ||
| 115 | |||
| 116 | for (const group of groups) { | ||
| 117 | for (const descriptor of group) { | ||
| 118 | if (!merged.has(descriptor.path)) { | ||
| 119 | merged.set(descriptor.path, descriptor); | ||
| 120 | } | ||
| 121 | } | ||
| 122 | } | ||
| 123 | |||
| 124 | return [...merged.values()].sort((left, right) => | ||
| 125 | left.path.localeCompare(right.path), | ||
| 126 | ); | ||
| 127 | }; | ||
| 128 | |||
| 129 | const dedupeSearchHits = ( | ||
| 130 | groups: ReadonlyArray<readonly SearchHit[]>, | ||
| 131 | ): SearchHit[] => { | ||
| 132 | const merged = new Map<string, SearchHit>(); | ||
| 133 | |||
| 134 | for (const group of groups) { | ||
| 135 | for (const hit of group) { | ||
| 136 | if (!merged.has(hit.path)) { | ||
| 137 | merged.set(hit.path, hit); | ||
| 138 | } | ||
| 139 | } | ||
| 140 | } | ||
| 141 | |||
| 142 | return [...merged.values()].sort( | ||
| 143 | (left, right) => right.score - left.score || left.path.localeCompare(right.path), | ||
| 144 | ); | ||
| 145 | }; | ||
| 146 | |||
| 60 | export function createToolCatalogFromTools(input: { | 147 | export function createToolCatalogFromTools(input: { |
| 61 | tools: ToolMap; | 148 | tools: ToolMap; |
| 62 | defaultNamespace?: string; | 149 | defaultNamespace?: string; |
| 63 | }): ToolCatalog { | 150 | }): ToolCatalog { |
| 64 | const descriptors = toolDescriptorsFromTools({ | 151 | return createToolCatalogFromEntries({ |
| 65 | tools: input.tools, | 152 | entries: toolDescriptorsFromTools({ |
| 153 | tools: input.tools, | ||
| 154 | }).map((descriptor) => ({ | ||
| 155 | descriptor, | ||
| 156 | ...(input.defaultNamespace !== undefined | ||
| 157 | ? { namespace: input.defaultNamespace } | ||
| 158 | : {}), | ||
| 159 | })), | ||
| 66 | }); | 160 | }); |
| 67 | const byPath = new Map(descriptors.map((descriptor) => [descriptor.path as string, descriptor])); | 161 | } |
| 162 | |||
| 163 | export function createToolCatalogFromEntries(input: { | ||
| 164 | entries: ReadonlyArray<ToolCatalogEntry>; | ||
| 165 | getSchemaBundle?: (input: { id: string }) => Effect.Effect<ToolSchemaBundle | null, unknown>; | ||
| 166 | }): ToolCatalog { | ||
| 167 | const entries = [...input.entries]; | ||
| 168 | const byPath = new Map( | ||
| 169 | entries.map((entry) => [entry.descriptor.path as string, entry]), | ||
| 170 | ); | ||
| 68 | const namespaceCounts = new Map<string, number>(); | 171 | const namespaceCounts = new Map<string, number>(); |
| 69 | 172 | ||
| 70 | for (const descriptor of descriptors) { | 173 | for (const entry of entries) { |
| 71 | const namespace = input.defaultNamespace ?? namespaceFromPath(descriptor.path); | 174 | const namespace = namespaceFromCatalogEntry(entry); |
| 72 | namespaceCounts.set(namespace, (namespaceCounts.get(namespace) ?? 0) + 1); | 175 | namespaceCounts.set(namespace, (namespaceCounts.get(namespace) ?? 0) + 1); |
| 73 | } | 176 | } |
| 74 | 177 | ||
| @@ -81,23 +184,22 @@ export function createToolCatalogFromTools(input: { · 6 unmodified lines | |||
| 81 | ), | 184 | ), |
| 82 | listTools: ({ namespace, query, limit, includeSchemas = false }) => | 185 | listTools: ({ namespace, query, limit, includeSchemas = false }) => |
| 83 | Effect.succeed( | 186 | Effect.succeed( |
| 84 | descriptors | 187 | entries |
| 85 | .filter((descriptor) => { | 188 | .filter((entry) => |
| 86 | const descriptorNamespace = input.defaultNamespace ?? namespaceFromPath(descriptor.path); | 189 | !namespace || namespaceFromCatalogEntry(entry) === namespace, |
| 87 | return !namespace || descriptorNamespace === namespace; | 190 | ) |
| 88 | }) | 191 | .filter((entry) => { |
| 89 | .filter((descriptor) => { | ||
| 90 | if (!query) { | 192 | if (!query) { |
| 91 | return true; | 193 | return true; |
| 92 | } | 194 | } |
| 93 | 195 | ||
| 94 | const haystack = searchableTextForTool(descriptor); | 196 | const haystack = searchableTextForEntry(entry); |
| 95 | return tokenize(query).every((token) => haystack.includes(token)); | 197 | return tokenize(query).every((token) => haystack.includes(token)); |
| 96 | }) | 198 | }) |
| 97 | .slice(0, limit) | 199 | .slice(0, limit) |
| 98 | .map((descriptor) => | 200 | .map((entry) => |
| 99 | projectDescriptor({ | 201 | projectDescriptor({ |
| 100 | descriptor, | 202 | descriptor: entry.descriptor, |
| 101 | includeSchemas, | 203 | includeSchemas, |
| 102 | }) | 204 | }) |
| 103 | ), | 205 | ), |
| @@ -106,33 +208,27 @@ export function createToolCatalogFromTools(input: { · 2 unmodified lines | |||
| 106 | Effect.succeed( | 208 | Effect.succeed( |
| 107 | byPath.get(path) | 209 | byPath.get(path) |
| 108 | ? projectDescriptor({ | 210 | ? projectDescriptor({ |
| 109 | descriptor: byPath.get(path)!, | 211 | descriptor: byPath.get(path)!.descriptor, |
| 110 | includeSchemas, | 212 | includeSchemas, |
| 111 | }) | 213 | }) |
| 112 | : null, | 214 | : null, |
| 113 | ), | 215 | ), |
| 114 | getSchemaBundle: () => Effect.succeed(null), | 216 | getSchemaBundle: ({ id }) => |
| 217 | input.getSchemaBundle | ||
| 218 | ? input.getSchemaBundle({ id }) | ||
| 219 | : Effect.succeed(null), | ||
| 115 | searchTools: ({ query, namespace, limit }) => { | 220 | searchTools: ({ query, namespace, limit }) => { |
| 116 | const queryTokens = tokenize(query); | 221 | const queryTokens = tokenize(query); |
| 117 | 222 | ||
| 118 | return Effect.succeed( | 223 | return Effect.succeed( |
| 119 | descriptors | 224 | entries |
| 120 | .filter((descriptor) => { | 225 | .filter((entry) => |
| 121 | const descriptorNamespace = input.defaultNamespace ?? namespaceFromPath(descriptor.path); | 226 | !namespace || namespaceFromCatalogEntry(entry) === namespace, |
| 122 | return !namespace || descriptorNamespace === namespace; | 227 | ) |
| 123 | }) | 228 | .map((entry) => ({ |
| 124 | .map((descriptor) => { | 229 | path: entry.descriptor.path, |
| 125 | const haystack = searchableTextForTool(descriptor); | 230 | score: scoreCatalogEntry(queryTokens, entry), |
| 126 | const score = queryTokens.reduce( | 231 | })) |
| 127 | (total, token) => total + (haystack.includes(token) ? 1 : 0), | ||
| 128 | 0, | ||
| 129 | ); | ||
| 130 | |||
| 131 | return { | ||
| 132 | path: descriptor.path, | ||
| 133 | score, | ||
| 134 | }; | ||
| 135 | }) | ||
| 136 | .filter((hit) => hit.score > 0) | 232 | .filter((hit) => hit.score > 0) |
| 137 | .sort((left, right) => right.score - left.score) | 233 | .sort((left, right) => right.score - left.score) |
| 138 | .slice(0, limit), | 234 | .slice(0, limit), |
| @@ -141,6 +237,82 @@ export function createToolCatalogFromTools(input: { · 2 unmodified lines | |||
| 141 | } satisfies ToolCatalog; | 237 | } satisfies ToolCatalog; |
| 142 | } | 238 | } |
| 143 | 239 | ||
| 240 | export function mergeToolCatalogs(input: { | ||
| 241 | catalogs: ReadonlyArray<ToolCatalog>; | ||
| 242 | }): ToolCatalog { | ||
| 243 | const catalogs = [...input.catalogs]; | ||
| 244 | |||
| 245 | return { | ||
| 246 | listNamespaces: ({ limit }) => | ||
| 247 | Effect.gen(function* () { | ||
| 248 | const groups = yield* Effect.forEach( | ||
| 249 | catalogs, | ||
| 250 | (catalog) => catalog.listNamespaces({ limit: Math.max(limit, limit * catalogs.length) }), | ||
| 251 | { concurrency: "unbounded" }, | ||
| 252 | ); | ||
| 253 | |||
| 254 | return mergeToolNamespaces(groups).slice(0, limit); | ||
| 255 | }), | ||
| 256 | |||
| 257 | listTools: ({ namespace, query, limit, includeSchemas = false }) => | ||
| 258 | Effect.gen(function* () { | ||
| 259 | const groups = yield* Effect.forEach( | ||
| 260 | catalogs, | ||
| 261 | (catalog) => | ||
| 262 | catalog.listTools({ | ||
| 263 | ...(namespace !== undefined ? { namespace } : {}), | ||
| 264 | ...(query !== undefined ? { query } : {}), | ||
| 265 | limit: Math.max(limit, limit * catalogs.length), | ||
| 266 | includeSchemas, | ||
| 267 | }), | ||
| 268 | { concurrency: "unbounded" }, | ||
| 269 | ); | ||
| 270 | |||
| 271 | return dedupeToolDescriptors(groups).slice(0, limit); | ||
| 272 | }), | ||
| 273 | |||
| 274 | getToolByPath: ({ path, includeSchemas }) => | ||
| 275 | Effect.gen(function* () { | ||
| 276 | for (const catalog of catalogs) { | ||
| 277 | const descriptor = yield* catalog.getToolByPath({ path, includeSchemas }); | ||
| 278 | if (descriptor) { | ||
| 279 | return descriptor; | ||
| 280 | } | ||
| 281 | } | ||
| 282 | |||
| 283 | return null; | ||
| 284 | }), | ||
| 285 | |||
| 286 | getSchemaBundle: ({ id }) => | ||
| 287 | Effect.gen(function* () { | ||
| 288 | for (const catalog of catalogs) { | ||
| 289 | const bundle = yield* catalog.getSchemaBundle({ id }); | ||
| 290 | if (bundle) { | ||
| 291 | return bundle; | ||
| 292 | } | ||
| 293 | } | ||
| 294 | |||
| 295 | return null; | ||
| 296 | }), | ||
| 297 | |||
| 298 | searchTools: ({ query, namespace, limit }) => | ||
| 299 | Effect.gen(function* () { | ||
| 300 | const groups = yield* Effect.forEach( | ||
| 301 | catalogs, | ||
| 302 | (catalog) => | ||
| 303 | catalog.searchTools({ | ||
| 304 | query, | ||
| 305 | ...(namespace !== undefined ? { namespace } : {}), | ||
| 306 | limit: Math.max(limit, limit * catalogs.length), | ||
| 307 | }), | ||
| 308 | { concurrency: "unbounded" }, | ||
| 309 | ); | ||
| 310 | |||
| 311 | return dedupeSearchHits(groups).slice(0, limit); | ||
| 312 | }), | ||
| 313 | } satisfies ToolCatalog; | ||
| 314 | } | ||
| 315 | |||
| 144 | export function createStaticDiscoveryFromTools(input: { | 316 | export function createStaticDiscoveryFromTools(input: { |
| 145 | tools: ToolMap; | 317 | tools: ToolMap; |
| 146 | sourceKey?: string; | 318 | sourceKey?: string; |
| @@ -9,6 +9,7 @@ import * as TestClock from "effect/TestClock"; · 8 unmodified lines | |||
| 9 | import { | 9 | import { |
| 10 | allowAllToolInteractions, | 10 | allowAllToolInteractions, |
| 11 | createToolCatalogDiscovery, | 11 | createToolCatalogDiscovery, |
| 12 | createToolCatalogFromEntries, | ||
| 12 | createToolCatalogFromTools, | 13 | createToolCatalogFromTools, |
| 13 | createStaticDiscoveryFromTools, | 14 | createStaticDiscoveryFromTools, |
| 14 | createSystemToolMap, | 15 | createSystemToolMap, |
| @@ -355,6 +356,83 @@ describe("codemode-core", () => { · 340 unmodified lines | |||
| 355 | }), | 356 | }), |
| 356 | ); | 357 | ); |
| 357 | 358 | ||
| 359 | it.effect("builds a tool catalog from descriptor entries", () => | ||
| 360 | Effect.gen(function* () { | ||
| 361 | const catalog = createToolCatalogFromEntries({ | ||
| 362 | entries: [ | ||
| 363 | { | ||
| 364 | namespace: "github.repos", | ||
| 365 | searchText: "github repository details get repo", | ||
| 366 | descriptor: { | ||
| 367 | path: asToolPath("github.repos.getRepo"), | ||
| 368 | sourceKey: "source.github", | ||
| 369 | description: "Get repository details", | ||
| 370 | inputType: "{ owner: string; repo: string }", | ||
| 371 | outputType: "Repository", | ||
| 372 | inputSchemaJson: '{"type":"object"}', | ||
| 373 | }, | ||
| 374 | }, | ||
| 375 | { | ||
| 376 | namespace: "github.issues", | ||
| 377 | searchText: "github issues list repository issues", | ||
| 378 | score: (queryTokens) => | ||
| 379 | queryTokens.includes("issues") && queryTokens.includes("repository") | ||
| 380 | ? 50 | ||
| 381 | : 0, | ||
| 382 | descriptor: { | ||
| 383 | path: asToolPath("github.issues.list"), | ||
| 384 | sourceKey: "source.github", | ||
| 385 | description: "List repository issues", | ||
| 386 | inputType: "{ owner: string; repo: string }", | ||
| 387 | outputType: "Issue[]", | ||
| 388 | }, | ||
| 389 | }, | ||
| 390 | ], | ||
| 391 | getSchemaBundle: ({ id }) => | ||
| 392 | Effect.succeed( | ||
| 393 | id === "bundle_1" | ||
| 394 | ? { | ||
| 395 | id, | ||
| 396 | kind: "json_schema_ref_map", | ||
| 397 | hash: "hash_1", | ||
| 398 | refsJson: "{}", | ||
| 399 | } | ||
| 400 | : null, | ||
| 401 | ), | ||
| 402 | }); | ||
| 403 | |||
| 404 | const namespaces = yield* catalog.listNamespaces({ limit: 10 }); | ||
| 405 | expect(namespaces).toEqual([ | ||
| 406 | { namespace: "github.repos", toolCount: 1 }, | ||
| 407 | { namespace: "github.issues", toolCount: 1 }, | ||
| 408 | ]); | ||
| 409 | |||
| 410 | const listed = yield* catalog.listTools({ | ||
| 411 | namespace: "github.repos", | ||
| 412 | query: "repository details", | ||
| 413 | limit: 10, | ||
| 414 | includeSchemas: false, | ||
| 415 | }); | ||
| 416 | expect(listed[0]?.path).toBe("github.repos.getRepo"); | ||
| 417 | expect(listed[0]?.inputSchemaJson).toBeUndefined(); | ||
| 418 | |||
| 419 | const discovered = yield* catalog.searchTools({ | ||
| 420 | query: "repository issues", | ||
| 421 | limit: 10, | ||
| 422 | }); | ||
| 423 | expect(discovered[0]?.path).toBe("github.issues.list"); | ||
| 424 | |||
| 425 | const described = yield* catalog.getToolByPath({ | ||
| 426 | path: asToolPath("github.repos.getRepo"), | ||
| 427 | includeSchemas: true, | ||
| 428 | }); | ||
| 429 | expect(described?.inputSchemaJson).toContain('"type"'); | ||
| 430 | |||
| 431 | const bundle = yield* catalog.getSchemaBundle({ id: "bundle_1" }); | ||
| 432 | expect(bundle?.id).toBe("bundle_1"); | ||
| 433 | }), | ||
| 434 | ); | ||
| 435 | |||
| 358 | it.effect("system tools can be composed as normal tools", () => | 436 | it.effect("system tools can be composed as normal tools", () => |
| 359 | Effect.gen(function* () { | 437 | Effect.gen(function* () { |
| 360 | const catalog = createToolCatalogFromTools({ | 438 | const catalog = createToolCatalogFromTools({ |
| @@ -3,11 +3,13 @@ import * as Schema from "effect/Schema"; · 2 unmodified lines | |||
| 3 | 3 | ||
| 4 | import { createDiscoveryPrimitivesFromToolCatalog } from "./discovery"; | 4 | import { createDiscoveryPrimitivesFromToolCatalog } from "./discovery"; |
| 5 | import { toTool } from "./tool-map"; | 5 | import { toTool } from "./tool-map"; |
| 6 | import type { | 6 | import { |
| 7 | DiscoveryPrimitives, | 7 | ToolDescriptorSchema, |
| 8 | ToolCatalog, | 8 | ToolNamespaceSchema, |
| 9 | ToolMap, | 9 | ToolSchemaBundleSchema, |
| 10 | ToolPath, | 10 | type ToolCatalog, |
| 11 | type ToolMap, | ||
| 12 | type ToolPath, | ||
| 11 | } from "./types"; | 13 | } from "./types"; |
| 12 | 14 | ||
| 13 | const asToolPath = (value: string): ToolPath => value as ToolPath; | 15 | const asToolPath = (value: string): ToolPath => value as ToolPath; |
| @@ -20,13 +22,7 @@ const catalogNamespacesInputSchema = Schema.standardSchemaV1( · 6 unmodified lines | |||
| 20 | 22 | ||
| 21 | const catalogNamespacesOutputSchema = Schema.standardSchemaV1( | 23 | const catalogNamespacesOutputSchema = Schema.standardSchemaV1( |
| 22 | Schema.Struct({ | 24 | Schema.Struct({ |
| 23 | namespaces: Schema.Array( | 25 | namespaces: Schema.Array(ToolNamespaceSchema), |
| 24 | Schema.Struct({ | ||
| 25 | namespace: Schema.String, | ||
| 26 | displayName: Schema.optional(Schema.String), | ||
| 27 | toolCount: Schema.optional(Schema.Number), | ||
| 28 | }), | ||
| 29 | ), | ||
| 30 | }), | 26 | }), |
| 31 | ); | 27 | ); |
| 32 | 28 | ||
| @@ -41,23 +37,7 @@ const catalogToolsInputSchema = Schema.standardSchemaV1( · 8 unmodified lines | |||
| 41 | 37 | ||
| 42 | const catalogToolsOutputSchema = Schema.standardSchemaV1( | 38 | const catalogToolsOutputSchema = Schema.standardSchemaV1( |
| 43 | Schema.Struct({ | 39 | Schema.Struct({ |
| 44 | results: Schema.Array( | 40 | results: Schema.Array(ToolDescriptorSchema), |
| 45 | Schema.Struct({ | ||
| 46 | path: Schema.String, | ||
| 47 | sourceKey: Schema.String, | ||
| 48 | description: Schema.optional(Schema.String), | ||
| 49 | interaction: Schema.optional(Schema.String), | ||
| 50 | inputType: Schema.optional(Schema.String), | ||
| 51 | outputType: Schema.optional(Schema.String), | ||
| 52 | inputSchemaJson: Schema.optional(Schema.String), | ||
| 53 | outputSchemaJson: Schema.optional(Schema.String), | ||
| 54 | schemaBundleId: Schema.optional(Schema.String), | ||
| 55 | exampleInputJson: Schema.optional(Schema.String), | ||
| 56 | exampleOutputJson: Schema.optional(Schema.String), | ||
| 57 | providerKind: Schema.optional(Schema.String), | ||
| 58 | providerDataJson: Schema.optional(Schema.String), | ||
| 59 | }), | ||
| 60 | ), | ||
| 61 | }), | 41 | }), |
| 62 | ); | 42 | ); |
| 63 | 43 | ||
| @@ -69,7 +49,7 @@ const describeToolInputSchema = Schema.standardSchemaV1( · 5 unmodified lines | |||
| 69 | ); | 49 | ); |
| 70 | 50 | ||
| 71 | const describeToolOutputSchema = Schema.standardSchemaV1( | 51 | const describeToolOutputSchema = Schema.standardSchemaV1( |
| 72 | Schema.NullOr(Schema.Unknown), | 52 | Schema.NullOr(ToolDescriptorSchema), |
| 73 | ); | 53 | ); |
| 74 | 54 | ||
| 75 | const describeSchemaBundleInputSchema = Schema.standardSchemaV1( | 55 | const describeSchemaBundleInputSchema = Schema.standardSchemaV1( |
| @@ -79,14 +59,7 @@ const describeSchemaBundleInputSchema = Schema.standardSchemaV1( · 3 unmodified lines | |||
| 79 | ); | 59 | ); |
| 80 | 60 | ||
| 81 | const describeSchemaBundleOutputSchema = Schema.standardSchemaV1( | 61 | const describeSchemaBundleOutputSchema = Schema.standardSchemaV1( |
| 82 | Schema.NullOr( | 62 | Schema.NullOr(ToolSchemaBundleSchema), |
| 83 | Schema.Struct({ | ||
| 84 | id: Schema.String, | ||
| 85 | kind: Schema.String, | ||
| 86 | hash: Schema.String, | ||
| 87 | refsJson: Schema.String, | ||
| 88 | }), | ||
| 89 | ), | ||
| 90 | ); | 63 | ); |
| 91 | 64 | ||
| 92 | const discoverInputSchema = Schema.standardSchemaV1( | 65 | const discoverInputSchema = Schema.standardSchemaV1( |
| @@ -97,17 +70,12 @@ const discoverInputSchema = Schema.standardSchemaV1( · 4 unmodified lines | |||
| 97 | }), | 70 | }), |
| 98 | ); | 71 | ); |
| 99 | 72 | ||
| 100 | const discoverResultItemSchema = Schema.Struct({ | 73 | const discoverResultItemSchema = Schema.extend( |
| 101 | path: Schema.String, | 74 | ToolDescriptorSchema, |
| 102 | score: Schema.Number, | 75 | Schema.Struct({ |
| 103 | description: Schema.optional(Schema.String), | 76 | score: Schema.Number, |
| 104 | interaction: Schema.optional(Schema.String), | 77 | }), |
| 105 | inputType: Schema.optional(Schema.String), | 78 | ); |
| 106 | outputType: Schema.optional(Schema.String), | ||
| 107 | inputSchemaJson: Schema.optional(Schema.String), | ||
| 108 | outputSchemaJson: Schema.optional(Schema.String), | ||
| 109 | schemaBundleId: Schema.optional(Schema.String), | ||
| 110 | }); | ||
| 111 | 79 | ||
| 112 | const discoverOutputSchema = Schema.standardSchemaV1( | 80 | const discoverOutputSchema = Schema.standardSchemaV1( |
| 113 | Schema.Struct({ | 81 | Schema.Struct({ |
| @@ -118,7 +86,8 @@ const discoverOutputSchema = Schema.standardSchemaV1( · 4 unmodified lines | |||
| 118 | ); | 86 | ); |
| 119 | 87 | ||
| 120 | export type CreateSystemToolMapInput = { | 88 | export type CreateSystemToolMapInput = { |
| 121 | catalog: ToolCatalog; | 89 | catalog?: ToolCatalog; |
| 90 | getCatalog?: () => ToolCatalog; | ||
| 122 | sourceKey?: string; | 91 | sourceKey?: string; |
| 123 | }; | 92 | }; |
| 124 | 93 | ||
| @@ -126,130 +95,137 @@ export const createSystemToolMap = ( · 1 unmodified line | |||
| 126 | input: CreateSystemToolMapInput, | 95 | input: CreateSystemToolMapInput, |
| 127 | ): ToolMap => { | 96 | ): ToolMap => { |
| 128 | const sourceKey = input.sourceKey ?? "system"; | 97 | const sourceKey = input.sourceKey ?? "system"; |
| 129 | const primitives: DiscoveryPrimitives = createDiscoveryPrimitivesFromToolCatalog({ | 98 | const getCatalog = (): ToolCatalog => { |
| 130 | catalog: input.catalog, | 99 | if (input.catalog) { |
| 131 | }); | 100 | return input.catalog; |
| 101 | } | ||
| 102 | if (input.getCatalog) { | ||
| 103 | return input.getCatalog(); | ||
| 104 | } | ||
| 105 | |||
| 106 | throw new Error("createSystemToolMap requires a catalog or getCatalog"); | ||
| 107 | }; | ||
| 108 | const getPrimitives = () => | ||
| 109 | createDiscoveryPrimitivesFromToolCatalog({ | ||
| 110 | catalog: getCatalog(), | ||
| 111 | }) as Required<ReturnType<typeof createDiscoveryPrimitivesFromToolCatalog>>; | ||
| 132 | 112 | ||
| 133 | const tools: ToolMap = {}; | 113 | const tools: ToolMap = {}; |
| 134 | 114 | ||
| 135 | if (primitives.catalog) { | 115 | tools["catalog.namespaces"] = toTool({ |
| 136 | tools["catalog.namespaces"] = toTool({ | 116 | tool: { |
| 137 | tool: { | 117 | description: "List available namespaces with display names and tool counts", |
| 138 | description: "List available namespaces with display names and tool counts", | 118 | inputSchema: catalogNamespacesInputSchema, |
| 139 | inputSchema: catalogNamespacesInputSchema, | 119 | outputSchema: catalogNamespacesOutputSchema, |
| 140 | outputSchema: catalogNamespacesOutputSchema, | 120 | execute: ({ limit }: { limit?: number }) => |
| 141 | execute: ({ limit }: { limit?: number }) => | 121 | Effect.runPromise( |
| 142 | Effect.runPromise( | 122 | getPrimitives().catalog.namespaces({ |
| 143 | primitives.catalog!.namespaces({ | 123 | ...(limit !== undefined ? { limit } : {}), |
| 144 | ...(limit !== undefined ? { limit } : {}), | 124 | }), |
| 145 | }), | 125 | ), |
| 146 | ), | 126 | }, |
| 147 | }, | 127 | metadata: { |
| 148 | metadata: { | 128 | sourceKey, |
| 149 | sourceKey, | 129 | interaction: "auto", |
| 150 | interaction: "auto", | 130 | }, |
| 151 | }, | 131 | }); |
| 152 | }); | ||
| 153 | 132 | ||
| 154 | tools["catalog.tools"] = toTool({ | 133 | tools["catalog.tools"] = toTool({ |
| 155 | tool: { | 134 | tool: { |
| 156 | description: "List tools with optional namespace and query filters", | 135 | description: "List tools with optional namespace and query filters", |
| 157 | inputSchema: catalogToolsInputSchema, | 136 | inputSchema: catalogToolsInputSchema, |
| 158 | outputSchema: catalogToolsOutputSchema, | 137 | outputSchema: catalogToolsOutputSchema, |
| 159 | execute: ( | 138 | execute: ( |
| 160 | input: { | 139 | toolInput: { |
| 161 | namespace?: string; | 140 | namespace?: string; |
| 162 | query?: string; | 141 | query?: string; |
| 163 | limit?: number; | 142 | limit?: number; |
| 164 | includeSchemas?: boolean; | 143 | includeSchemas?: boolean; |
| 165 | }, | 144 | }, |
| 166 | ) => | 145 | ) => |
| 167 | Effect.runPromise( | 146 | Effect.runPromise( |
| 168 | primitives.catalog!.tools({ | 147 | getPrimitives().catalog.tools({ |
| 169 | ...(input.namespace !== undefined ? { namespace: input.namespace } : {}), | 148 | ...(toolInput.namespace !== undefined |
| 170 | ...(input.query !== undefined ? { query: input.query } : {}), | 149 | ? { namespace: toolInput.namespace } |
| 171 | ...(input.limit !== undefined ? { limit: input.limit } : {}), | 150 | : {}), |
| 172 | ...(input.includeSchemas !== undefined | 151 | ...(toolInput.query !== undefined ? { query: toolInput.query } : {}), |
| 173 | ? { includeSchemas: input.includeSchemas } | 152 | ...(toolInput.limit !== undefined ? { limit: toolInput.limit } : {}), |
| 174 | : {}), | 153 | ...(toolInput.includeSchemas !== undefined |
| 175 | }), | 154 | ? { includeSchemas: toolInput.includeSchemas } |
| 176 | ), | 155 | : {}), |
| 177 | }, | 156 | }), |
| 178 | metadata: { | 157 | ), |
| 179 | sourceKey, | 158 | }, |
| 180 | interaction: "auto", | 159 | metadata: { |
| 181 | }, | 160 | sourceKey, |
| 182 | }); | 161 | interaction: "auto", |
| 183 | } | 162 | }, |
| 163 | }); | ||
| 184 | 164 | ||
| 185 | if (primitives.describe) { | 165 | tools["describe.tool"] = toTool({ |
| 186 | tools["describe.tool"] = toTool({ | 166 | tool: { |
| 187 | tool: { | 167 | description: "Get metadata and optional schemas for a tool path", |
| 188 | description: "Get metadata and optional schemas for a tool path", | 168 | inputSchema: describeToolInputSchema, |
| 189 | inputSchema: describeToolInputSchema, | 169 | outputSchema: describeToolOutputSchema, |
| 190 | outputSchema: describeToolOutputSchema, | 170 | execute: ({ path, includeSchemas }: { path: string; includeSchemas?: boolean }) => |
| 191 | execute: ({ path, includeSchemas }: { path: string; includeSchemas?: boolean }) => | 171 | Effect.runPromise( |
| 192 | Effect.runPromise( | 172 | getPrimitives().describe.tool({ |
| 193 | primitives.describe!.tool({ | 173 | path: asToolPath(path), |
| 194 | path: asToolPath(path), | 174 | ...(includeSchemas !== undefined ? { includeSchemas } : {}), |
| 195 | ...(includeSchemas !== undefined ? { includeSchemas } : {}), | 175 | }), |
| 196 | }), | 176 | ), |
| 197 | ), | 177 | }, |
| 198 | }, | 178 | metadata: { |
| 199 | metadata: { | 179 | sourceKey, |
| 200 | sourceKey, | 180 | interaction: "auto", |
| 201 | interaction: "auto", | 181 | }, |
| 202 | }, | 182 | }); |
| 203 | }); | ||
| 204 | 183 | ||
| 205 | tools["describe.schemaBundle"] = toTool({ | 184 | tools["describe.schemaBundle"] = toTool({ |
| 206 | tool: { | 185 | tool: { |
| 207 | description: "Get a shared schema bundle by id", | 186 | description: "Get a shared schema bundle by id", |
| 208 | inputSchema: describeSchemaBundleInputSchema, | 187 | inputSchema: describeSchemaBundleInputSchema, |
| 209 | outputSchema: describeSchemaBundleOutputSchema, | 188 | outputSchema: describeSchemaBundleOutputSchema, |
| 210 | execute: ({ id }: { id: string }) => | 189 | execute: ({ id }: { id: string }) => |
| 211 | Effect.runPromise( | 190 | Effect.runPromise( |
| 212 | primitives.describe!.schemaBundle({ | 191 | getPrimitives().describe.schemaBundle({ |
| 213 | id, | 192 | id, |
| 214 | }), | 193 | }), |
| 215 | ), | 194 | ), |
| 216 | }, | 195 | }, |
| 217 | metadata: { | 196 | metadata: { |
| 218 | sourceKey, | 197 | sourceKey, |
| 219 | interaction: "auto", | 198 | interaction: "auto", |
| 220 | }, | 199 | }, |
| 221 | }); | 200 | }); |
| 222 | } | ||
| 223 | 201 | ||
| 224 | if (primitives.discover) { | 202 | tools.discover = toTool({ |
| 225 | tools.discover = toTool({ | 203 | tool: { |
| 226 | tool: { | 204 | description: "Search tools by intent and return ranked matches", |
| 227 | description: "Search tools by intent and return ranked matches", | 205 | inputSchema: discoverInputSchema, |
| 228 | inputSchema: discoverInputSchema, | 206 | outputSchema: discoverOutputSchema, |
| 229 | outputSchema: discoverOutputSchema, | 207 | execute: ( |
| 230 | execute: ( | 208 | toolInput: { |
| 231 | input: { | 209 | query: string; |
| 232 | query: string; | 210 | limit?: number; |
| 233 | limit?: number; | 211 | includeSchemas?: boolean; |
| 234 | includeSchemas?: boolean; | 212 | }, |
| 235 | }, | 213 | ) => |
| 236 | ) => | 214 | Effect.runPromise( |
| 237 | Effect.runPromise( | 215 | getPrimitives().discover({ |
| 238 | primitives.discover!({ | 216 | query: toolInput.query, |
| 239 | query: input.query, | 217 | ...(toolInput.limit !== undefined ? { limit: toolInput.limit } : {}), |
| 240 | ...(input.limit !== undefined ? { limit: input.limit } : {}), | 218 | ...(toolInput.includeSchemas !== undefined |
| 241 | ...(input.includeSchemas !== undefined | 219 | ? { includeSchemas: toolInput.includeSchemas } |
| 242 | ? { includeSchemas: input.includeSchemas } | 220 | : {}), |
| 243 | : {}), | 221 | }), |
| 244 | }), | 222 | ), |
| 245 | ), | 223 | }, |
| 246 | }, | 224 | metadata: { |
| 247 | metadata: { | 225 | sourceKey, |
| 248 | sourceKey, | 226 | interaction: "auto", |
| 249 | interaction: "auto", | 227 | }, |
| 250 | }, | 228 | }); |
| 251 | }); | ||
| 252 | } | ||
| 253 | 229 | ||
| 254 | return tools; | 230 | return tools; |
| 255 | }; | 231 | }; |
| @@ -1,5 +1,6 @@ | |||
| 1 | import type { StandardSchemaV1 } from "@standard-schema/spec"; | 1 | import type { StandardSchemaV1 } from "@standard-schema/spec"; |
| 2 | import type * as Effect from "effect/Effect"; | 2 | import type * as Effect from "effect/Effect"; |
| 3 | import * as Schema from "effect/Schema"; | ||
| 3 | 4 | ||
| 4 | export type ToolPath = string & { readonly __toolPath: unique symbol }; | 5 | export type ToolPath = string & { readonly __toolPath: unique symbol }; |
| 5 | 6 | ||
| @@ -142,6 +143,25 @@ export type ToolDescriptor = { · 136 unmodified lines | |||
| 142 | providerDataJson?: string; | 143 | providerDataJson?: string; |
| 143 | }; | 144 | }; |
| 144 | 145 | ||
| 146 | export const ToolDescriptorSchema = Schema.Struct({ | ||
| 147 | path: Schema.String, | ||
| 148 | sourceKey: Schema.String, | ||
| 149 | description: Schema.optional(Schema.String), | ||
| 150 | interaction: Schema.optional( | ||
| 151 | Schema.Union(Schema.Literal("auto"), Schema.Literal("required")), | ||
| 152 | ), | ||
| 153 | elicitation: Schema.optional(Schema.Unknown), | ||
| 154 | inputType: Schema.optional(Schema.String), | ||
| 155 | outputType: Schema.optional(Schema.String), | ||
| 156 | inputSchemaJson: Schema.optional(Schema.String), | ||
| 157 | outputSchemaJson: Schema.optional(Schema.String), | ||
| 158 | schemaBundleId: Schema.optional(Schema.String), | ||
| 159 | exampleInputJson: Schema.optional(Schema.String), | ||
| 160 | exampleOutputJson: Schema.optional(Schema.String), | ||
| 161 | providerKind: Schema.optional(Schema.String), | ||
| 162 | providerDataJson: Schema.optional(Schema.String), | ||
| 163 | }); | ||
| 164 | |||
| 145 | export type ToolSchemaBundle = { | 165 | export type ToolSchemaBundle = { |
| 146 | id: string; | 166 | id: string; |
| 147 | kind: string; | 167 | kind: string; |
| @@ -149,17 +169,37 @@ export type ToolSchemaBundle = { · 1 unmodified line | |||
| 149 | refsJson: string; | 169 | refsJson: string; |
| 150 | }; | 170 | }; |
| 151 | 171 | ||
| 172 | export const ToolSchemaBundleSchema = Schema.Struct({ | ||
| 173 | id: Schema.String, | ||
| 174 | kind: Schema.String, | ||
| 175 | hash: Schema.String, | ||
| 176 | refsJson: Schema.String, | ||
| 177 | }); | ||
| 178 | |||
| 152 | export type ToolNamespace = { | 179 | export type ToolNamespace = { |
| 153 | namespace: string; | 180 | namespace: string; |
| 154 | displayName?: string; | 181 | displayName?: string; |
| 155 | toolCount?: number; | 182 | toolCount?: number; |
| 156 | }; | 183 | }; |
| 157 | 184 | ||
| 185 | export const ToolNamespaceSchema = Schema.Struct({ | ||
| 186 | namespace: Schema.String, | ||
| 187 | displayName: Schema.optional(Schema.String), | ||
| 188 | toolCount: Schema.optional(Schema.Number), | ||
| 189 | }); | ||
| 190 | |||
| 158 | export type SearchHit = { | 191 | export type SearchHit = { |
| 159 | path: ToolPath; | 192 | path: ToolPath; |
| 160 | score: number; | 193 | score: number; |
| 161 | }; | 194 | }; |
| 162 | 195 | ||
| 196 | export type ToolCatalogEntry = { | ||
| 197 | descriptor: ToolDescriptor; | ||
| 198 | namespace?: string; | ||
| 199 | searchText?: string; | ||
| 200 | score?: (queryTokens: readonly string[]) => number; | ||
| 201 | }; | ||
| 202 | |||
| 163 | export interface ToolCatalog { | 203 | export interface ToolCatalog { |
| 164 | listNamespaces(input: { | 204 | listNamespaces(input: { |
| 165 | limit: number; | 205 | limit: number; |