Skip to content

Proposal: delegated authored tools, durable park via the runtime-action protocol #2758

Description

@jcourson-bg

What problem are you trying to solve?

An authored tool that starts a long-running external job (a
workflow run, a batch job) has no way to wait for the result
without holding execute open on an in-process Promise. That is
not durable: a host restart loses the waiter, and recovery is
author-built polling. We ship exactly that workaround today (an
in-memory Promise keyed by callId plus a 30s status-poll
fallback).

Eve already solves this internally for subagents: runtime
actions. The turn persists eve.runtime.pendingActionBatch, the
driver returns action: "park" with pendingRuntimeActionKeys,
the parent turn workflow waits on the turn's durable inbox hook,
and the child settles via resumeHook with a
runtime-action-result. None of this is exposed to authored
tools; ToolContext has only session metadata, abortSignal,
callId, and token helpers.

Proposed solution

Add execution: "delegated" to defineTool:

import { defineTool } from "eve/tools";
import { z } from "zod";

const JobResult = z.object({
  status: z.enum(["succeeded", "failed"]),
  detail: z.string(),
});

export default defineTool({
  execution: "delegated",
  description: "Run a durable external job.",
  inputSchema: z.object({ spec: z.string() }),
  outputSchema: JobResult,
  async execute({ spec }, ctx, action) {
    await enqueueJob({
      spec,
      completion: action.completion, // opaque { id, url }
      idempotencyKey: action.id,     // stable across replays
    });
    return action.pending();         // typed sentinel; never reaches the model
  },
});

The worker completes via an SDK helper (raw HTTP to the
completion URL as an escape hatch):

import { completeAction } from "eve/actions";

await completeAction({
  completion: job.completion,
  output: { status: "succeeded", detail: "Report generated" },
});

Eve validates the output against outputSchema, settles it as an
ordinary action.result, the same turn continues, and the model
sees a normal typed tool result.

Expected mapping to existing machinery:

  1. The call becomes a runtime action — a sibling of
    subagent-call — with a stable action id registered in the
    pending batch BEFORE execute runs, so a fast completion
    cannot race registration.
  2. The driver parks exactly as it does for subagents.
  3. Completion routes through the active turn inbox via
    resumeHook.
  4. Settlement reuses batch resolution, history synthesis, and
    toModelOutput.

Reliability semantics requested:

  • Duplicate-safe completion: idempotent by action id.
  • At-least-once client: completeAction() retries retryable
    failures with the idempotency key; if Eve is down, the worker
    keeps its durable result and retries — no exactly-once
    requirement.
  • Optional per-action deadline that settles as a failed tool
    result instead of parking forever.
  • Cancellation terminally settles the action and revokes the
    completion capability; a late completion must not revive it.

Open questions for maintainers: hook payload retention across
long outages, completion credential expiry, and a planned
status/reconciliation lookup.

Suggested implementation prompt

No response

Alternatives considered

  • approval / ask_question: wait for humans, not machine
    events.
  • sleep: a timer, not a callback.
  • execution: "background" plus task.send: in-process,
    documented as not restart-safe.
  • DevKit createHook: throws inside turnStep's "use step",
    so authors cannot build this themselves.
  • Our current workaround: an in-memory Promise plus polling.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions