Skip to content

Commit 9fe9347

Browse files
committed
Simplify Composio tool integration
1 parent 216ebda commit 9fe9347

14 files changed

Lines changed: 326 additions & 306 deletions

File tree

common/src/tools/constants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { COMPOSIO_META_TOOL_NAMES } from '../constants/composio'
2+
13
import type { ToolResultOutput } from '../types/messages/content-part'
24
import type { Tool } from 'ai'
35

@@ -56,6 +58,7 @@ export const toolNames = [
5658
'web_search',
5759
'write_file',
5860
'write_todos',
61+
...COMPOSIO_META_TOOL_NAMES,
5962
] as const
6063

6164
export const publishedTools = [

common/src/tools/list.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { applyPatchParams } from './params/tool/apply-patch'
77
import { askUserParams } from './params/tool/ask-user'
88
import { browserLogsParams } from './params/tool/browser-logs'
99
import { codeSearchParams } from './params/tool/code-search'
10+
import { composioMetaToolParams } from './params/tool/composio'
1011
import { createPlanParams } from './params/tool/create-plan'
1112
import { endTurnParams } from './params/tool/end-turn'
1213
import { findFilesParams } from './params/tool/find-files'
@@ -77,6 +78,7 @@ export const toolParams = {
7778
web_search: webSearchParams,
7879
write_file: writeFileParams,
7980
write_todos: writeTodosParams,
81+
...composioMetaToolParams,
8082
} satisfies {
8183
[K in ToolName]: $ToolParams<K>
8284
}
@@ -151,6 +153,22 @@ export const clientToolCallSchema = z.discriminatedUnion('toolName', [
151153
toolName: z.literal('write_file'),
152154
input: FileChangeSchema,
153155
}),
156+
z.object({
157+
toolName: z.literal('COMPOSIO_MANAGE_CONNECTIONS'),
158+
input: toolParams.COMPOSIO_MANAGE_CONNECTIONS.inputSchema,
159+
}),
160+
z.object({
161+
toolName: z.literal('COMPOSIO_MULTI_EXECUTE_TOOL'),
162+
input: toolParams.COMPOSIO_MULTI_EXECUTE_TOOL.inputSchema,
163+
}),
164+
z.object({
165+
toolName: z.literal('COMPOSIO_SEARCH_TOOLS'),
166+
input: toolParams.COMPOSIO_SEARCH_TOOLS.inputSchema,
167+
}),
168+
z.object({
169+
toolName: z.literal('COMPOSIO_GET_TOOL_SCHEMAS'),
170+
input: toolParams.COMPOSIO_GET_TOOL_SCHEMAS.inputSchema,
171+
}),
154172
])
155173
export const clientToolNames = clientToolCallSchema.def.options.map(
156174
(opt) => opt.shape.toolName.value,
@@ -163,4 +181,4 @@ export type ClientToolCall<T extends ClientToolName = ClientToolName> = Extract<
163181
> &
164182
Pick<ToolCallPart, 'toolCallId' | 'toolName' | 'input' | 'providerOptions'>
165183

166-
export type PublishedClientToolName = ClientToolName & PublishedToolName
184+
export type PublishedClientToolName = Extract<ClientToolName, PublishedToolName>
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import { COMPOSIO_META_TOOL_NAMES } from '../../../constants/composio'
2+
import z from 'zod/v4'
3+
4+
import { jsonToolResultSchema } from '../utils'
5+
6+
import type { $ToolParams } from '../../constants'
7+
8+
const sessionIdParam = z
9+
.string()
10+
.optional()
11+
.describe('Session ID returned by COMPOSIO_SEARCH_TOOLS, when available.')
12+
13+
const composioMetaToolInputSchemas = {
14+
COMPOSIO_SEARCH_TOOLS: z
15+
.object({
16+
queries: z
17+
.array(z.unknown())
18+
.min(1)
19+
.describe(
20+
'Structured English search queries. Split independent app/API actions into separate queries.',
21+
),
22+
session: z
23+
.object({
24+
generate_id: z.boolean().optional(),
25+
id: z.string().optional(),
26+
})
27+
.catchall(z.unknown())
28+
.describe(
29+
'Use { generate_id: true } for a new workflow, or { id } to continue one.',
30+
),
31+
model: z.string().optional().describe('Client LLM model name.'),
32+
})
33+
.catchall(z.unknown()),
34+
COMPOSIO_GET_TOOL_SCHEMAS: z
35+
.object({
36+
tool_slugs: z
37+
.array(z.string())
38+
.min(1)
39+
.describe('Composio tool slugs to retrieve schemas for.'),
40+
include: z
41+
.array(z.string())
42+
.optional()
43+
.describe('Schema fields to include, e.g. input_schema/output_schema.'),
44+
session_id: sessionIdParam,
45+
})
46+
.catchall(z.unknown()),
47+
COMPOSIO_MANAGE_CONNECTIONS: z
48+
.object({
49+
toolkits: z
50+
.array(z.string())
51+
.min(1)
52+
.describe('Toolkit slugs to check or connect, such as gmail/github.'),
53+
reinitiate_all: z
54+
.boolean()
55+
.optional()
56+
.describe('Force reconnection even if active credentials exist.'),
57+
session_id: sessionIdParam,
58+
})
59+
.catchall(z.unknown()),
60+
COMPOSIO_MULTI_EXECUTE_TOOL: z
61+
.object({
62+
tools: z
63+
.array(z.record(z.string(), z.unknown()))
64+
.min(1)
65+
.describe('Logically independent Composio tools to execute.'),
66+
thought: z
67+
.string()
68+
.optional()
69+
.describe('One concise sentence explaining the execution intent.'),
70+
sync_response_to_workbench: z
71+
.boolean()
72+
.default(false)
73+
.describe('Always use false. Codebuff disables Composio workbench.'),
74+
session_id: sessionIdParam,
75+
})
76+
.catchall(z.unknown()),
77+
}
78+
79+
const composioMetaToolDescriptions = {
80+
COMPOSIO_SEARCH_TOOLS:
81+
'Discover relevant Composio tools across external apps. Use this first for requests involving services like Gmail, GitHub, Slack, Linear, Notion, Google Calendar, or Google Sheets.',
82+
COMPOSIO_GET_TOOL_SCHEMAS:
83+
'Retrieve complete input schemas for specific Composio tool slugs returned by COMPOSIO_SEARCH_TOOLS.',
84+
COMPOSIO_MANAGE_CONNECTIONS:
85+
'Check or initiate user authentication for external app toolkits. Use when search/execution indicates a toolkit is not connected.',
86+
COMPOSIO_MULTI_EXECUTE_TOOL:
87+
'Execute one or more discovered Composio app tools in the current workflow session. Do not use workbench offloading.',
88+
}
89+
90+
const composioOutputSchema = jsonToolResultSchema(
91+
z.union([
92+
z.json(),
93+
z.object({
94+
errorMessage: z.string(),
95+
status: z.number().optional(),
96+
}),
97+
]),
98+
)
99+
100+
export const composioMetaToolParams = {
101+
COMPOSIO_MANAGE_CONNECTIONS: {
102+
toolName: 'COMPOSIO_MANAGE_CONNECTIONS',
103+
endsAgentStep: true,
104+
description: composioMetaToolDescriptions.COMPOSIO_MANAGE_CONNECTIONS,
105+
inputSchema: composioMetaToolInputSchemas.COMPOSIO_MANAGE_CONNECTIONS,
106+
outputSchema: composioOutputSchema,
107+
},
108+
COMPOSIO_MULTI_EXECUTE_TOOL: {
109+
toolName: 'COMPOSIO_MULTI_EXECUTE_TOOL',
110+
endsAgentStep: true,
111+
description: composioMetaToolDescriptions.COMPOSIO_MULTI_EXECUTE_TOOL,
112+
inputSchema: composioMetaToolInputSchemas.COMPOSIO_MULTI_EXECUTE_TOOL,
113+
outputSchema: composioOutputSchema,
114+
},
115+
COMPOSIO_SEARCH_TOOLS: {
116+
toolName: 'COMPOSIO_SEARCH_TOOLS',
117+
endsAgentStep: true,
118+
description: composioMetaToolDescriptions.COMPOSIO_SEARCH_TOOLS,
119+
inputSchema: composioMetaToolInputSchemas.COMPOSIO_SEARCH_TOOLS,
120+
outputSchema: composioOutputSchema,
121+
},
122+
COMPOSIO_GET_TOOL_SCHEMAS: {
123+
toolName: 'COMPOSIO_GET_TOOL_SCHEMAS',
124+
endsAgentStep: true,
125+
description: composioMetaToolDescriptions.COMPOSIO_GET_TOOL_SCHEMAS,
126+
inputSchema: composioMetaToolInputSchemas.COMPOSIO_GET_TOOL_SCHEMAS,
127+
outputSchema: composioOutputSchema,
128+
},
129+
} satisfies {
130+
[K in (typeof COMPOSIO_META_TOOL_NAMES)[number]]: $ToolParams<K>
131+
}

packages/agent-runtime/src/tools/handlers/list.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ import { handleApplyPatch } from './tool/apply-patch'
44
import { handleAskUser } from './tool/ask-user'
55
import { handleBrowserLogs } from './tool/browser-logs'
66
import { handleCodeSearch } from './tool/code-search'
7+
import {
8+
handleComposioGetToolSchemas,
9+
handleComposioManageConnections,
10+
handleComposioMultiExecute,
11+
handleComposioSearchTools,
12+
} from './tool/composio'
713
import { handleCreatePlan } from './tool/create-plan'
814
import { handleEndTurn } from './tool/end-turn'
915
import { handleFindFiles } from './tool/find-files'
@@ -53,6 +59,10 @@ export const codebuffToolHandlers = {
5359
ask_user: handleAskUser,
5460
browser_logs: handleBrowserLogs,
5561
code_search: handleCodeSearch,
62+
COMPOSIO_MANAGE_CONNECTIONS: handleComposioManageConnections,
63+
COMPOSIO_MULTI_EXECUTE_TOOL: handleComposioMultiExecute,
64+
COMPOSIO_SEARCH_TOOLS: handleComposioSearchTools,
65+
COMPOSIO_GET_TOOL_SCHEMAS: handleComposioGetToolSchemas,
5666
create_plan: handleCreatePlan,
5767
end_turn: handleEndTurn,
5868
find_files: handleFindFiles,
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import type { ComposioMetaToolName } from '@codebuff/common/constants/composio'
2+
import type { CodebuffToolOutput } from '@codebuff/common/tools/list'
3+
import type { CodebuffToolHandlerFunction } from '../handler-function-type'
4+
5+
function makeComposioHandler<T extends ComposioMetaToolName>() {
6+
return (async ({ toolCall, requestClientToolCall }) => {
7+
if (!requestClientToolCall) {
8+
return {
9+
output: [
10+
{
11+
type: 'json',
12+
value: {
13+
errorMessage: 'Composio tools are not available in this runtime.',
14+
},
15+
},
16+
],
17+
}
18+
}
19+
20+
return {
21+
output: (await (requestClientToolCall as any)(
22+
toolCall,
23+
)) as CodebuffToolOutput<T>,
24+
}
25+
}) satisfies CodebuffToolHandlerFunction<T>
26+
}
27+
28+
export const handleComposioManageConnections =
29+
makeComposioHandler<'COMPOSIO_MANAGE_CONNECTIONS'>()
30+
export const handleComposioMultiExecute =
31+
makeComposioHandler<'COMPOSIO_MULTI_EXECUTE_TOOL'>()
32+
export const handleComposioSearchTools =
33+
makeComposioHandler<'COMPOSIO_SEARCH_TOOLS'>()
34+
export const handleComposioGetToolSchemas =
35+
makeComposioHandler<'COMPOSIO_GET_TOOL_SCHEMAS'>()

sdk/src/__tests__/composio.test.ts

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,28 @@
11
import { afterEach, describe, expect, mock, test } from 'bun:test'
22

33
import { COMPOSIO_META_TOOL_NAMES } from '@codebuff/common/constants/composio'
4+
import { clientToolNames, toolParams } from '@codebuff/common/tools/list'
45

5-
import { getComposioMetaToolDefinitions } from '../composio'
6+
import {
7+
executeComposioToolViaServer,
8+
normalizeComposioInput,
9+
} from '../composio'
610

7-
describe('getComposioMetaToolDefinitions', () => {
11+
describe('Composio SDK tools', () => {
812
const originalFetch = globalThis.fetch
913

1014
afterEach(() => {
1115
globalThis.fetch = originalFetch
1216
})
1317

14-
test('returns static Composio meta tool definitions without discovery fetch', () => {
18+
test('registers Composio meta tools as static client tools without discovery fetch', () => {
1519
const fetchMock = mock(async () => new Response('{}'))
1620
globalThis.fetch = fetchMock as unknown as typeof fetch
1721

18-
const tools = getComposioMetaToolDefinitions({
19-
apiKey: 'codebuff-api-key',
20-
})
21-
22-
expect(tools.map((tool) => tool.toolName)).toEqual([
23-
...COMPOSIO_META_TOOL_NAMES,
24-
])
22+
for (const toolName of COMPOSIO_META_TOOL_NAMES) {
23+
expect(clientToolNames).toContain(toolName)
24+
expect(toolParams[toolName].inputSchema).toBeDefined()
25+
}
2526
expect(fetchMock).not.toHaveBeenCalled()
2627
})
2728

@@ -50,16 +51,20 @@ describe('getComposioMetaToolDefinitions', () => {
5051
)
5152
globalThis.fetch = fetchMock as unknown as typeof fetch
5253

53-
const searchTool = getComposioMetaToolDefinitions({
54+
const output = await executeComposioToolViaServer({
5455
apiKey: 'codebuff-api-key',
55-
}).find((tool) => tool.toolName === 'COMPOSIO_SEARCH_TOOLS')
56-
57-
const output = await searchTool?.execute({
58-
queries: ['find gmail tools'],
59-
session: { generate_id: true },
56+
toolName: 'COMPOSIO_SEARCH_TOOLS',
57+
input: {
58+
queries: ['find gmail tools'],
59+
session: { generate_id: true },
60+
},
6061
})
6162

6263
expect(output).toEqual([{ type: 'json', value: { ok: true } }])
6364
expect(fetchMock).toHaveBeenCalledTimes(1)
6465
})
66+
67+
test('normalizes non-object Composio inputs for server execution', () => {
68+
expect(normalizeComposioInput('gmail')).toEqual({ value: 'gmail' })
69+
})
6570
})

0 commit comments

Comments
 (0)