// --------------------------------------------------------------------------- // @executor-js/execution/promise — Promise-native surface for the execution // engine. // --------------------------------------------------------------------------- // // `engine.ts` is Effect-native; this module runs each method with // `Effect.runPromise` at the boundary so hosts that can't compose Effects // (the MCP SDK tool handlers, plain async call sites) can still use the // engine. Callers already inside an Effect context should import directly // from `@executor-js/execution` to keep trace context intact. // --------------------------------------------------------------------------- import { Data, Effect } from "effect"; import type * as Cause from "effect/Cause"; import type { ElicitationContext, ElicitationResponse, Executor as EffectExecutor, } from "@executor-js/sdk/core"; import { ToolId } from "@executor-js/sdk/core"; import type { Executor as PromiseExecutor } from "@executor-js/sdk/promise"; import type { CodeExecutionError, CodeExecutor, ExecuteResult } from "@executor-js/codemode-core"; import { createExecutionEngine as createEffectExecutionEngine, type ExecutionEngine as EffectExecutionEngine, type ExecutionResult, type PausedExecution, type ResumeResponse, } from "./engine"; export type ElicitationHandler = (ctx: ElicitationContext) => Promise; export type ExecutionEngineConfig = { readonly executor: PromiseExecutor; readonly codeExecutor: CodeExecutor; }; export type ExecutionEngine = { readonly execute: ( code: string, options: { readonly onElicitation: ElicitationHandler }, ) => Promise; readonly executeWithPause: (code: string) => Promise; readonly resume: ( executionId: string, response: ResumeResponse, ) => Promise; readonly getDescription: () => Promise; }; class PromiseExecutorError extends Data.TaggedError("PromiseExecutorError")<{ readonly cause: unknown; }> {} /** * Wrap a Promise-style executor into the Effect shape the engine consumes. */ const fromPromise = (try_: () => Promise): Effect.Effect => Effect.tryPromise({ try: try_, catch: (cause) => new PromiseExecutorError({ cause }) }).pipe( // oxlint-disable-next-line executor/no-effect-escape-hatch -- boundary: Promise executor facade has already erased the SDK typed error channel Effect.orDie, ); type EffectInvokeOptions = Parameters[2]; type PromiseInvokeOptions = Parameters[2]; const toPromiseInvokeOptions = (options: EffectInvokeOptions): PromiseInvokeOptions => { const onElicitation = options?.onElicitation; if (!onElicitation) return undefined; if (onElicitation === "accept-all") return { onElicitation }; return { onElicitation: (ctx) => onElicitation({ ...ctx, toolId: ToolId.make(ctx.toolId), }), }; }; const wrapPromiseExecutor = (pe: PromiseExecutor): EffectExecutor => ({ scopes: pe.scopes, tools: { invoke: (id, args, options) => fromPromise(() => pe.tools.invoke(id, args, toPromiseInvokeOptions(options))), list: (filter) => fromPromise(() => pe.tools.list(filter)), schema: (id) => fromPromise(() => pe.tools.schema(id)), definitions: () => fromPromise(() => pe.tools.definitions()), }, sources: { list: () => fromPromise(() => pe.sources.list()), remove: (input) => fromPromise(() => pe.sources.remove(input)), refresh: (input) => fromPromise(() => pe.sources.refresh(input)), detect: (url) => fromPromise(() => pe.sources.detect(url)), definitions: (id) => fromPromise(() => pe.sources.definitions(id)), }, secrets: { get: (id) => fromPromise(() => pe.secrets.get(id)), getAtScope: (id, scope) => fromPromise(() => pe.secrets.getAtScope(id, scope)), status: (id) => fromPromise(() => pe.secrets.status(id)), set: (input) => fromPromise(() => pe.secrets.set(input)), remove: (input) => fromPromise(() => pe.secrets.remove(input)), list: () => fromPromise(() => pe.secrets.list()), listAll: () => fromPromise(() => pe.secrets.listAll()), usages: (id) => fromPromise(() => pe.secrets.usages(id)), providers: () => fromPromise(() => pe.secrets.providers()), }, connections: { get: (id) => fromPromise(() => pe.connections.get(id)), getAtScope: (id, scope) => fromPromise(() => pe.connections.getAtScope(id, scope)), list: () => fromPromise(() => pe.connections.list()), create: (input) => fromPromise(() => pe.connections.create(input)), updateTokens: (input) => fromPromise(() => pe.connections.updateTokens(input)), setIdentityLabel: (id, label) => fromPromise(() => pe.connections.setIdentityLabel(id, label)), accessToken: (id) => fromPromise(() => pe.connections.accessToken(id)), accessTokenAtScope: (id, scope) => fromPromise(() => pe.connections.accessTokenAtScope(id, scope)), remove: (input) => fromPromise(() => pe.connections.remove(input)), usages: (id) => fromPromise(() => pe.connections.usages(id)), providers: () => fromPromise(() => pe.connections.providers()), }, credentialBindings: { listForSource: (input) => fromPromise(() => pe.credentialBindings.listForSource(input)), resolve: (input) => fromPromise(() => pe.credentialBindings.resolve(input)), set: (input) => fromPromise(() => pe.credentialBindings.set(input)), remove: (input) => fromPromise(() => pe.credentialBindings.remove(input)), replaceForSource: (input) => fromPromise(() => pe.credentialBindings.replaceForSource(input)), removeForSource: (input) => fromPromise(() => pe.credentialBindings.removeForSource(input)), usagesForSecret: (id) => fromPromise(() => pe.credentialBindings.usagesForSecret(id)), usagesForConnection: (id) => fromPromise(() => pe.credentialBindings.usagesForConnection(id)), }, oauth: { probe: (input) => fromPromise(() => pe.oauth.probe(input)), start: (input) => fromPromise(() => pe.oauth.start(input)), complete: (input) => fromPromise(() => pe.oauth.complete(input)), cancel: (sessionId, tokenScope) => fromPromise(() => pe.oauth.cancel(sessionId, tokenScope)), }, policies: { list: () => fromPromise(() => pe.policies.list()), create: (input) => fromPromise(() => pe.policies.create(input)), update: (input) => fromPromise(() => pe.policies.update(input)), remove: (input) => fromPromise(() => pe.policies.remove(input)), resolve: (id) => fromPromise(() => pe.policies.resolve(id)), }, close: () => fromPromise(() => pe.close()), }); /** * Promise-wrap an Effect-native `ExecutionEngine` (from `./engine`). * Exposed separately so callers that already hold an Effect engine * (apps/cloud's execution-stack composes both) can convert it for hosts * that need the Promise surface (host-mcp). */ export const toPromiseExecutionEngine = ( engine: EffectExecutionEngine, ): ExecutionEngine => ({ execute: (code, options) => Effect.runPromise( engine.execute(code, { onElicitation: (ctx) => // oxlint-disable-next-line executor/no-effect-escape-hatch -- boundary: host-provided Promise elicitation callback is outside the Effect error model Effect.tryPromise(() => options.onElicitation(ctx)).pipe(Effect.orDie), }), ), executeWithPause: (code) => Effect.runPromise(engine.executeWithPause(code)), resume: (executionId, response) => Effect.runPromise(engine.resume(executionId, response)), getDescription: () => Effect.runPromise(engine.getDescription), }); export const createExecutionEngine = ( config: ExecutionEngineConfig, ): ExecutionEngine => toPromiseExecutionEngine( createEffectExecutionEngine({ executor: wrapPromiseExecutor(config.executor), codeExecutor: config.codeExecutor, }), ); // --------------------------------------------------------------------------- // Re-exports — plain types/helpers that don't carry Effect signatures. // --------------------------------------------------------------------------- export { formatExecuteResult, formatPausedExecution } from "./engine"; export type { ExecutionResult, PausedExecution, ResumeResponse }; export { buildExecuteDescription } from "./description"; export { ExecutionToolError } from "./errors";