From b25c03cdea754a96f721c72e934ff6cc167be3c2 Mon Sep 17 00:00:00 2001 From: PratikDhanave Date: Tue, 22 Sep 2026 10:54:48 +0530 Subject: [PATCH 1/2] Add agent-as-tool argument name and description customization agenttool hardcoded a single "query" string argument. Add ArgName and ArgDescription config options (defaulting to "query" / "input query to invoke the agent") so callers can rename/re-describe the argument the model sees, mirroring the .NET/Python as-tool argument options. The schema and Call now use the configured name. Partial for #1139 (session propagation is left for the design discussion on that issue). --- tool/agenttool/agenttool.go | 51 +++++++++++++++++++++++++------- tool/agenttool/agenttool_test.go | 35 ++++++++++++++++++++++ 2 files changed, 75 insertions(+), 11 deletions(-) diff --git a/tool/agenttool/agenttool.go b/tool/agenttool/agenttool.go index 655ccac0..50523146 100644 --- a/tool/agenttool/agenttool.go +++ b/tool/agenttool/agenttool.go @@ -16,6 +16,13 @@ import ( // used when an agent exposes no description of its own. const defaultDescription = "Invoke an agent to retrieve some information." +// Defaults for the single string argument the tool exposes, matching the .NET +// AIAgent function-tool schema. +const ( + defaultArgName = "query" + defaultArgDescription = "input query to invoke the agent" +) + // invalidNameChars matches every run of characters that are not valid in a // function name, matching the .NET SanitizeAgentName regex so agent display // names such as "Weather Agent" become valid provider function names. @@ -24,6 +31,15 @@ var invalidNameChars = regexp.MustCompile(`[^0-9A-Za-z]+`) // Config represents the configuration for [New]. type Config struct { RunOptions []agent.Option + + // ArgName overrides the name of the single string argument the tool exposes + // to the model. Defaults to "query". Mirrors the .NET/Python as-tool + // argument-name option. + ArgName string + + // ArgDescription overrides the description of that argument. Defaults to + // "input query to invoke the agent". + ArgDescription string } // New creates a new FuncTool that invokes the given agent with the provided @@ -32,15 +48,27 @@ func New(a *agent.Agent, config Config) tool.FuncTool { if a == nil { panic("agenttool: agent is required") } + argName := config.ArgName + if argName == "" { + argName = defaultArgName + } + argDescription := config.ArgDescription + if argDescription == "" { + argDescription = defaultArgDescription + } return functool{ - opts: slices.Clone(config.RunOptions), - agent: a, + opts: slices.Clone(config.RunOptions), + agent: a, + argName: argName, + argDescription: argDescription, } } type functool struct { - opts []agent.Option - agent *agent.Agent + opts []agent.Option + agent *agent.Agent + argName string + argDescription string } func (t functool) Name() string { @@ -66,12 +94,12 @@ func (t functool) Schema() any { return map[string]any{ "type": "object", "properties": map[string]any{ - "query": map[string]any{ + t.argName: map[string]any{ "type": "string", - "description": "input query to invoke the agent", + "description": t.argDescription, }, }, - "required": []string{"query"}, + "required": []string{t.argName}, } } @@ -82,16 +110,17 @@ func (t functool) ReturnSchema() any { } func (t functool) Call(ctx context.Context, args string) (any, error) { - var in struct { - Query string `json:"query"` - } if args == "" { args = "{}" } + // The argument name is configurable, so decode into a map and read the + // configured key rather than a fixed struct field. + var in map[string]any if err := json.Unmarshal([]byte(args), &in); err != nil { return nil, err } - resp, err := t.agent.RunText(ctx, in.Query, t.opts...).Collect() + query, _ := in[t.argName].(string) + resp, err := t.agent.RunText(ctx, query, t.opts...).Collect() if err != nil { return "", err } diff --git a/tool/agenttool/agenttool_test.go b/tool/agenttool/agenttool_test.go index 1c6d3d68..c9187d0f 100644 --- a/tool/agenttool/agenttool_test.go +++ b/tool/agenttool/agenttool_test.go @@ -73,6 +73,41 @@ func TestNew_ExposesAgentMetadataAndSchemas(t *testing.T) { } } +func TestNew_CustomArgNameAndDescription(t *testing.T) { + var capturedMessages []*message.Message + a := agenttest.New(agenttest.NewResponseBuilder( + func(_ context.Context, messages []*message.Message, _ ...agent.Option) { + capturedMessages = messages + }, + ).AddText("ok").Build()) + tl := agenttool.New(a, agenttool.Config{ArgName: "task", ArgDescription: "the task to run"}) + + // Schema reflects the custom argument. + schema := tl.Schema().(map[string]any) + properties := schema["properties"].(map[string]any) + if _, ok := properties["task"]; !ok { + t.Fatalf("schema properties = %#v, want a \"task\" argument", properties) + } + if _, ok := properties["query"]; ok { + t.Fatalf("schema still exposes the default \"query\" argument") + } + arg := properties["task"].(map[string]any) + if arg["description"] != "the task to run" { + t.Errorf("task description = %v, want %q", arg["description"], "the task to run") + } + if req := schema["required"].([]string); len(req) != 1 || req[0] != "task" { + t.Errorf("required = %v, want [task]", req) + } + + // Call reads the custom key and forwards its value to the agent. + if _, err := tl.Call(t.Context(), `{"task":"do the thing"}`); err != nil { + t.Fatalf("Call() error = %v", err) + } + if len(capturedMessages) == 0 || capturedMessages[len(capturedMessages)-1].String() != "do the thing" { + t.Fatalf("agent received %v, want the task text", capturedMessages) + } +} + func TestNew_PanicsWithNilAgent(t *testing.T) { defer func() { if recover() == nil { From 11d0e69075de33f36e5a183c7e019d8b82d0f69b Mon Sep 17 00:00:00 2001 From: PratikDhanave Date: Thu, 8 Oct 2026 10:54:22 +0530 Subject: [PATCH 2/2] Address review: reject wrong-typed agent-tool argument instead of blanking the query --- tool/agenttool/agenttool.go | 16 ++++++++++++---- tool/agenttool/agenttool_test.go | 19 +++++++++++++++++++ 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/tool/agenttool/agenttool.go b/tool/agenttool/agenttool.go index 50523146..691274b8 100644 --- a/tool/agenttool/agenttool.go +++ b/tool/agenttool/agenttool.go @@ -113,13 +113,21 @@ func (t functool) Call(ctx context.Context, args string) (any, error) { if args == "" { args = "{}" } - // The argument name is configurable, so decode into a map and read the - // configured key rather than a fixed struct field. - var in map[string]any + // The argument name is configurable, so decode into a map of raw messages + // and unmarshal the configured key into a string. Other entries stay as raw + // JSON so unrelated arguments are ignored, while a wrong type for the + // configured key surfaces a JSON decoding error rather than silently + // invoking the agent with an empty message. + var in map[string]json.RawMessage if err := json.Unmarshal([]byte(args), &in); err != nil { return nil, err } - query, _ := in[t.argName].(string) + var query string + if raw, ok := in[t.argName]; ok { + if err := json.Unmarshal(raw, &query); err != nil { + return nil, err + } + } resp, err := t.agent.RunText(ctx, query, t.opts...).Collect() if err != nil { return "", err diff --git a/tool/agenttool/agenttool_test.go b/tool/agenttool/agenttool_test.go index ac6ae4d6..93507663 100644 --- a/tool/agenttool/agenttool_test.go +++ b/tool/agenttool/agenttool_test.go @@ -257,6 +257,25 @@ func TestCall_InvalidJSONReturnsError(t *testing.T) { } } +// A non-string value for the configured argument must be rejected as a JSON +// decoding error rather than silently invoking the agent with an empty query, +// matching the schema's declared string type. +func TestCall_WrongArgTypeReturnsError(t *testing.T) { + var invoked bool + a := agenttest.New(agenttest.NewResponseBuilder( + func(context.Context, []*message.Message, ...agent.Option) { invoked = true }, + ).AddText("unused").Build()) + tl := agenttool.New(a, agenttool.Config{ArgName: "task"}) + + _, err := tl.Call(t.Context(), `{"task":123}`) + if err == nil { + t.Fatal("expected a JSON decoding error for a non-string argument") + } + if invoked { + t.Fatal("agent should not be invoked when the argument has the wrong type") + } +} + func TestCall_PropagatesAgentError(t *testing.T) { expectedErr := errors.New("agent failed") a := agenttest.New(agenttest.NewResponseBuilder().AddError(expectedErr).Build())