Tools and Approvals

Purpose and Scope

Tools are the main way an eve agent performs typed actions outside ordinary text generation. A tool can call an internal API, run a query, send a refund, update a ticket, or perform any other operation that should stay in application code you control. eve’s first-party tool model pairs that executable code with schemas and model-facing descriptions, so the model can request a capability without owning its implementation. Approvals add the human-in-the-loop boundary for actions that should not run automatically, especially when the action has financial, legal, tenant-specific, irreversible, or external side effects.

Sources: docs/tools/human-in-the-loop.md

Human-in-the-loop, abbreviated HITL, means the run durably pauses and waits for a person before continuing. The repository documentation describes two HITL triggers: approvals, where a tool call waits for a human decision before or instead of running, and questions, where the agent asks a clarifying question mid-turn. This page focuses on the approval side because it is the safety mechanism attached directly to tools. The important operational property is that the session can remain parked for seconds or days and then resume where it stopped.

Sources: docs/tools/human-in-the-loop.md

Relevant Source Files

  • docs/tools/human-in-the-loop.md — Defines the human-in-the-loop concept, explains approval-triggered pauses, shows the refund_charge tool example, documents never(), once(), and always(), and describes custom approval policy return values.

Defining Agent Tools

In eve, a tool is normally authored as a file under agent/tools/, and the filename becomes the model-facing tool name. For example, agent/tools/get_weather.ts is exposed as get_weather. The tool definition uses defineTool from eve/tools, includes a model-readable description, declares an inputSchema, and provides an execute(input, ctx) implementation. The input schema may be a Zod schema, another Standard Schema, or a plain JSON Schema object; for a no-argument tool, use an empty object schema rather than omitting the schema.

Tool code runs in the application runtime, not in the sandbox. That distinction matters for both power and risk: a tool can import shared application modules, read process.env, call privileged services, and participate in durable pause and resume behavior. The model sees descriptors during discovery, but authored tools are not executed just because eve discovers them. Execution happens only when the model calls the tool during a run, so the schema and description are the contract that governs how the model reaches application code.

A minimal tool looks like this:

import { defineTool } from "eve/tools";
import { z } from "zod";
 
export default defineTool({
  description: "Return mock weather data for a city.",
  inputSchema: z.object({ city: z.string().min(1) }),
  async execute({ city }) {
    return { city, condition: "Sunny", temperatureF: 72 };
  },
});

The ctx parameter gives the executor runtime accessors that should be treated as part of the tool contract. It includes session metadata and authentication, an abort signal for cancellation-aware work, a sandbox handle through ctx.getSandbox(), and skill access through ctx.getSkill(id). Use these accessors instead of trying to reconstruct run state from global variables. In particular, session auth is what lets a tool enforce caller and tenant boundaries in the executor, even if an approval policy already evaluated the request.

Approval Flow

Approval is a property on a tool definition. When the model decides to call that tool, eve evaluates the approval configuration before the executor runs. If the result requires a person, the run parks at session.waiting; channels can render the request, collect the answer, and resume the same run after approval or denial. This is not just a user-interface feature. It is part of the durable execution model, so the waiting state survives long pauses and the run continues from the approval point rather than starting over.

Sources: docs/tools/human-in-the-loop.md

The repository documentation shows the canonical shape with a refund tool. The sensitive operation is defined as normal code, but approval: always() forces a human decision before every call. Omitting approval behaves like never(), which means the tool can execute without a prompt. That default is appropriate for low-risk read-only tools, but it is intentionally not a safe default for refunds, charges, emails, tenant mutations, healthcare, employment, housing, legal, safety-impacting, or other user-impacting actions.

Sources: docs/tools/human-in-the-loop.md

import { defineTool } from "eve/tools";
import { always } from "eve/tools/approval";
import { z } from "zod";
 
export default defineTool({
  description: "Refund a charge.",
  inputSchema: z.object({
    tenantId: z.string(),
    chargeId: z.string(),
    amount: z.number(),
  }),
  approval: always(),
  async execute(input) {
    return refund(input);
  },
});

Approval Helpers and Policy Reference

HelperBehavior
never()Never require approval. This is also the behavior when approval is omitted.
once()Require approval only the first time the tool runs in a session, then auto-allow later calls.
always()Require approval before every call.

For simple cases, the helpers from eve/tools/approval are enough. For input-dependent or tenant-dependent decisions, provide a custom policy function instead of a helper. That policy receives session context plus fields such as toolName, toolInput, and approvedTools, then returns an AI SDK 7 approval status either synchronously or as a promise. The documentation calls out ctx.session.auth.current for the caller of the current turn and ctx.session.auth.initiator for the caller that created the session, which are the usual anchors for tenant and authorization checks.

Sources: docs/tools/human-in-the-loop.md

Approval policy return values communicate whether eve should pause, continue, approve automatically, or deny automatically. Return "user-approval" to pause for a person, "not-applicable" to continue without a prompt, "approved" to allow automatically, or "denied" to block automatically. Policies can also return objects like { type: "denied", reason: "Caller cannot access this tenant." } so the model receives an explanation. For compatibility with older predicate-style policies, boolean returns are supported: true maps to a human approval request and false maps to no prompt.

Sources: docs/tools/human-in-the-loop.md

approval: ({ session, toolInput }) => {
  const callerTenant = session.auth.current?.attributes.tenantId;
  if (callerTenant === undefined || callerTenant !== toolInput?.tenantId) {
    return { type: "denied", reason: "Caller cannot access this tenant." };
  }
  return (toolInput?.amount ?? 0) > 1000 ? "user-approval" : "not-applicable";
},

The public approval-related types are Approval, ApprovalContext, and ApprovalStatus, exported from both eve/tools and eve/tools/approval. Use these names when building shared policy adapters, especially in multi-tenant applications where authored tools, OpenAPI connection operations, and MCP tools should all consult the same tenant policy service. The key design rule is that approval is a gate, not a replacement for authorization. Your executor should still derive the tenant from trusted session state and enforce access before performing the side effect.

Sources: docs/tools/human-in-the-loop.md

Durability, Replays, and Human Safeguards

Approvals also help make non-idempotent work safer under durable execution. eve records completed steps, so completed work is not re-run during replay; however, work interrupted mid-execution can run again. Placing a charge, refund, email, or other external mutation behind always() means a restarted attempt cannot silently repeat the side effect without another human decision. For high-risk actions, combine approval with idempotency keys, server-side authorization, audit logging, and executor-level validation so the approval request and the final operation agree on the same trusted facts.

Sources: docs/tools/human-in-the-loop.md

The most robust approval designs keep model-controlled input separate from trusted authorization facts. Let the model propose a tool input, but validate tenant, user, resource, and amount against ctx.session.auth and your application database. If the model includes a tenantId, treat it as an input to check, not proof. If a policy denies the call, return a reason that explains the boundary without leaking sensitive details. If a policy asks for approval, render enough context for the reviewer to understand the action before they accept or reject it.

Implementation Checklist

  1. Put each authored action in agent/tools/<name>.ts so the path-derived name is clear to the model.
  2. Define a precise description and a required inputSchema; add an outputSchema when callers need structured results.
  3. Use execute(input, ctx) for the actual application-side work, and enforce auth in the executor even when an approval policy already ran.
  4. Add approval: always() for sensitive side effects, once() for session-scoped trust, or a custom policy for tenant, amount, role, or environment rules.
  5. Return explicit approval statuses and reasons when denial or automatic approval should be visible to the model.
  6. Treat approvals as part of durability and auditability: log decisions, use idempotency, and avoid relying on model-provided tenant claims.

Next Steps

After defining a safe tool surface, read the adjacent concepts that determine where the tool can be invoked. Use the client and sessions documentation to understand how waiting states reach users, the channels documentation to learn how approval prompts are rendered outside the native eve surface, and the multi-tenant patterns guide when the same policy must apply across authored tools, MCP connections, and OpenAPI operations. For local development, test both the approved and denied paths before shipping a tool that performs real external side effects.