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 | 6 | DescribePrimitive, |
| 7 | 7 | DiscoverPrimitive, |
| 8 | 8 | DiscoveryPrimitives, |
| 9 | + SearchHit, | |
| 9 | 10 | ToolCatalog, |
| 11 | + ToolCatalogEntry, | |
| 10 | 12 | ToolDescriptor, |
| 11 | 13 | ToolMap, |
| 14 | + ToolNamespace, | |
| 12 | 15 | ToolPath, |
| 16 | + ToolSchemaBundle, | |
| 13 | 17 | } from "./types"; |
| 14 | 18 | |
| 15 | 19 | const tokenize = (value: string): string[] => |
| @@ -57,18 +61,117 @@ const namespaceFromPath = (path: string): string => { · 41 unmodified lines | ||
| 57 | 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 | 147 | export function createToolCatalogFromTools(input: { |
| 61 | 148 | tools: ToolMap; |
| 62 | 149 | defaultNamespace?: string; |
| 63 | 150 | }): ToolCatalog { |
| 64 | − const descriptors = toolDescriptorsFromTools({ | |
| 65 | − tools: input.tools, | |
| 151 | + return createToolCatalogFromEntries({ | |
| 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 | 171 | const namespaceCounts = new Map<string, number>(); |
| 69 | 172 | |
| 70 | − for (const descriptor of descriptors) { | |
| 71 | − const namespace = input.defaultNamespace ?? namespaceFromPath(descriptor.path); | |
| 173 | + for (const entry of entries) { | |
| 174 | + const namespace = namespaceFromCatalogEntry(entry); | |
| 72 | 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 | 185 | listTools: ({ namespace, query, limit, includeSchemas = false }) => |
| 83 | 186 | Effect.succeed( |
| 84 | − descriptors | |
| 85 | − .filter((descriptor) => { | |
| 86 | − const descriptorNamespace = input.defaultNamespace ?? namespaceFromPath(descriptor.path); | |
| 87 | − return !namespace || descriptorNamespace === namespace; | |
| 88 | − }) | |
| 89 | − .filter((descriptor) => { | |
| 187 | + entries | |
| 188 | + .filter((entry) => | |
| 189 | + !namespace || namespaceFromCatalogEntry(entry) === namespace, | |
| 190 | + ) | |
| 191 | + .filter((entry) => { | |
| 90 | 192 | if (!query) { |
| 91 | 193 | return true; |
| 92 | 194 | } |
| 93 | 195 | |
| 94 | − const haystack = searchableTextForTool(descriptor); | |
| 196 | + const haystack = searchableTextForEntry(entry); | |
| 95 | 197 | return tokenize(query).every((token) => haystack.includes(token)); |
| 96 | 198 | }) |
| 97 | 199 | .slice(0, limit) |
| 98 | − .map((descriptor) => | |
| 200 | + .map((entry) => | |
| 99 | 201 | projectDescriptor({ |
| 100 | − descriptor, | |
| 202 | + descriptor: entry.descriptor, | |
| 101 | 203 | includeSchemas, |
| 102 | 204 | }) |
| 103 | 205 | ), |
| @@ -106,33 +208,27 @@ export function createToolCatalogFromTools(input: { · 2 unmodified lines | ||
| 106 | 208 | Effect.succeed( |
| 107 | 209 | byPath.get(path) |
| 108 | 210 | ? projectDescriptor({ |
| 109 | − descriptor: byPath.get(path)!, | |
| 211 | + descriptor: byPath.get(path)!.descriptor, | |
| 110 | 212 | includeSchemas, |
| 111 | 213 | }) |
| 112 | 214 | : null, |
| 113 | 215 | ), |
| 114 | − getSchemaBundle: () => Effect.succeed(null), | |
| 216 | + getSchemaBundle: ({ id }) => | |
| 217 | + input.getSchemaBundle | |
| 218 | + ? input.getSchemaBundle({ id }) | |
| 219 | + : Effect.succeed(null), | |
| 115 | 220 | searchTools: ({ query, namespace, limit }) => { |
| 116 | 221 | const queryTokens = tokenize(query); |
| 117 | 222 | |
| 118 | 223 | return Effect.succeed( |
| 119 | − descriptors | |
| 120 | − .filter((descriptor) => { | |
| 121 | − const descriptorNamespace = input.defaultNamespace ?? namespaceFromPath(descriptor.path); | |
| 122 | − return !namespace || descriptorNamespace === namespace; | |
| 123 | − }) | |
| 124 | − .map((descriptor) => { | |
| 125 | − const haystack = searchableTextForTool(descriptor); | |
| 126 | − const score = queryTokens.reduce( | |
| 127 | − (total, token) => total + (haystack.includes(token) ? 1 : 0), | |
| 128 | − 0, | |
| 129 | − ); | |
| 130 | − | |
| 131 | − return { | |
| 132 | − path: descriptor.path, | |
| 133 | − score, | |
| 134 | − }; | |
| 135 | − }) | |
| 224 | + entries | |
| 225 | + .filter((entry) => | |
| 226 | + !namespace || namespaceFromCatalogEntry(entry) === namespace, | |
| 227 | + ) | |
| 228 | + .map((entry) => ({ | |
| 229 | + path: entry.descriptor.path, | |
| 230 | + score: scoreCatalogEntry(queryTokens, entry), | |
| 231 | + })) | |
| 136 | 232 | .filter((hit) => hit.score > 0) |
| 137 | 233 | .sort((left, right) => right.score - left.score) |
| 138 | 234 | .slice(0, limit), |
| @@ -141,6 +237,82 @@ export function createToolCatalogFromTools(input: { · 2 unmodified lines | ||
| 141 | 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 | 316 | export function createStaticDiscoveryFromTools(input: { |
| 145 | 317 | tools: ToolMap; |
| 146 | 318 | sourceKey?: string; |
| @@ -9,6 +9,7 @@ import * as TestClock from "effect/TestClock"; · 8 unmodified lines | ||
| 9 | 9 | import { |
| 10 | 10 | allowAllToolInteractions, |
| 11 | 11 | createToolCatalogDiscovery, |
| 12 | + createToolCatalogFromEntries, | |
| 12 | 13 | createToolCatalogFromTools, |
| 13 | 14 | createStaticDiscoveryFromTools, |
| 14 | 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 | 436 | it.effect("system tools can be composed as normal tools", () => |
| 359 | 437 | Effect.gen(function* () { |
| 360 | 438 | const catalog = createToolCatalogFromTools({ |
| @@ -3,11 +3,13 @@ import * as Schema from "effect/Schema"; · 2 unmodified lines | ||
| 3 | 3 | |
| 4 | 4 | import { createDiscoveryPrimitivesFromToolCatalog } from "./discovery"; |
| 5 | 5 | import { toTool } from "./tool-map"; |
| 6 | −import type { | |
| 7 | − DiscoveryPrimitives, | |
| 8 | − ToolCatalog, | |
| 9 | − ToolMap, | |
| 10 | − ToolPath, | |
| 6 | +import { | |
| 7 | + ToolDescriptorSchema, | |
| 8 | + ToolNamespaceSchema, | |
| 9 | + ToolSchemaBundleSchema, | |
| 10 | + type ToolCatalog, | |
| 11 | + type ToolMap, | |
| 12 | + type ToolPath, | |
| 11 | 13 | } from "./types"; |
| 12 | 14 | |
| 13 | 15 | const asToolPath = (value: string): ToolPath => value as ToolPath; |
| @@ -20,13 +22,7 @@ const catalogNamespacesInputSchema = Schema.standardSchemaV1( · 6 unmodified lines | ||
| 20 | 22 | |
| 21 | 23 | const catalogNamespacesOutputSchema = Schema.standardSchemaV1( |
| 22 | 24 | Schema.Struct({ |
| 23 | − namespaces: Schema.Array( | |
| 24 | − Schema.Struct({ | |
| 25 | − namespace: Schema.String, | |
| 26 | − displayName: Schema.optional(Schema.String), | |
| 27 | − toolCount: Schema.optional(Schema.Number), | |
| 28 | − }), | |
| 29 | − ), | |
| 25 | + namespaces: Schema.Array(ToolNamespaceSchema), | |
| 30 | 26 | }), |
| 31 | 27 | ); |
| 32 | 28 | |
| @@ -41,23 +37,7 @@ const catalogToolsInputSchema = Schema.standardSchemaV1( · 8 unmodified lines | ||
| 41 | 37 | |
| 42 | 38 | const catalogToolsOutputSchema = Schema.standardSchemaV1( |
| 43 | 39 | Schema.Struct({ |
| 44 | − results: Schema.Array( | |
| 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 | − ), | |
| 40 | + results: Schema.Array(ToolDescriptorSchema), | |
| 61 | 41 | }), |
| 62 | 42 | ); |
| 63 | 43 | |
| @@ -69,7 +49,7 @@ const describeToolInputSchema = Schema.standardSchemaV1( · 5 unmodified lines | ||
| 69 | 49 | ); |
| 70 | 50 | |
| 71 | 51 | const describeToolOutputSchema = Schema.standardSchemaV1( |
| 72 | − Schema.NullOr(Schema.Unknown), | |
| 52 | + Schema.NullOr(ToolDescriptorSchema), | |
| 73 | 53 | ); |
| 74 | 54 | |
| 75 | 55 | const describeSchemaBundleInputSchema = Schema.standardSchemaV1( |
| @@ -79,14 +59,7 @@ const describeSchemaBundleInputSchema = Schema.standardSchemaV1( · 3 unmodified lines | ||
| 79 | 59 | ); |
| 80 | 60 | |
| 81 | 61 | const describeSchemaBundleOutputSchema = Schema.standardSchemaV1( |
| 82 | − Schema.NullOr( | |
| 83 | − Schema.Struct({ | |
| 84 | − id: Schema.String, | |
| 85 | − kind: Schema.String, | |
| 86 | − hash: Schema.String, | |
| 87 | − refsJson: Schema.String, | |
| 88 | − }), | |
| 89 | − ), | |
| 62 | + Schema.NullOr(ToolSchemaBundleSchema), | |
| 90 | 63 | ); |
| 91 | 64 | |
| 92 | 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({ | |
| 101 | − path: Schema.String, | |
| 102 | − score: Schema.Number, | |
| 103 | − description: Schema.optional(Schema.String), | |
| 104 | − interaction: Schema.optional(Schema.String), | |
| 105 | − inputType: Schema.optional(Schema.String), | |
| 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 | −}); | |
| 73 | +const discoverResultItemSchema = Schema.extend( | |
| 74 | + ToolDescriptorSchema, | |
| 75 | + Schema.Struct({ | |
| 76 | + score: Schema.Number, | |
| 77 | + }), | |
| 78 | +); | |
| 111 | 79 | |
| 112 | 80 | const discoverOutputSchema = Schema.standardSchemaV1( |
| 113 | 81 | Schema.Struct({ |
| @@ -118,7 +86,8 @@ const discoverOutputSchema = Schema.standardSchemaV1( · 4 unmodified lines | ||
| 118 | 86 | ); |
| 119 | 87 | |
| 120 | 88 | export type CreateSystemToolMapInput = { |
| 121 | − catalog: ToolCatalog; | |
| 89 | + catalog?: ToolCatalog; | |
| 90 | + getCatalog?: () => ToolCatalog; | |
| 122 | 91 | sourceKey?: string; |
| 123 | 92 | }; |
| 124 | 93 | |
| @@ -126,130 +95,137 @@ export const createSystemToolMap = ( · 1 unmodified line | ||
| 126 | 95 | input: CreateSystemToolMapInput, |
| 127 | 96 | ): ToolMap => { |
| 128 | 97 | const sourceKey = input.sourceKey ?? "system"; |
| 129 | − const primitives: DiscoveryPrimitives = createDiscoveryPrimitivesFromToolCatalog({ | |
| 130 | − catalog: input.catalog, | |
| 131 | − }); | |
| 98 | + const getCatalog = (): ToolCatalog => { | |
| 99 | + if (input.catalog) { | |
| 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 | 113 | const tools: ToolMap = {}; |
| 134 | 114 | |
| 135 | − if (primitives.catalog) { | |
| 136 | − tools["catalog.namespaces"] = toTool({ | |
| 137 | − tool: { | |
| 138 | − description: "List available namespaces with display names and tool counts", | |
| 139 | − inputSchema: catalogNamespacesInputSchema, | |
| 140 | − outputSchema: catalogNamespacesOutputSchema, | |
| 141 | − execute: ({ limit }: { limit?: number }) => | |
| 142 | − Effect.runPromise( | |
| 143 | − primitives.catalog!.namespaces({ | |
| 144 | − ...(limit !== undefined ? { limit } : {}), | |
| 145 | − }), | |
| 146 | − ), | |
| 147 | − }, | |
| 148 | − metadata: { | |
| 149 | − sourceKey, | |
| 150 | − interaction: "auto", | |
| 151 | − }, | |
| 152 | − }); | |
| 115 | + tools["catalog.namespaces"] = toTool({ | |
| 116 | + tool: { | |
| 117 | + description: "List available namespaces with display names and tool counts", | |
| 118 | + inputSchema: catalogNamespacesInputSchema, | |
| 119 | + outputSchema: catalogNamespacesOutputSchema, | |
| 120 | + execute: ({ limit }: { limit?: number }) => | |
| 121 | + Effect.runPromise( | |
| 122 | + getPrimitives().catalog.namespaces({ | |
| 123 | + ...(limit !== undefined ? { limit } : {}), | |
| 124 | + }), | |
| 125 | + ), | |
| 126 | + }, | |
| 127 | + metadata: { | |
| 128 | + sourceKey, | |
| 129 | + interaction: "auto", | |
| 130 | + }, | |
| 131 | + }); | |
| 153 | 132 | |
| 154 | − tools["catalog.tools"] = toTool({ | |
| 155 | − tool: { | |
| 156 | − description: "List tools with optional namespace and query filters", | |
| 157 | − inputSchema: catalogToolsInputSchema, | |
| 158 | − outputSchema: catalogToolsOutputSchema, | |
| 159 | − execute: ( | |
| 160 | − input: { | |
| 161 | − namespace?: string; | |
| 162 | − query?: string; | |
| 163 | − limit?: number; | |
| 164 | − includeSchemas?: boolean; | |
| 165 | − }, | |
| 166 | − ) => | |
| 167 | − Effect.runPromise( | |
| 168 | − primitives.catalog!.tools({ | |
| 169 | − ...(input.namespace !== undefined ? { namespace: input.namespace } : {}), | |
| 170 | − ...(input.query !== undefined ? { query: input.query } : {}), | |
| 171 | − ...(input.limit !== undefined ? { limit: input.limit } : {}), | |
| 172 | − ...(input.includeSchemas !== undefined | |
| 173 | − ? { includeSchemas: input.includeSchemas } | |
| 174 | − : {}), | |
| 175 | − }), | |
| 176 | − ), | |
| 177 | − }, | |
| 178 | − metadata: { | |
| 179 | − sourceKey, | |
| 180 | − interaction: "auto", | |
| 181 | − }, | |
| 182 | − }); | |
| 183 | − } | |
| 133 | + tools["catalog.tools"] = toTool({ | |
| 134 | + tool: { | |
| 135 | + description: "List tools with optional namespace and query filters", | |
| 136 | + inputSchema: catalogToolsInputSchema, | |
| 137 | + outputSchema: catalogToolsOutputSchema, | |
| 138 | + execute: ( | |
| 139 | + toolInput: { | |
| 140 | + namespace?: string; | |
| 141 | + query?: string; | |
| 142 | + limit?: number; | |
| 143 | + includeSchemas?: boolean; | |
| 144 | + }, | |
| 145 | + ) => | |
| 146 | + Effect.runPromise( | |
| 147 | + getPrimitives().catalog.tools({ | |
| 148 | + ...(toolInput.namespace !== undefined | |
| 149 | + ? { namespace: toolInput.namespace } | |
| 150 | + : {}), | |
| 151 | + ...(toolInput.query !== undefined ? { query: toolInput.query } : {}), | |
| 152 | + ...(toolInput.limit !== undefined ? { limit: toolInput.limit } : {}), | |
| 153 | + ...(toolInput.includeSchemas !== undefined | |
| 154 | + ? { includeSchemas: toolInput.includeSchemas } | |
| 155 | + : {}), | |
| 156 | + }), | |
| 157 | + ), | |
| 158 | + }, | |
| 159 | + metadata: { | |
| 160 | + sourceKey, | |
| 161 | + interaction: "auto", | |
| 162 | + }, | |
| 163 | + }); | |
| 184 | 164 | |
| 185 | − if (primitives.describe) { | |
| 186 | − tools["describe.tool"] = toTool({ | |
| 187 | − tool: { | |
| 188 | − description: "Get metadata and optional schemas for a tool path", | |
| 189 | − inputSchema: describeToolInputSchema, | |
| 190 | − outputSchema: describeToolOutputSchema, | |
| 191 | − execute: ({ path, includeSchemas }: { path: string; includeSchemas?: boolean }) => | |
| 192 | − Effect.runPromise( | |
| 193 | − primitives.describe!.tool({ | |
| 194 | − path: asToolPath(path), | |
| 195 | − ...(includeSchemas !== undefined ? { includeSchemas } : {}), | |
| 196 | − }), | |
| 197 | − ), | |
| 198 | − }, | |
| 199 | − metadata: { | |
| 200 | − sourceKey, | |
| 201 | − interaction: "auto", | |
| 202 | − }, | |
| 203 | − }); | |
| 165 | + tools["describe.tool"] = toTool({ | |
| 166 | + tool: { | |
| 167 | + description: "Get metadata and optional schemas for a tool path", | |
| 168 | + inputSchema: describeToolInputSchema, | |
| 169 | + outputSchema: describeToolOutputSchema, | |
| 170 | + execute: ({ path, includeSchemas }: { path: string; includeSchemas?: boolean }) => | |
| 171 | + Effect.runPromise( | |
| 172 | + getPrimitives().describe.tool({ | |
| 173 | + path: asToolPath(path), | |
| 174 | + ...(includeSchemas !== undefined ? { includeSchemas } : {}), | |
| 175 | + }), | |
| 176 | + ), | |
| 177 | + }, | |
| 178 | + metadata: { | |
| 179 | + sourceKey, | |
| 180 | + interaction: "auto", | |
| 181 | + }, | |
| 182 | + }); | |
| 204 | 183 | |
| 205 | − tools["describe.schemaBundle"] = toTool({ | |
| 206 | − tool: { | |
| 207 | − description: "Get a shared schema bundle by id", | |
| 208 | − inputSchema: describeSchemaBundleInputSchema, | |
| 209 | − outputSchema: describeSchemaBundleOutputSchema, | |
| 210 | − execute: ({ id }: { id: string }) => | |
| 211 | − Effect.runPromise( | |
| 212 | − primitives.describe!.schemaBundle({ | |
| 213 | − id, | |
| 214 | − }), | |
| 215 | − ), | |
| 216 | − }, | |
| 217 | − metadata: { | |
| 218 | − sourceKey, | |
| 219 | − interaction: "auto", | |
| 220 | − }, | |
| 221 | − }); | |
| 222 | − } | |
| 184 | + tools["describe.schemaBundle"] = toTool({ | |
| 185 | + tool: { | |
| 186 | + description: "Get a shared schema bundle by id", | |
| 187 | + inputSchema: describeSchemaBundleInputSchema, | |
| 188 | + outputSchema: describeSchemaBundleOutputSchema, | |
| 189 | + execute: ({ id }: { id: string }) => | |
| 190 | + Effect.runPromise( | |
| 191 | + getPrimitives().describe.schemaBundle({ | |
| 192 | + id, | |
| 193 | + }), | |
| 194 | + ), | |
| 195 | + }, | |
| 196 | + metadata: { | |
| 197 | + sourceKey, | |
| 198 | + interaction: "auto", | |
| 199 | + }, | |
| 200 | + }); | |
| 223 | 201 | |
| 224 | − if (primitives.discover) { | |
| 225 | − tools.discover = toTool({ | |
| 226 | − tool: { | |
| 227 | − description: "Search tools by intent and return ranked matches", | |
| 228 | − inputSchema: discoverInputSchema, | |
| 229 | − outputSchema: discoverOutputSchema, | |
| 230 | − execute: ( | |
| 231 | − input: { | |
| 232 | − query: string; | |
| 233 | − limit?: number; | |
| 234 | − includeSchemas?: boolean; | |
| 235 | − }, | |
| 236 | − ) => | |
| 237 | − Effect.runPromise( | |
| 238 | − primitives.discover!({ | |
| 239 | − query: input.query, | |
| 240 | − ...(input.limit !== undefined ? { limit: input.limit } : {}), | |
| 241 | − ...(input.includeSchemas !== undefined | |
| 242 | − ? { includeSchemas: input.includeSchemas } | |
| 243 | − : {}), | |
| 244 | − }), | |
| 245 | − ), | |
| 246 | − }, | |
| 247 | − metadata: { | |
| 248 | − sourceKey, | |
| 249 | − interaction: "auto", | |
| 250 | − }, | |
| 251 | − }); | |
| 252 | − } | |
| 202 | + tools.discover = toTool({ | |
| 203 | + tool: { | |
| 204 | + description: "Search tools by intent and return ranked matches", | |
| 205 | + inputSchema: discoverInputSchema, | |
| 206 | + outputSchema: discoverOutputSchema, | |
| 207 | + execute: ( | |
| 208 | + toolInput: { | |
| 209 | + query: string; | |
| 210 | + limit?: number; | |
| 211 | + includeSchemas?: boolean; | |
| 212 | + }, | |
| 213 | + ) => | |
| 214 | + Effect.runPromise( | |
| 215 | + getPrimitives().discover({ | |
| 216 | + query: toolInput.query, | |
| 217 | + ...(toolInput.limit !== undefined ? { limit: toolInput.limit } : {}), | |
| 218 | + ...(toolInput.includeSchemas !== undefined | |
| 219 | + ? { includeSchemas: toolInput.includeSchemas } | |
| 220 | + : {}), | |
| 221 | + }), | |
| 222 | + ), | |
| 223 | + }, | |
| 224 | + metadata: { | |
| 225 | + sourceKey, | |
| 226 | + interaction: "auto", | |
| 227 | + }, | |
| 228 | + }); | |
| 253 | 229 | |
| 254 | 230 | return tools; |
| 255 | 231 | }; |
| @@ -1,5 +1,6 @@ | ||
| 1 | 1 | import type { StandardSchemaV1 } from "@standard-schema/spec"; |
| 2 | 2 | import type * as Effect from "effect/Effect"; |
| 3 | +import * as Schema from "effect/Schema"; | |
| 3 | 4 | |
| 4 | 5 | export type ToolPath = string & { readonly __toolPath: unique symbol }; |
| 5 | 6 | |
| @@ -142,6 +143,25 @@ export type ToolDescriptor = { · 136 unmodified lines | ||
| 142 | 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 | 165 | export type ToolSchemaBundle = { |
| 146 | 166 | id: string; |
| 147 | 167 | kind: string; |
| @@ -149,17 +169,37 @@ export type ToolSchemaBundle = { · 1 unmodified line | ||
| 149 | 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 | 179 | export type ToolNamespace = { |
| 153 | 180 | namespace: string; |
| 154 | 181 | displayName?: string; |
| 155 | 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 | 191 | export type SearchHit = { |
| 159 | 192 | path: ToolPath; |
| 160 | 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 | 203 | export interface ToolCatalog { |
| 164 | 204 | listNamespaces(input: { |
| 165 | 205 | limit: number; |