Skip to content

[dotnet][1.0.1] Custom client tool (AIFunction) never invoked - runtime returns opaque "Tool execution failed" without dispatching to the registered handler #1637

@mit2nil

Description

@mit2nil

Summary

A registered custom client tool (a Microsoft.Extensions.AI AIFunction, exposed via SessionConfig.Tools + AvailableTools = new ToolSet().AddCustom("*")) is never invoked. The model emits a valid tool call with valid arguments and the PreToolUse hook fires, but the runtime then reports PostToolUseFailure with the opaque message "Tool execution failed" and a failed ToolExecutionCompletewithout ever calling the tool's AIFunction.InvokeAsync / DelegatingAIFunction.InvokeCoreAsync.

The same code/config works in a single‑user local run but fails consistently in a deployed multi‑user Linux server.

Environment

  • GitHub.Copilot.SDK (.NET): 1.0.1
  • Mode: CopilotClientMode.Empty (in‑process; no CLI subprocess)
  • Provider: Azure OpenAI BYOK (provider.type = "azure"), wire API responses, api‑version 2025-04-01-preview
  • Model: an Azure OpenAI chat model with tool calling (GPT‑5‑class)
  • Runtime: .NET 8
  • Failing: Linux x64 containers, long‑running multi‑user server (concurrent sessions)
  • Working: Windows, single‑user local session — identical provider/model/tool config
  • Logging: CopilotClientOptions.LogLevel = "all", Logger bridged to host ILogger

Observed event sequence

For a single failing tool call the SDK emits (in order):

  1. AssistantMessage / AssistantReasoning — model decides to call the custom tool.
  2. ToolExecutionStartHookStart/HookEndHook.PreToolUse — pre-tool hook runs and succeeds (tool name + input args present and correct).
  3. PermissionRequestedPermissionCompleted — permission check succeeds.
  4. ExternalToolRequested — runtime requests the client tool (args still valid).
  5. Hook.PostToolUseFailureres = tool_invocation_failed, Error = "Tool execution failed" (21 chars; no exception type/message/stack).
  6. ExternalToolCompletedHookStart/HookEnd.
  7. ToolExecutionCompleteres = tool execution failed.
  8. AssistantTurnEnd.

The registered AIFunction handler is never entered between steps 4 and 5. The model then retries and fails identically (a second PreToolUse → ExternalToolRequested → PostToolUseFailure cycle).

Trace (SDK event names only — no identifiers/payloads)

SDK event trace: Hook.PreToolUse and PermissionCompleted succeed, then ExternalToolRequested is immediately followed by Hook.PostToolUseFailure (tool_invocation_failed) and a failed ToolExecutionComplete — with no handler invocation in between

Both red rows are the failure: Hook.PostToolUseFailure (tool_invocation_failed) and SdkEvent.ToolExecutionComplete (tool execution failed). Note Hook.PreToolUse and PermissionCompleted immediately above succeed, and ExternalToolRequested/ExternalToolCompleted bracket the failure — yet the client handler is never called.

How we know the handler is never invoked

  • Every custom tool is wrapped in a DelegatingAIFunction that logs on entry (Debug) and on throw (Error) inside InvokeCoreAsync. With the host and SDK at trace/all verbosity, neither the entry nor the throw log is ever emitted for the failing call.
  • The handler's own first‑line "invoking" log (Information) is also absent.
  • ⇒ The failure happens upstream of the handler, inside the runtime's client‑tool dispatch.

Arguments are valid (host‑side binding ruled out)

The model produced valid args on multiple retries, e.g.:

{"RequiredStringProp":"<short text>"}
{"RequiredStringProp":"<short text>","OptionalProp":null}

The single required parameter is always present; optional params carry = null defaults. So this is not a host‑side argument‑binding / schema‑validation failure (those would surface inside InvokeCoreAsync, which is never reached).

Observability gap

The only failure signal the host receives is PostToolUseFailureHookInput.Error.ToString() == "Tool execution failed" — no exception, inner error, stack, or dispatch reason. Even with LogLevel = all and Logger bridged to ILogger, the runtime emits no diagnostic explaining why the client tool was not dispatched. (Related observability gap: #1220.)

Questions for maintainers

  1. Under what conditions does the runtime fail a client tool with "Tool execution failed" without invoking the registered handler? (dispatch/lookup/marshaling failure, callId correlation, multi‑session tool‑registry issues, etc.)
  2. Where does the runtime log the underlying reason, and how can we surface it (config / log category / event) from the .NET SDK in a server deployment? LogLevel="all" + bridged Logger did not surface it.
  3. Is there a known difference between single‑session and concurrent multi‑session (multi‑user server) client‑tool dispatch in 1.0.1?
  4. Possibly related: Anthropic BYOK provider handles tool execution internally — tool.call JSON-RPC never sent to python sdk #709 (BYOK provider handling tool execution internally / not delegating tool.call to the SDK — reported for Anthropic/Python, where it was noted OpenAI BYOK worked). Is there an analogous path for Azure OpenAI BYOK with the responses wire API on the .NET runtime?

Minimal repro sketch (generic)

var client = factory.Create(new CopilotClientOptions
{
    Mode = CopilotClientMode.Empty,
    LogLevel = new CopilotLogLevel("all"),
    Logger = logger,
});
await client.StartAsync();

AIFunction tool = AIFunctionFactory.Create(
    ([Description("Short required text")] string requiredProp,
     [Description("Optional")] string? optionalProp = null,
     CancellationToken ct = default) =>
    {
        logger.LogInformation("HANDLER ENTERED"); // never logged in the failing env
        return ValueTask.FromResult<object?>("ok");
    },
    name: "my_custom_tool",
    description: "Example custom client tool");

var config = new SessionConfig
{
    Model = "<azure-openai-deployment>",   // Azure OpenAI BYOK, wireApi = responses
    Tools = new List<AIFunctionDeclaration> { tool },
    AvailableTools = new ToolSet()
        .AddBuiltIn(BuiltInTools.Isolated)
        .AddCustom("*"),
    Hooks = /* PreToolUse + PostToolUse + PostToolUseFailure handlers */,
};

// Drive a prompt that makes the model call my_custom_tool.
// Observed: PreToolUse fires, then PostToolUseFailure Error="Tool execution failed";
// "HANDLER ENTERED" is never logged.

Note: we cannot share a fully self‑contained repro yet — it reproduces only in our concurrent multi‑user Linux server and not in a single‑user local session with identical provider/model/tool config. Happy to provide more detail, traces (privately), or test a patch.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions