Address PR review: consolidate types, refactor CopilotAgent, sort toolkits

- Move ZSearchResultTool and ZNormalizedToolResult into composio/types.ts
- Convert CopilotAgent from static const to async buildCopilotAgent()
- Simplify loadAgent to delegate to buildCopilotAgent()
- Sort CURATED_TOOLKITS alphabetically by slug
- Remove inline type annotations in composio-handler, use inferred types
- Bump search limit from 15→50 for unscoped queries
- Add docstrings explaining inferToolkitSlug fallback behavior
- Add IPC schema reference comment for composio channels

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
tusharmagar 2026-04-03 11:04:26 +05:30
parent ad2c9f232c
commit a2c92c7491
7 changed files with 97 additions and 104 deletions

View file

@ -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<ReturnType<typeof composioClient.listToolkits>>['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,
};
}

View file

@ -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<z.infer<typeof Agent>> {
if (id === "copilot" || id === "rowboatx") {
// Rebuild tools from current BuiltinTools (includes Composio meta-tools).
const tools: Record<string, z.infer<typeof ToolAttachment>> = {};
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') {

View file

@ -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<string, z.infer<typeof ToolAttachment>> = {};
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<z.infer<typeof Agent>> {
const tools: Record<string, z.infer<typeof ToolAttachment>> = {};
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<typeof Agent> = {
name: "rowboatx",
description: "Rowboatx copilot",
instructions: CopilotInstructions,
tools,
}

View file

@ -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<string, unknown>; required?: string[] } }> }> {
): Promise<{ items: NormalizedToolResult[] }> {
const params: Record<string, string> = {
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] : ''),

View file

@ -231,3 +231,41 @@ export const ZLocalConnectedAccount = z.object({
export type LocalConnectedAccount = z.infer<typeof ZLocalConnectedAccount>;
export type ConnectedAccountStatus = z.infer<typeof ZConnectedAccountStatus>;
/**
* 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<typeof ZNormalizedToolResult>;

View file

@ -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' },
];

View file

@ -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({