Unified · Split
| @@ -25,7 +25,8 @@ export const toSourceStoreError = ( · 24 unmodified lines | ||
| 25 | 25 | ): SourceStoreError => |
| 26 | 26 | new SourceStoreError({ |
| 27 | 27 | operation, |
| 28 | − filePath: "convex://control-plane", | |
| 28 | + backend: "convex", | |
| 29 | + location: "control-plane", | |
| 29 | 30 | message: "Convex control plane call failed", |
| 30 | 31 | reason: null, |
| 31 | 32 | details: renderCause(cause), |
| @@ -12,6 +12,7 @@ const setup = () => · 11 unmodified lines | ||
| 12 | 12 | "./mcp.ts": () => import("./mcp"), |
| 13 | 13 | "./executor.ts": () => import("./executor"), |
| 14 | 14 | "./runtimeCallbacks.ts": () => import("./runtimeCallbacks"), |
| 15 | + "./source_tool_registry.ts": () => import("./source_tool_registry"), | |
| 15 | 16 | "./controlPlane.ts": () => import("./controlPlane"), |
| 16 | 17 | "./_generated/api.js": () => import("./_generated/api.js"), |
| 17 | 18 | }); |
| @@ -1,11 +1,25 @@ | ||
| 1 | −import { createRunExecutor } from "@executor-v2/engine"; | |
| 1 | +import { | |
| 2 | + createRunExecutor, | |
| 3 | + type ToolRegistry, | |
| 4 | +} from "@executor-v2/engine"; | |
| 2 | 5 | import type { ExecuteRunInput, ExecuteRunResult } from "@executor-v2/sdk"; |
| 3 | 6 | import * as Effect from "effect/Effect"; |
| 4 | 7 | |
| 5 | −import { executeRuntimeRunInConvex } from "./runtime_execution_port"; | |
| 8 | +import { createExecuteRuntimeRunInConvex } from "./runtime_execution_port"; | |
| 6 | 9 | |
| 7 | −const runExecutor = createRunExecutor(executeRuntimeRunInConvex); | |
| 10 | +export type ExecuteRunImplOptions = { | |
| 11 | + toolRegistry?: ToolRegistry; | |
| 12 | +}; | |
| 8 | 13 | |
| 9 | 14 | export const executeRunImpl = ( |
| 10 | 15 | input: ExecuteRunInput, |
| 11 | −): Effect.Effect<ExecuteRunResult> => runExecutor.executeRun(input); | |
| 16 | + options: ExecuteRunImplOptions = {}, | |
| 17 | +): Effect.Effect<ExecuteRunResult> => { | |
| 18 | + const runExecutor = createRunExecutor( | |
| 19 | + createExecuteRuntimeRunInConvex({ | |
| 20 | + toolRegistry: options.toolRegistry, | |
| 21 | + }), | |
| 22 | + ); | |
| 23 | + | |
| 24 | + return runExecutor.executeRun(input); | |
| 25 | +}; | |
| @@ -4,10 +4,24 @@ import * as Effect from "effect/Effect"; · 3 unmodified lines | ||
| 4 | 4 | |
| 5 | 5 | import { httpAction } from "./_generated/server"; |
| 6 | 6 | import { executeRunImpl } from "./executor"; |
| 7 | +import { createConvexSourceToolRegistry } from "./source_tool_registry"; | |
| 8 | + | |
| 9 | +const readConfiguredWorkspaceId = (value: string | undefined): string => { | |
| 10 | + const normalized = value?.trim(); | |
| 11 | + return normalized && normalized.length > 0 ? normalized : "ws_local"; | |
| 12 | +}; | |
| 13 | + | |
| 14 | +const workspaceId = readConfiguredWorkspaceId(process.env.CONVEX_WORKSPACE_ID); | |
| 7 | 15 | |
| 8 | 16 | export const mcpHandler = httpAction(async (ctx, request) => { |
| 17 | + const toolRegistry = createConvexSourceToolRegistry(ctx, workspaceId); | |
| 18 | + | |
| 9 | 19 | const runClient = createExecutorRunClient((input) => |
| 10 | − Effect.runPromise(executeRunImpl(input)), | |
| 20 | + Effect.runPromise( | |
| 21 | + executeRunImpl(input, { | |
| 22 | + toolRegistry, | |
| 23 | + }), | |
| 24 | + ), | |
| 11 | 25 | ); |
| 12 | 26 | |
| 13 | 27 | return handleMcpHttpRequest(request, { |
| @@ -1,15 +1,18 @@ | ||
| 1 | 1 | import { |
| 2 | − createRuntimeToolCallHandler, | |
| 3 | − createUnimplementedRuntimeToolInvoker, | |
| 2 | + RuntimeAdapterError, | |
| 3 | + createRuntimeToolCallService, | |
| 4 | 4 | } from "@executor-v2/engine"; |
| 5 | −import type { RuntimeToolCallResult } from "@executor-v2/sdk"; | |
| 5 | +import type { | |
| 6 | + RuntimeToolCallRequest, | |
| 7 | + RuntimeToolCallResult, | |
| 8 | +} from "@executor-v2/sdk"; | |
| 6 | 9 | import * as Data from "effect/Data"; |
| 7 | 10 | import * as Effect from "effect/Effect"; |
| 8 | 11 | import * as ParseResult from "effect/ParseResult"; |
| 9 | 12 | import * as Schema from "effect/Schema"; |
| 10 | 13 | |
| 11 | 14 | import { httpAction, type ActionCtx } from "./_generated/server"; |
| 12 | −import { createConvexResolveToolCredentials } from "./credential_resolver"; | |
| 15 | +import { createConvexSourceToolRegistry } from "./source_tool_registry"; | |
| 13 | 16 | |
| 14 | 17 | class RuntimeToolCallBadRequestError extends Data.TaggedError( |
| 15 | 18 | "RuntimeToolCallBadRequestError", |
| @@ -40,6 +43,13 @@ const RuntimeToolCallRequestSchema = Schema.Struct({ · 24 unmodified lines | ||
| 40 | 43 | |
| 41 | 44 | const decodeRuntimeToolCallRequest = Schema.decodeUnknown(RuntimeToolCallRequestSchema); |
| 42 | 45 | |
| 46 | +const readConfiguredWorkspaceId = (value: string | undefined): string => { | |
| 47 | + const normalized = value?.trim(); | |
| 48 | + return normalized && normalized.length > 0 ? normalized : "ws_local"; | |
| 49 | +}; | |
| 50 | + | |
| 51 | +const fallbackWorkspaceId = readConfiguredWorkspaceId(process.env.CONVEX_WORKSPACE_ID); | |
| 52 | + | |
| 43 | 53 | const badRequest = (message: string): Response => |
| 44 | 54 | Response.json( |
| 45 | 55 | { |
| @@ -53,20 +63,13 @@ const badRequest = (message: string): Response => · 7 unmodified lines | ||
| 53 | 63 | const formatBadRequestMessage = (error: RuntimeToolCallBadRequestError): string => |
| 54 | 64 | error.details.length > 0 ? `${error.message}: ${error.details}` : error.message; |
| 55 | 65 | |
| 56 | −const formatUnknownDetails = (cause: unknown): string => String(cause); | |
| 66 | +const formatRuntimeAdapterError = (error: RuntimeAdapterError): string => | |
| 67 | + error.details ? `${error.message}: ${error.details}` : error.message; | |
| 57 | 68 | |
| 58 | −const handleToolCallHttpEffect = ( | |
| 59 | − ctx: ActionCtx, | |
| 60 | − request: Request, | |
| 61 | −): Effect.Effect<Response, never> => { | |
| 62 | − const resolveCredentials = createConvexResolveToolCredentials(ctx); | |
| 63 | − const invokeRuntimeTool = createUnimplementedRuntimeToolInvoker("convex"); | |
| 64 | − const handleToolCall = createRuntimeToolCallHandler({ | |
| 65 | − resolveCredentials, | |
| 66 | − invokeRuntimeTool, | |
| 67 | − }); | |
| 69 | +const formatUnknownDetails = (cause: unknown): string => String(cause); | |
| 68 | 70 | |
| 69 | − return Effect.gen(function* () { | |
| 71 | +const decodeToolCallRequest = (request: Request): Effect.Effect<RuntimeToolCallRequest, RuntimeToolCallBadRequestError> => | |
| 72 | + Effect.gen(function* () { | |
| 70 | 73 | const body = yield* Effect.tryPromise({ |
| 71 | 74 | try: () => request.json(), |
| 72 | 75 | catch: (cause) => |
| @@ -76,7 +79,7 @@ const handleToolCallHttpEffect = ( · 3 unmodified lines | ||
| 76 | 79 | }), |
| 77 | 80 | }); |
| 78 | 81 | |
| 79 | − const input = yield* decodeRuntimeToolCallRequest(body).pipe( | |
| 82 | + return yield* decodeRuntimeToolCallRequest(body).pipe( | |
| 80 | 83 | Effect.mapError( |
| 81 | 84 | (cause) => |
| 82 | 85 | new RuntimeToolCallBadRequestError({ |
| @@ -85,15 +88,43 @@ const handleToolCallHttpEffect = ( · 2 unmodified lines | ||
| 85 | 88 | }), |
| 86 | 89 | ), |
| 87 | 90 | ); |
| 91 | + }); | |
| 92 | + | |
| 93 | +const handleToolCallHttpEffect = ( | |
| 94 | + ctx: ActionCtx, | |
| 95 | + request: Request, | |
| 96 | +): Effect.Effect<Response, never> => | |
| 97 | + Effect.gen(function* () { | |
| 98 | + const input = yield* decodeToolCallRequest(request); | |
| 99 | + | |
| 100 | + const workspaceId = | |
| 101 | + input.credentialContext?.workspaceId?.trim() || fallbackWorkspaceId; | |
| 102 | + | |
| 103 | + const toolRegistry = createConvexSourceToolRegistry(ctx, workspaceId); | |
| 104 | + const runtimeToolCallService = createRuntimeToolCallService(toolRegistry); | |
| 105 | + | |
| 106 | + const result = yield* runtimeToolCallService.callTool(input).pipe( | |
| 107 | + Effect.map( | |
| 108 | + (value): RuntimeToolCallResult => ({ | |
| 109 | + ok: true, | |
| 110 | + value, | |
| 111 | + }), | |
| 112 | + ), | |
| 113 | + Effect.catchTag("RuntimeAdapterError", (error) => | |
| 114 | + Effect.succeed<RuntimeToolCallResult>({ | |
| 115 | + ok: false, | |
| 116 | + kind: "failed", | |
| 117 | + error: formatRuntimeAdapterError(error), | |
| 118 | + }), | |
| 119 | + ), | |
| 120 | + ); | |
| 88 | 121 | |
| 89 | − const result = yield* handleToolCall(input); | |
| 90 | 122 | return Response.json(result, { status: 200 }); |
| 91 | 123 | }).pipe( |
| 92 | 124 | Effect.catchTag("RuntimeToolCallBadRequestError", (error) => |
| 93 | 125 | Effect.succeed(badRequest(formatBadRequestMessage(error))), |
| 94 | 126 | ), |
| 95 | 127 | ); |
| 96 | −}; | |
| 97 | 128 | |
| 98 | 129 | export const handleToolCallHttp = httpAction((ctx, request) => |
| 99 | 130 | Effect.runPromise(handleToolCallHttpEffect(ctx, request)), |
| @@ -1,8 +1,10 @@ | ||
| 1 | 1 | import { |
| 2 | − RuntimeAdapterError, | |
| 3 | 2 | RuntimeExecutionPortError, |
| 3 | + createRuntimeToolCallService, | |
| 4 | + createStaticToolRegistry, | |
| 4 | 5 | type ExecuteRuntimeRun, |
| 5 | 6 | type ExecuteRuntimeRunInput, |
| 7 | + type ToolRegistry, | |
| 6 | 8 | makeRuntimeAdapterRegistry, |
| 7 | 9 | type RuntimeExecuteError, |
| 8 | 10 | } from "@executor-v2/engine"; |
| @@ -13,56 +15,60 @@ const convexRuntimeAdapter = makeLocalInProcessRuntimeAdapter(); · 4 unmodified lines | ||
| 13 | 15 | |
| 14 | 16 | const runtimeAdapters = makeRuntimeAdapterRegistry([convexRuntimeAdapter]); |
| 15 | 17 | |
| 16 | −const missingToolCallService = (runId: string, toolPath: string): RuntimeAdapterError => | |
| 17 | − new RuntimeAdapterError({ | |
| 18 | − operation: "call_tool", | |
| 19 | − runtimeKind: convexRuntimeAdapter.kind, | |
| 20 | − message: `No runtime tool-call service configured in convex for ${toolPath}`, | |
| 21 | − details: `runId=${runId}`, | |
| 22 | − }); | |
| 18 | +export type ConvexRuntimeExecutionPortOptions = { | |
| 19 | + toolRegistry?: ToolRegistry; | |
| 20 | +}; | |
| 23 | 21 | |
| 24 | −export const executeRuntimeRunInConvex: ExecuteRuntimeRun = ( | |
| 25 | − input: ExecuteRuntimeRunInput, | |
| 26 | −) => | |
| 27 | − Effect.gen(function* () { | |
| 28 | − const runtimeAdapter = yield* runtimeAdapters.get(convexRuntimeAdapter.kind).pipe( | |
| 29 | − Effect.mapError( | |
| 30 | − (error) => | |
| 31 | − new RuntimeExecutionPortError({ | |
| 32 | − operation: "resolve_runtime_adapter", | |
| 33 | − message: error.message, | |
| 34 | − details: null, | |
| 35 | − }), | |
| 36 | − ), | |
| 37 | − ); | |
| 22 | +export const createExecuteRuntimeRunInConvex = ( | |
| 23 | + options: ConvexRuntimeExecutionPortOptions = {}, | |
| 24 | +): ExecuteRuntimeRun => { | |
| 25 | + const toolRegistry = | |
| 26 | + options.toolRegistry ?? | |
| 27 | + createStaticToolRegistry({ | |
| 28 | + tools: {}, | |
| 29 | + }); | |
| 30 | + const toolCallService = createRuntimeToolCallService(toolRegistry); | |
| 38 | 31 | |
| 39 | − const isAvailable = yield* runtimeAdapter.isAvailable(); | |
| 40 | − if (!isAvailable) { | |
| 41 | − return yield* new RuntimeExecutionPortError({ | |
| 42 | − operation: "runtime_available", | |
| 43 | − message: `Runtime '${convexRuntimeAdapter.kind}' is not available in this convex process.`, | |
| 44 | − details: null, | |
| 45 | − }); | |
| 46 | − } | |
| 47 | − | |
| 48 | − return yield* runtimeAdapter | |
| 49 | − .execute({ | |
| 50 | − runId: input.runId, | |
| 51 | − code: input.code, | |
| 52 | − timeoutMs: input.timeoutMs, | |
| 53 | − toolCallService: { | |
| 54 | − callTool: (toolInput) => | |
| 55 | − Effect.fail(missingToolCallService(toolInput.runId, toolInput.toolPath)), | |
| 56 | − }, | |
| 57 | − }) | |
| 58 | − .pipe( | |
| 32 | + return (input: ExecuteRuntimeRunInput) => | |
| 33 | + Effect.gen(function* () { | |
| 34 | + const runtimeAdapter = yield* runtimeAdapters.get(convexRuntimeAdapter.kind).pipe( | |
| 59 | 35 | Effect.mapError( |
| 60 | − (error: RuntimeExecuteError) => | |
| 36 | + (error) => | |
| 61 | 37 | new RuntimeExecutionPortError({ |
| 62 | − operation: "runtime_execute", | |
| 38 | + operation: "resolve_runtime_adapter", | |
| 63 | 39 | message: error.message, |
| 64 | − details: error.details, | |
| 40 | + details: null, | |
| 65 | 41 | }), |
| 66 | 42 | ), |
| 67 | 43 | ); |
| 68 | − }); | |
| 44 | + | |
| 45 | + const isAvailable = yield* runtimeAdapter.isAvailable(); | |
| 46 | + if (!isAvailable) { | |
| 47 | + return yield* new RuntimeExecutionPortError({ | |
| 48 | + operation: "runtime_available", | |
| 49 | + message: `Runtime '${convexRuntimeAdapter.kind}' is not available in this convex process.`, | |
| 50 | + details: null, | |
| 51 | + }); | |
| 52 | + } | |
| 53 | + | |
| 54 | + return yield* runtimeAdapter | |
| 55 | + .execute({ | |
| 56 | + runId: input.runId, | |
| 57 | + code: input.code, | |
| 58 | + timeoutMs: input.timeoutMs, | |
| 59 | + toolCallService, | |
| 60 | + }) | |
| 61 | + .pipe( | |
| 62 | + Effect.mapError( | |
| 63 | + (error: RuntimeExecuteError) => | |
| 64 | + new RuntimeExecutionPortError({ | |
| 65 | + operation: "runtime_execute", | |
| 66 | + message: error.message, | |
| 67 | + details: error.details, | |
| 68 | + }), | |
| 69 | + ), | |
| 70 | + ); | |
| 71 | + }); | |
| 72 | +}; | |
| 73 | + | |
| 74 | +export const executeRuntimeRunInConvex = createExecuteRuntimeRunInConvex(); | |
| @@ -0,0 +1,186 @@ | ||
| 1 | +import { | |
| 2 | + createSourceToolRegistry, | |
| 3 | + makeOpenApiToolProvider, | |
| 4 | + makeToolProviderRegistry, | |
| 5 | +} from "@executor-v2/engine"; | |
| 6 | +import { | |
| 7 | + SourceStoreError, | |
| 8 | + ToolArtifactStoreError, | |
| 9 | + type SourceStore, | |
| 10 | + type ToolArtifactStore, | |
| 11 | +} from "@executor-v2/persistence-ports"; | |
| 12 | +import { | |
| 13 | + SourceSchema, | |
| 14 | + ToolArtifactSchema, | |
| 15 | + type Source, | |
| 16 | + type SourceId, | |
| 17 | + type ToolArtifact, | |
| 18 | + type WorkspaceId, | |
| 19 | +} from "@executor-v2/schema"; | |
| 20 | +import { v } from "convex/values"; | |
| 21 | +import * as Effect from "effect/Effect"; | |
| 22 | +import * as Option from "effect/Option"; | |
| 23 | +import * as Schema from "effect/Schema"; | |
| 24 | + | |
| 25 | +import { api } from "./_generated/api"; | |
| 26 | +import { query, type ActionCtx } from "./_generated/server"; | |
| 27 | + | |
| 28 | +const runtimeApi = api as any; | |
| 29 | + | |
| 30 | +const decodeSource = Schema.decodeUnknownSync(SourceSchema); | |
| 31 | +const decodeToolArtifact = Schema.decodeUnknownSync(ToolArtifactSchema); | |
| 32 | + | |
| 33 | +const stripConvexSystemFields = ( | |
| 34 | + value: Record<string, unknown>, | |
| 35 | +): Record<string, unknown> => { | |
| 36 | + const { _id: _ignoredId, _creationTime: _ignoredCreationTime, ...rest } = value; | |
| 37 | + return rest; | |
| 38 | +}; | |
| 39 | + | |
| 40 | +const unsupportedSourceStoreMutation = ( | |
| 41 | + operation: "upsert" | "removeById", | |
| 42 | +): SourceStoreError => | |
| 43 | + new SourceStoreError({ | |
| 44 | + operation, | |
| 45 | + backend: "convex", | |
| 46 | + location: "source-tool-registry", | |
| 47 | + message: `SourceStore.${operation} is not supported in source tool registry runtime`, | |
| 48 | + reason: "unsupported_operation", | |
| 49 | + details: null, | |
| 50 | + }); | |
| 51 | + | |
| 52 | +const unsupportedToolArtifactMutation = (): ToolArtifactStoreError => | |
| 53 | + new ToolArtifactStoreError({ | |
| 54 | + operation: "upsert", | |
| 55 | + backend: "convex", | |
| 56 | + location: "source-tool-registry", | |
| 57 | + message: "ToolArtifactStore.upsert is not supported in source tool registry runtime", | |
| 58 | + reason: "unsupported_operation", | |
| 59 | + details: null, | |
| 60 | + }); | |
| 61 | + | |
| 62 | +const sourceStoreQueryError = ( | |
| 63 | + operation: string, | |
| 64 | + cause: unknown, | |
| 65 | +): SourceStoreError => | |
| 66 | + new SourceStoreError({ | |
| 67 | + operation, | |
| 68 | + backend: "convex", | |
| 69 | + location: "source-tool-registry", | |
| 70 | + message: "SourceStore query failed", | |
| 71 | + reason: "convex_query_error", | |
| 72 | + details: String(cause), | |
| 73 | + }); | |
| 74 | + | |
| 75 | +const toolArtifactStoreQueryError = ( | |
| 76 | + operation: string, | |
| 77 | + cause: unknown, | |
| 78 | +): ToolArtifactStoreError => | |
| 79 | + new ToolArtifactStoreError({ | |
| 80 | + operation, | |
| 81 | + backend: "convex", | |
| 82 | + location: "source-tool-registry", | |
| 83 | + message: "ToolArtifactStore query failed", | |
| 84 | + reason: "convex_query_error", | |
| 85 | + details: String(cause), | |
| 86 | + }); | |
| 87 | + | |
| 88 | +export const listSourcesForWorkspace = query({ | |
| 89 | + args: { | |
| 90 | + workspaceId: v.string(), | |
| 91 | + }, | |
| 92 | + handler: async (ctx, args): Promise<Array<Source>> => { | |
| 93 | + const rows = await ctx.db | |
| 94 | + .query("sources") | |
| 95 | + .withIndex("by_workspaceId", (q) => q.eq("workspaceId", args.workspaceId)) | |
| 96 | + .collect(); | |
| 97 | + | |
| 98 | + return rows.map((row) => | |
| 99 | + decodeSource(stripConvexSystemFields(row as unknown as Record<string, unknown>)), | |
| 100 | + ); | |
| 101 | + }, | |
| 102 | +}); | |
| 103 | + | |
| 104 | +export const getToolArtifactBySource = query({ | |
| 105 | + args: { | |
| 106 | + workspaceId: v.string(), | |
| 107 | + sourceId: v.string(), | |
| 108 | + }, | |
| 109 | + handler: async (ctx, args): Promise<ToolArtifact | null> => { | |
| 110 | + const rows = await ctx.db | |
| 111 | + .query("toolArtifacts") | |
| 112 | + .withIndex("by_sourceId", (q) => q.eq("sourceId", args.sourceId)) | |
| 113 | + .collect(); | |
| 114 | + | |
| 115 | + const record = rows.find((row) => row.workspaceId === args.workspaceId) ?? null; | |
| 116 | + if (!record) { | |
| 117 | + return null; | |
| 118 | + } | |
| 119 | + | |
| 120 | + return decodeToolArtifact( | |
| 121 | + stripConvexSystemFields(record as unknown as Record<string, unknown>), | |
| 122 | + ); | |
| 123 | + }, | |
| 124 | +}); | |
| 125 | + | |
| 126 | +const createConvexSourceStore = (ctx: ActionCtx): SourceStore => ({ | |
| 127 | + getById: (workspaceId: WorkspaceId, sourceId: SourceId) => | |
| 128 | + Effect.tryPromise({ | |
| 129 | + try: () => | |
| 130 | + ctx | |
| 131 | + .runQuery(runtimeApi.source_tool_registry.listSourcesForWorkspace, { | |
| 132 | + workspaceId, | |
| 133 | + }) | |
| 134 | + .then((sources: Array<Source>) => | |
| 135 | + Option.fromNullable( | |
| 136 | + sources.find((source: Source) => source.id === sourceId) ?? null, | |
| 137 | + ), | |
| 138 | + ), | |
| 139 | + catch: (cause) => sourceStoreQueryError("getById", cause), | |
| 140 | + }), | |
| 141 | + | |
| 142 | + listByWorkspace: (workspaceId: WorkspaceId) => | |
| 143 | + Effect.tryPromise({ | |
| 144 | + try: () => | |
| 145 | + ctx.runQuery(runtimeApi.source_tool_registry.listSourcesForWorkspace, { | |
| 146 | + workspaceId, | |
| 147 | + }), | |
| 148 | + catch: (cause) => sourceStoreQueryError("listByWorkspace", cause), | |
| 149 | + }), | |
| 150 | + | |
| 151 | + upsert: () => Effect.fail(unsupportedSourceStoreMutation("upsert")), | |
| 152 | + | |
| 153 | + removeById: () => Effect.fail(unsupportedSourceStoreMutation("removeById")), | |
| 154 | +}); | |
| 155 | + | |
| 156 | +const createConvexToolArtifactStore = (ctx: ActionCtx): ToolArtifactStore => ({ | |
| 157 | + getBySource: (workspaceId: WorkspaceId, sourceId: SourceId) => | |
| 158 | + Effect.tryPromise({ | |
| 159 | + try: () => | |
| 160 | + ctx | |
| 161 | + .runQuery(runtimeApi.source_tool_registry.getToolArtifactBySource, { | |
| 162 | + workspaceId, | |
| 163 | + sourceId, | |
| 164 | + }) | |
| 165 | + .then((artifact) => Option.fromNullable(artifact)), | |
| 166 | + catch: (cause) => toolArtifactStoreQueryError("getBySource", cause), | |
| 167 | + }), | |
| 168 | + | |
| 169 | + upsert: () => Effect.fail(unsupportedToolArtifactMutation()), | |
| 170 | +}); | |
| 171 | + | |
| 172 | +export const createConvexSourceToolRegistry = ( | |
| 173 | + ctx: ActionCtx, | |
| 174 | + workspaceId: string, | |
| 175 | +) => { | |
| 176 | + const sourceStore = createConvexSourceStore(ctx); | |
| 177 | + const toolArtifactStore = createConvexToolArtifactStore(ctx); | |
| 178 | + const toolProviderRegistry = makeToolProviderRegistry([makeOpenApiToolProvider()]); | |
| 179 | + | |
| 180 | + return createSourceToolRegistry({ | |
| 181 | + workspaceId, | |
| 182 | + sourceStore, | |
| 183 | + toolArtifactStore, | |
| 184 | + toolProviderRegistry, | |
| 185 | + }); | |
| 186 | +}; | |
| @@ -19,7 +19,8 @@ const toSourceStoreError = ( · 18 unmodified lines | ||
| 19 | 19 | ): SourceStoreError => |
| 20 | 20 | new SourceStoreError({ |
| 21 | 21 | operation, |
| 22 | − filePath: "local://snapshot.json", | |
| 22 | + backend: "local-file", | |
| 23 | + location: "snapshot.json", | |
| 23 | 24 | message, |
| 24 | 25 | reason: null, |
| 25 | 26 | details, |
| @@ -5,17 +5,23 @@ import { · 4 unmodified lines | ||
| 5 | 5 | makeControlPlaneSourcesService, |
| 6 | 6 | makeControlPlaneWebHandler, |
| 7 | 7 | makeSourceCatalogService, |
| 8 | + makeSourceManagerService, | |
| 8 | 9 | } from "@executor-v2/management-api"; |
| 9 | 10 | import { |
| 11 | + RuntimeAdapterError, | |
| 10 | 12 | createRunExecutor, |
| 11 | − createRuntimeToolCallHandler, | |
| 12 | − createUnimplementedRuntimeToolInvoker, | |
| 13 | + createRuntimeToolCallService, | |
| 14 | + createSourceToolRegistry, | |
| 15 | + makeOpenApiToolProvider, | |
| 13 | 16 | makeRuntimeAdapterRegistry, |
| 17 | + makeToolProviderRegistry, | |
| 14 | 18 | } from "@executor-v2/engine"; |
| 15 | 19 | import { |
| 16 | 20 | makeLocalSourceStore, |
| 17 | 21 | makeLocalStateStore, |
| 22 | + makeLocalToolArtifactStore, | |
| 18 | 23 | } from "@executor-v2/persistence-local"; |
| 24 | +import { type RuntimeToolCallResult } from "@executor-v2/sdk"; | |
| 19 | 25 | import { makeCloudflareWorkerLoaderRuntimeAdapter } from "@executor-v2/runtime-cloudflare-worker-loader"; |
| 20 | 26 | import { makeDenoSubprocessRuntimeAdapter } from "@executor-v2/runtime-deno-subprocess"; |
| 21 | 27 | import { makeLocalInProcessRuntimeAdapter } from "@executor-v2/runtime-local-inproc"; |
| @@ -24,7 +30,6 @@ import * as Layer from "effect/Layer"; · 2 unmodified lines | ||
| 24 | 30 | |
| 25 | 31 | import { PmActorLive } from "./actor"; |
| 26 | 32 | import { createPmApprovalsService } from "./approvals-service"; |
| 27 | −import { createPmResolveToolCredentials } from "./credential-resolver"; | |
| 28 | 33 | import { startPmHttpServer } from "./http-server"; |
| 29 | 34 | import { createPmMcpHandler } from "./mcp-handler"; |
| 30 | 35 | import { createPmExecuteRuntimeRun } from "./runtime-execution-port"; |
| @@ -46,7 +51,16 @@ const readConfiguredRuntimeKind = (value: string | undefined): string | undefine · 15 unmodified lines | ||
| 46 | 51 | return normalized.length > 0 ? normalized : undefined; |
| 47 | 52 | }; |
| 48 | 53 | |
| 54 | +const readConfiguredWorkspaceId = (value: string | undefined): string => { | |
| 55 | + const normalized = value?.trim(); | |
| 56 | + return normalized && normalized.length > 0 ? normalized : "ws_local"; | |
| 57 | +}; | |
| 58 | + | |
| 59 | +const formatRuntimeAdapterError = (error: RuntimeAdapterError): string => | |
| 60 | + error.details ? `${error.message}: ${error.details}` : error.message; | |
| 61 | + | |
| 49 | 62 | const port = parsePort(process.env.PORT); |
| 63 | +const workspaceId = readConfiguredWorkspaceId(process.env.PM_WORKSPACE_ID); | |
| 50 | 64 | |
| 51 | 65 | const pmRuntimeAdapters = [ |
| 52 | 66 | makeLocalInProcessRuntimeAdapter(), |
| @@ -55,7 +69,6 @@ const pmRuntimeAdapters = [ · 2 unmodified lines | ||
| 55 | 69 | ]; |
| 56 | 70 | |
| 57 | 71 | const runtimeAdapters = makeRuntimeAdapterRegistry(pmRuntimeAdapters); |
| 58 | − | |
| 59 | 72 | const defaultRuntimeKind = |
| 60 | 73 | readConfiguredRuntimeKind(process.env.PM_RUNTIME_KIND) ?? pmRuntimeAdapters[0].kind; |
| 61 | 74 | |
| @@ -71,10 +84,55 @@ const localStateStore = await Effect.runPromise( · 9 unmodified lines | ||
| 71 | 84 | }).pipe(Effect.provide(BunContext.layer)), |
| 72 | 85 | ); |
| 73 | 86 | |
| 87 | +const toolArtifactStore = await Effect.runPromise( | |
| 88 | + makeLocalToolArtifactStore({ | |
| 89 | + rootDir: pmStateRootDir, | |
| 90 | + }).pipe(Effect.provide(BunContext.layer)), | |
| 91 | +); | |
| 92 | + | |
| 74 | 93 | const sourceCatalog = makeSourceCatalogService(sourceStore); |
| 94 | +const sourceManager = makeSourceManagerService(toolArtifactStore); | |
| 95 | +const baseSourcesService = makeControlPlaneSourcesService(sourceCatalog); | |
| 96 | +const sourcesService = { | |
| 97 | + ...baseSourcesService, | |
| 98 | + upsertSource: (input: Parameters<typeof baseSourcesService.upsertSource>[0]) => | |
| 99 | + Effect.gen(function* () { | |
| 100 | + const source = yield* baseSourcesService.upsertSource(input); | |
| 101 | + | |
| 102 | + if (source.kind !== "openapi") { | |
| 103 | + return source; | |
| 104 | + } | |
| 105 | + | |
| 106 | + const openApiSpecResult = yield* Effect.tryPromise({ | |
| 107 | + try: async () => { | |
| 108 | + const response = await fetch(source.endpoint); | |
| 109 | + if (!response.ok) { | |
| 110 | + throw new Error(`Failed fetching OpenAPI spec (${response.status})`); | |
| 111 | + } | |
| 112 | + | |
| 113 | + return await response.json(); | |
| 114 | + }, | |
| 115 | + catch: (cause) => String(cause), | |
| 116 | + }).pipe(Effect.either); | |
| 117 | + | |
| 118 | + if (openApiSpecResult._tag === "Left") { | |
| 119 | + return source; | |
| 120 | + } | |
| 121 | + | |
| 122 | + yield* sourceManager | |
| 123 | + .refreshOpenApiArtifact({ | |
| 124 | + source, | |
| 125 | + openApiSpec: openApiSpecResult.right, | |
| 126 | + }) | |
| 127 | + .pipe(Effect.ignore); | |
| 128 | + | |
| 129 | + return source; | |
| 130 | + }), | |
| 131 | +}; | |
| 132 | + | |
| 75 | 133 | const approvalsService = createPmApprovalsService(localStateStore); |
| 76 | 134 | const controlPlaneService = makeControlPlaneService({ |
| 77 | − sources: makeControlPlaneSourcesService(sourceCatalog), | |
| 135 | + sources: sourcesService, | |
| 78 | 136 | approvals: approvalsService, |
| 79 | 137 | }); |
| 80 | 138 | |
| @@ -83,24 +141,40 @@ const controlPlaneWebHandler = makeControlPlaneWebHandler( · 2 unmodified lines | ||
| 83 | 141 | PmActorLive(localStateStore), |
| 84 | 142 | ); |
| 85 | 143 | |
| 86 | −const resolveCredentials = createPmResolveToolCredentials(localStateStore); | |
| 87 | −const invokeRuntimeTool = createUnimplementedRuntimeToolInvoker("pm"); | |
| 88 | −const handleToolCall = createRuntimeToolCallHandler({ | |
| 89 | − resolveCredentials, | |
| 90 | − invokeRuntimeTool, | |
| 144 | +const toolProviderRegistry = makeToolProviderRegistry([makeOpenApiToolProvider()]); | |
| 145 | +const toolRegistry = createSourceToolRegistry({ | |
| 146 | + workspaceId, | |
| 147 | + sourceStore, | |
| 148 | + toolArtifactStore, | |
| 149 | + toolProviderRegistry, | |
| 91 | 150 | }); |
| 151 | +const runtimeToolCallService = createRuntimeToolCallService(toolRegistry); | |
| 92 | 152 | |
| 93 | 153 | const executeRuntimeRun = createPmExecuteRuntimeRun({ |
| 94 | 154 | defaultRuntimeKind, |
| 95 | 155 | runtimeAdapters, |
| 96 | − handleToolCall, | |
| 156 | + toolRegistry, | |
| 97 | 157 | }); |
| 98 | 158 | |
| 99 | 159 | const runExecutor = createRunExecutor(executeRuntimeRun); |
| 100 | 160 | const handleMcp = createPmMcpHandler(runExecutor.executeRun); |
| 101 | 161 | |
| 102 | 162 | const handleToolCallHttp = createPmToolCallHttpHandler((input) => |
| 103 | − Effect.runPromise(handleToolCall(input)), | |
| 163 | + Effect.runPromise( | |
| 164 | + runtimeToolCallService.callTool(input).pipe( | |
| 165 | + Effect.map((value): RuntimeToolCallResult => ({ | |
| 166 | + ok: true, | |
| 167 | + value, | |
| 168 | + })), | |
| 169 | + Effect.catchTag("RuntimeAdapterError", (error) => | |
| 170 | + Effect.succeed<RuntimeToolCallResult>({ | |
| 171 | + ok: false, | |
| 172 | + kind: "failed", | |
| 173 | + error: formatRuntimeAdapterError(error), | |
| 174 | + }), | |
| 175 | + ), | |
| 176 | + ), | |
| 177 | + ), | |
| 104 | 178 | ); |
| 105 | 179 | |
| 106 | 180 | const server = startPmHttpServer({ |
| @@ -1,62 +1,25 @@ | ||
| 1 | 1 | import { |
| 2 | − RuntimeAdapterError, | |
| 2 | + createRuntimeToolCallService, | |
| 3 | 3 | RuntimeExecutionPortError, |
| 4 | 4 | type ExecuteRuntimeRun, |
| 5 | 5 | type ExecuteRuntimeRunInput, |
| 6 | 6 | type RuntimeAdapterRegistry, |
| 7 | 7 | type RuntimeExecuteError, |
| 8 | − type RuntimeToolCallService, | |
| 8 | + type ToolRegistry, | |
| 9 | 9 | } from "@executor-v2/engine"; |
| 10 | −import { | |
| 11 | − type RuntimeToolCallRequest, | |
| 12 | − type RuntimeToolCallResult, | |
| 13 | −} from "@executor-v2/sdk"; | |
| 14 | 10 | import * as Effect from "effect/Effect"; |
| 15 | 11 | |
| 16 | 12 | export type PmRuntimeExecutionPortOptions = { |
| 17 | 13 | defaultRuntimeKind: string; |
| 18 | 14 | runtimeAdapters: RuntimeAdapterRegistry; |
| 19 | − handleToolCall: ( | |
| 20 | − input: RuntimeToolCallRequest, | |
| 21 | − ) => Effect.Effect<RuntimeToolCallResult, never>; | |
| 15 | + toolRegistry: ToolRegistry; | |
| 22 | 16 | }; |
| 23 | 17 | |
| 24 | −const runtimeToolCallService = ( | |
| 25 | − options: PmRuntimeExecutionPortOptions, | |
| 26 | −): RuntimeToolCallService => ({ | |
| 27 | − callTool: (input) => | |
| 28 | − options.handleToolCall(input).pipe( | |
| 29 | − Effect.flatMap((result) => { | |
| 30 | − if (result.ok) { | |
| 31 | − return Effect.succeed(result.value); | |
| 32 | − } | |
| 33 | − | |
| 34 | − if (result.kind === "pending") { | |
| 35 | − return Effect.fail( | |
| 36 | − new RuntimeAdapterError({ | |
| 37 | − operation: "call_tool", | |
| 38 | − runtimeKind: options.defaultRuntimeKind, | |
| 39 | − message: `Tool call pending approval: ${input.toolPath}`, | |
| 40 | − details: result.error ?? `approvalId=${result.approvalId}`, | |
| 41 | − }), | |
| 42 | − ); | |
| 43 | − } | |
| 44 | − | |
| 45 | − return Effect.fail( | |
| 46 | − new RuntimeAdapterError({ | |
| 47 | − operation: "call_tool", | |
| 48 | − runtimeKind: options.defaultRuntimeKind, | |
| 49 | − message: `Tool call ${result.kind}: ${input.toolPath}`, | |
| 50 | − details: result.error, | |
| 51 | − }), | |
| 52 | − ); | |
| 53 | − }), | |
| 54 | − ), | |
| 55 | −}); | |
| 56 | − | |
| 57 | 18 | export const createPmExecuteRuntimeRun = ( |
| 58 | 19 | options: PmRuntimeExecutionPortOptions, |
| 59 | 20 | ): ExecuteRuntimeRun => { |
| 21 | + const runtimeToolCallService = createRuntimeToolCallService(options.toolRegistry); | |
| 22 | + | |
| 60 | 23 | return (input: ExecuteRuntimeRunInput) => |
| 61 | 24 | Effect.gen(function* () { |
| 62 | 25 | const runtimeAdapter = yield* options.runtimeAdapters |
| @@ -86,7 +49,7 @@ export const createPmExecuteRuntimeRun = ( · 23 unmodified lines | ||
| 86 | 49 | runId: input.runId, |
| 87 | 50 | code: input.code, |
| 88 | 51 | timeoutMs: input.timeoutMs, |
| 89 | − toolCallService: runtimeToolCallService(options), | |
| 52 | + toolCallService: runtimeToolCallService, | |
| 90 | 53 | }) |
| 91 | 54 | .pipe( |
| 92 | 55 | Effect.mapError( |
| @@ -33,6 +33,8 @@ export { · 32 unmodified lines | ||
| 33 | 33 | type ToolRegistryToolSummary, |
| 34 | 34 | } from "./tool-registry"; |
| 35 | 35 | |
| 36 | +export { createSourceToolRegistry } from "./source-tool-registry"; | |
| 37 | + | |
| 36 | 38 | export { |
| 37 | 39 | makeOpenApiToolProvider, |
| 38 | 40 | openApiToolDescriptorsFromManifest, |
| @@ -0,0 +1,270 @@ | ||
| 1 | +import { createServer, type IncomingMessage, type ServerResponse } from "node:http"; | |
| 2 | + | |
| 3 | +import { | |
| 4 | + HttpApi, | |
| 5 | + HttpApiEndpoint, | |
| 6 | + HttpApiGroup, | |
| 7 | + HttpApiSchema, | |
| 8 | + OpenApi, | |
| 9 | +} from "@effect/platform"; | |
| 10 | +import { describe, expect, it } from "@effect/vitest"; | |
| 11 | +import { makeSourceManagerService } from "@executor-v2/management-api"; | |
| 12 | +import { | |
| 13 | + type SourceStore, | |
| 14 | + type ToolArtifactStore, | |
| 15 | +} from "@executor-v2/persistence-ports"; | |
| 16 | +import { SourceSchema, type Source, type ToolArtifact } from "@executor-v2/schema"; | |
| 17 | +import * as Data from "effect/Data"; | |
| 18 | +import * as Effect from "effect/Effect"; | |
| 19 | +import * as Option from "effect/Option"; | |
| 20 | +import * as Schema from "effect/Schema"; | |
| 21 | + | |
| 22 | +import { makeOpenApiToolProvider } from "./openapi-provider"; | |
| 23 | +import { makeToolProviderRegistry } from "./tool-providers"; | |
| 24 | +import { createSourceToolRegistry } from "./source-tool-registry"; | |
| 25 | + | |
| 26 | +const decodeSource = Schema.decodeUnknownSync(SourceSchema); | |
| 27 | + | |
| 28 | +type TestServer = { | |
| 29 | + baseUrl: string; | |
| 30 | + requests: Array<string>; | |
| 31 | + close: () => Promise<void>; | |
| 32 | +}; | |
| 33 | + | |
| 34 | +class TestServerReleaseError extends Data.TaggedError("TestServerReleaseError")<{ | |
| 35 | + message: string; | |
| 36 | +}> {} | |
| 37 | + | |
| 38 | +const githubOwnerParam = HttpApiSchema.param("owner", Schema.String); | |
| 39 | +const githubRepoParam = HttpApiSchema.param("repo", Schema.String); | |
| 40 | + | |
| 41 | +class GitHubReposApi extends HttpApiGroup.make("repos").add( | |
| 42 | + HttpApiEndpoint.get("getRepo")`/repos/${githubOwnerParam}/${githubRepoParam}`.addSuccess( | |
| 43 | + Schema.Unknown, | |
| 44 | + ), | |
| 45 | +) {} | |
| 46 | + | |
| 47 | +class GitHubApi extends HttpApi.make("github").add(GitHubReposApi) {} | |
| 48 | + | |
| 49 | +const githubOpenApiSpec = OpenApi.fromApi(GitHubApi); | |
| 50 | + | |
| 51 | +const jsonResponse = (res: ServerResponse, statusCode: number, body: unknown): void => { | |
| 52 | + res.statusCode = statusCode; | |
| 53 | + res.setHeader("content-type", "application/json"); | |
| 54 | + res.end(JSON.stringify(body)); | |
| 55 | +}; | |
| 56 | + | |
| 57 | +const getHeaderValue = (req: IncomingMessage, key: string): string | null => { | |
| 58 | + const value = req.headers[key]; | |
| 59 | + if (typeof value === "string") { | |
| 60 | + return value; | |
| 61 | + } | |
| 62 | + | |
| 63 | + if (Array.isArray(value)) { | |
| 64 | + return value[0] ?? null; | |
| 65 | + } | |
| 66 | + | |
| 67 | + return null; | |
| 68 | +}; | |
| 69 | + | |
| 70 | +const makeTestServer = Effect.acquireRelease( | |
| 71 | + Effect.promise<TestServer>( | |
| 72 | + () => | |
| 73 | + new Promise<TestServer>((resolve, reject) => { | |
| 74 | + const requests: Array<string> = []; | |
| 75 | + | |
| 76 | + const server = createServer((req, res) => { | |
| 77 | + const host = getHeaderValue(req, "host") ?? "127.0.0.1"; | |
| 78 | + const url = new URL(req.url ?? "/", `http://${host}`); | |
| 79 | + | |
| 80 | + if (url.pathname === "/repos/octocat/hello-world" && req.method === "GET") { | |
| 81 | + requests.push(url.pathname); | |
| 82 | + jsonResponse(res, 200, { | |
| 83 | + full_name: "octocat/hello-world", | |
| 84 | + stargazers_count: 42, | |
| 85 | + }); | |
| 86 | + return; | |
| 87 | + } | |
| 88 | + | |
| 89 | + jsonResponse(res, 404, { | |
| 90 | + error: "not found", | |
| 91 | + }); | |
| 92 | + }); | |
| 93 | + | |
| 94 | + server.once("error", (error) => reject(error)); | |
| 95 | + | |
| 96 | + server.listen(0, "127.0.0.1", () => { | |
| 97 | + const address = server.address(); | |
| 98 | + if (!address || typeof address === "string") { | |
| 99 | + reject(new Error("failed to resolve test server address")); | |
| 100 | + return; | |
| 101 | + } | |
| 102 | + | |
| 103 | + resolve({ | |
| 104 | + baseUrl: `http://127.0.0.1:${address.port}`, | |
| 105 | + requests, | |
| 106 | + close: () => | |
| 107 | + new Promise<void>((closeResolve, closeReject) => { | |
| 108 | + server.close((error) => { | |
| 109 | + if (error) { | |
| 110 | + closeReject(error); | |
| 111 | + return; | |
| 112 | + } | |
| 113 | + | |
| 114 | + closeResolve(); | |
| 115 | + }); | |
| 116 | + }), | |
| 117 | + }); | |
| 118 | + }); | |
| 119 | + }), | |
| 120 | + ), | |
| 121 | + (server) => | |
| 122 | + Effect.tryPromise({ | |
| 123 | + try: () => server.close(), | |
| 124 | + catch: (cause) => | |
| 125 | + new TestServerReleaseError({ | |
| 126 | + message: cause instanceof Error ? cause.message : String(cause), | |
| 127 | + }), | |
| 128 | + }).pipe(Effect.orDie), | |
| 129 | +); | |
| 130 | + | |
| 131 | +describe("source tool registry", () => { | |
| 132 | + it.scoped("discovers and invokes source-backed tools", () => | |
| 133 | + Effect.gen(function* () { | |
| 134 | + const server = yield* makeTestServer; | |
| 135 | + | |
| 136 | + const source: Source = decodeSource({ | |
| 137 | + id: "src_github", | |
| 138 | + workspaceId: "ws_local", | |
| 139 | + name: "github", | |
| 140 | + kind: "openapi", | |
| 141 | + endpoint: server.baseUrl, | |
| 142 | + status: "connected", | |
| 143 | + enabled: true, | |
| 144 | + configJson: "{}", | |
| 145 | + sourceHash: null, | |
| 146 | + lastError: null, | |
| 147 | + createdAt: Date.now(), | |
| 148 | + updatedAt: Date.now(), | |
| 149 | + }); | |
| 150 | + | |
| 151 | + const sources: Array<Source> = [source]; | |
| 152 | + | |
| 153 | + const sourceStore: SourceStore = { | |
| 154 | + getById: (workspaceId, sourceId) => | |
| 155 | + Effect.succeed( | |
| 156 | + Option.fromNullable( | |
| 157 | + sources.find( | |
| 158 | + (candidate) => | |
| 159 | + candidate.workspaceId === workspaceId && candidate.id === sourceId, | |
| 160 | + ), | |
| 161 | + ), | |
| 162 | + ), | |
| 163 | + listByWorkspace: (workspaceId) => | |
| 164 | + Effect.succeed( | |
| 165 | + sources.filter((candidate) => candidate.workspaceId === workspaceId), | |
| 166 | + ), | |
| 167 | + upsert: (nextSource) => | |
| 168 | + Effect.sync(() => { | |
| 169 | + const index = sources.findIndex( | |
| 170 | + (candidate) => | |
| 171 | + candidate.workspaceId === nextSource.workspaceId && | |
| 172 | + candidate.id === nextSource.id, | |
| 173 | + ); | |
| 174 | + | |
| 175 | + if (index >= 0) { | |
| 176 | + sources[index] = nextSource; | |
| 177 | + return; | |
| 178 | + } | |
| 179 | + | |
| 180 | + sources.push(nextSource); | |
| 181 | + }), | |
| 182 | + removeById: (workspaceId, sourceId) => | |
| 183 | + Effect.sync(() => { | |
| 184 | + const initialLength = sources.length; | |
| 185 | + const nextSources = sources.filter( | |
| 186 | + (candidate) => | |
| 187 | + !(candidate.workspaceId === workspaceId && candidate.id === sourceId), | |
| 188 | + ); | |
| 189 | + sources.splice(0, sources.length, ...nextSources); | |
| 190 | + return initialLength !== sources.length; | |
| 191 | + }), | |
| 192 | + }; | |
| 193 | + | |
| 194 | + const artifactsByKey = new Map<string, ToolArtifact>(); | |
| 195 | + | |
| 196 | + const toolArtifactStore: ToolArtifactStore = { | |
| 197 | + getBySource: (workspaceId, sourceId) => | |
| 198 | + Effect.succeed( | |
| 199 | + Option.fromNullable(artifactsByKey.get(`${workspaceId}:${sourceId}`)), | |
| 200 | + ), | |
| 201 | + upsert: (artifact) => | |
| 202 | + Effect.sync(() => { | |
| 203 | + artifactsByKey.set(`${artifact.workspaceId}:${artifact.sourceId}`, artifact); | |
| 204 | + }), | |
| 205 | + }; | |
| 206 | + | |
| 207 | + const sourceManager = makeSourceManagerService(toolArtifactStore); | |
| 208 | + yield* sourceManager.refreshOpenApiArtifact({ | |
| 209 | + source, | |
| 210 | + openApiSpec: githubOpenApiSpec, | |
| 211 | + }); | |
| 212 | + | |
| 213 | + const toolProviderRegistry = makeToolProviderRegistry([makeOpenApiToolProvider()]); | |
| 214 | + | |
| 215 | + const toolRegistry = createSourceToolRegistry({ | |
| 216 | + workspaceId: source.workspaceId, | |
| 217 | + sourceStore, | |
| 218 | + toolArtifactStore, | |
| 219 | + toolProviderRegistry, | |
| 220 | + }); | |
| 221 | + | |
| 222 | + const discovered = yield* toolRegistry.discover({ | |
| 223 | + query: "repo", | |
| 224 | + limit: 5, | |
| 225 | + }); | |
| 226 | + | |
| 227 | + expect(discovered.bestPath).not.toBeNull(); | |
| 228 | + expect(discovered.results.length).toBeGreaterThan(0); | |
| 229 | + | |
| 230 | + const bestPath = discovered.bestPath; | |
| 231 | + if (!bestPath) { | |
| 232 | + throw new Error("expected discover to return bestPath"); | |
| 233 | + } | |
| 234 | + | |
| 235 | + const invocationResult = yield* toolRegistry.callTool({ | |
| 236 | + runId: "run_source_registry_1", | |
| 237 | + callId: "call_source_registry_1", | |
| 238 | + toolPath: bestPath, | |
| 239 | + input: { | |
| 240 | + owner: "octocat", | |
| 241 | + repo: "hello-world", | |
| 242 | + }, | |
| 243 | + }); | |
| 244 | + | |
| 245 | + expect(invocationResult).toMatchObject({ | |
| 246 | + status: 200, | |
| 247 | + body: { | |
| 248 | + full_name: "octocat/hello-world", | |
| 249 | + stargazers_count: 42, | |
| 250 | + }, | |
| 251 | + }); | |
| 252 | + | |
| 253 | + const namespaces = yield* toolRegistry.catalogNamespaces({}); | |
| 254 | + expect(namespaces.total).toBeGreaterThan(0); | |
| 255 | + expect(namespaces.namespaces[0]?.samplePaths.length).toBeGreaterThan(0); | |
| 256 | + | |
| 257 | + const namespace = namespaces.namespaces[0]?.namespace; | |
| 258 | + if (!namespace) { | |
| 259 | + throw new Error("expected at least one namespace"); | |
| 260 | + } | |
| 261 | + | |
| 262 | + const catalog = yield* toolRegistry.catalogTools({ | |
| 263 | + namespace, | |
| 264 | + }); | |
| 265 | + | |
| 266 | + expect(catalog.results.length).toBeGreaterThan(0); | |
| 267 | + expect(server.requests).toEqual(["/repos/octocat/hello-world"]); | |
| 268 | + }), | |
| 269 | + ); | |
| 270 | +}); | |
| @@ -0,0 +1,365 @@ | ||
| 1 | +import { | |
| 2 | + type SourceStore, | |
| 3 | + type SourceStoreError, | |
| 4 | + type ToolArtifactStore, | |
| 5 | + type ToolArtifactStoreError, | |
| 6 | +} from "@executor-v2/persistence-ports"; | |
| 7 | +import type { Source, WorkspaceId } from "@executor-v2/schema"; | |
| 8 | +import * as Effect from "effect/Effect"; | |
| 9 | +import * as Option from "effect/Option"; | |
| 10 | + | |
| 11 | +import { openApiToolDescriptorsFromManifest } from "./openapi-provider"; | |
| 12 | +import { | |
| 13 | + RuntimeAdapterError, | |
| 14 | + type RuntimeAdapterKind, | |
| 15 | +} from "./runtime-adapters"; | |
| 16 | +import type { | |
| 17 | + CanonicalToolDescriptor, | |
| 18 | + ToolProviderError, | |
| 19 | + ToolProviderRegistry, | |
| 20 | + ToolProviderRegistryError, | |
| 21 | +} from "./tool-providers"; | |
| 22 | +import type { | |
| 23 | + ToolRegistry, | |
| 24 | + ToolRegistryCatalogNamespacesInput, | |
| 25 | + ToolRegistryCatalogNamespacesOutput, | |
| 26 | + ToolRegistryCatalogToolsInput, | |
| 27 | + ToolRegistryCatalogToolsOutput, | |
| 28 | + ToolRegistryCallInput, | |
| 29 | + ToolRegistryDiscoverInput, | |
| 30 | + ToolRegistryDiscoverOutput, | |
| 31 | + ToolRegistryToolSummary, | |
| 32 | +} from "./tool-registry"; | |
| 33 | + | |
| 34 | +const sourceToolRegistryRuntimeKind: RuntimeAdapterKind = "source-tool-registry"; | |
| 35 | + | |
| 36 | +type SourceToolRegistryOptions = { | |
| 37 | + workspaceId: string; | |
| 38 | + sourceStore: SourceStore; | |
| 39 | + toolArtifactStore: ToolArtifactStore; | |
| 40 | + toolProviderRegistry: ToolProviderRegistry; | |
| 41 | +}; | |
| 42 | + | |
| 43 | +type SourceToolEntry = { | |
| 44 | + path: string; | |
| 45 | + namespace: string; | |
| 46 | + source: Source; | |
| 47 | + descriptor: CanonicalToolDescriptor; | |
| 48 | +}; | |
| 49 | + | |
| 50 | +const toRuntimeAdapterError = ( | |
| 51 | + operation: string, | |
| 52 | + message: string, | |
| 53 | + details: string | null, | |
| 54 | +): RuntimeAdapterError => | |
| 55 | + new RuntimeAdapterError({ | |
| 56 | + operation, | |
| 57 | + runtimeKind: sourceToolRegistryRuntimeKind, | |
| 58 | + message, | |
| 59 | + details, | |
| 60 | + }); | |
| 61 | + | |
| 62 | +const sourceStoreErrorToRuntimeAdapterError = ( | |
| 63 | + operation: string, | |
| 64 | + cause: SourceStoreError, | |
| 65 | +): RuntimeAdapterError => | |
| 66 | + toRuntimeAdapterError( | |
| 67 | + operation, | |
| 68 | + cause.message, | |
| 69 | + cause.details ?? cause.reason ?? cause.location, | |
| 70 | + ); | |
| 71 | + | |
| 72 | +const toolArtifactStoreErrorToRuntimeAdapterError = ( | |
| 73 | + operation: string, | |
| 74 | + cause: ToolArtifactStoreError, | |
| 75 | +): RuntimeAdapterError => | |
| 76 | + toRuntimeAdapterError( | |
| 77 | + operation, | |
| 78 | + cause.message, | |
| 79 | + cause.details ?? cause.reason ?? cause.location, | |
| 80 | + ); | |
| 81 | + | |
| 82 | +const toolProviderErrorToRuntimeAdapterError = ( | |
| 83 | + operation: string, | |
| 84 | + cause: ToolProviderError, | |
| 85 | +): RuntimeAdapterError => | |
| 86 | + toRuntimeAdapterError(operation, cause.message, cause.details ?? null); | |
| 87 | + | |
| 88 | +const toolProviderRegistryErrorToRuntimeAdapterError = ( | |
| 89 | + operation: string, | |
| 90 | + cause: ToolProviderRegistryError, | |
| 91 | +): RuntimeAdapterError => | |
| 92 | + toRuntimeAdapterError(operation, cause.message, null); | |
| 93 | + | |
| 94 | +const normalizeNamespacePart = (value: string): string => { | |
| 95 | + const normalized = value | |
| 96 | + .trim() | |
| 97 | + .toLowerCase() | |
| 98 | + .replace(/[^a-z0-9]+/g, "_") | |
| 99 | + .replace(/^_+|_+$/g, ""); | |
| 100 | + | |
| 101 | + return normalized.length > 0 ? normalized : "source"; | |
| 102 | +}; | |
| 103 | + | |
| 104 | +const sourceNamespace = (source: Source): string => { | |
| 105 | + const sourceIdSuffix = source.id.slice(-6).toLowerCase(); | |
| 106 | + return `${normalizeNamespacePart(source.name)}_${sourceIdSuffix}`; | |
| 107 | +}; | |
| 108 | + | |
| 109 | +const sourceToolPath = ( | |
| 110 | + source: Source, | |
| 111 | + descriptor: CanonicalToolDescriptor, | |
| 112 | +): string => `${sourceNamespace(source)}.${descriptor.toolId}`; | |
| 113 | + | |
| 114 | +const scoreSummary = (summary: ToolRegistryToolSummary, query: string): number => { | |
| 115 | + if (query.length === 0) { | |
| 116 | + return 1; | |
| 117 | + } | |
| 118 | + | |
| 119 | + const lowerQuery = query.toLowerCase(); | |
| 120 | + const lowerPath = summary.path.toLowerCase(); | |
| 121 | + const lowerSource = (summary.source ?? "").toLowerCase(); | |
| 122 | + const lowerDescription = (summary.description ?? "").toLowerCase(); | |
| 123 | + | |
| 124 | + if (lowerPath === lowerQuery) { | |
| 125 | + return 100; | |
| 126 | + } | |
| 127 | + | |
| 128 | + if (lowerPath.startsWith(lowerQuery)) { | |
| 129 | + return 80; | |
| 130 | + } | |
| 131 | + | |
| 132 | + if (lowerPath.includes(lowerQuery)) { | |
| 133 | + return 60; | |
| 134 | + } | |
| 135 | + | |
| 136 | + if (lowerSource.includes(lowerQuery)) { | |
| 137 | + return 40; | |
| 138 | + } | |
| 139 | + | |
| 140 | + if (lowerDescription.includes(lowerQuery)) { | |
| 141 | + return 30; | |
| 142 | + } | |
| 143 | + | |
| 144 | + return 0; | |
| 145 | +}; | |
| 146 | + | |
| 147 | +const summaryFromEntry = (entry: SourceToolEntry): ToolRegistryToolSummary => ({ | |
| 148 | + path: entry.path, | |
| 149 | + source: entry.source.name, | |
| 150 | + approval: "auto", | |
| 151 | + description: entry.descriptor.description ?? undefined, | |
| 152 | +}); | |
| 153 | + | |
| 154 | +const normalizeToolCallInput = ( | |
| 155 | + input: unknown, | |
| 156 | +): Record<string, unknown> => { | |
| 157 | + if (!input || typeof input !== "object" || Array.isArray(input)) { | |
| 158 | + return {}; | |
| 159 | + } | |
| 160 | + | |
| 161 | + return input as Record<string, unknown>; | |
| 162 | +}; | |
| 163 | + | |
| 164 | +const describeOutput = (value: unknown): string => { | |
| 165 | + if (typeof value === "string") { | |
| 166 | + return value; | |
| 167 | + } | |
| 168 | + | |
| 169 | + try { | |
| 170 | + return JSON.stringify(value); | |
| 171 | + } catch { | |
| 172 | + return String(value); | |
| 173 | + } | |
| 174 | +}; | |
| 175 | + | |
| 176 | +const loadSourceEntries = ( | |
| 177 | + options: SourceToolRegistryOptions, | |
| 178 | +): Effect.Effect<ReadonlyArray<SourceToolEntry>, RuntimeAdapterError> => | |
| 179 | + Effect.gen(function* () { | |
| 180 | + const workspaceSources = yield* options.sourceStore | |
| 181 | + .listByWorkspace(options.workspaceId as WorkspaceId) | |
| 182 | + .pipe( | |
| 183 | + Effect.mapError((cause) => | |
| 184 | + sourceStoreErrorToRuntimeAdapterError("list_sources", cause), | |
| 185 | + ), | |
| 186 | + ); | |
| 187 | + | |
| 188 | + const enabledSources = workspaceSources.filter((source) => source.enabled); | |
| 189 | + | |
| 190 | + const bySource = yield* Effect.forEach(enabledSources, (source) => | |
| 191 | + Effect.gen(function* () { | |
| 192 | + if (source.kind !== "openapi") { | |
| 193 | + return [] as Array<SourceToolEntry>; | |
| 194 | + } | |
| 195 | + | |
| 196 | + const artifactOption = yield* options.toolArtifactStore | |
| 197 | + .getBySource(source.workspaceId, source.id) | |
| 198 | + .pipe( | |
| 199 | + Effect.mapError((cause) => | |
| 200 | + toolArtifactStoreErrorToRuntimeAdapterError("get_source_artifact", cause), | |
| 201 | + ), | |
| 202 | + ); | |
| 203 | + | |
| 204 | + if (Option.isNone(artifactOption)) { | |
| 205 | + return [] as Array<SourceToolEntry>; | |
| 206 | + } | |
| 207 | + | |
| 208 | + const artifact = artifactOption.value; | |
| 209 | + const descriptors = yield* openApiToolDescriptorsFromManifest( | |
| 210 | + source, | |
| 211 | + artifact.manifestJson, | |
| 212 | + ).pipe( | |
| 213 | + Effect.mapError((cause) => | |
| 214 | + toolProviderErrorToRuntimeAdapterError( | |
| 215 | + "decode_source_manifest", | |
| 216 | + cause, | |
| 217 | + ), | |
| 218 | + ), | |
| 219 | + ); | |
| 220 | + | |
| 221 | + const namespace = sourceNamespace(source); | |
| 222 | + | |
| 223 | + return descriptors.map((descriptor) => ({ | |
| 224 | + path: sourceToolPath(source, descriptor), | |
| 225 | + namespace, | |
| 226 | + source, | |
| 227 | + descriptor, | |
| 228 | + })); | |
| 229 | + }), | |
| 230 | + ); | |
| 231 | + | |
| 232 | + return bySource.flat(); | |
| 233 | + }); | |
| 234 | + | |
| 235 | +const discoverTools = ( | |
| 236 | + entries: ReadonlyArray<SourceToolEntry>, | |
| 237 | + input: ToolRegistryDiscoverInput, | |
| 238 | +): ToolRegistryDiscoverOutput => { | |
| 239 | + const limit = Math.max(1, Math.min(50, input.limit ?? 8)); | |
| 240 | + const query = (input.query ?? "").trim().toLowerCase(); | |
| 241 | + | |
| 242 | + const ranked = entries | |
| 243 | + .map((entry) => ({ | |
| 244 | + summary: summaryFromEntry(entry), | |
| 245 | + score: scoreSummary(summaryFromEntry(entry), query), | |
| 246 | + })) | |
| 247 | + .filter((item) => item.score > 0) | |
| 248 | + .sort((left, right) => right.score - left.score) | |
| 249 | + .slice(0, limit) | |
| 250 | + .map((item) => item.summary); | |
| 251 | + | |
| 252 | + return { | |
| 253 | + bestPath: ranked[0]?.path ?? null, | |
| 254 | + results: ranked, | |
| 255 | + total: ranked.length, | |
| 256 | + }; | |
| 257 | +}; | |
| 258 | + | |
| 259 | +const catalogNamespaces = ( | |
| 260 | + entries: ReadonlyArray<SourceToolEntry>, | |
| 261 | + input: ToolRegistryCatalogNamespacesInput, | |
| 262 | +): ToolRegistryCatalogNamespacesOutput => { | |
| 263 | + const limit = Math.max(1, Math.min(200, input.limit ?? 50)); | |
| 264 | + const grouped = new Map<string, Array<string>>(); | |
| 265 | + | |
| 266 | + for (const entry of entries) { | |
| 267 | + const paths = grouped.get(entry.namespace) ?? []; | |
| 268 | + paths.push(entry.path); | |
| 269 | + grouped.set(entry.namespace, paths); | |
| 270 | + } | |
| 271 | + | |
| 272 | + const namespaces = [...grouped.entries()] | |
| 273 | + .sort((left, right) => left[0].localeCompare(right[0])) | |
| 274 | + .map(([namespace, paths]) => ({ | |
| 275 | + namespace, | |
| 276 | + toolCount: paths.length, | |
| 277 | + samplePaths: [...paths] | |
| 278 | + .sort((left, right) => left.localeCompare(right)) | |
| 279 | + .slice(0, 3), | |
| 280 | + })); | |
| 281 | + | |
| 282 | + return { | |
| 283 | + namespaces: namespaces.slice(0, limit), | |
| 284 | + total: namespaces.length, | |
| 285 | + }; | |
| 286 | +}; | |
| 287 | + | |
| 288 | +const catalogTools = ( | |
| 289 | + entries: ReadonlyArray<SourceToolEntry>, | |
| 290 | + input: ToolRegistryCatalogToolsInput, | |
| 291 | +): ToolRegistryCatalogToolsOutput => { | |
| 292 | + const limit = Math.max(1, Math.min(200, input.limit ?? 50)); | |
| 293 | + const query = (input.query ?? "").trim().toLowerCase(); | |
| 294 | + const namespace = (input.namespace ?? "").trim().toLowerCase(); | |
| 295 | + | |
| 296 | + const filtered = entries | |
| 297 | + .filter((entry) => | |
| 298 | + namespace.length === 0 ? true : entry.namespace.toLowerCase() === namespace, | |
| 299 | + ) | |
| 300 | + .map(summaryFromEntry) | |
| 301 | + .filter((summary) => scoreSummary(summary, query) > 0) | |
| 302 | + .slice(0, limit); | |
| 303 | + | |
| 304 | + return { | |
| 305 | + results: filtered, | |
| 306 | + total: filtered.length, | |
| 307 | + }; | |
| 308 | +}; | |
| 309 | + | |
| 310 | +export const createSourceToolRegistry = ( | |
| 311 | + options: SourceToolRegistryOptions, | |
| 312 | +): ToolRegistry => ({ | |
| 313 | + callTool: (input: ToolRegistryCallInput) => | |
| 314 | + Effect.gen(function* () { | |
| 315 | + const entries = yield* loadSourceEntries(options); | |
| 316 | + const entry = entries.find((candidate) => candidate.path === input.toolPath); | |
| 317 | + | |
| 318 | + if (!entry) { | |
| 319 | + return yield* toRuntimeAdapterError( | |
| 320 | + "call_tool", | |
| 321 | + `Unknown tool path: ${input.toolPath}`, | |
| 322 | + "Use tools.discover({ query }) or tools.catalog.tools({ namespace }) to find available tool paths.", | |
| 323 | + ); | |
| 324 | + } | |
| 325 | + | |
| 326 | + const invocation = yield* options.toolProviderRegistry | |
| 327 | + .invoke({ | |
| 328 | + source: entry.source, | |
| 329 | + tool: entry.descriptor, | |
| 330 | + args: normalizeToolCallInput(input.input), | |
| 331 | + }) | |
| 332 | + .pipe( | |
| 333 | + Effect.mapError((cause) => | |
| 334 | + cause._tag === "ToolProviderError" | |
| 335 | + ? toolProviderErrorToRuntimeAdapterError("invoke_tool", cause) | |
| 336 | + : toolProviderRegistryErrorToRuntimeAdapterError("invoke_tool", cause), | |
| 337 | + ), | |
| 338 | + ); | |
| 339 | + | |
| 340 | + if (invocation.isError) { | |
| 341 | + return yield* toRuntimeAdapterError( | |
| 342 | + "call_tool", | |
| 343 | + `Tool call returned provider error: ${input.toolPath}`, | |
| 344 | + describeOutput(invocation.output), | |
| 345 | + ); | |
| 346 | + } | |
| 347 | + | |
| 348 | + return invocation.output; | |
| 349 | + }), | |
| 350 | + | |
| 351 | + discover: (input: ToolRegistryDiscoverInput) => | |
| 352 | + loadSourceEntries(options).pipe( | |
| 353 | + Effect.map((entries) => discoverTools(entries, input)), | |
| 354 | + ), | |
| 355 | + | |
| 356 | + catalogNamespaces: (input: ToolRegistryCatalogNamespacesInput) => | |
| 357 | + loadSourceEntries(options).pipe( | |
| 358 | + Effect.map((entries) => catalogNamespaces(entries, input)), | |
| 359 | + ), | |
| 360 | + | |
| 361 | + catalogTools: (input: ToolRegistryCatalogToolsInput) => | |
| 362 | + loadSourceEntries(options).pipe( | |
| 363 | + Effect.map((entries) => catalogTools(entries, input)), | |
| 364 | + ), | |
| 365 | +}); | |
| @@ -6,7 +6,8 @@ import type * as Option from "effect/Option"; · 5 unmodified lines | ||
| 6 | 6 | |
| 7 | 7 | export class SourceStoreError extends Data.TaggedError("SourceStoreError")<{ |
| 8 | 8 | operation: string; |
| 9 | − filePath: string; | |
| 9 | + backend: string; | |
| 10 | + location: string; | |
| 10 | 11 | message: string; |
| 11 | 12 | reason: string | null; |
| 12 | 13 | details: string | null; |
| @@ -6,7 +6,8 @@ import type * as Option from "effect/Option"; · 5 unmodified lines | ||
| 6 | 6 | |
| 7 | 7 | export class ToolArtifactStoreError extends Data.TaggedError("ToolArtifactStoreError")<{ |
| 8 | 8 | operation: string; |
| 9 | − filePath: string; | |
| 9 | + backend: string; | |
| 10 | + location: string; | |
| 10 | 11 | message: string; |
| 11 | 12 | reason: string | null; |
| 12 | 13 | details: string | null; |
| @@ -8,7 +8,8 @@ import type { LocalStateSnapshot } from "./local-state-snapshot"; · 7 unmodified lines | ||
| 8 | 8 | |
| 9 | 9 | export class LocalStateStoreError extends Data.TaggedError("LocalStateStoreError")<{ |
| 10 | 10 | operation: string; |
| 11 | − filePath: string; | |
| 11 | + backend: string; | |
| 12 | + location: string; | |
| 12 | 13 | message: string; |
| 13 | 14 | reason: string | null; |
| 14 | 15 | details: string | null; |
| @@ -5,7 +5,8 @@ import * as ParseResult from "effect/ParseResult"; · 4 unmodified lines | ||
| 5 | 5 | |
| 6 | 6 | export type PersistenceErrorData = { |
| 7 | 7 | operation: string; |
| 8 | − filePath: string; | |
| 8 | + backend: string; | |
| 9 | + location: string; | |
| 9 | 10 | message: string; |
| 10 | 11 | reason: string | null; |
| 11 | 12 | details: string | null; |
| @@ -13,9 +14,11 @@ export type PersistenceErrorData = { · 1 unmodified line | ||
| 13 | 14 | |
| 14 | 15 | type PersistenceErrorFactory<E> = (data: PersistenceErrorData) => E; |
| 15 | 16 | |
| 17 | +const defaultPersistenceBackend = "local-file"; | |
| 18 | + | |
| 16 | 19 | export const withPlatformPersistenceError = |
| 17 | 20 | <E>(makeError: PersistenceErrorFactory<E>) => |
| 18 | − <A>(operation: string, filePath: string) => | |
| 21 | + <A>(operation: string, location: string, backend = defaultPersistenceBackend) => | |
| 19 | 22 | ( |
| 20 | 23 | self: Effect.Effect<A, PlatformError.PlatformError>, |
| 21 | 24 | ): Effect.Effect<A, E> => |
| @@ -26,7 +29,8 @@ export const withPlatformPersistenceError = · 4 unmodified lines | ||
| 26 | 29 | Effect.fail( |
| 27 | 30 | makeError({ |
| 28 | 31 | operation, |
| 29 | − filePath, | |
| 32 | + backend, | |
| 33 | + location, | |
| 30 | 34 | message: cause.message, |
| 31 | 35 | reason: cause.reason, |
| 32 | 36 | details: cause.description ?? null, |
| @@ -36,7 +40,8 @@ export const withPlatformPersistenceError = · 3 unmodified lines | ||
| 36 | 40 | Effect.fail( |
| 37 | 41 | makeError({ |
| 38 | 42 | operation, |
| 39 | − filePath, | |
| 43 | + backend, | |
| 44 | + location, | |
| 40 | 45 | message: cause.message, |
| 41 | 46 | reason: null, |
| 42 | 47 | details: cause.description ?? null, |
| @@ -60,14 +65,16 @@ export const withNotFoundFallback = · 17 unmodified lines | ||
| 60 | 65 | export const toSchemaPersistenceError = <E>( |
| 61 | 66 | makeError: PersistenceErrorFactory<E>, |
| 62 | 67 | operation: string, |
| 63 | − filePath: string, | |
| 68 | + location: string, | |
| 64 | 69 | message: string, |
| 65 | 70 | cause: ParseResult.ParseError, |
| 66 | 71 | details?: string, |
| 72 | + backend = defaultPersistenceBackend, | |
| 67 | 73 | ): E => |
| 68 | 74 | makeError({ |
| 69 | 75 | operation, |
| 70 | − filePath, | |
| 76 | + backend, | |
| 77 | + location, | |
| 71 | 78 | message, |
| 72 | 79 | reason: "InvalidData", |
| 73 | 80 | details: |
| @@ -79,13 +86,15 @@ export const toSchemaPersistenceError = <E>( · 5 unmodified lines | ||
| 79 | 86 | export const toInvalidDataError = <E>( |
| 80 | 87 | makeError: PersistenceErrorFactory<E>, |
| 81 | 88 | operation: string, |
| 82 | − filePath: string, | |
| 89 | + location: string, | |
| 83 | 90 | message: string, |
| 84 | 91 | details: string, |
| 92 | + backend = defaultPersistenceBackend, | |
| 85 | 93 | ): E => |
| 86 | 94 | makeError({ |
| 87 | 95 | operation, |
| 88 | − filePath, | |
| 96 | + backend, | |
| 97 | + location, | |
| 89 | 98 | message, |
| 90 | 99 | reason: "InvalidData", |
| 91 | 100 | details, |