What problem are you trying to solve?
We embed Eve in our product. Some tools should be answered by the user in our UI, not executed by Eve — exactly how ask_question already works: the model calls the tool, Eve emits input.requested, the run parks durably, and it resumes when the user responds.
We can't build a tool like that with the public API today:
- Every public tool must have an
execute function. ask_question itself has no execute — but that shape is internal-only.
- The parking path is hardcoded to
ask_question by name (harness/input-extraction.ts). A second tool with no execute is silently skipped — it never parks, never errors.
- There's no public way to attach a tool the way Eve attaches
connection_search — always present during graph resolution, not removable by disableTool() or template edits. We need that for a tool that must accompany a feature whenever it's enabled.
The workarounds aren't great: forking Eve is what we're trying to avoid; a fake execute that blocks until the user answers dies on serverless timeouts; ask_question can't render a purpose-built UI keyed to the tool and its input; tool approval parks durably but renders as approve/deny, not tool-specific input.
Proposed solution
- A public
defineClientTool (or equivalent): model-visible tool with input/output schemas and no execute.
- Client tools declare how their call becomes an input request — prompt, display kind, options — so any of them park through the existing
input.requested flow, not just ask_question.
- A hook to register dynamic tool resolvers during runtime graph resolution, appended at the same point as Eve's own
connection_search resolver, with the same protection from disableTool().
- No behavior change for existing tools: authored dynamic tools still override static tools by name; step-scoped resolvers still work.
Example:
import { defineClientTool } from "eve/tools";
import { z } from "zod";
export default defineClientTool({
description: "Ask the user to choose a plan.",
inputSchema: z.object({ accountId: z.string() }),
outputSchema: z.object({ optionId: z.string().optional(), text: z.string().optional() }),
inputRequest: {
prompt: "Choose a plan",
display: "select",
options: [
{ id: "basic", label: "Basic" },
{ id: "pro", label: "Pro" },
],
},
});
Model calls choose_plan → Eve emits input.requested with the declared prompt/options and the original call in request.action → run parks → client submits inputResponses → run resumes.
Alternatives considered
No response
What problem are you trying to solve?
We embed Eve in our product. Some tools should be answered by the user in our UI, not executed by Eve — exactly how
ask_questionalready works: the model calls the tool, Eve emitsinput.requested, the run parks durably, and it resumes when the user responds.We can't build a tool like that with the public API today:
executefunction.ask_questionitself has noexecute— but that shape is internal-only.ask_questionby name (harness/input-extraction.ts). A second tool with noexecuteis silently skipped — it never parks, never errors.connection_search— always present during graph resolution, not removable bydisableTool()or template edits. We need that for a tool that must accompany a feature whenever it's enabled.The workarounds aren't great: forking Eve is what we're trying to avoid; a fake
executethat blocks until the user answers dies on serverless timeouts;ask_questioncan't render a purpose-built UI keyed to the tool and its input; tool approval parks durably but renders as approve/deny, not tool-specific input.Proposed solution
defineClientTool(or equivalent): model-visible tool with input/output schemas and noexecute.input.requestedflow, not justask_question.connection_searchresolver, with the same protection fromdisableTool().Example:
Model calls
choose_plan→ Eve emitsinput.requestedwith the declared prompt/options and the original call inrequest.action→ run parks → client submitsinputResponses→ run resumes.Alternatives considered
No response