From a9b327f17942b28a16800c3dd93b569bb1bac6dc Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 6 May 2026 17:06:19 +0000 Subject: [PATCH 1/5] feat: [feat]: add `ignoreSelectors` to `extract()` --- .stats.yml | 4 +-- .../Sessions/SessionExtractParamsTest.cs | 30 +++++++++++++++++++ .../Models/Sessions/SessionExtractParams.cs | 26 ++++++++++++++++ 3 files changed, 58 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index b17eb0a..128128c 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 8 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase/stagehand-dbbff1a35360850898f7d60588e257faeac145a73cfcae634cfeb1b70109b6af.yml -openapi_spec_hash: 28c4b734a5309067c39bb4c4b709b9ab +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase/stagehand-7182c741edd5e22cda9bd855d31ca7e60a97a409222bb887edf87b9ce15dd493.yml +openapi_spec_hash: 174581867a9191c491b22855b64c4f19 config_hash: a962ae71493deb11a1c903256fb25386 diff --git a/src/Stagehand.Tests/Models/Sessions/SessionExtractParamsTest.cs b/src/Stagehand.Tests/Models/Sessions/SessionExtractParamsTest.cs index 9c34681..acbc7a0 100644 --- a/src/Stagehand.Tests/Models/Sessions/SessionExtractParamsTest.cs +++ b/src/Stagehand.Tests/Models/Sessions/SessionExtractParamsTest.cs @@ -20,6 +20,7 @@ public void FieldRoundtrip_Works() Instruction = "Extract all product names and prices from the page", Options = new() { + IgnoreSelectors = ["nav", ".cookie-banner", "#sidebar-ads"], Model = new ModelConfig() { ModelName = "openai/gpt-5.4-mini", @@ -43,6 +44,7 @@ public void FieldRoundtrip_Works() string expectedInstruction = "Extract all product names and prices from the page"; SessionExtractParamsOptions expectedOptions = new() { + IgnoreSelectors = ["nav", ".cookie-banner", "#sidebar-ads"], Model = new ModelConfig() { ModelName = "openai/gpt-5.4-mini", @@ -129,6 +131,7 @@ public void OptionalNullableParamsUnsetAreNotSet_Works() Instruction = "Extract all product names and prices from the page", Options = new() { + IgnoreSelectors = ["nav", ".cookie-banner", "#sidebar-ads"], Model = new ModelConfig() { ModelName = "openai/gpt-5.4-mini", @@ -160,6 +163,7 @@ public void OptionalNullableParamsSetToNullAreSetToNull_Works() Instruction = "Extract all product names and prices from the page", Options = new() { + IgnoreSelectors = ["nav", ".cookie-banner", "#sidebar-ads"], Model = new ModelConfig() { ModelName = "openai/gpt-5.4-mini", @@ -241,6 +245,7 @@ public void CopyConstructor_Works() Instruction = "Extract all product names and prices from the page", Options = new() { + IgnoreSelectors = ["nav", ".cookie-banner", "#sidebar-ads"], Model = new ModelConfig() { ModelName = "openai/gpt-5.4-mini", @@ -272,6 +277,7 @@ public void FieldRoundtrip_Works() { var model = new SessionExtractParamsOptions { + IgnoreSelectors = ["nav", ".cookie-banner", "#sidebar-ads"], Model = new ModelConfig() { ModelName = "openai/gpt-5.4-mini", @@ -284,6 +290,7 @@ public void FieldRoundtrip_Works() Timeout = 30000, }; + List expectedIgnoreSelectors = ["nav", ".cookie-banner", "#sidebar-ads"]; SessionExtractParamsOptionsModel expectedModel = new ModelConfig() { ModelName = "openai/gpt-5.4-mini", @@ -295,6 +302,12 @@ public void FieldRoundtrip_Works() string expectedSelector = "#main-content"; double expectedTimeout = 30000; + Assert.NotNull(model.IgnoreSelectors); + Assert.Equal(expectedIgnoreSelectors.Count, model.IgnoreSelectors.Count); + for (int i = 0; i < expectedIgnoreSelectors.Count; i++) + { + Assert.Equal(expectedIgnoreSelectors[i], model.IgnoreSelectors[i]); + } Assert.Equal(expectedModel, model.Model); Assert.Equal(expectedSelector, model.Selector); Assert.Equal(expectedTimeout, model.Timeout); @@ -305,6 +318,7 @@ public void SerializationRoundtrip_Works() { var model = new SessionExtractParamsOptions { + IgnoreSelectors = ["nav", ".cookie-banner", "#sidebar-ads"], Model = new ModelConfig() { ModelName = "openai/gpt-5.4-mini", @@ -331,6 +345,7 @@ public void FieldRoundtripThroughSerialization_Works() { var model = new SessionExtractParamsOptions { + IgnoreSelectors = ["nav", ".cookie-banner", "#sidebar-ads"], Model = new ModelConfig() { ModelName = "openai/gpt-5.4-mini", @@ -350,6 +365,7 @@ public void FieldRoundtripThroughSerialization_Works() ); Assert.NotNull(deserialized); + List expectedIgnoreSelectors = ["nav", ".cookie-banner", "#sidebar-ads"]; SessionExtractParamsOptionsModel expectedModel = new ModelConfig() { ModelName = "openai/gpt-5.4-mini", @@ -361,6 +377,12 @@ public void FieldRoundtripThroughSerialization_Works() string expectedSelector = "#main-content"; double expectedTimeout = 30000; + Assert.NotNull(deserialized.IgnoreSelectors); + Assert.Equal(expectedIgnoreSelectors.Count, deserialized.IgnoreSelectors.Count); + for (int i = 0; i < expectedIgnoreSelectors.Count; i++) + { + Assert.Equal(expectedIgnoreSelectors[i], deserialized.IgnoreSelectors[i]); + } Assert.Equal(expectedModel, deserialized.Model); Assert.Equal(expectedSelector, deserialized.Selector); Assert.Equal(expectedTimeout, deserialized.Timeout); @@ -371,6 +393,7 @@ public void Validation_Works() { var model = new SessionExtractParamsOptions { + IgnoreSelectors = ["nav", ".cookie-banner", "#sidebar-ads"], Model = new ModelConfig() { ModelName = "openai/gpt-5.4-mini", @@ -391,6 +414,8 @@ public void OptionalNonNullablePropertiesUnsetAreNotSet_Works() { var model = new SessionExtractParamsOptions { }; + Assert.Null(model.IgnoreSelectors); + Assert.False(model.RawData.ContainsKey("ignoreSelectors")); Assert.Null(model.Model); Assert.False(model.RawData.ContainsKey("model")); Assert.Null(model.Selector); @@ -413,11 +438,14 @@ public void OptionalNonNullablePropertiesSetToNullAreNotSet_Works() var model = new SessionExtractParamsOptions { // Null should be interpreted as omitted for these properties + IgnoreSelectors = null, Model = null, Selector = null, Timeout = null, }; + Assert.Null(model.IgnoreSelectors); + Assert.False(model.RawData.ContainsKey("ignoreSelectors")); Assert.Null(model.Model); Assert.False(model.RawData.ContainsKey("model")); Assert.Null(model.Selector); @@ -432,6 +460,7 @@ public void OptionalNonNullablePropertiesSetToNullValidation_Works() var model = new SessionExtractParamsOptions { // Null should be interpreted as omitted for these properties + IgnoreSelectors = null, Model = null, Selector = null, Timeout = null, @@ -445,6 +474,7 @@ public void CopyConstructor_Works() { var model = new SessionExtractParamsOptions { + IgnoreSelectors = ["nav", ".cookie-banner", "#sidebar-ads"], Model = new ModelConfig() { ModelName = "openai/gpt-5.4-mini", diff --git a/src/Stagehand/Models/Sessions/SessionExtractParams.cs b/src/Stagehand/Models/Sessions/SessionExtractParams.cs index 461b816..a549d13 100644 --- a/src/Stagehand/Models/Sessions/SessionExtractParams.cs +++ b/src/Stagehand/Models/Sessions/SessionExtractParams.cs @@ -1,5 +1,6 @@ using System.Collections.Frozen; using System.Collections.Generic; +using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Net.Http; using System.Text; @@ -255,6 +256,30 @@ public override int GetHashCode() )] public sealed record class SessionExtractParamsOptions : JsonModel { + /// + /// Selectors for elements and subtrees that should be excluded from extraction + /// + public IReadOnlyList? IgnoreSelectors + { + get + { + this._rawData.Freeze(); + return this._rawData.GetNullableStruct>("ignoreSelectors"); + } + init + { + if (value == null) + { + return; + } + + this._rawData.Set?>( + "ignoreSelectors", + value == null ? null : ImmutableArray.ToImmutableArray(value) + ); + } + } + /// /// Model configuration object or model name string (e.g., 'openai/gpt-5-nano') /// @@ -321,6 +346,7 @@ public double? Timeout /// public override void Validate() { + _ = this.IgnoreSelectors; this.Model?.Validate(); _ = this.Selector; _ = this.Timeout; From 15712866df03d50e920ab9f71e24e30d7b17e455 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 6 May 2026 17:12:04 +0000 Subject: [PATCH 2/5] feat: [STG-1808] Deprecate Browserbase project ID --- .stats.yml | 6 +++--- README.md | 2 +- src/Stagehand/Core/ClientOptions.cs | 14 ++++++-------- src/Stagehand/IStagehandClient.cs | 10 ++++++---- .../Models/Sessions/SessionStartParams.cs | 5 +++++ src/Stagehand/StagehandClient.cs | 4 ++-- 6 files changed, 23 insertions(+), 18 deletions(-) diff --git a/.stats.yml b/.stats.yml index 128128c..d79c632 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 8 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase/stagehand-7182c741edd5e22cda9bd855d31ca7e60a97a409222bb887edf87b9ce15dd493.yml -openapi_spec_hash: 174581867a9191c491b22855b64c4f19 -config_hash: a962ae71493deb11a1c903256fb25386 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase/stagehand-f10429ab9f004c9a7f9f362d7fa82915e0dc04b1ba08a7bc9fd442ed5854cc77.yml +openapi_spec_hash: 818f2e6e7eb5eb6f21f346b0b9828dce +config_hash: 1fb12ae9b478488bc1e56bfbdc210b01 diff --git a/README.md b/README.md index 1655bcb..9ee090f 100644 --- a/README.md +++ b/README.md @@ -397,7 +397,7 @@ See this table for the available options: | Property | Environment variable | Required | Default value | | ---------------------- | ------------------------ | -------- | ----------------------------------------- | | `BrowserbaseApiKey` | `BROWSERBASE_API_KEY` | true | - | -| `BrowserbaseProjectID` | `BROWSERBASE_PROJECT_ID` | true | - | +| `BrowserbaseProjectID` | `BROWSERBASE_PROJECT_ID` | false | - | | `ModelApiKey` | `MODEL_API_KEY` | true | - | | `BaseUrl` | `STAGEHAND_API_URL` | true | `"https://api.stagehand.browserbase.com"` | diff --git a/src/Stagehand/Core/ClientOptions.cs b/src/Stagehand/Core/ClientOptions.cs index 3036b34..3bfc38b 100644 --- a/src/Stagehand/Core/ClientOptions.cs +++ b/src/Stagehand/Core/ClientOptions.cs @@ -107,20 +107,18 @@ public string BrowserbaseApiKey } /// - /// Your [Browserbase Project ID](https://www.browserbase.com/settings) + /// Deprecated. Browserbase API keys are now project-scoped, so this value is + /// no longer required. /// - Lazy _browserbaseProjectID = new(() => + Lazy _browserbaseProjectID = new(() => Environment.GetEnvironmentVariable("BROWSERBASE_PROJECT_ID") - ?? throw new StagehandInvalidDataException( - string.Format("{0} cannot be null", nameof(BrowserbaseProjectID)), - new ArgumentNullException(nameof(BrowserbaseProjectID)) - ) ); /// - /// Your [Browserbase Project ID](https://www.browserbase.com/settings) + /// Deprecated. Browserbase API keys are now project-scoped, so this value is + /// no longer required. /// - public string BrowserbaseProjectID + public string? BrowserbaseProjectID { readonly get { return _browserbaseProjectID.Value; } set { _browserbaseProjectID = new(() => value); } diff --git a/src/Stagehand/IStagehandClient.cs b/src/Stagehand/IStagehandClient.cs index 8784d65..6e5bc12 100644 --- a/src/Stagehand/IStagehandClient.cs +++ b/src/Stagehand/IStagehandClient.cs @@ -42,9 +42,10 @@ public interface IStagehandClient : IDisposable string BrowserbaseApiKey { get; init; } /// - /// Your [Browserbase Project ID](https://www.browserbase.com/settings) + /// Deprecated. Browserbase API keys are now project-scoped, so this value is + /// no longer required. /// - string BrowserbaseProjectID { get; init; } + string? BrowserbaseProjectID { get; init; } /// /// Your LLM provider API key (e.g. OPENAI_API_KEY, ANTHROPIC_API_KEY, etc.) @@ -93,9 +94,10 @@ public interface IStagehandClientWithRawResponse : IDisposable string BrowserbaseApiKey { get; init; } /// - /// Your [Browserbase Project ID](https://www.browserbase.com/settings) + /// Deprecated. Browserbase API keys are now project-scoped, so this value is + /// no longer required. /// - string BrowserbaseProjectID { get; init; } + string? BrowserbaseProjectID { get; init; } /// /// Your LLM provider API key (e.g. OPENAI_API_KEY, ANTHROPIC_API_KEY, etc.) diff --git a/src/Stagehand/Models/Sessions/SessionStartParams.cs b/src/Stagehand/Models/Sessions/SessionStartParams.cs index b89f2df..f1381e3 100644 --- a/src/Stagehand/Models/Sessions/SessionStartParams.cs +++ b/src/Stagehand/Models/Sessions/SessionStartParams.cs @@ -1451,6 +1451,11 @@ public bool? KeepAlive } } + /// + /// Deprecated. Browserbase API keys are now project-scoped, so this field is + /// no longer required. + /// + [System::Obsolete("deprecated")] public string? ProjectID { get diff --git a/src/Stagehand/StagehandClient.cs b/src/Stagehand/StagehandClient.cs index 777bc7d..e2cad9e 100644 --- a/src/Stagehand/StagehandClient.cs +++ b/src/Stagehand/StagehandClient.cs @@ -59,7 +59,7 @@ public string BrowserbaseApiKey } /// - public string BrowserbaseProjectID + public string? BrowserbaseProjectID { get { return this._options.BrowserbaseProjectID; } init { this._options.BrowserbaseProjectID = value; } @@ -168,7 +168,7 @@ public string BrowserbaseApiKey } /// - public string BrowserbaseProjectID + public string? BrowserbaseProjectID { get { return this._options.BrowserbaseProjectID; } init { this._options.BrowserbaseProjectID = value; } From 1c3a03c6961d3c4fab99e2852ce0ea4dc4778444 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 6 May 2026 18:52:34 +0000 Subject: [PATCH 3/5] feat: remove experimental requirement on agent variables (#2079) --- .stats.yml | 4 +- .../Sessions/SessionExecuteParamsTest.cs | 354 ++++++++ .../Services/SessionServiceTest.cs | 8 + .../Models/Sessions/SessionExecuteParams.cs | 785 ++++++++++++++++++ 4 files changed, 1149 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index d79c632..391cde3 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 8 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase/stagehand-f10429ab9f004c9a7f9f362d7fa82915e0dc04b1ba08a7bc9fd442ed5854cc77.yml -openapi_spec_hash: 818f2e6e7eb5eb6f21f346b0b9828dce +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase/stagehand-6f6bfb81d092f30a5e2005328c97d61b9ea36132bb19e9e79e55294b9534ce20.yml +openapi_spec_hash: f3fc1e3688a38dc2c28f7178f7d534e5 config_hash: 1fb12ae9b478488bc1e56bfbdc210b01 diff --git a/src/Stagehand.Tests/Models/Sessions/SessionExecuteParamsTest.cs b/src/Stagehand.Tests/Models/Sessions/SessionExecuteParamsTest.cs index fbb252a..20aaaca 100644 --- a/src/Stagehand.Tests/Models/Sessions/SessionExecuteParamsTest.cs +++ b/src/Stagehand.Tests/Models/Sessions/SessionExecuteParamsTest.cs @@ -47,6 +47,10 @@ public void FieldRoundtrip_Works() MaxSteps = 20, ToolTimeout = 30000, UseSearch = true, + Variables = new Dictionary() + { + { "foo", "string" }, + }, }, FrameID = "frameId", ShouldCache = true, @@ -85,6 +89,7 @@ public void FieldRoundtrip_Works() MaxSteps = 20, ToolTimeout = 30000, UseSearch = true, + Variables = new Dictionary() { { "foo", "string" } }, }; string expectedFrameID = "frameId"; bool expectedShouldCache = true; @@ -136,6 +141,10 @@ public void OptionalNonNullableParamsUnsetAreNotSet_Works() MaxSteps = 20, ToolTimeout = 30000, UseSearch = true, + Variables = new Dictionary() + { + { "foo", "string" }, + }, }, FrameID = "frameId", }; @@ -183,6 +192,10 @@ public void OptionalNonNullableParamsSetToNullAreNotSet_Works() MaxSteps = 20, ToolTimeout = 30000, UseSearch = true, + Variables = new Dictionary() + { + { "foo", "string" }, + }, }, FrameID = "frameId", @@ -234,6 +247,10 @@ public void OptionalNullableParamsUnsetAreNotSet_Works() MaxSteps = 20, ToolTimeout = 30000, UseSearch = true, + Variables = new Dictionary() + { + { "foo", "string" }, + }, }, ShouldCache = true, XStreamResponse = SessionExecuteParamsXStreamResponse.True, @@ -280,6 +297,10 @@ public void OptionalNullableParamsSetToNullAreSetToNull_Works() MaxSteps = 20, ToolTimeout = 30000, UseSearch = true, + Variables = new Dictionary() + { + { "foo", "string" }, + }, }, ShouldCache = true, XStreamResponse = SessionExecuteParamsXStreamResponse.True, @@ -328,6 +349,10 @@ public void Url_Works() MaxSteps = 20, ToolTimeout = 30000, UseSearch = true, + Variables = new Dictionary() + { + { "foo", "string" }, + }, }, }; @@ -388,6 +413,10 @@ public void AddHeadersToRequest_Works() MaxSteps = 20, ToolTimeout = 30000, UseSearch = true, + Variables = new Dictionary() + { + { "foo", "string" }, + }, }, XStreamResponse = SessionExecuteParamsXStreamResponse.True, }; @@ -442,6 +471,10 @@ public void CopyConstructor_Works() MaxSteps = 20, ToolTimeout = 30000, UseSearch = true, + Variables = new Dictionary() + { + { "foo", "string" }, + }, }, FrameID = "frameId", ShouldCache = true, @@ -997,6 +1030,7 @@ public void FieldRoundtrip_Works() MaxSteps = 20, ToolTimeout = 30000, UseSearch = true, + Variables = new Dictionary() { { "foo", "string" } }, }; string expectedInstruction = @@ -1005,12 +1039,24 @@ public void FieldRoundtrip_Works() double expectedMaxSteps = 20; double expectedToolTimeout = 30000; bool expectedUseSearch = true; + Dictionary expectedVariables = new() + { + { "foo", "string" }, + }; Assert.Equal(expectedInstruction, model.Instruction); Assert.Equal(expectedHighlightCursor, model.HighlightCursor); Assert.Equal(expectedMaxSteps, model.MaxSteps); Assert.Equal(expectedToolTimeout, model.ToolTimeout); Assert.Equal(expectedUseSearch, model.UseSearch); + Assert.NotNull(model.Variables); + Assert.Equal(expectedVariables.Count, model.Variables.Count); + foreach (var item in expectedVariables) + { + Assert.True(model.Variables.TryGetValue(item.Key, out var value)); + + Assert.Equal(value, model.Variables[item.Key]); + } } [Fact] @@ -1024,6 +1070,7 @@ public void SerializationRoundtrip_Works() MaxSteps = 20, ToolTimeout = 30000, UseSearch = true, + Variables = new Dictionary() { { "foo", "string" } }, }; string json = JsonSerializer.Serialize(model, ModelBase.SerializerOptions); @@ -1046,6 +1093,7 @@ public void FieldRoundtripThroughSerialization_Works() MaxSteps = 20, ToolTimeout = 30000, UseSearch = true, + Variables = new Dictionary() { { "foo", "string" } }, }; string element = JsonSerializer.Serialize(model, ModelBase.SerializerOptions); @@ -1061,12 +1109,24 @@ public void FieldRoundtripThroughSerialization_Works() double expectedMaxSteps = 20; double expectedToolTimeout = 30000; bool expectedUseSearch = true; + Dictionary expectedVariables = new() + { + { "foo", "string" }, + }; Assert.Equal(expectedInstruction, deserialized.Instruction); Assert.Equal(expectedHighlightCursor, deserialized.HighlightCursor); Assert.Equal(expectedMaxSteps, deserialized.MaxSteps); Assert.Equal(expectedToolTimeout, deserialized.ToolTimeout); Assert.Equal(expectedUseSearch, deserialized.UseSearch); + Assert.NotNull(deserialized.Variables); + Assert.Equal(expectedVariables.Count, deserialized.Variables.Count); + foreach (var item in expectedVariables) + { + Assert.True(deserialized.Variables.TryGetValue(item.Key, out var value)); + + Assert.Equal(value, deserialized.Variables[item.Key]); + } } [Fact] @@ -1080,6 +1140,7 @@ public void Validation_Works() MaxSteps = 20, ToolTimeout = 30000, UseSearch = true, + Variables = new Dictionary() { { "foo", "string" } }, }; model.Validate(); @@ -1102,6 +1163,8 @@ public void OptionalNonNullablePropertiesUnsetAreNotSet_Works() Assert.False(model.RawData.ContainsKey("toolTimeout")); Assert.Null(model.UseSearch); Assert.False(model.RawData.ContainsKey("useSearch")); + Assert.Null(model.Variables); + Assert.False(model.RawData.ContainsKey("variables")); } [Fact] @@ -1129,6 +1192,7 @@ public void OptionalNonNullablePropertiesSetToNullAreNotSet_Works() MaxSteps = null, ToolTimeout = null, UseSearch = null, + Variables = null, }; Assert.Null(model.HighlightCursor); @@ -1139,6 +1203,8 @@ public void OptionalNonNullablePropertiesSetToNullAreNotSet_Works() Assert.False(model.RawData.ContainsKey("toolTimeout")); Assert.Null(model.UseSearch); Assert.False(model.RawData.ContainsKey("useSearch")); + Assert.Null(model.Variables); + Assert.False(model.RawData.ContainsKey("variables")); } [Fact] @@ -1154,6 +1220,7 @@ public void OptionalNonNullablePropertiesSetToNullValidation_Works() MaxSteps = null, ToolTimeout = null, UseSearch = null, + Variables = null, }; model.Validate(); @@ -1170,6 +1237,7 @@ public void CopyConstructor_Works() MaxSteps = 20, ToolTimeout = 30000, UseSearch = true, + Variables = new Dictionary() { { "foo", "string" } }, }; ExecuteOptions copied = new(model); @@ -1178,6 +1246,292 @@ public void CopyConstructor_Works() } } +public class ExecuteOptionsVariableTest : TestBase +{ + [Fact] + public void StringValidationWorks() + { + ExecuteOptionsVariable value = "string"; + value.Validate(); + } + + [Fact] + public void DoubleValidationWorks() + { + ExecuteOptionsVariable value = 0; + value.Validate(); + } + + [Fact] + public void BoolValidationWorks() + { + ExecuteOptionsVariable value = true; + value.Validate(); + } + + [Fact] + public void ExecuteOptionsVariableUnionMember3ValidationWorks() + { + ExecuteOptionsVariable value = new ExecuteOptionsVariableUnionMember3() + { + Value = "string", + Description = "description", + }; + value.Validate(); + } + + [Fact] + public void StringSerializationRoundtripWorks() + { + ExecuteOptionsVariable value = "string"; + string element = JsonSerializer.Serialize(value, ModelBase.SerializerOptions); + var deserialized = JsonSerializer.Deserialize( + element, + ModelBase.SerializerOptions + ); + + Assert.Equal(value, deserialized); + } + + [Fact] + public void DoubleSerializationRoundtripWorks() + { + ExecuteOptionsVariable value = 0; + string element = JsonSerializer.Serialize(value, ModelBase.SerializerOptions); + var deserialized = JsonSerializer.Deserialize( + element, + ModelBase.SerializerOptions + ); + + Assert.Equal(value, deserialized); + } + + [Fact] + public void BoolSerializationRoundtripWorks() + { + ExecuteOptionsVariable value = true; + string element = JsonSerializer.Serialize(value, ModelBase.SerializerOptions); + var deserialized = JsonSerializer.Deserialize( + element, + ModelBase.SerializerOptions + ); + + Assert.Equal(value, deserialized); + } + + [Fact] + public void ExecuteOptionsVariableUnionMember3SerializationRoundtripWorks() + { + ExecuteOptionsVariable value = new ExecuteOptionsVariableUnionMember3() + { + Value = "string", + Description = "description", + }; + string element = JsonSerializer.Serialize(value, ModelBase.SerializerOptions); + var deserialized = JsonSerializer.Deserialize( + element, + ModelBase.SerializerOptions + ); + + Assert.Equal(value, deserialized); + } +} + +public class ExecuteOptionsVariableUnionMember3Test : TestBase +{ + [Fact] + public void FieldRoundtrip_Works() + { + var model = new ExecuteOptionsVariableUnionMember3 + { + Value = "string", + Description = "description", + }; + + ExecuteOptionsVariableUnionMember3Value expectedValue = "string"; + string expectedDescription = "description"; + + Assert.Equal(expectedValue, model.Value); + Assert.Equal(expectedDescription, model.Description); + } + + [Fact] + public void SerializationRoundtrip_Works() + { + var model = new ExecuteOptionsVariableUnionMember3 + { + Value = "string", + Description = "description", + }; + + string json = JsonSerializer.Serialize(model, ModelBase.SerializerOptions); + var deserialized = JsonSerializer.Deserialize( + json, + ModelBase.SerializerOptions + ); + + Assert.Equal(model, deserialized); + } + + [Fact] + public void FieldRoundtripThroughSerialization_Works() + { + var model = new ExecuteOptionsVariableUnionMember3 + { + Value = "string", + Description = "description", + }; + + string element = JsonSerializer.Serialize(model, ModelBase.SerializerOptions); + var deserialized = JsonSerializer.Deserialize( + element, + ModelBase.SerializerOptions + ); + Assert.NotNull(deserialized); + + ExecuteOptionsVariableUnionMember3Value expectedValue = "string"; + string expectedDescription = "description"; + + Assert.Equal(expectedValue, deserialized.Value); + Assert.Equal(expectedDescription, deserialized.Description); + } + + [Fact] + public void Validation_Works() + { + var model = new ExecuteOptionsVariableUnionMember3 + { + Value = "string", + Description = "description", + }; + + model.Validate(); + } + + [Fact] + public void OptionalNonNullablePropertiesUnsetAreNotSet_Works() + { + var model = new ExecuteOptionsVariableUnionMember3 { Value = "string" }; + + Assert.Null(model.Description); + Assert.False(model.RawData.ContainsKey("description")); + } + + [Fact] + public void OptionalNonNullablePropertiesUnsetValidation_Works() + { + var model = new ExecuteOptionsVariableUnionMember3 { Value = "string" }; + + model.Validate(); + } + + [Fact] + public void OptionalNonNullablePropertiesSetToNullAreNotSet_Works() + { + var model = new ExecuteOptionsVariableUnionMember3 + { + Value = "string", + + // Null should be interpreted as omitted for these properties + Description = null, + }; + + Assert.Null(model.Description); + Assert.False(model.RawData.ContainsKey("description")); + } + + [Fact] + public void OptionalNonNullablePropertiesSetToNullValidation_Works() + { + var model = new ExecuteOptionsVariableUnionMember3 + { + Value = "string", + + // Null should be interpreted as omitted for these properties + Description = null, + }; + + model.Validate(); + } + + [Fact] + public void CopyConstructor_Works() + { + var model = new ExecuteOptionsVariableUnionMember3 + { + Value = "string", + Description = "description", + }; + + ExecuteOptionsVariableUnionMember3 copied = new(model); + + Assert.Equal(model, copied); + } +} + +public class ExecuteOptionsVariableUnionMember3ValueTest : TestBase +{ + [Fact] + public void StringValidationWorks() + { + ExecuteOptionsVariableUnionMember3Value value = "string"; + value.Validate(); + } + + [Fact] + public void DoubleValidationWorks() + { + ExecuteOptionsVariableUnionMember3Value value = 0; + value.Validate(); + } + + [Fact] + public void BoolValidationWorks() + { + ExecuteOptionsVariableUnionMember3Value value = true; + value.Validate(); + } + + [Fact] + public void StringSerializationRoundtripWorks() + { + ExecuteOptionsVariableUnionMember3Value value = "string"; + string element = JsonSerializer.Serialize(value, ModelBase.SerializerOptions); + var deserialized = JsonSerializer.Deserialize( + element, + ModelBase.SerializerOptions + ); + + Assert.Equal(value, deserialized); + } + + [Fact] + public void DoubleSerializationRoundtripWorks() + { + ExecuteOptionsVariableUnionMember3Value value = 0; + string element = JsonSerializer.Serialize(value, ModelBase.SerializerOptions); + var deserialized = JsonSerializer.Deserialize( + element, + ModelBase.SerializerOptions + ); + + Assert.Equal(value, deserialized); + } + + [Fact] + public void BoolSerializationRoundtripWorks() + { + ExecuteOptionsVariableUnionMember3Value value = true; + string element = JsonSerializer.Serialize(value, ModelBase.SerializerOptions); + var deserialized = JsonSerializer.Deserialize( + element, + ModelBase.SerializerOptions + ); + + Assert.Equal(value, deserialized); + } +} + public class SessionExecuteParamsXStreamResponseTest : TestBase { [Theory] diff --git a/src/Stagehand.Tests/Services/SessionServiceTest.cs b/src/Stagehand.Tests/Services/SessionServiceTest.cs index 779dafa..bf5d2b0 100644 --- a/src/Stagehand.Tests/Services/SessionServiceTest.cs +++ b/src/Stagehand.Tests/Services/SessionServiceTest.cs @@ -81,6 +81,10 @@ public async Task Execute_Works() MaxSteps = 20, ToolTimeout = 30000, UseSearch = true, + Variables = new Dictionary() + { + { "foo", "string" }, + }, }, }, TestContext.Current.CancellationToken @@ -126,6 +130,10 @@ public async Task ExecuteStreaming_Works() MaxSteps = 20, ToolTimeout = 30000, UseSearch = true, + Variables = new Dictionary() + { + { "foo", "string" }, + }, }, }, TestContext.Current.CancellationToken diff --git a/src/Stagehand/Models/Sessions/SessionExecuteParams.cs b/src/Stagehand/Models/Sessions/SessionExecuteParams.cs index b4749b8..7d2c67f 100644 --- a/src/Stagehand/Models/Sessions/SessionExecuteParams.cs +++ b/src/Stagehand/Models/Sessions/SessionExecuteParams.cs @@ -1105,6 +1105,32 @@ public bool? UseSearch } } + /// + /// Variables available to the agent via %variableName% syntax in supported tools + /// + public IReadOnlyDictionary? Variables + { + get + { + this._rawData.Freeze(); + return this._rawData.GetNullableClass>( + "variables" + ); + } + init + { + if (value == null) + { + return; + } + + this._rawData.Set?>( + "variables", + value == null ? null : FrozenDictionary.ToFrozenDictionary(value) + ); + } + } + /// public override void Validate() { @@ -1113,6 +1139,13 @@ public override void Validate() _ = this.MaxSteps; _ = this.ToolTimeout; _ = this.UseSearch; + if (this.Variables != null) + { + foreach (var item in this.Variables.Values) + { + item.Validate(); + } + } } public ExecuteOptions() { } @@ -1157,6 +1190,758 @@ public ExecuteOptions FromRawUnchecked(IReadOnlyDictionary ExecuteOptions.FromRawUnchecked(rawData); } +[JsonConverter(typeof(ExecuteOptionsVariableConverter))] +public record class ExecuteOptionsVariable : ModelBase +{ + public object? Value { get; } = null; + + JsonElement? _element = null; + + public JsonElement Json + { + get + { + return this._element ??= JsonSerializer.SerializeToElement( + this.Value, + ModelBase.SerializerOptions + ); + } + } + + public ExecuteOptionsVariable(string value, JsonElement? element = null) + { + this.Value = value; + this._element = element; + } + + public ExecuteOptionsVariable(double value, JsonElement? element = null) + { + this.Value = value; + this._element = element; + } + + public ExecuteOptionsVariable(bool value, JsonElement? element = null) + { + this.Value = value; + this._element = element; + } + + public ExecuteOptionsVariable( + ExecuteOptionsVariableUnionMember3 value, + JsonElement? element = null + ) + { + this.Value = value; + this._element = element; + } + + public ExecuteOptionsVariable(JsonElement element) + { + this._element = element; + } + + /// + /// Returns true and sets the out parameter if the instance was constructed with a variant of + /// type . + /// + /// Consider using or if you need to handle every variant. + /// + /// + /// + /// if (instance.TryPickString(out var value)) { + /// // `value` is of type `string` + /// Console.WriteLine(value); + /// } + /// + /// + /// + public bool TryPickString([NotNullWhen(true)] out string? value) + { + value = this.Value as string; + return value != null; + } + + /// + /// Returns true and sets the out parameter if the instance was constructed with a variant of + /// type . + /// + /// Consider using or if you need to handle every variant. + /// + /// + /// + /// if (instance.TryPickDouble(out var value)) { + /// // `value` is of type `double` + /// Console.WriteLine(value); + /// } + /// + /// + /// + public bool TryPickDouble([NotNullWhen(true)] out double? value) + { + value = this.Value as double?; + return value != null; + } + + /// + /// Returns true and sets the out parameter if the instance was constructed with a variant of + /// type . + /// + /// Consider using or if you need to handle every variant. + /// + /// + /// + /// if (instance.TryPickBool(out var value)) { + /// // `value` is of type `bool` + /// Console.WriteLine(value); + /// } + /// + /// + /// + public bool TryPickBool([NotNullWhen(true)] out bool? value) + { + value = this.Value as bool?; + return value != null; + } + + /// + /// Returns true and sets the out parameter if the instance was constructed with a variant of + /// type . + /// + /// Consider using or if you need to handle every variant. + /// + /// + /// + /// if (instance.TryPickExecuteOptionsVariableUnionMember3(out var value)) { + /// // `value` is of type `ExecuteOptionsVariableUnionMember3` + /// Console.WriteLine(value); + /// } + /// + /// + /// + public bool TryPickExecuteOptionsVariableUnionMember3( + [NotNullWhen(true)] out ExecuteOptionsVariableUnionMember3? value + ) + { + value = this.Value as ExecuteOptionsVariableUnionMember3; + return value != null; + } + + /// + /// Calls the function parameter corresponding to the variant the instance was constructed with. + /// + /// Use the TryPick method(s) if you don't need to handle every variant, or + /// if you need your function parameters to return something. + /// + /// + /// Thrown when the instance was constructed with an unknown variant (e.g. deserialized from raw data + /// that doesn't match any variant's expected shape). + /// + /// + /// + /// + /// instance.Switch( + /// (string value) => {...}, + /// (double value) => {...}, + /// (bool value) => {...}, + /// (ExecuteOptionsVariableUnionMember3 value) => {...} + /// ); + /// + /// + /// + public void Switch( + System::Action @string, + System::Action @double, + System::Action @bool, + System::Action executeOptionsVariableUnionMember3 + ) + { + switch (this.Value) + { + case string value: + @string(value); + break; + case double value: + @double(value); + break; + case bool value: + @bool(value); + break; + case ExecuteOptionsVariableUnionMember3 value: + executeOptionsVariableUnionMember3(value); + break; + default: + throw new StagehandInvalidDataException( + "Data did not match any variant of ExecuteOptionsVariable" + ); + } + } + + /// + /// Calls the function parameter corresponding to the variant the instance was constructed with and + /// returns its result. + /// + /// Use the TryPick method(s) if you don't need to handle every variant, or + /// if you don't need your function parameters to return a value. + /// + /// + /// Thrown when the instance was constructed with an unknown variant (e.g. deserialized from raw data + /// that doesn't match any variant's expected shape). + /// + /// + /// + /// + /// var result = instance.Match( + /// (string value) => {...}, + /// (double value) => {...}, + /// (bool value) => {...}, + /// (ExecuteOptionsVariableUnionMember3 value) => {...} + /// ); + /// + /// + /// + public T Match( + System::Func @string, + System::Func @double, + System::Func @bool, + System::Func executeOptionsVariableUnionMember3 + ) + { + return this.Value switch + { + string value => @string(value), + double value => @double(value), + bool value => @bool(value), + ExecuteOptionsVariableUnionMember3 value => executeOptionsVariableUnionMember3(value), + _ => throw new StagehandInvalidDataException( + "Data did not match any variant of ExecuteOptionsVariable" + ), + }; + } + + public static implicit operator ExecuteOptionsVariable(string value) => new(value); + + public static implicit operator ExecuteOptionsVariable(double value) => new(value); + + public static implicit operator ExecuteOptionsVariable(bool value) => new(value); + + public static implicit operator ExecuteOptionsVariable( + ExecuteOptionsVariableUnionMember3 value + ) => new(value); + + /// + /// Validates that the instance was constructed with a known variant and that this variant is valid + /// (based on its own Validate method). + /// + /// This is useful for instances constructed from raw JSON data (e.g. deserialized from an API response). + /// + /// + /// Thrown when the instance does not pass validation. + /// + /// + public override void Validate() + { + if (this.Value == null) + { + throw new StagehandInvalidDataException( + "Data did not match any variant of ExecuteOptionsVariable" + ); + } + this.Switch( + (_) => { }, + (_) => { }, + (_) => { }, + (executeOptionsVariableUnionMember3) => executeOptionsVariableUnionMember3.Validate() + ); + } + + public virtual bool Equals(ExecuteOptionsVariable? other) => + other != null + && this.VariantIndex() == other.VariantIndex() + && JsonElement.DeepEquals(this.Json, other.Json); + + public override int GetHashCode() + { + return 0; + } + + public override string ToString() => + JsonSerializer.Serialize( + FriendlyJsonPrinter.PrintValue(this.Json), + ModelBase.ToStringSerializerOptions + ); + + int VariantIndex() + { + return this.Value switch + { + string _ => 0, + double _ => 1, + bool _ => 2, + ExecuteOptionsVariableUnionMember3 _ => 3, + _ => -1, + }; + } +} + +sealed class ExecuteOptionsVariableConverter : JsonConverter +{ + public override ExecuteOptionsVariable? Read( + ref Utf8JsonReader reader, + System::Type typeToConvert, + JsonSerializerOptions options + ) + { + var element = JsonSerializer.Deserialize(ref reader, options); + try + { + var deserialized = JsonSerializer.Deserialize( + element, + options + ); + if (deserialized != null) + { + deserialized.Validate(); + return new(deserialized, element); + } + } + catch (System::Exception e) when (e is JsonException || e is StagehandInvalidDataException) + { + // ignore + } + + try + { + var deserialized = JsonSerializer.Deserialize(element, options); + if (deserialized != null) + { + return new(deserialized, element); + } + } + catch (System::Exception e) when (e is JsonException || e is StagehandInvalidDataException) + { + // ignore + } + + try + { + return new(JsonSerializer.Deserialize(element, options), element); + } + catch (System::Exception e) when (e is JsonException || e is StagehandInvalidDataException) + { + // ignore + } + + try + { + return new(JsonSerializer.Deserialize(element, options), element); + } + catch (System::Exception e) when (e is JsonException || e is StagehandInvalidDataException) + { + // ignore + } + + return new(element); + } + + public override void Write( + Utf8JsonWriter writer, + ExecuteOptionsVariable value, + JsonSerializerOptions options + ) + { + JsonSerializer.Serialize(writer, value.Json, options); + } +} + +[JsonConverter( + typeof(JsonModelConverter< + ExecuteOptionsVariableUnionMember3, + ExecuteOptionsVariableUnionMember3FromRaw + >) +)] +public sealed record class ExecuteOptionsVariableUnionMember3 : JsonModel +{ + public required ExecuteOptionsVariableUnionMember3Value Value + { + get + { + this._rawData.Freeze(); + return this._rawData.GetNotNullClass("value"); + } + init { this._rawData.Set("value", value); } + } + + public string? Description + { + get + { + this._rawData.Freeze(); + return this._rawData.GetNullableClass("description"); + } + init + { + if (value == null) + { + return; + } + + this._rawData.Set("description", value); + } + } + + /// + public override void Validate() + { + this.Value.Validate(); + _ = this.Description; + } + + public ExecuteOptionsVariableUnionMember3() { } + +#pragma warning disable CS8618 + [SetsRequiredMembers] + public ExecuteOptionsVariableUnionMember3( + ExecuteOptionsVariableUnionMember3 executeOptionsVariableUnionMember3 + ) + : base(executeOptionsVariableUnionMember3) { } +#pragma warning restore CS8618 + + public ExecuteOptionsVariableUnionMember3(IReadOnlyDictionary rawData) + { + this._rawData = new(rawData); + } + +#pragma warning disable CS8618 + [SetsRequiredMembers] + ExecuteOptionsVariableUnionMember3(FrozenDictionary rawData) + { + this._rawData = new(rawData); + } +#pragma warning restore CS8618 + + /// + public static ExecuteOptionsVariableUnionMember3 FromRawUnchecked( + IReadOnlyDictionary rawData + ) + { + return new(FrozenDictionary.ToFrozenDictionary(rawData)); + } + + [SetsRequiredMembers] + public ExecuteOptionsVariableUnionMember3(ExecuteOptionsVariableUnionMember3Value value) + : this() + { + this.Value = value; + } +} + +class ExecuteOptionsVariableUnionMember3FromRaw : IFromRawJson +{ + /// + public ExecuteOptionsVariableUnionMember3 FromRawUnchecked( + IReadOnlyDictionary rawData + ) => ExecuteOptionsVariableUnionMember3.FromRawUnchecked(rawData); +} + +[JsonConverter(typeof(ExecuteOptionsVariableUnionMember3ValueConverter))] +public record class ExecuteOptionsVariableUnionMember3Value : ModelBase +{ + public object? Value { get; } = null; + + JsonElement? _element = null; + + public JsonElement Json + { + get + { + return this._element ??= JsonSerializer.SerializeToElement( + this.Value, + ModelBase.SerializerOptions + ); + } + } + + public ExecuteOptionsVariableUnionMember3Value(string value, JsonElement? element = null) + { + this.Value = value; + this._element = element; + } + + public ExecuteOptionsVariableUnionMember3Value(double value, JsonElement? element = null) + { + this.Value = value; + this._element = element; + } + + public ExecuteOptionsVariableUnionMember3Value(bool value, JsonElement? element = null) + { + this.Value = value; + this._element = element; + } + + public ExecuteOptionsVariableUnionMember3Value(JsonElement element) + { + this._element = element; + } + + /// + /// Returns true and sets the out parameter if the instance was constructed with a variant of + /// type . + /// + /// Consider using or if you need to handle every variant. + /// + /// + /// + /// if (instance.TryPickString(out var value)) { + /// // `value` is of type `string` + /// Console.WriteLine(value); + /// } + /// + /// + /// + public bool TryPickString([NotNullWhen(true)] out string? value) + { + value = this.Value as string; + return value != null; + } + + /// + /// Returns true and sets the out parameter if the instance was constructed with a variant of + /// type . + /// + /// Consider using or if you need to handle every variant. + /// + /// + /// + /// if (instance.TryPickDouble(out var value)) { + /// // `value` is of type `double` + /// Console.WriteLine(value); + /// } + /// + /// + /// + public bool TryPickDouble([NotNullWhen(true)] out double? value) + { + value = this.Value as double?; + return value != null; + } + + /// + /// Returns true and sets the out parameter if the instance was constructed with a variant of + /// type . + /// + /// Consider using or if you need to handle every variant. + /// + /// + /// + /// if (instance.TryPickBool(out var value)) { + /// // `value` is of type `bool` + /// Console.WriteLine(value); + /// } + /// + /// + /// + public bool TryPickBool([NotNullWhen(true)] out bool? value) + { + value = this.Value as bool?; + return value != null; + } + + /// + /// Calls the function parameter corresponding to the variant the instance was constructed with. + /// + /// Use the TryPick method(s) if you don't need to handle every variant, or + /// if you need your function parameters to return something. + /// + /// + /// Thrown when the instance was constructed with an unknown variant (e.g. deserialized from raw data + /// that doesn't match any variant's expected shape). + /// + /// + /// + /// + /// instance.Switch( + /// (string value) => {...}, + /// (double value) => {...}, + /// (bool value) => {...} + /// ); + /// + /// + /// + public void Switch( + System::Action @string, + System::Action @double, + System::Action @bool + ) + { + switch (this.Value) + { + case string value: + @string(value); + break; + case double value: + @double(value); + break; + case bool value: + @bool(value); + break; + default: + throw new StagehandInvalidDataException( + "Data did not match any variant of ExecuteOptionsVariableUnionMember3Value" + ); + } + } + + /// + /// Calls the function parameter corresponding to the variant the instance was constructed with and + /// returns its result. + /// + /// Use the TryPick method(s) if you don't need to handle every variant, or + /// if you don't need your function parameters to return a value. + /// + /// + /// Thrown when the instance was constructed with an unknown variant (e.g. deserialized from raw data + /// that doesn't match any variant's expected shape). + /// + /// + /// + /// + /// var result = instance.Match( + /// (string value) => {...}, + /// (double value) => {...}, + /// (bool value) => {...} + /// ); + /// + /// + /// + public T Match( + System::Func @string, + System::Func @double, + System::Func @bool + ) + { + return this.Value switch + { + string value => @string(value), + double value => @double(value), + bool value => @bool(value), + _ => throw new StagehandInvalidDataException( + "Data did not match any variant of ExecuteOptionsVariableUnionMember3Value" + ), + }; + } + + public static implicit operator ExecuteOptionsVariableUnionMember3Value(string value) => + new(value); + + public static implicit operator ExecuteOptionsVariableUnionMember3Value(double value) => + new(value); + + public static implicit operator ExecuteOptionsVariableUnionMember3Value(bool value) => + new(value); + + /// + /// Validates that the instance was constructed with a known variant and that this variant is valid + /// (based on its own Validate method). + /// + /// This is useful for instances constructed from raw JSON data (e.g. deserialized from an API response). + /// + /// + /// Thrown when the instance does not pass validation. + /// + /// + public override void Validate() + { + if (this.Value == null) + { + throw new StagehandInvalidDataException( + "Data did not match any variant of ExecuteOptionsVariableUnionMember3Value" + ); + } + } + + public virtual bool Equals(ExecuteOptionsVariableUnionMember3Value? other) => + other != null + && this.VariantIndex() == other.VariantIndex() + && JsonElement.DeepEquals(this.Json, other.Json); + + public override int GetHashCode() + { + return 0; + } + + public override string ToString() => + JsonSerializer.Serialize( + FriendlyJsonPrinter.PrintValue(this.Json), + ModelBase.ToStringSerializerOptions + ); + + int VariantIndex() + { + return this.Value switch + { + string _ => 0, + double _ => 1, + bool _ => 2, + _ => -1, + }; + } +} + +sealed class ExecuteOptionsVariableUnionMember3ValueConverter + : JsonConverter +{ + public override ExecuteOptionsVariableUnionMember3Value? Read( + ref Utf8JsonReader reader, + System::Type typeToConvert, + JsonSerializerOptions options + ) + { + var element = JsonSerializer.Deserialize(ref reader, options); + try + { + var deserialized = JsonSerializer.Deserialize(element, options); + if (deserialized != null) + { + return new(deserialized, element); + } + } + catch (System::Exception e) when (e is JsonException || e is StagehandInvalidDataException) + { + // ignore + } + + try + { + return new(JsonSerializer.Deserialize(element, options), element); + } + catch (System::Exception e) when (e is JsonException || e is StagehandInvalidDataException) + { + // ignore + } + + try + { + return new(JsonSerializer.Deserialize(element, options), element); + } + catch (System::Exception e) when (e is JsonException || e is StagehandInvalidDataException) + { + // ignore + } + + return new(element); + } + + public override void Write( + Utf8JsonWriter writer, + ExecuteOptionsVariableUnionMember3Value value, + JsonSerializerOptions options + ) + { + JsonSerializer.Serialize(writer, value.Json, options); + } +} + /// /// Whether to stream the response via SSE /// From 57e11a144083cfd91f7fb6fae14270ab7812aef8 Mon Sep 17 00:00:00 2001 From: Sam F <43347795+monadoid@users.noreply.github.com> Date: Wed, 6 May 2026 21:55:04 +0200 Subject: [PATCH 4/5] STG-1808: Deprecate browserbase project ID (#75) * Deprecate browserbase project ID * Fix CI formatting --------- Co-authored-by: samfinton --- README.md | 13 ++++++------- examples/.env.example | 2 -- examples/Env.cs | 8 +------- examples/local_browser_playwright_example.cs | 2 +- .../local_server_multiregion_browser_example.cs | 4 ++-- examples/remote_browser_playwright_example.cs | 2 +- src/Stagehand/Core/ClientOptions.cs | 6 ++---- src/Stagehand/Core/ParamsBase.cs | 4 ---- src/Stagehand/IStagehandClient.cs | 4 ++-- 9 files changed, 15 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 9ee090f..5482eec 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,7 @@ namespace Stagehand.Examples static async Task Main(string[] args) { Env.Load(); - // Uses environment variables: STAGEHAND_API_URL, BROWSERBASE_API_KEY, BROWSERBASE_PROJECT_ID, MODEL_API_KEY + // Uses environment variables: BROWSERBASE_API_KEY and MODEL_API_KEY StagehandClient client = new(); // Start a new remote Browserbase session (Playwright-backed) @@ -325,10 +325,8 @@ namespace Stagehand.Examples Set your environment variables (from `examples/.env.example`): -- `STAGEHAND_API_URL` - `MODEL_API_KEY` - `BROWSERBASE_API_KEY` -- `BROWSERBASE_PROJECT_ID` ```bash cp examples/.env.example examples/.env @@ -373,7 +371,7 @@ Configure the client using environment variables: ```csharp using Stagehand; -// Configured using the BROWSERBASE_API_KEY, BROWSERBASE_PROJECT_ID, MODEL_API_KEY and STAGEHAND_API_URL environment variables +// Configured using BROWSERBASE_API_KEY, MODEL_API_KEY, and STAGEHAND_API_URL, with STAGEHAND_BASE_URL as a fallback StagehandClient client = new(); ``` @@ -385,7 +383,6 @@ using Stagehand; StagehandClient client = new() { BrowserbaseApiKey = "My Browserbase API Key", - BrowserbaseProjectID = "My Browserbase Project ID", ModelApiKey = "My Model API Key", }; ``` @@ -397,9 +394,11 @@ See this table for the available options: | Property | Environment variable | Required | Default value | | ---------------------- | ------------------------ | -------- | ----------------------------------------- | | `BrowserbaseApiKey` | `BROWSERBASE_API_KEY` | true | - | -| `BrowserbaseProjectID` | `BROWSERBASE_PROJECT_ID` | false | - | +| `BrowserbaseProjectID` | - | false | - | | `ModelApiKey` | `MODEL_API_KEY` | true | - | -| `BaseUrl` | `STAGEHAND_API_URL` | true | `"https://api.stagehand.browserbase.com"` | +| `BaseUrl` | `STAGEHAND_API_URL` | false | `"https://api.stagehand.browserbase.com"` | + +`BrowserbaseProjectID` is deprecated, accepted for backwards compatibility, and ignored. `STAGEHAND_BASE_URL` remains supported as a deprecated fallback when `STAGEHAND_API_URL` is unset. ### Modifying configuration diff --git a/examples/.env.example b/examples/.env.example index 6272bb0..0033442 100644 --- a/examples/.env.example +++ b/examples/.env.example @@ -1,4 +1,2 @@ -STAGEHAND_API_URL=https://api.stagehand.browserbase.com MODEL_API_KEY=sk-proj-your-llm-api-key-here BROWSERBASE_API_KEY=bb_live_your_api_key_here -BROWSERBASE_PROJECT_ID=your-bb-project-uuid-here diff --git a/examples/Env.cs b/examples/Env.cs index a7f3d63..b04d511 100644 --- a/examples/Env.cs +++ b/examples/Env.cs @@ -6,13 +6,7 @@ namespace Stagehand.Examples { internal static class Env { - private static readonly string[] RequiredKeys = - [ - "STAGEHAND_API_URL", - "MODEL_API_KEY", - "BROWSERBASE_API_KEY", - "BROWSERBASE_PROJECT_ID", - ]; + private static readonly string[] RequiredKeys = ["MODEL_API_KEY", "BROWSERBASE_API_KEY"]; public static void Load() { diff --git a/examples/local_browser_playwright_example.cs b/examples/local_browser_playwright_example.cs index 17168ba..25a2d12 100644 --- a/examples/local_browser_playwright_example.cs +++ b/examples/local_browser_playwright_example.cs @@ -17,7 +17,7 @@ public static async Task RunAsync() { Env.Load(); // Uses environment variables: MODEL_API_KEY - // In local mode, BROWSERBASE_API_KEY and BROWSERBASE_PROJECT_ID can be any value. + // In local mode, BROWSERBASE_API_KEY can be any value. StagehandClient client = new(); // Start a new local session diff --git a/examples/local_server_multiregion_browser_example.cs b/examples/local_server_multiregion_browser_example.cs index 58b4426..af3c357 100644 --- a/examples/local_server_multiregion_browser_example.cs +++ b/examples/local_server_multiregion_browser_example.cs @@ -16,8 +16,8 @@ internal static class LocalServerMultiregionBrowserExample public static async Task RunAsync() { Env.Load(); - // Uses environment variables: BROWSERBASE_API_KEY, BROWSERBASE_PROJECT_ID, MODEL_API_KEY - // STAGEHAND_API_URL should point to the local Stagehand server. + // Uses environment variables: BROWSERBASE_API_KEY and MODEL_API_KEY + // STAGEHAND_API_URL should point to the local Stagehand server; STAGEHAND_BASE_URL is a fallback. StagehandClient client = new(); var startResponse = await client.Sessions.Start( diff --git a/examples/remote_browser_playwright_example.cs b/examples/remote_browser_playwright_example.cs index 96c5130..3d0e2e5 100644 --- a/examples/remote_browser_playwright_example.cs +++ b/examples/remote_browser_playwright_example.cs @@ -16,7 +16,7 @@ internal static class RemoteBrowserPlaywrightExample public static async Task RunAsync() { Env.Load(); - // Uses environment variables: BROWSERBASE_API_KEY, BROWSERBASE_PROJECT_ID, MODEL_API_KEY + // Uses environment variables: BROWSERBASE_API_KEY and MODEL_API_KEY StagehandClient client = new(); // Start a new remote Browserbase session (Playwright-backed) diff --git a/src/Stagehand/Core/ClientOptions.cs b/src/Stagehand/Core/ClientOptions.cs index 3bfc38b..fc9fa76 100644 --- a/src/Stagehand/Core/ClientOptions.cs +++ b/src/Stagehand/Core/ClientOptions.cs @@ -110,13 +110,11 @@ public string BrowserbaseApiKey /// Deprecated. Browserbase API keys are now project-scoped, so this value is /// no longer required. /// - Lazy _browserbaseProjectID = new(() => - Environment.GetEnvironmentVariable("BROWSERBASE_PROJECT_ID") - ); + Lazy _browserbaseProjectID = new(() => null); /// /// Deprecated. Browserbase API keys are now project-scoped, so this value is - /// no longer required. + /// no longer required. Accepted for backwards compatibility; it is not sent to the API. /// public string? BrowserbaseProjectID { diff --git a/src/Stagehand/Core/ParamsBase.cs b/src/Stagehand/Core/ParamsBase.cs index 2ed2e58..d683eca 100644 --- a/src/Stagehand/Core/ParamsBase.cs +++ b/src/Stagehand/Core/ParamsBase.cs @@ -215,10 +215,6 @@ internal static void AddDefaultHeaders(HttpRequestMessage request, ClientOptions { request.Headers.Add("x-bb-api-key", options.BrowserbaseApiKey); } - if (options.BrowserbaseProjectID != null) - { - request.Headers.Add("x-bb-project-id", options.BrowserbaseProjectID); - } if (options.ModelApiKey != null) { request.Headers.Add("x-model-api-key", options.ModelApiKey); diff --git a/src/Stagehand/IStagehandClient.cs b/src/Stagehand/IStagehandClient.cs index 6e5bc12..21d8e8d 100644 --- a/src/Stagehand/IStagehandClient.cs +++ b/src/Stagehand/IStagehandClient.cs @@ -43,7 +43,7 @@ public interface IStagehandClient : IDisposable /// /// Deprecated. Browserbase API keys are now project-scoped, so this value is - /// no longer required. + /// no longer required. Accepted for backwards compatibility; it is not sent to the API. /// string? BrowserbaseProjectID { get; init; } @@ -95,7 +95,7 @@ public interface IStagehandClientWithRawResponse : IDisposable /// /// Deprecated. Browserbase API keys are now project-scoped, so this value is - /// no longer required. + /// no longer required. Accepted for backwards compatibility; it is not sent to the API. /// string? BrowserbaseProjectID { get; init; } From 62159d72197319356305be33c3ce356d03b47025 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 6 May 2026 19:55:32 +0000 Subject: [PATCH 5/5] release: 3.20.0 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 10 ++++++++++ src/Stagehand/Stagehand.csproj | 2 +- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 911c623..d11c8fc 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "3.19.3" + ".": "3.20.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ecbc28..65365ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## 3.20.0 (2026-05-06) + +Full Changelog: [v3.19.3...v3.20.0](https://github.com/browserbase/stagehand-net/compare/v3.19.3...v3.20.0) + +### Features + +* [feat]: add `ignoreSelectors` to `extract()` ([a9b327f](https://github.com/browserbase/stagehand-net/commit/a9b327f17942b28a16800c3dd93b569bb1bac6dc)) +* [STG-1808] Deprecate Browserbase project ID ([1571286](https://github.com/browserbase/stagehand-net/commit/15712866df03d50e920ab9f71e24e30d7b17e455)) +* remove experimental requirement on agent variables ([#2079](https://github.com/browserbase/stagehand-net/issues/2079)) ([1c3a03c](https://github.com/browserbase/stagehand-net/commit/1c3a03c6961d3c4fab99e2852ce0ea4dc4778444)) + ## 3.19.3 (2026-05-05) Full Changelog: [v3.18.0...v3.19.3](https://github.com/browserbase/stagehand-net/compare/v3.18.0...v3.19.3) diff --git a/src/Stagehand/Stagehand.csproj b/src/Stagehand/Stagehand.csproj index b626bc2..c81c48b 100644 --- a/src/Stagehand/Stagehand.csproj +++ b/src/Stagehand/Stagehand.csproj @@ -3,7 +3,7 @@ Stagehand C# Stagehand - 3.19.3 + 3.20.0 The official .NET library for the Stagehand API. Library README.md