History

use effect in pm

Rhys Sullivan committed 8 months ago

Commit 31348e61 parent c4376dc7 files · +81 −118Browse files

Unified · Split

7 files
  1. v2/apps/pm/README.md +1 −1
  2. v2/apps/pm/package.json +2 −0
  3. v2/apps/pm/src/http-server.ts +34 −29
  4. v2/apps/pm/src/main.ts +2 −9
  5. v2/apps/pm/src/tool-call-handler.test.ts +14 −36
  6. v2/apps/pm/src/tool-call-handler.ts +26 −43
  7. v2/bun.lock +2 −0
v2/apps/pm/README.md+1−1
@@ -13,5 +13,5 @@ App wiring is now split by responsibility: · 12 unmodified lines
13- `src/config.ts`: Effect config service (`PORT`)13- `src/config.ts`: Effect config service (`PORT`)
14- `src/run-executor.ts`: run execution service for an injected runtime adapter14- `src/run-executor.ts`: run execution service for an injected runtime adapter
15- `src/mcp-handler.ts`: MCP transport wiring to run client15- `src/mcp-handler.ts`: MCP transport wiring to run client
16- `src/http-server.ts`: Bun server startup16- `src/http-server.ts`: Effect HTTP server startup
17- `src/main.ts`: Layer composition + process entrypoint17- `src/main.ts`: Layer composition + process entrypoint
v2/apps/pm/package.json+2−0
@@ -9,6 +9,8 @@ · 8 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 "@effect/platform": "catalog:",
13 "@effect/platform-bun": "^0.86.0",
12 "@executor-v2/engine": "workspace:*",14 "@executor-v2/engine": "workspace:*",
13 "@executor-v2/mcp-gateway": "workspace:*",15 "@executor-v2/mcp-gateway": "workspace:*",
14 "@executor-v2/sdk": "workspace:*",16 "@executor-v2/sdk": "workspace:*",
v2/apps/pm/src/http-server.ts+34−29
@@ -1,40 +1,45 @@
1import {
2 HttpRouter,
3 HttpServer,
4 HttpServerRequest,
5 HttpServerResponse,
6} from "@effect/platform";
7import { BunHttpServer, BunHttpServerRequest } from "@effect/platform-bun";
1import * as Effect from "effect/Effect";8import * as Effect from "effect/Effect";
9import * as Layer from "effect/Layer";
2 10
3import { PmConfig } from "./config";11import { PmConfig } from "./config";
4import { PmMcpHandler } from "./mcp-handler";12import { PmMcpHandler } from "./mcp-handler";
5import { PmToolCallHttpHandler } from "./tool-call-handler";13import { handleToolCallHttp } from "./tool-call-handler";
14
15const fromWebHandler = (handler: (request: Request) => Promise<Response>) =>
16 Effect.gen(function* () {
17 const request = yield* HttpServerRequest.HttpServerRequest;
18 const response = yield* Effect.promise(() =>
19 handler(BunHttpServerRequest.toRequest(request))
20 );
21
22 return HttpServerResponse.raw(response);
23 });
6 24
7export const startPmHttpServer = Effect.fn("@executor-v2/app-pm/http.start")(function* () {25export const startPmHttpServer = Effect.fn("@executor-v2/app-pm/http.start")(function* () {
8 const { port } = yield* PmConfig;26 const { port } = yield* PmConfig;
9 const { handleMcp } = yield* PmMcpHandler;27 const { handleMcp } = yield* PmMcpHandler;
10 const { handleToolCallHttp } = yield* PmToolCallHttpHandler;
11
12 const server = Bun.serve({
13 port,
14 routes: {
15 "/healthz": {
16 GET: () => Response.json({ ok: true, service: "pm" }, { status: 200 }),
17 },
18 "/mcp": {
19 GET: handleMcp,
20 POST: handleMcp,
21 DELETE: handleMcp,
22 },
23 "/v1/mcp": {
24 GET: handleMcp,
25 POST: handleMcp,
26 DELETE: handleMcp,
27 },
28 "/runtime/tool-call": {
29 POST: handleToolCallHttp,
30 },
31 "/v1/runtime/tool-call": {
32 POST: handleToolCallHttp,
33 },
34 },
35 });
36 28
37 yield* Effect.logInfo(`executor-v2 PM listening on http://127.0.0.1:${server.port}`);29 const httpLive = HttpRouter.empty.pipe(
30 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)),
35 HttpRouter.post("/v1/mcp", fromWebHandler(handleMcp)),
36 HttpRouter.del("/v1/mcp", fromWebHandler(handleMcp)),
37 HttpRouter.post("/runtime/tool-call", handleToolCallHttp),
38 HttpRouter.post("/v1/runtime/tool-call", handleToolCallHttp),
39 HttpServer.serve(),
40 HttpServer.withLogAddress,
41 Layer.provide(BunHttpServer.layer({ port })),
42 );
38 43
39 return server;44 return yield* Layer.launch(httpLive);
40});45});
v2/apps/pm/src/main.ts+2−9
@@ -10,10 +10,7 @@ import { · 9 unmodified lines
10 PmRunExecutorLive,10 PmRunExecutorLive,
11 PmToolProviderRegistryLive,11 PmToolProviderRegistryLive,
12} from "./run-executor";12} from "./run-executor";
13import {13import { PmToolCallHandlerLive } from "./tool-call-handler";
14 PmToolCallHandlerLive,
15 PmToolCallHttpHandlerLive,
16} from "./tool-call-handler";
17 14
18const runtimeAdapter = makeLocalInProcessRuntimeAdapter();15const runtimeAdapter = makeLocalInProcessRuntimeAdapter();
19const PmMcpDependenciesLive = Layer.merge(16const PmMcpDependenciesLive = Layer.merge(
@@ -21,14 +18,10 @@ const PmMcpDependenciesLive = Layer.merge( · 1 unmodified line
21 PmToolProviderRegistryLive,18 PmToolProviderRegistryLive,
22);19);
23 20
24const PmToolCallDependenciesLive = PmToolCallHttpHandlerLive.pipe(
25 Layer.provide(PmToolCallHandlerLive),
26);
27
28const PmAppLive = Layer.mergeAll(21const PmAppLive = Layer.mergeAll(
29 PmConfigLive,22 PmConfigLive,
30 PmMcpHandlerLive.pipe(Layer.provide(PmMcpDependenciesLive)),23 PmMcpHandlerLive.pipe(Layer.provide(PmMcpDependenciesLive)),
31 PmToolCallDependenciesLive,24 PmToolCallHandlerLive,
32);25);
33 26
34const program = startPmHttpServer().pipe(Effect.provide(PmAppLive));27const program = startPmHttpServer().pipe(Effect.provide(PmAppLive));
v2/apps/pm/src/tool-call-handler.test.ts+14−36
@@ -1,12 +1,10 @@
1import { describe, expect, it } from "@effect/vitest";1import { describe, expect, it } from "@effect/vitest";
2import * as Effect from "effect/Effect";2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
4 3
5import {4import {
5 handleToolCallBody,
6 PmToolCallHandler,6 PmToolCallHandler,
7 PmToolCallHandlerLive,7 PmToolCallHandlerLive,
8 PmToolCallHttpHandler,
9 PmToolCallHttpHandlerLive,
10} from "./tool-call-handler";8} from "./tool-call-handler";
11 9
12describe("PM runtime tool-call handling", () => {10describe("PM runtime tool-call handling", () => {
@@ -29,40 +27,20 @@ describe("PM runtime tool-call handling", () => { · 16 unmodified lines
29 }).pipe(Effect.provide(PmToolCallHandlerLive)),27 }).pipe(Effect.provide(PmToolCallHandlerLive)),
30 );28 );
31 29
32 it.effect("decodes callback request payload and returns JSON result", () =>30 it.effect("decodes callback request payload and returns failed callback result", () =>
33 Effect.gen(function* () {31 Effect.gen(function* () {
34 const httpHandler = yield* PmToolCallHttpHandler;32 const result = yield* handleToolCallBody({
35 33 runId: "run_2",
36 const response = yield* Effect.tryPromise(() =>34 callId: "call_2",
37 httpHandler.handleToolCallHttp(35 toolPath: "tools.example.weather",
38 new Request("http://127.0.0.1/runtime/tool-call", {36 input: { city: "London" },
39 method: "POST",37 });
40 headers: { "content-type": "application/json" },
41 body: JSON.stringify({
42 runId: "run_2",
43 callId: "call_2",
44 toolPath: "tools.example.weather",
45 input: { city: "London" },
46 }),
47 }),
48 ),
49 );
50
51 expect(response.status).toBe(200);
52
53 const payload = (yield* Effect.tryPromise(() => response.json())) as {
54 ok: boolean;
55 kind?: string;
56 error?: string;
57 };
58 38
59 expect(payload.ok).toBe(false);39 expect(result.ok).toBe(false);
60 expect(payload.kind).toBe("failed");40 if (!result.ok) {
61 expect(payload.error).toContain("tools.example.weather");41 expect(result.kind).toBe("failed");
62 }).pipe(42 expect(result.error).toContain("tools.example.weather");
63 Effect.provide(43 }
64 PmToolCallHttpHandlerLive.pipe(Layer.provide(PmToolCallHandlerLive)),44 }).pipe(Effect.provide(PmToolCallHandlerLive)),
65 ),
66 ),
67 );45 );
68});46});
v2/apps/pm/src/tool-call-handler.ts+26−43
@@ -2,11 +2,11 @@ import type { · 1 unmodified line
2 RuntimeToolCallRequest,2 RuntimeToolCallRequest,
3 RuntimeToolCallResult,3 RuntimeToolCallResult,
4} from "@executor-v2/sdk";4} from "@executor-v2/sdk";
5import { HttpServerRequest, HttpServerResponse } from "@effect/platform";
5import * as Context from "effect/Context";6import * as Context from "effect/Context";
6import * as Data from "effect/Data";7import * as Data from "effect/Data";
7import * as Effect from "effect/Effect";8import * as Effect from "effect/Effect";
8import * as Layer from "effect/Layer";9import * as Layer from "effect/Layer";
9import * as Runtime from "effect/Runtime";
10import * as Schema from "effect/Schema";10import * as Schema from "effect/Schema";
11 11
12export type PmToolCallHandlerService = {12export type PmToolCallHandlerService = {
@@ -77,48 +77,31 @@ export const PmToolCallHandlerLive = Layer.succeed( · 64 unmodified lines
77 }),77 }),
78);78);
79 79
80export type PmToolCallHttpHandlerService = {80export const handleToolCallBody = Effect.fn(
81 handleToolCallHttp: (request: Request) => Promise<Response>;81 "@executor-v2/app-pm/tool-call.handle-body",
82};82)(function* (body: unknown) {
83 83 const handler = yield* PmToolCallHandler;
84export class PmToolCallHttpHandler extends Context.Tag("@executor-v2/app-pm/PmToolCallHttpHandler")<84 const input = yield* decodeRuntimeToolCallRequest(body).pipe(
85 PmToolCallHttpHandler,85 Effect.mapError(decodeRequestBodyError),
86 PmToolCallHttpHandlerService86 );
87>() {}
88 87
89export const PmToolCallHttpHandlerLive = Layer.effect(88 return yield* handler.handleToolCall(input);
90 PmToolCallHttpHandler,89});
91 Effect.gen(function* () {
92 const handler = yield* PmToolCallHandler;
93 const runtime = yield* Effect.runtime<never>();
94 const runPromise = Runtime.runPromise(runtime);
95
96 const handleToolCallHttp = (request: Request): Promise<Response> =>
97 runPromise(
98 Effect.tryPromise({
99 try: () => request.json(),
100 catch: decodeRequestBodyError,
101 }).pipe(
102 Effect.flatMap((body) => decodeRuntimeToolCallRequest(body)),
103 Effect.flatMap((input) => handler.handleToolCall(input)),
104 Effect.map((result) => Response.json(result, { status: 200 })),
105 Effect.catchAll((error) =>
106 Effect.succeed(
107 Response.json(
108 {
109 ok: false,
110 kind: "failed",
111 error: errorToText(error),
112 } satisfies RuntimeToolCallResult,
113 { status: 400 },
114 ),
115 ),
116 ),
117 ),
118 );
119 90
120 return PmToolCallHttpHandler.of({91export const handleToolCallHttp = Effect.gen(function* () {
121 handleToolCallHttp,92 const body = yield* HttpServerRequest.schemaBodyJson(Schema.Unknown);
122 });93 const result = yield* handleToolCallBody(body);
123 }),94
95 return yield* HttpServerResponse.json(result, { status: 200 });
96}).pipe(
97 Effect.catchAll((error) =>
98 HttpServerResponse.json(
99 {
100 ok: false,
101 kind: "failed",
102 error: errorToText(error),
103 } satisfies RuntimeToolCallResult,
104 { status: 400 },
105 ),
106 ),
124);107);
v2/bun.lock+2−0
@@ -37,6 +37,8 @@ · 36 unmodified lines
37 "apps/pm": {37 "apps/pm": {
38 "name": "@executor-v2/app-pm",38 "name": "@executor-v2/app-pm",
39 "dependencies": {39 "dependencies": {
40 "@effect/platform": "catalog:",
41 "@effect/platform-bun": "^0.86.0",
40 "@executor-v2/engine": "workspace:*",42 "@executor-v2/engine": "workspace:*",
41 "@executor-v2/mcp-gateway": "workspace:*",43 "@executor-v2/mcp-gateway": "workspace:*",
42 "@executor-v2/sdk": "workspace:*",44 "@executor-v2/sdk": "workspace:*",