- Update styling logic in the ToolRow and GroupNode components to differentiate selected and source tools, improving visual clarity. - Modify OpenAPI preparation to always include parameter schemas, ensuring consistent schema handling. - Add tests to validate input and output hints for OpenAPI tools, ensuring accurate type representation. - Update OpenAPI cache versioning to reflect recent changes and improvements in tool discovery. - Refactor workspace tool inventory logic to streamline state management and improve performance.
Unified · Split
| @@ -80,7 +80,9 @@ export function GroupNode({ · 79 unmodified lines | |||
| 80 | "sticky bg-background/95 backdrop-blur-sm", | 80 | "sticky bg-background/95 backdrop-blur-sm", |
| 81 | isExpanded && "border-b border-border/30", | 81 | isExpanded && "border-b border-border/30", |
| 82 | isGroupSelected | 82 | isGroupSelected |
| 83 | ? "bg-primary/10 ring-1 ring-primary/20" | 83 | ? isSource |
| 84 | ? "bg-primary/10 ring-1 ring-primary/20" | ||
| 85 | : "bg-accent/20 ring-1 ring-accent/30" | ||
| 84 | : "hover:bg-accent/30", | 86 | : "hover:bg-accent/30", |
| 85 | )} | 87 | )} |
| 86 | style={{ | 88 | style={{ |
| @@ -50,9 +50,11 @@ const ToolRow = memo(function ToolRow({ · 49 unmodified lines | |||
| 50 | className={cn( | 50 | className={cn( |
| 51 | "flex items-center gap-2 px-2 py-1.5 transition-colors cursor-pointer group/tool", | 51 | "flex items-center gap-2 px-2 py-1.5 transition-colors cursor-pointer group/tool", |
| 52 | expanded | 52 | expanded |
| 53 | ? "sticky bg-background/95 backdrop-blur-sm bg-accent/30" | 53 | ? selected |
| 54 | ? "sticky bg-accent ring-1 ring-accent/30" | ||
| 55 | : "sticky bg-card" | ||
| 54 | : selected | 56 | : selected |
| 55 | ? "bg-primary/5 ring-1 ring-primary/10" | 57 | ? "bg-accent ring-1 ring-accent/30" |
| 56 | : "hover:bg-accent/20", | 58 | : "hover:bg-accent/20", |
| 57 | )} | 59 | )} |
| 58 | style={{ | 60 | style={{ |
| @@ -233,7 +233,7 @@ export async function prepareOpenApiSpec( · 232 unmodified lines | |||
| 233 | { | 233 | { |
| 234 | includeSchemas: profile === "full", | 234 | includeSchemas: profile === "full", |
| 235 | includeTypeHints: true, | 235 | includeTypeHints: true, |
| 236 | includeParameterSchemas: profile === "full", | 236 | includeParameterSchemas: true, |
| 237 | resolveSchemaRefs: profile === "full", | 237 | resolveSchemaRefs: profile === "full", |
| 238 | }, | 238 | }, |
| 239 | ); | 239 | ); |
| @@ -100,6 +100,8 @@ describe("OpenAPI schema-first typing", () => { · 99 unmodified lines | |||
| 100 | expect(getTool!.typing?.outputSchema).toBeDefined(); | 100 | expect(getTool!.typing?.outputSchema).toBeDefined(); |
| 101 | expect(getTool!.typing?.requiredInputKeys ?? []).toContain("id"); | 101 | expect(getTool!.typing?.requiredInputKeys ?? []).toContain("id"); |
| 102 | expect(getTool!.typing?.previewInputKeys ?? []).toContain("include"); | 102 | expect(getTool!.typing?.previewInputKeys ?? []).toContain("include"); |
| 103 | expect(getTool!.typing?.inputHint).toContain("id: string"); | ||
| 104 | expect(getTool!.typing?.inputHint).toContain("include?:"); | ||
| 103 | expect(getTool!.typing?.typedRef).toBeDefined(); | 105 | expect(getTool!.typing?.typedRef).toBeDefined(); |
| 104 | }); | 106 | }); |
| 105 | 107 | ||
| @@ -301,4 +301,34 @@ describe("real-world OpenAPI specs", () => { · 300 unmodified lines | |||
| 301 | }, | 301 | }, |
| 302 | 300_000, | 302 | 300_000, |
| 303 | ); | 303 | ); |
| 304 | |||
| 305 | test( | ||
| 306 | "openai: assistants cancel run keeps path parameter types in inventory mode", | ||
| 307 | async () => { | ||
| 308 | const openAiUrl = "https://app.stainless.com/api/spec/documented/openai/openapi.documented.yml"; | ||
| 309 | const prepared = await prepareOpenApiSpec(openAiUrl, "openai", { | ||
| 310 | includeDts: false, | ||
| 311 | profile: "inventory", | ||
| 312 | }); | ||
| 313 | |||
| 314 | const tools = buildOpenApiToolsFromPrepared( | ||
| 315 | { | ||
| 316 | type: "openapi", | ||
| 317 | name: "openai", | ||
| 318 | spec: openAiUrl, | ||
| 319 | baseUrl: prepared.servers[0] || "https://api.openai.com", | ||
| 320 | }, | ||
| 321 | prepared, | ||
| 322 | ); | ||
| 323 | |||
| 324 | const tool = tools.find((t) => t.path === "openai.assistants.cancel_run"); | ||
| 325 | expect(tool).toBeDefined(); | ||
| 326 | |||
| 327 | const inputHint = tool!.typing?.inputHint ?? ""; | ||
| 328 | expect(inputHint).toContain("thread_id: string"); | ||
| 329 | expect(inputHint).toContain("run_id: string"); | ||
| 330 | expect(inputHint.includes("unknown")).toBe(false); | ||
| 331 | }, | ||
| 332 | 300_000, | ||
| 333 | ); | ||
| 304 | }); | 334 | }); |
| @@ -412,6 +412,61 @@ test("discover depth 0 keeps full signatures", async () => { · 411 unmodified lines | |||
| 412 | ); | 412 | ); |
| 413 | }); | 413 | }); |
| 414 | 414 | ||
| 415 | test("discover prefers schema over lossy typed hints", async () => { | ||
| 416 | const tool = createDiscoverTool([ | ||
| 417 | { | ||
| 418 | path: "openai.batch.create_batch", | ||
| 419 | description: "Create batch", | ||
| 420 | approval: "required", | ||
| 421 | source: "openapi:openai", | ||
| 422 | typing: { | ||
| 423 | inputHint: "{ input_file_id: ...; endpoint: ... }", | ||
| 424 | outputHint: "{ id: string; errors?: { message?:... } }", | ||
| 425 | inputSchema: { | ||
| 426 | type: "object", | ||
| 427 | properties: { | ||
| 428 | input_file_id: { type: "string" }, | ||
| 429 | endpoint: { type: "string" }, | ||
| 430 | completion_window: { type: "string" }, | ||
| 431 | }, | ||
| 432 | required: ["input_file_id", "endpoint", "completion_window"], | ||
| 433 | }, | ||
| 434 | outputSchema: { | ||
| 435 | type: "object", | ||
| 436 | properties: { | ||
| 437 | id: { type: "string" }, | ||
| 438 | errors: { | ||
| 439 | type: "object", | ||
| 440 | properties: { | ||
| 441 | message: { type: "string" }, | ||
| 442 | }, | ||
| 443 | }, | ||
| 444 | }, | ||
| 445 | required: ["id"], | ||
| 446 | }, | ||
| 447 | }, | ||
| 448 | run: async () => ({ id: "b_123" }), | ||
| 449 | } satisfies ToolDefinition, | ||
| 450 | ]); | ||
| 451 | |||
| 452 | const result = await tool.run( | ||
| 453 | { query: "create batch", depth: 2 }, | ||
| 454 | { taskId: "t", workspaceId: TEST_WORKSPACE_ID, isToolAllowed: () => true }, | ||
| 455 | ) as { | ||
| 456 | results: Array<{ | ||
| 457 | signatureInfo: { | ||
| 458 | input: string; | ||
| 459 | output: string; | ||
| 460 | }; | ||
| 461 | }>; | ||
| 462 | }; | ||
| 463 | |||
| 464 | expect(result.results[0]?.signatureInfo.input).toContain("input_file_id: string"); | ||
| 465 | expect(result.results[0]?.signatureInfo.input.includes("...")).toBe(false); | ||
| 466 | expect(result.results[0]?.signatureInfo.output).toContain("message?: string"); | ||
| 467 | expect(result.results[0]?.signatureInfo.output.includes("...")).toBe(false); | ||
| 468 | }); | ||
| 469 | |||
| 415 | test("discover returns null bestPath when there are no matches", async () => { | 470 | test("discover returns null bestPath when there are no matches", async () => { |
| 416 | const tool = createDiscoverTool([ | 471 | const tool = createDiscoverTool([ |
| 417 | { | 472 | { |
| @@ -30,7 +30,7 @@ import { · 29 unmodified lines | |||
| 30 | 30 | ||
| 31 | const OPENAPI_SPEC_CACHE_TTL_MS = 5 * 60 * 60_000; | 31 | const OPENAPI_SPEC_CACHE_TTL_MS = 5 * 60 * 60_000; |
| 32 | 32 | ||
| 33 | const OPENAPI_PREPARED_CACHE_VERSION = "openapi_v5"; | 33 | const OPENAPI_PREPARED_CACHE_VERSION = "openapi_v6"; |
| 34 | 34 | ||
| 35 | const openApiAuthModeSchema = z.enum(["static", "account", "workspace", "organization"]); | 35 | const openApiAuthModeSchema = z.enum(["static", "account", "workspace", "organization"]); |
| 36 | 36 | ||
| @@ -1504,11 +1504,6 @@ export async function rebuildWorkspaceToolInventoryForContext( · 1503 unmodified lines | |||
| 1504 | sources.filter((source) => source.enabled), | 1504 | sources.filter((source) => source.enabled), |
| 1505 | ); | 1505 | ); |
| 1506 | 1506 | ||
| 1507 | const alreadyReady = Boolean( | ||
| 1508 | state?.readyBuildId | ||
| 1509 | && !state?.buildingBuildId | ||
| 1510 | && state?.signature === registrySignature, | ||
| 1511 | ); | ||
| 1512 | const alreadyBuildingTarget = Boolean( | 1507 | const alreadyBuildingTarget = Boolean( |
| 1513 | state?.buildingBuildId | 1508 | state?.buildingBuildId |
| 1514 | && state?.buildingSignature === registrySignature, | 1509 | && state?.buildingSignature === registrySignature, |
| @@ -1523,7 +1518,7 @@ export async function rebuildWorkspaceToolInventoryForContext( · 8 unmodified lines | |||
| 1523 | && Date.now() - buildingStartedAt > REGISTRY_BUILD_STALE_MS, | 1518 | && Date.now() - buildingStartedAt > REGISTRY_BUILD_STALE_MS, |
| 1524 | ); | 1519 | ); |
| 1525 | 1520 | ||
| 1526 | if (alreadyReady || (alreadyBuildingTarget && !staleBuildingTarget)) { | 1521 | if (alreadyBuildingTarget && !staleBuildingTarget) { |
| 1527 | return { rebuilt: false }; | 1522 | return { rebuilt: false }; |
| 1528 | } | 1523 | } |
| 1529 | 1524 | ||