import { Context, Effect } from "effect"; import type { ToolId, SecretId, PolicyId } from "./ids"; import type { SecretProvider, SecretRef, SecretStore, SetSecretInput } from "./secrets"; import type { ToolMetadata, ToolSchema, ToolInvocationResult, ToolRegistry, ToolListFilter, InvokeOptions, } from "./tools"; import type { Source, SourceDetectionResult, SourceRegistry } from "./sources"; import type { Policy, PolicyEngine } from "./policies"; import type { Scope } from "./scope"; import type { ExecutorPlugin, PluginExtensions, PluginHandle, } from "./plugin"; import type { ToolNotFoundError, ToolInvocationError, SecretNotFoundError, SecretResolutionError, PolicyDeniedError, } from "./errors"; import { FormElicitation, ElicitationDeclinedError, ElicitationResponse, type ElicitationHandler, } from "./elicitation"; const resolveElicitationHandler = (options: InvokeOptions): ElicitationHandler => options.onElicitation === "accept-all" ? () => Effect.succeed(new ElicitationResponse({ action: "accept" })) : options.onElicitation; // --------------------------------------------------------------------------- // Executor — the main public API, expands with plugins // --------------------------------------------------------------------------- export type Executor< TPlugins extends readonly ExecutorPlugin[] = [], > = { readonly scope: Scope; readonly tools: { readonly list: (filter?: ToolListFilter) => Effect.Effect; readonly schema: ( toolId: string, ) => Effect.Effect; /** Shared schema definitions across all tools */ readonly definitions: () => Effect.Effect>; readonly invoke: ( toolId: string, args: unknown, options: InvokeOptions, ) => Effect.Effect< ToolInvocationResult, | ToolNotFoundError | ToolInvocationError | PolicyDeniedError | ElicitationDeclinedError >; }; readonly sources: { readonly list: () => Effect.Effect; readonly remove: (sourceId: string) => Effect.Effect; readonly refresh: (sourceId: string) => Effect.Effect; readonly detect: (url: string) => Effect.Effect; }; readonly policies: { readonly list: () => Effect.Effect; readonly add: ( policy: Omit, ) => Effect.Effect; readonly remove: (policyId: string) => Effect.Effect; }; readonly secrets: { readonly list: () => Effect.Effect; /** Resolve a secret value by id */ readonly resolve: ( secretId: SecretId, ) => Effect.Effect; /** Check if a secret can be resolved */ readonly status: ( secretId: SecretId, ) => Effect.Effect<"resolved" | "missing">; /** Store a secret value (creates ref + writes to provider) */ readonly set: (input: Omit) => Effect.Effect; readonly remove: ( secretId: SecretId, ) => Effect.Effect; /** Register a secret provider */ readonly addProvider: (provider: SecretProvider) => Effect.Effect; /** List registered provider keys */ readonly providers: () => Effect.Effect; }; readonly close: () => Effect.Effect; } & PluginExtensions; // --------------------------------------------------------------------------- // Resolved services — what we need to build an Executor // --------------------------------------------------------------------------- export type ToolRegistryService = Context.Tag.Service; export type SourceRegistryService = Context.Tag.Service; export type SecretStoreService = Context.Tag.Service; export type PolicyEngineService = Context.Tag.Service; export interface ExecutorConfig< TPlugins extends readonly ExecutorPlugin[] = [], > { readonly scope: Scope; readonly tools: ToolRegistryService; readonly sources: SourceRegistryService; readonly secrets: SecretStoreService; readonly policies: PolicyEngineService; readonly plugins?: TPlugins; } // --------------------------------------------------------------------------- // createExecutor — builds an Executor, initializes plugins // --------------------------------------------------------------------------- export const createExecutor = < const TPlugins extends readonly ExecutorPlugin[] = [], >( config: ExecutorConfig, ): Effect.Effect, Error> => Effect.gen(function* () { const { scope, tools, sources, secrets, policies, plugins = [] } = config; // Initialize all plugins const handles = new Map>(); const extensions: Record = {}; for (const plugin of plugins) { const handle = yield* plugin.init({ scope, tools, sources, secrets, policies, }); handles.set(plugin.key, handle); extensions[plugin.key] = handle.extension; } const base = { scope, tools: { list: (filter?: ToolListFilter) => tools.list(filter), schema: (toolId: string) => tools.schema(toolId as ToolId), definitions: () => tools.definitions(), invoke: (toolId: string, args: unknown, options: InvokeOptions) => { const tid = toolId as ToolId; return Effect.gen(function* () { yield* policies.check({ scopeId: scope.id, toolId: tid }); // Dynamically resolve annotations from the plugin const annotations = yield* tools.resolveAnnotations(tid); if (annotations?.requiresApproval) { const handler = resolveElicitationHandler(options); const response = yield* handler({ toolId: tid, args, request: new FormElicitation({ message: annotations.approvalDescription ?? `Approve ${toolId}?`, requestedSchema: {}, }), }); if (response.action !== "accept") { return yield* new ElicitationDeclinedError({ toolId: tid, action: response.action, }); } } return yield* tools.invoke(tid, args, options); }); }, }, sources: { list: () => sources.list(), remove: (sourceId: string) => sources.remove(sourceId), refresh: (sourceId: string) => sources.refresh(sourceId), detect: (url: string) => sources.detect(url), }, policies: { list: () => policies.list(scope.id), add: (policy: Omit) => policies.add({ ...policy, scopeId: scope.id }), remove: (policyId: string) => policies.remove(policyId as PolicyId), }, secrets: { list: () => secrets.list(scope.id), resolve: (secretId: SecretId) => secrets.resolve(secretId, scope.id), status: (secretId: SecretId) => secrets.status(secretId, scope.id), set: (input: Omit) => secrets.set({ ...input, scopeId: scope.id }), remove: (secretId: SecretId) => secrets.remove(secretId), addProvider: (provider: SecretProvider) => secrets.addProvider(provider), providers: () => secrets.providers(), }, close: () => Effect.gen(function* () { for (const handle of handles.values()) { if (handle.close) yield* handle.close(); } }), }; return Object.assign(base, extensions) as Executor; });