import { describe, expect, it } from "@effect/vitest";import { Effect, Fiber } from "effect"; import { ElicitationResponse, FormElicitation, createExecutor, definePlugin, makeTestConfig,} from "@executor-js/sdk";import { makeQuickJsExecutor } from "@executor-js/runtime-quickjs";import { createExecutionEngine } from "./engine";import { describeTool, makeExecutorToolInvoker, searchTools } from "./tool-invoker"; const codeExecutor = makeQuickJsExecutor(); const RepoInputSchema = { type: "object", properties: { owner: { type: "string" }, repo: { type: "string" }, }, required: ["owner", "repo"], additionalProperties: false,} as const; const ContactInputSchema = { type: "object", properties: { email: { type: "string" }, }, required: ["email"], additionalProperties: false,} as const; const EmptyInputSchema = { type: "object", properties: {}, additionalProperties: false,} as const; const acceptAll = () => Effect.succeed(new ElicitationResponse({ action: "accept" })); // ---------------------------------------------------------------------------// Test plugins — each one declares a namespace as a static source with N// tools. Handlers return static data; the suite only cares about discovery// + elicitation flow, not real invocation semantics.// --------------------------------------------------------------------------- const githubPlugin = definePlugin(() => ({ id: "github-test" as const, storage: () => ({}), staticSources: () => [ { id: "github", kind: "in-memory", name: "GitHub", tools: [ { name: "listRepositoryIssues", description: "List issues for a repository", inputSchema: RepoInputSchema, handler: () => Effect.succeed([]), }, { name: "getRepositoryDetails", description: "Get repository details including the default branch", inputSchema: RepoInputSchema, handler: () => Effect.succeed({ defaultBranch: "main" }), }, { name: "searchDocs", description: "Search GitHub API documentation", inputSchema: EmptyInputSchema, handler: () => Effect.succeed([]), }, ], }, ],})); const crmPlugin = definePlugin(() => ({ id: "crm-test" as const, storage: () => ({}), staticSources: () => [ { id: "crm", kind: "in-memory", name: "CRM", tools: [ { name: "createContact", description: "Create a CRM contact record", inputSchema: ContactInputSchema, handler: () => Effect.succeed({ id: "contact_1" }), }, { name: "listContacts", description: "List CRM contacts", inputSchema: EmptyInputSchema, handler: () => Effect.succeed([]), }, ], }, ],})); const errorPlugin = definePlugin(() => ({ id: "error-test" as const, storage: () => ({}), staticSources: () => [ { id: "records", kind: "in-memory", name: "Records", tools: [ { name: "queryRows", description: "Query rows", inputSchema: EmptyInputSchema, handler: () => Effect.succeed({ data: null, error: { message: 'Field with name "DisplayName" does not exist', code: "invalid_query", }, }), }, ], }, ],})); const makeSearchExecutor = () => createExecutor(makeTestConfig({ plugins: [githubPlugin(), crmPlugin()] as const })); describe("tool discovery", () => { it.effect("ranks matches using ids, namespaces, camelCase names, and descriptions", () => Effect.gen(function* () { const executor = yield* makeSearchExecutor(); const githubMatches = yield* searchTools(executor, "github issues", 5); expect(githubMatches.items.map((match) => match.path)).toEqual([ "github.listRepositoryIssues", ]); expect(githubMatches.items[0]?.score ?? 0).toBeGreaterThan(0); expect(githubMatches.hasMore).toBe(false); expect(githubMatches.nextOffset).toBeNull(); const repoMatches = yield* searchTools(executor, "repo details", 5); expect(repoMatches.items[0]?.path).toBe("github.getRepositoryDetails"); const crmMatches = yield* searchTools(executor, "crm create contact", 5); expect(crmMatches.items[0]?.path).toBe("crm.createContact"); expect(crmMatches.items[0]?.score ?? 0).toBeGreaterThan(crmMatches.items[1]?.score ?? 0); }), ); it.effect("returns no matches for empty queries instead of listing arbitrary tools", () => Effect.gen(function* () { const executor = yield* makeSearchExecutor(); const matches = yield* searchTools(executor, "", 5); expect(matches.items).toEqual([]); expect(matches.total).toBe(0); expect(matches.hasMore).toBe(false); expect(matches.nextOffset).toBeNull(); }), ); it.effect("paginates ranked matches via limit + offset with hasMore + nextOffset", () => Effect.gen(function* () { const executor = yield* makeSearchExecutor(); // "list" matches `listRepositoryIssues`, `searchDocs` (description has // "documentation" which tokenises adjacent), `listContacts`, etc. // The exact match set isn't important — the pagination invariants are. const all = yield* searchTools(executor, "list", 100); expect(all.items.length).toBeGreaterThan(1); expect(all.total).toBe(all.items.length); expect(all.hasMore).toBe(false); expect(all.nextOffset).toBeNull(); // First page (limit 1) — matches truncate, hasMore + nextOffset surface. const firstPage = yield* searchTools(executor, "list", 1); expect(firstPage.items).toEqual([all.items[0]]); expect(firstPage.total).toBe(all.total); expect(firstPage.hasMore).toBe(true); expect(firstPage.nextOffset).toBe(1); // Second page using nextOffset — order matches the un-paginated rank. const secondPage = yield* searchTools(executor, "list", 1, { offset: firstPage.nextOffset!, }); expect(secondPage.items).toEqual([all.items[1]]); expect(secondPage.total).toBe(all.total); // Whether hasMore is true depends on total; at minimum it's consistent. expect(secondPage.hasMore).toBe(all.total > 2); expect(secondPage.nextOffset).toBe(secondPage.hasMore ? 2 : null); // Offset past the end — empty page, no more. const past = yield* searchTools(executor, "list", 5, { offset: all.total + 10 }); expect(past.items).toEqual([]); expect(past.total).toBe(all.total); expect(past.hasMore).toBe(false); expect(past.nextOffset).toBeNull(); }), ); it.effect("can narrow discovery to a namespace", () => Effect.gen(function* () { const executor = yield* makeSearchExecutor(); const githubOnly = yield* searchTools(executor, "list", 5, { namespace: "github", }); expect(githubOnly.items.map((match) => match.path)).toEqual(["github.listRepositoryIssues"]); const crmOnly = yield* searchTools(executor, "list", 5, { namespace: "crm", }); expect(crmOnly.items.map((match) => match.path)).toEqual(["crm.listContacts"]); const sandboxResult = yield* createExecutionEngine({ executor, codeExecutor }).execute( 'return await tools.search({ namespace: "crm", query: "create contact", limit: 5 });', { onElicitation: acceptAll }, ); expect(sandboxResult.error).toBeUndefined(); expect(sandboxResult.result).toEqual( expect.objectContaining({ items: [expect.objectContaining({ path: "crm.createContact" })], total: 1, hasMore: false, nextOffset: null, }), ); }), ); it.effect("supports executor-scoped source listing and tool search", () => Effect.gen(function* () { const executor = yield* makeSearchExecutor(); const listed = yield* createExecutionEngine({ executor, codeExecutor }).execute( "return await tools.executor.sources.list();", { onElicitation: acceptAll }, ); expect(listed.error).toBeUndefined(); expect(listed.result).toEqual( expect.objectContaining({ items: expect.arrayContaining([ expect.objectContaining({ id: "github", toolCount: 3 }), expect.objectContaining({ id: "crm", toolCount: 2 }), ]), total: 2, hasMore: false, nextOffset: null, }), ); const searched = yield* createExecutionEngine({ executor, codeExecutor }).execute( 'return await tools.search({ query: "list contacts", namespace: "crm", limit: 5 });', { onElicitation: acceptAll }, ); expect(searched.error).toBeUndefined(); expect(searched.result).toEqual( expect.objectContaining({ items: [expect.objectContaining({ path: "crm.listContacts" })], }), ); }), ); it.effect("paginates source listings via limit + offset", () => Effect.gen(function* () { const executor = yield* makeSearchExecutor(); const engine = createExecutionEngine({ executor, codeExecutor }); // total = 2 (github, crm), sorted by name ("CRM" < "GitHub") const firstPage = yield* engine.execute( "return await tools.executor.sources.list({ limit: 1 });", { onElicitation: acceptAll }, ); expect(firstPage.error).toBeUndefined(); expect(firstPage.result).toEqual( expect.objectContaining({ items: [expect.objectContaining({ id: "crm" })], total: 2, hasMore: true, nextOffset: 1, }), ); const secondPage = yield* engine.execute( "return await tools.executor.sources.list({ limit: 1, offset: 1 });", { onElicitation: acceptAll }, ); expect(secondPage.error).toBeUndefined(); expect(secondPage.result).toEqual( expect.objectContaining({ items: [expect.objectContaining({ id: "github" })], total: 2, hasMore: false, nextOffset: null, }), ); }), ); it.effect("rejects negative offsets via the engine validator", () => Effect.gen(function* () { const executor = yield* makeSearchExecutor(); const engine = createExecutionEngine({ executor, codeExecutor }); const badSearch = yield* engine.execute( [ "try {", ' await tools.search({ query: "list", offset: -1 });', ' return "unexpected";', "} catch (error) {", " return error instanceof Error ? error.message : String(error);", "}", ].join("\n"), { onElicitation: acceptAll }, ); expect(badSearch.error).toBeUndefined(); expect(String(badSearch.result)).toContain( "tools.search offset must be a non-negative number when provided", ); const badList = yield* engine.execute( [ "try {", " await tools.executor.sources.list({ offset: -5 });", ' return "unexpected";', "} catch (error) {", " return error instanceof Error ? error.message : String(error);", "}", ].join("\n"), { onElicitation: acceptAll }, ); expect(badList.error).toBeUndefined(); expect(String(badList.result)).toContain( "tools.executor.sources.list offset must be a non-negative number when provided", ); }), ); it.effect("describes tools with TypeScript previews", () => Effect.gen(function* () { const executor = yield* makeSearchExecutor(); const described = yield* describeTool(executor, "github.listRepositoryIssues"); expect(described.path).toBe("github.listRepositoryIssues"); expect(described.name).toBe("listRepositoryIssues"); expect(described.description).toBe("List issues for a repository"); expect(described.inputTypeScript).toBe("{ owner: string; repo: string }"); expect(described.outputTypeScript).toBeUndefined(); expect(described.typeScriptDefinitions).toBeUndefined(); }), ); it.effect("rejects malformed discover calls inside the sandbox", () => Effect.gen(function* () { const executor = yield* makeSearchExecutor(); const engine = createExecutionEngine({ executor, codeExecutor }); const invalid = yield* engine.execute( [ "try {", ' await tools.search("github issues");', ' return "unexpected";', "} catch (error) {", " return error instanceof Error ? error.message : String(error);", "}", ].join("\n"), { onElicitation: acceptAll }, ); expect(invalid.error).toBeUndefined(); expect(String(invalid.result)).toContain( "tools.search expects an object: { query?: string; namespace?: string; limit?: number; offset?: number }", ); const emptyQuery = yield* engine.execute( 'return await tools.search({ query: "", limit: 5 });', { onElicitation: acceptAll }, ); expect(emptyQuery.error).toBeUndefined(); expect(emptyQuery.result).toEqual({ items: [], total: 0, hasMore: false, nextOffset: null, }); const invalidDescribe = yield* engine.execute( [ "try {", ' await tools.describe.tool({ path: "github.listRepositoryIssues", includeSchemas: true });', ' return "unexpected";', "} catch (error) {", " return error instanceof Error ? error.message : String(error);", "}", ].join("\n"), { onElicitation: acceptAll }, ); expect(invalidDescribe.error).toBeUndefined(); expect(String(invalidDescribe.result)).toContain( "tools.describe.tool no longer accepts includeSchemas", ); const invalidSearch = yield* engine.execute( 'try { return await tools.search("crm"); } catch (error) { return error instanceof Error ? error.message : String(error); }', { onElicitation: acceptAll }, ); expect(invalidSearch.error).toBeUndefined(); expect(String(invalidSearch.result)).toContain("tools.search expects an object"); }), ); it.effect("converts message-bearing tool error results into execution errors", () => Effect.gen(function* () { const executor = yield* createExecutor(makeTestConfig({ plugins: [errorPlugin()] as const })); const invoker = makeExecutorToolInvoker(executor, { invokeOptions: { onElicitation: acceptAll }, }); const error = yield* Effect.flip(invoker.invoke({ path: "records.queryRows", args: {} })); expect(error).toEqual( expect.objectContaining({ message: 'Field with name "DisplayName" does not exist', }), ); }), );}); // ---------------------------------------------------------------------------// pause/resume — multiple elicitations in a single execution// --------------------------------------------------------------------------- const apiPlugin = definePlugin(() => ({ id: "api-test" as const, storage: () => ({}), staticSources: () => [ { id: "api", kind: "in-memory", name: "API", tools: [ { name: "multiApproval", description: "A tool that elicits twice", inputSchema: EmptyInputSchema, handler: ({ elicit }) => Effect.gen(function* () { const r1 = yield* elicit( new FormElicitation({ message: "First approval", requestedSchema: {}, }), ); const r2 = yield* elicit( new FormElicitation({ message: "Second approval", requestedSchema: {}, }), ); return { first: r1, second: r2 }; }), }, { name: "singleApproval", description: "A tool that elicits exactly once and then returns a value. Mirrors the shape of a typical `gmail.users.labels.create` style operation: one approval, one side effect, one success response.", inputSchema: EmptyInputSchema, handler: ({ elicit }) => Effect.gen(function* () { const r = yield* elicit( new FormElicitation({ message: "Only approval", requestedSchema: {}, }), ); return { ok: true, response: r }; }), }, ], }, ],})); describe("pause/resume with multiple elicitations", () => { const makeElicitingExecutor = () => createExecutor(makeTestConfig({ plugins: [apiPlugin()] as const })); it.effect( "resume does not hang when execution hits a second elicitation", () =>Lines 1–500 of 577. Load more
import { describe, expect, it } from "@effect/vitest"; import { Effect, Fiber } from "effect"; import { ElicitationResponse, FormElicitation, createExecutor, definePlugin, makeTestConfig, } from "@executor-js/sdk"; import { makeQuickJsExecutor } from "@executor-js/runtime-quickjs"; import { createExecutionEngine } from "./engine"; import { describeTool, makeExecutorToolInvoker, searchTools } from "./tool-invoker"; const codeExecutor = makeQuickJsExecutor(); const RepoInputSchema = { type: "object", properties: { owner: { type: "string" }, repo: { type: "string" }, }, required: ["owner", "repo"], additionalProperties: false, } as const; const ContactInputSchema = { type: "object", properties: { email: { type: "string" }, }, required: ["email"], additionalProperties: false, } as const; const EmptyInputSchema = { type: "object", properties: {}, additionalProperties: false, } as const; const acceptAll = () => Effect.succeed(new ElicitationResponse({ action: "accept" })); // --------------------------------------------------------------------------- // Test plugins — each one declares a namespace as a static source with N // tools. Handlers return static data; the suite only cares about discovery // + elicitation flow, not real invocation semantics. // --------------------------------------------------------------------------- const githubPlugin = definePlugin(() => ({ id: "github-test" as const, storage: () => ({}), staticSources: () => [ { id: "github", kind: "in-memory", name: "GitHub", tools: [ { name: "listRepositoryIssues", description: "List issues for a repository", inputSchema: RepoInputSchema, handler: () => Effect.succeed([]), }, { name: "getRepositoryDetails", description: "Get repository details including the default branch", inputSchema: RepoInputSchema, handler: () => Effect.succeed({ defaultBranch: "main" }), }, { name: "searchDocs", description: "Search GitHub API documentation", inputSchema: EmptyInputSchema, handler: () => Effect.succeed([]), }, ], }, ], })); const crmPlugin = definePlugin(() => ({ id: "crm-test" as const, storage: () => ({}), staticSources: () => [ { id: "crm", kind: "in-memory", name: "CRM", tools: [ { name: "createContact", description: "Create a CRM contact record", inputSchema: ContactInputSchema, handler: () => Effect.succeed({ id: "contact_1" }), }, { name: "listContacts", description: "List CRM contacts", inputSchema: EmptyInputSchema, handler: () => Effect.succeed([]), }, ], }, ], })); const errorPlugin = definePlugin(() => ({ id: "error-test" as const, storage: () => ({}), staticSources: () => [ { id: "records", kind: "in-memory", name: "Records", tools: [ { name: "queryRows", description: "Query rows", inputSchema: EmptyInputSchema, handler: () => Effect.succeed({ data: null, error: { message: 'Field with name "DisplayName" does not exist', code: "invalid_query", }, }), }, ], }, ], })); const makeSearchExecutor = () => createExecutor(makeTestConfig({ plugins: [githubPlugin(), crmPlugin()] as const })); describe("tool discovery", () => { it.effect("ranks matches using ids, namespaces, camelCase names, and descriptions", () => Effect.gen(function* () { const executor = yield* makeSearchExecutor(); const githubMatches = yield* searchTools(executor, "github issues", 5); expect(githubMatches.items.map((match) => match.path)).toEqual([ "github.listRepositoryIssues", ]); expect(githubMatches.items[0]?.score ?? 0).toBeGreaterThan(0); expect(githubMatches.hasMore).toBe(false); expect(githubMatches.nextOffset).toBeNull(); const repoMatches = yield* searchTools(executor, "repo details", 5); expect(repoMatches.items[0]?.path).toBe("github.getRepositoryDetails"); const crmMatches = yield* searchTools(executor, "crm create contact", 5); expect(crmMatches.items[0]?.path).toBe("crm.createContact"); expect(crmMatches.items[0]?.score ?? 0).toBeGreaterThan(crmMatches.items[1]?.score ?? 0); }), ); it.effect("returns no matches for empty queries instead of listing arbitrary tools", () => Effect.gen(function* () { const executor = yield* makeSearchExecutor(); const matches = yield* searchTools(executor, "", 5); expect(matches.items).toEqual([]); expect(matches.total).toBe(0); expect(matches.hasMore).toBe(false); expect(matches.nextOffset).toBeNull(); }), ); it.effect("paginates ranked matches via limit + offset with hasMore + nextOffset", () => Effect.gen(function* () { const executor = yield* makeSearchExecutor(); // "list" matches `listRepositoryIssues`, `searchDocs` (description has // "documentation" which tokenises adjacent), `listContacts`, etc. // The exact match set isn't important — the pagination invariants are. const all = yield* searchTools(executor, "list", 100); expect(all.items.length).toBeGreaterThan(1); expect(all.total).toBe(all.items.length); expect(all.hasMore).toBe(false); expect(all.nextOffset).toBeNull(); // First page (limit 1) — matches truncate, hasMore + nextOffset surface. const firstPage = yield* searchTools(executor, "list", 1); expect(firstPage.items).toEqual([all.items[0]]); expect(firstPage.total).toBe(all.total); expect(firstPage.hasMore).toBe(true); expect(firstPage.nextOffset).toBe(1); // Second page using nextOffset — order matches the un-paginated rank. const secondPage = yield* searchTools(executor, "list", 1, { offset: firstPage.nextOffset!, }); expect(secondPage.items).toEqual([all.items[1]]); expect(secondPage.total).toBe(all.total); // Whether hasMore is true depends on total; at minimum it's consistent. expect(secondPage.hasMore).toBe(all.total > 2); expect(secondPage.nextOffset).toBe(secondPage.hasMore ? 2 : null); // Offset past the end — empty page, no more. const past = yield* searchTools(executor, "list", 5, { offset: all.total + 10 }); expect(past.items).toEqual([]); expect(past.total).toBe(all.total); expect(past.hasMore).toBe(false); expect(past.nextOffset).toBeNull(); }), ); it.effect("can narrow discovery to a namespace", () => Effect.gen(function* () { const executor = yield* makeSearchExecutor(); const githubOnly = yield* searchTools(executor, "list", 5, { namespace: "github", }); expect(githubOnly.items.map((match) => match.path)).toEqual(["github.listRepositoryIssues"]); const crmOnly = yield* searchTools(executor, "list", 5, { namespace: "crm", }); expect(crmOnly.items.map((match) => match.path)).toEqual(["crm.listContacts"]); const sandboxResult = yield* createExecutionEngine({ executor, codeExecutor }).execute( 'return await tools.search({ namespace: "crm", query: "create contact", limit: 5 });', { onElicitation: acceptAll }, ); expect(sandboxResult.error).toBeUndefined(); expect(sandboxResult.result).toEqual( expect.objectContaining({ items: [expect.objectContaining({ path: "crm.createContact" })], total: 1, hasMore: false, nextOffset: null, }), ); }), ); it.effect("supports executor-scoped source listing and tool search", () => Effect.gen(function* () { const executor = yield* makeSearchExecutor(); const listed = yield* createExecutionEngine({ executor, codeExecutor }).execute( "return await tools.executor.sources.list();", { onElicitation: acceptAll }, ); expect(listed.error).toBeUndefined(); expect(listed.result).toEqual( expect.objectContaining({ items: expect.arrayContaining([ expect.objectContaining({ id: "github", toolCount: 3 }), expect.objectContaining({ id: "crm", toolCount: 2 }), ]), total: 2, hasMore: false, nextOffset: null, }), ); const searched = yield* createExecutionEngine({ executor, codeExecutor }).execute( 'return await tools.search({ query: "list contacts", namespace: "crm", limit: 5 });', { onElicitation: acceptAll }, ); expect(searched.error).toBeUndefined(); expect(searched.result).toEqual( expect.objectContaining({ items: [expect.objectContaining({ path: "crm.listContacts" })], }), ); }), ); it.effect("paginates source listings via limit + offset", () => Effect.gen(function* () { const executor = yield* makeSearchExecutor(); const engine = createExecutionEngine({ executor, codeExecutor }); // total = 2 (github, crm), sorted by name ("CRM" < "GitHub") const firstPage = yield* engine.execute( "return await tools.executor.sources.list({ limit: 1 });", { onElicitation: acceptAll }, ); expect(firstPage.error).toBeUndefined(); expect(firstPage.result).toEqual( expect.objectContaining({ items: [expect.objectContaining({ id: "crm" })], total: 2, hasMore: true, nextOffset: 1, }), ); const secondPage = yield* engine.execute( "return await tools.executor.sources.list({ limit: 1, offset: 1 });", { onElicitation: acceptAll }, ); expect(secondPage.error).toBeUndefined(); expect(secondPage.result).toEqual( expect.objectContaining({ items: [expect.objectContaining({ id: "github" })], total: 2, hasMore: false, nextOffset: null, }), ); }), ); it.effect("rejects negative offsets via the engine validator", () => Effect.gen(function* () { const executor = yield* makeSearchExecutor(); const engine = createExecutionEngine({ executor, codeExecutor }); const badSearch = yield* engine.execute( [ "try {", ' await tools.search({ query: "list", offset: -1 });', ' return "unexpected";', "} catch (error) {", " return error instanceof Error ? error.message : String(error);", "}", ].join("\n"), { onElicitation: acceptAll }, ); expect(badSearch.error).toBeUndefined(); expect(String(badSearch.result)).toContain( "tools.search offset must be a non-negative number when provided", ); const badList = yield* engine.execute( [ "try {", " await tools.executor.sources.list({ offset: -5 });", ' return "unexpected";', "} catch (error) {", " return error instanceof Error ? error.message : String(error);", "}", ].join("\n"), { onElicitation: acceptAll }, ); expect(badList.error).toBeUndefined(); expect(String(badList.result)).toContain( "tools.executor.sources.list offset must be a non-negative number when provided", ); }), ); it.effect("describes tools with TypeScript previews", () => Effect.gen(function* () { const executor = yield* makeSearchExecutor(); const described = yield* describeTool(executor, "github.listRepositoryIssues"); expect(described.path).toBe("github.listRepositoryIssues"); expect(described.name).toBe("listRepositoryIssues"); expect(described.description).toBe("List issues for a repository"); expect(described.inputTypeScript).toBe("{ owner: string; repo: string }"); expect(described.outputTypeScript).toBeUndefined(); expect(described.typeScriptDefinitions).toBeUndefined(); }), ); it.effect("rejects malformed discover calls inside the sandbox", () => Effect.gen(function* () { const executor = yield* makeSearchExecutor(); const engine = createExecutionEngine({ executor, codeExecutor }); const invalid = yield* engine.execute( [ "try {", ' await tools.search("github issues");', ' return "unexpected";', "} catch (error) {", " return error instanceof Error ? error.message : String(error);", "}", ].join("\n"), { onElicitation: acceptAll }, ); expect(invalid.error).toBeUndefined(); expect(String(invalid.result)).toContain( "tools.search expects an object: { query?: string; namespace?: string; limit?: number; offset?: number }", ); const emptyQuery = yield* engine.execute( 'return await tools.search({ query: "", limit: 5 });', { onElicitation: acceptAll }, ); expect(emptyQuery.error).toBeUndefined(); expect(emptyQuery.result).toEqual({ items: [], total: 0, hasMore: false, nextOffset: null, }); const invalidDescribe = yield* engine.execute( [ "try {", ' await tools.describe.tool({ path: "github.listRepositoryIssues", includeSchemas: true });', ' return "unexpected";', "} catch (error) {", " return error instanceof Error ? error.message : String(error);", "}", ].join("\n"), { onElicitation: acceptAll }, ); expect(invalidDescribe.error).toBeUndefined(); expect(String(invalidDescribe.result)).toContain( "tools.describe.tool no longer accepts includeSchemas", ); const invalidSearch = yield* engine.execute( 'try { return await tools.search("crm"); } catch (error) { return error instanceof Error ? error.message : String(error); }', { onElicitation: acceptAll }, ); expect(invalidSearch.error).toBeUndefined(); expect(String(invalidSearch.result)).toContain("tools.search expects an object"); }), ); it.effect("converts message-bearing tool error results into execution errors", () => Effect.gen(function* () { const executor = yield* createExecutor(makeTestConfig({ plugins: [errorPlugin()] as const })); const invoker = makeExecutorToolInvoker(executor, { invokeOptions: { onElicitation: acceptAll }, }); const error = yield* Effect.flip(invoker.invoke({ path: "records.queryRows", args: {} })); expect(error).toEqual( expect.objectContaining({ message: 'Field with name "DisplayName" does not exist', }), ); }), ); }); // --------------------------------------------------------------------------- // pause/resume — multiple elicitations in a single execution // --------------------------------------------------------------------------- const apiPlugin = definePlugin(() => ({ id: "api-test" as const, storage: () => ({}), staticSources: () => [ { id: "api", kind: "in-memory", name: "API", tools: [ { name: "multiApproval", description: "A tool that elicits twice", inputSchema: EmptyInputSchema, handler: ({ elicit }) => Effect.gen(function* () { const r1 = yield* elicit( new FormElicitation({ message: "First approval", requestedSchema: {}, }), ); const r2 = yield* elicit( new FormElicitation({ message: "Second approval", requestedSchema: {}, }), ); return { first: r1, second: r2 }; }), }, { name: "singleApproval", description: "A tool that elicits exactly once and then returns a value. Mirrors the shape of a typical `gmail.users.labels.create` style operation: one approval, one side effect, one success response.", inputSchema: EmptyInputSchema, handler: ({ elicit }) => Effect.gen(function* () { const r = yield* elicit( new FormElicitation({ message: "Only approval", requestedSchema: {}, }), ); return { ok: true, response: r }; }), }, ], }, ], })); describe("pause/resume with multiple elicitations", () => { const makeElicitingExecutor = () => createExecutor(makeTestConfig({ plugins: [apiPlugin()] as const })); it.effect( "resume does not hang when execution hits a second elicitation", () =>