From 237177a6751ed3c36a32cf39af5f8ded28802ffb Mon Sep 17 00:00:00 2001 From: "roomote-roomote[bot]" <301996811+roomote-roomote[bot]@users.noreply.github.com> Date: Sun, 6 Sep 2026 17:28:37 +0000 Subject: [PATCH 1/3] fix: avoid dangling refs in Fast integration tool schemas (#2295) Co-authored-by: @daniel-lxs <57051444+daniel-lxs@users.noreply.github.com> --- packages/cloud-agents/package.json | 1 + .../fast-agent-native-tool-schemas.test.ts | 97 +++++++++++++++---- .../fast-agent-native-tool-bridge.ts | 4 +- pnpm-lock.yaml | 3 + 4 files changed, 86 insertions(+), 19 deletions(-) diff --git a/packages/cloud-agents/package.json b/packages/cloud-agents/package.json index bb0636afa..5b7f118e9 100644 --- a/packages/cloud-agents/package.json +++ b/packages/cloud-agents/package.json @@ -93,6 +93,7 @@ "@types/ioredis-mock": "^8.2.6", "@types/node": "^24.10.13", "@types/jsdom": "21.1.7", + "ajv": "^8.20.0", "ioredis-mock": "^8.13.1", "vitest": "^4.1.1" } diff --git a/packages/cloud-agents/src/server/fast-agent/__tests__/fast-agent-native-tool-schemas.test.ts b/packages/cloud-agents/src/server/fast-agent/__tests__/fast-agent-native-tool-schemas.test.ts index bf644133a..9f487a53b 100644 --- a/packages/cloud-agents/src/server/fast-agent/__tests__/fast-agent-native-tool-schemas.test.ts +++ b/packages/cloud-agents/src/server/fast-agent/__tests__/fast-agent-native-tool-schemas.test.ts @@ -4,7 +4,12 @@ import { tmpdir } from 'node:os'; import { dirname, join } from 'node:path'; import { pathToFileURL } from 'node:url'; -import { FAST_AGENT_NATIVE_TOOL_NAMES } from '@roomote/types'; +import { + CALL_INTEGRATION_TOOL_TOOL, + FAST_AGENT_NATIVE_TOOL_NAMES, +} from '@roomote/types'; +import { z } from 'zod'; +import { Ajv2020 } from 'ajv/dist/2020.js'; import { getFastAgentNativeToolRuntime } from '../fast-agent-native-tool-bridge'; @@ -12,8 +17,9 @@ import { getFastAgentNativeToolRuntime } from '../fast-agent-native-tool-bridge' * Guards the JSON schema OpenAI receives for every Fast native tool. * * OpenCode loads each generated tool module with its own zod 4, treats - * `args` as a record of field schemas (wrapping it in `z.object`), and ships - * `z.toJSONSchema` of that to the provider. A tool that declares `args` as a + * `args` as a record of field schemas (wrapping it in `z.object`), and + * normalizes `z.toJSONSchema` before sending it to the provider. A tool + * that declares `args` as a * bare schema instead of a record (a `z.union`, say) turns into a schema * carrying zod internals, which OpenAI rejects with * `invalid_function_parameters` on every request, taking down every Fast turn @@ -192,12 +198,22 @@ function toOpenCodeJsonSchema(zod: ZodV4, args: unknown) { )} ${nonZod.length === 1 ? 'is' : 'are'} not. OpenCode wraps args in z.object itself; a bare schema (z.union, z.object) as args ships its internals to the provider.`, ); } - return zod.z.toJSONSchema(zod.z.object(args as Record), { - io: 'input', - }); + const schema = zod.z.toJSONSchema( + zod.z.object(args as Record), + { + io: 'input', + }, + ); + // OpenCode v1.18.10 tool/registry.ts zodJsonSchema renames the dictionary + // without rewriting refs. Testing raw Zod output missed this boundary. + const { $defs, ...rest } = schema; + return JSON.parse( + JSON.stringify($defs ? { ...rest, definitions: $defs } : rest), + ); } describe('Fast native tool schemas as OpenAI receives them', () => { + const validator = new Ajv2020({ strict: false }); let workDir: string; let zod: ZodV4; let tools: LoadedTool[]; @@ -266,6 +282,7 @@ describe('Fast native tool schemas as OpenAI receives them', () => { let schema: unknown; try { schema = toOpenCodeJsonSchema(zod, tool.args ?? {}); + validator.compile(schema as object); } catch (error) { failures.push( `${tool.name}: ${error instanceof Error ? error.message : String(error)}`, @@ -311,23 +328,13 @@ describe('Fast native tool schemas as OpenAI receives them', () => { ); const schema = toOpenCodeJsonSchema(zod, callTool?.args ?? {}) as { properties?: Record; - $defs?: Record; }; const argsSchema = schema.properties?.args as - | { type?: string; additionalProperties?: { $ref?: string } } + | { type?: string; additionalProperties?: { anyOf?: unknown[] } } | undefined; - const valueSchemaName = argsSchema?.additionalProperties?.$ref?.replace( - '#/$defs/', - '', - ); - const valueSchema = valueSchemaName - ? (schema.$defs?.[valueSchemaName] as - | { anyOf?: Array<{ type?: string }> } - | undefined) - : undefined; expect(argsSchema?.type).toBe('object'); - expect(valueSchema?.anyOf).toEqual( + expect(argsSchema?.additionalProperties?.anyOf).toEqual( expect.arrayContaining([ expect.objectContaining({ type: 'string' }), expect.objectContaining({ type: 'object' }), @@ -336,6 +343,60 @@ describe('Fast native tool schemas as OpenAI receives them', () => { ); }); + it('detects dangling refs after OpenCode normalizes recursive Zod schemas', () => { + const args = { + args: zod.z.record(zod.z.string(), zod.z.json()).optional(), + }; + expect(() => + validator.compile( + zod.z.toJSONSchema(zod.z.object(args), { io: 'input' }), + ), + ).not.toThrow(); + expect(() => validator.compile(toOpenCodeJsonSchema(zod, args))).toThrow( + /can't resolve reference #\/\$defs\//, + ); + }); + + it('preserves nested JSON through serialized native schema validation and server parsing', () => { + const callTool = tools.find( + (tool) => tool.name === FAST_AGENT_NATIVE_TOOL_NAMES.callIntegrationTool, + )!; + const validate = validator.compile( + toOpenCodeJsonSchema(zod, callTool.args), + ); + const nativeSchema = zod.z.object(callTool.args as Record); + const serverSchema = z.object(CALL_INTEGRATION_TOOL_TOOL.inputSchema); + const base = { integrationId: 'example', toolName: 'nested_tool' }; + for (const args of [ + undefined, + {}, + { + text: 'value', + number: 1.5, + enabled: true, + nullable: null, + list: [ + null, + false, + 42, + 'text', + [], + {}, + { nested: [{ 'arbitrary/key': { values: [1, null] } }] }, + ], + object: { nested: { list: [[{ value: 'preserved' }]] } }, + }, + ]) { + const input = JSON.parse(JSON.stringify({ ...base, args })); + expect(validate(input), JSON.stringify(validate.errors)).toBe(true); + expect(nativeSchema.parse(input)).toEqual(input); + expect(serverSchema.parse(input)).toEqual(input); + } + for (const args of [null, 'text', [], 42, false]) { + expect(validate({ ...base, args })).toBe(false); + } + }); + it('rejects a bare union or object as args, the shape that broke OpenAI models', () => { const { z } = zod; const question = z.object({ id: z.string() }); diff --git a/packages/cloud-agents/src/server/fast-agent/fast-agent-native-tool-bridge.ts b/packages/cloud-agents/src/server/fast-agent/fast-agent-native-tool-bridge.ts index 62e07a6eb..d3111bbe3 100644 --- a/packages/cloud-agents/src/server/fast-agent/fast-agent-native-tool-bridge.ts +++ b/packages/cloud-agents/src/server/fast-agent/fast-agent-native-tool-bridge.ts @@ -437,7 +437,9 @@ export default { args: { integrationId: z.string().min(1).describe(${JSON.stringify(CALL_INTEGRATION_TOOL_ARG_DESCRIPTIONS.integrationId)}), toolName: z.string().min(1).describe(${JSON.stringify(CALL_INTEGRATION_TOOL_ARG_DESCRIPTIONS.toolName)}), - args: z.record(z.string(), z.json()).optional().describe(${JSON.stringify(CALL_INTEGRATION_TOOL_ARG_DESCRIPTIONS.args)}), + // OpenCode renames $defs without rewriting refs. Keep JSON value types + // concrete but non-recursive; nested values are validated server-side. + args: z.record(z.string(), z.union([z.string(), z.number(), z.boolean(), z.null(), z.array(z.unknown()), z.record(z.string(), z.unknown())])).optional().describe(${JSON.stringify(CALL_INTEGRATION_TOOL_ARG_DESCRIPTIONS.args)}), }, execute: (args, context) => invoke(${JSON.stringify(CALL_INTEGRATION_TOOL_TOOL.name)}, args, context), } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e37dd3d25..55d2bb322 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1201,6 +1201,9 @@ importers: '@types/node': specifier: ^24.10.13 version: 24.12.4 + ajv: + specifier: ^8.20.0 + version: 8.20.0 ioredis-mock: specifier: ^8.13.1 version: 8.13.1(@types/ioredis-mock@8.2.6(ioredis@5.10.1))(ioredis@5.10.1) From 6c20f966a54f059bee7bfea516ef173c4ad6c441 Mon Sep 17 00:00:00 2001 From: "@daniel-lxs" <57051444+daniel-lxs@users.noreply.github.com> Date: Sun, 6 Sep 2026 19:14:38 +0000 Subject: [PATCH 2/3] chore: describe Fast integration schema hotfix --- .changeset/fast-integration-schema-hotfix.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/fast-integration-schema-hotfix.md diff --git a/.changeset/fast-integration-schema-hotfix.md b/.changeset/fast-integration-schema-hotfix.md new file mode 100644 index 000000000..74e9ef3f5 --- /dev/null +++ b/.changeset/fast-integration-schema-hotfix.md @@ -0,0 +1,5 @@ +--- +'@roomote/web': patch +--- + +Fix Fast turns failing with integration tool schema errors while preserving support for nested integration arguments. From ccae4eae9d68b32fd64e7892599caaef8057ed4a Mon Sep 17 00:00:00 2001 From: "@daniel-lxs" <57051444+daniel-lxs@users.noreply.github.com> Date: Sun, 6 Sep 2026 19:18:12 +0000 Subject: [PATCH 3/3] chore: release Roomote 1.3.2 --- .changeset/fast-integration-schema-hotfix.md | 5 ----- CHANGELOG.md | 12 ++++++++++++ package.json | 2 +- 3 files changed, 13 insertions(+), 6 deletions(-) delete mode 100644 .changeset/fast-integration-schema-hotfix.md diff --git a/.changeset/fast-integration-schema-hotfix.md b/.changeset/fast-integration-schema-hotfix.md deleted file mode 100644 index 74e9ef3f5..000000000 --- a/.changeset/fast-integration-schema-hotfix.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@roomote/web': patch ---- - -Fix Fast turns failing with integration tool schema errors while preserving support for nested integration arguments. diff --git a/CHANGELOG.md b/CHANGELOG.md index cc95b8949..8ee260fa7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ This file tracks product releases for Roomote (single monorepo version). Automated release entries are prepended by `pnpm run version`. +## 1.3.2 (2026-09-06) + +Roomote 1.3.2 fixes integration tool schema errors that can prevent Fast conversations from responding. + +### Highlights + +- Keep Fast integration calls working with nested objects, arrays, and other JSON arguments. + +### Patch changes + +- Fix Fast turns failing with integration tool schema errors while preserving support for nested integration arguments. + ## 1.3.1 (2026-09-05) Roomote 1.3.1 improves Fast, Live Preview, chat, and MCP coordination while adding focused controls for pull request reviews and custom automations. diff --git a/package.json b/package.json index d68c6277a..431b72184 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "roomote", - "version": "1.3.1", + "version": "1.3.2", "license": "FCL-1.0-ALv2", "packageManager": "pnpm@10.29.3", "engines": {