Skip to content

Commit efd5295

Browse files
Add gated Gravity Index tool (#567)
Co-authored-by: James Grugett <jahooma@gmail.com>
1 parent e5a93b2 commit efd5295

18 files changed

Lines changed: 1538 additions & 4 deletions

File tree

agents/context-pruner.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,14 @@ const definition: AgentDefinition = {
291291
const query = input.query as string | undefined
292292
return query ? `Web search: "${query}"` : 'Web search'
293293
}
294+
case 'gravity_index': {
295+
const query = input.query as string | undefined
296+
const action = input.action as string | undefined
297+
if (query) {
298+
return `Gravity Index ${action ?? 'search'}: "${query}"`
299+
}
300+
return action ? `Gravity Index ${action}` : 'Gravity Index'
301+
}
294302
case 'read_docs': {
295303
const libraryTitle = input.libraryTitle as string | undefined
296304
const topic = input.topic as string | undefined
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import fs from 'fs'
2+
import os from 'os'
3+
import path from 'path'
4+
5+
import { API_KEY_ENV_VAR } from '@codebuff/common/constants/paths'
6+
import { CodebuffClient, type AgentDefinition } from '@codebuff/sdk'
7+
import { describe, expect, it } from 'bun:test'
8+
9+
import base2Free from '../base2/base2-free'
10+
11+
import type { PrintModeEvent } from '@codebuff/common/types/print-mode'
12+
13+
describe('Gravity Index SDK E2E', () => {
14+
it(
15+
'test agent uses gravity_index for third-party service selection',
16+
async () => {
17+
const apiKey = process.env[API_KEY_ENV_VAR]
18+
if (!apiKey) {
19+
console.warn(
20+
`Skipping Gravity Index E2E: set ${API_KEY_ENV_VAR} to run.`,
21+
)
22+
return
23+
}
24+
25+
const tmpDir = await fs.promises.mkdtemp(
26+
path.join(os.tmpdir(), 'gravity-index-e2e-'),
27+
)
28+
const events: PrintModeEvent[] = []
29+
const gravityIndexTestAgent = {
30+
...(base2Free as AgentDefinition),
31+
id: 'base2-free-gravity-index-e2e',
32+
displayName: 'Base2 Free Gravity Index E2E',
33+
toolNames: [
34+
...((base2Free as AgentDefinition).toolNames ?? []),
35+
'gravity_index',
36+
],
37+
systemPrompt: `${(base2Free as AgentDefinition).systemPrompt}
38+
39+
For this E2E test, use the gravity_index tool when asked to recommend third-party developer services.`,
40+
} satisfies AgentDefinition
41+
42+
try {
43+
const client = new CodebuffClient({
44+
apiKey,
45+
cwd: tmpDir,
46+
projectFiles: {
47+
'package.json': JSON.stringify({
48+
scripts: {},
49+
dependencies: { next: '^15.0.0' },
50+
}),
51+
},
52+
agentDefinitions: [gravityIndexTestAgent],
53+
handleEvent: (event) => {
54+
events.push(event)
55+
},
56+
})
57+
58+
const run = await client.run({
59+
agent: gravityIndexTestAgent.id,
60+
prompt:
61+
'Use the Gravity Index to recommend a transactional email API for a Next.js app. Include the tracked API-key signup URL from the tool result.',
62+
maxAgentSteps: 4,
63+
})
64+
65+
if (run.output.type === 'error') {
66+
throw new Error(run.output.message)
67+
}
68+
69+
const toolCalls = events.filter((event) => event.type === 'tool_call')
70+
expect(
71+
toolCalls.some(
72+
(event) =>
73+
'toolName' in event && event.toolName === 'gravity_index',
74+
),
75+
).toBe(true)
76+
77+
const outputText = events
78+
.filter((event) => event.type === 'text')
79+
.map((event) => ('text' in event ? event.text : ''))
80+
.join('')
81+
expect(outputText).toMatch(/https:\/\/index\.trygravity\.ai\/go\//)
82+
} finally {
83+
await fs.promises.rm(tmpDir, { recursive: true, force: true })
84+
}
85+
},
86+
{ timeout: 300_000 },
87+
)
88+
})

agents/types/tools.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export type ToolName =
99
| 'end_turn'
1010
| 'find_files'
1111
| 'glob'
12+
| 'gravity_index'
1213
| 'list_directory'
1314
| 'lookup_agent_info'
1415
| 'propose_str_replace'
@@ -41,6 +42,7 @@ export interface ToolParamsMap {
4142
end_turn: EndTurnParams
4243
find_files: FindFilesParams
4344
glob: GlobParams
45+
gravity_index: GravityIndexParams
4446
list_directory: ListDirectoryParams
4547
lookup_agent_info: LookupAgentInfoParams
4648
propose_str_replace: ProposeStrReplaceParams
@@ -156,6 +158,47 @@ export interface GlobParams {
156158
cwd?: string
157159
}
158160

161+
/**
162+
* Search, browse, inspect, or report integrations in the Gravity Index.
163+
*/
164+
export type GravityIndexParams =
165+
| {
166+
/** Search for the best service recommendation. */
167+
action: 'search'
168+
/** What the user needs, including stack, constraints, and required capabilities when known. */
169+
query: string
170+
/** Continue a previous Gravity Index search as a follow-up. */
171+
search_id?: string
172+
/** Optional structured context about the project, stack, or constraints. */
173+
context?: Record<string, any>
174+
}
175+
| {
176+
/** Browse catalog services by category and/or keyword. */
177+
action: 'browse'
178+
/** Optional category filter, e.g. Database, Auth, Payments, Hosting, Email, AI. */
179+
category?: string
180+
/** Optional keyword filter, e.g. sendgrid or postgres. */
181+
q?: string
182+
}
183+
| {
184+
/** List every category with service counts. */
185+
action: 'list_categories'
186+
}
187+
| {
188+
/** Fetch full detail for a single service by slug. */
189+
action: 'get_service'
190+
/** Service slug, e.g. supabase, stripe, sendgrid. */
191+
slug: string
192+
}
193+
| {
194+
/** Report that an integration from a prior search was completed. */
195+
action: 'report_integration'
196+
/** search_id from the earlier search result. */
197+
search_id: string
198+
/** Slug of the service that was actually integrated. */
199+
integrated_slug: string
200+
}
201+
159202
/**
160203
* List files and directories in the specified path. Returns separate arrays of file names and directory names.
161204
*/

common/src/constants/analytics-events.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,11 @@ export enum AnalyticsEvent {
124124
DOCS_SEARCH_INSUFFICIENT_CREDITS = 'api.docs_search_insufficient_credits',
125125
DOCS_SEARCH_ERROR = 'api.docs_search_error',
126126

127+
GRAVITY_INDEX_REQUEST = 'api.gravity_index_request',
128+
GRAVITY_INDEX_AUTH_ERROR = 'api.gravity_index_auth_error',
129+
GRAVITY_INDEX_VALIDATION_ERROR = 'api.gravity_index_validation_error',
130+
GRAVITY_INDEX_ERROR = 'api.gravity_index_error',
131+
127132
// Web - Feedback API
128133
FEEDBACK_SUBMITTED = 'api.feedback_submitted',
129134
FEEDBACK_AUTH_ERROR = 'api.feedback_auth_error',

common/src/templates/initial-agents-dir/types/tools.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export type ToolName =
99
| 'end_turn'
1010
| 'find_files'
1111
| 'glob'
12+
| 'gravity_index'
1213
| 'list_directory'
1314
| 'lookup_agent_info'
1415
| 'propose_str_replace'
@@ -41,6 +42,7 @@ export interface ToolParamsMap {
4142
end_turn: EndTurnParams
4243
find_files: FindFilesParams
4344
glob: GlobParams
45+
gravity_index: GravityIndexParams
4446
list_directory: ListDirectoryParams
4547
lookup_agent_info: LookupAgentInfoParams
4648
propose_str_replace: ProposeStrReplaceParams
@@ -156,6 +158,47 @@ export interface GlobParams {
156158
cwd?: string
157159
}
158160

161+
/**
162+
* Search, browse, inspect, or report integrations in the Gravity Index.
163+
*/
164+
export type GravityIndexParams =
165+
| {
166+
/** Search for the best service recommendation. */
167+
action: 'search'
168+
/** What the user needs, including stack, constraints, and required capabilities when known. */
169+
query: string
170+
/** Continue a previous Gravity Index search as a follow-up. */
171+
search_id?: string
172+
/** Optional structured context about the project, stack, or constraints. */
173+
context?: Record<string, any>
174+
}
175+
| {
176+
/** Browse catalog services by category and/or keyword. */
177+
action: 'browse'
178+
/** Optional category filter, e.g. Database, Auth, Payments, Hosting, Email, AI. */
179+
category?: string
180+
/** Optional keyword filter, e.g. sendgrid or postgres. */
181+
q?: string
182+
}
183+
| {
184+
/** List every category with service counts. */
185+
action: 'list_categories'
186+
}
187+
| {
188+
/** Fetch full detail for a single service by slug. */
189+
action: 'get_service'
190+
/** Service slug, e.g. supabase, stripe, sendgrid. */
191+
slug: string
192+
}
193+
| {
194+
/** Report that an integration from a prior search was completed. */
195+
action: 'report_integration'
196+
/** search_id from the earlier search result. */
197+
search_id: string
198+
/** Slug of the service that was actually integrated. */
199+
integrated_slug: string
200+
}
201+
159202
/**
160203
* List files and directories in the specified path. Returns separate arrays of file names and directory names.
161204
*/
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { describe, expect, test } from 'bun:test'
2+
3+
import { compileToolDefinitions } from '../compile-tool-definitions'
4+
5+
describe('compileToolDefinitions', () => {
6+
test('emits type aliases for root union tool schemas', () => {
7+
const definitions = compileToolDefinitions()
8+
9+
expect(definitions).toContain('export type GravityIndexParams =')
10+
expect(definitions).not.toContain('export interface GravityIndexParams {')
11+
expect(definitions).toContain('"action": "search"')
12+
expect(definitions).toContain('"action": "report_integration"')
13+
})
14+
15+
test('keeps object tool schemas as interfaces', () => {
16+
const definitions = compileToolDefinitions()
17+
18+
expect(definitions).toContain('export interface WebSearchParams {')
19+
})
20+
})

common/src/tools/compile-tool-definitions.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,24 @@ export function compileToolDefinitions(): string {
1818

1919
// Convert Zod schema to TypeScript interface using JSON schema
2020
let typeDefinition: string
21+
let jsonSchema: unknown
2122
try {
22-
const jsonSchema = z.toJSONSchema(parameterSchema, { io: 'input' })
23+
jsonSchema = z.toJSONSchema(parameterSchema, { io: 'input' })
2324
typeDefinition = jsonSchemaToTypeScript(jsonSchema)
2425
} catch (error) {
2526
console.warn(`Failed to convert schema for ${toolName}:`, error)
2627
typeDefinition = '{ [key: string]: any }'
2728
}
2829

30+
const typeName = `${toPascalCase(toolName)}Params`
31+
const declaration = canEmitInterface(jsonSchema)
32+
? `export interface ${typeName} ${typeDefinition}`
33+
: `export type ${typeName} = ${typeDefinition}`
34+
2935
return `/**
3036
* ${parameterSchema.description || `Parameters for ${toolName} tool`}
3137
*/
32-
export interface ${toPascalCase(toolName)}Params ${typeDefinition}`
38+
${declaration}`
3339
})
3440
.join('\n\n')
3541

@@ -89,10 +95,22 @@ function jsonSchemaToTypeScript(schema: any): string {
8995
return getTypeFromJsonSchema(schema)
9096
}
9197

98+
function canEmitInterface(schema: any): boolean {
99+
return (
100+
schema.type === 'object' &&
101+
!!schema.properties &&
102+
!schema.anyOf &&
103+
!schema.oneOf
104+
)
105+
}
106+
92107
/**
93108
* Gets TypeScript type from JSON Schema property
94109
*/
95110
function getTypeFromJsonSchema(prop: any): string {
111+
if (prop.const !== undefined) {
112+
return JSON.stringify(prop.const)
113+
}
96114
if (prop.type === 'string') {
97115
if (prop.enum) {
98116
return prop.enum.map((v: string) => `"${v}"`).join(' | ')

common/src/tools/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export const toolNames = [
3030
'end_turn',
3131
'find_files',
3232
'glob',
33+
'gravity_index',
3334
'list_directory',
3435
'lookup_agent_info',
3536
'propose_str_replace',
@@ -62,6 +63,7 @@ export const publishedTools = [
6263
'end_turn',
6364
'find_files',
6465
'glob',
66+
'gravity_index',
6567
'list_directory',
6668
'lookup_agent_info',
6769
'propose_str_replace',

common/src/tools/list.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { createPlanParams } from './params/tool/create-plan'
1111
import { endTurnParams } from './params/tool/end-turn'
1212
import { findFilesParams } from './params/tool/find-files'
1313
import { globParams } from './params/tool/glob'
14+
import { gravityIndexParams } from './params/tool/gravity-index'
1415
import { listDirectoryParams } from './params/tool/list-directory'
1516
import { lookupAgentInfoParams } from './params/tool/lookup-agent-info'
1617
import { proposeStrReplaceParams } from './params/tool/propose-str-replace'
@@ -49,6 +50,7 @@ export const toolParams = {
4950
end_turn: endTurnParams,
5051
find_files: findFilesParams,
5152
glob: globParams,
53+
gravity_index: gravityIndexParams,
5254
list_directory: listDirectoryParams,
5355
lookup_agent_info: lookupAgentInfoParams,
5456
propose_str_replace: proposeStrReplaceParams,

0 commit comments

Comments
 (0)