Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/gambit-core/schemas/graders/respond.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { z } from "zod";

export default z.string().optional();
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from "./plain_chat_input_optional.ts";
3 changes: 3 additions & 0 deletions packages/gambit-core/schemas/scenarios/plain_chat_output.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { z } from "zod";

export default z.string();
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from "./plain_chat_output.ts";
12 changes: 12 additions & 0 deletions packages/gambit-core/snippets/init.md
Original file line number Diff line number Diff line change
@@ -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.
4 changes: 2 additions & 2 deletions packages/gambit-core/src/markdown.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
40 changes: 39 additions & 1 deletion packages/gambit-core/src/markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<string, unknown>;
const out: Record<string, unknown> = {};
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,
Expand Down Expand Up @@ -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) ||
Expand Down
37 changes: 37 additions & 0 deletions packages/gambit-core/src/runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown> | 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");
});
11 changes: 11 additions & 0 deletions packages/gambit-core/src/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,13 +185,24 @@ function toProviderParams(
if (!params) return undefined;
const {
model: _model,
additionalParams,
temperature,
top_p,
frequency_penalty,
presence_penalty,
max_tokens,
} = params;
const out: Record<string, unknown> = {};
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) {
Expand Down
5 changes: 5 additions & 0 deletions packages/gambit-core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, JSONValue>;
};

export type Guardrails = {
Expand Down
8 changes: 2 additions & 6 deletions simulator-ui/src/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,16 +107,12 @@ export default function Chat() {

return (
<div className="test-bot-sidebar flex-column gap-8 flex-1 build-chat-panel">
<div className="flex-row gap-8 items-center">
<div className="flex-column flex-1 gap-4">
<div className="test-bot-thread">
<div className="imessage-thread" ref={transcriptRef}>
<div className="placeholder">
Use this chat to update deck files via Gambit Bot. Tool calls show
file writes and why they happened.
</div>
</div>
</div>
<div className="test-bot-thread">
<div className="imessage-thread" ref={transcriptRef}>
{run.messages.length === 0 && (
<div className="placeholder">No messages yet.</div>
)}
Expand Down