Unified · Split
| @@ -8,6 +8,7 @@ import { rehydrateTools, type SerializedTool } from "../../core/src/tool/source- · 7 unmodified lines | ||
| 8 | 8 | import { getDecisionForContext, getToolDecision } from "./policy"; |
| 9 | 9 | import { normalizeToolPathForLookup, toPreferredToolPath } from "./tool_paths"; |
| 10 | 10 | import { baseTools } from "./workspace_tools"; |
| 11 | +import { sourceSignature } from "./tool_source_loading"; | |
| 11 | 12 | |
| 12 | 13 | export function getGraphqlDecision( |
| 13 | 14 | task: TaskRecord, |
| @@ -69,13 +70,22 @@ async function resolveRegistryBuildId( · 55 unmodified lines | ||
| 69 | 70 | ctx: ActionCtx, |
| 70 | 71 | workspaceId: TaskRecord["workspaceId"], |
| 71 | 72 | ): Promise<string> { |
| 72 | − const state = await ctx.runQuery(internal.toolRegistry.getState, { workspaceId }) as | |
| 73 | − | null | |
| 74 | − | { readyBuildId?: string }; | |
| 73 | + const [state, sources] = await Promise.all([ | |
| 74 | + ctx.runQuery(internal.toolRegistry.getState, { workspaceId }) as Promise<null | { signature: string; readyBuildId?: string }>, | |
| 75 | + ctx.runQuery(internal.database.listToolSources, { workspaceId }) as Promise<Array<{ id: string; updatedAt: number; enabled: boolean }>>, | |
| 76 | + ]); | |
| 77 | + | |
| 78 | + const enabledSources = sources.filter((source) => source.enabled); | |
| 79 | + const signature = sourceSignature(workspaceId, enabledSources); | |
| 80 | + const expectedSignature = `toolreg_v1|${signature}`; | |
| 75 | 81 | const buildId = state?.readyBuildId; |
| 76 | − if (!buildId) { | |
| 77 | − throw new Error("Tool registry is not ready yet. Open Tools in the UI to build the registry."); | |
| 82 | + | |
| 83 | + if (!buildId || state.signature !== expectedSignature) { | |
| 84 | + throw new Error( | |
| 85 | + "Tool registry is not ready (or is stale). Open Tools to refresh, or call listToolsWithWarnings to rebuild.", | |
| 86 | + ); | |
| 78 | 87 | } |
| 88 | + | |
| 79 | 89 | return buildId; |
| 80 | 90 | } |
| 81 | 91 | |
| @@ -21,6 +21,7 @@ import { publishTaskEvent } from "./events"; · 20 unmodified lines | ||
| 21 | 21 | import { completeToolCall, denyToolCall, failToolCall } from "./tool_call_lifecycle"; |
| 22 | 22 | import { assertPersistedCallRunnable, resolveCredentialHeaders } from "./tool_call_credentials"; |
| 23 | 23 | import { getGraphqlDecision, resolveToolForCall } from "./tool_call_resolution"; |
| 24 | +import { sourceSignature } from "./tool_source_loading"; | |
| 24 | 25 | |
| 25 | 26 | function createApprovalId(): string { |
| 26 | 27 | return `approval_${crypto.randomUUID()}`; |
| @@ -61,12 +62,18 @@ export async function invokeTool(ctx: ActionCtx, task: TaskRecord, call: ToolCal · 34 unmodified lines | ||
| 61 | 62 | // Fast system tools are handled server-side from the registry. |
| 62 | 63 | if (toolPath === "discover" || toolPath === "catalog.namespaces" || toolPath === "catalog.tools") { |
| 63 | 64 | const toolRegistry = internal.toolRegistry; |
| 64 | − const state = await ctx.runQuery(toolRegistry.getState, { workspaceId: task.workspaceId }) as | |
| 65 | − | null | |
| 66 | − | { readyBuildId?: string }; | |
| 65 | + const [state, sources] = await Promise.all([ | |
| 66 | + ctx.runQuery(toolRegistry.getState, { workspaceId: task.workspaceId }) as Promise<null | { signature: string; readyBuildId?: string }>, | |
| 67 | + ctx.runQuery(internal.database.listToolSources, { workspaceId: task.workspaceId }) as Promise<Array<{ id: string; updatedAt: number; enabled: boolean }>>, | |
| 68 | + ]); | |
| 69 | + const enabledSources = sources.filter((source) => source.enabled); | |
| 70 | + const signature = sourceSignature(task.workspaceId, enabledSources); | |
| 71 | + const expectedSignature = `toolreg_v1|${signature}`; | |
| 67 | 72 | const buildId = state?.readyBuildId; |
| 68 | − if (!buildId) { | |
| 69 | − throw new Error("Tool registry is not ready yet. Open Tools in the UI to build the registry."); | |
| 73 | + if (!buildId || state.signature !== expectedSignature) { | |
| 74 | + throw new Error( | |
| 75 | + "Tool registry is not ready (or is stale). Open Tools to refresh, or call listToolsWithWarnings to rebuild.", | |
| 76 | + ); | |
| 70 | 77 | } |
| 71 | 78 | |
| 72 | 79 | const payload = input && typeof input === "object" ? (input as Record<string, unknown>) : {}; |
| @@ -12,7 +12,7 @@ import { · 11 unmodified lines | ||
| 12 | 12 | type CompiledToolSourceArtifact, |
| 13 | 13 | type WorkspaceToolSnapshot, |
| 14 | 14 | } from "../../core/src/tool-sources"; |
| 15 | −import { serializeTools } from "../../core/src/tool/source-serialization"; | |
| 15 | +import type { SerializedTool } from "../../core/src/tool/source-serialization"; | |
| 16 | 16 | import type { ExternalToolSourceConfig } from "../../core/src/tool/source-types"; |
| 17 | 17 | import type { |
| 18 | 18 | AccessPolicyRecord, |
| @@ -370,20 +370,18 @@ async function buildWorkspaceToolRegistry( · 351 unmodified lines | ||
| 370 | 370 | ctx: ActionCtx, |
| 371 | 371 | args: { |
| 372 | 372 | workspaceId: Id<"workspaces">; |
| 373 | − signature: string; | |
| 374 | − tools: ToolDefinition[]; | |
| 373 | + registrySignature: string; | |
| 374 | + serializedTools: SerializedTool[]; | |
| 375 | 375 | }, |
| 376 | 376 | ): Promise<{ buildId: string }> { |
| 377 | 377 | const buildId = `toolreg_${crypto.randomUUID()}`; |
| 378 | 378 | await ctx.runMutation(internal.toolRegistry.beginBuild, { |
| 379 | 379 | workspaceId: args.workspaceId, |
| 380 | − signature: args.signature, | |
| 380 | + signature: args.registrySignature, | |
| 381 | 381 | buildId, |
| 382 | 382 | }); |
| 383 | 383 | |
| 384 | − const serialized = serializeTools(args.tools); | |
| 385 | − | |
| 386 | − const entries = serialized.map((st) => { | |
| 384 | + const entries = args.serializedTools.map((st) => { | |
| 387 | 385 | if (st.path === "discover" || st.path.startsWith("catalog.")) { |
| 388 | 386 | return null; |
| 389 | 387 | } |
| @@ -487,6 +485,25 @@ async function buildWorkspaceToolRegistry( · 97 unmodified lines | ||
| 487 | 485 | return { buildId }; |
| 488 | 486 | } |
| 489 | 487 | |
| 488 | +async function maybeBuildWorkspaceToolRegistry( | |
| 489 | + ctx: ActionCtx, | |
| 490 | + args: { | |
| 491 | + workspaceId: Id<"workspaces">; | |
| 492 | + registrySignature: string; | |
| 493 | + serializedTools: SerializedTool[]; | |
| 494 | + }, | |
| 495 | +): Promise<void> { | |
| 496 | + const state = await ctx.runQuery(internal.toolRegistry.getState, { workspaceId: args.workspaceId }) as | |
| 497 | + | null | |
| 498 | + | { signature: string; readyBuildId?: string }; | |
| 499 | + | |
| 500 | + if (state?.readyBuildId && state.signature === args.registrySignature) { | |
| 501 | + return; | |
| 502 | + } | |
| 503 | + | |
| 504 | + await buildWorkspaceToolRegistry(ctx, args); | |
| 505 | +} | |
| 506 | + | |
| 490 | 507 | export async function getWorkspaceTools( |
| 491 | 508 | ctx: ActionCtx, |
| 492 | 509 | workspaceId: Id<"workspaces">, |
| @@ -517,6 +534,7 @@ export async function getWorkspaceTools( · 24 unmodified lines | ||
| 517 | 534 | traceStep("listToolSources", listSourcesStartedAt); |
| 518 | 535 | const hasOpenApiSource = sources.some((source: { type: string }) => source.type === "openapi"); |
| 519 | 536 | const signature = sourceSignature(workspaceId, sources); |
| 537 | + const registrySignature = `toolreg_v1|${signature}`; | |
| 520 | 538 | const debugBase: Omit<WorkspaceToolsDebug, "mode" | "normalizedSourceCount" | "cacheHit" | "cacheFresh" | "timedOutSources" | "durationMs" | "trace"> = { |
| 521 | 539 | includeDts, |
| 522 | 540 | sourceTimeoutMs: sourceTimeoutMs ?? null, |
| @@ -545,6 +563,17 @@ export async function getWorkspaceTools( · 22 unmodified lines | ||
| 545 | 563 | const typesStorageId = cacheEntry.typesStorageId as Id<"_storage"> | undefined; |
| 546 | 564 | if (cacheEntry.isFresh) { |
| 547 | 565 | if (typesStorageId) { |
| 566 | + // Registry is derived from the cached snapshot. Keep it in sync. | |
| 567 | + try { | |
| 568 | + await maybeBuildWorkspaceToolRegistry(ctx, { | |
| 569 | + workspaceId, | |
| 570 | + registrySignature, | |
| 571 | + serializedTools: snapshot.externalArtifacts.flatMap((artifact) => artifact.tools), | |
| 572 | + }); | |
| 573 | + } catch (error) { | |
| 574 | + const msg = error instanceof Error ? error.message : String(error); | |
| 575 | + console.warn(`[executor] workspace tool registry build failed for '${workspaceId}': ${msg}`); | |
| 576 | + } | |
| 548 | 577 | return { |
| 549 | 578 | tools: merged, |
| 550 | 579 | warnings: snapshot.warnings, |
| @@ -677,10 +706,10 @@ export async function getWorkspaceTools( · 126 unmodified lines | ||
| 677 | 706 | |
| 678 | 707 | // Build a per-tool registry for fast discover + invocation. |
| 679 | 708 | const registryStartedAt = Date.now(); |
| 680 | − await buildWorkspaceToolRegistry(ctx, { | |
| 709 | + await maybeBuildWorkspaceToolRegistry(ctx, { | |
| 681 | 710 | workspaceId, |
| 682 | − signature, | |
| 683 | − tools: allTools, | |
| 711 | + registrySignature, | |
| 712 | + serializedTools: externalArtifacts.flatMap((artifact) => artifact.tools), | |
| 684 | 713 | }); |
| 685 | 714 | traceStep("toolRegistryWrite", registryStartedAt); |
| 686 | 715 | |