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
5 changes: 5 additions & 0 deletions .changeset/lucky-goats-wait.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@browserbasehq/stagehand": patch
---

Fix: zod bug when pinning zod to v3 and using structured output in agent
41 changes: 28 additions & 13 deletions packages/core/lib/v3/agent/utils/handleDoneToolCall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,26 @@ import { z } from "zod";
import { tool } from "ai";
import { LogLine } from "../../types/public/logs";
import { StagehandZodObject } from "../../zodCompat";
import { getZFactory } from "../../../utils";
Comment thread
tkattkat marked this conversation as resolved.
import type { StagehandZodSchema } from "../../zodCompat";

interface DoneResult {
reasoning: string;
taskComplete: boolean;
messages: ModelMessage[];
output?: Record<string, unknown>;
}

const baseDoneSchema = z.object({
reasoning: z
.string()
.describe("Brief summary of what actions were taken and the outcome"),
taskComplete: z
.boolean()
.describe("true if the task was fully completed, false otherwise"),
});
function buildBaseDoneSchema(factory: typeof z) {
return factory.object({
reasoning: factory
.string()
.describe("Brief summary of what actions were taken and the outcome"),
taskComplete: factory
.boolean()
.describe("true if the task was fully completed, false otherwise"),
});
}

/**
* Force a done tool call at the end of an agent run.
Expand All @@ -38,6 +43,12 @@ export async function handleDoneToolCall(options: {
message: "Agent calling tool: done",
level: 1,
});
// Use the same Zod version as the user's outputSchema to avoid v3/v4 mixing
const factory = outputSchema
? getZFactory(outputSchema as StagehandZodSchema)
: z;
const baseDoneSchema = buildBaseDoneSchema(factory);

// Merge base done schema with user-provided output schema if present
const doneToolSchema = outputSchema
? baseDoneSchema.extend({
Expand All @@ -50,10 +61,12 @@ export async function handleDoneToolCall(options: {
const outputInstructions = outputSchema
? `\n\nThe user also requested the following information from this task. Provide it in the "output" field:\n${JSON.stringify(
Object.fromEntries(
Object.entries(outputSchema.shape).map(([key, value]) => [
key,
value.description || "no description",
]),
Object.entries(outputSchema.shape).map(
([key, value]: [string, StagehandZodSchema]) => [
key,
value.description || "no description",
],
),
),
null,
2,
Expand Down Expand Up @@ -110,7 +123,9 @@ Call the "done" tool with:
};
}

const input = doneToolCall.input as z.infer<typeof baseDoneSchema> & {
const input = doneToolCall.input as {
reasoning: string;
taskComplete: boolean;
output?: Record<string, unknown>;
};
logger({
Expand Down
Loading