From 7f4c8af8bac58da7b969aec17cca8471952aa8f1 Mon Sep 17 00:00:00 2001 From: bft-codebot Date: Thu, 5 Feb 2026 19:50:44 +0000 Subject: [PATCH] sync(bfmono): feat(gambit-core): add schema guards and model param passthrough (+19 more) (bfmono@cf9b23778) This PR is an automated gambitmono sync of bfmono Gambit packages. - Source: `packages/gambit/` - Core: `packages/gambit-core/` - bfmono rev: cf9b23778 Changes: - cf9b23778 feat(gambit-core): add schema guards and model param passthrough - d0e5a9617 [gambit] move chat message into transcript so it scrolls - 5c6125d99 feat(simulator-ui): open workbench drawer by default - 7c9cd05f8 feat(simulator): gate chat accordion by env flag - a2599068e feat(simulator-ui): add build chat history loading - 9911dae22 feat(simulator-ui): add workbench chat drawer accordion - 8cab8ec1f feat(simulator-ui): dock calibrate drawer and sync updates - d41ba101d Add AAR for phase 3.1.5 deck format build tab - b1b5e2a7e Prefer PROMPT.md after build scaffolds - 50fac8f7b Update Build tab for deck format 1.0 - c5d99df6a feat(gambit-core): add deck format 1.0 stdlib assets - e76d1c56f feat(gambit): avoid invalid test deck fetch on stale selection - b8e21aaca feat(gambit): require model params in bot-written decks - 1ccbe6710 feat(gambit): stabilize test bot kickoff for user-start decks - f239ec86b feat(gambit): format gambit-bot instructions - 6fda040ac feat(gambit): build bot: reinforce test deck wiring rule - b16340e2a feat(gambit): build bot: require test deck creation - c1f30dd25 feat(gambit): build tab: preview-first file selector - 5bcd52f53 feat(demo-runner): update build demo screenshot slugs - 10d071ace feat(gambit): add build file browser UI Do not edit this repo directly; make changes in bfmono and re-run the sync. --- .../gambit-core/schemas/graders/respond.ts | 4 +- .../scenarios/plain_chat_input_optional.ts | 3 ++ .../plain_chat_input_optional.zod.ts | 1 + .../schemas/scenarios/plain_chat_output.ts | 3 ++ .../scenarios/plain_chat_output.zod.ts | 1 + packages/gambit-core/snippets/init.md | 12 ++++++ packages/gambit-core/src/markdown.test.ts | 4 +- packages/gambit-core/src/markdown.ts | 40 ++++++++++++++++++- packages/gambit-core/src/runtime.test.ts | 37 +++++++++++++++++ packages/gambit-core/src/runtime.ts | 11 +++++ packages/gambit-core/src/types.ts | 5 +++ simulator-ui/src/Chat.tsx | 8 +--- 12 files changed, 118 insertions(+), 11 deletions(-) create mode 100644 packages/gambit-core/schemas/scenarios/plain_chat_input_optional.ts create mode 100644 packages/gambit-core/schemas/scenarios/plain_chat_input_optional.zod.ts create mode 100644 packages/gambit-core/schemas/scenarios/plain_chat_output.ts create mode 100644 packages/gambit-core/schemas/scenarios/plain_chat_output.zod.ts create mode 100644 packages/gambit-core/snippets/init.md diff --git a/packages/gambit-core/schemas/graders/respond.ts b/packages/gambit-core/schemas/graders/respond.ts index d225716fc..4d690e7f4 100644 --- a/packages/gambit-core/schemas/graders/respond.ts +++ b/packages/gambit-core/schemas/graders/respond.ts @@ -2,8 +2,8 @@ import { z } from "zod"; export default z.object({ payload: z.any().optional(), - status: z.string().optional(), + status: z.number().int().optional(), message: z.string().optional(), - code: z.any().optional(), + code: z.string().optional(), meta: z.record(z.any()).optional(), }); diff --git a/packages/gambit-core/schemas/scenarios/plain_chat_input_optional.ts b/packages/gambit-core/schemas/scenarios/plain_chat_input_optional.ts new file mode 100644 index 000000000..3807c1c29 --- /dev/null +++ b/packages/gambit-core/schemas/scenarios/plain_chat_input_optional.ts @@ -0,0 +1,3 @@ +import { z } from "zod"; + +export default z.string().optional(); diff --git a/packages/gambit-core/schemas/scenarios/plain_chat_input_optional.zod.ts b/packages/gambit-core/schemas/scenarios/plain_chat_input_optional.zod.ts new file mode 100644 index 000000000..9a631461f --- /dev/null +++ b/packages/gambit-core/schemas/scenarios/plain_chat_input_optional.zod.ts @@ -0,0 +1 @@ +export { default } from "./plain_chat_input_optional.ts"; diff --git a/packages/gambit-core/schemas/scenarios/plain_chat_output.ts b/packages/gambit-core/schemas/scenarios/plain_chat_output.ts new file mode 100644 index 000000000..91d0a8d3f --- /dev/null +++ b/packages/gambit-core/schemas/scenarios/plain_chat_output.ts @@ -0,0 +1,3 @@ +import { z } from "zod"; + +export default z.string(); diff --git a/packages/gambit-core/schemas/scenarios/plain_chat_output.zod.ts b/packages/gambit-core/schemas/scenarios/plain_chat_output.zod.ts new file mode 100644 index 000000000..675f3e136 --- /dev/null +++ b/packages/gambit-core/schemas/scenarios/plain_chat_output.zod.ts @@ -0,0 +1 @@ +export { default } from "./plain_chat_output.ts"; diff --git a/packages/gambit-core/snippets/init.md b/packages/gambit-core/snippets/init.md new file mode 100644 index 000000000..c5f0d3b24 --- /dev/null +++ b/packages/gambit-core/snippets/init.md @@ -0,0 +1,12 @@ ++++ +label = "Fill missing init fields" ++++ + +When you receive a user message with: + +{ "type": "gambit_test_bot_init_fill", "missing": ["path.to.field", "..."], +"current": { ... }, "schemaHints": [ ... ] } + +Return ONLY valid JSON that supplies values for the missing fields. Do not +include any fields that are not listed in "missing". If the only missing path is +"(root)", return the full init JSON value. diff --git a/packages/gambit-core/src/markdown.test.ts b/packages/gambit-core/src/markdown.test.ts index 0639acd41..d22831d78 100644 --- a/packages/gambit-core/src/markdown.test.ts +++ b/packages/gambit-core/src/markdown.test.ts @@ -97,8 +97,8 @@ Schema deck. const deck = await loadMarkdownDeck(deckPath); assert(deck.contextSchema, "expected context schema to resolve"); - const parsed = deck.contextSchema.parse({ status: "ok" }); - assertEquals(parsed, { status: "ok" }); + const parsed = deck.contextSchema.parse({ status: 200 }); + assertEquals(parsed, { status: 200 }); }); Deno.test("markdown deck warns on legacy schema URIs", async () => { diff --git a/packages/gambit-core/src/markdown.ts b/packages/gambit-core/src/markdown.ts index fc22bd886..d800c6771 100644 --- a/packages/gambit-core/src/markdown.ts +++ b/packages/gambit-core/src/markdown.ts @@ -11,7 +11,7 @@ import { } from "./constants.ts"; import { isCardDefinition, isDeckDefinition } from "./definitions.ts"; import { loadCard } from "./loader.ts"; -import { mergeZodObjects } from "./schema.ts"; +import { mergeZodObjects, toJsonSchema } from "./schema.ts"; import { resolveBuiltinSchemaPath } from "./builtins.ts"; import type { ActionDeckDefinition, @@ -49,6 +49,27 @@ const END_TEXT = ` If the entire workflow is finished and no further user turns should be sent, call the \`${GAMBIT_TOOL_END}\` tool with optional \`message\` and \`payload\` fields to explicitly end the session. `.trim(); +function normalizeJsonSchema(value: unknown): unknown { + if (Array.isArray(value)) { + return value.map((entry) => normalizeJsonSchema(entry)); + } + if (value && typeof value === "object") { + const record = value as Record; + const out: Record = {}; + for (const key of Object.keys(record).sort()) { + out[key] = normalizeJsonSchema(record[key]); + } + return out; + } + return value; +} + +function schemasMatchDeep(a: ZodTypeAny, b: ZodTypeAny): boolean { + const aJson = normalizeJsonSchema(toJsonSchema(a as never)); + const bJson = normalizeJsonSchema(toJsonSchema(b as never)); + return JSON.stringify(aJson) === JSON.stringify(bJson); +} + function warnLegacyMarker( marker: keyof typeof LEGACY_MARKER_WARNINGS, replacement: string, @@ -475,6 +496,23 @@ export async function loadMarkdownDeck( ); } + if ( + contextSchema && executeContextSchema && + !schemasMatchDeep(contextSchema, executeContextSchema) + ) { + logger.warn( + `[gambit] deck at ${resolved} has mismatched contextSchema between PROMPT.md and execute module (pre-1.0: warn; 1.0+: error)`, + ); + } + if ( + responseSchema && executeResponseSchema && + !schemasMatchDeep(responseSchema, executeResponseSchema) + ) { + logger.warn( + `[gambit] deck at ${resolved} has mismatched responseSchema between PROMPT.md and execute module (pre-1.0: warn; 1.0+: error)`, + ); + } + const allCards = flattenCards(cards); const cleanedBody = replaced.body; const allowEnd = Boolean(deckMeta.allowEnd) || diff --git a/packages/gambit-core/src/runtime.test.ts b/packages/gambit-core/src/runtime.test.ts index e6988dd94..6ebbcc5a2 100644 --- a/packages/gambit-core/src/runtime.test.ts +++ b/packages/gambit-core/src/runtime.test.ts @@ -1837,3 +1837,40 @@ Deck. assert(Array.isArray(resolvedInput.model)); }); + +Deno.test("modelParams.additionalParams pass through and top-level wins", async () => { + const dir = await Deno.makeTempDir(); + const deckPath = await writeTempDeck( + dir, + "root.deck.md", + ` ++++ +modelParams = { model = "dummy-model", temperature = 0.2, additionalParams = { temperature = 0.9, seed = 42, my_param = "x" } } ++++ + +Deck. +`.trim(), + ); + + let seenParams: Record | undefined; + const provider: ModelProvider = { + chat: (input) => { + seenParams = input.params; + return Promise.resolve({ + message: { role: "assistant", content: "ok" }, + finishReason: "stop", + }); + }, + }; + + await runDeck({ + path: deckPath, + input: "hi", + modelProvider: provider, + isRoot: true, + }); + + assertEquals(seenParams?.temperature, 0.2); + assertEquals(seenParams?.seed, 42); + assertEquals(seenParams?.my_param, "x"); +}); diff --git a/packages/gambit-core/src/runtime.ts b/packages/gambit-core/src/runtime.ts index a7485eabe..3e1fe868b 100644 --- a/packages/gambit-core/src/runtime.ts +++ b/packages/gambit-core/src/runtime.ts @@ -185,6 +185,7 @@ function toProviderParams( if (!params) return undefined; const { model: _model, + additionalParams, temperature, top_p, frequency_penalty, @@ -192,6 +193,16 @@ function toProviderParams( max_tokens, } = params; const out: Record = {}; + if ( + additionalParams && + typeof additionalParams === "object" && + !Array.isArray(additionalParams) + ) { + for (const [key, value] of Object.entries(additionalParams)) { + if (value === undefined) continue; + out[key] = value; + } + } if (temperature !== undefined) out.temperature = temperature; if (top_p !== undefined) out.top_p = top_p; if (frequency_penalty !== undefined) { diff --git a/packages/gambit-core/src/types.ts b/packages/gambit-core/src/types.ts index fe637277f..9e4bc251b 100644 --- a/packages/gambit-core/src/types.ts +++ b/packages/gambit-core/src/types.ts @@ -26,6 +26,11 @@ export type ModelParams = { frequency_penalty?: number; presence_penalty?: number; max_tokens?: number; + /** + * Provider-specific pass-through parameters. Values must be JSON-serializable. + * Top-level supported fields take precedence when keys overlap. + */ + additionalParams?: Record; }; export type Guardrails = { diff --git a/simulator-ui/src/Chat.tsx b/simulator-ui/src/Chat.tsx index 76871073e..d7d4da089 100644 --- a/simulator-ui/src/Chat.tsx +++ b/simulator-ui/src/Chat.tsx @@ -107,16 +107,12 @@ export default function Chat() { return (
-
-
+
+
Use this chat to update deck files via Gambit Bot. Tool calls show file writes and why they happened.
-
-
-
-
{run.messages.length === 0 && (
No messages yet.
)}