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
7 changes: 7 additions & 0 deletions .changeset/quiet-tools-describe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@executor-js/execution": patch
"@executor-js/plugin-openapi": patch
"executor": patch
---

Expose TypeScript contracts for built-in Executor discovery tools and describe OpenAPI tool results with their transport envelope so read-only sandbox calls match their input and output shapes.
44 changes: 44 additions & 0 deletions packages/core/execution/src/tool-invoker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,50 @@ describe("tool discovery", () => {
}),
);

it.effect("describes built-in discovery tool shapes that accept their runtime output", () =>
Effect.gen(function* () {
const executor = yield* makeSearchExecutor();
const engine = createExecutionEngine({ executor, codeExecutor });

const execution = yield* engine.execute(
[
"const searchDetails = await tools.describe.tool({ path: 'search' });",
"const sourceDetails = await tools.describe.tool({ path: 'executor.sources.list' });",
"const describeDetails = await tools.describe.tool({ path: 'describe.tool' });",
"return {",
" searchDetails,",
" searchResult: await tools.search({ query: 'repo details', limit: 2 }),",
" sourceDetails,",
" sourceResult: await tools.executor.sources.list({ limit: 2 }),",
" describeDetails,",
" describeResult: await tools.describe.tool({ path: 'github.getRepositoryDetails' }),",
"};",
].join("\n"),
{ onElicitation: acceptAll },
);

expect(execution.error).toBeUndefined();
const observed = execution.result as {
readonly searchDetails: DescribedToolContract;
readonly searchResult: unknown;
readonly sourceDetails: DescribedToolContract;
readonly sourceResult: unknown;
readonly describeDetails: DescribedToolContract;
readonly describeResult: unknown;
};

expect(
typeCheckDescribedInvocation(observed.searchDetails, observed.searchResult, ""),
).toEqual([]);
expect(
typeCheckDescribedInvocation(observed.sourceDetails, observed.sourceResult, ""),
).toEqual([]);
expect(
typeCheckDescribedInvocation(observed.describeDetails, observed.describeResult, ""),
).toEqual([]);
}),
);

it.effect("rejects malformed discover calls inside the sandbox", () =>
Effect.gen(function* () {
const executor = yield* makeSearchExecutor();
Expand Down
62 changes: 62 additions & 0 deletions packages/core/execution/src/tool-invoker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,65 @@ const withToolResultDefinitions = (
ToolError: TOOL_ERROR_TYPESCRIPT,
});

type DescribedTool = {
readonly path: string;
readonly name: string;
readonly description?: string;
readonly inputTypeScript?: string;
readonly outputTypeScript?: string;
readonly typeScriptDefinitions?: Record<string, string>;
};

const BUILTIN_TOOL_DESCRIPTIONS: ReadonlyMap<string, DescribedTool> = new Map<
string,
DescribedTool
>([
[
"search",
{
path: "search",
name: "search",
description: "Search available Executor tools.",
inputTypeScript: "{ query: string; namespace?: string; limit?: number; offset?: number; }",
outputTypeScript:
"{ items: ToolDiscoveryResult[]; total: number; hasMore: boolean; nextOffset: number | null; }",
typeScriptDefinitions: {
ToolDiscoveryResult:
"{ path: string; name: string; description?: string; sourceId: string; score: number; }",
},
},
],
[
"executor.sources.list",
{
path: "executor.sources.list",
name: "executor.sources.list",
description: "List configured and built-in Executor sources.",
inputTypeScript: "{ query?: string; limit?: number; offset?: number; }",
outputTypeScript:
"{ items: ExecutorSourceListItem[]; total: number; hasMore: boolean; nextOffset: number | null; }",
typeScriptDefinitions: {
ExecutorSourceListItem:
"{ id: string; name: string; kind: string; runtime?: boolean; canRemove?: boolean; canRefresh?: boolean; toolCount: number; }",
},
},
],
[
"describe.tool",
{
path: "describe.tool",
name: "describe.tool",
description: "Describe a tool's compact TypeScript input and output shapes.",
inputTypeScript: "{ path: string; }",
outputTypeScript: "DescribedTool",
typeScriptDefinitions: {
DescribedTool:
"{ path: string; name: string; description?: string; inputTypeScript?: string; outputTypeScript?: string; typeScriptDefinitions?: { [k: string]: string; }; }",
},
},
],
]);

const newCorrelationId = (): string => {
// 8-hex-char correlation id; enough entropy to disambiguate within a
// single deployment without leaking host process info.
Expand Down Expand Up @@ -534,6 +593,9 @@ export const describeTool = Effect.fn("executor.tools.describe")(function* (
) {
yield* Effect.annotateCurrentSpan({ "mcp.tool.name": path });

const builtin = BUILTIN_TOOL_DESCRIPTIONS.get(path);
if (builtin) return builtin;

// Single tools.schema() call — it already fetches the tool row
// internally. No need to also call tools.list() just for name/description.
const schema: ToolSchema | null = yield* executor.tools.schema(path);
Expand Down
Loading
Loading