Compare commits
5 Commits
main
...
fix/gemini
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1528a88ec3 | ||
|
|
85bccff3fb | ||
|
|
b30127c2ba | ||
|
|
133889a2f6 | ||
|
|
d42f31c2a5 |
@ -30,6 +30,7 @@
|
||||
- Browser tools: add remote CDP URL support, Linux launcher options (`executablePath`, `noSandbox`), and surface `cdpUrl` in status.
|
||||
|
||||
### Fixes
|
||||
- Agents: clean Gemini tool schemas and avoid caching empty model discovery results (#126) — thanks @mcinteerj
|
||||
- Gateway CLI: read `CLAWDIS_GATEWAY_PASSWORD` from environment in `callGateway()` — allows `doctor`/`health` commands to auth without explicit `--password` flag.
|
||||
- Auto-reply: suppress stray `HEARTBEAT_OK` acks so they never get delivered as messages.
|
||||
- Discord: include recent guild context when replying to mentions and add `discord.historyLimit` to tune how many messages are captured.
|
||||
|
||||
@ -58,8 +58,18 @@ export async function loadModelCatalog(params?: {
|
||||
: undefined;
|
||||
models.push({ id, name, provider, contextWindow });
|
||||
}
|
||||
|
||||
if (models.length === 0) {
|
||||
// If we found nothing, don't cache this result so we can try again.
|
||||
modelCatalogPromise = null;
|
||||
}
|
||||
if (models.length === 0) {
|
||||
// If we found nothing, don't cache this result so we can try again.
|
||||
modelCatalogPromise = null;
|
||||
}
|
||||
} catch {
|
||||
// Leave models empty on discovery errors.
|
||||
// Leave models empty on discovery errors and don't cache.
|
||||
modelCatalogPromise = null;
|
||||
}
|
||||
|
||||
return models.sort((a, b) => {
|
||||
|
||||
@ -408,6 +408,8 @@ export async function runEmbeddedPiAgent(params: {
|
||||
const promptSkills = resolvePromptSkills(skillsSnapshot, skillEntries);
|
||||
const tools = createClawdisCodingTools({
|
||||
bash: params.config?.agent?.bash,
|
||||
provider,
|
||||
surface: params.surface,
|
||||
});
|
||||
const machineName = await getMachineDisplayName();
|
||||
const runtimeInfo = {
|
||||
|
||||
@ -72,6 +72,50 @@ describe("createClawdisCodingTools", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps anyOf variants for non-Gemini providers", () => {
|
||||
const tools = createClawdisCodingTools({ provider: "openai" });
|
||||
const browser = tools.find((tool) => tool.name === "clawdis_browser");
|
||||
expect(browser).toBeDefined();
|
||||
const parameters = browser?.parameters as {
|
||||
anyOf?: Array<{ properties?: Record<string, unknown> }>;
|
||||
};
|
||||
expect(Array.isArray(parameters.anyOf)).toBe(true);
|
||||
expect(parameters.anyOf?.length ?? 0).toBeGreaterThan(0);
|
||||
const hasConst = parameters.anyOf?.some((variant) => {
|
||||
const action = variant.properties?.action as
|
||||
| { const?: unknown }
|
||||
| undefined;
|
||||
return typeof action?.const === "string";
|
||||
});
|
||||
expect(hasConst).toBe(true);
|
||||
});
|
||||
|
||||
it("strips anyOf for google-gemini-cli tools", () => {
|
||||
const tools = createClawdisCodingTools({ provider: "google-gemini-cli" });
|
||||
const browser = tools.find((tool) => tool.name === "clawdis_browser");
|
||||
expect(browser).toBeDefined();
|
||||
const parameters = browser?.parameters as {
|
||||
anyOf?: unknown;
|
||||
properties?: Record<string, unknown>;
|
||||
required?: string[];
|
||||
};
|
||||
expect(parameters.anyOf).toBeUndefined();
|
||||
expect(parameters.properties?.action).toBeDefined();
|
||||
expect(parameters.required ?? []).toContain("action");
|
||||
});
|
||||
|
||||
it("strips anyOf for google-antigravity tools", () => {
|
||||
const tools = createClawdisCodingTools({ provider: "google-antigravity" });
|
||||
const browser = tools.find((tool) => tool.name === "clawdis_browser");
|
||||
expect(browser).toBeDefined();
|
||||
const parameters = browser?.parameters as {
|
||||
anyOf?: unknown;
|
||||
properties?: Record<string, unknown>;
|
||||
};
|
||||
expect(parameters.anyOf).toBeUndefined();
|
||||
expect(parameters.properties?.action).toBeDefined();
|
||||
});
|
||||
|
||||
it("includes bash and process tools", () => {
|
||||
const tools = createClawdisCodingTools();
|
||||
expect(tools.some((tool) => tool.name === "bash")).toBe(true);
|
||||
|
||||
@ -141,13 +141,80 @@ function mergePropertySchemas(existing: unknown, incoming: unknown): unknown {
|
||||
return existing;
|
||||
}
|
||||
|
||||
function normalizeToolParameters(tool: AnyAgentTool): AnyAgentTool {
|
||||
// Gemini tool schemas accept an OpenAPI-ish subset; strip unsupported bits.
|
||||
function cleanSchemaForGemini(schema: unknown): unknown {
|
||||
if (!schema || typeof schema !== "object") return schema;
|
||||
if (Array.isArray(schema)) return schema.map(cleanSchemaForGemini);
|
||||
|
||||
const obj = schema as Record<string, unknown>;
|
||||
const hasAnyOf = "anyOf" in obj && Array.isArray(obj.anyOf);
|
||||
const cleaned: Record<string, unknown> = {};
|
||||
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
if (key === "patternProperties") {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (key === "const") {
|
||||
cleaned.enum = [value];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (key === "type" && hasAnyOf) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (key === "properties" && value && typeof value === "object") {
|
||||
const props = value as Record<string, unknown>;
|
||||
cleaned[key] = Object.fromEntries(
|
||||
Object.entries(props).map(([k, v]) => [k, cleanSchemaForGemini(v)]),
|
||||
);
|
||||
} else if (key === "items" && value && typeof value === "object") {
|
||||
cleaned[key] = cleanSchemaForGemini(value);
|
||||
} else if (key === "anyOf" && Array.isArray(value)) {
|
||||
cleaned[key] = value.map((v) => cleanSchemaForGemini(v));
|
||||
} else if (key === "oneOf" && Array.isArray(value)) {
|
||||
cleaned[key] = value.map((v) => cleanSchemaForGemini(v));
|
||||
} else if (key === "allOf" && Array.isArray(value)) {
|
||||
cleaned[key] = value.map((v) => cleanSchemaForGemini(v));
|
||||
} else if (
|
||||
key === "additionalProperties" &&
|
||||
value &&
|
||||
typeof value === "object"
|
||||
) {
|
||||
cleaned[key] = cleanSchemaForGemini(value);
|
||||
} else {
|
||||
cleaned[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
// Only Gemini providers need schema cleanup; other providers can keep richer JSON Schema.
|
||||
function shouldCleanSchemaForGemini(provider?: string): boolean {
|
||||
return provider === "google-gemini-cli" || provider === "google-antigravity";
|
||||
}
|
||||
|
||||
function normalizeToolParameters(
|
||||
tool: AnyAgentTool,
|
||||
options?: { cleanForGemini?: boolean },
|
||||
): AnyAgentTool {
|
||||
const schema =
|
||||
tool.parameters && typeof tool.parameters === "object"
|
||||
? (tool.parameters as Record<string, unknown>)
|
||||
: undefined;
|
||||
if (!schema) return tool;
|
||||
if ("type" in schema && "properties" in schema) return tool;
|
||||
const cleanForGemini = options?.cleanForGemini === true;
|
||||
if (
|
||||
"type" in schema &&
|
||||
"properties" in schema &&
|
||||
!Array.isArray(schema.anyOf)
|
||||
) {
|
||||
return cleanForGemini
|
||||
? { ...tool, parameters: cleanSchemaForGemini(schema) }
|
||||
: tool;
|
||||
}
|
||||
if (!Array.isArray(schema.anyOf)) return tool;
|
||||
const mergedProperties: Record<string, unknown> = {};
|
||||
const requiredCounts = new Map<string, number>();
|
||||
@ -191,21 +258,29 @@ function normalizeToolParameters(tool: AnyAgentTool): AnyAgentTool {
|
||||
.map(([key]) => key)
|
||||
: undefined;
|
||||
|
||||
const mergedSchema = {
|
||||
...schema,
|
||||
type: "object",
|
||||
properties:
|
||||
Object.keys(mergedProperties).length > 0
|
||||
? mergedProperties
|
||||
: (schema.properties ?? {}),
|
||||
...(mergedRequired && mergedRequired.length > 0
|
||||
? { required: mergedRequired }
|
||||
: {}),
|
||||
additionalProperties:
|
||||
"additionalProperties" in schema ? schema.additionalProperties : true,
|
||||
};
|
||||
|
||||
// Preserve anyOf for non-Gemini providers; Gemini rejects top-level anyOf.
|
||||
return {
|
||||
...tool,
|
||||
parameters: {
|
||||
...schema,
|
||||
type: "object",
|
||||
properties:
|
||||
Object.keys(mergedProperties).length > 0
|
||||
? mergedProperties
|
||||
: (schema.properties ?? {}),
|
||||
...(mergedRequired && mergedRequired.length > 0
|
||||
? { required: mergedRequired }
|
||||
: {}),
|
||||
additionalProperties:
|
||||
"additionalProperties" in schema ? schema.additionalProperties : true,
|
||||
},
|
||||
parameters: cleanForGemini
|
||||
? (() => {
|
||||
const { anyOf: _unusedAnyOf, ...rest } = mergedSchema;
|
||||
return cleanSchemaForGemini(rest);
|
||||
})()
|
||||
: mergedSchema,
|
||||
};
|
||||
}
|
||||
|
||||
@ -294,8 +369,21 @@ function createClawdisReadTool(base: AnyAgentTool): AnyAgentTool {
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeSurface(surface?: string): string | undefined {
|
||||
const trimmed = surface?.trim().toLowerCase();
|
||||
return trimmed ? trimmed : undefined;
|
||||
}
|
||||
|
||||
function shouldIncludeDiscordTool(surface?: string): boolean {
|
||||
const normalized = normalizeSurface(surface);
|
||||
if (!normalized) return false;
|
||||
return normalized === "discord" || normalized.startsWith("discord:");
|
||||
}
|
||||
|
||||
export function createClawdisCodingTools(options?: {
|
||||
bash?: BashToolDefaults & ProcessToolDefaults;
|
||||
surface?: string;
|
||||
provider?: string;
|
||||
}): AnyAgentTool[] {
|
||||
const bashToolName = "bash";
|
||||
const base = (codingTools as unknown as AnyAgentTool[]).flatMap((tool) => {
|
||||
@ -314,5 +402,12 @@ export function createClawdisCodingTools(options?: {
|
||||
createWhatsAppLoginTool(),
|
||||
...createClawdisTools(),
|
||||
];
|
||||
return tools.map(normalizeToolParameters);
|
||||
const allowDiscord = shouldIncludeDiscordTool(options?.surface);
|
||||
const filtered = allowDiscord
|
||||
? tools
|
||||
: tools.filter((tool) => tool.name !== "discord");
|
||||
const cleanForGemini = shouldCleanSchemaForGemini(options?.provider);
|
||||
return filtered.map((tool) =>
|
||||
normalizeToolParameters(tool, { cleanForGemini }),
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user