diff --git a/apps/gateway-worker/README.md b/apps/gateway-worker/README.md index 3a8f7e14..fa5c3c18 100644 --- a/apps/gateway-worker/README.md +++ b/apps/gateway-worker/README.md @@ -92,6 +92,8 @@ OAuth link. Composio v3.1 REST pages and catalog/tool payloads are byte-bounded before parsing, then schema- and cardinality-bounded so a provider pagination fault cannot grow Worker memory without limit. +The lightweight `/v1/composer/skills` catalog reads active connected-app slugs from the reconciled +database state alongside custom skills; opening `@` never waits on a provider catalog or account sync. Catalog and connected-account provider snapshots may load in parallel, but DB reconciliation begins only after both external reads settle. Connect creates the provider link first and compensates by deleting it if response validation diff --git a/apps/gateway-worker/src/core-http-routes.ts b/apps/gateway-worker/src/core-http-routes.ts index c07de040..e1bf0627 100644 --- a/apps/gateway-worker/src/core-http-routes.ts +++ b/apps/gateway-worker/src/core-http-routes.ts @@ -6,7 +6,7 @@ import { authenticate } from "./authenticate"; import { type GatewayApp, type GatewayContext, requestDatabase } from "./gateway-env"; import { rateLimit, rateLimitPublic, withRateLimitHeaders } from "./rate-limit"; import { readDownstreamReleaseHealth } from "./release-health"; -import { listUserSkillsRoute } from "./skills-routes"; +import { listComposerSkillsRoute, listUserSkillsRoute } from "./skills-routes"; import { clientErrorRoute, clientUserEventRoute, vitalsRoute } from "./telemetry-routes"; export function registerCoreHttpRoutes(app: GatewayApp): void { @@ -82,6 +82,11 @@ function registerOutputRoute(app: GatewayApp): void { } function registerSkillRoutes(app: GatewayApp): void { + app.get("/v1/composer/skills", async (c) => { + const userId = await authenticate(c); + await rateLimit(c, userId); + return listComposerSkillsRoute(requestDatabase(c), userId); + }); app.get("/v1/skills", async (c) => { const userId = await authenticate(c); await rateLimit(c, userId); diff --git a/apps/gateway-worker/src/skills-routes.ts b/apps/gateway-worker/src/skills-routes.ts index a7259287..b0e5973b 100644 --- a/apps/gateway-worker/src/skills-routes.ts +++ b/apps/gateway-worker/src/skills-routes.ts @@ -1,11 +1,18 @@ import { type DatabaseHandle, + listActiveUserIntegrationNames, listUserSkillSummaries, type UserSkillSummary, withUserDb, } from "@cheatcode/db"; -import type { UserId } from "@cheatcode/types"; -import { MAX_USER_SKILLS, UserSkillSchema, UserSkillsResponseSchema } from "@cheatcode/types/api"; +import { IntegrationNameSchema, integrationDisplayName, type UserId } from "@cheatcode/types"; +import { + ComposerSkillsResponseSchema, + MAX_CONNECTED_APP_SKILLS, + MAX_USER_SKILLS, + UserSkillSchema, + UserSkillsResponseSchema, +} from "@cheatcode/types/api"; function skillSummary(record: UserSkillSummary): unknown { return UserSkillSchema.parse({ @@ -29,3 +36,32 @@ export async function listUserSkillsRoute( return Response.json(UserSkillsResponseSchema.parse({ skills: rows.map(skillSummary) })); }); } + +/** `GET /v1/composer/skills` — custom skills plus active connected-app capabilities. */ +export async function listComposerSkillsRoute( + database: DatabaseHandle, + userId: UserId, +): Promise { + return withUserDb(database, userId, async ({ transaction }) => { + const catalog = await transaction(async (tx) => { + const skillRows = await listUserSkillSummaries(tx, userId, MAX_USER_SKILLS); + const integrationNames = await listActiveUserIntegrationNames( + tx, + userId, + MAX_CONNECTED_APP_SKILLS, + ); + return { + connectedApps: integrationNames.flatMap(connectedAppSummary), + skills: skillRows.map(skillSummary), + }; + }); + return Response.json(ComposerSkillsResponseSchema.parse(catalog)); + }); +} + +function connectedAppSummary(name: string): Array<{ displayName: string; name: string }> { + const parsed = IntegrationNameSchema.safeParse(name); + return parsed.success + ? [{ displayName: integrationDisplayName(parsed.data), name: parsed.data }] + : []; +} diff --git a/apps/web/README.md b/apps/web/README.md index 050a4e37..13123ff4 100644 --- a/apps/web/README.md +++ b/apps/web/README.md @@ -37,7 +37,8 @@ as raw bounded requests, show per-batch progress and actionable failures, and be a compact `/uploads/...` reference after each successful save. `/` is exclusively the persistent project-file browser and merges durable uploads with generated Deliverables. A selected Deliverable inserts its stable `/deliverables//` project reference; -`@` is exclusively the user-skill picker. The file browser reads durable project-file metadata and +`@` is exclusively the skill picker: it merges custom skills with active connected apps from the +lightweight database-backed skill catalog. The file browser reads durable project-file metadata and does not create or wake Daytona merely because the user opens it. Computer preview wakeups run only while Browser is selected; opening Files never revives an unrelated dev server or changes the selected surface. Browser wakeups rotate the preview session diff --git a/apps/web/src/components/chat/chat-panel-controller.ts b/apps/web/src/components/chat/chat-panel-controller.ts index c420695a..1927a14b 100644 --- a/apps/web/src/components/chat/chat-panel-controller.ts +++ b/apps/web/src/components/chat/chat-panel-controller.ts @@ -33,7 +33,7 @@ import type { OlderMessagesLoadResult } from "@/components/chat/use-message-list import { agentModelRequestValue } from "@/lib/agent-models"; import { cancelRun, getThread } from "@/lib/api/project-thread"; import { invalidateChatLists, projectKeys, threadKeys } from "@/lib/api/query-keys"; -import { USER_SKILLS_QUERY } from "@/lib/api/skills"; +import { COMPOSER_SKILLS_QUERY, USER_SKILLS_QUERY } from "@/lib/api/skills"; import { useAppStore } from "@/lib/store/app-store"; import { rememberStreamSeq, streamResumeCursor } from "@/lib/stream/stream-seq"; @@ -402,6 +402,7 @@ function handleSkillCreatedData( ): void { const parsed = CHEATCODE_DATA_SCHEMAS["skill-created"].safeParse(data); if (parsed.success) { + void queryClient.invalidateQueries({ queryKey: COMPOSER_SKILLS_QUERY }); void queryClient.invalidateQueries({ queryKey: USER_SKILLS_QUERY }); actions.setActiveComputerTab("files"); actions.setPreviewPanelOpen(true); diff --git a/apps/web/src/components/composer/composer-context-chips.tsx b/apps/web/src/components/composer/composer-context-chips.tsx index cbac8097..d2834d86 100644 --- a/apps/web/src/components/composer/composer-context-chips.tsx +++ b/apps/web/src/components/composer/composer-context-chips.tsx @@ -1,30 +1,10 @@ "use client"; -import type { IntegrationName } from "@cheatcode/types"; +import { type IntegrationName, integrationDisplayName } from "@cheatcode/types"; import { Link as LinkIcon, X } from "@/components/ui"; import { CheatcodeMark } from "@/components/ui/cheatcode-mark"; import { cn } from "@/lib/ui/cn"; -// Curated display names for the most common toolkits. Any other connected toolkit -// slug falls back to a prettified label via toolLabel(). -const TOOL_LABELS: Record = { - github: "GitHub", - gmail: "Gmail", - linear: "Linear", - notion: "Notion", - slack: "Slack", -}; - -function toolLabel(slug: string): string { - return ( - TOOL_LABELS[slug] ?? - slug - .split("_") - .map((word) => (word ? word.charAt(0).toUpperCase() + word.slice(1) : word)) - .join(" ") - ); -} - export function ComposerContextChips({ className, onClearSkill, @@ -49,7 +29,7 @@ export function ComposerContextChips({ ) : null} {tool ? ( listUserSkills(options.getToken, signal), - queryKey: USER_SKILLS_QUERY, + const skillsCatalog = useQuery({ + queryFn: ({ signal }) => fetchComposerSkills(options.getToken, signal), + queryKey: COMPOSER_SKILLS_QUERY, staleTime: 60_000, }); const triggers = useComposerTriggers({ @@ -67,7 +67,7 @@ export function useComposerMenu(options: UseComposerMenuOptions): ComposerMenuCo const items = triggers.kind === "slash" ? fileItems - : skillMenuItems(triggers.query, userSkills.data ?? [], userSkills.isPending); + : skillMenuItems(triggers.query, skillsCatalog.data, skillsCatalog.isPending); return { ariaLabel: triggers.kind === "slash" ? "Project files" : "Skills", handleKeyDown: (event) => triggers.handleMenuKeyDown(event, items), @@ -91,10 +91,10 @@ function selectComposerItem( function skillMenuItems( query: string, - userSkills: Parameters[1], + catalog: Awaited> | undefined, isPending: boolean, ): ComposerMenuItem[] { - const items = slashSkillItems(query, userSkills); + const items = mentionSkillItems(query, catalog?.skills, catalog?.connectedApps); if (items.length > 0) { return items; } diff --git a/apps/web/src/lib/api/skills.ts b/apps/web/src/lib/api/skills.ts index d7f8c5f0..d68d780c 100644 --- a/apps/web/src/lib/api/skills.ts +++ b/apps/web/src/lib/api/skills.ts @@ -1,6 +1,8 @@ "use client"; import { + type ComposerSkillsResponse, + ComposerSkillsResponseSchema, type SandboxIdeSession, SandboxIdeSessionSchema, type UserSkill, @@ -13,8 +15,20 @@ import { readBoundedJsonResponse, } from "@/lib/api/authorized-fetch"; +export const COMPOSER_SKILLS_QUERY = ["composer-skills"] as const; export const USER_SKILLS_QUERY = ["user-skills"] as const; +/** Custom skills and active connected apps available to the composer. */ +export async function fetchComposerSkills( + getToken: () => Promise, + signal?: AbortSignal, +): Promise { + const response = await authorizedFetch(getToken, "/v1/composer/skills", signal ? { signal } : {}); + return ComposerSkillsResponseSchema.parse( + await readBoundedJsonResponse(response, API_RESPONSE_LIMIT_BYTES.metadata), + ); +} + /** The caller's custom skills (body-less summaries). */ export async function listUserSkills( getToken: () => Promise, diff --git a/packages/agent-core/README.md b/packages/agent-core/README.md index c735eb3f..b2162cc9 100644 --- a/packages/agent-core/README.md +++ b/packages/agent-core/README.md @@ -179,5 +179,10 @@ Composio REST tool discovery and execution responses are byte-bounded before parsing, then projected into bounded, valid JSON before entering model context. Toolkit names use the shared open-slug contract from `@cheatcode/types/integrations` across API, context, and tool boundaries. +Discovery first uses Composio's full-text query. Because that query can return zero for an +over-specific natural-language phrase even when the toolkit has the required action, a zero-result +search performs one broad toolkit fetch and deterministically ranks bounded candidates by the +action, object, name, slug, and description terms. The agent still executes only an exact returned +slug and concrete toolkit version. Callers must honor the returned truncation flag and narrow tool discovery with `search` when a schema does not fit. diff --git a/packages/agent-core/src/mastra/tool-defs/composio-tool.ts b/packages/agent-core/src/mastra/tool-defs/composio-tool.ts index 1eb4b729..66a52c21 100644 --- a/packages/agent-core/src/mastra/tool-defs/composio-tool.ts +++ b/packages/agent-core/src/mastra/tool-defs/composio-tool.ts @@ -12,6 +12,7 @@ const MAX_COMPOSIO_TOOL_PARAMETERS_CHARS = 10_000; const MAX_COMPOSIO_OUTPUT_NODES = 2_000; const MAX_COMPOSIO_OUTPUT_STRING_CHARS = 40_000; const MAX_COMPOSIO_OUTPUT_DEPTH = 6; +const COMPOSIO_RELAXED_MATCH_LIMIT = 12; // Composio's tool-list API silently returns only its small default page (~10) at the // base toolkit version; request the documented max so large toolkits (github/gmail/ // notion) are not under-enumerated. Docs: docs.composio.dev/docs/tools-direct/fetching-tools. @@ -54,6 +55,7 @@ const ComposioListToolsInputSchema = z.strictObject({ const ComposioListToolsOutputSchema = z.strictObject({ error: z.string().max(1_000).nullable(), integration: IntegrationNameSchema, + searchRelaxed: z.boolean(), success: z.boolean(), toolCount: z.number().int().nonnegative(), toolsJson: z.string().max(MAX_COMPOSIO_OUTPUT_CHARS), @@ -125,6 +127,7 @@ const ComposioRawToolSchema = z.object({ const ComposioRawToolListSchema = z.array(ComposioRawToolSchema).max(COMPOSIO_LIST_LIMIT); +type ComposioRawTool = z.infer; type ComposioListToolsInput = z.infer; type ComposioExecuteInput = z.infer; type ComposioExecuteOutput = z.infer; @@ -156,6 +159,12 @@ interface ComposioExecutionTarget { version: string; } +interface ComposioToolDiscovery { + hasMore: boolean; + searchRelaxed: boolean; + tools: ComposioRawTool[]; +} + function requestContextFromToolContext(context: unknown): { get(key: string): unknown } { return RequestContextReaderSchema.parse( typeof context === "object" && context !== null @@ -199,26 +208,16 @@ async function listComposioTools( } try { - const page = await new ComposioClient(runtime.apiKey).listTools( - { - limit: COMPOSIO_LIST_LIMIT, - ...(input.search ? { search: input.search } : {}), - toolkit: input.integration, - }, - COMPOSIO_LIST_TIMEOUT_MS, - ); - const parsed = ComposioRawToolListSchema.safeParse(page.items); - if (!parsed.success) { - return composioListFailure(input, "Composio returned an unexpected tool list shape."); - } - const bounded = boundedToolListJson(parsed.data, MAX_COMPOSIO_OUTPUT_CHARS); + const discovery = await discoverComposioTools(new ComposioClient(runtime.apiKey), input); + const bounded = boundedToolListJson(discovery.tools, MAX_COMPOSIO_OUTPUT_CHARS); return ComposioListToolsOutputSchema.parse({ error: null, integration: input.integration, + searchRelaxed: discovery.searchRelaxed, success: true, - toolCount: parsed.data.length, + toolCount: discovery.tools.length, toolsJson: bounded.text, - toolsTruncated: bounded.truncated || page.nextCursor !== null, + toolsTruncated: bounded.truncated || discovery.hasMore, }); } catch (error) { createLogger().warn("composio_tool_list_failed", { error }); @@ -226,6 +225,75 @@ async function listComposioTools( } } +async function discoverComposioTools( + client: ComposioClient, + input: ComposioListToolsInput, +): Promise { + const page = await client.listTools( + { + limit: COMPOSIO_LIST_LIMIT, + ...(input.search ? { search: input.search } : {}), + toolkit: input.integration, + }, + COMPOSIO_LIST_TIMEOUT_MS, + ); + const tools = ComposioRawToolListSchema.parse(page.items); + if (tools.length > 0 || !input.search) { + return { hasMore: page.nextCursor !== null, searchRelaxed: false, tools }; + } + const broadPage = await client.listTools( + { limit: COMPOSIO_LIST_LIMIT, toolkit: input.integration }, + COMPOSIO_LIST_TIMEOUT_MS, + ); + const broadTools = ComposioRawToolListSchema.parse(broadPage.items); + const ranked = rankComposioTools(broadTools, input.search); + return { + hasMore: ranked.length < broadTools.length || broadPage.nextCursor !== null, + searchRelaxed: true, + tools: ranked, + }; +} + +function rankComposioTools(tools: readonly ComposioRawTool[], search: string): ComposioRawTool[] { + const terms = searchTerms(search); + const scored = tools + .filter((tool) => tool.isDeprecated !== true) + .map((tool, index) => ({ index, score: toolSearchScore(tool, terms), tool })) + .filter((entry) => entry.score > 0) + .sort((left, right) => right.score - left.score || left.index - right.index) + .slice(0, COMPOSIO_RELAXED_MATCH_LIMIT) + .map((entry) => entry.tool); + return scored.length > 0 + ? scored + : tools.filter((tool) => tool.isDeprecated !== true).slice(0, COMPOSIO_RELAXED_MATCH_LIMIT); +} + +function toolSearchScore(tool: ComposioRawTool, terms: ReadonlySet): number { + const slug = searchTerms(tool.slug); + const name = searchTerms(tool.name ?? ""); + const description = searchTerms(tool.description ?? ""); + let score = 0; + for (const term of terms) { + if (slug.has(term)) score += 12; + if (name.has(term)) score += 8; + if (description.has(term)) score += 2; + } + return score; +} + +function searchTerms(value: string): Set { + return new Set( + (value.toLowerCase().match(/[a-z0-9]+/gu) ?? []) + .map(normalizeSearchTerm) + .filter((term) => term.length > 1), + ); +} + +function normalizeSearchTerm(term: string): string { + if (term.length > 4 && term.endsWith("ies")) return `${term.slice(0, -3)}y`; + return term.length > 3 && term.endsWith("s") ? term.slice(0, -1) : term; +} + async function executeComposioAction( input: ComposioExecuteInput, runtime: ComposioRuntimeContext, @@ -332,6 +400,7 @@ function composioListFailure( return ComposioListToolsOutputSchema.parse({ error, integration: input.integration, + searchRelaxed: false, success: false, toolCount: 0, toolsJson: "[]", @@ -523,7 +592,7 @@ function isRecord(value: unknown): value is Record { export const mastraComposioListTools = createTool({ id: "composio_list_tools", description: - "List available Composio action tools for a user-connected integration before choosing an exact action slug. If toolsTruncated is true, call again with a `search` keyword to narrow to the action you need.", + "List available Composio action tools for a user-connected integration before choosing an exact action slug. Natural-language searches that are too strict are relaxed automatically; inspect searchRelaxed and the returned candidates. If toolsTruncated is true and no candidate fits, call again with a shorter action or object keyword.", inputSchema: ComposioListToolsInputSchema, outputSchema: ComposioListToolsOutputSchema, execute: async (input, context) => listComposioTools(input, composioRuntimeFromContext(context)), diff --git a/packages/db/README.md b/packages/db/README.md index 2c85c01c..c93a1c23 100644 --- a/packages/db/README.md +++ b/packages/db/README.md @@ -99,6 +99,7 @@ Public exports include: - project-scoped generated-output catalog and exact referenced-output reads - model-context suffix reads with logical-turn and byte bounds - BYOK and integration helpers +- a distinct active-integration projection for the composer capability catalog - entitlement and usage helpers - caller-configured user-skill list primitives plus locked count/insert/update composition - entitlement-read/project-lock composition for billing-owned lazy-materialization limits and diff --git a/packages/db/src/index.ts b/packages/db/src/index.ts index b6786cfd..2823c5e8 100644 --- a/packages/db/src/index.ts +++ b/packages/db/src/index.ts @@ -46,6 +46,7 @@ export { deleteUserIntegrationAccounts, expireComposioConnection, findUserIntegrationByConnectionId, + listActiveUserIntegrationNames, listAgentIntegrations, listUserIntegrations, setDefaultUserIntegration, diff --git a/packages/db/src/integrations.ts b/packages/db/src/integrations.ts index d02f12a8..e1fa224d 100644 --- a/packages/db/src/integrations.ts +++ b/packages/db/src/integrations.ts @@ -143,6 +143,26 @@ export async function listAgentIntegrations( ); } +/** Distinct active toolkit slugs for the lightweight composer capability catalog. */ +export async function listActiveUserIntegrationNames( + db: Database, + userId: UserId, + limit: number, +): Promise { + const rows = await db + .selectDistinct({ integration: userIntegrations.integration }) + .from(userIntegrations) + .where( + and( + eq(userIntegrations.userId, userId), + sql`lower(${userIntegrations.status}) in ('active', 'authorized', 'connected', 'enabled')`, + ), + ) + .orderBy(userIntegrations.integration) + .limit(limit); + return rows.map((row) => row.integration); +} + export async function findUserIntegrationByConnectionId( db: Database, input: { composioConnectionId: string; integration: string; userId: UserId }, diff --git a/packages/types/README.md b/packages/types/README.md index 5b637811..6e68c44d 100644 --- a/packages/types/README.md +++ b/packages/types/README.md @@ -16,6 +16,7 @@ capability discovery contracts, error codes, and UI message types. response URL validation without loading the general API contract barrel - `errors.ts`: locked error code catalog - `@cheatcode/types/integrations`: canonical open Composio toolkit-slug schema and constraints +- `/v1/skills` and `/v1/composer/skills` response contracts for custom skills and active connected-app entries - `@cheatcode/types/internal`: Worker-only Gateway-to-Agent route manifest, service-binding deletion contracts, and workspace/sandbox-transition evidence - `models.ts`: catalog IDs plus the open provider-prefixed logical-model schema diff --git a/packages/types/src/api.ts b/packages/types/src/api.ts index 52f54f64..4cf43c80 100644 --- a/packages/types/src/api.ts +++ b/packages/types/src/api.ts @@ -551,6 +551,9 @@ export const GreetingResponseSchema = z.strictObject({ /** Operational ceiling that keeps the per-user skill catalog bounded. */ export const MAX_USER_SKILLS = 100; +/** Operational ceiling for connected apps exposed in the composer skill catalog. */ +export const MAX_CONNECTED_APP_SKILLS = 100; + /** A user-created skill (client-safe projection; `body` only travels on detail/create). */ export const UserSkillSchema = z.strictObject({ category: z.string().max(80), @@ -562,10 +565,20 @@ export const UserSkillSchema = z.strictObject({ updatedAt: z.string().datetime(), }); +const ConnectedAppSkillSchema = z.strictObject({ + displayName: z.string().min(1).max(200), + name: IntegrationNameSchema, +}); + export const UserSkillsResponseSchema = z.strictObject({ skills: z.array(UserSkillSchema).max(MAX_USER_SKILLS), }); +export const ComposerSkillsResponseSchema = z.strictObject({ + connectedApps: z.array(ConnectedAppSkillSchema).max(MAX_CONNECTED_APP_SKILLS), + skills: z.array(UserSkillSchema).max(MAX_USER_SKILLS), +}); + export type SandboxHourPoint = z.infer; export type CreateRun = z.infer; export type CreateThread = z.infer; @@ -605,4 +618,6 @@ export type SandboxTerminalContext = z.infer; export type ActivityHistoryResponse = z.infer; export type ActivityRunPoint = z.infer; +export type ComposerSkillsResponse = z.infer; +export type ConnectedAppSkill = z.infer; export type UserSkill = z.infer; diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index eb1b353a..570f9278 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -35,7 +35,7 @@ export { ErrorCodeSchema, ErrorResponseSchema } from "./errors"; export type { AgentRunId, ProjectId, ThreadId, UserId } from "./ids"; export { toAgentRunId, toProjectId, toThreadId, toUserId } from "./ids"; export type { IntegrationName } from "./integrations"; -export { IntegrationNameSchema } from "./integrations"; +export { IntegrationNameSchema, integrationDisplayName } from "./integrations"; export type { CatalogModelId, LogicalModelId } from "./models"; export { AGENT_MODEL_CATALOG, diff --git a/packages/types/src/integrations.ts b/packages/types/src/integrations.ts index 70b992e3..504c7b77 100644 --- a/packages/types/src/integrations.ts +++ b/packages/types/src/integrations.ts @@ -2,6 +2,17 @@ import { z } from "zod"; const INTEGRATION_NAME_MAX_LENGTH = 64; const INTEGRATION_NAME_PATTERN = /^[a-z0-9_]+$/u; +const INTEGRATION_DISPLAY_NAMES: Readonly> = { + github: "GitHub", + gmail: "Gmail", + googlecalendar: "Google Calendar", + googledocs: "Google Docs", + googledrive: "Google Drive", + googlesheets: "Google Sheets", + linear: "Linear", + notion: "Notion", + slack: "Slack", +}; /** Open Composio toolkit slug, such as `github` or `google_calendar`. */ export const IntegrationNameSchema = z @@ -15,3 +26,15 @@ export const IntegrationNameSchema = z ); export type IntegrationName = z.infer; + +/** Stable user-facing label for an open Composio toolkit slug. */ +export function integrationDisplayName(slug: IntegrationName): string { + return ( + INTEGRATION_DISPLAY_NAMES[slug] ?? + slug + .split("_") + .filter(Boolean) + .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) + .join(" ") + ); +} diff --git a/skills/connected-apps/SKILL.md b/skills/connected-apps/SKILL.md index 8461e7f8..571dceac 100644 --- a/skills/connected-apps/SKILL.md +++ b/skills/connected-apps/SKILL.md @@ -14,8 +14,8 @@ Perform the user's requested action through their connected account. This playbo ## Workflow 1. Identify the provider, intended action, target object, and whether the operation mutates external state. -2. Call `composio_list_tools` with the provider and a focused search term to discover the exact supported action. -3. If the list is truncated, narrow the search rather than guessing a tool slug. +2. Call `composio_list_tools` with the provider and a concise action/object search to discover the exact supported action. The runtime automatically relaxes an over-specific zero-result query into ranked toolkit candidates. +3. Inspect the returned candidates and their input schemas. If the list is truncated and no candidate fits, retry with a shorter action or object keyword rather than guessing a tool slug. 4. Call `composio_execute` only for the explicit action the user requested. 5. Verify the returned identifier, status, or content before reporting success.