diff --git a/internal/llminternal/outputschema_processor.go b/internal/llminternal/outputschema_processor.go index e64847532..62c6cf4ea 100644 --- a/internal/llminternal/outputschema_processor.go +++ b/internal/llminternal/outputschema_processor.go @@ -23,6 +23,7 @@ import ( "google.golang.org/adk/agent" "google.golang.org/adk/internal/llminternal/googlellm" + "google.golang.org/adk/internal/schemautil" "google.golang.org/adk/internal/toolinternal/toolutils" "google.golang.org/adk/internal/utils" "google.golang.org/adk/model" @@ -134,7 +135,11 @@ func (t *setModelResponseTool) Run(ctx tool.Context, args any) (map[string]any, if !ok { return nil, fmt.Errorf("unexpected args type for set_model_response: %T", args) } - if err := utils.ValidateMapOnSchema(m, t.schema, false); err != nil { + resolved, err := schemautil.GenaiToResolvedJSONSchema(t.schema) + if err != nil { + return nil, fmt.Errorf("failed to resolve output schema: %w", err) + } + if err := resolved.Validate(m); err != nil { return nil, fmt.Errorf("invalid output schema: %w", err) } return m, nil diff --git a/internal/schemautil/convert.go b/internal/schemautil/convert.go new file mode 100644 index 000000000..089300169 --- /dev/null +++ b/internal/schemautil/convert.go @@ -0,0 +1,100 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package schemautil provides utilities for schema conversion and validation. +package schemautil + +import ( + "encoding/json" + "strings" + + "github.com/google/jsonschema-go/jsonschema" + "google.golang.org/genai" +) + +// GenaiToJSONSchema converts a genai.Schema to a jsonschema.Schema. +func GenaiToJSONSchema(gs *genai.Schema) (*jsonschema.Schema, error) { + if gs == nil { + return nil, nil + } + + // Marshal to intermediate map + data, err := json.Marshal(gs) + if err != nil { + return nil, err + } + + var m map[string]any + if err := json.Unmarshal(data, &m); err != nil { + return nil, err + } + + // Normalize type to lowercase (genai uses "STRING", jsonschema expects "string") + normalizeTypes(m) + + // Marshal back and unmarshal to jsonschema.Schema + data, err = json.Marshal(m) + if err != nil { + return nil, err + } + + var js jsonschema.Schema + if err := json.Unmarshal(data, &js); err != nil { + return nil, err + } + + return &js, nil +} + +// normalizeTypes recursively lowercases type fields in the schema map. +func normalizeTypes(m map[string]any) { + if t, ok := m["type"].(string); ok { + m["type"] = strings.ToLower(t) + } + + // Recurse into properties + if props, ok := m["properties"].(map[string]any); ok { + for _, v := range props { + if prop, ok := v.(map[string]any); ok { + normalizeTypes(prop) + } + } + } + + // Recurse into items + if items, ok := m["items"].(map[string]any); ok { + normalizeTypes(items) + } + + // Recurse into anyOf + if anyOf, ok := m["anyOf"].([]any); ok { + for _, v := range anyOf { + if s, ok := v.(map[string]any); ok { + normalizeTypes(s) + } + } + } +} + +// GenaiToResolvedJSONSchema converts a genai.Schema to a resolved jsonschema. +func GenaiToResolvedJSONSchema(gs *genai.Schema) (*jsonschema.Resolved, error) { + if gs == nil { + return nil, nil + } + js, err := GenaiToJSONSchema(gs) + if err != nil { + return nil, err + } + return js.Resolve(nil) +} diff --git a/internal/schemautil/convert_test.go b/internal/schemautil/convert_test.go new file mode 100644 index 000000000..b1545fc51 --- /dev/null +++ b/internal/schemautil/convert_test.go @@ -0,0 +1,221 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package schemautil + +import ( + "testing" + + "google.golang.org/genai" +) + +func TestGenaiToJSONSchema_Nil(t *testing.T) { + js, err := GenaiToJSONSchema(nil) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if js != nil { + t.Errorf("expected nil, got %v", js) + } +} + +func TestGenaiToJSONSchema_BasicTypes(t *testing.T) { + tests := []struct { + name string + genaiType genai.Type + wantType string + }{ + {"string", genai.TypeString, "string"}, + {"integer", genai.TypeInteger, "integer"}, + {"number", genai.TypeNumber, "number"}, + {"boolean", genai.TypeBoolean, "boolean"}, + {"array", genai.TypeArray, "array"}, + {"object", genai.TypeObject, "object"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gs := &genai.Schema{Type: tt.genaiType} + js, err := GenaiToJSONSchema(gs) + if err != nil { + t.Fatalf("GenaiToJSONSchema error: %v", err) + } + if js.Type != tt.wantType { + t.Errorf("Type = %q, want %q", js.Type, tt.wantType) + } + }) + } +} + +func TestGenaiToJSONSchema_Enum(t *testing.T) { + gs := &genai.Schema{ + Type: genai.TypeString, + Enum: []string{"red", "green", "blue"}, + } + js, err := GenaiToJSONSchema(gs) + if err != nil { + t.Fatalf("GenaiToJSONSchema error: %v", err) + } + + if len(js.Enum) != 3 { + t.Fatalf("Enum length = %d, want 3", len(js.Enum)) + } + for i, want := range []string{"red", "green", "blue"} { + if js.Enum[i] != want { + t.Errorf("Enum[%d] = %v, want %q", i, js.Enum[i], want) + } + } +} + +func TestGenaiToJSONSchema_EnumValidation(t *testing.T) { + gs := &genai.Schema{ + Type: genai.TypeString, + Enum: []string{"red", "green", "blue"}, + } + resolved, err := GenaiToResolvedJSONSchema(gs) + if err != nil { + t.Fatalf("GenaiToResolvedJSONSchema error: %v", err) + } + + if err := resolved.Validate("red"); err != nil { + t.Errorf("Validate('red') error = %v, want nil", err) + } + + if err := resolved.Validate("purple"); err == nil { + t.Error("Validate('purple') error = nil, want error") + } +} + +func TestGenaiToJSONSchema_Properties(t *testing.T) { + gs := &genai.Schema{ + Type: genai.TypeObject, + Properties: map[string]*genai.Schema{ + "name": {Type: genai.TypeString, Description: "The name"}, + "age": {Type: genai.TypeInteger}, + }, + Required: []string{"name"}, + } + js, err := GenaiToJSONSchema(gs) + if err != nil { + t.Fatalf("GenaiToJSONSchema error: %v", err) + } + + if len(js.Properties) != 2 { + t.Fatalf("Properties length = %d, want 2", len(js.Properties)) + } + if js.Properties["name"].Type != "string" { + t.Errorf("Properties[name].Type = %q, want string", js.Properties["name"].Type) + } + if js.Properties["name"].Description != "The name" { + t.Errorf("Properties[name].Description = %q, want 'The name'", js.Properties["name"].Description) + } + if js.Properties["age"].Type != "integer" { + t.Errorf("Properties[age].Type = %q, want integer", js.Properties["age"].Type) + } + if len(js.Required) != 1 || js.Required[0] != "name" { + t.Errorf("Required = %v, want [name]", js.Required) + } +} + +func TestGenaiToJSONSchema_ObjectValidation(t *testing.T) { + gs := &genai.Schema{ + Type: genai.TypeObject, + Properties: map[string]*genai.Schema{ + "color": { + Type: genai.TypeString, + Enum: []string{"red", "green", "blue"}, + }, + }, + Required: []string{"color"}, + } + resolved, err := GenaiToResolvedJSONSchema(gs) + if err != nil { + t.Fatalf("GenaiToResolvedJSONSchema error: %v", err) + } + + valid := map[string]any{"color": "red"} + if err := resolved.Validate(valid); err != nil { + t.Errorf("Validate(valid) error = %v, want nil", err) + } + + invalid := map[string]any{"color": "purple"} + if err := resolved.Validate(invalid); err == nil { + t.Error("Validate(invalid) error = nil, want error for invalid enum") + } + + missing := map[string]any{} + if err := resolved.Validate(missing); err == nil { + t.Error("Validate(missing) error = nil, want error for missing required") + } +} + +func TestGenaiToJSONSchema_Array(t *testing.T) { + gs := &genai.Schema{ + Type: genai.TypeArray, + Items: &genai.Schema{ + Type: genai.TypeString, + Enum: []string{"a", "b", "c"}, + }, + } + resolved, err := GenaiToResolvedJSONSchema(gs) + if err != nil { + t.Fatalf("GenaiToResolvedJSONSchema error: %v", err) + } + + valid := []any{"a", "b"} + if err := resolved.Validate(valid); err != nil { + t.Errorf("Validate(valid) error = %v, want nil", err) + } + + invalid := []any{"a", "d"} + if err := resolved.Validate(invalid); err == nil { + t.Error("Validate(invalid) error = nil, want error for invalid enum in array item") + } +} + +func TestGenaiToJSONSchema_NumericConstraints(t *testing.T) { + min := 0.0 + max := 100.0 + gs := &genai.Schema{ + Type: genai.TypeNumber, + Minimum: &min, + Maximum: &max, + } + resolved, err := GenaiToResolvedJSONSchema(gs) + if err != nil { + t.Fatalf("GenaiToResolvedJSONSchema error: %v", err) + } + + if err := resolved.Validate(50.0); err != nil { + t.Errorf("Validate(50) error = %v, want nil", err) + } + + if err := resolved.Validate(-1.0); err == nil { + t.Error("Validate(-1) error = nil, want error") + } + + if err := resolved.Validate(101.0); err == nil { + t.Error("Validate(101) error = nil, want error") + } +} + +func TestGenaiToResolvedJSONSchema_Nil(t *testing.T) { + resolved, err := GenaiToResolvedJSONSchema(nil) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if resolved != nil { + t.Error("expected nil resolved schema for nil input") + } +} diff --git a/internal/utils/schema_test.go b/internal/utils/schema_test.go deleted file mode 100644 index eba4946ae..000000000 --- a/internal/utils/schema_test.go +++ /dev/null @@ -1,354 +0,0 @@ -// Copyright 2025 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package utils - -import ( - "reflect" - "testing" - - "google.golang.org/genai" -) - -func TestMatchType(t *testing.T) { - tests := []struct { - name string - value any - schema *genai.Schema - isInput bool - wantMatch bool - wantErr bool - }{ - { - name: "nil schema", - value: "test", - schema: nil, - isInput: true, - wantMatch: false, - wantErr: true, - }, - { - name: "nil value", - value: nil, - schema: &genai.Schema{Type: genai.TypeString}, - isInput: true, - wantMatch: false, - wantErr: false, - }, - { - name: "string match", - value: "test", - schema: &genai.Schema{Type: genai.TypeString}, - isInput: true, - wantMatch: true, - wantErr: false, - }, - { - name: "string mismatch", - value: 123.0, - schema: &genai.Schema{Type: genai.TypeString}, - isInput: true, - wantMatch: false, - wantErr: false, - }, - { - name: "integer match", - value: 123.0, - schema: &genai.Schema{Type: genai.TypeInteger}, - isInput: true, - wantMatch: true, - wantErr: false, - }, - { - name: "integer mismatch float", - value: 123.45, - schema: &genai.Schema{Type: genai.TypeInteger}, - isInput: true, - wantMatch: false, - wantErr: false, - }, - { - name: "integer mismatch type", - value: "123", - schema: &genai.Schema{Type: genai.TypeInteger}, - isInput: true, - wantMatch: false, - wantErr: false, - }, - { - name: "number match", - value: 123.45, - schema: &genai.Schema{Type: genai.TypeNumber}, - isInput: true, - wantMatch: true, - wantErr: false, - }, - { - name: "number mismatch", - value: "123.45", - schema: &genai.Schema{Type: genai.TypeNumber}, - isInput: true, - wantMatch: false, - wantErr: false, - }, - { - name: "boolean match", - value: true, - schema: &genai.Schema{Type: genai.TypeBoolean}, - isInput: true, - wantMatch: true, - wantErr: false, - }, - { - name: "boolean mismatch", - value: "true", - schema: &genai.Schema{Type: genai.TypeBoolean}, - isInput: true, - wantMatch: false, - wantErr: false, - }, - { - name: "array match", - value: []any{"a", "b"}, - schema: &genai.Schema{Type: genai.TypeArray, Items: &genai.Schema{Type: genai.TypeString}}, - isInput: true, - wantMatch: true, - wantErr: false, - }, - { - name: "array mismatch type", - value: "not an array", - schema: &genai.Schema{Type: genai.TypeArray, Items: &genai.Schema{Type: genai.TypeString}}, - isInput: true, - wantMatch: false, - wantErr: false, - }, - { - name: "array mismatch item type", - value: []any{"a", 1.0}, - schema: &genai.Schema{Type: genai.TypeArray, Items: &genai.Schema{Type: genai.TypeString}}, - isInput: true, - wantMatch: false, - wantErr: false, - }, - { - name: "array missing items", - value: []any{"a", "b"}, - schema: &genai.Schema{Type: genai.TypeArray}, - isInput: true, - wantMatch: false, - wantErr: true, - }, - { - name: "object match", - value: map[string]any{"foo": "bar"}, - schema: &genai.Schema{Type: genai.TypeObject, Properties: map[string]*genai.Schema{"foo": {Type: genai.TypeString}}}, - isInput: true, - wantMatch: true, - wantErr: false, - }, - { - name: "object mismatch type", - value: "not an object", - schema: &genai.Schema{Type: genai.TypeObject, Properties: map[string]*genai.Schema{"foo": {Type: genai.TypeString}}}, - isInput: true, - wantMatch: false, - wantErr: false, - }, - { - name: "object mismatch property type", - value: map[string]any{"foo": 123.0}, - schema: &genai.Schema{Type: genai.TypeObject, Properties: map[string]*genai.Schema{"foo": {Type: genai.TypeString}}}, - isInput: true, - wantMatch: false, - wantErr: true, // This will fail ValidateMapOnSchema, which returns error - }, - { - name: "unsupported type", - value: 123, - schema: &genai.Schema{Type: "UNSUPPORTED"}, - isInput: true, - wantMatch: false, - wantErr: true, - }, - { - name: "lowercase type in schema", - value: "test", - schema: &genai.Schema{Type: "string"}, - isInput: true, - wantMatch: true, - wantErr: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - gotMatch, err := matchType(tt.value, tt.schema, tt.isInput) - if (err != nil) != tt.wantErr { - t.Errorf("matchType() error = %v, wantErr %v", err, tt.wantErr) - return - } - if gotMatch != tt.wantMatch { - t.Errorf("matchType() = %v, want %v", gotMatch, tt.wantMatch) - } - }) - } -} - -func TestValidateMapOnSchema(t *testing.T) { - schema := &genai.Schema{ - Type: genai.TypeObject, - Properties: map[string]*genai.Schema{ - "str_field": {Type: genai.TypeString}, - "int_field": {Type: genai.TypeInteger}, - }, - Required: []string{"str_field"}, - } - schemaNilProps := &genai.Schema{ - Type: genai.TypeObject, - } - - tests := []struct { - name string - args map[string]any - schema *genai.Schema - isInput bool - wantErr bool - }{ - { - name: "valid map", - args: map[string]any{"str_field": "hello", "int_field": 123.0}, - schema: schema, - isInput: true, - wantErr: false, - }, - { - name: "valid map with only required fields", - args: map[string]any{"str_field": "hello"}, - schema: schema, - isInput: true, - wantErr: false, - }, - { - name: "missing required field", - args: map[string]any{"int_field": 123.0}, - schema: schema, - isInput: true, - wantErr: true, - }, - { - name: "extra field", - args: map[string]any{"str_field": "hello", "extra": "field"}, - schema: schema, - isInput: true, - wantErr: true, - }, - { - name: "type mismatch", - args: map[string]any{"str_field": 123.0}, - schema: schema, - isInput: true, - wantErr: true, - }, - { - name: "nil schema", - args: map[string]any{"str_field": "hello"}, - schema: nil, - isInput: true, - wantErr: true, - }, - { - name: "nil properties and no args", - args: map[string]any{}, - schema: schemaNilProps, - isInput: true, - wantErr: false, - }, - { - name: "nil properties and some args", - args: map[string]any{"some": "arg"}, - schema: schemaNilProps, - isInput: true, - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if err := ValidateMapOnSchema(tt.args, tt.schema, tt.isInput); (err != nil) != tt.wantErr { - t.Errorf("ValidateMapOnSchema() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } -} - -func TestValidateOutputSchema(t *testing.T) { - schema := &genai.Schema{ - Type: genai.TypeObject, - Properties: map[string]*genai.Schema{ - "result": {Type: genai.TypeString}, - }, - Required: []string{"result"}, - } - - tests := []struct { - name string - output string - schema *genai.Schema - wantOutput map[string]any - wantErr bool - }{ - { - name: "valid output", - output: `{"result": "success"}`, - schema: schema, - wantOutput: map[string]any{"result": "success"}, - wantErr: false, - }, - { - name: "invalid json", - output: `{"result": "success"`, - schema: schema, - wantOutput: nil, - wantErr: true, - }, - { - name: "schema mismatch", - output: `{"wrong_key": "failure"}`, - schema: schema, - wantOutput: nil, - wantErr: true, - }, - { - name: "nil schema", - output: `{"result": "success"}`, - schema: nil, - wantOutput: nil, - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - gotOutput, err := ValidateOutputSchema(tt.output, tt.schema) - if (err != nil) != tt.wantErr { - t.Errorf("ValidateOutputSchema() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !tt.wantErr && !reflect.DeepEqual(gotOutput, tt.wantOutput) { - t.Errorf("ValidateOutputSchema() = %v, want %v", gotOutput, tt.wantOutput) - } - }) - } -} diff --git a/internal/utils/schema_utils.go b/internal/utils/schema_utils.go deleted file mode 100644 index 0a4d00654..000000000 --- a/internal/utils/schema_utils.go +++ /dev/null @@ -1,138 +0,0 @@ -// Copyright 2025 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package utils - -import ( - "encoding/json" - "fmt" - "math" - "reflect" - "strings" - - "google.golang.org/genai" -) - -// matchType checks if the value matches the schema type. -func matchType(value any, schema *genai.Schema, isInput bool) (bool, error) { - if schema == nil { - return false, fmt.Errorf("schema is nil") - } - - if value == nil { - return false, nil - } - - // Convert type to upper case to match the type in the schema. - switch genai.Type(strings.ToUpper(string(schema.Type))) { - case genai.TypeString: - _, ok := value.(string) - return ok, nil - case genai.TypeInteger: - f, ok := value.(float64) - if !ok { - return false, nil - } - return f == math.Trunc(f), nil - case genai.TypeBoolean: - _, ok := value.(bool) - return ok, nil - case genai.TypeNumber: - _, ok := value.(float64) - return ok, nil - case genai.TypeArray: - val := reflect.ValueOf(value) - if val.Kind() != reflect.Slice { - return false, nil - } - if schema.Items == nil { - return false, fmt.Errorf("array schema missing items definition") - } - for i := 0; i < val.Len(); i++ { - ok, err := matchType(val.Index(i).Interface(), schema.Items, isInput) - if err != nil { - return false, fmt.Errorf("array item %d: %w", i, err) - } - if !ok { - return false, nil - } - } - return true, nil - case genai.TypeObject: - obj, ok := value.(map[string]any) - if !ok { - return false, nil - } - err := ValidateMapOnSchema(obj, schema, isInput) - return err == nil, err - default: - return false, fmt.Errorf("unsupported type: %s", schema.Type) - } -} - -// ValidateMapOnSchema validates a map against a schema. -func ValidateMapOnSchema(args map[string]any, schema *genai.Schema, isInput bool) error { - if schema == nil { - return fmt.Errorf("schema cannot be nil") - } - - properties := schema.Properties - if properties == nil { - properties = make(map[string]*genai.Schema) - } - - argType := "input" - if !isInput { - argType = "output" - } - - for key, value := range args { - propSchema, exists := properties[key] - if !exists { - // Note: OpenAPI schemas can allow additional properties. This implementation assumes strictness. - return fmt.Errorf("%s arg: '%q' does not exist in schema properties", argType, key) - } - ok, err := matchType(value, propSchema, isInput) - if err != nil { - return fmt.Errorf("%s arg: '%q' validation failed: %w", argType, key, err) - } - if !ok { - return fmt.Errorf("%s arg: '%q' type mismatch, expected schema type %s, got value %v of type %T", argType, key, propSchema.Type, value, value) - } - } - - for _, requiredKey := range schema.Required { - if _, exists := args[requiredKey]; !exists { - return fmt.Errorf("%q args does not contain required key: '%q'", argType, requiredKey) - } - } - return nil -} - -// ValidateOutputSchema validates an output JSON string against a schema. -func ValidateOutputSchema(output string, schema *genai.Schema) (map[string]any, error) { - if schema == nil { - return nil, fmt.Errorf("schema cannot be nil") - } - var outputMap map[string]any - err := json.Unmarshal([]byte(output), &outputMap) - if err != nil { - return nil, fmt.Errorf("failed to parse output JSON: %w", err) - } - - if err := ValidateMapOnSchema(outputMap, schema, false); err != nil { // isInput = false - return nil, err - } - return outputMap, nil -} diff --git a/tool/agenttool/agent_tool.go b/tool/agenttool/agent_tool.go index 2cfe1a890..f3cebfe27 100644 --- a/tool/agenttool/agent_tool.go +++ b/tool/agenttool/agent_tool.go @@ -27,7 +27,7 @@ import ( "google.golang.org/adk/agent" "google.golang.org/adk/artifact" "google.golang.org/adk/internal/llminternal" - "google.golang.org/adk/internal/utils" + "google.golang.org/adk/internal/schemautil" "google.golang.org/adk/memory" "google.golang.org/adk/model" "google.golang.org/adk/runner" @@ -143,7 +143,11 @@ func (t *agentTool) Run(toolCtx tool.Context, args any) (map[string]any, error) var content *genai.Content var err error if agentInputSchema != nil { - if err = utils.ValidateMapOnSchema(margs, agentInputSchema, true); err != nil { + resolved, resolveErr := schemautil.GenaiToResolvedJSONSchema(agentInputSchema) + if resolveErr != nil { + return nil, fmt.Errorf("failed to resolve input schema for agent %s: %w", t.agent.Name(), resolveErr) + } + if err = resolved.Validate(margs); err != nil { return nil, fmt.Errorf("argument validation failed for agent %s: %w", t.agent.Name(), err) } jsonData, err := json.Marshal(margs) @@ -236,10 +240,16 @@ func (t *agentTool) Run(toolCtx tool.Context, args any) (map[string]any, error) return nil, fmt.Errorf("internal error: failed to convert to llm agent") } if agentOutputSchema := llminternal.Reveal(internalLlmAgent).OutputSchema; agentOutputSchema != nil { - // Assuming schemautils.ValidateOutputSchema parses the JSON string outputText - // and validates it against the agentOutputSchema, returning a map[string]any. - parsedOutput, err := utils.ValidateOutputSchema(outputText, agentOutputSchema) - if err != nil { + // Parse the JSON output and validate against the schema. + var parsedOutput map[string]any + if err := json.Unmarshal([]byte(outputText), &parsedOutput); err != nil { + return nil, fmt.Errorf("failed to parse output JSON for sub-agent %s: %w", t.agent.Name(), err) + } + resolved, resolveErr := schemautil.GenaiToResolvedJSONSchema(agentOutputSchema) + if resolveErr != nil { + return nil, fmt.Errorf("failed to resolve output schema for sub-agent %s: %w", t.agent.Name(), resolveErr) + } + if err := resolved.Validate(parsedOutput); err != nil { return nil, fmt.Errorf("output validation failed for sub-agent %s: %w", t.agent.Name(), err) } return parsedOutput, nil