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
6 changes: 6 additions & 0 deletions .changeset/eve-tomodeloutput-durable.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@github-tools/eve-extension': patch
'@github-tools/sdk': patch
---

Apply built-in eve `toModelOutput` formatters through an inline callback that only closes over the tool name. On eve 0.44.x this keeps tools like `getFileContent` from failing durable-descriptor validation and dropping the whole GitHub toolset.
8 changes: 4 additions & 4 deletions apps/chat/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
"db:migrate": "nuxt db migrate"
},
"dependencies": {
"@ai-sdk/gateway": "^4.0.41",
"@ai-sdk/vue": "^4.0.52",
"@ai-sdk/workflow": "^1.0.52",
"@ai-sdk/gateway": "^4.0.56",
"@ai-sdk/vue": "^4.0.70",
"@ai-sdk/workflow": "^1.0.70",
"@github-tools/sdk": "workspace:*",
"@iconify-json/logos": "^1.2.12",
"@iconify-json/lucide": "^1.2.121",
Expand All @@ -31,7 +31,7 @@
"@workflow/ai": "^4.2.0",
"@workflow/nitro": "4.1.6",
"@workflow/nuxt": "^4.0.16",
"ai": "^7.0.52",
"ai": "^7.0.70",
"date-fns": "^4.4.0",
"drizzle-orm": "^0.45.2",
"h3": "^1.15.11",
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/content/docs/2.frameworks/1.eve-extension.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ export default githubExtension({

## Durable multi-turn sessions

The extension registers each tool with an **authored inline** `execute` that only closes over a serializable tool `name`, then rebuilds session options from the extension config on every call via `@github-tools/sdk/eve-runtime`. Tools resolve on `step.started` so registration stays fresh across durable steps. That pattern survives multi-turn eve Workflow replay (see [#51](https://github.com/vercel-labs/github-tools/issues/51)). Prefer this mount over the deprecated [`createGithubTools`](/deprecated/eve) / [`connectGithubTools`](/deprecated/eve) paths for Slack / multi-turn durable agents — those register tools from inside `node_modules` and are skipped on replay.
The extension registers each tool with an **authored inline** `execute` and `toModelOutput` that only close over a serializable tool `name`, then rebuilds session options from the extension config on every call via `@github-tools/sdk/eve-runtime`. Tools resolve on `step.started` so registration stays fresh across durable steps. That pattern survives multi-turn eve Workflow replay (see [#51](https://github.com/vercel-labs/github-tools/issues/51), [#99](https://github.com/vercel-labs/github-tools/issues/99)). Prefer this mount over the deprecated [`createGithubTools`](/deprecated/eve) / [`connectGithubTools`](/deprecated/eve) paths for Slack / multi-turn durable agents — those register tools from inside `node_modules` and are skipped on replay. Author `overrides.toModelOutput` inline in the agent; a function imported from a library will not get a durable descriptor.

## Durable approval, done right

Expand Down
4 changes: 3 additions & 1 deletion apps/docs/content/docs/5.api/2.reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -294,12 +294,14 @@ export default createGithubTools({

## `@github-tools/sdk/eve-runtime`

Shared eve primitives for `@github-tools/eve-extension` and advanced integrations: tool descriptors, `executeGithubEveTool`, approval helpers (`mapEveApprovalValue`, `resolveEveApproval`), and related types. **Not deprecated** — this is the supported low-level surface the extension imports. Prefer the extension mount for agents; use this subpath only when building on top of the same runtime.
Shared eve primitives for `@github-tools/eve-extension` and advanced integrations: tool descriptors, `executeGithubEveTool`, `formatGithubEveToolOutput` / `hasGithubEveToolModelOutput` (built-in `toModelOutput` lookup by tool name), approval helpers (`mapEveApprovalValue`, `resolveEveApproval`), and related types. **Not deprecated** — this is the supported low-level surface the extension imports. Prefer the extension mount for agents; use this subpath only when building on top of the same runtime.

```ts [import-eve-runtime.ts]
import {
listEveToolDescriptors,
executeGithubEveTool,
formatGithubEveToolOutput,
hasGithubEveToolModelOutput,
mapEveApprovalValue,
resolveEveApproval,
} from '@github-tools/sdk/eve-runtime'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ export default githubExtension({
})
```

Built-in `toModelOutput` formatters are applied via an inline callback that only closes over the tool name. Author `overrides.toModelOutput` inline in the agent — a library function will not get a durable descriptor on eve 0.44+.

## Approval

- Default: write tools → `always()`
Expand Down
2 changes: 1 addition & 1 deletion packages/github-tools-eve-extension/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export default githubExtension({

> `code-review` pairs cleanly with a Connect `connector`. `maintainer` and `repo-explorer` include gist tools, and GitHub only grants gist access to user access tokens, never the installation tokens Connect mints, so gist calls 403 over Connect. Write tools already require approval via `always()` by default, so a plain `{ someTool: true }` is a no-op, use a predicate (as above) when you actually want to narrow or loosen the default.

Tools are registered with **inline** `execute` handlers in the extension package so they survive multi-turn durable eve Workflow replay (see [#51](https://github.com/vercel-labs/github-tools/issues/51)). Do not use the deprecated `@github-tools/sdk/connect/eve` one-liner for durable Slack/multi-turn agents.
Tools are registered with **inline** `execute` and `toModelOutput` handlers in the extension package so they survive multi-turn durable eve Workflow replay (see [#51](https://github.com/vercel-labs/github-tools/issues/51), [#99](https://github.com/vercel-labs/github-tools/issues/99)). Do not use the deprecated `@github-tools/sdk/connect/eve` one-liner for durable Slack/multi-turn agents.

`connector` also accepts a `() => string | Promise<string>` resolver, so the same config can pick a connector dynamically (e.g. by environment):

Expand Down
11 changes: 8 additions & 3 deletions packages/github-tools-eve-extension/extension/tools/github.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { connectGithubToken } from '@github-tools/sdk/connect'
import {
executeGithubEveTool,
formatGithubEveToolOutput,
hasGithubEveToolModelOutput,
isEveApprovalDisabled,
listEveToolDescriptors,
mapEveApprovalValue,
Expand All @@ -17,8 +19,9 @@ import extension from '../extension'

/**
* Rebuild options from extension config on every call.
* Durable `execute` only closes over a serializable tool `name` (#51); reading
* config here avoids a module-level store that races across concurrent sessions.
* Durable `execute` / `toModelOutput` only close over a serializable tool `name`
* (#51, #99); reading config here avoids a module-level store that races across
* concurrent sessions.
*/
function buildSessionOptions(): EveGithubToolsOptions {
const {
Expand Down Expand Up @@ -110,7 +113,9 @@ export default defineDynamic({
}),
...(override?.toModelOutput !== undefined
? { toModelOutput: override.toModelOutput }
: entry.toModelOutput ? { toModelOutput: entry.toModelOutput } : {}),
: hasGithubEveToolModelOutput(name)
? { toModelOutput: (output: unknown) => formatGithubEveToolOutput(name, output) }
: {}),
...(override?.outputSchema !== undefined && {
outputSchema: override.outputSchema,
}),
Expand Down
2 changes: 2 additions & 0 deletions packages/github-tools/src/eve-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ export {
listResolvedEveToolNames,
listEveToolDescriptors,
executeGithubEveTool,
formatGithubEveToolOutput,
hasGithubEveToolModelOutput,
} from './eve/build'
export { mapEveApprovalValue, resolveEveApproval, resolveEveToolApproval, isEveApprovalDisabled } from './eve/approval'
export type {
Expand Down
23 changes: 22 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, listResolvedEveToolNames } from './build'
import { buildEveToolDefinition, buildEveToolMap, createEveGithubToolsDynamic, formatGithubEveToolOutput, hasGithubEveToolModelOutput, listResolvedEveToolNames } from './build'
import { getEveTools } from './load-eve'

describe('createGithubTools eve integration', () => {
Expand Down Expand Up @@ -126,4 +126,25 @@ describe('createGithubTools eve integration', () => {
expect(tools.addIssueComment?.approval).toBeUndefined()
expect(tools.listIssues?.approval).toBeUndefined()
})

it('looks up built-in toModelOutput formatters by tool name', () => {
expect(hasGithubEveToolModelOutput('getFileContent')).toBe(true)
expect(hasGithubEveToolModelOutput('listIssues')).toBe(false)
expect(formatGithubEveToolOutput('getFileContent', {
type: 'file',
path: 'README.md',
sha: 'abc',
size: 5,
content: 'hello',
})).toEqual({
type: 'json',
value: {
type: 'file',
path: 'README.md',
sha: 'abc',
size: 5,
content: 'hello',
},
})
})
})
4 changes: 3 additions & 1 deletion packages/github-tools/src/eve/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { resolvePresetTools, type CombinedPresetToolNames, type GithubToolPreset
import { createGithubTokenResolver } from '../core/token'
import { isEveApprovalDisabled, mapEveApprovalValue, resolveEveToolApproval } from './approval'
import { getEveTools } from './load-eve'
import { ALL_GITHUB_TOOL_NAMES, createToolRegistry, type GithubToolName, type ToolBuildContext } from './registry'
import { ALL_GITHUB_TOOL_NAMES, createToolRegistry, formatGithubEveToolOutput, hasGithubEveToolModelOutput, type GithubToolName, type ToolBuildContext } from './registry'
import { runGithubToolStep } from './steps'
import type { EveGithubToolsOptions, EveToolFactoryOptions, EveToolOverrides } from './types'

Expand Down Expand Up @@ -164,6 +164,8 @@ export function listEveToolDescriptors(options: EveGithubToolsOptions = {}) {
}))
}

export { formatGithubEveToolOutput, hasGithubEveToolModelOutput }

/**
* Execute a GitHub tool by name with the given eve options (token/context/attribution).
* Used by `@github-tools/eve-extension` so `execute` only closes over a serializable tool name.
Expand Down
35 changes: 30 additions & 5 deletions packages/github-tools/src/eve/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,31 @@ function modelOutputAdapter(
return (output: unknown) => fn({ toolCallId: '', input: {}, output }) as ToolModelOutput
}

const GITHUB_EVE_TOOL_MODEL_OUTPUT = {
getFileContent: modelOutputAdapter(getFileContentToModelOutput),
listPullRequestFiles: modelOutputAdapter(listPullRequestFilesToModelOutput),
getPullRequestContext: modelOutputAdapter(getPullRequestContextToModelOutput),
getCommit: modelOutputAdapter(getCommitToModelOutput),
compareCommits: modelOutputAdapter(compareCommitsToModelOutput),
} satisfies Partial<Record<GithubToolName, (output: unknown) => ToolModelOutput>>

/** Whether a GitHub tool has a built-in eve `toModelOutput` projection. */
export function hasGithubEveToolModelOutput(name: GithubToolName): boolean {
return Object.hasOwn(GITHUB_EVE_TOOL_MODEL_OUTPUT, name)
}

/**
* Apply the built-in eve `toModelOutput` projection for a tool.
* Used by `@github-tools/eve-extension` so the callback only closes over a serializable tool name.
*/
export function formatGithubEveToolOutput(name: GithubToolName, output: unknown): ToolModelOutput {
const format = GITHUB_EVE_TOOL_MODEL_OUTPUT[name as keyof typeof GITHUB_EVE_TOOL_MODEL_OUTPUT]
if (!format) {
throw new Error(`No toModelOutput formatter for GitHub tool: ${name}`)
}
return format(output)
}

export function createToolRegistry(ctx: ToolBuildContext): ToolRegistryEntry[] {
const entries: ToolRegistryEntry[] = [
{
Expand All @@ -83,7 +108,7 @@ export function createToolRegistry(ctx: ToolBuildContext): ToolRegistryEntry[] {
description: repository.getFileContentDescription,
inputSchema: repository.getFileContentInputSchema,
execute: withToken(repository.getFileContentCore, ctx),
toModelOutput: modelOutputAdapter(getFileContentToModelOutput),
toModelOutput: GITHUB_EVE_TOOL_MODEL_OUTPUT.getFileContent,
},
{
name: 'getRepositoryTree',
Expand Down Expand Up @@ -182,7 +207,7 @@ export function createToolRegistry(ctx: ToolBuildContext): ToolRegistryEntry[] {
description: pullRequests.listPullRequestFilesDescription,
inputSchema: pullRequests.listPullRequestFilesInputSchema,
execute: withToken(pullRequests.listPullRequestFilesCore, ctx),
toModelOutput: modelOutputAdapter(listPullRequestFilesToModelOutput),
toModelOutput: GITHUB_EVE_TOOL_MODEL_OUTPUT.listPullRequestFiles,
},
{
name: 'listPullRequestReviews',
Expand All @@ -209,7 +234,7 @@ export function createToolRegistry(ctx: ToolBuildContext): ToolRegistryEntry[] {
description: bundles.getPullRequestContextDescription,
inputSchema: bundles.getPullRequestContextInputSchema,
execute: withToken(bundles.getPullRequestContextCore, ctx),
toModelOutput: modelOutputAdapter(getPullRequestContextToModelOutput),
toModelOutput: GITHUB_EVE_TOOL_MODEL_OUTPUT.getPullRequestContext,
},
{
name: 'getIssueContext',
Expand Down Expand Up @@ -419,7 +444,7 @@ export function createToolRegistry(ctx: ToolBuildContext): ToolRegistryEntry[] {
description: commits.getCommitDescription,
inputSchema: commits.getCommitInputSchema,
execute: withToken(commits.getCommitCore, ctx),
toModelOutput: modelOutputAdapter(getCommitToModelOutput),
toModelOutput: GITHUB_EVE_TOOL_MODEL_OUTPUT.getCommit,
},
{
name: 'getBlame',
Expand All @@ -432,7 +457,7 @@ export function createToolRegistry(ctx: ToolBuildContext): ToolRegistryEntry[] {
description: commits.compareCommitsDescription,
inputSchema: commits.compareCommitsInputSchema,
execute: withToken(commits.compareCommitsCore, ctx),
toModelOutput: modelOutputAdapter(compareCommitsToModelOutput),
toModelOutput: GITHUB_EVE_TOOL_MODEL_OUTPUT.compareCommits,
},
{
name: 'listGists',
Expand Down
Loading
Loading