Skip to content

llminternal: OutputSchema mutations leak across requests #1792

Description

@ktsoator

Describe the Bug:

basicRequestProcessor assigns state.OutputSchema directly to req.Config.ResponseSchema (source). Both fields reference the same *genai.Schema, so a BeforeModel callback can modify the agent's schema and affect other requests. A shallow struct copy would still share the Properties map and nested schema pointers.

Reproduction:

  1. Configure an agent with no tools and an OutputSchema containing a string property named answer.
  2. In a BeforeModel callback, change the request's answer property type to genai.TypeNumber.
  3. Check the original OutputSchema and another request's schema.

Expected: Both still describe answer as a string.

Actual: Both now describe it as a number.

Reproduction test and command

Append this test to internal/llminternal/basic_processor_test.go; it uses the existing imports and test helpers.

func TestBasicRequestProcessor_OutputSchemaIsolation(t *testing.T) {
	schema := &genai.Schema{
		Type: genai.TypeObject,
		Properties: map[string]*genai.Schema{
			"answer": {Type: genai.TypeString},
		},
	}
	base, err := agent.New(agent.Config{Name: "testAgent"})
	if err != nil {
		t.Fatal(err)
	}
	a := &mockLLMAgent{Agent: base, s: &State{OutputSchema: schema}}
	ctx := icontext.NewInvocationContext(t.Context(), icontext.InvocationContextParams{Agent: a})
	requests := []*model.LLMRequest{{}, {}}
	for _, req := range requests {
		for _, err := range basicRequestProcessor(ctx, req, &Flow{}) {
			if err != nil {
				t.Fatal(err)
			}
		}
	}

	// A BeforeModel callback can perform this same request mutation.
	requests[0].Config.ResponseSchema.Properties["answer"].Type = genai.TypeNumber
	if schema.Properties["answer"].Type != genai.TypeString {
		t.Error("request mutation changed the agent's OutputSchema")
	}
	if requests[1].Config.ResponseSchema.Properties["answer"].Type != genai.TypeString {
		t.Error("request mutation changed another request's schema")
	}
}

Run:

go test -race -mod=readonly -count=1 ./internal/llminternal -run '^TestBasicRequestProcessor_OutputSchemaIsolation$'

Related to #1484 and #1546. This assignment bypasses clone, so the fix in #1546 does not address it.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions