Skip to content
Open
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
4 changes: 4 additions & 0 deletions packages/eve/src/cli/dev/tui/tool-presentation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,10 @@ describe("presentTool", () => {
grep: { pattern: "useEve" },
load_skill: { skill: "commit" },
read_file: { filePath: "/workspace/a.ts" },
task_await: { taskIds: ["task_abc"] },
task_cancel: { taskIds: ["task_abc"] },
task_peek: { taskIds: ["task_abc"] },
task_sleep: { seconds: 30 },
todo: { todos: [] },
web_fetch: { url: "https://example.com" },
web_search: { query: "eve framework" },
Expand Down
50 changes: 50 additions & 0 deletions packages/eve/src/cli/dev/tui/tool-presentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,38 @@ const BUILTIN_TOOL_COPY: Readonly<Record<string, BuiltinToolCopy>> = {
singularNoun: "file",
pluralNoun: "files",
},
task_await: {
verb: "Await",
pastVerb: "Awaited",
argKey: "taskIds",
extractItem: taskIdsArg,
singularNoun: "task",
pluralNoun: "tasks",
},
task_cancel: {
verb: "Cancel",
pastVerb: "Cancelled",
argKey: "taskIds",
extractItem: taskIdsArg,
singularNoun: "task",
pluralNoun: "tasks",
},
task_peek: {
verb: "Check",
pastVerb: "Checked",
argKey: "taskIds",
extractItem: taskIdsArg,
singularNoun: "task",
pluralNoun: "tasks",
},
task_sleep: {
verb: "Pause",
pastVerb: "Paused",
argKey: "seconds",
extractItem: sleepSecondsArg,
singularNoun: "pause",
pluralNoun: "pauses",
},
web_fetch: {
verb: "Fetch",
pastVerb: "Fetched",
Expand Down Expand Up @@ -359,6 +391,24 @@ function webSearchActionArg(input: unknown): string | undefined {
* renders verbatim in aggregated rows, so a model-controlled value must lose
* its terminal controls here, not at the render call sites.
*/
/** Joins a `taskIds: string[]` argument into one salient line. */
function taskIdsArg(input: unknown): string | undefined {
if (input === null || typeof input !== "object" || Array.isArray(input)) return undefined;
const value = (input as Record<string, unknown>).taskIds;
if (!Array.isArray(value)) return undefined;
const ids = value.filter((id): id is string => typeof id === "string");
if (ids.length === 0) return undefined;
return salientLine(ids.join(", "));
}

/** Formats a `seconds: number` argument as a duration. */
function sleepSecondsArg(input: unknown): string | undefined {
if (input === null || typeof input !== "object" || Array.isArray(input)) return undefined;
const value = (input as Record<string, unknown>).seconds;
if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
return `${String(value)}s`;
}

function salientArg(input: unknown, key: string): string | undefined {
if (input === null || typeof input !== "object" || Array.isArray(input)) return undefined;
const value = (input as Record<string, unknown>)[key];
Expand Down
45 changes: 45 additions & 0 deletions packages/eve/src/execution/node-step.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,51 @@ describe("createNodeHarnessTools", () => {

expect(tools.has("agent")).toBe(false);
});

it("does not inject task tools without experimental.tasks", () => {
const tools = createNodeHarnessTools({ node: createTestNode() });

for (const name of ["task_peek", "task_await", "task_cancel", "task_sleep"]) {
expect(tools.has(name)).toBe(false);
}
});

it("injects the task tools when experimental.tasks is on", () => {
const node = createTestNode();
const tools = createNodeHarnessTools({
node: {
...node,
agent: {
...node.agent,
config: { experimental: { tasks: true }, model: { id: "test-model" }, name: "test" },
},
},
});

for (const name of ["task_peek", "task_await", "task_cancel"]) {
expect(tools.get(name)?.runtimeAction).toEqual({ kind: "task-control" });
expect(tools.get(name)?.execute).toBeUndefined();
}
expect(tools.get("task_sleep")?.execute).toBeDefined();
expect(tools.get("task_sleep")?.runtimeAction).toBeUndefined();
});

it("respects disableTool for individual task tools", () => {
const node = createTestNode();
const tools = createNodeHarnessTools({
node: {
...node,
agent: {
...node.agent,
config: { experimental: { tasks: true }, model: { id: "test-model" }, name: "test" },
disabledFrameworkTools: ["task_cancel"],
},
},
});

expect(tools.has("task_peek")).toBe(true);
expect(tools.has("task_cancel")).toBe(false);
});
});

describe("createExecutionNodeStep", () => {
Expand Down
18 changes: 18 additions & 0 deletions packages/eve/src/execution/node-step.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ import {
AGENT_TOOL_NAME,
isImplicitAgentToolAvailable,
} from "#runtime/framework-tools/agent.js";
import {
createTaskToolHarnessDefinitions,
isTaskToolAvailable,
} from "#runtime/framework-tools/tasks.js";
import type { ResolvedRuntimeAgentNode } from "#runtime/graph.js";

import type { PreparedRuntimeTool } from "#runtime/sessions/turn.js";
Expand Down Expand Up @@ -218,6 +222,20 @@ export function createNodeHarnessTools(input: {
});
}

const tasksEnabled = input.node.agent.config?.experimental?.tasks === true;
for (const definition of createTaskToolHarnessDefinitions()) {
if (
isTaskToolAvailable({
disabledFrameworkTools: input.node.agent.disabledFrameworkTools,
hasAuthoredTool: tools.has(definition.name),
tasksEnabled,
toolName: definition.name,
})
) {
tools.set(definition.name, definition);
}
}

return tools;
}

Expand Down
36 changes: 36 additions & 0 deletions packages/eve/src/harness/advertised-tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,35 @@ describe("getAdvertisedTools for definition arrays", () => {

expect(advertisedTools.map((tool) => tool.name)).toEqual(["add", "delegate"]);
});

it("keeps the task tools in the root session", () => {
const tools = new Map([
["add", createTool("add")],
["task_peek", createTaskControlTool("task_peek")],
["task_sleep", createTool("task_sleep")],
]) satisfies HarnessToolMap;

const advertisedTools = getAdvertisedTools({ session: {}, tools });

expect([...advertisedTools.keys()]).toEqual(["add", "task_peek", "task_sleep"]);
});

it("removes the task tools from delegated sessions", () => {
const tools = new Map([
["add", createTool("add")],
["task_await", createTaskControlTool("task_await")],
["task_cancel", createTaskControlTool("task_cancel")],
["task_peek", createTaskControlTool("task_peek")],
["task_sleep", createTool("task_sleep")],
]) satisfies HarnessToolMap;

const advertisedTools = getAdvertisedTools({
session: { rootSessionId: "root-session", subagentDepth: 1 },
tools,
});

expect([...advertisedTools.keys()]).toEqual(["add"]);
});
});

function createTool(name: string): HarnessToolDefinition {
Expand Down Expand Up @@ -166,6 +195,13 @@ function createBuiltInAgentTool(): HarnessToolDefinition {
};
}

function createTaskControlTool(name: string): HarnessToolDefinition {
return {
...createTool(name),
runtimeAction: { kind: "task-control" },
};
}

function createSession(overrides: Partial<HarnessSession> = {}): HarnessSession {
return {
agent: {
Expand Down
25 changes: 20 additions & 5 deletions packages/eve/src/harness/advertised-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { ToolSet } from "ai";
import type { HarnessToolDefinition } from "#harness/execute-tool.js";
import { resolveSubagentDepth } from "#harness/subagent-depth.js";
import { AGENT_TOOL_NAME } from "#runtime/framework-tools/agent.js";
import { TASK_TOOL_NAMES } from "#runtime/framework-tools/tasks.js";
import { ROOT_RUNTIME_AGENT_NODE_ID } from "#runtime/graph.js";
import {
ensureWorkflowContinuationSecurity,
Expand Down Expand Up @@ -162,15 +163,29 @@ function shouldHideDelegationTool(
definition: HarnessToolDefinition,
session: AdvertisedToolSession,
): boolean {
if (isRootOnlyFrameworkTool(definition)) {
return session.rootSessionId !== undefined || resolveSubagentDepth(session).currentDepth > 0;
}

return false;
}

/**
* Tools that only a root session may see. The `agent` self-delegation
* tool and the `experimental.tasks` controls are injected from the root
* node's config, which self-delegated children share — session shape is
* the only signal that separates the root from its children.
*/
function isRootOnlyFrameworkTool(definition: HarnessToolDefinition): boolean {
if (
definition.name !== AGENT_TOOL_NAME ||
definition.runtimeAction?.kind !== "subagent-call" ||
definition.runtimeAction.nodeId !== ROOT_RUNTIME_AGENT_NODE_ID
definition.name === AGENT_TOOL_NAME &&
definition.runtimeAction?.kind === "subagent-call" &&
definition.runtimeAction.nodeId === ROOT_RUNTIME_AGENT_NODE_ID
) {
return false;
return true;

@vercel vercel Bot Aug 1, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isRootOnlyFrameworkTool hides any tool whose name is in TASK_TOOL_NAMES from subagent/child sessions, so an authored tool that merely shares a task tool name (e.g. task_sleep) is wrongly hidden even when experimental.tasks is disabled.

Fix on Vercel

}

return session.rootSessionId !== undefined || resolveSubagentDepth(session).currentDepth > 0;
return TASK_TOOL_NAMES.has(definition.name);
}

function isToolDefinitionList(
Expand Down
19 changes: 13 additions & 6 deletions packages/eve/src/harness/execute-tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,20 @@ import type { ToolExecuteOptions } from "#shared/tool-definition.js";
*
* These tools are surfaced to the model without a local `execute` function.
* The harness records the tool call and the runtime executes it later.
*
* `task-control` marks the `experimental.tasks` parent tools
* (`task_peek`, `task_await`, `task_cancel`): they carry no child
* address of their own — the dispatch step resolves targets through the
* session task index by tool name.
*/
export type HarnessRuntimeActionDefinition = {
readonly kind: "remote-agent-call" | "subagent-call";
readonly nodeId: string;
readonly remoteAgentName?: string;
readonly subagentName: string;
};
export type HarnessRuntimeActionDefinition =
| {
readonly kind: "remote-agent-call" | "subagent-call";
readonly nodeId: string;
readonly remoteAgentName?: string;
readonly subagentName: string;
}
| { readonly kind: "task-control" };

/**
* Unified harness-owned tool definition.
Expand Down
24 changes: 15 additions & 9 deletions packages/eve/src/harness/workflow-runtime-action-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,21 @@ export function buildRuntimeActionFromWorkflowInterrupt(
};
}

return {
callId,
description: "",
input: toolInput,
kind: "subagent-call",
name: toolName,
nodeId: runtimeAction.nodeId,
subagentName: runtimeAction.subagentName,
};
if (runtimeAction.kind === "subagent-call") {
return {
callId,
description: "",
input: toolInput,
kind: "subagent-call",
name: toolName,
nodeId: runtimeAction.nodeId,
subagentName: runtimeAction.subagentName,
};
}

// Dynamic workflows only interrupt on delegation tools; task controls
// never enter a workflow sandbox.
throw new Error(`Workflow runtime actions cannot carry "${runtimeAction.kind}" tools.`);
}

/** Returns every pending runtime-action interrupt in deterministic ledger order. */
Expand Down
2 changes: 2 additions & 0 deletions packages/eve/src/runtime/framework-tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
createSkillToolDefinition,
SKILL_TOOL_DEFINITION,
} from "#runtime/framework-tools/skill.js";
import { TASK_TOOL_DEFINITIONS } from "#runtime/framework-tools/tasks.js";
import { TODO_TOOL_DEFINITION } from "#runtime/framework-tools/todo.js";
import { WEB_FETCH_TOOL_DEFINITION } from "#runtime/framework-tools/web-fetch.js";
import { WEB_SEARCH_TOOL_DEFINITION } from "#runtime/framework-tools/web-search.js";
Expand Down Expand Up @@ -37,6 +38,7 @@ const REGISTERED_FRAMEWORK_TOOLS: readonly ResolvedToolDefinition[] = [
const ALL_FRAMEWORK_TOOLS: readonly ResolvedToolDefinition[] = [
...REGISTERED_FRAMEWORK_TOOLS,
AGENT_TOOL_DEFINITION,
...TASK_TOOL_DEFINITIONS,
];

/**
Expand Down
Loading
Loading