diff --git a/go/session.go b/go/session.go index ca67cb2c8..4aacb5131 100644 --- a/go/session.go +++ b/go/session.go @@ -1288,6 +1288,9 @@ func (s *Session) handleBroadcastEvent(event SessionEvent) { case *ExternalToolRequestedData: handler, ok := s.getToolHandler(d.ToolName) if !ok { + if d.ToolName == "" && d.ToolCallID != "" { + s.respondToMissingToolName(d.RequestID, d.Traceparent, d.Tracestate) + } return } var tp, ts string @@ -1335,6 +1338,29 @@ func (s *Session) handleBroadcastEvent(event SessionEvent) { } } +func (s *Session) respondToMissingToolName(requestID string, traceparent, tracestate *string) { + var tp, ts string + if traceparent != nil { + tp = *traceparent + } + if tracestate != nil { + ts = *tracestate + } + + ctx := contextWithTraceParent(context.Background(), tp, ts) + resultType := "failure" + errMsg := "tool name is missing or incorrect" + s.RPC.Tools.HandlePendingToolCall(ctx, &rpc.HandlePendingToolCallRequest{ + RequestID: requestID, + Result: rpc.ExternalToolTextResultForLlm{ + TextResultForLlm: "Tool call failed: tool name is missing or incorrect. Retry using one of the registered tool names.", + ResultType: &resultType, + Error: &errMsg, + ToolTelemetry: map[string]any{}, + }, + }) +} + // executeToolAndRespond executes a tool handler and sends the result back via RPC. func (s *Session) executeToolAndRespond(requestID, toolName, toolCallID string, arguments any, handler ToolHandler, traceparent, tracestate string) { ctx := contextWithTraceParent(context.Background(), traceparent, tracestate) diff --git a/go/session_test.go b/go/session_test.go index 15cfbcf57..5aadd0b71 100644 --- a/go/session_test.go +++ b/go/session_test.go @@ -167,6 +167,102 @@ func readTestJSONRPCFrame(r io.Reader) ([]byte, error) { return data, err } +func TestSession_HandleBroadcastEventRespondsToMissingToolName(t *testing.T) { + stdinR, stdinW := io.Pipe() + stdoutR, stdoutW := io.Pipe() + defer stdinR.Close() + defer stdinW.Close() + defer stdoutR.Close() + defer stdoutW.Close() + + client := jsonrpc2.NewClient(stdinW, stdoutR) + client.Start() + defer client.Stop() + + paramsCh := make(chan map[string]any, 1) + errCh := make(chan error, 1) + + go func() { + frame, err := readTestJSONRPCFrame(stdinR) + if err != nil { + errCh <- err + return + } + + var request struct { + ID json.RawMessage `json:"id"` + Method string `json:"method"` + Params map[string]any `json:"params"` + } + if err := json.Unmarshal(frame, &request); err != nil { + errCh <- err + return + } + if request.Method != "session.tools.handlePendingToolCall" { + errCh <- fmt.Errorf("expected session.tools.handlePendingToolCall, got %s", request.Method) + return + } + + paramsCh <- request.Params + + response := map[string]any{ + "jsonrpc": "2.0", + "id": json.RawMessage(request.ID), + "result": map[string]any{"success": true}, + } + data, err := json.Marshal(response) + if err != nil { + errCh <- err + return + } + if _, err := fmt.Fprintf(stdoutW, "Content-Length: %d\r\n\r\n%s", len(data), data); err != nil { + errCh <- err + return + } + }() + + session := &Session{ + SessionID: "sess-1", + client: client, + RPC: rpc.NewSessionRPC(client, "sess-1"), + } + + session.handleBroadcastEvent(SessionEvent{Data: &ExternalToolRequestedData{ + RequestID: "req-1", + SessionID: "sess-1", + ToolCallID: "call-1", + ToolName: "", + }}) + + select { + case params := <-paramsCh: + if params["sessionId"] != "sess-1" { + t.Fatalf("expected sessionId sess-1, got %v", params["sessionId"]) + } + if params["requestId"] != "req-1" { + t.Fatalf("expected requestId req-1, got %v", params["requestId"]) + } + result, ok := params["result"].(map[string]any) + if !ok { + t.Fatalf("expected structured result, got %T", params["result"]) + } + if result["resultType"] != "failure" { + t.Fatalf("expected resultType failure, got %v", result["resultType"]) + } + if result["error"] != "tool name is missing or incorrect" { + t.Fatalf("unexpected error: %v", result["error"]) + } + text, ok := result["textResultForLlm"].(string) + if !ok || !strings.Contains(text, "tool name is missing") { + t.Fatalf("unexpected textResultForLlm: %v", result["textResultForLlm"]) + } + case err := <-errCh: + t.Fatal(err) + case <-time.After(2 * time.Second): + t.Fatal("timed out waiting for pending tool call response") + } +} + func TestSession_On(t *testing.T) { t.Run("multiple handlers all receive events", func(t *testing.T) { session, cleanup := newTestSession()