From a1b47bf165024f94a11bd62839a0922ca9be1d09 Mon Sep 17 00:00:00 2001 From: Alex Hancock Date: Tue, 23 Sep 2025 09:30:03 -0400 Subject: [PATCH 1/9] chore(release): release version 1.9.0 --- Cargo.lock | 12 ++++++------ Cargo.toml | 2 +- ui/desktop/package-lock.json | 4 ++-- ui/desktop/package.json | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 22173c044526..ab7af7853814 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2653,7 +2653,7 @@ dependencies = [ [[package]] name = "goose" -version = "1.8.0" +version = "1.9.0" dependencies = [ "ahash", "anyhow", @@ -2727,7 +2727,7 @@ dependencies = [ [[package]] name = "goose-bench" -version = "1.8.0" +version = "1.9.0" dependencies = [ "anyhow", "async-trait", @@ -2750,7 +2750,7 @@ dependencies = [ [[package]] name = "goose-cli" -version = "1.8.0" +version = "1.9.0" dependencies = [ "agent-client-protocol", "anstream", @@ -2804,7 +2804,7 @@ dependencies = [ [[package]] name = "goose-mcp" -version = "1.8.0" +version = "1.9.0" dependencies = [ "anyhow", "async-trait", @@ -2870,7 +2870,7 @@ dependencies = [ [[package]] name = "goose-server" -version = "1.8.0" +version = "1.9.0" dependencies = [ "anyhow", "async-trait", @@ -2909,7 +2909,7 @@ dependencies = [ [[package]] name = "goose-test" -version = "1.8.0" +version = "1.9.0" dependencies = [ "clap", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index ec3aa42bd292..e30ba9ee5a7f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ resolver = "2" [workspace.package] edition = "2021" -version = "1.8.0" +version = "1.9.0" authors = ["Block "] license = "Apache-2.0" repository = "https://github.com/block/goose" diff --git a/ui/desktop/package-lock.json b/ui/desktop/package-lock.json index f77fbb07693c..e89218688f78 100644 --- a/ui/desktop/package-lock.json +++ b/ui/desktop/package-lock.json @@ -1,12 +1,12 @@ { "name": "goose-app", - "version": "1.8.0", + "version": "1.9.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "goose-app", - "version": "1.8.0", + "version": "1.9.0", "license": "Apache-2.0", "dependencies": { "@ai-sdk/openai": "^2.0.14", diff --git a/ui/desktop/package.json b/ui/desktop/package.json index b3b068aad043..18b48ee281db 100644 --- a/ui/desktop/package.json +++ b/ui/desktop/package.json @@ -1,7 +1,7 @@ { "name": "goose-app", "productName": "Goose", - "version": "1.8.0", + "version": "1.9.0", "description": "Goose App", "engines": { "node": "^22.17.1" From cd4cadb1d49aa427cbb239ecd3b7b94799297111 Mon Sep 17 00:00:00 2001 From: Alex Hancock Date: Tue, 23 Sep 2025 09:32:16 -0400 Subject: [PATCH 2/9] chore: update openapi spec version --- ui/desktop/openapi.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/desktop/openapi.json b/ui/desktop/openapi.json index fd2b4b835e3f..43663159126d 100644 --- a/ui/desktop/openapi.json +++ b/ui/desktop/openapi.json @@ -10,7 +10,7 @@ "license": { "name": "Apache-2.0" }, - "version": "1.8.0" + "version": "1.9.0" }, "paths": { "/agent/add_sub_recipes": { From 4f6db756ae7a32e5deea9131c6d7ee6965d7ce32 Mon Sep 17 00:00:00 2001 From: David Katz Date: Mon, 29 Sep 2025 13:38:55 -0400 Subject: [PATCH 3/9] chore(release): release version 1.9.1 --- Cargo.lock | 12 ++++++------ Cargo.toml | 2 +- ui/desktop/package-lock.json | 4 ++-- ui/desktop/package.json | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ab7af7853814..3b85f597991b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2653,7 +2653,7 @@ dependencies = [ [[package]] name = "goose" -version = "1.9.0" +version = "1.9.1" dependencies = [ "ahash", "anyhow", @@ -2727,7 +2727,7 @@ dependencies = [ [[package]] name = "goose-bench" -version = "1.9.0" +version = "1.9.1" dependencies = [ "anyhow", "async-trait", @@ -2750,7 +2750,7 @@ dependencies = [ [[package]] name = "goose-cli" -version = "1.9.0" +version = "1.9.1" dependencies = [ "agent-client-protocol", "anstream", @@ -2804,7 +2804,7 @@ dependencies = [ [[package]] name = "goose-mcp" -version = "1.9.0" +version = "1.9.1" dependencies = [ "anyhow", "async-trait", @@ -2870,7 +2870,7 @@ dependencies = [ [[package]] name = "goose-server" -version = "1.9.0" +version = "1.9.1" dependencies = [ "anyhow", "async-trait", @@ -2909,7 +2909,7 @@ dependencies = [ [[package]] name = "goose-test" -version = "1.9.0" +version = "1.9.1" dependencies = [ "clap", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index e30ba9ee5a7f..ddc42f25c0eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ resolver = "2" [workspace.package] edition = "2021" -version = "1.9.0" +version = "1.9.1" authors = ["Block "] license = "Apache-2.0" repository = "https://github.com/block/goose" diff --git a/ui/desktop/package-lock.json b/ui/desktop/package-lock.json index e89218688f78..c18c4e6f7669 100644 --- a/ui/desktop/package-lock.json +++ b/ui/desktop/package-lock.json @@ -1,12 +1,12 @@ { "name": "goose-app", - "version": "1.9.0", + "version": "1.9.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "goose-app", - "version": "1.9.0", + "version": "1.9.1", "license": "Apache-2.0", "dependencies": { "@ai-sdk/openai": "^2.0.14", diff --git a/ui/desktop/package.json b/ui/desktop/package.json index 18b48ee281db..2846a4dc1c81 100644 --- a/ui/desktop/package.json +++ b/ui/desktop/package.json @@ -1,7 +1,7 @@ { "name": "goose-app", "productName": "Goose", - "version": "1.9.0", + "version": "1.9.1", "description": "Goose App", "engines": { "node": "^22.17.1" From ef0b7456baa8354430f866bc18df2f8c2b58d87a Mon Sep 17 00:00:00 2001 From: David Katz Date: Mon, 29 Sep 2025 17:20:57 -0400 Subject: [PATCH 4/9] Add filtering for agentVisible: false messages on streaming providers (#4847) --- crates/goose/src/context_mgmt/common.rs | 3 +-- crates/goose/src/conversation/mod.rs | 21 ++++++++++++++++- crates/goose/src/providers/base.rs | 23 +++---------------- crates/goose/src/providers/bedrock.rs | 1 + crates/goose/src/providers/claude_code.rs | 2 +- crates/goose/src/providers/cursor_agent.rs | 2 +- .../goose/src/providers/formats/anthropic.rs | 3 +-- .../goose/src/providers/formats/databricks.rs | 2 +- crates/goose/src/providers/formats/google.rs | 1 + crates/goose/src/providers/formats/openai.rs | 2 +- .../goose/src/providers/formats/snowflake.rs | 2 +- crates/goose/src/providers/gemini_cli.rs | 2 +- 12 files changed, 33 insertions(+), 31 deletions(-) diff --git a/crates/goose/src/context_mgmt/common.rs b/crates/goose/src/context_mgmt/common.rs index 8bb5ff005279..106f8a28b8b3 100644 --- a/crates/goose/src/context_mgmt/common.rs +++ b/crates/goose/src/context_mgmt/common.rs @@ -43,10 +43,9 @@ pub fn get_messages_token_counts_async( token_counter: &AsyncTokenCounter, messages: &[Message], ) -> Vec { - // Calculate current token count of each message, use count_chat_tokens to ensure we - // capture the full content of the message, include ToolRequests and ToolResponses messages .iter() + .filter(|m| m.is_agent_visible()) .map(|msg| token_counter.count_chat_tokens("", std::slice::from_ref(msg), &[])) .collect() } diff --git a/crates/goose/src/conversation/mod.rs b/crates/goose/src/conversation/mod.rs index f1dbfa1e75b8..4005d1322e03 100644 --- a/crates/goose/src/conversation/mod.rs +++ b/crates/goose/src/conversation/mod.rs @@ -1,4 +1,4 @@ -use crate::conversation::message::{Message, MessageContent}; +use crate::conversation::message::{Message, MessageContent, MessageMetadata}; use rmcp::model::Role; use serde::{Deserialize, Serialize}; use std::collections::HashSet; @@ -102,6 +102,25 @@ impl Conversation { self.0.clear(); } + pub fn filtered_messages(&self, filter: F) -> Vec + where + F: Fn(&MessageMetadata) -> bool, + { + self.0 + .iter() + .filter(|msg| filter(&msg.metadata)) + .cloned() + .collect() + } + + pub fn agent_visible_messages(&self) -> Vec { + self.filtered_messages(|meta| meta.agent_visible) + } + + pub fn user_visible_messages(&self) -> Vec { + self.filtered_messages(|meta| meta.user_visible) + } + fn validate(self) -> Result { let (_messages, issues) = fix_messages(self.0.clone()); if !issues.is_empty() { diff --git a/crates/goose/src/providers/base.rs b/crates/goose/src/providers/base.rs index 3cc034269156..14c03911d325 100644 --- a/crates/goose/src/providers/base.rs +++ b/crates/goose/src/providers/base.rs @@ -328,7 +328,6 @@ pub trait Provider: Send + Sync { ) -> Result<(Message, ProviderUsage), ProviderError>; // Default implementation: use the provider's configured model - // This method filters messages to only include agent_visible ones async fn complete( &self, system: &str, @@ -336,20 +335,11 @@ pub trait Provider: Send + Sync { tools: &[Tool], ) -> Result<(Message, ProviderUsage), ProviderError> { let model_config = self.get_model_config(); - - // Filter messages to only include agent_visible ones - let agent_visible_messages: Vec = messages - .iter() - .filter(|m| m.is_agent_visible()) - .cloned() - .collect(); - - self.complete_with_model(&model_config, system, &agent_visible_messages, tools) + self.complete_with_model(&model_config, system, messages, tools) .await } // Check if a fast model is configured, otherwise fall back to regular model - // This method filters messages to only include agent_visible ones async fn complete_fast( &self, system: &str, @@ -359,15 +349,8 @@ pub trait Provider: Send + Sync { let model_config = self.get_model_config(); let fast_config = model_config.use_fast_model(); - // Filter messages to only include agent_visible ones - let agent_visible_messages: Vec = messages - .iter() - .filter(|m| m.is_agent_visible()) - .cloned() - .collect(); - match self - .complete_with_model(&fast_config, system, &agent_visible_messages, tools) + .complete_with_model(&fast_config, system, messages, tools) .await { Ok(result) => Ok(result), @@ -379,7 +362,7 @@ pub trait Provider: Send + Sync { e, model_config.model_name ); - self.complete_with_model(&model_config, system, &agent_visible_messages, tools) + self.complete_with_model(&model_config, system, messages, tools) .await } else { Err(e) diff --git a/crates/goose/src/providers/bedrock.rs b/crates/goose/src/providers/bedrock.rs index d91922464beb..4289443327ac 100644 --- a/crates/goose/src/providers/bedrock.rs +++ b/crates/goose/src/providers/bedrock.rs @@ -124,6 +124,7 @@ impl BedrockProvider { .set_messages(Some( messages .iter() + .filter(|m| m.is_agent_visible()) .map(to_bedrock_message) .collect::>()?, )); diff --git a/crates/goose/src/providers/claude_code.rs b/crates/goose/src/providers/claude_code.rs index 3907d9e6cf1c..16c36c23326b 100644 --- a/crates/goose/src/providers/claude_code.rs +++ b/crates/goose/src/providers/claude_code.rs @@ -129,7 +129,7 @@ impl ClaudeCodeProvider { fn messages_to_claude_format(&self, _system: &str, messages: &[Message]) -> Result { let mut claude_messages = Vec::new(); - for message in messages { + for message in messages.iter().filter(|m| m.is_agent_visible()) { let role = match message.role { Role::User => "user", Role::Assistant => "assistant", diff --git a/crates/goose/src/providers/cursor_agent.rs b/crates/goose/src/providers/cursor_agent.rs index a3f0d12858db..fc00521613e0 100644 --- a/crates/goose/src/providers/cursor_agent.rs +++ b/crates/goose/src/providers/cursor_agent.rs @@ -133,7 +133,7 @@ impl CursorAgentProvider { full_prompt.push_str("\n\n"); // Add conversation history - for message in messages { + for message in messages.iter().filter(|m| m.is_agent_visible()) { let role_prefix = match message.role { Role::User => "Human: ", Role::Assistant => "Assistant: ", diff --git a/crates/goose/src/providers/formats/anthropic.rs b/crates/goose/src/providers/formats/anthropic.rs index 0f1ebc726d62..0a49356ef2d3 100644 --- a/crates/goose/src/providers/formats/anthropic.rs +++ b/crates/goose/src/providers/formats/anthropic.rs @@ -32,8 +32,7 @@ const DATA_FIELD: &str = "data"; pub fn format_messages(messages: &[Message]) -> Vec { let mut anthropic_messages = Vec::new(); - // Convert messages to Anthropic format - for message in messages { + for message in messages.iter().filter(|m| m.is_agent_visible()) { let role = match message.role { Role::User => USER_ROLE, Role::Assistant => ASSISTANT_ROLE, diff --git a/crates/goose/src/providers/formats/databricks.rs b/crates/goose/src/providers/formats/databricks.rs index b7472ca23464..6baad2627ca4 100644 --- a/crates/goose/src/providers/formats/databricks.rs +++ b/crates/goose/src/providers/formats/databricks.rs @@ -29,7 +29,7 @@ struct DatabricksMessage { /// even though the message structure is otherwise following openai, the enum switches this fn format_messages(messages: &[Message], image_format: &ImageFormat) -> Vec { let mut result = Vec::new(); - for message in messages { + for message in messages.iter().filter(|m| m.is_agent_visible()) { let mut converted = DatabricksMessage { content: Value::Null, role: match message.role { diff --git a/crates/goose/src/providers/formats/google.rs b/crates/goose/src/providers/formats/google.rs index e4e01e2a221e..4f87be0afadf 100644 --- a/crates/goose/src/providers/formats/google.rs +++ b/crates/goose/src/providers/formats/google.rs @@ -16,6 +16,7 @@ use std::ops::Deref; pub fn format_messages(messages: &[Message]) -> Vec { messages .iter() + .filter(|m| m.is_agent_visible()) .filter(|message| { message .content diff --git a/crates/goose/src/providers/formats/openai.rs b/crates/goose/src/providers/formats/openai.rs index 897293109e73..ed423a3bac0a 100644 --- a/crates/goose/src/providers/formats/openai.rs +++ b/crates/goose/src/providers/formats/openai.rs @@ -59,7 +59,7 @@ struct StreamingChunk { /// even though the message structure is otherwise following openai, the enum switches this pub fn format_messages(messages: &[Message], image_format: &ImageFormat) -> Vec { let mut messages_spec = Vec::new(); - for message in messages { + for message in messages.iter().filter(|m| m.is_agent_visible()) { let mut converted = json!({ "role": message.role }); diff --git a/crates/goose/src/providers/formats/snowflake.rs b/crates/goose/src/providers/formats/snowflake.rs index 2a467050e9fd..80cff46559a3 100644 --- a/crates/goose/src/providers/formats/snowflake.rs +++ b/crates/goose/src/providers/formats/snowflake.rs @@ -13,7 +13,7 @@ pub fn format_messages(messages: &[Message]) -> Vec { let mut snowflake_messages = Vec::new(); // Convert messages to Snowflake format - for message in messages { + for message in messages.iter().filter(|m| m.is_agent_visible()) { let role = match message.role { Role::User => "user", Role::Assistant => "assistant", diff --git a/crates/goose/src/providers/gemini_cli.rs b/crates/goose/src/providers/gemini_cli.rs index 4b14270244fd..35d59fb8b18d 100644 --- a/crates/goose/src/providers/gemini_cli.rs +++ b/crates/goose/src/providers/gemini_cli.rs @@ -140,7 +140,7 @@ impl GeminiCliProvider { full_prompt.push_str("\n\n"); // Add conversation history - for message in messages { + for message in messages.iter().filter(|m| m.is_agent_visible()) { let role_prefix = match message.role { Role::User => "Human: ", Role::Assistant => "Assistant: ", From 7624f6789c3b5cc0074b158e0780800f6b16da9a Mon Sep 17 00:00:00 2001 From: David Katz Date: Fri, 26 Sep 2025 14:23:02 -0400 Subject: [PATCH 5/9] Update databricks flash model (#4836) --- crates/goose/src/providers/databricks.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/goose/src/providers/databricks.rs b/crates/goose/src/providers/databricks.rs index 3792b10ab64a..22506e21f364 100644 --- a/crates/goose/src/providers/databricks.rs +++ b/crates/goose/src/providers/databricks.rs @@ -39,7 +39,7 @@ const DEFAULT_SCOPES: &[&str] = &["all-apis", "offline_access"]; const DEFAULT_TIMEOUT_SECS: u64 = 600; pub const DATABRICKS_DEFAULT_MODEL: &str = "databricks-claude-sonnet-4"; -const DATABRICKS_DEFAULT_FAST_MODEL: &str = "gemini-1-5-flash"; +const DATABRICKS_DEFAULT_FAST_MODEL: &str = "gemini-2-5-flash"; pub const DATABRICKS_KNOWN_MODELS: &[&str] = &[ "databricks-claude-3-7-sonnet", "databricks-meta-llama-3-3-70b-instruct", From 1943ff0114b7016d024b41eba049f2cdae3cd012 Mon Sep 17 00:00:00 2001 From: David Katz Date: Fri, 26 Sep 2025 14:02:29 -0400 Subject: [PATCH 6/9] Fix: Token count UI doesn't re-render if it's open. (#4822) --- ui/desktop/src/components/alerts/types.ts | 2 +- .../src/components/bottom_menu/BottomMenuAlertPopover.tsx | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/ui/desktop/src/components/alerts/types.ts b/ui/desktop/src/components/alerts/types.ts index af8867261836..f7aba0815087 100644 --- a/ui/desktop/src/components/alerts/types.ts +++ b/ui/desktop/src/components/alerts/types.ts @@ -20,5 +20,5 @@ export interface Alert { compactButtonDisabled?: boolean; onCompact?: () => void; compactIcon?: React.ReactNode; - autoCompactThreshold?: number; // Add this for showing the auto-compact threshold + autoCompactThreshold?: number; } diff --git a/ui/desktop/src/components/bottom_menu/BottomMenuAlertPopover.tsx b/ui/desktop/src/components/bottom_menu/BottomMenuAlertPopover.tsx index 86a59ec013e8..a7288891f0ca 100644 --- a/ui/desktop/src/components/bottom_menu/BottomMenuAlertPopover.tsx +++ b/ui/desktop/src/components/bottom_menu/BottomMenuAlertPopover.tsx @@ -1,5 +1,6 @@ import { useRef, useEffect, useCallback, useState } from 'react'; import { FaCircle } from 'react-icons/fa'; +import { isEqual } from 'lodash'; import { cn } from '../../utils'; import { Alert, AlertType } from '../alerts'; import { AlertBox } from '../alerts'; @@ -104,10 +105,10 @@ export default function BottomMenuAlertPopover({ alerts }: AlertPopoverProps) { return; } - // Find new or changed alerts + // Find new or changed alerts using deep comparison const changedAlerts = alerts.filter((alert, index) => { const prevAlert = previousAlertsRef.current[index]; - return !prevAlert || prevAlert.type !== alert.type || prevAlert.message !== alert.message; + return !prevAlert || !isEqual(prevAlert, alert); }); previousAlertsRef.current = alerts; From 12a0a4a862d35c391421a0cdf1543313339b4895 Mon Sep 17 00:00:00 2001 From: Jack Amadeo Date: Thu, 25 Sep 2025 21:37:12 -0400 Subject: [PATCH 7/9] fix: pricing integration test (#4837) --- crates/goose/tests/pricing_integration_test.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/goose/tests/pricing_integration_test.rs b/crates/goose/tests/pricing_integration_test.rs index 36fcddbc7cc4..0637389d03be 100644 --- a/crates/goose/tests/pricing_integration_test.rs +++ b/crates/goose/tests/pricing_integration_test.rs @@ -21,7 +21,7 @@ async fn test_pricing_cache_performance() { ("anthropic", "claude-sonnet-4"), ("openai", "gpt-4o"), ("openai", "gpt-4o-mini"), - ("google", "gemini-flash-1.5"), + ("google", "gemini-2.5-flash"), ("anthropic", "claude-opus-4"), ]; From 3438863c2302da8915a2bbe6c81436be66a9965c Mon Sep 17 00:00:00 2001 From: sings-to-bees-on-wednesdays <222684290+sings-to-bees-on-wednesdays@users.noreply.github.com> Date: Mon, 29 Sep 2025 19:26:56 +0000 Subject: [PATCH 8/9] fix(token_counter): fix panic with GitHub Copilot (#4632) Signed-off-by: sings-to-bees-on-wednesdays <222684290+sings-to-bees-on-wednesdays@users.noreply.github.com> main was broken. this seems important --- crates/goose/src/token_counter.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/goose/src/token_counter.rs b/crates/goose/src/token_counter.rs index c95493df519d..31f16a796f96 100644 --- a/crates/goose/src/token_counter.rs +++ b/crates/goose/src/token_counter.rs @@ -90,7 +90,9 @@ impl AsyncTokenCounter { let line = format!("{}:{}", name, description); func_token_count += self.count_tokens(&line); - if let serde_json::Value::Object(properties) = &tool.input_schema["properties"] { + if let Some(serde_json::Value::Object(properties)) = + tool.input_schema.get("properties") + { if !properties.is_empty() { func_token_count += prop_init; for (key, value) in properties { From 6555d4375fcab1d1ad90fe859d86f32d9afe848c Mon Sep 17 00:00:00 2001 From: Zane <75694352+zanesq@users.noreply.github.com> Date: Tue, 30 Sep 2025 10:33:37 -0700 Subject: [PATCH 9/9] fix optional recipe schema zod validation (#4900) --- ui/desktop/src/recipe/validation.ts | 50 ++++++++++++----------------- 1 file changed, 20 insertions(+), 30 deletions(-) diff --git a/ui/desktop/src/recipe/validation.ts b/ui/desktop/src/recipe/validation.ts index b76eae4a9b99..065e2c2f3e41 100644 --- a/ui/desktop/src/recipe/validation.ts +++ b/ui/desktop/src/recipe/validation.ts @@ -187,40 +187,30 @@ function openApiSchemaToZod(schema: Record): z.ZodTypeAny { shape[propName] = openApiSchemaToZod(propSchema as Record); } - // Make optional properties optional - if (schema.required && Array.isArray(schema.required)) { - const optionalShape: Record = {}; - for (const [propName, zodSchema] of Object.entries(shape)) { - if (schema.required.includes(propName)) { - optionalShape[propName] = zodSchema; - } else { - optionalShape[propName] = zodSchema.optional(); - } - } - let objectSchema = z.object(optionalShape); - - if (schema.additionalProperties === true) { - return schema.nullable - ? objectSchema.passthrough().nullable() - : objectSchema.passthrough(); - } else if (schema.additionalProperties === false) { - return schema.nullable ? objectSchema.strict().nullable() : objectSchema.strict(); + // Make optional properties optional based on required array + const optionalShape: Record = {}; + const requiredFields = + schema.required && Array.isArray(schema.required) ? schema.required : []; + + for (const [propName, zodSchema] of Object.entries(shape)) { + if (requiredFields.includes(propName)) { + optionalShape[propName] = zodSchema; + } else { + optionalShape[propName] = zodSchema.optional(); } + } - return schema.nullable ? objectSchema.nullable() : objectSchema; - } else { - let objectSchema = z.object(shape); + let objectSchema = z.object(optionalShape); - if (schema.additionalProperties === true) { - return schema.nullable - ? objectSchema.passthrough().nullable() - : objectSchema.passthrough(); - } else if (schema.additionalProperties === false) { - return schema.nullable ? objectSchema.strict().nullable() : objectSchema.strict(); - } - - return schema.nullable ? objectSchema.nullable() : objectSchema; + if (schema.additionalProperties === true) { + return schema.nullable + ? objectSchema.passthrough().nullable() + : objectSchema.passthrough(); + } else if (schema.additionalProperties === false) { + return schema.nullable ? objectSchema.strict().nullable() : objectSchema.strict(); } + + return schema.nullable ? objectSchema.nullable() : objectSchema; } return schema.nullable ? z.record(z.any()).nullable() : z.record(z.any());