From 1a5cca36e38520ceacc9f60de92854fc574e6b0b Mon Sep 17 00:00:00 2001 From: Hugo Richard Date: Wed, 26 Aug 2026 10:35:48 +0100 Subject: [PATCH 1/2] fix(eve): return tool errors as execute results Eve's tool-loop logs thrown execute errors without always appending a tool_result, which Anthropic rejects as MODEL_CALL_FAILED. Return { error } so the pairing stays intact. --- .changeset/eve-tool-error-result.md | 5 +++++ .../content/docs/2.frameworks/1.eve-extension.md | 2 ++ .../github-tools-agents/references/eve-extension.md | 2 +- packages/github-tools/src/eve/build.test.ts | 12 +++++++++++- packages/github-tools/src/eve/steps.ts | 12 +++++++++--- 5 files changed, 28 insertions(+), 5 deletions(-) create mode 100644 .changeset/eve-tool-error-result.md diff --git a/.changeset/eve-tool-error-result.md b/.changeset/eve-tool-error-result.md new file mode 100644 index 0000000..5a2f07f --- /dev/null +++ b/.changeset/eve-tool-error-result.md @@ -0,0 +1,5 @@ +--- +'@github-tools/sdk': patch +--- + +Eve GitHub tool execute now returns `{ error }` on failure instead of throwing, so the model always gets a `tool_result` and the turn does not die with `MODEL_CALL_FAILED`. diff --git a/apps/docs/content/docs/2.frameworks/1.eve-extension.md b/apps/docs/content/docs/2.frameworks/1.eve-extension.md index 2d87cec..e9989b3 100644 --- a/apps/docs/content/docs/2.frameworks/1.eve-extension.md +++ b/apps/docs/content/docs/2.frameworks/1.eve-extension.md @@ -212,6 +212,8 @@ The extension registers each tool with an **authored inline** `execute` and `toM Object-shaped execute results include `rateLimit` (`remaining`, `limit`, `reset`, `resource`). `toModelOutput` strips it so the model never sees the remaining count; `toolResultFrom` and channels still do. See [Rate-limit metadata](/api/reference#rate-limit-metadata). +If execute fails (token mint, GitHub 403/429, …), the tool returns `{ error }` instead of throwing so the model always receives a `tool_result`. A thrown error in eve's tool-loop can leave a `tool_use` unpaired and kill the turn. + ## Durable approval, done right Approval **pauses the session durably** until a human responds, and policies are expressive: diff --git a/apps/docs/skills/github-tools-agents/references/eve-extension.md b/apps/docs/skills/github-tools-agents/references/eve-extension.md index e95309f..f53d208 100644 --- a/apps/docs/skills/github-tools-agents/references/eve-extension.md +++ b/apps/docs/skills/github-tools-agents/references/eve-extension.md @@ -55,7 +55,7 @@ export default githubExtension({ }) ``` -Built-in `toModelOutput` formatters are applied via an inline callback that only closes over the tool name. That callback also strips `rateLimit` from the model-facing payload. Author `overrides.toModelOutput` inline in the agent — a library function will not get a durable descriptor on eve 0.44+. +Built-in `toModelOutput` formatters are applied via an inline callback that only closes over the tool name. That callback also strips `rateLimit` from the model-facing payload. Author `overrides.toModelOutput` inline in the agent — a library function will not get a durable descriptor on eve 0.44+. Execute failures return `{ error }` so the model still receives a `tool_result`. ## Approval diff --git a/packages/github-tools/src/eve/build.test.ts b/packages/github-tools/src/eve/build.test.ts index 11d2bb0..536e344 100644 --- a/packages/github-tools/src/eve/build.test.ts +++ b/packages/github-tools/src/eve/build.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it, vi } from 'vitest' import { PRESET_TOOLS } from '../core/presets' import * as repositoryCore from '../core/repository' -import { buildEveToolDefinition, buildEveToolMap, createEveGithubToolsDynamic, formatGithubEveToolOutput, hasGithubEveToolModelOutput, listResolvedEveToolNames } from './build' +import { buildEveToolDefinition, buildEveToolMap, createEveGithubToolsDynamic, executeGithubEveTool, formatGithubEveToolOutput, hasGithubEveToolModelOutput, listResolvedEveToolNames } from './build' import { getEveTools } from './load-eve' describe('createGithubTools eve integration', () => { @@ -168,6 +168,16 @@ describe('createGithubTools eve integration', () => { }) }) + it('returns an error payload instead of throwing when execute fails', async () => { + await expect(executeGithubEveTool('getRepository', { owner: 'octocat', repo: 'hello-world' }, { + token: async () => { + throw new Error('Unable to find project root directory. Have you linked your project with `vc link?`') + }, + })).resolves.toEqual({ + error: 'Unable to find project root directory. Have you linked your project with `vc link?`', + }) + }) + it('returns stripped json for tools without a built-in formatter', () => { expect(formatGithubEveToolOutput('getRepository', { name: 'hello-world', diff --git a/packages/github-tools/src/eve/steps.ts b/packages/github-tools/src/eve/steps.ts index 65d5944..69f06b0 100644 --- a/packages/github-tools/src/eve/steps.ts +++ b/packages/github-tools/src/eve/steps.ts @@ -19,7 +19,13 @@ export async function runGithubToolStep( input: Record, ctx: ToolBuildContext, ) { - // Resolve the token before entering the step so only a serializable string crosses the boundary. - const token = await resolveGithubToken(ctx.token) - return executeGithubToolStep(name, input, { ...ctx, token }) + try { + // Resolve the token before entering the step so only a serializable string crosses the boundary. + const token = await resolveGithubToken(ctx.token) + return await executeGithubToolStep(name, input, { ...ctx, token }) + } catch (error) { + // Eve's tool-loop logs thrown execute errors but does not always append a + // tool_result. Returning a payload keeps the Anthropic tool_use/tool_result pairing intact. + return { error: error instanceof Error ? error.message : String(error) } + } } From 0ab3107b2958e81c5b1e31cb5b52383382a0388c Mon Sep 17 00:00:00 2001 From: "vercel[bot]" <35613825+vercel[bot]@users.noreply.github.com> Date: Wed, 26 Aug 2026 09:42:35 +0000 Subject: [PATCH 2/2] Fix: When `listPullRequestFiles` (or other array formatters) fails, `runGithubToolStep` returns `{ error }`, and `listPullRequestFilesToModelOutput` calls `.map` on that object, throwing `TypeError: files.map is not a function` inside model-output formatting. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit fixes the issue reported at packages/github-tools/src/eve/steps.ts:28 ## Bug `runGithubToolStep` (`packages/github-tools/src/eve/steps.ts`) was changed to catch execute failures and return an error payload instead of throwing: ```ts } catch (error) { return { error: error instanceof Error ? error.message : String(error) } } ``` Both eve wiring paths pass the `execute` return value into `toModelOutput`: * `build.ts` (`buildEveToolDefinition` / `buildEveToolMap`): `toModelOutput: (output) => formatGithubEveToolOutput(name, output)` * `executeGithubEveTool` (used by `@github-tools/eve-extension`) returns the same `runGithubToolStep` result, which eve then feeds to `toModelOutput`. `formatGithubEveToolOutput` (`registry.ts`) strips `rateLimit` then dispatches to a per-tool formatter. `stripRateLimit({ error })` leaves the object unchanged (no `rateLimit` key). For `listPullRequestFiles` it dispatches to `listPullRequestFilesToModelOutput` (`core/model-output.ts`): ```ts export function listPullRequestFilesToModelOutput({ output }) { const files = output as ListPullRequestFilesOutput return { type: 'json', value: truncatePatchFields(files) } // files.map(...) } ``` **Trigger:** any failed `listPullRequestFiles` call (e.g. GitHub 404/403, network error). The failure produces `output = { error: '...' }`, an object, so `truncatePatchFields(files)` → `files.map(...)` throws `TypeError: files.map is not a function`. This throws *inside model-output formatting*, re-breaking the tool loop and defeating the whole point of returning a payload (preserving `tool_use`/`tool_result` pairing). The other array formatters (`getCommit`, `compareCommits`, `getPullRequestContext`) guard `files` with a ternary, and `getFileContent` guards with `'content' in result`, so `listPullRequestFiles` is the one that crashes on the top-level payload. (The others would still return an error payload verbatim, but wouldn't crash.) ## Fix Short-circuit in `formatGithubEveToolOutput` — the single dispatch point shared by all paths — when the (rate-limit-stripped) output is an `{ error: string }` payload, returning it as `{ type: 'json', value }` without running the success-shaped formatter: ```ts if (isErrorPayload(stripped)) { return { type: 'json', value: stripped } } ``` `isErrorPayload` matches a non-array object with a string `error` field. This is safe because none of the GitHub tool success outputs carry a top-level `error: string` field. Typecheck (`tsc --noEmit`) passes. Co-authored-by: Vercel Co-authored-by: HugoRCD --- packages/github-tools/src/eve/registry.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/github-tools/src/eve/registry.ts b/packages/github-tools/src/eve/registry.ts index b3512cf..b542302 100644 --- a/packages/github-tools/src/eve/registry.ts +++ b/packages/github-tools/src/eve/registry.ts @@ -86,6 +86,13 @@ export function hasGithubEveToolModelOutput(name: GithubToolName): boolean { */ export function formatGithubEveToolOutput(name: GithubToolName, output: unknown): ToolModelOutput { const stripped = stripRateLimit(output) + // `runGithubToolStep` returns `{ error }` when execute throws. The per-tool + // formatters assume the success shape (e.g. `listPullRequestFilesToModelOutput` + // calls `.map` on the payload), so dispatching an error payload would throw a + // second time inside model-output formatting and re-break the tool loop. + if (isErrorPayload(stripped)) { + return { type: 'json', value: stripped } + } const format = GITHUB_EVE_TOOL_MODEL_OUTPUT[name as keyof typeof GITHUB_EVE_TOOL_MODEL_OUTPUT] if (!format) { return { type: 'json', value: stripped } @@ -93,6 +100,15 @@ export function formatGithubEveToolOutput(name: GithubToolName, output: unknown) return format(stripped) } +function isErrorPayload(output: unknown): output is { error: string } { + return ( + output != null + && typeof output === 'object' + && !Array.isArray(output) + && typeof (output as { error?: unknown }).error === 'string' + ) +} + export function createToolRegistry(ctx: ToolBuildContext): ToolRegistryEntry[] { const entries: ToolRegistryEntry[] = [ {