Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apps/cloud/src/auth/context.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Context, Effect, Layer } from "effect";
import { makeUserStore } from "../services/user-store";
import { DbService } from "../services/db";
import { UserStoreError, withServiceLogging } from "./errors";
import { UserStoreError, tryPromiseService, withServiceLogging } from "./errors";

// AuthContext is defined in ./middleware.ts to keep middleware-related types together.
export { AuthContext } from "./middleware";
Expand All @@ -17,7 +17,7 @@ const makeService = (store: RawStore) => ({
withServiceLogging(
"user_store",
() => new UserStoreError(),
Effect.tryPromise({ try: () => fn(store), catch: (e) => e }),
tryPromiseService(() => fn(store)),
),
});

Expand Down
23 changes: 22 additions & 1 deletion apps/cloud/src/auth/errors.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { HttpApiSchema } from "@effect/platform";
import { Effect, Schema } from "effect";
import { Data, Effect, Schema } from "effect";

export class UserStoreError extends Schema.TaggedError<UserStoreError>()(
"UserStoreError",
Expand All @@ -13,6 +13,27 @@ export class WorkOSError extends Schema.TaggedError<WorkOSError>()(
HttpApiSchema.annotations({ status: 500 }),
) {}

/**
* Private wrapper used by service adapters that lift Promise APIs into
* Effect. `withServiceLogging` immediately remaps these into a public-facing
* tagged error, so callers never observe this tag directly — its only job is
* to keep the internal failure channel typed instead of `unknown` / `Error`.
*/
export class ServiceAdapterError extends Data.TaggedError(
"ServiceAdapterError",
)<{
readonly cause: unknown;
}> {}

/** Lift a Promise-returning function into Effect with a typed failure channel. */
export const tryPromiseService = <A>(
fn: () => Promise<A>,
): Effect.Effect<A, ServiceAdapterError> =>
Effect.tryPromise({
try: fn,
catch: (cause) => new ServiceAdapterError({ cause }),
});

/**
* Service-boundary error wrapper. Logs the full Cause chain (drizzle
* query/params, pg error codes, nested Error.cause, etc.) via Effect's
Expand Down
4 changes: 2 additions & 2 deletions apps/cloud/src/auth/workos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import { Context, Effect, Layer } from "effect";
import { WorkOS } from "@workos-inc/node/worker";
import { WorkOSError, withServiceLogging } from "./errors";
import { WorkOSError, tryPromiseService, withServiceLogging } from "./errors";
import { server } from "../env";

const COOKIE_NAME = "wos-session";
Expand All @@ -29,7 +29,7 @@ const make = Effect.gen(function* () {
withServiceLogging(
"workos",
() => new WorkOSError(),
Effect.tryPromise({ try: () => fn(workos), catch: (e) => e }),
tryPromiseService(() => fn(workos)),
);

const authenticateSealedSession = (sessionData: string) =>
Expand Down
12 changes: 8 additions & 4 deletions apps/cloud/src/mcp-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// ---------------------------------------------------------------------------

import { DurableObject, env } from "cloudflare:workers";
import { Effect, Layer } from "effect";
import { Data, Effect, Layer } from "effect";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { WorkerTransport, type TransportState } from "agents/mcp";

Expand Down Expand Up @@ -39,15 +39,19 @@ const DbLive = DbService.Live;
const UserStoreLive = UserStoreService.Live.pipe(Layer.provide(DbLive));
const Services = Layer.mergeAll(DbLive, UserStoreLive);

class OrganizationNotFoundError extends Data.TaggedError(
"OrganizationNotFoundError",
)<{
readonly organizationId: string;
}> {}

const initSession = (organizationId: string) =>
Effect.gen(function* () {
const users = yield* UserStoreService;
const org = yield* users.use((store) => store.getOrganization(organizationId));

if (!org) {
return yield* Effect.fail(
new Error(`Organization ${organizationId} not found`),
);
return yield* new OrganizationNotFoundError({ organizationId });
}

const executor = yield* createOrgExecutor(
Expand Down
3 changes: 2 additions & 1 deletion examples/promise-sdk/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"esModuleInterop": true,
"skipLibCheck": true,
"outDir": "dist",
"rootDir": "src"
"rootDir": "src",
"types": ["node"]
},
"include": ["src"]
}
13 changes: 11 additions & 2 deletions packages/core/api/src/handlers/executions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,20 @@ export const ExecutionsHandlers = HttpApiBuilder.group(
});
}

const formatted = formatExecuteResult(result);
if (result.status === "completed") {
const formatted = formatExecuteResult(result.result);
return {
text: formatted.text,
structured: formatted.structured,
isError: formatted.isError,
};
}

const formatted = formatPausedExecution(result.execution);
return {
text: formatted.text,
structured: formatted.structured,
isError: formatted.isError,
isError: false,
};
}),
),
Expand Down
19 changes: 10 additions & 9 deletions packages/core/execution/src/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,13 @@ export type ExecutionResult =
export type PausedExecution = {
readonly id: string;
readonly elicitationContext: ElicitationContext;
/** Deferred the caller completes with the user's response to resume the fiber. */
};

/** Internal representation with Effect runtime state for pause/resume. */
type InternalPausedExecution = PausedExecution & {
readonly response: Deferred.Deferred<typeof ElicitationResponse.Type>;
/** The fiber running the sandboxed code — stays alive across pause/resume cycles. */
readonly fiber: Fiber.Fiber<ExecuteResult, unknown>;
/** Ref to the current pause signal — swapped by resume() before unblocking. */
readonly pauseSignalRef: Ref.Ref<Deferred.Deferred<PausedExecution>>;
readonly pauseSignalRef: Ref.Ref<Deferred.Deferred<InternalPausedExecution>>;
};

export type ResumeResponse = {
Expand Down Expand Up @@ -288,7 +289,7 @@ const runEffect = <A>(effect: Effect.Effect<A, unknown>): Promise<A> =>
export const createExecutionEngine = (config: ExecutionEngineConfig): ExecutionEngine => {
const { executor } = config;
const codeExecutor = config.codeExecutor ?? makeQuickJsExecutor();
const pausedExecutions = new Map<string, PausedExecution>();
const pausedExecutions = new Map<string, InternalPausedExecution>();
let nextId = 0;

/**
Expand All @@ -298,7 +299,7 @@ export const createExecutionEngine = (config: ExecutionEngineConfig): ExecutionE
*/
const awaitCompletionOrPause = (
fiber: Fiber.Fiber<ExecuteResult, unknown>,
pauseSignal: Deferred.Deferred<PausedExecution>,
pauseSignal: Deferred.Deferred<InternalPausedExecution>,
): Effect.Effect<ExecutionResult> =>
Effect.race(
Fiber.join(fiber).pipe(
Expand All @@ -321,7 +322,7 @@ export const createExecutionEngine = (config: ExecutionEngineConfig): ExecutionE
// it each time it fires, so resume() can swap in a fresh Deferred
// before unblocking the fiber.
const pauseSignalRef = yield* Ref.make(
yield* Deferred.make<PausedExecution>(),
yield* Deferred.make<InternalPausedExecution>(),
);

// Will be set once the fiber is forked.
Expand All @@ -332,7 +333,7 @@ export const createExecutionEngine = (config: ExecutionEngineConfig): ExecutionE
const responseDeferred = yield* Deferred.make<typeof ElicitationResponse.Type>();
const id = `exec_${++nextId}`;

const paused: PausedExecution = {
const paused: InternalPausedExecution = {
id,
elicitationContext: ctx,
response: responseDeferred,
Expand Down Expand Up @@ -371,7 +372,7 @@ export const createExecutionEngine = (config: ExecutionEngineConfig): ExecutionE

// Swap in a fresh pause signal BEFORE unblocking the fiber, so the
// next elicitation handler call signals this new Deferred.
const nextSignal = yield* Deferred.make<PausedExecution>();
const nextSignal = yield* Deferred.make<InternalPausedExecution>();
yield* Ref.set(paused.pauseSignalRef, nextSignal);

yield* Deferred.succeed(paused.response, {
Expand Down
8 changes: 3 additions & 5 deletions packages/core/execution/src/tool-invoker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
inMemoryToolsPlugin,
makeTestConfig,
tool,
type ToolId,
} from "@executor/sdk";
import { createExecutionEngine } from "./engine";
import { describeTool, searchTools } from "./tool-invoker";
Expand All @@ -22,7 +21,6 @@ const ContactInput = Schema.Struct({
email: Schema.String,
});

import type { ExecutionResult } from "./engine";
import { FormElicitation } from "@executor/sdk";

const acceptAll = () => Effect.succeed(new ElicitationResponse({ action: "accept" }));
Expand Down Expand Up @@ -344,8 +342,8 @@ describe("pause/resume with multiple elicitations", () => {
engine.executeWithPause(code),
);
expect(outcome1.status).toBe("paused");
if (outcome1.status !== "paused") throw new Error("expected pause");
expect(outcome1.execution.elicitationContext.request.message).toBe(
const paused1 = outcome1 as Extract<typeof outcome1, { status: "paused" }>;
expect(paused1.execution.elicitationContext.request.message).toBe(
"First approval",
);

Expand All @@ -354,7 +352,7 @@ describe("pause/resume with multiple elicitations", () => {
// result or the completion).
const outcome2 = yield* Effect.promise(() =>
Promise.race([
engine.resume(outcome1.execution.id, { action: "accept" }),
engine.resume(paused1.execution.id, { action: "accept" }),
new Promise<never>((_, reject) =>
setTimeout(
() =>
Expand Down
4 changes: 1 addition & 3 deletions packages/core/execution/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,7 @@
"plugins": [
{
"name": "@effect/language-service",
"diagnosticSeverity": {
"globalErrorInEffectCatch": "off"
}
"diagnosticSeverity": {}
}
]
},
Expand Down
Loading