History

feat: modularize v2 control plane and remove rpc legacy

Split control-plane and Convex handlers into domain-focused modules, keep v1-only transport routes, and remove obsolete rpc/state-convex scaffolding so local and remote share the same Effect HttpApi architecture.

Rhys Sullivan committed 8 months ago

Commit fc53fa81 parent 31348e650 files · +1379 −526Browse files

Unified · Split

50 files
  1. v2/PLAN.md +10 −11
  2. v2/apps/convex/README.md +6 −4
  3. v2/apps/convex/convex/_generated/api.d.ts +10 −2
  4. v2/apps/convex/convex/controlPlane.ts +12 −0
  5. v2/apps/convex/convex/control_plane/errors.ts +75 −0
  6. v2/apps/convex/convex/control_plane/http.ts +65 −0
  7. v2/apps/convex/convex/control_plane/service.ts +41 −0
  8. v2/apps/convex/convex/control_plane/sources.ts +140 −0
  9. v2/apps/convex/convex/executor.test.ts +61 −35
  10. v2/apps/convex/convex/executor.ts +2 −44
  11. v2/apps/convex/convex/http.ts +30 −6
  12. v2/apps/convex/convex/mcp.ts +6 −6
  13. v2/apps/convex/convex/rpc_exit.ts +0 −50
  14. v2/apps/convex/convex/run_executor.ts +27 −32
  15. v2/apps/convex/convex/runtimeCallbacks.ts +55 −84
  16. v2/apps/convex/convex/schema.ts +56 −1
  17. v2/apps/convex/package.json +2 −1
  18. v2/apps/pm/README.md +5 −2
  19. v2/apps/pm/package.json +2 −0
  20. v2/apps/pm/src/execute.e2e.test.ts +1 −1
  21. v2/apps/pm/src/http-server.ts +25 −4
  22. v2/apps/pm/src/main.ts +13 −1
  23. v2/apps/pm/src/run-executor.ts +11 −15
  24. v2/apps/pm/src/tool-call-handler.ts +21 −22
  25. v2/bun.lock +17 −17
  26. v2/packages/control-plane/package.json +6 −4
  27. v2/packages/control-plane/src/api.ts +26 −0
  28. v2/packages/control-plane/src/client.ts +159 −0
  29. v2/packages/control-plane/src/errors.ts +22 −0
  30. v2/packages/control-plane/src/http.ts +20 −0
  31. v2/packages/control-plane/src/index.ts +44 −0
  32. v2/packages/control-plane/src/service.ts +37 −0
  33. v2/packages/control-plane/src/sources/api.ts +51 −0
  34. v2/packages/control-plane/src/sources/http.ts +67 −0
  35. v2/packages/control-plane/src/sources/index.ts +16 −0
  36. v2/packages/control-plane/src/sources/service.ts +45 −0
  37. v2/packages/control-plane/tsconfig.json +4 −1
  38. v2/packages/engine/src/run-client.ts +17 −18
  39. v2/packages/rpc/README.md +0 −3
  40. v2/packages/rpc/package.json +0 −8
  41. v2/packages/rpc/src/index.ts +0 −1
  42. v2/packages/source-manager/package.json +2 −1
  43. v2/packages/source-manager/src/index.ts +12 −0
  44. v2/packages/source-manager/src/source-catalog.ts +158 −0
  45. v2/packages/state-convex/README.md +0 −8
  46. v2/packages/state-convex/src/convex-ids.ts +0 −25
  47. v2/packages/state-convex/src/index.ts +0 −26
  48. v2/packages/state-convex/src/profiles.ts +0 −33
  49. v2/packages/state-convex/src/schema.ts +0 −56
  50. v2/packages/state-convex/tsconfig.json +0 −4
v2/PLAN.md+10−11
@@ -64,7 +64,7 @@ Control is optional capability, not required for execute-only environments. · 63 unmodified lines
64How clients talk to executor.64How clients talk to executor.
65 65
66- MCP server/gateway66- MCP server/gateway
67- OpenAPI/RPC control API67- OpenAPI control API
68- AI SDK tool bridge68- AI SDK tool bridge
69 69
70Transport does not own core execution semantics.70Transport does not own core execution semantics.
@@ -167,7 +167,7 @@ This is mostly transport/API behavior; core runtime pipeline stays the same. · 96 unmodified lines
167 167
168---168---
169 169
170## 8) MCP, RPC, and AI SDK strategy170## 8) MCP, OpenAPI, and AI SDK strategy
171 171
172### MCP172### MCP
173 173
@@ -179,7 +179,7 @@ Top-level model-facing tools stay minimal: · 5 unmodified lines
179 179
180### Control API180### Control API
181 181
182Control is exposed through Executor OpenAPI/RPC contracts.182Control is exposed through Executor OpenAPI contracts.
183 183
184- Host/app control calls should use control API directly.184- Host/app control calls should use control API directly.
185- Model control calls can use `tools.executor.*` inside `execute` code.185- Model control calls can use `tools.executor.*` inside `execute` code.
@@ -287,16 +287,15 @@ v2/ · 101 unmodified lines
287 schema/287 schema/
288 state-contracts/288 state-contracts/
289 state-local-file/289 state-local-file/
290 state-convex/
291 domain/290 domain/
292 approvals/291 approvals/
293 source-manager/292 source-manager/
293 control-plane/
294 engine/294 engine/
295 mcp-gateway/295 mcp-gateway/
296 oauth/296 oauth/
297 secrets/297 secrets/
298 sync/298 sync/
299 rpc/
300 sdk/299 sdk/
301 ai-sdk-adapter/300 ai-sdk-adapter/
302```301```
@@ -313,15 +312,15 @@ Planned follow-up split (after behavior stabilizes): · 10 unmodified lines
313confect -> schema -> state-contracts312confect -> schema -> state-contracts
314 313
315state-contracts -> state-local-file314state-contracts -> state-local-file
316state-contracts -> state-convex
317 315
318schema + state-contracts + oauth + secrets -> domain316schema + state-contracts + oauth + secrets -> domain
319domain -> approvals/source-manager/engine317domain -> approvals/source-manager/engine
318source-manager + schema -> control-plane
320approvals/source-manager/engine -> mcp-gateway319approvals/source-manager/engine -> mcp-gateway
321domain/features -> rpc -> sdk -> ai-sdk-adapter320domain/features -> sdk -> ai-sdk-adapter
322 321
323apps/pm -> mcp-gateway + domain + state-local-file + rpc322apps/pm -> mcp-gateway + domain + state-local-file + control-plane
324apps/convex -> mcp-gateway + domain + state-convex + rpc323apps/convex -> mcp-gateway + domain + control-plane
325apps/cli/web -> sdk (and/or ai-sdk bridge)324apps/cli/web -> sdk (and/or ai-sdk bridge)
326```325```
327Rules:326Rules:
@@ -367,7 +366,7 @@ Rules: · 39 unmodified lines
367### Phase 5: transport polish366### Phase 5: transport polish
368 367
369- MCP surface stabilization368- MCP surface stabilization
370- OpenAPI/RPC control surface stabilization369- OpenAPI control surface stabilization
371- AI SDK bridge (`toAiSdkTools`) and docs/examples370- AI SDK bridge (`toAiSdkTools`) and docs/examples
372 371
373---372---
@@ -414,7 +413,7 @@ Still missing: · 40 unmodified lines
414- shared tool invocation service wired with persisted approval lifecycle in v2 path413- shared tool invocation service wired with persisted approval lifecycle in v2 path
415- cloudflare-worker-loader runtime adapter implementation in v2 runtime path414- cloudflare-worker-loader runtime adapter implementation in v2 runtime path
416- parity conformance tests for runtime/provider/pending-approval/cancel flows415- parity conformance tests for runtime/provider/pending-approval/cancel flows
417- full rpc/sdk/ai-sdk adapter wiring and usage examples416- full openapi/sdk/ai-sdk adapter wiring and usage examples
418 417
419---418---
420 419
v2/apps/convex/README.md+6−4
@@ -3,7 +3,9 @@ · 2 unmodified lines
3Convex remote server app scaffold for Executor v2.3Convex remote server app scaffold for Executor v2.
4 4
5Current scaffold includes:5Current scaffold includes:
6 Convex schema wiring at `convex/schema.ts`, sourced from `@executor-v2/persistence-convex`6 Convex schema wiring lives directly in `convex/schema.ts`
7 MCP HTTP endpoint at `convex/http.ts` and `convex/mcp.ts`, wired through a run client7 MCP HTTP endpoint at `GET/POST/DELETE /v1/mcp` via `convex/http.ts` and `convex/mcp.ts`
8 runtime callback endpoints at `POST /runtime/tool-call` and `POST /v1/runtime/tool-call`8 runtime callback endpoint at `POST /v1/runtime/tool-call`
9 action-level callback handler scaffold in `convex/runtimeCallbacks.ts`9 control-plane source endpoints at `GET/POST /v1/workspaces/:workspaceId/sources`
10 control-plane source removal endpoint at `DELETE /v1/workspaces/:workspaceId/sources/:sourceId`
11 generated OpenAPI spec endpoint at `GET /v1/openapi.json`
v2/apps/convex/convex/_generated/api.d.ts+10−2
@@ -8,10 +8,14 @@ · 7 unmodified lines
8 * @module8 * @module
9 */9 */
10 10
11import type * as controlPlane from "../controlPlane.js";
12import type * as control_plane_errors from "../control_plane/errors.js";
13import type * as control_plane_http from "../control_plane/http.js";
14import type * as control_plane_service from "../control_plane/service.js";
15import type * as control_plane_sources from "../control_plane/sources.js";
11import type * as executor from "../executor.js";16import type * as executor from "../executor.js";
12import type * as http from "../http.js";17import type * as http from "../http.js";
13import type * as mcp from "../mcp.js";18import type * as mcp from "../mcp.js";
14import type * as rpc_exit from "../rpc_exit.js";
15import type * as run_executor from "../run_executor.js";19import type * as run_executor from "../run_executor.js";
16import type * as runtimeCallbacks from "../runtimeCallbacks.js";20import type * as runtimeCallbacks from "../runtimeCallbacks.js";
17import type * as runtime_adapter from "../runtime_adapter.js";21import type * as runtime_adapter from "../runtime_adapter.js";
@@ -23,10 +27,14 @@ import type { · 5 unmodified lines
23} from "convex/server";27} from "convex/server";
24 28
25declare const fullApi: ApiFromModules<{29declare const fullApi: ApiFromModules<{
30 controlPlane: typeof controlPlane;
31 "control_plane/errors": typeof control_plane_errors;
32 "control_plane/http": typeof control_plane_http;
33 "control_plane/service": typeof control_plane_service;
34 "control_plane/sources": typeof control_plane_sources;
26 executor: typeof executor;35 executor: typeof executor;
27 http: typeof http;36 http: typeof http;
28 mcp: typeof mcp;37 mcp: typeof mcp;
29 rpc_exit: typeof rpc_exit;
30 run_executor: typeof run_executor;38 run_executor: typeof run_executor;
31 runtimeCallbacks: typeof runtimeCallbacks;39 runtimeCallbacks: typeof runtimeCallbacks;
32 runtime_adapter: typeof runtime_adapter;40 runtime_adapter: typeof runtime_adapter;
v2/apps/convex/convex/controlPlane.ts+12−0
@@ -0,0 +1,12 @@
1import {
2 listSources as listSourcesImpl,
3 removeSource as removeSourceImpl,
4 upsertSource as upsertSourceImpl,
5} from "./control_plane/sources";
6import { controlPlaneHttpHandler as controlPlaneHttpHandlerImpl } from "./control_plane/http";
7
8export const listSources = listSourcesImpl;
9export const upsertSource = upsertSourceImpl;
10export const removeSource = removeSourceImpl;
11
12export const controlPlaneHttpHandler = controlPlaneHttpHandlerImpl;
v2/apps/convex/convex/control_plane/errors.ts+75−0
@@ -0,0 +1,75 @@
1import { SourceStoreError } from "@executor-v2/persistence-ports";
2
3type ControlPlaneErrorCode =
4 | "bad_request"
5 | "not_found"
6 | "method_not_allowed"
7 | "internal";
8
9type ControlPlaneErrorResponse = {
10 ok: false;
11 error: {
12 code: ControlPlaneErrorCode;
13 message: string;
14 details: string | null;
15 };
16};
17
18const renderCause = (cause: unknown): string => String(cause);
19
20export const toSourceStoreError = (
21 operation: string,
22 cause: unknown,
23): SourceStoreError =>
24 new SourceStoreError({
25 operation,
26 filePath: "convex://control-plane",
27 message: "Convex control plane call failed",
28 reason: null,
29 details: renderCause(cause),
30 });
31
32const toErrorCode = (status: number): ControlPlaneErrorCode => {
33 switch (status) {
34 case 400:
35 return "bad_request";
36 case 404:
37 return "not_found";
38 case 405:
39 return "method_not_allowed";
40 default:
41 return "internal";
42 }
43};
44
45export const controlPlaneErrorResponse = (
46 status: number,
47 message: string,
48 details: string | null,
49): Response =>
50 Response.json(
51 {
52 ok: false,
53 error: {
54 code: toErrorCode(status),
55 message,
56 details,
57 },
58 } satisfies ControlPlaneErrorResponse,
59 { status },
60 );
61
62const isJsonResponse = (response: Response): boolean =>
63 response.headers.get("content-type")?.toLowerCase().includes("application/json") ?? false;
64
65export const normalizeControlPlaneErrorResponse = (response: Response): Response => {
66 if (response.ok || isJsonResponse(response)) {
67 return response;
68 }
69
70 return controlPlaneErrorResponse(
71 response.status,
72 "Control plane request failed",
73 response.statusText || null,
74 );
75};
v2/apps/convex/convex/control_plane/http.ts+65−0
@@ -0,0 +1,65 @@
1import {
2 ControlPlaneService,
3 controlPlaneOpenApiSpec,
4 makeControlPlaneWebHandler,
5} from "@executor-v2/control-plane";
6import * as Effect from "effect/Effect";
7import * as Layer from "effect/Layer";
8
9import { httpAction, type ActionCtx } from "../_generated/server";
10import {
11 controlPlaneErrorResponse,
12 normalizeControlPlaneErrorResponse,
13 toSourceStoreError,
14} from "./errors";
15import { makeConvexControlPlaneService } from "./service";
16
17const isOpenApiRequest = (request: Request): boolean =>
18 new URL(request.url).pathname === "/v1/openapi.json";
19
20const handleControlPlaneRequest = (
21 ctx: ActionCtx,
22 request: Request,
23): Effect.Effect<Response, never> =>
24 Effect.scoped(
25 Effect.gen(function* () {
26 if (isOpenApiRequest(request)) {
27 return Response.json(controlPlaneOpenApiSpec, { status: 200 });
28 }
29
30 const webHandler = makeControlPlaneWebHandler(
31 Layer.succeed(ControlPlaneService, makeConvexControlPlaneService(ctx)),
32 );
33
34 yield* Effect.addFinalizer(() =>
35 Effect.tryPromise({
36 try: () => webHandler.dispose(),
37 catch: (cause) => toSourceStoreError("controlPlane.dispose", cause),
38 }).pipe(
39 Effect.asVoid,
40 Effect.catchAll(() => Effect.void),
41 ),
42 );
43
44 const response = yield* Effect.tryPromise({
45 try: () => webHandler.handler(request),
46 catch: (cause) => toSourceStoreError("controlPlane.http", cause),
47 });
48
49 return normalizeControlPlaneErrorResponse(response);
50 }),
51 ).pipe(
52 Effect.catchAll((error) =>
53 Effect.succeed(
54 controlPlaneErrorResponse(
55 500,
56 "Control plane request failed",
57 error.details,
58 ),
59 ),
60 ),
61 );
62
63export const controlPlaneHttpHandler = httpAction((ctx, request) =>
64 Effect.runPromise(handleControlPlaneRequest(ctx, request)),
65);
v2/apps/convex/convex/control_plane/service.ts+41−0
@@ -0,0 +1,41 @@
1import {
2 ControlPlaneService,
3 type ControlPlaneServiceShape,
4} from "@executor-v2/control-plane";
5import * as Effect from "effect/Effect";
6
7import { api } from "../_generated/api";
8import type { ActionCtx } from "../_generated/server";
9import { toSourceStoreError } from "./errors";
10
11export const makeConvexControlPlaneService = (
12 ctx: ActionCtx,
13): ControlPlaneServiceShape =>
14 ControlPlaneService.of({
15 listSources: (workspaceId) =>
16 Effect.tryPromise({
17 try: () =>
18 ctx.runQuery(api.controlPlane.listSources, {
19 workspaceId,
20 }),
21 catch: (cause) => toSourceStoreError("controlPlane.listSources", cause),
22 }),
23 upsertSource: (input) =>
24 Effect.tryPromise({
25 try: () =>
26 ctx.runMutation(api.controlPlane.upsertSource, {
27 workspaceId: input.workspaceId,
28 payload: input.payload,
29 }),
30 catch: (cause) => toSourceStoreError("controlPlane.upsertSource", cause),
31 }),
32 removeSource: (input) =>
33 Effect.tryPromise({
34 try: () =>
35 ctx.runMutation(api.controlPlane.removeSource, {
36 workspaceId: input.workspaceId,
37 sourceId: input.sourceId,
38 }),
39 catch: (cause) => toSourceStoreError("controlPlane.removeSource", cause),
40 }),
41 });
v2/apps/convex/convex/control_plane/sources.ts+140−0
@@ -0,0 +1,140 @@
1import { type UpsertSourcePayload } from "@executor-v2/control-plane";
2import { SourceSchema, type Source } from "@executor-v2/schema";
3import { v } from "convex/values";
4import * as Schema from "effect/Schema";
5
6import { mutation, query } from "../_generated/server";
7
8const decodeSource = Schema.decodeUnknownSync(SourceSchema);
9
10const sourceStoreKey = (source: Source): string => `${source.workspaceId}:${source.id}`;
11
12const sortSources = (sources: ReadonlyArray<Source>): Array<Source> =>
13 [...sources].sort((left, right) => {
14 const leftName = left.name.toLowerCase();
15 const rightName = right.name.toLowerCase();
16
17 if (leftName === rightName) {
18 return sourceStoreKey(left).localeCompare(sourceStoreKey(right));
19 }
20
21 return leftName.localeCompare(rightName);
22 });
23
24const toSource = (document: Record<string, unknown>): Source => {
25 const { _id: _ignoredId, _creationTime: _ignoredCreationTime, ...source } = document;
26 return decodeSource(source);
27};
28
29const sourceKindValidator = v.union(
30 v.literal("mcp"),
31 v.literal("openapi"),
32 v.literal("graphql"),
33 v.literal("internal"),
34);
35
36const sourceStatusValidator = v.union(
37 v.literal("draft"),
38 v.literal("probing"),
39 v.literal("auth_required"),
40 v.literal("connected"),
41 v.literal("error"),
42);
43
44export const listSources = query({
45 args: {
46 workspaceId: v.string(),
47 },
48 handler: async (ctx, args): Promise<Array<Source>> => {
49 const documents = await ctx.db
50 .query("sources")
51 .withIndex("by_workspaceId", (q) => q.eq("workspaceId", args.workspaceId))
52 .collect();
53
54 return sortSources(
55 documents.map((document) =>
56 toSource(document as unknown as Record<string, unknown>),
57 ),
58 );
59 },
60});
61
62export const upsertSource = mutation({
63 args: {
64 workspaceId: v.string(),
65 payload: v.object({
66 id: v.optional(v.string()),
67 name: v.string(),
68 kind: sourceKindValidator,
69 endpoint: v.string(),
70 status: v.optional(sourceStatusValidator),
71 enabled: v.optional(v.boolean()),
72 configJson: v.optional(v.string()),
73 sourceHash: v.optional(v.union(v.string(), v.null())),
74 lastError: v.optional(v.union(v.string(), v.null())),
75 }),
76 },
77 handler: async (ctx, args): Promise<Source> => {
78 const now = Date.now();
79 const sourceId = args.payload.id ?? `src_${crypto.randomUUID()}`;
80
81 const existing = await ctx.db
82 .query("sources")
83 .withIndex("by_domainId", (q) => q.eq("id", sourceId))
84 .unique();
85
86 const existingInWorkspace =
87 existing && existing.workspaceId === args.workspaceId ? existing : null;
88
89 const payload = args.payload as UpsertSourcePayload;
90
91 const source = decodeSource({
92 id: sourceId,
93 workspaceId: args.workspaceId,
94 name: payload.name,
95 kind: payload.kind,
96 endpoint: payload.endpoint,
97 status: payload.status ?? "draft",
98 enabled: payload.enabled ?? true,
99 configJson: payload.configJson ?? "{}",
100 sourceHash: payload.sourceHash ?? null,
101 lastError: payload.lastError ?? null,
102 createdAt: existingInWorkspace?.createdAt ?? now,
103 updatedAt: now,
104 });
105
106 if (existingInWorkspace) {
107 await ctx.db.patch(existingInWorkspace._id, source);
108 } else {
109 await ctx.db.insert("sources", source);
110 }
111
112 return source;
113 },
114});
115
116export const removeSource = mutation({
117 args: {
118 workspaceId: v.string(),
119 sourceId: v.string(),
120 },
121 handler: async (
122 ctx,
123 args,
124 ): Promise<{
125 removed: boolean;
126 }> => {
127 const existing = await ctx.db
128 .query("sources")
129 .withIndex("by_domainId", (q) => q.eq("id", args.sourceId))
130 .unique();
131
132 if (!existing || existing.workspaceId !== args.workspaceId) {
133 return { removed: false };
134 }
135
136 await ctx.db.delete(existing._id);
137
138 return { removed: true };
139 },
140});
v2/apps/convex/convex/executor.test.ts+61−35
@@ -3,7 +3,7 @@ import { convexTest } from "convex-test"; · 2 unmodified lines
3import * as Effect from "effect/Effect";3import * as Effect from "effect/Effect";
4 4
5import { api } from "./_generated/api";5import { api } from "./_generated/api";
6import { unwrapRpcSuccess } from "./rpc_exit";6import { executeRunImpl } from "./executor";
7import schema from "./schema";7import schema from "./schema";
8 8
9const setup = () =>9const setup = () =>
@@ -12,54 +12,80 @@ const setup = () => · 2 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 "./controlPlane.ts": () => import("./controlPlane"),
15 "./_generated/api.js": () => import("./_generated/api.js"),16 "./_generated/api.js": () => import("./_generated/api.js"),
16 });17 });
17 18
18describe("Convex executor actions", () => {19describe("Convex executor and control-plane", () => {
19 it.effect("executes code via executeRun action", () =>20 it.effect("executes code via executeRunImpl", () =>
20 Effect.gen(function* () {21 Effect.gen(function* () {
21 const t = setup();22 const result = yield* executeRunImpl({
22 23 code: "return 6 * 7;",
23 const result = unwrapRpcSuccess<{24 });
24 status: string;
25 result?: unknown;
26 }>(
27 yield* Effect.tryPromise(() =>
28 t.action(api.executor.executeRun, {
29 code: "return 6 * 7;",
30 }),
31 ),
32 "executor.executeRun",
33 );
34 25
35 expect(result.status).toBe("completed");26 expect(result.status).toBe("completed");
36 expect(result.result).toBe(42);27 expect(result.result).toBe(42);
37 }),28 }),
38 );29 );
39 30
40 it.effect("returns failed callback result from runtime callback action", () =>31 it.effect("upserts, lists, and removes sources", () =>
41 Effect.gen(function* () {32 Effect.gen(function* () {
42 const t = setup();33 const t = setup();
43 34
44 const result = unwrapRpcSuccess<{35 const added = (yield* Effect.tryPromise(() =>
45 ok: boolean;36 t.mutation(api.controlPlane.upsertSource, {
46 kind?: string;37 workspaceId: "ws_1",
47 error?: string;38 payload: {
48 }>(39 id: "src_1",
49 yield* Effect.tryPromise(() =>40 name: "Weather",
50 t.action(api.runtimeCallbacks.handleToolCall, {41 kind: "openapi",
51 runId: "run_1",42 endpoint: "https://example.com/openapi.json",
52 callId: "call_1",43 enabled: true,
53 toolPath: "tools.example.lookup",44 configJson: "{}",
54 input: { query: "ping" },45 status: "draft",
55 }),46 sourceHash: null,
56 ),47 lastError: null,
57 "runtimeCallbacks.handleToolCall",48 },
58 );49 }),
50 )) as {
51 id: string;
52 workspaceId: string;
53 name: string;
54 };
55
56 expect(added.id).toBe("src_1");
57 expect(added.workspaceId).toBe("ws_1");
58 expect(added.name).toBe("Weather");
59
60 const listed = (yield* Effect.tryPromise(() =>
61 t.query(api.controlPlane.listSources, {
62 workspaceId: "ws_1",
63 }),
64 )) as Array<{
65 id: string;
66 }>;
67
68 expect(listed).toHaveLength(1);
69 expect(listed[0]?.id).toBe("src_1");
70
71 const removed = (yield* Effect.tryPromise(() =>
72 t.mutation(api.controlPlane.removeSource, {
73 workspaceId: "ws_1",
74 sourceId: "src_1",
75 }),
76 )) as {
77 removed: boolean;
78 };
79
80 expect(removed.removed).toBe(true);
81
82 const listedAfterRemove = (yield* Effect.tryPromise(() =>
83 t.query(api.controlPlane.listSources, {
84 workspaceId: "ws_1",
85 }),
86 )) as Array<unknown>;
59 87
60 expect(result.ok).toBe(false);88 expect(listedAfterRemove).toHaveLength(0);
61 expect(result.kind).toBe("failed");
62 expect(result.error).toContain("tools.example.lookup");
63 }),89 }),
64 );90 );
65});91});
v2/apps/convex/convex/executor.ts+2−44
@@ -1,9 +1,6 @@
1import { createRpcFactory, makeRpcModule } from "@executor-v2/confect/rpc";1import type { ExecuteRunInput, ExecuteRunResult } from "@executor-v2/sdk";
2import { executorConfectSchema } from "@executor-v2/persistence-convex";
3import type { ExecuteRunInput } from "@executor-v2/sdk";
4import * as Effect from "effect/Effect";2import * as Effect from "effect/Effect";
5import * as Layer from "effect/Layer";3import * as Layer from "effect/Layer";
6import * as Schema from "effect/Schema";
7 4
8import {5import {
9 ConvexRunExecutor,6 ConvexRunExecutor,
@@ -11,24 +8,6 @@ import { · 1 unmodified line
11 ConvexToolProviderRegistryLive,8 ConvexToolProviderRegistryLive,
12} from "./run_executor";9} from "./run_executor";
13 10
14const executeRunResultSchema = Schema.Struct({
15 runId: Schema.String,
16 status: Schema.Union(
17 Schema.Literal("completed"),
18 Schema.Literal("failed"),
19 Schema.Literal("timed_out"),
20 Schema.Literal("denied"),
21 ),
22 result: Schema.optional(Schema.Unknown),
23 error: Schema.optional(Schema.String),
24 exitCode: Schema.optional(Schema.Number),
25 durationMs: Schema.optional(Schema.Number),
26});
27
28const factory = createRpcFactory({
29 schema: executorConfectSchema,
30});
31
32const ConvexExecuteDependenciesLive = Layer.merge(11const ConvexExecuteDependenciesLive = Layer.merge(
33 ConvexRunExecutorLive,12 ConvexRunExecutorLive,
34 ConvexToolProviderRegistryLive,13 ConvexToolProviderRegistryLive,
@@ -36,29 +15,8 @@ const ConvexExecuteDependenciesLive = Layer.merge( · 1 unmodified line
36 15
37export const executeRunImpl = (16export const executeRunImpl = (
38 input: ExecuteRunInput,17 input: ExecuteRunInput,
39): Effect.Effect<typeof executeRunResultSchema.Type> =>18): Effect.Effect<ExecuteRunResult> =>
40 Effect.gen(function* () {19 Effect.gen(function* () {
41 const runExecutor = yield* ConvexRunExecutor;20 const runExecutor = yield* ConvexRunExecutor;
42 return yield* runExecutor.executeRun(input);21 return yield* runExecutor.executeRun(input);
43 }).pipe(Effect.provide(ConvexExecuteDependenciesLive));22 }).pipe(Effect.provide(ConvexExecuteDependenciesLive));
44
45const executeRunEndpoint = factory.action({
46 payload: {
47 code: Schema.String,
48 timeoutMs: Schema.optional(Schema.Number),
49 },
50 success: executeRunResultSchema,
51});
52
53executeRunEndpoint.implement((payload) =>
54 executeRunImpl({
55 code: payload.code,
56 timeoutMs: payload.timeoutMs,
57 }),
58);
59
60const executorRpc = makeRpcModule({
61 executeRun: executeRunEndpoint,
62});
63
64export const executeRun = executorRpc.handlers.executeRun;
v2/apps/convex/convex/http.ts+30−6
@@ -1,24 +1,48 @@
1import { httpRouter } from "convex/server";1import { httpRouter } from "convex/server";
2
3import { controlPlaneHttpHandler } from "./controlPlane";
2import { mcpHandler } from "./mcp";4import { mcpHandler } from "./mcp";
3import { handleToolCallHttp } from "./runtimeCallbacks";5import { handleToolCallHttp } from "./runtimeCallbacks";
4 6
5const http = httpRouter();7const http = httpRouter();
6 8
7http.route({ path: "/mcp", method: "POST", handler: mcpHandler });
8http.route({ path: "/mcp", method: "GET", handler: mcpHandler });
9http.route({ path: "/mcp", method: "DELETE", handler: mcpHandler });
10http.route({ path: "/v1/mcp", method: "POST", handler: mcpHandler });9http.route({ path: "/v1/mcp", method: "POST", handler: mcpHandler });
11http.route({ path: "/v1/mcp", method: "GET", handler: mcpHandler });10http.route({ path: "/v1/mcp", method: "GET", handler: mcpHandler });
12http.route({ path: "/v1/mcp", method: "DELETE", handler: mcpHandler });11http.route({ path: "/v1/mcp", method: "DELETE", handler: mcpHandler });
13http.route({12http.route({
14 path: "/runtime/tool-call",13 path: "/v1/runtime/tool-call",
15 method: "POST",14 method: "POST",
16 handler: handleToolCallHttp,15 handler: handleToolCallHttp,
17});16});
18http.route({17http.route({
19 path: "/v1/runtime/tool-call",18 pathPrefix: "/v1/",
19 method: "GET",
20 handler: controlPlaneHttpHandler,
21});
22http.route({
23 pathPrefix: "/v1/",
20 method: "POST",24 method: "POST",
21 handler: handleToolCallHttp,25 handler: controlPlaneHttpHandler,
26});
27http.route({
28 pathPrefix: "/v1/",
29 method: "PUT",
30 handler: controlPlaneHttpHandler,
31});
32http.route({
33 pathPrefix: "/v1/",
34 method: "PATCH",
35 handler: controlPlaneHttpHandler,
36});
37http.route({
38 pathPrefix: "/v1/",
39 method: "DELETE",
40 handler: controlPlaneHttpHandler,
41});
42http.route({
43 pathPrefix: "/v1/",
44 method: "OPTIONS",
45 handler: controlPlaneHttpHandler,
22});46});
23 47
24export default http;48export default http;
v2/apps/convex/convex/mcp.ts+6−6
@@ -1,13 +1,13 @@
1import { handleMcpHttpRequest } from "@executor-v2/mcp-gateway";1import { handleMcpHttpRequest } from "@executor-v2/mcp-gateway";
2import { createExecutorRunClient } from "@executor-v2/sdk";2import { createExecutorRunClient } from "@executor-v2/sdk";
3import { api } from "./_generated/api";3import * as Effect from "effect/Effect";
4
4import { httpAction } from "./_generated/server";5import { httpAction } from "./_generated/server";
5import { unwrapRpcSuccess } from "./rpc_exit";6import { executeRunImpl } from "./executor";
6export const mcpHandler = httpAction(async (ctx, request) => {7
8export const mcpHandler = httpAction(async (_ctx, request) => {
7 const runClient = createExecutorRunClient((input) =>9 const runClient = createExecutorRunClient((input) =>
8 ctx10 Effect.runPromise(executeRunImpl(input)),
9 .runAction(api.executor.executeRun, input)
10 .then((result) => unwrapRpcSuccess(result, "executor.executeRun")),
11 );11 );
12 12
13 return handleMcpHttpRequest(request, {13 return handleMcpHttpRequest(request, {
v2/apps/convex/convex/rpc_exit.ts+0−50
@@ -1,50 +0,0 @@
1type RpcExitSuccess<T> = {
2 _tag: "Success";
3 value: T;
4};
5
6type RpcExitFailure = {
7 _tag: "Failure";
8 cause: unknown;
9};
10
11const isRpcExitSuccess = <T>(value: unknown): value is RpcExitSuccess<T> =>
12 typeof value === "object" &&
13 value !== null &&
14 "_tag" in value &&
15 value._tag === "Success" &&
16 "value" in value;
17
18const isRpcExitFailure = (value: unknown): value is RpcExitFailure =>
19 typeof value === "object" &&
20 value !== null &&
21 "_tag" in value &&
22 value._tag === "Failure";
23
24const renderUnknown = (value: unknown): string => {
25 if (value instanceof Error) {
26 return value.message;
27 }
28
29 try {
30 return JSON.stringify(value);
31 } catch {
32 return String(value);
33 }
34};
35
36export const unwrapRpcSuccess = <T>(value: unknown, endpoint: string): T => {
37 if (isRpcExitSuccess<T>(value)) {
38 return value.value;
39 }
40
41 if (isRpcExitFailure(value)) {
42 throw new Error(
43 `RPC endpoint '${endpoint}' returned failure exit: ${renderUnknown(value.cause)}`,
44 );
45 }
46
47 throw new Error(
48 `RPC endpoint '${endpoint}' returned invalid exit payload: ${renderUnknown(value)}`,
49 );
50};
v2/apps/convex/convex/run_executor.ts+27−32
@@ -1,14 +1,14 @@
1import {1import {
2 makeLocalInProcessRuntimeAdapter,
2 makeToolProviderRegistry,3 makeToolProviderRegistry,
3 ToolProviderRegistryService,4 ToolProviderRegistryService,
4} from "@executor-v2/engine/tool-providers";5 type RuntimeExecuteError,
6} from "@executor-v2/engine";
5import type { ExecuteRunInput, ExecuteRunResult } from "@executor-v2/sdk";7import type { ExecuteRunInput, ExecuteRunResult } from "@executor-v2/sdk";
6import * as Context from "effect/Context";8import * as Context from "effect/Context";
7import * as Effect from "effect/Effect";9import * as Effect from "effect/Effect";
8import * as Layer from "effect/Layer";10import * as Layer from "effect/Layer";
9 11
10import { makeLocalInProcessRuntimeAdapter } from "./runtime_adapter";
11
12export type ConvexRunExecutorService = {12export type ConvexRunExecutorService = {
13 executeRun: (13 executeRun: (
14 input: ExecuteRunInput,14 input: ExecuteRunInput,
@@ -21,21 +21,16 @@ export class ConvexRunExecutor extends Context.Tag( · 6 unmodified lines
21 21
22const runtimeAdapter = makeLocalInProcessRuntimeAdapter();22const runtimeAdapter = makeLocalInProcessRuntimeAdapter();
23 23
24const errorToText = (error: unknown): string => {24const formatRuntimeExecuteError = (error: RuntimeExecuteError): string => {
25 if (error instanceof Error) {25 switch (error._tag) {
26 return error.message;26 case "RuntimeAdapterError":
27 }27 case "LocalCodeRunnerError":
28 28 case "DenoSubprocessRunnerError":
29 if (29 case "ToolProviderError":
30 typeof error === "object" &&30 return error.details ? `${error.message}: ${error.details}` : error.message;
31 error !== null &&31 case "ToolProviderRegistryError":
32 "message" in error &&32 return error.message;
33 typeof error.message === "string"
34 ) {
35 return error.message;
36 }33 }
37
38 return String(error);
39};34};
40 35
41const executeRun = Effect.fn(36const executeRun = Effect.fn(
@@ -59,21 +54,21 @@ const executeRun = Effect.fn( · 17 unmodified lines
59 tools: [],54 tools: [],
60 })55 })
61 .pipe(56 .pipe(
62 Effect.map(57 Effect.map(
63 (result): ExecuteRunResult => ({58 (result): ExecuteRunResult => ({
64 runId,59 runId,
65 status: "completed",60 status: "completed",
66 result,61 result,
67 }),62 }),
68 ),63 ),
69 Effect.catchAll((error) =>64 Effect.catchAll((error) =>
70 Effect.succeed({65 Effect.succeed({
71 runId,66 runId,
72 status: "failed",67 status: "failed",
73 error: errorToText(error),68 error: formatRuntimeExecuteError(error),
74 } satisfies ExecuteRunResult),69 } satisfies ExecuteRunResult),
75 ),70 ),
76 );71 );
77});72});
78 73
79export const ConvexRunExecutorLive = Layer.succeed(74export const ConvexRunExecutorLive = Layer.succeed(
v2/apps/convex/convex/runtimeCallbacks.ts+55−84
@@ -1,27 +1,13 @@
1import { createRpcFactory, makeRpcModule } from "@executor-v2/confect/rpc";
2import { executorConfectSchema } from "@executor-v2/persistence-convex";
3import type {1import type {
4 RuntimeToolCallRequest,2 RuntimeToolCallRequest,
5 RuntimeToolCallResult,3 RuntimeToolCallResult,
6} from "@executor-v2/sdk";4} from "@executor-v2/sdk";
5import * as Data from "effect/Data";
7import * as Effect from "effect/Effect";6import * as Effect from "effect/Effect";
7import * as ParseResult from "effect/ParseResult";
8import * as Schema from "effect/Schema";8import * as Schema from "effect/Schema";
9import { api } from "./_generated/api";
10import { httpAction } from "./_generated/server";
11import { unwrapRpcSuccess } from "./rpc_exit";
12
13const runtimeToolCallResultSchema = Schema.Struct({
14 ok: Schema.Boolean,
15 kind: Schema.optional(Schema.String),
16 error: Schema.optional(Schema.String),
17 value: Schema.optional(Schema.Unknown),
18 approvalId: Schema.optional(Schema.String),
19 retryAfterMs: Schema.optional(Schema.Number),
20});
21 9
22const factory = createRpcFactory({10import { httpAction } from "./_generated/server";
23 schema: executorConfectSchema,
24});
25 11
26export const handleToolCallImpl = (12export const handleToolCallImpl = (
27 input: RuntimeToolCallRequest,13 input: RuntimeToolCallRequest,
@@ -32,35 +18,26 @@ export const handleToolCallImpl = ( · 4 unmodified lines
32 error: `Convex runtime callback received tool '${input.toolPath}', but callback invocation is not wired yet.`,18 error: `Convex runtime callback received tool '${input.toolPath}', but callback invocation is not wired yet.`,
33 });19 });
34 20
35const handleToolCallEndpoint = factory.action({21class RuntimeToolCallBadRequestError extends Data.TaggedError(
36 payload: {22 "RuntimeToolCallBadRequestError",
37 runId: Schema.String,23)<{
38 callId: Schema.String,24 message: string;
39 toolPath: Schema.String,25 details: string;
40 input: Schema.optional(26}> {}
41 Schema.Record({
42 key: Schema.String,
43 value: Schema.Unknown,
44 }),
45 ),
46 },
47 success: runtimeToolCallResultSchema,
48});
49
50handleToolCallEndpoint.implement((payload) =>
51 handleToolCallImpl({
52 runId: payload.runId,
53 callId: payload.callId,
54 toolPath: payload.toolPath,
55 input: payload.input,
56 }),
57);
58 27
59const runtimeCallbacksRpc = makeRpcModule({28const RuntimeToolCallRequestSchema = Schema.Struct({
60 handleToolCall: handleToolCallEndpoint,29 runId: Schema.String,
30 callId: Schema.String,
31 toolPath: Schema.String,
32 input: Schema.optional(
33 Schema.Record({
34 key: Schema.String,
35 value: Schema.Unknown,
36 }),
37 ),
61});38});
62 39
63export const handleToolCall = runtimeCallbacksRpc.handlers.handleToolCall;40const decodeRuntimeToolCallRequest = Schema.decodeUnknown(RuntimeToolCallRequestSchema);
64 41
65const badRequest = (message: string): Response =>42const badRequest = (message: string): Response =>
66 Response.json(43 Response.json(
@@ -72,48 +49,42 @@ const badRequest = (message: string): Response => · 5 unmodified lines
72 { status: 400 },49 { status: 400 },
73 );50 );
74 51
75export const handleToolCallHttp = httpAction(async (ctx, request) => {52const formatBadRequestMessage = (error: RuntimeToolCallBadRequestError): string =>
76 let body: unknown;53 error.details.length > 0 ? `${error.message}: ${error.details}` : error.message;
77 try {
78 body = await request.json();
79 } catch (cause) {
80 return badRequest(
81 `Invalid runtime callback request body: ${
82 cause instanceof Error ? cause.message : String(cause)
83 }`,
84 );
85 }
86
87 if (typeof body !== "object" || body === null) {
88 return badRequest("Runtime callback request body must be an object");
89 }
90 54
91 const payload = body as Partial<RuntimeToolCallRequest>;55const formatUnknownDetails = (cause: unknown): string => String(cause);
92 56
93 if (57const handleToolCallHttpEffect = (
94 typeof payload.runId !== "string" ||58 request: Request,
95 typeof payload.callId !== "string" ||59): Effect.Effect<Response, never> =>
96 typeof payload.toolPath !== "string"60 Effect.gen(function* () {
97 ) {61 const body = yield* Effect.tryPromise({
98 return badRequest("Runtime callback request body is missing required fields");62 try: () => request.json(),
99 }63 catch: (cause) =>
64 new RuntimeToolCallBadRequestError({
65 message: "Invalid runtime callback request body",
66 details: formatUnknownDetails(cause),
67 }),
68 });
100 69
101 let result: RuntimeToolCallResult;70 const input = yield* decodeRuntimeToolCallRequest(body).pipe(
102 try {71 Effect.mapError(
103 result = unwrapRpcSuccess(72 (cause) =>
104 await ctx.runAction(api.runtimeCallbacks.handleToolCall, {73 new RuntimeToolCallBadRequestError({
105 runId: payload.runId,74 message: "Runtime callback request body is invalid",
106 callId: payload.callId,75 details: ParseResult.TreeFormatter.formatErrorSync(cause),
107 toolPath: payload.toolPath,76 }),
108 input:77 ),
109 payload.input && typeof payload.input === "object"
110 ? (payload.input as Record<string, unknown>)
111 : undefined,
112 }),
113 "runtimeCallbacks.handleToolCall",
114 );78 );
115 } catch (cause) {79
116 return badRequest(cause instanceof Error ? cause.message : String(cause));80 const result = yield* handleToolCallImpl(input);
117 }81 return Response.json(result, { status: 200 });
118 return Response.json(result, { status: 200 });82 }).pipe(
119});83 Effect.catchTag("RuntimeToolCallBadRequestError", (error) =>
84 Effect.succeed(badRequest(formatBadRequestMessage(error))),
85 ),
86 );
87
88export const handleToolCallHttp = httpAction((_ctx, request) =>
89 Effect.runPromise(handleToolCallHttpEffect(request)),
90);
v2/apps/convex/convex/schema.ts+56−1
@@ -1,3 +1,58 @@
1import { executorConfectSchema } from "@executor-v2/persistence-convex";1import { defineSchema, defineTable, type TablesFromSchemaDefinition } from "@executor-v2/confect";
2import {
3 ApprovalSchema,
4 CredentialRefSchema,
5 EventEnvelopeSchema,
6 OAuthTokenSchema,
7 PolicySchema,
8 ProfileSchema,
9 SourceSchema,
10 SyncStateSchema,
11 TaskRunSchema,
12 ToolArtifactSchema,
13 WorkspaceSchema,
14} from "@executor-v2/schema";
15
16export const executorConfectSchema = defineSchema({
17 profiles: defineTable(ProfileSchema).index("by_domainId", ["id"]),
18 workspaces: defineTable(WorkspaceSchema)
19 .index("by_domainId", ["id"])
20 .index("by_profileId", ["profileId"]),
21 sources: defineTable(SourceSchema)
22 .index("by_domainId", ["id"])
23 .index("by_workspaceId", ["workspaceId"]),
24 toolArtifacts: defineTable(ToolArtifactSchema)
25 .index("by_domainId", ["id"])
26 .index("by_workspaceId", ["workspaceId"])
27 .index("by_sourceId", ["sourceId"]),
28 credentialRefs: defineTable(CredentialRefSchema)
29 .index("by_domainId", ["id"])
30 .index("by_workspaceId", ["workspaceId"])
31 .index("by_sourceId", ["sourceId"]),
32 oauthTokens: defineTable(OAuthTokenSchema)
33 .index("by_domainId", ["id"])
34 .index("by_workspaceId", ["workspaceId"])
35 .index("by_sourceId", ["sourceId"]),
36 policies: defineTable(PolicySchema)
37 .index("by_domainId", ["id"])
38 .index("by_workspaceId", ["workspaceId"]),
39 approvals: defineTable(ApprovalSchema)
40 .index("by_domainId", ["id"])
41 .index("by_workspaceId", ["workspaceId"])
42 .index("by_taskRunId", ["taskRunId"]),
43 taskRuns: defineTable(TaskRunSchema)
44 .index("by_domainId", ["id"])
45 .index("by_workspaceId", ["workspaceId"])
46 .index("by_sessionId", ["sessionId"]),
47 syncStates: defineTable(SyncStateSchema)
48 .index("by_domainId", ["id"])
49 .index("by_workspaceId", ["workspaceId"]),
50 events: defineTable(EventEnvelopeSchema)
51 .index("by_domainId", ["id"])
52 .index("by_workspaceId", ["workspaceId"])
53 .index("by_workspaceId_sequence", ["workspaceId", "sequence"]),
54});
55
56export type ExecutorConfectTables = TablesFromSchemaDefinition<typeof executorConfectSchema>;
2 57
3export default executorConfectSchema.convexSchemaDefinition;58export default executorConfectSchema.convexSchemaDefinition;
v2/apps/convex/package.json+2−1
@@ -10,9 +10,10 @@ · 9 unmodified lines
10 },10 },
11 "dependencies": {11 "dependencies": {
12 "@executor-v2/confect": "workspace:*",12 "@executor-v2/confect": "workspace:*",
13 "@executor-v2/control-plane": "workspace:*",
13 "@executor-v2/engine": "workspace:*",14 "@executor-v2/engine": "workspace:*",
14 "@executor-v2/mcp-gateway": "workspace:*",15 "@executor-v2/mcp-gateway": "workspace:*",
15 "@executor-v2/persistence-convex": "workspace:*",16 "@executor-v2/schema": "workspace:*",
16 "@executor-v2/sdk": "workspace:*",17 "@executor-v2/sdk": "workspace:*",
17 "convex": "catalog:",18 "convex": "catalog:",
18 "effect": "catalog:"19 "effect": "catalog:"
v2/apps/pm/README.md+5−2
@@ -3,11 +3,14 @@ · 2 unmodified lines
3Local Process Manager app scaffold for Executor v2.3Local Process Manager app scaffold for Executor v2.
4 4
5Current scaffold includes:5Current scaffold includes:
6- local MCP endpoint at `POST /mcp`6- local MCP endpoint at `GET/POST/DELETE /v1/mcp`
7- health endpoint at `GET /healthz`7- health endpoint at `GET /healthz`
8- MCP tool routing via `@executor-v2/mcp-gateway`8- MCP tool routing via `@executor-v2/mcp-gateway`
9- local runtime adapter injection (`makeLocalInProcessRuntimeAdapter()` in entry wiring)9- local runtime adapter injection (`makeLocalInProcessRuntimeAdapter()` in entry wiring)
10 runtime callback endpoint at `POST /runtime/tool-call`10 runtime callback endpoint at `POST /v1/runtime/tool-call`
11- control-plane source endpoints at `GET/POST /v1/workspaces/:workspaceId/sources`
12- control-plane source removal endpoint at `DELETE /v1/workspaces/:workspaceId/sources/:sourceId`
13- generated OpenAPI spec endpoint at `GET /v1/openapi.json`
11 14
12App wiring is now split by responsibility:15App wiring is now split by responsibility:
13- `src/config.ts`: Effect config service (`PORT`)16- `src/config.ts`: Effect config service (`PORT`)
v2/apps/pm/package.json+2−0
@@ -11,9 +11,11 @@ · 10 unmodified lines
11 "dependencies": {11 "dependencies": {
12 "@effect/platform": "catalog:",12 "@effect/platform": "catalog:",
13 "@effect/platform-bun": "^0.86.0",13 "@effect/platform-bun": "^0.86.0",
14 "@executor-v2/control-plane": "workspace:*",
14 "@executor-v2/engine": "workspace:*",15 "@executor-v2/engine": "workspace:*",
15 "@executor-v2/mcp-gateway": "workspace:*",16 "@executor-v2/mcp-gateway": "workspace:*",
16 "@executor-v2/sdk": "workspace:*",17 "@executor-v2/sdk": "workspace:*",
18 "@executor-v2/persistence-local": "workspace:*",
17 "effect": "catalog:"19 "effect": "catalog:"
18 },20 },
19 "devDependencies": {21 "devDependencies": {
v2/apps/pm/src/execute.e2e.test.ts+1−1
@@ -109,7 +109,7 @@ const withMcpClient = (port: number) => · 108 unmodified lines
109 Effect.tryPromise({109 Effect.tryPromise({
110 try: async () => {110 try: async () => {
111 const transport = new StreamableHTTPClientTransport(111 const transport = new StreamableHTTPClientTransport(
112 new URL(`http://127.0.0.1:${port}/mcp`),112 new URL(`http://127.0.0.1:${port}/v1/mcp`),
113 );113 );
114 const client = new Client(114 const client = new Client(
115 { name: "executor-v2-pm-e2e", version: "0.0.0" },115 { name: "executor-v2-pm-e2e", version: "0.0.0" },
v2/apps/pm/src/http-server.ts+25−4
@@ -5,6 +5,11 @@ import { · 4 unmodified lines
5 HttpServerResponse,5 HttpServerResponse,
6} from "@effect/platform";6} from "@effect/platform";
7import { BunHttpServer, BunHttpServerRequest } from "@effect/platform-bun";7import { BunHttpServer, BunHttpServerRequest } from "@effect/platform-bun";
8import {
9 ControlPlaneService,
10 controlPlaneOpenApiSpec,
11 makeControlPlaneWebHandler,
12} from "@executor-v2/control-plane";
8import * as Effect from "effect/Effect";13import * as Effect from "effect/Effect";
9import * as Layer from "effect/Layer";14import * as Layer from "effect/Layer";
10 15
@@ -25,17 +30,33 @@ const fromWebHandler = (handler: (request: Request) => Promise<Response>) => · 14 unmodified lines
25export const startPmHttpServer = Effect.fn("@executor-v2/app-pm/http.start")(function* () {30export const startPmHttpServer = Effect.fn("@executor-v2/app-pm/http.start")(function* () {
26 const { port } = yield* PmConfig;31 const { port } = yield* PmConfig;
27 const { handleMcp } = yield* PmMcpHandler;32 const { handleMcp } = yield* PmMcpHandler;
33 const controlPlaneService = yield* ControlPlaneService;
34 const controlPlaneWebHandler = yield* Effect.sync(() =>
35 makeControlPlaneWebHandler(
36 Layer.succeed(ControlPlaneService, controlPlaneService),
37 ),
38 );
39
40 yield* Effect.addFinalizer(() =>
41 Effect.promise(() => controlPlaneWebHandler.dispose()),
42 );
28 43
29 const httpLive = HttpRouter.empty.pipe(44 const httpLive = HttpRouter.empty.pipe(
30 HttpRouter.get("/healthz", HttpServerResponse.json({ ok: true, service: "pm" })),45 HttpRouter.get("/healthz", HttpServerResponse.json({ ok: true, service: "pm" })),
31 HttpRouter.get("/mcp", fromWebHandler(handleMcp)),
32 HttpRouter.post("/mcp", fromWebHandler(handleMcp)),
33 HttpRouter.del("/mcp", fromWebHandler(handleMcp)),
34 HttpRouter.get("/v1/mcp", fromWebHandler(handleMcp)),46 HttpRouter.get("/v1/mcp", fromWebHandler(handleMcp)),
35 HttpRouter.post("/v1/mcp", fromWebHandler(handleMcp)),47 HttpRouter.post("/v1/mcp", fromWebHandler(handleMcp)),
36 HttpRouter.del("/v1/mcp", fromWebHandler(handleMcp)),48 HttpRouter.del("/v1/mcp", fromWebHandler(handleMcp)),
37 HttpRouter.post("/runtime/tool-call", handleToolCallHttp),
38 HttpRouter.post("/v1/runtime/tool-call", handleToolCallHttp),49 HttpRouter.post("/v1/runtime/tool-call", handleToolCallHttp),
50 HttpRouter.get("/v1/workspaces/:workspaceId/sources", fromWebHandler(controlPlaneWebHandler.handler)),
51 HttpRouter.post("/v1/workspaces/:workspaceId/sources", fromWebHandler(controlPlaneWebHandler.handler)),
52 HttpRouter.del(
53 "/v1/workspaces/:workspaceId/sources/:sourceId",
54 fromWebHandler(controlPlaneWebHandler.handler),
55 ),
56 HttpRouter.get(
57 "/v1/openapi.json",
58 HttpServerResponse.unsafeJson(controlPlaneOpenApiSpec),
59 ),
39 HttpServer.serve(),60 HttpServer.serve(),
40 HttpServer.withLogAddress,61 HttpServer.withLogAddress,
41 Layer.provide(BunHttpServer.layer({ port })),62 Layer.provide(BunHttpServer.layer({ port })),
v2/apps/pm/src/main.ts+13−1
@@ -1,4 +1,7 @@
1import * as BunContext from "@effect/platform-bun/BunContext";
2import { ControlPlaneServiceLive } from "@executor-v2/control-plane";
1import { makeLocalInProcessRuntimeAdapter } from "@executor-v2/engine";3import { makeLocalInProcessRuntimeAdapter } from "@executor-v2/engine";
4import { LocalSourceStoreLive } from "@executor-v2/persistence-local";
2 5
3import * as Effect from "effect/Effect";6import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";7import * as Layer from "effect/Layer";
@@ -18,12 +21,21 @@ const PmMcpDependenciesLive = Layer.merge( · 13 unmodified lines
18 PmToolProviderRegistryLive,21 PmToolProviderRegistryLive,
19);22);
20 23
24const PmControlPlaneDependenciesLive = ControlPlaneServiceLive.pipe(
25 Layer.provide(
26 LocalSourceStoreLive({
27 rootDir: process.env.PM_STATE_ROOT_DIR ?? ".executor-v2/pm-state",
28 }).pipe(Layer.provide(BunContext.layer)),
29 ),
30);
31
21const PmAppLive = Layer.mergeAll(32const PmAppLive = Layer.mergeAll(
22 PmConfigLive,33 PmConfigLive,
23 PmMcpHandlerLive.pipe(Layer.provide(PmMcpDependenciesLive)),34 PmMcpHandlerLive.pipe(Layer.provide(PmMcpDependenciesLive)),
24 PmToolCallHandlerLive,35 PmToolCallHandlerLive,
36 PmControlPlaneDependenciesLive,
25);37);
26 38
27const program = startPmHttpServer().pipe(Effect.provide(PmAppLive));39const program = Effect.scoped(startPmHttpServer()).pipe(Effect.provide(PmAppLive));
28 40
29await Effect.runPromise(program);41await Effect.runPromise(program);
v2/apps/pm/src/run-executor.ts+11−15
@@ -2,6 +2,7 @@ import { · 1 unmodified line
2 makeToolProviderRegistry,2 makeToolProviderRegistry,
3 ToolProviderRegistryService,3 ToolProviderRegistryService,
4 type RuntimeAdapter,4 type RuntimeAdapter,
5 type RuntimeExecuteError,
5} from "@executor-v2/engine";6} from "@executor-v2/engine";
6import type { ExecuteRunInput, ExecuteRunResult } from "@executor-v2/sdk";7import type { ExecuteRunInput, ExecuteRunResult } from "@executor-v2/sdk";
7import * as Context from "effect/Context";8import * as Context from "effect/Context";
@@ -19,21 +20,16 @@ export class PmRunExecutor extends Context.Tag("@executor-v2/app-pm/PmRunExecuto · 11 unmodified lines
19 PmRunExecutorService20 PmRunExecutorService
20>() {}21>() {}
21 22
22const errorToText = (error: unknown): string => {23const formatRuntimeExecuteError = (error: RuntimeExecuteError): string => {
23 if (error instanceof Error) {24 switch (error._tag) {
24 return error.message;25 case "RuntimeAdapterError":
26 case "LocalCodeRunnerError":
27 case "DenoSubprocessRunnerError":
28 case "ToolProviderError":
29 return error.details ? `${error.message}: ${error.details}` : error.message;
30 case "ToolProviderRegistryError":
31 return error.message;
25 }32 }
26
27 if (
28 typeof error === "object" &&
29 error !== null &&
30 "message" in error &&
31 typeof error.message === "string"
32 ) {
33 return error.message;
34 }
35
36 return String(error);
37};33};
38 34
39const makeExecuteRun = (runtimeAdapter: RuntimeAdapter) =>35const makeExecuteRun = (runtimeAdapter: RuntimeAdapter) =>
@@ -69,7 +65,7 @@ const makeExecuteRun = (runtimeAdapter: RuntimeAdapter) => · 29 unmodified lines
69 Effect.succeed({65 Effect.succeed({
70 runId,66 runId,
71 status: "failed",67 status: "failed",
72 error: errorToText(error),68 error: formatRuntimeExecuteError(error),
73 } satisfies ExecuteRunResult),69 } satisfies ExecuteRunResult),
74 ),70 ),
75 );71 );
v2/apps/pm/src/tool-call-handler.ts+21−22
@@ -7,6 +7,7 @@ import * as Context from "effect/Context"; · 6 unmodified lines
7import * as Data from "effect/Data";7import * as Data from "effect/Data";
8import * as Effect from "effect/Effect";8import * as Effect from "effect/Effect";
9import * as Layer from "effect/Layer";9import * as Layer from "effect/Layer";
10import * as ParseResult from "effect/ParseResult";
10import * as Schema from "effect/Schema";11import * as Schema from "effect/Schema";
11 12
12export type PmToolCallHandlerService = {13export type PmToolCallHandlerService = {
@@ -37,29 +38,25 @@ const RuntimeToolCallRequestSchema = Schema.Struct({ · 24 unmodified lines
37 38
38const decodeRuntimeToolCallRequest = Schema.decodeUnknown(RuntimeToolCallRequestSchema);39const decodeRuntimeToolCallRequest = Schema.decodeUnknown(RuntimeToolCallRequestSchema);
39 40
40const errorToText = (error: unknown): string => {
41 if (error instanceof Error) {
42 return error.message;
43 }
44
45 if (
46 typeof error === "object" &&
47 error !== null &&
48 "message" in error &&
49 typeof error.message === "string"
50 ) {
51 return error.message;
52 }
53
54 return String(error);
55};
56
57const decodeRequestBodyError = (cause: unknown): PmToolCallHttpRequestError =>41const decodeRequestBodyError = (cause: unknown): PmToolCallHttpRequestError =>
58 new PmToolCallHttpRequestError({42 new PmToolCallHttpRequestError({
59 message: "Invalid runtime callback request body",43 message: "Invalid runtime callback request body",
60 details: cause instanceof Error ? cause.message : String(cause),44 details: String(cause),
61 });45 });
62 46
47const decodeRequestPayloadError = (cause: unknown): PmToolCallHttpRequestError =>
48 new PmToolCallHttpRequestError({
49 message: "Runtime callback request body is invalid",
50 details: ParseResult.isParseError(cause)
51 ? ParseResult.TreeFormatter.formatErrorSync(cause)
52 : String(cause),
53 });
54
55const formatHttpRequestError = (error: PmToolCallHttpRequestError): string =>
56 error.details && error.details.length > 0
57 ? `${error.message}: ${error.details}`
58 : error.message;
59
63const handleToolCall = Effect.fn("@executor-v2/app-pm/tool-call.handle")(function* (60const handleToolCall = Effect.fn("@executor-v2/app-pm/tool-call.handle")(function* (
64 input: RuntimeToolCallRequest,61 input: RuntimeToolCallRequest,
65) {62) {
@@ -82,24 +79,26 @@ export const handleToolCallBody = Effect.fn( · 16 unmodified lines
82)(function* (body: unknown) {79)(function* (body: unknown) {
83 const handler = yield* PmToolCallHandler;80 const handler = yield* PmToolCallHandler;
84 const input = yield* decodeRuntimeToolCallRequest(body).pipe(81 const input = yield* decodeRuntimeToolCallRequest(body).pipe(
85 Effect.mapError(decodeRequestBodyError),82 Effect.mapError(decodeRequestPayloadError),
86 );83 );
87 84
88 return yield* handler.handleToolCall(input);85 return yield* handler.handleToolCall(input);
89});86});
90 87
91export const handleToolCallHttp = Effect.gen(function* () {88export const handleToolCallHttp = Effect.gen(function* () {
92 const body = yield* HttpServerRequest.schemaBodyJson(Schema.Unknown);89 const body = yield* HttpServerRequest.schemaBodyJson(Schema.Unknown).pipe(
90 Effect.mapError(decodeRequestBodyError),
91 );
93 const result = yield* handleToolCallBody(body);92 const result = yield* handleToolCallBody(body);
94 93
95 return yield* HttpServerResponse.json(result, { status: 200 });94 return yield* HttpServerResponse.json(result, { status: 200 });
96}).pipe(95}).pipe(
97 Effect.catchAll((error) =>96 Effect.catchTag("PmToolCallHttpRequestError", (error) =>
98 HttpServerResponse.json(97 HttpServerResponse.json(
99 {98 {
100 ok: false,99 ok: false,
101 kind: "failed",100 kind: "failed",
102 error: errorToText(error),101 error: formatHttpRequestError(error),
103 } satisfies RuntimeToolCallResult,102 } satisfies RuntimeToolCallResult,
104 { status: 400 },103 { status: 400 },
105 ),104 ),
v2/bun.lock+17−17
@@ -21,9 +21,10 @@ · 20 unmodified lines
21 "name": "@executor-v2/app-convex",21 "name": "@executor-v2/app-convex",
22 "dependencies": {22 "dependencies": {
23 "@executor-v2/confect": "workspace:*",23 "@executor-v2/confect": "workspace:*",
24 "@executor-v2/control-plane": "workspace:*",
24 "@executor-v2/engine": "workspace:*",25 "@executor-v2/engine": "workspace:*",
25 "@executor-v2/mcp-gateway": "workspace:*",26 "@executor-v2/mcp-gateway": "workspace:*",
26 "@executor-v2/persistence-convex": "workspace:*",27 "@executor-v2/schema": "workspace:*",
27 "@executor-v2/sdk": "workspace:*",28 "@executor-v2/sdk": "workspace:*",
28 "convex": "catalog:",29 "convex": "catalog:",
29 "effect": "catalog:",30 "effect": "catalog:",
@@ -39,8 +40,10 @@ · 9 unmodified lines
39 "dependencies": {40 "dependencies": {
40 "@effect/platform": "catalog:",41 "@effect/platform": "catalog:",
41 "@effect/platform-bun": "^0.86.0",42 "@effect/platform-bun": "^0.86.0",
43 "@executor-v2/control-plane": "workspace:*",
42 "@executor-v2/engine": "workspace:*",44 "@executor-v2/engine": "workspace:*",
43 "@executor-v2/mcp-gateway": "workspace:*",45 "@executor-v2/mcp-gateway": "workspace:*",
46 "@executor-v2/persistence-local": "workspace:*",
44 "@executor-v2/sdk": "workspace:*",47 "@executor-v2/sdk": "workspace:*",
45 "effect": "catalog:",48 "effect": "catalog:",
46 },49 },
@@ -87,6 +90,17 @@ · 40 unmodified lines
87 "vitest": "catalog:",90 "vitest": "catalog:",
88 },91 },
89 },92 },
93 "packages/control-plane": {
94 "name": "@executor-v2/control-plane",
95 "dependencies": {
96 "@effect-atom/atom": "^0.4.11",
97 "@effect/platform": "catalog:",
98 "@executor-v2/persistence-ports": "workspace:*",
99 "@executor-v2/schema": "workspace:*",
100 "@executor-v2/source-manager": "workspace:*",
101 "effect": "catalog:",
102 },
103 },
90 "packages/domain": {104 "packages/domain": {
91 "name": "@executor-v2/domain",105 "name": "@executor-v2/domain",
92 },106 },
@@ -114,9 +128,6 @@ · 21 unmodified lines
114 "packages/oauth": {128 "packages/oauth": {
115 "name": "@executor-v2/oauth",129 "name": "@executor-v2/oauth",
116 },130 },
117 "packages/rpc": {
118 "name": "@executor-v2/rpc",
119 },
120 "packages/schema": {131 "packages/schema": {
121 "name": "@executor-v2/schema",132 "name": "@executor-v2/schema",
122 "dependencies": {133 "dependencies": {
@@ -148,15 +159,6 @@ · 25 unmodified lines
148 "effect": "catalog:",159 "effect": "catalog:",
149 },160 },
150 },161 },
151 "packages/state-convex": {
152 "name": "@executor-v2/persistence-convex",
153 "dependencies": {
154 "@executor-v2/confect": "workspace:*",
155 "@executor-v2/persistence-ports": "workspace:*",
156 "@executor-v2/schema": "workspace:*",
157 "effect": "catalog:",
158 },
159 },
160 "packages/state-local-file": {162 "packages/state-local-file": {
161 "name": "@executor-v2/persistence-local",163 "name": "@executor-v2/persistence-local",
162 "dependencies": {164 "dependencies": {
@@ -300,6 +302,8 @@ · 137 unmodified lines
300 302
301 "@executor-v2/confect": ["@executor-v2/confect@workspace:packages/confect"],303 "@executor-v2/confect": ["@executor-v2/confect@workspace:packages/confect"],
302 304
305 "@executor-v2/control-plane": ["@executor-v2/control-plane@workspace:packages/control-plane"],
306
303 "@executor-v2/domain": ["@executor-v2/domain@workspace:packages/domain"],307 "@executor-v2/domain": ["@executor-v2/domain@workspace:packages/domain"],
304 308
305 "@executor-v2/engine": ["@executor-v2/engine@workspace:packages/engine"],309 "@executor-v2/engine": ["@executor-v2/engine@workspace:packages/engine"],
@@ -308,14 +312,10 @@ · 2 unmodified lines
308 312
309 "@executor-v2/oauth": ["@executor-v2/oauth@workspace:packages/oauth"],313 "@executor-v2/oauth": ["@executor-v2/oauth@workspace:packages/oauth"],
310 314
311 "@executor-v2/persistence-convex": ["@executor-v2/persistence-convex@workspace:packages/state-convex"],
312
313 "@executor-v2/persistence-local": ["@executor-v2/persistence-local@workspace:packages/state-local-file"],315 "@executor-v2/persistence-local": ["@executor-v2/persistence-local@workspace:packages/state-local-file"],
314 316
315 "@executor-v2/persistence-ports": ["@executor-v2/persistence-ports@workspace:packages/state-contracts"],317 "@executor-v2/persistence-ports": ["@executor-v2/persistence-ports@workspace:packages/state-contracts"],
316 318
317 "@executor-v2/rpc": ["@executor-v2/rpc@workspace:packages/rpc"],
318
319 "@executor-v2/schema": ["@executor-v2/schema@workspace:packages/schema"],319 "@executor-v2/schema": ["@executor-v2/schema@workspace:packages/schema"],
320 320
321 "@executor-v2/sdk": ["@executor-v2/sdk@workspace:packages/sdk"],321 "@executor-v2/sdk": ["@executor-v2/sdk@workspace:packages/sdk"],
v2/packages/control-plane/package.jsonrenamed from v2/packages/state-convex/package.json+6−4
@@ -1,5 +1,5 @@
1{1{
2 "name": "@executor-v2/persistence-convex",2 "name": "@executor-v2/control-plane",
3 "private": true,3 "private": true,
4 "type": "module",4 "type": "module",
5 "exports": {5 "exports": {
@@ -9,9 +9,11 @@ · 3 unmodified lines
9 "typecheck": "bunx tsc --noEmit -p tsconfig.json"9 "typecheck": "bunx tsc --noEmit -p tsconfig.json"
10 },10 },
11 "dependencies": {11 "dependencies": {
12 "@executor-v2/confect": "workspace:*",12 "@effect-atom/atom": "^0.4.11",
13 "@executor-v2/schema": "workspace:*",13 "@effect/platform": "catalog:",
14 "@executor-v2/persistence-ports": "workspace:*",14 "@executor-v2/persistence-ports": "workspace:*",
15 "@executor-v2/schema": "workspace:*",
16 "@executor-v2/source-manager": "workspace:*",
15 "effect": "catalog:"17 "effect": "catalog:"
16 }18 }
17}19}
v2/packages/control-plane/src/api.ts+26−0
@@ -0,0 +1,26 @@
1import { HttpApi, OpenApi } from "@effect/platform";
2
3import { SourcesApi } from "./sources/api";
4
5export {
6 ControlPlaneBadRequestError,
7 ControlPlaneStorageError,
8} from "./errors";
9
10export {
11 RemoveSourceResultSchema,
12 UpsertSourcePayloadSchema,
13 type RemoveSourceResult,
14 type UpsertSourcePayload,
15} from "./sources/api";
16
17export class ControlPlaneApi extends HttpApi.make("controlPlane")
18 .add(SourcesApi)
19 .annotateContext(
20 OpenApi.annotations({
21 title: "Executor v2 Control Plane API",
22 description: "Backend-agnostic control plane API",
23 }),
24 ) {}
25
26export const controlPlaneOpenApiSpec = OpenApi.fromApi(ControlPlaneApi);
v2/packages/control-plane/src/client.ts+159−0
@@ -0,0 +1,159 @@
1import { Atom, Result } from "@effect-atom/atom";
2import {
3 FetchHttpClient,
4 HttpApiClient,
5 HttpApiError,
6 HttpClientError,
7} from "@effect/platform";
8import type {
9 Source,
10 SourceId,
11 WorkspaceId,
12} from "@executor-v2/schema";
13import * as Effect from "effect/Effect";
14import * as ParseResult from "effect/ParseResult";
15
16import {
17 ControlPlaneApi,
18 type ControlPlaneBadRequestError,
19 type ControlPlaneStorageError,
20 type RemoveSourceResult,
21 type UpsertSourcePayload,
22} from "./api";
23
24export type ControlPlaneClientOptions = {
25 baseUrl: string;
26};
27
28export type ControlPlaneClientError =
29 | ControlPlaneBadRequestError
30 | ControlPlaneStorageError
31 | HttpApiError.HttpApiDecodeError
32 | HttpClientError.HttpClientError
33 | ParseResult.ParseError;
34
35export type ListSourcesRequest = {
36 workspaceId: WorkspaceId;
37};
38
39export type UpsertSourceRequest = {
40 workspaceId: WorkspaceId;
41 payload: UpsertSourcePayload;
42};
43
44export type RemoveSourceRequest = {
45 workspaceId: WorkspaceId;
46 sourceId: SourceId;
47};
48
49export type ControlPlaneReadOptions = {
50 enabled?: boolean;
51};
52
53export const makeControlPlaneClient = (options: ControlPlaneClientOptions) =>
54 HttpApiClient.make(ControlPlaneApi, {
55 baseUrl: options.baseUrl,
56 });
57
58const isReadEnabled = (options?: ControlPlaneReadOptions): boolean =>
59 options?.enabled ?? true;
60
61const makeDisabledResultAtom = <Success, Error>() =>
62 Atom.make(Result.initial<Success, Error>());
63
64export const createControlPlaneAtomClient = (
65 options: ControlPlaneClientOptions,
66) => {
67 const baseLayer = FetchHttpClient.layer;
68 const runtime = Atom.runtime(baseLayer);
69
70 const listSourcesEffect = (
71 input: ListSourcesRequest,
72 ): Effect.Effect<ReadonlyArray<Source>, ControlPlaneClientError, never> =>
73 Effect.gen(function* () {
74 const client = yield* makeControlPlaneClient(options);
75 return yield* client.sources.list({
76 path: {
77 workspaceId: input.workspaceId,
78 },
79 });
80 }).pipe(Effect.provide(baseLayer));
81
82 const upsertSourceEffect = (
83 input: UpsertSourceRequest,
84 ): Effect.Effect<Source, ControlPlaneClientError, never> =>
85 Effect.gen(function* () {
86 const client = yield* makeControlPlaneClient(options);
87 return yield* client.sources.upsert({
88 path: {
89 workspaceId: input.workspaceId,
90 },
91 payload: input.payload,
92 });
93 }).pipe(Effect.provide(baseLayer));
94
95 const removeSourceEffect = (
96 input: RemoveSourceRequest,
97 ): Effect.Effect<RemoveSourceResult, ControlPlaneClientError, never> =>
98 Effect.gen(function* () {
99 const client = yield* makeControlPlaneClient(options);
100 return yield* client.sources.remove({
101 path: {
102 workspaceId: input.workspaceId,
103 sourceId: input.sourceId,
104 },
105 });
106 }).pipe(Effect.provide(baseLayer));
107
108 const listSourcesFamily = Atom.family((workspaceId: WorkspaceId) =>
109 runtime.atom(
110 listSourcesEffect({
111 workspaceId,
112 }),
113 ),
114 );
115
116 const listDisabledAtom = makeDisabledResultAtom<
117 ReadonlyArray<Source>,
118 ControlPlaneClientError
119 >();
120
121 const upsertSourceMutate = runtime.fn<UpsertSourceRequest>()(
122 Effect.fnUntraced(function* (input) {
123 return yield* upsertSourceEffect(input);
124 }),
125 );
126
127 const removeSourceMutate = runtime.fn<RemoveSourceRequest>()(
128 Effect.fnUntraced(function* (input) {
129 return yield* removeSourceEffect(input);
130 }),
131 );
132
133 return {
134 runtime,
135 sources: {
136 list: {
137 query: (input: ListSourcesRequest, readOptions?: ControlPlaneReadOptions) =>
138 isReadEnabled(readOptions)
139 ? listSourcesFamily(input.workspaceId)
140 : listDisabledAtom,
141 queryEffect: listSourcesEffect,
142 queryPromise: (input: ListSourcesRequest) =>
143 Effect.runPromise(listSourcesEffect(input)),
144 },
145 upsert: {
146 mutate: upsertSourceMutate,
147 mutateEffect: upsertSourceEffect,
148 mutatePromise: (input: UpsertSourceRequest) =>
149 Effect.runPromise(upsertSourceEffect(input)),
150 },
151 remove: {
152 mutate: removeSourceMutate,
153 mutateEffect: removeSourceEffect,
154 mutatePromise: (input: RemoveSourceRequest) =>
155 Effect.runPromise(removeSourceEffect(input)),
156 },
157 },
158 };
159};
v2/packages/control-plane/src/errors.ts+22−0
@@ -0,0 +1,22 @@
1import { HttpApiSchema } from "@effect/platform";
2import * as Schema from "effect/Schema";
3
4export class ControlPlaneBadRequestError extends Schema.TaggedError<ControlPlaneBadRequestError>()(
5 "ControlPlaneBadRequestError",
6 {
7 operation: Schema.String,
8 message: Schema.String,
9 details: Schema.String,
10 },
11 HttpApiSchema.annotations({ status: 400 }),
12) {}
13
14export class ControlPlaneStorageError extends Schema.TaggedError<ControlPlaneStorageError>()(
15 "ControlPlaneStorageError",
16 {
17 operation: Schema.String,
18 message: Schema.String,
19 details: Schema.String,
20 },
21 HttpApiSchema.annotations({ status: 500 }),
22) {}
v2/packages/control-plane/src/http.ts+20−0
@@ -0,0 +1,20 @@
1import { HttpApiBuilder, HttpServer } from "@effect/platform";
2import * as Layer from "effect/Layer";
3
4import { ControlPlaneApi } from "./api";
5import { ControlPlaneService } from "./service";
6import { ControlPlaneSourcesLive } from "./sources/http";
7
8export const ControlPlaneApiLive = HttpApiBuilder.api(ControlPlaneApi).pipe(
9 Layer.provide(ControlPlaneSourcesLive),
10);
11
12export const makeControlPlaneWebHandler = <E>(
13 serviceLayer: Layer.Layer<ControlPlaneService, E, never>,
14) =>
15 HttpApiBuilder.toWebHandler(
16 Layer.mergeAll(
17 ControlPlaneApiLive.pipe(Layer.provide(serviceLayer)),
18 HttpServer.layerContext,
19 ),
20 );
v2/packages/control-plane/src/index.ts+44−0
@@ -0,0 +1,44 @@
1export {
2 ControlPlaneApi,
3 RemoveSourceResultSchema,
4 UpsertSourcePayloadSchema,
5 controlPlaneOpenApiSpec,
6 type RemoveSourceResult,
7 type UpsertSourcePayload,
8} from "./api";
9
10export {
11 ControlPlaneBadRequestError,
12 ControlPlaneStorageError,
13} from "./errors";
14
15export {
16 ControlPlaneService,
17 ControlPlaneServiceLive,
18 makeControlPlaneService,
19 type ControlPlaneServiceShape,
20} from "./service";
21
22export {
23 ControlPlaneApiLive,
24 makeControlPlaneWebHandler,
25} from "./http";
26
27export {
28 ControlPlaneSourcesLive,
29 makeControlPlaneSourcesService,
30 type ControlPlaneSourcesServiceShape,
31 type RemoveSourceInput,
32 type UpsertSourceInput,
33} from "./sources";
34
35export {
36 createControlPlaneAtomClient,
37 makeControlPlaneClient,
38 type ControlPlaneClientError,
39 type ControlPlaneClientOptions,
40 type ControlPlaneReadOptions,
41 type ListSourcesRequest,
42 type RemoveSourceRequest,
43 type UpsertSourceRequest,
44} from "./client";
v2/packages/control-plane/src/service.ts+37−0
@@ -0,0 +1,37 @@
1import { SourceCatalog, SourceCatalogLive } from "@executor-v2/source-manager/source-catalog";
2import * as Context from "effect/Context";
3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
5
6import {
7 makeControlPlaneSourcesService,
8 type ControlPlaneSourcesServiceShape,
9} from "./sources/service";
10
11export type ControlPlaneServiceShape = ControlPlaneSourcesServiceShape;
12
13export class ControlPlaneService extends Context.Tag("@executor-v2/control-plane/ControlPlaneService")<
14 ControlPlaneService,
15 ControlPlaneServiceShape
16>() {}
17
18export const makeControlPlaneService = (
19 services: {
20 sources: ControlPlaneSourcesServiceShape;
21 },
22): ControlPlaneServiceShape => ({
23 ...services.sources,
24});
25
26export const ControlPlaneServiceLive = Layer.effect(
27 ControlPlaneService,
28 Effect.gen(function* () {
29 const sourceCatalog = yield* SourceCatalog;
30
31 return ControlPlaneService.of(
32 makeControlPlaneService({
33 sources: makeControlPlaneSourcesService(sourceCatalog),
34 }),
35 );
36 }),
37).pipe(Layer.provide(SourceCatalogLive));
v2/packages/control-plane/src/sources/api.ts+51−0
@@ -0,0 +1,51 @@
1import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "@effect/platform";
2import { SourceIdSchema, SourceSchema, WorkspaceIdSchema } from "@executor-v2/schema";
3import * as Schema from "effect/Schema";
4
5import { ControlPlaneBadRequestError, ControlPlaneStorageError } from "../errors";
6
7const RequiredUpsertSourcePayloadSchema = SourceSchema.pipe(
8 Schema.pick("name", "kind", "endpoint"),
9);
10
11const OptionalUpsertSourcePayloadSchema = SourceSchema.pipe(
12 Schema.pick("id", "status", "enabled", "configJson", "sourceHash", "lastError"),
13 Schema.partialWith({ exact: true }),
14);
15
16export const UpsertSourcePayloadSchema = RequiredUpsertSourcePayloadSchema.pipe(
17 Schema.extend(OptionalUpsertSourcePayloadSchema),
18);
19
20export type UpsertSourcePayload = typeof UpsertSourcePayloadSchema.Type;
21
22export const RemoveSourceResultSchema = Schema.Struct({
23 removed: Schema.Boolean,
24});
25
26export type RemoveSourceResult = typeof RemoveSourceResultSchema.Type;
27
28const workspaceIdParam = HttpApiSchema.param("workspaceId", WorkspaceIdSchema);
29const sourceIdParam = HttpApiSchema.param("sourceId", SourceIdSchema);
30
31export class SourcesApi extends HttpApiGroup.make("sources")
32 .add(
33 HttpApiEndpoint.get("list")`/workspaces/${workspaceIdParam}/sources`
34 .addSuccess(Schema.Array(SourceSchema))
35 .addError(ControlPlaneBadRequestError)
36 .addError(ControlPlaneStorageError),
37 )
38 .add(
39 HttpApiEndpoint.post("upsert")`/workspaces/${workspaceIdParam}/sources`
40 .setPayload(UpsertSourcePayloadSchema)
41 .addSuccess(SourceSchema)
42 .addError(ControlPlaneBadRequestError)
43 .addError(ControlPlaneStorageError),
44 )
45 .add(
46 HttpApiEndpoint.del("remove")`/workspaces/${workspaceIdParam}/sources/${sourceIdParam}`
47 .addSuccess(RemoveSourceResultSchema)
48 .addError(ControlPlaneBadRequestError)
49 .addError(ControlPlaneStorageError),
50 )
51 .prefix("/v1") {}
v2/packages/control-plane/src/sources/http.ts+67−0
@@ -0,0 +1,67 @@
1import { HttpApiBuilder } from "@effect/platform";
2import { type SourceStoreError } from "@executor-v2/persistence-ports";
3import * as Effect from "effect/Effect";
4
5import { ControlPlaneBadRequestError, ControlPlaneStorageError } from "../errors";
6import { ControlPlaneService } from "../service";
7import { ControlPlaneApi } from "../api";
8
9const toStorageError = (
10 operation: string,
11 cause: SourceStoreError,
12): ControlPlaneStorageError =>
13 new ControlPlaneStorageError({
14 operation,
15 message: "Control plane operation failed",
16 details: cause.details ?? cause.message,
17 });
18
19export const ControlPlaneSourcesLive = HttpApiBuilder.group(
20 ControlPlaneApi,
21 "sources",
22 (handlers) =>
23 handlers
24 .handle("list", ({ path }) =>
25 Effect.gen(function* () {
26 const service = yield* ControlPlaneService;
27 return yield* service.listSources(path.workspaceId);
28 }).pipe(
29 Effect.catchTag("SourceStoreError", (cause) =>
30 toStorageError("sources.list", cause),
31 ),
32 ),
33 )
34 .handle("upsert", ({ path, payload }) =>
35 Effect.gen(function* () {
36 const service = yield* ControlPlaneService;
37 return yield* service.upsertSource({
38 workspaceId: path.workspaceId,
39 payload,
40 });
41 }).pipe(
42 Effect.catchTag("SourceCatalogValidationError", (error) =>
43 new ControlPlaneBadRequestError({
44 operation: error.operation,
45 message: error.message,
46 details: error.details,
47 }),
48 ),
49 Effect.catchTag("SourceStoreError", (cause) =>
50 toStorageError("sources.upsert", cause),
51 ),
52 ),
53 )
54 .handle("remove", ({ path }) =>
55 Effect.gen(function* () {
56 const service = yield* ControlPlaneService;
57 return yield* service.removeSource({
58 workspaceId: path.workspaceId,
59 sourceId: path.sourceId,
60 });
61 }).pipe(
62 Effect.catchTag("SourceStoreError", (cause) =>
63 toStorageError("sources.remove", cause),
64 ),
65 ),
66 ),
67);
v2/packages/control-plane/src/sources/index.ts+16−0
@@ -0,0 +1,16 @@
1export {
2 RemoveSourceResultSchema,
3 SourcesApi,
4 UpsertSourcePayloadSchema,
5 type RemoveSourceResult,
6 type UpsertSourcePayload,
7} from "./api";
8
9export {
10 makeControlPlaneSourcesService,
11 type ControlPlaneSourcesServiceShape,
12 type RemoveSourceInput,
13 type UpsertSourceInput,
14} from "./service";
15
16export { ControlPlaneSourcesLive } from "./http";
v2/packages/control-plane/src/sources/service.ts+45−0
@@ -0,0 +1,45 @@
1import { type SourceStoreError } from "@executor-v2/persistence-ports";
2import {
3 SourceCatalogValidationError,
4 type SourceCatalogService,
5} from "@executor-v2/source-manager/source-catalog";
6import { type Source, type SourceId, type WorkspaceId } from "@executor-v2/schema";
7import * as Effect from "effect/Effect";
8
9import type { RemoveSourceResult, UpsertSourcePayload } from "./api";
10
11export type UpsertSourceInput = {
12 workspaceId: WorkspaceId;
13 payload: UpsertSourcePayload;
14};
15
16export type RemoveSourceInput = {
17 workspaceId: WorkspaceId;
18 sourceId: SourceId;
19};
20
21export type ControlPlaneSourcesServiceShape = {
22 listSources: (
23 workspaceId: WorkspaceId,
24 ) => Effect.Effect<ReadonlyArray<Source>, SourceStoreError>;
25 upsertSource: (
26 input: UpsertSourceInput,
27 ) => Effect.Effect<Source, SourceStoreError | SourceCatalogValidationError>;
28 removeSource: (input: RemoveSourceInput) => Effect.Effect<RemoveSourceResult, SourceStoreError>;
29};
30
31export const makeControlPlaneSourcesService = (
32 sourceCatalog: SourceCatalogService,
33): ControlPlaneSourcesServiceShape => ({
34 listSources: (workspaceId) => sourceCatalog.listSources(workspaceId),
35 upsertSource: (input) =>
36 sourceCatalog.upsertSource({
37 workspaceId: input.workspaceId,
38 payload: input.payload,
39 }),
40 removeSource: (input) =>
41 sourceCatalog.removeSource({
42 workspaceId: input.workspaceId,
43 sourceId: input.sourceId,
44 }),
45});
v2/packages/control-plane/tsconfig.jsonrenamed from v2/packages/rpc/tsconfig.json+4−1
@@ -1,4 +1,7 @@
1{1{
2 "extends": "../../tsconfig.json",2 "extends": "../../tsconfig.json",
3 "compilerOptions": {
4 "types": ["node", "bun"]
5 },
3 "include": ["src"]6 "include": ["src"]
4}7}
v2/packages/engine/src/run-client.ts+17−18
@@ -9,7 +9,11 @@ import { · 8 unmodified lines
9 type ToolProvider,9 type ToolProvider,
10 type ToolProviderRegistry,10 type ToolProviderRegistry,
11} from "./tool-providers";11} from "./tool-providers";
12import type { RuntimeAdapter, RuntimeRunnableTool } from "./runtime-adapters";12import type {
13 RuntimeAdapter,
14 RuntimeExecuteError,
15 RuntimeRunnableTool,
16} from "./runtime-adapters";
13 17
14export type RuntimeRunClientExecuteInput = {18export type RuntimeRunClientExecuteInput = {
15 code: string;19 code: string;
@@ -57,21 +61,16 @@ export type CreateInMemoryRuntimeRunClientOptions = { · 41 unmodified lines
57 makeRunId?: () => string;61 makeRunId?: () => string;
58};62};
59 63
60const errorToText = (error: unknown): string => {64const formatRuntimeExecuteError = (error: RuntimeExecuteError): string => {
61 if (error instanceof Error) {65 switch (error._tag) {
62 return error.message;66 case "RuntimeAdapterError":
67 case "LocalCodeRunnerError":
68 case "DenoSubprocessRunnerError":
69 case "ToolProviderError":
70 return error.details ? `${error.message}: ${error.details}` : error.message;
71 case "ToolProviderRegistryError":
72 return error.message;
63 }73 }
64
65 if (
66 typeof error === "object" &&
67 error !== null &&
68 "message" in error &&
69 typeof error.message === "string"
70 ) {
71 return error.message;
72 }
73
74 return String(error);
75};74};
76 75
77const toInMemoryDescriptor = (76const toInMemoryDescriptor = (
@@ -127,7 +126,7 @@ const makeInMemoryToolProvider = ( · 49 unmodified lines
127 operation: "invoke",126 operation: "invoke",
128 providerKind: "in_memory",127 providerKind: "in_memory",
129 message: `In-memory tool invocation failed: ${input.tool.toolId}`,128 message: `In-memory tool invocation failed: ${input.tool.toolId}`,
130 details: cause instanceof Error ? cause.message : String(cause),129 details: String(cause),
131 }),130 }),
132 });131 });
133 },132 },
@@ -152,7 +151,7 @@ export const createRuntimeRunClient = ( · 18 unmodified lines
152 return {151 return {
153 runId,152 runId,
154 status: "failed",153 status: "failed",
155 error: errorToText(availabilityResult.left),154 error: "Runtime availability check failed",
156 };155 };
157 }156 }
158 157
@@ -185,7 +184,7 @@ export const createRuntimeRunClient = ( · 26 unmodified lines
185 return {184 return {
186 runId,185 runId,
187 status: "failed",186 status: "failed",
188 error: errorToText(executionResult.left),187 error: formatRuntimeExecuteError(executionResult.left),
189 };188 };
190 }189 }
191 190
v2/packages/rpc/README.md+0−3
@@ -1,3 +0,0 @@
1# rpc
2
3Effect RPC contract scaffold for Executor v2.
v2/packages/rpc/package.json+0−8
@@ -1,8 +0,0 @@
1{
2 "name": "@executor-v2/rpc",
3 "private": true,
4 "type": "module",
5 "scripts": {
6 "typecheck": "bunx tsc --noEmit -p tsconfig.json"
7 }
8}
v2/packages/rpc/src/index.ts+0−1
@@ -1 +0,0 @@
1export {};
v2/packages/source-manager/package.json+2−1
@@ -3,7 +3,8 @@ · 2 unmodified lines
3 "private": true,3 "private": true,
4 "type": "module",4 "type": "module",
5 "exports": {5 "exports": {
6 ".": "./src/index.ts"6 ".": "./src/index.ts",
7 "./source-catalog": "./src/source-catalog.ts"
7 },8 },
8 "scripts": {9 "scripts": {
9 "test": "bunx vitest run --no-watch src/*.test.ts",10 "test": "bunx vitest run --no-watch src/*.test.ts",
v2/packages/source-manager/src/index.ts+12−0
@@ -5,3 +5,15 @@ export { · 4 unmodified lines
5 type RefreshOpenApiArtifactRequest,5 type RefreshOpenApiArtifactRequest,
6 type SourceManagerService,6 type SourceManagerService,
7} from "./service";7} from "./service";
8
9export {
10 SourceCatalog,
11 SourceCatalogLive,
12 SourceCatalogValidationError,
13 makeSourceCatalogService,
14 type RemoveSourceRequest,
15 type RemoveSourceResult,
16 type SourceCatalogService,
17 type UpsertSourcePayload,
18 type UpsertSourceRequest,
19} from "./source-catalog";
v2/packages/source-manager/src/source-catalog.ts+158−0
@@ -0,0 +1,158 @@
1import {
2 SourceStoreError,
3 SourceStoreService,
4 type SourceStore,
5} from "@executor-v2/persistence-ports";
6import {
7 SourceIdSchema,
8 SourceSchema,
9 type Source,
10 type SourceId,
11 type SourceKind,
12 type SourceStatus,
13 type WorkspaceId,
14} from "@executor-v2/schema";
15import * as Context from "effect/Context";
16import * as Data from "effect/Data";
17import * as Effect from "effect/Effect";
18import * as Layer from "effect/Layer";
19import * as Option from "effect/Option";
20import * as ParseResult from "effect/ParseResult";
21import * as Schema from "effect/Schema";
22
23export class SourceCatalogValidationError extends Data.TaggedError(
24 "SourceCatalogValidationError",
25)<{
26 operation: string;
27 message: string;
28 details: string;
29}> {}
30
31export type UpsertSourcePayload = {
32 id?: SourceId;
33 name: string;
34 kind: SourceKind;
35 endpoint: string;
36 status?: SourceStatus;
37 enabled?: boolean;
38 configJson?: string;
39 sourceHash?: string | null;
40 lastError?: string | null;
41};
42
43export type UpsertSourceRequest = {
44 workspaceId: WorkspaceId;
45 payload: UpsertSourcePayload;
46 now?: () => number;
47};
48
49export type RemoveSourceRequest = {
50 workspaceId: WorkspaceId;
51 sourceId: SourceId;
52};
53
54export type RemoveSourceResult = {
55 removed: boolean;
56};
57
58export type SourceCatalogService = {
59 listSources: (
60 workspaceId: WorkspaceId,
61 ) => Effect.Effect<ReadonlyArray<Source>, SourceStoreError>;
62 upsertSource: (
63 input: UpsertSourceRequest,
64 ) => Effect.Effect<Source, SourceStoreError | SourceCatalogValidationError>;
65 removeSource: (
66 input: RemoveSourceRequest,
67 ) => Effect.Effect<RemoveSourceResult, SourceStoreError>;
68};
69
70export class SourceCatalog extends Context.Tag("@executor-v2/source-manager/SourceCatalog")<
71 SourceCatalog,
72 SourceCatalogService
73>() {}
74
75const decodeSourceId = Schema.decodeUnknown(SourceIdSchema);
76const decodeSource = Schema.decodeUnknown(SourceSchema);
77
78const sourceStoreKey = (source: Source): string => `${source.workspaceId}:${source.id}`;
79
80const sortSources = (sources: ReadonlyArray<Source>): Array<Source> =>
81 [...sources].sort((left, right) => {
82 const leftName = left.name.toLowerCase();
83 const rightName = right.name.toLowerCase();
84
85 if (leftName === rightName) {
86 return sourceStoreKey(left).localeCompare(sourceStoreKey(right));
87 }
88
89 return leftName.localeCompare(rightName);
90 });
91
92const toSourceCatalogValidationError = (
93 operation: string,
94 cause: unknown,
95): SourceCatalogValidationError =>
96 new SourceCatalogValidationError({
97 operation,
98 message: "Invalid source payload",
99 details: ParseResult.isParseError(cause)
100 ? ParseResult.TreeFormatter.formatErrorSync(cause)
101 : String(cause),
102 });
103
104const createSourceId = (): Effect.Effect<SourceId, SourceCatalogValidationError> =>
105 decodeSourceId(`src_${crypto.randomUUID()}`).pipe(
106 Effect.mapError((cause) => toSourceCatalogValidationError("sources.upsert.id", cause)),
107 );
108
109const timestamp = (clock?: () => number): number => clock?.() ?? Date.now();
110
111export const makeSourceCatalogService = (
112 sourceStore: SourceStore,
113): SourceCatalogService => ({
114 listSources: (workspaceId) =>
115 sourceStore.listByWorkspace(workspaceId).pipe(Effect.map(sortSources)),
116 upsertSource: (input) =>
117 Effect.gen(function* () {
118 const sourceId = input.payload.id ?? (yield* createSourceId());
119 const now = timestamp(input.now);
120 const existingOption = yield* sourceStore.getById(input.workspaceId, sourceId);
121 const existing = Option.getOrUndefined(existingOption);
122
123 const source = yield* decodeSource({
124 id: sourceId,
125 workspaceId: input.workspaceId,
126 name: input.payload.name,
127 kind: input.payload.kind,
128 endpoint: input.payload.endpoint,
129 status: input.payload.status ?? "draft",
130 enabled: input.payload.enabled ?? true,
131 configJson: input.payload.configJson ?? "{}",
132 sourceHash: input.payload.sourceHash ?? null,
133 lastError: input.payload.lastError ?? null,
134 createdAt: existing?.createdAt ?? now,
135 updatedAt: now,
136 }).pipe(
137 Effect.mapError((cause) =>
138 toSourceCatalogValidationError("sources.upsert.payload", cause),
139 ),
140 );
141
142 yield* sourceStore.upsert(source);
143 return source;
144 }),
145 removeSource: (input) =>
146 sourceStore.removeById(input.workspaceId, input.sourceId).pipe(
147 Effect.map((removed): RemoveSourceResult => ({ removed })),
148 ),
149});
150
151export const SourceCatalogLive = Layer.effect(
152 SourceCatalog,
153 Effect.gen(function* () {
154 const sourceStore = yield* SourceStoreService;
155
156 return SourceCatalog.of(makeSourceCatalogService(sourceStore));
157 }),
158);
v2/packages/state-convex/README.md+0−8
@@ -1,8 +0,0 @@
1# persistence-convex
2
3Convex-backed persistence adapter scaffold for Executor v2.
4
5Current scaffold includes:
6- Convex table-typed ID schemas in `src/convex-ids.ts`
7- shared Convex schema projection in `src/schema.ts`
8- first adapter implementation for `ProfileStore` in `src/profiles.ts`
v2/packages/state-convex/src/convex-ids.ts+0−25
@@ -1,25 +0,0 @@
1import { Id } from "@executor-v2/confect";
2
3export const ProfileDocIdSchema = Id("profiles");
4export const WorkspaceDocIdSchema = Id("workspaces");
5export const SourceDocIdSchema = Id("sources");
6export const ToolArtifactDocIdSchema = Id("toolArtifacts");
7export const CredentialRefDocIdSchema = Id("credentialRefs");
8export const OAuthTokenDocIdSchema = Id("oauthTokens");
9export const PolicyDocIdSchema = Id("policies");
10export const ApprovalDocIdSchema = Id("approvals");
11export const TaskRunDocIdSchema = Id("taskRuns");
12export const SyncStateDocIdSchema = Id("syncStates");
13export const EventDocIdSchema = Id("events");
14
15export type ProfileDocId = typeof ProfileDocIdSchema.Type;
16export type WorkspaceDocId = typeof WorkspaceDocIdSchema.Type;
17export type SourceDocId = typeof SourceDocIdSchema.Type;
18export type ToolArtifactDocId = typeof ToolArtifactDocIdSchema.Type;
19export type CredentialRefDocId = typeof CredentialRefDocIdSchema.Type;
20export type OAuthTokenDocId = typeof OAuthTokenDocIdSchema.Type;
21export type PolicyDocId = typeof PolicyDocIdSchema.Type;
22export type ApprovalDocId = typeof ApprovalDocIdSchema.Type;
23export type TaskRunDocId = typeof TaskRunDocIdSchema.Type;
24export type SyncStateDocId = typeof SyncStateDocIdSchema.Type;
25export type EventDocId = typeof EventDocIdSchema.Type;
v2/packages/state-convex/src/index.ts+0−26
@@ -1,26 +0,0 @@
1export {
2 ApprovalDocIdSchema,
3 CredentialRefDocIdSchema,
4 EventDocIdSchema,
5 OAuthTokenDocIdSchema,
6 PolicyDocIdSchema,
7 ProfileDocIdSchema,
8 SourceDocIdSchema,
9 SyncStateDocIdSchema,
10 TaskRunDocIdSchema,
11 ToolArtifactDocIdSchema,
12 WorkspaceDocIdSchema,
13 type ApprovalDocId,
14 type CredentialRefDocId,
15 type EventDocId,
16 type OAuthTokenDocId,
17 type PolicyDocId,
18 type ProfileDocId,
19 type SourceDocId,
20 type SyncStateDocId,
21 type TaskRunDocId,
22 type ToolArtifactDocId,
23 type WorkspaceDocId,
24} from "./convex-ids";
25export { makeConvexProfileStore } from "./profiles";
26export { executorConfectSchema, type ExecutorConfectTables } from "./schema";
v2/packages/state-convex/src/profiles.ts+0−33
@@ -1,33 +0,0 @@
1import type {
2 ConfectQueryCtx,
3 DocumentFromTable,
4} from "@executor-v2/confect";
5import { ProfileSchema, type Profile, type ProfileId } from "@executor-v2/schema";
6import type { ProfileStore } from "@executor-v2/persistence-ports";
7import * as Effect from "effect/Effect";
8import { pipe } from "effect/Function";
9import * as Option from "effect/Option";
10import * as Schema from "effect/Schema";
11
12import type { ExecutorConfectTables } from "./schema";
13
14type ProfileDocument = DocumentFromTable<ExecutorConfectTables, "profiles">;
15
16const toProfile = (document: ProfileDocument): Profile => {
17 const { _id: _ignoredId, _creationTime: _ignoredCreationTime, ...profile } = document;
18 return Schema.decodeUnknownSync(ProfileSchema)(profile);
19};
20
21export const makeConvexProfileStore = (
22 ctx: ConfectQueryCtx<ExecutorConfectTables>,
23): ProfileStore => ({
24 getById: (id: ProfileId) =>
25 pipe(
26 ctx.db
27 .query("profiles")
28 .withIndex("by_domainId", (q) => q.eq("id", id))
29 .unique(),
30 Effect.orDie,
31 Effect.map(Option.map(toProfile)),
32 ),
33});
v2/packages/state-convex/src/schema.ts+0−56
@@ -1,56 +0,0 @@
1import { defineSchema, defineTable, type TablesFromSchemaDefinition } from "@executor-v2/confect";
2import {
3 ApprovalSchema,
4 CredentialRefSchema,
5 EventEnvelopeSchema,
6 OAuthTokenSchema,
7 PolicySchema,
8 ProfileSchema,
9 SourceSchema,
10 SyncStateSchema,
11 TaskRunSchema,
12 ToolArtifactSchema,
13 WorkspaceSchema,
14} from "@executor-v2/schema";
15
16export const executorConfectSchema = defineSchema({
17 profiles: defineTable(ProfileSchema).index("by_domainId", ["id"]),
18 workspaces: defineTable(WorkspaceSchema)
19 .index("by_domainId", ["id"])
20 .index("by_profileId", ["profileId"]),
21 sources: defineTable(SourceSchema)
22 .index("by_domainId", ["id"])
23 .index("by_workspaceId", ["workspaceId"]),
24 toolArtifacts: defineTable(ToolArtifactSchema)
25 .index("by_domainId", ["id"])
26 .index("by_workspaceId", ["workspaceId"])
27 .index("by_sourceId", ["sourceId"]),
28 credentialRefs: defineTable(CredentialRefSchema)
29 .index("by_domainId", ["id"])
30 .index("by_workspaceId", ["workspaceId"])
31 .index("by_sourceId", ["sourceId"]),
32 oauthTokens: defineTable(OAuthTokenSchema)
33 .index("by_domainId", ["id"])
34 .index("by_workspaceId", ["workspaceId"])
35 .index("by_sourceId", ["sourceId"]),
36 policies: defineTable(PolicySchema)
37 .index("by_domainId", ["id"])
38 .index("by_workspaceId", ["workspaceId"]),
39 approvals: defineTable(ApprovalSchema)
40 .index("by_domainId", ["id"])
41 .index("by_workspaceId", ["workspaceId"])
42 .index("by_taskRunId", ["taskRunId"]),
43 taskRuns: defineTable(TaskRunSchema)
44 .index("by_domainId", ["id"])
45 .index("by_workspaceId", ["workspaceId"])
46 .index("by_sessionId", ["sessionId"]),
47 syncStates: defineTable(SyncStateSchema)
48 .index("by_domainId", ["id"])
49 .index("by_workspaceId", ["workspaceId"]),
50 events: defineTable(EventEnvelopeSchema)
51 .index("by_domainId", ["id"])
52 .index("by_workspaceId", ["workspaceId"])
53 .index("by_workspaceId_sequence", ["workspaceId", "sequence"]),
54});
55
56export type ExecutorConfectTables = TablesFromSchemaDefinition<typeof executorConfectSchema>;
v2/packages/state-convex/tsconfig.json+0−4
@@ -1,4 +0,0 @@
1{
2 "extends": "../../tsconfig.json",
3 "include": ["src"]
4}