Merge pull request #665 from sebslight/fix/cloud-code-assist-schema-and-tool-ids
fix(agents): harden Cloud Code Assist compatibility
This commit is contained in:
@@ -363,6 +363,37 @@ describe("sanitizeSessionMessagesImages", () => {
|
||||
expect((content as Array<{ type?: string }>)[0]?.type).toBe("toolCall");
|
||||
});
|
||||
|
||||
it("sanitizes tool ids for assistant blocks and tool results", async () => {
|
||||
const input = [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "toolUse", id: "call_abc|item:123", name: "test", input: {} },
|
||||
{
|
||||
type: "toolCall",
|
||||
id: "call_abc|item:456",
|
||||
name: "bash",
|
||||
arguments: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: "toolResult",
|
||||
toolUseId: "call_abc|item:123",
|
||||
content: [{ type: "text", text: "ok" }],
|
||||
},
|
||||
] satisfies AgentMessage[];
|
||||
|
||||
const out = await sanitizeSessionMessagesImages(input, "test");
|
||||
|
||||
const assistant = out[0] as { content?: Array<{ id?: string }> };
|
||||
expect(assistant.content?.[0]?.id).toBe("call_abc_item_123");
|
||||
expect(assistant.content?.[1]?.id).toBe("call_abc_item_456");
|
||||
|
||||
const toolResult = out[1] as { toolUseId?: string };
|
||||
expect(toolResult.toolUseId).toBe("call_abc_item_123");
|
||||
});
|
||||
|
||||
it("filters whitespace-only assistant text blocks", async () => {
|
||||
const input = [
|
||||
{
|
||||
|
||||
@@ -106,12 +106,20 @@ export async function sanitizeSessionMessagesImages(
|
||||
const sanitizedToolCallId = toolMsg.toolCallId
|
||||
? sanitizeToolCallId(toolMsg.toolCallId)
|
||||
: undefined;
|
||||
const toolUseId = (toolMsg as { toolUseId?: unknown }).toolUseId;
|
||||
const sanitizedToolUseId =
|
||||
typeof toolUseId === "string" && toolUseId
|
||||
? sanitizeToolCallId(toolUseId)
|
||||
: undefined;
|
||||
const sanitizedMsg = {
|
||||
...toolMsg,
|
||||
content: nextContent,
|
||||
...(sanitizedToolCallId && {
|
||||
toolCallId: sanitizedToolCallId,
|
||||
}),
|
||||
...(sanitizedToolUseId && {
|
||||
toolUseId: sanitizedToolUseId,
|
||||
}),
|
||||
};
|
||||
out.push(sanitizedMsg);
|
||||
continue;
|
||||
@@ -146,18 +154,24 @@ export async function sanitizeSessionMessagesImages(
|
||||
// Also sanitize tool call IDs in assistant messages (function call blocks)
|
||||
const sanitizedContent = await Promise.all(
|
||||
filteredContent.map(async (block) => {
|
||||
if (!block || typeof block !== "object") return block;
|
||||
|
||||
const type = (block as { type?: unknown }).type;
|
||||
const id = (block as { id?: unknown }).id;
|
||||
if (typeof id !== "string" || !id) return block;
|
||||
|
||||
// Cloud Code Assist tool blocks require ids matching ^[a-zA-Z0-9_-]+$.
|
||||
if (
|
||||
block &&
|
||||
typeof block === "object" &&
|
||||
(block as { type?: unknown }).type === "functionCall" &&
|
||||
(block as { id?: unknown }).id
|
||||
type === "functionCall" ||
|
||||
type === "toolUse" ||
|
||||
type === "toolCall"
|
||||
) {
|
||||
const functionBlock = block as { type: string; id: string };
|
||||
return {
|
||||
...functionBlock,
|
||||
id: sanitizeToolCallId(functionBlock.id),
|
||||
...(block as unknown as Record<string, unknown>),
|
||||
id: sanitizeToolCallId(id),
|
||||
};
|
||||
}
|
||||
|
||||
return block;
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -384,6 +384,19 @@ describe("createClawdbotCodingTools", () => {
|
||||
"$ref",
|
||||
"$defs",
|
||||
"definitions",
|
||||
"examples",
|
||||
"minLength",
|
||||
"maxLength",
|
||||
"minimum",
|
||||
"maximum",
|
||||
"multipleOf",
|
||||
"pattern",
|
||||
"format",
|
||||
"minItems",
|
||||
"maxItems",
|
||||
"uniqueItems",
|
||||
"minProperties",
|
||||
"maxProperties",
|
||||
]);
|
||||
|
||||
const findUnsupportedKeywords = (
|
||||
@@ -398,9 +411,24 @@ describe("createClawdbotCodingTools", () => {
|
||||
});
|
||||
return found;
|
||||
}
|
||||
for (const [key, value] of Object.entries(
|
||||
schema as Record<string, unknown>,
|
||||
)) {
|
||||
|
||||
const record = schema as Record<string, unknown>;
|
||||
const properties =
|
||||
record.properties &&
|
||||
typeof record.properties === "object" &&
|
||||
!Array.isArray(record.properties)
|
||||
? (record.properties as Record<string, unknown>)
|
||||
: undefined;
|
||||
if (properties) {
|
||||
for (const [key, value] of Object.entries(properties)) {
|
||||
found.push(
|
||||
...findUnsupportedKeywords(value, `${path}.properties.${key}`),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(record)) {
|
||||
if (key === "properties") continue;
|
||||
if (unsupportedKeywords.has(key)) {
|
||||
found.push(`${path}.${key}`);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,23 @@ const UNSUPPORTED_SCHEMA_KEYWORDS = new Set([
|
||||
"$ref",
|
||||
"$defs",
|
||||
"definitions",
|
||||
// Non-standard (OpenAPI) keyword; Claude validators reject it.
|
||||
"examples",
|
||||
|
||||
// Cloud Code Assist appears to validate tool schemas more strictly/quirkily than
|
||||
// draft 2020-12 in practice; these constraints frequently trigger 400s.
|
||||
"minLength",
|
||||
"maxLength",
|
||||
"minimum",
|
||||
"maximum",
|
||||
"multipleOf",
|
||||
"pattern",
|
||||
"format",
|
||||
"minItems",
|
||||
"maxItems",
|
||||
"uniqueItems",
|
||||
"minProperties",
|
||||
"maxProperties",
|
||||
]);
|
||||
|
||||
// Check if an anyOf/oneOf array contains only literal values that can be flattened.
|
||||
@@ -134,14 +151,14 @@ function cleanSchemaForGeminiWithDefs(
|
||||
const result: Record<string, unknown> = {
|
||||
...(cleaned as Record<string, unknown>),
|
||||
};
|
||||
for (const key of ["description", "title", "default", "examples"]) {
|
||||
for (const key of ["description", "title", "default"]) {
|
||||
if (key in obj && obj[key] !== undefined) result[key] = obj[key];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const result: Record<string, unknown> = {};
|
||||
for (const key of ["description", "title", "default", "examples"]) {
|
||||
for (const key of ["description", "title", "default"]) {
|
||||
if (key in obj && obj[key] !== undefined) result[key] = obj[key];
|
||||
}
|
||||
return result;
|
||||
@@ -157,7 +174,7 @@ function cleanSchemaForGeminiWithDefs(
|
||||
type: flattened.type,
|
||||
enum: flattened.enum,
|
||||
};
|
||||
for (const key of ["description", "title", "default", "examples"]) {
|
||||
for (const key of ["description", "title", "default"]) {
|
||||
if (key in obj && obj[key] !== undefined) result[key] = obj[key];
|
||||
}
|
||||
return result;
|
||||
@@ -171,7 +188,7 @@ function cleanSchemaForGeminiWithDefs(
|
||||
type: flattened.type,
|
||||
enum: flattened.enum,
|
||||
};
|
||||
for (const key of ["description", "title", "default", "examples"]) {
|
||||
for (const key of ["description", "title", "default"]) {
|
||||
if (key in obj && obj[key] !== undefined) result[key] = obj[key];
|
||||
}
|
||||
return result;
|
||||
|
||||
Reference in New Issue
Block a user