History

feat: wire source-backed tool registry into PM and Convex

Rhys Sullivan committed 8 months ago

Commit fe2d7fc1 parent 16bd61617 files · +1078 −138Browse files

Unified · Split

17 files
  1. v2/apps/convex/convex/control_plane/errors.ts +2 −1
  2. v2/apps/convex/convex/executor.test.ts +1 −0
  3. v2/apps/convex/convex/executor.ts +18 −4
  4. v2/apps/convex/convex/mcp.ts +15 −1
  5. v2/apps/convex/convex/runtimeCallbacks.ts +50 −19
  6. v2/apps/convex/convex/runtime_execution_port.ts +52 −46
  7. v2/apps/convex/convex/source_tool_registry.ts +186 −0
  8. v2/apps/pm/src/approvals-service.ts +2 −1
  9. v2/apps/pm/src/main.ts +86 −12
  10. v2/apps/pm/src/runtime-execution-port.ts +6 −43
  11. v2/packages/engine/src/index.ts +2 −0
  12. v2/packages/engine/src/source-tool-registry.test.ts +270 −0
  13. v2/packages/engine/src/source-tool-registry.ts +365 −0
  14. v2/packages/state-contracts/src/source-store.ts +2 −1
  15. v2/packages/state-contracts/src/tool-artifacts.ts +2 −1
  16. v2/packages/state-local-file/src/local-state-store.ts +2 −1
  17. v2/packages/state-local-file/src/persistence-error.ts +17 −8
v2/apps/convex/convex/control_plane/errors.ts+2−1
@@ -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),
v2/apps/convex/convex/executor.test.ts+1−0
@@ -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 });
v2/apps/convex/convex/executor.ts+18−4
@@ -1,11 +1,25 @@
1import { createRunExecutor } from "@executor-v2/engine";1import {
2 createRunExecutor,
3 type ToolRegistry,
4} from "@executor-v2/engine";
2import type { ExecuteRunInput, ExecuteRunResult } from "@executor-v2/sdk";5import type { ExecuteRunInput, ExecuteRunResult } from "@executor-v2/sdk";
3import * as Effect from "effect/Effect";6import * as Effect from "effect/Effect";
4 7
5import { executeRuntimeRunInConvex } from "./runtime_execution_port";8import { createExecuteRuntimeRunInConvex } from "./runtime_execution_port";
6 9
7const runExecutor = createRunExecutor(executeRuntimeRunInConvex);10export type ExecuteRunImplOptions = {
11 toolRegistry?: ToolRegistry;
12};
8 13
9export const executeRunImpl = (14export 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};
v2/apps/convex/convex/mcp.ts+15−1
@@ -4,10 +4,24 @@ import * as Effect from "effect/Effect"; · 3 unmodified lines
4 4
5import { httpAction } from "./_generated/server";5import { httpAction } from "./_generated/server";
6import { executeRunImpl } from "./executor";6import { executeRunImpl } from "./executor";
7import { createConvexSourceToolRegistry } from "./source_tool_registry";
8
9const readConfiguredWorkspaceId = (value: string | undefined): string => {
10 const normalized = value?.trim();
11 return normalized && normalized.length > 0 ? normalized : "ws_local";
12};
13
14const workspaceId = readConfiguredWorkspaceId(process.env.CONVEX_WORKSPACE_ID);
7 15
8export const mcpHandler = httpAction(async (ctx, request) => {16export 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, {
v2/apps/convex/convex/runtimeCallbacks.ts+50−19
@@ -1,15 +1,18 @@
1import {1import {
2 createRuntimeToolCallHandler,2 RuntimeAdapterError,
3 createUnimplementedRuntimeToolInvoker,3 createRuntimeToolCallService,
4} from "@executor-v2/engine";4} from "@executor-v2/engine";
5import type { RuntimeToolCallResult } from "@executor-v2/sdk";5import type {
6 RuntimeToolCallRequest,
7 RuntimeToolCallResult,
8} from "@executor-v2/sdk";
6import * as Data from "effect/Data";9import * as Data from "effect/Data";
7import * as Effect from "effect/Effect";10import * as Effect from "effect/Effect";
8import * as ParseResult from "effect/ParseResult";11import * as ParseResult from "effect/ParseResult";
9import * as Schema from "effect/Schema";12import * as Schema from "effect/Schema";
10 13
11import { httpAction, type ActionCtx } from "./_generated/server";14import { httpAction, type ActionCtx } from "./_generated/server";
12import { createConvexResolveToolCredentials } from "./credential_resolver";15import { createConvexSourceToolRegistry } from "./source_tool_registry";
13 16
14class RuntimeToolCallBadRequestError extends Data.TaggedError(17class RuntimeToolCallBadRequestError extends Data.TaggedError(
15 "RuntimeToolCallBadRequestError",18 "RuntimeToolCallBadRequestError",
@@ -40,6 +43,13 @@ const RuntimeToolCallRequestSchema = Schema.Struct({ · 24 unmodified lines
40 43
41const decodeRuntimeToolCallRequest = Schema.decodeUnknown(RuntimeToolCallRequestSchema);44const decodeRuntimeToolCallRequest = Schema.decodeUnknown(RuntimeToolCallRequestSchema);
42 45
46const readConfiguredWorkspaceId = (value: string | undefined): string => {
47 const normalized = value?.trim();
48 return normalized && normalized.length > 0 ? normalized : "ws_local";
49};
50
51const fallbackWorkspaceId = readConfiguredWorkspaceId(process.env.CONVEX_WORKSPACE_ID);
52
43const badRequest = (message: string): Response =>53const badRequest = (message: string): Response =>
44 Response.json(54 Response.json(
45 {55 {
@@ -53,20 +63,13 @@ const badRequest = (message: string): Response => · 7 unmodified lines
53const formatBadRequestMessage = (error: RuntimeToolCallBadRequestError): string =>63const 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
56const formatUnknownDetails = (cause: unknown): string => String(cause);66const formatRuntimeAdapterError = (error: RuntimeAdapterError): string =>
67 error.details ? `${error.message}: ${error.details}` : error.message;
57 68
58const handleToolCallHttpEffect = (69const 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* () {71const 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
93const 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
98export const handleToolCallHttp = httpAction((ctx, request) =>129export const handleToolCallHttp = httpAction((ctx, request) =>
99 Effect.runPromise(handleToolCallHttpEffect(ctx, request)),130 Effect.runPromise(handleToolCallHttpEffect(ctx, request)),
v2/apps/convex/convex/runtime_execution_port.ts+52−46
@@ -1,8 +1,10 @@
1import {1import {
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
14const runtimeAdapters = makeRuntimeAdapterRegistry([convexRuntimeAdapter]);16const runtimeAdapters = makeRuntimeAdapterRegistry([convexRuntimeAdapter]);
15 17
16const missingToolCallService = (runId: string, toolPath: string): RuntimeAdapterError =>18export 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
24export const executeRuntimeRunInConvex: ExecuteRuntimeRun = (22export 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
74export const executeRuntimeRunInConvex = createExecuteRuntimeRunInConvex();
v2/apps/convex/convex/source_tool_registry.ts+186−0
@@ -0,0 +1,186 @@
1import {
2 createSourceToolRegistry,
3 makeOpenApiToolProvider,
4 makeToolProviderRegistry,
5} from "@executor-v2/engine";
6import {
7 SourceStoreError,
8 ToolArtifactStoreError,
9 type SourceStore,
10 type ToolArtifactStore,
11} from "@executor-v2/persistence-ports";
12import {
13 SourceSchema,
14 ToolArtifactSchema,
15 type Source,
16 type SourceId,
17 type ToolArtifact,
18 type WorkspaceId,
19} from "@executor-v2/schema";
20import { v } from "convex/values";
21import * as Effect from "effect/Effect";
22import * as Option from "effect/Option";
23import * as Schema from "effect/Schema";
24
25import { api } from "./_generated/api";
26import { query, type ActionCtx } from "./_generated/server";
27
28const runtimeApi = api as any;
29
30const decodeSource = Schema.decodeUnknownSync(SourceSchema);
31const decodeToolArtifact = Schema.decodeUnknownSync(ToolArtifactSchema);
32
33const stripConvexSystemFields = (
34 value: Record<string, unknown>,
35): Record<string, unknown> => {
36 const { _id: _ignoredId, _creationTime: _ignoredCreationTime, ...rest } = value;
37 return rest;
38};
39
40const 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
52const 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
62const 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
75const 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
88export 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
104export 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
126const 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
156const 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
172export 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};
v2/apps/pm/src/approvals-service.ts+2−1
@@ -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,
v2/apps/pm/src/main.ts+86−12
@@ -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";
9import {10import {
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";
15import {19import {
16 makeLocalSourceStore,20 makeLocalSourceStore,
17 makeLocalStateStore,21 makeLocalStateStore,
22 makeLocalToolArtifactStore,
18} from "@executor-v2/persistence-local";23} from "@executor-v2/persistence-local";
24import { type RuntimeToolCallResult } from "@executor-v2/sdk";
19import { makeCloudflareWorkerLoaderRuntimeAdapter } from "@executor-v2/runtime-cloudflare-worker-loader";25import { makeCloudflareWorkerLoaderRuntimeAdapter } from "@executor-v2/runtime-cloudflare-worker-loader";
20import { makeDenoSubprocessRuntimeAdapter } from "@executor-v2/runtime-deno-subprocess";26import { makeDenoSubprocessRuntimeAdapter } from "@executor-v2/runtime-deno-subprocess";
21import { makeLocalInProcessRuntimeAdapter } from "@executor-v2/runtime-local-inproc";27import { makeLocalInProcessRuntimeAdapter } from "@executor-v2/runtime-local-inproc";
@@ -24,7 +30,6 @@ import * as Layer from "effect/Layer"; · 2 unmodified lines
24 30
25import { PmActorLive } from "./actor";31import { PmActorLive } from "./actor";
26import { createPmApprovalsService } from "./approvals-service";32import { createPmApprovalsService } from "./approvals-service";
27import { createPmResolveToolCredentials } from "./credential-resolver";
28import { startPmHttpServer } from "./http-server";33import { startPmHttpServer } from "./http-server";
29import { createPmMcpHandler } from "./mcp-handler";34import { createPmMcpHandler } from "./mcp-handler";
30import { createPmExecuteRuntimeRun } from "./runtime-execution-port";35import { 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
54const readConfiguredWorkspaceId = (value: string | undefined): string => {
55 const normalized = value?.trim();
56 return normalized && normalized.length > 0 ? normalized : "ws_local";
57};
58
59const formatRuntimeAdapterError = (error: RuntimeAdapterError): string =>
60 error.details ? `${error.message}: ${error.details}` : error.message;
61
49const port = parsePort(process.env.PORT);62const port = parsePort(process.env.PORT);
63const workspaceId = readConfiguredWorkspaceId(process.env.PM_WORKSPACE_ID);
50 64
51const pmRuntimeAdapters = [65const pmRuntimeAdapters = [
52 makeLocalInProcessRuntimeAdapter(),66 makeLocalInProcessRuntimeAdapter(),
@@ -55,7 +69,6 @@ const pmRuntimeAdapters = [ · 2 unmodified lines
55];69];
56 70
57const runtimeAdapters = makeRuntimeAdapterRegistry(pmRuntimeAdapters);71const runtimeAdapters = makeRuntimeAdapterRegistry(pmRuntimeAdapters);
58
59const defaultRuntimeKind =72const 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
87const toolArtifactStore = await Effect.runPromise(
88 makeLocalToolArtifactStore({
89 rootDir: pmStateRootDir,
90 }).pipe(Effect.provide(BunContext.layer)),
91);
92
74const sourceCatalog = makeSourceCatalogService(sourceStore);93const sourceCatalog = makeSourceCatalogService(sourceStore);
94const sourceManager = makeSourceManagerService(toolArtifactStore);
95const baseSourcesService = makeControlPlaneSourcesService(sourceCatalog);
96const 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
75const approvalsService = createPmApprovalsService(localStateStore);133const approvalsService = createPmApprovalsService(localStateStore);
76const controlPlaneService = makeControlPlaneService({134const 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
86const resolveCredentials = createPmResolveToolCredentials(localStateStore);144const toolProviderRegistry = makeToolProviderRegistry([makeOpenApiToolProvider()]);
87const invokeRuntimeTool = createUnimplementedRuntimeToolInvoker("pm");145const toolRegistry = createSourceToolRegistry({
88const handleToolCall = createRuntimeToolCallHandler({146 workspaceId,
89 resolveCredentials,147 sourceStore,
90 invokeRuntimeTool,148 toolArtifactStore,
149 toolProviderRegistry,
91});150});
151const runtimeToolCallService = createRuntimeToolCallService(toolRegistry);
92 152
93const executeRuntimeRun = createPmExecuteRuntimeRun({153const executeRuntimeRun = createPmExecuteRuntimeRun({
94 defaultRuntimeKind,154 defaultRuntimeKind,
95 runtimeAdapters,155 runtimeAdapters,
96 handleToolCall,156 toolRegistry,
97});157});
98 158
99const runExecutor = createRunExecutor(executeRuntimeRun);159const runExecutor = createRunExecutor(executeRuntimeRun);
100const handleMcp = createPmMcpHandler(runExecutor.executeRun);160const handleMcp = createPmMcpHandler(runExecutor.executeRun);
101 161
102const handleToolCallHttp = createPmToolCallHttpHandler((input) =>162const 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
106const server = startPmHttpServer({180const server = startPmHttpServer({
v2/apps/pm/src/runtime-execution-port.ts+6−43
@@ -1,62 +1,25 @@
1import {1import {
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";
10import {
11 type RuntimeToolCallRequest,
12 type RuntimeToolCallResult,
13} from "@executor-v2/sdk";
14import * as Effect from "effect/Effect";10import * as Effect from "effect/Effect";
15 11
16export type PmRuntimeExecutionPortOptions = {12export 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
24const 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
57export const createPmExecuteRuntimeRun = (18export 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.runtimeAdapters25 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(
v2/packages/engine/src/index.ts+2−0
@@ -33,6 +33,8 @@ export { · 32 unmodified lines
33 type ToolRegistryToolSummary,33 type ToolRegistryToolSummary,
34} from "./tool-registry";34} from "./tool-registry";
35 35
36export { createSourceToolRegistry } from "./source-tool-registry";
37
36export {38export {
37 makeOpenApiToolProvider,39 makeOpenApiToolProvider,
38 openApiToolDescriptorsFromManifest,40 openApiToolDescriptorsFromManifest,
v2/packages/engine/src/source-tool-registry.test.ts+270−0
@@ -0,0 +1,270 @@
1import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
2
3import {
4 HttpApi,
5 HttpApiEndpoint,
6 HttpApiGroup,
7 HttpApiSchema,
8 OpenApi,
9} from "@effect/platform";
10import { describe, expect, it } from "@effect/vitest";
11import { makeSourceManagerService } from "@executor-v2/management-api";
12import {
13 type SourceStore,
14 type ToolArtifactStore,
15} from "@executor-v2/persistence-ports";
16import { SourceSchema, type Source, type ToolArtifact } from "@executor-v2/schema";
17import * as Data from "effect/Data";
18import * as Effect from "effect/Effect";
19import * as Option from "effect/Option";
20import * as Schema from "effect/Schema";
21
22import { makeOpenApiToolProvider } from "./openapi-provider";
23import { makeToolProviderRegistry } from "./tool-providers";
24import { createSourceToolRegistry } from "./source-tool-registry";
25
26const decodeSource = Schema.decodeUnknownSync(SourceSchema);
27
28type TestServer = {
29 baseUrl: string;
30 requests: Array<string>;
31 close: () => Promise<void>;
32};
33
34class TestServerReleaseError extends Data.TaggedError("TestServerReleaseError")<{
35 message: string;
36}> {}
37
38const githubOwnerParam = HttpApiSchema.param("owner", Schema.String);
39const githubRepoParam = HttpApiSchema.param("repo", Schema.String);
40
41class GitHubReposApi extends HttpApiGroup.make("repos").add(
42 HttpApiEndpoint.get("getRepo")`/repos/${githubOwnerParam}/${githubRepoParam}`.addSuccess(
43 Schema.Unknown,
44 ),
45) {}
46
47class GitHubApi extends HttpApi.make("github").add(GitHubReposApi) {}
48
49const githubOpenApiSpec = OpenApi.fromApi(GitHubApi);
50
51const 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
57const 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
70const 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
131describe("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});
v2/packages/engine/src/source-tool-registry.ts+365−0
@@ -0,0 +1,365 @@
1import {
2 type SourceStore,
3 type SourceStoreError,
4 type ToolArtifactStore,
5 type ToolArtifactStoreError,
6} from "@executor-v2/persistence-ports";
7import type { Source, WorkspaceId } from "@executor-v2/schema";
8import * as Effect from "effect/Effect";
9import * as Option from "effect/Option";
10
11import { openApiToolDescriptorsFromManifest } from "./openapi-provider";
12import {
13 RuntimeAdapterError,
14 type RuntimeAdapterKind,
15} from "./runtime-adapters";
16import type {
17 CanonicalToolDescriptor,
18 ToolProviderError,
19 ToolProviderRegistry,
20 ToolProviderRegistryError,
21} from "./tool-providers";
22import type {
23 ToolRegistry,
24 ToolRegistryCatalogNamespacesInput,
25 ToolRegistryCatalogNamespacesOutput,
26 ToolRegistryCatalogToolsInput,
27 ToolRegistryCatalogToolsOutput,
28 ToolRegistryCallInput,
29 ToolRegistryDiscoverInput,
30 ToolRegistryDiscoverOutput,
31 ToolRegistryToolSummary,
32} from "./tool-registry";
33
34const sourceToolRegistryRuntimeKind: RuntimeAdapterKind = "source-tool-registry";
35
36type SourceToolRegistryOptions = {
37 workspaceId: string;
38 sourceStore: SourceStore;
39 toolArtifactStore: ToolArtifactStore;
40 toolProviderRegistry: ToolProviderRegistry;
41};
42
43type SourceToolEntry = {
44 path: string;
45 namespace: string;
46 source: Source;
47 descriptor: CanonicalToolDescriptor;
48};
49
50const 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
62const 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
72const 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
82const toolProviderErrorToRuntimeAdapterError = (
83 operation: string,
84 cause: ToolProviderError,
85): RuntimeAdapterError =>
86 toRuntimeAdapterError(operation, cause.message, cause.details ?? null);
87
88const toolProviderRegistryErrorToRuntimeAdapterError = (
89 operation: string,
90 cause: ToolProviderRegistryError,
91): RuntimeAdapterError =>
92 toRuntimeAdapterError(operation, cause.message, null);
93
94const 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
104const sourceNamespace = (source: Source): string => {
105 const sourceIdSuffix = source.id.slice(-6).toLowerCase();
106 return `${normalizeNamespacePart(source.name)}_${sourceIdSuffix}`;
107};
108
109const sourceToolPath = (
110 source: Source,
111 descriptor: CanonicalToolDescriptor,
112): string => `${sourceNamespace(source)}.${descriptor.toolId}`;
113
114const 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
147const summaryFromEntry = (entry: SourceToolEntry): ToolRegistryToolSummary => ({
148 path: entry.path,
149 source: entry.source.name,
150 approval: "auto",
151 description: entry.descriptor.description ?? undefined,
152});
153
154const 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
164const 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
176const 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
235const 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
259const 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
288const 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
310export 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});
v2/packages/state-contracts/src/source-store.ts+2−1
@@ -6,7 +6,8 @@ import type * as Option from "effect/Option"; · 5 unmodified lines
6 6
7export class SourceStoreError extends Data.TaggedError("SourceStoreError")<{7export 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;
v2/packages/state-contracts/src/tool-artifacts.ts+2−1
@@ -6,7 +6,8 @@ import type * as Option from "effect/Option"; · 5 unmodified lines
6 6
7export class ToolArtifactStoreError extends Data.TaggedError("ToolArtifactStoreError")<{7export 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;
v2/packages/state-local-file/src/local-state-store.ts+2−1
@@ -8,7 +8,8 @@ import type { LocalStateSnapshot } from "./local-state-snapshot"; · 7 unmodified lines
8 8
9export class LocalStateStoreError extends Data.TaggedError("LocalStateStoreError")<{9export 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;
v2/packages/state-local-file/src/persistence-error.ts+17−8
@@ -5,7 +5,8 @@ import * as ParseResult from "effect/ParseResult"; · 4 unmodified lines
5 5
6export type PersistenceErrorData = {6export 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
14type PersistenceErrorFactory<E> = (data: PersistenceErrorData) => E;15type PersistenceErrorFactory<E> = (data: PersistenceErrorData) => E;
15 16
17const defaultPersistenceBackend = "local-file";
18
16export const withPlatformPersistenceError =19export 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
60export const toSchemaPersistenceError = <E>(65export 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
79export const toInvalidDataError = <E>(86export 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,