Skip to content

Public API for client-side tools that park for user input, like ask_question #593

Description

@dcosson

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

  1. A public defineClientTool (or equivalent): model-visible tool with input/output schemas and no execute.
  2. 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.
  3. 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().
  4. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions