Merge pull request #599 from mcinteerj/fix/gemini-tool-schemas

fix: simplify tool schemas for Gemini compatibility
This commit is contained in:
Peter Steinberger
2026-01-09 19:19:01 +00:00
committed by GitHub
6 changed files with 52 additions and 24 deletions

View File

@@ -48,6 +48,7 @@
- Discord: stop provider when gateway reconnects are exhausted and surface errors. (#514) — thanks @joshp123 - Discord: stop provider when gateway reconnects are exhausted and surface errors. (#514) — thanks @joshp123
- Agents: strip empty assistant text blocks from session history to avoid Claude API 400s. (#210) - Agents: strip empty assistant text blocks from session history to avoid Claude API 400s. (#210)
- Agents: scrub unsupported JSON Schema keywords from tool schemas for Cloud Code Assist API compatibility. (#567) — thanks @erikpr1994 - Agents: scrub unsupported JSON Schema keywords from tool schemas for Cloud Code Assist API compatibility. (#567) — thanks @erikpr1994
- Agents: simplify session tool schemas for Gemini compatibility. (#599) — thanks @mcinteerj
- Auto-reply: preserve block reply ordering with timeout fallback for streaming. (#503) — thanks @joshp123 - Auto-reply: preserve block reply ordering with timeout fallback for streaming. (#503) — thanks @joshp123
- Auto-reply: block reply ordering fix (duplicate PR superseded by #503). (#483) — thanks @AbhisekBasu1 - Auto-reply: block reply ordering fix (duplicate PR superseded by #503). (#483) — thanks @AbhisekBasu1
- Auto-reply: avoid splitting outbound chunks inside parentheses. (#499) — thanks @philipp-spiess - Auto-reply: avoid splitting outbound chunks inside parentheses. (#499) — thanks @philipp-spiess

View File

@@ -23,6 +23,43 @@ vi.mock("../config/config.js", async (importOriginal) => {
import { createClawdbotTools } from "./clawdbot-tools.js"; import { createClawdbotTools } from "./clawdbot-tools.js";
describe("sessions tools", () => { describe("sessions tools", () => {
it("uses number (not integer) in tool schemas for Gemini compatibility", () => {
const tools = createClawdbotTools();
const byName = (name: string) => {
const tool = tools.find((candidate) => candidate.name === name);
expect(tool).toBeDefined();
if (!tool) throw new Error(`missing ${name} tool`);
return tool;
};
const schemaProp = (toolName: string, prop: string) => {
const tool = byName(toolName);
const schema = tool.parameters as {
anyOf?: unknown;
oneOf?: unknown;
properties?: Record<string, unknown>;
};
expect(schema.anyOf).toBeUndefined();
expect(schema.oneOf).toBeUndefined();
const properties = schema.properties ?? {};
const value = properties[prop] as { type?: unknown } | undefined;
expect(value).toBeDefined();
if (!value) throw new Error(`missing ${toolName} schema prop: ${prop}`);
return value;
};
expect(schemaProp("sessions_history", "limit").type).toBe("number");
expect(schemaProp("sessions_list", "limit").type).toBe("number");
expect(schemaProp("sessions_list", "activeMinutes").type).toBe("number");
expect(schemaProp("sessions_list", "messageLimit").type).toBe("number");
expect(schemaProp("sessions_send", "timeoutSeconds").type).toBe("number");
expect(schemaProp("sessions_spawn", "runTimeoutSeconds").type).toBe(
"number",
);
expect(schemaProp("sessions_spawn", "timeoutSeconds").type).toBe("number");
});
it("sessions_list filters kinds and includes messages", async () => { it("sessions_list filters kinds and includes messages", async () => {
callGatewayMock.mockReset(); callGatewayMock.mockReset();
callGatewayMock.mockImplementation(async (opts: unknown) => { callGatewayMock.mockImplementation(async (opts: unknown) => {

View File

@@ -18,7 +18,7 @@ import {
const SessionsHistoryToolSchema = Type.Object({ const SessionsHistoryToolSchema = Type.Object({
sessionKey: Type.String(), sessionKey: Type.String(),
limit: Type.Optional(Type.Integer({ minimum: 1 })), limit: Type.Optional(Type.Number({ minimum: 1 })),
includeTools: Type.Optional(Type.Boolean()), includeTools: Type.Optional(Type.Boolean()),
}); });

View File

@@ -46,9 +46,9 @@ type SessionListRow = {
const SessionsListToolSchema = Type.Object({ const SessionsListToolSchema = Type.Object({
kinds: Type.Optional(Type.Array(Type.String())), kinds: Type.Optional(Type.Array(Type.String())),
limit: Type.Optional(Type.Integer({ minimum: 1 })), limit: Type.Optional(Type.Number({ minimum: 1 })),
activeMinutes: Type.Optional(Type.Integer({ minimum: 1 })), activeMinutes: Type.Optional(Type.Number({ minimum: 1 })),
messageLimit: Type.Optional(Type.Integer({ minimum: 0 })), messageLimit: Type.Optional(Type.Number({ minimum: 0 })),
}); });
function resolveSandboxSessionToolsVisibility( function resolveSandboxSessionToolsVisibility(

View File

@@ -30,25 +30,15 @@ import {
resolvePingPongTurns, resolvePingPongTurns,
} from "./sessions-send-helpers.js"; } from "./sessions-send-helpers.js";
const SessionsSendToolSchema = Type.Union([ const SessionsSendToolSchema = Type.Object({
Type.Object( sessionKey: Type.Optional(Type.String()),
{ label: Type.Optional(
sessionKey: Type.String(), Type.String({ minLength: 1, maxLength: SESSION_LABEL_MAX_LENGTH }),
message: Type.String(),
timeoutSeconds: Type.Optional(Type.Integer({ minimum: 0 })),
},
{ additionalProperties: false },
), ),
Type.Object( agentId: Type.Optional(Type.String({ minLength: 1, maxLength: 64 })),
{ message: Type.String(),
label: Type.String({ minLength: 1, maxLength: SESSION_LABEL_MAX_LENGTH }), timeoutSeconds: Type.Optional(Type.Number({ minimum: 0 })),
agentId: Type.Optional(Type.String({ minLength: 1, maxLength: 64 })), });
message: Type.String(),
timeoutSeconds: Type.Optional(Type.Integer({ minimum: 0 })),
},
{ additionalProperties: false },
),
]);
export function createSessionsSendTool(opts?: { export function createSessionsSendTool(opts?: {
agentSessionKey?: string; agentSessionKey?: string;

View File

@@ -25,9 +25,9 @@ const SessionsSpawnToolSchema = Type.Object({
label: Type.Optional(Type.String()), label: Type.Optional(Type.String()),
agentId: Type.Optional(Type.String()), agentId: Type.Optional(Type.String()),
model: Type.Optional(Type.String()), model: Type.Optional(Type.String()),
runTimeoutSeconds: Type.Optional(Type.Integer({ minimum: 0 })), runTimeoutSeconds: Type.Optional(Type.Number({ minimum: 0 })),
// Back-compat alias. Prefer runTimeoutSeconds. // Back-compat alias. Prefer runTimeoutSeconds.
timeoutSeconds: Type.Optional(Type.Integer({ minimum: 0 })), timeoutSeconds: Type.Optional(Type.Number({ minimum: 0 })),
cleanup: Type.Optional( cleanup: Type.Optional(
Type.Union([Type.Literal("delete"), Type.Literal("keep")]), Type.Union([Type.Literal("delete"), Type.Literal("keep")]),
), ),