diff --git a/provider/foundryprovider/agent.go b/provider/foundryprovider/agent.go index 5407e076..a05bfeca 100644 --- a/provider/foundryprovider/agent.go +++ b/provider/foundryprovider/agent.go @@ -37,6 +37,10 @@ type AgentConfig struct { // DisableStoreOutput disables service-side Responses output storage. // Use this when local session history providers own conversation state. + // By default, store-disabled runs also request `reasoning.encrypted_content` + // so reasoning items can be replayed statelessly; use + // [openaiprovider.ResponsesIncludeReasoningEncryptedContent] in RunOptions to + // opt out. DisableStoreOutput bool // OpenAIOptions configure the OpenAI-compatible per-agent client. Foundry-owned diff --git a/provider/foundryprovider/agent_test.go b/provider/foundryprovider/agent_test.go index c15fa696..c9c55c30 100644 --- a/provider/foundryprovider/agent_test.go +++ b/provider/foundryprovider/agent_test.go @@ -10,6 +10,7 @@ import ( "github.com/microsoft/agent-framework-go/agent" "github.com/microsoft/agent-framework-go/provider/foundryprovider" + "github.com/microsoft/agent-framework-go/provider/openaiprovider" "github.com/openai/openai-go/v3/option" ) @@ -84,6 +85,33 @@ func TestNewAgentDisableStoreOutputSetsStoreFalse(t *testing.T) { } } +func TestNewAgentRunOptionCanDisableReasoningEncryptedContentInclude(t *testing.T) { + server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body := jsonMap(t, mustReadBody(t, r)) + if body["store"] != false { + t.Fatalf("store = %#v, want false", body["store"]) + } + if _, ok := body["include"]; ok { + t.Fatalf("include = %#v, want omitted", body["include"]) + } + writeResponsesOK(w) + })) + defer server.Close() + + foundryAgent := newFoundryAgent(t, server, foundryprovider.ModelDeployment("gpt-4o-mini"), foundryprovider.AgentConfig{ + DisableStoreOutput: true, + Config: agent.Config{ + RunOptions: []agent.Option{ + openaiprovider.ResponsesIncludeReasoningEncryptedContent(false), + }, + }, + }) + + if _, err := foundryAgent.RunText(t.Context(), "hello").Collect(); err != nil { + t.Fatalf("RunText error = %v", err) + } +} + func TestNewAgentRunsAgainstFoundryAgentEndpoint(t *testing.T) { server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/projects/proj/agents/my-agent/endpoint/protocols/openai/responses" { diff --git a/provider/openaiprovider/chat.go b/provider/openaiprovider/chat.go index 185a663f..037f8ccd 100644 --- a/provider/openaiprovider/chat.go +++ b/provider/openaiprovider/chat.go @@ -65,7 +65,10 @@ type AgentConfig struct { // DisableStoreOutput is used only by [NewResponsesAgent]. It disables // service-side output storage and prevents response IDs from being saved into - // agent sessions for later continuation. + // agent sessions for later continuation. While enabled, Responses requests + // also include `reasoning.encrypted_content` by default so reasoning items + // can be replayed statelessly; use [ResponsesIncludeReasoningEncryptedContent] + // to opt out per run. // It is ignored by [NewChatCompletionsAgent]. DisableStoreOutput bool diff --git a/provider/openaiprovider/responses.go b/provider/openaiprovider/responses.go index 8470d1e8..38f98d71 100644 --- a/provider/openaiprovider/responses.go +++ b/provider/openaiprovider/responses.go @@ -72,11 +72,25 @@ func (o responsesNewParamsOpt) MAFValue() any { return responses.ResponseNewParams(o) } +type responsesIncludeReasoningEncryptedContentOpt bool + +func (o responsesIncludeReasoningEncryptedContentOpt) MAFValue() any { + return bool(o) +} + // ResponsesNewParams allows passing custom parameters to the underlying OpenAI Responses API calls. func ResponsesNewParams(params responses.ResponseNewParams) agent.Option { return responsesNewParamsOpt(params) } +// ResponsesIncludeReasoningEncryptedContent controls whether [NewResponsesAgent] +// automatically requests `reasoning.encrypted_content` while Responses output +// storage is disabled. The default is true so reasoning items can be replayed +// across stateless turns. +func ResponsesIncludeReasoningEncryptedContent(enabled bool) agent.Option { + return responsesIncludeReasoningEncryptedContentOpt(enabled) +} + func (a *responsesClient) formatOf(v any) (agent.ResponseFormat, error) { return jsonformat.ForType(reflect.TypeOf(v)) } @@ -233,7 +247,8 @@ func responsesBuildCompletionParams(config AgentConfig, messages []*message.Mess if param.IsOmitted(params.Store) { params.Store = openai.Bool(false) } - if !slices.Contains(params.Include, responses.ResponseIncludableReasoningEncryptedContent) { + if include, ok := agent.GetOption(opts, ResponsesIncludeReasoningEncryptedContent); (!ok || include) && + !slices.Contains(params.Include, responses.ResponseIncludableReasoningEncryptedContent) { params.Include = append(params.Include, responses.ResponseIncludableReasoningEncryptedContent) } } diff --git a/provider/openaiprovider/responses_test.go b/provider/openaiprovider/responses_test.go index 67be1ef0..66b18d4c 100644 --- a/provider/openaiprovider/responses_test.go +++ b/provider/openaiprovider/responses_test.go @@ -5979,6 +5979,55 @@ func TestResponsesNewParamsStoreFalseDoesNotDuplicateReasoningInclude(t *testing } } +func TestResponsesIncludeReasoningEncryptedContentFalseSkipsAutomaticInclude(t *testing.T) { + const input = ` + { + "store":false, + "model":"gpt-4o-mini", + "input":[{ + "type":"message", + "role":"user", + "content":[{"type":"input_text","text":"hello"}] + }] + } + ` + + const output = ` + { + "id": "resp_67890", + "object": "response", + "created_at": 1741891428, + "status": "completed", + "model": "gpt-4o-mini-2024-07-18", + "output": [{ + "type": "message", + "id": "msg_67d32764fcdc8191bcf2e444d4088804058a5e08c46a181d", + "status": "completed", + "role": "assistant", + "content": [{"type": "output_text", "text": "Hello!", "annotations": []}] + }] + } + ` + + server := newTestResponsesServer(t, input, output) + defer server.Close() + + a := openaiprovider.NewResponsesAgent( + openai.NewClient(option.WithBaseURL(server.URL)), + openaiprovider.AgentConfig{ + Model: "gpt-4o-mini", + }, + ) + + _, err := a.RunText(t.Context(), "hello", + openaiprovider.ResponsesNewParams(responses.ResponseNewParams{Store: openai.Bool(false)}), + openaiprovider.ResponsesIncludeReasoningEncryptedContent(false), + ).Collect() + if err != nil { + t.Fatalf("error = %v", err) + } +} + func TestResponsesConversationId_AsConversationId_NonStreaming(t *testing.T) { const input = ` {