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/eve-tool-error-result.md
Original file line number Diff line number Diff line change
@@ -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`.
2 changes: 2 additions & 0 deletions apps/docs/content/docs/2.frameworks/1.eve-extension.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
12 changes: 11 additions & 1 deletion packages/github-tools/src/eve/build.test.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down Expand Up @@ -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',
Expand Down
16 changes: 16 additions & 0 deletions packages/github-tools/src/eve/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,29 @@ 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 }
}
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[] = [
{
Expand Down
12 changes: 9 additions & 3 deletions packages/github-tools/src/eve/steps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,13 @@ export async function runGithubToolStep(
input: Record<string, unknown>,
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.
Comment thread
vercel[bot] marked this conversation as resolved.
return { error: error instanceof Error ? error.message : String(error) }
}
}
Loading