Unified · Split
| @@ -81,9 +81,9 @@ const readBindingStringArray = (source: Source, key: string): Array<string> | un · 80 unmodified lines | |||
| 81 | : undefined; | 81 | : undefined; |
| 82 | }; | 82 | }; |
| 83 | 83 | ||
| 84 | const readBindingTransport = (source: Source): "auto" | "streamable-http" | "sse" | undefined => { | 84 | const readBindingTransport = (source: Source): "auto" | "streamable-http" | "sse" | "stdio" | undefined => { |
| 85 | const candidate = source.binding.transport; | 85 | const candidate = source.binding.transport; |
| 86 | return candidate === "auto" || candidate === "streamable-http" || candidate === "sse" | 86 | return candidate === "auto" || candidate === "streamable-http" || candidate === "sse" || candidate === "stdio" |
| 87 | ? candidate | 87 | ? candidate |
| 88 | : undefined; | 88 | : undefined; |
| 89 | }; | 89 | }; |
| @@ -106,6 +106,10 @@ const refreshPayloadFromSource = (source: Source): ConnectSourcePayload | null = · 16 unmodified lines | |||
| 106 | ...(readBindingTransport(source) ? { transport: readBindingTransport(source) } : {}), | 106 | ...(readBindingTransport(source) ? { transport: readBindingTransport(source) } : {}), |
| 107 | queryParams: readBindingStringMap(source, "queryParams"), | 107 | queryParams: readBindingStringMap(source, "queryParams"), |
| 108 | headers: readBindingStringMap(source, "headers"), | 108 | headers: readBindingStringMap(source, "headers"), |
| 109 | command: readBindingString(source, "command"), | ||
| 110 | args: readBindingStringArray(source, "args"), | ||
| 111 | env: readBindingStringMap(source, "env"), | ||
| 112 | cwd: readBindingString(source, "cwd"), | ||
| 109 | }; | 113 | }; |
| 110 | case "openapi": { | 114 | case "openapi": { |
| 111 | const specUrl = readBindingString(source, "specUrl"); | 115 | const specUrl = readBindingString(source, "specUrl"); |
| @@ -364,7 +368,12 @@ function ModelView(props: { · 252 unmodified lines | |||
| 364 | 368 | ||
| 365 | const filteredTools = props.bundle.tools.filter((tool) => { | 369 | const filteredTools = props.bundle.tools.filter((tool) => { |
| 366 | if (terms.length === 0) return true; | 370 | if (terms.length === 0) return true; |
| 367 | const corpus = [tool.path, tool.method ?? ""] | 371 | const corpus = [ |
| 372 | tool.path, | ||
| 373 | tool.method ?? "", | ||
| 374 | tool.inputTypePreview ?? "", | ||
| 375 | tool.outputTypePreview ?? "", | ||
| 376 | ] | ||
| 368 | .join(" ") | 377 | .join(" ") |
| 369 | .toLowerCase(); | 378 | .toLowerCase(); |
| 370 | return terms.every((t) => corpus.includes(t)); | 379 | return terms.every((t) => corpus.includes(t)); |
| @@ -733,6 +742,14 @@ function ToolDetailPanel(props: { detail: SourceInspectionToolDetail }) { · 362 unmodified lines | |||
| 733 | </span> | 742 | </span> |
| 734 | )} | 743 | )} |
| 735 | </div> | 744 | </div> |
| 745 | <div className="mt-2 rounded-md border border-border bg-card/70 px-2.5 py-2"> | ||
| 746 | <div className="mb-1 text-[10px] font-medium uppercase tracking-wider text-muted-foreground/60"> | ||
| 747 | TypeScript Signature | ||
| 748 | </div> | ||
| 749 | <pre className="overflow-x-auto whitespace-pre-wrap font-mono text-[12px] text-foreground"> | ||
| 750 | {detail.contract.callDeclaration} | ||
| 751 | </pre> | ||
| 752 | </div> | ||
| 736 | </div> | 753 | </div> |
| 737 | </div> | 754 | </div> |
| 738 | </div> | 755 | </div> |
| @@ -740,6 +757,66 @@ function ToolDetailPanel(props: { detail: SourceInspectionToolDetail }) { · 1 unmodified line | |||
| 740 | {/* Body */} | 757 | {/* Body */} |
| 741 | <div className="flex-1 overflow-y-auto"> | 758 | <div className="flex-1 overflow-y-auto"> |
| 742 | <div className="px-5 py-4 space-y-4"> | 759 | <div className="px-5 py-4 space-y-4"> |
| 760 | <section className="overflow-hidden rounded-lg border border-border bg-card/60"> | ||
| 761 | <div className="border-b border-border px-3 py-2 text-[11px] font-medium uppercase tracking-wider text-muted-foreground/70"> | ||
| 762 | Contract | ||
| 763 | </div> | ||
| 764 | <div className="grid grid-cols-1 gap-3 p-3 xl:grid-cols-2"> | ||
| 765 | <DocumentPanel | ||
| 766 | title="Input Type" | ||
| 767 | body={detail.contract.input.typeDeclaration} | ||
| 768 | lang="typescript" | ||
| 769 | empty="No input type." | ||
| 770 | /> | ||
| 771 | <DocumentPanel | ||
| 772 | title="Output Type" | ||
| 773 | body={detail.contract.output.typeDeclaration} | ||
| 774 | lang="typescript" | ||
| 775 | empty="No output type." | ||
| 776 | /> | ||
| 777 | </div> | ||
| 778 | </section> | ||
| 779 | |||
| 780 | <section className="overflow-hidden rounded-lg border border-border bg-card/60"> | ||
| 781 | <div className="border-b border-border px-3 py-2 text-[11px] font-medium uppercase tracking-wider text-muted-foreground/70"> | ||
| 782 | Schemas | ||
| 783 | </div> | ||
| 784 | <div className="grid grid-cols-1 gap-3 p-3 xl:grid-cols-2"> | ||
| 785 | <DocumentPanel | ||
| 786 | title="Input Schema" | ||
| 787 | body={detail.contract.input.schemaJson} | ||
| 788 | lang="json" | ||
| 789 | empty="No input schema." | ||
| 790 | compact | ||
| 791 | /> | ||
| 792 | <DocumentPanel | ||
| 793 | title="Output Schema" | ||
| 794 | body={detail.contract.output.schemaJson} | ||
| 795 | lang="json" | ||
| 796 | empty="No output schema." | ||
| 797 | compact | ||
| 798 | /> | ||
| 799 | {detail.contract.input.exampleJson && ( | ||
| 800 | <DocumentPanel | ||
| 801 | title="Example Request" | ||
| 802 | body={detail.contract.input.exampleJson} | ||
| 803 | lang="json" | ||
| 804 | empty="" | ||
| 805 | compact | ||
| 806 | /> | ||
| 807 | )} | ||
| 808 | {detail.contract.output.exampleJson && ( | ||
| 809 | <DocumentPanel | ||
| 810 | title="Example Response" | ||
| 811 | body={detail.contract.output.exampleJson} | ||
| 812 | lang="json" | ||
| 813 | empty="" | ||
| 814 | compact | ||
| 815 | /> | ||
| 816 | )} | ||
| 817 | </div> | ||
| 818 | </section> | ||
| 819 | |||
| 743 | {detail.sections.map((section, index) => { | 820 | {detail.sections.map((section, index) => { |
| 744 | if (section.kind === "facts") { | 821 | if (section.kind === "facts") { |
| 745 | return ( | 822 | return ( |
| @@ -885,6 +962,30 @@ function DiscoveryView(props: { · 139 unmodified lines | |||
| 885 | {item.description} | 962 | {item.description} |
| 886 | </p> | 963 | </p> |
| 887 | )} | 964 | )} |
| 965 | {(item.inputTypePreview || item.outputTypePreview) && ( | ||
| 966 | <div className="mt-2 grid grid-cols-1 gap-2 md:grid-cols-2"> | ||
| 967 | {item.inputTypePreview && ( | ||
| 968 | <div className="rounded-md border border-border/70 bg-background/70 px-2.5 py-2"> | ||
| 969 | <div className="mb-1 text-[10px] font-medium uppercase tracking-wider text-muted-foreground/55"> | ||
| 970 | Input | ||
| 971 | </div> | ||
| 972 | <div className="font-mono text-[11px] text-foreground/85 line-clamp-3"> | ||
| 973 | {item.inputTypePreview} | ||
| 974 | </div> | ||
| 975 | </div> | ||
| 976 | )} | ||
| 977 | {item.outputTypePreview && ( | ||
| 978 | <div className="rounded-md border border-border/70 bg-background/70 px-2.5 py-2"> | ||
| 979 | <div className="mb-1 text-[10px] font-medium uppercase tracking-wider text-muted-foreground/55"> | ||
| 980 | Output | ||
| 981 | </div> | ||
| 982 | <div className="font-mono text-[11px] text-foreground/85 line-clamp-3"> | ||
| 983 | {item.outputTypePreview} | ||
| 984 | </div> | ||
| 985 | </div> | ||
| 986 | )} | ||
| 987 | </div> | ||
| 988 | )} | ||
| 888 | </button> | 989 | </button> |
| 889 | ))} | 990 | ))} |
| 890 | </div> | 991 | </div> |
| @@ -27,12 +27,41 @@ const searchableTextForTool = (tool: ToolDescriptor): string => · 26 unmodified lines | |||
| 27 | tool.path, | 27 | tool.path, |
| 28 | tool.sourceKey, | 28 | tool.sourceKey, |
| 29 | tool.description ?? "", | 29 | tool.description ?? "", |
| 30 | tool.inputTypePreview ?? "", | 30 | tool.contract?.inputTypePreview ?? "", |
| 31 | tool.outputTypePreview ?? "", | 31 | tool.contract?.outputTypePreview ?? "", |
| 32 | ] | 32 | ] |
| 33 | .join(" ") | 33 | .join(" ") |
| 34 | .toLowerCase(); | 34 | .toLowerCase(); |
| 35 | 35 | ||
| 36 | const descriptorContract = ( | ||
| 37 | descriptor: ToolDescriptor, | ||
| 38 | includeSchemas: boolean, | ||
| 39 | ): ToolDescriptor["contract"] | undefined => { | ||
| 40 | const contract = descriptor.contract; | ||
| 41 | if (!contract) { | ||
| 42 | return undefined; | ||
| 43 | } | ||
| 44 | |||
| 45 | if (includeSchemas) { | ||
| 46 | return contract; | ||
| 47 | } | ||
| 48 | |||
| 49 | return { | ||
| 50 | ...(contract.inputTypePreview !== undefined | ||
| 51 | ? { inputTypePreview: contract.inputTypePreview } | ||
| 52 | : {}), | ||
| 53 | ...(contract.outputTypePreview !== undefined | ||
| 54 | ? { outputTypePreview: contract.outputTypePreview } | ||
| 55 | : {}), | ||
| 56 | ...(contract.exampleInput !== undefined | ||
| 57 | ? { exampleInput: contract.exampleInput } | ||
| 58 | : {}), | ||
| 59 | ...(contract.exampleOutput !== undefined | ||
| 60 | ? { exampleOutput: contract.exampleOutput } | ||
| 61 | : {}), | ||
| 62 | }; | ||
| 63 | }; | ||
| 64 | |||
| 36 | const projectDescriptor = (input: { | 65 | const projectDescriptor = (input: { |
| 37 | descriptor: ToolDescriptor; | 66 | descriptor: ToolDescriptor; |
| 38 | includeSchemas: boolean; | 67 | includeSchemas: boolean; |
| @@ -45,8 +74,9 @@ const projectDescriptor = (input: { · 6 unmodified lines | |||
| 45 | 74 | ||
| 46 | return { | 75 | return { |
| 47 | ...descriptor, | 76 | ...descriptor, |
| 48 | inputSchema: undefined, | 77 | ...(descriptorContract(descriptor, false) |
| 49 | outputSchema: undefined, | 78 | ? { contract: descriptorContract(descriptor, false) } |
| 79 | : {}), | ||
| 50 | }; | 80 | }; |
| 51 | }; | 81 | }; |
| 52 | 82 | ||
| @@ -383,13 +413,8 @@ export function createDiscoveryPrimitivesFromToolCatalog(input: { · 330 unmodified lines | |||
| 383 | score: hit.score, | 413 | score: hit.score, |
| 384 | description: descriptor.description, | 414 | description: descriptor.description, |
| 385 | interaction: descriptor.interaction ?? "auto", | 415 | interaction: descriptor.interaction ?? "auto", |
| 386 | inputTypePreview: descriptor.inputTypePreview, | 416 | ...(descriptorContract(descriptor, includeSchemas) |
| 387 | outputTypePreview: descriptor.outputTypePreview, | 417 | ? { contract: descriptorContract(descriptor, includeSchemas) } |
| 388 | ...(includeSchemas | ||
| 389 | ? { | ||
| 390 | inputSchema: descriptor.inputSchema, | ||
| 391 | outputSchema: descriptor.outputSchema, | ||
| 392 | } | ||
| 393 | : {}), | 418 | : {}), |
| 394 | }; | 419 | }; |
| 395 | }) | 420 | }) |
| @@ -451,7 +476,7 @@ export function buildDynamicExecuteDescription(input: { · 55 unmodified lines | |||
| 451 | "Workflow:", | 476 | "Workflow:", |
| 452 | '1) const matches = await tools.discover({ query: "<intent>", limit: 12 });', | 477 | '1) const matches = await tools.discover({ query: "<intent>", limit: 12 });', |
| 453 | "2) const details = await tools.describe.tool({ path, includeSchemas: true });", | 478 | "2) const details = await tools.describe.tool({ path, includeSchemas: true });", |
| 454 | "3) Read details.inputSchema/details.outputSchema when you need the projected shape.", | 479 | "3) Read details.contract?.inputSchema/details.contract?.outputSchema when you need the projected shape.", |
| 455 | "4) Call selected tools.<path>(input).", | 480 | "4) Call selected tools.<path>(input).", |
| 456 | "Do not use fetch; use tools.* only.", | 481 | "Do not use fetch; use tools.* only.", |
| 457 | ].join("\n"); | 482 | ].join("\n"); |
| @@ -289,16 +289,20 @@ describe("codemode-core", () => { · 288 unmodified lines | |||
| 289 | path: asToolPath("source.docs.search"), | 289 | path: asToolPath("source.docs.search"), |
| 290 | sourceKey: "source.docs", | 290 | sourceKey: "source.docs", |
| 291 | description: "Search docs", | 291 | description: "Search docs", |
| 292 | inputTypePreview: "object", | 292 | contract: { |
| 293 | outputTypePreview: "object", | 293 | inputTypePreview: "object", |
| 294 | outputTypePreview: "object", | ||
| 295 | }, | ||
| 294 | }, | 296 | }, |
| 295 | "source.issues.create": { | 297 | "source.issues.create": { |
| 296 | path: asToolPath("source.issues.create"), | 298 | path: asToolPath("source.issues.create"), |
| 297 | sourceKey: "source.issues", | 299 | sourceKey: "source.issues", |
| 298 | description: "Create issue", | 300 | description: "Create issue", |
| 299 | interaction: "required", | 301 | interaction: "required", |
| 300 | inputTypePreview: "object", | 302 | contract: { |
| 301 | outputTypePreview: "object", | 303 | inputTypePreview: "object", |
| 304 | outputTypePreview: "object", | ||
| 305 | }, | ||
| 302 | }, | 306 | }, |
| 303 | }; | 307 | }; |
| 304 | 308 | ||
| @@ -327,8 +331,24 @@ describe("codemode-core", () => { · 22 unmodified lines | |||
| 327 | .slice(0, limit) | 331 | .slice(0, limit) |
| 328 | .map((descriptor) => ({ | 332 | .map((descriptor) => ({ |
| 329 | ...descriptor, | 333 | ...descriptor, |
| 330 | inputSchema: includeSchemas ? descriptor.inputSchema : undefined, | 334 | ...(descriptor.contract |
| 331 | outputSchema: includeSchemas ? descriptor.outputSchema : undefined, | 335 | ? { |
| 336 | contract: { | ||
| 337 | ...(descriptor.contract.inputTypePreview !== undefined | ||
| 338 | ? { inputTypePreview: descriptor.contract.inputTypePreview } | ||
| 339 | : {}), | ||
| 340 | ...(descriptor.contract.outputTypePreview !== undefined | ||
| 341 | ? { outputTypePreview: descriptor.contract.outputTypePreview } | ||
| 342 | : {}), | ||
| 343 | ...(includeSchemas && descriptor.contract.inputSchema !== undefined | ||
| 344 | ? { inputSchema: descriptor.contract.inputSchema } | ||
| 345 | : {}), | ||
| 346 | ...(includeSchemas && descriptor.contract.outputSchema !== undefined | ||
| 347 | ? { outputSchema: descriptor.contract.outputSchema } | ||
| 348 | : {}), | ||
| 349 | }, | ||
| 350 | } | ||
| 351 | : {}), | ||
| 332 | })), | 352 | })), |
| 333 | ), | 353 | ), |
| 334 | getToolByPath: ({ path }) => | 354 | getToolByPath: ({ path }) => |
| @@ -367,9 +387,11 @@ describe("codemode-core", () => { · 32 unmodified lines | |||
| 367 | path: asToolPath("github.repos.getRepo"), | 387 | path: asToolPath("github.repos.getRepo"), |
| 368 | sourceKey: "source.github", | 388 | sourceKey: "source.github", |
| 369 | description: "Get repository details", | 389 | description: "Get repository details", |
| 370 | inputTypePreview: "{ owner: string; repo: string }", | 390 | contract: { |
| 371 | outputTypePreview: "Repository", | 391 | inputTypePreview: "{ owner: string; repo: string }", |
| 372 | inputSchema: { type: "object" }, | 392 | outputTypePreview: "Repository", |
| 393 | inputSchema: { type: "object" }, | ||
| 394 | }, | ||
| 373 | }, | 395 | }, |
| 374 | }, | 396 | }, |
| 375 | { | 397 | { |
| @@ -383,8 +405,10 @@ describe("codemode-core", () => { · 7 unmodified lines | |||
| 383 | path: asToolPath("github.issues.list"), | 405 | path: asToolPath("github.issues.list"), |
| 384 | sourceKey: "source.github", | 406 | sourceKey: "source.github", |
| 385 | description: "List repository issues", | 407 | description: "List repository issues", |
| 386 | inputTypePreview: "{ owner: string; repo: string }", | 408 | contract: { |
| 387 | outputTypePreview: "Issue[]", | 409 | inputTypePreview: "{ owner: string; repo: string }", |
| 410 | outputTypePreview: "Issue[]", | ||
| 411 | }, | ||
| 388 | }, | 412 | }, |
| 389 | }, | 413 | }, |
| 390 | ], | 414 | ], |
| @@ -403,7 +427,7 @@ describe("codemode-core", () => { · 12 unmodified lines | |||
| 403 | includeSchemas: false, | 427 | includeSchemas: false, |
| 404 | }); | 428 | }); |
| 405 | expect(listed[0]?.path).toBe("github.repos.getRepo"); | 429 | expect(listed[0]?.path).toBe("github.repos.getRepo"); |
| 406 | expect(listed[0]?.inputSchema).toBeUndefined(); | 430 | expect(listed[0]?.contract?.inputSchema).toBeUndefined(); |
| 407 | 431 | ||
| 408 | const discovered = yield* catalog.searchTools({ | 432 | const discovered = yield* catalog.searchTools({ |
| 409 | query: "repository issues", | 433 | query: "repository issues", |
| @@ -415,7 +439,7 @@ describe("codemode-core", () => { · 5 unmodified lines | |||
| 415 | path: asToolPath("github.repos.getRepo"), | 439 | path: asToolPath("github.repos.getRepo"), |
| 416 | includeSchemas: true, | 440 | includeSchemas: true, |
| 417 | }); | 441 | }); |
| 418 | expect(described?.inputSchema).toEqual({ type: "object" }); | 442 | expect(described?.contract?.inputSchema).toEqual({ type: "object" }); |
| 419 | }), | 443 | }), |
| 420 | ); | 444 | ); |
| 421 | 445 | ||
| @@ -420,12 +420,26 @@ export function toolDescriptorsFromTools(input: { · 419 unmodified lines | |||
| 420 | const metadata = entry.metadata; | 420 | const metadata = entry.metadata; |
| 421 | const definition = entry.tool; | 421 | const definition = entry.tool; |
| 422 | const inputSchema = | 422 | const inputSchema = |
| 423 | metadata?.inputSchema | 423 | metadata?.contract?.inputSchema |
| 424 | ?? deriveSchemaValue(definition.inputSchema) | 424 | ?? deriveSchemaValue(definition.inputSchema) |
| 425 | ?? deriveSchemaValue(definition.parameters); | 425 | ?? deriveSchemaValue(definition.parameters); |
| 426 | const outputSchema = | 426 | const outputSchema = |
| 427 | metadata?.outputSchema | 427 | metadata?.contract?.outputSchema |
| 428 | ?? deriveSchemaValue(definition.outputSchema); | 428 | ?? deriveSchemaValue(definition.outputSchema); |
| 429 | const contract = { | ||
| 430 | inputTypePreview: | ||
| 431 | metadata?.contract?.inputTypePreview ?? inferTypeFromSchemaValue(inputSchema, "unknown"), | ||
| 432 | outputTypePreview: | ||
| 433 | metadata?.contract?.outputTypePreview ?? inferTypeFromSchemaValue(outputSchema, "unknown"), | ||
| 434 | ...(inputSchema !== undefined ? { inputSchema } : {}), | ||
| 435 | ...(outputSchema !== undefined ? { outputSchema } : {}), | ||
| 436 | ...(metadata?.contract?.exampleInput !== undefined | ||
| 437 | ? { exampleInput: metadata.contract.exampleInput } | ||
| 438 | : {}), | ||
| 439 | ...(metadata?.contract?.exampleOutput !== undefined | ||
| 440 | ? { exampleOutput: metadata.contract.exampleOutput } | ||
| 441 | : {}), | ||
| 442 | } satisfies NonNullable<ToolDescriptor["contract"]>; | ||
| 429 | 443 | ||
| 430 | return { | 444 | return { |
| 431 | path: entry.path, | 445 | path: entry.path, |
| @@ -433,18 +447,7 @@ export function toolDescriptorsFromTools(input: { · 1 unmodified line | |||
| 433 | description: definition.description, | 447 | description: definition.description, |
| 434 | interaction: metadata?.interaction, | 448 | interaction: metadata?.interaction, |
| 435 | elicitation: metadata?.elicitation, | 449 | elicitation: metadata?.elicitation, |
| 436 | inputTypePreview: | 450 | contract, |
| 437 | metadata?.inputTypePreview ?? inferTypeFromSchemaValue(inputSchema, "unknown"), | ||
| 438 | outputTypePreview: | ||
| 439 | metadata?.outputTypePreview ?? inferTypeFromSchemaValue(outputSchema, "unknown"), | ||
| 440 | ...(inputSchema !== undefined ? { inputSchema } : {}), | ||
| 441 | ...(outputSchema !== undefined ? { outputSchema } : {}), | ||
| 442 | ...(metadata?.exampleInput !== undefined | ||
| 443 | ? { exampleInput: metadata.exampleInput } | ||
| 444 | : {}), | ||
| 445 | ...(metadata?.exampleOutput !== undefined | ||
| 446 | ? { exampleOutput: metadata.exampleOutput } | ||
| 447 | : {}), | ||
| 448 | ...(metadata?.providerKind ? { providerKind: metadata.providerKind } : {}), | 451 | ...(metadata?.providerKind ? { providerKind: metadata.providerKind } : {}), |
| 449 | ...(metadata?.providerData !== undefined | 452 | ...(metadata?.providerData !== undefined |
| 450 | ? { providerData: metadata.providerData } | 453 | ? { providerData: metadata.providerData } |
| @@ -75,15 +75,28 @@ export type OnElicitation = ( · 74 unmodified lines | |||
| 75 | input: ToolElicitationRequest, | 75 | input: ToolElicitationRequest, |
| 76 | ) => Effect.Effect<ElicitationResponse, unknown>; | 76 | ) => Effect.Effect<ElicitationResponse, unknown>; |
| 77 | 77 | ||
| 78 | export type ToolMetadata = { | 78 | export type ToolContract = { |
| 79 | interaction?: "auto" | "required"; | ||
| 80 | elicitation?: ElicitationRequest; | ||
| 81 | inputTypePreview?: string; | 79 | inputTypePreview?: string; |
| 82 | outputTypePreview?: string; | 80 | outputTypePreview?: string; |
| 83 | inputSchema?: unknown; | 81 | inputSchema?: unknown; |
| 84 | outputSchema?: unknown; | 82 | outputSchema?: unknown; |
| 85 | exampleInput?: unknown; | 83 | exampleInput?: unknown; |
| 86 | exampleOutput?: unknown; | 84 | exampleOutput?: unknown; |
| 85 | }; | ||
| 86 | |||
| 87 | export const ToolContractSchema = Schema.Struct({ | ||
| 88 | inputTypePreview: Schema.optional(Schema.String), | ||
| 89 | outputTypePreview: Schema.optional(Schema.String), | ||
| 90 | inputSchema: Schema.optional(Schema.Unknown), | ||
| 91 | outputSchema: Schema.optional(Schema.Unknown), | ||
| 92 | exampleInput: Schema.optional(Schema.Unknown), | ||
| 93 | exampleOutput: Schema.optional(Schema.Unknown), | ||
| 94 | }); | ||
| 95 | |||
| 96 | export type ToolMetadata = { | ||
| 97 | interaction?: "auto" | "required"; | ||
| 98 | elicitation?: ElicitationRequest; | ||
| 99 | contract?: ToolContract; | ||
| 87 | sourceKey?: string; | 100 | sourceKey?: string; |
| 88 | providerKind?: string; | 101 | providerKind?: string; |
| 89 | providerData?: unknown; | 102 | providerData?: unknown; |
| @@ -131,12 +144,7 @@ export type ToolDescriptor = { · 41 unmodified lines | |||
| 131 | description?: string; | 144 | description?: string; |
| 132 | interaction?: "auto" | "required"; | 145 | interaction?: "auto" | "required"; |
| 133 | elicitation?: ElicitationRequest; | 146 | elicitation?: ElicitationRequest; |
| 134 | inputTypePreview?: string; | 147 | contract?: ToolContract; |
| 135 | outputTypePreview?: string; | ||
| 136 | inputSchema?: unknown; | ||
| 137 | outputSchema?: unknown; | ||
| 138 | exampleInput?: unknown; | ||
| 139 | exampleOutput?: unknown; | ||
| 140 | providerKind?: string; | 148 | providerKind?: string; |
| 141 | providerData?: unknown; | 149 | providerData?: unknown; |
| 142 | }; | 150 | }; |
| @@ -149,12 +157,7 @@ export const ToolDescriptorSchema = Schema.Struct({ · 6 unmodified lines | |||
| 149 | Schema.Union(Schema.Literal("auto"), Schema.Literal("required")), | 157 | Schema.Union(Schema.Literal("auto"), Schema.Literal("required")), |
| 150 | ), | 158 | ), |
| 151 | elicitation: Schema.optional(Schema.Unknown), | 159 | elicitation: Schema.optional(Schema.Unknown), |
| 152 | inputTypePreview: Schema.optional(Schema.String), | 160 | contract: Schema.optional(ToolContractSchema), |
| 153 | outputTypePreview: Schema.optional(Schema.String), | ||
| 154 | inputSchema: Schema.optional(Schema.Unknown), | ||
| 155 | outputSchema: Schema.optional(Schema.Unknown), | ||
| 156 | exampleInput: Schema.optional(Schema.Unknown), | ||
| 157 | exampleOutput: Schema.optional(Schema.Unknown), | ||
| 158 | providerKind: Schema.optional(Schema.String), | 161 | providerKind: Schema.optional(Schema.String), |
| 159 | providerData: Schema.optional(Schema.Unknown), | 162 | providerData: Schema.optional(Schema.Unknown), |
| 160 | }); | 163 | }); |
| @@ -2,7 +2,12 @@ import { createHash } from "node:crypto"; · 1 unmodified line | |||
| 2 | 2 | ||
| 3 | import type { ProjectedCatalog } from "@executor/ir/catalog"; | 3 | import type { ProjectedCatalog } from "@executor/ir/catalog"; |
| 4 | import type { ShapeSymbolId } from "@executor/ir/ids"; | 4 | import type { ShapeSymbolId } from "@executor/ir/ids"; |
| 5 | import type { CatalogV1, ShapeNode, ShapeSymbol } from "@executor/ir/model"; | 5 | import type { |
| 6 | CatalogV1, | ||
| 7 | DocumentationBlock, | ||
| 8 | ShapeNode, | ||
| 9 | ShapeSymbol, | ||
| 10 | } from "@executor/ir/model"; | ||
| 6 | 11 | ||
| 7 | export type CatalogTypeRoot = { | 12 | export type CatalogTypeRoot = { |
| 8 | readonly shapeId: ShapeSymbolId; | 13 | readonly shapeId: ShapeSymbolId; |
| @@ -128,7 +133,9 @@ const objectTypeLiteral = ( · 119 unmodified lines | |||
| 128 | 133 | ||
| 129 | return [ | 134 | return [ |
| 130 | "{", | 135 | "{", |
| 131 | ...lines.map((line) => `${indent}${line}`), | 136 | ...lines.flatMap((line) => |
| 137 | line.split("\n").map((segment) => `${indent}${segment}`) | ||
| 138 | ), | ||
| 132 | `${indent.slice(0, -2)}}`, | 139 | `${indent.slice(0, -2)}}`, |
| 133 | ].join("\n"); | 140 | ].join("\n"); |
| 134 | }; | 141 | }; |
| @@ -153,6 +160,73 @@ const isSyntheticShapeLabel = (value: string): boolean => · 18 unmodified lines | |||
| 153 | 160 | ||
| 154 | const looksLikeHumanPhrase = (value: string): boolean => /\s/.test(value.trim()); | 161 | const looksLikeHumanPhrase = (value: string): boolean => /\s/.test(value.trim()); |
| 155 | 162 | ||
| 163 | const cleanDocText = (value: string | undefined): string | null => { | ||
| 164 | const trimmed = value?.trim(); | ||
| 165 | return trimmed && trimmed.length > 0 ? trimmed : null; | ||
| 166 | }; | ||
| 167 | |||
| 168 | const escapeJsDocText = (value: string): string => value.replace(/\*\//g, "*\\/"); | ||
| 169 | |||
| 170 | const pushParagraphLines = ( | ||
| 171 | lines: Array<string>, | ||
| 172 | value: string | null, | ||
| 173 | ): void => { | ||
| 174 | if (!value) { | ||
| 175 | return; | ||
| 176 | } | ||
| 177 | |||
| 178 | if (lines.length > 0) { | ||
| 179 | lines.push(""); | ||
| 180 | } | ||
| 181 | |||
| 182 | for (const line of value.split(/\r?\n/)) { | ||
| 183 | lines.push(escapeJsDocText(line.trimEnd())); | ||
| 184 | } | ||
| 185 | }; | ||
| 186 | |||
| 187 | export const documentationComment = (input: { | ||
| 188 | title?: string; | ||
| 189 | docs?: DocumentationBlock; | ||
| 190 | deprecated?: boolean; | ||
| 191 | includeTitle?: boolean; | ||
| 192 | }): string | null => { | ||
| 193 | const lines: Array<string> = []; | ||
| 194 | const title = input.includeTitle && input.title && looksLikeHumanPhrase(input.title) | ||
| 195 | ? cleanDocText(input.title) | ||
| 196 | : null; | ||
| 197 | const summary = cleanDocText(input.docs?.summary); | ||
| 198 | const description = cleanDocText(input.docs?.description); | ||
| 199 | const externalDocsUrl = cleanDocText(input.docs?.externalDocsUrl); | ||
| 200 | |||
| 201 | pushParagraphLines(lines, title && title !== summary ? title : null); | ||
| 202 | pushParagraphLines(lines, summary); | ||
| 203 | pushParagraphLines(lines, description && description !== summary ? description : null); | ||
| 204 | |||
| 205 | if (externalDocsUrl) { | ||
| 206 | if (lines.length > 0) { | ||
| 207 | lines.push(""); | ||
| 208 | } | ||
| 209 | lines.push(`@see ${escapeJsDocText(externalDocsUrl)}`); | ||
| 210 | } | ||
| 211 | |||
| 212 | if (input.deprecated) { | ||
| 213 | if (lines.length > 0) { | ||
| 214 | lines.push(""); | ||
| 215 | } | ||
| 216 | lines.push("@deprecated"); | ||
| 217 | } | ||
| 218 | |||
| 219 | if (lines.length === 0) { | ||
| 220 | return null; | ||
| 221 | } | ||
| 222 | |||
| 223 | return [ | ||
| 224 | "/**", | ||
| 225 | ...lines.map((line) => line.length > 0 ? ` * ${line}` : " *"), | ||
| 226 | " */", | ||
| 227 | ].join("\n"); | ||
| 228 | }; | ||
| 229 | |||
| 156 | const compactAliasHint = (value: string): string => { | 230 | const compactAliasHint = (value: string): string => { |
| 157 | const segments = typeNameWords(value); | 231 | const segments = typeNameWords(value); |
| 158 | if (segments.length <= 5) { | 232 | if (segments.length <= 5) { |
| @@ -254,16 +328,27 @@ const renderObjectNode = ( · 95 unmodified lines | |||
| 254 | stack: readonly ShapeSymbolId[], | 328 | stack: readonly ShapeSymbolId[], |
| 255 | aliasHint: string | undefined, | 329 | aliasHint: string | undefined, |
| 256 | renderShape: RenderShape, | 330 | renderShape: RenderShape, |
| 331 | includeDocs: boolean, | ||
| 257 | ): string => { | 332 | ): string => { |
| 258 | const required = new Set(node.required); | 333 | const required = new Set(node.required); |
| 259 | const lines = Object.keys(node.fields) | 334 | const lines = Object.keys(node.fields) |
| 260 | .sort((left, right) => left.localeCompare(right)) | 335 | .sort((left, right) => left.localeCompare(right)) |
| 261 | .map((key) => | 336 | .map((key) => { |
| 262 | `${formatPropertyKey(key)}${required.has(key) ? "" : "?"}: ${renderShape(node.fields[key]!.shapeId, { | 337 | const field = node.fields[key]!; |
| 263 | stack, | 338 | const propertyLine = |
| 264 | aliasHint: aliasHint ? joinTypeNameSegments(aliasHint, key) : key, | 339 | `${formatPropertyKey(key)}${required.has(key) ? "" : "?"}: ${renderShape(field.shapeId, { |
| 265 | })};` | 340 | stack, |
| 266 | ); | 341 | aliasHint: aliasHint ? joinTypeNameSegments(aliasHint, key) : key, |
| 342 | })};`; | ||
| 343 | const comment = includeDocs | ||
| 344 | ? documentationComment({ | ||
| 345 | docs: field.docs, | ||
| 346 | deprecated: field.deprecated, | ||
| 347 | }) | ||
| 348 | : null; | ||
| 349 | |||
| 350 | return comment ? `${comment}\n${propertyLine}` : propertyLine; | ||
| 351 | }); | ||
| 267 | 352 | ||
| 268 | const patternShapeIds = Object.values(node.patternProperties ?? {}); | 353 | const patternShapeIds = Object.values(node.patternProperties ?? {}); |
| 269 | const hasUnknownIndex = node.additionalProperties === true; | 354 | const hasUnknownIndex = node.additionalProperties === true; |
| @@ -290,6 +375,7 @@ const renderObjectFields = ( · 20 unmodified lines | |||
| 290 | stack: readonly ShapeSymbolId[], | 375 | stack: readonly ShapeSymbolId[], |
| 291 | aliasHint: string | undefined, | 376 | aliasHint: string | undefined, |
| 292 | renderShape: RenderShape, | 377 | renderShape: RenderShape, |
| 378 | includeDocs: boolean, | ||
| 293 | ): string => | 379 | ): string => |
| 294 | renderObjectNode( | 380 | renderObjectNode( |
| 295 | { | 381 | { |
| @@ -301,6 +387,7 @@ const renderObjectFields = ( · 5 unmodified lines | |||
| 301 | stack, | 387 | stack, |
| 302 | aliasHint, | 388 | aliasHint, |
| 303 | renderShape, | 389 | renderShape, |
| 390 | includeDocs, | ||
| 304 | ); | 391 | ); |
| 305 | 392 | ||
| 306 | const renderShapeBody = ( | 393 | const renderShapeBody = ( |
| @@ -309,6 +396,7 @@ const renderShapeBody = ( · 2 unmodified lines | |||
| 309 | stack: readonly ShapeSymbolId[], | 396 | stack: readonly ShapeSymbolId[], |
| 310 | aliasHint: string | undefined, | 397 | aliasHint: string | undefined, |
| 311 | renderShape: RenderShape, | 398 | renderShape: RenderShape, |
| 399 | includeDocs: boolean, | ||
| 312 | ): string => { | 400 | ): string => { |
| 313 | const shape = getShapeSymbol(catalog, shapeId); | 401 | const shape = getShapeSymbol(catalog, shapeId); |
| 314 | const node = shape.node; | 402 | const node = shape.node; |
| @@ -321,7 +409,7 @@ const renderShapeBody = ( · 6 unmodified lines | |||
| 321 | return renderInlineShapeNode(node); | 409 | return renderInlineShapeNode(node); |
| 322 | case "object": | 410 | case "object": |
| 323 | case "graphqlInterface": | 411 | case "graphqlInterface": |
| 324 | return renderObjectFields(node, stack, aliasHint, renderShape); | 412 | return renderObjectFields(node, stack, aliasHint, renderShape, includeDocs); |
| 325 | case "array": | 413 | case "array": |
| 326 | return `Array<${wrapCompositeType(renderShape(node.itemShapeId, { | 414 | return `Array<${wrapCompositeType(renderShape(node.itemShapeId, { |
| 327 | stack, | 415 | stack, |
| @@ -714,6 +802,7 @@ export const createCatalogTypeProjector = (input: { · 386 unmodified lines | |||
| 714 | stack: readonly ShapeSymbolId[], | 802 | stack: readonly ShapeSymbolId[], |
| 715 | aliasHint: string | undefined, | 803 | aliasHint: string | undefined, |
| 716 | renderShape: RenderShape, | 804 | renderShape: RenderShape, |
| 805 | includeDocs: boolean, | ||
| 717 | ): string | null => { | 806 | ): string | null => { |
| 718 | const variants = shapeIds.map((shapeId) => resolveUnionVariantObject(shapeId)); | 807 | const variants = shapeIds.map((shapeId) => resolveUnionVariantObject(shapeId)); |
| 719 | if (variants.some((variant) => variant === null)) { | 808 | if (variants.some((variant) => variant === null)) { |
| @@ -769,7 +858,7 @@ export const createCatalogTypeProjector = (input: { · 49 unmodified lines | |||
| 769 | } | 858 | } |
| 770 | 859 | ||
| 771 | const baseText = baseHasSharedStructure | 860 | const baseText = baseHasSharedStructure |
| 772 | ? renderObjectNode(sharedNode, stack, aliasHint, renderShape) | 861 | ? renderObjectNode(sharedNode, stack, aliasHint, renderShape, includeDocs) |
| 773 | : null; | 862 | : null; |
| 774 | 863 | ||
| 775 | const variantTexts = objectVariants.map((variant, index) => { | 864 | const variantTexts = objectVariants.map((variant, index) => { |
| @@ -786,6 +875,7 @@ export const createCatalogTypeProjector = (input: { · 10 unmodified lines | |||
| 786 | stack, | 875 | stack, |
| 787 | aliasHint ? joinTypeNameSegments(aliasHint, variantAliasLabel(discriminator, index)) : variantAliasLabel(discriminator, index), | 876 | aliasHint ? joinTypeNameSegments(aliasHint, variantAliasLabel(discriminator, index)) : variantAliasLabel(discriminator, index), |
| 788 | renderShape, | 877 | renderShape, |
| 878 | includeDocs, | ||
| 789 | ); | 879 | ); |
| 790 | }); | 880 | }); |
| 791 | const unionText = variantTexts.map((variantText) => wrapCompositeType(variantText)).join(" | "); | 881 | const unionText = variantTexts.map((variantText) => wrapCompositeType(variantText)).join(" | "); |
| @@ -857,15 +947,21 @@ export const createCatalogTypeProjector = (input: { · 65 unmodified lines | |||
| 857 | switch (shape.node.type) { | 947 | switch (shape.node.type) { |
| 858 | case "anyOf": | 948 | case "anyOf": |
| 859 | case "oneOf": { | 949 | case "oneOf": { |
| 860 | const normalized = normalizedUnionRender(shape.node.items, stack, aliasHint, renderDeclarationShape); | 950 | const normalized = normalizedUnionRender( |
| 861 | return normalized ?? renderShapeBody(catalog, shapeId, stack, aliasHint, renderDeclarationShape); | 951 | shape.node.items, |
| 952 | stack, | ||
| 953 | aliasHint, | ||
| 954 | renderDeclarationShape, | ||
| 955 | true, | ||
| 956 | ); | ||
| 957 | return normalized ?? renderShapeBody(catalog, shapeId, stack, aliasHint, renderDeclarationShape, true); | ||
| 862 | } | 958 | } |
| 863 | case "graphqlUnion": { | 959 | case "graphqlUnion": { |
| 864 | const normalized = normalizedUnionRender(shape.node.memberTypeIds, stack, aliasHint, renderDeclarationShape); | 960 | const normalized = normalizedUnionRender(shape.node.memberTypeIds, stack, aliasHint, renderDeclarationShape, true); |
| 865 | return normalized ?? renderShapeBody(catalog, shapeId, stack, aliasHint, renderDeclarationShape); | 961 | return normalized ?? renderShapeBody(catalog, shapeId, stack, aliasHint, renderDeclarationShape, true); |
| 866 | } | 962 | } |
| 867 | default: | 963 | default: |
| 868 | return renderShapeBody(catalog, shapeId, stack, aliasHint, renderDeclarationShape); | 964 | return renderShapeBody(catalog, shapeId, stack, aliasHint, renderDeclarationShape, true); |
| 869 | } | 965 | } |
| 870 | }; | 966 | }; |
| 871 | 967 | ||
| @@ -921,16 +1017,49 @@ export const createCatalogTypeProjector = (input: { · 49 unmodified lines | |||
| 921 | switch (shape.node.type) { | 1017 | switch (shape.node.type) { |
| 922 | case "anyOf": | 1018 | case "anyOf": |
| 923 | case "oneOf": { | 1019 | case "oneOf": { |
| 924 | const normalized = normalizedUnionRender(shape.node.items, [...stack, shapeId], options.aliasHint, renderSelfContainedShape); | 1020 | const normalized = normalizedUnionRender( |
| 925 | return normalized ?? renderShapeBody(catalog, shapeId, [...stack, shapeId], options.aliasHint, renderSelfContainedShape); | 1021 | shape.node.items, |
| 1022 | [...stack, shapeId], | ||
| 1023 | options.aliasHint, | ||
| 1024 | renderSelfContainedShape, | ||
| 1025 | false, | ||
| 1026 | ); | ||
| 1027 | return normalized ?? renderShapeBody( | ||
| 1028 | catalog, | ||
| 1029 | shapeId, | ||
| 1030 | [...stack, shapeId], | ||
| 1031 | options.aliasHint, | ||
| 1032 | renderSelfContainedShape, | ||
| 1033 | false, | ||
| 1034 | ); | ||
| 926 | } | 1035 | } |
| 927 | case "graphqlUnion": { | 1036 | case "graphqlUnion": { |
| 928 | const normalized = normalizedUnionRender(shape.node.memberTypeIds, [...stack, shapeId], options.aliasHint, renderSelfContainedShape); | 1037 | const normalized = normalizedUnionRender( |
| 929 | return normalized ?? renderShapeBody(catalog, shapeId, [...stack, shapeId], options.aliasHint, renderSelfContainedShape); | 1038 | shape.node.memberTypeIds, |
| 1039 | [...stack, shapeId], | ||
| 1040 | options.aliasHint, | ||
| 1041 | renderSelfContainedShape, | ||
| 1042 | false, | ||
| 1043 | ); | ||
| 1044 | return normalized ?? renderShapeBody( | ||
| 1045 | catalog, | ||
| 1046 | shapeId, | ||
| 1047 | [...stack, shapeId], | ||
| 1048 | options.aliasHint, | ||
| 1049 | renderSelfContainedShape, | ||
| 1050 | false, | ||
| 1051 | ); | ||
| 930 | } | 1052 | } |
| 931 | } | 1053 | } |
| 932 | 1054 | ||
| 933 | return renderShapeBody(catalog, shapeId, [...stack, shapeId], options.aliasHint, renderSelfContainedShape); | 1055 | return renderShapeBody( |
| 1056 | catalog, | ||
| 1057 | shapeId, | ||
| 1058 | [...stack, shapeId], | ||
| 1059 | options.aliasHint, | ||
| 1060 | renderSelfContainedShape, | ||
| 1061 | false, | ||
| 1062 | ); | ||
| 934 | }; | 1063 | }; |
| 935 | 1064 | ||
| 936 | const supportingDeclarations = (): readonly string[] => { | 1065 | const supportingDeclarations = (): readonly string[] => { |
| @@ -960,11 +1089,25 @@ export const createCatalogTypeProjector = (input: { · 23 unmodified lines | |||
| 960 | const canonicalType = canonicalTypeForSignature(signature); | 1089 | const canonicalType = canonicalTypeForSignature(signature); |
| 961 | const representativeShapeId = representativeShapeIdForSignature(signature); | 1090 | const representativeShapeId = representativeShapeIdForSignature(signature); |
| 962 | const aliasName = aliasNameForSignature(signature); | 1091 | const aliasName = aliasNameForSignature(signature); |
| 1092 | const representativeShape = getShapeSymbol(catalog, representativeShapeId); | ||
| 1093 | const comment = documentationComment({ | ||
| 1094 | title: representativeShape.title, | ||
| 1095 | docs: representativeShape.docs, | ||
| 1096 | deprecated: representativeShape.deprecated, | ||
| 1097 | includeTitle: true, | ||
| 1098 | }); | ||
| 963 | if (canonicalType !== aliasName) { | 1099 | if (canonicalType !== aliasName) { |
| 964 | return `type ${aliasName} = ${canonicalType};`; | 1100 | return comment |
| 1101 | ? `${comment}\ntype ${aliasName} = ${canonicalType};` | ||
| 1102 | : `type ${aliasName} = ${canonicalType};`; | ||
| 965 | } | 1103 | } |
| 966 | const body = renderDeclarationShapeBody(representativeShapeId, [representativeShapeId], aliasName); | 1104 | const body = renderDeclarationShapeBody(representativeShapeId, [representativeShapeId], aliasName); |
| 967 | return body === aliasName ? "" : `type ${aliasName} = ${body};`; | 1105 | if (body === aliasName) { |
| 1106 | return ""; | ||
| 1107 | } | ||
| 1108 | |||
| 1109 | const declaration = `type ${aliasName} = ${body};`; | ||
| 1110 | return comment ? `${comment}\n${declaration}` : declaration; | ||
| 968 | }) | 1111 | }) |
| 969 | .filter((declaration) => declaration.length > 0); | 1112 | .filter((declaration) => declaration.length > 0); |
| 970 | }; | 1113 | }; |
| @@ -3,7 +3,7 @@ import parserBabel from "prettier/plugins/babel"; · 2 unmodified lines | |||
| 3 | import parserEstree from "prettier/plugins/estree"; | 3 | import parserEstree from "prettier/plugins/estree"; |
| 4 | import parserTypescript from "prettier/plugins/typescript"; | 4 | import parserTypescript from "prettier/plugins/typescript"; |
| 5 | 5 | ||
| 6 | export type PrettierParser = "json" | "typescript"; | 6 | export type PrettierParser = "json" | "typescript" | "typescript-module"; |
| 7 | 7 | ||
| 8 | const plugins = [parserBabel, parserEstree, parserTypescript]; | 8 | const plugins = [parserBabel, parserEstree, parserTypescript]; |
| 9 | 9 | ||
| @@ -31,7 +31,7 @@ export async function formatWithPrettier( · 21 unmodified lines | |||
| 31 | } | 31 | } |
| 32 | 32 | ||
| 33 | const result = await format(input, { | 33 | const result = await format(input, { |
| 34 | parser, | 34 | parser: parser === "typescript-module" ? "typescript" : parser, |
| 35 | plugins, | 35 | plugins, |
| 36 | printWidth: 60, | 36 | printWidth: 60, |
| 37 | tabWidth: 2, | 37 | tabWidth: 2, |
| @@ -353,7 +353,7 @@ describe("source-catalog-runtime", () => { · 352 unmodified lines | |||
| 353 | }); | 353 | }); |
| 354 | 354 | ||
| 355 | expect(tool).toBeDefined(); | 355 | expect(tool).toBeDefined(); |
| 356 | expect(tool?.descriptor.inputSchema).toMatchObject({ | 356 | expect(tool?.descriptor.contract?.inputSchema).toMatchObject({ |
| 357 | type: "object", | 357 | type: "object", |
| 358 | properties: { | 358 | properties: { |
| 359 | filter: { | 359 | filter: { |
| @@ -368,11 +368,11 @@ describe("source-catalog-runtime", () => { · 8 unmodified lines | |||
| 368 | }, | 368 | }, |
| 369 | }); | 369 | }); |
| 370 | 370 | ||
| 371 | const serializedInput = JSON.stringify(tool?.descriptor.inputSchema); | 371 | const serializedInput = JSON.stringify(tool?.descriptor.contract?.inputSchema); |
| 372 | expect(serializedInput).not.toContain("\"$ref\":\"#/$defs/shape_"); | 372 | expect(serializedInput).not.toContain("\"$ref\":\"#/$defs/shape_"); |
| 373 | expect(serializedInput).not.toContain("\"shape_"); | 373 | expect(serializedInput).not.toContain("\"shape_"); |
| 374 | expect(tool?.descriptor.inputTypePreview).toContain("filter?: {"); | 374 | expect(tool?.descriptor.contract?.inputTypePreview).toContain("filter?: {"); |
| 375 | expect(tool?.descriptor.outputTypePreview).toContain("nodes?: {"); | 375 | expect(tool?.descriptor.contract?.outputTypePreview).toContain("nodes?: {"); |
| 376 | })); | 376 | })); |
| 377 | 377 | ||
| 378 | it.effect("can skip type previews when only list metadata is needed", () => | 378 | it.effect("can skip type previews when only list metadata is needed", () => |
| @@ -384,10 +384,10 @@ describe("source-catalog-runtime", () => { · 5 unmodified lines | |||
| 384 | }); | 384 | }); |
| 385 | 385 | ||
| 386 | expect(tool).toBeDefined(); | 386 | expect(tool).toBeDefined(); |
| 387 | expect(tool?.descriptor.inputSchema).toBeUndefined(); | 387 | expect(tool?.descriptor.contract?.inputSchema).toBeUndefined(); |
| 388 | expect(tool?.descriptor.outputSchema).toBeUndefined(); | 388 | expect(tool?.descriptor.contract?.outputSchema).toBeUndefined(); |
| 389 | expect(tool?.descriptor.inputTypePreview).toBeUndefined(); | 389 | expect(tool?.descriptor.contract?.inputTypePreview).toBeUndefined(); |
| 390 | expect(tool?.descriptor.outputTypePreview).toBeUndefined(); | 390 | expect(tool?.descriptor.contract?.outputTypePreview).toBeUndefined(); |
| 391 | })); | 391 | })); |
| 392 | 392 | ||
| 393 | it.effect("projects a single tool by path without expanding the whole catalog", () => | 393 | it.effect("projects a single tool by path without expanding the whole catalog", () => |
| @@ -400,7 +400,7 @@ describe("source-catalog-runtime", () => { · 6 unmodified lines | |||
| 400 | 400 | ||
| 401 | expect(tool).not.toBeNull(); | 401 | expect(tool).not.toBeNull(); |
| 402 | expect(tool?.path).toBe("linear.teams"); | 402 | expect(tool?.path).toBe("linear.teams"); |
| 403 | expect(tool?.descriptor.inputSchema).toMatchObject({ | 403 | expect(tool?.descriptor.contract?.inputSchema).toMatchObject({ |
| 404 | type: "object", | 404 | type: "object", |
| 405 | properties: { | 405 | properties: { |
| 406 | filter: { | 406 | filter: { |
| @@ -569,12 +569,12 @@ describe("source-catalog-runtime", () => { · 162 unmodified lines | |||
| 569 | }); | 569 | }); |
| 570 | 570 | ||
| 571 | expect(tool).not.toBeNull(); | 571 | expect(tool).not.toBeNull(); |
| 572 | expect(tool?.descriptor.outputTypePreview).toContain("actor?: string;"); | 572 | expect(tool?.descriptor.contract?.outputTypePreview).toContain("actor?: string;"); |
| 573 | expect(tool?.descriptor.outputTypePreview).toContain('action: "blocked";'); | 573 | expect(tool?.descriptor.contract?.outputTypePreview).toContain('action: "blocked";'); |
| 574 | expect(tool?.descriptor.outputTypePreview).toContain('action: "unblocked";'); | 574 | expect(tool?.descriptor.contract?.outputTypePreview).toContain('action: "unblocked";'); |
| 575 | expect(tool?.descriptor.outputTypePreview).toContain('action: "route-blocked";'); | 575 | expect(tool?.descriptor.contract?.outputTypePreview).toContain('action: "route-blocked";'); |
| 576 | expect(tool?.descriptor.outputTypePreview).toContain("& ("); | 576 | expect(tool?.descriptor.contract?.outputTypePreview).toContain("& ("); |
| 577 | expect(tool?.descriptor.outputTypePreview).not.toContain("Member2"); | 577 | expect(tool?.descriptor.contract?.outputTypePreview).not.toContain("Member2"); |
| 578 | })); | 578 | })); |
| 579 | 579 | ||
| 580 | it.effect("uses the shared TS projector for unsupported preview shapes", () => | 580 | it.effect("uses the shared TS projector for unsupported preview shapes", () => |
| @@ -622,6 +622,6 @@ describe("source-catalog-runtime", () => { · 41 unmodified lines | |||
| 622 | }); | 622 | }); |
| 623 | 623 | ||
| 624 | expect(tool).not.toBeNull(); | 624 | expect(tool).not.toBeNull(); |
| 625 | expect(tool?.descriptor.inputTypePreview).toContain("unsupported?: unknown;"); | 625 | expect(tool?.descriptor.contract?.inputTypePreview).toContain("unsupported?: unknown;"); |
| 626 | })); | 626 | })); |
| 627 | }); | 627 | }); |
| @@ -19,6 +19,7 @@ import { · 18 unmodified lines | |||
| 19 | projectCatalogForAgentSdk, | 19 | projectCatalogForAgentSdk, |
| 20 | type ProjectedCatalog, | 20 | type ProjectedCatalog, |
| 21 | } from "@executor/ir/catalog"; | 21 | } from "@executor/ir/catalog"; |
| 22 | import type { ShapeSymbolId } from "@executor/ir/ids"; | ||
| 22 | import type { | 23 | import type { |
| 23 | Capability, | 24 | Capability, |
| 24 | CatalogSnapshotV1, | 25 | CatalogSnapshotV1, |
| @@ -29,10 +30,13 @@ import type { · 4 unmodified lines | |||
| 29 | import { LocalSourceArtifactMissingError } from "../../local/errors"; | 30 | import { LocalSourceArtifactMissingError } from "../../local/errors"; |
| 30 | import { | 31 | import { |
| 31 | createCatalogTypeProjector, | 32 | createCatalogTypeProjector, |
| 33 | documentationComment, | ||
| 32 | joinTypeNameSegments, | 34 | joinTypeNameSegments, |
| 33 | projectedCatalogTypeRoots, | 35 | projectedCatalogTypeRoots, |
| 36 | shapeAllowsOmittedArgs, | ||
| 34 | type CatalogTypeProjector, | 37 | type CatalogTypeProjector, |
| 35 | } from "../catalog-typescript"; | 38 | } from "../catalog-typescript"; |
| 39 | import { formatWithPrettier } from "../prettier-format"; | ||
| 36 | import { | 40 | import { |
| 37 | RuntimeLocalWorkspaceService, | 41 | RuntimeLocalWorkspaceService, |
| 38 | type RuntimeLocalWorkspaceState, | 42 | type RuntimeLocalWorkspaceState, |
| @@ -85,6 +89,24 @@ export type LoadedSourceCatalogToolIndexEntry = Omit< · 46 unmodified lines | |||
| 85 | "revision" | "projectedDescriptor" | "typeProjector" | 89 | "revision" | "projectedDescriptor" | "typeProjector" |
| 86 | >; | 90 | >; |
| 87 | 91 | ||
| 92 | export type LoadedSourceCatalogToolContractSide = { | ||
| 93 | shapeId: string | null; | ||
| 94 | typePreview: string | null; | ||
| 95 | typeDeclaration: string | null; | ||
| 96 | schemaJson: string | null; | ||
| 97 | exampleJson: string | null; | ||
| 98 | }; | ||
| 99 | |||
| 100 | export type LoadedSourceCatalogToolContract = { | ||
| 101 | callSignature: string; | ||
| 102 | callDeclaration: string; | ||
| 103 | callShapeId: string; | ||
| 104 | resultShapeId: string | null; | ||
| 105 | responseSetId: string; | ||
| 106 | input: LoadedSourceCatalogToolContractSide; | ||
| 107 | output: LoadedSourceCatalogToolContractSide; | ||
| 108 | }; | ||
| 109 | |||
| 88 | export const catalogToolCatalogEntry = (input: { | 110 | export const catalogToolCatalogEntry = (input: { |
| 89 | tool: LoadedSourceCatalogToolIndexEntry; | 111 | tool: LoadedSourceCatalogToolIndexEntry; |
| 90 | score: (queryTokens: readonly string[]) => number; | 112 | score: (queryTokens: readonly string[]) => number; |
| @@ -102,6 +124,14 @@ const catalogNamespaceFromPath = (path: string): string => { · 11 unmodified lines | |||
| 102 | 124 | ||
| 103 | const descriptorPath = (descriptor: CatalogToolDescriptor): string => descriptor.path; | 125 | const descriptorPath = (descriptor: CatalogToolDescriptor): string => descriptor.path; |
| 104 | 126 | ||
| 127 | const optionalJsonString = (value: unknown): string | null => { | ||
| 128 | if (value === undefined || value === null) { | ||
| 129 | return null; | ||
| 130 | } | ||
| 131 | |||
| 132 | return JSON.stringify(value, null, 2); | ||
| 133 | }; | ||
| 134 | |||
| 105 | const projectedToolPath = (projected: ProjectedCatalog, capability: Capability): string => | 135 | const projectedToolPath = (projected: ProjectedCatalog, capability: Capability): string => |
| 106 | projected.toolDescriptors[capability.id]?.toolPath.join(".") ?? ""; | 136 | projected.toolDescriptors[capability.id]?.toolPath.join(".") ?? ""; |
| 107 | 137 | ||
| @@ -535,10 +565,12 @@ const codemodeDescriptorFromCapability = (input: { · 427 unmodified lines | |||
| 535 | sourceKey: input.source.id, | 565 | sourceKey: input.source.id, |
| 536 | description: input.capability.surface.summary ?? input.capability.surface.description, | 566 | description: input.capability.surface.summary ?? input.capability.surface.description, |
| 537 | interaction, | 567 | interaction, |
| 538 | ...(inputSchema !== undefined ? { inputSchema } : {}), | 568 | contract: { |
| 539 | ...(outputSchema !== undefined ? { outputSchema } : {}), | 569 | inputTypePreview, |
| 540 | inputTypePreview, | 570 | ...(outputTypePreview !== undefined ? { outputTypePreview } : {}), |
| 541 | ...(outputTypePreview !== undefined ? { outputTypePreview } : {}), | 571 | ...(inputSchema !== undefined ? { inputSchema } : {}), |
| 572 | ...(outputSchema !== undefined ? { outputSchema } : {}), | ||
| 573 | }, | ||
| 542 | providerKind: input.executable.adapterKey, | 574 | providerKind: input.executable.adapterKey, |
| 543 | providerData: { | 575 | providerData: { |
| 544 | capabilityId: input.capability.id, | 576 | capabilityId: input.capability.id, |
| @@ -576,6 +608,8 @@ const loadedCatalogToolFromCapability = (input: { · 31 unmodified lines | |||
| 576 | input.capability.surface.title, | 608 | input.capability.surface.title, |
| 577 | input.capability.surface.summary, | 609 | input.capability.surface.summary, |
| 578 | input.capability.surface.description, | 610 | input.capability.surface.description, |
| 611 | descriptor.contract?.inputTypePreview, | ||
| 612 | descriptor.contract?.outputTypePreview, | ||
| 579 | ...(searchDoc?.tags ?? []), | 613 | ...(searchDoc?.tags ?? []), |
| 580 | ...(searchDoc?.protocolHints ?? []), | 614 | ...(searchDoc?.protocolHints ?? []), |
| 581 | ...(searchDoc?.authHints ?? []), | 615 | ...(searchDoc?.authHints ?? []), |
| @@ -853,6 +887,199 @@ export const expandCatalogTools = (input: { · 271 unmodified lines | |||
| 853 | ), | 887 | ), |
| 854 | ); | 888 | ); |
| 855 | 889 | ||
| 890 | const declarationBlockForShape = (input: { | ||
| 891 | catalog: CatalogV1; | ||
| 892 | shapeId: ShapeSymbolId; | ||
| 893 | aliasHint: string; | ||
| 894 | }): Effect.Effect<string, Error, never> => | ||
| 895 | Effect.tryPromise({ | ||
| 896 | try: async () => { | ||
| 897 | const projector = createCatalogTypeProjector({ | ||
| 898 | catalog: input.catalog, | ||
| 899 | roots: [{ | ||
| 900 | shapeId: input.shapeId, | ||
| 901 | aliasHint: input.aliasHint, | ||
| 902 | }], | ||
| 903 | }); | ||
| 904 | const rootType = projector.renderDeclarationShape(input.shapeId, { | ||
| 905 | aliasHint: input.aliasHint, | ||
| 906 | }); | ||
| 907 | const supportingDeclarations = projector.supportingDeclarations(); | ||
| 908 | const rootDeclarationPrefix = `type ${input.aliasHint} =`; | ||
| 909 | const declarationText = supportingDeclarations.some((declaration) => | ||
| 910 | declaration.includes(rootDeclarationPrefix) | ||
| 911 | ) | ||
| 912 | ? supportingDeclarations.join("\n\n") | ||
| 913 | : [ | ||
| 914 | ...supportingDeclarations, | ||
| 915 | typeAliasDeclaration({ | ||
| 916 | catalog: input.catalog, | ||
| 917 | shapeId: input.shapeId, | ||
| 918 | aliasHint: input.aliasHint, | ||
| 919 | body: rootType, | ||
| 920 | }), | ||
| 921 | ].join("\n\n"); | ||
| 922 | |||
| 923 | return formatWithPrettier(declarationText, "typescript-module"); | ||
| 924 | }, | ||
| 925 | catch: (cause) => | ||
| 926 | cause instanceof Error ? cause : new Error(String(cause)), | ||
| 927 | }); | ||
| 928 | |||
| 929 | const formattedOptionalTypeExpression = ( | ||
| 930 | value: string | undefined, | ||
| 931 | ): Effect.Effect<string | null, Error, never> => | ||
| 932 | value === undefined | ||
| 933 | ? Effect.succeed(null) | ||
| 934 | : Effect.tryPromise({ | ||
| 935 | try: () => formatWithPrettier(value, "typescript"), | ||
| 936 | catch: (cause) => | ||
| 937 | cause instanceof Error ? cause : new Error(String(cause)), | ||
| 938 | }); | ||
| 939 | |||
| 940 | const formattedOptionalJson = ( | ||
| 941 | value: unknown, | ||
| 942 | ): Effect.Effect<string | null, Error, never> => { | ||
| 943 | const serialized = optionalJsonString(value); | ||
| 944 | return serialized === null | ||
| 945 | ? Effect.succeed(null) | ||
| 946 | : Effect.tryPromise({ | ||
| 947 | try: () => formatWithPrettier(serialized, "json"), | ||
| 948 | catch: (cause) => | ||
| 949 | cause instanceof Error ? cause : new Error(String(cause)), | ||
| 950 | }); | ||
| 951 | }; | ||
| 952 | |||
| 953 | const lowerCamelCase = (value: string): string => | ||
| 954 | value.length === 0 ? "tool" : `${value.slice(0, 1).toLowerCase()}${value.slice(1)}`; | ||
| 955 | |||
| 956 | const typeAliasDeclaration = (input: { | ||
| 957 | catalog: CatalogV1; | ||
| 958 | shapeId: ShapeSymbolId; | ||
| 959 | aliasHint: string; | ||
| 960 | body: string; | ||
| 961 | }): string => { | ||
| 962 | const shape = input.catalog.symbols[input.shapeId]; | ||
| 963 | const comment = shape?.kind === "shape" | ||
| 964 | ? documentationComment({ | ||
| 965 | title: shape.title, | ||
| 966 | docs: shape.docs, | ||
| 967 | deprecated: shape.deprecated, | ||
| 968 | includeTitle: true, | ||
| 969 | }) | ||
| 970 | : null; | ||
| 971 | const declaration = `type ${input.aliasHint} = ${input.body};`; | ||
| 972 | return comment ? `${comment}\n${declaration}` : declaration; | ||
| 973 | }; | ||
| 974 | |||
| 975 | export const buildLoadedSourceCatalogToolContract = ( | ||
| 976 | tool: LoadedSourceCatalogTool, | ||
| 977 | ): Effect.Effect<LoadedSourceCatalogToolContract, Error, never> => { | ||
| 978 | const inputAlias = joinTypeNameSegments(...tool.projectedDescriptor.toolPath, "call"); | ||
| 979 | const outputAlias = joinTypeNameSegments(...tool.projectedDescriptor.toolPath, "result"); | ||
| 980 | const inputShapeId = tool.projectedDescriptor.callShapeId; | ||
| 981 | const outputShapeId = tool.projectedDescriptor.resultShapeId ?? null; | ||
| 982 | const argsOptional = shapeAllowsOmittedArgs(tool.projectedCatalog, inputShapeId); | ||
| 983 | const outputTypeName = outputShapeId ? outputAlias : "unknown"; | ||
| 984 | const callFunctionName = lowerCamelCase( | ||
| 985 | joinTypeNameSegments(...tool.projectedDescriptor.toolPath), | ||
| 986 | ); | ||
| 987 | const callComment = documentationComment({ | ||
| 988 | title: tool.capability.surface.title, | ||
| 989 | docs: { | ||
| 990 | ...(tool.capability.surface.summary | ||
| 991 | ? { summary: tool.capability.surface.summary } | ||
| 992 | : {}), | ||
| 993 | ...(tool.capability.surface.description | ||
| 994 | ? { description: tool.capability.surface.description } | ||
| 995 | : {}), | ||
| 996 | }, | ||
| 997 | includeTitle: true, | ||
| 998 | }); | ||
| 999 | |||
| 1000 | return Effect.gen(function* () { | ||
| 1001 | const [ | ||
| 1002 | inputTypePreview, | ||
| 1003 | outputTypePreview, | ||
| 1004 | inputTypeDeclaration, | ||
| 1005 | outputTypeDeclaration, | ||
| 1006 | inputSchemaJson, | ||
| 1007 | outputSchemaJson, | ||
| 1008 | callSignature, | ||
| 1009 | callDeclaration, | ||
| 1010 | ] = | ||
| 1011 | yield* Effect.all([ | ||
| 1012 | formattedOptionalTypeExpression(tool.descriptor.contract?.inputTypePreview), | ||
| 1013 | formattedOptionalTypeExpression(tool.descriptor.contract?.outputTypePreview), | ||
| 1014 | declarationBlockForShape({ | ||
| 1015 | catalog: tool.projectedCatalog, | ||
| 1016 | shapeId: inputShapeId, | ||
| 1017 | aliasHint: inputAlias, | ||
| 1018 | }), | ||
| 1019 | outputShapeId | ||
| 1020 | ? declarationBlockForShape({ | ||
| 1021 | catalog: tool.projectedCatalog, | ||
| 1022 | shapeId: outputShapeId, | ||
| 1023 | aliasHint: outputAlias, | ||
| 1024 | }) | ||
| 1025 | : Effect.succeed<string | null>(null), | ||
| 1026 | formattedOptionalJson( | ||
| 1027 | tool.descriptor.contract?.inputSchema | ||
| 1028 | ?? shapeToJsonSchema(tool.projectedCatalog, inputShapeId), | ||
| 1029 | ), | ||
| 1030 | outputShapeId | ||
| 1031 | ? formattedOptionalJson( | ||
| 1032 | tool.descriptor.contract?.outputSchema | ||
| 1033 | ?? shapeToJsonSchema(tool.projectedCatalog, outputShapeId), | ||
| 1034 | ) | ||
| 1035 | : Effect.succeed<string | null>(null), | ||
| 1036 | Effect.tryPromise({ | ||
| 1037 | try: () => | ||
| 1038 | formatWithPrettier( | ||
| 1039 | `(${argsOptional ? "args?" : "args"}: ${inputAlias}) => Promise<${outputTypeName}>`, | ||
| 1040 | "typescript", | ||
| 1041 | ), | ||
| 1042 | catch: (cause) => | ||
| 1043 | cause instanceof Error ? cause : new Error(String(cause)), | ||
| 1044 | }), | ||
| 1045 | Effect.tryPromise({ | ||
| 1046 | try: () => | ||
| 1047 | formatWithPrettier( | ||
| 1048 | [ | ||
| 1049 | ...(callComment ? [callComment] : []), | ||
| 1050 | `declare function ${callFunctionName}(${argsOptional ? "args?" : "args"}: ${inputAlias}): Promise<${outputTypeName}>;`, | ||
| 1051 | ].join("\n"), | ||
| 1052 | "typescript-module", | ||
| 1053 | ), | ||
| 1054 | catch: (cause) => | ||
| 1055 | cause instanceof Error ? cause : new Error(String(cause)), | ||
| 1056 | }), | ||
| 1057 | ]); | ||
| 1058 | |||
| 1059 | return { | ||
| 1060 | callSignature, | ||
| 1061 | callDeclaration, | ||
| 1062 | callShapeId: inputShapeId, | ||
| 1063 | resultShapeId: outputShapeId, | ||
| 1064 | responseSetId: tool.projectedDescriptor.responseSetId, | ||
| 1065 | input: { | ||
| 1066 | shapeId: inputShapeId, | ||
| 1067 | typePreview: inputTypePreview, | ||
| 1068 | typeDeclaration: inputTypeDeclaration, | ||
| 1069 | schemaJson: inputSchemaJson, | ||
| 1070 | exampleJson: null, | ||
| 1071 | }, | ||
| 1072 | output: { | ||
| 1073 | shapeId: outputShapeId, | ||
| 1074 | typePreview: outputTypePreview, | ||
| 1075 | typeDeclaration: outputTypeDeclaration, | ||
| 1076 | schemaJson: outputSchemaJson, | ||
| 1077 | exampleJson: null, | ||
| 1078 | }, | ||
| 1079 | } satisfies LoadedSourceCatalogToolContract; | ||
| 1080 | }); | ||
| 1081 | }; | ||
| 1082 | |||
| 856 | export const expandCatalogToolByPath = (input: { | 1083 | export const expandCatalogToolByPath = (input: { |
| 857 | catalogs: readonly LoadedSourceCatalog[]; | 1084 | catalogs: readonly LoadedSourceCatalog[]; |
| 858 | path: string; | 1085 | path: string; |
| @@ -186,6 +186,13 @@ const prepareExecutorAddSourceInput = (input: { · 185 unmodified lines | |||
| 186 | endpoint: input.args.endpoint, | 186 | endpoint: input.args.endpoint, |
| 187 | name: input.args.name ?? null, | 187 | name: input.args.name ?? null, |
| 188 | namespace: input.args.namespace ?? null, | 188 | namespace: input.args.namespace ?? null, |
| 189 | transport: input.args.transport ?? null, | ||
| 190 | queryParams: input.args.queryParams ?? null, | ||
| 191 | headers: input.args.headers ?? null, | ||
| 192 | command: input.args.command ?? null, | ||
| 193 | args: input.args.args ?? null, | ||
| 194 | env: input.args.env ?? null, | ||
| 195 | cwd: input.args.cwd ?? null, | ||
| 189 | workspaceId: input.workspaceId, | 196 | workspaceId: input.workspaceId, |
| 190 | actorAccountId: input.accountId, | 197 | actorAccountId: input.accountId, |
| 191 | executionId: input.executionId, | 198 | executionId: input.executionId, |
| @@ -468,10 +475,12 @@ export const createExecutorToolMap = (input: { · 276 unmodified lines | |||
| 468 | }, | 475 | }, |
| 469 | }, | 476 | }, |
| 470 | metadata: { | 477 | metadata: { |
| 471 | inputTypePreview: EXECUTOR_SOURCES_ADD_INPUT_HINT, | 478 | contract: { |
| 472 | outputTypePreview: EXECUTOR_SOURCES_ADD_OUTPUT_SIGNATURE, | 479 | inputTypePreview: EXECUTOR_SOURCES_ADD_INPUT_HINT, |
| 473 | inputSchema: EXECUTOR_SOURCES_ADD_INPUT_SCHEMA, | 480 | outputTypePreview: EXECUTOR_SOURCES_ADD_OUTPUT_SIGNATURE, |
| 474 | outputSchema: EXECUTOR_SOURCES_ADD_OUTPUT_SCHEMA, | 481 | inputSchema: EXECUTOR_SOURCES_ADD_INPUT_SCHEMA, |
| 482 | outputSchema: EXECUTOR_SOURCES_ADD_OUTPUT_SCHEMA, | ||
| 483 | }, | ||
| 475 | sourceKey: "executor", | 484 | sourceKey: "executor", |
| 476 | interaction: "auto", | 485 | interaction: "auto", |
| 477 | }, | 486 | }, |
| @@ -362,10 +362,10 @@ describe("source adapter fixture matrix", () => { · 361 unmodified lines | |||
| 362 | Object.keys(snapshot.catalog.capabilities).length, | 362 | Object.keys(snapshot.catalog.capabilities).length, |
| 363 | ).toBeGreaterThan(250); | 363 | ).toBeGreaterThan(250); |
| 364 | expect(tool).toBeDefined(); | 364 | expect(tool).toBeDefined(); |
| 365 | expect(tool?.descriptor.inputTypePreview).not.toContain("unknown"); | 365 | expect(tool?.descriptor.contract?.inputTypePreview).not.toContain("unknown"); |
| 366 | expect(tool?.descriptor.outputTypePreview).toContain("data:"); | 366 | expect(tool?.descriptor.contract?.outputTypePreview).toContain("data:"); |
| 367 | expect(tool?.descriptor.outputTypePreview).toContain("status:"); | 367 | expect(tool?.descriptor.contract?.outputTypePreview).toContain("status:"); |
| 368 | expect(tool?.descriptor.inputSchema).toMatchObject({ | 368 | expect(tool?.descriptor.contract?.inputSchema).toMatchObject({ |
| 369 | type: "object", | 369 | type: "object", |
| 370 | properties: { | 370 | properties: { |
| 371 | idOrName: { | 371 | idOrName: { |
| @@ -428,8 +428,8 @@ describe("source adapter fixture matrix", () => { · 56 unmodified lines | |||
| 428 | 428 | ||
| 429 | expect(manifest.tools.length).toBeGreaterThan(50); | 429 | expect(manifest.tools.length).toBeGreaterThan(50); |
| 430 | expect(tool).toBeDefined(); | 430 | expect(tool).toBeDefined(); |
| 431 | expect(tool?.descriptor.inputTypePreview).toContain("key_name"); | 431 | expect(tool?.descriptor.contract?.inputTypePreview).toContain("key_name"); |
| 432 | expect(tool?.descriptor.inputSchema).toMatchObject({ | 432 | expect(tool?.descriptor.contract?.inputSchema).toMatchObject({ |
| 433 | type: "object", | 433 | type: "object", |
| 434 | properties: { | 434 | properties: { |
| 435 | body: { | 435 | body: { |
| @@ -444,7 +444,7 @@ describe("source adapter fixture matrix", () => { · 8 unmodified lines | |||
| 444 | }, | 444 | }, |
| 445 | required: ["body"], | 445 | required: ["body"], |
| 446 | }); | 446 | }); |
| 447 | expect(tool?.descriptor.outputSchema).toMatchObject({ | 447 | expect(tool?.descriptor.contract?.outputSchema).toMatchObject({ |
| 448 | type: "object", | 448 | type: "object", |
| 449 | properties: { | 449 | properties: { |
| 450 | data: { | 450 | data: { |
| @@ -452,7 +452,7 @@ describe("source adapter fixture matrix", () => { · 1 unmodified line | |||
| 452 | }, | 452 | }, |
| 453 | }, | 453 | }, |
| 454 | }); | 454 | }); |
| 455 | const outputSchema = tool?.descriptor.outputSchema; | 455 | const outputSchema = tool?.descriptor.contract?.outputSchema; |
| 456 | const dataVariants = | 456 | const dataVariants = |
| 457 | outputSchema && typeof outputSchema === "object" | 457 | outputSchema && typeof outputSchema === "object" |
| 458 | ? ( | 458 | ? ( |
| @@ -979,10 +979,10 @@ describe("source adapter fixture matrix", () => { · 520 unmodified lines | |||
| 979 | Object.keys(snapshot.catalog.capabilities).length, | 979 | Object.keys(snapshot.catalog.capabilities).length, |
| 980 | ).toBeGreaterThan(10); | 980 | ).toBeGreaterThan(10); |
| 981 | expect(tool).toBeDefined(); | 981 | expect(tool).toBeDefined(); |
| 982 | expect(tool?.descriptor.inputTypePreview).not.toContain("unknown"); | 982 | expect(tool?.descriptor.contract?.inputTypePreview).not.toContain("unknown"); |
| 983 | expect(tool?.descriptor.outputTypePreview).toContain("data:"); | 983 | expect(tool?.descriptor.contract?.outputTypePreview).toContain("data:"); |
| 984 | expect(tool?.descriptor.outputTypePreview).toContain("status:"); | 984 | expect(tool?.descriptor.contract?.outputTypePreview).toContain("status:"); |
| 985 | expect(tool?.descriptor.inputSchema).toMatchObject({ | 985 | expect(tool?.descriptor.contract?.inputSchema).toMatchObject({ |
| 986 | type: "object", | 986 | type: "object", |
| 987 | properties: { | 987 | properties: { |
| 988 | spreadsheetId: { | 988 | spreadsheetId: { |
| @@ -1001,7 +1001,7 @@ describe("source adapter fixture matrix", () => { · 12 unmodified lines | |||
| 1001 | }, | 1001 | }, |
| 1002 | }, | 1002 | }, |
| 1003 | }); | 1003 | }); |
| 1004 | expect(JSON.stringify(tool?.descriptor.outputSchema)).toContain( | 1004 | expect(JSON.stringify(tool?.descriptor.contract?.outputSchema)).toContain( |
| 1005 | '"gridProperties"', | 1005 | '"gridProperties"', |
| 1006 | ); | 1006 | ); |
| 1007 | expect( | 1007 | expect( |
| @@ -1111,11 +1111,11 @@ describe("source adapter fixture matrix", () => { · 103 unmodified lines | |||
| 1111 | ), | 1111 | ), |
| 1112 | ).toEqual([]); | 1112 | ).toEqual([]); |
| 1113 | expect(tool).toBeDefined(); | 1113 | expect(tool).toBeDefined(); |
| 1114 | expect(tool?.descriptor.inputTypePreview).toContain("{"); | 1114 | expect(tool?.descriptor.contract?.inputTypePreview).toContain("{"); |
| 1115 | expect(tool?.descriptor.inputTypePreview).toContain("input: {"); | 1115 | expect(tool?.descriptor.contract?.inputTypePreview).toContain("input: {"); |
| 1116 | expect(tool?.descriptor.outputTypePreview).toContain("data:"); | 1116 | expect(tool?.descriptor.contract?.outputTypePreview).toContain("data:"); |
| 1117 | expect(tool?.descriptor.outputTypePreview).not.toContain("unknown[]"); | 1117 | expect(tool?.descriptor.contract?.outputTypePreview).not.toContain("unknown[]"); |
| 1118 | expect(tool?.descriptor.inputSchema).toMatchObject({ | 1118 | expect(tool?.descriptor.contract?.inputSchema).toMatchObject({ |
| 1119 | type: "object", | 1119 | type: "object", |
| 1120 | required: ["input"], | 1120 | required: ["input"], |
| 1121 | properties: { | 1121 | properties: { |
| @@ -1303,7 +1303,7 @@ describe("source adapter fixture matrix", () => { · 181 unmodified lines | |||
| 1303 | ); | 1303 | ); |
| 1304 | } | 1304 | } |
| 1305 | 1305 | ||
| 1306 | expect(tool.descriptor.outputSchema).toMatchObject({ | 1306 | expect(tool.descriptor.contract?.outputSchema).toMatchObject({ |
| 1307 | type: "object", | 1307 | type: "object", |
| 1308 | properties: { | 1308 | properties: { |
| 1309 | data: { | 1309 | data: { |
| @@ -1,11 +1,13 @@ | |||
| 1 | import { describe, expect, it } from "@effect/vitest"; | 1 | import { describe, expect, it } from "@effect/vitest"; |
| 2 | import * as Effect from "effect/Effect"; | ||
| 2 | 3 | ||
| 3 | import type { LoadedSourceCatalogTool } from "../catalog/source/runtime"; | 4 | import type { LoadedSourceCatalogTool } from "../catalog/source/runtime"; |
| 4 | import { inspectionToolDetailFromTool } from "./source-inspection"; | 5 | import { inspectionToolDetailFromTool } from "./source-inspection"; |
| 5 | 6 | ||
| 6 | describe("source inspection", () => { | 7 | describe("source inspection", () => { |
| 7 | it("keeps tool detail lightweight by default", () => { | 8 | it.effect("keeps the shared TypeScript contract first-class on tool detail", () => |
| 8 | const detail = inspectionToolDetailFromTool({ | 9 | Effect.gen(function* () { |
| 10 | const detail = yield* inspectionToolDetailFromTool({ | ||
| 9 | path: "linear.administrableTeams", | 11 | path: "linear.administrableTeams", |
| 10 | source: { | 12 | source: { |
| 11 | id: "linear", | 13 | id: "linear", |
| @@ -30,7 +32,9 @@ describe("source inspection", () => { · 18 unmodified lines | |||
| 30 | }, | 32 | }, |
| 31 | projection: { | 33 | projection: { |
| 32 | callShapeId: "shape_call", | 34 | callShapeId: "shape_call", |
| 33 | resultShapeId: "shape_result", | 35 | resultDataShapeId: "shape_result_data", |
| 36 | resultErrorShapeId: "shape_result_error", | ||
| 37 | responseSetId: "response_set_1", | ||
| 34 | }, | 38 | }, |
| 35 | display: { | 39 | display: { |
| 36 | protocol: "graphql", | 40 | protocol: "graphql", |
| @@ -47,26 +51,220 @@ describe("source inspection", () => { · 10 unmodified lines | |||
| 47 | toolPath: ["linear", "administrableTeams"], | 51 | toolPath: ["linear", "administrableTeams"], |
| 48 | callShapeId: "shape_call", | 52 | callShapeId: "shape_call", |
| 49 | resultShapeId: "shape_result", | 53 | resultShapeId: "shape_result", |
| 54 | responseSetId: "response_set_1", | ||
| 50 | }, | 55 | }, |
| 51 | descriptor: {}, | 56 | descriptor: { |
| 57 | inputTypePreview: "{ input: string }", | ||
| 58 | outputTypePreview: "{ data: string | null; error: unknown | null; headers: Record<string, string>; status: number | null }", | ||
| 59 | inputSchema: { | ||
| 60 | type: "object", | ||
| 61 | properties: { | ||
| 62 | input: { type: "string" }, | ||
| 63 | }, | ||
| 64 | required: ["input"], | ||
| 65 | }, | ||
| 66 | outputSchema: { | ||
| 67 | type: "object", | ||
| 68 | properties: { | ||
| 69 | data: { type: ["string", "null"] }, | ||
| 70 | }, | ||
| 71 | required: ["data"], | ||
| 72 | }, | ||
| 73 | }, | ||
| 74 | projectedCatalog: { | ||
| 75 | version: "ir.v1", | ||
| 76 | documents: {}, | ||
| 77 | resources: {}, | ||
| 78 | scopes: {}, | ||
| 79 | capabilities: {}, | ||
| 80 | executables: {}, | ||
| 81 | responseSets: {}, | ||
| 82 | diagnostics: {}, | ||
| 83 | symbols: { | ||
| 84 | shape_call: { | ||
| 85 | id: "shape_call", | ||
| 86 | kind: "shape", | ||
| 87 | resourceId: "res_1", | ||
| 88 | title: "AdministrableTeamsCall", | ||
| 89 | node: { | ||
| 90 | type: "object", | ||
| 91 | fields: { | ||
| 92 | input: { | ||
| 93 | shapeId: "shape_call_input", | ||
| 94 | docs: { | ||
| 95 | description: "The exact input string to filter by.", | ||
| 96 | }, | ||
| 97 | }, | ||
| 98 | }, | ||
| 99 | required: ["input"], | ||
| 100 | additionalProperties: false, | ||
| 101 | }, | ||
| 102 | synthetic: false, | ||
| 103 | provenance: [], | ||
| 104 | }, | ||
| 105 | shape_call_input: { | ||
| 106 | id: "shape_call_input", | ||
| 107 | kind: "shape", | ||
| 108 | resourceId: "res_1", | ||
| 109 | title: "Input", | ||
| 110 | node: { | ||
| 111 | type: "scalar", | ||
| 112 | scalar: "string", | ||
| 113 | }, | ||
| 114 | synthetic: false, | ||
| 115 | provenance: [], | ||
| 116 | }, | ||
| 117 | shape_result: { | ||
| 118 | id: "shape_result", | ||
| 119 | kind: "shape", | ||
| 120 | resourceId: "res_1", | ||
| 121 | title: "AdministrableTeamsResult", | ||
| 122 | node: { | ||
| 123 | type: "object", | ||
| 124 | fields: { | ||
| 125 | data: { | ||
| 126 | shapeId: "shape_result_data_nullable", | ||
| 127 | docs: { | ||
| 128 | description: "Matching team data when present.", | ||
| 129 | }, | ||
| 130 | }, | ||
| 131 | error: { | ||
| 132 | shapeId: "shape_result_error_nullable", | ||
| 133 | }, | ||
| 134 | headers: { | ||
| 135 | shapeId: "shape_result_headers", | ||
| 136 | }, | ||
| 137 | status: { | ||
| 138 | shapeId: "shape_result_status_nullable", | ||
| 139 | }, | ||
| 140 | }, | ||
| 141 | required: ["data", "error", "headers", "status"], | ||
| 142 | additionalProperties: false, | ||
| 143 | }, | ||
| 144 | synthetic: true, | ||
| 145 | provenance: [], | ||
| 146 | }, | ||
| 147 | shape_result_data_nullable: { | ||
| 148 | id: "shape_result_data_nullable", | ||
| 149 | kind: "shape", | ||
| 150 | resourceId: "res_1", | ||
| 151 | node: { | ||
| 152 | type: "nullable", | ||
| 153 | itemShapeId: "shape_result_data", | ||
| 154 | }, | ||
| 155 | synthetic: true, | ||
| 156 | provenance: [], | ||
| 157 | }, | ||
| 158 | shape_result_data: { | ||
| 159 | id: "shape_result_data", | ||
| 160 | kind: "shape", | ||
| 161 | resourceId: "res_1", | ||
| 162 | node: { | ||
| 163 | type: "scalar", | ||
| 164 | scalar: "string", | ||
| 165 | }, | ||
| 166 | synthetic: false, | ||
| 167 | provenance: [], | ||
| 168 | }, | ||
| 169 | shape_result_error_nullable: { | ||
| 170 | id: "shape_result_error_nullable", | ||
| 171 | kind: "shape", | ||
| 172 | resourceId: "res_1", | ||
| 173 | node: { | ||
| 174 | type: "nullable", | ||
| 175 | itemShapeId: "shape_result_error", | ||
| 176 | }, | ||
| 177 | synthetic: true, | ||
| 178 | provenance: [], | ||
| 179 | }, | ||
| 180 | shape_result_error: { | ||
| 181 | id: "shape_result_error", | ||
| 182 | kind: "shape", | ||
| 183 | resourceId: "res_1", | ||
| 184 | node: { | ||
| 185 | type: "unknown", | ||
| 186 | }, | ||
| 187 | synthetic: false, | ||
| 188 | provenance: [], | ||
| 189 | }, | ||
| 190 | shape_result_headers: { | ||
| 191 | id: "shape_result_headers", | ||
| 192 | kind: "shape", | ||
| 193 | resourceId: "res_1", | ||
| 194 | node: { | ||
| 195 | type: "object", | ||
| 196 | fields: {}, | ||
| 197 | additionalProperties: "shape_result_header_value", | ||
| 198 | }, | ||
| 199 | synthetic: true, | ||
| 200 | provenance: [], | ||
| 201 | }, | ||
| 202 | shape_result_header_value: { | ||
| 203 | id: "shape_result_header_value", | ||
| 204 | kind: "shape", | ||
| 205 | resourceId: "res_1", | ||
| 206 | node: { | ||
| 207 | type: "scalar", | ||
| 208 | scalar: "string", | ||
| 209 | }, | ||
| 210 | synthetic: true, | ||
| 211 | provenance: [], | ||
| 212 | }, | ||
| 213 | shape_result_status_nullable: { | ||
| 214 | id: "shape_result_status_nullable", | ||
| 215 | kind: "shape", | ||
| 216 | resourceId: "res_1", | ||
| 217 | node: { | ||
| 218 | type: "nullable", | ||
| 219 | itemShapeId: "shape_result_status", | ||
| 220 | }, | ||
| 221 | synthetic: true, | ||
| 222 | provenance: [], | ||
| 223 | }, | ||
| 224 | shape_result_status: { | ||
| 225 | id: "shape_result_status", | ||
| 226 | kind: "shape", | ||
| 227 | resourceId: "res_1", | ||
| 228 | node: { | ||
| 229 | type: "scalar", | ||
| 230 | scalar: "integer", | ||
| 231 | }, | ||
| 232 | synthetic: true, | ||
| 233 | provenance: [], | ||
| 234 | }, | ||
| 235 | }, | ||
| 236 | }, | ||
| 237 | typeProjector: {} as LoadedSourceCatalogTool["typeProjector"], | ||
| 52 | } as LoadedSourceCatalogTool); | 238 | } as LoadedSourceCatalogTool); |
| 53 | 239 | ||
| 54 | expect(detail.summary.path).toBe("linear.administrableTeams"); | 240 | expect(detail.summary.path).toBe("linear.administrableTeams"); |
| 55 | expect(detail.summary.method).toBe("query"); | 241 | expect(detail.summary.method).toBe("query"); |
| 242 | |||
| 243 | expect(detail.contract.callSignature).toContain("Promise<"); | ||
| 244 | expect(detail.contract.callDeclaration).toContain("/**"); | ||
| 245 | expect(detail.contract.callDeclaration).toContain("Administrable Teams"); | ||
| 246 | expect(detail.contract.callDeclaration).toContain("declare function linearAdministrableTeams"); | ||
| 247 | expect(detail.contract.callShapeId).toBe("shape_call"); | ||
| 248 | expect(detail.contract.resultShapeId).toBe("shape_result"); | ||
| 249 | expect(detail.contract.input.typeDeclaration).toContain("type LinearAdministrableTeamsCall"); | ||
| 250 | expect(detail.contract.output.typeDeclaration).toContain("type LinearAdministrableTeamsResult"); | ||
| 251 | expect(detail.contract.input.typeDeclaration).toContain("The exact input string to filter by."); | ||
| 252 | expect(detail.contract.output.typeDeclaration).toContain("Matching team data when present."); | ||
| 253 | expect(detail.contract.input.schemaJson).toContain("\"input\""); | ||
| 254 | expect(detail.contract.output.schemaJson).toContain("\"data\""); | ||
| 56 | 255 | ||
| 57 | const sectionTitles = detail.sections.map((section) => section.title); | 256 | const sectionTitles = detail.sections.map((section) => section.title); |
| 58 | expect(sectionTitles).not.toContain("Input Type"); | 257 | expect(sectionTitles).not.toContain("Input Schema"); |
| 59 | expect(sectionTitles).not.toContain("Output Type"); | 258 | expect(sectionTitles).not.toContain("Output Schema"); |
| 60 | expect(sectionTitles).not.toContain("Input Schema"); | ||
| 61 | expect(sectionTitles).not.toContain("Output Schema"); | ||
| 62 | 259 | ||
| 63 | const overview = detail.sections.find((section) => section.kind === "facts"); | 260 | const overview = detail.sections.find((section) => section.kind === "facts"); |
| 64 | expect(overview).toBeDefined(); | 261 | expect(overview).toBeDefined(); |
| 65 | expect(overview?.items).toEqual( | 262 | expect(overview?.items).toEqual( |
| 66 | expect.arrayContaining([ | 263 | expect.arrayContaining([ |
| 67 | expect.objectContaining({ label: "Call shape", value: "shape_call" }), | 264 | expect.objectContaining({ label: "Signature" }), |
| 68 | expect.objectContaining({ label: "Result shape", value: "shape_result" }), | 265 | expect.objectContaining({ label: "Call shape", value: "shape_call" }), |
| 69 | ]), | 266 | expect.objectContaining({ label: "Result shape", value: "shape_result" }), |
| 70 | ); | 267 | ]), |
| 71 | }); | 268 | ); |
| 269 | })); | ||
| 72 | }); | 270 | }); |
| @@ -19,6 +19,7 @@ import * as Effect from "effect/Effect"; · 18 unmodified lines | |||
| 19 | import { LocalSourceArtifactMissingError } from "../local/errors"; | 19 | import { LocalSourceArtifactMissingError } from "../local/errors"; |
| 20 | import { operationErrors } from "../policy/operation-errors"; | 20 | import { operationErrors } from "../policy/operation-errors"; |
| 21 | import { | 21 | import { |
| 22 | buildLoadedSourceCatalogToolContract, | ||
| 22 | expandCatalogToolByPath, | 23 | expandCatalogToolByPath, |
| 23 | expandCatalogTools, | 24 | expandCatalogTools, |
| 24 | loadSourceWithCatalog, | 25 | loadSourceWithCatalog, |
| @@ -107,6 +108,12 @@ const executableDetails = (tool: LoadedSourceCatalogTool) => { · 82 unmodified lines | |||
| 107 | const inspectionToolListItemFromTool = (tool: LoadedSourceCatalogTool): SourceInspectionToolListItem => ({ | 108 | const inspectionToolListItemFromTool = (tool: LoadedSourceCatalogTool): SourceInspectionToolListItem => ({ |
| 108 | path: tool.path, | 109 | path: tool.path, |
| 109 | method: executableDetails(tool).method, | 110 | method: executableDetails(tool).method, |
| 111 | ...(tool.descriptor.contract?.inputTypePreview | ||
| 112 | ? { inputTypePreview: tool.descriptor.contract.inputTypePreview } | ||
| 113 | : {}), | ||
| 114 | ...(tool.descriptor.contract?.outputTypePreview | ||
| 115 | ? { outputTypePreview: tool.descriptor.contract.outputTypePreview } | ||
| 116 | : {}), | ||
| 110 | }); | 117 | }); |
| 111 | 118 | ||
| 112 | const persistedToolSummaryFromTool = (tool: LoadedSourceCatalogTool): SourceInspectionToolSummary => { | 119 | const persistedToolSummaryFromTool = (tool: LoadedSourceCatalogTool): SourceInspectionToolSummary => { |
| @@ -128,11 +135,11 @@ const persistedToolSummaryFromTool = (tool: LoadedSourceCatalogTool): SourceInsp · 15 unmodified lines | |||
| 128 | tags: [...details.tags], | 135 | tags: [...details.tags], |
| 129 | method: details.method, | 136 | method: details.method, |
| 130 | pathTemplate: details.pathTemplate, | 137 | pathTemplate: details.pathTemplate, |
| 131 | ...(tool.descriptor.inputTypePreview | 138 | ...(tool.descriptor.contract?.inputTypePreview |
| 132 | ? { inputTypePreview: tool.descriptor.inputTypePreview } | 139 | ? { inputTypePreview: tool.descriptor.contract.inputTypePreview } |
| 133 | : {}), | 140 | : {}), |
| 134 | ...(tool.descriptor.outputTypePreview | 141 | ...(tool.descriptor.contract?.outputTypePreview |
| 135 | ? { outputTypePreview: tool.descriptor.outputTypePreview } | 142 | ? { outputTypePreview: tool.descriptor.contract.outputTypePreview } |
| 136 | : {}), | 143 | : {}), |
| 137 | }; | 144 | }; |
| 138 | }; | 145 | }; |
| @@ -157,85 +164,89 @@ const jsonSection = ( · 18 unmodified lines | |||
| 157 | 164 | ||
| 158 | export const inspectionToolDetailFromTool = ( | 165 | export const inspectionToolDetailFromTool = ( |
| 159 | tool: LoadedSourceCatalogTool, | 166 | tool: LoadedSourceCatalogTool, |
| 160 | ): SourceInspectionToolDetail => { | 167 | ): Effect.Effect<SourceInspectionToolDetail, Error, never> => |
| 161 | const summary = persistedToolSummaryFromTool(tool); | 168 | Effect.gen(function* () { |
| 162 | const details = executableDetails(tool); | 169 | const summary = persistedToolSummaryFromTool(tool); |
| 163 | const overviewItems = [ | 170 | const details = executableDetails(tool); |
| 164 | { label: "Protocol", value: details.protocol }, | 171 | const contract = yield* buildLoadedSourceCatalogToolContract(tool); |
| 165 | ...(details.method ? [{ label: "Method", value: details.method, mono: true }] : []), | 172 | const overviewItems = [ |
| 166 | ...(details.pathTemplate ? [{ label: "Target", value: details.pathTemplate, mono: true }] : []), | 173 | { label: "Protocol", value: details.protocol }, |
| 167 | ...(details.operationId ? [{ label: "Operation", value: details.operationId, mono: true }] : []), | 174 | ...(details.method ? [{ label: "Method", value: details.method, mono: true }] : []), |
| 168 | ...(details.group ? [{ label: "Group", value: details.group, mono: true }] : []), | 175 | ...(details.pathTemplate ? [{ label: "Target", value: details.pathTemplate, mono: true }] : []), |
| 169 | ...(details.leaf ? [{ label: "Leaf", value: details.leaf, mono: true }] : []), | 176 | ...(details.operationId ? [{ label: "Operation", value: details.operationId, mono: true }] : []), |
| 170 | ...(details.rawToolId ? [{ label: "Raw tool", value: details.rawToolId, mono: true }] : []), | 177 | ...(details.group ? [{ label: "Group", value: details.group, mono: true }] : []), |
| 171 | { label: "Call shape", value: tool.projectedDescriptor.callShapeId, mono: true }, | 178 | ...(details.leaf ? [{ label: "Leaf", value: details.leaf, mono: true }] : []), |
| 172 | ...(tool.projectedDescriptor.resultShapeId | 179 | ...(details.rawToolId ? [{ label: "Raw tool", value: details.rawToolId, mono: true }] : []), |
| 173 | ? [{ label: "Result shape", value: tool.projectedDescriptor.resultShapeId, mono: true }] | 180 | { label: "Signature", value: contract.callSignature, mono: true }, |
| 174 | : []), | 181 | { label: "Call shape", value: contract.callShapeId, mono: true }, |
| 175 | ]; | 182 | ...(contract.resultShapeId |
| 176 | const nativeSections = [ | 183 | ? [{ label: "Result shape", value: contract.resultShapeId, mono: true }] |
| 177 | ...(tool.capability.native ?? []).map((blob, index) => ({ | 184 | : []), |
| 178 | kind: "code" as const, | 185 | { label: "Response set", value: contract.responseSetId, mono: true }, |
| 179 | title: `Capability native ${String(index + 1)}: ${blob.kind}`, | 186 | ]; |
| 180 | language: nativeEncodingLanguage(blob.encoding), | 187 | const nativeSections = [ |
| 181 | body: | 188 | ...(tool.capability.native ?? []).map((blob, index) => ({ |
| 182 | typeof blob.value === "string" | 189 | kind: "code" as const, |
| 183 | ? blob.value | 190 | title: `Capability native ${String(index + 1)}: ${blob.kind}`, |
| 184 | : JSON.stringify(blob.value ?? null, null, 2), | 191 | language: nativeEncodingLanguage(blob.encoding), |
| 185 | })), | 192 | body: |
| 186 | ...(tool.executable.native ?? []).map((blob, index) => ({ | 193 | typeof blob.value === "string" |
| 187 | kind: "code" as const, | 194 | ? blob.value |
| 188 | title: `Executable native ${String(index + 1)}: ${blob.kind}`, | 195 | : JSON.stringify(blob.value ?? null, null, 2), |
| 189 | language: nativeEncodingLanguage(blob.encoding), | 196 | })), |
| 190 | body: | 197 | ...(tool.executable.native ?? []).map((blob, index) => ({ |
| 191 | typeof blob.value === "string" | 198 | kind: "code" as const, |
| 192 | ? blob.value | 199 | title: `Executable native ${String(index + 1)}: ${blob.kind}`, |
| 193 | : JSON.stringify(blob.value ?? null, null, 2), | 200 | language: nativeEncodingLanguage(blob.encoding), |
| 194 | })), | 201 | body: |
| 195 | ]; | 202 | typeof blob.value === "string" |
| 196 | const sections = [ | 203 | ? blob.value |
| 197 | { | 204 | : JSON.stringify(blob.value ?? null, null, 2), |
| 198 | kind: "facts" as const, | 205 | })), |
| 199 | title: "Overview", | 206 | ]; |
| 200 | items: overviewItems, | 207 | const sections = [ |
| 201 | }, | 208 | { |
| 202 | ...(summary.description | 209 | kind: "facts" as const, |
| 203 | ? [{ | 210 | title: "Overview", |
| 204 | kind: "markdown" as const, | 211 | items: overviewItems, |
| 205 | title: "Description", | 212 | }, |
| 206 | body: summary.description, | 213 | ...(summary.description |
| 207 | }] | 214 | ? [{ |
| 208 | : []), | 215 | kind: "markdown" as const, |
| 209 | ...([ | 216 | title: "Description", |
| 210 | jsonSection("Input Schema", tool.descriptor.inputSchema), | 217 | body: summary.description, |
| 211 | jsonSection("Output Schema", tool.descriptor.outputSchema), | 218 | }] |
| 212 | jsonSection("Capability", tool.capability), | 219 | : []), |
| 213 | jsonSection("Executable", { | 220 | ...([ |
| 214 | id: tool.executable.id, | 221 | jsonSection("Capability", tool.capability), |
| 215 | adapterKey: tool.executable.adapterKey, | 222 | jsonSection("Executable", { |
| 216 | bindingVersion: tool.executable.bindingVersion, | 223 | id: tool.executable.id, |
| 217 | binding: tool.executable.binding, | 224 | adapterKey: tool.executable.adapterKey, |
| 218 | projection: tool.executable.projection, | 225 | bindingVersion: tool.executable.bindingVersion, |
| 219 | display: tool.executable.display ?? null, | 226 | binding: tool.executable.binding, |
| 220 | }), | 227 | projection: tool.executable.projection, |
| 221 | jsonSection("Documentation", { | 228 | display: tool.executable.display ?? null, |
| 222 | summary: tool.capability.surface.summary, | 229 | }), |
| 223 | description: tool.capability.surface.description, | 230 | jsonSection("Documentation", { |
| 224 | }), | 231 | summary: tool.capability.surface.summary, |
| 225 | ].filter((section) => section !== null) as Array<SourceInspectionToolDetail["sections"][number]>), | 232 | description: tool.capability.surface.description, |
| 226 | ...nativeSections, | 233 | }), |
| 227 | ]; | 234 | ].filter((section) => section !== null) as Array<SourceInspectionToolDetail["sections"][number]>), |
| 228 | 235 | ...nativeSections, | |
| 229 | return ({ | 236 | ]; |
| 230 | summary, | 237 | |
| 231 | sections, | 238 | return { |
| 239 | summary, | ||
| 240 | contract, | ||
| 241 | sections, | ||
| 242 | } satisfies SourceInspectionToolDetail; | ||
| 232 | }); | 243 | }); |
| 233 | }; | ||
| 234 | 244 | ||
| 235 | const resolveSourceInspection = (input: { | 245 | const resolveSourceInspection = (input: { |
| 236 | workspaceId: WorkspaceId; | 246 | workspaceId: WorkspaceId; |
| 237 | sourceId: SourceId; | 247 | sourceId: SourceId; |
| 238 | includeSchemas: boolean; | 248 | includeSchemas: boolean; |
| 249 | includeTypePreviews: boolean; | ||
| 239 | }) => | 250 | }) => |
| 240 | Effect.gen(function* () { | 251 | Effect.gen(function* () { |
| 241 | const loaded = yield* loadSourceCatalogOrEmpty({ | 252 | const loaded = yield* loadSourceCatalogOrEmpty({ |
| @@ -255,7 +266,7 @@ const resolveSourceInspection = (input: { · 13 unmodified lines | |||
| 255 | const tools = yield* expandCatalogTools({ | 266 | const tools = yield* expandCatalogTools({ |
| 256 | catalogs: [loaded.catalogEntry], | 267 | catalogs: [loaded.catalogEntry], |
| 257 | includeSchemas: input.includeSchemas, | 268 | includeSchemas: input.includeSchemas, |
| 258 | includeTypePreviews: input.includeSchemas, | 269 | includeTypePreviews: input.includeTypePreviews, |
| 259 | }); | 270 | }); |
| 260 | 271 | ||
| 261 | return { | 272 | return { |
| @@ -289,8 +300,8 @@ const resolveSourceInspectionTool = (input: { · 27 unmodified lines | |||
| 289 | const tool = yield* expandCatalogToolByPath({ | 300 | const tool = yield* expandCatalogToolByPath({ |
| 290 | catalogs: [loaded.catalogEntry], | 301 | catalogs: [loaded.catalogEntry], |
| 291 | path: input.toolPath, | 302 | path: input.toolPath, |
| 292 | includeSchemas: false, | 303 | includeSchemas: true, |
| 293 | includeTypePreviews: false, | 304 | includeTypePreviews: true, |
| 294 | }); | 305 | }); |
| 295 | 306 | ||
| 296 | return { | 307 | return { |
| @@ -382,6 +393,7 @@ export const getSourceInspection = (input: { · 85 unmodified lines | |||
| 382 | const inspection = yield* resolveSourceInspection({ | 393 | const inspection = yield* resolveSourceInspection({ |
| 383 | ...input, | 394 | ...input, |
| 384 | includeSchemas: false, | 395 | includeSchemas: false, |
| 396 | includeTypePreviews: true, | ||
| 385 | }); | 397 | }); |
| 386 | 398 | ||
| 387 | return { | 399 | return { |
| @@ -420,7 +432,7 @@ export const getSourceInspectionToolDetail = (input: { · 32 unmodified lines | |||
| 420 | ); | 432 | ); |
| 421 | } | 433 | } |
| 422 | 434 | ||
| 423 | return inspectionToolDetailFromTool(tool); | 435 | return yield* inspectionToolDetailFromTool(tool); |
| 424 | }).pipe( | 436 | }).pipe( |
| 425 | Effect.mapError((cause) => | 437 | Effect.mapError((cause) => |
| 426 | mapInspectionError( | 438 | mapInspectionError( |
| @@ -440,6 +452,7 @@ export const discoverSourceInspectionTools = (input: { · 13 unmodified lines | |||
| 440 | workspaceId: input.workspaceId, | 452 | workspaceId: input.workspaceId, |
| 441 | sourceId: input.sourceId, | 453 | sourceId: input.sourceId, |
| 442 | includeSchemas: false, | 454 | includeSchemas: false, |
| 455 | includeTypePreviews: true, | ||
| 443 | }); | 456 | }); |
| 444 | const queryTokens = tokenize(input.payload.query); | 457 | const queryTokens = tokenize(input.payload.query); |
| 445 | const results = inspection.tools | 458 | const results = inspection.tools |
| @@ -56,6 +56,26 @@ export const SourceInspectionSectionSchema = Schema.Union( · 55 unmodified lines | |||
| 56 | export const SourceInspectionToolListItemSchema = Schema.Struct({ | 56 | export const SourceInspectionToolListItemSchema = Schema.Struct({ |
| 57 | path: Schema.String, | 57 | path: Schema.String, |
| 58 | method: Schema.NullOr(Schema.String), | 58 | method: Schema.NullOr(Schema.String), |
| 59 | inputTypePreview: Schema.optional(Schema.String), | ||
| 60 | outputTypePreview: Schema.optional(Schema.String), | ||
| 61 | }); | ||
| 62 | |||
| 63 | export const SourceInspectionToolContractSideSchema = Schema.Struct({ | ||
| 64 | shapeId: Schema.NullOr(Schema.String), | ||
| 65 | typePreview: Schema.NullOr(Schema.String), | ||
| 66 | typeDeclaration: Schema.NullOr(Schema.String), | ||
| 67 | schemaJson: Schema.NullOr(Schema.String), | ||
| 68 | exampleJson: Schema.NullOr(Schema.String), | ||
| 69 | }); | ||
| 70 | |||
| 71 | export const SourceInspectionToolContractSchema = Schema.Struct({ | ||
| 72 | callSignature: Schema.String, | ||
| 73 | callDeclaration: Schema.String, | ||
| 74 | callShapeId: Schema.String, | ||
| 75 | resultShapeId: Schema.NullOr(Schema.String), | ||
| 76 | responseSetId: Schema.String, | ||
| 77 | input: SourceInspectionToolContractSideSchema, | ||
| 78 | output: SourceInspectionToolContractSideSchema, | ||
| 59 | }); | 79 | }); |
| 60 | 80 | ||
| 61 | export const SourceInspectionSchema = Schema.Struct({ | 81 | export const SourceInspectionSchema = Schema.Struct({ |
| @@ -68,6 +88,7 @@ export const SourceInspectionSchema = Schema.Struct({ · 6 unmodified lines | |||
| 68 | 88 | ||
| 69 | export const SourceInspectionToolDetailSchema = Schema.Struct({ | 89 | export const SourceInspectionToolDetailSchema = Schema.Struct({ |
| 70 | summary: SourceInspectionToolSummarySchema, | 90 | summary: SourceInspectionToolSummarySchema, |
| 91 | contract: SourceInspectionToolContractSchema, | ||
| 71 | sections: Schema.Array(SourceInspectionSectionSchema), | 92 | sections: Schema.Array(SourceInspectionSectionSchema), |
| 72 | }); | 93 | }); |
| 73 | 94 | ||
| @@ -109,6 +130,10 @@ export type SourceInspectionSection = · 35 unmodified lines | |||
| 109 | typeof SourceInspectionSectionSchema.Type; | 130 | typeof SourceInspectionSectionSchema.Type; |
| 110 | export type SourceInspectionToolListItem = | 131 | export type SourceInspectionToolListItem = |
| 111 | typeof SourceInspectionToolListItemSchema.Type; | 132 | typeof SourceInspectionToolListItemSchema.Type; |
| 133 | export type SourceInspectionToolContractSide = | ||
| 134 | typeof SourceInspectionToolContractSideSchema.Type; | ||
| 135 | export type SourceInspectionToolContract = | ||
| 136 | typeof SourceInspectionToolContractSchema.Type; | ||
| 112 | export type SourceInspection = typeof SourceInspectionSchema.Type; | 137 | export type SourceInspection = typeof SourceInspectionSchema.Type; |
| 113 | export type SourceInspectionToolDetail = | 138 | export type SourceInspectionToolDetail = |
| 114 | typeof SourceInspectionToolDetailSchema.Type; | 139 | typeof SourceInspectionToolDetailSchema.Type; |
| @@ -25,6 +25,27 @@ describe("control-plane-schema", () => { · 24 unmodified lines | |||
| 25 | method: "GET", | 25 | method: "GET", |
| 26 | pathTemplate: "/issues", | 26 | pathTemplate: "/issues", |
| 27 | }, | 27 | }, |
| 28 | contract: { | ||
| 29 | callSignature: "(args: GithubIssuesListCall) => Promise<GithubIssuesListResult>", | ||
| 30 | callDeclaration: "declare function githubIssuesList(args: GithubIssuesListCall): Promise<GithubIssuesListResult>;", | ||
| 31 | callShapeId: "shape_call", | ||
| 32 | resultShapeId: "shape_result", | ||
| 33 | responseSetId: "response_set_1", | ||
| 34 | input: { | ||
| 35 | shapeId: "shape_call", | ||
| 36 | typePreview: "{ owner: string; repo: string }", | ||
| 37 | typeDeclaration: "type GithubIssuesListCall = { owner: string; repo: string };", | ||
| 38 | schemaJson: "{}", | ||
| 39 | exampleJson: null, | ||
| 40 | }, | ||
| 41 | output: { | ||
| 42 | shapeId: "shape_result", | ||
| 43 | typePreview: "{ data: unknown; error: unknown; headers: Record<string, string>; status: number | null }", | ||
| 44 | typeDeclaration: "type GithubIssuesListResult = { data: unknown; error: unknown; headers: Record<string, string>; status: number | null };", | ||
| 45 | schemaJson: "{}", | ||
| 46 | exampleJson: null, | ||
| 47 | }, | ||
| 48 | }, | ||
| 28 | sections: [{ | 49 | sections: [{ |
| 29 | kind: "facts", | 50 | kind: "facts", |
| 30 | title: "Overview", | 51 | title: "Overview", |
| @@ -456,12 +456,16 @@ export const createGoogleDiscoveryToolFromDefinition = ( · 455 unmodified lines | |||
| 456 | interaction: input.definition.method === "get" || input.definition.method === "head" | 456 | interaction: input.definition.method === "get" || input.definition.method === "head" |
| 457 | ? "auto" | 457 | ? "auto" |
| 458 | : "required", | 458 | : "required", |
| 459 | ...(presentation.inputSchema !== undefined | 459 | contract: { |
| 460 | ? { inputSchema: presentation.inputSchema } | 460 | inputTypePreview: presentation.inputTypePreview, |
| 461 | : {}), | 461 | outputTypePreview: presentation.outputTypePreview, |
| 462 | ...(presentation.outputSchema !== undefined | 462 | ...(presentation.inputSchema !== undefined |
| 463 | ? { outputSchema: presentation.outputSchema } | 463 | ? { inputSchema: presentation.inputSchema } |
| 464 | : {}), | 464 | : {}), |
| 465 | ...(presentation.outputSchema !== undefined | ||
| 466 | ? { outputSchema: presentation.outputSchema } | ||
| 467 | : {}), | ||
| 468 | }, | ||
| 465 | sourceKey: input.sourceKey, | 469 | sourceKey: input.sourceKey, |
| 466 | providerKind: "google_discovery", | 470 | providerKind: "google_discovery", |
| 467 | providerData, | 471 | providerData, |
| @@ -1437,25 +1437,27 @@ export const createGraphqlToolFromPersistedOperation = (input: { · 1436 unmodified lines | |||
| 1437 | : providerData.operationType === "query" | 1437 | : providerData.operationType === "query" |
| 1438 | ? "auto" | 1438 | ? "auto" |
| 1439 | : "required", | 1439 | : "required", |
| 1440 | inputTypePreview: typeSignatureFromSchema( | 1440 | contract: { |
| 1441 | input.inputSchema, | 1441 | inputTypePreview: typeSignatureFromSchema( |
| 1442 | "unknown", | 1442 | input.inputSchema, |
| 1443 | GRAPHQL_PRESENTATION_TYPE_MAX_LENGTH, | 1443 | "unknown", |
| 1444 | ), | 1444 | GRAPHQL_PRESENTATION_TYPE_MAX_LENGTH, |
| 1445 | outputTypePreview: typeSignatureFromSchema( | 1445 | ), |
| 1446 | input.outputSchema, | 1446 | outputTypePreview: typeSignatureFromSchema( |
| 1447 | "unknown", | 1447 | input.outputSchema, |
| 1448 | GRAPHQL_PRESENTATION_TYPE_MAX_LENGTH, | 1448 | "unknown", |
| 1449 | ), | 1449 | GRAPHQL_PRESENTATION_TYPE_MAX_LENGTH, |
| 1450 | ...(input.inputSchema !== undefined | 1450 | ), |
| 1451 | ? { inputSchema: input.inputSchema } | 1451 | ...(input.inputSchema !== undefined |
| 1452 | : {}), | 1452 | ? { inputSchema: input.inputSchema } |
| 1453 | ...(input.outputSchema !== undefined | 1453 | : {}), |
| 1454 | ? { outputSchema: input.outputSchema } | 1454 | ...(input.outputSchema !== undefined |
| 1455 | : {}), | 1455 | ? { outputSchema: input.outputSchema } |
| 1456 | ...(input.exampleInput !== undefined | 1456 | : {}), |
| 1457 | ? { exampleInput: input.exampleInput } | 1457 | ...(input.exampleInput !== undefined |
| 1458 | : {}), | 1458 | ? { exampleInput: input.exampleInput } |
| 1459 | : {}), | ||
| 1460 | }, | ||
| 1459 | sourceKey: input.sourceKey, | 1461 | sourceKey: input.sourceKey, |
| 1460 | providerKind: "graphql", | 1462 | providerKind: "graphql", |
| 1461 | providerData, | 1463 | providerData, |
| @@ -1524,17 +1526,19 @@ export const graphqlToolDescriptorFromDefinition = (input: { · 62 unmodified lines | |||
| 1524 | description: input.definition.description, | 1526 | description: input.definition.description, |
| 1525 | interaction: | 1527 | interaction: |
| 1526 | input.definition.operationType === "query" ? "auto" : "required", | 1528 | input.definition.operationType === "query" ? "auto" : "required", |
| 1527 | inputTypePreview: presentation.inputTypePreview, | 1529 | contract: { |
| 1528 | outputTypePreview: presentation.outputTypePreview, | 1530 | inputTypePreview: presentation.inputTypePreview, |
| 1529 | ...(input.includeSchemas && presentation.inputSchema !== undefined | 1531 | outputTypePreview: presentation.outputTypePreview, |
| 1530 | ? { inputSchema: presentation.inputSchema } | 1532 | ...(input.includeSchemas && presentation.inputSchema !== undefined |
| 1531 | : {}), | 1533 | ? { inputSchema: presentation.inputSchema } |
| 1532 | ...(input.includeSchemas && presentation.outputSchema !== undefined | 1534 | : {}), |
| 1533 | ? { outputSchema: presentation.outputSchema } | 1535 | ...(input.includeSchemas && presentation.outputSchema !== undefined |
| 1534 | : {}), | 1536 | ? { outputSchema: presentation.outputSchema } |
| 1535 | ...(presentation.exampleInput !== undefined | 1537 | : {}), |
| 1536 | ? { exampleInput: presentation.exampleInput } | 1538 | ...(presentation.exampleInput !== undefined |
| 1537 | : {}), | 1539 | ? { exampleInput: presentation.exampleInput } |
| 1540 | : {}), | ||
| 1541 | }, | ||
| 1538 | providerKind: "graphql", | 1542 | providerKind: "graphql", |
| 1539 | providerData: presentation.providerData, | 1543 | providerData: presentation.providerData, |
| 1540 | }; | 1544 | }; |
| @@ -4,7 +4,8 @@ import { · 3 unmodified lines | |||
| 4 | type ToolInput, | 4 | type ToolInput, |
| 5 | type ToolPath, | 5 | type ToolPath, |
| 6 | } from "@executor/codemode-core"; | 6 | } from "@executor/codemode-core"; |
| 7 | import { createSdkMcpConnector } from "./connection"; | 7 | import { createPooledMcpConnector } from "./connection-pool"; |
| 8 | import { createSdkMcpConnector, isMcpStdioTransport } from "./connection"; | ||
| 8 | import { | 9 | import { |
| 9 | createMcpToolsFromManifest, | 10 | createMcpToolsFromManifest, |
| 10 | discoverMcpToolsFromConnector, | 11 | discoverMcpToolsFromConnector, |
| @@ -26,8 +27,8 @@ import { · 15 unmodified lines | |||
| 26 | encodeBindingConfig, | 27 | encodeBindingConfig, |
| 27 | McpConnectFieldsSchema, | 28 | McpConnectFieldsSchema, |
| 28 | OptionalNullableStringSchema, | 29 | OptionalNullableStringSchema, |
| 29 | SourceConnectCommonFieldsSchema, | ||
| 30 | SourceTransportSchema, | 30 | SourceTransportSchema, |
| 31 | StringArraySchema, | ||
| 31 | StringMapSchema, | 32 | StringMapSchema, |
| 32 | createCatalogImportMetadata, | 33 | createCatalogImportMetadata, |
| 33 | EXECUTABLE_BINDING_VERSION, | 34 | EXECUTABLE_BINDING_VERSION, |
| @@ -35,6 +36,7 @@ import { · 1 unmodified line | |||
| 35 | type Source, | 36 | type Source, |
| 36 | type SourceAdapter, | 37 | type SourceAdapter, |
| 37 | type SourceCatalogSyncResult, | 38 | type SourceCatalogSyncResult, |
| 39 | type SourceTransport, | ||
| 38 | } from "@executor/source-core"; | 40 | } from "@executor/source-core"; |
| 39 | import * as Effect from "effect/Effect"; | 41 | import * as Effect from "effect/Effect"; |
| 40 | import * as Schema from "effect/Schema"; | 42 | import * as Schema from "effect/Schema"; |
| @@ -69,27 +71,38 @@ const namespaceFromSourceName = (name: string): string => { · 28 unmodified lines | |||
| 69 | return normalized.length > 0 ? normalized : "source"; | 71 | return normalized.length > 0 ? normalized : "source"; |
| 70 | }; | 72 | }; |
| 71 | 73 | ||
| 74 | const OptionalNullableStringArraySchema = Schema.optional( | ||
| 75 | Schema.NullOr(StringArraySchema), | ||
| 76 | ); | ||
| 77 | |||
| 72 | const McpConnectPayloadSchema = Schema.extend( | 78 | const McpConnectPayloadSchema = Schema.extend( |
| 73 | SourceConnectCommonFieldsSchema, | 79 | McpConnectFieldsSchema, |
| 74 | Schema.extend( | 80 | Schema.Struct({ |
| 75 | McpConnectFieldsSchema, | 81 | kind: Schema.Literal("mcp"), |
| 76 | Schema.Struct({ | 82 | endpoint: OptionalNullableStringSchema, |
| 77 | kind: Schema.Literal("mcp"), | 83 | name: OptionalNullableStringSchema, |
| 78 | }), | 84 | namespace: OptionalNullableStringSchema, |
| 79 | ), | 85 | }), |
| 80 | ); | 86 | ); |
| 81 | 87 | ||
| 82 | const McpExecutorAddInputSchema = Schema.Struct({ | 88 | const McpExecutorAddInputSchema = Schema.extend( |
| 83 | kind: Schema.optional(Schema.Literal("mcp")), | 89 | McpConnectFieldsSchema, |
| 84 | endpoint: Schema.String, | 90 | Schema.Struct({ |
| 85 | name: OptionalNullableStringSchema, | 91 | kind: Schema.optional(Schema.Literal("mcp")), |
| 86 | namespace: OptionalNullableStringSchema, | 92 | endpoint: OptionalNullableStringSchema, |
| 87 | }); | 93 | name: OptionalNullableStringSchema, |
| 94 | namespace: OptionalNullableStringSchema, | ||
| 95 | }), | ||
| 96 | ); | ||
| 88 | 97 | ||
| 89 | const McpBindingConfigSchema = Schema.Struct({ | 98 | const McpBindingConfigSchema = Schema.Struct({ |
| 90 | transport: Schema.NullOr(SourceTransportSchema), | 99 | transport: Schema.NullOr(SourceTransportSchema), |
| 91 | queryParams: Schema.NullOr(StringMapSchema), | 100 | queryParams: Schema.NullOr(StringMapSchema), |
| 92 | headers: Schema.NullOr(StringMapSchema), | 101 | headers: Schema.NullOr(StringMapSchema), |
| 102 | command: Schema.NullOr(Schema.String), | ||
| 103 | args: Schema.NullOr(StringArraySchema), | ||
| 104 | env: Schema.NullOr(StringMapSchema), | ||
| 105 | cwd: Schema.NullOr(Schema.String), | ||
| 93 | }); | 106 | }); |
| 94 | 107 | ||
| 95 | type McpBindingConfig = typeof McpBindingConfigSchema.Type; | 108 | type McpBindingConfig = typeof McpBindingConfigSchema.Type; |
| @@ -98,6 +111,10 @@ const McpSourceBindingPayloadSchema = Schema.Struct({ · 2 unmodified lines | |||
| 98 | transport: Schema.optional(Schema.NullOr(SourceTransportSchema)), | 111 | transport: Schema.optional(Schema.NullOr(SourceTransportSchema)), |
| 99 | queryParams: Schema.optional(Schema.NullOr(StringMapSchema)), | 112 | queryParams: Schema.optional(Schema.NullOr(StringMapSchema)), |
| 100 | headers: Schema.optional(Schema.NullOr(StringMapSchema)), | 113 | headers: Schema.optional(Schema.NullOr(StringMapSchema)), |
| 114 | command: Schema.optional(Schema.NullOr(Schema.String)), | ||
| 115 | args: OptionalNullableStringArraySchema, | ||
| 116 | env: Schema.optional(Schema.NullOr(StringMapSchema)), | ||
| 117 | cwd: Schema.optional(Schema.NullOr(Schema.String)), | ||
| 101 | }); | 118 | }); |
| 102 | 119 | ||
| 103 | const McpExecutableBindingSchema = Schema.Struct({ | 120 | const McpExecutableBindingSchema = Schema.Struct({ |
| @@ -127,6 +144,86 @@ const bindingHasAnyField = ( · 23 unmodified lines | |||
| 127 | !Array.isArray(value) && | 144 | !Array.isArray(value) && |
| 128 | fields.some((field) => Object.prototype.hasOwnProperty.call(value, field)); | 145 | fields.some((field) => Object.prototype.hasOwnProperty.call(value, field)); |
| 129 | 146 | ||
| 147 | const trimOrNull = (value: string | null | undefined): string | null => { | ||
| 148 | if (value === null || value === undefined) { | ||
| 149 | return null; | ||
| 150 | } | ||
| 151 | |||
| 152 | const trimmed = value.trim(); | ||
| 153 | return trimmed.length > 0 ? trimmed : null; | ||
| 154 | }; | ||
| 155 | |||
| 156 | const normalizeStringArray = ( | ||
| 157 | value: ReadonlyArray<string> | null | undefined, | ||
| 158 | ): string[] | null => { | ||
| 159 | if (!value || value.length === 0) { | ||
| 160 | return null; | ||
| 161 | } | ||
| 162 | |||
| 163 | const normalized = value | ||
| 164 | .map((entry) => entry.trim()) | ||
| 165 | .filter((entry) => entry.length > 0); | ||
| 166 | |||
| 167 | return normalized.length > 0 ? normalized : null; | ||
| 168 | }; | ||
| 169 | |||
| 170 | const normalizeMcpBindingConfig = ( | ||
| 171 | bindingConfig: { | ||
| 172 | transport?: SourceTransport | null | undefined; | ||
| 173 | queryParams?: Record<string, string> | null | undefined; | ||
| 174 | headers?: Record<string, string> | null | undefined; | ||
| 175 | command?: string | null | undefined; | ||
| 176 | args?: ReadonlyArray<string> | null | undefined; | ||
| 177 | env?: Record<string, string> | null | undefined; | ||
| 178 | cwd?: string | null | undefined; | ||
| 179 | }, | ||
| 180 | ): Effect.Effect<McpBindingConfig, Error, never> => | ||
| 181 | Effect.gen(function* () { | ||
| 182 | const command = trimOrNull(bindingConfig.command); | ||
| 183 | const isStdio = isMcpStdioTransport({ | ||
| 184 | transport: bindingConfig.transport ?? undefined, | ||
| 185 | command: command ?? undefined, | ||
| 186 | }); | ||
| 187 | |||
| 188 | if (isStdio) { | ||
| 189 | if (command === null) { | ||
| 190 | return yield* sourceCoreEffectError("mcp/adapter", "MCP stdio transport requires a command"); | ||
| 191 | } | ||
| 192 | |||
| 193 | if (bindingConfig.queryParams && Object.keys(bindingConfig.queryParams).length > 0) { | ||
| 194 | return yield* sourceCoreEffectError("mcp/adapter", "MCP stdio transport does not support query params"); | ||
| 195 | } | ||
| 196 | |||
| 197 | if (bindingConfig.headers && Object.keys(bindingConfig.headers).length > 0) { | ||
| 198 | return yield* sourceCoreEffectError("mcp/adapter", "MCP stdio transport does not support request headers"); | ||
| 199 | } | ||
| 200 | |||
| 201 | return { | ||
| 202 | transport: "stdio", | ||
| 203 | queryParams: null, | ||
| 204 | headers: null, | ||
| 205 | command, | ||
| 206 | args: normalizeStringArray(bindingConfig.args), | ||
| 207 | env: bindingConfig.env ?? null, | ||
| 208 | cwd: trimOrNull(bindingConfig.cwd), | ||
| 209 | } satisfies McpBindingConfig; | ||
| 210 | } | ||
| 211 | |||
| 212 | if (command !== null || bindingConfig.args || bindingConfig.env || trimOrNull(bindingConfig.cwd) !== null) { | ||
| 213 | return yield* sourceCoreEffectError("mcp/adapter", "MCP process settings require transport: \"stdio\""); | ||
| 214 | } | ||
| 215 | |||
| 216 | return { | ||
| 217 | transport: bindingConfig.transport ?? null, | ||
| 218 | queryParams: bindingConfig.queryParams ?? null, | ||
| 219 | headers: bindingConfig.headers ?? null, | ||
| 220 | command: null, | ||
| 221 | args: null, | ||
| 222 | env: null, | ||
| 223 | cwd: null, | ||
| 224 | } satisfies McpBindingConfig; | ||
| 225 | }); | ||
| 226 | |||
| 130 | const mcpBindingConfigFromSource = ( | 227 | const mcpBindingConfigFromSource = ( |
| 131 | source: Pick<Source, "id" | "bindingVersion" | "binding">, | 228 | source: Pick<Source, "id" | "bindingVersion" | "binding">, |
| 132 | ): Effect.Effect<McpBindingConfig, Error, never> => | 229 | ): Effect.Effect<McpBindingConfig, Error, never> => |
| @@ -145,14 +242,10 @@ const mcpBindingConfigFromSource = ( · 12 unmodified lines | |||
| 145 | expectedVersion: MCP_BINDING_CONFIG_VERSION, | 242 | expectedVersion: MCP_BINDING_CONFIG_VERSION, |
| 146 | schema: McpSourceBindingPayloadSchema, | 243 | schema: McpSourceBindingPayloadSchema, |
| 147 | value: source.binding, | 244 | value: source.binding, |
| 148 | allowedKeys: ["transport", "queryParams", "headers"], | 245 | allowedKeys: ["transport", "queryParams", "headers", "command", "args", "env", "cwd"], |
| 149 | }); | 246 | }); |
| 150 | 247 | ||
| 151 | return { | 248 | return yield* normalizeMcpBindingConfig(bindingConfig); |
| 152 | transport: bindingConfig.transport ?? null, | ||
| 153 | queryParams: bindingConfig.queryParams ?? null, | ||
| 154 | headers: bindingConfig.headers ?? null, | ||
| 155 | } satisfies McpBindingConfig; | ||
| 156 | }); | 249 | }); |
| 157 | 250 | ||
| 158 | const effectFromMcpManifestEntry = ( | 251 | const effectFromMcpManifestEntry = ( |
| @@ -233,7 +326,8 @@ export const mcpSourceAdapter: SourceAdapter = { · 74 unmodified lines | |||
| 233 | connectPayloadSchema: McpConnectPayloadSchema, | 326 | connectPayloadSchema: McpConnectPayloadSchema, |
| 234 | executorAddInputSchema: McpExecutorAddInputSchema, | 327 | executorAddInputSchema: McpExecutorAddInputSchema, |
| 235 | executorAddHelpText: [ | 328 | executorAddHelpText: [ |
| 236 | 'Omit kind or set kind: "mcp". endpoint is the MCP server URL.', | 329 | 'Omit kind or set kind: "mcp". For remote servers, provide endpoint plus optional transport/queryParams/headers.', |
| 330 | 'For local servers, set transport: "stdio" and provide command plus optional args/env/cwd.', | ||
| 237 | ], | 331 | ], |
| 238 | executorAddInputSignatureWidth: 240, | 332 | executorAddInputSignatureWidth: 240, |
| 239 | serializeBindingConfig: (source) => | 333 | serializeBindingConfig: (source) => |
| @@ -264,6 +358,10 @@ export const mcpSourceAdapter: SourceAdapter = { · 24 unmodified lines | |||
| 264 | transport: bindingConfig.transport, | 358 | transport: bindingConfig.transport, |
| 265 | queryParams: bindingConfig.queryParams, | 359 | queryParams: bindingConfig.queryParams, |
| 266 | headers: bindingConfig.headers, | 360 | headers: bindingConfig.headers, |
| 361 | command: bindingConfig.command, | ||
| 362 | args: bindingConfig.args, | ||
| 363 | env: bindingConfig.env, | ||
| 364 | cwd: bindingConfig.cwd, | ||
| 267 | })), | 365 | })), |
| 268 | sourceConfigFromSource: (source) => | 366 | sourceConfigFromSource: (source) => |
| 269 | Effect.runSync( | 367 | Effect.runSync( |
| @@ -273,6 +371,10 @@ export const mcpSourceAdapter: SourceAdapter = { · 3 unmodified lines | |||
| 273 | transport: bindingConfig.transport, | 371 | transport: bindingConfig.transport, |
| 274 | queryParams: bindingConfig.queryParams, | 372 | queryParams: bindingConfig.queryParams, |
| 275 | headers: bindingConfig.headers, | 373 | headers: bindingConfig.headers, |
| 374 | command: bindingConfig.command, | ||
| 375 | args: bindingConfig.args, | ||
| 376 | env: bindingConfig.env, | ||
| 377 | cwd: bindingConfig.cwd, | ||
| 276 | })), | 378 | })), |
| 277 | ), | 379 | ), |
| 278 | validateSource: (source) => | 380 | validateSource: (source) => |
| @@ -286,6 +388,10 @@ export const mcpSourceAdapter: SourceAdapter = { · 7 unmodified lines | |||
| 286 | transport: bindingConfig.transport, | 388 | transport: bindingConfig.transport, |
| 287 | queryParams: bindingConfig.queryParams, | 389 | queryParams: bindingConfig.queryParams, |
| 288 | headers: bindingConfig.headers, | 390 | headers: bindingConfig.headers, |
| 391 | command: bindingConfig.command, | ||
| 392 | args: bindingConfig.args, | ||
| 393 | env: bindingConfig.env, | ||
| 394 | cwd: bindingConfig.cwd, | ||
| 289 | }, | 395 | }, |
| 290 | }; | 396 | }; |
| 291 | }), | 397 | }), |
| @@ -309,6 +415,10 @@ export const mcpSourceAdapter: SourceAdapter = { · 17 unmodified lines | |||
| 309 | authCookies: auth.cookies, | 415 | authCookies: auth.cookies, |
| 310 | }), | 416 | }), |
| 311 | authProvider: auth.authProvider, | 417 | authProvider: auth.authProvider, |
| 418 | command: bindingConfig.command ?? undefined, | ||
| 419 | args: bindingConfig.args ?? undefined, | ||
| 420 | env: bindingConfig.env ?? undefined, | ||
| 421 | cwd: bindingConfig.cwd ?? undefined, | ||
| 312 | }), | 422 | }), |
| 313 | catch: (cause) => | 423 | catch: (cause) => |
| 314 | cause instanceof Error ? cause : new Error(String(cause)), | 424 | cause instanceof Error ? cause : new Error(String(cause)), |
| @@ -353,19 +463,30 @@ export const mcpSourceAdapter: SourceAdapter = { · 38 unmodified lines | |||
| 353 | const bindingConfig = Effect.runSync( | 463 | const bindingConfig = Effect.runSync( |
| 354 | mcpBindingConfigFromSource(input.source), | 464 | mcpBindingConfigFromSource(input.source), |
| 355 | ); | 465 | ); |
| 356 | const connector = createSdkMcpConnector({ | 466 | const connector = createPooledMcpConnector({ |
| 357 | endpoint: input.source.endpoint, | 467 | connect: createSdkMcpConnector({ |
| 358 | transport: bindingConfig.transport ?? undefined, | 468 | endpoint: input.source.endpoint, |
| 359 | queryParams: { | 469 | transport: bindingConfig.transport ?? undefined, |
| 360 | ...bindingConfig.queryParams, | 470 | queryParams: { |
| 361 | ...input.auth.queryParams, | 471 | ...bindingConfig.queryParams, |
| 362 | }, | 472 | ...input.auth.queryParams, |
| 363 | headers: headersWithAuthCookies({ | 473 | }, |
| 364 | headers: bindingConfig.headers ?? {}, | 474 | headers: headersWithAuthCookies({ |
| 365 | authHeaders: input.auth.headers, | 475 | headers: bindingConfig.headers ?? {}, |
| 366 | authCookies: input.auth.cookies, | 476 | authHeaders: input.auth.headers, |
| 477 | authCookies: input.auth.cookies, | ||
| 478 | }), | ||
| 479 | authProvider: input.auth.authProvider, | ||
| 480 | command: bindingConfig.command ?? undefined, | ||
| 481 | args: bindingConfig.args ?? undefined, | ||
| 482 | env: bindingConfig.env ?? undefined, | ||
| 483 | cwd: bindingConfig.cwd ?? undefined, | ||
| 367 | }), | 484 | }), |
| 368 | authProvider: input.auth.authProvider, | 485 | runId: |
| 486 | typeof input.context?.runId === "string" && input.context.runId.length > 0 | ||
| 487 | ? input.context.runId | ||
| 488 | : undefined, | ||
| 489 | sourceKey: input.source.id, | ||
| 369 | }); | 490 | }); |
| 370 | const tools = createMcpToolsFromManifest({ | 491 | const tools = createMcpToolsFromManifest({ |
| 371 | manifest: { | 492 | manifest: { |
| @@ -392,8 +513,8 @@ export const mcpSourceAdapter: SourceAdapter = { · 20 unmodified lines | |||
| 392 | icons: null, | 513 | icons: null, |
| 393 | meta: null, | 514 | meta: null, |
| 394 | rawTool: null, | 515 | rawTool: null, |
| 395 | inputSchema: input.descriptor.inputSchema, | 516 | inputSchema: input.descriptor.contract?.inputSchema, |
| 396 | outputSchema: input.descriptor.outputSchema, | 517 | outputSchema: input.descriptor.contract?.outputSchema, |
| 397 | }, | 518 | }, |
| 398 | ], | 519 | ], |
| 399 | }, | 520 | }, |
| @@ -430,8 +551,14 @@ export const mcpSourceAdapter: SourceAdapter = { · 30 unmodified lines | |||
| 430 | metadata: { | 551 | metadata: { |
| 431 | sourceKey: input.source.id, | 552 | sourceKey: input.source.id, |
| 432 | interaction: input.descriptor.interaction, | 553 | interaction: input.descriptor.interaction, |
| 433 | inputSchema: input.descriptor.inputSchema, | 554 | contract: { |
| 434 | outputSchema: input.descriptor.outputSchema, | 555 | ...(input.descriptor.contract?.inputSchema !== undefined |
| 556 | ? { inputSchema: input.descriptor.contract.inputSchema } | ||
| 557 | : {}), | ||
| 558 | ...(input.descriptor.contract?.outputSchema !== undefined | ||
| 559 | ? { outputSchema: input.descriptor.contract.outputSchema } | ||
| 560 | : {}), | ||
| 561 | }, | ||
| 435 | providerKind: input.descriptor.providerKind, | 562 | providerKind: input.descriptor.providerKind, |
| 436 | providerData: input.descriptor.providerData, | 563 | providerData: input.descriptor.providerData, |
| 437 | }, | 564 | }, |
| @@ -377,7 +377,7 @@ describe("codemode-mcp", () => { · 376 unmodified lines | |||
| 377 | 377 | ||
| 378 | const toolDefinition = resolveToolDefinition(discovered.tools["source.mcp.echo"]!); | 378 | const toolDefinition = resolveToolDefinition(discovered.tools["source.mcp.echo"]!); |
| 379 | expect(toolDefinition.metadata?.sourceKey).toBe("mcp.demo"); | 379 | expect(toolDefinition.metadata?.sourceKey).toBe("mcp.demo"); |
| 380 | expect(JSON.stringify(toolDefinition.metadata?.inputSchema)).toContain("value"); | 380 | expect(JSON.stringify(toolDefinition.metadata?.contract?.inputSchema)).toContain("value"); |
| 381 | 381 | ||
| 382 | const invoke = resolveToolExecutor(discovered.tools, "source.mcp.echo"); | 382 | const invoke = resolveToolExecutor(discovered.tools, "source.mcp.echo"); |
| 383 | const invocationResult = yield* Effect.promise(() => invoke({ value: "hello" })); | 383 | const invocationResult = yield* Effect.promise(() => invoke({ value: "hello" })); |
| @@ -503,12 +503,14 @@ export const createMcpToolsFromManifest = (input: { · 502 unmodified lines | |||
| 503 | }, | 503 | }, |
| 504 | metadata: { | 504 | metadata: { |
| 505 | sourceKey, | 505 | sourceKey, |
| 506 | ...(entry.inputSchema !== undefined | 506 | contract: { |
| 507 | ? { inputSchema: entry.inputSchema } | 507 | ...(entry.inputSchema !== undefined |
| 508 | : {}), | 508 | ? { inputSchema: entry.inputSchema } |
| 509 | ...(entry.outputSchema !== undefined | 509 | : {}), |
| 510 | ? { outputSchema: entry.outputSchema } | 510 | ...(entry.outputSchema !== undefined |
| 511 | : {}), | 511 | ? { outputSchema: entry.outputSchema } |
| 512 | : {}), | ||
| 513 | }, | ||
| 512 | }, | 514 | }, |
| 513 | }), | 515 | }), |
| 514 | ] as const; | 516 | ] as const; |
| @@ -111,7 +111,7 @@ describe("openapi-extraction real specs", () => { · 110 unmodified lines | |||
| 111 | 111 | ||
| 112 | for (const expectedTool of expectedTools) { | 112 | for (const expectedTool of expectedTools) { |
| 113 | const resolved = resolveToolDefinition(tools[expectedTool.expectedPath]!); | 113 | const resolved = resolveToolDefinition(tools[expectedTool.expectedPath]!); |
| 114 | const inputSchema = parseInputSchema(resolved.metadata?.inputSchema); | 114 | const inputSchema = parseInputSchema(resolved.metadata?.contract?.inputSchema); |
| 115 | 115 | ||
| 116 | expect(inputSchema?.type).toBe("object"); | 116 | expect(inputSchema?.type).toBe("object"); |
| 117 | expect(Object.keys(inputSchema?.properties ?? {})).toEqual( | 117 | expect(Object.keys(inputSchema?.properties ?? {})).toEqual( |
| @@ -354,7 +354,7 @@ describe("openapi-tools", () => { · 353 unmodified lines | |||
| 354 | expect(resolvedGet.metadata?.sourceKey).toBe("api.demo"); | 354 | expect(resolvedGet.metadata?.sourceKey).toBe("api.demo"); |
| 355 | expect(resolvedGet.metadata?.providerKind).toBe("openapi"); | 355 | expect(resolvedGet.metadata?.providerKind).toBe("openapi"); |
| 356 | expect(resolvedGet.metadata?.providerData).toMatchObject({ group: "repos" }); | 356 | expect(resolvedGet.metadata?.providerData).toMatchObject({ group: "repos" }); |
| 357 | expect(resolvedGet.metadata?.inputSchema).toBeDefined(); | 357 | expect(resolvedGet.metadata?.contract?.inputSchema).toBeDefined(); |
| 358 | }), | 358 | }), |
| 359 | ); | 359 | ); |
| 360 | 360 | ||
| @@ -541,20 +541,22 @@ export const createOpenApiToolFromDefinition = ( · 540 unmodified lines | |||
| 541 | }, | 541 | }, |
| 542 | metadata: { | 542 | metadata: { |
| 543 | sourceKey: input.sourceKey, | 543 | sourceKey: input.sourceKey, |
| 544 | inputTypePreview: presentation.inputTypePreview, | 544 | contract: { |
| 545 | outputTypePreview: presentation.outputTypePreview, | 545 | inputTypePreview: presentation.inputTypePreview, |
| 546 | ...((presentation.inputSchema ?? input.definition.typing?.inputSchema) !== undefined | 546 | outputTypePreview: presentation.outputTypePreview, |
| 547 | ? { inputSchema: presentation.inputSchema ?? input.definition.typing?.inputSchema } | 547 | ...((presentation.inputSchema ?? input.definition.typing?.inputSchema) !== undefined |
| 548 | : {}), | 548 | ? { inputSchema: presentation.inputSchema ?? input.definition.typing?.inputSchema } |
| 549 | ...((presentation.outputSchema ?? input.definition.typing?.outputSchema) !== undefined | 549 | : {}), |
| 550 | ? { outputSchema: presentation.outputSchema ?? input.definition.typing?.outputSchema } | 550 | ...((presentation.outputSchema ?? input.definition.typing?.outputSchema) !== undefined |
| 551 | : {}), | 551 | ? { outputSchema: presentation.outputSchema ?? input.definition.typing?.outputSchema } |
| 552 | ...(presentation.exampleInput !== undefined | 552 | : {}), |
| 553 | ? { exampleInput: presentation.exampleInput } | 553 | ...(presentation.exampleInput !== undefined |
| 554 | : {}), | 554 | ? { exampleInput: presentation.exampleInput } |
| 555 | ...(presentation.exampleOutput !== undefined | 555 | : {}), |
| 556 | ? { exampleOutput: presentation.exampleOutput } | 556 | ...(presentation.exampleOutput !== undefined |
| 557 | : {}), | 557 | ? { exampleOutput: presentation.exampleOutput } |
| 558 | : {}), | ||
| 559 | }, | ||
| 558 | providerKind: "openapi", | 560 | providerKind: "openapi", |
| 559 | providerData: presentation.providerData, | 561 | providerData: presentation.providerData, |
| 560 | }, | 562 | }, |