diff --git a/apps/x/apps/main/src/composio-handler.ts b/apps/x/apps/main/src/composio-handler.ts index f4692916..3289c9c3 100644 --- a/apps/x/apps/main/src/composio-handler.ts +++ b/apps/x/apps/main/src/composio-handler.ts @@ -307,20 +307,10 @@ export async function useComposioForGoogleCalendar(): Promise<{ enabled: boolean } /** - * List available Composio toolkits — filtered to curated list only + * List available Composio toolkits — filtered to curated list only. + * Return type matches the ZToolkit schema from core/composio/types.ts. */ -export async function listToolkits(): Promise<{ - items: Array<{ - slug: string; - name: string; - meta: { description: string; logo: string; tools_count: number; triggers_count: number }; - no_auth?: boolean; - auth_schemes?: string[]; - composio_managed_auth_schemes?: string[]; - }>; - nextCursor: string | null; - totalItems: number; -}> { +export async function listToolkits() { // Paginate through all API pages to collect every curated toolkit type ToolkitItem = Awaited>['items'][number]; const allItems: ToolkitItem[] = []; @@ -335,7 +325,7 @@ export async function listToolkits(): Promise<{ const filtered = allItems.filter(item => CURATED_TOOLKIT_SLUGS.has(item.slug)); return { items: filtered, - nextCursor: null, + nextCursor: null as string | null, totalItems: filtered.length, }; } diff --git a/apps/x/packages/core/src/agents/runtime.ts b/apps/x/packages/core/src/agents/runtime.ts index 7d6006a7..34e2b401 100644 --- a/apps/x/packages/core/src/agents/runtime.ts +++ b/apps/x/packages/core/src/agents/runtime.ts @@ -10,8 +10,7 @@ import { LlmStepStreamEvent } from "@x/shared/dist/llm-step-events.js"; import { execTool } from "../application/lib/exec-tool.js"; import { AskHumanRequestEvent, RunEvent, ToolPermissionRequestEvent } from "@x/shared/dist/runs.js"; import { BuiltinTools } from "../application/lib/builtin-tools.js"; -import { CopilotAgent } from "../application/assistant/agent.js"; -import { buildCopilotInstructions } from "../application/assistant/instructions.js"; +import { buildCopilotAgent } from "../application/assistant/agent.js"; import { isBlocked, extractCommandNames } from "../application/lib/command-executor.js"; import container from "../di/container.js"; import { IModelConfigRepo } from "../models/repo.js"; @@ -370,14 +369,7 @@ function formatLlmStreamError(rawError: unknown): string { export async function loadAgent(id: string): Promise> { if (id === "copilot" || id === "rowboatx") { - // Rebuild tools from current BuiltinTools (includes Composio meta-tools). - const tools: Record> = {}; - for (const name of Object.keys(BuiltinTools)) { - tools[name] = { type: "builtin", name }; - } - // Rebuild instructions to include current Composio tools section - const instructions = await buildCopilotInstructions(); - return { ...CopilotAgent, tools, instructions }; + return buildCopilotAgent(); } if (id === 'note_creation') { diff --git a/apps/x/packages/core/src/application/assistant/agent.ts b/apps/x/packages/core/src/application/assistant/agent.ts index 4904642e..0096601c 100644 --- a/apps/x/packages/core/src/application/assistant/agent.ts +++ b/apps/x/packages/core/src/application/assistant/agent.ts @@ -1,19 +1,23 @@ import { Agent, ToolAttachment } from "@x/shared/dist/agent.js"; import z from "zod"; -import { CopilotInstructions } from "./instructions.js"; +import { buildCopilotInstructions } from "./instructions.js"; import { BuiltinTools } from "../lib/builtin-tools.js"; -const tools: Record> = {}; -for (const name of Object.keys(BuiltinTools)) { - tools[name] = { - type: "builtin", - name, +/** + * Build the CopilotAgent dynamically. + * Tools are derived from the current BuiltinTools (which include Composio meta-tools), + * and instructions include the live Composio connection status. + */ +export async function buildCopilotAgent(): Promise> { + const tools: Record> = {}; + for (const name of Object.keys(BuiltinTools)) { + tools[name] = { type: "builtin", name }; + } + const instructions = await buildCopilotInstructions(); + return { + name: "rowboatx", + description: "Rowboatx copilot", + instructions, + tools, }; } - -export const CopilotAgent: z.infer = { - name: "rowboatx", - description: "Rowboatx copilot", - instructions: CopilotInstructions, - tools, -} \ No newline at end of file diff --git a/apps/x/packages/core/src/composio/client.ts b/apps/x/packages/core/src/composio/client.ts index e16573d6..722cb4a4 100644 --- a/apps/x/packages/core/src/composio/client.ts +++ b/apps/x/packages/core/src/composio/client.ts @@ -14,8 +14,10 @@ import { ZExecuteActionRequest, ZExecuteActionResponse, ZListResponse, + ZSearchResultTool, ZTool, ZToolkit, + type NormalizedToolResult, } from "./types.js"; import { isSignedIn } from "../account/account.js"; import { getAccessToken } from "../auth/tokens.js"; @@ -283,52 +285,34 @@ export async function deleteConnectedAccount(connectedAccountId: string): Promis } /** - * Schema for search results: includes toolkit info and full input_parameters. - */ -const ZSearchResultTool = z.object({ - slug: z.string(), - name: z.string(), - description: z.string(), - toolkit: z.object({ - slug: z.string(), - name: z.string(), - logo: z.string(), - }).optional(), - input_parameters: z.object({ - type: z.literal('object').optional().default('object'), - properties: z.record(z.string(), z.unknown()).optional().default({}), - required: z.array(z.string()).optional(), - }).optional().default({ type: 'object', properties: {} }), -}).passthrough(); - -/** - * Infer toolkit slug from a tool slug. - * Tool naming convention: TOOLKIT_ACTION (e.g., GITHUB_CREATE_ISSUE → github). - * For multi-word toolkit slugs (GOOGLECALENDAR_CREATE_EVENT), the prefix before - * the first underscore matches the toolkit slug. + * Infer toolkit slug from a tool slug when the API omits the toolkit object. + * Convention: TOOLKIT_ACTION (e.g., GITHUB_CREATE_ISSUE → github). + * + * This fallback exists because the Composio /tools search endpoint occasionally + * returns results without the `toolkit` field — observed with certain search + * queries and when the tool is from a less-common integration. The inference is + * a best-effort heuristic: lowercase the first segment before the first underscore. */ function inferToolkitSlug(toolSlug: string): string { - // Tool slugs are uppercase: GITHUB_CREATE_ISSUE, GMAIL_SEND_EMAIL, etc. - // The toolkit prefix is everything before the first action word. - // Strategy: lowercase the slug and try progressively shorter prefixes. const lower = toolSlug.toLowerCase(); const parts = lower.split('_'); - // Try joining from 1 part up to all-but-1 to find a known prefix - // Most common: single-word prefix (github, gmail, slack) return parts[0] ?? lower; } /** * Search for tools across all toolkits (or optionally filtered by specific toolkit slugs). * Returns tools with full input_parameters so the agent knows what params to pass. + * + * Uses a limit of 50 (not 15) to avoid the curated-filter-after-limit problem where + * in-scope results at position 16+ would be discarded if earlier results are out-of-scope. */ export async function searchTools( searchQuery: string, toolkitSlugs?: string[], -): Promise<{ items: Array<{ slug: string; name: string; description: string; toolkitSlug: string; inputParameters: { type: 'object'; properties: Record; required?: string[] } }> }> { +): Promise<{ items: NormalizedToolResult[] }> { const params: Record = { search: searchQuery, - limit: '15', + limit: '50', }; if (toolkitSlugs && toolkitSlugs.length === 1) { params.toolkit_slug = toolkitSlugs[0]; @@ -336,12 +320,10 @@ export async function searchTools( const result = await composioApiCall(ZListResponse(ZSearchResultTool), "/tools", params); - const items = result.items.map((item) => ({ + const items: NormalizedToolResult[] = result.items.map((item) => ({ slug: item.slug, name: item.name, description: item.description, - // Use toolkit.slug from API response, fall back to inferring from tool slug, - // and finally fall back to the requested toolkit slug if one was provided. toolkitSlug: item.toolkit?.slug || inferToolkitSlug(item.slug) || (toolkitSlugs?.length === 1 ? toolkitSlugs[0] : ''), diff --git a/apps/x/packages/core/src/composio/types.ts b/apps/x/packages/core/src/composio/types.ts index 968c9abd..5585d63f 100644 --- a/apps/x/packages/core/src/composio/types.ts +++ b/apps/x/packages/core/src/composio/types.ts @@ -231,3 +231,41 @@ export const ZLocalConnectedAccount = z.object({ export type LocalConnectedAccount = z.infer; export type ConnectedAccountStatus = z.infer; + +/** + * Tool schema for search results. + * Unlike ZTool, `toolkit` is optional because the Composio /tools search endpoint + * sometimes omits the toolkit object from results. `input_parameters` uses + * lenient defaults so tools with no params (e.g. LINKEDIN_GET_MY_INFO) parse cleanly. + */ +export const ZSearchResultTool = z.object({ + slug: z.string(), + name: z.string(), + description: z.string(), + toolkit: z.object({ + slug: z.string(), + name: z.string(), + logo: z.string(), + }).optional(), + input_parameters: z.object({ + type: z.literal('object').optional().default('object'), + properties: z.record(z.string(), z.unknown()).optional().default({}), + required: z.array(z.string()).optional(), + }).optional().default({ type: 'object', properties: {} }), +}).passthrough(); + +/** + * Normalized tool result returned from searchTools(). + */ +export const ZNormalizedToolResult = z.object({ + slug: z.string(), + name: z.string(), + description: z.string(), + toolkitSlug: z.string(), + inputParameters: z.object({ + type: z.literal('object'), + properties: z.record(z.string(), z.unknown()), + required: z.array(z.string()).optional(), + }), +}); +export type NormalizedToolResult = z.infer; diff --git a/apps/x/packages/shared/src/composio.ts b/apps/x/packages/shared/src/composio.ts index e752678a..c904c9a3 100644 --- a/apps/x/packages/shared/src/composio.ts +++ b/apps/x/packages/shared/src/composio.ts @@ -1,6 +1,7 @@ /** * Curated Composio toolkits available to Rowboat users. * Single source of truth for slugs, display names, and categories. + * Sorted by slug (ASC) for maintainability. */ export type ToolkitCategory = 'communication' | 'productivity' | 'development' | 'crm' | 'social' | 'storage' | 'support'; @@ -12,46 +13,31 @@ export interface CuratedToolkit { } export const CURATED_TOOLKITS: CuratedToolkit[] = [ - // Communication + { slug: 'airtable', displayName: 'Airtable', category: 'productivity' }, + { slug: 'asana', displayName: 'Asana', category: 'productivity' }, + { slug: 'cal', displayName: 'Cal.com', category: 'productivity' }, + { slug: 'calendly', displayName: 'Calendly', category: 'productivity' }, + { slug: 'dropbox', displayName: 'Dropbox', category: 'storage' }, + { slug: 'github', displayName: 'GitHub', category: 'development' }, { slug: 'gmail', displayName: 'Gmail', category: 'communication' }, - { slug: 'slack', displayName: 'Slack', category: 'communication' }, - { slug: 'microsoft_outlook', displayName: 'Microsoft Outlook', category: 'communication' }, - { slug: 'microsoft_teams', displayName: 'Microsoft Teams', category: 'communication' }, - - // Productivity { slug: 'googlecalendar', displayName: 'Google Calendar', category: 'productivity' }, { slug: 'googledocs', displayName: 'Google Docs', category: 'productivity' }, - { slug: 'googlesheets', displayName: 'Google Sheets', category: 'productivity' }, - { slug: 'notion', displayName: 'Notion', category: 'productivity' }, - { slug: 'airtable', displayName: 'Airtable', category: 'productivity' }, - { slug: 'calendly', displayName: 'Calendly', category: 'productivity' }, - { slug: 'cal', displayName: 'Cal.com', category: 'productivity' }, - - // Storage { slug: 'googledrive', displayName: 'Google Drive', category: 'storage' }, - { slug: 'dropbox', displayName: 'Dropbox', category: 'storage' }, - { slug: 'onedrive', displayName: 'OneDrive', category: 'storage' }, - - // Development - { slug: 'github', displayName: 'GitHub', category: 'development' }, - { slug: 'linear', displayName: 'Linear', category: 'development' }, - { slug: 'jira', displayName: 'Jira', category: 'development' }, - - // Project Management - { slug: 'asana', displayName: 'Asana', category: 'productivity' }, - { slug: 'trello', displayName: 'Trello', category: 'productivity' }, - - // CRM & Sales + { slug: 'googlesheets', displayName: 'Google Sheets', category: 'productivity' }, { slug: 'hubspot', displayName: 'HubSpot', category: 'crm' }, - { slug: 'salesforce', displayName: 'Salesforce', category: 'crm' }, - - // Social - { slug: 'linkedin', displayName: 'LinkedIn', category: 'social' }, - { slug: 'twitter', displayName: 'X', category: 'social' }, - { slug: 'reddit', displayName: 'Reddit', category: 'social' }, - - // Support { slug: 'intercom', displayName: 'Intercom', category: 'support' }, + { slug: 'jira', displayName: 'Jira', category: 'development' }, + { slug: 'linear', displayName: 'Linear', category: 'development' }, + { slug: 'linkedin', displayName: 'LinkedIn', category: 'social' }, + { slug: 'microsoft_outlook', displayName: 'Microsoft Outlook', category: 'communication' }, + { slug: 'microsoft_teams', displayName: 'Microsoft Teams', category: 'communication' }, + { slug: 'notion', displayName: 'Notion', category: 'productivity' }, + { slug: 'onedrive', displayName: 'OneDrive', category: 'storage' }, + { slug: 'reddit', displayName: 'Reddit', category: 'social' }, + { slug: 'salesforce', displayName: 'Salesforce', category: 'crm' }, + { slug: 'slack', displayName: 'Slack', category: 'communication' }, + { slug: 'trello', displayName: 'Trello', category: 'productivity' }, + { slug: 'twitter', displayName: 'X', category: 'social' }, { slug: 'zendesk', displayName: 'Zendesk', category: 'support' }, ]; diff --git a/apps/x/packages/shared/src/ipc.ts b/apps/x/packages/shared/src/ipc.ts index 465ef09f..fae87f7f 100644 --- a/apps/x/packages/shared/src/ipc.ts +++ b/apps/x/packages/shared/src/ipc.ts @@ -398,6 +398,7 @@ const ipcSchemas = { res: z.null(), }, // Composio Tools Library channels + // Response schema mirrors core/composio/types.ts ZToolkit (kept inline to avoid cross-package import) 'composio:list-toolkits': { req: z.object({}), res: z.object({