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
5 changes: 5 additions & 0 deletions .changeset/fair-input-request-kinds.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"eve": minor
---

Input requests now include a required `kind` discriminator so clients can route tool approvals, questions, and session-limit decisions without inferring behavior from tool names or request IDs. Descendant session-limit Stop responses now let the parent own turn cancellation, avoiding a parent-child wait cycle.
5 changes: 5 additions & 0 deletions docs/tools/human-in-the-loop.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,11 @@ Approvals and questions share one protocol:
3. The turn parks at `session.waiting`, durably, for as long as it takes.
4. The client answers with `inputResponses` (structured, keyed by `requestId`) or a normal follow-up `message`. A follow-up whose text matches an option ID, option label, or numeric option index resolves automatically, including approval options such as `approve` and `deny`.

Each request includes a `kind` discriminator: `tool-approval`, `question`, or
`session-limit`. Clients should use `kind` to choose behavior and presentation;
`toolName` and `requestId` identify the action and request but do not encode its
semantics.

The run picks back up exactly where it parked. Because the pause is durable, nothing is held in memory while it waits — the process can restart and the parked turn survives.

For approval requests, unrelated follow-up text does not deny the tool call. eve keeps the approval pending and holds that text until the approval is answered, then replays it as the next message in the session.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { defineDynamic, defineInstructions } from "#public/instructions/index.js";

export default defineDynamic({
events: {
"session.started": () =>
defineInstructions({
markdown: "Answer with evidence from the current session.",
}),
},
});
11 changes: 11 additions & 0 deletions packages/eve/extension-contracts/compatibility/dynamicSkill/v2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { defineDynamic, defineSkill } from "#public/skills/index.js";

export default defineDynamic({
events: {
"session.started": () =>
defineSkill({
description: "Triage incoming requests.",
markdown: "# Triage\n\nInspect the request before acting.",
}),
},
});
12 changes: 12 additions & 0 deletions packages/eve/extension-contracts/compatibility/dynamicTool/v5.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { defineDynamic, defineTool } from "#public/tools/index.js";

export default defineDynamic({
events: {
"session.started": () =>
defineTool({
description: "Return the current status.",
inputSchema: { type: "object", properties: {} },
execute: () => ({ status: "ready" }),
}),
},
});
12 changes: 12 additions & 0 deletions packages/eve/extension-contracts/compatibility/hook/v3.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { defineHook } from "#public/hooks/index.js";

export default defineHook({
events: {
"input.requested"(event) {
console.info(
"input requested",
event.data.requests.map((request) => request.requestId),
);
},
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"kind": "eve-extension-capability-contract",
"capability": "dynamicInstructions",
"epoch": 3,
"sha256": "69eb1e3ef30127f0fed202466271dd4d31604eabc1e70cc971db1be12957bb4f",
"exports": ["defineDynamic"]
}
7 changes: 7 additions & 0 deletions packages/eve/extension-contracts/reports/dynamicSkill/v3.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"kind": "eve-extension-capability-contract",
"capability": "dynamicSkill",
"epoch": 3,
"sha256": "69eb1e3ef30127f0fed202466271dd4d31604eabc1e70cc971db1be12957bb4f",
"exports": ["defineDynamic"]
}
13 changes: 13 additions & 0 deletions packages/eve/extension-contracts/reports/dynamicTool/v6.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"kind": "eve-extension-capability-contract",
"capability": "dynamicTool",
"epoch": 6,
"sha256": "4f92a15e8f67c172876b3026a788d3244455dbb7c697c0cc1082eb59532851a8",
"exports": [
"DynamicToolEntry",
"DynamicToolEvents",
"DynamicToolResult",
"DynamicToolSet",
"defineDynamic"
]
}
7 changes: 7 additions & 0 deletions packages/eve/extension-contracts/reports/hook/v4.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"kind": "eve-extension-capability-contract",
"capability": "hook",
"epoch": 4,
"sha256": "b0d3042903e52d32bef0791348b776cdde87ffb596676f605e4f65995b3f6be2",
"exports": ["defineHook"]
}
5 changes: 5 additions & 0 deletions packages/eve/src/channel/resolve-text.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const APPROVAL_REQUEST: InputRequest = {
action: { callId: "call-1", input: { command: "rm -rf" }, kind: "tool-call", toolName: "bash" },
allowFreeform: false,
display: "confirmation",
kind: "tool-approval",
options: [
{ id: "approve", label: "Approve", style: "primary" },
{ id: "deny", label: "Deny", style: "danger" },
Expand All @@ -17,6 +18,7 @@ const APPROVAL_REQUEST: InputRequest = {
const SELECT_REQUEST: InputRequest = {
action: { callId: "call-2", input: {}, kind: "tool-call", toolName: "ask_question" },
display: "select",
kind: "question",
options: [
{ id: "postgres", label: "Postgres" },
{ id: "mysql", label: "MySQL" },
Expand All @@ -30,6 +32,7 @@ const FREEFORM_REQUEST: InputRequest = {
action: { callId: "call-3", input: {}, kind: "tool-call", toolName: "ask_question" },
allowFreeform: true,
display: "text",
kind: "question",
prompt: "What is your name?",
requestId: "req-3",
};
Expand All @@ -38,6 +41,7 @@ const SELECT_WITH_FREEFORM_REQUEST: InputRequest = {
action: { callId: "call-4", input: {}, kind: "tool-call", toolName: "ask_question" },
allowFreeform: true,
display: "select",
kind: "question",
options: [
{ id: "red", label: "Red" },
{ id: "blue", label: "Blue" },
Expand Down Expand Up @@ -120,6 +124,7 @@ describe("resolveTextToResponse", () => {
it("falls back to freeform for requests with no options", () => {
const noOptions: InputRequest = {
action: { callId: "call-5", input: {}, kind: "tool-call", toolName: "ask_question" },
kind: "question",
prompt: "Tell me something.",
requestId: "req-5",
};
Expand Down
1 change: 1 addition & 0 deletions packages/eve/src/cli/dev/tui/runner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -924,6 +924,7 @@ describe("EveTUIRunner native continuation state", () => {
toolName: "get_weather",
},
display: "confirmation",
kind: "tool-approval",
options: [
{ id: "approve", label: "Approve" },
{ id: "deny", label: "Deny" },
Expand Down
14 changes: 6 additions & 8 deletions packages/eve/src/cli/dev/tui/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1879,7 +1879,11 @@ async function* eveEventsToTUIStream(
for (const request of requests) {
const toolCallId = request.action.callId;

if (!knownToolCalls.has(toolCallId)) {
// The session-limit continuation is harness-authored — no model
// tool call exists behind it, so fabricating a transcript entry
// would render a phantom call. Its question rendering already
// carries the prompt copy.
if (request.kind !== "session-limit" && !knownToolCalls.has(toolCallId)) {
knownToolCalls.add(toolCallId);
yield {
type: "tool-call",
Expand All @@ -1893,7 +1897,7 @@ async function* eveEventsToTUIStream(
seenInputRequestIds.add(request.requestId);
pendingInputRequests.set(request.requestId, request);

if (isQuestionRequest(request)) {
if (request.kind !== "tool-approval") {
upsertPendingQuestion(turnState, request);
continue;
}
Expand Down Expand Up @@ -2212,12 +2216,6 @@ function toFailureEvent(
return failure;
}

function isQuestionRequest(request: InputRequest): boolean {
if (request.display === "select" || request.display === "text") return true;
if (request.display === "confirmation") return false;
return request.options !== undefined && request.options.length > 0;
}

function toAgentTUIInputQuestion(request: InputRequest): AgentTUIInputQuestion {
const display: "select" | "text" =
request.display === "text"
Expand Down
15 changes: 13 additions & 2 deletions packages/eve/src/cli/invoke/invoke.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const localTarget = {
const resume = { session: cursor, target };
const request = {
action: { callId: "call-1", input: {}, kind: "tool-call" as const, toolName: "bash" },
kind: "tool-approval" as const,
display: "confirmation" as const,
options: [
{ id: "approve", label: "Approve" },
Expand Down Expand Up @@ -240,8 +241,18 @@ describe("resolveInvokeOperation", () => {
const previous = parseInvokeResumeInput({
status: "input-required",
requests: [
{ options: request.options, prompt: request.prompt, requestId: request.requestId },
{ options: request.options, prompt: request.prompt, requestId: "approval-2" },
{
kind: request.kind,
options: request.options,
prompt: request.prompt,
requestId: request.requestId,
},
{
kind: request.kind,
options: request.options,
prompt: request.prompt,
requestId: "approval-2",
},
],
resume,
});
Expand Down
3 changes: 2 additions & 1 deletion packages/eve/src/cli/invoke/result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,10 @@ export type InvokeAuthenticationFailure = Extract<

/** Projects a runtime input request to the stable invocation-facing contract. */
export function projectInvocationInputRequest(request: InputRequest): InvocationInputRequest {
const { allowFreeform, options, prompt, requestId } = request;
const { allowFreeform, kind, options, prompt, requestId } = request;
return {
allowFreeform,
kind,
options: options?.map(({ description, id, label }) => ({ description, id, label })),
prompt,
requestId,
Expand Down
8 changes: 7 additions & 1 deletion packages/eve/src/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,9 +127,15 @@ export type {

export { isCurrentTurnBoundaryEvent, isTurnFailureEvent } from "#protocol/message.js";

export type { InputOption, InputRequest, InputResponse } from "#runtime/input/types.js";
export type {
InputOption,
InputRequest,
InputRequestKind,
InputResponse,
} from "#runtime/input/types.js";
export {
inputOptionSchema,
inputRequestKindSchema,
inputRequestSchema,
inputResponseSchema,
isInputRequest,
Expand Down
1 change: 1 addition & 0 deletions packages/eve/src/client/message-action-parts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export function toMessageInputRequest(request: InputRequest): EveMessageInputReq
return {
allowFreeform: request.allowFreeform,
display: request.display,
kind: request.kind,
options: request.options,
prompt: request.prompt,
requestId: request.requestId,
Expand Down
8 changes: 5 additions & 3 deletions packages/eve/src/client/message-reducer-types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { InputResponse } from "#runtime/input/types.js";
import type { InputRequest, InputResponse } from "#runtime/input/types.js";
import type { AuthorizationOutcome } from "#protocol/message.js";

/**
Expand Down Expand Up @@ -231,12 +231,14 @@ export interface EveMessageToolMetadata {
* is the question, `display` selects the control (`"confirmation"`, `"select"`,
* or `"text"`), `options` lists selectable choices (each with a `label` and
* optional `style`), and `allowFreeform` permits a typed response alongside the
* options. `requestId` is the stable identifier the client returns in the
* responding {@link InputResponse}.
* options. `kind` identifies the framework-owned request source. `requestId`
* is the stable identifier the client returns in the responding
* {@link InputResponse}.
*/
export interface EveMessageInputRequest {
readonly allowFreeform?: boolean;
readonly display?: "confirmation" | "select" | "text";
readonly kind: InputRequest["kind"];
readonly options?: readonly {
readonly description?: string;
readonly id: string;
Expand Down
5 changes: 5 additions & 0 deletions packages/eve/src/client/message-reducer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,7 @@ describe("defaultMessageReducer", () => {
toolName: "bash",
},
display: "confirmation",
kind: "tool-approval",
options: [
{ id: "approve", label: "Yes", style: "primary" },
{ id: "deny", label: "No", style: "danger" },
Expand Down Expand Up @@ -404,6 +405,7 @@ describe("defaultMessageReducer", () => {
inputRequest: {
allowFreeform: undefined,
display: "confirmation",
kind: "tool-approval",
options: [
{ id: "approve", label: "Yes", style: "primary" },
{ id: "deny", label: "No", style: "danger" },
Expand Down Expand Up @@ -437,6 +439,7 @@ describe("defaultMessageReducer", () => {
toolName: "bash",
},
display: "confirmation",
kind: "tool-approval",
options: [
{ id: "approve", label: "Yes", style: "primary" },
{ id: "deny", label: "No", style: "danger" },
Expand Down Expand Up @@ -481,6 +484,7 @@ describe("defaultMessageReducer", () => {
inputRequest: {
allowFreeform: undefined,
display: "confirmation",
kind: "tool-approval",
options: [
{ id: "approve", label: "Yes", style: "primary" },
{ id: "deny", label: "No", style: "danger" },
Expand Down Expand Up @@ -515,6 +519,7 @@ describe("defaultMessageReducer", () => {
toolName: "bash",
},
display: "confirmation",
kind: "tool-approval",
options: [
{ id: "approve", label: "Yes", style: "primary" },
{ id: "deny", label: "No", style: "danger" },
Expand Down
1 change: 1 addition & 0 deletions packages/eve/src/client/session-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ describe("summarizeTurnEvents", () => {
it("projects the complete waiting-turn lifecycle in one pass", () => {
const request = {
action: { callId: "call_1", input: {}, kind: "tool-call" as const, toolName: "bash" },
kind: "tool-approval" as const,
display: "confirmation" as const,
options: [{ id: "approve", label: "Approve" }],
prompt: "Approve?",
Expand Down
8 changes: 4 additions & 4 deletions packages/eve/src/compiler/extension-compatibility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@ interface ExtensionCapabilityContract {
const EXTENSION_CAPABILITY_CONTRACTS = {
extension: { current: 1, supported: [1], dropped: {} },
tool: { current: 3, supported: [1, 2, 3], dropped: {} },
dynamicTool: { current: 5, supported: [1, 2, 3, 4, 5], dropped: {} },
dynamicTool: { current: 6, supported: [1, 2, 3, 4, 5, 6], dropped: {} },
connection: { current: 2, supported: [1, 2], dropped: {} },
hook: { current: 3, supported: [1, 2, 3], dropped: {} },
hook: { current: 4, supported: [1, 2, 3, 4], dropped: {} },
skill: { current: 1, supported: [1], dropped: {} },
dynamicSkill: { current: 2, supported: [1, 2], dropped: {} },
dynamicSkill: { current: 3, supported: [1, 2, 3], dropped: {} },
instructions: { current: 1, supported: [1], dropped: {} },
dynamicInstructions: { current: 2, supported: [1, 2], dropped: {} },
dynamicInstructions: { current: 3, supported: [1, 2, 3], dropped: {} },
config: { current: 1, supported: [1], dropped: {} },
state: { current: 2, supported: [1, 2], dropped: {} },
} as const satisfies Record<string, ExtensionCapabilityContract>;
Expand Down
1 change: 1 addition & 0 deletions packages/eve/src/evals/runner/derive-run-facts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ function inputRequested(requestIds: readonly string[]): UnstampedMessageStreamEv
kind: "tool-call" as const,
toolName: "bash",
},
kind: "tool-approval" as const,
prompt: "Approve?",
requestId,
})),
Expand Down
1 change: 1 addition & 0 deletions packages/eve/src/evals/runner/execute-task.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -719,6 +719,7 @@ function inputRequested(
action: { callId: "call_1", input: { command: "pwd" }, kind: "tool-call", toolName },
allowFreeform: false,
display: "confirmation",
kind: "tool-approval",
options: [
{ id: "approve", label: "Approve" },
{ id: "deny", label: "Deny" },
Expand Down
1 change: 1 addition & 0 deletions packages/eve/src/execution/subagent-adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ function sampleRequest(): InputRequest {
kind: "tool-call",
toolName: "create_issue",
},
kind: "tool-approval",
options: [
{ id: "approve", label: "Approve" },
{ id: "deny", label: "Deny" },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ function buildApprovalRequest(requestId: string): InputRequest {
toolName: "create_issue",
},
display: "confirmation",
kind: "tool-approval",
options: [
{ id: "approve", label: "Approve", style: "primary" },
{ id: "deny", label: "Deny", style: "danger" },
Expand Down
1 change: 1 addition & 0 deletions packages/eve/src/execution/workflow-steps.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1482,6 +1482,7 @@ describe("runProxySubagentEventStep", () => {
kind: "tool-call",
toolName: "dangerous_tool",
},
kind: "tool-approval",
options: [
{ id: "approve", label: "Approve" },
{ id: "deny", label: "Deny" },
Expand Down
Loading
Loading