Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion agent/x/agentmcp/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -1038,7 +1038,10 @@ func toolInputSchemaMap(schema any) map[string]any {
if typ, ok := m["type"].(string); ok && typ != "" {
out["type"] = typ
}
if properties, ok := m["properties"].(map[string]any); ok && len(properties) > 0 {
// Preserve an empty "properties" object: dropping it collapses the
// schema to nil by the time the tool definition is rebuilt, and a
// nil properties serializes to JSON null, which OpenAI rejects.
if properties, ok := m["properties"].(map[string]any); ok {
out["properties"] = properties
}
if required, ok := m["required"].([]any); ok && len(required) > 0 {
Expand Down
24 changes: 24 additions & 0 deletions agent/x/agentmcp/manager_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,30 @@ import (
"github.com/coder/quartz"
)

// TestToolInputSchemaMapPreservesEmptyProperties verifies that a tool
// schema with an empty "properties" object (for example
// {"type": "object", "properties": {}}) keeps "properties" in the wire
// copy. Dropping it collapses the schema to nil by the time coderd
// rebuilds the tool definition, and a nil properties serializes to JSON
// null, which OpenAI rejects with "None is not of type 'object'".
func TestToolInputSchemaMapPreservesEmptyProperties(t *testing.T) {
t.Parallel()

out := toolInputSchemaMap(map[string]any{
"type": "object",
"properties": map[string]any{},
})
require.NotNil(t, out, "schema must not collapse to nil")
properties, ok := out["properties"].(map[string]any)
require.True(t, ok, "properties must be preserved as a map, got %T", out["properties"])
require.NotNil(t, properties, "properties must not be nil")

// Verify it serializes to {} not null or absent.
bs, err := json.Marshal(out)
require.NoError(t, err)
require.JSONEq(t, `{"type":"object","properties":{}}`, string(bs))
}

func TestSplitToolName(t *testing.T) {
t.Parallel()

Expand Down
9 changes: 8 additions & 1 deletion coderd/x/chatd/chatloop/chatloop.go
Original file line number Diff line number Diff line change
Expand Up @@ -1628,9 +1628,16 @@ func buildToolDefinitions(tools []fantasy.AgentTool, activeTools []string, provi
continue
}

// Substitute an empty object for nil properties so that a tool
// with no parameters never serializes "properties" to null,
// which OpenAI rejects.
properties := info.Parameters
if properties == nil {
properties = map[string]any{}
}
inputSchema := map[string]any{
"type": "object",
"properties": info.Parameters,
"properties": properties,
}
// Only include "required" when non-empty so that a nil slice
// never serializes to null, which OpenAI rejects.
Expand Down
47 changes: 47 additions & 0 deletions coderd/x/chatd/chatloop/chatloop_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package chatloop

import (
"context"
"encoding/json"
"iter"
"testing"

Expand Down Expand Up @@ -81,6 +82,52 @@ func TestProcessStepStreamPersistsRedactedThinkingOnEnd(t *testing.T) {
require.Equal(t, "redacted-payload", metadata.RedactedData)
}

// staticParametersTool returns a fixed ToolInfo, letting a test control
// Parameters directly. fantasy.NewAgentTool always generates a non-nil
// Parameters map, so it cannot reproduce the nil Parameters that MCP tool
// wrappers report for a schema with an empty "properties" object.
type staticParametersTool struct {
fantasy.AgentTool
info fantasy.ToolInfo
}

func (t staticParametersTool) Info() fantasy.ToolInfo { return t.info }

func (staticParametersTool) ProviderOptions() fantasy.ProviderOptions { return nil }

// TestBuildToolDefinitionsNilPropertiesBecomesEmptyObject verifies that a
// tool whose input schema has no properties (for example an MCP tool
// reporting {"type": "object", "properties": {}}) still serializes
// "properties" as an empty JSON object. A nil Parameters map would serialize
// to null, which OpenAI rejects with "Invalid schema for function ... None is
// not of type 'object'".
func TestBuildToolDefinitionsNilPropertiesBecomesEmptyObject(t *testing.T) {
t.Parallel()

tool := staticParametersTool{
info: fantasy.ToolInfo{
Name: "document_graphql_schema",
Description: "Run a GraphQL query",
Parameters: nil,
},
}

defs := buildToolDefinitions([]fantasy.AgentTool{tool}, nil, nil)
require.Len(t, defs, 1)

ft, ok := defs[0].(fantasy.FunctionTool)
require.True(t, ok, "expected a fantasy.FunctionTool")

properties, ok := ft.InputSchema["properties"].(map[string]any)
require.True(t, ok, "properties must be a map, got %T", ft.InputSchema["properties"])
require.NotNil(t, properties, "properties must not be nil")

// Verify it serializes to {} not null.
bs, err := json.Marshal(ft.InputSchema)
require.NoError(t, err)
require.JSONEq(t, `{"type":"object","properties":{}}`, string(bs))
}

func TestFlushActiveStatePreservesEmptySignedReasoning(t *testing.T) {
t.Parallel()

Expand Down
25 changes: 25 additions & 0 deletions coderd/x/chatd/context_prompt_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -735,6 +735,31 @@ func TestWorkspaceMCPToolInfosFromResources(t *testing.T) {
require.Equal(t, "playwright__navigate", infos[0].Name)
})

t.Run("EmptyPropertiesYieldsEmptySchema", func(t *testing.T) {
t.Parallel()

// An MCP tool reporting {"type": "object", "properties": {}} must
// produce a non-nil empty Schema. A nil Schema serializes to JSON
// null downstream, which OpenAI rejects.
schema := mustStruct(t, map[string]any{
"type": "object",
"properties": map[string]any{},
})
resources := []database.ChatContextResource{
mcpServerResource(t, "github", &agentproto.MCPServerBody{
ServerName: "github",
Tools: []*agentproto.MCPTool{
{Name: "document_graphql_schema", Description: "Run a GraphQL query", InputSchema: schema},
},
}, database.WorkspaceAgentContextResourceStatusOk),
}

infos := workspaceMCPToolInfosFromResources(resources)
require.Len(t, infos, 1)
require.NotNil(t, infos[0].Schema, "Schema must not be nil for an empty properties object")
require.Empty(t, infos[0].Schema)
})

t.Run("NoMCPServersYieldsNil", func(t *testing.T) {
t.Parallel()

Expand Down
Loading