diff --git a/src/main/services/composio/composio-client.ts b/src/main/services/composio/composio-client.ts index eb385b2a..c4cb78ec 100644 --- a/src/main/services/composio/composio-client.ts +++ b/src/main/services/composio/composio-client.ts @@ -7,11 +7,8 @@ export interface ComposioToolSchema { name: string; description: string; toolkitSlug: string | null; - parameters: { - type: string; - properties: Record; - required?: string[]; - }; + /** The action's input JSON Schema exactly as Composio validates it ($defs, combinations and limits kept). */ + parameters: Record; } export interface ComposioAuthConfigSummary { @@ -214,17 +211,16 @@ export class ComposioClient { } function normalizeToolSchema(raw: Record): ComposioToolSchema { - const params = raw.input_parameters ?? raw.parameters ?? raw.inputSchema ?? {}; - const schema = typeof params === "object" && params !== null ? params as Record : {}; + const name = String(raw.slug ?? raw.name ?? ""); + const field = ["input_parameters", "parameters", "inputSchema"].find((key) => key in raw); + const params = field === undefined ? undefined : raw[field]; + // An action without an input schema takes no input; anything else must be a schema object, passed on unchanged. + if (params !== undefined && (typeof params !== "object" || params === null || Array.isArray(params))) throw new Error(`Composio action ${name} has an input schema that is not an object.`); const toolkit = (raw.toolkit ?? {}) as Record; return { - name: String(raw.slug ?? raw.name ?? ""), + name, description: String(raw.description ?? "").slice(0, 300), toolkitSlug: typeof toolkit.slug === "string" ? toolkit.slug : null, - parameters: { - type: "object", - properties: (schema.properties ?? {}) as Record, - required: Array.isArray(schema.required) ? schema.required as string[] : [], - }, + parameters: params === undefined ? { type: "object", properties: {} } : params as Record, }; } diff --git a/tests/unit/composio-tool-pages.test.ts b/tests/unit/composio-tool-pages.test.ts index 84ec1271..96ed1333 100644 --- a/tests/unit/composio-tool-pages.test.ts +++ b/tests/unit/composio-tool-pages.test.ts @@ -109,4 +109,28 @@ describe("agents reach every action through search and run", () => { expect(h.connection.db.prepare("select count(*) as n from run_actions").get()).toEqual({ n: 0 }); h.connection.close(); }); + + test("an action's input schema reaches the agent whole: references, combinations and limits included", async () => { + const input = { + type: "object", + $defs: { Reviewer: { type: "object", properties: { login: { type: "string" } }, required: ["login"] } }, + properties: { + title: { type: "string", maxLength: 256 }, + reviewers: { type: "array", items: { $ref: "#/$defs/Reviewer" } }, + base: { anyOf: [{ type: "string" }, { type: "null" }] }, + }, + required: ["title"], + additionalProperties: false, + oneOf: [{ required: ["base"] }, { required: ["reviewers"] }], + }; + pages.set("github:", { items: [{ ...tool("GITHUB_CREATE_A_PULL_REQUEST"), description: "Create a pull request", input_parameters: input }], next_cursor: null }); + const h = company("ak_schema"); + const found = JSON.parse(await h.call("composio_find_actions", { query: "pull request" })) as { matches: Array<{ input: unknown }> }; + expect(found.matches[0].input).toEqual(input); + pages.set("github:", { items: [{ ...tool("GITHUB_BROKEN"), input_parameters: null }], next_cursor: null }); + const broken = company("ak_schema_null"); + expect(await broken.call("composio_find_actions", { query: "broken" })).toBe("ERROR: Composio action GITHUB_BROKEN has an input schema that is not an object."); + broken.connection.close(); + h.connection.close(); + }); });