Unified · Split
| @@ -25,7 +25,8 @@ export const toSourceStoreError = ( · 24 unmodified lines | |||
| 25 | ): SourceStoreError => | 25 | ): SourceStoreError => |
| 26 | new SourceStoreError({ | 26 | new SourceStoreError({ |
| 27 | operation, | 27 | operation, |
| 28 | filePath: "convex://control-plane", | 28 | backend: "convex", |
| 29 | location: "control-plane", | ||
| 29 | message: "Convex control plane call failed", | 30 | message: "Convex control plane call failed", |
| 30 | reason: null, | 31 | reason: null, |
| 31 | details: renderCause(cause), | 32 | details: renderCause(cause), |
| @@ -12,6 +12,7 @@ const setup = () => · 11 unmodified lines | |||
| 12 | "./mcp.ts": () => import("./mcp"), | 12 | "./mcp.ts": () => import("./mcp"), |
| 13 | "./executor.ts": () => import("./executor"), | 13 | "./executor.ts": () => import("./executor"), |
| 14 | "./runtimeCallbacks.ts": () => import("./runtimeCallbacks"), | 14 | "./runtimeCallbacks.ts": () => import("./runtimeCallbacks"), |
| 15 | "./source_tool_registry.ts": () => import("./source_tool_registry"), | ||
| 15 | "./controlPlane.ts": () => import("./controlPlane"), | 16 | "./controlPlane.ts": () => import("./controlPlane"), |
| 16 | "./_generated/api.js": () => import("./_generated/api.js"), | 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 | import type { ExecuteRunInput, ExecuteRunResult } from "@executor-v2/sdk"; | 5 | import type { ExecuteRunInput, ExecuteRunResult } from "@executor-v2/sdk"; |
| 3 | import * as Effect from "effect/Effect"; | 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 | export const executeRunImpl = ( | 14 | export const executeRunImpl = ( |
| 10 | input: ExecuteRunInput, | 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 | import { httpAction } from "./_generated/server"; | 5 | import { httpAction } from "./_generated/server"; |
| 6 | import { executeRunImpl } from "./executor"; | 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 | export const mcpHandler = httpAction(async (ctx, request) => { | 16 | export const mcpHandler = httpAction(async (ctx, request) => { |
| 17 | const toolRegistry = createConvexSourceToolRegistry(ctx, workspaceId); | ||
| 18 | |||
| 9 | const runClient = createExecutorRunClient((input) => | 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 | return handleMcpHttpRequest(request, { | 27 | return handleMcpHttpRequest(request, { |
| @@ -1,15 +1,18 @@ | |||
| 1 | import { | 1 | import { |
| 2 | createRuntimeToolCallHandler, | 2 | RuntimeAdapterError, |
| 3 | createUnimplementedRuntimeToolInvoker, | 3 | createRuntimeToolCallService, |
| 4 | } from "@executor-v2/engine"; | 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 | import * as Data from "effect/Data"; | 9 | import * as Data from "effect/Data"; |
| 7 | import * as Effect from "effect/Effect"; | 10 | import * as Effect from "effect/Effect"; |
| 8 | import * as ParseResult from "effect/ParseResult"; | 11 | import * as ParseResult from "effect/ParseResult"; |
| 9 | import * as Schema from "effect/Schema"; | 12 | import * as Schema from "effect/Schema"; |
| 10 | 13 | ||
| 11 | import { httpAction, type ActionCtx } from "./_generated/server"; | 14 | import { httpAction, type ActionCtx } from "./_generated/server"; |
| 12 | import { createConvexResolveToolCredentials } from "./credential_resolver"; | 15 | import { createConvexSourceToolRegistry } from "./source_tool_registry"; |
| 13 | 16 | ||
| 14 | class RuntimeToolCallBadRequestError extends Data.TaggedError( | 17 | class RuntimeToolCallBadRequestError extends Data.TaggedError( |
| 15 | "RuntimeToolCallBadRequestError", | 18 | "RuntimeToolCallBadRequestError", |
| @@ -40,6 +43,13 @@ const RuntimeToolCallRequestSchema = Schema.Struct({ · 24 unmodified lines | |||
| 40 | 43 | ||
| 41 | const decodeRuntimeToolCallRequest = Schema.decodeUnknown(RuntimeToolCallRequestSchema); | 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 | const badRequest = (message: string): Response => | 53 | const badRequest = (message: string): Response => |
| 44 | Response.json( | 54 | Response.json( |
| 45 | { | 55 | { |
| @@ -53,20 +63,13 @@ const badRequest = (message: string): Response => · 7 unmodified lines | |||
| 53 | const formatBadRequestMessage = (error: RuntimeToolCallBadRequestError): string => | 63 | const formatBadRequestMessage = (error: RuntimeToolCallBadRequestError): string => |
| 54 | error.details.length > 0 ? `${error.message}: ${error.details}` : error.message; | 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 = ( | 69 | const formatUnknownDetails = (cause: unknown): string => String(cause); |
| 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 | }); | ||
| 68 | 70 | ||
| 69 | return Effect.gen(function* () { | 71 | const decodeToolCallRequest = (request: Request): Effect.Effect<RuntimeToolCallRequest, RuntimeToolCallBadRequestError> => |
| 72 | Effect.gen(function* () { | ||
| 70 | const body = yield* Effect.tryPromise({ | 73 | const body = yield* Effect.tryPromise({ |
| 71 | try: () => request.json(), | 74 | try: () => request.json(), |
| 72 | catch: (cause) => | 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 | Effect.mapError( | 83 | Effect.mapError( |
| 81 | (cause) => | 84 | (cause) => |
| 82 | new RuntimeToolCallBadRequestError({ | 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 | return Response.json(result, { status: 200 }); | 122 | return Response.json(result, { status: 200 }); |
| 91 | }).pipe( | 123 | }).pipe( |
| 92 | Effect.catchTag("RuntimeToolCallBadRequestError", (error) => | 124 | Effect.catchTag("RuntimeToolCallBadRequestError", (error) => |
| 93 | Effect.succeed(badRequest(formatBadRequestMessage(error))), | 125 | Effect.succeed(badRequest(formatBadRequestMessage(error))), |
| 94 | ), | 126 | ), |
| 95 | ); | 127 | ); |
| 96 | }; | ||
| 97 | 128 | ||
| 98 | export const handleToolCallHttp = httpAction((ctx, request) => | 129 | export const handleToolCallHttp = httpAction((ctx, request) => |
| 99 | Effect.runPromise(handleToolCallHttpEffect(ctx, request)), | 130 | Effect.runPromise(handleToolCallHttpEffect(ctx, request)), |
| @@ -1,8 +1,10 @@ | |||
| 1 | import { | 1 | import { |
| 2 | RuntimeAdapterError, | ||
| 3 | RuntimeExecutionPortError, | 2 | RuntimeExecutionPortError, |
| 3 | createRuntimeToolCallService, | ||
| 4 | createStaticToolRegistry, | ||
| 4 | type ExecuteRuntimeRun, | 5 | type ExecuteRuntimeRun, |
| 5 | type ExecuteRuntimeRunInput, | 6 | type ExecuteRuntimeRunInput, |
| 7 | type ToolRegistry, | ||
| 6 | makeRuntimeAdapterRegistry, | 8 | makeRuntimeAdapterRegistry, |
| 7 | type RuntimeExecuteError, | 9 | type RuntimeExecuteError, |
| 8 | } from "@executor-v2/engine"; | 10 | } from "@executor-v2/engine"; |
| @@ -13,56 +15,60 @@ const convexRuntimeAdapter = makeLocalInProcessRuntimeAdapter(); · 4 unmodified lines | |||
| 13 | 15 | ||
| 14 | const runtimeAdapters = makeRuntimeAdapterRegistry([convexRuntimeAdapter]); | 16 | const runtimeAdapters = makeRuntimeAdapterRegistry([convexRuntimeAdapter]); |
| 15 | 17 | ||
| 16 | const missingToolCallService = (runId: string, toolPath: string): RuntimeAdapterError => | 18 | export type ConvexRuntimeExecutionPortOptions = { |
| 17 | new RuntimeAdapterError({ | 19 | toolRegistry?: ToolRegistry; |
| 18 | operation: "call_tool", | 20 | }; |
| 19 | runtimeKind: convexRuntimeAdapter.kind, | ||
| 20 | message: `No runtime tool-call service configured in convex for ${toolPath}`, | ||
| 21 | details: `runId=${runId}`, | ||
| 22 | }); | ||
| 23 | 21 | ||
| 24 | export const executeRuntimeRunInConvex: ExecuteRuntimeRun = ( | 22 | export const createExecuteRuntimeRunInConvex = ( |
| 25 | input: ExecuteRuntimeRunInput, | 23 | options: ConvexRuntimeExecutionPortOptions = {}, |
| 26 | ) => | 24 | ): ExecuteRuntimeRun => { |
| 27 | Effect.gen(function* () { | 25 | const toolRegistry = |
| 28 | const runtimeAdapter = yield* runtimeAdapters.get(convexRuntimeAdapter.kind).pipe( | 26 | options.toolRegistry ?? |
| 29 | Effect.mapError( | 27 | createStaticToolRegistry({ |
| 30 | (error) => | 28 | tools: {}, |
| 31 | new RuntimeExecutionPortError({ | 29 | }); |
| 32 | operation: "resolve_runtime_adapter", | 30 | const toolCallService = createRuntimeToolCallService(toolRegistry); |
| 33 | message: error.message, | ||
| 34 | details: null, | ||
| 35 | }), | ||
| 36 | ), | ||
| 37 | ); | ||
| 38 | 31 | ||
| 39 | const isAvailable = yield* runtimeAdapter.isAvailable(); | 32 | return (input: ExecuteRuntimeRunInput) => |
| 40 | if (!isAvailable) { | 33 | Effect.gen(function* () { |
| 41 | return yield* new RuntimeExecutionPortError({ | 34 | const runtimeAdapter = yield* runtimeAdapters.get(convexRuntimeAdapter.kind).pipe( |
| 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( | ||
| 59 | Effect.mapError( | 35 | Effect.mapError( |
| 60 | (error: RuntimeExecuteError) => | 36 | (error) => |
| 61 | new RuntimeExecutionPortError({ | 37 | new RuntimeExecutionPortError({ |
| 62 | operation: "runtime_execute", | 38 | operation: "resolve_runtime_adapter", |
| 63 | message: error.message, | 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 | ): SourceStoreError => | 19 | ): SourceStoreError => |
| 20 | new SourceStoreError({ | 20 | new SourceStoreError({ |
| 21 | operation, | 21 | operation, |
| 22 | filePath: "local://snapshot.json", | 22 | backend: "local-file", |
| 23 | location: "snapshot.json", | ||
| 23 | message, | 24 | message, |
| 24 | reason: null, | 25 | reason: null, |
| 25 | details, | 26 | details, |
| @@ -5,17 +5,23 @@ import { · 4 unmodified lines | |||
| 5 | makeControlPlaneSourcesService, | 5 | makeControlPlaneSourcesService, |
| 6 | makeControlPlaneWebHandler, | 6 | makeControlPlaneWebHandler, |
| 7 | makeSourceCatalogService, | 7 | makeSourceCatalogService, |
| 8 | makeSourceManagerService, | ||
| 8 | } from "@executor-v2/management-api"; | 9 | } from "@executor-v2/management-api"; |
| 9 | import { | 10 | import { |
| 11 | RuntimeAdapterError, | ||
| 10 | createRunExecutor, | 12 | createRunExecutor, |
| 11 | createRuntimeToolCallHandler, | 13 | createRuntimeToolCallService, |
| 12 | createUnimplementedRuntimeToolInvoker, | 14 | createSourceToolRegistry, |
| 15 | makeOpenApiToolProvider, | ||
| 13 | makeRuntimeAdapterRegistry, | 16 | makeRuntimeAdapterRegistry, |
| 17 | makeToolProviderRegistry, | ||
| 14 | } from "@executor-v2/engine"; | 18 | } from "@executor-v2/engine"; |
| 15 | import { | 19 | import { |
| 16 | makeLocalSourceStore, | 20 | makeLocalSourceStore, |
| 17 | makeLocalStateStore, | 21 | makeLocalStateStore, |
| 22 | makeLocalToolArtifactStore, | ||
| 18 | } from "@executor-v2/persistence-local"; | 23 | } from "@executor-v2/persistence-local"; |
| 24 | import { type RuntimeToolCallResult } from "@executor-v2/sdk"; | ||
| 19 | import { makeCloudflareWorkerLoaderRuntimeAdapter } from "@executor-v2/runtime-cloudflare-worker-loader"; | 25 | import { makeCloudflareWorkerLoaderRuntimeAdapter } from "@executor-v2/runtime-cloudflare-worker-loader"; |
| 20 | import { makeDenoSubprocessRuntimeAdapter } from "@executor-v2/runtime-deno-subprocess"; | 26 | import { makeDenoSubprocessRuntimeAdapter } from "@executor-v2/runtime-deno-subprocess"; |
| 21 | import { makeLocalInProcessRuntimeAdapter } from "@executor-v2/runtime-local-inproc"; | 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 | import { PmActorLive } from "./actor"; | 31 | import { PmActorLive } from "./actor"; |
| 26 | import { createPmApprovalsService } from "./approvals-service"; | 32 | import { createPmApprovalsService } from "./approvals-service"; |
| 27 | import { createPmResolveToolCredentials } from "./credential-resolver"; | ||
| 28 | import { startPmHttpServer } from "./http-server"; | 33 | import { startPmHttpServer } from "./http-server"; |
| 29 | import { createPmMcpHandler } from "./mcp-handler"; | 34 | import { createPmMcpHandler } from "./mcp-handler"; |
| 30 | import { createPmExecuteRuntimeRun } from "./runtime-execution-port"; | 35 | import { createPmExecuteRuntimeRun } from "./runtime-execution-port"; |
| @@ -46,7 +51,16 @@ const readConfiguredRuntimeKind = (value: string | undefined): string | undefine · 15 unmodified lines | |||
| 46 | return normalized.length > 0 ? normalized : undefined; | 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 | const port = parsePort(process.env.PORT); | 62 | const port = parsePort(process.env.PORT); |
| 63 | const workspaceId = readConfiguredWorkspaceId(process.env.PM_WORKSPACE_ID); | ||
| 50 | 64 | ||
| 51 | const pmRuntimeAdapters = [ | 65 | const pmRuntimeAdapters = [ |
| 52 | makeLocalInProcessRuntimeAdapter(), | 66 | makeLocalInProcessRuntimeAdapter(), |
| @@ -55,7 +69,6 @@ const pmRuntimeAdapters = [ · 2 unmodified lines | |||
| 55 | ]; | 69 | ]; |
| 56 | 70 | ||
| 57 | const runtimeAdapters = makeRuntimeAdapterRegistry(pmRuntimeAdapters); | 71 | const runtimeAdapters = makeRuntimeAdapterRegistry(pmRuntimeAdapters); |
| 58 | |||
| 59 | const defaultRuntimeKind = | 72 | const defaultRuntimeKind = |
| 60 | readConfiguredRuntimeKind(process.env.PM_RUNTIME_KIND) ?? pmRuntimeAdapters[0].kind; | 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 | }).pipe(Effect.provide(BunContext.layer)), | 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 | const sourceCatalog = makeSourceCatalogService(sourceStore); | 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 | const approvalsService = createPmApprovalsService(localStateStore); | 133 | const approvalsService = createPmApprovalsService(localStateStore); |
| 76 | const controlPlaneService = makeControlPlaneService({ | 134 | const controlPlaneService = makeControlPlaneService({ |
| 77 | sources: makeControlPlaneSourcesService(sourceCatalog), | 135 | sources: sourcesService, |
| 78 | approvals: approvalsService, | 136 | approvals: approvalsService, |
| 79 | }); | 137 | }); |
| 80 | 138 | ||
| @@ -83,24 +141,40 @@ const controlPlaneWebHandler = makeControlPlaneWebHandler( · 2 unmodified lines | |||
| 83 | PmActorLive(localStateStore), | 141 | PmActorLive(localStateStore), |
| 84 | ); | 142 | ); |
| 85 | 143 | ||
| 86 | const resolveCredentials = createPmResolveToolCredentials(localStateStore); | 144 | const toolProviderRegistry = makeToolProviderRegistry([makeOpenApiToolProvider()]); |
| 87 | const invokeRuntimeTool = createUnimplementedRuntimeToolInvoker("pm"); | 145 | const toolRegistry = createSourceToolRegistry({ |
| 88 | const handleToolCall = createRuntimeToolCallHandler({ | 146 | workspaceId, |
| 89 | resolveCredentials, | 147 | sourceStore, |
| 90 | invokeRuntimeTool, | 148 | toolArtifactStore, |
| 149 | toolProviderRegistry, | ||
| 91 | }); | 150 | }); |
| 151 | const runtimeToolCallService = createRuntimeToolCallService(toolRegistry); | ||
| 92 | 152 | ||
| 93 | const executeRuntimeRun = createPmExecuteRuntimeRun({ | 153 | const executeRuntimeRun = createPmExecuteRuntimeRun({ |
| 94 | defaultRuntimeKind, | 154 | defaultRuntimeKind, |
| 95 | runtimeAdapters, | 155 | runtimeAdapters, |
| 96 | handleToolCall, | 156 | toolRegistry, |
| 97 | }); | 157 | }); |
| 98 | 158 | ||
| 99 | const runExecutor = createRunExecutor(executeRuntimeRun); | 159 | const runExecutor = createRunExecutor(executeRuntimeRun); |
| 100 | const handleMcp = createPmMcpHandler(runExecutor.executeRun); | 160 | const handleMcp = createPmMcpHandler(runExecutor.executeRun); |
| 101 | 161 | ||
| 102 | const handleToolCallHttp = createPmToolCallHttpHandler((input) => | 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 | const server = startPmHttpServer({ | 180 | const server = startPmHttpServer({ |
| @@ -1,62 +1,25 @@ | |||
| 1 | import { | 1 | import { |
| 2 | RuntimeAdapterError, | 2 | createRuntimeToolCallService, |
| 3 | RuntimeExecutionPortError, | 3 | RuntimeExecutionPortError, |
| 4 | type ExecuteRuntimeRun, | 4 | type ExecuteRuntimeRun, |
| 5 | type ExecuteRuntimeRunInput, | 5 | type ExecuteRuntimeRunInput, |
| 6 | type RuntimeAdapterRegistry, | 6 | type RuntimeAdapterRegistry, |
| 7 | type RuntimeExecuteError, | 7 | type RuntimeExecuteError, |
| 8 | type RuntimeToolCallService, | 8 | type ToolRegistry, |
| 9 | } from "@executor-v2/engine"; | 9 | } from "@executor-v2/engine"; |
| 10 | import { | ||
| 11 | type RuntimeToolCallRequest, | ||
| 12 | type RuntimeToolCallResult, | ||
| 13 | } from "@executor-v2/sdk"; | ||
| 14 | import * as Effect from "effect/Effect"; | 10 | import * as Effect from "effect/Effect"; |
| 15 | 11 | ||
| 16 | export type PmRuntimeExecutionPortOptions = { | 12 | export type PmRuntimeExecutionPortOptions = { |
| 17 | defaultRuntimeKind: string; | 13 | defaultRuntimeKind: string; |
| 18 | runtimeAdapters: RuntimeAdapterRegistry; | 14 | runtimeAdapters: RuntimeAdapterRegistry; |
| 19 | handleToolCall: ( | 15 | toolRegistry: ToolRegistry; |
| 20 | input: RuntimeToolCallRequest, | ||
| 21 | ) => Effect.Effect<RuntimeToolCallResult, never>; | ||
| 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 | export const createPmExecuteRuntimeRun = ( | 18 | export const createPmExecuteRuntimeRun = ( |
| 58 | options: PmRuntimeExecutionPortOptions, | 19 | options: PmRuntimeExecutionPortOptions, |
| 59 | ): ExecuteRuntimeRun => { | 20 | ): ExecuteRuntimeRun => { |
| 21 | const runtimeToolCallService = createRuntimeToolCallService(options.toolRegistry); | ||
| 22 | |||
| 60 | return (input: ExecuteRuntimeRunInput) => | 23 | return (input: ExecuteRuntimeRunInput) => |
| 61 | Effect.gen(function* () { | 24 | Effect.gen(function* () { |
| 62 | const runtimeAdapter = yield* options.runtimeAdapters | 25 | const runtimeAdapter = yield* options.runtimeAdapters |
| @@ -86,7 +49,7 @@ export const createPmExecuteRuntimeRun = ( · 23 unmodified lines | |||
| 86 | runId: input.runId, | 49 | runId: input.runId, |
| 87 | code: input.code, | 50 | code: input.code, |
| 88 | timeoutMs: input.timeoutMs, | 51 | timeoutMs: input.timeoutMs, |
| 89 | toolCallService: runtimeToolCallService(options), | 52 | toolCallService: runtimeToolCallService, |
| 90 | }) | 53 | }) |
| 91 | .pipe( | 54 | .pipe( |
| 92 | Effect.mapError( | 55 | Effect.mapError( |
| @@ -33,6 +33,8 @@ export { · 32 unmodified lines | |||
| 33 | type ToolRegistryToolSummary, | 33 | type ToolRegistryToolSummary, |
| 34 | } from "./tool-registry"; | 34 | } from "./tool-registry"; |
| 35 | 35 | ||
| 36 | export { createSourceToolRegistry } from "./source-tool-registry"; | ||
| 37 | |||
| 36 | export { | 38 | export { |
| 37 | makeOpenApiToolProvider, | 39 | makeOpenApiToolProvider, |
| 38 | openApiToolDescriptorsFromManifest, | 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 | export class SourceStoreError extends Data.TaggedError("SourceStoreError")<{ | 7 | export class SourceStoreError extends Data.TaggedError("SourceStoreError")<{ |
| 8 | operation: string; | 8 | operation: string; |
| 9 | filePath: string; | 9 | backend: string; |
| 10 | location: string; | ||
| 10 | message: string; | 11 | message: string; |
| 11 | reason: string | null; | 12 | reason: string | null; |
| 12 | details: string | null; | 13 | details: string | null; |
| @@ -6,7 +6,8 @@ import type * as Option from "effect/Option"; · 5 unmodified lines | |||
| 6 | 6 | ||
| 7 | export class ToolArtifactStoreError extends Data.TaggedError("ToolArtifactStoreError")<{ | 7 | export class ToolArtifactStoreError extends Data.TaggedError("ToolArtifactStoreError")<{ |
| 8 | operation: string; | 8 | operation: string; |
| 9 | filePath: string; | 9 | backend: string; |
| 10 | location: string; | ||
| 10 | message: string; | 11 | message: string; |
| 11 | reason: string | null; | 12 | reason: string | null; |
| 12 | details: string | null; | 13 | details: string | null; |
| @@ -8,7 +8,8 @@ import type { LocalStateSnapshot } from "./local-state-snapshot"; · 7 unmodified lines | |||
| 8 | 8 | ||
| 9 | export class LocalStateStoreError extends Data.TaggedError("LocalStateStoreError")<{ | 9 | export class LocalStateStoreError extends Data.TaggedError("LocalStateStoreError")<{ |
| 10 | operation: string; | 10 | operation: string; |
| 11 | filePath: string; | 11 | backend: string; |
| 12 | location: string; | ||
| 12 | message: string; | 13 | message: string; |
| 13 | reason: string | null; | 14 | reason: string | null; |
| 14 | details: string | null; | 15 | details: string | null; |
| @@ -5,7 +5,8 @@ import * as ParseResult from "effect/ParseResult"; · 4 unmodified lines | |||
| 5 | 5 | ||
| 6 | export type PersistenceErrorData = { | 6 | export type PersistenceErrorData = { |
| 7 | operation: string; | 7 | operation: string; |
| 8 | filePath: string; | 8 | backend: string; |
| 9 | location: string; | ||
| 9 | message: string; | 10 | message: string; |
| 10 | reason: string | null; | 11 | reason: string | null; |
| 11 | details: string | null; | 12 | details: string | null; |
| @@ -13,9 +14,11 @@ export type PersistenceErrorData = { · 1 unmodified line | |||
| 13 | 14 | ||
| 14 | type PersistenceErrorFactory<E> = (data: PersistenceErrorData) => E; | 15 | type PersistenceErrorFactory<E> = (data: PersistenceErrorData) => E; |
| 15 | 16 | ||
| 17 | const defaultPersistenceBackend = "local-file"; | ||
| 18 | |||
| 16 | export const withPlatformPersistenceError = | 19 | export const withPlatformPersistenceError = |
| 17 | <E>(makeError: PersistenceErrorFactory<E>) => | 20 | <E>(makeError: PersistenceErrorFactory<E>) => |
| 18 | <A>(operation: string, filePath: string) => | 21 | <A>(operation: string, location: string, backend = defaultPersistenceBackend) => |
| 19 | ( | 22 | ( |
| 20 | self: Effect.Effect<A, PlatformError.PlatformError>, | 23 | self: Effect.Effect<A, PlatformError.PlatformError>, |
| 21 | ): Effect.Effect<A, E> => | 24 | ): Effect.Effect<A, E> => |
| @@ -26,7 +29,8 @@ export const withPlatformPersistenceError = · 4 unmodified lines | |||
| 26 | Effect.fail( | 29 | Effect.fail( |
| 27 | makeError({ | 30 | makeError({ |
| 28 | operation, | 31 | operation, |
| 29 | filePath, | 32 | backend, |
| 33 | location, | ||
| 30 | message: cause.message, | 34 | message: cause.message, |
| 31 | reason: cause.reason, | 35 | reason: cause.reason, |
| 32 | details: cause.description ?? null, | 36 | details: cause.description ?? null, |
| @@ -36,7 +40,8 @@ export const withPlatformPersistenceError = · 3 unmodified lines | |||
| 36 | Effect.fail( | 40 | Effect.fail( |
| 37 | makeError({ | 41 | makeError({ |
| 38 | operation, | 42 | operation, |
| 39 | filePath, | 43 | backend, |
| 44 | location, | ||
| 40 | message: cause.message, | 45 | message: cause.message, |
| 41 | reason: null, | 46 | reason: null, |
| 42 | details: cause.description ?? null, | 47 | details: cause.description ?? null, |
| @@ -60,14 +65,16 @@ export const withNotFoundFallback = · 17 unmodified lines | |||
| 60 | export const toSchemaPersistenceError = <E>( | 65 | export const toSchemaPersistenceError = <E>( |
| 61 | makeError: PersistenceErrorFactory<E>, | 66 | makeError: PersistenceErrorFactory<E>, |
| 62 | operation: string, | 67 | operation: string, |
| 63 | filePath: string, | 68 | location: string, |
| 64 | message: string, | 69 | message: string, |
| 65 | cause: ParseResult.ParseError, | 70 | cause: ParseResult.ParseError, |
| 66 | details?: string, | 71 | details?: string, |
| 72 | backend = defaultPersistenceBackend, | ||
| 67 | ): E => | 73 | ): E => |
| 68 | makeError({ | 74 | makeError({ |
| 69 | operation, | 75 | operation, |
| 70 | filePath, | 76 | backend, |
| 77 | location, | ||
| 71 | message, | 78 | message, |
| 72 | reason: "InvalidData", | 79 | reason: "InvalidData", |
| 73 | details: | 80 | details: |
| @@ -79,13 +86,15 @@ export const toSchemaPersistenceError = <E>( · 5 unmodified lines | |||
| 79 | export const toInvalidDataError = <E>( | 86 | export const toInvalidDataError = <E>( |
| 80 | makeError: PersistenceErrorFactory<E>, | 87 | makeError: PersistenceErrorFactory<E>, |
| 81 | operation: string, | 88 | operation: string, |
| 82 | filePath: string, | 89 | location: string, |
| 83 | message: string, | 90 | message: string, |
| 84 | details: string, | 91 | details: string, |
| 92 | backend = defaultPersistenceBackend, | ||
| 85 | ): E => | 93 | ): E => |
| 86 | makeError({ | 94 | makeError({ |
| 87 | operation, | 95 | operation, |
| 88 | filePath, | 96 | backend, |
| 97 | location, | ||
| 89 | message, | 98 | message, |
| 90 | reason: "InvalidData", | 99 | reason: "InvalidData", |
| 91 | details, | 100 | details, |