History

Build tool registry from serialized artifacts

Rhys Sullivan committed 8 months ago

Commit 5ecb1f61 parent b096a823 files · +66 −20Browse files

Unified · Split

3 files
  1. executor/packages/convex/runtime/tool_call_resolution.ts +15 −5
  2. executor/packages/convex/runtime/tool_invocation.ts +12 −5
  3. executor/packages/convex/runtime/workspace_tools.ts +39 −10
executor/packages/convex/runtime/tool_call_resolution.ts+15−5
@@ -8,6 +8,7 @@ import { rehydrateTools, type SerializedTool } from "../../core/src/tool/source- · 7 unmodified lines
8import { getDecisionForContext, getToolDecision } from "./policy";8import { getDecisionForContext, getToolDecision } from "./policy";
9import { normalizeToolPathForLookup, toPreferredToolPath } from "./tool_paths";9import { normalizeToolPathForLookup, toPreferredToolPath } from "./tool_paths";
10import { baseTools } from "./workspace_tools";10import { baseTools } from "./workspace_tools";
11import { sourceSignature } from "./tool_source_loading";
11 12
12export function getGraphqlDecision(13export function getGraphqlDecision(
13 task: TaskRecord,14 task: TaskRecord,
@@ -69,13 +70,22 @@ async function resolveRegistryBuildId( · 55 unmodified lines
69 ctx: ActionCtx,70 ctx: ActionCtx,
70 workspaceId: TaskRecord["workspaceId"],71 workspaceId: TaskRecord["workspaceId"],
71): Promise<string> {72): Promise<string> {
72 const state = await ctx.runQuery(internal.toolRegistry.getState, { workspaceId }) as73 const [state, sources] = await Promise.all([
73 | null74 ctx.runQuery(internal.toolRegistry.getState, { workspaceId }) as Promise<null | { signature: string; readyBuildId?: string }>,
74 | { 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 const buildId = state?.readyBuildId;81 const buildId = state?.readyBuildId;
76 if (!buildId) {82
77 throw new Error("Tool registry is not ready yet. Open Tools in the UI to build the registry.");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 return buildId;89 return buildId;
80}90}
81 91
executor/packages/convex/runtime/tool_invocation.ts+12−5
@@ -21,6 +21,7 @@ import { publishTaskEvent } from "./events"; · 20 unmodified lines
21import { completeToolCall, denyToolCall, failToolCall } from "./tool_call_lifecycle";21import { completeToolCall, denyToolCall, failToolCall } from "./tool_call_lifecycle";
22import { assertPersistedCallRunnable, resolveCredentialHeaders } from "./tool_call_credentials";22import { assertPersistedCallRunnable, resolveCredentialHeaders } from "./tool_call_credentials";
23import { getGraphqlDecision, resolveToolForCall } from "./tool_call_resolution";23import { getGraphqlDecision, resolveToolForCall } from "./tool_call_resolution";
24import { sourceSignature } from "./tool_source_loading";
24 25
25function createApprovalId(): string {26function createApprovalId(): string {
26 return `approval_${crypto.randomUUID()}`;27 return `approval_${crypto.randomUUID()}`;
@@ -61,12 +62,18 @@ export async function invokeTool(ctx: ActionCtx, task: TaskRecord, call: ToolCal · 34 unmodified lines
61 // Fast system tools are handled server-side from the registry.62 // Fast system tools are handled server-side from the registry.
62 if (toolPath === "discover" || toolPath === "catalog.namespaces" || toolPath === "catalog.tools") {63 if (toolPath === "discover" || toolPath === "catalog.namespaces" || toolPath === "catalog.tools") {
63 const toolRegistry = internal.toolRegistry;64 const toolRegistry = internal.toolRegistry;
64 const state = await ctx.runQuery(toolRegistry.getState, { workspaceId: task.workspaceId }) as65 const [state, sources] = await Promise.all([
65 | null66 ctx.runQuery(toolRegistry.getState, { workspaceId: task.workspaceId }) as Promise<null | { signature: string; readyBuildId?: string }>,
66 | { 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 const buildId = state?.readyBuildId;72 const buildId = state?.readyBuildId;
68 if (!buildId) {73 if (!buildId || state.signature !== expectedSignature) {
69 throw new Error("Tool registry is not ready yet. Open Tools in the UI to build the registry.");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 const payload = input && typeof input === "object" ? (input as Record<string, unknown>) : {};79 const payload = input && typeof input === "object" ? (input as Record<string, unknown>) : {};
executor/packages/convex/runtime/workspace_tools.ts+39−10
@@ -12,7 +12,7 @@ import { · 11 unmodified lines
12 type CompiledToolSourceArtifact,12 type CompiledToolSourceArtifact,
13 type WorkspaceToolSnapshot,13 type WorkspaceToolSnapshot,
14} from "../../core/src/tool-sources";14} from "../../core/src/tool-sources";
15import { serializeTools } from "../../core/src/tool/source-serialization";15import type { SerializedTool } from "../../core/src/tool/source-serialization";
16import type { ExternalToolSourceConfig } from "../../core/src/tool/source-types";16import type { ExternalToolSourceConfig } from "../../core/src/tool/source-types";
17import type {17import type {
18 AccessPolicyRecord,18 AccessPolicyRecord,
@@ -370,20 +370,18 @@ async function buildWorkspaceToolRegistry( · 351 unmodified lines
370 ctx: ActionCtx,370 ctx: ActionCtx,
371 args: {371 args: {
372 workspaceId: Id<"workspaces">;372 workspaceId: Id<"workspaces">;
373 signature: string;373 registrySignature: string;
374 tools: ToolDefinition[];374 serializedTools: SerializedTool[];
375 },375 },
376): Promise<{ buildId: string }> {376): Promise<{ buildId: string }> {
377 const buildId = `toolreg_${crypto.randomUUID()}`;377 const buildId = `toolreg_${crypto.randomUUID()}`;
378 await ctx.runMutation(internal.toolRegistry.beginBuild, {378 await ctx.runMutation(internal.toolRegistry.beginBuild, {
379 workspaceId: args.workspaceId,379 workspaceId: args.workspaceId,
380 signature: args.signature,380 signature: args.registrySignature,
381 buildId,381 buildId,
382 });382 });
383 383
384 const serialized = serializeTools(args.tools);384 const entries = args.serializedTools.map((st) => {
385
386 const entries = serialized.map((st) => {
387 if (st.path === "discover" || st.path.startsWith("catalog.")) {385 if (st.path === "discover" || st.path.startsWith("catalog.")) {
388 return null;386 return null;
389 }387 }
@@ -487,6 +485,25 @@ async function buildWorkspaceToolRegistry( · 97 unmodified lines
487 return { buildId };485 return { buildId };
488}486}
489 487
488async 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
490export async function getWorkspaceTools(507export async function getWorkspaceTools(
491 ctx: ActionCtx,508 ctx: ActionCtx,
492 workspaceId: Id<"workspaces">,509 workspaceId: Id<"workspaces">,
@@ -517,6 +534,7 @@ export async function getWorkspaceTools( · 24 unmodified lines
517 traceStep("listToolSources", listSourcesStartedAt);534 traceStep("listToolSources", listSourcesStartedAt);
518 const hasOpenApiSource = sources.some((source: { type: string }) => source.type === "openapi");535 const hasOpenApiSource = sources.some((source: { type: string }) => source.type === "openapi");
519 const signature = sourceSignature(workspaceId, sources);536 const signature = sourceSignature(workspaceId, sources);
537 const registrySignature = `toolreg_v1|${signature}`;
520 const debugBase: Omit<WorkspaceToolsDebug, "mode" | "normalizedSourceCount" | "cacheHit" | "cacheFresh" | "timedOutSources" | "durationMs" | "trace"> = {538 const debugBase: Omit<WorkspaceToolsDebug, "mode" | "normalizedSourceCount" | "cacheHit" | "cacheFresh" | "timedOutSources" | "durationMs" | "trace"> = {
521 includeDts,539 includeDts,
522 sourceTimeoutMs: sourceTimeoutMs ?? null,540 sourceTimeoutMs: sourceTimeoutMs ?? null,
@@ -545,6 +563,17 @@ export async function getWorkspaceTools( · 22 unmodified lines
545 const typesStorageId = cacheEntry.typesStorageId as Id<"_storage"> | undefined;563 const typesStorageId = cacheEntry.typesStorageId as Id<"_storage"> | undefined;
546 if (cacheEntry.isFresh) {564 if (cacheEntry.isFresh) {
547 if (typesStorageId) {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 return {577 return {
549 tools: merged,578 tools: merged,
550 warnings: snapshot.warnings,579 warnings: snapshot.warnings,
@@ -677,10 +706,10 @@ export async function getWorkspaceTools( · 126 unmodified lines
677 706
678 // Build a per-tool registry for fast discover + invocation.707 // Build a per-tool registry for fast discover + invocation.
679 const registryStartedAt = Date.now();708 const registryStartedAt = Date.now();
680 await buildWorkspaceToolRegistry(ctx, {709 await maybeBuildWorkspaceToolRegistry(ctx, {
681 workspaceId,710 workspaceId,
682 signature,711 registrySignature,
683 tools: allTools,712 serializedTools: externalArtifacts.flatMap((artifact) => artifact.tools),
684 });713 });
685 traceStep("toolRegistryWrite", registryStartedAt);714 traceStep("toolRegistryWrite", registryStartedAt);
686 715