diff --git a/packages/agent-core/README.md b/packages/agent-core/README.md index b2162cc9..cb49e0e7 100644 --- a/packages/agent-core/README.md +++ b/packages/agent-core/README.md @@ -177,6 +177,10 @@ so the agent does not rediscover, convert, or screenshot the same file before fi Composio REST tool discovery and execution responses are byte-bounded before parsing, then projected into bounded, valid JSON before entering model context. +The durable tool executor binds each checkpointed tool-call identity into the +request context before Mastra execution. Composio quota charging derives its +idempotency key from that explicit context value instead of relying on +compatibility-layer execution fields that Mastra does not preserve. 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 diff --git a/packages/agent-core/src/mastra/context.ts b/packages/agent-core/src/mastra/context.ts index 26578f29..9607a02d 100644 --- a/packages/agent-core/src/mastra/context.ts +++ b/packages/agent-core/src/mastra/context.ts @@ -27,6 +27,7 @@ export const CONTEXT = { runIntent: "runIntent", selectedSkill: "selectedSkill", selectedTool: "selectedTool", + toolCallId: "toolCallId", userSkillCreator: "userSkillCreator", userSkillLoader: "userSkillLoader", userSkills: "userSkills", diff --git a/packages/agent-core/src/mastra/durable-agent-step.ts b/packages/agent-core/src/mastra/durable-agent-step.ts index c52ac280..55590f7e 100644 --- a/packages/agent-core/src/mastra/durable-agent-step.ts +++ b/packages/agent-core/src/mastra/durable-agent-step.ts @@ -1,6 +1,7 @@ import type { RequestContext } from "@mastra/core/request-context"; import { type JSONValue, type ModelMessage, modelMessageSchema } from "ai"; import { z } from "zod"; +import { CONTEXT } from "./context"; import { mastra } from "./index"; import { cheatcodeTools } from "./tool-defs/tool-set"; @@ -122,6 +123,10 @@ export async function executeGeneralAgentTool( if (!tool?.execute) { throw new Error(`Unknown or non-executable agent tool: ${options.toolName}`); } + // Mastra's execution wrapper does not preserve arbitrary top-level compatibility + // fields. Bind the durable call identity explicitly so request-scoped tools can + // derive stable idempotency keys without serializing them into model arguments. + options.requestContext.set(CONTEXT.toolCallId, options.toolCallId); return tool.execute(options.input, { ...(options.abortSignal ? { abortSignal: options.abortSignal } : {}), // Repository tools use requestContext for their runtime dependencies and 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 66a52c21..8fabfe28 100644 --- a/packages/agent-core/src/mastra/tool-defs/composio-tool.ts +++ b/packages/agent-core/src/mastra/tool-defs/composio-tool.ts @@ -612,8 +612,8 @@ export const mastraComposioExecute = createTool({ }); function composioQuotaEventId(context: unknown): string { - const record = typeof context === "object" && context !== null ? context : {}; - const candidate = (record as Record)["toolCallId"]; + const requestContext = requestContextFromToolContext(context); + const candidate = requestContext.get(CONTEXT.toolCallId); if (typeof candidate !== "string" || candidate.length === 0 || candidate.length > 180) { throw new Error("Composio execution requires a bounded tool-call id."); }