- 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 | 80 | "sticky bg-background/95 backdrop-blur-sm", |
| 81 | 81 | isExpanded && "border-b border-border/30", |
| 82 | 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 | 86 | : "hover:bg-accent/30", |
| 85 | 87 | )} |
| 86 | 88 | style={{ |
| @@ -50,9 +50,11 @@ const ToolRow = memo(function ToolRow({ · 49 unmodified lines | ||
| 50 | 50 | className={cn( |
| 51 | 51 | "flex items-center gap-2 px-2 py-1.5 transition-colors cursor-pointer group/tool", |
| 52 | 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 | 56 | : selected |
| 55 | − ? "bg-primary/5 ring-1 ring-primary/10" | |
| 57 | + ? "bg-accent ring-1 ring-accent/30" | |
| 56 | 58 | : "hover:bg-accent/20", |
| 57 | 59 | )} |
| 58 | 60 | style={{ |
| @@ -233,7 +233,7 @@ export async function prepareOpenApiSpec( · 232 unmodified lines | ||
| 233 | 233 | { |
| 234 | 234 | includeSchemas: profile === "full", |
| 235 | 235 | includeTypeHints: true, |
| 236 | − includeParameterSchemas: profile === "full", | |
| 236 | + includeParameterSchemas: true, | |
| 237 | 237 | resolveSchemaRefs: profile === "full", |
| 238 | 238 | }, |
| 239 | 239 | ); |
| @@ -100,6 +100,8 @@ describe("OpenAPI schema-first typing", () => { · 99 unmodified lines | ||
| 100 | 100 | expect(getTool!.typing?.outputSchema).toBeDefined(); |
| 101 | 101 | expect(getTool!.typing?.requiredInputKeys ?? []).toContain("id"); |
| 102 | 102 | expect(getTool!.typing?.previewInputKeys ?? []).toContain("include"); |
| 103 | + expect(getTool!.typing?.inputHint).toContain("id: string"); | |
| 104 | + expect(getTool!.typing?.inputHint).toContain("include?:"); | |
| 103 | 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 | 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 | 470 | test("discover returns null bestPath when there are no matches", async () => { |
| 416 | 471 | const tool = createDiscoverTool([ |
| 417 | 472 | { |
| @@ -30,7 +30,7 @@ import { · 29 unmodified lines | ||
| 30 | 30 | |
| 31 | 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 | 35 | const openApiAuthModeSchema = z.enum(["static", "account", "workspace", "organization"]); |
| 36 | 36 | |
| @@ -1504,11 +1504,6 @@ export async function rebuildWorkspaceToolInventoryForContext( · 1503 unmodified lines | ||
| 1504 | 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 | 1507 | const alreadyBuildingTarget = Boolean( |
| 1513 | 1508 | state?.buildingBuildId |
| 1514 | 1509 | && state?.buildingSignature === registrySignature, |
| @@ -1523,7 +1518,7 @@ export async function rebuildWorkspaceToolInventoryForContext( · 8 unmodified lines | ||
| 1523 | 1518 | && Date.now() - buildingStartedAt > REGISTRY_BUILD_STALE_MS, |
| 1524 | 1519 | ); |
| 1525 | 1520 | |
| 1526 | − if (alreadyReady || (alreadyBuildingTarget && !staleBuildingTarget)) { | |
| 1521 | + if (alreadyBuildingTarget && !staleBuildingTarget) { | |
| 1527 | 1522 | return { rebuilt: false }; |
| 1528 | 1523 | } |
| 1529 | 1524 | |